@7365admin1/module-hygiene 4.29.1-staging.14 → 4.29.1-staging.16

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
@@ -930,6 +930,11 @@ function capitalizeText(text) {
930
930
  return text.toLowerCase().split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
931
931
  }
932
932
 
933
+ // src/utils/escape-regex.util.ts
934
+ function escapeRegex(value) {
935
+ return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
936
+ }
937
+
933
938
  // src/repositories/hygiene-area.repository.ts
934
939
  function useAreaRepo() {
935
940
  const db = useAtlas2.getDb();
@@ -1026,7 +1031,7 @@ function useAreaRepo() {
1026
1031
  cacheOptions.type = type;
1027
1032
  }
1028
1033
  if (search) {
1029
- query.$or = [{ name: { $regex: search, $options: "i" } }];
1034
+ query.$or = [{ name: { $regex: escapeRegex(search), $options: "i" } }];
1030
1035
  cacheOptions.search = search;
1031
1036
  }
1032
1037
  const cacheKey = makeCacheKey2(namespace_collection, cacheOptions);
@@ -1500,7 +1505,7 @@ function useUnitRepository() {
1500
1505
  throw new BadRequestError6("Invalid site ID format.");
1501
1506
  }
1502
1507
  if (search) {
1503
- query.$or = [{ name: { $regex: search, $options: "i" } }];
1508
+ query.$or = [{ name: { $regex: escapeRegex(search), $options: "i" } }];
1504
1509
  cacheOptions.search = search;
1505
1510
  }
1506
1511
  const cacheKey = makeCacheKey3(namespace_collection, cacheOptions);
@@ -4431,6 +4436,265 @@ function useAreaChecklistRepo() {
4431
4436
  };
4432
4437
  }
4433
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
+
4434
4698
  // src/services/hygiene-area-checklist.service.ts
