@iservice365/module-hygiene 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/index.d.ts +503 -1
- package/dist/index.js +4680 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4693 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -3
- package/dist/public/user-invite.hbs +0 -142
package/dist/index.mjs
CHANGED
|
@@ -1 +1,4694 @@
|
|
|
1
|
+
// src/models/hygiene-area.model.ts
|
|
2
|
+
import { BadRequestError, logger } from "@iservice365/node-server-utils";
|
|
3
|
+
import Joi from "joi";
|
|
4
|
+
import { ObjectId } from "mongodb";
|
|
5
|
+
var areaSchema = Joi.object({
|
|
6
|
+
name: Joi.string().required(),
|
|
7
|
+
site: Joi.string().hex().required(),
|
|
8
|
+
createdBy: Joi.string().hex().required(),
|
|
9
|
+
checklist: Joi.array().items(
|
|
10
|
+
Joi.object({
|
|
11
|
+
_id: Joi.string().hex().required(),
|
|
12
|
+
name: Joi.string().required()
|
|
13
|
+
})
|
|
14
|
+
).optional()
|
|
15
|
+
});
|
|
16
|
+
function MArea(value) {
|
|
17
|
+
const { error } = areaSchema.validate(value);
|
|
18
|
+
if (error) {
|
|
19
|
+
logger.info(`Hygiene Area Model: ${error.message}`);
|
|
20
|
+
throw new BadRequestError(error.message);
|
|
21
|
+
}
|
|
22
|
+
if (value.site) {
|
|
23
|
+
try {
|
|
24
|
+
value.site = new ObjectId(value.site);
|
|
25
|
+
} catch (error2) {
|
|
26
|
+
throw new BadRequestError("Invalid site ID format.");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (value.createdBy) {
|
|
30
|
+
try {
|
|
31
|
+
value.createdBy = new ObjectId(value.createdBy);
|
|
32
|
+
} catch (error2) {
|
|
33
|
+
throw new BadRequestError("Invalid createdBy ID format.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (value.checklist && Array.isArray(value.checklist)) {
|
|
37
|
+
value.checklist = value.checklist.map((item) => {
|
|
38
|
+
try {
|
|
39
|
+
return {
|
|
40
|
+
...item,
|
|
41
|
+
_id: new ObjectId(item._id)
|
|
42
|
+
};
|
|
43
|
+
} catch (error2) {
|
|
44
|
+
throw new BadRequestError(
|
|
45
|
+
`Invalid checklist item ID format: ${item._id}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
name: value.name,
|
|
52
|
+
site: value.site,
|
|
53
|
+
createdBy: value.createdBy,
|
|
54
|
+
checklist: value.checklist,
|
|
55
|
+
status: value.status ?? "active",
|
|
56
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
57
|
+
updatedAt: value.updatedAt ?? "",
|
|
58
|
+
deletedAt: value.deletedAt ?? ""
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/repositories/hygiene-area.repository.ts
|
|
63
|
+
import { ObjectId as ObjectId2 } from "mongodb";
|
|
64
|
+
import {
|
|
65
|
+
useAtlas,
|
|
66
|
+
InternalServerError,
|
|
67
|
+
paginate,
|
|
68
|
+
BadRequestError as BadRequestError2,
|
|
69
|
+
useCache,
|
|
70
|
+
logger as logger2,
|
|
71
|
+
makeCacheKey,
|
|
72
|
+
NotFoundError
|
|
73
|
+
} from "@iservice365/node-server-utils";
|
|
74
|
+
function useAreaRepository() {
|
|
75
|
+
const db = useAtlas.getDb();
|
|
76
|
+
if (!db) {
|
|
77
|
+
throw new InternalServerError("Unable to connect to server.");
|
|
78
|
+
}
|
|
79
|
+
const namespace_collection = "hygiene-areas";
|
|
80
|
+
const collection = db.collection(namespace_collection);
|
|
81
|
+
const { delNamespace, setCache, getCache } = useCache(namespace_collection);
|
|
82
|
+
async function createIndex() {
|
|
83
|
+
try {
|
|
84
|
+
await collection.createIndexes([
|
|
85
|
+
{ key: { site: 1 } },
|
|
86
|
+
{ key: { createdBy: 1 } },
|
|
87
|
+
{ key: { status: 1 } }
|
|
88
|
+
]);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw new InternalServerError("Failed to create index on hygiene area.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function createTextIndex() {
|
|
94
|
+
try {
|
|
95
|
+
await collection.createIndex({ name: "text" });
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new InternalServerError(
|
|
98
|
+
"Failed to create text index on hygiene area."
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function createUniqueIndex() {
|
|
103
|
+
try {
|
|
104
|
+
await collection.createIndex(
|
|
105
|
+
{ name: 1, site: 1, deletedAt: 1 },
|
|
106
|
+
{ unique: true }
|
|
107
|
+
);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw new InternalServerError(
|
|
110
|
+
"Failed to create unique index on hygiene area."
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function createArea(value, session) {
|
|
115
|
+
try {
|
|
116
|
+
value = MArea(value);
|
|
117
|
+
const res = await collection.insertOne(value, { session });
|
|
118
|
+
delNamespace().then(() => {
|
|
119
|
+
logger2.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
120
|
+
}).catch((err) => {
|
|
121
|
+
logger2.error(
|
|
122
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
123
|
+
err
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
return res.insertedId;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const isDuplicated = error.message.includes("duplicate");
|
|
129
|
+
if (isDuplicated) {
|
|
130
|
+
throw new BadRequestError2("Area already exists.");
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function getAreas({
|
|
136
|
+
page = 1,
|
|
137
|
+
limit = 10,
|
|
138
|
+
search = "",
|
|
139
|
+
startDate = "",
|
|
140
|
+
endDate = "",
|
|
141
|
+
site = ""
|
|
142
|
+
}) {
|
|
143
|
+
page = page > 0 ? page - 1 : 0;
|
|
144
|
+
const query = {
|
|
145
|
+
status: { $ne: "deleted" }
|
|
146
|
+
};
|
|
147
|
+
const cacheOptions = {
|
|
148
|
+
page,
|
|
149
|
+
limit
|
|
150
|
+
};
|
|
151
|
+
if (site) {
|
|
152
|
+
try {
|
|
153
|
+
site = new ObjectId2(site);
|
|
154
|
+
cacheOptions.site = site.toString();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw new BadRequestError2("Invalid site ID format.");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (search) {
|
|
160
|
+
query.$or = [{ name: { $regex: search, $options: "i" } }];
|
|
161
|
+
cacheOptions.search = search;
|
|
162
|
+
}
|
|
163
|
+
if (startDate && endDate) {
|
|
164
|
+
query.createdAt = {
|
|
165
|
+
$gte: new Date(startDate),
|
|
166
|
+
$lte: new Date(endDate)
|
|
167
|
+
};
|
|
168
|
+
cacheOptions.startDate = new Date(startDate).toISOString().split("T")[0];
|
|
169
|
+
cacheOptions.endDate = new Date(endDate).toISOString().split("T")[0];
|
|
170
|
+
} else if (startDate) {
|
|
171
|
+
query.createdAt = { $gte: new Date(startDate) };
|
|
172
|
+
cacheOptions.startDate = new Date(startDate).toISOString().split("T")[0];
|
|
173
|
+
} else if (endDate) {
|
|
174
|
+
query.createdAt = { $lte: new Date(endDate) };
|
|
175
|
+
cacheOptions.endDate = new Date(endDate).toISOString().split("T")[0];
|
|
176
|
+
}
|
|
177
|
+
const cacheKey = makeCacheKey(namespace_collection, cacheOptions);
|
|
178
|
+
const cachedData = await getCache(cacheKey);
|
|
179
|
+
if (cachedData) {
|
|
180
|
+
logger2.info(`Cache hit for key: ${cacheKey}`);
|
|
181
|
+
return cachedData;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const items = await collection.aggregate([
|
|
185
|
+
{ $match: query },
|
|
186
|
+
{
|
|
187
|
+
$lookup: {
|
|
188
|
+
from: "sites",
|
|
189
|
+
localField: "site",
|
|
190
|
+
foreignField: "_id",
|
|
191
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
192
|
+
as: "site"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
$unwind: {
|
|
197
|
+
path: "$site",
|
|
198
|
+
preserveNullAndEmptyArrays: true
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
$lookup: {
|
|
203
|
+
from: "users",
|
|
204
|
+
localField: "createdBy",
|
|
205
|
+
foreignField: "_id",
|
|
206
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
207
|
+
as: "createdBy"
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
$unwind: {
|
|
212
|
+
path: "$createdBy",
|
|
213
|
+
preserveNullAndEmptyArrays: true
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
$project: {
|
|
218
|
+
name: 1,
|
|
219
|
+
site: "$site._id",
|
|
220
|
+
siteName: "$site.name",
|
|
221
|
+
createdByName: "$createdBy.name",
|
|
222
|
+
checklist: 1,
|
|
223
|
+
status: 1,
|
|
224
|
+
createdAt: 1
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{ $sort: { _id: -1 } },
|
|
228
|
+
{ $skip: page * limit },
|
|
229
|
+
{ $limit: limit }
|
|
230
|
+
]).toArray();
|
|
231
|
+
const length = await collection.countDocuments(query);
|
|
232
|
+
const data = paginate(items, page, limit, length);
|
|
233
|
+
setCache(cacheKey, data, 15 * 60).then(() => {
|
|
234
|
+
logger2.info(`Cache set for key: ${cacheKey}`);
|
|
235
|
+
}).catch((err) => {
|
|
236
|
+
logger2.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
237
|
+
});
|
|
238
|
+
return data;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function getAreaById(_id) {
|
|
244
|
+
try {
|
|
245
|
+
_id = new ObjectId2(_id);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
throw new BadRequestError2("Invalid area ID format.");
|
|
248
|
+
}
|
|
249
|
+
const query = {
|
|
250
|
+
_id,
|
|
251
|
+
status: { $ne: "deleted" }
|
|
252
|
+
};
|
|
253
|
+
const cacheKey = makeCacheKey(namespace_collection, {
|
|
254
|
+
_id: _id.toString()
|
|
255
|
+
});
|
|
256
|
+
const cachedData = await getCache(cacheKey);
|
|
257
|
+
if (cachedData) {
|
|
258
|
+
logger2.info(`Cache hit for key: ${cacheKey}`);
|
|
259
|
+
return cachedData;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const data = await collection.aggregate([
|
|
263
|
+
{ $match: query },
|
|
264
|
+
{
|
|
265
|
+
$project: {
|
|
266
|
+
name: 1,
|
|
267
|
+
checklist: 1
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
]).toArray();
|
|
271
|
+
if (!data || !data.length) {
|
|
272
|
+
throw new NotFoundError("Area not found.");
|
|
273
|
+
}
|
|
274
|
+
setCache(cacheKey, data[0], 15 * 60).then(() => {
|
|
275
|
+
logger2.info(`Cache set for key: ${cacheKey}`);
|
|
276
|
+
}).catch((err) => {
|
|
277
|
+
logger2.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
278
|
+
});
|
|
279
|
+
return data[0];
|
|
280
|
+
} catch (error) {
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function getAreaByName(name, site) {
|
|
285
|
+
try {
|
|
286
|
+
if (site)
|
|
287
|
+
site = new ObjectId2(site);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
throw new BadRequestError2("Invalid site ID format.");
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
return await collection.findOne({
|
|
293
|
+
name: { $regex: new RegExp(`^${name}$`, "i") },
|
|
294
|
+
...site && { site }
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw new BadRequestError2("Unable to fetch area by name.");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function updateArea(_id, value) {
|
|
301
|
+
try {
|
|
302
|
+
_id = new ObjectId2(_id);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
throw new BadRequestError2("Invalid area ID format.");
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const updateValue = { ...value, updatedAt: /* @__PURE__ */ new Date() };
|
|
308
|
+
const res = await collection.updateOne({ _id }, { $set: updateValue });
|
|
309
|
+
if (res.modifiedCount === 0) {
|
|
310
|
+
throw new InternalServerError("Unable to update cleaning area.");
|
|
311
|
+
}
|
|
312
|
+
delNamespace().then(() => {
|
|
313
|
+
logger2.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
314
|
+
}).catch((err) => {
|
|
315
|
+
logger2.error(
|
|
316
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
317
|
+
err
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
return res.modifiedCount;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
const isDuplicated = error.message.includes("duplicate");
|
|
323
|
+
if (isDuplicated) {
|
|
324
|
+
throw new BadRequestError2("Area already exists.");
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async function updateAreaChecklist(_id, value) {
|
|
330
|
+
try {
|
|
331
|
+
_id = new ObjectId2(_id);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
throw new BadRequestError2("Invalid area ID format.");
|
|
334
|
+
}
|
|
335
|
+
if (value.checklist && Array.isArray(value.checklist)) {
|
|
336
|
+
value.checklist = value.checklist.map((item) => {
|
|
337
|
+
try {
|
|
338
|
+
return {
|
|
339
|
+
...item,
|
|
340
|
+
_id: new ObjectId2(item._id)
|
|
341
|
+
};
|
|
342
|
+
} catch (error) {
|
|
343
|
+
throw new BadRequestError2(
|
|
344
|
+
`Invalid checklist item ID format: ${item._id}`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const updateValue = { ...value, updatedAt: /* @__PURE__ */ new Date() };
|
|
351
|
+
const res = await collection.updateOne({ _id }, { $set: updateValue });
|
|
352
|
+
if (res.modifiedCount === 0) {
|
|
353
|
+
throw new InternalServerError("Unable to update cleaning area.");
|
|
354
|
+
}
|
|
355
|
+
delNamespace().then(() => {
|
|
356
|
+
logger2.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
357
|
+
}).catch((err) => {
|
|
358
|
+
logger2.error(
|
|
359
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
360
|
+
err
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
return res.modifiedCount;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function deleteArea(_id, session) {
|
|
369
|
+
try {
|
|
370
|
+
_id = new ObjectId2(_id);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
throw new BadRequestError2("Invalid area ID format.");
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const updateValue = {
|
|
376
|
+
status: "deleted",
|
|
377
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
378
|
+
deletedAt: /* @__PURE__ */ new Date()
|
|
379
|
+
};
|
|
380
|
+
const res = await collection.updateOne(
|
|
381
|
+
{ _id },
|
|
382
|
+
{ $set: updateValue },
|
|
383
|
+
{ session }
|
|
384
|
+
);
|
|
385
|
+
if (res.modifiedCount === 0) {
|
|
386
|
+
throw new InternalServerError("Unable to delete area.");
|
|
387
|
+
}
|
|
388
|
+
delNamespace().then(() => {
|
|
389
|
+
logger2.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
390
|
+
}).catch((err) => {
|
|
391
|
+
logger2.error(
|
|
392
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
393
|
+
err
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
return res.modifiedCount;
|
|
397
|
+
} catch (error) {
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
createIndex,
|
|
403
|
+
createTextIndex,
|
|
404
|
+
createUniqueIndex,
|
|
405
|
+
createArea,
|
|
406
|
+
getAreas,
|
|
407
|
+
getAreaById,
|
|
408
|
+
getAreaByName,
|
|
409
|
+
updateArea,
|
|
410
|
+
updateAreaChecklist,
|
|
411
|
+
deleteArea
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/services/hygiene-area.service.ts
|
|
416
|
+
import {
|
|
417
|
+
BadRequestError as BadRequestError3,
|
|
418
|
+
logger as logger3,
|
|
419
|
+
NotFoundError as NotFoundError2,
|
|
420
|
+
useAtlas as useAtlas2
|
|
421
|
+
} from "@iservice365/node-server-utils";
|
|
422
|
+
function useAreaService() {
|
|
423
|
+
const { createArea: _createArea } = useAreaRepository();
|
|
424
|
+
async function uploadByFile({
|
|
425
|
+
dataJson,
|
|
426
|
+
createdBy,
|
|
427
|
+
site
|
|
428
|
+
}) {
|
|
429
|
+
let dataArray;
|
|
430
|
+
try {
|
|
431
|
+
dataArray = JSON.parse(dataJson);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
throw new BadRequestError3("Invalid JSON format for data in excel");
|
|
434
|
+
}
|
|
435
|
+
if (!dataArray || dataArray.length === 0) {
|
|
436
|
+
throw new NotFoundError2("No data found in the uploaded file");
|
|
437
|
+
}
|
|
438
|
+
const session = useAtlas2.getClient()?.startSession();
|
|
439
|
+
const insertedAreaIds = [];
|
|
440
|
+
try {
|
|
441
|
+
session?.startTransaction();
|
|
442
|
+
for (const row of dataArray) {
|
|
443
|
+
if (!row?.AREA_NAME) {
|
|
444
|
+
logger3.warn("Skipping row with missing AREA_NAME:", row);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const insertedId = await _createArea(
|
|
449
|
+
{
|
|
450
|
+
name: String(row.AREA_NAME).trim(),
|
|
451
|
+
site,
|
|
452
|
+
createdBy
|
|
453
|
+
},
|
|
454
|
+
session
|
|
455
|
+
);
|
|
456
|
+
insertedAreaIds.push(insertedId);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
logger3.error(
|
|
459
|
+
`Error creating area "${row.AREA_NAME}":`,
|
|
460
|
+
error.message
|
|
461
|
+
);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
await session?.commitTransaction();
|
|
466
|
+
logger3.info(`Successfully uploaded ${insertedAreaIds.length} areas`);
|
|
467
|
+
return {
|
|
468
|
+
message: `Successfully uploaded ${insertedAreaIds.length} areas`
|
|
469
|
+
};
|
|
470
|
+
} catch (error) {
|
|
471
|
+
await session?.abortTransaction();
|
|
472
|
+
logger3.error("Error while uploading area information", error);
|
|
473
|
+
throw error;
|
|
474
|
+
} finally {
|
|
475
|
+
session?.endSession();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
uploadByFile
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/controllers/hygiene-area.controller.ts
|
|
484
|
+
import { BadRequestError as BadRequestError4, logger as logger4 } from "@iservice365/node-server-utils";
|
|
485
|
+
import Joi2 from "joi";
|
|
486
|
+
|
|
487
|
+
// src/utils/convert-excel.util.ts
|
|
488
|
+
import { Readable } from "stream";
|
|
489
|
+
import * as xlsx from "xlsx";
|
|
490
|
+
function convertBufferFile(bufferFile) {
|
|
491
|
+
return new Promise((resolve, reject) => {
|
|
492
|
+
const fileStream = Readable.from(bufferFile);
|
|
493
|
+
let fileBuffer = Buffer.alloc(0);
|
|
494
|
+
fileStream.on("data", (chunk) => {
|
|
495
|
+
fileBuffer = Buffer.concat([fileBuffer, chunk]);
|
|
496
|
+
});
|
|
497
|
+
fileStream.on("end", () => {
|
|
498
|
+
try {
|
|
499
|
+
const workbook = xlsx.read(fileBuffer, { type: "buffer" });
|
|
500
|
+
const sheetName = workbook.SheetNames[0];
|
|
501
|
+
const sheet = workbook.Sheets[sheetName];
|
|
502
|
+
const jsonData = xlsx.utils.sheet_to_json(sheet);
|
|
503
|
+
resolve(jsonData);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
reject("Error parsing file");
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
fileStream.on("error", (err) => {
|
|
509
|
+
reject("Error Reading File: " + err.message);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/controllers/hygiene-area.controller.ts
|
|
515
|
+
function useAreaController() {
|
|
516
|
+
const {
|
|
517
|
+
createArea: _createArea,
|
|
518
|
+
getAreas: _getAll,
|
|
519
|
+
getAreaById: _getAreaById,
|
|
520
|
+
updateArea: _updateArea,
|
|
521
|
+
updateAreaChecklist: _updateAreaChecklist,
|
|
522
|
+
deleteArea: _deleteById
|
|
523
|
+
} = useAreaRepository();
|
|
524
|
+
const { uploadByFile: _uploadByFile } = useAreaService();
|
|
525
|
+
async function createArea(req, res, next) {
|
|
526
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
527
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
528
|
+
{}
|
|
529
|
+
) : {};
|
|
530
|
+
const createdBy = cookies["user"] || "";
|
|
531
|
+
const payload = { ...req.body, createdBy };
|
|
532
|
+
const { error } = areaSchema.validate(payload);
|
|
533
|
+
if (error) {
|
|
534
|
+
logger4.log({ level: "error", message: error.message });
|
|
535
|
+
next(new BadRequestError4(error.message));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
const id = await _createArea(payload);
|
|
540
|
+
res.status(201).json({ message: "Area created successfully.", id });
|
|
541
|
+
return;
|
|
542
|
+
} catch (error2) {
|
|
543
|
+
logger4.log({ level: "error", message: error2.message });
|
|
544
|
+
next(error2);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function getAll(req, res, next) {
|
|
549
|
+
const query = req.query;
|
|
550
|
+
const validation = Joi2.object({
|
|
551
|
+
page: Joi2.number().min(1).optional().allow("", null),
|
|
552
|
+
limit: Joi2.number().min(1).optional().allow("", null),
|
|
553
|
+
search: Joi2.string().optional().allow("", null),
|
|
554
|
+
startDate: Joi2.alternatives().try(Joi2.date(), Joi2.string()).optional().allow("", null),
|
|
555
|
+
endDate: Joi2.alternatives().try(Joi2.date(), Joi2.string()).optional().allow("", null),
|
|
556
|
+
site: Joi2.string().hex().optional().allow("", null)
|
|
557
|
+
});
|
|
558
|
+
const { error } = validation.validate(query);
|
|
559
|
+
if (error) {
|
|
560
|
+
next(new BadRequestError4(error.message));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const page = parseInt(req.query.page) ?? 1;
|
|
564
|
+
const limit = parseInt(req.query.limit) ?? 20;
|
|
565
|
+
const search = req.query.search ?? "";
|
|
566
|
+
const site = req.query.site ?? "";
|
|
567
|
+
const startDate = req.query.startDate ?? "";
|
|
568
|
+
const endDate = req.query.endDate ?? "";
|
|
569
|
+
try {
|
|
570
|
+
const data = await _getAll({
|
|
571
|
+
page,
|
|
572
|
+
limit,
|
|
573
|
+
search,
|
|
574
|
+
site,
|
|
575
|
+
startDate,
|
|
576
|
+
endDate
|
|
577
|
+
});
|
|
578
|
+
res.json(data);
|
|
579
|
+
return;
|
|
580
|
+
} catch (error2) {
|
|
581
|
+
next(error2);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function getAreaById(req, res, next) {
|
|
586
|
+
const validation = Joi2.string().hex().required();
|
|
587
|
+
const _id = req.params.id;
|
|
588
|
+
const { error } = validation.validate(_id);
|
|
589
|
+
if (error) {
|
|
590
|
+
logger4.log({ level: "error", message: error.message });
|
|
591
|
+
next(new BadRequestError4(error.message));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
const data = await _getAreaById(_id);
|
|
596
|
+
res.json(data);
|
|
597
|
+
return;
|
|
598
|
+
} catch (error2) {
|
|
599
|
+
logger4.log({ level: "error", message: error2.message });
|
|
600
|
+
next(error2);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async function updateArea(req, res, next) {
|
|
605
|
+
const payload = { id: req.params.id, ...req.body };
|
|
606
|
+
const schema = Joi2.object({
|
|
607
|
+
id: Joi2.string().hex().required(),
|
|
608
|
+
name: Joi2.string().required()
|
|
609
|
+
});
|
|
610
|
+
const { error } = schema.validate(payload);
|
|
611
|
+
if (error) {
|
|
612
|
+
logger4.log({ level: "error", message: error.message });
|
|
613
|
+
next(new BadRequestError4(error.message));
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
const { id, ...value } = payload;
|
|
618
|
+
await _updateArea(id, value);
|
|
619
|
+
res.json({ message: "Area updated successfully." });
|
|
620
|
+
return;
|
|
621
|
+
} catch (error2) {
|
|
622
|
+
logger4.log({ level: "error", message: error2.message });
|
|
623
|
+
next(error2);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
async function updateAreaChecklist(req, res, next) {
|
|
628
|
+
const payload = { id: req.params.id, ...req.body };
|
|
629
|
+
const schema = Joi2.object({
|
|
630
|
+
id: Joi2.string().hex().required(),
|
|
631
|
+
checklist: Joi2.array().items(
|
|
632
|
+
Joi2.object({
|
|
633
|
+
_id: Joi2.string().hex().required(),
|
|
634
|
+
name: Joi2.string().required()
|
|
635
|
+
}).required()
|
|
636
|
+
).min(1).unique("_id", { ignoreUndefined: true }).messages({
|
|
637
|
+
"array.unique": "Duplicate checklist items are not allowed"
|
|
638
|
+
})
|
|
639
|
+
});
|
|
640
|
+
const { error } = schema.validate(payload);
|
|
641
|
+
if (error) {
|
|
642
|
+
logger4.log({ level: "error", message: error.message });
|
|
643
|
+
next(new BadRequestError4(error.message));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
const { id, ...value } = payload;
|
|
648
|
+
await _updateAreaChecklist(id, value);
|
|
649
|
+
res.json({ message: "Area updated successfully." });
|
|
650
|
+
return;
|
|
651
|
+
} catch (error2) {
|
|
652
|
+
logger4.log({ level: "error", message: error2.message });
|
|
653
|
+
next(error2);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async function deleteArea(req, res, next) {
|
|
658
|
+
const id = req.params.id;
|
|
659
|
+
const validation = Joi2.object({
|
|
660
|
+
id: Joi2.string().hex().required()
|
|
661
|
+
});
|
|
662
|
+
const { error } = validation.validate({ id });
|
|
663
|
+
if (error) {
|
|
664
|
+
logger4.log({ level: "error", message: error.message });
|
|
665
|
+
next(new BadRequestError4(error.message));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
await _deleteById(id);
|
|
670
|
+
res.json({ message: "Area deleted successfully." });
|
|
671
|
+
return;
|
|
672
|
+
} catch (error2) {
|
|
673
|
+
logger4.log({ level: "error", message: error2.message });
|
|
674
|
+
next(error2);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
async function uploadByFile(req, res, next) {
|
|
679
|
+
if (!req.file) {
|
|
680
|
+
next(new BadRequestError4("File is required!"));
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
684
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
685
|
+
{}
|
|
686
|
+
) : {};
|
|
687
|
+
const createdBy = cookies["user"] || "";
|
|
688
|
+
const { site } = req.body;
|
|
689
|
+
const schema = Joi2.object({
|
|
690
|
+
site: Joi2.string().hex().optional().allow("", null),
|
|
691
|
+
createdBy: Joi2.string().hex().required()
|
|
692
|
+
});
|
|
693
|
+
const { error } = schema.validate({ site, createdBy });
|
|
694
|
+
if (error) {
|
|
695
|
+
logger4.log({ level: "error", message: error.message });
|
|
696
|
+
next(new BadRequestError4(error.message));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
const xlsData = await convertBufferFile(req.file.buffer);
|
|
701
|
+
const dataJson = JSON.stringify(xlsData);
|
|
702
|
+
const result = await _uploadByFile({ dataJson, createdBy, site });
|
|
703
|
+
return res.status(201).json(result);
|
|
704
|
+
} catch (error2) {
|
|
705
|
+
logger4.log({ level: "error", message: error2.message });
|
|
706
|
+
next(error2);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
createArea,
|
|
712
|
+
getAll,
|
|
713
|
+
getAreaById,
|
|
714
|
+
updateArea,
|
|
715
|
+
updateAreaChecklist,
|
|
716
|
+
deleteArea,
|
|
717
|
+
uploadByFile
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/models/hygiene-toilet-location.model.ts
|
|
722
|
+
import { BadRequestError as BadRequestError5 } from "@iservice365/node-server-utils";
|
|
723
|
+
import Joi3 from "joi";
|
|
724
|
+
import { ObjectId as ObjectId3 } from "mongodb";
|
|
725
|
+
var toiletLocationSchema = Joi3.object({
|
|
726
|
+
name: Joi3.string().required(),
|
|
727
|
+
site: Joi3.string().hex().required(),
|
|
728
|
+
createdBy: Joi3.string().hex().required(),
|
|
729
|
+
checklist: Joi3.array().items(
|
|
730
|
+
Joi3.object({
|
|
731
|
+
_id: Joi3.string().hex().required(),
|
|
732
|
+
name: Joi3.string().required()
|
|
733
|
+
})
|
|
734
|
+
).optional(),
|
|
735
|
+
updatedAt: Joi3.date().optional().allow("", null),
|
|
736
|
+
status: Joi3.string().allow("", null).optional()
|
|
737
|
+
});
|
|
738
|
+
function MToiletLocation(value) {
|
|
739
|
+
const { error } = toiletLocationSchema.validate(value);
|
|
740
|
+
if (error) {
|
|
741
|
+
throw new BadRequestError5(error.message);
|
|
742
|
+
}
|
|
743
|
+
if (value.site) {
|
|
744
|
+
try {
|
|
745
|
+
value.site = new ObjectId3(value.site);
|
|
746
|
+
} catch (error2) {
|
|
747
|
+
throw new BadRequestError5("Invalid site ID format.");
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (value.createdBy) {
|
|
751
|
+
try {
|
|
752
|
+
value.createdBy = new ObjectId3(value.createdBy);
|
|
753
|
+
} catch (error2) {
|
|
754
|
+
throw new BadRequestError5("Invalid createdBy ID format.");
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (value.checklist && Array.isArray(value.checklist)) {
|
|
758
|
+
value.checklist = value.checklist.map((item) => {
|
|
759
|
+
try {
|
|
760
|
+
return {
|
|
761
|
+
...item,
|
|
762
|
+
_id: new ObjectId3(item._id)
|
|
763
|
+
};
|
|
764
|
+
} catch (error2) {
|
|
765
|
+
throw new BadRequestError5(
|
|
766
|
+
`Invalid checklist item ID format: ${item._id}`
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
name: value.name,
|
|
773
|
+
site: value.site,
|
|
774
|
+
createdBy: value.createdBy,
|
|
775
|
+
checklist: value.checklist,
|
|
776
|
+
status: value.status ?? "",
|
|
777
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
778
|
+
updatedAt: value.updatedAt ?? "",
|
|
779
|
+
deletedAt: value.deletedAt ?? ""
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/repositories/hygiene-toilet-location.repository.ts
|
|
784
|
+
import { ObjectId as ObjectId4 } from "mongodb";
|
|
785
|
+
import {
|
|
786
|
+
useAtlas as useAtlas3,
|
|
787
|
+
InternalServerError as InternalServerError2,
|
|
788
|
+
paginate as paginate2,
|
|
789
|
+
BadRequestError as BadRequestError6,
|
|
790
|
+
NotFoundError as NotFoundError3,
|
|
791
|
+
useCache as useCache2,
|
|
792
|
+
logger as logger5,
|
|
793
|
+
makeCacheKey as makeCacheKey2
|
|
794
|
+
} from "@iservice365/node-server-utils";
|
|
795
|
+
function useToiletLocationRepository() {
|
|
796
|
+
const db = useAtlas3.getDb();
|
|
797
|
+
if (!db) {
|
|
798
|
+
throw new InternalServerError2("Unable to connect to server.");
|
|
799
|
+
}
|
|
800
|
+
const namespace_collection = "hygiene-toilet-locations";
|
|
801
|
+
const collection = db.collection(namespace_collection);
|
|
802
|
+
const { delNamespace, setCache, getCache, delCache } = useCache2(namespace_collection);
|
|
803
|
+
async function createIndexes() {
|
|
804
|
+
try {
|
|
805
|
+
await collection.createIndexes([
|
|
806
|
+
{ key: { site: 1 } },
|
|
807
|
+
{ key: { name: "text" } }
|
|
808
|
+
]);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
throw new InternalServerError2("Failed to create index on site.");
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async function createUniqueIndex() {
|
|
814
|
+
try {
|
|
815
|
+
await collection.createIndex(
|
|
816
|
+
{ name: 1, site: 1, deletedAt: 1 },
|
|
817
|
+
{ unique: true }
|
|
818
|
+
);
|
|
819
|
+
} catch (error) {
|
|
820
|
+
throw new InternalServerError2(
|
|
821
|
+
"Failed to create unique index on hygiene area."
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async function create(value, session) {
|
|
826
|
+
try {
|
|
827
|
+
value = MToiletLocation(value);
|
|
828
|
+
const res = await collection.insertOne(value, { session });
|
|
829
|
+
delNamespace().then(() => {
|
|
830
|
+
logger5.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
831
|
+
}).catch((err) => {
|
|
832
|
+
logger5.error(
|
|
833
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
834
|
+
err
|
|
835
|
+
);
|
|
836
|
+
});
|
|
837
|
+
return res.insertedId;
|
|
838
|
+
} catch (error) {
|
|
839
|
+
const isDuplicated = error.message.includes("duplicate");
|
|
840
|
+
if (isDuplicated) {
|
|
841
|
+
throw new BadRequestError6("Toilet location already exists.");
|
|
842
|
+
}
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async function updateToiletLocation(_id, value) {
|
|
847
|
+
try {
|
|
848
|
+
_id = new ObjectId4(_id);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
throw new BadRequestError6("Invalid area ID format.");
|
|
851
|
+
}
|
|
852
|
+
if (value.checklist && Array.isArray(value.checklist)) {
|
|
853
|
+
value.checklist = value.checklist.map((item) => {
|
|
854
|
+
try {
|
|
855
|
+
return {
|
|
856
|
+
...item,
|
|
857
|
+
_id: new ObjectId4(item._id)
|
|
858
|
+
};
|
|
859
|
+
} catch (error) {
|
|
860
|
+
throw new BadRequestError6(
|
|
861
|
+
`Invalid checklist item ID format: ${item._id}`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
try {
|
|
867
|
+
const updateValue = { ...value, updatedAt: /* @__PURE__ */ new Date() };
|
|
868
|
+
const res = await collection.updateOne({ _id }, { $set: updateValue });
|
|
869
|
+
if (res.modifiedCount === 0) {
|
|
870
|
+
throw new InternalServerError2("Unable to update toilet location.");
|
|
871
|
+
}
|
|
872
|
+
delNamespace().then(() => {
|
|
873
|
+
logger5.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
874
|
+
}).catch((err) => {
|
|
875
|
+
logger5.error(
|
|
876
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
877
|
+
err
|
|
878
|
+
);
|
|
879
|
+
});
|
|
880
|
+
return res.modifiedCount;
|
|
881
|
+
} catch (error) {
|
|
882
|
+
const isDuplicated = error.message.includes("duplicate");
|
|
883
|
+
if (isDuplicated) {
|
|
884
|
+
throw new BadRequestError6("Toilet location already exists.");
|
|
885
|
+
}
|
|
886
|
+
throw error;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async function deleteToiletLocation(_id, session) {
|
|
890
|
+
try {
|
|
891
|
+
_id = new ObjectId4(_id);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
throw new BadRequestError6("Invalid area ID format.");
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
const updateValue = {
|
|
897
|
+
status: "deleted",
|
|
898
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
899
|
+
deletedAt: /* @__PURE__ */ new Date()
|
|
900
|
+
};
|
|
901
|
+
const res = await collection.updateOne(
|
|
902
|
+
{ _id },
|
|
903
|
+
{ $set: updateValue },
|
|
904
|
+
{ session }
|
|
905
|
+
);
|
|
906
|
+
if (res.modifiedCount === 0) {
|
|
907
|
+
throw new InternalServerError2("Unable to delete toilet location.");
|
|
908
|
+
}
|
|
909
|
+
delNamespace().then(() => {
|
|
910
|
+
logger5.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
911
|
+
}).catch((err) => {
|
|
912
|
+
logger5.error(
|
|
913
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
914
|
+
err
|
|
915
|
+
);
|
|
916
|
+
});
|
|
917
|
+
return res.modifiedCount;
|
|
918
|
+
} catch (error) {
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
async function getToiletLocations({
|
|
923
|
+
page = 1,
|
|
924
|
+
limit = 10,
|
|
925
|
+
search = "",
|
|
926
|
+
sort = {},
|
|
927
|
+
startDate = "",
|
|
928
|
+
endDate = "",
|
|
929
|
+
site = ""
|
|
930
|
+
}) {
|
|
931
|
+
page = page > 0 ? page - 1 : 0;
|
|
932
|
+
let dateFilter = {};
|
|
933
|
+
try {
|
|
934
|
+
site = new ObjectId4(site);
|
|
935
|
+
} catch (error) {
|
|
936
|
+
throw new BadRequestError6("Invalid site ID format.");
|
|
937
|
+
}
|
|
938
|
+
const query = {
|
|
939
|
+
status: { $ne: "deleted" }
|
|
940
|
+
};
|
|
941
|
+
const cacheOptions = {
|
|
942
|
+
site: site.toString()
|
|
943
|
+
};
|
|
944
|
+
sort = Object.keys(sort).length > 0 ? sort : { _id: -1 };
|
|
945
|
+
cacheOptions.sort = JSON.stringify(sort);
|
|
946
|
+
if (search) {
|
|
947
|
+
query.$or = [{ name: { $regex: search, $options: "i" } }];
|
|
948
|
+
cacheOptions.search = search;
|
|
949
|
+
}
|
|
950
|
+
delNamespace().then(() => {
|
|
951
|
+
logger5.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
952
|
+
}).catch((err) => {
|
|
953
|
+
logger5.error(
|
|
954
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
955
|
+
err
|
|
956
|
+
);
|
|
957
|
+
});
|
|
958
|
+
const cacheKey = makeCacheKey2(namespace_collection, cacheOptions);
|
|
959
|
+
const cachedData = await getCache(cacheKey);
|
|
960
|
+
if (cachedData) {
|
|
961
|
+
logger5.info(`Cache hit for key: ${cacheKey}`);
|
|
962
|
+
return cachedData;
|
|
963
|
+
}
|
|
964
|
+
if (startDate && endDate) {
|
|
965
|
+
dateFilter = {
|
|
966
|
+
createdAt: {
|
|
967
|
+
$gte: new Date(startDate),
|
|
968
|
+
$lte: new Date(endDate)
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
} else if (startDate) {
|
|
972
|
+
dateFilter = { createdAt: { $gte: new Date(startDate) } };
|
|
973
|
+
} else if (endDate) {
|
|
974
|
+
dateFilter = { createdAt: { $lte: new Date(endDate) } };
|
|
975
|
+
}
|
|
976
|
+
try {
|
|
977
|
+
const items = await collection.aggregate([
|
|
978
|
+
{
|
|
979
|
+
$match: {
|
|
980
|
+
...dateFilter,
|
|
981
|
+
...query
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
$lookup: {
|
|
986
|
+
from: "sites",
|
|
987
|
+
localField: "site",
|
|
988
|
+
foreignField: "_id",
|
|
989
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
990
|
+
as: "site"
|
|
991
|
+
}
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
$unwind: {
|
|
995
|
+
path: "$site",
|
|
996
|
+
preserveNullAndEmptyArrays: true
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
$lookup: {
|
|
1001
|
+
from: "users",
|
|
1002
|
+
localField: "createdBy",
|
|
1003
|
+
foreignField: "_id",
|
|
1004
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
1005
|
+
as: "createdBy"
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
$unwind: {
|
|
1010
|
+
path: "$createdBy",
|
|
1011
|
+
preserveNullAndEmptyArrays: true
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
$project: {
|
|
1016
|
+
name: 1,
|
|
1017
|
+
site: "$site._id",
|
|
1018
|
+
siteName: "$site.name",
|
|
1019
|
+
createdByName: "$createdBy.name",
|
|
1020
|
+
checklist: 1,
|
|
1021
|
+
status: 1,
|
|
1022
|
+
createdAt: 1
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
{ $sort: { _id: -1 } },
|
|
1026
|
+
{ $skip: page * limit },
|
|
1027
|
+
{ $limit: limit }
|
|
1028
|
+
]).toArray();
|
|
1029
|
+
const length = await collection.countDocuments(query);
|
|
1030
|
+
const data = paginate2(items, page, limit, length);
|
|
1031
|
+
setCache(cacheKey, data, 15 * 60).then(() => {
|
|
1032
|
+
logger5.info(`Cache set for key: ${cacheKey}`);
|
|
1033
|
+
}).catch((err) => {
|
|
1034
|
+
logger5.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
1035
|
+
});
|
|
1036
|
+
return data;
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
throw error;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
async function getToiletLocationByName(name, site) {
|
|
1042
|
+
try {
|
|
1043
|
+
if (site)
|
|
1044
|
+
site = new ObjectId4(site);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
throw new BadRequestError6("Invalid site ID format.");
|
|
1047
|
+
}
|
|
1048
|
+
try {
|
|
1049
|
+
return await collection.findOne({
|
|
1050
|
+
name: { $regex: new RegExp(`^${name}$`, "i") },
|
|
1051
|
+
...site && { site }
|
|
1052
|
+
});
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
throw new BadRequestError6("Unable to fetch toilet location by name.");
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
async function updateToiletLocationChecklist(_id, value) {
|
|
1058
|
+
try {
|
|
1059
|
+
_id = new ObjectId4(_id);
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
throw new BadRequestError6("Invalid area ID format.");
|
|
1062
|
+
}
|
|
1063
|
+
if (value.checklist && Array.isArray(value.checklist)) {
|
|
1064
|
+
value.checklist = value.checklist.map((item) => {
|
|
1065
|
+
try {
|
|
1066
|
+
return {
|
|
1067
|
+
...item,
|
|
1068
|
+
_id: new ObjectId4(item._id)
|
|
1069
|
+
};
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
throw new BadRequestError6(
|
|
1072
|
+
`Invalid checklist item ID format: ${item._id}`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
try {
|
|
1078
|
+
const updateValue = { ...value, updatedAt: /* @__PURE__ */ new Date() };
|
|
1079
|
+
const res = await collection.updateOne({ _id }, { $set: updateValue });
|
|
1080
|
+
if (res.modifiedCount === 0) {
|
|
1081
|
+
throw new InternalServerError2("Unable to update toilet location.");
|
|
1082
|
+
}
|
|
1083
|
+
delNamespace().then(() => {
|
|
1084
|
+
logger5.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
1085
|
+
}).catch((err) => {
|
|
1086
|
+
logger5.error(
|
|
1087
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
1088
|
+
err
|
|
1089
|
+
);
|
|
1090
|
+
});
|
|
1091
|
+
return res.modifiedCount;
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
throw error;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
async function getToiletLocationById(_id) {
|
|
1097
|
+
try {
|
|
1098
|
+
_id = new ObjectId4(_id);
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
throw new BadRequestError6("Invalid area ID format.");
|
|
1101
|
+
}
|
|
1102
|
+
const query = {
|
|
1103
|
+
_id,
|
|
1104
|
+
status: { $ne: "deleted" }
|
|
1105
|
+
};
|
|
1106
|
+
const cacheKey = makeCacheKey2(namespace_collection, {
|
|
1107
|
+
_id: _id.toString()
|
|
1108
|
+
});
|
|
1109
|
+
const cachedData = await getCache(cacheKey);
|
|
1110
|
+
if (cachedData) {
|
|
1111
|
+
logger5.info(`Cache hit for key: ${cacheKey}`);
|
|
1112
|
+
return cachedData;
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
const data = await collection.aggregate([
|
|
1116
|
+
{ $match: query },
|
|
1117
|
+
{
|
|
1118
|
+
$project: {
|
|
1119
|
+
name: 1,
|
|
1120
|
+
checklist: 1
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
]).toArray();
|
|
1124
|
+
if (!data || !data.length) {
|
|
1125
|
+
throw new NotFoundError3("Area not found.");
|
|
1126
|
+
}
|
|
1127
|
+
setCache(cacheKey, data[0], 15 * 60).then(() => {
|
|
1128
|
+
logger5.info(`Cache set for key: ${cacheKey}`);
|
|
1129
|
+
}).catch((err) => {
|
|
1130
|
+
logger5.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
1131
|
+
});
|
|
1132
|
+
return data[0];
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
createIndexes,
|
|
1139
|
+
createUniqueIndex,
|
|
1140
|
+
getToiletLocations,
|
|
1141
|
+
create,
|
|
1142
|
+
updateToiletLocation,
|
|
1143
|
+
deleteToiletLocation,
|
|
1144
|
+
getToiletLocationByName,
|
|
1145
|
+
getToiletLocationById,
|
|
1146
|
+
updateToiletLocationChecklist
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// src/services/hygiene-toilet-location.service.ts
|
|
1151
|
+
import {
|
|
1152
|
+
BadRequestError as BadRequestError7,
|
|
1153
|
+
logger as logger6,
|
|
1154
|
+
NotFoundError as NotFoundError4,
|
|
1155
|
+
useAtlas as useAtlas4
|
|
1156
|
+
} from "@iservice365/node-server-utils";
|
|
1157
|
+
function useToiletLocationService() {
|
|
1158
|
+
const { create: _createToilet } = useToiletLocationRepository();
|
|
1159
|
+
async function uploadByFile({
|
|
1160
|
+
dataJson,
|
|
1161
|
+
createdBy,
|
|
1162
|
+
site
|
|
1163
|
+
}) {
|
|
1164
|
+
let dataArray;
|
|
1165
|
+
try {
|
|
1166
|
+
dataArray = JSON.parse(dataJson);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
throw new BadRequestError7("Invalid JSON format for data in excel");
|
|
1169
|
+
}
|
|
1170
|
+
if (!dataArray || dataArray.length === 0) {
|
|
1171
|
+
throw new NotFoundError4("No data found in the uploaded file");
|
|
1172
|
+
}
|
|
1173
|
+
const session = useAtlas4.getClient()?.startSession();
|
|
1174
|
+
const insertedAreaIds = [];
|
|
1175
|
+
try {
|
|
1176
|
+
session?.startTransaction();
|
|
1177
|
+
for (const row of dataArray) {
|
|
1178
|
+
if (!row?.AREA_NAME) {
|
|
1179
|
+
logger6.warn("Skipping row with missing TOILET NAME:", row);
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
try {
|
|
1183
|
+
const insertedId = await _createToilet(
|
|
1184
|
+
{
|
|
1185
|
+
name: String(row.TOILET_NAME).trim(),
|
|
1186
|
+
site,
|
|
1187
|
+
createdBy
|
|
1188
|
+
},
|
|
1189
|
+
session
|
|
1190
|
+
);
|
|
1191
|
+
insertedAreaIds.push(insertedId);
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
logger6.error(
|
|
1194
|
+
`Error creating area "${row.AREA_NAME}":`,
|
|
1195
|
+
error.message
|
|
1196
|
+
);
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
await session?.commitTransaction();
|
|
1201
|
+
logger6.info(`Successfully uploaded ${insertedAreaIds.length} areas`);
|
|
1202
|
+
return {
|
|
1203
|
+
message: `Successfully uploaded ${insertedAreaIds.length} areas`
|
|
1204
|
+
};
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
await session?.abortTransaction();
|
|
1207
|
+
logger6.error("Error while uploading area information", error);
|
|
1208
|
+
throw error;
|
|
1209
|
+
} finally {
|
|
1210
|
+
session?.endSession();
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return {
|
|
1214
|
+
uploadByFile
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// src/controllers/hygiene-toilet-location.controller.ts
|
|
1219
|
+
import Joi4 from "joi";
|
|
1220
|
+
import { BadRequestError as BadRequestError8, logger as logger7 } from "@iservice365/node-server-utils";
|
|
1221
|
+
function useToiletLocationController() {
|
|
1222
|
+
const { uploadByFile: _uploadByFile } = useToiletLocationService();
|
|
1223
|
+
const {
|
|
1224
|
+
create: _createToiletLocation,
|
|
1225
|
+
updateToiletLocation: _updateToiletLocation,
|
|
1226
|
+
updateToiletLocationChecklist: _updateToiletLocationChecklist,
|
|
1227
|
+
getToiletLocationById: _getToiletLocationById,
|
|
1228
|
+
getToiletLocations: _getAll,
|
|
1229
|
+
deleteToiletLocation: _deleteById
|
|
1230
|
+
} = useToiletLocationRepository();
|
|
1231
|
+
async function getAll(req, res, next) {
|
|
1232
|
+
const query = req.query;
|
|
1233
|
+
const validation = Joi4.object({
|
|
1234
|
+
page: Joi4.number().min(1).optional().allow("", null),
|
|
1235
|
+
limit: Joi4.number().min(1).optional().allow("", null),
|
|
1236
|
+
search: Joi4.string().optional().allow("", null),
|
|
1237
|
+
site: Joi4.string().hex().optional().allow("", null)
|
|
1238
|
+
});
|
|
1239
|
+
const { error } = validation.validate(query);
|
|
1240
|
+
if (error) {
|
|
1241
|
+
next(new BadRequestError8(error.message));
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
const page = parseInt(req.query.page) ?? 1;
|
|
1245
|
+
let limit = parseInt(req.query.limit) ?? 20;
|
|
1246
|
+
limit = isNaN(limit) ? 20 : limit;
|
|
1247
|
+
const sort = req.query.sort ? String(req.query.sort).split(",") : "";
|
|
1248
|
+
const sortOrder = req.query.sortOrder ? String(req.query.sortOrder).split(",") : "";
|
|
1249
|
+
const sortObj = {};
|
|
1250
|
+
if (sort && Array.isArray(sort) && sort.length && sortOrder && Array.isArray(sortOrder) && sortOrder.length) {
|
|
1251
|
+
sort.forEach((field, index) => {
|
|
1252
|
+
sortObj[field] = sortOrder[index] === "desc" ? -1 : 1;
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
const site = req.query.site ?? "";
|
|
1256
|
+
const search = req.query.search ?? "";
|
|
1257
|
+
try {
|
|
1258
|
+
const buildings = await _getAll({
|
|
1259
|
+
page,
|
|
1260
|
+
limit,
|
|
1261
|
+
sort: sortObj,
|
|
1262
|
+
site,
|
|
1263
|
+
search
|
|
1264
|
+
});
|
|
1265
|
+
res.json(buildings);
|
|
1266
|
+
return;
|
|
1267
|
+
} catch (error2) {
|
|
1268
|
+
next(error2);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
async function createToiletLocation(req, res, next) {
|
|
1272
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
1273
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
1274
|
+
{}
|
|
1275
|
+
) : {};
|
|
1276
|
+
const createdBy = cookies["user"] || "";
|
|
1277
|
+
const payload = { ...req.body, createdBy };
|
|
1278
|
+
const schema = Joi4.object({
|
|
1279
|
+
name: Joi4.string().required(),
|
|
1280
|
+
site: Joi4.string().hex().optional().allow("", null),
|
|
1281
|
+
status: Joi4.string().optional().allow("", null),
|
|
1282
|
+
createdBy: Joi4.string().hex().optional().allow("", null)
|
|
1283
|
+
});
|
|
1284
|
+
const { error } = schema.validate(payload);
|
|
1285
|
+
if (error) {
|
|
1286
|
+
next(new BadRequestError8(error.message));
|
|
1287
|
+
logger7.info(`Controller: ${error.message}`);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
try {
|
|
1291
|
+
const result = await _createToiletLocation(payload);
|
|
1292
|
+
res.status(201).json(result);
|
|
1293
|
+
return;
|
|
1294
|
+
} catch (error2) {
|
|
1295
|
+
next(error2);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
async function updateToiletLocation(req, res, next) {
|
|
1299
|
+
const payload = { id: req.params.id, ...req.body };
|
|
1300
|
+
const schema = Joi4.object({
|
|
1301
|
+
id: Joi4.string().hex().required(),
|
|
1302
|
+
name: Joi4.string().required(),
|
|
1303
|
+
checklist: Joi4.array().items(
|
|
1304
|
+
Joi4.object({
|
|
1305
|
+
_id: Joi4.string().hex().required(),
|
|
1306
|
+
name: Joi4.string().required()
|
|
1307
|
+
}).optional()
|
|
1308
|
+
),
|
|
1309
|
+
site: Joi4.string().hex().optional().allow("", null),
|
|
1310
|
+
status: Joi4.string().optional().allow("", null)
|
|
1311
|
+
});
|
|
1312
|
+
const { error } = schema.validate(payload);
|
|
1313
|
+
if (error) {
|
|
1314
|
+
logger7.log({ level: "error", message: error.message });
|
|
1315
|
+
next(new BadRequestError8(error.message));
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
try {
|
|
1319
|
+
const { id, ...value } = payload;
|
|
1320
|
+
await _updateToiletLocation(id, value);
|
|
1321
|
+
res.status(201).json({ message: "Toilet location updated successfully." });
|
|
1322
|
+
return;
|
|
1323
|
+
} catch (error2) {
|
|
1324
|
+
logger7.log({ level: "error", message: error2.message });
|
|
1325
|
+
next(error2);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
async function deleteToiletLocation(req, res, next) {
|
|
1329
|
+
const id = req.params.id;
|
|
1330
|
+
const validation = Joi4.object({
|
|
1331
|
+
id: Joi4.string().hex().required()
|
|
1332
|
+
});
|
|
1333
|
+
const { error } = validation.validate({ id });
|
|
1334
|
+
if (error) {
|
|
1335
|
+
next(new BadRequestError8(error.message));
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
try {
|
|
1339
|
+
const message = await _deleteById(id);
|
|
1340
|
+
res.json(message);
|
|
1341
|
+
return;
|
|
1342
|
+
} catch (error2) {
|
|
1343
|
+
next(error2);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
async function updateToiletLocationChecklist(req, res, next) {
|
|
1347
|
+
const payload = { id: req.params.id, ...req.body };
|
|
1348
|
+
const schema = Joi4.object({
|
|
1349
|
+
id: Joi4.string().hex().required(),
|
|
1350
|
+
checklist: Joi4.array().items(
|
|
1351
|
+
Joi4.object({
|
|
1352
|
+
_id: Joi4.string().hex().required(),
|
|
1353
|
+
name: Joi4.string().required()
|
|
1354
|
+
}).required()
|
|
1355
|
+
).min(1).unique("_id", { ignoreUndefined: true }).messages({
|
|
1356
|
+
"array.unique": "Duplicate checklist items are not allowed"
|
|
1357
|
+
})
|
|
1358
|
+
});
|
|
1359
|
+
const { error } = schema.validate(payload);
|
|
1360
|
+
if (error) {
|
|
1361
|
+
logger7.log({ level: "error", message: error.message });
|
|
1362
|
+
next(new BadRequestError8(error.message));
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
const { id, ...value } = payload;
|
|
1367
|
+
await _updateToiletLocationChecklist(id, value);
|
|
1368
|
+
res.json({ message: "Toilet location updated successfully." });
|
|
1369
|
+
return;
|
|
1370
|
+
} catch (error2) {
|
|
1371
|
+
logger7.log({ level: "error", message: error2.message });
|
|
1372
|
+
next(error2);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
async function getToiletLocationById(req, res, next) {
|
|
1377
|
+
const validation = Joi4.string().hex().required();
|
|
1378
|
+
const _id = req.params.id;
|
|
1379
|
+
const { error } = validation.validate(_id);
|
|
1380
|
+
if (error) {
|
|
1381
|
+
logger7.log({ level: "error", message: error.message });
|
|
1382
|
+
next(new BadRequestError8(error.message));
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
try {
|
|
1386
|
+
const data = await _getToiletLocationById(_id);
|
|
1387
|
+
res.json(data);
|
|
1388
|
+
return;
|
|
1389
|
+
} catch (error2) {
|
|
1390
|
+
logger7.log({ level: "error", message: error2.message });
|
|
1391
|
+
next(error2);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
async function uploadByFile(req, res, next) {
|
|
1396
|
+
if (!req.file) {
|
|
1397
|
+
next(new BadRequestError8("File is required!"));
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
1401
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
1402
|
+
{}
|
|
1403
|
+
) : {};
|
|
1404
|
+
const createdBy = cookies["user"] || "";
|
|
1405
|
+
const { site } = req.body;
|
|
1406
|
+
const schema = Joi4.object({
|
|
1407
|
+
site: Joi4.string().hex().optional().allow("", null),
|
|
1408
|
+
createdBy: Joi4.string().hex().required()
|
|
1409
|
+
});
|
|
1410
|
+
const { error } = schema.validate({ site, createdBy });
|
|
1411
|
+
if (error) {
|
|
1412
|
+
logger7.log({ level: "error", message: error.message });
|
|
1413
|
+
next(new BadRequestError8(error.message));
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
try {
|
|
1417
|
+
const xlsData = await convertBufferFile(req.file.buffer);
|
|
1418
|
+
const dataJson = JSON.stringify(xlsData);
|
|
1419
|
+
const result = await _uploadByFile({ dataJson, createdBy, site });
|
|
1420
|
+
return res.status(201).json(result);
|
|
1421
|
+
} catch (error2) {
|
|
1422
|
+
logger7.log({ level: "error", message: error2.message });
|
|
1423
|
+
next(error2);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
return {
|
|
1428
|
+
getAll,
|
|
1429
|
+
createToiletLocation,
|
|
1430
|
+
updateToiletLocation,
|
|
1431
|
+
deleteToiletLocation,
|
|
1432
|
+
updateToiletLocationChecklist,
|
|
1433
|
+
getToiletLocationById,
|
|
1434
|
+
uploadByFile
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// src/models/hygiene-parent-checklist.model.ts
|
|
1439
|
+
import { BadRequestError as BadRequestError9, logger as logger8 } from "@iservice365/node-server-utils";
|
|
1440
|
+
import Joi5 from "joi";
|
|
1441
|
+
import { ObjectId as ObjectId5 } from "mongodb";
|
|
1442
|
+
var allowedTypes = ["cleaner", "toilet"];
|
|
1443
|
+
var allowedStatus = [
|
|
1444
|
+
"To Do",
|
|
1445
|
+
"Pending",
|
|
1446
|
+
"In Progress",
|
|
1447
|
+
"Completed",
|
|
1448
|
+
"Expired"
|
|
1449
|
+
];
|
|
1450
|
+
var parentChecklistSchema = Joi5.object({
|
|
1451
|
+
date: Joi5.date().required(),
|
|
1452
|
+
status: Joi5.array().items(
|
|
1453
|
+
Joi5.object({
|
|
1454
|
+
type: Joi5.string().required().valid(...allowedTypes),
|
|
1455
|
+
site: Joi5.string().hex().required()
|
|
1456
|
+
})
|
|
1457
|
+
).optional()
|
|
1458
|
+
});
|
|
1459
|
+
function MParentChecklist(value) {
|
|
1460
|
+
const { error } = parentChecklistSchema.validate(value);
|
|
1461
|
+
if (error) {
|
|
1462
|
+
logger8.info(`Hygiene Parent Checklist Model: ${error.message}`);
|
|
1463
|
+
throw new BadRequestError9(error.message);
|
|
1464
|
+
}
|
|
1465
|
+
if (value.status && Array.isArray(value.status)) {
|
|
1466
|
+
value.status = value.status.map((item) => {
|
|
1467
|
+
try {
|
|
1468
|
+
return {
|
|
1469
|
+
...item,
|
|
1470
|
+
site: new ObjectId5(item.site),
|
|
1471
|
+
status: item.status || "To Do",
|
|
1472
|
+
completedAt: item.completedAt || ""
|
|
1473
|
+
};
|
|
1474
|
+
} catch (error2) {
|
|
1475
|
+
throw new BadRequestError9(
|
|
1476
|
+
`Invalid status site ID format: ${item.site}`
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
return {
|
|
1482
|
+
date: new Date(value.date),
|
|
1483
|
+
status: value.status,
|
|
1484
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1485
|
+
updatedAt: value.updatedAt ?? ""
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// src/repositories/hygiene-parent-checklist.repository.ts
|
|
1490
|
+
import { ObjectId as ObjectId6 } from "mongodb";
|
|
1491
|
+
import {
|
|
1492
|
+
useAtlas as useAtlas5,
|
|
1493
|
+
InternalServerError as InternalServerError3,
|
|
1494
|
+
paginate as paginate3,
|
|
1495
|
+
useCache as useCache3,
|
|
1496
|
+
logger as logger9,
|
|
1497
|
+
makeCacheKey as makeCacheKey3,
|
|
1498
|
+
BadRequestError as BadRequestError10
|
|
1499
|
+
} from "@iservice365/node-server-utils";
|
|
1500
|
+
function useParentChecklistRepo() {
|
|
1501
|
+
const db = useAtlas5.getDb();
|
|
1502
|
+
if (!db) {
|
|
1503
|
+
throw new InternalServerError3("Unable to connect to server.");
|
|
1504
|
+
}
|
|
1505
|
+
const namespace_collection = "hygiene-parent-checklist";
|
|
1506
|
+
const collection = db.collection(namespace_collection);
|
|
1507
|
+
const { delNamespace, setCache, getCache } = useCache3(namespace_collection);
|
|
1508
|
+
async function createIndex() {
|
|
1509
|
+
try {
|
|
1510
|
+
await collection.createIndexes([
|
|
1511
|
+
{ key: { date: 1 } },
|
|
1512
|
+
{ key: { "status.type": 1, "status.site": 1 } }
|
|
1513
|
+
]);
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
throw new InternalServerError3(
|
|
1516
|
+
"Failed to create index on hygiene parent checklist."
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
async function createParentChecklist(value, session) {
|
|
1521
|
+
try {
|
|
1522
|
+
const currentDate = value.date ? new Date(value.date) : /* @__PURE__ */ new Date();
|
|
1523
|
+
const startOfDay = new Date(currentDate);
|
|
1524
|
+
startOfDay.setUTCHours(0, 0, 0, 0);
|
|
1525
|
+
const endOfDay = new Date(currentDate);
|
|
1526
|
+
endOfDay.setUTCHours(23, 59, 59, 999);
|
|
1527
|
+
const existingChecklist = await collection.findOne({
|
|
1528
|
+
date: {
|
|
1529
|
+
$gte: startOfDay,
|
|
1530
|
+
$lte: endOfDay
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
if (existingChecklist) {
|
|
1534
|
+
const dateStr2 = currentDate.toISOString().split("T")[0];
|
|
1535
|
+
logger9.info(`Parent checklist already exists for today: ${dateStr2}`);
|
|
1536
|
+
return existingChecklist._id;
|
|
1537
|
+
}
|
|
1538
|
+
const processedValue = MParentChecklist(value);
|
|
1539
|
+
const result = await collection.insertOne(processedValue, { session });
|
|
1540
|
+
delNamespace().then(() => {
|
|
1541
|
+
logger9.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
1542
|
+
}).catch((err) => {
|
|
1543
|
+
logger9.error(
|
|
1544
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
1545
|
+
err
|
|
1546
|
+
);
|
|
1547
|
+
});
|
|
1548
|
+
const dateStr = currentDate.toISOString().split("T")[0];
|
|
1549
|
+
logger9.info(
|
|
1550
|
+
`Created new parent checklist ${result.insertedId} for today: ${dateStr}`
|
|
1551
|
+
);
|
|
1552
|
+
return result.insertedId;
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
logger9.error("Failed to create daily parent checklist", error);
|
|
1555
|
+
throw error;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
async function getAllParentChecklist({
|
|
1559
|
+
page = 1,
|
|
1560
|
+
limit = 10,
|
|
1561
|
+
search = "",
|
|
1562
|
+
site,
|
|
1563
|
+
type,
|
|
1564
|
+
startDate = "",
|
|
1565
|
+
endDate = ""
|
|
1566
|
+
}) {
|
|
1567
|
+
page = page > 0 ? page - 1 : 0;
|
|
1568
|
+
const query = {};
|
|
1569
|
+
const cacheOptions = {
|
|
1570
|
+
page,
|
|
1571
|
+
limit
|
|
1572
|
+
};
|
|
1573
|
+
try {
|
|
1574
|
+
site = new ObjectId6(site);
|
|
1575
|
+
cacheOptions.site = site.toString();
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
throw new BadRequestError10("Invalid site ID format.");
|
|
1578
|
+
}
|
|
1579
|
+
cacheOptions.type = type;
|
|
1580
|
+
query.status = {
|
|
1581
|
+
$elemMatch: {
|
|
1582
|
+
site: new ObjectId6(site),
|
|
1583
|
+
type
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
if (search) {
|
|
1587
|
+
query.$or = [{ name: { $regex: search, $options: "i" } }];
|
|
1588
|
+
cacheOptions.search = search;
|
|
1589
|
+
}
|
|
1590
|
+
if (startDate && endDate) {
|
|
1591
|
+
query.createdAt = {
|
|
1592
|
+
$gte: new Date(startDate),
|
|
1593
|
+
$lte: new Date(endDate)
|
|
1594
|
+
};
|
|
1595
|
+
cacheOptions.startDate = new Date(startDate).toISOString().split("T")[0];
|
|
1596
|
+
cacheOptions.endDate = new Date(endDate).toISOString().split("T")[0];
|
|
1597
|
+
} else if (startDate) {
|
|
1598
|
+
query.createdAt = { $gte: new Date(startDate) };
|
|
1599
|
+
cacheOptions.startDate = new Date(startDate).toISOString().split("T")[0];
|
|
1600
|
+
} else if (endDate) {
|
|
1601
|
+
query.createdAt = { $lte: new Date(endDate) };
|
|
1602
|
+
cacheOptions.endDate = new Date(endDate).toISOString().split("T")[0];
|
|
1603
|
+
}
|
|
1604
|
+
const cacheKey = makeCacheKey3(namespace_collection, cacheOptions);
|
|
1605
|
+
const cachedData = await getCache(cacheKey);
|
|
1606
|
+
if (cachedData) {
|
|
1607
|
+
logger9.info(`Cache hit for key: ${cacheKey}`);
|
|
1608
|
+
return cachedData;
|
|
1609
|
+
}
|
|
1610
|
+
try {
|
|
1611
|
+
const pipeline = [{ $match: query }];
|
|
1612
|
+
const filterConditions = [];
|
|
1613
|
+
filterConditions.push({ $eq: ["$$this.site", new ObjectId6(site)] });
|
|
1614
|
+
filterConditions.push({ $eq: ["$$this.type", type] });
|
|
1615
|
+
pipeline.push({
|
|
1616
|
+
$addFields: {
|
|
1617
|
+
filteredStatus: {
|
|
1618
|
+
$filter: {
|
|
1619
|
+
input: "$status",
|
|
1620
|
+
cond: { $and: filterConditions }
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
pipeline.push({
|
|
1626
|
+
$match: {
|
|
1627
|
+
filteredStatus: { $ne: [] }
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
pipeline.push({
|
|
1631
|
+
$addFields: {
|
|
1632
|
+
statusObj: { $arrayElemAt: ["$filteredStatus", 0] }
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
pipeline.push({
|
|
1636
|
+
$project: {
|
|
1637
|
+
_id: 1,
|
|
1638
|
+
date: 1,
|
|
1639
|
+
status: "$statusObj.status",
|
|
1640
|
+
completedAt: "$statusObj.completedAt",
|
|
1641
|
+
createdAt: 1
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
pipeline.push(
|
|
1645
|
+
{ $sort: { _id: -1 } },
|
|
1646
|
+
{ $skip: page * limit },
|
|
1647
|
+
{ $limit: limit }
|
|
1648
|
+
);
|
|
1649
|
+
const items = await collection.aggregate(pipeline).toArray();
|
|
1650
|
+
const length = await collection.countDocuments(query);
|
|
1651
|
+
const data = paginate3(items, page, limit, length);
|
|
1652
|
+
setCache(cacheKey, data, 15 * 60).then(() => {
|
|
1653
|
+
logger9.info(`Cache set for key: ${cacheKey}`);
|
|
1654
|
+
}).catch((err) => {
|
|
1655
|
+
logger9.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
1656
|
+
});
|
|
1657
|
+
return data;
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
throw error;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
async function updateParentChecklistStatuses(date) {
|
|
1663
|
+
try {
|
|
1664
|
+
const currentDate = /* @__PURE__ */ new Date();
|
|
1665
|
+
const dateToUpdate = date || new Date(currentDate.getTime() - 24 * 60 * 60 * 1e3);
|
|
1666
|
+
const startOfDay = new Date(dateToUpdate);
|
|
1667
|
+
startOfDay.setUTCHours(0, 0, 0, 0);
|
|
1668
|
+
const endOfDay = new Date(dateToUpdate);
|
|
1669
|
+
endOfDay.setUTCHours(23, 59, 59, 999);
|
|
1670
|
+
logger9.info(
|
|
1671
|
+
`Updating parent checklist statuses for date: ${dateToUpdate.toISOString().split("T")[0]}`
|
|
1672
|
+
);
|
|
1673
|
+
const statusUpdates = await collection.aggregate([
|
|
1674
|
+
{
|
|
1675
|
+
$match: {
|
|
1676
|
+
createdAt: {
|
|
1677
|
+
$gte: startOfDay,
|
|
1678
|
+
$lte: endOfDay
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
},
|
|
1682
|
+
{
|
|
1683
|
+
$lookup: {
|
|
1684
|
+
from: "hygiene-checklist.areas",
|
|
1685
|
+
localField: "_id",
|
|
1686
|
+
foreignField: "parentChecklist",
|
|
1687
|
+
pipeline: [
|
|
1688
|
+
{
|
|
1689
|
+
$group: {
|
|
1690
|
+
_id: {
|
|
1691
|
+
site: "$site",
|
|
1692
|
+
type: "$type"
|
|
1693
|
+
},
|
|
1694
|
+
completedCount: {
|
|
1695
|
+
$sum: {
|
|
1696
|
+
$cond: [{ $eq: ["$status", "Completed"] }, 1, 0]
|
|
1697
|
+
}
|
|
1698
|
+
},
|
|
1699
|
+
inProgressCount: {
|
|
1700
|
+
$sum: {
|
|
1701
|
+
$cond: [{ $eq: ["$status", "In Progress"] }, 1, 0]
|
|
1702
|
+
}
|
|
1703
|
+
},
|
|
1704
|
+
toDoCount: {
|
|
1705
|
+
$sum: {
|
|
1706
|
+
$cond: [
|
|
1707
|
+
{
|
|
1708
|
+
$or: [
|
|
1709
|
+
{ $eq: ["$status", "To Do"] },
|
|
1710
|
+
{ $eq: ["$status", "Pending"] }
|
|
1711
|
+
]
|
|
1712
|
+
},
|
|
1713
|
+
1,
|
|
1714
|
+
0
|
|
1715
|
+
]
|
|
1716
|
+
}
|
|
1717
|
+
},
|
|
1718
|
+
totalCount: { $sum: 1 }
|
|
1719
|
+
}
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
$addFields: {
|
|
1723
|
+
finalStatus: {
|
|
1724
|
+
$cond: {
|
|
1725
|
+
if: {
|
|
1726
|
+
$and: [
|
|
1727
|
+
{ $gt: ["$completedCount", 0] },
|
|
1728
|
+
{ $eq: ["$inProgressCount", 0] },
|
|
1729
|
+
{ $eq: ["$toDoCount", 0] }
|
|
1730
|
+
]
|
|
1731
|
+
},
|
|
1732
|
+
then: "Completed",
|
|
1733
|
+
else: {
|
|
1734
|
+
$cond: {
|
|
1735
|
+
if: {
|
|
1736
|
+
$and: [
|
|
1737
|
+
{ $eq: ["$completedCount", 0] },
|
|
1738
|
+
{ $eq: ["$inProgressCount", 0] }
|
|
1739
|
+
]
|
|
1740
|
+
},
|
|
1741
|
+
then: "Expired",
|
|
1742
|
+
else: "In Progress"
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
},
|
|
1747
|
+
completedAt: {
|
|
1748
|
+
$cond: {
|
|
1749
|
+
if: {
|
|
1750
|
+
$and: [
|
|
1751
|
+
{ $gt: ["$completedCount", 0] },
|
|
1752
|
+
{ $eq: ["$inProgressCount", 0] },
|
|
1753
|
+
{ $eq: ["$toDoCount", 0] }
|
|
1754
|
+
]
|
|
1755
|
+
},
|
|
1756
|
+
then: /* @__PURE__ */ new Date(),
|
|
1757
|
+
else: null
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
],
|
|
1763
|
+
as: "areaStats"
|
|
1764
|
+
}
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
$addFields: {
|
|
1768
|
+
newStatus: {
|
|
1769
|
+
$map: {
|
|
1770
|
+
input: "$areaStats",
|
|
1771
|
+
as: "stat",
|
|
1772
|
+
in: {
|
|
1773
|
+
site: "$$stat._id.site",
|
|
1774
|
+
type: "$$stat._id.type",
|
|
1775
|
+
status: "$$stat.finalStatus",
|
|
1776
|
+
completedAt: "$$stat.completedAt"
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
},
|
|
1782
|
+
{ $match: { newStatus: { $ne: [] } } },
|
|
1783
|
+
{
|
|
1784
|
+
$project: {
|
|
1785
|
+
_id: 1,
|
|
1786
|
+
newStatus: 1
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
]).toArray();
|
|
1790
|
+
logger9.info(
|
|
1791
|
+
`Found ${statusUpdates.length} parent checklists to potentially update`
|
|
1792
|
+
);
|
|
1793
|
+
if (statusUpdates.length === 0) {
|
|
1794
|
+
logger9.info(
|
|
1795
|
+
`No parent checklists found for date range: ${startOfDay.toISOString()} to ${endOfDay.toISOString()}`
|
|
1796
|
+
);
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
const bulkOps = statusUpdates.map((update) => {
|
|
1800
|
+
const statusTypes = update.newStatus.map((s) => `${s.type}(${s.status})`).join(", ");
|
|
1801
|
+
logger9.info(
|
|
1802
|
+
`Updating parent checklist ${update._id} with ${update.newStatus.length} status entries: [${statusTypes}]`
|
|
1803
|
+
);
|
|
1804
|
+
return {
|
|
1805
|
+
updateOne: {
|
|
1806
|
+
filter: { _id: update._id },
|
|
1807
|
+
update: {
|
|
1808
|
+
$set: {
|
|
1809
|
+
status: update.newStatus,
|
|
1810
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
});
|
|
1816
|
+
let result = null;
|
|
1817
|
+
if (bulkOps.length > 0) {
|
|
1818
|
+
result = await collection.bulkWrite(bulkOps);
|
|
1819
|
+
delNamespace().then(() => {
|
|
1820
|
+
logger9.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
1821
|
+
}).catch((err) => {
|
|
1822
|
+
logger9.error(
|
|
1823
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
1824
|
+
err
|
|
1825
|
+
);
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
logger9.info(`Updated statuses for ${bulkOps.length} parent checklists.`);
|
|
1829
|
+
return result;
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
logger9.error("Failed to update parent checklist statuses", error);
|
|
1832
|
+
throw error;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return {
|
|
1836
|
+
createIndex,
|
|
1837
|
+
createParentChecklist,
|
|
1838
|
+
getAllParentChecklist,
|
|
1839
|
+
updateParentChecklistStatuses
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// src/controllers/hygiene-parent-checklist.controller.ts
|
|
1844
|
+
import { BadRequestError as BadRequestError11, logger as logger10 } from "@iservice365/node-server-utils";
|
|
1845
|
+
import Joi6 from "joi";
|
|
1846
|
+
function useParentChecklistController() {
|
|
1847
|
+
const {
|
|
1848
|
+
createParentChecklist: _createParentChecklist,
|
|
1849
|
+
getAllParentChecklist: _getAllParentChecklist
|
|
1850
|
+
} = useParentChecklistRepo();
|
|
1851
|
+
async function createParentChecklist(req, res, next) {
|
|
1852
|
+
const payload = req.body;
|
|
1853
|
+
const { error } = parentChecklistSchema.validate(payload);
|
|
1854
|
+
if (error) {
|
|
1855
|
+
logger10.log({ level: "error", message: error.message });
|
|
1856
|
+
next(new BadRequestError11(error.message));
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
try {
|
|
1860
|
+
const id = await _createParentChecklist(payload);
|
|
1861
|
+
res.status(201).json({ message: "Parent checklist created successfully.", id });
|
|
1862
|
+
return;
|
|
1863
|
+
} catch (error2) {
|
|
1864
|
+
logger10.log({ level: "error", message: error2.message });
|
|
1865
|
+
next(error2);
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
async function getAllParentChecklist(req, res, next) {
|
|
1870
|
+
const query = { ...req.query, ...req.params };
|
|
1871
|
+
const validation = Joi6.object({
|
|
1872
|
+
page: Joi6.number().min(1).optional().allow("", null),
|
|
1873
|
+
limit: Joi6.number().min(1).optional().allow("", null),
|
|
1874
|
+
search: Joi6.string().optional().allow("", null),
|
|
1875
|
+
site: Joi6.string().hex().required(),
|
|
1876
|
+
type: Joi6.string().required().valid(...allowedTypes),
|
|
1877
|
+
startDate: Joi6.alternatives().try(Joi6.date(), Joi6.string()).optional().allow("", null),
|
|
1878
|
+
endDate: Joi6.alternatives().try(Joi6.date(), Joi6.string()).optional().allow("", null)
|
|
1879
|
+
});
|
|
1880
|
+
const { error } = validation.validate(query);
|
|
1881
|
+
if (error) {
|
|
1882
|
+
logger10.log({ level: "error", message: error.message });
|
|
1883
|
+
next(new BadRequestError11(error.message));
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
const page = parseInt(req.query.page) ?? 1;
|
|
1887
|
+
const limit = parseInt(req.query.limit) ?? 20;
|
|
1888
|
+
const search = req.query.search ?? "";
|
|
1889
|
+
const site = req.params.site ?? "";
|
|
1890
|
+
const type = req.params.type ?? "";
|
|
1891
|
+
const startDate = req.query.startDate ?? "";
|
|
1892
|
+
const endDate = req.query.endDate ?? "";
|
|
1893
|
+
try {
|
|
1894
|
+
const data = await _getAllParentChecklist({
|
|
1895
|
+
page,
|
|
1896
|
+
limit,
|
|
1897
|
+
search,
|
|
1898
|
+
site,
|
|
1899
|
+
type,
|
|
1900
|
+
startDate,
|
|
1901
|
+
endDate
|
|
1902
|
+
});
|
|
1903
|
+
res.json(data);
|
|
1904
|
+
return;
|
|
1905
|
+
} catch (error2) {
|
|
1906
|
+
logger10.log({ level: "error", message: error2.message });
|
|
1907
|
+
next(error2);
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
return {
|
|
1912
|
+
createParentChecklist,
|
|
1913
|
+
getAllParentChecklist
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// src/models/hygiene-unit.model.ts
|
|
1918
|
+
import { BadRequestError as BadRequestError12, logger as logger11 } from "@iservice365/node-server-utils";
|
|
1919
|
+
import Joi7 from "joi";
|
|
1920
|
+
import { ObjectId as ObjectId7 } from "mongodb";
|
|
1921
|
+
var unitSchema = Joi7.object({
|
|
1922
|
+
name: Joi7.string().required(),
|
|
1923
|
+
site: Joi7.string().hex().required(),
|
|
1924
|
+
createdBy: Joi7.string().hex().required()
|
|
1925
|
+
});
|
|
1926
|
+
function MUnit(value) {
|
|
1927
|
+
const { error } = unitSchema.validate(value);
|
|
1928
|
+
if (error) {
|
|
1929
|
+
logger11.info(`Hygiene Unit Model: ${error.message}`);
|
|
1930
|
+
throw new BadRequestError12(error.message);
|
|
1931
|
+
}
|
|
1932
|
+
if (value.site) {
|
|
1933
|
+
try {
|
|
1934
|
+
value.site = new ObjectId7(value.site);
|
|
1935
|
+
} catch (error2) {
|
|
1936
|
+
throw new BadRequestError12("Invalid site ID format.");
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
if (value.createdBy) {
|
|
1940
|
+
try {
|
|
1941
|
+
value.createdBy = new ObjectId7(value.createdBy);
|
|
1942
|
+
} catch (error2) {
|
|
1943
|
+
throw new BadRequestError12("Invalid createdBy ID format.");
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
return {
|
|
1947
|
+
name: value.name,
|
|
1948
|
+
createdBy: value.createdBy,
|
|
1949
|
+
site: value.site,
|
|
1950
|
+
status: "active",
|
|
1951
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1952
|
+
updatedAt: value.updatedAt ?? "",
|
|
1953
|
+
deletedAt: value.deletedAt ?? ""
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
// src/services/hygiene-unit.service.ts
|
|
1958
|
+
import {
|
|
1959
|
+
BadRequestError as BadRequestError14,
|
|
1960
|
+
logger as logger13,
|
|
1961
|
+
NotFoundError as NotFoundError5,
|
|
1962
|
+
useAtlas as useAtlas7
|
|
1963
|
+
} from "@iservice365/node-server-utils";
|
|
1964
|
+
|
|
1965
|
+
// src/repositories/hygiene-unit.repository.ts
|
|
1966
|
+
import { ObjectId as ObjectId8 } from "mongodb";
|
|
1967
|
+
import {
|
|
1968
|
+
useAtlas as useAtlas6,
|
|
1969
|
+
InternalServerError as InternalServerError4,
|
|
1970
|
+
paginate as paginate4,
|
|
1971
|
+
BadRequestError as BadRequestError13,
|
|
1972
|
+
useCache as useCache4,
|
|
1973
|
+
logger as logger12,
|
|
1974
|
+
makeCacheKey as makeCacheKey4
|
|
1975
|
+
} from "@iservice365/node-server-utils";
|
|
1976
|
+
function useUnitRepository() {
|
|
1977
|
+
const db = useAtlas6.getDb();
|
|
1978
|
+
if (!db) {
|
|
1979
|
+
throw new InternalServerError4("Unable to connect to server.");
|
|
1980
|
+
}
|
|
1981
|
+
const namespace_collection = "hygiene-units";
|
|
1982
|
+
const collection = db.collection(namespace_collection);
|
|
1983
|
+
const { delNamespace, setCache, getCache } = useCache4(namespace_collection);
|
|
1984
|
+
async function createIndex() {
|
|
1985
|
+
try {
|
|
1986
|
+
await collection.createIndexes([
|
|
1987
|
+
{ key: { site: 1 } },
|
|
1988
|
+
{ key: { createdBy: 1 } },
|
|
1989
|
+
{ key: { status: 1 } }
|
|
1990
|
+
]);
|
|
1991
|
+
} catch (error) {
|
|
1992
|
+
throw new InternalServerError4("Failed to create index on hygiene unit.");
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
async function createTextIndex() {
|
|
1996
|
+
try {
|
|
1997
|
+
await collection.createIndex({ name: "text" });
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
throw new InternalServerError4(
|
|
2000
|
+
"Failed to create text index on hygiene unit."
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
async function createUniqueIndex() {
|
|
2005
|
+
try {
|
|
2006
|
+
await collection.createIndex(
|
|
2007
|
+
{ name: 1, site: 1, deletedAt: 1 },
|
|
2008
|
+
{ unique: true }
|
|
2009
|
+
);
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
throw new InternalServerError4(
|
|
2012
|
+
"Failed to create unique index on hygiene unit."
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
async function createUnit(value, session) {
|
|
2017
|
+
try {
|
|
2018
|
+
value = MUnit(value);
|
|
2019
|
+
const res = await collection.insertOne(value, { session });
|
|
2020
|
+
delNamespace().then(() => {
|
|
2021
|
+
logger12.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
2022
|
+
}).catch((err) => {
|
|
2023
|
+
logger12.error(
|
|
2024
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
2025
|
+
err
|
|
2026
|
+
);
|
|
2027
|
+
});
|
|
2028
|
+
return res.insertedId;
|
|
2029
|
+
} catch (error) {
|
|
2030
|
+
const isDuplicated = error.message.includes("duplicate");
|
|
2031
|
+
if (isDuplicated) {
|
|
2032
|
+
throw new BadRequestError13("Unit already exists.");
|
|
2033
|
+
}
|
|
2034
|
+
throw error;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
async function getUnits({
|
|
2038
|
+
page = 1,
|
|
2039
|
+
limit = 10,
|
|
2040
|
+
search = "",
|
|
2041
|
+
startDate = "",
|
|
2042
|
+
endDate = "",
|
|
2043
|
+
site = ""
|
|
2044
|
+
}) {
|
|
2045
|
+
page = page > 0 ? page - 1 : 0;
|
|
2046
|
+
try {
|
|
2047
|
+
site = new ObjectId8(site);
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
throw new BadRequestError13("Invalid site ID format.");
|
|
2050
|
+
}
|
|
2051
|
+
const query = {
|
|
2052
|
+
status: { $ne: "deleted" },
|
|
2053
|
+
site
|
|
2054
|
+
};
|
|
2055
|
+
const cacheOptions = {
|
|
2056
|
+
page,
|
|
2057
|
+
limit,
|
|
2058
|
+
site: site.toString()
|
|
2059
|
+
};
|
|
2060
|
+
if (search) {
|
|
2061
|
+
query.$or = [{ name: { $regex: search, $options: "i" } }];
|
|
2062
|
+
cacheOptions.search = search;
|
|
2063
|
+
}
|
|
2064
|
+
if (startDate && endDate) {
|
|
2065
|
+
query.createdAt = {
|
|
2066
|
+
$gte: new Date(startDate),
|
|
2067
|
+
$lte: new Date(endDate)
|
|
2068
|
+
};
|
|
2069
|
+
cacheOptions.startDate = new Date(startDate).toISOString().split("T")[0];
|
|
2070
|
+
cacheOptions.endDate = new Date(endDate).toISOString().split("T")[0];
|
|
2071
|
+
} else if (startDate) {
|
|
2072
|
+
query.createdAt = { $gte: new Date(startDate) };
|
|
2073
|
+
cacheOptions.startDate = new Date(startDate).toISOString().split("T")[0];
|
|
2074
|
+
} else if (endDate) {
|
|
2075
|
+
query.createdAt = { $lte: new Date(endDate) };
|
|
2076
|
+
cacheOptions.endDate = new Date(endDate).toISOString().split("T")[0];
|
|
2077
|
+
}
|
|
2078
|
+
const cacheKey = makeCacheKey4(namespace_collection, cacheOptions);
|
|
2079
|
+
const cachedData = await getCache(cacheKey);
|
|
2080
|
+
if (cachedData) {
|
|
2081
|
+
logger12.info(`Cache hit for key: ${cacheKey}`);
|
|
2082
|
+
return cachedData;
|
|
2083
|
+
}
|
|
2084
|
+
try {
|
|
2085
|
+
const items = await collection.aggregate([
|
|
2086
|
+
{ $match: query },
|
|
2087
|
+
{
|
|
2088
|
+
$lookup: {
|
|
2089
|
+
from: "sites",
|
|
2090
|
+
localField: "site",
|
|
2091
|
+
foreignField: "_id",
|
|
2092
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
2093
|
+
as: "site"
|
|
2094
|
+
}
|
|
2095
|
+
},
|
|
2096
|
+
{
|
|
2097
|
+
$unwind: {
|
|
2098
|
+
path: "$site",
|
|
2099
|
+
preserveNullAndEmptyArrays: true
|
|
2100
|
+
}
|
|
2101
|
+
},
|
|
2102
|
+
{
|
|
2103
|
+
$lookup: {
|
|
2104
|
+
from: "users",
|
|
2105
|
+
localField: "createdBy",
|
|
2106
|
+
foreignField: "_id",
|
|
2107
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
2108
|
+
as: "createdBy"
|
|
2109
|
+
}
|
|
2110
|
+
},
|
|
2111
|
+
{
|
|
2112
|
+
$unwind: {
|
|
2113
|
+
path: "$createdBy",
|
|
2114
|
+
preserveNullAndEmptyArrays: true
|
|
2115
|
+
}
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
$project: {
|
|
2119
|
+
name: 1,
|
|
2120
|
+
site: "$site._id",
|
|
2121
|
+
siteName: "$site.name",
|
|
2122
|
+
createdByName: "$createdBy.name",
|
|
2123
|
+
checklist: 1,
|
|
2124
|
+
status: 1,
|
|
2125
|
+
createdAt: 1
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
2128
|
+
{ $sort: { _id: -1 } },
|
|
2129
|
+
{ $skip: page * limit },
|
|
2130
|
+
{ $limit: limit }
|
|
2131
|
+
]).toArray();
|
|
2132
|
+
const length = await collection.countDocuments(query);
|
|
2133
|
+
const data = paginate4(items, page, limit, length);
|
|
2134
|
+
setCache(cacheKey, data, 15 * 60).then(() => {
|
|
2135
|
+
logger12.info(`Cache set for key: ${cacheKey}`);
|
|
2136
|
+
}).catch((err) => {
|
|
2137
|
+
logger12.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
2138
|
+
});
|
|
2139
|
+
return data;
|
|
2140
|
+
} catch (error) {
|
|
2141
|
+
throw error;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
async function getUnitByName(name, site) {
|
|
2145
|
+
try {
|
|
2146
|
+
if (site)
|
|
2147
|
+
site = new ObjectId8(site);
|
|
2148
|
+
} catch (error) {
|
|
2149
|
+
throw new BadRequestError13("Invalid site ID format.");
|
|
2150
|
+
}
|
|
2151
|
+
try {
|
|
2152
|
+
return await collection.findOne({
|
|
2153
|
+
name: { $regex: new RegExp(`^${name}$`, "i") },
|
|
2154
|
+
...site && { site }
|
|
2155
|
+
});
|
|
2156
|
+
} catch (error) {
|
|
2157
|
+
throw new BadRequestError13("Unable to fetch unit by name.");
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
async function getUnitById(id, site) {
|
|
2161
|
+
try {
|
|
2162
|
+
id = typeof id === "string" ? new ObjectId8(id) : id;
|
|
2163
|
+
} catch (error) {
|
|
2164
|
+
throw new BadRequestError13("Invalid unit ID format.");
|
|
2165
|
+
}
|
|
2166
|
+
try {
|
|
2167
|
+
if (site)
|
|
2168
|
+
site = new ObjectId8(site);
|
|
2169
|
+
} catch (error) {
|
|
2170
|
+
throw new BadRequestError13(
|
|
2171
|
+
"Unable to fetch unit by ID, Invalid site ID format."
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
try {
|
|
2175
|
+
return await collection.findOne({ _id: id, ...site && { site } });
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
throw new BadRequestError13("Unable to fetch unit by id.");
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
async function updateUnit(_id, value) {
|
|
2181
|
+
try {
|
|
2182
|
+
_id = new ObjectId8(_id);
|
|
2183
|
+
} catch (error) {
|
|
2184
|
+
throw new BadRequestError13("Invalid unit ID format.");
|
|
2185
|
+
}
|
|
2186
|
+
try {
|
|
2187
|
+
const updateValue = { ...value, updatedAt: /* @__PURE__ */ new Date() };
|
|
2188
|
+
const res = await collection.updateOne({ _id }, { $set: updateValue });
|
|
2189
|
+
if (res.modifiedCount === 0) {
|
|
2190
|
+
throw new InternalServerError4("Unable to update cleaning unit.");
|
|
2191
|
+
}
|
|
2192
|
+
delNamespace().then(() => {
|
|
2193
|
+
logger12.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
2194
|
+
}).catch((err) => {
|
|
2195
|
+
logger12.error(
|
|
2196
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
2197
|
+
err
|
|
2198
|
+
);
|
|
2199
|
+
});
|
|
2200
|
+
return res.modifiedCount;
|
|
2201
|
+
} catch (error) {
|
|
2202
|
+
const isDuplicated = error.message.includes("duplicate");
|
|
2203
|
+
if (isDuplicated) {
|
|
2204
|
+
throw new BadRequestError13("Area already exists.");
|
|
2205
|
+
}
|
|
2206
|
+
throw error;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
async function deleteUnit(_id, session) {
|
|
2210
|
+
try {
|
|
2211
|
+
_id = new ObjectId8(_id);
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
throw new BadRequestError13("Invalid unit ID format.");
|
|
2214
|
+
}
|
|
2215
|
+
try {
|
|
2216
|
+
const updateValue = {
|
|
2217
|
+
status: "deleted",
|
|
2218
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
2219
|
+
deletedAt: /* @__PURE__ */ new Date()
|
|
2220
|
+
};
|
|
2221
|
+
const res = await collection.updateOne(
|
|
2222
|
+
{ _id },
|
|
2223
|
+
{ $set: updateValue },
|
|
2224
|
+
{ session }
|
|
2225
|
+
);
|
|
2226
|
+
if (res.modifiedCount === 0) {
|
|
2227
|
+
throw new InternalServerError4("Unable to delete unit.");
|
|
2228
|
+
}
|
|
2229
|
+
delNamespace().then(() => {
|
|
2230
|
+
logger12.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
2231
|
+
}).catch((err) => {
|
|
2232
|
+
logger12.error(
|
|
2233
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
2234
|
+
err
|
|
2235
|
+
);
|
|
2236
|
+
});
|
|
2237
|
+
return res.modifiedCount;
|
|
2238
|
+
} catch (error) {
|
|
2239
|
+
throw error;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
return {
|
|
2243
|
+
createIndex,
|
|
2244
|
+
createTextIndex,
|
|
2245
|
+
createUniqueIndex,
|
|
2246
|
+
createUnit,
|
|
2247
|
+
getUnits,
|
|
2248
|
+
getUnitByName,
|
|
2249
|
+
getUnitById,
|
|
2250
|
+
updateUnit,
|
|
2251
|
+
deleteUnit
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// src/services/hygiene-unit.service.ts
|
|
2256
|
+
function useUnitService() {
|
|
2257
|
+
const { createUnit: _createUnit } = useUnitRepository();
|
|
2258
|
+
async function uploadByFile({
|
|
2259
|
+
dataJson,
|
|
2260
|
+
createdBy,
|
|
2261
|
+
site
|
|
2262
|
+
}) {
|
|
2263
|
+
let dataArray;
|
|
2264
|
+
try {
|
|
2265
|
+
dataArray = JSON.parse(dataJson);
|
|
2266
|
+
} catch (error) {
|
|
2267
|
+
throw new BadRequestError14("Invalid JSON format for data in excel");
|
|
2268
|
+
}
|
|
2269
|
+
if (!dataArray || dataArray.length === 0) {
|
|
2270
|
+
throw new NotFoundError5("No data found in the uploaded file");
|
|
2271
|
+
}
|
|
2272
|
+
const session = useAtlas7.getClient()?.startSession();
|
|
2273
|
+
const insertedUnitIds = [];
|
|
2274
|
+
try {
|
|
2275
|
+
session?.startTransaction();
|
|
2276
|
+
for (const row of dataArray) {
|
|
2277
|
+
if (!row?.UNIT_NAME) {
|
|
2278
|
+
logger13.warn("Skipping row with missing UNIT_NAME:", row);
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
try {
|
|
2282
|
+
const insertedId = await _createUnit(
|
|
2283
|
+
{
|
|
2284
|
+
name: String(row.UNIT_NAME).trim(),
|
|
2285
|
+
site,
|
|
2286
|
+
createdBy
|
|
2287
|
+
},
|
|
2288
|
+
session
|
|
2289
|
+
);
|
|
2290
|
+
insertedUnitIds.push(insertedId);
|
|
2291
|
+
} catch (error) {
|
|
2292
|
+
logger13.error(
|
|
2293
|
+
`Error creating unit "${row.UNIT_NAME}":`,
|
|
2294
|
+
error.message
|
|
2295
|
+
);
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
await session?.commitTransaction();
|
|
2300
|
+
logger13.info(`Successfully uploaded ${insertedUnitIds.length} units`);
|
|
2301
|
+
return {
|
|
2302
|
+
message: `Successfully uploaded ${insertedUnitIds.length} units`
|
|
2303
|
+
};
|
|
2304
|
+
} catch (error) {
|
|
2305
|
+
await session?.abortTransaction();
|
|
2306
|
+
logger13.error("Error while uploading unit information", error);
|
|
2307
|
+
throw error;
|
|
2308
|
+
} finally {
|
|
2309
|
+
session?.endSession();
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
return {
|
|
2313
|
+
uploadByFile
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// src/controllers/hygiene-unit.controller.ts
|
|
2318
|
+
import { BadRequestError as BadRequestError15, logger as logger14 } from "@iservice365/node-server-utils";
|
|
2319
|
+
import Joi8 from "joi";
|
|
2320
|
+
function useUnitController() {
|
|
2321
|
+
const {
|
|
2322
|
+
createUnit: _createUnit,
|
|
2323
|
+
getUnits: _getUnits,
|
|
2324
|
+
updateUnit: _updateUnit,
|
|
2325
|
+
deleteUnit: _deleteUnit
|
|
2326
|
+
} = useUnitRepository();
|
|
2327
|
+
const { uploadByFile: _uploadByFile } = useUnitService();
|
|
2328
|
+
async function createUnit(req, res, next) {
|
|
2329
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
2330
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
2331
|
+
{}
|
|
2332
|
+
) : {};
|
|
2333
|
+
const createdBy = cookies["user"] || "";
|
|
2334
|
+
const payload = { ...req.body, createdBy };
|
|
2335
|
+
const { error } = unitSchema.validate(payload);
|
|
2336
|
+
if (error) {
|
|
2337
|
+
logger14.log({ level: "error", message: error.message });
|
|
2338
|
+
next(new BadRequestError15(error.message));
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
try {
|
|
2342
|
+
const id = await _createUnit(payload);
|
|
2343
|
+
res.status(201).json({ message: "Unit created successfully.", id });
|
|
2344
|
+
return;
|
|
2345
|
+
} catch (error2) {
|
|
2346
|
+
logger14.log({ level: "error", message: error2.message });
|
|
2347
|
+
next(error2);
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
async function getAll(req, res, next) {
|
|
2352
|
+
const query = req.query;
|
|
2353
|
+
const validation = Joi8.object({
|
|
2354
|
+
page: Joi8.number().min(1).optional().allow("", null),
|
|
2355
|
+
limit: Joi8.number().min(1).optional().allow("", null),
|
|
2356
|
+
search: Joi8.string().optional().allow("", null),
|
|
2357
|
+
startDate: Joi8.alternatives().try(Joi8.date(), Joi8.string()).optional().allow("", null),
|
|
2358
|
+
endDate: Joi8.alternatives().try(Joi8.date(), Joi8.string()).optional().allow("", null),
|
|
2359
|
+
site: Joi8.string().hex().optional().allow("", null)
|
|
2360
|
+
});
|
|
2361
|
+
const { error } = validation.validate(query);
|
|
2362
|
+
if (error) {
|
|
2363
|
+
logger14.log({ level: "error", message: error.message });
|
|
2364
|
+
next(new BadRequestError15(error.message));
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
const page = parseInt(req.query.page) ?? 1;
|
|
2368
|
+
const limit = parseInt(req.query.limit) ?? 20;
|
|
2369
|
+
const search = req.query.search ?? "";
|
|
2370
|
+
const site = req.query.site ?? "";
|
|
2371
|
+
const startDate = req.query.startDate ?? "";
|
|
2372
|
+
const endDate = req.query.endDate ?? "";
|
|
2373
|
+
try {
|
|
2374
|
+
const data = await _getUnits({
|
|
2375
|
+
page,
|
|
2376
|
+
limit,
|
|
2377
|
+
search,
|
|
2378
|
+
site,
|
|
2379
|
+
startDate,
|
|
2380
|
+
endDate
|
|
2381
|
+
});
|
|
2382
|
+
res.json(data);
|
|
2383
|
+
return;
|
|
2384
|
+
} catch (error2) {
|
|
2385
|
+
logger14.log({ level: "error", message: error2.message });
|
|
2386
|
+
next(error2);
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
async function updateUnit(req, res, next) {
|
|
2391
|
+
const payload = { id: req.params.id, ...req.body };
|
|
2392
|
+
const schema = Joi8.object({
|
|
2393
|
+
id: Joi8.string().hex().required(),
|
|
2394
|
+
name: Joi8.string().required()
|
|
2395
|
+
});
|
|
2396
|
+
const { error } = schema.validate(payload);
|
|
2397
|
+
if (error) {
|
|
2398
|
+
logger14.log({ level: "error", message: error.message });
|
|
2399
|
+
next(new BadRequestError15(error.message));
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
try {
|
|
2403
|
+
const { id, ...value } = payload;
|
|
2404
|
+
await _updateUnit(id, value);
|
|
2405
|
+
res.json({ message: "Unit updated successfully." });
|
|
2406
|
+
return;
|
|
2407
|
+
} catch (error2) {
|
|
2408
|
+
logger14.log({ level: "error", message: error2.message });
|
|
2409
|
+
next(error2);
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
async function deleteUnit(req, res, next) {
|
|
2414
|
+
const id = req.params.id;
|
|
2415
|
+
const validation = Joi8.object({
|
|
2416
|
+
id: Joi8.string().hex().required()
|
|
2417
|
+
});
|
|
2418
|
+
const { error } = validation.validate({ id });
|
|
2419
|
+
if (error) {
|
|
2420
|
+
logger14.log({ level: "error", message: error.message });
|
|
2421
|
+
next(new BadRequestError15(error.message));
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
try {
|
|
2425
|
+
await _deleteUnit(id);
|
|
2426
|
+
res.json({ message: "Unit deleted successfully." });
|
|
2427
|
+
return;
|
|
2428
|
+
} catch (error2) {
|
|
2429
|
+
logger14.log({ level: "error", message: error2.message });
|
|
2430
|
+
next(error2);
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
async function uploadByFile(req, res, next) {
|
|
2435
|
+
if (!req.file) {
|
|
2436
|
+
next(new BadRequestError15("File is required!"));
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
2440
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
2441
|
+
{}
|
|
2442
|
+
) : {};
|
|
2443
|
+
const createdBy = cookies["user"] || "";
|
|
2444
|
+
const { site } = req.body;
|
|
2445
|
+
const schema = Joi8.object({
|
|
2446
|
+
site: Joi8.string().hex().optional().allow("", null),
|
|
2447
|
+
createdBy: Joi8.string().hex().required()
|
|
2448
|
+
});
|
|
2449
|
+
const { error } = schema.validate({ site, createdBy });
|
|
2450
|
+
if (error) {
|
|
2451
|
+
logger14.log({ level: "error", message: error.message });
|
|
2452
|
+
next(new BadRequestError15(error.message));
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
try {
|
|
2456
|
+
const xlsData = await convertBufferFile(req.file.buffer);
|
|
2457
|
+
const dataJson = JSON.stringify(xlsData);
|
|
2458
|
+
const result = await _uploadByFile({ dataJson, createdBy, site });
|
|
2459
|
+
return res.status(201).json(result);
|
|
2460
|
+
} catch (error2) {
|
|
2461
|
+
logger14.log({ level: "error", message: error2.message });
|
|
2462
|
+
next(error2);
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return {
|
|
2467
|
+
createUnit,
|
|
2468
|
+
getAll,
|
|
2469
|
+
updateUnit,
|
|
2470
|
+
deleteUnit,
|
|
2471
|
+
uploadByFile
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// src/models/hygiene-schedule-task-area.model.ts
|
|
2476
|
+
import { BadRequestError as BadRequestError16 } from "@iservice365/node-server-utils";
|
|
2477
|
+
import Joi9 from "joi";
|
|
2478
|
+
import { ObjectId as ObjectId9 } from "mongodb";
|
|
2479
|
+
var scheduleTaskAreaSchema = Joi9.object({
|
|
2480
|
+
name: Joi9.string().required(),
|
|
2481
|
+
site: Joi9.string().hex().required(),
|
|
2482
|
+
createdBy: Joi9.string().hex().required(),
|
|
2483
|
+
checklist: Joi9.array().items(
|
|
2484
|
+
Joi9.object({
|
|
2485
|
+
_id: Joi9.string().hex().required(),
|
|
2486
|
+
name: Joi9.string().required()
|
|
2487
|
+
})
|
|
2488
|
+
).optional(),
|
|
2489
|
+
updatedAt: Joi9.date().optional().allow("", null),
|
|
2490
|
+
status: Joi9.string().allow("", null).optional()
|
|
2491
|
+
});
|
|
2492
|
+
function MScheduleTaskArea(value) {
|
|
2493
|
+
const { error } = scheduleTaskAreaSchema.validate(value);
|
|
2494
|
+
if (error) {
|
|
2495
|
+
throw new BadRequestError16(error.message);
|
|
2496
|
+
}
|
|
2497
|
+
if (value.site) {
|
|
2498
|
+
try {
|
|
2499
|
+
value.site = new ObjectId9(value.site);
|
|
2500
|
+
} catch (error2) {
|
|
2501
|
+
throw new BadRequestError16("Invalid site ID format.");
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
if (value.createdBy) {
|
|
2505
|
+
try {
|
|
2506
|
+
value.createdBy = new ObjectId9(value.createdBy);
|
|
2507
|
+
} catch (error2) {
|
|
2508
|
+
throw new BadRequestError16("Invalid createdBy ID format.");
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
if (value.checklist && Array.isArray(value.checklist)) {
|
|
2512
|
+
value.checklist = value.checklist.map((item) => {
|
|
2513
|
+
try {
|
|
2514
|
+
return {
|
|
2515
|
+
...item,
|
|
2516
|
+
_id: new ObjectId9(item._id)
|
|
2517
|
+
};
|
|
2518
|
+
} catch (error2) {
|
|
2519
|
+
throw new BadRequestError16(
|
|
2520
|
+
`Invalid checklist item ID format: ${item._id}`
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
return {
|
|
2526
|
+
name: value.name,
|
|
2527
|
+
site: value.site,
|
|
2528
|
+
createdBy: value.createdBy,
|
|
2529
|
+
checklist: value.checklist,
|
|
2530
|
+
status: value.status ?? "",
|
|
2531
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2532
|
+
updatedAt: value.updatedAt ?? "",
|
|
2533
|
+
deletedAt: value.deletedAt ?? ""
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// src/repositories/hygiene-schedule-task-area.repository.ts
|
|
2538
|
+
import { ObjectId as ObjectId10 } from "mongodb";
|
|
2539
|
+
import {
|
|
2540
|
+
useAtlas as useAtlas8,
|
|
2541
|
+
InternalServerError as InternalServerError5,
|
|
2542
|
+
paginate as paginate5,
|
|
2543
|
+
BadRequestError as BadRequestError17,
|
|
2544
|
+
NotFoundError as NotFoundError6,
|
|
2545
|
+
useCache as useCache5,
|
|
2546
|
+
logger as logger15,
|
|
2547
|
+
makeCacheKey as makeCacheKey5
|
|
2548
|
+
} from "@iservice365/node-server-utils";
|
|
2549
|
+
function useScheduleTaskAreaRepository() {
|
|
2550
|
+
const db = useAtlas8.getDb();
|
|
2551
|
+
if (!db) {
|
|
2552
|
+
throw new InternalServerError5("Unable to connect to server.");
|
|
2553
|
+
}
|
|
2554
|
+
const namespace_collection = "hygiene-schedule-task-areas";
|
|
2555
|
+
const collection = db.collection(namespace_collection);
|
|
2556
|
+
const { delNamespace, setCache, getCache, delCache } = useCache5(namespace_collection);
|
|
2557
|
+
async function createIndexes() {
|
|
2558
|
+
try {
|
|
2559
|
+
await collection.createIndexes([
|
|
2560
|
+
{ key: { site: 1 } },
|
|
2561
|
+
{ key: { name: "text" } }
|
|
2562
|
+
]);
|
|
2563
|
+
} catch (error) {
|
|
2564
|
+
throw new InternalServerError5("Failed to create index on site.");
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
async function createUniqueIndex() {
|
|
2568
|
+
try {
|
|
2569
|
+
await collection.createIndex(
|
|
2570
|
+
{ name: 1, site: 1, deletedAt: 1 },
|
|
2571
|
+
{ unique: true }
|
|
2572
|
+
);
|
|
2573
|
+
} catch (error) {
|
|
2574
|
+
throw new InternalServerError5(
|
|
2575
|
+
"Failed to create unique index on hygiene area."
|
|
2576
|
+
);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
async function createScheduleTaskArea(value, session) {
|
|
2580
|
+
try {
|
|
2581
|
+
value = MScheduleTaskArea(value);
|
|
2582
|
+
const res = await collection.insertOne(value, { session });
|
|
2583
|
+
delNamespace().then(() => {
|
|
2584
|
+
logger15.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
2585
|
+
}).catch((err) => {
|
|
2586
|
+
logger15.error(
|
|
2587
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
2588
|
+
err
|
|
2589
|
+
);
|
|
2590
|
+
});
|
|
2591
|
+
return res.insertedId;
|
|
2592
|
+
} catch (error) {
|
|
2593
|
+
const isDuplicated = error.message.includes("duplicate");
|
|
2594
|
+
if (isDuplicated) {
|
|
2595
|
+
throw new BadRequestError17("Area already exists.");
|
|
2596
|
+
}
|
|
2597
|
+
throw error;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
async function updateScheduleTaskArea(_id, params) {
|
|
2601
|
+
try {
|
|
2602
|
+
_id = new ObjectId10(_id);
|
|
2603
|
+
} catch (error) {
|
|
2604
|
+
throw new BadRequestError17("Invalid area ID format.");
|
|
2605
|
+
}
|
|
2606
|
+
try {
|
|
2607
|
+
const updateValue = { ...params, updatedAt: /* @__PURE__ */ new Date() };
|
|
2608
|
+
const res = await collection.updateOne({ _id }, { $set: updateValue });
|
|
2609
|
+
if (res.modifiedCount === 0) {
|
|
2610
|
+
throw new InternalServerError5("Unable to update cleaning area.");
|
|
2611
|
+
}
|
|
2612
|
+
delNamespace().then(() => {
|
|
2613
|
+
logger15.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
2614
|
+
}).catch((err) => {
|
|
2615
|
+
logger15.error(
|
|
2616
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
2617
|
+
err
|
|
2618
|
+
);
|
|
2619
|
+
});
|
|
2620
|
+
return res.modifiedCount;
|
|
2621
|
+
} catch (error) {
|
|
2622
|
+
const isDuplicated = error.message.includes("duplicate");
|
|
2623
|
+
if (isDuplicated) {
|
|
2624
|
+
throw new BadRequestError17("Area already exists.");
|
|
2625
|
+
}
|
|
2626
|
+
throw error;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
async function deleteScheduleTaskArea(_id, session) {
|
|
2630
|
+
try {
|
|
2631
|
+
_id = new ObjectId10(_id);
|
|
2632
|
+
} catch (error) {
|
|
2633
|
+
throw new BadRequestError17("Invalid area ID format.");
|
|
2634
|
+
}
|
|
2635
|
+
try {
|
|
2636
|
+
const updateValue = {
|
|
2637
|
+
status: "deleted",
|
|
2638
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
2639
|
+
deletedAt: /* @__PURE__ */ new Date()
|
|
2640
|
+
};
|
|
2641
|
+
const res = await collection.updateOne(
|
|
2642
|
+
{ _id },
|
|
2643
|
+
{ $set: updateValue },
|
|
2644
|
+
{ session }
|
|
2645
|
+
);
|
|
2646
|
+
if (res.modifiedCount === 0) {
|
|
2647
|
+
throw new InternalServerError5("Unable to delete area.");
|
|
2648
|
+
}
|
|
2649
|
+
delNamespace().then(() => {
|
|
2650
|
+
logger15.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
2651
|
+
}).catch((err) => {
|
|
2652
|
+
logger15.error(
|
|
2653
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
2654
|
+
err
|
|
2655
|
+
);
|
|
2656
|
+
});
|
|
2657
|
+
return res.modifiedCount;
|
|
2658
|
+
} catch (error) {
|
|
2659
|
+
throw error;
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
async function getScheduleTaskAreas({
|
|
2663
|
+
page = 1,
|
|
2664
|
+
limit = 10,
|
|
2665
|
+
search = "",
|
|
2666
|
+
startDate = "",
|
|
2667
|
+
endDate = "",
|
|
2668
|
+
site = ""
|
|
2669
|
+
}) {
|
|
2670
|
+
page = page > 0 ? page - 1 : 0;
|
|
2671
|
+
const query = {
|
|
2672
|
+
status: { $ne: "deleted" }
|
|
2673
|
+
};
|
|
2674
|
+
const cacheOptions = {
|
|
2675
|
+
page,
|
|
2676
|
+
limit
|
|
2677
|
+
};
|
|
2678
|
+
if (site) {
|
|
2679
|
+
try {
|
|
2680
|
+
site = new ObjectId10(site);
|
|
2681
|
+
cacheOptions.site = site.toString();
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
throw new BadRequestError17("Invalid site ID format.");
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
if (search) {
|
|
2687
|
+
query.$or = [{ name: { $regex: search, $options: "i" } }];
|
|
2688
|
+
cacheOptions.search = search;
|
|
2689
|
+
}
|
|
2690
|
+
if (startDate && endDate) {
|
|
2691
|
+
query.createdAt = {
|
|
2692
|
+
$gte: new Date(startDate),
|
|
2693
|
+
$lte: new Date(endDate)
|
|
2694
|
+
};
|
|
2695
|
+
cacheOptions.startDate = new Date(startDate).toISOString().split("T")[0];
|
|
2696
|
+
cacheOptions.endDate = new Date(endDate).toISOString().split("T")[0];
|
|
2697
|
+
} else if (startDate) {
|
|
2698
|
+
query.createdAt = { $gte: new Date(startDate) };
|
|
2699
|
+
cacheOptions.startDate = new Date(startDate).toISOString().split("T")[0];
|
|
2700
|
+
} else if (endDate) {
|
|
2701
|
+
query.createdAt = { $lte: new Date(endDate) };
|
|
2702
|
+
cacheOptions.endDate = new Date(endDate).toISOString().split("T")[0];
|
|
2703
|
+
}
|
|
2704
|
+
const cacheKey = makeCacheKey5(namespace_collection, cacheOptions);
|
|
2705
|
+
const cachedData = await getCache(cacheKey);
|
|
2706
|
+
if (cachedData) {
|
|
2707
|
+
logger15.info(`Cache hit for key: ${cacheKey}`);
|
|
2708
|
+
return cachedData;
|
|
2709
|
+
}
|
|
2710
|
+
try {
|
|
2711
|
+
const items = await collection.aggregate([
|
|
2712
|
+
{
|
|
2713
|
+
$match: query
|
|
2714
|
+
},
|
|
2715
|
+
{
|
|
2716
|
+
$lookup: {
|
|
2717
|
+
from: "sites",
|
|
2718
|
+
localField: "site",
|
|
2719
|
+
foreignField: "_id",
|
|
2720
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
2721
|
+
as: "site"
|
|
2722
|
+
}
|
|
2723
|
+
},
|
|
2724
|
+
{
|
|
2725
|
+
$unwind: {
|
|
2726
|
+
path: "$site",
|
|
2727
|
+
preserveNullAndEmptyArrays: true
|
|
2728
|
+
}
|
|
2729
|
+
},
|
|
2730
|
+
{
|
|
2731
|
+
$lookup: {
|
|
2732
|
+
from: "users",
|
|
2733
|
+
localField: "createdBy",
|
|
2734
|
+
foreignField: "_id",
|
|
2735
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
2736
|
+
as: "createdBy"
|
|
2737
|
+
}
|
|
2738
|
+
},
|
|
2739
|
+
{
|
|
2740
|
+
$unwind: {
|
|
2741
|
+
path: "$createdBy",
|
|
2742
|
+
preserveNullAndEmptyArrays: true
|
|
2743
|
+
}
|
|
2744
|
+
},
|
|
2745
|
+
{
|
|
2746
|
+
$project: {
|
|
2747
|
+
name: 1,
|
|
2748
|
+
site: "$site._id",
|
|
2749
|
+
siteName: "$site.name",
|
|
2750
|
+
createdByName: "$createdBy.name",
|
|
2751
|
+
checklist: 1,
|
|
2752
|
+
status: 1,
|
|
2753
|
+
createdAt: 1
|
|
2754
|
+
}
|
|
2755
|
+
},
|
|
2756
|
+
{ $sort: { _id: -1 } },
|
|
2757
|
+
{ $skip: page * limit },
|
|
2758
|
+
{ $limit: limit }
|
|
2759
|
+
]).toArray();
|
|
2760
|
+
const length = await collection.countDocuments(query);
|
|
2761
|
+
const data = paginate5(items, page, limit, length);
|
|
2762
|
+
setCache(cacheKey, data, 15 * 60).then(() => {
|
|
2763
|
+
logger15.info(`Cache set for key: ${cacheKey}`);
|
|
2764
|
+
}).catch((err) => {
|
|
2765
|
+
logger15.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
2766
|
+
});
|
|
2767
|
+
return data;
|
|
2768
|
+
} catch (error) {
|
|
2769
|
+
throw error;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
async function getScheduleTaskAreaByName(name, site) {
|
|
2773
|
+
try {
|
|
2774
|
+
if (site)
|
|
2775
|
+
site = new ObjectId10(site);
|
|
2776
|
+
} catch (error) {
|
|
2777
|
+
throw new BadRequestError17("Invalid site ID format.");
|
|
2778
|
+
}
|
|
2779
|
+
try {
|
|
2780
|
+
return await collection.findOne({
|
|
2781
|
+
name: { $regex: new RegExp(`^${name}$`, "i") },
|
|
2782
|
+
...site && { site }
|
|
2783
|
+
});
|
|
2784
|
+
} catch (error) {
|
|
2785
|
+
throw new BadRequestError17("Unable to fetch schedule task area by name.");
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
async function getScheduleTaskAreaById(id, site) {
|
|
2789
|
+
try {
|
|
2790
|
+
id = typeof id === "string" ? new ObjectId10(id) : id;
|
|
2791
|
+
} catch (error) {
|
|
2792
|
+
throw new BadRequestError17("Invalid unit ID format.");
|
|
2793
|
+
}
|
|
2794
|
+
try {
|
|
2795
|
+
if (site)
|
|
2796
|
+
site = new ObjectId10(site);
|
|
2797
|
+
} catch (error) {
|
|
2798
|
+
throw new BadRequestError17(
|
|
2799
|
+
"Unable to fetch schedule task area by ID, Invalid site ID format."
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
try {
|
|
2803
|
+
return await collection.findOne({ _id: id, ...site && { site } });
|
|
2804
|
+
} catch (error) {
|
|
2805
|
+
throw new BadRequestError17("Unable to fetch schedule task area by id.");
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
async function getAreaById(_id) {
|
|
2809
|
+
try {
|
|
2810
|
+
_id = new ObjectId10(_id);
|
|
2811
|
+
} catch (error) {
|
|
2812
|
+
throw new BadRequestError17("Invalid area ID format.");
|
|
2813
|
+
}
|
|
2814
|
+
const query = {
|
|
2815
|
+
_id,
|
|
2816
|
+
status: { $ne: "deleted" }
|
|
2817
|
+
};
|
|
2818
|
+
const cacheKey = makeCacheKey5(namespace_collection, {
|
|
2819
|
+
_id: _id.toString()
|
|
2820
|
+
});
|
|
2821
|
+
const cachedData = await getCache(cacheKey);
|
|
2822
|
+
if (cachedData) {
|
|
2823
|
+
logger15.info(`Cache hit for key: ${cacheKey}`);
|
|
2824
|
+
return cachedData;
|
|
2825
|
+
}
|
|
2826
|
+
try {
|
|
2827
|
+
const data = await collection.aggregate([
|
|
2828
|
+
{ $match: query },
|
|
2829
|
+
{
|
|
2830
|
+
$project: {
|
|
2831
|
+
name: 1,
|
|
2832
|
+
checklist: 1
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
]).toArray();
|
|
2836
|
+
if (!data || !data.length) {
|
|
2837
|
+
throw new NotFoundError6("Area not found.");
|
|
2838
|
+
}
|
|
2839
|
+
setCache(cacheKey, data[0], 15 * 60).then(() => {
|
|
2840
|
+
logger15.info(`Cache set for key: ${cacheKey}`);
|
|
2841
|
+
}).catch((err) => {
|
|
2842
|
+
logger15.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
2843
|
+
});
|
|
2844
|
+
return data[0];
|
|
2845
|
+
} catch (error) {
|
|
2846
|
+
throw error;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
return {
|
|
2850
|
+
createUniqueIndex,
|
|
2851
|
+
createScheduleTaskArea,
|
|
2852
|
+
updateScheduleTaskArea,
|
|
2853
|
+
deleteScheduleTaskArea,
|
|
2854
|
+
getScheduleTaskAreas,
|
|
2855
|
+
createIndexes,
|
|
2856
|
+
getScheduleTaskAreaByName,
|
|
2857
|
+
getScheduleTaskAreaById,
|
|
2858
|
+
getAreaById
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
// src/services/hygiene-schedule-task-area.service.ts
|
|
2863
|
+
import {
|
|
2864
|
+
BadRequestError as BadRequestError18,
|
|
2865
|
+
logger as logger16,
|
|
2866
|
+
NotFoundError as NotFoundError7,
|
|
2867
|
+
useAtlas as useAtlas9
|
|
2868
|
+
} from "@iservice365/node-server-utils";
|
|
2869
|
+
function useScheduleTaskAreaService() {
|
|
2870
|
+
const { createScheduleTaskArea: _create } = useScheduleTaskAreaRepository();
|
|
2871
|
+
async function uploadByFile({
|
|
2872
|
+
dataJson,
|
|
2873
|
+
createdBy,
|
|
2874
|
+
site
|
|
2875
|
+
}) {
|
|
2876
|
+
let dataArray;
|
|
2877
|
+
try {
|
|
2878
|
+
dataArray = JSON.parse(dataJson);
|
|
2879
|
+
} catch (error) {
|
|
2880
|
+
throw new BadRequestError18("Invalid JSON format for data in excel");
|
|
2881
|
+
}
|
|
2882
|
+
if (!dataArray || dataArray.length === 0) {
|
|
2883
|
+
throw new NotFoundError7("No data found in the uploaded file");
|
|
2884
|
+
}
|
|
2885
|
+
const session = useAtlas9.getClient()?.startSession();
|
|
2886
|
+
const insertedAreaIds = [];
|
|
2887
|
+
try {
|
|
2888
|
+
session?.startTransaction();
|
|
2889
|
+
for (const row of dataArray) {
|
|
2890
|
+
if (!row?.AREA_NAME) {
|
|
2891
|
+
logger16.warn("Skipping row with missing AREA_NAME:", row);
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
try {
|
|
2895
|
+
const insertedId = await _create(
|
|
2896
|
+
{
|
|
2897
|
+
name: String(row.AREA_NAME).trim(),
|
|
2898
|
+
site,
|
|
2899
|
+
createdBy
|
|
2900
|
+
},
|
|
2901
|
+
session
|
|
2902
|
+
);
|
|
2903
|
+
insertedAreaIds.push(insertedId);
|
|
2904
|
+
} catch (error) {
|
|
2905
|
+
logger16.error(
|
|
2906
|
+
`Error creating area "${row.AREA_NAME}":`,
|
|
2907
|
+
error.message
|
|
2908
|
+
);
|
|
2909
|
+
continue;
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
await session?.commitTransaction();
|
|
2913
|
+
logger16.info(`Successfully uploaded ${insertedAreaIds.length} areas`);
|
|
2914
|
+
return {
|
|
2915
|
+
message: `Successfully uploaded ${insertedAreaIds.length} areas`
|
|
2916
|
+
};
|
|
2917
|
+
} catch (error) {
|
|
2918
|
+
await session?.abortTransaction();
|
|
2919
|
+
logger16.error("Error while uploading area information", error);
|
|
2920
|
+
throw error;
|
|
2921
|
+
} finally {
|
|
2922
|
+
session?.endSession();
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
return {
|
|
2926
|
+
uploadByFile
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// src/controllers/hygiene-schedule-task-area.controller.ts
|
|
2931
|
+
import { BadRequestError as BadRequestError19, logger as logger17 } from "@iservice365/node-server-utils";
|
|
2932
|
+
import Joi10 from "joi";
|
|
2933
|
+
function useScheduleTaskAreaController() {
|
|
2934
|
+
const { uploadByFile: _uploadByFile } = useScheduleTaskAreaService();
|
|
2935
|
+
const {
|
|
2936
|
+
getScheduleTaskAreas: _getAll,
|
|
2937
|
+
createScheduleTaskArea: _createScheduleTaskArea,
|
|
2938
|
+
updateScheduleTaskArea: _updateScheduleTaskArea,
|
|
2939
|
+
deleteScheduleTaskArea: _deleteById,
|
|
2940
|
+
getAreaById: _getAreaById
|
|
2941
|
+
} = useScheduleTaskAreaRepository();
|
|
2942
|
+
async function getAll(req, res, next) {
|
|
2943
|
+
const query = req.query;
|
|
2944
|
+
const validation = Joi10.object({
|
|
2945
|
+
page: Joi10.number().min(1).optional().allow("", null),
|
|
2946
|
+
limit: Joi10.number().min(1).optional().allow("", null),
|
|
2947
|
+
search: Joi10.string().optional().allow("", null),
|
|
2948
|
+
startDate: Joi10.alternatives().try(Joi10.date(), Joi10.string()).optional().allow("", null),
|
|
2949
|
+
endDate: Joi10.alternatives().try(Joi10.date(), Joi10.string()).optional().allow("", null),
|
|
2950
|
+
site: Joi10.string().hex().optional().allow("", null)
|
|
2951
|
+
});
|
|
2952
|
+
const { error } = validation.validate(query);
|
|
2953
|
+
if (error) {
|
|
2954
|
+
next(new BadRequestError19(error.message));
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
const page = parseInt(req.query.page) ?? 1;
|
|
2958
|
+
const limit = parseInt(req.query.limit) ?? 20;
|
|
2959
|
+
const search = req.query.search ?? "";
|
|
2960
|
+
const site = req.query.site ?? "";
|
|
2961
|
+
const startDate = req.query.startDate ?? "";
|
|
2962
|
+
const endDate = req.query.endDate ?? "";
|
|
2963
|
+
try {
|
|
2964
|
+
const data = await _getAll({
|
|
2965
|
+
page,
|
|
2966
|
+
limit,
|
|
2967
|
+
search,
|
|
2968
|
+
site,
|
|
2969
|
+
startDate,
|
|
2970
|
+
endDate
|
|
2971
|
+
});
|
|
2972
|
+
res.json(data);
|
|
2973
|
+
return;
|
|
2974
|
+
} catch (error2) {
|
|
2975
|
+
next(error2);
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
async function createScheduleTaskArea(req, res, next) {
|
|
2980
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
2981
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
2982
|
+
{}
|
|
2983
|
+
) : {};
|
|
2984
|
+
const createdBy = cookies["user"] || "";
|
|
2985
|
+
const payload = { ...req.body, createdBy };
|
|
2986
|
+
const { error } = scheduleTaskAreaSchema.validate(payload);
|
|
2987
|
+
if (error) {
|
|
2988
|
+
logger17.log({ level: "error", message: error.message });
|
|
2989
|
+
next(new BadRequestError19(error.message));
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
try {
|
|
2993
|
+
const id = await _createScheduleTaskArea(payload);
|
|
2994
|
+
res.status(201).json({ message: "Area created successfully.", id });
|
|
2995
|
+
return;
|
|
2996
|
+
} catch (error2) {
|
|
2997
|
+
logger17.log({ level: "error", message: error2.message });
|
|
2998
|
+
next(error2);
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
async function updateScheduleTaskArea(req, res, next) {
|
|
3003
|
+
const payload = { id: req.params.id, ...req.body };
|
|
3004
|
+
const schema = Joi10.object({
|
|
3005
|
+
id: Joi10.string().hex().required(),
|
|
3006
|
+
name: Joi10.string().required()
|
|
3007
|
+
});
|
|
3008
|
+
const { error } = schema.validate(payload);
|
|
3009
|
+
if (error) {
|
|
3010
|
+
next(new BadRequestError19(error.message));
|
|
3011
|
+
logger17.info(`Controller: ${error.message}`);
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
try {
|
|
3015
|
+
const { id, ...value } = payload;
|
|
3016
|
+
await _updateScheduleTaskArea(id, value);
|
|
3017
|
+
res.json({ message: "Area updated successfully." });
|
|
3018
|
+
return;
|
|
3019
|
+
} catch (error2) {
|
|
3020
|
+
logger17.log({ level: "error", message: error2.message });
|
|
3021
|
+
next(error2);
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
async function deleteScheduleTaskArea(req, res, next) {
|
|
3026
|
+
const id = req.params.id;
|
|
3027
|
+
const validation = Joi10.object({
|
|
3028
|
+
id: Joi10.string().hex().required()
|
|
3029
|
+
});
|
|
3030
|
+
const { error } = validation.validate({ id });
|
|
3031
|
+
if (error) {
|
|
3032
|
+
logger17.log({ level: "error", message: error.message });
|
|
3033
|
+
next(new BadRequestError19(error.message));
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
try {
|
|
3037
|
+
await _deleteById(id);
|
|
3038
|
+
res.json({ message: "Area deleted successfully." });
|
|
3039
|
+
return;
|
|
3040
|
+
} catch (error2) {
|
|
3041
|
+
logger17.log({ level: "error", message: error2.message });
|
|
3042
|
+
next(error2);
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
async function uploadByFile(req, res, next) {
|
|
3047
|
+
if (!req.file) {
|
|
3048
|
+
next(new BadRequestError19("File is required!"));
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
3052
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
3053
|
+
{}
|
|
3054
|
+
) : {};
|
|
3055
|
+
const createdBy = cookies["user"] || "";
|
|
3056
|
+
const { site } = req.body;
|
|
3057
|
+
const schema = Joi10.object({
|
|
3058
|
+
site: Joi10.string().hex().optional().allow("", null),
|
|
3059
|
+
createdBy: Joi10.string().hex().required()
|
|
3060
|
+
});
|
|
3061
|
+
const { error } = schema.validate({ site, createdBy });
|
|
3062
|
+
if (error) {
|
|
3063
|
+
logger17.log({ level: "error", message: error.message });
|
|
3064
|
+
next(new BadRequestError19(error.message));
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
try {
|
|
3068
|
+
const xlsData = await convertBufferFile(req.file.buffer);
|
|
3069
|
+
const dataJson = JSON.stringify(xlsData);
|
|
3070
|
+
const result = await _uploadByFile({ dataJson, createdBy, site });
|
|
3071
|
+
return res.status(201).json(result);
|
|
3072
|
+
} catch (error2) {
|
|
3073
|
+
logger17.log({ level: "error", message: error2.message });
|
|
3074
|
+
next(error2);
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
async function getAreaById(req, res, next) {
|
|
3079
|
+
const validation = Joi10.string().hex().required();
|
|
3080
|
+
const _id = req.params.id;
|
|
3081
|
+
const { error } = validation.validate(_id);
|
|
3082
|
+
if (error) {
|
|
3083
|
+
logger17.log({ level: "error", message: error.message });
|
|
3084
|
+
next(new BadRequestError19(error.message));
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
try {
|
|
3088
|
+
const data = await _getAreaById(_id);
|
|
3089
|
+
res.json(data);
|
|
3090
|
+
return;
|
|
3091
|
+
} catch (error2) {
|
|
3092
|
+
logger17.log({ level: "error", message: error2.message });
|
|
3093
|
+
next(error2);
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
return {
|
|
3098
|
+
getAll,
|
|
3099
|
+
getAreaById,
|
|
3100
|
+
createScheduleTaskArea,
|
|
3101
|
+
updateScheduleTaskArea,
|
|
3102
|
+
deleteScheduleTaskArea,
|
|
3103
|
+
uploadByFile
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// src/models/hygiene-area-checklist.model.ts
|
|
3108
|
+
import { BadRequestError as BadRequestError20, logger as logger18 } from "@iservice365/node-server-utils";
|
|
3109
|
+
import Joi11 from "joi";
|
|
3110
|
+
import { ObjectId as ObjectId11 } from "mongodb";
|
|
3111
|
+
var areaChecklistSchema = Joi11.object({
|
|
3112
|
+
type: Joi11.string().required().valid(...allowedTypes),
|
|
3113
|
+
site: Joi11.string().hex().required(),
|
|
3114
|
+
parentChecklist: Joi11.string().hex().required(),
|
|
3115
|
+
area: Joi11.string().hex().required(),
|
|
3116
|
+
name: Joi11.string().optional().allow("", null),
|
|
3117
|
+
createdBy: Joi11.string().hex().optional().allow("", null)
|
|
3118
|
+
});
|
|
3119
|
+
function MAreaChecklist(value) {
|
|
3120
|
+
const { error } = areaChecklistSchema.validate(value);
|
|
3121
|
+
if (error) {
|
|
3122
|
+
logger18.info(`Hygiene Checklist Area Model: ${error.message}`);
|
|
3123
|
+
throw new BadRequestError20(error.message);
|
|
3124
|
+
}
|
|
3125
|
+
if (value.site) {
|
|
3126
|
+
try {
|
|
3127
|
+
value.site = new ObjectId11(value.site);
|
|
3128
|
+
} catch (error2) {
|
|
3129
|
+
throw new BadRequestError20("Invalid site ID format.");
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
if (value.parentChecklist) {
|
|
3133
|
+
try {
|
|
3134
|
+
value.parentChecklist = new ObjectId11(value.parentChecklist);
|
|
3135
|
+
} catch (error2) {
|
|
3136
|
+
throw new BadRequestError20("Invalid checklist ID format.");
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
if (value.area) {
|
|
3140
|
+
try {
|
|
3141
|
+
value.area = new ObjectId11(value.area);
|
|
3142
|
+
} catch (error2) {
|
|
3143
|
+
throw new BadRequestError20("Invalid area ID format.");
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
if (value.createdBy) {
|
|
3147
|
+
try {
|
|
3148
|
+
value.createdBy = new ObjectId11(value.createdBy);
|
|
3149
|
+
} catch (error2) {
|
|
3150
|
+
throw new BadRequestError20("Invalid createdBy ID format.");
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
if (value.acceptedBy) {
|
|
3154
|
+
try {
|
|
3155
|
+
value.acceptedBy = new ObjectId11(value.acceptedBy);
|
|
3156
|
+
} catch (error2) {
|
|
3157
|
+
throw new BadRequestError20("Invalid acceptedBy ID format.");
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
return {
|
|
3161
|
+
type: value.type,
|
|
3162
|
+
site: value.site,
|
|
3163
|
+
parentChecklist: value.parentChecklist,
|
|
3164
|
+
area: value.area,
|
|
3165
|
+
name: value.name ?? "",
|
|
3166
|
+
status: value.status ?? "To Do",
|
|
3167
|
+
createdBy: value.createdBy ?? "",
|
|
3168
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
3169
|
+
acceptedBy: value.acceptedBy ?? "",
|
|
3170
|
+
acceptedAt: value.acceptedAt ?? "",
|
|
3171
|
+
startedAt: value.startedAt ?? "",
|
|
3172
|
+
endedAt: value.endedAt ?? "",
|
|
3173
|
+
signature: value.signature ?? "",
|
|
3174
|
+
updatedAt: value.updatedAt ?? ""
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
// src/repositories/hygiene-area-checklist.repository.ts
|
|
3179
|
+
import {
|
|
3180
|
+
BadRequestError as BadRequestError21,
|
|
3181
|
+
InternalServerError as InternalServerError7,
|
|
3182
|
+
logger as logger19,
|
|
3183
|
+
makeCacheKey as makeCacheKey6,
|
|
3184
|
+
paginate as paginate6,
|
|
3185
|
+
useAtlas as useAtlas10,
|
|
3186
|
+
useCache as useCache6
|
|
3187
|
+
} from "@iservice365/node-server-utils";
|
|
3188
|
+
import { ObjectId as ObjectId12 } from "mongodb";
|
|
3189
|
+
function useAreaChecklistRepo() {
|
|
3190
|
+
const db = useAtlas10.getDb();
|
|
3191
|
+
if (!db) {
|
|
3192
|
+
throw new InternalServerError7("Unable to connect to server.");
|
|
3193
|
+
}
|
|
3194
|
+
const namespace_collection = "hygiene-checklist.areas";
|
|
3195
|
+
const unit_checklist_collection = "hygiene-checklist.units";
|
|
3196
|
+
const collection = db.collection(namespace_collection);
|
|
3197
|
+
const unitChecklistCollection = db.collection(unit_checklist_collection);
|
|
3198
|
+
const { delNamespace, setCache, getCache } = useCache6(namespace_collection);
|
|
3199
|
+
const { delNamespace: delUnitNamespace } = useCache6(
|
|
3200
|
+
unit_checklist_collection
|
|
3201
|
+
);
|
|
3202
|
+
async function createIndex() {
|
|
3203
|
+
try {
|
|
3204
|
+
await collection.createIndexes([
|
|
3205
|
+
{ key: { type: 1 } },
|
|
3206
|
+
{ key: { site: 1 } },
|
|
3207
|
+
{ key: { parentChecklist: 1 } },
|
|
3208
|
+
{ key: { area: 1 } },
|
|
3209
|
+
{ key: { createdAt: 1 } },
|
|
3210
|
+
{ key: { acceptedBy: 1 } }
|
|
3211
|
+
]);
|
|
3212
|
+
} catch (error) {
|
|
3213
|
+
throw new InternalServerError7(
|
|
3214
|
+
"Failed to create index on hygiene checklist area."
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
async function createTextIndex() {
|
|
3219
|
+
try {
|
|
3220
|
+
await collection.createIndex({ name: "text" });
|
|
3221
|
+
} catch (error) {
|
|
3222
|
+
throw new InternalServerError7(
|
|
3223
|
+
"Failed to create text index on hygiene checklist area."
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
async function createAreaChecklist(value, session) {
|
|
3228
|
+
try {
|
|
3229
|
+
const siteId = new ObjectId12(value.site);
|
|
3230
|
+
const parentChecklistId = new ObjectId12(value.parentChecklist);
|
|
3231
|
+
const areaId = new ObjectId12(value.area);
|
|
3232
|
+
const currentDate = /* @__PURE__ */ new Date();
|
|
3233
|
+
const startOfDay = new Date(currentDate);
|
|
3234
|
+
startOfDay.setUTCHours(0, 0, 0, 0);
|
|
3235
|
+
const endOfDay = new Date(currentDate);
|
|
3236
|
+
endOfDay.setUTCHours(23, 59, 59, 999);
|
|
3237
|
+
const existingChecklist = await collection.findOne({
|
|
3238
|
+
type: value.type,
|
|
3239
|
+
site: siteId,
|
|
3240
|
+
parentChecklist: parentChecklistId,
|
|
3241
|
+
area: areaId,
|
|
3242
|
+
createdAt: {
|
|
3243
|
+
$gte: startOfDay,
|
|
3244
|
+
$lte: endOfDay
|
|
3245
|
+
}
|
|
3246
|
+
});
|
|
3247
|
+
if (existingChecklist) {
|
|
3248
|
+
logger19.info(
|
|
3249
|
+
`Area checklist already exists for area ${areaId} on ${currentDate.toISOString().split("T")[0]}`
|
|
3250
|
+
);
|
|
3251
|
+
return existingChecklist._id;
|
|
3252
|
+
}
|
|
3253
|
+
const processedValue = MAreaChecklist(value);
|
|
3254
|
+
const result = await collection.insertOne(processedValue, { session });
|
|
3255
|
+
delNamespace().then(() => {
|
|
3256
|
+
logger19.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
3257
|
+
}).catch((err) => {
|
|
3258
|
+
logger19.error(
|
|
3259
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
3260
|
+
err
|
|
3261
|
+
);
|
|
3262
|
+
});
|
|
3263
|
+
return result.insertedId;
|
|
3264
|
+
} catch (error) {
|
|
3265
|
+
throw error;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
async function getAllAreaChecklist({
|
|
3269
|
+
page = 1,
|
|
3270
|
+
limit = 10,
|
|
3271
|
+
search = "",
|
|
3272
|
+
site,
|
|
3273
|
+
type,
|
|
3274
|
+
parentChecklist
|
|
3275
|
+
}) {
|
|
3276
|
+
page = page > 0 ? page - 1 : 0;
|
|
3277
|
+
const query = { type };
|
|
3278
|
+
const cacheOptions = {
|
|
3279
|
+
page,
|
|
3280
|
+
limit
|
|
3281
|
+
};
|
|
3282
|
+
try {
|
|
3283
|
+
query.site = new ObjectId12(site);
|
|
3284
|
+
cacheOptions.site = site.toString();
|
|
3285
|
+
} catch (error) {
|
|
3286
|
+
throw new BadRequestError21("Invalid site ID format.");
|
|
3287
|
+
}
|
|
3288
|
+
try {
|
|
3289
|
+
query.parentChecklist = new ObjectId12(parentChecklist);
|
|
3290
|
+
cacheOptions.parentChecklist = parentChecklist.toString();
|
|
3291
|
+
} catch (error) {
|
|
3292
|
+
throw new BadRequestError21("Invalid parent checklist ID format.");
|
|
3293
|
+
}
|
|
3294
|
+
if (search) {
|
|
3295
|
+
query.$text = { $search: search };
|
|
3296
|
+
cacheOptions.search = search;
|
|
3297
|
+
}
|
|
3298
|
+
const cacheKey = makeCacheKey6(namespace_collection, cacheOptions);
|
|
3299
|
+
const cachedData = await getCache(cacheKey);
|
|
3300
|
+
if (cachedData) {
|
|
3301
|
+
logger19.info(`Cache hit for key: ${cacheKey}`);
|
|
3302
|
+
return cachedData;
|
|
3303
|
+
}
|
|
3304
|
+
try {
|
|
3305
|
+
const pipeline = [
|
|
3306
|
+
{ $match: query },
|
|
3307
|
+
{
|
|
3308
|
+
$lookup: {
|
|
3309
|
+
from: "users",
|
|
3310
|
+
localField: "acceptedBy",
|
|
3311
|
+
foreignField: "_id",
|
|
3312
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
3313
|
+
as: "acceptedBy"
|
|
3314
|
+
}
|
|
3315
|
+
},
|
|
3316
|
+
{
|
|
3317
|
+
$unwind: {
|
|
3318
|
+
path: "$acceptedBy",
|
|
3319
|
+
preserveNullAndEmptyArrays: true
|
|
3320
|
+
}
|
|
3321
|
+
},
|
|
3322
|
+
{
|
|
3323
|
+
$project: {
|
|
3324
|
+
name: 1,
|
|
3325
|
+
acceptedByName: "$acceptedBy.name",
|
|
3326
|
+
status: 1,
|
|
3327
|
+
createdAt: 1
|
|
3328
|
+
}
|
|
3329
|
+
},
|
|
3330
|
+
{ $sort: { _id: -1 } },
|
|
3331
|
+
{ $skip: page * limit },
|
|
3332
|
+
{ $limit: limit }
|
|
3333
|
+
];
|
|
3334
|
+
const items = await collection.aggregate(pipeline).toArray();
|
|
3335
|
+
const length = await collection.countDocuments(query);
|
|
3336
|
+
const data = paginate6(items, page, limit, length);
|
|
3337
|
+
setCache(cacheKey, data, 15 * 60).then(() => {
|
|
3338
|
+
logger19.info(`Cache set for key: ${cacheKey}`);
|
|
3339
|
+
}).catch((err) => {
|
|
3340
|
+
logger19.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
3341
|
+
});
|
|
3342
|
+
return data;
|
|
3343
|
+
} catch (error) {
|
|
3344
|
+
throw error;
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
async function getAreaChecklistHistory({
|
|
3348
|
+
page = 1,
|
|
3349
|
+
limit = 10,
|
|
3350
|
+
search = "",
|
|
3351
|
+
site,
|
|
3352
|
+
type,
|
|
3353
|
+
parentChecklist,
|
|
3354
|
+
status,
|
|
3355
|
+
createdAt,
|
|
3356
|
+
user
|
|
3357
|
+
}) {
|
|
3358
|
+
page = page > 0 ? page - 1 : 0;
|
|
3359
|
+
const query = { type };
|
|
3360
|
+
const cacheOptions = {
|
|
3361
|
+
page,
|
|
3362
|
+
limit
|
|
3363
|
+
};
|
|
3364
|
+
try {
|
|
3365
|
+
query.site = new ObjectId12(site);
|
|
3366
|
+
cacheOptions.site = site.toString();
|
|
3367
|
+
} catch (error) {
|
|
3368
|
+
throw new BadRequestError21("Invalid site ID format.");
|
|
3369
|
+
}
|
|
3370
|
+
try {
|
|
3371
|
+
query.parentChecklist = new ObjectId12(parentChecklist);
|
|
3372
|
+
cacheOptions.parentChecklist = parentChecklist.toString();
|
|
3373
|
+
} catch (error) {
|
|
3374
|
+
throw new BadRequestError21("Invalid parent checklist ID format.");
|
|
3375
|
+
}
|
|
3376
|
+
if (search) {
|
|
3377
|
+
query.$text = { $search: search };
|
|
3378
|
+
cacheOptions.search = search;
|
|
3379
|
+
}
|
|
3380
|
+
if (createdAt) {
|
|
3381
|
+
query.createdAt = {
|
|
3382
|
+
$gte: /* @__PURE__ */ new Date(`${createdAt}T00:00:00Z`),
|
|
3383
|
+
$lte: /* @__PURE__ */ new Date(`${createdAt}T23:59:59Z`)
|
|
3384
|
+
};
|
|
3385
|
+
cacheOptions.createdAt = new Date(createdAt).toISOString().split("T")[0];
|
|
3386
|
+
}
|
|
3387
|
+
if (status) {
|
|
3388
|
+
query.status = status;
|
|
3389
|
+
cacheOptions.status = status;
|
|
3390
|
+
} else {
|
|
3391
|
+
query.status = { $in: ["In Progress", "Completed"] };
|
|
3392
|
+
}
|
|
3393
|
+
if (user) {
|
|
3394
|
+
try {
|
|
3395
|
+
query.acceptedBy = new ObjectId12(user);
|
|
3396
|
+
cacheOptions.user = user.toString();
|
|
3397
|
+
} catch (error) {
|
|
3398
|
+
throw new BadRequestError21("Invalid user ID format.");
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
const cacheKey = makeCacheKey6(namespace_collection, cacheOptions);
|
|
3402
|
+
const cachedData = await getCache(cacheKey);
|
|
3403
|
+
if (cachedData) {
|
|
3404
|
+
logger19.info(`Cache hit for key: ${cacheKey}`);
|
|
3405
|
+
return cachedData;
|
|
3406
|
+
}
|
|
3407
|
+
try {
|
|
3408
|
+
const pipeline = [
|
|
3409
|
+
{ $match: query },
|
|
3410
|
+
{
|
|
3411
|
+
$lookup: {
|
|
3412
|
+
from: "users",
|
|
3413
|
+
localField: "acceptedBy",
|
|
3414
|
+
foreignField: "_id",
|
|
3415
|
+
pipeline: [
|
|
3416
|
+
...search ? [{ $match: { name: { $regex: search, $options: "i" } } }] : [],
|
|
3417
|
+
{ $project: { name: 1 } }
|
|
3418
|
+
],
|
|
3419
|
+
as: "acceptedBy"
|
|
3420
|
+
}
|
|
3421
|
+
},
|
|
3422
|
+
{
|
|
3423
|
+
$unwind: {
|
|
3424
|
+
path: "$acceptedBy",
|
|
3425
|
+
preserveNullAndEmptyArrays: true
|
|
3426
|
+
}
|
|
3427
|
+
},
|
|
3428
|
+
{
|
|
3429
|
+
$project: {
|
|
3430
|
+
name: 1,
|
|
3431
|
+
createdAt: 1,
|
|
3432
|
+
acceptedByName: "$acceptedBy.name",
|
|
3433
|
+
status: 1,
|
|
3434
|
+
startedAt: 1,
|
|
3435
|
+
endedAt: 1
|
|
3436
|
+
}
|
|
3437
|
+
},
|
|
3438
|
+
{ $sort: { status: 1 } },
|
|
3439
|
+
{ $skip: page * limit },
|
|
3440
|
+
{ $limit: limit }
|
|
3441
|
+
];
|
|
3442
|
+
const items = await collection.aggregate(pipeline).toArray();
|
|
3443
|
+
const length = await collection.countDocuments(query);
|
|
3444
|
+
const data = paginate6(items, page, limit, length);
|
|
3445
|
+
setCache(cacheKey, data, 15 * 60).then(() => {
|
|
3446
|
+
logger19.info(`Cache set for key: ${cacheKey}`);
|
|
3447
|
+
}).catch((err) => {
|
|
3448
|
+
logger19.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
3449
|
+
});
|
|
3450
|
+
return data;
|
|
3451
|
+
} catch (error) {
|
|
3452
|
+
throw error;
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
async function getAreaChecklistHistoryDetails(_id) {
|
|
3456
|
+
try {
|
|
3457
|
+
_id = new ObjectId12(_id);
|
|
3458
|
+
} catch (error) {
|
|
3459
|
+
throw new BadRequestError21("Invalid area checklist ID format.");
|
|
3460
|
+
}
|
|
3461
|
+
const cacheKey = makeCacheKey6(namespace_collection, {
|
|
3462
|
+
_id: _id.toString()
|
|
3463
|
+
});
|
|
3464
|
+
const cachedData = await getCache(cacheKey);
|
|
3465
|
+
if (cachedData) {
|
|
3466
|
+
logger19.info(`Cache hit for key: ${cacheKey}`);
|
|
3467
|
+
return cachedData;
|
|
3468
|
+
}
|
|
3469
|
+
try {
|
|
3470
|
+
const areaPipeline = [
|
|
3471
|
+
{ $match: { _id } },
|
|
3472
|
+
{
|
|
3473
|
+
$lookup: {
|
|
3474
|
+
from: "users",
|
|
3475
|
+
localField: "createdBy",
|
|
3476
|
+
foreignField: "_id",
|
|
3477
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
3478
|
+
as: "createdBy"
|
|
3479
|
+
}
|
|
3480
|
+
},
|
|
3481
|
+
{
|
|
3482
|
+
$unwind: {
|
|
3483
|
+
path: "$createdBy",
|
|
3484
|
+
preserveNullAndEmptyArrays: true
|
|
3485
|
+
}
|
|
3486
|
+
},
|
|
3487
|
+
{
|
|
3488
|
+
$lookup: {
|
|
3489
|
+
from: "users",
|
|
3490
|
+
localField: "acceptedBy",
|
|
3491
|
+
foreignField: "_id",
|
|
3492
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
3493
|
+
as: "acceptedBy"
|
|
3494
|
+
}
|
|
3495
|
+
},
|
|
3496
|
+
{
|
|
3497
|
+
$unwind: {
|
|
3498
|
+
path: "$acceptedBy",
|
|
3499
|
+
preserveNullAndEmptyArrays: true
|
|
3500
|
+
}
|
|
3501
|
+
},
|
|
3502
|
+
{
|
|
3503
|
+
$project: {
|
|
3504
|
+
name: 1,
|
|
3505
|
+
createdAt: 1,
|
|
3506
|
+
createdByName: "$createdBy.name",
|
|
3507
|
+
startedAt: 1,
|
|
3508
|
+
endedAt: 1,
|
|
3509
|
+
status: 1,
|
|
3510
|
+
signature: 1,
|
|
3511
|
+
acceptedByName: "$acceptedBy.name"
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
];
|
|
3515
|
+
const unitPipeline = [
|
|
3516
|
+
{ $match: { areaChecklist: _id } },
|
|
3517
|
+
{
|
|
3518
|
+
$project: {
|
|
3519
|
+
name: 1,
|
|
3520
|
+
remarks: "$metadata.remarks",
|
|
3521
|
+
attachments: "$metadata.attachments",
|
|
3522
|
+
approve: { $ifNull: ["$approve", null] },
|
|
3523
|
+
reject: { $ifNull: ["$reject", null] }
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
];
|
|
3527
|
+
const [area, units] = await Promise.all([
|
|
3528
|
+
collection.aggregate(areaPipeline).toArray(),
|
|
3529
|
+
unitChecklistCollection.aggregate(unitPipeline).toArray()
|
|
3530
|
+
]);
|
|
3531
|
+
if (!area.length) {
|
|
3532
|
+
throw new BadRequestError21("Area checklist not found.");
|
|
3533
|
+
}
|
|
3534
|
+
const items = {
|
|
3535
|
+
area: area[0],
|
|
3536
|
+
units
|
|
3537
|
+
};
|
|
3538
|
+
setCache(cacheKey, items, 15 * 60).then(() => {
|
|
3539
|
+
logger19.info(`Cache set for key: ${cacheKey}`);
|
|
3540
|
+
}).catch((err) => {
|
|
3541
|
+
logger19.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
3542
|
+
});
|
|
3543
|
+
return items;
|
|
3544
|
+
} catch (error) {
|
|
3545
|
+
throw error;
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
async function acceptAreaChecklist(_id, acceptedBy) {
|
|
3549
|
+
try {
|
|
3550
|
+
_id = new ObjectId12(_id);
|
|
3551
|
+
} catch (error) {
|
|
3552
|
+
throw new BadRequestError21("Invalid area ID format.");
|
|
3553
|
+
}
|
|
3554
|
+
try {
|
|
3555
|
+
acceptedBy = new ObjectId12(acceptedBy);
|
|
3556
|
+
} catch (error) {
|
|
3557
|
+
throw new BadRequestError21("Invalid acceptedBy ID format.");
|
|
3558
|
+
}
|
|
3559
|
+
try {
|
|
3560
|
+
const now = /* @__PURE__ */ new Date();
|
|
3561
|
+
const updateValue = {
|
|
3562
|
+
acceptedBy,
|
|
3563
|
+
acceptedAt: now,
|
|
3564
|
+
startedAt: now,
|
|
3565
|
+
updatedAt: now
|
|
3566
|
+
};
|
|
3567
|
+
const res = await collection.updateOne({ _id }, { $set: updateValue });
|
|
3568
|
+
if (res.modifiedCount === 0) {
|
|
3569
|
+
throw new InternalServerError7("Unable to update cleaning area.");
|
|
3570
|
+
}
|
|
3571
|
+
delNamespace().then(() => {
|
|
3572
|
+
logger19.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
3573
|
+
}).catch((err) => {
|
|
3574
|
+
logger19.error(
|
|
3575
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
3576
|
+
err
|
|
3577
|
+
);
|
|
3578
|
+
});
|
|
3579
|
+
return res.modifiedCount;
|
|
3580
|
+
} catch (error) {
|
|
3581
|
+
throw error;
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
async function attachImageAreaChecklist(_id, attachments, session) {
|
|
3585
|
+
try {
|
|
3586
|
+
_id = new ObjectId12(_id);
|
|
3587
|
+
} catch (error) {
|
|
3588
|
+
throw new BadRequestError21("Invalid area checklist ID format.");
|
|
3589
|
+
}
|
|
3590
|
+
try {
|
|
3591
|
+
const now = /* @__PURE__ */ new Date();
|
|
3592
|
+
const updateValue = {
|
|
3593
|
+
metadata: { attachments },
|
|
3594
|
+
updatedAt: now
|
|
3595
|
+
};
|
|
3596
|
+
const res = await collection.updateOne(
|
|
3597
|
+
{ _id },
|
|
3598
|
+
{ $set: updateValue },
|
|
3599
|
+
{ session }
|
|
3600
|
+
);
|
|
3601
|
+
if (res.modifiedCount === 0) {
|
|
3602
|
+
throw new InternalServerError7(
|
|
3603
|
+
"Unable to update cleaning area checklist."
|
|
3604
|
+
);
|
|
3605
|
+
}
|
|
3606
|
+
delNamespace().then(() => {
|
|
3607
|
+
logger19.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
3608
|
+
}).catch((err) => {
|
|
3609
|
+
logger19.error(
|
|
3610
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
3611
|
+
err
|
|
3612
|
+
);
|
|
3613
|
+
});
|
|
3614
|
+
delUnitNamespace().then(() => {
|
|
3615
|
+
logger19.info(
|
|
3616
|
+
`Cache cleared for namespace: ${unit_checklist_collection}`
|
|
3617
|
+
);
|
|
3618
|
+
}).catch((err) => {
|
|
3619
|
+
logger19.error(
|
|
3620
|
+
`Failed to clear cache for namespace: ${unit_checklist_collection}`,
|
|
3621
|
+
err
|
|
3622
|
+
);
|
|
3623
|
+
});
|
|
3624
|
+
return res.modifiedCount;
|
|
3625
|
+
} catch (error) {
|
|
3626
|
+
throw error;
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
async function submitAreaChecklist(_id, signature) {
|
|
3630
|
+
try {
|
|
3631
|
+
_id = new ObjectId12(_id);
|
|
3632
|
+
} catch (error) {
|
|
3633
|
+
throw new BadRequestError21("Invalid area ID format.");
|
|
3634
|
+
}
|
|
3635
|
+
try {
|
|
3636
|
+
const now = /* @__PURE__ */ new Date();
|
|
3637
|
+
const updateValue = {
|
|
3638
|
+
signature,
|
|
3639
|
+
status: "In Progress",
|
|
3640
|
+
endedAt: now,
|
|
3641
|
+
updatedAt: now
|
|
3642
|
+
};
|
|
3643
|
+
const res = await collection.updateOne({ _id }, { $set: updateValue });
|
|
3644
|
+
if (res.modifiedCount === 0) {
|
|
3645
|
+
throw new InternalServerError7("Unable to update cleaning area.");
|
|
3646
|
+
}
|
|
3647
|
+
delNamespace().then(() => {
|
|
3648
|
+
logger19.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
3649
|
+
}).catch((err) => {
|
|
3650
|
+
logger19.error(
|
|
3651
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
3652
|
+
err
|
|
3653
|
+
);
|
|
3654
|
+
});
|
|
3655
|
+
return res.modifiedCount;
|
|
3656
|
+
} catch (error) {
|
|
3657
|
+
throw error;
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
async function completeAreaChecklist(_id, signature) {
|
|
3661
|
+
try {
|
|
3662
|
+
_id = new ObjectId12(_id);
|
|
3663
|
+
} catch (error) {
|
|
3664
|
+
throw new BadRequestError21("Invalid area ID format.");
|
|
3665
|
+
}
|
|
3666
|
+
try {
|
|
3667
|
+
const now = /* @__PURE__ */ new Date();
|
|
3668
|
+
const updateValue = {
|
|
3669
|
+
signature,
|
|
3670
|
+
status: "Completed",
|
|
3671
|
+
endedAt: now,
|
|
3672
|
+
updatedAt: now
|
|
3673
|
+
};
|
|
3674
|
+
const res = await collection.updateOne({ _id }, { $set: updateValue });
|
|
3675
|
+
if (res.modifiedCount === 0) {
|
|
3676
|
+
throw new InternalServerError7("Unable to update cleaning area.");
|
|
3677
|
+
}
|
|
3678
|
+
delNamespace().then(() => {
|
|
3679
|
+
logger19.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
3680
|
+
}).catch((err) => {
|
|
3681
|
+
logger19.error(
|
|
3682
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
3683
|
+
err
|
|
3684
|
+
);
|
|
3685
|
+
});
|
|
3686
|
+
return res.modifiedCount;
|
|
3687
|
+
} catch (error) {
|
|
3688
|
+
throw error;
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
return {
|
|
3692
|
+
createIndex,
|
|
3693
|
+
createTextIndex,
|
|
3694
|
+
createAreaChecklist,
|
|
3695
|
+
getAllAreaChecklist,
|
|
3696
|
+
getAreaChecklistHistory,
|
|
3697
|
+
getAreaChecklistHistoryDetails,
|
|
3698
|
+
acceptAreaChecklist,
|
|
3699
|
+
attachImageAreaChecklist,
|
|
3700
|
+
submitAreaChecklist,
|
|
3701
|
+
completeAreaChecklist
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
// src/controllers/hygiene-area-checklist.controller.ts
|
|
3706
|
+
import { BadRequestError as BadRequestError22, logger as logger21 } from "@iservice365/node-server-utils";
|
|
3707
|
+
import Joi12 from "joi";
|
|
3708
|
+
|
|
3709
|
+
// src/services/hygiene-area-checklist.service.ts
|
|
3710
|
+
import { logger as logger20 } from "@iservice365/node-server-utils";
|
|
3711
|
+
function useAreaChecklistService() {
|
|
3712
|
+
const { createAreaChecklist: _createAreaChecklist } = useAreaChecklistRepo();
|
|
3713
|
+
const { getAreas } = useAreaRepository();
|
|
3714
|
+
const { getToiletLocations } = useToiletLocationRepository();
|
|
3715
|
+
async function createAreaChecklist(value) {
|
|
3716
|
+
try {
|
|
3717
|
+
const results = [];
|
|
3718
|
+
if (value.type === "cleaner") {
|
|
3719
|
+
const cleanerAreasResult = await getAreas({
|
|
3720
|
+
site: value.site
|
|
3721
|
+
});
|
|
3722
|
+
const cleanerAreas = cleanerAreasResult.items || [];
|
|
3723
|
+
if (cleanerAreas.length === 0) {
|
|
3724
|
+
logger20.warn(`No cleaner areas found for site: ${value.site}`);
|
|
3725
|
+
return results;
|
|
3726
|
+
}
|
|
3727
|
+
for (const area of cleanerAreas) {
|
|
3728
|
+
const checklistData = {
|
|
3729
|
+
type: "cleaner",
|
|
3730
|
+
site: value.site,
|
|
3731
|
+
parentChecklist: value.parentChecklist,
|
|
3732
|
+
area: area._id.toString(),
|
|
3733
|
+
name: area.name,
|
|
3734
|
+
createdBy: value.createdBy
|
|
3735
|
+
};
|
|
3736
|
+
const insertedId = await _createAreaChecklist(checklistData);
|
|
3737
|
+
results.push(insertedId);
|
|
3738
|
+
}
|
|
3739
|
+
logger20.info(
|
|
3740
|
+
`Created ${results.length} cleaner area checklists for site: ${value.site}`
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3743
|
+
if (value.type === "toilet") {
|
|
3744
|
+
const toiletAreasResult = await getToiletLocations({
|
|
3745
|
+
site: value.site
|
|
3746
|
+
});
|
|
3747
|
+
const toiletAreas = toiletAreasResult.items || [];
|
|
3748
|
+
if (toiletAreas.length === 0) {
|
|
3749
|
+
logger20.warn(`No toilet locations found for site: ${value.site}`);
|
|
3750
|
+
return results;
|
|
3751
|
+
}
|
|
3752
|
+
for (const toiletLocation of toiletAreas) {
|
|
3753
|
+
const checklistData = {
|
|
3754
|
+
type: "toilet",
|
|
3755
|
+
site: value.site,
|
|
3756
|
+
parentChecklist: value.parentChecklist,
|
|
3757
|
+
area: toiletLocation._id,
|
|
3758
|
+
name: toiletLocation.name,
|
|
3759
|
+
createdBy: value.createdBy
|
|
3760
|
+
};
|
|
3761
|
+
const insertedId = await _createAreaChecklist(checklistData);
|
|
3762
|
+
results.push(insertedId);
|
|
3763
|
+
}
|
|
3764
|
+
logger20.info(
|
|
3765
|
+
`Created ${results.length} toilet area checklists for site: ${value.site}`
|
|
3766
|
+
);
|
|
3767
|
+
}
|
|
3768
|
+
return results;
|
|
3769
|
+
} catch (error) {
|
|
3770
|
+
logger20.error(`Error generating area checklists:`, error);
|
|
3771
|
+
throw error;
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
return { createAreaChecklist };
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
// src/controllers/hygiene-area-checklist.controller.ts
|
|
3778
|
+
function useAreaChecklistController() {
|
|
3779
|
+
const {
|
|
3780
|
+
getAllAreaChecklist: _getAllAreaChecklist,
|
|
3781
|
+
getAreaChecklistHistory: _getAreaChecklistHistory,
|
|
3782
|
+
getAreaChecklistHistoryDetails: _getAreaChecklistHistoryDetails,
|
|
3783
|
+
acceptAreaChecklist: _acceptAreaChecklist,
|
|
3784
|
+
attachImageAreaChecklist: _attachImageAreaChecklist,
|
|
3785
|
+
submitAreaChecklist: _submitAreaChecklist,
|
|
3786
|
+
completeAreaChecklist: _completeAreaChecklist
|
|
3787
|
+
} = useAreaChecklistRepo();
|
|
3788
|
+
const { createAreaChecklist: _createAreaChecklist } = useAreaChecklistService();
|
|
3789
|
+
async function createAreaChecklist(req, res, next) {
|
|
3790
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
3791
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
3792
|
+
{}
|
|
3793
|
+
) : {};
|
|
3794
|
+
const createdBy = cookies["user"] || "";
|
|
3795
|
+
const payload = { ...req.body, ...req.params, createdBy };
|
|
3796
|
+
const validation = Joi12.object({
|
|
3797
|
+
type: Joi12.string().required().valid(...allowedTypes),
|
|
3798
|
+
site: Joi12.string().hex().required(),
|
|
3799
|
+
parentChecklist: Joi12.string().hex().required(),
|
|
3800
|
+
createdBy: Joi12.string().hex().optional().allow("", null)
|
|
3801
|
+
});
|
|
3802
|
+
const { error } = validation.validate(payload);
|
|
3803
|
+
if (error) {
|
|
3804
|
+
logger21.log({ level: "error", message: error.message });
|
|
3805
|
+
next(new BadRequestError22(error.message));
|
|
3806
|
+
return;
|
|
3807
|
+
}
|
|
3808
|
+
try {
|
|
3809
|
+
await _createAreaChecklist(payload);
|
|
3810
|
+
res.status(201).json({ message: "Area checklists generated successfully." });
|
|
3811
|
+
return;
|
|
3812
|
+
} catch (error2) {
|
|
3813
|
+
logger21.log({ level: "error", message: error2.message });
|
|
3814
|
+
next(error2);
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
async function getAllAreaChecklist(req, res, next) {
|
|
3819
|
+
const query = { ...req.query, ...req.params };
|
|
3820
|
+
const validation = Joi12.object({
|
|
3821
|
+
page: Joi12.number().min(1).optional().allow("", null),
|
|
3822
|
+
limit: Joi12.number().min(1).optional().allow("", null),
|
|
3823
|
+
search: Joi12.string().optional().allow("", null),
|
|
3824
|
+
site: Joi12.string().hex().required(),
|
|
3825
|
+
type: Joi12.string().valid(...allowedTypes).required(),
|
|
3826
|
+
parentChecklist: Joi12.string().hex().required()
|
|
3827
|
+
});
|
|
3828
|
+
const { error } = validation.validate(query);
|
|
3829
|
+
if (error) {
|
|
3830
|
+
logger21.log({ level: "error", message: error.message });
|
|
3831
|
+
next(new BadRequestError22(error.message));
|
|
3832
|
+
return;
|
|
3833
|
+
}
|
|
3834
|
+
const page = parseInt(req.query.page) ?? 1;
|
|
3835
|
+
const limit = parseInt(req.query.limit) ?? 20;
|
|
3836
|
+
const search = req.query.search ?? "";
|
|
3837
|
+
const site = req.params.site ?? "";
|
|
3838
|
+
const type = req.params.type ?? "";
|
|
3839
|
+
const parentChecklist = req.params.parentChecklist ?? "";
|
|
3840
|
+
try {
|
|
3841
|
+
const data = await _getAllAreaChecklist({
|
|
3842
|
+
page,
|
|
3843
|
+
limit,
|
|
3844
|
+
search,
|
|
3845
|
+
site,
|
|
3846
|
+
type,
|
|
3847
|
+
parentChecklist
|
|
3848
|
+
});
|
|
3849
|
+
res.json(data);
|
|
3850
|
+
return;
|
|
3851
|
+
} catch (error2) {
|
|
3852
|
+
logger21.log({ level: "error", message: error2.message });
|
|
3853
|
+
next(error2);
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
async function getAreaChecklistHistory(req, res, next) {
|
|
3858
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
3859
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
3860
|
+
{}
|
|
3861
|
+
) : {};
|
|
3862
|
+
const user = cookies["user"] || "";
|
|
3863
|
+
const query = { ...req.query, ...req.params, user };
|
|
3864
|
+
const validation = Joi12.object({
|
|
3865
|
+
page: Joi12.number().min(1).optional().allow("", null),
|
|
3866
|
+
limit: Joi12.number().min(1).optional().allow("", null),
|
|
3867
|
+
search: Joi12.string().optional().allow("", null),
|
|
3868
|
+
site: Joi12.string().hex().required(),
|
|
3869
|
+
type: Joi12.string().valid(...allowedTypes).required(),
|
|
3870
|
+
parentChecklist: Joi12.string().hex().required(),
|
|
3871
|
+
status: Joi12.string().allow("", null, ...allowedStatus),
|
|
3872
|
+
createdAt: Joi12.alternatives().try(Joi12.date(), Joi12.string()).optional().allow("", null),
|
|
3873
|
+
user: Joi12.string().hex().optional().allow("", null)
|
|
3874
|
+
});
|
|
3875
|
+
const { error } = validation.validate(query);
|
|
3876
|
+
if (error) {
|
|
3877
|
+
logger21.log({ level: "error", message: error.message });
|
|
3878
|
+
next(new BadRequestError22(error.message));
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
const page = parseInt(req.query.page) ?? 1;
|
|
3882
|
+
const limit = parseInt(req.query.limit) ?? 20;
|
|
3883
|
+
const search = req.query.search ?? "";
|
|
3884
|
+
const site = req.params.site ?? "";
|
|
3885
|
+
const type = req.params.type ?? "";
|
|
3886
|
+
const parentChecklist = req.params.parentChecklist ?? "";
|
|
3887
|
+
const status = req.query.status ?? "";
|
|
3888
|
+
const createdAt = req.query.createdAt ?? "";
|
|
3889
|
+
try {
|
|
3890
|
+
const data = await _getAreaChecklistHistory({
|
|
3891
|
+
page,
|
|
3892
|
+
limit,
|
|
3893
|
+
search,
|
|
3894
|
+
site,
|
|
3895
|
+
type,
|
|
3896
|
+
parentChecklist,
|
|
3897
|
+
status,
|
|
3898
|
+
createdAt,
|
|
3899
|
+
user
|
|
3900
|
+
});
|
|
3901
|
+
res.json(data);
|
|
3902
|
+
return;
|
|
3903
|
+
} catch (error2) {
|
|
3904
|
+
logger21.log({ level: "error", message: error2.message });
|
|
3905
|
+
next(error2);
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
async function getAreaChecklistHistoryDetails(req, res, next) {
|
|
3910
|
+
const validation = Joi12.string().hex().required();
|
|
3911
|
+
const _id = req.params.id;
|
|
3912
|
+
const { error } = validation.validate(_id);
|
|
3913
|
+
if (error) {
|
|
3914
|
+
logger21.log({ level: "error", message: error.message });
|
|
3915
|
+
next(new BadRequestError22(error.message));
|
|
3916
|
+
return;
|
|
3917
|
+
}
|
|
3918
|
+
try {
|
|
3919
|
+
const data = await _getAreaChecklistHistoryDetails(_id);
|
|
3920
|
+
res.json(data);
|
|
3921
|
+
return;
|
|
3922
|
+
} catch (error2) {
|
|
3923
|
+
logger21.log({ level: "error", message: error2.message });
|
|
3924
|
+
next(error2);
|
|
3925
|
+
return;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
async function acceptAreaChecklist(req, res, next) {
|
|
3929
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
3930
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
3931
|
+
{}
|
|
3932
|
+
) : {};
|
|
3933
|
+
const acceptedBy = cookies["user"] || "";
|
|
3934
|
+
const payload = { id: req.params.id, acceptedBy };
|
|
3935
|
+
const validation = Joi12.object({
|
|
3936
|
+
id: Joi12.string().hex().required(),
|
|
3937
|
+
acceptedBy: Joi12.string().hex().required()
|
|
3938
|
+
});
|
|
3939
|
+
const { error } = validation.validate(payload);
|
|
3940
|
+
if (error) {
|
|
3941
|
+
logger21.log({ level: "error", message: error.message });
|
|
3942
|
+
next(new BadRequestError22(error.message));
|
|
3943
|
+
return;
|
|
3944
|
+
}
|
|
3945
|
+
try {
|
|
3946
|
+
await _acceptAreaChecklist(payload.id, payload.acceptedBy);
|
|
3947
|
+
res.json({ message: "Area checklist updated successfully." });
|
|
3948
|
+
return;
|
|
3949
|
+
} catch (error2) {
|
|
3950
|
+
logger21.log({ level: "error", message: error2.message });
|
|
3951
|
+
next(error2);
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
async function attachImageAreaChecklist(req, res, next) {
|
|
3956
|
+
const payload = { id: req.params.id, attachments: req.body.attachments };
|
|
3957
|
+
const validation = Joi12.object({
|
|
3958
|
+
id: Joi12.string().hex().required(),
|
|
3959
|
+
attachments: Joi12.array().items(Joi12.string()).optional()
|
|
3960
|
+
});
|
|
3961
|
+
const { error } = validation.validate(payload);
|
|
3962
|
+
if (error) {
|
|
3963
|
+
logger21.log({ level: "error", message: error.message });
|
|
3964
|
+
next(new BadRequestError22(error.message));
|
|
3965
|
+
return;
|
|
3966
|
+
}
|
|
3967
|
+
try {
|
|
3968
|
+
await _attachImageAreaChecklist(payload.id, payload.attachments);
|
|
3969
|
+
res.json({ message: "Area checklist attachments updated successfully." });
|
|
3970
|
+
return;
|
|
3971
|
+
} catch (error2) {
|
|
3972
|
+
logger21.log({ level: "error", message: error2.message });
|
|
3973
|
+
next(error2);
|
|
3974
|
+
return;
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
async function submitAreaChecklist(req, res, next) {
|
|
3978
|
+
const payload = { id: req.params.id, signature: req.body.signature };
|
|
3979
|
+
const validation = Joi12.object({
|
|
3980
|
+
id: Joi12.string().hex().required(),
|
|
3981
|
+
signature: Joi12.string().required()
|
|
3982
|
+
});
|
|
3983
|
+
const { error } = validation.validate(payload);
|
|
3984
|
+
if (error) {
|
|
3985
|
+
logger21.log({ level: "error", message: error.message });
|
|
3986
|
+
next(new BadRequestError22(error.message));
|
|
3987
|
+
return;
|
|
3988
|
+
}
|
|
3989
|
+
try {
|
|
3990
|
+
await _submitAreaChecklist(payload.id, payload.signature);
|
|
3991
|
+
res.json({ message: "Area checklist submitted successfully." });
|
|
3992
|
+
return;
|
|
3993
|
+
} catch (error2) {
|
|
3994
|
+
logger21.log({ level: "error", message: error2.message });
|
|
3995
|
+
next(error2);
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
async function completeAreaChecklist(req, res, next) {
|
|
4000
|
+
const payload = { id: req.params.id, signature: req.body.signature };
|
|
4001
|
+
const validation = Joi12.object({
|
|
4002
|
+
id: Joi12.string().hex().required(),
|
|
4003
|
+
signature: Joi12.string().required()
|
|
4004
|
+
});
|
|
4005
|
+
const { error } = validation.validate(payload);
|
|
4006
|
+
if (error) {
|
|
4007
|
+
logger21.log({ level: "error", message: error.message });
|
|
4008
|
+
next(new BadRequestError22(error.message));
|
|
4009
|
+
return;
|
|
4010
|
+
}
|
|
4011
|
+
try {
|
|
4012
|
+
await _completeAreaChecklist(payload.id, payload.signature);
|
|
4013
|
+
res.json({ message: "Area checklist completed successfully." });
|
|
4014
|
+
return;
|
|
4015
|
+
} catch (error2) {
|
|
4016
|
+
logger21.log({ level: "error", message: error2.message });
|
|
4017
|
+
next(error2);
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
return {
|
|
4022
|
+
createAreaChecklist,
|
|
4023
|
+
getAllAreaChecklist,
|
|
4024
|
+
getAreaChecklistHistory,
|
|
4025
|
+
getAreaChecklistHistoryDetails,
|
|
4026
|
+
acceptAreaChecklist,
|
|
4027
|
+
attachImageAreaChecklist,
|
|
4028
|
+
submitAreaChecklist,
|
|
4029
|
+
completeAreaChecklist
|
|
4030
|
+
};
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
// src/models/hygiene-unit-checklist.model.ts
|
|
4034
|
+
import { BadRequestError as BadRequestError23, logger as logger22 } from "@iservice365/node-server-utils";
|
|
4035
|
+
import Joi13 from "joi";
|
|
4036
|
+
import { ObjectId as ObjectId13 } from "mongodb";
|
|
4037
|
+
var unitChecklistSchema = Joi13.object({
|
|
4038
|
+
site: Joi13.string().hex().required(),
|
|
4039
|
+
type: Joi13.string().valid(...allowedTypes).required(),
|
|
4040
|
+
parentChecklist: Joi13.string().hex().required(),
|
|
4041
|
+
areaChecklist: Joi13.string().hex().required(),
|
|
4042
|
+
unit: Joi13.string().hex().required(),
|
|
4043
|
+
name: Joi13.string().optional().allow("", null),
|
|
4044
|
+
createdBy: Joi13.string().hex().optional().allow("", null)
|
|
4045
|
+
});
|
|
4046
|
+
function MUnitChecklist(value) {
|
|
4047
|
+
const { error } = unitChecklistSchema.validate(value);
|
|
4048
|
+
if (error) {
|
|
4049
|
+
logger22.info(`Hygiene Checklist Unit Model: ${error.message}`);
|
|
4050
|
+
throw new BadRequestError23(error.message);
|
|
4051
|
+
}
|
|
4052
|
+
if (value.site) {
|
|
4053
|
+
try {
|
|
4054
|
+
value.site = new ObjectId13(value.site);
|
|
4055
|
+
} catch (error2) {
|
|
4056
|
+
throw new BadRequestError23("Invalid site ID format.");
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
if (value.parentChecklist) {
|
|
4060
|
+
try {
|
|
4061
|
+
value.parentChecklist = new ObjectId13(value.parentChecklist);
|
|
4062
|
+
} catch (error2) {
|
|
4063
|
+
throw new BadRequestError23("Invalid parent checklist ID format.");
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
if (value.areaChecklist) {
|
|
4067
|
+
try {
|
|
4068
|
+
value.areaChecklist = new ObjectId13(value.areaChecklist);
|
|
4069
|
+
} catch (error2) {
|
|
4070
|
+
throw new BadRequestError23("Invalid area checklist ID format.");
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
if (value.unit) {
|
|
4074
|
+
try {
|
|
4075
|
+
value.unit = new ObjectId13(value.unit);
|
|
4076
|
+
} catch (error2) {
|
|
4077
|
+
throw new BadRequestError23("Invalid unit ID format.");
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
if (value.createdBy) {
|
|
4081
|
+
try {
|
|
4082
|
+
value.createdBy = new ObjectId13(value.createdBy);
|
|
4083
|
+
} catch (error2) {
|
|
4084
|
+
throw new BadRequestError23("Invalid createdBy ID format.");
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
return {
|
|
4088
|
+
site: value.site,
|
|
4089
|
+
type: value.type,
|
|
4090
|
+
parentChecklist: value.parentChecklist,
|
|
4091
|
+
areaChecklist: value.areaChecklist,
|
|
4092
|
+
unit: value.unit,
|
|
4093
|
+
name: value.name ?? "",
|
|
4094
|
+
approve: false,
|
|
4095
|
+
reject: false,
|
|
4096
|
+
createdBy: value.createdBy ?? "",
|
|
4097
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
4098
|
+
updatedAt: value.updatedAt ?? ""
|
|
4099
|
+
};
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
// src/repositories/hygiene-unit-checklist.repository.ts
|
|
4103
|
+
import {
|
|
4104
|
+
BadRequestError as BadRequestError24,
|
|
4105
|
+
InternalServerError as InternalServerError8,
|
|
4106
|
+
logger as logger23,
|
|
4107
|
+
makeCacheKey as makeCacheKey7,
|
|
4108
|
+
paginate as paginate7,
|
|
4109
|
+
useAtlas as useAtlas11,
|
|
4110
|
+
useCache as useCache7
|
|
4111
|
+
} from "@iservice365/node-server-utils";
|
|
4112
|
+
import { ObjectId as ObjectId14 } from "mongodb";
|
|
4113
|
+
function useUnitChecklistRepo() {
|
|
4114
|
+
const db = useAtlas11.getDb();
|
|
4115
|
+
if (!db) {
|
|
4116
|
+
throw new InternalServerError8("Unable to connect to server.");
|
|
4117
|
+
}
|
|
4118
|
+
const namespace_collection = "hygiene-checklist.units";
|
|
4119
|
+
const collection = db.collection(namespace_collection);
|
|
4120
|
+
const { delNamespace, setCache, getCache } = useCache7(namespace_collection);
|
|
4121
|
+
async function createIndex() {
|
|
4122
|
+
try {
|
|
4123
|
+
await collection.createIndexes([
|
|
4124
|
+
{ key: { site: 1 } },
|
|
4125
|
+
{ key: { type: 1 } },
|
|
4126
|
+
{ key: { parentChecklist: 1 } },
|
|
4127
|
+
{ key: { areaChecklist: 1 } },
|
|
4128
|
+
{ key: { "metadata.workOrder.category": 1 } },
|
|
4129
|
+
{ key: { "metadata.workOrder.createdBy": 1 } },
|
|
4130
|
+
{ key: { "metadata.workOrder.serviceProvider": 1 } },
|
|
4131
|
+
{ key: { "metadata.workOrder.organization": 1 } },
|
|
4132
|
+
{ key: { "metadata.workOrder.site": 1 } }
|
|
4133
|
+
]);
|
|
4134
|
+
} catch (error) {
|
|
4135
|
+
throw new InternalServerError8(
|
|
4136
|
+
"Failed to create index on hygiene unit checklist."
|
|
4137
|
+
);
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
async function createTextIndex() {
|
|
4141
|
+
try {
|
|
4142
|
+
await collection.createIndex({ name: "text" });
|
|
4143
|
+
} catch (error) {
|
|
4144
|
+
throw new InternalServerError8(
|
|
4145
|
+
"Failed to create text index on hygiene unit checklist."
|
|
4146
|
+
);
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
async function createUnitChecklist(value, session) {
|
|
4150
|
+
try {
|
|
4151
|
+
value = MUnitChecklist(value);
|
|
4152
|
+
const res = await collection.insertOne(value, { session });
|
|
4153
|
+
delNamespace().then(() => {
|
|
4154
|
+
logger23.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
4155
|
+
}).catch((err) => {
|
|
4156
|
+
logger23.error(
|
|
4157
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
4158
|
+
err
|
|
4159
|
+
);
|
|
4160
|
+
});
|
|
4161
|
+
return res.insertedId;
|
|
4162
|
+
} catch (error) {
|
|
4163
|
+
throw error;
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
async function getAllUnitChecklist({
|
|
4167
|
+
page = 1,
|
|
4168
|
+
limit = 10,
|
|
4169
|
+
search = "",
|
|
4170
|
+
site,
|
|
4171
|
+
type,
|
|
4172
|
+
parentChecklist,
|
|
4173
|
+
areaChecklist
|
|
4174
|
+
}) {
|
|
4175
|
+
page = page > 0 ? page - 1 : 0;
|
|
4176
|
+
const query = { type };
|
|
4177
|
+
const cacheOptions = {
|
|
4178
|
+
page,
|
|
4179
|
+
limit
|
|
4180
|
+
};
|
|
4181
|
+
try {
|
|
4182
|
+
query.site = new ObjectId14(site);
|
|
4183
|
+
cacheOptions.site = site.toString();
|
|
4184
|
+
} catch (error) {
|
|
4185
|
+
throw new BadRequestError24("Invalid site ID format.");
|
|
4186
|
+
}
|
|
4187
|
+
try {
|
|
4188
|
+
query.parentChecklist = new ObjectId14(parentChecklist);
|
|
4189
|
+
cacheOptions.parentChecklist = parentChecklist.toString();
|
|
4190
|
+
} catch (error) {
|
|
4191
|
+
throw new BadRequestError24("Invalid parent checklist ID format.");
|
|
4192
|
+
}
|
|
4193
|
+
try {
|
|
4194
|
+
query.areaChecklist = new ObjectId14(areaChecklist);
|
|
4195
|
+
cacheOptions.areaChecklist = areaChecklist.toString();
|
|
4196
|
+
} catch (error) {
|
|
4197
|
+
throw new BadRequestError24("Invalid area checklist ID format.");
|
|
4198
|
+
}
|
|
4199
|
+
if (search) {
|
|
4200
|
+
query.$text = { $search: search };
|
|
4201
|
+
cacheOptions.search = search;
|
|
4202
|
+
}
|
|
4203
|
+
const cacheKey = makeCacheKey7(namespace_collection, cacheOptions);
|
|
4204
|
+
const cachedData = await getCache(cacheKey);
|
|
4205
|
+
if (cachedData) {
|
|
4206
|
+
logger23.info(`Cache hit for key: ${cacheKey}`);
|
|
4207
|
+
return cachedData;
|
|
4208
|
+
}
|
|
4209
|
+
try {
|
|
4210
|
+
const areaAttachmentsResult = await collection.aggregate([
|
|
4211
|
+
{ $match: query },
|
|
4212
|
+
{
|
|
4213
|
+
$lookup: {
|
|
4214
|
+
from: "hygiene-checklist.areas",
|
|
4215
|
+
localField: "areaChecklist",
|
|
4216
|
+
foreignField: "_id",
|
|
4217
|
+
pipeline: [
|
|
4218
|
+
{ $project: { attachments: "$metadata.attachments" } }
|
|
4219
|
+
],
|
|
4220
|
+
as: "areaChecklistData"
|
|
4221
|
+
}
|
|
4222
|
+
},
|
|
4223
|
+
{
|
|
4224
|
+
$unwind: {
|
|
4225
|
+
path: "$areaChecklistData",
|
|
4226
|
+
preserveNullAndEmptyArrays: true
|
|
4227
|
+
}
|
|
4228
|
+
},
|
|
4229
|
+
{
|
|
4230
|
+
$group: {
|
|
4231
|
+
_id: null,
|
|
4232
|
+
attachments: { $first: "$areaChecklistData.attachments" }
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
]).toArray();
|
|
4236
|
+
const areaAttachments = areaAttachmentsResult.length > 0 ? areaAttachmentsResult[0].attachments || [] : [];
|
|
4237
|
+
const pipeline = [
|
|
4238
|
+
{ $match: query },
|
|
4239
|
+
{
|
|
4240
|
+
$lookup: {
|
|
4241
|
+
from: "organizations",
|
|
4242
|
+
localField: "metadata.workOrder.category",
|
|
4243
|
+
foreignField: "_id",
|
|
4244
|
+
pipeline: [{ $project: { nature: 1 } }],
|
|
4245
|
+
as: "categoryData"
|
|
4246
|
+
}
|
|
4247
|
+
},
|
|
4248
|
+
{
|
|
4249
|
+
$lookup: {
|
|
4250
|
+
from: "users",
|
|
4251
|
+
localField: "metadata.workOrder.createdBy",
|
|
4252
|
+
foreignField: "_id",
|
|
4253
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
4254
|
+
as: "createdByData"
|
|
4255
|
+
}
|
|
4256
|
+
},
|
|
4257
|
+
{
|
|
4258
|
+
$lookup: {
|
|
4259
|
+
from: "service-providers",
|
|
4260
|
+
localField: "metadata.workOrder.serviceProvider",
|
|
4261
|
+
foreignField: "_id",
|
|
4262
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
4263
|
+
as: "serviceProviderData"
|
|
4264
|
+
}
|
|
4265
|
+
},
|
|
4266
|
+
{
|
|
4267
|
+
$lookup: {
|
|
4268
|
+
from: "organizations",
|
|
4269
|
+
localField: "metadata.workOrder.organization",
|
|
4270
|
+
foreignField: "_id",
|
|
4271
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
4272
|
+
as: "organizationData"
|
|
4273
|
+
}
|
|
4274
|
+
},
|
|
4275
|
+
{
|
|
4276
|
+
$lookup: {
|
|
4277
|
+
from: "sites",
|
|
4278
|
+
localField: "metadata.workOrder.site",
|
|
4279
|
+
foreignField: "_id",
|
|
4280
|
+
pipeline: [{ $project: { name: 1 } }],
|
|
4281
|
+
as: "siteData"
|
|
4282
|
+
}
|
|
4283
|
+
},
|
|
4284
|
+
{
|
|
4285
|
+
$addFields: {
|
|
4286
|
+
"metadata.workOrder.categoryName": {
|
|
4287
|
+
$arrayElemAt: ["$categoryData.nature", 0]
|
|
4288
|
+
},
|
|
4289
|
+
"metadata.workOrder.createdByName": {
|
|
4290
|
+
$arrayElemAt: ["$createdByData.name", 0]
|
|
4291
|
+
},
|
|
4292
|
+
"metadata.workOrder.serviceProviderName": {
|
|
4293
|
+
$arrayElemAt: ["$serviceProviderData.name", 0]
|
|
4294
|
+
},
|
|
4295
|
+
"metadata.workOrder.organizationName": {
|
|
4296
|
+
$arrayElemAt: ["$organizationData.name", 0]
|
|
4297
|
+
},
|
|
4298
|
+
"metadata.workOrder.siteName": {
|
|
4299
|
+
$arrayElemAt: ["$siteData.name", 0]
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
},
|
|
4303
|
+
{
|
|
4304
|
+
$project: {
|
|
4305
|
+
name: 1,
|
|
4306
|
+
remarks: "$metadata.remarks",
|
|
4307
|
+
attachments: "$metadata.attachments",
|
|
4308
|
+
workOrder: "$metadata.workOrder",
|
|
4309
|
+
approve: { $ifNull: ["$approve", null] },
|
|
4310
|
+
reject: { $ifNull: ["$reject", null] }
|
|
4311
|
+
}
|
|
4312
|
+
},
|
|
4313
|
+
{ $sort: { _id: -1 } },
|
|
4314
|
+
{ $skip: page * limit },
|
|
4315
|
+
{ $limit: limit }
|
|
4316
|
+
];
|
|
4317
|
+
const items = await collection.aggregate(pipeline).toArray();
|
|
4318
|
+
const length = await collection.countDocuments(query);
|
|
4319
|
+
const paginatedData = paginate7(items, page, limit, length);
|
|
4320
|
+
const data = {
|
|
4321
|
+
attachments: areaAttachments,
|
|
4322
|
+
...paginatedData
|
|
4323
|
+
};
|
|
4324
|
+
setCache(cacheKey, data, 15 * 60).then(() => {
|
|
4325
|
+
logger23.info(`Cache set for key: ${cacheKey}`);
|
|
4326
|
+
}).catch((err) => {
|
|
4327
|
+
logger23.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
4328
|
+
});
|
|
4329
|
+
return data;
|
|
4330
|
+
} catch (error) {
|
|
4331
|
+
throw error;
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
async function getUnitChecklistById(_id) {
|
|
4335
|
+
try {
|
|
4336
|
+
_id = new ObjectId14(_id);
|
|
4337
|
+
} catch (error) {
|
|
4338
|
+
throw new BadRequestError24("Invalid unit checklist ID format.");
|
|
4339
|
+
}
|
|
4340
|
+
try {
|
|
4341
|
+
return await collection.findOne({ _id });
|
|
4342
|
+
} catch (error) {
|
|
4343
|
+
throw error;
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
async function updateUnitChecklist(_id, value, session) {
|
|
4347
|
+
try {
|
|
4348
|
+
_id = new ObjectId14(_id);
|
|
4349
|
+
} catch (error) {
|
|
4350
|
+
throw new BadRequestError24("Invalid unit checklist ID format.");
|
|
4351
|
+
}
|
|
4352
|
+
if (value.checkedBy && typeof value.checkedBy === "string") {
|
|
4353
|
+
try {
|
|
4354
|
+
value.checkedBy = new ObjectId14(value.checkedBy);
|
|
4355
|
+
} catch (error) {
|
|
4356
|
+
throw new BadRequestError24("Invalid checkedBy ID format.");
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
if ("metadata" in value && value.metadata?.workOrder) {
|
|
4360
|
+
const workOrder = value.metadata.workOrder;
|
|
4361
|
+
if (workOrder.category && typeof workOrder.category === "string") {
|
|
4362
|
+
try {
|
|
4363
|
+
workOrder.category = new ObjectId14(workOrder.category);
|
|
4364
|
+
} catch (error) {
|
|
4365
|
+
throw new BadRequestError24("Invalid category ID format.");
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
if (workOrder.createdBy && typeof workOrder.createdBy === "string") {
|
|
4369
|
+
try {
|
|
4370
|
+
workOrder.createdBy = new ObjectId14(workOrder.createdBy);
|
|
4371
|
+
} catch (error) {
|
|
4372
|
+
throw new BadRequestError24("Invalid createdBy ID format.");
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
if (workOrder.serviceProvider && typeof workOrder.serviceProvider === "string") {
|
|
4376
|
+
try {
|
|
4377
|
+
workOrder.serviceProvider = new ObjectId14(workOrder.serviceProvider);
|
|
4378
|
+
} catch (error) {
|
|
4379
|
+
throw new BadRequestError24("Invalid serviceProvider ID format.");
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
if (workOrder.organization && typeof workOrder.organization === "string") {
|
|
4383
|
+
try {
|
|
4384
|
+
workOrder.organization = new ObjectId14(workOrder.organization);
|
|
4385
|
+
} catch (error) {
|
|
4386
|
+
throw new BadRequestError24("Invalid organization ID format.");
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
if (workOrder.site && typeof workOrder.site === "string") {
|
|
4390
|
+
try {
|
|
4391
|
+
workOrder.site = new ObjectId14(workOrder.site);
|
|
4392
|
+
} catch (error) {
|
|
4393
|
+
throw new BadRequestError24("Invalid site ID format.");
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
try {
|
|
4398
|
+
const updateValue = { ...value, updatedAt: /* @__PURE__ */ new Date() };
|
|
4399
|
+
const res = await collection.updateOne(
|
|
4400
|
+
{ _id },
|
|
4401
|
+
{ $set: updateValue },
|
|
4402
|
+
{ session }
|
|
4403
|
+
);
|
|
4404
|
+
if (res.modifiedCount === 0) {
|
|
4405
|
+
throw new InternalServerError8("Unable to update unit checklist.");
|
|
4406
|
+
}
|
|
4407
|
+
delNamespace().then(() => {
|
|
4408
|
+
logger23.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
4409
|
+
}).catch((err) => {
|
|
4410
|
+
logger23.error(
|
|
4411
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
4412
|
+
err
|
|
4413
|
+
);
|
|
4414
|
+
});
|
|
4415
|
+
return res.modifiedCount;
|
|
4416
|
+
} catch (error) {
|
|
4417
|
+
throw error;
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
return {
|
|
4421
|
+
createIndex,
|
|
4422
|
+
createTextIndex,
|
|
4423
|
+
createUnitChecklist,
|
|
4424
|
+
getAllUnitChecklist,
|
|
4425
|
+
getUnitChecklistById,
|
|
4426
|
+
updateUnitChecklist
|
|
4427
|
+
};
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
// src/controllers/hygiene-unit-checklist.controller.ts
|
|
4431
|
+
import { BadRequestError as BadRequestError25, logger as logger25 } from "@iservice365/node-server-utils";
|
|
4432
|
+
import Joi14 from "joi";
|
|
4433
|
+
|
|
4434
|
+
// src/services/hygiene-unit-checklist.service.ts
|
|
4435
|
+
import { logger as logger24, NotFoundError as NotFoundError8 } from "@iservice365/node-server-utils";
|
|
4436
|
+
import {
|
|
4437
|
+
useSiteRepo,
|
|
4438
|
+
useWorkOrderService
|
|
4439
|
+
} from "@iservice365/core";
|
|
4440
|
+
function useUnitChecklistService() {
|
|
4441
|
+
const {
|
|
4442
|
+
getUnitChecklistById: _getUnitChecklistById,
|
|
4443
|
+
updateUnitChecklist: _updateUnitChecklist
|
|
4444
|
+
} = useUnitChecklistRepo();
|
|
4445
|
+
const { getSiteById } = useSiteRepo();
|
|
4446
|
+
const { createWorkOrder } = useWorkOrderService();
|
|
4447
|
+
async function approveUnitChecklist(id, value) {
|
|
4448
|
+
try {
|
|
4449
|
+
value.approve = true;
|
|
4450
|
+
value.reject = false;
|
|
4451
|
+
const result = await _updateUnitChecklist(id, value);
|
|
4452
|
+
return result;
|
|
4453
|
+
} catch (error) {
|
|
4454
|
+
logger24.error(`Error updating unit checklist with id ${id}:`, error);
|
|
4455
|
+
throw error;
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
async function rejectUnitChecklist(id, value, fullHost) {
|
|
4459
|
+
try {
|
|
4460
|
+
value.reject = true;
|
|
4461
|
+
value.approve = false;
|
|
4462
|
+
if (value.metadata?.workOrder) {
|
|
4463
|
+
const existingChecklist = await _getUnitChecklistById(id);
|
|
4464
|
+
if (!existingChecklist)
|
|
4465
|
+
throw new NotFoundError8("Unit checklist not found.");
|
|
4466
|
+
const site = await getSiteById(
|
|
4467
|
+
existingChecklist.site
|
|
4468
|
+
);
|
|
4469
|
+
if (!site)
|
|
4470
|
+
throw new NotFoundError8("Site not found.");
|
|
4471
|
+
const workOrderData = {
|
|
4472
|
+
...value.metadata.workOrder,
|
|
4473
|
+
attachments: value.metadata.attachments,
|
|
4474
|
+
createdBy: value.checkedBy,
|
|
4475
|
+
organization: site.orgId,
|
|
4476
|
+
site: site._id
|
|
4477
|
+
};
|
|
4478
|
+
const workOrder = await createWorkOrder(
|
|
4479
|
+
workOrderData,
|
|
4480
|
+
fullHost
|
|
4481
|
+
);
|
|
4482
|
+
if (!workOrder)
|
|
4483
|
+
throw new NotFoundError8("Failed to create work order.");
|
|
4484
|
+
}
|
|
4485
|
+
const result = await _updateUnitChecklist(id, value);
|
|
4486
|
+
return result;
|
|
4487
|
+
} catch (error) {
|
|
4488
|
+
logger24.error(`Error updating unit checklist with id ${id}:`, error);
|
|
4489
|
+
throw error;
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
return {
|
|
4493
|
+
approveUnitChecklist,
|
|
4494
|
+
rejectUnitChecklist
|
|
4495
|
+
};
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
// src/controllers/hygiene-unit-checklist.controller.ts
|
|
4499
|
+
import { workOrderSchema } from "@iservice365/core";
|
|
4500
|
+
function useUnitChecklistController() {
|
|
4501
|
+
const {
|
|
4502
|
+
createUnitChecklist: _createUnitChecklist,
|
|
4503
|
+
getAllUnitChecklist: _getAllUnitChecklist
|
|
4504
|
+
} = useUnitChecklistRepo();
|
|
4505
|
+
const {
|
|
4506
|
+
approveUnitChecklist: _approveUnitChecklist,
|
|
4507
|
+
rejectUnitChecklist: _rejectUnitChecklist
|
|
4508
|
+
} = useUnitChecklistService();
|
|
4509
|
+
async function createUnitChecklist(req, res, next) {
|
|
4510
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
4511
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
4512
|
+
{}
|
|
4513
|
+
) : {};
|
|
4514
|
+
const createdBy = cookies["user"] || "";
|
|
4515
|
+
const payload = { ...req.body, ...req.params, createdBy };
|
|
4516
|
+
const { error } = unitChecklistSchema.validate(payload);
|
|
4517
|
+
if (error) {
|
|
4518
|
+
logger25.log({ level: "error", message: error.message });
|
|
4519
|
+
next(new BadRequestError25(error.message));
|
|
4520
|
+
return;
|
|
4521
|
+
}
|
|
4522
|
+
try {
|
|
4523
|
+
const id = await _createUnitChecklist(payload);
|
|
4524
|
+
res.status(201).json({ message: "Unit checklist created successfully.", id });
|
|
4525
|
+
return;
|
|
4526
|
+
} catch (error2) {
|
|
4527
|
+
logger25.log({ level: "error", message: error2.message });
|
|
4528
|
+
next(error2);
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
async function getAllUnitChecklist(req, res, next) {
|
|
4533
|
+
const query = { ...req.query, ...req.params };
|
|
4534
|
+
const validation = Joi14.object({
|
|
4535
|
+
page: Joi14.number().min(1).optional().allow("", null),
|
|
4536
|
+
limit: Joi14.number().min(1).optional().allow("", null),
|
|
4537
|
+
search: Joi14.string().optional().allow("", null),
|
|
4538
|
+
site: Joi14.string().hex().required(),
|
|
4539
|
+
type: Joi14.string().valid(...allowedTypes).required(),
|
|
4540
|
+
parentChecklist: Joi14.string().hex().required(),
|
|
4541
|
+
areaChecklist: Joi14.string().hex().required()
|
|
4542
|
+
});
|
|
4543
|
+
const { error } = validation.validate(query);
|
|
4544
|
+
if (error) {
|
|
4545
|
+
logger25.log({ level: "error", message: error.message });
|
|
4546
|
+
next(new BadRequestError25(error.message));
|
|
4547
|
+
return;
|
|
4548
|
+
}
|
|
4549
|
+
const page = parseInt(req.query.page) ?? 1;
|
|
4550
|
+
const limit = parseInt(req.query.limit) ?? 20;
|
|
4551
|
+
const search = req.query.search ?? "";
|
|
4552
|
+
const site = req.params.site ?? "";
|
|
4553
|
+
const type = req.params.type ?? "";
|
|
4554
|
+
const parentChecklist = req.params.parentChecklist ?? "";
|
|
4555
|
+
const areaChecklist = req.params.areaChecklist ?? "";
|
|
4556
|
+
try {
|
|
4557
|
+
const data = await _getAllUnitChecklist({
|
|
4558
|
+
page,
|
|
4559
|
+
limit,
|
|
4560
|
+
search,
|
|
4561
|
+
site,
|
|
4562
|
+
type,
|
|
4563
|
+
parentChecklist,
|
|
4564
|
+
areaChecklist
|
|
4565
|
+
});
|
|
4566
|
+
res.json(data);
|
|
4567
|
+
return;
|
|
4568
|
+
} catch (error2) {
|
|
4569
|
+
logger25.log({ level: "error", message: error2.message });
|
|
4570
|
+
next(error2);
|
|
4571
|
+
return;
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
async function approveUnitChecklist(req, res, next) {
|
|
4575
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
4576
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
4577
|
+
{}
|
|
4578
|
+
) : {};
|
|
4579
|
+
const checkedBy = cookies["user"] || "";
|
|
4580
|
+
const payload = { id: req.params.id, checkedBy };
|
|
4581
|
+
const validation = Joi14.object({
|
|
4582
|
+
id: Joi14.string().hex().required(),
|
|
4583
|
+
checkedBy: Joi14.string().hex().required()
|
|
4584
|
+
});
|
|
4585
|
+
const { error } = validation.validate(payload);
|
|
4586
|
+
if (error) {
|
|
4587
|
+
logger25.log({ level: "error", message: error.message });
|
|
4588
|
+
next(new BadRequestError25(error.message));
|
|
4589
|
+
return;
|
|
4590
|
+
}
|
|
4591
|
+
try {
|
|
4592
|
+
const { id, ...value } = payload;
|
|
4593
|
+
await _approveUnitChecklist(id, value);
|
|
4594
|
+
res.json({ message: "Unit checklist approved successfully." });
|
|
4595
|
+
return;
|
|
4596
|
+
} catch (error2) {
|
|
4597
|
+
logger25.log({ level: "error", message: error2.message });
|
|
4598
|
+
next(error2);
|
|
4599
|
+
return;
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
async function rejectUnitChecklist(req, res, next) {
|
|
4603
|
+
const cookies = req.headers.cookie ? req.headers.cookie.split(";").map((cookie) => cookie.trim().split("=")).reduce(
|
|
4604
|
+
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
4605
|
+
{}
|
|
4606
|
+
) : {};
|
|
4607
|
+
const checkedBy = cookies["user"] || "";
|
|
4608
|
+
if (req.body.workOrder) {
|
|
4609
|
+
req.body.workOrder.createdBy = checkedBy;
|
|
4610
|
+
}
|
|
4611
|
+
const payload = { id: req.params.id, checkedBy, ...req.body };
|
|
4612
|
+
const validation = Joi14.object({
|
|
4613
|
+
id: Joi14.string().hex().required(),
|
|
4614
|
+
attachments: Joi14.array().items(Joi14.string()).optional(),
|
|
4615
|
+
remarks: Joi14.string().required(),
|
|
4616
|
+
workOrder: workOrderSchema.optional(),
|
|
4617
|
+
checkedBy: Joi14.string().hex().required()
|
|
4618
|
+
});
|
|
4619
|
+
const { error } = validation.validate(payload);
|
|
4620
|
+
if (error) {
|
|
4621
|
+
logger25.log({ level: "error", message: error.message });
|
|
4622
|
+
next(new BadRequestError25(error.message));
|
|
4623
|
+
return;
|
|
4624
|
+
}
|
|
4625
|
+
try {
|
|
4626
|
+
const { id, attachments, remarks, workOrder, ...value } = payload;
|
|
4627
|
+
value.metadata = {
|
|
4628
|
+
attachments: attachments || [],
|
|
4629
|
+
remarks: remarks || "",
|
|
4630
|
+
workOrder
|
|
4631
|
+
};
|
|
4632
|
+
if (value.metadata.workOrder) {
|
|
4633
|
+
if (value.metadata.workOrder.category) {
|
|
4634
|
+
value.metadata.workOrder.category = value.metadata.workOrder.category;
|
|
4635
|
+
}
|
|
4636
|
+
if (value.metadata.workOrder.serviceProvider) {
|
|
4637
|
+
value.metadata.workOrder.serviceProvider = value.metadata.workOrder.serviceProvider;
|
|
4638
|
+
}
|
|
4639
|
+
value.metadata.workOrder.createdBy = checkedBy;
|
|
4640
|
+
}
|
|
4641
|
+
const fullHost = req.headers.origin;
|
|
4642
|
+
await _rejectUnitChecklist(id, value, fullHost);
|
|
4643
|
+
res.json({ message: "Unit checklist rejected successfully." });
|
|
4644
|
+
return;
|
|
4645
|
+
} catch (error2) {
|
|
4646
|
+
logger25.log({ level: "error", message: error2.message });
|
|
4647
|
+
next(error2);
|
|
4648
|
+
return;
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
return {
|
|
4652
|
+
createUnitChecklist,
|
|
4653
|
+
getAllUnitChecklist,
|
|
4654
|
+
approveUnitChecklist,
|
|
4655
|
+
rejectUnitChecklist
|
|
4656
|
+
};
|
|
4657
|
+
}
|
|
4658
|
+
export {
|
|
4659
|
+
MArea,
|
|
4660
|
+
MAreaChecklist,
|
|
4661
|
+
MParentChecklist,
|
|
4662
|
+
MScheduleTaskArea,
|
|
4663
|
+
MToiletLocation,
|
|
4664
|
+
MUnit,
|
|
4665
|
+
MUnitChecklist,
|
|
4666
|
+
allowedStatus,
|
|
4667
|
+
allowedTypes,
|
|
4668
|
+
areaChecklistSchema,
|
|
4669
|
+
areaSchema,
|
|
4670
|
+
parentChecklistSchema,
|
|
4671
|
+
scheduleTaskAreaSchema,
|
|
4672
|
+
toiletLocationSchema,
|
|
4673
|
+
unitChecklistSchema,
|
|
4674
|
+
unitSchema,
|
|
4675
|
+
useAreaChecklistController,
|
|
4676
|
+
useAreaChecklistRepo,
|
|
4677
|
+
useAreaController,
|
|
4678
|
+
useAreaRepository,
|
|
4679
|
+
useAreaService,
|
|
4680
|
+
useParentChecklistController,
|
|
4681
|
+
useParentChecklistRepo,
|
|
4682
|
+
useScheduleTaskAreaController,
|
|
4683
|
+
useScheduleTaskAreaRepository,
|
|
4684
|
+
useScheduleTaskAreaService,
|
|
4685
|
+
useToiletLocationController,
|
|
4686
|
+
useToiletLocationRepository,
|
|
4687
|
+
useToiletLocationService,
|
|
4688
|
+
useUnitChecklistController,
|
|
4689
|
+
useUnitChecklistRepo,
|
|
4690
|
+
useUnitController,
|
|
4691
|
+
useUnitRepository,
|
|
4692
|
+
useUnitService
|
|
4693
|
+
};
|
|
1
4694
|
//# sourceMappingURL=index.mjs.map
|