@7365admin1/module-hygiene 4.30.2 → 4.31.0

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/dist/index.js CHANGED
@@ -924,21 +924,30 @@ function useHygieneDashboardController() {
924
924
  }
925
925
 
926
926
  // src/models/hygiene-area.model.ts
927
- var import_joi2 = __toESM(require("joi"));
927
+ var import_joi3 = __toESM(require("joi"));
928
928
  var import_mongodb2 = require("mongodb");
929
929
  var import_node_server_utils3 = require("@7365admin1/node-server-utils");
930
930
  var import_core2 = require("@7365admin1/core");
931
- var areaSchema = import_joi2.default.object({
932
- site: import_joi2.default.string().hex().required(),
933
- serviceType: import_joi2.default.string().valid(...Object.values(import_core2.AppServiceType)).required(),
934
- name: import_joi2.default.string().required(),
935
- type: import_joi2.default.string().valid(...allowedTypes).required(),
936
- set: import_joi2.default.number().min(0).optional(),
937
- units: import_joi2.default.array().items(
938
- import_joi2.default.object({
939
- unit: import_joi2.default.string().hex().required(),
940
- name: import_joi2.default.string().required()
941
- }).optional()
931
+
932
+ // src/utils/area-unit.util.ts
933
+ var import_joi2 = __toESM(require("joi"));
934
+ var areaUnitSchema = import_joi2.default.object({
935
+ unit: import_joi2.default.string().hex().required(),
936
+ name: import_joi2.default.string().required()
937
+ });
938
+ function toAreaUnit(unit) {
939
+ return { unit: String(unit._id), name: unit.name };
940
+ }
941
+
942
+ // src/models/hygiene-area.model.ts
943
+ var areaSchema = import_joi3.default.object({
944
+ site: import_joi3.default.string().hex().required(),
945
+ serviceType: import_joi3.default.string().valid(...Object.values(import_core2.AppServiceType)).required(),
946
+ name: import_joi3.default.string().required(),
947
+ type: import_joi3.default.string().valid(...allowedTypes).required(),
948
+ set: import_joi3.default.number().min(0).optional(),
949
+ units: import_joi3.default.array().items(
950
+ areaUnitSchema.optional()
942
951
  ).min(1).unique("unit", { ignoreUndefined: true }).messages({
943
952
  "array.unique": "Duplicate area units are not allowed"
944
953
  })
@@ -993,6 +1002,11 @@ function capitalizeText(text) {
993
1002
  return text.toLowerCase().split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
994
1003
  }
995
1004
 
1005
+ // src/utils/escape-regex.util.ts
1006
+ function escapeRegex(value) {
1007
+ return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1008
+ }
1009
+
996
1010
  // src/repositories/hygiene-area.repository.ts
997
1011
  function useAreaRepo() {
998
1012
  const db = import_node_server_utils4.useAtlas.getDb();
@@ -1089,7 +1103,7 @@ function useAreaRepo() {
1089
1103
  cacheOptions.type = type;
1090
1104
  }
1091
1105
  if (search) {
1092
- query.$or = [{ name: { $regex: search, $options: "i" } }];
1106
+ query.$or = [{ name: { $regex: escapeRegex(search), $options: "i" } }];
1093
1107
  cacheOptions.search = search;
1094
1108
  }
1095
1109
  const cacheKey = (0, import_node_server_utils4.makeCacheKey)(namespace_collection, cacheOptions);
@@ -1431,14 +1445,14 @@ var import_mongodb5 = require("mongodb");
1431
1445
  var import_node_server_utils7 = require("@7365admin1/node-server-utils");
1432
1446
 
1433
1447
  // src/models/hygiene-unit.model.ts
1434
- var import_joi3 = __toESM(require("joi"));
1448
+ var import_joi4 = __toESM(require("joi"));
1435
1449
  var import_mongodb4 = require("mongodb");
1436
1450
  var import_node_server_utils6 = require("@7365admin1/node-server-utils");
1437
1451
  var import_core3 = require("@7365admin1/core");
1438
- var unitSchema = import_joi3.default.object({
1439
- site: import_joi3.default.string().hex().required(),
1440
- serviceType: import_joi3.default.string().valid(...Object.values(import_core3.AppServiceType)).required(),
1441
- name: import_joi3.default.string().required()
1452
+ var unitSchema = import_joi4.default.object({
1453
+ site: import_joi4.default.string().hex().required(),
1454
+ serviceType: import_joi4.default.string().valid(...Object.values(import_core3.AppServiceType)).required(),
1455
+ name: import_joi4.default.string().required()
1442
1456
  });
1443
1457
  function MUnit(value) {
1444
1458
  const { error } = unitSchema.validate(value);
@@ -1551,7 +1565,7 @@ function useUnitRepository() {
1551
1565
  throw new import_node_server_utils7.BadRequestError("Invalid site ID format.");
1552
1566
  }
1553
1567
  if (search) {
1554
- query.$or = [{ name: { $regex: search, $options: "i" } }];
1568
+ query.$or = [{ name: { $regex: escapeRegex(search), $options: "i" } }];
1555
1569
  cacheOptions.search = search;
1556
1570
  }
1557
1571
  const cacheKey = (0, import_node_server_utils7.makeCacheKey)(namespace_collection, cacheOptions);
@@ -1773,10 +1787,7 @@ function useAreaService() {
1773
1787
  }
1774
1788
  }
1775
1789
  if (foundUnit) {
1776
- areaUnits.push({
1777
- unit: foundUnit._id,
1778
- name: foundUnit.name
1779
- });
1790
+ areaUnits.push(toAreaUnit(foundUnit));
1780
1791
  }
1781
1792
  }
1782
1793
  if (areaUnits.length > 0) {
@@ -1873,7 +1884,7 @@ function useAreaService() {
1873
1884
  }
1874
1885
 
1875
1886
  // src/controllers/hygiene-area.controller.ts
1876
- var import_joi4 = __toESM(require("joi"));
1887
+ var import_joi5 = __toESM(require("joi"));
1877
1888
  var import_node_server_utils9 = require("@7365admin1/node-server-utils");
1878
1889
  var import_core4 = require("@7365admin1/core");
1879
1890
 
@@ -1934,13 +1945,13 @@ function useAreaController() {
1934
1945
  }
1935
1946
  async function getAreas(req, res, next) {
1936
1947
  const query = { ...req.query, ...req.params };
1937
- const validation = import_joi4.default.object({
1938
- page: import_joi4.default.number().min(1).optional().allow("", null),
1939
- limit: import_joi4.default.number().min(1).optional().allow("", null),
1940
- search: import_joi4.default.string().optional().allow("", null),
1941
- type: import_joi4.default.string().valid("all", ...allowedTypes).optional().allow("", null),
1942
- site: import_joi4.default.string().hex().required(),
1943
- serviceType: import_joi4.default.string().valid(...Object.values(import_core4.AppServiceType)).required()
1948
+ const validation = import_joi5.default.object({
1949
+ page: import_joi5.default.number().min(1).optional().allow("", null),
1950
+ limit: import_joi5.default.number().min(1).optional().allow("", null),
1951
+ search: import_joi5.default.string().optional().allow("", null),
1952
+ type: import_joi5.default.string().valid("all", ...allowedTypes).optional().allow("", null),
1953
+ site: import_joi5.default.string().hex().required(),
1954
+ serviceType: import_joi5.default.string().valid(...Object.values(import_core4.AppServiceType)).required()
1944
1955
  });
1945
1956
  const { error } = validation.validate(query);
1946
1957
  if (error) {
@@ -1971,7 +1982,7 @@ function useAreaController() {
1971
1982
  }
1972
1983
  }
1973
1984
  async function getAreaById(req, res, next) {
1974
- const validation = import_joi4.default.string().hex().required();
1985
+ const validation = import_joi5.default.string().hex().required();
1975
1986
  const _id = req.params.id;
1976
1987
  const { error, value } = validation.validate(_id);
1977
1988
  if (error) {
@@ -1991,15 +2002,15 @@ function useAreaController() {
1991
2002
  }
1992
2003
  async function updateArea(req, res, next) {
1993
2004
  const payload = { id: req.params.id, ...req.body };
1994
- const schema = import_joi4.default.object({
1995
- id: import_joi4.default.string().hex().required(),
1996
- name: import_joi4.default.string().optional().allow("", null),
1997
- type: import_joi4.default.string().optional().allow("", null, ...allowedTypes),
1998
- set: import_joi4.default.number().min(0).optional(),
1999
- units: import_joi4.default.array().items(
2000
- import_joi4.default.object({
2001
- unit: import_joi4.default.string().hex().required(),
2002
- name: import_joi4.default.string().required()
2005
+ const schema = import_joi5.default.object({
2006
+ id: import_joi5.default.string().hex().required(),
2007
+ name: import_joi5.default.string().optional().allow("", null),
2008
+ type: import_joi5.default.string().optional().allow("", null, ...allowedTypes),
2009
+ set: import_joi5.default.number().min(0).optional(),
2010
+ units: import_joi5.default.array().items(
2011
+ import_joi5.default.object({
2012
+ unit: import_joi5.default.string().hex().required(),
2013
+ name: import_joi5.default.string().required()
2003
2014
  }).required()
2004
2015
  ).min(1).unique("unit", { ignoreUndefined: true }).optional().messages({
2005
2016
  "array.unique": "Duplicate area units are not allowed"
@@ -2024,7 +2035,7 @@ function useAreaController() {
2024
2035
  }
2025
2036
  async function deleteArea(req, res, next) {
2026
2037
  const id = req.params.id;
2027
- const validation = import_joi4.default.string().hex().required();
2038
+ const validation = import_joi5.default.string().hex().required();
2028
2039
  const { error, value } = validation.validate(id);
2029
2040
  if (error) {
2030
2041
  import_node_server_utils9.logger.log({ level: "error", message: error.message });
@@ -2047,9 +2058,9 @@ function useAreaController() {
2047
2058
  return;
2048
2059
  }
2049
2060
  const query = { ...req.query, ...req.params };
2050
- const validation = import_joi4.default.object({
2051
- site: import_joi4.default.string().hex().required(),
2052
- serviceType: import_joi4.default.string().valid(...Object.values(import_core4.AppServiceType)).required()
2061
+ const validation = import_joi5.default.object({
2062
+ site: import_joi5.default.string().hex().required(),
2063
+ serviceType: import_joi5.default.string().valid(...Object.values(import_core4.AppServiceType)).required()
2053
2064
  });
2054
2065
  const { error, value } = validation.validate(query);
2055
2066
  if (error) {
@@ -2070,9 +2081,9 @@ function useAreaController() {
2070
2081
  }
2071
2082
  async function exportAreas(req, res, next) {
2072
2083
  const query = { ...req.query, ...req.params };
2073
- const validation = import_joi4.default.object({
2074
- site: import_joi4.default.string().hex().required(),
2075
- serviceType: import_joi4.default.string().valid(...Object.values(import_core4.AppServiceType)).required()
2084
+ const validation = import_joi5.default.object({
2085
+ site: import_joi5.default.string().hex().required(),
2086
+ serviceType: import_joi5.default.string().valid(...Object.values(import_core4.AppServiceType)).required()
2076
2087
  });
2077
2088
  const { error, value } = validation.validate(query);
2078
2089
  if (error) {
@@ -2354,7 +2365,7 @@ function useUnitService() {
2354
2365
  }
2355
2366
 
2356
2367
  // src/controllers/hygiene-unit.controller.ts
2357
- var import_joi5 = __toESM(require("joi"));
2368
+ var import_joi6 = __toESM(require("joi"));
2358
2369
  var import_node_server_utils12 = require("@7365admin1/node-server-utils");
2359
2370
  var import_core5 = require("@7365admin1/core");
2360
2371
  function useUnitController() {
@@ -2385,12 +2396,12 @@ function useUnitController() {
2385
2396
  }
2386
2397
  async function getUnits(req, res, next) {
2387
2398
  const query = { ...req.query, ...req.params };
2388
- const validation = import_joi5.default.object({
2389
- page: import_joi5.default.number().min(1).optional().allow("", null),
2390
- limit: import_joi5.default.number().min(1).optional().allow("", null),
2391
- search: import_joi5.default.string().optional().allow("", null),
2392
- site: import_joi5.default.string().hex().required(),
2393
- serviceType: import_joi5.default.string().valid(...Object.values(import_core5.AppServiceType)).required()
2399
+ const validation = import_joi6.default.object({
2400
+ page: import_joi6.default.number().min(1).optional().allow("", null),
2401
+ limit: import_joi6.default.number().min(1).optional().allow("", null),
2402
+ search: import_joi6.default.string().optional().allow("", null),
2403
+ site: import_joi6.default.string().hex().required(),
2404
+ serviceType: import_joi6.default.string().valid(...Object.values(import_core5.AppServiceType)).required()
2394
2405
  });
2395
2406
  const { error } = validation.validate(query);
2396
2407
  if (error) {
@@ -2421,9 +2432,9 @@ function useUnitController() {
2421
2432
  }
2422
2433
  async function updateUnit(req, res, next) {
2423
2434
  const payload = { id: req.params.id, ...req.body };
2424
- const validation = import_joi5.default.object({
2425
- id: import_joi5.default.string().hex().required(),
2426
- name: import_joi5.default.string().required()
2435
+ const validation = import_joi6.default.object({
2436
+ id: import_joi6.default.string().hex().required(),
2437
+ name: import_joi6.default.string().required()
2427
2438
  });
2428
2439
  const { error } = validation.validate(payload);
2429
2440
  if (error) {
@@ -2444,8 +2455,8 @@ function useUnitController() {
2444
2455
  }
2445
2456
  async function deleteUnit(req, res, next) {
2446
2457
  const id = req.params.id;
2447
- const validation = import_joi5.default.object({
2448
- id: import_joi5.default.string().hex().required()
2458
+ const validation = import_joi6.default.object({
2459
+ id: import_joi6.default.string().hex().required()
2449
2460
  });
2450
2461
  const { error, value } = validation.validate({ id });
2451
2462
  if (error) {
@@ -2469,9 +2480,9 @@ function useUnitController() {
2469
2480
  return;
2470
2481
  }
2471
2482
  const query = { ...req.query, ...req.params };
2472
- const validation = import_joi5.default.object({
2473
- site: import_joi5.default.string().hex().required(),
2474
- serviceType: import_joi5.default.string().valid(...Object.values(import_core5.AppServiceType)).required()
2483
+ const validation = import_joi6.default.object({
2484
+ site: import_joi6.default.string().hex().required(),
2485
+ serviceType: import_joi6.default.string().valid(...Object.values(import_core5.AppServiceType)).required()
2475
2486
  });
2476
2487
  const { error, value } = validation.validate(query);
2477
2488
  if (error) {
@@ -2492,9 +2503,9 @@ function useUnitController() {
2492
2503
  }
2493
2504
  async function exportUnits(req, res, next) {
2494
2505
  const query = { ...req.query, ...req.params };
2495
- const validation = import_joi5.default.object({
2496
- site: import_joi5.default.string().hex().required(),
2497
- serviceType: import_joi5.default.string().valid(...Object.values(import_core5.AppServiceType)).required()
2506
+ const validation = import_joi6.default.object({
2507
+ site: import_joi6.default.string().hex().required(),
2508
+ serviceType: import_joi6.default.string().valid(...Object.values(import_core5.AppServiceType)).required()
2498
2509
  });
2499
2510
  const { error, value } = validation.validate(query);
2500
2511
  if (error) {
@@ -2537,15 +2548,15 @@ function useUnitController() {
2537
2548
  }
2538
2549
 
2539
2550
  // src/models/hygiene-parent-checklist.model.ts
2540
- var import_joi6 = __toESM(require("joi"));
2551
+ var import_joi7 = __toESM(require("joi"));
2541
2552
  var import_mongodb6 = require("mongodb");
2542
2553
  var import_node_server_utils13 = require("@7365admin1/node-server-utils");
2543
2554
  var import_core6 = require("@7365admin1/core");
2544
- var parentChecklistSchema = import_joi6.default.object({
2545
- createdAt: import_joi6.default.alternatives().try(import_joi6.default.date(), import_joi6.default.string()).optional().allow("", null),
2546
- site: import_joi6.default.string().hex().required(),
2547
- serviceType: import_joi6.default.string().valid(...Object.values(import_core6.AppServiceType)).required(),
2548
- dateStr: import_joi6.default.string().optional().allow("", null)
2555
+ var parentChecklistSchema = import_joi7.default.object({
2556
+ createdAt: import_joi7.default.alternatives().try(import_joi7.default.date(), import_joi7.default.string()).optional().allow("", null),
2557
+ site: import_joi7.default.string().hex().required(),
2558
+ serviceType: import_joi7.default.string().valid(...Object.values(import_core6.AppServiceType)).required(),
2559
+ dateStr: import_joi7.default.string().optional().allow("", null)
2549
2560
  });
2550
2561
  function MParentChecklist(value) {
2551
2562
  const { error } = parentChecklistSchema.validate(value);
@@ -3118,7 +3129,7 @@ function useParentChecklistRepo() {
3118
3129
  }
3119
3130
 
3120
3131
  // src/controllers/hygiene-parent-checklist.controller.ts
3121
- var import_joi7 = __toESM(require("joi"));
3132
+ var import_joi8 = __toESM(require("joi"));
3122
3133
  var import_node_server_utils15 = require("@7365admin1/node-server-utils");
3123
3134
  var import_core8 = require("@7365admin1/core");
3124
3135
  function useParentChecklistController() {
@@ -3128,10 +3139,10 @@ function useParentChecklistController() {
3128
3139
  } = useParentChecklistRepo();
3129
3140
  async function createParentChecklist(req, res, next) {
3130
3141
  const payload = req.body;
3131
- const validation = import_joi7.default.object({
3132
- createdAt: import_joi7.default.alternatives().try(import_joi7.default.date(), import_joi7.default.string()).optional().allow("", null),
3133
- site: import_joi7.default.string().hex().required(),
3134
- serviceType: import_joi7.default.string().valid(...Object.values(import_core8.AppServiceType)).optional().allow("", null)
3142
+ const validation = import_joi8.default.object({
3143
+ createdAt: import_joi8.default.alternatives().try(import_joi8.default.date(), import_joi8.default.string()).optional().allow("", null),
3144
+ site: import_joi8.default.string().hex().required(),
3145
+ serviceType: import_joi8.default.string().valid(...Object.values(import_core8.AppServiceType)).optional().allow("", null)
3135
3146
  });
3136
3147
  const { error } = validation.validate(payload);
3137
3148
  if (error) {
@@ -3151,15 +3162,15 @@ function useParentChecklistController() {
3151
3162
  }
3152
3163
  async function getAllParentChecklist(req, res, next) {
3153
3164
  const query = { ...req.query, ...req.params };
3154
- const validation = import_joi7.default.object({
3155
- page: import_joi7.default.number().min(1).optional().allow("", null),
3156
- limit: import_joi7.default.number().min(1).optional().allow("", null),
3157
- search: import_joi7.default.string().optional().allow("", null),
3158
- site: import_joi7.default.string().hex().required(),
3159
- serviceType: import_joi7.default.string().valid(...Object.values(import_core8.AppServiceType)).required(),
3160
- startDate: import_joi7.default.alternatives().try(import_joi7.default.date(), import_joi7.default.string()).optional().allow("", null),
3161
- endDate: import_joi7.default.alternatives().try(import_joi7.default.date(), import_joi7.default.string()).optional().allow("", null),
3162
- status: import_joi7.default.string().valid(...allowedStatus, "all").optional().allow("", null)
3165
+ const validation = import_joi8.default.object({
3166
+ page: import_joi8.default.number().min(1).optional().allow("", null),
3167
+ limit: import_joi8.default.number().min(1).optional().allow("", null),
3168
+ search: import_joi8.default.string().optional().allow("", null),
3169
+ site: import_joi8.default.string().hex().required(),
3170
+ serviceType: import_joi8.default.string().valid(...Object.values(import_core8.AppServiceType)).required(),
3171
+ startDate: import_joi8.default.alternatives().try(import_joi8.default.date(), import_joi8.default.string()).optional().allow("", null),
3172
+ endDate: import_joi8.default.alternatives().try(import_joi8.default.date(), import_joi8.default.string()).optional().allow("", null),
3173
+ status: import_joi8.default.string().valid(...allowedStatus, "all").optional().allow("", null)
3163
3174
  });
3164
3175
  const { error } = validation.validate(query);
3165
3176
  if (error) {
@@ -3201,28 +3212,28 @@ function useParentChecklistController() {
3201
3212
  }
3202
3213
 
3203
3214
  // src/models/hygiene-area-checklist.model.ts
3204
- var import_joi8 = __toESM(require("joi"));
3215
+ var import_joi9 = __toESM(require("joi"));
3205
3216
  var import_mongodb8 = require("mongodb");
3206
3217
  var import_node_server_utils16 = require("@7365admin1/node-server-utils");
3207
3218
  var import_core9 = require("@7365admin1/core");
3208
3219
  var allowedChecklistStatus = ["open", "completed", "closed"];
3209
- var areaChecklistSchema = import_joi8.default.object({
3210
- schedule: import_joi8.default.string().hex().required(),
3211
- serviceType: import_joi8.default.string().valid(...Object.values(import_core9.AppServiceType)).required(),
3212
- area: import_joi8.default.string().hex().required(),
3213
- name: import_joi8.default.string().required(),
3214
- type: import_joi8.default.string().valid(...allowedTypes).required(),
3215
- checklist: import_joi8.default.array().items(
3216
- import_joi8.default.object({
3217
- set: import_joi8.default.number().min(0).required(),
3218
- units: import_joi8.default.array().items(
3219
- import_joi8.default.object({
3220
- unit: import_joi8.default.string().hex().required(),
3221
- name: import_joi8.default.string().required()
3220
+ var areaChecklistSchema = import_joi9.default.object({
3221
+ schedule: import_joi9.default.string().hex().required(),
3222
+ serviceType: import_joi9.default.string().valid(...Object.values(import_core9.AppServiceType)).required(),
3223
+ area: import_joi9.default.string().hex().required(),
3224
+ name: import_joi9.default.string().required(),
3225
+ type: import_joi9.default.string().valid(...allowedTypes).required(),
3226
+ checklist: import_joi9.default.array().items(
3227
+ import_joi9.default.object({
3228
+ set: import_joi9.default.number().min(0).required(),
3229
+ units: import_joi9.default.array().items(
3230
+ import_joi9.default.object({
3231
+ unit: import_joi9.default.string().hex().required(),
3232
+ name: import_joi9.default.string().required()
3222
3233
  }).required()
3223
3234
  ).min(1).required(),
3224
- isScheduleTask: import_joi8.default.boolean().optional().default(false),
3225
- scheduleTaskId: import_joi8.default.string().hex().optional().allow(null)
3235
+ isScheduleTask: import_joi9.default.boolean().optional().default(false),
3236
+ scheduleTaskId: import_joi9.default.string().hex().optional().allow(null)
3226
3237
  }).required()
3227
3238
  ).optional().default([])
3228
3239
  });
@@ -4460,6 +4471,265 @@ function useAreaChecklistRepo() {
4460
4471
  };
4461
4472
  }
4462
4473
 
4474
+ // src/utils/schedule-task-rules.util.ts
4475
+ var import_joi10 = __toESM(require("joi"));
4476
+ var REPEATS = ["none", "daily", "weekdays", "weekly", "monthly"];
4477
+ var SGT_MS = 8 * 60 * 60 * 1e3;
4478
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4479
+ var TIME_RE = /^([0-1]\d|2[0-3]):([0-5]\d)$/;
4480
+ var pad = (n) => String(n).padStart(2, "0");
4481
+ function sgtParts(now) {
4482
+ const s = new Date(now.getTime() + SGT_MS);
4483
+ const date = `${s.getUTCFullYear()}-${pad(s.getUTCMonth() + 1)}-${pad(s.getUTCDate())}`;
4484
+ return { date, minutes: s.getUTCHours() * 60 + s.getUTCMinutes(), weekday: s.getUTCDay() };
4485
+ }
4486
+ function isRealDate(value) {
4487
+ if (typeof value !== "string" || !DATE_RE.test(value))
4488
+ return false;
4489
+ const [y, m, d] = value.split("-").map(Number);
4490
+ const t = new Date(Date.UTC(y, m - 1, d));
4491
+ return t.getUTCFullYear() === y && t.getUTCMonth() === m - 1 && t.getUTCDate() === d;
4492
+ }
4493
+ var toMinutes = (time) => {
4494
+ const [h, m] = time.split(":").map(Number);
4495
+ return h * 60 + m;
4496
+ };
4497
+ var weekdayOf = (date) => {
4498
+ const [y, m, d] = date.split("-").map(Number);
4499
+ return new Date(Date.UTC(y, m - 1, d)).getUTCDay();
4500
+ };
4501
+ var daysInMonth = (date) => {
4502
+ const [y, m] = date.split("-").map(Number);
4503
+ return new Date(Date.UTC(y, m, 0)).getUTCDate();
4504
+ };
4505
+ var addDays = (date, n) => {
4506
+ const [y, m, d] = date.split("-").map(Number);
4507
+ const t = new Date(Date.UTC(y, m - 1, d + n));
4508
+ return `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}-${pad(t.getUTCDate())}`;
4509
+ };
4510
+ var isRepeating = (task) => Boolean(task.repeat) && task.repeat !== "none";
4511
+ function occursOn(task, date) {
4512
+ if (task.endDate && date > task.endDate)
4513
+ return false;
4514
+ if (!isRepeating(task))
4515
+ return Array.isArray(task.dates) && task.dates.includes(date);
4516
+ if (task.startDate && date < task.startDate)
4517
+ return false;
4518
+ const weekday = weekdayOf(date);
4519
+ switch (task.repeat) {
4520
+ case "daily":
4521
+ return true;
4522
+ case "weekdays":
4523
+ return weekday >= 1 && weekday <= 5;
4524
+ case "weekly":
4525
+ return Array.isArray(task.daysOfWeek) && task.daysOfWeek.includes(weekday);
4526
+ case "monthly": {
4527
+ const wanted = task.dayOfMonth ?? (task.startDate ? Number(task.startDate.slice(8, 10)) : 1);
4528
+ return Number(date.slice(8, 10)) === Math.min(wanted, daysInMonth(date));
4529
+ }
4530
+ default:
4531
+ return false;
4532
+ }
4533
+ }
4534
+ function savedTodayAt(task, today) {
4535
+ const stamp = task.updatedAt || task.createdAt;
4536
+ if (!stamp)
4537
+ return null;
4538
+ const saved = new Date(stamp);
4539
+ if (Number.isNaN(saved.getTime()))
4540
+ return null;
4541
+ const p = sgtParts(saved);
4542
+ return p.date === today ? p.minutes : null;
4543
+ }
4544
+ function shouldRunNow(task, now = /* @__PURE__ */ new Date()) {
4545
+ if (task.paused || !TIME_RE.test(task.time ?? ""))
4546
+ return false;
4547
+ const { date, minutes } = sgtParts(now);
4548
+ if (task.lastRunDate === date || !occursOn(task, date))
4549
+ return false;
4550
+ const due = toMinutes(task.time);
4551
+ if (minutes < due)
4552
+ return false;
4553
+ const saved = savedTodayAt(task, date);
4554
+ return !(saved !== null && saved >= due);
4555
+ }
4556
+ var iso = (date, time) => `${date}T${time}:00+08:00`;
4557
+ function upcoming(task, now) {
4558
+ if (typeof task.time !== "string" || !TIME_RE.test(task.time))
4559
+ return null;
4560
+ const { date: today, minutes } = sgtParts(now);
4561
+ const due = toMinutes(task.time);
4562
+ if (occursOn(task, today) && (minutes < due || shouldRunNow({ ...task, paused: false }, now))) {
4563
+ return iso(today, task.time);
4564
+ }
4565
+ if (!isRepeating(task)) {
4566
+ const later = (task.dates ?? []).filter((d) => d > today && (!task.endDate || d <= task.endDate)).sort();
4567
+ return later.length ? iso(later[0], task.time) : null;
4568
+ }
4569
+ const from = task.startDate && task.startDate > today ? task.startDate : today;
4570
+ for (let i = from === today ? 1 : 0; i <= 400; i++) {
4571
+ const d = addDays(from, i);
4572
+ if (task.endDate && d > task.endDate)
4573
+ return null;
4574
+ if (occursOn(task, d))
4575
+ return iso(d, task.time);
4576
+ }
4577
+ return null;
4578
+ }
4579
+ function nextRun(task, now = /* @__PURE__ */ new Date()) {
4580
+ return task.paused ? null : upcoming(task, now);
4581
+ }
4582
+ function scheduleState(task, now = /* @__PURE__ */ new Date()) {
4583
+ if (task.paused)
4584
+ return "paused";
4585
+ return upcoming(task, now) ? "active" : "ended";
4586
+ }
4587
+ function lastRun(task) {
4588
+ return task.lastRunDate && TIME_RE.test(task.time ?? "") ? iso(task.lastRunDate, task.time) : null;
4589
+ }
4590
+ function scheduleDisplay(task, now = /* @__PURE__ */ new Date()) {
4591
+ try {
4592
+ return {
4593
+ nextRun: nextRun(task, now),
4594
+ lastRun: lastRun(task),
4595
+ scheduleStatus: scheduleState(task, now)
4596
+ };
4597
+ } catch {
4598
+ return { nextRun: null, lastRun: null, scheduleStatus: null };
4599
+ }
4600
+ }
4601
+ function validateScheduleRules(task, now = /* @__PURE__ */ new Date()) {
4602
+ const repeating = isRepeating(task);
4603
+ const { date: today, minutes } = sgtParts(now);
4604
+ if (repeating) {
4605
+ if (!task.startDate)
4606
+ return "Please choose a start date for a repeating schedule.";
4607
+ if (task.repeat === "weekly" && !(task.daysOfWeek && task.daysOfWeek.length)) {
4608
+ return "Pick at least one weekday for a weekly repeat.";
4609
+ }
4610
+ }
4611
+ const start = repeating ? task.startDate : [...task.dates ?? []].sort()[0];
4612
+ if (task.endDate && start && start > task.endDate) {
4613
+ return "The end date must be on or after the start date.";
4614
+ }
4615
+ if (task.endDate && task.endDate < today)
4616
+ return "The end date has already passed.";
4617
+ if (!repeating) {
4618
+ const dates = (task.dates ?? []).filter((d) => !task.endDate || d <= task.endDate);
4619
+ if (dates.length && dates.every((d) => d < today)) {
4620
+ return "All the dates you picked have already passed. Pick today or a later date.";
4621
+ }
4622
+ if (dates.length && dates.every((d) => d <= today) && dates.includes(today) && TIME_RE.test(task.time) && toMinutes(task.time) <= minutes) {
4623
+ return "That time has already passed today. Pick a later time or another date.";
4624
+ }
4625
+ }
4626
+ if (!upcoming({ ...task, paused: false, updatedAt: now }, now)) {
4627
+ return "This schedule would never run. Check the dates, end date and time.";
4628
+ }
4629
+ return null;
4630
+ }
4631
+ var scheduleDate = import_joi10.default.string().pattern(DATE_RE).custom((value, helpers) => isRealDate(value) ? value : helpers.error("date.invalid")).messages({
4632
+ "string.pattern.base": "Dates must look like 2026-10-21.",
4633
+ "string.empty": "Please choose a date.",
4634
+ "date.invalid": "That date does not exist. Dates must look like 2026-10-21.",
4635
+ "string.base": "Dates must look like 2026-10-21."
4636
+ });
4637
+ var REPEATING = import_joi10.default.valid("daily", "weekdays", "weekly", "monthly").required();
4638
+ var scheduleTaskFields = {
4639
+ title: import_joi10.default.string().trim().required().messages({
4640
+ "any.required": "Please enter a title.",
4641
+ "string.empty": "Please enter a title.",
4642
+ "string.base": "Please enter a title."
4643
+ }),
4644
+ time: import_joi10.default.string().pattern(TIME_RE).required().messages({
4645
+ "any.required": "Please choose a time.",
4646
+ "string.empty": "Please choose a time.",
4647
+ "string.pattern.base": "Time must be in 24-hour format like 08:30.",
4648
+ "string.base": "Time must be in 24-hour format like 08:30."
4649
+ }),
4650
+ dates: import_joi10.default.when("repeat", {
4651
+ is: REPEATING,
4652
+ then: import_joi10.default.array().items(scheduleDate).optional().default([]),
4653
+ otherwise: import_joi10.default.array().min(1).items(scheduleDate).required()
4654
+ }).messages({
4655
+ "any.required": "Pick at least one date, or choose a repeat.",
4656
+ "array.min": "Pick at least one date, or choose a repeat.",
4657
+ "array.base": "Dates must be a list of dates."
4658
+ }),
4659
+ description: import_joi10.default.string().optional().allow("", null),
4660
+ areas: import_joi10.default.array().min(1).items(
4661
+ import_joi10.default.object({
4662
+ name: import_joi10.default.string().required(),
4663
+ value: import_joi10.default.any().required()
4664
+ })
4665
+ ).required().messages({
4666
+ "any.required": "Pick at least one area.",
4667
+ "array.min": "Pick at least one area.",
4668
+ "array.base": "Pick at least one area."
4669
+ }),
4670
+ repeat: import_joi10.default.string().valid(...REPEATS).optional().allow("", null).messages({ "any.only": "Repeat must be one of: none, daily, weekdays, weekly, monthly." }),
4671
+ daysOfWeek: import_joi10.default.array().items(import_joi10.default.number().integer().min(0).max(6)).unique().optional().allow(null).messages({
4672
+ "number.base": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4673
+ "number.min": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4674
+ "number.max": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4675
+ "number.integer": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4676
+ "array.unique": "Each weekday can only be picked once."
4677
+ }),
4678
+ dayOfMonth: import_joi10.default.number().integer().min(1).max(31).optional().allow(null).messages({ "number.base": "Day of the month must be 1 to 31.", "number.min": "Day of the month must be 1 to 31.", "number.max": "Day of the month must be 1 to 31." }),
4679
+ startDate: scheduleDate.optional().allow("", null),
4680
+ endDate: scheduleDate.optional().allow("", null),
4681
+ paused: import_joi10.default.boolean().optional().allow(null).messages({ "boolean.base": "Paused must be true or false." })
4682
+ };
4683
+ var OPTIONAL_KEYS = ["repeat", "daysOfWeek", "dayOfMonth", "startDate", "endDate", "paused"];
4684
+ var isBlank = (v) => v === "" || v === null;
4685
+ function cleanScheduleInput(value) {
4686
+ const out = { ...value };
4687
+ for (const key of OPTIONAL_KEYS)
4688
+ if (isBlank(out[key]))
4689
+ delete out[key];
4690
+ if (out.repeat === "none") {
4691
+ delete out.daysOfWeek;
4692
+ delete out.dayOfMonth;
4693
+ delete out.startDate;
4694
+ }
4695
+ return out;
4696
+ }
4697
+ var TIMING_KEYS = ["time", "dates", "repeat", "daysOfWeek", "dayOfMonth", "startDate", "endDate"];
4698
+ function splitScheduleUpdate(payload) {
4699
+ const set = {};
4700
+ const unset = [];
4701
+ for (const [key, value] of Object.entries(payload)) {
4702
+ if (OPTIONAL_KEYS.includes(key) && isBlank(value))
4703
+ unset.push(key);
4704
+ else if (value !== void 0)
4705
+ set[key] = value;
4706
+ }
4707
+ if (set.repeat === "none") {
4708
+ for (const key of ["daysOfWeek", "dayOfMonth", "startDate"]) {
4709
+ if (!unset.includes(key))
4710
+ unset.push(key);
4711
+ delete set[key];
4712
+ }
4713
+ }
4714
+ return { set, unset };
4715
+ }
4716
+ function regularSetsMissing(checklist, setCount) {
4717
+ const regular = (checklist ?? []).filter((c) => c.isScheduleTask !== true).length;
4718
+ return Math.max(0, setCount - regular);
4719
+ }
4720
+ var normTiming = (t) => ({
4721
+ time: t.time ?? null,
4722
+ dates: [...Array.isArray(t.dates) ? t.dates : []].sort(),
4723
+ repeat: !t.repeat || t.repeat === "none" ? "none" : t.repeat,
4724
+ daysOfWeek: [...Array.isArray(t.daysOfWeek) ? t.daysOfWeek : []].sort(),
4725
+ dayOfMonth: t.dayOfMonth ?? null,
4726
+ startDate: t.startDate || null,
4727
+ endDate: t.endDate || null
4728
+ });
4729
+ function timingChanged(stored, after) {
4730
+ return JSON.stringify(normTiming(stored)) !== JSON.stringify(normTiming(after));
4731
+ }
4732
+
4463
4733
  // src/services/hygiene-area-checklist.service.ts
4464
4734
  function useAreaChecklistService() {
4465
4735
  const { getAreasForChecklist } = useAreaRepo();
@@ -4527,10 +4797,10 @@ function useAreaChecklistService() {
4527
4797
  `Area checklist for area ${area.name} exists but has no auto-gen sets. Inserting auto-gen sets.`
4528
4798
  );
4529
4799
  const setCount2 = Number(area.set) || 1;
4530
- const totalExistingSets = existing.checklist?.length ?? 0;
4531
- if (totalExistingSets >= setCount2) {
4800
+ const setsToAdd = regularSetsMissing(existing.checklist, setCount2);
4801
+ if (setsToAdd <= 0) {
4532
4802
  import_node_server_utils18.logger.info(
4533
- `Area checklist for area ${area.name} already has ${totalExistingSets} set(s) (configured: ${setCount2}). Skipping auto-gen sets.`
4803
+ `Area checklist for area ${area.name} already has its ${setCount2} regular set(s). Skipping auto-gen sets.`
4534
4804
  );
4535
4805
  return existing._id;
4536
4806
  }
@@ -4538,7 +4808,6 @@ function useAreaChecklistService() {
4538
4808
  (max, c) => Math.max(max, c.set ?? 0),
4539
4809
  0
4540
4810
  ) ?? 0;
4541
- const setsToAdd = setCount2 - totalExistingSets;
4542
4811
  const autoGenSets = Array.from(
4543
4812
  { length: setsToAdd },
4544
4813
  (_, index) => ({
@@ -4699,7 +4968,7 @@ function useAreaChecklistService() {
4699
4968
  }
4700
4969
 
4701
4970
  // src/controllers/hygiene-area-checklist.controller.ts
4702
- var import_joi9 = __toESM(require("joi"));
4971
+ var import_joi11 = __toESM(require("joi"));
4703
4972
  var import_node_server_utils20 = require("@7365admin1/node-server-utils");
4704
4973
  var import_core11 = require("@7365admin1/core");
4705
4974
 
@@ -4940,9 +5209,9 @@ function useAreaChecklistController() {
4940
5209
  site: req.params.site,
4941
5210
  schedule: req.params.schedule
4942
5211
  };
4943
- const validation = import_joi9.default.object({
4944
- site: import_joi9.default.string().hex().required(),
4945
- schedule: import_joi9.default.string().hex().required()
5212
+ const validation = import_joi11.default.object({
5213
+ site: import_joi11.default.string().hex().required(),
5214
+ schedule: import_joi11.default.string().hex().required()
4946
5215
  });
4947
5216
  const { error, value } = validation.validate(payload);
4948
5217
  if (error) {
@@ -4962,14 +5231,14 @@ function useAreaChecklistController() {
4962
5231
  }
4963
5232
  async function getAllAreaChecklist(req, res, next) {
4964
5233
  const query = { ...req.query, ...req.params };
4965
- const validation = import_joi9.default.object({
4966
- page: import_joi9.default.number().min(1).optional().allow("", null),
4967
- limit: import_joi9.default.number().min(1).optional().allow("", null),
4968
- search: import_joi9.default.string().optional().allow("", null),
4969
- type: import_joi9.default.string().valid(...allowedTypes, "all").optional().allow("", null),
4970
- status: import_joi9.default.string().valid(...allowedStatus, "all").optional().allow("", null),
4971
- schedule: import_joi9.default.string().hex().required(),
4972
- serviceType: import_joi9.default.string().valid(...Object.values(import_core11.AppServiceType)).required()
5234
+ const validation = import_joi11.default.object({
5235
+ page: import_joi11.default.number().min(1).optional().allow("", null),
5236
+ limit: import_joi11.default.number().min(1).optional().allow("", null),
5237
+ search: import_joi11.default.string().optional().allow("", null),
5238
+ type: import_joi11.default.string().valid(...allowedTypes, "all").optional().allow("", null),
5239
+ status: import_joi11.default.string().valid(...allowedStatus, "all").optional().allow("", null),
5240
+ schedule: import_joi11.default.string().hex().required(),
5241
+ serviceType: import_joi11.default.string().valid(...Object.values(import_core11.AppServiceType)).required()
4973
5242
  });
4974
5243
  const { error } = validation.validate(query);
4975
5244
  if (error) {
@@ -5004,14 +5273,14 @@ function useAreaChecklistController() {
5004
5273
  }
5005
5274
  async function getAreaChecklistHistory(req, res, next) {
5006
5275
  const query = { ...req.query, ...req.params };
5007
- const validation = import_joi9.default.object({
5008
- page: import_joi9.default.number().min(1).optional().allow("", null),
5009
- limit: import_joi9.default.number().min(1).optional().allow("", null),
5010
- search: import_joi9.default.string().optional().allow("", null),
5011
- type: import_joi9.default.string().optional().allow("", ...allowedTypes),
5012
- schedule: import_joi9.default.string().hex().required(),
5013
- status: import_joi9.default.string().allow("", null, ...allowedStatus),
5014
- createdAt: import_joi9.default.alternatives().try(import_joi9.default.date(), import_joi9.default.string()).optional().allow("", null)
5276
+ const validation = import_joi11.default.object({
5277
+ page: import_joi11.default.number().min(1).optional().allow("", null),
5278
+ limit: import_joi11.default.number().min(1).optional().allow("", null),
5279
+ search: import_joi11.default.string().optional().allow("", null),
5280
+ type: import_joi11.default.string().optional().allow("", ...allowedTypes),
5281
+ schedule: import_joi11.default.string().hex().required(),
5282
+ status: import_joi11.default.string().allow("", null, ...allowedStatus),
5283
+ createdAt: import_joi11.default.alternatives().try(import_joi11.default.date(), import_joi11.default.string()).optional().allow("", null)
5015
5284
  });
5016
5285
  const { error } = validation.validate(query);
5017
5286
  if (error) {
@@ -5045,7 +5314,7 @@ function useAreaChecklistController() {
5045
5314
  }
5046
5315
  }
5047
5316
  async function getAreaChecklistHistoryDetails(req, res, next) {
5048
- const validation = import_joi9.default.string().hex().required();
5317
+ const validation = import_joi11.default.string().hex().required();
5049
5318
  const _id = req.params.id;
5050
5319
  const { error, value } = validation.validate(_id);
5051
5320
  if (error) {
@@ -5065,12 +5334,12 @@ function useAreaChecklistController() {
5065
5334
  }
5066
5335
  async function getAreaChecklistUnits(req, res, next) {
5067
5336
  const query = { ...req.query, ...req.params };
5068
- const validation = import_joi9.default.object({
5069
- page: import_joi9.default.number().min(1).optional().allow("", null),
5070
- limit: import_joi9.default.number().min(1).optional().allow("", null),
5071
- search: import_joi9.default.string().optional().allow("", null),
5072
- serviceType: import_joi9.default.string().valid(...Object.values(import_core11.AppServiceType)).required(),
5073
- id: import_joi9.default.string().hex().required()
5337
+ const validation = import_joi11.default.object({
5338
+ page: import_joi11.default.number().min(1).optional().allow("", null),
5339
+ limit: import_joi11.default.number().min(1).optional().allow("", null),
5340
+ search: import_joi11.default.string().optional().allow("", null),
5341
+ serviceType: import_joi11.default.string().valid(...Object.values(import_core11.AppServiceType)).required(),
5342
+ id: import_joi11.default.string().hex().required()
5074
5343
  });
5075
5344
  const { error } = validation.validate(query);
5076
5345
  if (error) {
@@ -5120,21 +5389,21 @@ function useAreaChecklistController() {
5120
5389
  ...decisionValues,
5121
5390
  completedBy
5122
5391
  };
5123
- const validation = import_joi9.default.object({
5124
- id: import_joi9.default.string().hex().required(),
5125
- set: import_joi9.default.number().integer().min(1).required(),
5126
- unit: import_joi9.default.string().hex().required(),
5127
- decision: import_joi9.default.string().valid("approve", "reject").required(),
5128
- approve: import_joi9.default.boolean().optional(),
5129
- reject: import_joi9.default.boolean().optional(),
5130
- remarks: import_joi9.default.string().optional().allow("", null),
5392
+ const validation = import_joi11.default.object({
5393
+ id: import_joi11.default.string().hex().required(),
5394
+ set: import_joi11.default.number().integer().min(1).required(),
5395
+ unit: import_joi11.default.string().hex().required(),
5396
+ decision: import_joi11.default.string().valid("approve", "reject").required(),
5397
+ approve: import_joi11.default.boolean().optional(),
5398
+ reject: import_joi11.default.boolean().optional(),
5399
+ remarks: import_joi11.default.string().optional().allow("", null),
5131
5400
  /*
5132
5401
  * Shape only. WHETHER a photo is required depends on the set's current
5133
5402
  * state in the database, which Joi cannot see — see the check below.
5134
5403
  * Each entry must still be a real id: a blank string carries no evidence.
5135
5404
  */
5136
- attachment: import_joi9.default.array().items(import_joi9.default.string().trim().min(1)).optional(),
5137
- completedBy: import_joi9.default.string().hex().required()
5405
+ attachment: import_joi11.default.array().items(import_joi11.default.string().trim().min(1)).optional(),
5406
+ completedBy: import_joi11.default.string().hex().required()
5138
5407
  });
5139
5408
  const { error } = validation.validate(payload);
5140
5409
  if (error) {
@@ -5171,7 +5440,7 @@ function useAreaChecklistController() {
5171
5440
  }
5172
5441
  }
5173
5442
  async function downloadChecklistPdf(req, res, next) {
5174
- const validation = import_joi9.default.string().hex().required();
5443
+ const validation = import_joi11.default.string().hex().required();
5175
5444
  const schedule = req.params.schedule;
5176
5445
  const { error, value } = validation.validate(schedule);
5177
5446
  if (error) {
@@ -5217,16 +5486,16 @@ function useAreaChecklistController() {
5217
5486
  }
5218
5487
 
5219
5488
  // src/models/hygiene-supply.model.ts
5220
- var import_joi10 = __toESM(require("joi"));
5489
+ var import_joi12 = __toESM(require("joi"));
5221
5490
  var import_mongodb12 = require("mongodb");
5222
5491
  var import_node_server_utils21 = require("@7365admin1/node-server-utils");
5223
5492
  var import_core12 = require("@7365admin1/core");
5224
- var supplySchema = import_joi10.default.object({
5225
- site: import_joi10.default.string().hex().required(),
5226
- serviceType: import_joi10.default.string().valid(...Object.values(import_core12.AppServiceType)).required(),
5227
- name: import_joi10.default.string().required(),
5228
- unitOfMeasurement: import_joi10.default.string().required(),
5229
- attachment: import_joi10.default.string().allow(null, "").optional()
5493
+ var supplySchema = import_joi12.default.object({
5494
+ site: import_joi12.default.string().hex().required(),
5495
+ serviceType: import_joi12.default.string().valid(...Object.values(import_core12.AppServiceType)).required(),
5496
+ name: import_joi12.default.string().required(),
5497
+ unitOfMeasurement: import_joi12.default.string().required(),
5498
+ attachment: import_joi12.default.string().allow(null, "").optional()
5230
5499
  });
5231
5500
  function MSupply(value) {
5232
5501
  const { error } = supplySchema.validate(value);
@@ -5354,7 +5623,7 @@ function useSupplyRepository() {
5354
5623
  throw new import_node_server_utils22.BadRequestError("Invalid site ID format.");
5355
5624
  }
5356
5625
  if (search) {
5357
- query.name = { $regex: search, $options: "i" };
5626
+ query.name = { $regex: escapeRegex(search), $options: "i" };
5358
5627
  cacheOptions.search = search;
5359
5628
  }
5360
5629
  const cacheKey = (0, import_node_server_utils22.makeCacheKey)(namespace_collection, cacheOptions);
@@ -5516,7 +5785,7 @@ function useSupplyRepository() {
5516
5785
  }
5517
5786
 
5518
5787
  // src/controllers/hygiene-supply.controller.ts
5519
- var import_joi11 = __toESM(require("joi"));
5788
+ var import_joi13 = __toESM(require("joi"));
5520
5789
  var import_node_server_utils23 = require("@7365admin1/node-server-utils");
5521
5790
  var import_core13 = require("@7365admin1/core");
5522
5791
  function useSupplyController() {
@@ -5547,12 +5816,12 @@ function useSupplyController() {
5547
5816
  }
5548
5817
  async function getSupplies(req, res, next) {
5549
5818
  const query = { ...req.query, ...req.params };
5550
- const validation = import_joi11.default.object({
5551
- page: import_joi11.default.number().min(1).optional().allow("", null),
5552
- limit: import_joi11.default.number().min(1).optional().allow("", null),
5553
- search: import_joi11.default.string().optional().allow("", null),
5554
- site: import_joi11.default.string().hex().required(),
5555
- serviceType: import_joi11.default.string().valid(...Object.values(import_core13.AppServiceType)).required()
5819
+ const validation = import_joi13.default.object({
5820
+ page: import_joi13.default.number().min(1).optional().allow("", null),
5821
+ limit: import_joi13.default.number().min(1).optional().allow("", null),
5822
+ search: import_joi13.default.string().optional().allow("", null),
5823
+ site: import_joi13.default.string().hex().required(),
5824
+ serviceType: import_joi13.default.string().valid(...Object.values(import_core13.AppServiceType)).required()
5556
5825
  });
5557
5826
  const { error } = validation.validate(query);
5558
5827
  if (error) {
@@ -5582,7 +5851,7 @@ function useSupplyController() {
5582
5851
  }
5583
5852
  }
5584
5853
  async function getSupplyById(req, res, next) {
5585
- const validation = import_joi11.default.string().hex().required();
5854
+ const validation = import_joi13.default.string().hex().required();
5586
5855
  const _id = req.params.id;
5587
5856
  const { error, value } = validation.validate(_id);
5588
5857
  if (error) {
@@ -5602,12 +5871,12 @@ function useSupplyController() {
5602
5871
  }
5603
5872
  async function updateSupply(req, res, next) {
5604
5873
  const payload = { id: req.params.id, ...req.body };
5605
- const validation = import_joi11.default.object({
5606
- id: import_joi11.default.string().hex().required(),
5607
- name: import_joi11.default.string().optional().allow("", null),
5608
- unitOfMeasurement: import_joi11.default.string().optional().allow("", null),
5609
- qty: import_joi11.default.number().min(0).optional().allow("", null),
5610
- attachment: import_joi11.default.string().optional().allow("", null)
5874
+ const validation = import_joi13.default.object({
5875
+ id: import_joi13.default.string().hex().required(),
5876
+ name: import_joi13.default.string().optional().allow("", null),
5877
+ unitOfMeasurement: import_joi13.default.string().optional().allow("", null),
5878
+ qty: import_joi13.default.number().min(0).optional().allow("", null),
5879
+ attachment: import_joi13.default.string().optional().allow("", null)
5611
5880
  });
5612
5881
  const { error } = validation.validate(payload);
5613
5882
  if (error) {
@@ -5628,7 +5897,7 @@ function useSupplyController() {
5628
5897
  }
5629
5898
  async function deleteSupply(req, res, next) {
5630
5899
  const id = req.params.id;
5631
- const validation = import_joi11.default.string().hex().required();
5900
+ const validation = import_joi13.default.string().hex().required();
5632
5901
  const { error, value } = validation.validate(id);
5633
5902
  if (error) {
5634
5903
  import_node_server_utils23.logger.log({ level: "error", message: error.message });
@@ -5655,18 +5924,18 @@ function useSupplyController() {
5655
5924
  }
5656
5925
 
5657
5926
  // src/models/hygiene-stock.model.ts
5658
- var import_joi12 = __toESM(require("joi"));
5927
+ var import_joi14 = __toESM(require("joi"));
5659
5928
  var import_mongodb14 = require("mongodb");
5660
5929
  var import_node_server_utils24 = require("@7365admin1/node-server-utils");
5661
5930
  var import_core14 = require("@7365admin1/core");
5662
- var stockSchema = import_joi12.default.object({
5663
- site: import_joi12.default.string().hex().required(),
5664
- serviceType: import_joi12.default.string().valid(...Object.values(import_core14.AppServiceType)).required(),
5665
- supply: import_joi12.default.string().hex().required(),
5666
- in: import_joi12.default.number().min(0).optional(),
5667
- out: import_joi12.default.number().min(0).optional(),
5668
- balance: import_joi12.default.number().min(0).required(),
5669
- remarks: import_joi12.default.string().optional().allow("", null)
5931
+ var stockSchema = import_joi14.default.object({
5932
+ site: import_joi14.default.string().hex().required(),
5933
+ serviceType: import_joi14.default.string().valid(...Object.values(import_core14.AppServiceType)).required(),
5934
+ supply: import_joi14.default.string().hex().required(),
5935
+ in: import_joi14.default.number().min(0).optional(),
5936
+ out: import_joi14.default.number().min(0).optional(),
5937
+ balance: import_joi14.default.number().min(0).required(),
5938
+ remarks: import_joi14.default.string().optional().allow("", null)
5670
5939
  });
5671
5940
  function MStock(value) {
5672
5941
  const { error } = stockSchema.validate(value);
@@ -5881,7 +6150,7 @@ function useStockService() {
5881
6150
  }
5882
6151
 
5883
6152
  // src/controllers/hygiene-stock.controller.ts
5884
- var import_joi13 = __toESM(require("joi"));
6153
+ var import_joi15 = __toESM(require("joi"));
5885
6154
  var import_node_server_utils27 = require("@7365admin1/node-server-utils");
5886
6155
  var import_core15 = require("@7365admin1/core");
5887
6156
  function useStockController() {
@@ -5889,12 +6158,12 @@ function useStockController() {
5889
6158
  const { createStock: _createStock } = useStockService();
5890
6159
  async function createStock(req, res, next) {
5891
6160
  const payload = { ...req.body, ...req.params };
5892
- const validation = import_joi13.default.object({
5893
- site: import_joi13.default.string().hex().required(),
5894
- serviceType: import_joi13.default.string().valid(...Object.values(import_core15.AppServiceType)).required(),
5895
- supply: import_joi13.default.string().hex().required(),
5896
- qty: import_joi13.default.number().min(0).required(),
5897
- remarks: import_joi13.default.string().optional().allow("", null)
6161
+ const validation = import_joi15.default.object({
6162
+ site: import_joi15.default.string().hex().required(),
6163
+ serviceType: import_joi15.default.string().valid(...Object.values(import_core15.AppServiceType)).required(),
6164
+ supply: import_joi15.default.string().hex().required(),
6165
+ qty: import_joi15.default.number().min(0).required(),
6166
+ remarks: import_joi15.default.string().optional().allow("", null)
5898
6167
  });
5899
6168
  const { error } = validation.validate(payload);
5900
6169
  if (error) {
@@ -5914,13 +6183,13 @@ function useStockController() {
5914
6183
  }
5915
6184
  async function getStocksBySupplyId(req, res, next) {
5916
6185
  const query = { ...req.query, ...req.params };
5917
- const validation = import_joi13.default.object({
5918
- page: import_joi13.default.number().min(1).optional().allow("", null),
5919
- limit: import_joi13.default.number().min(1).optional().allow("", null),
5920
- search: import_joi13.default.string().optional().allow("", null),
5921
- site: import_joi13.default.string().hex().required(),
5922
- serviceType: import_joi13.default.string().valid(...Object.values(import_core15.AppServiceType)).required(),
5923
- supply: import_joi13.default.string().hex().required()
6186
+ const validation = import_joi15.default.object({
6187
+ page: import_joi15.default.number().min(1).optional().allow("", null),
6188
+ limit: import_joi15.default.number().min(1).optional().allow("", null),
6189
+ search: import_joi15.default.string().optional().allow("", null),
6190
+ site: import_joi15.default.string().hex().required(),
6191
+ serviceType: import_joi15.default.string().valid(...Object.values(import_core15.AppServiceType)).required(),
6192
+ supply: import_joi15.default.string().hex().required()
5924
6193
  });
5925
6194
  const { error } = validation.validate(query);
5926
6195
  if (error) {
@@ -5982,21 +6251,21 @@ async function applyCheckOutDecision(decision, decide, returnStock) {
5982
6251
  }
5983
6252
 
5984
6253
  // src/models/hygiene-checkout-item.model.ts
5985
- var import_joi14 = __toESM(require("joi"));
6254
+ var import_joi16 = __toESM(require("joi"));
5986
6255
  var import_mongodb16 = require("mongodb");
5987
6256
  var import_node_server_utils28 = require("@7365admin1/node-server-utils");
5988
6257
  var import_core16 = require("@7365admin1/core");
5989
6258
  var allowedCheckOutItemStatus = ["pending", "completed"];
5990
- var checkOutItemSchema = import_joi14.default.object({
5991
- site: import_joi14.default.string().hex().required(),
5992
- serviceType: import_joi14.default.string().valid(...Object.values(import_core16.AppServiceType)).required(),
5993
- supply: import_joi14.default.string().hex().required(),
5994
- supplyName: import_joi14.default.string().required(),
5995
- supplyQty: import_joi14.default.number().min(0).required(),
5996
- qty: import_joi14.default.number().min(0).required(),
5997
- attachment: import_joi14.default.array().items(import_joi14.default.string()).optional().allow(null),
5998
- createdBy: import_joi14.default.string().hex().required(),
5999
- createdByName: import_joi14.default.string().required()
6259
+ var checkOutItemSchema = import_joi16.default.object({
6260
+ site: import_joi16.default.string().hex().required(),
6261
+ serviceType: import_joi16.default.string().valid(...Object.values(import_core16.AppServiceType)).required(),
6262
+ supply: import_joi16.default.string().hex().required(),
6263
+ supplyName: import_joi16.default.string().required(),
6264
+ supplyQty: import_joi16.default.number().min(0).required(),
6265
+ qty: import_joi16.default.number().min(0).required(),
6266
+ attachment: import_joi16.default.array().items(import_joi16.default.string()).optional().allow(null),
6267
+ createdBy: import_joi16.default.string().hex().required(),
6268
+ createdByName: import_joi16.default.string().required()
6000
6269
  });
6001
6270
  function MCheckOutItem(value) {
6002
6271
  const { error } = checkOutItemSchema.validate(value);
@@ -6120,7 +6389,7 @@ function useCheckOutItemRepository() {
6120
6389
  throw new import_node_server_utils29.BadRequestError("Invalid site ID format.");
6121
6390
  }
6122
6391
  if (search) {
6123
- query.supplyName = { $regex: search, $options: "i" };
6392
+ query.supplyName = { $regex: escapeRegex(search), $options: "i" };
6124
6393
  cacheOptions.search = search;
6125
6394
  }
6126
6395
  const cacheKey = (0, import_node_server_utils29.makeCacheKey)(namespace_collection, cacheOptions);
@@ -6476,7 +6745,7 @@ function useCheckOutItemService() {
6476
6745
  }
6477
6746
 
6478
6747
  // src/controllers/hygiene-checkout-item.controller.ts
6479
- var import_joi15 = __toESM(require("joi"));
6748
+ var import_joi17 = __toESM(require("joi"));
6480
6749
  var import_node_server_utils31 = require("@7365admin1/node-server-utils");
6481
6750
  var import_core18 = require("@7365admin1/core");
6482
6751
  function useCheckOutItemController() {
@@ -6500,13 +6769,13 @@ function useCheckOutItemController() {
6500
6769
  ...req.params,
6501
6770
  createdBy
6502
6771
  };
6503
- const validation = import_joi15.default.object({
6504
- site: import_joi15.default.string().hex().required(),
6505
- serviceType: import_joi15.default.string().valid(...Object.values(import_core18.AppServiceType)).required(),
6506
- supply: import_joi15.default.string().hex().required(),
6507
- qty: import_joi15.default.number().min(0).required(),
6508
- attachment: import_joi15.default.array().items(import_joi15.default.string()).optional().allow(null),
6509
- createdBy: import_joi15.default.string().hex().required()
6772
+ const validation = import_joi17.default.object({
6773
+ site: import_joi17.default.string().hex().required(),
6774
+ serviceType: import_joi17.default.string().valid(...Object.values(import_core18.AppServiceType)).required(),
6775
+ supply: import_joi17.default.string().hex().required(),
6776
+ qty: import_joi17.default.number().min(0).required(),
6777
+ attachment: import_joi17.default.array().items(import_joi17.default.string()).optional().allow(null),
6778
+ createdBy: import_joi17.default.string().hex().required()
6510
6779
  });
6511
6780
  const { error } = validation.validate(payload);
6512
6781
  if (error) {
@@ -6535,15 +6804,15 @@ function useCheckOutItemController() {
6535
6804
  ...req.params,
6536
6805
  createdBy
6537
6806
  };
6538
- const validation = import_joi15.default.object({
6539
- site: import_joi15.default.string().hex().required(),
6540
- serviceType: import_joi15.default.string().valid(...Object.values(import_core18.AppServiceType)).required(),
6541
- createdBy: import_joi15.default.string().hex().required(),
6542
- items: import_joi15.default.array().items(
6543
- import_joi15.default.object({
6544
- supply: import_joi15.default.string().hex().required(),
6545
- qty: import_joi15.default.number().min(0).required(),
6546
- attachment: import_joi15.default.array().items(import_joi15.default.string()).optional().allow(null)
6807
+ const validation = import_joi17.default.object({
6808
+ site: import_joi17.default.string().hex().required(),
6809
+ serviceType: import_joi17.default.string().valid(...Object.values(import_core18.AppServiceType)).required(),
6810
+ createdBy: import_joi17.default.string().hex().required(),
6811
+ items: import_joi17.default.array().items(
6812
+ import_joi17.default.object({
6813
+ supply: import_joi17.default.string().hex().required(),
6814
+ qty: import_joi17.default.number().min(0).required(),
6815
+ attachment: import_joi17.default.array().items(import_joi17.default.string()).optional().allow(null)
6547
6816
  })
6548
6817
  ).min(1).required()
6549
6818
  });
@@ -6565,12 +6834,12 @@ function useCheckOutItemController() {
6565
6834
  }
6566
6835
  async function getCheckOutItems(req, res, next) {
6567
6836
  const query = { ...req.query, ...req.params };
6568
- const validation = import_joi15.default.object({
6569
- page: import_joi15.default.number().min(1).optional().allow("", null),
6570
- limit: import_joi15.default.number().min(1).optional().allow("", null),
6571
- search: import_joi15.default.string().optional().allow("", null),
6572
- site: import_joi15.default.string().hex().required(),
6573
- serviceType: import_joi15.default.string().valid(...Object.values(import_core18.AppServiceType)).required()
6837
+ const validation = import_joi17.default.object({
6838
+ page: import_joi17.default.number().min(1).optional().allow("", null),
6839
+ limit: import_joi17.default.number().min(1).optional().allow("", null),
6840
+ search: import_joi17.default.string().optional().allow("", null),
6841
+ site: import_joi17.default.string().hex().required(),
6842
+ serviceType: import_joi17.default.string().valid(...Object.values(import_core18.AppServiceType)).required()
6574
6843
  });
6575
6844
  const { error } = validation.validate(query);
6576
6845
  if (error) {
@@ -6600,7 +6869,7 @@ function useCheckOutItemController() {
6600
6869
  }
6601
6870
  }
6602
6871
  async function getCheckOutItemById(req, res, next) {
6603
- const validation = import_joi15.default.string().hex().required();
6872
+ const validation = import_joi17.default.string().hex().required();
6604
6873
  const _id = req.params.id;
6605
6874
  const { error, value } = validation.validate(_id);
6606
6875
  if (error) {
@@ -6639,13 +6908,13 @@ function useCheckOutItemController() {
6639
6908
  ...decisionValues,
6640
6909
  completedBy
6641
6910
  };
6642
- const validation = import_joi15.default.object({
6643
- id: import_joi15.default.string().hex().required(),
6644
- decision: import_joi15.default.string().valid("approve", "disapprove").required(),
6645
- approve: import_joi15.default.boolean().required(),
6646
- reject: import_joi15.default.boolean().required(),
6647
- remarks: import_joi15.default.string().optional().allow("", null),
6648
- completedBy: import_joi15.default.string().hex().required()
6911
+ const validation = import_joi17.default.object({
6912
+ id: import_joi17.default.string().hex().required(),
6913
+ decision: import_joi17.default.string().valid("approve", "disapprove").required(),
6914
+ approve: import_joi17.default.boolean().required(),
6915
+ reject: import_joi17.default.boolean().required(),
6916
+ remarks: import_joi17.default.string().optional().allow("", null),
6917
+ completedBy: import_joi17.default.string().hex().required()
6649
6918
  });
6650
6919
  const { error } = validation.validate(payload);
6651
6920
  if (error) {
@@ -6678,23 +6947,15 @@ function useCheckOutItemController() {
6678
6947
  // src/models/hygiene-schedule-task.model.ts
6679
6948
  var import_node_server_utils32 = require("@7365admin1/node-server-utils");
6680
6949
  var import_core19 = require("@7365admin1/core");
6681
- var import_joi16 = __toESM(require("joi"));
6950
+ var import_joi18 = __toESM(require("joi"));
6682
6951
  var import_mongodb18 = require("mongodb");
6683
- var scheduleTaskSchema = import_joi16.default.object({
6684
- site: import_joi16.default.string().hex().required(),
6685
- serviceType: import_joi16.default.string().valid(...Object.values(import_core19.AppServiceType)).required(),
6686
- title: import_joi16.default.string().required(),
6687
- time: import_joi16.default.string().pattern(/^([0-1]\d|2[0-3]):([0-5]\d)$/).required(),
6688
- dates: import_joi16.default.array().min(1).items(
6689
- import_joi16.default.string().pattern(/^\d{4}-\d{2}-\d{2}$/).required()
6690
- ).required(),
6691
- description: import_joi16.default.string().optional().allow("", null),
6692
- areas: import_joi16.default.array().min(1).items(
6693
- import_joi16.default.object({
6694
- name: import_joi16.default.string().required(),
6695
- value: import_joi16.default.any().required()
6696
- })
6697
- ).required()
6952
+ var scheduleTaskSchema = import_joi18.default.object({
6953
+ site: import_joi18.default.string().hex().required(),
6954
+ serviceType: import_joi18.default.string().valid(...Object.values(import_core19.AppServiceType)).required(),
6955
+ ...scheduleTaskFields
6956
+ }).custom((value, helpers) => {
6957
+ const message = validateScheduleRules(cleanScheduleInput(value));
6958
+ return message ? helpers.message({ custom: message }) : value;
6698
6959
  });
6699
6960
  function MScheduleTask(value) {
6700
6961
  const { error } = scheduleTaskSchema.validate(value);
@@ -6721,14 +6982,26 @@ function MScheduleTask(value) {
6721
6982
  }
6722
6983
  });
6723
6984
  }
6985
+ const extra = cleanScheduleInput({
6986
+ repeat: value.repeat,
6987
+ daysOfWeek: value.daysOfWeek,
6988
+ dayOfMonth: value.dayOfMonth,
6989
+ startDate: value.startDate,
6990
+ endDate: value.endDate,
6991
+ paused: value.paused
6992
+ });
6993
+ for (const key of Object.keys(extra))
6994
+ if (extra[key] === void 0)
6995
+ delete extra[key];
6724
6996
  return {
6725
6997
  site: value.site,
6726
6998
  serviceType: value.serviceType,
6727
6999
  title: value.title,
6728
7000
  time: value.time,
6729
- dates: value.dates,
7001
+ dates: value.dates ?? [],
6730
7002
  description: value.description,
6731
7003
  areas: value.areas,
7004
+ ...extra,
6732
7005
  status: "active",
6733
7006
  createdAt: /* @__PURE__ */ new Date(),
6734
7007
  updatedAt: "",
@@ -6739,11 +7012,23 @@ function MScheduleTask(value) {
6739
7012
  // src/repositories/hygiene-schedule-task.repository.ts
6740
7013
  var import_mongodb19 = require("mongodb");
6741
7014
  var import_node_server_utils33 = require("@7365admin1/node-server-utils");
7015
+ var SCHEDULE_FIELDS = {
7016
+ time: 1,
7017
+ dates: 1,
7018
+ repeat: 1,
7019
+ daysOfWeek: 1,
7020
+ dayOfMonth: 1,
7021
+ startDate: 1,
7022
+ endDate: 1,
7023
+ paused: 1,
7024
+ lastRunDate: 1
7025
+ };
6742
7026
  function useScheduleTaskRepository() {
6743
7027
  const db = import_node_server_utils33.useAtlas.getDb();
6744
7028
  if (!db) {
6745
7029
  throw new import_node_server_utils33.InternalServerError("Unable to connect to server.");
6746
7030
  }
7031
+ const database = db;
6747
7032
  const namespace_collection = "site.schedule-tasks";
6748
7033
  const collection = db.collection(namespace_collection);
6749
7034
  const { delNamespace, setCache, getCache } = (0, import_node_server_utils33.useCache)(namespace_collection);
@@ -6811,14 +7096,14 @@ function useScheduleTaskRepository() {
6811
7096
  throw new import_node_server_utils33.BadRequestError("Invalid site ID format.");
6812
7097
  }
6813
7098
  if (search) {
6814
- query.$or = [{ title: { $regex: search, $options: "i" } }];
7099
+ query.$or = [{ title: { $regex: escapeRegex(search), $options: "i" } }];
6815
7100
  cacheOptions.search = search;
6816
7101
  }
6817
7102
  const cacheKey = (0, import_node_server_utils33.makeCacheKey)(namespace_collection, cacheOptions);
6818
7103
  const cachedData = await getCache(cacheKey);
6819
7104
  if (cachedData) {
6820
7105
  import_node_server_utils33.logger.info(`Cache hit for key: ${cacheKey}`);
6821
- return cachedData;
7106
+ return enrichList(cachedData, site, serviceType);
6822
7107
  }
6823
7108
  try {
6824
7109
  const items = await collection.aggregate([
@@ -6827,7 +7112,8 @@ function useScheduleTaskRepository() {
6827
7112
  $project: {
6828
7113
  title: 1,
6829
7114
  areas: 1,
6830
- status: 1
7115
+ status: 1,
7116
+ ...SCHEDULE_FIELDS
6831
7117
  }
6832
7118
  },
6833
7119
  { $sort: { _id: -1 } },
@@ -6841,15 +7127,104 @@ function useScheduleTaskRepository() {
6841
7127
  }).catch((err) => {
6842
7128
  import_node_server_utils33.logger.error(`Failed to set cache for key: ${cacheKey}`, err);
6843
7129
  });
6844
- return data;
7130
+ return enrichList(data, site, serviceType);
6845
7131
  } catch (error) {
6846
7132
  throw error;
6847
7133
  }
6848
7134
  }
6849
- async function getAllScheduleTask() {
7135
+ async function enrichList(data, site, serviceType) {
7136
+ const items = data?.items ?? [];
7137
+ const now = /* @__PURE__ */ new Date();
7138
+ let statuses = /* @__PURE__ */ new Map();
7139
+ try {
7140
+ statuses = await getRunStatuses(items, site, serviceType);
7141
+ } catch (error) {
7142
+ import_node_server_utils33.logger.error("Could not read schedule task run status", error);
7143
+ }
7144
+ return {
7145
+ ...data,
7146
+ items: items.map((item) => {
7147
+ try {
7148
+ return {
7149
+ ...item,
7150
+ ...scheduleDisplay(item, now),
7151
+ lastRunStatus: statuses.get(String(item._id)) ?? null
7152
+ };
7153
+ } catch (error) {
7154
+ import_node_server_utils33.logger.error(`Could not enrich schedule task ${item?._id}`, error);
7155
+ return { ...item, nextRun: null, lastRun: null, scheduleStatus: null, lastRunStatus: null };
7156
+ }
7157
+ })
7158
+ };
7159
+ }
7160
+ async function getRunStatuses(items, site, serviceType) {
7161
+ const result = /* @__PURE__ */ new Map();
7162
+ const ran = items.filter((i) => i.lastRunDate);
7163
+ if (!ran.length)
7164
+ return result;
7165
+ const parents = await database.collection("site.cleaning.schedules").find(
7166
+ {
7167
+ site,
7168
+ serviceType,
7169
+ dateStr: { $in: [...new Set(ran.map((i) => i.lastRunDate))] }
7170
+ },
7171
+ { projection: { dateStr: 1 } }
7172
+ ).toArray();
7173
+ if (!parents.length)
7174
+ return result;
7175
+ const dateOf = new Map(parents.map((p) => [String(p._id), p.dateStr]));
7176
+ const taskIds = ran.map((i) => new import_mongodb19.ObjectId(i._id));
7177
+ const rows = await database.collection("site.cleaning.schedule.areas").aggregate([
7178
+ {
7179
+ $match: {
7180
+ schedule: { $in: parents.map((p) => p._id) },
7181
+ "checklist.scheduleTaskId": { $in: taskIds }
7182
+ }
7183
+ },
7184
+ { $unwind: "$checklist" },
7185
+ { $match: { "checklist.scheduleTaskId": { $in: taskIds } } },
7186
+ { $unwind: "$checklist.units" },
7187
+ {
7188
+ $group: {
7189
+ _id: { task: "$checklist.scheduleTaskId", schedule: "$schedule" },
7190
+ total: { $sum: 1 },
7191
+ completed: {
7192
+ $sum: { $cond: [{ $eq: ["$checklist.units.status", "completed"] }, 1, 0] }
7193
+ },
7194
+ closed: {
7195
+ $sum: { $cond: [{ $eq: ["$checklist.units.status", "closed"] }, 1, 0] }
7196
+ }
7197
+ }
7198
+ }
7199
+ ]).toArray();
7200
+ const sums = /* @__PURE__ */ new Map();
7201
+ for (const row of rows) {
7202
+ const task = ran.find((i) => String(i._id) === String(row._id.task));
7203
+ if (!task || dateOf.get(String(row._id.schedule)) !== task.lastRunDate)
7204
+ continue;
7205
+ const sum = sums.get(String(task._id)) ?? { total: 0, completed: 0, closed: 0 };
7206
+ sum.total += row.total;
7207
+ sum.completed += row.completed;
7208
+ sum.closed += row.closed;
7209
+ sums.set(String(task._id), sum);
7210
+ }
7211
+ for (const [id, sum] of sums) {
7212
+ result.set(id, sum.completed === sum.total ? "done" : sum.closed > 0 ? "missed" : "pending");
7213
+ }
7214
+ return result;
7215
+ }
7216
+ async function getAllScheduleTask(today) {
6850
7217
  const query = {
6851
7218
  status: { $ne: "deleted" }
6852
7219
  };
7220
+ if (today) {
7221
+ query.paused = { $ne: true };
7222
+ query.lastRunDate = { $ne: today };
7223
+ query.$or = [
7224
+ { dates: today },
7225
+ { repeat: { $in: ["daily", "weekdays", "weekly", "monthly"] } }
7226
+ ];
7227
+ }
6853
7228
  try {
6854
7229
  const items = await collection.aggregate([{ $match: query }, { $sort: { _id: -1 } }]).toArray();
6855
7230
  return items;
@@ -6857,6 +7232,9 @@ function useScheduleTaskRepository() {
6857
7232
  throw error;
6858
7233
  }
6859
7234
  }
7235
+ function withScheduleDisplay(row) {
7236
+ return { ...row, ...scheduleDisplay(row, /* @__PURE__ */ new Date()) };
7237
+ }
6860
7238
  async function getScheduleTaskById(_id, session) {
6861
7239
  try {
6862
7240
  _id = new import_mongodb19.ObjectId(_id);
@@ -6874,7 +7252,7 @@ function useScheduleTaskRepository() {
6874
7252
  const cachedData = await getCache(cacheKey);
6875
7253
  if (cachedData) {
6876
7254
  import_node_server_utils33.logger.info(`Cache hit for key: ${cacheKey}`);
6877
- return cachedData;
7255
+ return withScheduleDisplay(cachedData);
6878
7256
  }
6879
7257
  } else {
6880
7258
  import_node_server_utils33.logger.info(`Skipping cache during transaction for key: ${cacheKey}`);
@@ -6885,12 +7263,13 @@ function useScheduleTaskRepository() {
6885
7263
  {
6886
7264
  $project: {
6887
7265
  title: 1,
6888
- time: 1,
6889
- dates: 1,
6890
7266
  description: 1,
6891
7267
  areas: 1,
6892
7268
  status: 1,
6893
- createdAt: 1
7269
+ createdAt: 1,
7270
+ site: 1,
7271
+ serviceType: 1,
7272
+ ...SCHEDULE_FIELDS
6894
7273
  }
6895
7274
  }
6896
7275
  ]).toArray();
@@ -6902,7 +7281,7 @@ function useScheduleTaskRepository() {
6902
7281
  }).catch((err) => {
6903
7282
  import_node_server_utils33.logger.error(`Failed to set cache for key: ${cacheKey}`, err);
6904
7283
  });
6905
- return data[0];
7284
+ return withScheduleDisplay(data[0]);
6906
7285
  } catch (error) {
6907
7286
  throw error;
6908
7287
  }
@@ -6926,16 +7305,20 @@ function useScheduleTaskRepository() {
6926
7305
  });
6927
7306
  }
6928
7307
  try {
6929
- const updateValue = { ...value, updatedAt: /* @__PURE__ */ new Date() };
7308
+ const { set, unset } = splitScheduleUpdate(value);
7309
+ const update = {
7310
+ $set: { ...set, updatedAt: /* @__PURE__ */ new Date() }
7311
+ };
7312
+ if (unset.length) {
7313
+ update.$unset = Object.fromEntries(unset.map((key) => [key, ""]));
7314
+ }
6930
7315
  const res = await collection.updateOne(
6931
- { _id },
6932
- { $set: updateValue },
7316
+ { _id, status: { $ne: "deleted" } },
7317
+ update,
6933
7318
  { session }
6934
7319
  );
6935
- if (res.modifiedCount === 0) {
6936
- throw new import_node_server_utils33.InternalServerError(
6937
- "Unable to update hygiene schedule task."
6938
- );
7320
+ if (res.matchedCount === 0) {
7321
+ throw new import_node_server_utils33.NotFoundError("Schedule task not found.");
6939
7322
  }
6940
7323
  delNamespace().then(() => {
6941
7324
  import_node_server_utils33.logger.info(`Cache cleared for namespace: ${namespace_collection}`);
@@ -6950,6 +7333,18 @@ function useScheduleTaskRepository() {
6950
7333
  throw error;
6951
7334
  }
6952
7335
  }
7336
+ async function markScheduleTaskRan(_id, date) {
7337
+ await collection.updateOne(
7338
+ { _id: new import_mongodb19.ObjectId(_id) },
7339
+ { $set: { lastRunDate: date, lastRunAt: /* @__PURE__ */ new Date() } }
7340
+ );
7341
+ delNamespace().catch((err) => {
7342
+ import_node_server_utils33.logger.error(
7343
+ `Failed to clear cache for namespace: ${namespace_collection}`,
7344
+ err
7345
+ );
7346
+ });
7347
+ }
6953
7348
  async function deleteScheduleTask(_id, session) {
6954
7349
  try {
6955
7350
  _id = new import_mongodb19.ObjectId(_id);
@@ -6989,6 +7384,7 @@ function useScheduleTaskRepository() {
6989
7384
  createScheduleTask,
6990
7385
  getScheduleTasks,
6991
7386
  getAllScheduleTask,
7387
+ markScheduleTaskRan,
6992
7388
  getScheduleTaskById,
6993
7389
  updateScheduleTask,
6994
7390
  deleteScheduleTask
@@ -7000,7 +7396,7 @@ var import_mongodb20 = require("mongodb");
7000
7396
  var import_node_server_utils34 = require("@7365admin1/node-server-utils");
7001
7397
  function useScheduleTaskService() {
7002
7398
  const { createParentChecklist } = useParentChecklistRepo();
7003
- const { getAllScheduleTask } = useScheduleTaskRepository();
7399
+ const { getAllScheduleTask, markScheduleTaskRan } = useScheduleTaskRepository();
7004
7400
  const {
7005
7401
  createAreaChecklist,
7006
7402
  getAreaChecklistByAreaAndSchedule,
@@ -7009,73 +7405,7 @@ function useScheduleTaskService() {
7009
7405
  const { getAreaById } = useAreaRepo();
7010
7406
  function checkScheduleConditions(schedule, serviceType, currentDate = /* @__PURE__ */ new Date()) {
7011
7407
  try {
7012
- const now = currentDate;
7013
- const timeString = now.toLocaleTimeString("en-US", {
7014
- hour: "2-digit",
7015
- minute: "2-digit",
7016
- hour12: false,
7017
- timeZone: "Asia/Singapore"
7018
- });
7019
- const [currentHour, currentMinute] = timeString.split(":").map(Number);
7020
- import_node_server_utils34.logger.info(
7021
- `Checking schedule ${schedule._id}: Current time ${currentHour}:${currentMinute}, Schedule time ${schedule.time}, Dates ${JSON.stringify(schedule.dates)}`
7022
- );
7023
- const currentDateFormatted = now.toLocaleDateString("en-CA", {
7024
- timeZone: "Asia/Singapore"
7025
- });
7026
- if (!schedule.dates || !Array.isArray(schedule.dates) || schedule.dates.length === 0) {
7027
- import_node_server_utils34.logger.info(`Schedule ${schedule._id}: No dates configured, skipping`);
7028
- return false;
7029
- }
7030
- if (!schedule.dates.includes(currentDateFormatted)) {
7031
- import_node_server_utils34.logger.info(
7032
- `Schedule ${schedule._id}: Current date ${currentDateFormatted} is not in scheduled dates [${schedule.dates.join(
7033
- ", "
7034
- )}]`
7035
- );
7036
- return false;
7037
- }
7038
- const [scheduleHour, scheduleMinute] = schedule.time.split(":").map(Number);
7039
- const timeMatches = currentHour === scheduleHour && currentMinute === scheduleMinute;
7040
- if (!timeMatches) {
7041
- import_node_server_utils34.logger.info(
7042
- `Schedule ${schedule._id}: Time does not match. Current: ${currentHour}:${currentMinute}, Expected: ${scheduleHour}:${scheduleMinute}`
7043
- );
7044
- return false;
7045
- }
7046
- const relevantTimestamp = schedule.updatedAt || schedule.createdAt;
7047
- if (relevantTimestamp) {
7048
- const savedAt = new Date(relevantTimestamp);
7049
- const savedHour = parseInt(
7050
- savedAt.toLocaleTimeString("en-US", {
7051
- hour: "2-digit",
7052
- hour12: false,
7053
- timeZone: "Asia/Singapore"
7054
- }),
7055
- 10
7056
- );
7057
- const savedMinute = savedAt.toLocaleString("en-US", {
7058
- minute: "2-digit",
7059
- timeZone: "Asia/Singapore"
7060
- });
7061
- const savedDateFormatted = savedAt.toLocaleDateString("en-CA", {
7062
- timeZone: "Asia/Singapore"
7063
- });
7064
- const savedMinNum = parseInt(savedMinute, 10);
7065
- const wasSavedThisMinute = savedDateFormatted === currentDateFormatted && savedHour === currentHour && savedMinNum === currentMinute;
7066
- if (wasSavedThisMinute) {
7067
- import_node_server_utils34.logger.info(
7068
- `Schedule ${schedule._id}: Skipping \u2014 task was saved this minute (${savedHour}:${String(
7069
- savedMinNum
7070
- ).padStart(2, "0")}). Will fire on next occurrence.`
7071
- );
7072
- return false;
7073
- }
7074
- }
7075
- import_node_server_utils34.logger.info(
7076
- `Schedule ${schedule._id}: All conditions matched - Date is within range and time matches`
7077
- );
7078
- return true;
7408
+ return shouldRunNow(schedule, currentDate);
7079
7409
  } catch (error) {
7080
7410
  import_node_server_utils34.logger.error(
7081
7411
  `Error checking schedule conditions for ${schedule._id}:`,
@@ -7084,18 +7414,22 @@ function useScheduleTaskService() {
7084
7414
  return false;
7085
7415
  }
7086
7416
  }
7417
+ const MAX_ATTEMPTS = 5;
7418
+ const attempts = /* @__PURE__ */ new Map();
7087
7419
  async function processScheduledTasks(currentDate) {
7088
7420
  try {
7089
7421
  import_node_server_utils34.logger.info("Starting scheduled task processing...");
7090
- const scheduleTasks = await getAllScheduleTask();
7422
+ const today = sgtParts(currentDate ?? /* @__PURE__ */ new Date()).date;
7423
+ const scheduleTasks = await getAllScheduleTask(today);
7091
7424
  if (!scheduleTasks || scheduleTasks.length === 0) {
7092
7425
  import_node_server_utils34.logger.info("No schedule tasks found to process");
7093
- return { processed: 0, validated: 0 };
7426
+ return { processed: 0, validated: 0, fired: [] };
7094
7427
  }
7095
7428
  import_node_server_utils34.logger.info(`Found ${scheduleTasks.length} schedule tasks to check`);
7096
7429
  let processedCount = 0;
7097
7430
  let validatedCount = 0;
7098
7431
  const validatedTasks = [];
7432
+ const fired = [];
7099
7433
  for (const scheduleTask of scheduleTasks) {
7100
7434
  try {
7101
7435
  const serviceType = scheduleTask.serviceType;
@@ -7138,6 +7472,8 @@ function useScheduleTaskService() {
7138
7472
  import_node_server_utils34.logger.info(
7139
7473
  `Using parent checklist ${parentChecklistId}, now creating/updating area checklists`
7140
7474
  );
7475
+ let failedAreas = 0;
7476
+ const addedAreas = [];
7141
7477
  for (const area of scheduleTask.areas) {
7142
7478
  try {
7143
7479
  const areaId = area.value.toString();
@@ -7217,6 +7553,7 @@ function useScheduleTaskService() {
7217
7553
  newSets
7218
7554
  );
7219
7555
  if (modified > 0) {
7556
+ addedAreas.push(area.name);
7220
7557
  import_node_server_utils34.logger.info(
7221
7558
  `Appended ${areaSetConfig} set(s) starting from ${startSetNumber} to area checklist for area ${area.name}`
7222
7559
  );
@@ -7249,13 +7586,32 @@ function useScheduleTaskService() {
7249
7586
  `Area ${area.name} (${areaId}): Creating new area checklist with schedule-task sets only`
7250
7587
  );
7251
7588
  await createAreaChecklist(checklistData);
7589
+ addedAreas.push(area.name);
7252
7590
  import_node_server_utils34.logger.info(`Created new area checklist for area ${area.name}`);
7253
7591
  }
7254
7592
  } catch (error) {
7255
7593
  import_node_server_utils34.logger.error(`Error processing area ${area.name}:`, error);
7594
+ failedAreas++;
7256
7595
  continue;
7257
7596
  }
7258
7597
  }
7598
+ const attemptKey = `${scheduleTask._id}:${today}`;
7599
+ const tried = (attempts.get(attemptKey) ?? 0) + 1;
7600
+ attempts.set(attemptKey, tried);
7601
+ if (failedAreas === 0 || tried >= MAX_ATTEMPTS) {
7602
+ await markScheduleTaskRan(scheduleTask._id, today);
7603
+ attempts.delete(attemptKey);
7604
+ }
7605
+ if (addedAreas.length > 0) {
7606
+ fired.push({
7607
+ _id: scheduleTask._id,
7608
+ site: scheduleTask.site,
7609
+ serviceType,
7610
+ title: scheduleTask.title,
7611
+ time: scheduleTask.time,
7612
+ areas: addedAreas
7613
+ });
7614
+ }
7259
7615
  processedCount++;
7260
7616
  validatedCount++;
7261
7617
  validatedTasks.push(scheduleTask);
@@ -7267,6 +7623,17 @@ function useScheduleTaskService() {
7267
7623
  `Error processing schedule task ${scheduleTask._id}:`,
7268
7624
  error
7269
7625
  );
7626
+ try {
7627
+ const key = `${scheduleTask._id}:${today}`;
7628
+ const tried = (attempts.get(key) ?? 0) + 1;
7629
+ attempts.set(key, tried);
7630
+ if (tried >= MAX_ATTEMPTS && scheduleTask._id) {
7631
+ await markScheduleTaskRan(scheduleTask._id, today);
7632
+ attempts.delete(key);
7633
+ }
7634
+ } catch (markError) {
7635
+ import_node_server_utils34.logger.error("Could not mark failing schedule task", markError);
7636
+ }
7270
7637
  continue;
7271
7638
  }
7272
7639
  }
@@ -7276,7 +7643,8 @@ function useScheduleTaskService() {
7276
7643
  return {
7277
7644
  processed: processedCount,
7278
7645
  validated: validatedCount,
7279
- tasks: validatedTasks
7646
+ tasks: validatedTasks,
7647
+ fired
7280
7648
  };
7281
7649
  } catch (error) {
7282
7650
  import_node_server_utils34.logger.error("Error processing scheduled tasks:", error);
@@ -7287,7 +7655,7 @@ function useScheduleTaskService() {
7287
7655
  }
7288
7656
 
7289
7657
  // src/controllers/hygiene-schedule-task.controller.ts
7290
- var import_joi17 = __toESM(require("joi"));
7658
+ var import_joi19 = __toESM(require("joi"));
7291
7659
  var import_node_server_utils35 = require("@7365admin1/node-server-utils");
7292
7660
  var import_core20 = require("@7365admin1/core");
7293
7661
  function useScheduleTaskController() {
@@ -7318,12 +7686,12 @@ function useScheduleTaskController() {
7318
7686
  }
7319
7687
  async function getScheduleTasks(req, res, next) {
7320
7688
  const query = { ...req.query, ...req.params };
7321
- const validation = import_joi17.default.object({
7322
- page: import_joi17.default.number().min(1).optional().allow("", null),
7323
- limit: import_joi17.default.number().min(1).optional().allow("", null),
7324
- search: import_joi17.default.string().optional().allow("", null),
7325
- site: import_joi17.default.string().hex().required(),
7326
- serviceType: import_joi17.default.string().valid(...Object.values(import_core20.AppServiceType)).required()
7689
+ const validation = import_joi19.default.object({
7690
+ page: import_joi19.default.number().min(1).optional().allow("", null),
7691
+ limit: import_joi19.default.number().min(1).optional().allow("", null),
7692
+ search: import_joi19.default.string().optional().allow("", null),
7693
+ site: import_joi19.default.string().hex().required(),
7694
+ serviceType: import_joi19.default.string().valid(...Object.values(import_core20.AppServiceType)).required()
7327
7695
  });
7328
7696
  const { error } = validation.validate(query);
7329
7697
  if (error) {
@@ -7331,8 +7699,8 @@ function useScheduleTaskController() {
7331
7699
  next(new import_node_server_utils35.BadRequestError(error.message));
7332
7700
  return;
7333
7701
  }
7334
- const page = parseInt(req.query.page) ?? 1;
7335
- const limit = parseInt(req.query.limit) ?? 10;
7702
+ const page = parseInt(req.query.page) || 1;
7703
+ const limit = parseInt(req.query.limit) || 10;
7336
7704
  const search = req.query.search ?? "";
7337
7705
  const site = req.params.site ?? "";
7338
7706
  const serviceType = req.query.serviceType ?? "";
@@ -7353,7 +7721,7 @@ function useScheduleTaskController() {
7353
7721
  }
7354
7722
  }
7355
7723
  async function getScheduleTaskById(req, res, next) {
7356
- const validation = import_joi17.default.string().hex().required();
7724
+ const validation = import_joi19.default.string().hex().required();
7357
7725
  const _id = req.params.id;
7358
7726
  const { error, value } = validation.validate(_id);
7359
7727
  if (error) {
@@ -7373,20 +7741,17 @@ function useScheduleTaskController() {
7373
7741
  }
7374
7742
  async function updateScheduleTask(req, res, next) {
7375
7743
  const payload = { id: req.params.id, ...req.body };
7376
- const validation = import_joi17.default.object({
7377
- id: import_joi17.default.string().hex().required(),
7378
- title: import_joi17.default.string().optional().allow("", null),
7379
- time: import_joi17.default.string().pattern(/^([0-1]\d|2[0-3]):([0-5]\d)$/).optional().allow("", null),
7380
- dates: import_joi17.default.array().min(1).items(
7381
- import_joi17.default.string().pattern(/^\d{4}-\d{2}-\d{2}$/).required()
7382
- ).optional(),
7383
- description: import_joi17.default.string().optional().allow("", null),
7384
- areas: import_joi17.default.array().min(1).items(
7385
- import_joi17.default.object({
7386
- name: import_joi17.default.string().required(),
7387
- value: import_joi17.default.any().required()
7388
- })
7389
- ).optional()
7744
+ const optional = Object.fromEntries(
7745
+ Object.entries(scheduleTaskFields).map(([key, rule]) => [
7746
+ key,
7747
+ key === "dates" ? import_joi19.default.array().min(1).items(scheduleDate).optional().messages({
7748
+ "array.min": "Pick at least one date, or choose a repeat."
7749
+ }) : rule.optional()
7750
+ ])
7751
+ );
7752
+ const validation = import_joi19.default.object({
7753
+ id: import_joi19.default.string().hex().required(),
7754
+ ...optional
7390
7755
  });
7391
7756
  const { error } = validation.validate(payload);
7392
7757
  if (error) {
@@ -7394,6 +7759,24 @@ function useScheduleTaskController() {
7394
7759
  next(new import_node_server_utils35.BadRequestError(error.message));
7395
7760
  return;
7396
7761
  }
7762
+ try {
7763
+ const { set, unset } = splitScheduleUpdate(req.body ?? {});
7764
+ if (TIMING_KEYS.some((key) => key in set || unset.includes(key))) {
7765
+ const stored = await _getScheduleTaskById(req.params.id);
7766
+ const merged = { ...stored, ...set };
7767
+ for (const key of unset)
7768
+ delete merged[key];
7769
+ const problem = timingChanged(stored, merged) ? validateScheduleRules(cleanScheduleInput(merged)) : null;
7770
+ if (problem) {
7771
+ next(new import_node_server_utils35.BadRequestError(problem));
7772
+ return;
7773
+ }
7774
+ }
7775
+ } catch (error2) {
7776
+ import_node_server_utils35.logger.log({ level: "error", message: error2.message });
7777
+ next(error2);
7778
+ return;
7779
+ }
7397
7780
  try {
7398
7781
  const { id, ...value } = payload;
7399
7782
  await _updateScheduleTask(id, value);
@@ -7406,7 +7789,7 @@ function useScheduleTaskController() {
7406
7789
  }
7407
7790
  }
7408
7791
  async function deleteScheduleTask(req, res, next) {
7409
- const validation = import_joi17.default.string().hex().required();
7792
+ const validation = import_joi19.default.string().hex().required();
7410
7793
  const _id = req.params.id;
7411
7794
  const { error, value } = validation.validate(_id);
7412
7795
  if (error) {
@@ -7650,17 +8033,17 @@ function useQRService() {
7650
8033
  }
7651
8034
 
7652
8035
  // src/controllers/hygiene-qr.controller.ts
7653
- var import_joi18 = __toESM(require("joi"));
8036
+ var import_joi20 = __toESM(require("joi"));
7654
8037
  var import_node_server_utils37 = require("@7365admin1/node-server-utils");
7655
8038
  function useQRController() {
7656
8039
  const { generateQRImage: _generateQRImage, generateQRPDF: _generateQRPDF } = useQRService();
7657
8040
  async function generateQR(req, res, next) {
7658
- const validation = import_joi18.default.object({
7659
- url: import_joi18.default.string().uri().required(),
7660
- filename: import_joi18.default.string().optional().allow("", null),
7661
- title: import_joi18.default.string().optional().allow("", null),
7662
- subtitle: import_joi18.default.string().optional().allow("", null),
7663
- download: import_joi18.default.boolean().optional().default(false)
8041
+ const validation = import_joi20.default.object({
8042
+ url: import_joi20.default.string().uri().required(),
8043
+ filename: import_joi20.default.string().optional().allow("", null),
8044
+ title: import_joi20.default.string().optional().allow("", null),
8045
+ subtitle: import_joi20.default.string().optional().allow("", null),
8046
+ download: import_joi20.default.boolean().optional().default(false)
7664
8047
  });
7665
8048
  const query = { ...req.query };
7666
8049
  const { error, value } = validation.validate(query);