@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.mjs CHANGED
@@ -843,21 +843,30 @@ function useHygieneDashboardController() {
843
843
  }
844
844
 
845
845
  // src/models/hygiene-area.model.ts
846
- import Joi2 from "joi";
846
+ import Joi3 from "joi";
847
847
  import { ObjectId as ObjectId2 } from "mongodb";
848
848
  import { BadRequestError as BadRequestError3, logger as logger3 } from "@7365admin1/node-server-utils";
849
849
  import { AppServiceType as AppServiceType2 } from "@7365admin1/core";
850
- var areaSchema = Joi2.object({
851
- site: Joi2.string().hex().required(),
852
- serviceType: Joi2.string().valid(...Object.values(AppServiceType2)).required(),
853
- name: Joi2.string().required(),
854
- type: Joi2.string().valid(...allowedTypes).required(),
855
- set: Joi2.number().min(0).optional(),
856
- units: Joi2.array().items(
857
- Joi2.object({
858
- unit: Joi2.string().hex().required(),
859
- name: Joi2.string().required()
860
- }).optional()
850
+
851
+ // src/utils/area-unit.util.ts
852
+ import Joi2 from "joi";
853
+ var areaUnitSchema = Joi2.object({
854
+ unit: Joi2.string().hex().required(),
855
+ name: Joi2.string().required()
856
+ });
857
+ function toAreaUnit(unit) {
858
+ return { unit: String(unit._id), name: unit.name };
859
+ }
860
+
861
+ // src/models/hygiene-area.model.ts
862
+ var areaSchema = Joi3.object({
863
+ site: Joi3.string().hex().required(),
864
+ serviceType: Joi3.string().valid(...Object.values(AppServiceType2)).required(),
865
+ name: Joi3.string().required(),
866
+ type: Joi3.string().valid(...allowedTypes).required(),
867
+ set: Joi3.number().min(0).optional(),
868
+ units: Joi3.array().items(
869
+ areaUnitSchema.optional()
861
870
  ).min(1).unique("unit", { ignoreUndefined: true }).messages({
862
871
  "array.unique": "Duplicate area units are not allowed"
863
872
  })
@@ -921,6 +930,11 @@ function capitalizeText(text) {
921
930
  return text.toLowerCase().split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
922
931
  }
923
932
 
933
+ // src/utils/escape-regex.util.ts
934
+ function escapeRegex(value) {
935
+ return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
936
+ }
937
+
924
938
  // src/repositories/hygiene-area.repository.ts
925
939
  function useAreaRepo() {
926
940
  const db = useAtlas2.getDb();
@@ -1017,7 +1031,7 @@ function useAreaRepo() {
1017
1031
  cacheOptions.type = type;
1018
1032
  }
1019
1033
  if (search) {
1020
- query.$or = [{ name: { $regex: search, $options: "i" } }];
1034
+ query.$or = [{ name: { $regex: escapeRegex(search), $options: "i" } }];
1021
1035
  cacheOptions.search = search;
1022
1036
  }
1023
1037
  const cacheKey = makeCacheKey2(namespace_collection, cacheOptions);
@@ -1371,14 +1385,14 @@ import {
1371
1385
  } from "@7365admin1/node-server-utils";
1372
1386
 
1373
1387
  // src/models/hygiene-unit.model.ts
1374
- import Joi3 from "joi";
1388
+ import Joi4 from "joi";
1375
1389
  import { ObjectId as ObjectId4 } from "mongodb";
1376
1390
  import { BadRequestError as BadRequestError5, logger as logger6 } from "@7365admin1/node-server-utils";
1377
1391
  import { AppServiceType as AppServiceType3 } from "@7365admin1/core";
1378
- var unitSchema = Joi3.object({
1379
- site: Joi3.string().hex().required(),
1380
- serviceType: Joi3.string().valid(...Object.values(AppServiceType3)).required(),
1381
- name: Joi3.string().required()
1392
+ var unitSchema = Joi4.object({
1393
+ site: Joi4.string().hex().required(),
1394
+ serviceType: Joi4.string().valid(...Object.values(AppServiceType3)).required(),
1395
+ name: Joi4.string().required()
1382
1396
  });
1383
1397
  function MUnit(value) {
1384
1398
  const { error } = unitSchema.validate(value);
@@ -1491,7 +1505,7 @@ function useUnitRepository() {
1491
1505
  throw new BadRequestError6("Invalid site ID format.");
1492
1506
  }
1493
1507
  if (search) {
1494
- query.$or = [{ name: { $regex: search, $options: "i" } }];
1508
+ query.$or = [{ name: { $regex: escapeRegex(search), $options: "i" } }];
1495
1509
  cacheOptions.search = search;
1496
1510
  }
1497
1511
  const cacheKey = makeCacheKey3(namespace_collection, cacheOptions);
@@ -1713,10 +1727,7 @@ function useAreaService() {
1713
1727
  }
1714
1728
  }
1715
1729
  if (foundUnit) {
1716
- areaUnits.push({
1717
- unit: foundUnit._id,
1718
- name: foundUnit.name
1719
- });
1730
+ areaUnits.push(toAreaUnit(foundUnit));
1720
1731
  }
1721
1732
  }
1722
1733
  if (areaUnits.length > 0) {
@@ -1813,7 +1824,7 @@ function useAreaService() {
1813
1824
  }
1814
1825
 
1815
1826
  // src/controllers/hygiene-area.controller.ts
1816
- import Joi4 from "joi";
1827
+ import Joi5 from "joi";
1817
1828
  import { BadRequestError as BadRequestError8, logger as logger9 } from "@7365admin1/node-server-utils";
1818
1829
  import { AppServiceType as AppServiceType4 } from "@7365admin1/core";
1819
1830
 
@@ -1874,13 +1885,13 @@ function useAreaController() {
1874
1885
  }
1875
1886
  async function getAreas(req, res, next) {
1876
1887
  const query = { ...req.query, ...req.params };
1877
- const validation = Joi4.object({
1878
- page: Joi4.number().min(1).optional().allow("", null),
1879
- limit: Joi4.number().min(1).optional().allow("", null),
1880
- search: Joi4.string().optional().allow("", null),
1881
- type: Joi4.string().valid("all", ...allowedTypes).optional().allow("", null),
1882
- site: Joi4.string().hex().required(),
1883
- serviceType: Joi4.string().valid(...Object.values(AppServiceType4)).required()
1888
+ const validation = Joi5.object({
1889
+ page: Joi5.number().min(1).optional().allow("", null),
1890
+ limit: Joi5.number().min(1).optional().allow("", null),
1891
+ search: Joi5.string().optional().allow("", null),
1892
+ type: Joi5.string().valid("all", ...allowedTypes).optional().allow("", null),
1893
+ site: Joi5.string().hex().required(),
1894
+ serviceType: Joi5.string().valid(...Object.values(AppServiceType4)).required()
1884
1895
  });
1885
1896
  const { error } = validation.validate(query);
1886
1897
  if (error) {
@@ -1911,7 +1922,7 @@ function useAreaController() {
1911
1922
  }
1912
1923
  }
1913
1924
  async function getAreaById(req, res, next) {
1914
- const validation = Joi4.string().hex().required();
1925
+ const validation = Joi5.string().hex().required();
1915
1926
  const _id = req.params.id;
1916
1927
  const { error, value } = validation.validate(_id);
1917
1928
  if (error) {
@@ -1931,15 +1942,15 @@ function useAreaController() {
1931
1942
  }
1932
1943
  async function updateArea(req, res, next) {
1933
1944
  const payload = { id: req.params.id, ...req.body };
1934
- const schema = Joi4.object({
1935
- id: Joi4.string().hex().required(),
1936
- name: Joi4.string().optional().allow("", null),
1937
- type: Joi4.string().optional().allow("", null, ...allowedTypes),
1938
- set: Joi4.number().min(0).optional(),
1939
- units: Joi4.array().items(
1940
- Joi4.object({
1941
- unit: Joi4.string().hex().required(),
1942
- name: Joi4.string().required()
1945
+ const schema = Joi5.object({
1946
+ id: Joi5.string().hex().required(),
1947
+ name: Joi5.string().optional().allow("", null),
1948
+ type: Joi5.string().optional().allow("", null, ...allowedTypes),
1949
+ set: Joi5.number().min(0).optional(),
1950
+ units: Joi5.array().items(
1951
+ Joi5.object({
1952
+ unit: Joi5.string().hex().required(),
1953
+ name: Joi5.string().required()
1943
1954
  }).required()
1944
1955
  ).min(1).unique("unit", { ignoreUndefined: true }).optional().messages({
1945
1956
  "array.unique": "Duplicate area units are not allowed"
@@ -1964,7 +1975,7 @@ function useAreaController() {
1964
1975
  }
1965
1976
  async function deleteArea(req, res, next) {
1966
1977
  const id = req.params.id;
1967
- const validation = Joi4.string().hex().required();
1978
+ const validation = Joi5.string().hex().required();
1968
1979
  const { error, value } = validation.validate(id);
1969
1980
  if (error) {
1970
1981
  logger9.log({ level: "error", message: error.message });
@@ -1987,9 +1998,9 @@ function useAreaController() {
1987
1998
  return;
1988
1999
  }
1989
2000
  const query = { ...req.query, ...req.params };
1990
- const validation = Joi4.object({
1991
- site: Joi4.string().hex().required(),
1992
- serviceType: Joi4.string().valid(...Object.values(AppServiceType4)).required()
2001
+ const validation = Joi5.object({
2002
+ site: Joi5.string().hex().required(),
2003
+ serviceType: Joi5.string().valid(...Object.values(AppServiceType4)).required()
1993
2004
  });
1994
2005
  const { error, value } = validation.validate(query);
1995
2006
  if (error) {
@@ -2010,9 +2021,9 @@ function useAreaController() {
2010
2021
  }
2011
2022
  async function exportAreas(req, res, next) {
2012
2023
  const query = { ...req.query, ...req.params };
2013
- const validation = Joi4.object({
2014
- site: Joi4.string().hex().required(),
2015
- serviceType: Joi4.string().valid(...Object.values(AppServiceType4)).required()
2024
+ const validation = Joi5.object({
2025
+ site: Joi5.string().hex().required(),
2026
+ serviceType: Joi5.string().valid(...Object.values(AppServiceType4)).required()
2016
2027
  });
2017
2028
  const { error, value } = validation.validate(query);
2018
2029
  if (error) {
@@ -2299,7 +2310,7 @@ function useUnitService() {
2299
2310
  }
2300
2311
 
2301
2312
  // src/controllers/hygiene-unit.controller.ts
2302
- import Joi5 from "joi";
2313
+ import Joi6 from "joi";
2303
2314
  import { BadRequestError as BadRequestError10, logger as logger12 } from "@7365admin1/node-server-utils";
2304
2315
  import { AppServiceType as AppServiceType5 } from "@7365admin1/core";
2305
2316
  function useUnitController() {
@@ -2330,12 +2341,12 @@ function useUnitController() {
2330
2341
  }
2331
2342
  async function getUnits(req, res, next) {
2332
2343
  const query = { ...req.query, ...req.params };
2333
- const validation = Joi5.object({
2334
- page: Joi5.number().min(1).optional().allow("", null),
2335
- limit: Joi5.number().min(1).optional().allow("", null),
2336
- search: Joi5.string().optional().allow("", null),
2337
- site: Joi5.string().hex().required(),
2338
- serviceType: Joi5.string().valid(...Object.values(AppServiceType5)).required()
2344
+ const validation = Joi6.object({
2345
+ page: Joi6.number().min(1).optional().allow("", null),
2346
+ limit: Joi6.number().min(1).optional().allow("", null),
2347
+ search: Joi6.string().optional().allow("", null),
2348
+ site: Joi6.string().hex().required(),
2349
+ serviceType: Joi6.string().valid(...Object.values(AppServiceType5)).required()
2339
2350
  });
2340
2351
  const { error } = validation.validate(query);
2341
2352
  if (error) {
@@ -2366,9 +2377,9 @@ function useUnitController() {
2366
2377
  }
2367
2378
  async function updateUnit(req, res, next) {
2368
2379
  const payload = { id: req.params.id, ...req.body };
2369
- const validation = Joi5.object({
2370
- id: Joi5.string().hex().required(),
2371
- name: Joi5.string().required()
2380
+ const validation = Joi6.object({
2381
+ id: Joi6.string().hex().required(),
2382
+ name: Joi6.string().required()
2372
2383
  });
2373
2384
  const { error } = validation.validate(payload);
2374
2385
  if (error) {
@@ -2389,8 +2400,8 @@ function useUnitController() {
2389
2400
  }
2390
2401
  async function deleteUnit(req, res, next) {
2391
2402
  const id = req.params.id;
2392
- const validation = Joi5.object({
2393
- id: Joi5.string().hex().required()
2403
+ const validation = Joi6.object({
2404
+ id: Joi6.string().hex().required()
2394
2405
  });
2395
2406
  const { error, value } = validation.validate({ id });
2396
2407
  if (error) {
@@ -2414,9 +2425,9 @@ function useUnitController() {
2414
2425
  return;
2415
2426
  }
2416
2427
  const query = { ...req.query, ...req.params };
2417
- const validation = Joi5.object({
2418
- site: Joi5.string().hex().required(),
2419
- serviceType: Joi5.string().valid(...Object.values(AppServiceType5)).required()
2428
+ const validation = Joi6.object({
2429
+ site: Joi6.string().hex().required(),
2430
+ serviceType: Joi6.string().valid(...Object.values(AppServiceType5)).required()
2420
2431
  });
2421
2432
  const { error, value } = validation.validate(query);
2422
2433
  if (error) {
@@ -2437,9 +2448,9 @@ function useUnitController() {
2437
2448
  }
2438
2449
  async function exportUnits(req, res, next) {
2439
2450
  const query = { ...req.query, ...req.params };
2440
- const validation = Joi5.object({
2441
- site: Joi5.string().hex().required(),
2442
- serviceType: Joi5.string().valid(...Object.values(AppServiceType5)).required()
2451
+ const validation = Joi6.object({
2452
+ site: Joi6.string().hex().required(),
2453
+ serviceType: Joi6.string().valid(...Object.values(AppServiceType5)).required()
2443
2454
  });
2444
2455
  const { error, value } = validation.validate(query);
2445
2456
  if (error) {
@@ -2482,15 +2493,15 @@ function useUnitController() {
2482
2493
  }
2483
2494
 
2484
2495
  // src/models/hygiene-parent-checklist.model.ts
2485
- import Joi6 from "joi";
2496
+ import Joi7 from "joi";
2486
2497
  import { ObjectId as ObjectId6 } from "mongodb";
2487
2498
  import { BadRequestError as BadRequestError11, logger as logger13 } from "@7365admin1/node-server-utils";
2488
2499
  import { AppServiceType as AppServiceType6 } from "@7365admin1/core";
2489
- var parentChecklistSchema = Joi6.object({
2490
- createdAt: Joi6.alternatives().try(Joi6.date(), Joi6.string()).optional().allow("", null),
2491
- site: Joi6.string().hex().required(),
2492
- serviceType: Joi6.string().valid(...Object.values(AppServiceType6)).required(),
2493
- dateStr: Joi6.string().optional().allow("", null)
2500
+ var parentChecklistSchema = Joi7.object({
2501
+ createdAt: Joi7.alternatives().try(Joi7.date(), Joi7.string()).optional().allow("", null),
2502
+ site: Joi7.string().hex().required(),
2503
+ serviceType: Joi7.string().valid(...Object.values(AppServiceType6)).required(),
2504
+ dateStr: Joi7.string().optional().allow("", null)
2494
2505
  });
2495
2506
  function MParentChecklist(value) {
2496
2507
  const { error } = parentChecklistSchema.validate(value);
@@ -3071,7 +3082,7 @@ function useParentChecklistRepo() {
3071
3082
  }
3072
3083
 
3073
3084
  // src/controllers/hygiene-parent-checklist.controller.ts
3074
- import Joi7 from "joi";
3085
+ import Joi8 from "joi";
3075
3086
  import { BadRequestError as BadRequestError13, logger as logger15 } from "@7365admin1/node-server-utils";
3076
3087
  import { AppServiceType as AppServiceType8 } from "@7365admin1/core";
3077
3088
  function useParentChecklistController() {
@@ -3081,10 +3092,10 @@ function useParentChecklistController() {
3081
3092
  } = useParentChecklistRepo();
3082
3093
  async function createParentChecklist(req, res, next) {
3083
3094
  const payload = req.body;
3084
- const validation = Joi7.object({
3085
- createdAt: Joi7.alternatives().try(Joi7.date(), Joi7.string()).optional().allow("", null),
3086
- site: Joi7.string().hex().required(),
3087
- serviceType: Joi7.string().valid(...Object.values(AppServiceType8)).optional().allow("", null)
3095
+ const validation = Joi8.object({
3096
+ createdAt: Joi8.alternatives().try(Joi8.date(), Joi8.string()).optional().allow("", null),
3097
+ site: Joi8.string().hex().required(),
3098
+ serviceType: Joi8.string().valid(...Object.values(AppServiceType8)).optional().allow("", null)
3088
3099
  });
3089
3100
  const { error } = validation.validate(payload);
3090
3101
  if (error) {
@@ -3104,15 +3115,15 @@ function useParentChecklistController() {
3104
3115
  }
3105
3116
  async function getAllParentChecklist(req, res, next) {
3106
3117
  const query = { ...req.query, ...req.params };
3107
- const validation = Joi7.object({
3108
- page: Joi7.number().min(1).optional().allow("", null),
3109
- limit: Joi7.number().min(1).optional().allow("", null),
3110
- search: Joi7.string().optional().allow("", null),
3111
- site: Joi7.string().hex().required(),
3112
- serviceType: Joi7.string().valid(...Object.values(AppServiceType8)).required(),
3113
- startDate: Joi7.alternatives().try(Joi7.date(), Joi7.string()).optional().allow("", null),
3114
- endDate: Joi7.alternatives().try(Joi7.date(), Joi7.string()).optional().allow("", null),
3115
- status: Joi7.string().valid(...allowedStatus, "all").optional().allow("", null)
3118
+ const validation = Joi8.object({
3119
+ page: Joi8.number().min(1).optional().allow("", null),
3120
+ limit: Joi8.number().min(1).optional().allow("", null),
3121
+ search: Joi8.string().optional().allow("", null),
3122
+ site: Joi8.string().hex().required(),
3123
+ serviceType: Joi8.string().valid(...Object.values(AppServiceType8)).required(),
3124
+ startDate: Joi8.alternatives().try(Joi8.date(), Joi8.string()).optional().allow("", null),
3125
+ endDate: Joi8.alternatives().try(Joi8.date(), Joi8.string()).optional().allow("", null),
3126
+ status: Joi8.string().valid(...allowedStatus, "all").optional().allow("", null)
3116
3127
  });
3117
3128
  const { error } = validation.validate(query);
3118
3129
  if (error) {
@@ -3154,28 +3165,28 @@ function useParentChecklistController() {
3154
3165
  }
3155
3166
 
3156
3167
  // src/models/hygiene-area-checklist.model.ts
3157
- import Joi8 from "joi";
3168
+ import Joi9 from "joi";
3158
3169
  import { ObjectId as ObjectId8 } from "mongodb";
3159
3170
  import { BadRequestError as BadRequestError14, logger as logger16 } from "@7365admin1/node-server-utils";
3160
3171
  import { AppServiceType as AppServiceType9 } from "@7365admin1/core";
3161
3172
  var allowedChecklistStatus = ["open", "completed", "closed"];
3162
- var areaChecklistSchema = Joi8.object({
3163
- schedule: Joi8.string().hex().required(),
3164
- serviceType: Joi8.string().valid(...Object.values(AppServiceType9)).required(),
3165
- area: Joi8.string().hex().required(),
3166
- name: Joi8.string().required(),
3167
- type: Joi8.string().valid(...allowedTypes).required(),
3168
- checklist: Joi8.array().items(
3169
- Joi8.object({
3170
- set: Joi8.number().min(0).required(),
3171
- units: Joi8.array().items(
3172
- Joi8.object({
3173
- unit: Joi8.string().hex().required(),
3174
- name: Joi8.string().required()
3173
+ var areaChecklistSchema = Joi9.object({
3174
+ schedule: Joi9.string().hex().required(),
3175
+ serviceType: Joi9.string().valid(...Object.values(AppServiceType9)).required(),
3176
+ area: Joi9.string().hex().required(),
3177
+ name: Joi9.string().required(),
3178
+ type: Joi9.string().valid(...allowedTypes).required(),
3179
+ checklist: Joi9.array().items(
3180
+ Joi9.object({
3181
+ set: Joi9.number().min(0).required(),
3182
+ units: Joi9.array().items(
3183
+ Joi9.object({
3184
+ unit: Joi9.string().hex().required(),
3185
+ name: Joi9.string().required()
3175
3186
  }).required()
3176
3187
  ).min(1).required(),
3177
- isScheduleTask: Joi8.boolean().optional().default(false),
3178
- scheduleTaskId: Joi8.string().hex().optional().allow(null)
3188
+ isScheduleTask: Joi9.boolean().optional().default(false),
3189
+ scheduleTaskId: Joi9.string().hex().optional().allow(null)
3179
3190
  }).required()
3180
3191
  ).optional().default([])
3181
3192
  });
@@ -4425,6 +4436,265 @@ function useAreaChecklistRepo() {
4425
4436
  };
4426
4437
  }
4427
4438
 
4439
+ // src/utils/schedule-task-rules.util.ts
4440
+ import Joi10 from "joi";
4441
+ var REPEATS = ["none", "daily", "weekdays", "weekly", "monthly"];
4442
+ var SGT_MS = 8 * 60 * 60 * 1e3;
4443
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4444
+ var TIME_RE = /^([0-1]\d|2[0-3]):([0-5]\d)$/;
4445
+ var pad = (n) => String(n).padStart(2, "0");
4446
+ function sgtParts(now) {
4447
+ const s = new Date(now.getTime() + SGT_MS);
4448
+ const date = `${s.getUTCFullYear()}-${pad(s.getUTCMonth() + 1)}-${pad(s.getUTCDate())}`;
4449
+ return { date, minutes: s.getUTCHours() * 60 + s.getUTCMinutes(), weekday: s.getUTCDay() };
4450
+ }
4451
+ function isRealDate(value) {
4452
+ if (typeof value !== "string" || !DATE_RE.test(value))
4453
+ return false;
4454
+ const [y, m, d] = value.split("-").map(Number);
4455
+ const t = new Date(Date.UTC(y, m - 1, d));
4456
+ return t.getUTCFullYear() === y && t.getUTCMonth() === m - 1 && t.getUTCDate() === d;
4457
+ }
4458
+ var toMinutes = (time) => {
4459
+ const [h, m] = time.split(":").map(Number);
4460
+ return h * 60 + m;
4461
+ };
4462
+ var weekdayOf = (date) => {
4463
+ const [y, m, d] = date.split("-").map(Number);
4464
+ return new Date(Date.UTC(y, m - 1, d)).getUTCDay();
4465
+ };
4466
+ var daysInMonth = (date) => {
4467
+ const [y, m] = date.split("-").map(Number);
4468
+ return new Date(Date.UTC(y, m, 0)).getUTCDate();
4469
+ };
4470
+ var addDays = (date, n) => {
4471
+ const [y, m, d] = date.split("-").map(Number);
4472
+ const t = new Date(Date.UTC(y, m - 1, d + n));
4473
+ return `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}-${pad(t.getUTCDate())}`;
4474
+ };
4475
+ var isRepeating = (task) => Boolean(task.repeat) && task.repeat !== "none";
4476
+ function occursOn(task, date) {
4477
+ if (task.endDate && date > task.endDate)
4478
+ return false;
4479
+ if (!isRepeating(task))
4480
+ return Array.isArray(task.dates) && task.dates.includes(date);
4481
+ if (task.startDate && date < task.startDate)
4482
+ return false;
4483
+ const weekday = weekdayOf(date);
4484
+ switch (task.repeat) {
4485
+ case "daily":
4486
+ return true;
4487
+ case "weekdays":
4488
+ return weekday >= 1 && weekday <= 5;
4489
+ case "weekly":
4490
+ return Array.isArray(task.daysOfWeek) && task.daysOfWeek.includes(weekday);
4491
+ case "monthly": {
4492
+ const wanted = task.dayOfMonth ?? (task.startDate ? Number(task.startDate.slice(8, 10)) : 1);
4493
+ return Number(date.slice(8, 10)) === Math.min(wanted, daysInMonth(date));
4494
+ }
4495
+ default:
4496
+ return false;
4497
+ }
4498
+ }
4499
+ function savedTodayAt(task, today) {
4500
+ const stamp = task.updatedAt || task.createdAt;
4501
+ if (!stamp)
4502
+ return null;
4503
+ const saved = new Date(stamp);
4504
+ if (Number.isNaN(saved.getTime()))
4505
+ return null;
4506
+ const p = sgtParts(saved);
4507
+ return p.date === today ? p.minutes : null;
4508
+ }
4509
+ function shouldRunNow(task, now = /* @__PURE__ */ new Date()) {
4510
+ if (task.paused || !TIME_RE.test(task.time ?? ""))
4511
+ return false;
4512
+ const { date, minutes } = sgtParts(now);
4513
+ if (task.lastRunDate === date || !occursOn(task, date))
4514
+ return false;
4515
+ const due = toMinutes(task.time);
4516
+ if (minutes < due)
4517
+ return false;
4518
+ const saved = savedTodayAt(task, date);
4519
+ return !(saved !== null && saved >= due);
4520
+ }
4521
+ var iso = (date, time) => `${date}T${time}:00+08:00`;
4522
+ function upcoming(task, now) {
4523
+ if (typeof task.time !== "string" || !TIME_RE.test(task.time))
4524
+ return null;
4525
+ const { date: today, minutes } = sgtParts(now);
4526
+ const due = toMinutes(task.time);
4527
+ if (occursOn(task, today) && (minutes < due || shouldRunNow({ ...task, paused: false }, now))) {
4528
+ return iso(today, task.time);
4529
+ }
4530
+ if (!isRepeating(task)) {
4531
+ const later = (task.dates ?? []).filter((d) => d > today && (!task.endDate || d <= task.endDate)).sort();
4532
+ return later.length ? iso(later[0], task.time) : null;
4533
+ }
4534
+ const from = task.startDate && task.startDate > today ? task.startDate : today;
4535
+ for (let i = from === today ? 1 : 0; i <= 400; i++) {
4536
+ const d = addDays(from, i);
4537
+ if (task.endDate && d > task.endDate)
4538
+ return null;
4539
+ if (occursOn(task, d))
4540
+ return iso(d, task.time);
4541
+ }
4542
+ return null;
4543
+ }
4544
+ function nextRun(task, now = /* @__PURE__ */ new Date()) {
4545
+ return task.paused ? null : upcoming(task, now);
4546
+ }
4547
+ function scheduleState(task, now = /* @__PURE__ */ new Date()) {
4548
+ if (task.paused)
4549
+ return "paused";
4550
+ return upcoming(task, now) ? "active" : "ended";
4551
+ }
4552
+ function lastRun(task) {
4553
+ return task.lastRunDate && TIME_RE.test(task.time ?? "") ? iso(task.lastRunDate, task.time) : null;
4554
+ }
4555
+ function scheduleDisplay(task, now = /* @__PURE__ */ new Date()) {
4556
+ try {
4557
+ return {
4558
+ nextRun: nextRun(task, now),
4559
+ lastRun: lastRun(task),
4560
+ scheduleStatus: scheduleState(task, now)
4561
+ };
4562
+ } catch {
4563
+ return { nextRun: null, lastRun: null, scheduleStatus: null };
4564
+ }
4565
+ }
4566
+ function validateScheduleRules(task, now = /* @__PURE__ */ new Date()) {
4567
+ const repeating = isRepeating(task);
4568
+ const { date: today, minutes } = sgtParts(now);
4569
+ if (repeating) {
4570
+ if (!task.startDate)
4571
+ return "Please choose a start date for a repeating schedule.";
4572
+ if (task.repeat === "weekly" && !(task.daysOfWeek && task.daysOfWeek.length)) {
4573
+ return "Pick at least one weekday for a weekly repeat.";
4574
+ }
4575
+ }
4576
+ const start = repeating ? task.startDate : [...task.dates ?? []].sort()[0];
4577
+ if (task.endDate && start && start > task.endDate) {
4578
+ return "The end date must be on or after the start date.";
4579
+ }
4580
+ if (task.endDate && task.endDate < today)
4581
+ return "The end date has already passed.";
4582
+ if (!repeating) {
4583
+ const dates = (task.dates ?? []).filter((d) => !task.endDate || d <= task.endDate);
4584
+ if (dates.length && dates.every((d) => d < today)) {
4585
+ return "All the dates you picked have already passed. Pick today or a later date.";
4586
+ }
4587
+ if (dates.length && dates.every((d) => d <= today) && dates.includes(today) && TIME_RE.test(task.time) && toMinutes(task.time) <= minutes) {
4588
+ return "That time has already passed today. Pick a later time or another date.";
4589
+ }
4590
+ }
4591
+ if (!upcoming({ ...task, paused: false, updatedAt: now }, now)) {
4592
+ return "This schedule would never run. Check the dates, end date and time.";
4593
+ }
4594
+ return null;
4595
+ }
4596
+ var scheduleDate = Joi10.string().pattern(DATE_RE).custom((value, helpers) => isRealDate(value) ? value : helpers.error("date.invalid")).messages({
4597
+ "string.pattern.base": "Dates must look like 2026-10-21.",
4598
+ "string.empty": "Please choose a date.",
4599
+ "date.invalid": "That date does not exist. Dates must look like 2026-10-21.",
4600
+ "string.base": "Dates must look like 2026-10-21."
4601
+ });
4602
+ var REPEATING = Joi10.valid("daily", "weekdays", "weekly", "monthly").required();
4603
+ var scheduleTaskFields = {
4604
+ title: Joi10.string().trim().required().messages({
4605
+ "any.required": "Please enter a title.",
4606
+ "string.empty": "Please enter a title.",
4607
+ "string.base": "Please enter a title."
4608
+ }),
4609
+ time: Joi10.string().pattern(TIME_RE).required().messages({
4610
+ "any.required": "Please choose a time.",
4611
+ "string.empty": "Please choose a time.",
4612
+ "string.pattern.base": "Time must be in 24-hour format like 08:30.",
4613
+ "string.base": "Time must be in 24-hour format like 08:30."
4614
+ }),
4615
+ dates: Joi10.when("repeat", {
4616
+ is: REPEATING,
4617
+ then: Joi10.array().items(scheduleDate).optional().default([]),
4618
+ otherwise: Joi10.array().min(1).items(scheduleDate).required()
4619
+ }).messages({
4620
+ "any.required": "Pick at least one date, or choose a repeat.",
4621
+ "array.min": "Pick at least one date, or choose a repeat.",
4622
+ "array.base": "Dates must be a list of dates."
4623
+ }),
4624
+ description: Joi10.string().optional().allow("", null),
4625
+ areas: Joi10.array().min(1).items(
4626
+ Joi10.object({
4627
+ name: Joi10.string().required(),
4628
+ value: Joi10.any().required()
4629
+ })
4630
+ ).required().messages({
4631
+ "any.required": "Pick at least one area.",
4632
+ "array.min": "Pick at least one area.",
4633
+ "array.base": "Pick at least one area."
4634
+ }),
4635
+ repeat: Joi10.string().valid(...REPEATS).optional().allow("", null).messages({ "any.only": "Repeat must be one of: none, daily, weekdays, weekly, monthly." }),
4636
+ daysOfWeek: Joi10.array().items(Joi10.number().integer().min(0).max(6)).unique().optional().allow(null).messages({
4637
+ "number.base": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4638
+ "number.min": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4639
+ "number.max": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4640
+ "number.integer": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4641
+ "array.unique": "Each weekday can only be picked once."
4642
+ }),
4643
+ dayOfMonth: Joi10.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." }),
4644
+ startDate: scheduleDate.optional().allow("", null),
4645
+ endDate: scheduleDate.optional().allow("", null),
4646
+ paused: Joi10.boolean().optional().allow(null).messages({ "boolean.base": "Paused must be true or false." })
4647
+ };
4648
+ var OPTIONAL_KEYS = ["repeat", "daysOfWeek", "dayOfMonth", "startDate", "endDate", "paused"];
4649
+ var isBlank = (v) => v === "" || v === null;
4650
+ function cleanScheduleInput(value) {
4651
+ const out = { ...value };
4652
+ for (const key of OPTIONAL_KEYS)
4653
+ if (isBlank(out[key]))
4654
+ delete out[key];
4655
+ if (out.repeat === "none") {
4656
+ delete out.daysOfWeek;
4657
+ delete out.dayOfMonth;
4658
+ delete out.startDate;
4659
+ }
4660
+ return out;
4661
+ }
4662
+ var TIMING_KEYS = ["time", "dates", "repeat", "daysOfWeek", "dayOfMonth", "startDate", "endDate"];
4663
+ function splitScheduleUpdate(payload) {
4664
+ const set = {};
4665
+ const unset = [];
4666
+ for (const [key, value] of Object.entries(payload)) {
4667
+ if (OPTIONAL_KEYS.includes(key) && isBlank(value))
4668
+ unset.push(key);
4669
+ else if (value !== void 0)
4670
+ set[key] = value;
4671
+ }
4672
+ if (set.repeat === "none") {
4673
+ for (const key of ["daysOfWeek", "dayOfMonth", "startDate"]) {
4674
+ if (!unset.includes(key))
4675
+ unset.push(key);
4676
+ delete set[key];
4677
+ }
4678
+ }
4679
+ return { set, unset };
4680
+ }
4681
+ function regularSetsMissing(checklist, setCount) {
4682
+ const regular = (checklist ?? []).filter((c) => c.isScheduleTask !== true).length;
4683
+ return Math.max(0, setCount - regular);
4684
+ }
4685
+ var normTiming = (t) => ({
4686
+ time: t.time ?? null,
4687
+ dates: [...Array.isArray(t.dates) ? t.dates : []].sort(),
4688
+ repeat: !t.repeat || t.repeat === "none" ? "none" : t.repeat,
4689
+ daysOfWeek: [...Array.isArray(t.daysOfWeek) ? t.daysOfWeek : []].sort(),
4690
+ dayOfMonth: t.dayOfMonth ?? null,
4691
+ startDate: t.startDate || null,
4692
+ endDate: t.endDate || null
4693
+ });
4694
+ function timingChanged(stored, after) {
4695
+ return JSON.stringify(normTiming(stored)) !== JSON.stringify(normTiming(after));
4696
+ }
4697
+
4428
4698
  // src/services/hygiene-area-checklist.service.ts
4429
4699
  function useAreaChecklistService() {
4430
4700
  const { getAreasForChecklist } = useAreaRepo();
@@ -4492,10 +4762,10 @@ function useAreaChecklistService() {
4492
4762
  `Area checklist for area ${area.name} exists but has no auto-gen sets. Inserting auto-gen sets.`
4493
4763
  );
4494
4764
  const setCount2 = Number(area.set) || 1;
4495
- const totalExistingSets = existing.checklist?.length ?? 0;
4496
- if (totalExistingSets >= setCount2) {
4765
+ const setsToAdd = regularSetsMissing(existing.checklist, setCount2);
4766
+ if (setsToAdd <= 0) {
4497
4767
  logger18.info(
4498
- `Area checklist for area ${area.name} already has ${totalExistingSets} set(s) (configured: ${setCount2}). Skipping auto-gen sets.`
4768
+ `Area checklist for area ${area.name} already has its ${setCount2} regular set(s). Skipping auto-gen sets.`
4499
4769
  );
4500
4770
  return existing._id;
4501
4771
  }
@@ -4503,7 +4773,6 @@ function useAreaChecklistService() {
4503
4773
  (max, c) => Math.max(max, c.set ?? 0),
4504
4774
  0
4505
4775
  ) ?? 0;
4506
- const setsToAdd = setCount2 - totalExistingSets;
4507
4776
  const autoGenSets = Array.from(
4508
4777
  { length: setsToAdd },
4509
4778
  (_, index) => ({
@@ -4664,7 +4933,7 @@ function useAreaChecklistService() {
4664
4933
  }
4665
4934
 
4666
4935
  // src/controllers/hygiene-area-checklist.controller.ts
4667
- import Joi9 from "joi";
4936
+ import Joi11 from "joi";
4668
4937
  import {
4669
4938
  BadRequestError as BadRequestError16,
4670
4939
  InternalServerError as InternalServerError7,
@@ -4909,9 +5178,9 @@ function useAreaChecklistController() {
4909
5178
  site: req.params.site,
4910
5179
  schedule: req.params.schedule
4911
5180
  };
4912
- const validation = Joi9.object({
4913
- site: Joi9.string().hex().required(),
4914
- schedule: Joi9.string().hex().required()
5181
+ const validation = Joi11.object({
5182
+ site: Joi11.string().hex().required(),
5183
+ schedule: Joi11.string().hex().required()
4915
5184
  });
4916
5185
  const { error, value } = validation.validate(payload);
4917
5186
  if (error) {
@@ -4931,14 +5200,14 @@ function useAreaChecklistController() {
4931
5200
  }
4932
5201
  async function getAllAreaChecklist(req, res, next) {
4933
5202
  const query = { ...req.query, ...req.params };
4934
- const validation = Joi9.object({
4935
- page: Joi9.number().min(1).optional().allow("", null),
4936
- limit: Joi9.number().min(1).optional().allow("", null),
4937
- search: Joi9.string().optional().allow("", null),
4938
- type: Joi9.string().valid(...allowedTypes, "all").optional().allow("", null),
4939
- status: Joi9.string().valid(...allowedStatus, "all").optional().allow("", null),
4940
- schedule: Joi9.string().hex().required(),
4941
- serviceType: Joi9.string().valid(...Object.values(AppServiceType10)).required()
5203
+ const validation = Joi11.object({
5204
+ page: Joi11.number().min(1).optional().allow("", null),
5205
+ limit: Joi11.number().min(1).optional().allow("", null),
5206
+ search: Joi11.string().optional().allow("", null),
5207
+ type: Joi11.string().valid(...allowedTypes, "all").optional().allow("", null),
5208
+ status: Joi11.string().valid(...allowedStatus, "all").optional().allow("", null),
5209
+ schedule: Joi11.string().hex().required(),
5210
+ serviceType: Joi11.string().valid(...Object.values(AppServiceType10)).required()
4942
5211
  });
4943
5212
  const { error } = validation.validate(query);
4944
5213
  if (error) {
@@ -4973,14 +5242,14 @@ function useAreaChecklistController() {
4973
5242
  }
4974
5243
  async function getAreaChecklistHistory(req, res, next) {
4975
5244
  const query = { ...req.query, ...req.params };
4976
- const validation = Joi9.object({
4977
- page: Joi9.number().min(1).optional().allow("", null),
4978
- limit: Joi9.number().min(1).optional().allow("", null),
4979
- search: Joi9.string().optional().allow("", null),
4980
- type: Joi9.string().optional().allow("", ...allowedTypes),
4981
- schedule: Joi9.string().hex().required(),
4982
- status: Joi9.string().allow("", null, ...allowedStatus),
4983
- createdAt: Joi9.alternatives().try(Joi9.date(), Joi9.string()).optional().allow("", null)
5245
+ const validation = Joi11.object({
5246
+ page: Joi11.number().min(1).optional().allow("", null),
5247
+ limit: Joi11.number().min(1).optional().allow("", null),
5248
+ search: Joi11.string().optional().allow("", null),
5249
+ type: Joi11.string().optional().allow("", ...allowedTypes),
5250
+ schedule: Joi11.string().hex().required(),
5251
+ status: Joi11.string().allow("", null, ...allowedStatus),
5252
+ createdAt: Joi11.alternatives().try(Joi11.date(), Joi11.string()).optional().allow("", null)
4984
5253
  });
4985
5254
  const { error } = validation.validate(query);
4986
5255
  if (error) {
@@ -5014,7 +5283,7 @@ function useAreaChecklistController() {
5014
5283
  }
5015
5284
  }
5016
5285
  async function getAreaChecklistHistoryDetails(req, res, next) {
5017
- const validation = Joi9.string().hex().required();
5286
+ const validation = Joi11.string().hex().required();
5018
5287
  const _id = req.params.id;
5019
5288
  const { error, value } = validation.validate(_id);
5020
5289
  if (error) {
@@ -5034,12 +5303,12 @@ function useAreaChecklistController() {
5034
5303
  }
5035
5304
  async function getAreaChecklistUnits(req, res, next) {
5036
5305
  const query = { ...req.query, ...req.params };
5037
- const validation = Joi9.object({
5038
- page: Joi9.number().min(1).optional().allow("", null),
5039
- limit: Joi9.number().min(1).optional().allow("", null),
5040
- search: Joi9.string().optional().allow("", null),
5041
- serviceType: Joi9.string().valid(...Object.values(AppServiceType10)).required(),
5042
- id: Joi9.string().hex().required()
5306
+ const validation = Joi11.object({
5307
+ page: Joi11.number().min(1).optional().allow("", null),
5308
+ limit: Joi11.number().min(1).optional().allow("", null),
5309
+ search: Joi11.string().optional().allow("", null),
5310
+ serviceType: Joi11.string().valid(...Object.values(AppServiceType10)).required(),
5311
+ id: Joi11.string().hex().required()
5043
5312
  });
5044
5313
  const { error } = validation.validate(query);
5045
5314
  if (error) {
@@ -5089,21 +5358,21 @@ function useAreaChecklistController() {
5089
5358
  ...decisionValues,
5090
5359
  completedBy
5091
5360
  };
5092
- const validation = Joi9.object({
5093
- id: Joi9.string().hex().required(),
5094
- set: Joi9.number().integer().min(1).required(),
5095
- unit: Joi9.string().hex().required(),
5096
- decision: Joi9.string().valid("approve", "reject").required(),
5097
- approve: Joi9.boolean().optional(),
5098
- reject: Joi9.boolean().optional(),
5099
- remarks: Joi9.string().optional().allow("", null),
5361
+ const validation = Joi11.object({
5362
+ id: Joi11.string().hex().required(),
5363
+ set: Joi11.number().integer().min(1).required(),
5364
+ unit: Joi11.string().hex().required(),
5365
+ decision: Joi11.string().valid("approve", "reject").required(),
5366
+ approve: Joi11.boolean().optional(),
5367
+ reject: Joi11.boolean().optional(),
5368
+ remarks: Joi11.string().optional().allow("", null),
5100
5369
  /*
5101
5370
  * Shape only. WHETHER a photo is required depends on the set's current
5102
5371
  * state in the database, which Joi cannot see — see the check below.
5103
5372
  * Each entry must still be a real id: a blank string carries no evidence.
5104
5373
  */
5105
- attachment: Joi9.array().items(Joi9.string().trim().min(1)).optional(),
5106
- completedBy: Joi9.string().hex().required()
5374
+ attachment: Joi11.array().items(Joi11.string().trim().min(1)).optional(),
5375
+ completedBy: Joi11.string().hex().required()
5107
5376
  });
5108
5377
  const { error } = validation.validate(payload);
5109
5378
  if (error) {
@@ -5140,7 +5409,7 @@ function useAreaChecklistController() {
5140
5409
  }
5141
5410
  }
5142
5411
  async function downloadChecklistPdf(req, res, next) {
5143
- const validation = Joi9.string().hex().required();
5412
+ const validation = Joi11.string().hex().required();
5144
5413
  const schedule = req.params.schedule;
5145
5414
  const { error, value } = validation.validate(schedule);
5146
5415
  if (error) {
@@ -5186,16 +5455,16 @@ function useAreaChecklistController() {
5186
5455
  }
5187
5456
 
5188
5457
  // src/models/hygiene-supply.model.ts
5189
- import Joi10 from "joi";
5458
+ import Joi12 from "joi";
5190
5459
  import { ObjectId as ObjectId12 } from "mongodb";
5191
5460
  import { BadRequestError as BadRequestError17, logger as logger21 } from "@7365admin1/node-server-utils";
5192
5461
  import { AppServiceType as AppServiceType11 } from "@7365admin1/core";
5193
- var supplySchema = Joi10.object({
5194
- site: Joi10.string().hex().required(),
5195
- serviceType: Joi10.string().valid(...Object.values(AppServiceType11)).required(),
5196
- name: Joi10.string().required(),
5197
- unitOfMeasurement: Joi10.string().required(),
5198
- attachment: Joi10.string().allow(null, "").optional()
5462
+ var supplySchema = Joi12.object({
5463
+ site: Joi12.string().hex().required(),
5464
+ serviceType: Joi12.string().valid(...Object.values(AppServiceType11)).required(),
5465
+ name: Joi12.string().required(),
5466
+ unitOfMeasurement: Joi12.string().required(),
5467
+ attachment: Joi12.string().allow(null, "").optional()
5199
5468
  });
5200
5469
  function MSupply(value) {
5201
5470
  const { error } = supplySchema.validate(value);
@@ -5332,7 +5601,7 @@ function useSupplyRepository() {
5332
5601
  throw new BadRequestError18("Invalid site ID format.");
5333
5602
  }
5334
5603
  if (search) {
5335
- query.name = { $regex: search, $options: "i" };
5604
+ query.name = { $regex: escapeRegex(search), $options: "i" };
5336
5605
  cacheOptions.search = search;
5337
5606
  }
5338
5607
  const cacheKey = makeCacheKey6(namespace_collection, cacheOptions);
@@ -5494,7 +5763,7 @@ function useSupplyRepository() {
5494
5763
  }
5495
5764
 
5496
5765
  // src/controllers/hygiene-supply.controller.ts
5497
- import Joi11 from "joi";
5766
+ import Joi13 from "joi";
5498
5767
  import { BadRequestError as BadRequestError19, logger as logger23 } from "@7365admin1/node-server-utils";
5499
5768
  import { AppServiceType as AppServiceType12 } from "@7365admin1/core";
5500
5769
  function useSupplyController() {
@@ -5525,12 +5794,12 @@ function useSupplyController() {
5525
5794
  }
5526
5795
  async function getSupplies(req, res, next) {
5527
5796
  const query = { ...req.query, ...req.params };
5528
- const validation = Joi11.object({
5529
- page: Joi11.number().min(1).optional().allow("", null),
5530
- limit: Joi11.number().min(1).optional().allow("", null),
5531
- search: Joi11.string().optional().allow("", null),
5532
- site: Joi11.string().hex().required(),
5533
- serviceType: Joi11.string().valid(...Object.values(AppServiceType12)).required()
5797
+ const validation = Joi13.object({
5798
+ page: Joi13.number().min(1).optional().allow("", null),
5799
+ limit: Joi13.number().min(1).optional().allow("", null),
5800
+ search: Joi13.string().optional().allow("", null),
5801
+ site: Joi13.string().hex().required(),
5802
+ serviceType: Joi13.string().valid(...Object.values(AppServiceType12)).required()
5534
5803
  });
5535
5804
  const { error } = validation.validate(query);
5536
5805
  if (error) {
@@ -5560,7 +5829,7 @@ function useSupplyController() {
5560
5829
  }
5561
5830
  }
5562
5831
  async function getSupplyById(req, res, next) {
5563
- const validation = Joi11.string().hex().required();
5832
+ const validation = Joi13.string().hex().required();
5564
5833
  const _id = req.params.id;
5565
5834
  const { error, value } = validation.validate(_id);
5566
5835
  if (error) {
@@ -5580,12 +5849,12 @@ function useSupplyController() {
5580
5849
  }
5581
5850
  async function updateSupply(req, res, next) {
5582
5851
  const payload = { id: req.params.id, ...req.body };
5583
- const validation = Joi11.object({
5584
- id: Joi11.string().hex().required(),
5585
- name: Joi11.string().optional().allow("", null),
5586
- unitOfMeasurement: Joi11.string().optional().allow("", null),
5587
- qty: Joi11.number().min(0).optional().allow("", null),
5588
- attachment: Joi11.string().optional().allow("", null)
5852
+ const validation = Joi13.object({
5853
+ id: Joi13.string().hex().required(),
5854
+ name: Joi13.string().optional().allow("", null),
5855
+ unitOfMeasurement: Joi13.string().optional().allow("", null),
5856
+ qty: Joi13.number().min(0).optional().allow("", null),
5857
+ attachment: Joi13.string().optional().allow("", null)
5589
5858
  });
5590
5859
  const { error } = validation.validate(payload);
5591
5860
  if (error) {
@@ -5606,7 +5875,7 @@ function useSupplyController() {
5606
5875
  }
5607
5876
  async function deleteSupply(req, res, next) {
5608
5877
  const id = req.params.id;
5609
- const validation = Joi11.string().hex().required();
5878
+ const validation = Joi13.string().hex().required();
5610
5879
  const { error, value } = validation.validate(id);
5611
5880
  if (error) {
5612
5881
  logger23.log({ level: "error", message: error.message });
@@ -5633,18 +5902,18 @@ function useSupplyController() {
5633
5902
  }
5634
5903
 
5635
5904
  // src/models/hygiene-stock.model.ts
5636
- import Joi12 from "joi";
5905
+ import Joi14 from "joi";
5637
5906
  import { ObjectId as ObjectId14 } from "mongodb";
5638
5907
  import { BadRequestError as BadRequestError20, logger as logger24 } from "@7365admin1/node-server-utils";
5639
5908
  import { AppServiceType as AppServiceType13 } from "@7365admin1/core";
5640
- var stockSchema = Joi12.object({
5641
- site: Joi12.string().hex().required(),
5642
- serviceType: Joi12.string().valid(...Object.values(AppServiceType13)).required(),
5643
- supply: Joi12.string().hex().required(),
5644
- in: Joi12.number().min(0).optional(),
5645
- out: Joi12.number().min(0).optional(),
5646
- balance: Joi12.number().min(0).required(),
5647
- remarks: Joi12.string().optional().allow("", null)
5909
+ var stockSchema = Joi14.object({
5910
+ site: Joi14.string().hex().required(),
5911
+ serviceType: Joi14.string().valid(...Object.values(AppServiceType13)).required(),
5912
+ supply: Joi14.string().hex().required(),
5913
+ in: Joi14.number().min(0).optional(),
5914
+ out: Joi14.number().min(0).optional(),
5915
+ balance: Joi14.number().min(0).required(),
5916
+ remarks: Joi14.string().optional().allow("", null)
5648
5917
  });
5649
5918
  function MStock(value) {
5650
5919
  const { error } = stockSchema.validate(value);
@@ -5871,7 +6140,7 @@ function useStockService() {
5871
6140
  }
5872
6141
 
5873
6142
  // src/controllers/hygiene-stock.controller.ts
5874
- import Joi13 from "joi";
6143
+ import Joi15 from "joi";
5875
6144
  import { BadRequestError as BadRequestError23, logger as logger26 } from "@7365admin1/node-server-utils";
5876
6145
  import { AppServiceType as AppServiceType14 } from "@7365admin1/core";
5877
6146
  function useStockController() {
@@ -5879,12 +6148,12 @@ function useStockController() {
5879
6148
  const { createStock: _createStock } = useStockService();
5880
6149
  async function createStock(req, res, next) {
5881
6150
  const payload = { ...req.body, ...req.params };
5882
- const validation = Joi13.object({
5883
- site: Joi13.string().hex().required(),
5884
- serviceType: Joi13.string().valid(...Object.values(AppServiceType14)).required(),
5885
- supply: Joi13.string().hex().required(),
5886
- qty: Joi13.number().min(0).required(),
5887
- remarks: Joi13.string().optional().allow("", null)
6151
+ const validation = Joi15.object({
6152
+ site: Joi15.string().hex().required(),
6153
+ serviceType: Joi15.string().valid(...Object.values(AppServiceType14)).required(),
6154
+ supply: Joi15.string().hex().required(),
6155
+ qty: Joi15.number().min(0).required(),
6156
+ remarks: Joi15.string().optional().allow("", null)
5888
6157
  });
5889
6158
  const { error } = validation.validate(payload);
5890
6159
  if (error) {
@@ -5904,13 +6173,13 @@ function useStockController() {
5904
6173
  }
5905
6174
  async function getStocksBySupplyId(req, res, next) {
5906
6175
  const query = { ...req.query, ...req.params };
5907
- const validation = Joi13.object({
5908
- page: Joi13.number().min(1).optional().allow("", null),
5909
- limit: Joi13.number().min(1).optional().allow("", null),
5910
- search: Joi13.string().optional().allow("", null),
5911
- site: Joi13.string().hex().required(),
5912
- serviceType: Joi13.string().valid(...Object.values(AppServiceType14)).required(),
5913
- supply: Joi13.string().hex().required()
6176
+ const validation = Joi15.object({
6177
+ page: Joi15.number().min(1).optional().allow("", null),
6178
+ limit: Joi15.number().min(1).optional().allow("", null),
6179
+ search: Joi15.string().optional().allow("", null),
6180
+ site: Joi15.string().hex().required(),
6181
+ serviceType: Joi15.string().valid(...Object.values(AppServiceType14)).required(),
6182
+ supply: Joi15.string().hex().required()
5914
6183
  });
5915
6184
  const { error } = validation.validate(query);
5916
6185
  if (error) {
@@ -5972,21 +6241,21 @@ async function applyCheckOutDecision(decision, decide, returnStock) {
5972
6241
  }
5973
6242
 
5974
6243
  // src/models/hygiene-checkout-item.model.ts
5975
- import Joi14 from "joi";
6244
+ import Joi16 from "joi";
5976
6245
  import { ObjectId as ObjectId16 } from "mongodb";
5977
6246
  import { BadRequestError as BadRequestError24, logger as logger27 } from "@7365admin1/node-server-utils";
5978
6247
  import { AppServiceType as AppServiceType15 } from "@7365admin1/core";
5979
6248
  var allowedCheckOutItemStatus = ["pending", "completed"];
5980
- var checkOutItemSchema = Joi14.object({
5981
- site: Joi14.string().hex().required(),
5982
- serviceType: Joi14.string().valid(...Object.values(AppServiceType15)).required(),
5983
- supply: Joi14.string().hex().required(),
5984
- supplyName: Joi14.string().required(),
5985
- supplyQty: Joi14.number().min(0).required(),
5986
- qty: Joi14.number().min(0).required(),
5987
- attachment: Joi14.array().items(Joi14.string()).optional().allow(null),
5988
- createdBy: Joi14.string().hex().required(),
5989
- createdByName: Joi14.string().required()
6249
+ var checkOutItemSchema = Joi16.object({
6250
+ site: Joi16.string().hex().required(),
6251
+ serviceType: Joi16.string().valid(...Object.values(AppServiceType15)).required(),
6252
+ supply: Joi16.string().hex().required(),
6253
+ supplyName: Joi16.string().required(),
6254
+ supplyQty: Joi16.number().min(0).required(),
6255
+ qty: Joi16.number().min(0).required(),
6256
+ attachment: Joi16.array().items(Joi16.string()).optional().allow(null),
6257
+ createdBy: Joi16.string().hex().required(),
6258
+ createdByName: Joi16.string().required()
5990
6259
  });
5991
6260
  function MCheckOutItem(value) {
5992
6261
  const { error } = checkOutItemSchema.validate(value);
@@ -6119,7 +6388,7 @@ function useCheckOutItemRepository() {
6119
6388
  throw new BadRequestError25("Invalid site ID format.");
6120
6389
  }
6121
6390
  if (search) {
6122
- query.supplyName = { $regex: search, $options: "i" };
6391
+ query.supplyName = { $regex: escapeRegex(search), $options: "i" };
6123
6392
  cacheOptions.search = search;
6124
6393
  }
6125
6394
  const cacheKey = makeCacheKey8(namespace_collection, cacheOptions);
@@ -6475,7 +6744,7 @@ function useCheckOutItemService() {
6475
6744
  }
6476
6745
 
6477
6746
  // src/controllers/hygiene-checkout-item.controller.ts
6478
- import Joi15 from "joi";
6747
+ import Joi17 from "joi";
6479
6748
  import { BadRequestError as BadRequestError27, logger as logger29 } from "@7365admin1/node-server-utils";
6480
6749
  import { AppServiceType as AppServiceType16 } from "@7365admin1/core";
6481
6750
  function useCheckOutItemController() {
@@ -6499,13 +6768,13 @@ function useCheckOutItemController() {
6499
6768
  ...req.params,
6500
6769
  createdBy
6501
6770
  };
6502
- const validation = Joi15.object({
6503
- site: Joi15.string().hex().required(),
6504
- serviceType: Joi15.string().valid(...Object.values(AppServiceType16)).required(),
6505
- supply: Joi15.string().hex().required(),
6506
- qty: Joi15.number().min(0).required(),
6507
- attachment: Joi15.array().items(Joi15.string()).optional().allow(null),
6508
- createdBy: Joi15.string().hex().required()
6771
+ const validation = Joi17.object({
6772
+ site: Joi17.string().hex().required(),
6773
+ serviceType: Joi17.string().valid(...Object.values(AppServiceType16)).required(),
6774
+ supply: Joi17.string().hex().required(),
6775
+ qty: Joi17.number().min(0).required(),
6776
+ attachment: Joi17.array().items(Joi17.string()).optional().allow(null),
6777
+ createdBy: Joi17.string().hex().required()
6509
6778
  });
6510
6779
  const { error } = validation.validate(payload);
6511
6780
  if (error) {
@@ -6534,15 +6803,15 @@ function useCheckOutItemController() {
6534
6803
  ...req.params,
6535
6804
  createdBy
6536
6805
  };
6537
- const validation = Joi15.object({
6538
- site: Joi15.string().hex().required(),
6539
- serviceType: Joi15.string().valid(...Object.values(AppServiceType16)).required(),
6540
- createdBy: Joi15.string().hex().required(),
6541
- items: Joi15.array().items(
6542
- Joi15.object({
6543
- supply: Joi15.string().hex().required(),
6544
- qty: Joi15.number().min(0).required(),
6545
- attachment: Joi15.array().items(Joi15.string()).optional().allow(null)
6806
+ const validation = Joi17.object({
6807
+ site: Joi17.string().hex().required(),
6808
+ serviceType: Joi17.string().valid(...Object.values(AppServiceType16)).required(),
6809
+ createdBy: Joi17.string().hex().required(),
6810
+ items: Joi17.array().items(
6811
+ Joi17.object({
6812
+ supply: Joi17.string().hex().required(),
6813
+ qty: Joi17.number().min(0).required(),
6814
+ attachment: Joi17.array().items(Joi17.string()).optional().allow(null)
6546
6815
  })
6547
6816
  ).min(1).required()
6548
6817
  });
@@ -6564,12 +6833,12 @@ function useCheckOutItemController() {
6564
6833
  }
6565
6834
  async function getCheckOutItems(req, res, next) {
6566
6835
  const query = { ...req.query, ...req.params };
6567
- const validation = Joi15.object({
6568
- page: Joi15.number().min(1).optional().allow("", null),
6569
- limit: Joi15.number().min(1).optional().allow("", null),
6570
- search: Joi15.string().optional().allow("", null),
6571
- site: Joi15.string().hex().required(),
6572
- serviceType: Joi15.string().valid(...Object.values(AppServiceType16)).required()
6836
+ const validation = Joi17.object({
6837
+ page: Joi17.number().min(1).optional().allow("", null),
6838
+ limit: Joi17.number().min(1).optional().allow("", null),
6839
+ search: Joi17.string().optional().allow("", null),
6840
+ site: Joi17.string().hex().required(),
6841
+ serviceType: Joi17.string().valid(...Object.values(AppServiceType16)).required()
6573
6842
  });
6574
6843
  const { error } = validation.validate(query);
6575
6844
  if (error) {
@@ -6599,7 +6868,7 @@ function useCheckOutItemController() {
6599
6868
  }
6600
6869
  }
6601
6870
  async function getCheckOutItemById(req, res, next) {
6602
- const validation = Joi15.string().hex().required();
6871
+ const validation = Joi17.string().hex().required();
6603
6872
  const _id = req.params.id;
6604
6873
  const { error, value } = validation.validate(_id);
6605
6874
  if (error) {
@@ -6638,13 +6907,13 @@ function useCheckOutItemController() {
6638
6907
  ...decisionValues,
6639
6908
  completedBy
6640
6909
  };
6641
- const validation = Joi15.object({
6642
- id: Joi15.string().hex().required(),
6643
- decision: Joi15.string().valid("approve", "disapprove").required(),
6644
- approve: Joi15.boolean().required(),
6645
- reject: Joi15.boolean().required(),
6646
- remarks: Joi15.string().optional().allow("", null),
6647
- completedBy: Joi15.string().hex().required()
6910
+ const validation = Joi17.object({
6911
+ id: Joi17.string().hex().required(),
6912
+ decision: Joi17.string().valid("approve", "disapprove").required(),
6913
+ approve: Joi17.boolean().required(),
6914
+ reject: Joi17.boolean().required(),
6915
+ remarks: Joi17.string().optional().allow("", null),
6916
+ completedBy: Joi17.string().hex().required()
6648
6917
  });
6649
6918
  const { error } = validation.validate(payload);
6650
6919
  if (error) {
@@ -6677,23 +6946,15 @@ function useCheckOutItemController() {
6677
6946
  // src/models/hygiene-schedule-task.model.ts
6678
6947
  import { BadRequestError as BadRequestError28, logger as logger30 } from "@7365admin1/node-server-utils";
6679
6948
  import { AppServiceType as AppServiceType17 } from "@7365admin1/core";
6680
- import Joi16 from "joi";
6949
+ import Joi18 from "joi";
6681
6950
  import { ObjectId as ObjectId18 } from "mongodb";
6682
- var scheduleTaskSchema = Joi16.object({
6683
- site: Joi16.string().hex().required(),
6684
- serviceType: Joi16.string().valid(...Object.values(AppServiceType17)).required(),
6685
- title: Joi16.string().required(),
6686
- time: Joi16.string().pattern(/^([0-1]\d|2[0-3]):([0-5]\d)$/).required(),
6687
- dates: Joi16.array().min(1).items(
6688
- Joi16.string().pattern(/^\d{4}-\d{2}-\d{2}$/).required()
6689
- ).required(),
6690
- description: Joi16.string().optional().allow("", null),
6691
- areas: Joi16.array().min(1).items(
6692
- Joi16.object({
6693
- name: Joi16.string().required(),
6694
- value: Joi16.any().required()
6695
- })
6696
- ).required()
6951
+ var scheduleTaskSchema = Joi18.object({
6952
+ site: Joi18.string().hex().required(),
6953
+ serviceType: Joi18.string().valid(...Object.values(AppServiceType17)).required(),
6954
+ ...scheduleTaskFields
6955
+ }).custom((value, helpers) => {
6956
+ const message = validateScheduleRules(cleanScheduleInput(value));
6957
+ return message ? helpers.message({ custom: message }) : value;
6697
6958
  });
6698
6959
  function MScheduleTask(value) {
6699
6960
  const { error } = scheduleTaskSchema.validate(value);
@@ -6720,14 +6981,26 @@ function MScheduleTask(value) {
6720
6981
  }
6721
6982
  });
6722
6983
  }
6984
+ const extra = cleanScheduleInput({
6985
+ repeat: value.repeat,
6986
+ daysOfWeek: value.daysOfWeek,
6987
+ dayOfMonth: value.dayOfMonth,
6988
+ startDate: value.startDate,
6989
+ endDate: value.endDate,
6990
+ paused: value.paused
6991
+ });
6992
+ for (const key of Object.keys(extra))
6993
+ if (extra[key] === void 0)
6994
+ delete extra[key];
6723
6995
  return {
6724
6996
  site: value.site,
6725
6997
  serviceType: value.serviceType,
6726
6998
  title: value.title,
6727
6999
  time: value.time,
6728
- dates: value.dates,
7000
+ dates: value.dates ?? [],
6729
7001
  description: value.description,
6730
7002
  areas: value.areas,
7003
+ ...extra,
6731
7004
  status: "active",
6732
7005
  createdAt: /* @__PURE__ */ new Date(),
6733
7006
  updatedAt: "",
@@ -6747,11 +7020,23 @@ import {
6747
7020
  makeCacheKey as makeCacheKey9,
6748
7021
  NotFoundError as NotFoundError7
6749
7022
  } from "@7365admin1/node-server-utils";
7023
+ var SCHEDULE_FIELDS = {
7024
+ time: 1,
7025
+ dates: 1,
7026
+ repeat: 1,
7027
+ daysOfWeek: 1,
7028
+ dayOfMonth: 1,
7029
+ startDate: 1,
7030
+ endDate: 1,
7031
+ paused: 1,
7032
+ lastRunDate: 1
7033
+ };
6750
7034
  function useScheduleTaskRepository() {
6751
7035
  const db = useAtlas14.getDb();
6752
7036
  if (!db) {
6753
7037
  throw new InternalServerError11("Unable to connect to server.");
6754
7038
  }
7039
+ const database = db;
6755
7040
  const namespace_collection = "site.schedule-tasks";
6756
7041
  const collection = db.collection(namespace_collection);
6757
7042
  const { delNamespace, setCache, getCache } = useCache9(namespace_collection);
@@ -6819,14 +7104,14 @@ function useScheduleTaskRepository() {
6819
7104
  throw new BadRequestError29("Invalid site ID format.");
6820
7105
  }
6821
7106
  if (search) {
6822
- query.$or = [{ title: { $regex: search, $options: "i" } }];
7107
+ query.$or = [{ title: { $regex: escapeRegex(search), $options: "i" } }];
6823
7108
  cacheOptions.search = search;
6824
7109
  }
6825
7110
  const cacheKey = makeCacheKey9(namespace_collection, cacheOptions);
6826
7111
  const cachedData = await getCache(cacheKey);
6827
7112
  if (cachedData) {
6828
7113
  logger31.info(`Cache hit for key: ${cacheKey}`);
6829
- return cachedData;
7114
+ return enrichList(cachedData, site, serviceType);
6830
7115
  }
6831
7116
  try {
6832
7117
  const items = await collection.aggregate([
@@ -6835,7 +7120,8 @@ function useScheduleTaskRepository() {
6835
7120
  $project: {
6836
7121
  title: 1,
6837
7122
  areas: 1,
6838
- status: 1
7123
+ status: 1,
7124
+ ...SCHEDULE_FIELDS
6839
7125
  }
6840
7126
  },
6841
7127
  { $sort: { _id: -1 } },
@@ -6849,15 +7135,104 @@ function useScheduleTaskRepository() {
6849
7135
  }).catch((err) => {
6850
7136
  logger31.error(`Failed to set cache for key: ${cacheKey}`, err);
6851
7137
  });
6852
- return data;
7138
+ return enrichList(data, site, serviceType);
6853
7139
  } catch (error) {
6854
7140
  throw error;
6855
7141
  }
6856
7142
  }
6857
- async function getAllScheduleTask() {
7143
+ async function enrichList(data, site, serviceType) {
7144
+ const items = data?.items ?? [];
7145
+ const now = /* @__PURE__ */ new Date();
7146
+ let statuses = /* @__PURE__ */ new Map();
7147
+ try {
7148
+ statuses = await getRunStatuses(items, site, serviceType);
7149
+ } catch (error) {
7150
+ logger31.error("Could not read schedule task run status", error);
7151
+ }
7152
+ return {
7153
+ ...data,
7154
+ items: items.map((item) => {
7155
+ try {
7156
+ return {
7157
+ ...item,
7158
+ ...scheduleDisplay(item, now),
7159
+ lastRunStatus: statuses.get(String(item._id)) ?? null
7160
+ };
7161
+ } catch (error) {
7162
+ logger31.error(`Could not enrich schedule task ${item?._id}`, error);
7163
+ return { ...item, nextRun: null, lastRun: null, scheduleStatus: null, lastRunStatus: null };
7164
+ }
7165
+ })
7166
+ };
7167
+ }
7168
+ async function getRunStatuses(items, site, serviceType) {
7169
+ const result = /* @__PURE__ */ new Map();
7170
+ const ran = items.filter((i) => i.lastRunDate);
7171
+ if (!ran.length)
7172
+ return result;
7173
+ const parents = await database.collection("site.cleaning.schedules").find(
7174
+ {
7175
+ site,
7176
+ serviceType,
7177
+ dateStr: { $in: [...new Set(ran.map((i) => i.lastRunDate))] }
7178
+ },
7179
+ { projection: { dateStr: 1 } }
7180
+ ).toArray();
7181
+ if (!parents.length)
7182
+ return result;
7183
+ const dateOf = new Map(parents.map((p) => [String(p._id), p.dateStr]));
7184
+ const taskIds = ran.map((i) => new ObjectId19(i._id));
7185
+ const rows = await database.collection("site.cleaning.schedule.areas").aggregate([
7186
+ {
7187
+ $match: {
7188
+ schedule: { $in: parents.map((p) => p._id) },
7189
+ "checklist.scheduleTaskId": { $in: taskIds }
7190
+ }
7191
+ },
7192
+ { $unwind: "$checklist" },
7193
+ { $match: { "checklist.scheduleTaskId": { $in: taskIds } } },
7194
+ { $unwind: "$checklist.units" },
7195
+ {
7196
+ $group: {
7197
+ _id: { task: "$checklist.scheduleTaskId", schedule: "$schedule" },
7198
+ total: { $sum: 1 },
7199
+ completed: {
7200
+ $sum: { $cond: [{ $eq: ["$checklist.units.status", "completed"] }, 1, 0] }
7201
+ },
7202
+ closed: {
7203
+ $sum: { $cond: [{ $eq: ["$checklist.units.status", "closed"] }, 1, 0] }
7204
+ }
7205
+ }
7206
+ }
7207
+ ]).toArray();
7208
+ const sums = /* @__PURE__ */ new Map();
7209
+ for (const row of rows) {
7210
+ const task = ran.find((i) => String(i._id) === String(row._id.task));
7211
+ if (!task || dateOf.get(String(row._id.schedule)) !== task.lastRunDate)
7212
+ continue;
7213
+ const sum = sums.get(String(task._id)) ?? { total: 0, completed: 0, closed: 0 };
7214
+ sum.total += row.total;
7215
+ sum.completed += row.completed;
7216
+ sum.closed += row.closed;
7217
+ sums.set(String(task._id), sum);
7218
+ }
7219
+ for (const [id, sum] of sums) {
7220
+ result.set(id, sum.completed === sum.total ? "done" : sum.closed > 0 ? "missed" : "pending");
7221
+ }
7222
+ return result;
7223
+ }
7224
+ async function getAllScheduleTask(today) {
6858
7225
  const query = {
6859
7226
  status: { $ne: "deleted" }
6860
7227
  };
7228
+ if (today) {
7229
+ query.paused = { $ne: true };
7230
+ query.lastRunDate = { $ne: today };
7231
+ query.$or = [
7232
+ { dates: today },
7233
+ { repeat: { $in: ["daily", "weekdays", "weekly", "monthly"] } }
7234
+ ];
7235
+ }
6861
7236
  try {
6862
7237
  const items = await collection.aggregate([{ $match: query }, { $sort: { _id: -1 } }]).toArray();
6863
7238
  return items;
@@ -6865,6 +7240,9 @@ function useScheduleTaskRepository() {
6865
7240
  throw error;
6866
7241
  }
6867
7242
  }
7243
+ function withScheduleDisplay(row) {
7244
+ return { ...row, ...scheduleDisplay(row, /* @__PURE__ */ new Date()) };
7245
+ }
6868
7246
  async function getScheduleTaskById(_id, session) {
6869
7247
  try {
6870
7248
  _id = new ObjectId19(_id);
@@ -6882,7 +7260,7 @@ function useScheduleTaskRepository() {
6882
7260
  const cachedData = await getCache(cacheKey);
6883
7261
  if (cachedData) {
6884
7262
  logger31.info(`Cache hit for key: ${cacheKey}`);
6885
- return cachedData;
7263
+ return withScheduleDisplay(cachedData);
6886
7264
  }
6887
7265
  } else {
6888
7266
  logger31.info(`Skipping cache during transaction for key: ${cacheKey}`);
@@ -6893,12 +7271,13 @@ function useScheduleTaskRepository() {
6893
7271
  {
6894
7272
  $project: {
6895
7273
  title: 1,
6896
- time: 1,
6897
- dates: 1,
6898
7274
  description: 1,
6899
7275
  areas: 1,
6900
7276
  status: 1,
6901
- createdAt: 1
7277
+ createdAt: 1,
7278
+ site: 1,
7279
+ serviceType: 1,
7280
+ ...SCHEDULE_FIELDS
6902
7281
  }
6903
7282
  }
6904
7283
  ]).toArray();
@@ -6910,7 +7289,7 @@ function useScheduleTaskRepository() {
6910
7289
  }).catch((err) => {
6911
7290
  logger31.error(`Failed to set cache for key: ${cacheKey}`, err);
6912
7291
  });
6913
- return data[0];
7292
+ return withScheduleDisplay(data[0]);
6914
7293
  } catch (error) {
6915
7294
  throw error;
6916
7295
  }
@@ -6934,16 +7313,20 @@ function useScheduleTaskRepository() {
6934
7313
  });
6935
7314
  }
6936
7315
  try {
6937
- const updateValue = { ...value, updatedAt: /* @__PURE__ */ new Date() };
7316
+ const { set, unset } = splitScheduleUpdate(value);
7317
+ const update = {
7318
+ $set: { ...set, updatedAt: /* @__PURE__ */ new Date() }
7319
+ };
7320
+ if (unset.length) {
7321
+ update.$unset = Object.fromEntries(unset.map((key) => [key, ""]));
7322
+ }
6938
7323
  const res = await collection.updateOne(
6939
- { _id },
6940
- { $set: updateValue },
7324
+ { _id, status: { $ne: "deleted" } },
7325
+ update,
6941
7326
  { session }
6942
7327
  );
6943
- if (res.modifiedCount === 0) {
6944
- throw new InternalServerError11(
6945
- "Unable to update hygiene schedule task."
6946
- );
7328
+ if (res.matchedCount === 0) {
7329
+ throw new NotFoundError7("Schedule task not found.");
6947
7330
  }
6948
7331
  delNamespace().then(() => {
6949
7332
  logger31.info(`Cache cleared for namespace: ${namespace_collection}`);
@@ -6958,6 +7341,18 @@ function useScheduleTaskRepository() {
6958
7341
  throw error;
6959
7342
  }
6960
7343
  }
7344
+ async function markScheduleTaskRan(_id, date) {
7345
+ await collection.updateOne(
7346
+ { _id: new ObjectId19(_id) },
7347
+ { $set: { lastRunDate: date, lastRunAt: /* @__PURE__ */ new Date() } }
7348
+ );
7349
+ delNamespace().catch((err) => {
7350
+ logger31.error(
7351
+ `Failed to clear cache for namespace: ${namespace_collection}`,
7352
+ err
7353
+ );
7354
+ });
7355
+ }
6961
7356
  async function deleteScheduleTask(_id, session) {
6962
7357
  try {
6963
7358
  _id = new ObjectId19(_id);
@@ -6997,6 +7392,7 @@ function useScheduleTaskRepository() {
6997
7392
  createScheduleTask,
6998
7393
  getScheduleTasks,
6999
7394
  getAllScheduleTask,
7395
+ markScheduleTaskRan,
7000
7396
  getScheduleTaskById,
7001
7397
  updateScheduleTask,
7002
7398
  deleteScheduleTask
@@ -7008,7 +7404,7 @@ import { ObjectId as ObjectId20 } from "mongodb";
7008
7404
  import { logger as logger32 } from "@7365admin1/node-server-utils";
7009
7405
  function useScheduleTaskService() {
7010
7406
  const { createParentChecklist } = useParentChecklistRepo();
7011
- const { getAllScheduleTask } = useScheduleTaskRepository();
7407
+ const { getAllScheduleTask, markScheduleTaskRan } = useScheduleTaskRepository();
7012
7408
  const {
7013
7409
  createAreaChecklist,
7014
7410
  getAreaChecklistByAreaAndSchedule,
@@ -7017,73 +7413,7 @@ function useScheduleTaskService() {
7017
7413
  const { getAreaById } = useAreaRepo();
7018
7414
  function checkScheduleConditions(schedule, serviceType, currentDate = /* @__PURE__ */ new Date()) {
7019
7415
  try {
7020
- const now = currentDate;
7021
- const timeString = now.toLocaleTimeString("en-US", {
7022
- hour: "2-digit",
7023
- minute: "2-digit",
7024
- hour12: false,
7025
- timeZone: "Asia/Singapore"
7026
- });
7027
- const [currentHour, currentMinute] = timeString.split(":").map(Number);
7028
- logger32.info(
7029
- `Checking schedule ${schedule._id}: Current time ${currentHour}:${currentMinute}, Schedule time ${schedule.time}, Dates ${JSON.stringify(schedule.dates)}`
7030
- );
7031
- const currentDateFormatted = now.toLocaleDateString("en-CA", {
7032
- timeZone: "Asia/Singapore"
7033
- });
7034
- if (!schedule.dates || !Array.isArray(schedule.dates) || schedule.dates.length === 0) {
7035
- logger32.info(`Schedule ${schedule._id}: No dates configured, skipping`);
7036
- return false;
7037
- }
7038
- if (!schedule.dates.includes(currentDateFormatted)) {
7039
- logger32.info(
7040
- `Schedule ${schedule._id}: Current date ${currentDateFormatted} is not in scheduled dates [${schedule.dates.join(
7041
- ", "
7042
- )}]`
7043
- );
7044
- return false;
7045
- }
7046
- const [scheduleHour, scheduleMinute] = schedule.time.split(":").map(Number);
7047
- const timeMatches = currentHour === scheduleHour && currentMinute === scheduleMinute;
7048
- if (!timeMatches) {
7049
- logger32.info(
7050
- `Schedule ${schedule._id}: Time does not match. Current: ${currentHour}:${currentMinute}, Expected: ${scheduleHour}:${scheduleMinute}`
7051
- );
7052
- return false;
7053
- }
7054
- const relevantTimestamp = schedule.updatedAt || schedule.createdAt;
7055
- if (relevantTimestamp) {
7056
- const savedAt = new Date(relevantTimestamp);
7057
- const savedHour = parseInt(
7058
- savedAt.toLocaleTimeString("en-US", {
7059
- hour: "2-digit",
7060
- hour12: false,
7061
- timeZone: "Asia/Singapore"
7062
- }),
7063
- 10
7064
- );
7065
- const savedMinute = savedAt.toLocaleString("en-US", {
7066
- minute: "2-digit",
7067
- timeZone: "Asia/Singapore"
7068
- });
7069
- const savedDateFormatted = savedAt.toLocaleDateString("en-CA", {
7070
- timeZone: "Asia/Singapore"
7071
- });
7072
- const savedMinNum = parseInt(savedMinute, 10);
7073
- const wasSavedThisMinute = savedDateFormatted === currentDateFormatted && savedHour === currentHour && savedMinNum === currentMinute;
7074
- if (wasSavedThisMinute) {
7075
- logger32.info(
7076
- `Schedule ${schedule._id}: Skipping \u2014 task was saved this minute (${savedHour}:${String(
7077
- savedMinNum
7078
- ).padStart(2, "0")}). Will fire on next occurrence.`
7079
- );
7080
- return false;
7081
- }
7082
- }
7083
- logger32.info(
7084
- `Schedule ${schedule._id}: All conditions matched - Date is within range and time matches`
7085
- );
7086
- return true;
7416
+ return shouldRunNow(schedule, currentDate);
7087
7417
  } catch (error) {
7088
7418
  logger32.error(
7089
7419
  `Error checking schedule conditions for ${schedule._id}:`,
@@ -7092,18 +7422,22 @@ function useScheduleTaskService() {
7092
7422
  return false;
7093
7423
  }
7094
7424
  }
7425
+ const MAX_ATTEMPTS = 5;
7426
+ const attempts = /* @__PURE__ */ new Map();
7095
7427
  async function processScheduledTasks(currentDate) {
7096
7428
  try {
7097
7429
  logger32.info("Starting scheduled task processing...");
7098
- const scheduleTasks = await getAllScheduleTask();
7430
+ const today = sgtParts(currentDate ?? /* @__PURE__ */ new Date()).date;
7431
+ const scheduleTasks = await getAllScheduleTask(today);
7099
7432
  if (!scheduleTasks || scheduleTasks.length === 0) {
7100
7433
  logger32.info("No schedule tasks found to process");
7101
- return { processed: 0, validated: 0 };
7434
+ return { processed: 0, validated: 0, fired: [] };
7102
7435
  }
7103
7436
  logger32.info(`Found ${scheduleTasks.length} schedule tasks to check`);
7104
7437
  let processedCount = 0;
7105
7438
  let validatedCount = 0;
7106
7439
  const validatedTasks = [];
7440
+ const fired = [];
7107
7441
  for (const scheduleTask of scheduleTasks) {
7108
7442
  try {
7109
7443
  const serviceType = scheduleTask.serviceType;
@@ -7146,6 +7480,8 @@ function useScheduleTaskService() {
7146
7480
  logger32.info(
7147
7481
  `Using parent checklist ${parentChecklistId}, now creating/updating area checklists`
7148
7482
  );
7483
+ let failedAreas = 0;
7484
+ const addedAreas = [];
7149
7485
  for (const area of scheduleTask.areas) {
7150
7486
  try {
7151
7487
  const areaId = area.value.toString();
@@ -7225,6 +7561,7 @@ function useScheduleTaskService() {
7225
7561
  newSets
7226
7562
  );
7227
7563
  if (modified > 0) {
7564
+ addedAreas.push(area.name);
7228
7565
  logger32.info(
7229
7566
  `Appended ${areaSetConfig} set(s) starting from ${startSetNumber} to area checklist for area ${area.name}`
7230
7567
  );
@@ -7257,13 +7594,32 @@ function useScheduleTaskService() {
7257
7594
  `Area ${area.name} (${areaId}): Creating new area checklist with schedule-task sets only`
7258
7595
  );
7259
7596
  await createAreaChecklist(checklistData);
7597
+ addedAreas.push(area.name);
7260
7598
  logger32.info(`Created new area checklist for area ${area.name}`);
7261
7599
  }
7262
7600
  } catch (error) {
7263
7601
  logger32.error(`Error processing area ${area.name}:`, error);
7602
+ failedAreas++;
7264
7603
  continue;
7265
7604
  }
7266
7605
  }
7606
+ const attemptKey = `${scheduleTask._id}:${today}`;
7607
+ const tried = (attempts.get(attemptKey) ?? 0) + 1;
7608
+ attempts.set(attemptKey, tried);
7609
+ if (failedAreas === 0 || tried >= MAX_ATTEMPTS) {
7610
+ await markScheduleTaskRan(scheduleTask._id, today);
7611
+ attempts.delete(attemptKey);
7612
+ }
7613
+ if (addedAreas.length > 0) {
7614
+ fired.push({
7615
+ _id: scheduleTask._id,
7616
+ site: scheduleTask.site,
7617
+ serviceType,
7618
+ title: scheduleTask.title,
7619
+ time: scheduleTask.time,
7620
+ areas: addedAreas
7621
+ });
7622
+ }
7267
7623
  processedCount++;
7268
7624
  validatedCount++;
7269
7625
  validatedTasks.push(scheduleTask);
@@ -7275,6 +7631,17 @@ function useScheduleTaskService() {
7275
7631
  `Error processing schedule task ${scheduleTask._id}:`,
7276
7632
  error
7277
7633
  );
7634
+ try {
7635
+ const key = `${scheduleTask._id}:${today}`;
7636
+ const tried = (attempts.get(key) ?? 0) + 1;
7637
+ attempts.set(key, tried);
7638
+ if (tried >= MAX_ATTEMPTS && scheduleTask._id) {
7639
+ await markScheduleTaskRan(scheduleTask._id, today);
7640
+ attempts.delete(key);
7641
+ }
7642
+ } catch (markError) {
7643
+ logger32.error("Could not mark failing schedule task", markError);
7644
+ }
7278
7645
  continue;
7279
7646
  }
7280
7647
  }
@@ -7284,7 +7651,8 @@ function useScheduleTaskService() {
7284
7651
  return {
7285
7652
  processed: processedCount,
7286
7653
  validated: validatedCount,
7287
- tasks: validatedTasks
7654
+ tasks: validatedTasks,
7655
+ fired
7288
7656
  };
7289
7657
  } catch (error) {
7290
7658
  logger32.error("Error processing scheduled tasks:", error);
@@ -7295,7 +7663,7 @@ function useScheduleTaskService() {
7295
7663
  }
7296
7664
 
7297
7665
  // src/controllers/hygiene-schedule-task.controller.ts
7298
- import Joi17 from "joi";
7666
+ import Joi19 from "joi";
7299
7667
  import { BadRequestError as BadRequestError30, logger as logger33 } from "@7365admin1/node-server-utils";
7300
7668
  import { AppServiceType as AppServiceType18 } from "@7365admin1/core";
7301
7669
  function useScheduleTaskController() {
@@ -7326,12 +7694,12 @@ function useScheduleTaskController() {
7326
7694
  }
7327
7695
  async function getScheduleTasks(req, res, next) {
7328
7696
  const query = { ...req.query, ...req.params };
7329
- const validation = Joi17.object({
7330
- page: Joi17.number().min(1).optional().allow("", null),
7331
- limit: Joi17.number().min(1).optional().allow("", null),
7332
- search: Joi17.string().optional().allow("", null),
7333
- site: Joi17.string().hex().required(),
7334
- serviceType: Joi17.string().valid(...Object.values(AppServiceType18)).required()
7697
+ const validation = Joi19.object({
7698
+ page: Joi19.number().min(1).optional().allow("", null),
7699
+ limit: Joi19.number().min(1).optional().allow("", null),
7700
+ search: Joi19.string().optional().allow("", null),
7701
+ site: Joi19.string().hex().required(),
7702
+ serviceType: Joi19.string().valid(...Object.values(AppServiceType18)).required()
7335
7703
  });
7336
7704
  const { error } = validation.validate(query);
7337
7705
  if (error) {
@@ -7339,8 +7707,8 @@ function useScheduleTaskController() {
7339
7707
  next(new BadRequestError30(error.message));
7340
7708
  return;
7341
7709
  }
7342
- const page = parseInt(req.query.page) ?? 1;
7343
- const limit = parseInt(req.query.limit) ?? 10;
7710
+ const page = parseInt(req.query.page) || 1;
7711
+ const limit = parseInt(req.query.limit) || 10;
7344
7712
  const search = req.query.search ?? "";
7345
7713
  const site = req.params.site ?? "";
7346
7714
  const serviceType = req.query.serviceType ?? "";
@@ -7361,7 +7729,7 @@ function useScheduleTaskController() {
7361
7729
  }
7362
7730
  }
7363
7731
  async function getScheduleTaskById(req, res, next) {
7364
- const validation = Joi17.string().hex().required();
7732
+ const validation = Joi19.string().hex().required();
7365
7733
  const _id = req.params.id;
7366
7734
  const { error, value } = validation.validate(_id);
7367
7735
  if (error) {
@@ -7381,20 +7749,17 @@ function useScheduleTaskController() {
7381
7749
  }
7382
7750
  async function updateScheduleTask(req, res, next) {
7383
7751
  const payload = { id: req.params.id, ...req.body };
7384
- const validation = Joi17.object({
7385
- id: Joi17.string().hex().required(),
7386
- title: Joi17.string().optional().allow("", null),
7387
- time: Joi17.string().pattern(/^([0-1]\d|2[0-3]):([0-5]\d)$/).optional().allow("", null),
7388
- dates: Joi17.array().min(1).items(
7389
- Joi17.string().pattern(/^\d{4}-\d{2}-\d{2}$/).required()
7390
- ).optional(),
7391
- description: Joi17.string().optional().allow("", null),
7392
- areas: Joi17.array().min(1).items(
7393
- Joi17.object({
7394
- name: Joi17.string().required(),
7395
- value: Joi17.any().required()
7396
- })
7397
- ).optional()
7752
+ const optional = Object.fromEntries(
7753
+ Object.entries(scheduleTaskFields).map(([key, rule]) => [
7754
+ key,
7755
+ key === "dates" ? Joi19.array().min(1).items(scheduleDate).optional().messages({
7756
+ "array.min": "Pick at least one date, or choose a repeat."
7757
+ }) : rule.optional()
7758
+ ])
7759
+ );
7760
+ const validation = Joi19.object({
7761
+ id: Joi19.string().hex().required(),
7762
+ ...optional
7398
7763
  });
7399
7764
  const { error } = validation.validate(payload);
7400
7765
  if (error) {
@@ -7402,6 +7767,24 @@ function useScheduleTaskController() {
7402
7767
  next(new BadRequestError30(error.message));
7403
7768
  return;
7404
7769
  }
7770
+ try {
7771
+ const { set, unset } = splitScheduleUpdate(req.body ?? {});
7772
+ if (TIMING_KEYS.some((key) => key in set || unset.includes(key))) {
7773
+ const stored = await _getScheduleTaskById(req.params.id);
7774
+ const merged = { ...stored, ...set };
7775
+ for (const key of unset)
7776
+ delete merged[key];
7777
+ const problem = timingChanged(stored, merged) ? validateScheduleRules(cleanScheduleInput(merged)) : null;
7778
+ if (problem) {
7779
+ next(new BadRequestError30(problem));
7780
+ return;
7781
+ }
7782
+ }
7783
+ } catch (error2) {
7784
+ logger33.log({ level: "error", message: error2.message });
7785
+ next(error2);
7786
+ return;
7787
+ }
7405
7788
  try {
7406
7789
  const { id, ...value } = payload;
7407
7790
  await _updateScheduleTask(id, value);
@@ -7414,7 +7797,7 @@ function useScheduleTaskController() {
7414
7797
  }
7415
7798
  }
7416
7799
  async function deleteScheduleTask(req, res, next) {
7417
- const validation = Joi17.string().hex().required();
7800
+ const validation = Joi19.string().hex().required();
7418
7801
  const _id = req.params.id;
7419
7802
  const { error, value } = validation.validate(_id);
7420
7803
  if (error) {
@@ -7658,17 +8041,17 @@ function useQRService() {
7658
8041
  }
7659
8042
 
7660
8043
  // src/controllers/hygiene-qr.controller.ts
7661
- import Joi18 from "joi";
8044
+ import Joi20 from "joi";
7662
8045
  import { BadRequestError as BadRequestError31, logger as logger35 } from "@7365admin1/node-server-utils";
7663
8046
  function useQRController() {
7664
8047
  const { generateQRImage: _generateQRImage, generateQRPDF: _generateQRPDF } = useQRService();
7665
8048
  async function generateQR(req, res, next) {
7666
- const validation = Joi18.object({
7667
- url: Joi18.string().uri().required(),
7668
- filename: Joi18.string().optional().allow("", null),
7669
- title: Joi18.string().optional().allow("", null),
7670
- subtitle: Joi18.string().optional().allow("", null),
7671
- download: Joi18.boolean().optional().default(false)
8049
+ const validation = Joi20.object({
8050
+ url: Joi20.string().uri().required(),
8051
+ filename: Joi20.string().optional().allow("", null),
8052
+ title: Joi20.string().optional().allow("", null),
8053
+ subtitle: Joi20.string().optional().allow("", null),
8054
+ download: Joi20.boolean().optional().default(false)
7672
8055
  });
7673
8056
  const query = { ...req.query };
7674
8057
  const { error, value } = validation.validate(query);