4435
4699
  function useAreaChecklistService() {
4436
4700
  const { getAreasForChecklist } = useAreaRepo();
@@ -4498,10 +4762,10 @@ function useAreaChecklistService() {
4498
4762
  `Area checklist for area ${area.name} exists but has no auto-gen sets. Inserting auto-gen sets.`
4499
4763
  );
4500
4764
  const setCount2 = Number(area.set) || 1;
4501
- const totalExistingSets = existing.checklist?.length ?? 0;
4502
- if (totalExistingSets >= setCount2) {
4765
+ const setsToAdd = regularSetsMissing(existing.checklist, setCount2);
4766
+ if (setsToAdd <= 0) {
4503
4767
  logger18.info(
4504
- `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.`
4505
4769
  );
4506
4770
  return existing._id;
4507
4771
  }
@@ -4509,7 +4773,6 @@ function useAreaChecklistService() {
4509
4773
  (max, c) => Math.max(max, c.set ?? 0),
4510
4774
  0
4511
4775
  ) ?? 0;
4512
- const setsToAdd = setCount2 - totalExistingSets;
4513
4776
  const autoGenSets = Array.from(
4514
4777
  { length: setsToAdd },
4515
4778
  (_, index) => ({
@@ -4670,7 +4933,7 @@ function useAreaChecklistService() {
4670
4933
  }
4671
4934
 
4672
4935
  // src/controllers/hygiene-area-checklist.controller.ts
4673
- import Joi10 from "joi";
4936
+ import Joi11 from "joi";
4674
4937
  import {
4675
4938
  BadRequestError as BadRequestError16,
4676
4939
  InternalServerError as InternalServerError7,
@@ -4915,9 +5178,9 @@ function useAreaChecklistController() {
4915
5178
  site: req.params.site,
4916
5179
  schedule: req.params.schedule
4917
5180
  };
4918
- const validation = Joi10.object({
4919
- site: Joi10.string().hex().required(),
4920
- schedule: Joi10.string().hex().required()
5181
+ const validation = Joi11.object({
5182
+ site: Joi11.string().hex().required(),
5183
+ schedule: Joi11.string().hex().required()
4921
5184
  });
4922
5185
  const { error, value } = validation.validate(payload);
4923
5186
  if (error) {
@@ -4937,14 +5200,14 @@ function useAreaChecklistController() {
4937
5200
  }
4938
5201
  async function getAllAreaChecklist(req, res, next) {
4939
5202
  const query = { ...req.query, ...req.params };
4940
- const validation = Joi10.object({
4941
- page: Joi10.number().min(1).optional().allow("", null),
4942
- limit: Joi10.number().min(1).optional().allow("", null),
4943
- search: Joi10.string().optional().allow("", null),
4944
- type: Joi10.string().valid(...allowedTypes, "all").optional().allow("", null),
4945
- status: Joi10.string().valid(...allowedStatus, "all").optional().allow("", null),
4946
- schedule: Joi10.string().hex().required(),
4947
- serviceType: Joi10.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()
4948
5211
  });
4949
5212
  const { error } = validation.validate(query);
4950
5213
  if (error) {
@@ -4979,14 +5242,14 @@ function useAreaChecklistController() {
4979
5242
  }
4980
5243
  async function getAreaChecklistHistory(req, res, next) {
4981
5244
  const query = { ...req.query, ...req.params };
4982
- const validation = Joi10.object({
4983
- page: Joi10.number().min(1).optional().allow("", null),
4984
- limit: Joi10.number().min(1).optional().allow("", null),
4985
- search: Joi10.string().optional().allow("", null),
4986
- type: Joi10.string().optional().allow("", ...allowedTypes),
4987
- schedule: Joi10.string().hex().required(),
4988
- status: Joi10.string().allow("", null, ...allowedStatus),
4989
- createdAt: Joi10.alternatives().try(Joi10.date(), Joi10.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)
4990
5253
  });
4991
5254
  const { error } = validation.validate(query);
4992
5255
  if (error) {
@@ -5020,7 +5283,7 @@ function useAreaChecklistController() {
5020
5283
  }
5021
5284
  }
5022
5285
  async function getAreaChecklistHistoryDetails(req, res, next) {
5023
- const validation = Joi10.string().hex().required();
5286
+ const validation = Joi11.string().hex().required();
5024
5287
  const _id = req.params.id;
5025
5288
  const { error, value } = validation.validate(_id);
5026
5289
  if (error) {
@@ -5040,12 +5303,12 @@ function useAreaChecklistController() {
5040
5303
  }
5041
5304
  async function getAreaChecklistUnits(req, res, next) {
5042
5305
  const query = { ...req.query, ...req.params };
5043
- const validation = Joi10.object({
5044
- page: Joi10.number().min(1).optional().allow("", null),
5045
- limit: Joi10.number().min(1).optional().allow("", null),
5046
- search: Joi10.string().optional().allow("", null),
5047
- serviceType: Joi10.string().valid(...Object.values(AppServiceType10)).required(),
5048
- id: Joi10.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()
5049
5312
  });
5050
5313
  const { error } = validation.validate(query);
5051
5314
  if (error) {
@@ -5095,21 +5358,21 @@ function useAreaChecklistController() {
5095
5358
  ...decisionValues,
5096
5359
  completedBy
5097
5360
  };
5098
- const validation = Joi10.object({
5099
- id: Joi10.string().hex().required(),
5100
- set: Joi10.number().integer().min(1).required(),
5101
- unit: Joi10.string().hex().required(),
5102
- decision: Joi10.string().valid("approve", "reject").required(),
5103
- approve: Joi10.boolean().optional(),
5104
- reject: Joi10.boolean().optional(),
5105
- remarks: Joi10.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),
5106
5369
  /*
5107
5370
  * Shape only. WHETHER a photo is required depends on the set's current
5108
5371
  * state in the database, which Joi cannot see — see the check below.
5109
5372
  * Each entry must still be a real id: a blank string carries no evidence.
5110
5373
  */
5111
- attachment: Joi10.array().items(Joi10.string().trim().min(1)).optional(),
5112
- completedBy: Joi10.string().hex().required()
5374
+ attachment: Joi11.array().items(Joi11.string().trim().min(1)).optional(),
5375
+ completedBy: Joi11.string().hex().required()
5113
5376
  });
5114
5377
  const { error } = validation.validate(payload);
5115
5378
  if (error) {
@@ -5146,7 +5409,7 @@ function useAreaChecklistController() {
5146
5409
  }
5147
5410
  }
5148
5411
  async function downloadChecklistPdf(req, res, next) {
5149
- const validation = Joi10.string().hex().required();
5412
+ const validation = Joi11.string().hex().required();
5150
5413
  const schedule = req.params.schedule;
5151
5414
  const { error, value } = validation.validate(schedule);
5152
5415
  if (error) {
@@ -5192,16 +5455,16 @@ function useAreaChecklistController() {
5192
5455
  }
5193
5456
 
5194
5457
  // src/models/hygiene-supply.model.ts
5195
- import Joi11 from "joi";
5458
+ import Joi12 from "joi";
5196
5459
  import { ObjectId as ObjectId12 } from "mongodb";
5197
5460
  import { BadRequestError as BadRequestError17, logger as logger21 } from "@7365admin1/node-server-utils";
5198
5461
  import { AppServiceType as AppServiceType11 } from "@7365admin1/core";
5199
- var supplySchema = Joi11.object({
5200
- site: Joi11.string().hex().required(),
5201
- serviceType: Joi11.string().valid(...Object.values(AppServiceType11)).required(),
5202
- name: Joi11.string().required(),
5203
- unitOfMeasurement: Joi11.string().required(),
5204
- attachment: Joi11.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()
5205
5468
  });
5206
5469
  function MSupply(value) {
5207
5470
  const { error } = supplySchema.validate(value);
@@ -5338,7 +5601,7 @@ function useSupplyRepository() {
5338
5601
  throw new BadRequestError18("Invalid site ID format.");
5339
5602
  }
5340
5603
  if (search) {
5341
- query.name = { $regex: search, $options: "i" };
5604
+ query.name = { $regex: escapeRegex(search), $options: "i" };
5342
5605
  cacheOptions.search = search;
5343
5606
  }
5344
5607
  const cacheKey = makeCacheKey6(namespace_collection, cacheOptions);
@@ -5500,7 +5763,7 @@ function useSupplyRepository() {
5500
5763
  }
5501
5764
 
5502
5765
  // src/controllers/hygiene-supply.controller.ts
5503
- import Joi12 from "joi";
5766
+ import Joi13 from "joi";
5504
5767
  import { BadRequestError as BadRequestError19, logger as logger23 } from "@7365admin1/node-server-utils";
5505
5768
  import { AppServiceType as AppServiceType12 } from "@7365admin1/core";
5506
5769
  function useSupplyController() {
@@ -5531,12 +5794,12 @@ function useSupplyController() {
5531
5794
  }
5532
5795
  async function getSupplies(req, res, next) {
5533
5796
  const query = { ...req.query, ...req.params };
5534
- const validation = Joi12.object({
5535
- page: Joi12.number().min(1).optional().allow("", null),
5536
- limit: Joi12.number().min(1).optional().allow("", null),
5537
- search: Joi12.string().optional().allow("", null),
5538
- site: Joi12.string().hex().required(),
5539
- serviceType: Joi12.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()
5540
5803
  });
5541
5804
  const { error } = validation.validate(query);
5542
5805
  if (error) {
@@ -5566,7 +5829,7 @@ function useSupplyController() {
5566
5829
  }
5567
5830
  }
5568
5831
  async function getSupplyById(req, res, next) {
5569
- const validation = Joi12.string().hex().required();
5832
+ const validation = Joi13.string().hex().required();
5570
5833
  const _id = req.params.id;
5571
5834
  const { error, value } = validation.validate(_id);
5572
5835
  if (error) {
@@ -5586,12 +5849,12 @@ function useSupplyController() {
5586
5849
  }
5587
5850
  async function updateSupply(req, res, next) {
5588
5851
  const payload = { id: req.params.id, ...req.body };
5589
- const validation = Joi12.object({
5590
- id: Joi12.string().hex().required(),
5591
- name: Joi12.string().optional().allow("", null),
5592
- unitOfMeasurement: Joi12.string().optional().allow("", null),
5593
- qty: Joi12.number().min(0).optional().allow("", null),
5594
- attachment: Joi12.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)
5595
5858
  });
5596
5859
  const { error } = validation.validate(payload);
5597
5860
  if (error) {
@@ -5612,7 +5875,7 @@ function useSupplyController() {
5612
5875
  }
5613
5876
  async function deleteSupply(req, res, next) {
5614
5877
  const id = req.params.id;
5615
- const validation = Joi12.string().hex().required();
5878
+ const validation = Joi13.string().hex().required();
5616
5879
  const { error, value } = validation.validate(id);
5617
5880
  if (error) {
5618
5881
  logger23.log({ level: "error", message: error.message });
@@ -5639,18 +5902,18 @@ function useSupplyController() {
5639
5902
  }
5640
5903
 
5641
5904
  // src/models/hygiene-stock.model.ts
5642
- import Joi13 from "joi";
5905
+ import Joi14 from "joi";
5643
5906
  import { ObjectId as ObjectId14 } from "mongodb";
5644
5907
  import { BadRequestError as BadRequestError20, logger as logger24 } from "@7365admin1/node-server-utils";
5645
5908
  import { AppServiceType as AppServiceType13 } from "@7365admin1/core";
5646
- var stockSchema = Joi13.object({
5647
- site: Joi13.string().hex().required(),
5648
- serviceType: Joi13.string().valid(...Object.values(AppServiceType13)).required(),
5649
- supply: Joi13.string().hex().required(),
5650
- in: Joi13.number().min(0).optional(),
5651
- out: Joi13.number().min(0).optional(),
5652
- balance: Joi13.number().min(0).required(),
5653
- remarks: Joi13.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)
5654
5917
  });
5655
5918
  function MStock(value) {
5656
5919
  const { error } = stockSchema.validate(value);
@@ -5877,7 +6140,7 @@ function useStockService() {
5877
6140
  }
5878
6141
 
5879
6142
  // src/controllers/hygiene-stock.controller.ts
5880
- import Joi14 from "joi";
6143
+ import Joi15 from "joi";
5881
6144
  import { BadRequestError as BadRequestError23, logger as logger26 } from "@7365admin1/node-server-utils";
5882
6145
  import { AppServiceType as AppServiceType14 } from "@7365admin1/core";
5883
6146
  function useStockController() {
@@ -5885,12 +6148,12 @@ function useStockController() {
5885
6148
  const { createStock: _createStock } = useStockService();
5886
6149
  async function createStock(req, res, next) {
5887
6150
  const payload = { ...req.body, ...req.params };
5888
- const validation = Joi14.object({
5889
- site: Joi14.string().hex().required(),
5890
- serviceType: Joi14.string().valid(...Object.values(AppServiceType14)).required(),
5891
- supply: Joi14.string().hex().required(),
5892
- qty: Joi14.number().min(0).required(),
5893
- remarks: Joi14.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)
5894
6157
  });
5895
6158
  const { error } = validation.validate(payload);
5896
6159
  if (error) {
@@ -5910,13 +6173,13 @@ function useStockController() {
5910
6173
  }
5911
6174
  async function getStocksBySupplyId(req, res, next) {
5912
6175
  const query = { ...req.query, ...req.params };
5913
- const validation = Joi14.object({
5914
- page: Joi14.number().min(1).optional().allow("", null),
5915
- limit: Joi14.number().min(1).optional().allow("", null),
5916
- search: Joi14.string().optional().allow("", null),
5917
- site: Joi14.string().hex().required(),
5918
- serviceType: Joi14.string().valid(...Object.values(AppServiceType14)).required(),
5919
- supply: Joi14.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()
5920
6183
  });
5921
6184
  const { error } = validation.validate(query);
5922
6185
  if (error) {
@@ -5978,21 +6241,21 @@ async function applyCheckOutDecision(decision, decide, returnStock) {
5978
6241
  }
5979
6242
 
5980
6243
  // src/models/hygiene-checkout-item.model.ts
5981
- import Joi15 from "joi";
6244
+ import Joi16 from "joi";
5982
6245
  import { ObjectId as ObjectId16 } from "mongodb";
5983
6246
  import { BadRequestError as BadRequestError24, logger as logger27 } from "@7365admin1/node-server-utils";
5984
6247
  import { AppServiceType as AppServiceType15 } from "@7365admin1/core";
5985
6248
  var allowedCheckOutItemStatus = ["pending", "completed"];
5986
- var checkOutItemSchema = Joi15.object({
5987
- site: Joi15.string().hex().required(),
5988
- serviceType: Joi15.string().valid(...Object.values(AppServiceType15)).required(),
5989
- supply: Joi15.string().hex().required(),
5990
- supplyName: Joi15.string().required(),
5991
- supplyQty: Joi15.number().min(0).required(),
5992
- qty: Joi15.number().min(0).required(),
5993
- attachment: Joi15.array().items(Joi15.string()).optional().allow(null),
5994
- createdBy: Joi15.string().hex().required(),
5995
- createdByName: Joi15.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()
5996
6259
  });
5997
6260
  function MCheckOutItem(value) {
5998
6261
  const { error } = checkOutItemSchema.validate(value);
@@ -6125,7 +6388,7 @@ function useCheckOutItemRepository() {
6125
6388
  throw new BadRequestError25("Invalid site ID format.");
6126
6389
  }
6127
6390
  if (search) {
6128
- query.supplyName = { $regex: search, $options: "i" };
6391
+ query.supplyName = { $regex: escapeRegex(search), $options: "i" };
6129
6392
  cacheOptions.search = search;
6130
6393
  }
6131
6394
  const cacheKey = makeCacheKey8(namespace_collection, cacheOptions);
@@ -6481,7 +6744,7 @@ function useCheckOutItemService() {
6481
6744
  }
6482
6745
 
6483
6746
  // src/controllers/hygiene-checkout-item.controller.ts
6484
- import Joi16 from "joi";
6747
+ import Joi17 from "joi";
6485
6748
  import { BadRequestError as BadRequestError27, logger as logger29 } from "@7365admin1/node-server-utils";
6486
6749
  import { AppServiceType as AppServiceType16 } from "@7365admin1/core";
6487
6750
  function useCheckOutItemController() {
@@ -6505,13 +6768,13 @@ function useCheckOutItemController() {
6505
6768
  ...req.params,
6506
6769
  createdBy
6507
6770
  };
6508
- const validation = Joi16.object({
6509
- site: Joi16.string().hex().required(),
6510
- serviceType: Joi16.string().valid(...Object.values(AppServiceType16)).required(),
6511
- supply: Joi16.string().hex().required(),
6512
- qty: Joi16.number().min(0).required(),
6513
- attachment: Joi16.array().items(Joi16.string()).optional().allow(null),
6514
- createdBy: Joi16.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()
6515
6778
  });
6516
6779
  const { error } = validation.validate(payload);
6517
6780
  if (error) {
@@ -6540,15 +6803,15 @@ function useCheckOutItemController() {
6540
6803
  ...req.params,
6541
6804
  createdBy
6542
6805
  };
6543
- const validation = Joi16.object({
6544
- site: Joi16.string().hex().required(),
6545
- serviceType: Joi16.string().valid(...Object.values(AppServiceType16)).required(),
6546
- createdBy: Joi16.string().hex().required(),
6547
- items: Joi16.array().items(
6548
- Joi16.object({
6549
- supply: Joi16.string().hex().required(),
6550
- qty: Joi16.number().min(0).required(),
6551
- attachment: Joi16.array().items(Joi16.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)
6552
6815
  })
6553
6816
  ).min(1).required()
6554
6817
  });
@@ -6570,12 +6833,12 @@ function useCheckOutItemController() {
6570
6833
  }
6571
6834
  async function getCheckOutItems(req, res, next) {
6572
6835
  const query = { ...req.query, ...req.params };
6573
- const validation = Joi16.object({
6574
- page: Joi16.number().min(1).optional().allow("", null),
6575
- limit: Joi16.number().min(1).optional().allow("", null),
6576
- search: Joi16.string().optional().allow("", null),
6577
- site: Joi16.string().hex().required(),
6578
- serviceType: Joi16.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()
6579
6842
  });
6580
6843
  const { error } = validation.validate(query);
6581
6844
  if (error) {
@@ -6605,7 +6868,7 @@ function useCheckOutItemController() {
6605
6868
  }
6606
6869
  }
6607
6870
  async function getCheckOutItemById(req, res, next) {
6608
- const validation = Joi16.string().hex().required();
6871
+ const validation = Joi17.string().hex().required();
6609
6872
  const _id = req.params.id;
6610
6873
  const { error, value } = validation.validate(_id);
6611
6874
  if (error) {
@@ -6644,13 +6907,13 @@ function useCheckOutItemController() {
6644
6907
  ...decisionValues,
6645
6908
  completedBy
6646
6909
  };
6647
- const validation = Joi16.object({
6648
- id: Joi16.string().hex().required(),
6649
- decision: Joi16.string().valid("approve", "disapprove").required(),
6650
- approve: Joi16.boolean().required(),
6651
- reject: Joi16.boolean().required(),
6652
- remarks: Joi16.string().optional().allow("", null),
6653
- completedBy: Joi16.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()
6654
6917
  });
6655
6918
  const { error } = validation.validate(payload);
6656
6919
  if (error) {
@@ -6683,23 +6946,15 @@ function useCheckOutItemController() {
6683
6946
  // src/models/hygiene-schedule-task.model.ts
6684
6947
  import { BadRequestError as BadRequestError28, logger as logger30 } from "@7365admin1/node-server-utils";
6685
6948
  import { AppServiceType as AppServiceType17 } from "@7365admin1/core";
6686
- import Joi17 from "joi";
6949
+ import Joi18 from "joi";
6687
6950
  import { ObjectId as ObjectId18 } from "mongodb";
6688
- var scheduleTaskSchema = Joi17.object({
6689
- site: Joi17.string().hex().required(),
6690
- serviceType: Joi17.string().valid(...Object.values(AppServiceType17)).required(),
6691
- title: Joi17.string().required(),
6692
- time: Joi17.string().pattern(/^([0-1]\d|2[0-3]):([0-5]\d)$/).required(),
6693
- dates: Joi17.array().min(1).items(
6694
- Joi17.string().pattern(/^\d{4}-\d{2}-\d{2}$/).required()
6695
- ).required(),
6696
- description: Joi17.string().optional().allow("", null),
6697
- areas: Joi17.array().min(1).items(
6698
- Joi17.object({
6699
- name: Joi17.string().required(),
6700
- value: Joi17.any().required()
6701
- })
6702
- ).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;
6703
6958
  });
6704
6959
  function MScheduleTask(value) {
6705
6960
  const { error } = scheduleTaskSchema.validate(value);
@@ -6726,14 +6981,26 @@ function MScheduleTask(value) {
6726
6981
  }
6727
6982
  });
6728
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];
6729
6995
  return {
6730
6996
  site: value.site,
6731
6997
  serviceType: value.serviceType,
6732
6998
  title: value.title,
6733
6999
  time: value.time,
6734
- dates: value.dates,
7000
+ dates: value.dates ?? [],
6735
7001
  description: value.description,
6736
7002
  areas: value.areas,
7003
+ ...extra,
6737
7004
  status: "active",
6738
7005
  createdAt: /* @__PURE__ */ new Date(),
6739
7006
  updatedAt: "",
@@ -6753,11 +7020,23 @@ import {
6753
7020
  makeCacheKey as makeCacheKey9,
6754
7021
  NotFoundError as NotFoundError7
6755
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
+ };
6756
7034
  function useScheduleTaskRepository() {
6757
7035
  const db = useAtlas14.getDb();
6758
7036
  if (!db) {
6759
7037
  throw new InternalServerError11("Unable to connect to server.");
6760
7038
  }
7039
+ const database = db;
6761
7040
  const namespace_collection = "site.schedule-tasks";
6762
7041
  const collection = db.collection(namespace_collection);
6763
7042
  const { delNamespace, setCache, getCache } = useCache9(namespace_collection);
@@ -6825,14 +7104,14 @@ function useScheduleTaskRepository() {
6825
7104
  throw new BadRequestError29("Invalid site ID format.");
6826
7105
  }
6827
7106
  if (search) {
6828
- query.$or = [{ title: { $regex: search, $options: "i" } }];
7107
+ query.$or = [{ title: { $regex: escapeRegex(search), $options: "i" } }];
6829
7108
  cacheOptions.search = search;
6830
7109
  }
6831
7110
  const cacheKey = makeCacheKey9(namespace_collection, cacheOptions);
6832
7111
  const cachedData = await getCache(cacheKey);
6833
7112
  if (cachedData) {
6834
7113
  logger31.info(`Cache hit for key: ${cacheKey}`);
6835
- return cachedData;
7114
+ return enrichList(cachedData, site, serviceType);
6836
7115
  }
6837
7116
  try {
6838
7117
  const items = await collection.aggregate([
@@ -6841,7 +7120,8 @@ function useScheduleTaskRepository() {
6841
7120
  $project: {
6842
7121
  title: 1,
6843
7122
  areas: 1,
6844
- status: 1
7123
+ status: 1,
7124
+ ...SCHEDULE_FIELDS
6845
7125
  }
6846
7126
  },
6847
7127
  { $sort: { _id: -1 } },
@@ -6855,15 +7135,104 @@ function useScheduleTaskRepository() {
6855
7135
  }).catch((err) => {
6856
7136
  logger31.error(`Failed to set cache for key: ${cacheKey}`, err);
6857
7137
  });
6858
- return data;
7138
+ return enrichList(data, site, serviceType);
6859
7139
  } catch (error) {
6860
7140
  throw error;
6861
7141
  }
6862
7142
  }
6863
- 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) {
6864
7225
  const query = {
6865
7226
  status: { $ne: "deleted" }
6866
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
+ }
6867
7236
  try {
6868
7237
  const items = await collection.aggregate([{ $match: query }, { $sort: { _id: -1 } }]).toArray();
6869
7238
  return items;
@@ -6871,6 +7240,9 @@ function useScheduleTaskRepository() {
6871
7240
  throw error;
6872
7241
  }
6873
7242
  }
7243
+ function withScheduleDisplay(row) {
7244
+ return { ...row, ...scheduleDisplay(row, /* @__PURE__ */ new Date()) };
7245
+ }
6874
7246
  async function getScheduleTaskById(_id, session) {
6875
7247
  try {
6876
7248
  _id = new ObjectId19(_id);
@@ -6888,7 +7260,7 @@ function useScheduleTaskRepository() {
6888
7260
  const cachedData = await getCache(cacheKey);
6889
7261
  if (cachedData) {
6890
7262
  logger31.info(`Cache hit for key: ${cacheKey}`);
6891
- return cachedData;
7263
+ return withScheduleDisplay(cachedData);
6892
7264
  }
6893
7265
  } else {
6894
7266
  logger31.info(`Skipping cache during transaction for key: ${cacheKey}`);
@@ -6899,12 +7271,13 @@ function useScheduleTaskRepository() {
6899
7271
  {
6900
7272
  $project: {
6901
7273
  title: 1,
6902
- time: 1,
6903
- dates: 1,
6904
7274
  description: 1,
6905
7275
  areas: 1,
6906
7276
  status: 1,
6907
- createdAt: 1
7277
+ createdAt: 1,
7278
+ site: 1,
7279
+ serviceType: 1,
7280
+ ...SCHEDULE_FIELDS
6908
7281
  }
6909
7282
  }
6910
7283
  ]).toArray();
@@ -6916,7 +7289,7 @@ function useScheduleTaskRepository() {
6916
7289
  }).catch((err) => {
6917
7290
  logger31.error(`Failed to set cache for key: ${cacheKey}`, err);
6918
7291
  });
6919
- return data[0];
7292
+ return withScheduleDisplay(data[0]);
6920
7293
  } catch (error) {
6921
7294
  throw error;
6922
7295
  }
@@ -6940,16 +7313,20 @@ function useScheduleTaskRepository() {
6940
7313
  });
6941
7314
  }
6942
7315
  try {
6943
- 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
+ }
6944
7323
  const res = await collection.updateOne(
6945
- { _id },
6946
- { $set: updateValue },
7324
+ { _id, status: { $ne: "deleted" } },
7325
+ update,
6947
7326
  { session }
6948
7327
  );
6949
- if (res.modifiedCount === 0) {
6950
- throw new InternalServerError11(
6951
- "Unable to update hygiene schedule task."
6952
- );
7328
+ if (res.matchedCount === 0) {
7329
+ throw new NotFoundError7("Schedule task not found.");
6953
7330
  }
6954
7331
  delNamespace().then(() => {
6955
7332
  logger31.info(`Cache cleared for namespace: ${namespace_collection}`);
@@ -6964,6 +7341,18 @@ function useScheduleTaskRepository() {
6964
7341
  throw error;
6965
7342
  }
6966
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
+ }
6967
7356
  async function deleteScheduleTask(_id, session) {
6968
7357
  try {
6969
7358
  _id = new ObjectId19(_id);
@@ -7003,6 +7392,7 @@ function useScheduleTaskRepository() {
7003
7392
  createScheduleTask,
7004
7393
  getScheduleTasks,
7005
7394
  getAllScheduleTask,
7395
+ markScheduleTaskRan,
7006
7396
  getScheduleTaskById,
7007
7397
  updateScheduleTask,
7008
7398
  deleteScheduleTask
@@ -7014,7 +7404,7 @@ import { ObjectId as ObjectId20 } from "mongodb";
7014
7404
  import { logger as logger32 } from "@7365admin1/node-server-utils";
7015
7405
  function useScheduleTaskService() {
7016
7406
  const { createParentChecklist } = useParentChecklistRepo();
7017
- const { getAllScheduleTask } = useScheduleTaskRepository();
7407
+ const { getAllScheduleTask, markScheduleTaskRan } = useScheduleTaskRepository();
7018
7408
  const {
7019
7409
  createAreaChecklist,
7020
7410
  getAreaChecklistByAreaAndSchedule,
@@ -7023,73 +7413,7 @@ function useScheduleTaskService() {
7023
7413
  const { getAreaById } = useAreaRepo();
7024
7414
  function checkScheduleConditions(schedule, serviceType, currentDate = /* @__PURE__ */ new Date()) {
7025
7415
  try {
7026
- const now = currentDate;
7027
- const timeString = now.toLocaleTimeString("en-US", {
7028
- hour: "2-digit",
7029
- minute: "2-digit",
7030
- hour12: false,
7031
- timeZone: "Asia/Singapore"
7032
- });
7033
- const [currentHour, currentMinute] = timeString.split(":").map(Number);
7034
- logger32.info(
7035
- `Checking schedule ${schedule._id}: Current time ${currentHour}:${currentMinute}, Schedule time ${schedule.time}, Dates ${JSON.stringify(schedule.dates)}`
7036
- );
7037
- const currentDateFormatted = now.toLocaleDateString("en-CA", {
7038
- timeZone: "Asia/Singapore"
7039
- });
7040
- if (!schedule.dates || !Array.isArray(schedule.dates) || schedule.dates.length === 0) {
7041
- logger32.info(`Schedule ${schedule._id}: No dates configured, skipping`);
7042
- return false;
7043
- }
7044
- if (!schedule.dates.includes(currentDateFormatted)) {
7045
- logger32.info(
7046
- `Schedule ${schedule._id}: Current date ${currentDateFormatted} is not in scheduled dates [${schedule.dates.join(
7047
- ", "
7048
- )}]`
7049
- );
7050
- return false;
7051
- }
7052
- const [scheduleHour, scheduleMinute] = schedule.time.split(":").map(Number);
7053
- const timeMatches = currentHour === scheduleHour && currentMinute === scheduleMinute;
7054
- if (!timeMatches) {
7055
- logger32.info(
7056
- `Schedule ${schedule._id}: Time does not match. Current: ${currentHour}:${currentMinute}, Expected: ${scheduleHour}:${scheduleMinute}`
7057
- );
7058
- return false;
7059
- }
7060
- const relevantTimestamp = schedule.updatedAt || schedule.createdAt;
7061
- if (relevantTimestamp) {
7062
- const savedAt = new Date(relevantTimestamp);
7063
- const savedHour = parseInt(
7064
- savedAt.toLocaleTimeString("en-US", {
7065
- hour: "2-digit",
7066
- hour12: false,
7067
- timeZone: "Asia/Singapore"
7068
- }),
7069
- 10
7070
- );
7071
- const savedMinute = savedAt.toLocaleString("en-US", {
7072
- minute: "2-digit",
7073
- timeZone: "Asia/Singapore"
7074
- });
7075
- const savedDateFormatted = savedAt.toLocaleDateString("en-CA", {
7076
- timeZone: "Asia/Singapore"
7077
- });
7078
- const savedMinNum = parseInt(savedMinute, 10);
7079
- const wasSavedThisMinute = savedDateFormatted === currentDateFormatted && savedHour === currentHour && savedMinNum === currentMinute;
7080
- if (wasSavedThisMinute) {
7081
- logger32.info(
7082
- `Schedule ${schedule._id}: Skipping \u2014 task was saved this minute (${savedHour}:${String(
7083
- savedMinNum
7084
- ).padStart(2, "0")}). Will fire on next occurrence.`
7085
- );
7086
- return false;
7087
- }
7088
- }
7089
- logger32.info(
7090
- `Schedule ${schedule._id}: All conditions matched - Date is within range and time matches`
7091
- );
7092
- return true;
7416
+ return shouldRunNow(schedule, currentDate);
7093
7417
  } catch (error) {
7094
7418
  logger32.error(
7095
7419
  `Error checking schedule conditions for ${schedule._id}:`,
@@ -7098,18 +7422,22 @@ function useScheduleTaskService() {
7098
7422
  return false;
7099
7423
  }
7100
7424
  }
7425
+ const MAX_ATTEMPTS = 5;
7426
+ const attempts = /* @__PURE__ */ new Map();
7101
7427
  async function processScheduledTasks(currentDate) {
7102
7428
  try {
7103
7429
  logger32.info("Starting scheduled task processing...");
7104
- const scheduleTasks = await getAllScheduleTask();
7430
+ const today = sgtParts(currentDate ?? /* @__PURE__ */ new Date()).date;
7431
+ const scheduleTasks = await getAllScheduleTask(today);
7105
7432
  if (!scheduleTasks || scheduleTasks.length === 0) {
7106
7433
  logger32.info("No schedule tasks found to process");
7107
- return { processed: 0, validated: 0 };
7434
+ return { processed: 0, validated: 0, fired: [] };
7108
7435
  }
7109
7436
  logger32.info(`Found ${scheduleTasks.length} schedule tasks to check`);
7110
7437
  let processedCount = 0;
7111
7438
  let validatedCount = 0;
7112
7439
  const validatedTasks = [];
7440
+ const fired = [];
7113
7441
  for (const scheduleTask of scheduleTasks) {
7114
7442
  try {
7115
7443
  const serviceType = scheduleTask.serviceType;
@@ -7152,6 +7480,8 @@ function useScheduleTaskService() {
7152
7480
  logger32.info(
7153
7481
  `Using parent checklist ${parentChecklistId}, now creating/updating area checklists`
7154
7482
  );
7483
+ let failedAreas = 0;
7484
+ const addedAreas = [];
7155
7485
  for (const area of scheduleTask.areas) {
7156
7486
  try {
7157
7487
  const areaId = area.value.toString();
@@ -7231,6 +7561,7 @@ function useScheduleTaskService() {
7231
7561
  newSets
7232
7562
  );
7233
7563
  if (modified > 0) {
7564
+ addedAreas.push(area.name);
7234
7565
  logger32.info(
7235
7566
  `Appended ${areaSetConfig} set(s) starting from ${startSetNumber} to area checklist for area ${area.name}`
7236
7567
  );
@@ -7263,13 +7594,32 @@ function useScheduleTaskService() {
7263
7594
  `Area ${area.name} (${areaId}): Creating new area checklist with schedule-task sets only`
7264
7595
  );
7265
7596
  await createAreaChecklist(checklistData);
7597
+ addedAreas.push(area.name);
7266
7598
  logger32.info(`Created new area checklist for area ${area.name}`);
7267
7599
  }
7268
7600
  } catch (error) {
7269
7601
  logger32.error(`Error processing area ${area.name}:`, error);
7602
+ failedAreas++;
7270
7603
  continue;
7271
7604
  }
7272
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
+ }
7273
7623
  processedCount++;
7274
7624
  validatedCount++;
7275
7625
  validatedTasks.push(scheduleTask);
@@ -7281,6 +7631,17 @@ function useScheduleTaskService() {
7281
7631
  `Error processing schedule task ${scheduleTask._id}:`,
7282
7632
  error
7283
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
+ }
7284
7645
  continue;
7285
7646
  }
7286
7647
  }
@@ -7290,7 +7651,8 @@ function useScheduleTaskService() {
7290
7651
  return {
7291
7652
  processed: processedCount,
7292
7653
  validated: validatedCount,
7293
- tasks: validatedTasks
7654
+ tasks: validatedTasks,
7655
+ fired
7294
7656
  };
7295
7657
  } catch (error) {
7296
7658
  logger32.error("Error processing scheduled tasks:", error);
@@ -7301,7 +7663,7 @@ function useScheduleTaskService() {
7301
7663
  }
7302
7664
 
7303
7665
  // src/controllers/hygiene-schedule-task.controller.ts
7304
- import Joi18 from "joi";
7666
+ import Joi19 from "joi";
7305
7667
  import { BadRequestError as BadRequestError30, logger as logger33 } from "@7365admin1/node-server-utils";
7306
7668
  import { AppServiceType as AppServiceType18 } from "@7365admin1/core";
7307
7669
  function useScheduleTaskController() {
@@ -7332,12 +7694,12 @@ function useScheduleTaskController() {
7332
7694
  }
7333
7695
  async function getScheduleTasks(req, res, next) {
7334
7696
  const query = { ...req.query, ...req.params };
7335
- const validation = Joi18.object({
7336
- page: Joi18.number().min(1).optional().allow("", null),
7337
- limit: Joi18.number().min(1).optional().allow("", null),
7338
- search: Joi18.string().optional().allow("", null),
7339
- site: Joi18.string().hex().required(),
7340
- serviceType: Joi18.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()
7341
7703
  });
7342
7704
  const { error } = validation.validate(query);
7343
7705
  if (error) {
@@ -7345,8 +7707,8 @@ function useScheduleTaskController() {
7345
7707
  next(new BadRequestError30(error.message));
7346
7708
  return;
7347
7709
  }
7348
- const page = parseInt(req.query.page) ?? 1;
7349
- const limit = parseInt(req.query.limit) ?? 10;
7710
+ const page = parseInt(req.query.page) || 1;
7711
+ const limit = parseInt(req.query.limit) || 10;
7350
7712
  const search = req.query.search ?? "";
7351
7713
  const site = req.params.site ?? "";
7352
7714
  const serviceType = req.query.serviceType ?? "";
@@ -7367,7 +7729,7 @@ function useScheduleTaskController() {
7367
7729
  }
7368
7730
  }
7369
7731
  async function getScheduleTaskById(req, res, next) {
7370
- const validation = Joi18.string().hex().required();
7732
+ const validation = Joi19.string().hex().required();
7371
7733
  const _id = req.params.id;
7372
7734
  const { error, value } = validation.validate(_id);
7373
7735
  if (error) {
@@ -7387,20 +7749,17 @@ function useScheduleTaskController() {
7387
7749
  }
7388
7750
  async function updateScheduleTask(req, res, next) {
7389
7751
  const payload = { id: req.params.id, ...req.body };
7390
- const validation = Joi18.object({
7391
- id: Joi18.string().hex().required(),
7392
- title: Joi18.string().optional().allow("", null),
7393
- time: Joi18.string().pattern(/^([0-1]\d|2[0-3]):([0-5]\d)$/).optional().allow("", null),
7394
- dates: Joi18.array().min(1).items(
7395
- Joi18.string().pattern(/^\d{4}-\d{2}-\d{2}$/).required()
7396
- ).optional(),
7397
- description: Joi18.string().optional().allow("", null),
7398
- areas: Joi18.array().min(1).items(
7399
- Joi18.object({
7400
- name: Joi18.string().required(),
7401
- value: Joi18.any().required()
7402
- })
7403
- ).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
7404
7763
  });
7405
7764
  const { error } = validation.validate(payload);
7406
7765
  if (error) {
@@ -7408,6 +7767,24 @@ function useScheduleTaskController() {
7408
7767
  next(new BadRequestError30(error.message));
7409
7768
  return;
7410
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
+ }
7411
7788
  try {
7412
7789
  const { id, ...value } = payload;
7413
7790
  await _updateScheduleTask(id, value);
@@ -7420,7 +7797,7 @@ function useScheduleTaskController() {
7420
7797
  }
7421
7798
  }
7422
7799
  async function deleteScheduleTask(req, res, next) {
7423
- const validation = Joi18.string().hex().required();
7800
+ const validation = Joi19.string().hex().required();
7424
7801
  const _id = req.params.id;
7425
7802
  const { error, value } = validation.validate(_id);
7426
7803
  if (error) {
@@ -7664,17 +8041,17 @@ function useQRService() {
7664
8041
  }
7665
8042
 
7666
8043
  // src/controllers/hygiene-qr.controller.ts
7667
- import Joi19 from "joi";
8044
+ import Joi20 from "joi";
7668
8045
  import { BadRequestError as BadRequestError31, logger as logger35 } from "@7365admin1/node-server-utils";
7669
8046
  function useQRController() {
7670
8047
  const { generateQRImage: _generateQRImage, generateQRPDF: _generateQRPDF } = useQRService();
7671
8048
  async function generateQR(req, res, next) {
7672
- const validation = Joi19.object({
7673
- url: Joi19.string().uri().required(),
7674
- filename: Joi19.string().optional().allow("", null),
7675
- title: Joi19.string().optional().allow("", null),
7676
- subtitle: Joi19.string().optional().allow("", null),
7677
- download: Joi19.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)
7678
8055
  });
7679
8056
  const query = { ...req.query };
7680
8057
  const { error, value } = validation.validate(query);