@7365admin1/module-hygiene 4.30.3 → 4.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1002,6 +1002,11 @@ function capitalizeText(text) {
1002
1002
  return text.toLowerCase().split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1003
1003
  }
1004
1004
 
1005
+ // src/utils/escape-regex.util.ts
1006
+ function escapeRegex(value) {
1007
+ return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1008
+ }
1009
+
1005
1010
  // src/repositories/hygiene-area.repository.ts
1006
1011
  function useAreaRepo() {
1007
1012
  const db = import_node_server_utils4.useAtlas.getDb();
@@ -1098,7 +1103,7 @@ function useAreaRepo() {
1098
1103
  cacheOptions.type = type;
1099
1104
  }
1100
1105
  if (search) {
1101
- query.$or = [{ name: { $regex: search, $options: "i" } }];
1106
+ query.$or = [{ name: { $regex: escapeRegex(search), $options: "i" } }];
1102
1107
  cacheOptions.search = search;
1103
1108
  }
1104
1109
  const cacheKey = (0, import_node_server_utils4.makeCacheKey)(namespace_collection, cacheOptions);
@@ -1560,7 +1565,7 @@ function useUnitRepository() {
1560
1565
  throw new import_node_server_utils7.BadRequestError("Invalid site ID format.");
1561
1566
  }
1562
1567
  if (search) {
1563
- query.$or = [{ name: { $regex: search, $options: "i" } }];
1568
+ query.$or = [{ name: { $regex: escapeRegex(search), $options: "i" } }];
1564
1569
  cacheOptions.search = search;
1565
1570
  }
1566
1571
  const cacheKey = (0, import_node_server_utils7.makeCacheKey)(namespace_collection, cacheOptions);
@@ -3228,7 +3233,9 @@ var areaChecklistSchema = import_joi9.default.object({
3228
3233
  }).required()
3229
3234
  ).min(1).required(),
3230
3235
  isScheduleTask: import_joi9.default.boolean().optional().default(false),
3231
- scheduleTaskId: import_joi9.default.string().hex().optional().allow(null)
3236
+ scheduleTaskId: import_joi9.default.string().hex().optional().allow(null),
3237
+ scheduleTaskTitle: import_joi9.default.string().optional().allow("", null),
3238
+ scheduleTaskTime: import_joi9.default.string().optional().allow("", null)
3232
3239
  }).required()
3233
3240
  ).optional().default([])
3234
3241
  });
@@ -3258,6 +3265,8 @@ function MAreaChecklist(value) {
3258
3265
  set: checklistItem.set,
3259
3266
  isScheduleTask: checklistItem.isScheduleTask ?? false,
3260
3267
  scheduleTaskId: checklistItem.scheduleTaskId ?? void 0,
3268
+ scheduleTaskTitle: checklistItem.scheduleTaskTitle ?? void 0,
3269
+ scheduleTaskTime: checklistItem.scheduleTaskTime ?? void 0,
3261
3270
  units: checklistItem.units.map((unit) => ({
3262
3271
  unit: new import_mongodb8.ObjectId(unit.unit),
3263
3272
  name: unit.name,
@@ -3740,6 +3749,9 @@ function useAreaChecklistRepo() {
3740
3749
  isScheduleTask: {
3741
3750
  $ifNull: ["$checklist.isScheduleTask", false]
3742
3751
  },
3752
+ scheduleTaskId: { $ifNull: ["$checklist.scheduleTaskId", null] },
3753
+ scheduleTaskTitle: { $ifNull: ["$checklist.scheduleTaskTitle", null] },
3754
+ scheduleTaskTime: { $ifNull: ["$checklist.scheduleTaskTime", null] },
3743
3755
  unit: "$checklist.units.unit",
3744
3756
  name: "$checklist.units.name",
3745
3757
  approve: "$checklist.units.approve",
@@ -3776,6 +3788,9 @@ function useAreaChecklistRepo() {
3776
3788
  $group: {
3777
3789
  _id: "$set",
3778
3790
  isScheduleTask: { $first: "$isScheduleTask" },
3791
+ scheduleTaskId: { $first: "$scheduleTaskId" },
3792
+ scheduleTaskTitle: { $first: "$scheduleTaskTitle" },
3793
+ scheduleTaskTime: { $first: "$scheduleTaskTime" },
3779
3794
  units: {
3780
3795
  $push: {
3781
3796
  unit: "$unit",
@@ -3796,6 +3811,9 @@ function useAreaChecklistRepo() {
3796
3811
  _id: 0,
3797
3812
  set: "$_id",
3798
3813
  isScheduleTask: 1,
3814
+ scheduleTaskId: 1,
3815
+ scheduleTaskTitle: 1,
3816
+ scheduleTaskTime: 1,
3799
3817
  units: 1
3800
3818
  }
3801
3819
  },
@@ -3927,6 +3945,8 @@ function useAreaChecklistRepo() {
3927
3945
  isScheduleTask: {
3928
3946
  $ifNull: ["$checklist.isScheduleTask", false]
3929
3947
  },
3948
+ scheduleTaskTitle: { $ifNull: ["$checklist.scheduleTaskTitle", null] },
3949
+ scheduleTaskTime: { $ifNull: ["$checklist.scheduleTaskTime", null] },
3930
3950
  scheduleTaskId: {
3931
3951
  $ifNull: ["$checklist.scheduleTaskId", null]
3932
3952
  },
@@ -3967,6 +3987,8 @@ function useAreaChecklistRepo() {
3967
3987
  $group: {
3968
3988
  _id: "$set",
3969
3989
  isScheduleTask: { $first: "$isScheduleTask" },
3990
+ scheduleTaskTitle: { $first: "$scheduleTaskTitle" },
3991
+ scheduleTaskTime: { $first: "$scheduleTaskTime" },
3970
3992
  scheduleTaskId: { $first: "$scheduleTaskId" },
3971
3993
  remarks: { $first: "$remarks" },
3972
3994
  attachment: { $first: "$attachment" },
@@ -3991,6 +4013,8 @@ function useAreaChecklistRepo() {
3991
4013
  _id: 0,
3992
4014
  set: "$_id",
3993
4015
  isScheduleTask: 1,
4016
+ scheduleTaskTitle: 1,
4017
+ scheduleTaskTime: 1,
3994
4018
  scheduleTaskId: 1,
3995
4019
  remarks: "$remarks",
3996
4020
  attachment: "$attachment",
@@ -4466,6 +4490,265 @@ function useAreaChecklistRepo() {
4466
4490
  };
4467
4491
  }
4468
4492
 
4493
+ // src/utils/schedule-task-rules.util.ts
4494
+ var import_joi10 = __toESM(require("joi"));
4495
+ var REPEATS = ["none", "daily", "weekdays", "weekly", "monthly"];
4496
+ var SGT_MS = 8 * 60 * 60 * 1e3;
4497
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4498
+ var TIME_RE = /^([0-1]\d|2[0-3]):([0-5]\d)$/;
4499
+ var pad = (n) => String(n).padStart(2, "0");
4500
+ function sgtParts(now) {
4501
+ const s = new Date(now.getTime() + SGT_MS);
4502
+ const date = `${s.getUTCFullYear()}-${pad(s.getUTCMonth() + 1)}-${pad(s.getUTCDate())}`;
4503
+ return { date, minutes: s.getUTCHours() * 60 + s.getUTCMinutes(), weekday: s.getUTCDay() };
4504
+ }
4505
+ function isRealDate(value) {
4506
+ if (typeof value !== "string" || !DATE_RE.test(value))
4507
+ return false;
4508
+ const [y, m, d] = value.split("-").map(Number);
4509
+ const t = new Date(Date.UTC(y, m - 1, d));
4510
+ return t.getUTCFullYear() === y && t.getUTCMonth() === m - 1 && t.getUTCDate() === d;
4511
+ }
4512
+ var toMinutes = (time) => {
4513
+ const [h, m] = time.split(":").map(Number);
4514
+ return h * 60 + m;
4515
+ };
4516
+ var weekdayOf = (date) => {
4517
+ const [y, m, d] = date.split("-").map(Number);
4518
+ return new Date(Date.UTC(y, m - 1, d)).getUTCDay();
4519
+ };
4520
+ var daysInMonth = (date) => {
4521
+ const [y, m] = date.split("-").map(Number);
4522
+ return new Date(Date.UTC(y, m, 0)).getUTCDate();
4523
+ };
4524
+ var addDays = (date, n) => {
4525
+ const [y, m, d] = date.split("-").map(Number);
4526
+ const t = new Date(Date.UTC(y, m - 1, d + n));
4527
+ return `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}-${pad(t.getUTCDate())}`;
4528
+ };
4529
+ var isRepeating = (task) => Boolean(task.repeat) && task.repeat !== "none";
4530
+ function occursOn(task, date) {
4531
+ if (task.endDate && date > task.endDate)
4532
+ return false;
4533
+ if (!isRepeating(task))
4534
+ return Array.isArray(task.dates) && task.dates.includes(date);
4535
+ if (task.startDate && date < task.startDate)
4536
+ return false;
4537
+ const weekday = weekdayOf(date);
4538
+ switch (task.repeat) {
4539
+ case "daily":
4540
+ return true;
4541
+ case "weekdays":
4542
+ return weekday >= 1 && weekday <= 5;
4543
+ case "weekly":
4544
+ return Array.isArray(task.daysOfWeek) && task.daysOfWeek.includes(weekday);
4545
+ case "monthly": {
4546
+ const wanted = task.dayOfMonth ?? (task.startDate ? Number(task.startDate.slice(8, 10)) : 1);
4547
+ return Number(date.slice(8, 10)) === Math.min(wanted, daysInMonth(date));
4548
+ }
4549
+ default:
4550
+ return false;
4551
+ }
4552
+ }
4553
+ function savedTodayAt(task, today) {
4554
+ const stamp = task.updatedAt || task.createdAt;
4555
+ if (!stamp)
4556
+ return null;
4557
+ const saved = new Date(stamp);
4558
+ if (Number.isNaN(saved.getTime()))
4559
+ return null;
4560
+ const p = sgtParts(saved);
4561
+ return p.date === today ? p.minutes : null;
4562
+ }
4563
+ function shouldRunNow(task, now = /* @__PURE__ */ new Date()) {
4564
+ if (task.paused || !TIME_RE.test(task.time ?? ""))
4565
+ return false;
4566
+ const { date, minutes } = sgtParts(now);
4567
+ if (task.lastRunDate === date || !occursOn(task, date))
4568
+ return false;
4569
+ const due = toMinutes(task.time);
4570
+ if (minutes < due)
4571
+ return false;
4572
+ const saved = savedTodayAt(task, date);
4573
+ return !(saved !== null && saved >= due);
4574
+ }
4575
+ var iso = (date, time) => `${date}T${time}:00+08:00`;
4576
+ function upcoming(task, now) {
4577
+ if (typeof task.time !== "string" || !TIME_RE.test(task.time))
4578
+ return null;
4579
+ const { date: today, minutes } = sgtParts(now);
4580
+ const due = toMinutes(task.time);
4581
+ if (occursOn(task, today) && (minutes < due || shouldRunNow({ ...task, paused: false }, now))) {
4582
+ return iso(today, task.time);
4583
+ }
4584
+ if (!isRepeating(task)) {
4585
+ const later = (task.dates ?? []).filter((d) => d > today && (!task.endDate || d <= task.endDate)).sort();
4586
+ return later.length ? iso(later[0], task.time) : null;
4587
+ }
4588
+ const from = task.startDate && task.startDate > today ? task.startDate : today;
4589
+ for (let i = from === today ? 1 : 0; i <= 400; i++) {
4590
+ const d = addDays(from, i);
4591
+ if (task.endDate && d > task.endDate)
4592
+ return null;
4593
+ if (occursOn(task, d))
4594
+ return iso(d, task.time);
4595
+ }
4596
+ return null;
4597
+ }
4598
+ function nextRun(task, now = /* @__PURE__ */ new Date()) {
4599
+ return task.paused ? null : upcoming(task, now);
4600
+ }
4601
+ function scheduleState(task, now = /* @__PURE__ */ new Date()) {
4602
+ if (task.paused)
4603
+ return "paused";
4604
+ return upcoming(task, now) ? "active" : "ended";
4605
+ }
4606
+ function lastRun(task) {
4607
+ return task.lastRunDate && TIME_RE.test(task.time ?? "") ? iso(task.lastRunDate, task.time) : null;
4608
+ }
4609
+ function scheduleDisplay(task, now = /* @__PURE__ */ new Date()) {
4610
+ try {
4611
+ return {
4612
+ nextRun: nextRun(task, now),
4613
+ lastRun: lastRun(task),
4614
+ scheduleStatus: scheduleState(task, now)
4615
+ };
4616
+ } catch {
4617
+ return { nextRun: null, lastRun: null, scheduleStatus: null };
4618
+ }
4619
+ }
4620
+ function validateScheduleRules(task, now = /* @__PURE__ */ new Date()) {
4621
+ const repeating = isRepeating(task);
4622
+ const { date: today, minutes } = sgtParts(now);
4623
+ if (repeating) {
4624
+ if (!task.startDate)
4625
+ return "Please choose a start date for a repeating schedule.";
4626
+ if (task.repeat === "weekly" && !(task.daysOfWeek && task.daysOfWeek.length)) {
4627
+ return "Pick at least one weekday for a weekly repeat.";
4628
+ }
4629
+ }
4630
+ const start = repeating ? task.startDate : [...task.dates ?? []].sort()[0];
4631
+ if (task.endDate && start && start > task.endDate) {
4632
+ return "The end date must be on or after the start date.";
4633
+ }
4634
+ if (task.endDate && task.endDate < today)
4635
+ return "The end date has already passed.";
4636
+ if (!repeating) {
4637
+ const dates = (task.dates ?? []).filter((d) => !task.endDate || d <= task.endDate);
4638
+ if (dates.length && dates.every((d) => d < today)) {
4639
+ return "All the dates you picked have already passed. Pick today or a later date.";
4640
+ }
4641
+ if (dates.length && dates.every((d) => d <= today) && dates.includes(today) && TIME_RE.test(task.time) && toMinutes(task.time) <= minutes) {
4642
+ return "That time has already passed today. Pick a later time or another date.";
4643
+ }
4644
+ }
4645
+ if (!upcoming({ ...task, paused: false, updatedAt: now }, now)) {
4646
+ return "This schedule would never run. Check the dates, end date and time.";
4647
+ }
4648
+ return null;
4649
+ }
4650
+ var scheduleDate = import_joi10.default.string().pattern(DATE_RE).custom((value, helpers) => isRealDate(value) ? value : helpers.error("date.invalid")).messages({
4651
+ "string.pattern.base": "Dates must look like 2026-10-21.",
4652
+ "string.empty": "Please choose a date.",
4653
+ "date.invalid": "That date does not exist. Dates must look like 2026-10-21.",
4654
+ "string.base": "Dates must look like 2026-10-21."
4655
+ });
4656
+ var REPEATING = import_joi10.default.valid("daily", "weekdays", "weekly", "monthly").required();
4657
+ var scheduleTaskFields = {
4658
+ title: import_joi10.default.string().trim().required().messages({
4659
+ "any.required": "Please enter a title.",
4660
+ "string.empty": "Please enter a title.",
4661
+ "string.base": "Please enter a title."
4662
+ }),
4663
+ time: import_joi10.default.string().pattern(TIME_RE).required().messages({
4664
+ "any.required": "Please choose a time.",
4665
+ "string.empty": "Please choose a time.",
4666
+ "string.pattern.base": "Time must be in 24-hour format like 08:30.",
4667
+ "string.base": "Time must be in 24-hour format like 08:30."
4668
+ }),
4669
+ dates: import_joi10.default.when("repeat", {
4670
+ is: REPEATING,
4671
+ then: import_joi10.default.array().items(scheduleDate).optional().default([]),
4672
+ otherwise: import_joi10.default.array().min(1).items(scheduleDate).required()
4673
+ }).messages({
4674
+ "any.required": "Pick at least one date, or choose a repeat.",
4675
+ "array.min": "Pick at least one date, or choose a repeat.",
4676
+ "array.base": "Dates must be a list of dates."
4677
+ }),
4678
+ description: import_joi10.default.string().optional().allow("", null),
4679
+ areas: import_joi10.default.array().min(1).items(
4680
+ import_joi10.default.object({
4681
+ name: import_joi10.default.string().required(),
4682
+ value: import_joi10.default.any().required()
4683
+ })
4684
+ ).required().messages({
4685
+ "any.required": "Pick at least one area.",
4686
+ "array.min": "Pick at least one area.",
4687
+ "array.base": "Pick at least one area."
4688
+ }),
4689
+ repeat: import_joi10.default.string().valid(...REPEATS).optional().allow("", null).messages({ "any.only": "Repeat must be one of: none, daily, weekdays, weekly, monthly." }),
4690
+ daysOfWeek: import_joi10.default.array().items(import_joi10.default.number().integer().min(0).max(6)).unique().optional().allow(null).messages({
4691
+ "number.base": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4692
+ "number.min": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4693
+ "number.max": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4694
+ "number.integer": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4695
+ "array.unique": "Each weekday can only be picked once."
4696
+ }),
4697
+ dayOfMonth: import_joi10.default.number().integer().min(1).max(31).optional().allow(null).messages({ "number.base": "Day of the month must be 1 to 31.", "number.min": "Day of the month must be 1 to 31.", "number.max": "Day of the month must be 1 to 31." }),
4698
+ startDate: scheduleDate.optional().allow("", null),
4699
+ endDate: scheduleDate.optional().allow("", null),
4700
+ paused: import_joi10.default.boolean().optional().allow(null).messages({ "boolean.base": "Paused must be true or false." })
4701
+ };
4702
+ var OPTIONAL_KEYS = ["repeat", "daysOfWeek", "dayOfMonth", "startDate", "endDate", "paused"];
4703
+ var isBlank = (v) => v === "" || v === null;
4704
+ function cleanScheduleInput(value) {
4705
+ const out = { ...value };
4706
+ for (const key of OPTIONAL_KEYS)
4707
+ if (isBlank(out[key]))
4708
+ delete out[key];
4709
+ if (out.repeat === "none") {
4710
+ delete out.daysOfWeek;
4711
+ delete out.dayOfMonth;
4712
+ delete out.startDate;
4713
+ }
4714
+ return out;
4715
+ }
4716
+ var TIMING_KEYS = ["time", "dates", "repeat", "daysOfWeek", "dayOfMonth", "startDate", "endDate"];
4717
+ function splitScheduleUpdate(payload) {
4718
+ const set = {};
4719
+ const unset = [];
4720
+ for (const [key, value] of Object.entries(payload)) {
4721
+ if (OPTIONAL_KEYS.includes(key) && isBlank(value))
4722
+ unset.push(key);
4723
+ else if (value !== void 0)
4724
+ set[key] = value;
4725
+ }
4726
+ if (set.repeat === "none") {
4727
+ for (const key of ["daysOfWeek", "dayOfMonth", "startDate"]) {
4728
+ if (!unset.includes(key))
4729
+ unset.push(key);
4730
+ delete set[key];
4731
+ }
4732
+ }
4733
+ return { set, unset };
4734
+ }
4735
+ function regularSetsMissing(checklist, setCount) {
4736
+ const regular = (checklist ?? []).filter((c) => c.isScheduleTask !== true).length;
4737
+ return Math.max(0, setCount - regular);
4738
+ }
4739
+ var normTiming = (t) => ({
4740
+ time: t.time ?? null,
4741
+ dates: [...Array.isArray(t.dates) ? t.dates : []].sort(),
4742
+ repeat: !t.repeat || t.repeat === "none" ? "none" : t.repeat,
4743
+ daysOfWeek: [...Array.isArray(t.daysOfWeek) ? t.daysOfWeek : []].sort(),
4744
+ dayOfMonth: t.dayOfMonth ?? null,
4745
+ startDate: t.startDate || null,
4746
+ endDate: t.endDate || null
4747
+ });
4748
+ function timingChanged(stored, after) {
4749
+ return JSON.stringify(normTiming(stored)) !== JSON.stringify(normTiming(after));
4750
+ }
4751
+
4469
4752
  // src/services/hygiene-area-checklist.service.ts
4470
4753
  function useAreaChecklistService() {
4471
4754
  const { getAreasForChecklist } = useAreaRepo();
@@ -4533,10 +4816,10 @@ function useAreaChecklistService() {
4533
4816
  `Area checklist for area ${area.name} exists but has no auto-gen sets. Inserting auto-gen sets.`
4534
4817
  );
4535
4818
  const setCount2 = Number(area.set) || 1;
4536
- const totalExistingSets = existing.checklist?.length ?? 0;
4537
- if (totalExistingSets >= setCount2) {
4819
+ const setsToAdd = regularSetsMissing(existing.checklist, setCount2);
4820
+ if (setsToAdd <= 0) {
4538
4821
  import_node_server_utils18.logger.info(
4539
- `Area checklist for area ${area.name} already has ${totalExistingSets} set(s) (configured: ${setCount2}). Skipping auto-gen sets.`
4822
+ `Area checklist for area ${area.name} already has its ${setCount2} regular set(s). Skipping auto-gen sets.`
4540
4823
  );
4541
4824
  return existing._id;
4542
4825
  }
@@ -4544,7 +4827,6 @@ function useAreaChecklistService() {
4544
4827
  (max, c) => Math.max(max, c.set ?? 0),
4545
4828
  0
4546
4829
  ) ?? 0;
4547
- const setsToAdd = setCount2 - totalExistingSets;
4548
4830
  const autoGenSets = Array.from(
4549
4831
  { length: setsToAdd },
4550
4832
  (_, index) => ({
@@ -4705,7 +4987,7 @@ function useAreaChecklistService() {
4705
4987
  }
4706
4988
 
4707
4989
  // src/controllers/hygiene-area-checklist.controller.ts
4708
- var import_joi10 = __toESM(require("joi"));
4990
+ var import_joi11 = __toESM(require("joi"));
4709
4991
  var import_node_server_utils20 = require("@7365admin1/node-server-utils");
4710
4992
  var import_core11 = require("@7365admin1/core");
4711
4993
 
@@ -4713,6 +4995,8 @@ var import_core11 = require("@7365admin1/core");
4713
4995
  var import_puppeteer = require("puppeteer");
4714
4996
  var import_node_server_utils19 = require("@7365admin1/node-server-utils");
4715
4997
  var import_mongodb11 = require("mongodb");
4998
+
4999
+ // src/utils/checklist-pdf-html.util.ts
4716
5000
  function escapeHtml(text) {
4717
5001
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
4718
5002
  }
@@ -4725,7 +5009,7 @@ function buildChecklistHtml(areaChecklists) {
4725
5009
  const unitRows = (setItem.units || []).map(
4726
5010
  (unit) => `
4727
5011
  <tr>
4728
- <td class="unit-name">${escapeHtml(unit.name || "")}</td>
5012
+ <td class="unit-name">${escapeHtml(unit.name || "")}${unit.approve || unit.reject ? "" : ' <span class="missed-label">Missed</span>'}</td>
4729
5013
  <td class="icon-cell">${unit.reject ? '<span class="reject-icon">&#10007;</span>' : ""}</td>
4730
5014
  <td class="icon-cell">${unit.approve ? '<span class="approve-icon">&#10003;</span>' : ""}</td>
4731
5015
  </tr>`
@@ -4862,6 +5146,12 @@ function buildChecklistHtml(areaChecklists) {
4862
5146
  font-size: 16px;
4863
5147
  font-weight: bold;
4864
5148
  }
5149
+ .missed-label {
5150
+ color: #b3261e;
5151
+ font-size: 11px;
5152
+ font-weight: bold;
5153
+ margin-left: 6px;
5154
+ }
4865
5155
  .reject-icon {
4866
5156
  color: #e74c3c;
4867
5157
  font-size: 16px;
@@ -4877,6 +5167,8 @@ function buildChecklistHtml(areaChecklists) {
4877
5167
  </body>
4878
5168
  </html>`;
4879
5169
  }
5170
+
5171
+ // src/services/hygiene-checklist-pdf.service.ts
4880
5172
  function useChecklistPdfService() {
4881
5173
  async function generateChecklistPdf(scheduleId) {
4882
5174
  const db = import_node_server_utils19.useAtlas.getDb();
@@ -4946,9 +5238,9 @@ function useAreaChecklistController() {
4946
5238
  site: req.params.site,
4947
5239
  schedule: req.params.schedule
4948
5240
  };
4949
- const validation = import_joi10.default.object({
4950
- site: import_joi10.default.string().hex().required(),
4951
- schedule: import_joi10.default.string().hex().required()
5241
+ const validation = import_joi11.default.object({
5242
+ site: import_joi11.default.string().hex().required(),
5243
+ schedule: import_joi11.default.string().hex().required()
4952
5244
  });
4953
5245
  const { error, value } = validation.validate(payload);
4954
5246
  if (error) {
@@ -4968,14 +5260,14 @@ function useAreaChecklistController() {
4968
5260
  }
4969
5261
  async function getAllAreaChecklist(req, res, next) {
4970
5262
  const query = { ...req.query, ...req.params };
4971
- const validation = import_joi10.default.object({
4972
- page: import_joi10.default.number().min(1).optional().allow("", null),
4973
- limit: import_joi10.default.number().min(1).optional().allow("", null),
4974
- search: import_joi10.default.string().optional().allow("", null),
4975
- type: import_joi10.default.string().valid(...allowedTypes, "all").optional().allow("", null),
4976
- status: import_joi10.default.string().valid(...allowedStatus, "all").optional().allow("", null),
4977
- schedule: import_joi10.default.string().hex().required(),
4978
- serviceType: import_joi10.default.string().valid(...Object.values(import_core11.AppServiceType)).required()
5263
+ const validation = import_joi11.default.object({
5264
+ page: import_joi11.default.number().min(1).optional().allow("", null),
5265
+ limit: import_joi11.default.number().min(1).optional().allow("", null),
5266
+ search: import_joi11.default.string().optional().allow("", null),
5267
+ type: import_joi11.default.string().valid(...allowedTypes, "all").optional().allow("", null),
5268
+ status: import_joi11.default.string().valid(...allowedStatus, "all").optional().allow("", null),
5269
+ schedule: import_joi11.default.string().hex().required(),
5270
+ serviceType: import_joi11.default.string().valid(...Object.values(import_core11.AppServiceType)).required()
4979
5271
  });
4980
5272
  const { error } = validation.validate(query);
4981
5273
  if (error) {
@@ -5010,14 +5302,14 @@ function useAreaChecklistController() {
5010
5302
  }
5011
5303
  async function getAreaChecklistHistory(req, res, next) {
5012
5304
  const query = { ...req.query, ...req.params };
5013
- const validation = import_joi10.default.object({
5014
- page: import_joi10.default.number().min(1).optional().allow("", null),
5015
- limit: import_joi10.default.number().min(1).optional().allow("", null),
5016
- search: import_joi10.default.string().optional().allow("", null),
5017
- type: import_joi10.default.string().optional().allow("", ...allowedTypes),
5018
- schedule: import_joi10.default.string().hex().required(),
5019
- status: import_joi10.default.string().allow("", null, ...allowedStatus),
5020
- createdAt: import_joi10.default.alternatives().try(import_joi10.default.date(), import_joi10.default.string()).optional().allow("", null)
5305
+ const validation = import_joi11.default.object({
5306
+ page: import_joi11.default.number().min(1).optional().allow("", null),
5307
+ limit: import_joi11.default.number().min(1).optional().allow("", null),
5308
+ search: import_joi11.default.string().optional().allow("", null),
5309
+ type: import_joi11.default.string().optional().allow("", ...allowedTypes),
5310
+ schedule: import_joi11.default.string().hex().required(),
5311
+ status: import_joi11.default.string().allow("", null, ...allowedStatus),
5312
+ createdAt: import_joi11.default.alternatives().try(import_joi11.default.date(), import_joi11.default.string()).optional().allow("", null)
5021
5313
  });
5022
5314
  const { error } = validation.validate(query);
5023
5315
  if (error) {
@@ -5051,7 +5343,7 @@ function useAreaChecklistController() {
5051
5343
  }
5052
5344
  }
5053
5345
  async function getAreaChecklistHistoryDetails(req, res, next) {
5054
- const validation = import_joi10.default.string().hex().required();
5346
+ const validation = import_joi11.default.string().hex().required();
5055
5347
  const _id = req.params.id;
5056
5348
  const { error, value } = validation.validate(_id);
5057
5349
  if (error) {
@@ -5071,12 +5363,12 @@ function useAreaChecklistController() {
5071
5363
  }
5072
5364
  async function getAreaChecklistUnits(req, res, next) {
5073
5365
  const query = { ...req.query, ...req.params };
5074
- const validation = import_joi10.default.object({
5075
- page: import_joi10.default.number().min(1).optional().allow("", null),
5076
- limit: import_joi10.default.number().min(1).optional().allow("", null),
5077
- search: import_joi10.default.string().optional().allow("", null),
5078
- serviceType: import_joi10.default.string().valid(...Object.values(import_core11.AppServiceType)).required(),
5079
- id: import_joi10.default.string().hex().required()
5366
+ const validation = import_joi11.default.object({
5367
+ page: import_joi11.default.number().min(1).optional().allow("", null),
5368
+ limit: import_joi11.default.number().min(1).optional().allow("", null),
5369
+ search: import_joi11.default.string().optional().allow("", null),
5370
+ serviceType: import_joi11.default.string().valid(...Object.values(import_core11.AppServiceType)).required(),
5371
+ id: import_joi11.default.string().hex().required()
5080
5372
  });
5081
5373
  const { error } = validation.validate(query);
5082
5374
  if (error) {
@@ -5126,21 +5418,21 @@ function useAreaChecklistController() {
5126
5418
  ...decisionValues,
5127
5419
  completedBy
5128
5420
  };
5129
- const validation = import_joi10.default.object({
5130
- id: import_joi10.default.string().hex().required(),
5131
- set: import_joi10.default.number().integer().min(1).required(),
5132
- unit: import_joi10.default.string().hex().required(),
5133
- decision: import_joi10.default.string().valid("approve", "reject").required(),
5134
- approve: import_joi10.default.boolean().optional(),
5135
- reject: import_joi10.default.boolean().optional(),
5136
- remarks: import_joi10.default.string().optional().allow("", null),
5421
+ const validation = import_joi11.default.object({
5422
+ id: import_joi11.default.string().hex().required(),
5423
+ set: import_joi11.default.number().integer().min(1).required(),
5424
+ unit: import_joi11.default.string().hex().required(),
5425
+ decision: import_joi11.default.string().valid("approve", "reject").required(),
5426
+ approve: import_joi11.default.boolean().optional(),
5427
+ reject: import_joi11.default.boolean().optional(),
5428
+ remarks: import_joi11.default.string().optional().allow("", null),
5137
5429
  /*
5138
5430
  * Shape only. WHETHER a photo is required depends on the set's current
5139
5431
  * state in the database, which Joi cannot see — see the check below.
5140
5432
  * Each entry must still be a real id: a blank string carries no evidence.
5141
5433
  */
5142
- attachment: import_joi10.default.array().items(import_joi10.default.string().trim().min(1)).optional(),
5143
- completedBy: import_joi10.default.string().hex().required()
5434
+ attachment: import_joi11.default.array().items(import_joi11.default.string().trim().min(1)).optional(),
5435
+ completedBy: import_joi11.default.string().hex().required()
5144
5436
  });
5145
5437
  const { error } = validation.validate(payload);
5146
5438
  if (error) {
@@ -5177,7 +5469,7 @@ function useAreaChecklistController() {
5177
5469
  }
5178
5470
  }
5179
5471
  async function downloadChecklistPdf(req, res, next) {
5180
- const validation = import_joi10.default.string().hex().required();
5472
+ const validation = import_joi11.default.string().hex().required();
5181
5473
  const schedule = req.params.schedule;
5182
5474
  const { error, value } = validation.validate(schedule);
5183
5475
  if (error) {
@@ -5223,16 +5515,16 @@ function useAreaChecklistController() {
5223
5515
  }
5224
5516
 
5225
5517
  // src/models/hygiene-supply.model.ts
5226
- var import_joi11 = __toESM(require("joi"));
5518
+ var import_joi12 = __toESM(require("joi"));
5227
5519
  var import_mongodb12 = require("mongodb");
5228
5520
  var import_node_server_utils21 = require("@7365admin1/node-server-utils");
5229
5521
  var import_core12 = require("@7365admin1/core");
5230
- var supplySchema = import_joi11.default.object({
5231
- site: import_joi11.default.string().hex().required(),
5232
- serviceType: import_joi11.default.string().valid(...Object.values(import_core12.AppServiceType)).required(),
5233
- name: import_joi11.default.string().required(),
5234
- unitOfMeasurement: import_joi11.default.string().required(),
5235
- attachment: import_joi11.default.string().allow(null, "").optional()
5522
+ var supplySchema = import_joi12.default.object({
5523
+ site: import_joi12.default.string().hex().required(),
5524
+ serviceType: import_joi12.default.string().valid(...Object.values(import_core12.AppServiceType)).required(),
5525
+ name: import_joi12.default.string().required(),
5526
+ unitOfMeasurement: import_joi12.default.string().required(),
5527
+ attachment: import_joi12.default.string().allow(null, "").optional()
5236
5528
  });
5237
5529
  function MSupply(value) {
5238
5530
  const { error } = supplySchema.validate(value);
@@ -5360,7 +5652,7 @@ function useSupplyRepository() {
5360
5652
  throw new import_node_server_utils22.BadRequestError("Invalid site ID format.");
5361
5653
  }
5362
5654
  if (search) {
5363
- query.name = { $regex: search, $options: "i" };
5655
+ query.name = { $regex: escapeRegex(search), $options: "i" };
5364
5656
  cacheOptions.search = search;
5365
5657
  }
5366
5658
  const cacheKey = (0, import_node_server_utils22.makeCacheKey)(namespace_collection, cacheOptions);
@@ -5522,7 +5814,7 @@ function useSupplyRepository() {
5522
5814
  }
5523
5815
 
5524
5816
  // src/controllers/hygiene-supply.controller.ts
5525
- var import_joi12 = __toESM(require("joi"));
5817
+ var import_joi13 = __toESM(require("joi"));
5526
5818
  var import_node_server_utils23 = require("@7365admin1/node-server-utils");
5527
5819
  var import_core13 = require("@7365admin1/core");
5528
5820
  function useSupplyController() {
@@ -5553,12 +5845,12 @@ function useSupplyController() {
5553
5845
  }
5554
5846
  async function getSupplies(req, res, next) {
5555
5847
  const query = { ...req.query, ...req.params };
5556
- const validation = import_joi12.default.object({
5557
- page: import_joi12.default.number().min(1).optional().allow("", null),
5558
- limit: import_joi12.default.number().min(1).optional().allow("", null),
5559
- search: import_joi12.default.string().optional().allow("", null),
5560
- site: import_joi12.default.string().hex().required(),
5561
- serviceType: import_joi12.default.string().valid(...Object.values(import_core13.AppServiceType)).required()
5848
+ const validation = import_joi13.default.object({
5849
+ page: import_joi13.default.number().min(1).optional().allow("", null),
5850
+ limit: import_joi13.default.number().min(1).optional().allow("", null),
5851
+ search: import_joi13.default.string().optional().allow("", null),
5852
+ site: import_joi13.default.string().hex().required(),
5853
+ serviceType: import_joi13.default.string().valid(...Object.values(import_core13.AppServiceType)).required()
5562
5854
  });
5563
5855
  const { error } = validation.validate(query);
5564
5856
  if (error) {
@@ -5588,7 +5880,7 @@ function useSupplyController() {
5588
5880
  }
5589
5881
  }
5590
5882
  async function getSupplyById(req, res, next) {
5591
- const validation = import_joi12.default.string().hex().required();
5883
+ const validation = import_joi13.default.string().hex().required();
5592
5884
  const _id = req.params.id;
5593
5885
  const { error, value } = validation.validate(_id);
5594
5886
  if (error) {
@@ -5608,12 +5900,12 @@ function useSupplyController() {
5608
5900
  }
5609
5901
  async function updateSupply(req, res, next) {
5610
5902
  const payload = { id: req.params.id, ...req.body };
5611
- const validation = import_joi12.default.object({
5612
- id: import_joi12.default.string().hex().required(),
5613
- name: import_joi12.default.string().optional().allow("", null),
5614
- unitOfMeasurement: import_joi12.default.string().optional().allow("", null),
5615
- qty: import_joi12.default.number().min(0).optional().allow("", null),
5616
- attachment: import_joi12.default.string().optional().allow("", null)
5903
+ const validation = import_joi13.default.object({
5904
+ id: import_joi13.default.string().hex().required(),
5905
+ name: import_joi13.default.string().optional().allow("", null),
5906
+ unitOfMeasurement: import_joi13.default.string().optional().allow("", null),
5907
+ qty: import_joi13.default.number().min(0).optional().allow("", null),
5908
+ attachment: import_joi13.default.string().optional().allow("", null)
5617
5909
  });
5618
5910
  const { error } = validation.validate(payload);
5619
5911
  if (error) {
@@ -5634,7 +5926,7 @@ function useSupplyController() {
5634
5926
  }
5635
5927
  async function deleteSupply(req, res, next) {
5636
5928
  const id = req.params.id;
5637
- const validation = import_joi12.default.string().hex().required();
5929
+ const validation = import_joi13.default.string().hex().required();
5638
5930
  const { error, value } = validation.validate(id);
5639
5931
  if (error) {
5640
5932
  import_node_server_utils23.logger.log({ level: "error", message: error.message });
@@ -5661,18 +5953,18 @@ function useSupplyController() {
5661
5953
  }
5662
5954
 
5663
5955
  // src/models/hygiene-stock.model.ts
5664
- var import_joi13 = __toESM(require("joi"));
5956
+ var import_joi14 = __toESM(require("joi"));
5665
5957
  var import_mongodb14 = require("mongodb");
5666
5958
  var import_node_server_utils24 = require("@7365admin1/node-server-utils");
5667
5959
  var import_core14 = require("@7365admin1/core");
5668
- var stockSchema = import_joi13.default.object({
5669
- site: import_joi13.default.string().hex().required(),
5670
- serviceType: import_joi13.default.string().valid(...Object.values(import_core14.AppServiceType)).required(),
5671
- supply: import_joi13.default.string().hex().required(),
5672
- in: import_joi13.default.number().min(0).optional(),
5673
- out: import_joi13.default.number().min(0).optional(),
5674
- balance: import_joi13.default.number().min(0).required(),
5675
- remarks: import_joi13.default.string().optional().allow("", null)
5960
+ var stockSchema = import_joi14.default.object({
5961
+ site: import_joi14.default.string().hex().required(),
5962
+ serviceType: import_joi14.default.string().valid(...Object.values(import_core14.AppServiceType)).required(),
5963
+ supply: import_joi14.default.string().hex().required(),
5964
+ in: import_joi14.default.number().min(0).optional(),
5965
+ out: import_joi14.default.number().min(0).optional(),
5966
+ balance: import_joi14.default.number().min(0).required(),
5967
+ remarks: import_joi14.default.string().optional().allow("", null)
5676
5968
  });
5677
5969
  function MStock(value) {
5678
5970
  const { error } = stockSchema.validate(value);
@@ -5887,7 +6179,7 @@ function useStockService() {
5887
6179
  }
5888
6180
 
5889
6181
  // src/controllers/hygiene-stock.controller.ts
5890
- var import_joi14 = __toESM(require("joi"));
6182
+ var import_joi15 = __toESM(require("joi"));
5891
6183
  var import_node_server_utils27 = require("@7365admin1/node-server-utils");
5892
6184
  var import_core15 = require("@7365admin1/core");
5893
6185
  function useStockController() {
@@ -5895,12 +6187,12 @@ function useStockController() {
5895
6187
  const { createStock: _createStock } = useStockService();
5896
6188
  async function createStock(req, res, next) {
5897
6189
  const payload = { ...req.body, ...req.params };
5898
- const validation = import_joi14.default.object({
5899
- site: import_joi14.default.string().hex().required(),
5900
- serviceType: import_joi14.default.string().valid(...Object.values(import_core15.AppServiceType)).required(),
5901
- supply: import_joi14.default.string().hex().required(),
5902
- qty: import_joi14.default.number().min(0).required(),
5903
- remarks: import_joi14.default.string().optional().allow("", null)
6190
+ const validation = import_joi15.default.object({
6191
+ site: import_joi15.default.string().hex().required(),
6192
+ serviceType: import_joi15.default.string().valid(...Object.values(import_core15.AppServiceType)).required(),
6193
+ supply: import_joi15.default.string().hex().required(),
6194
+ qty: import_joi15.default.number().min(0).required(),
6195
+ remarks: import_joi15.default.string().optional().allow("", null)
5904
6196
  });
5905
6197
  const { error } = validation.validate(payload);
5906
6198
  if (error) {
@@ -5920,13 +6212,13 @@ function useStockController() {
5920
6212
  }
5921
6213
  async function getStocksBySupplyId(req, res, next) {
5922
6214
  const query = { ...req.query, ...req.params };
5923
- const validation = import_joi14.default.object({
5924
- page: import_joi14.default.number().min(1).optional().allow("", null),
5925
- limit: import_joi14.default.number().min(1).optional().allow("", null),
5926
- search: import_joi14.default.string().optional().allow("", null),
5927
- site: import_joi14.default.string().hex().required(),
5928
- serviceType: import_joi14.default.string().valid(...Object.values(import_core15.AppServiceType)).required(),
5929
- supply: import_joi14.default.string().hex().required()
6215
+ const validation = import_joi15.default.object({
6216
+ page: import_joi15.default.number().min(1).optional().allow("", null),
6217
+ limit: import_joi15.default.number().min(1).optional().allow("", null),
6218
+ search: import_joi15.default.string().optional().allow("", null),
6219
+ site: import_joi15.default.string().hex().required(),
6220
+ serviceType: import_joi15.default.string().valid(...Object.values(import_core15.AppServiceType)).required(),
6221
+ supply: import_joi15.default.string().hex().required()
5930
6222
  });
5931
6223
  const { error } = validation.validate(query);
5932
6224
  if (error) {
@@ -5988,21 +6280,21 @@ async function applyCheckOutDecision(decision, decide, returnStock) {
5988
6280
  }
5989
6281
 
5990
6282
  // src/models/hygiene-checkout-item.model.ts
5991
- var import_joi15 = __toESM(require("joi"));
6283
+ var import_joi16 = __toESM(require("joi"));
5992
6284
  var import_mongodb16 = require("mongodb");
5993
6285
  var import_node_server_utils28 = require("@7365admin1/node-server-utils");
5994
6286
  var import_core16 = require("@7365admin1/core");
5995
6287
  var allowedCheckOutItemStatus = ["pending", "completed"];
5996
- var checkOutItemSchema = import_joi15.default.object({
5997
- site: import_joi15.default.string().hex().required(),
5998
- serviceType: import_joi15.default.string().valid(...Object.values(import_core16.AppServiceType)).required(),
5999
- supply: import_joi15.default.string().hex().required(),
6000
- supplyName: import_joi15.default.string().required(),
6001
- supplyQty: import_joi15.default.number().min(0).required(),
6002
- qty: import_joi15.default.number().min(0).required(),
6003
- attachment: import_joi15.default.array().items(import_joi15.default.string()).optional().allow(null),
6004
- createdBy: import_joi15.default.string().hex().required(),
6005
- createdByName: import_joi15.default.string().required()
6288
+ var checkOutItemSchema = import_joi16.default.object({
6289
+ site: import_joi16.default.string().hex().required(),
6290
+ serviceType: import_joi16.default.string().valid(...Object.values(import_core16.AppServiceType)).required(),
6291
+ supply: import_joi16.default.string().hex().required(),
6292
+ supplyName: import_joi16.default.string().required(),
6293
+ supplyQty: import_joi16.default.number().min(0).required(),
6294
+ qty: import_joi16.default.number().min(0).required(),
6295
+ attachment: import_joi16.default.array().items(import_joi16.default.string()).optional().allow(null),
6296
+ createdBy: import_joi16.default.string().hex().required(),
6297
+ createdByName: import_joi16.default.string().required()
6006
6298
  });
6007
6299
  function MCheckOutItem(value) {
6008
6300
  const { error } = checkOutItemSchema.validate(value);
@@ -6126,7 +6418,7 @@ function useCheckOutItemRepository() {
6126
6418
  throw new import_node_server_utils29.BadRequestError("Invalid site ID format.");
6127
6419
  }
6128
6420
  if (search) {
6129
- query.supplyName = { $regex: search, $options: "i" };
6421
+ query.supplyName = { $regex: escapeRegex(search), $options: "i" };
6130
6422
  cacheOptions.search = search;
6131
6423
  }
6132
6424
  const cacheKey = (0, import_node_server_utils29.makeCacheKey)(namespace_collection, cacheOptions);
@@ -6482,7 +6774,7 @@ function useCheckOutItemService() {
6482
6774
  }
6483
6775
 
6484
6776
  // src/controllers/hygiene-checkout-item.controller.ts
6485
- var import_joi16 = __toESM(require("joi"));
6777
+ var import_joi17 = __toESM(require("joi"));
6486
6778
  var import_node_server_utils31 = require("@7365admin1/node-server-utils");
6487
6779
  var import_core18 = require("@7365admin1/core");
6488
6780
  function useCheckOutItemController() {
@@ -6506,13 +6798,13 @@ function useCheckOutItemController() {
6506
6798
  ...req.params,
6507
6799
  createdBy
6508
6800
  };
6509
- const validation = import_joi16.default.object({
6510
- site: import_joi16.default.string().hex().required(),
6511
- serviceType: import_joi16.default.string().valid(...Object.values(import_core18.AppServiceType)).required(),
6512
- supply: import_joi16.default.string().hex().required(),
6513
- qty: import_joi16.default.number().min(0).required(),
6514
- attachment: import_joi16.default.array().items(import_joi16.default.string()).optional().allow(null),
6515
- createdBy: import_joi16.default.string().hex().required()
6801
+ const validation = import_joi17.default.object({
6802
+ site: import_joi17.default.string().hex().required(),
6803
+ serviceType: import_joi17.default.string().valid(...Object.values(import_core18.AppServiceType)).required(),
6804
+ supply: import_joi17.default.string().hex().required(),
6805
+ qty: import_joi17.default.number().min(0).required(),
6806
+ attachment: import_joi17.default.array().items(import_joi17.default.string()).optional().allow(null),
6807
+ createdBy: import_joi17.default.string().hex().required()
6516
6808
  });
6517
6809
  const { error } = validation.validate(payload);
6518
6810
  if (error) {
@@ -6541,15 +6833,15 @@ function useCheckOutItemController() {
6541
6833
  ...req.params,
6542
6834
  createdBy
6543
6835
  };
6544
- const validation = import_joi16.default.object({
6545
- site: import_joi16.default.string().hex().required(),
6546
- serviceType: import_joi16.default.string().valid(...Object.values(import_core18.AppServiceType)).required(),
6547
- createdBy: import_joi16.default.string().hex().required(),
6548
- items: import_joi16.default.array().items(
6549
- import_joi16.default.object({
6550
- supply: import_joi16.default.string().hex().required(),
6551
- qty: import_joi16.default.number().min(0).required(),
6552
- attachment: import_joi16.default.array().items(import_joi16.default.string()).optional().allow(null)
6836
+ const validation = import_joi17.default.object({
6837
+ site: import_joi17.default.string().hex().required(),
6838
+ serviceType: import_joi17.default.string().valid(...Object.values(import_core18.AppServiceType)).required(),
6839
+ createdBy: import_joi17.default.string().hex().required(),
6840
+ items: import_joi17.default.array().items(
6841
+ import_joi17.default.object({
6842
+ supply: import_joi17.default.string().hex().required(),
6843
+ qty: import_joi17.default.number().min(0).required(),
6844
+ attachment: import_joi17.default.array().items(import_joi17.default.string()).optional().allow(null)
6553
6845
  })
6554
6846
  ).min(1).required()
6555
6847
  });
@@ -6571,12 +6863,12 @@ function useCheckOutItemController() {
6571
6863
  }
6572
6864
  async function getCheckOutItems(req, res, next) {
6573
6865
  const query = { ...req.query, ...req.params };
6574
- const validation = import_joi16.default.object({
6575
- page: import_joi16.default.number().min(1).optional().allow("", null),
6576
- limit: import_joi16.default.number().min(1).optional().allow("", null),
6577
- search: import_joi16.default.string().optional().allow("", null),
6578
- site: import_joi16.default.string().hex().required(),
6579
- serviceType: import_joi16.default.string().valid(...Object.values(import_core18.AppServiceType)).required()
6866
+ const validation = import_joi17.default.object({
6867
+ page: import_joi17.default.number().min(1).optional().allow("", null),
6868
+ limit: import_joi17.default.number().min(1).optional().allow("", null),
6869
+ search: import_joi17.default.string().optional().allow("", null),
6870
+ site: import_joi17.default.string().hex().required(),
6871
+ serviceType: import_joi17.default.string().valid(...Object.values(import_core18.AppServiceType)).required()
6580
6872
  });
6581
6873
  const { error } = validation.validate(query);
6582
6874
  if (error) {
@@ -6606,7 +6898,7 @@ function useCheckOutItemController() {
6606
6898
  }
6607
6899
  }
6608
6900
  async function getCheckOutItemById(req, res, next) {
6609
- const validation = import_joi16.default.string().hex().required();
6901
+ const validation = import_joi17.default.string().hex().required();
6610
6902
  const _id = req.params.id;
6611
6903
  const { error, value } = validation.validate(_id);
6612
6904
  if (error) {
@@ -6645,13 +6937,13 @@ function useCheckOutItemController() {
6645
6937
  ...decisionValues,
6646
6938
  completedBy
6647
6939
  };
6648
- const validation = import_joi16.default.object({
6649
- id: import_joi16.default.string().hex().required(),
6650
- decision: import_joi16.default.string().valid("approve", "disapprove").required(),
6651
- approve: import_joi16.default.boolean().required(),
6652
- reject: import_joi16.default.boolean().required(),
6653
- remarks: import_joi16.default.string().optional().allow("", null),
6654
- completedBy: import_joi16.default.string().hex().required()
6940
+ const validation = import_joi17.default.object({
6941
+ id: import_joi17.default.string().hex().required(),
6942
+ decision: import_joi17.default.string().valid("approve", "disapprove").required(),
6943
+ approve: import_joi17.default.boolean().required(),
6944
+ reject: import_joi17.default.boolean().required(),
6945
+ remarks: import_joi17.default.string().optional().allow("", null),
6946
+ completedBy: import_joi17.default.string().hex().required()
6655
6947
  });
6656
6948
  const { error } = validation.validate(payload);
6657
6949
  if (error) {
@@ -6684,23 +6976,15 @@ function useCheckOutItemController() {
6684
6976
  // src/models/hygiene-schedule-task.model.ts
6685
6977
  var import_node_server_utils32 = require("@7365admin1/node-server-utils");
6686
6978
  var import_core19 = require("@7365admin1/core");
6687
- var import_joi17 = __toESM(require("joi"));
6979
+ var import_joi18 = __toESM(require("joi"));
6688
6980
  var import_mongodb18 = require("mongodb");
6689
- var scheduleTaskSchema = import_joi17.default.object({
6690
- site: import_joi17.default.string().hex().required(),
6691
- serviceType: import_joi17.default.string().valid(...Object.values(import_core19.AppServiceType)).required(),
6692
- title: import_joi17.default.string().required(),
6693
- time: import_joi17.default.string().pattern(/^([0-1]\d|2[0-3]):([0-5]\d)$/).required(),
6694
- dates: import_joi17.default.array().min(1).items(
6695
- import_joi17.default.string().pattern(/^\d{4}-\d{2}-\d{2}$/).required()
6696
- ).required(),
6697
- description: import_joi17.default.string().optional().allow("", null),
6698
- areas: import_joi17.default.array().min(1).items(
6699
- import_joi17.default.object({
6700
- name: import_joi17.default.string().required(),
6701
- value: import_joi17.default.any().required()
6702
- })
6703
- ).required()
6981
+ var scheduleTaskSchema = import_joi18.default.object({
6982
+ site: import_joi18.default.string().hex().required(),
6983
+ serviceType: import_joi18.default.string().valid(...Object.values(import_core19.AppServiceType)).required(),
6984
+ ...scheduleTaskFields
6985
+ }).custom((value, helpers) => {
6986
+ const message = validateScheduleRules(cleanScheduleInput(value));
6987
+ return message ? helpers.message({ custom: message }) : value;
6704
6988
  });
6705
6989
  function MScheduleTask(value) {
6706
6990
  const { error } = scheduleTaskSchema.validate(value);
@@ -6727,14 +7011,26 @@ function MScheduleTask(value) {
6727
7011
  }
6728
7012
  });
6729
7013
  }
7014
+ const extra = cleanScheduleInput({
7015
+ repeat: value.repeat,
7016
+ daysOfWeek: value.daysOfWeek,
7017
+ dayOfMonth: value.dayOfMonth,
7018
+ startDate: value.startDate,
7019
+ endDate: value.endDate,
7020
+ paused: value.paused
7021
+ });
7022
+ for (const key of Object.keys(extra))
7023
+ if (extra[key] === void 0)
7024
+ delete extra[key];
6730
7025
  return {
6731
7026
  site: value.site,
6732
7027
  serviceType: value.serviceType,
6733
7028
  title: value.title,
6734
7029
  time: value.time,
6735
- dates: value.dates,
7030
+ dates: value.dates ?? [],
6736
7031
  description: value.description,
6737
7032
  areas: value.areas,
7033
+ ...extra,
6738
7034
  status: "active",
6739
7035
  createdAt: /* @__PURE__ */ new Date(),
6740
7036
  updatedAt: "",
@@ -6745,11 +7041,23 @@ function MScheduleTask(value) {
6745
7041
  // src/repositories/hygiene-schedule-task.repository.ts
6746
7042
  var import_mongodb19 = require("mongodb");
6747
7043
  var import_node_server_utils33 = require("@7365admin1/node-server-utils");
7044
+ var SCHEDULE_FIELDS = {
7045
+ time: 1,
7046
+ dates: 1,
7047
+ repeat: 1,
7048
+ daysOfWeek: 1,
7049
+ dayOfMonth: 1,
7050
+ startDate: 1,
7051
+ endDate: 1,
7052
+ paused: 1,
7053
+ lastRunDate: 1
7054
+ };
6748
7055
  function useScheduleTaskRepository() {
6749
7056
  const db = import_node_server_utils33.useAtlas.getDb();
6750
7057
  if (!db) {
6751
7058
  throw new import_node_server_utils33.InternalServerError("Unable to connect to server.");
6752
7059
  }
7060
+ const database = db;
6753
7061
  const namespace_collection = "site.schedule-tasks";
6754
7062
  const collection = db.collection(namespace_collection);
6755
7063
  const { delNamespace, setCache, getCache } = (0, import_node_server_utils33.useCache)(namespace_collection);
@@ -6817,14 +7125,14 @@ function useScheduleTaskRepository() {
6817
7125
  throw new import_node_server_utils33.BadRequestError("Invalid site ID format.");
6818
7126
  }
6819
7127
  if (search) {
6820
- query.$or = [{ title: { $regex: search, $options: "i" } }];
7128
+ query.$or = [{ title: { $regex: escapeRegex(search), $options: "i" } }];
6821
7129
  cacheOptions.search = search;
6822
7130
  }
6823
7131
  const cacheKey = (0, import_node_server_utils33.makeCacheKey)(namespace_collection, cacheOptions);
6824
7132
  const cachedData = await getCache(cacheKey);
6825
7133
  if (cachedData) {
6826
7134
  import_node_server_utils33.logger.info(`Cache hit for key: ${cacheKey}`);
6827
- return cachedData;
7135
+ return enrichList(cachedData, site, serviceType);
6828
7136
  }
6829
7137
  try {
6830
7138
  const items = await collection.aggregate([
@@ -6833,7 +7141,8 @@ function useScheduleTaskRepository() {
6833
7141
  $project: {
6834
7142
  title: 1,
6835
7143
  areas: 1,
6836
- status: 1
7144
+ status: 1,
7145
+ ...SCHEDULE_FIELDS
6837
7146
  }
6838
7147
  },
6839
7148
  { $sort: { _id: -1 } },
@@ -6847,15 +7156,104 @@ function useScheduleTaskRepository() {
6847
7156
  }).catch((err) => {
6848
7157
  import_node_server_utils33.logger.error(`Failed to set cache for key: ${cacheKey}`, err);
6849
7158
  });
6850
- return data;
7159
+ return enrichList(data, site, serviceType);
6851
7160
  } catch (error) {
6852
7161
  throw error;
6853
7162
  }
6854
7163
  }
6855
- async function getAllScheduleTask() {
7164
+ async function enrichList(data, site, serviceType) {
7165
+ const items = data?.items ?? [];
7166
+ const now = /* @__PURE__ */ new Date();
7167
+ let statuses = /* @__PURE__ */ new Map();
7168
+ try {
7169
+ statuses = await getRunStatuses(items, site, serviceType);
7170
+ } catch (error) {
7171
+ import_node_server_utils33.logger.error("Could not read schedule task run status", error);
7172
+ }
7173
+ return {
7174
+ ...data,
7175
+ items: items.map((item) => {
7176
+ try {
7177
+ return {
7178
+ ...item,
7179
+ ...scheduleDisplay(item, now),
7180
+ lastRunStatus: statuses.get(String(item._id)) ?? null
7181
+ };
7182
+ } catch (error) {
7183
+ import_node_server_utils33.logger.error(`Could not enrich schedule task ${item?._id}`, error);
7184
+ return { ...item, nextRun: null, lastRun: null, scheduleStatus: null, lastRunStatus: null };
7185
+ }
7186
+ })
7187
+ };
7188
+ }
7189
+ async function getRunStatuses(items, site, serviceType) {
7190
+ const result = /* @__PURE__ */ new Map();
7191
+ const ran = items.filter((i) => i.lastRunDate);
7192
+ if (!ran.length)
7193
+ return result;
7194
+ const parents = await database.collection("site.cleaning.schedules").find(
7195
+ {
7196
+ site,
7197
+ serviceType,
7198
+ dateStr: { $in: [...new Set(ran.map((i) => i.lastRunDate))] }
7199
+ },
7200
+ { projection: { dateStr: 1 } }
7201
+ ).toArray();
7202
+ if (!parents.length)
7203
+ return result;
7204
+ const dateOf = new Map(parents.map((p) => [String(p._id), p.dateStr]));
7205
+ const taskIds = ran.map((i) => new import_mongodb19.ObjectId(i._id));
7206
+ const rows = await database.collection("site.cleaning.schedule.areas").aggregate([
7207
+ {
7208
+ $match: {
7209
+ schedule: { $in: parents.map((p) => p._id) },
7210
+ "checklist.scheduleTaskId": { $in: taskIds }
7211
+ }
7212
+ },
7213
+ { $unwind: "$checklist" },
7214
+ { $match: { "checklist.scheduleTaskId": { $in: taskIds } } },
7215
+ { $unwind: "$checklist.units" },
7216
+ {
7217
+ $group: {
7218
+ _id: { task: "$checklist.scheduleTaskId", schedule: "$schedule" },
7219
+ total: { $sum: 1 },
7220
+ completed: {
7221
+ $sum: { $cond: [{ $eq: ["$checklist.units.status", "completed"] }, 1, 0] }
7222
+ },
7223
+ closed: {
7224
+ $sum: { $cond: [{ $eq: ["$checklist.units.status", "closed"] }, 1, 0] }
7225
+ }
7226
+ }
7227
+ }
7228
+ ]).toArray();
7229
+ const sums = /* @__PURE__ */ new Map();
7230
+ for (const row of rows) {
7231
+ const task = ran.find((i) => String(i._id) === String(row._id.task));
7232
+ if (!task || dateOf.get(String(row._id.schedule)) !== task.lastRunDate)
7233
+ continue;
7234
+ const sum = sums.get(String(task._id)) ?? { total: 0, completed: 0, closed: 0 };
7235
+ sum.total += row.total;
7236
+ sum.completed += row.completed;
7237
+ sum.closed += row.closed;
7238
+ sums.set(String(task._id), sum);
7239
+ }
7240
+ for (const [id, sum] of sums) {
7241
+ result.set(id, sum.completed === sum.total ? "done" : sum.closed > 0 ? "missed" : "pending");
7242
+ }
7243
+ return result;
7244
+ }
7245
+ async function getAllScheduleTask(today) {
6856
7246
  const query = {
6857
7247
  status: { $ne: "deleted" }
6858
7248
  };
7249
+ if (today) {
7250
+ query.paused = { $ne: true };
7251
+ query.lastRunDate = { $ne: today };
7252
+ query.$or = [
7253
+ { dates: today },
7254
+ { repeat: { $in: ["daily", "weekdays", "weekly", "monthly"] } }
7255
+ ];
7256
+ }
6859
7257
  try {
6860
7258
  const items = await collection.aggregate([{ $match: query }, { $sort: { _id: -1 } }]).toArray();
6861
7259
  return items;
@@ -6863,6 +7261,9 @@ function useScheduleTaskRepository() {
6863
7261
  throw error;
6864
7262
  }
6865
7263
  }
7264
+ function withScheduleDisplay(row) {
7265
+ return { ...row, ...scheduleDisplay(row, /* @__PURE__ */ new Date()) };
7266
+ }
6866
7267
  async function getScheduleTaskById(_id, session) {
6867
7268
  try {
6868
7269
  _id = new import_mongodb19.ObjectId(_id);
@@ -6880,7 +7281,7 @@ function useScheduleTaskRepository() {
6880
7281
  const cachedData = await getCache(cacheKey);
6881
7282
  if (cachedData) {
6882
7283
  import_node_server_utils33.logger.info(`Cache hit for key: ${cacheKey}`);
6883
- return cachedData;
7284
+ return withScheduleDisplay(cachedData);
6884
7285
  }
6885
7286
  } else {
6886
7287
  import_node_server_utils33.logger.info(`Skipping cache during transaction for key: ${cacheKey}`);
@@ -6891,12 +7292,13 @@ function useScheduleTaskRepository() {
6891
7292
  {
6892
7293
  $project: {
6893
7294
  title: 1,
6894
- time: 1,
6895
- dates: 1,
6896
7295
  description: 1,
6897
7296
  areas: 1,
6898
7297
  status: 1,
6899
- createdAt: 1
7298
+ createdAt: 1,
7299
+ site: 1,
7300
+ serviceType: 1,
7301
+ ...SCHEDULE_FIELDS
6900
7302
  }
6901
7303
  }
6902
7304
  ]).toArray();
@@ -6908,7 +7310,7 @@ function useScheduleTaskRepository() {
6908
7310
  }).catch((err) => {
6909
7311
  import_node_server_utils33.logger.error(`Failed to set cache for key: ${cacheKey}`, err);
6910
7312
  });
6911
- return data[0];
7313
+ return withScheduleDisplay(data[0]);
6912
7314
  } catch (error) {
6913
7315
  throw error;
6914
7316
  }
@@ -6932,16 +7334,20 @@ function useScheduleTaskRepository() {
6932
7334
  });
6933
7335
  }
6934
7336
  try {
6935
- const updateValue = { ...value, updatedAt: /* @__PURE__ */ new Date() };
7337
+ const { set, unset } = splitScheduleUpdate(value);
7338
+ const update = {
7339
+ $set: { ...set, updatedAt: /* @__PURE__ */ new Date() }
7340
+ };
7341
+ if (unset.length) {
7342
+ update.$unset = Object.fromEntries(unset.map((key) => [key, ""]));
7343
+ }
6936
7344
  const res = await collection.updateOne(
6937
- { _id },
6938
- { $set: updateValue },
7345
+ { _id, status: { $ne: "deleted" } },
7346
+ update,
6939
7347
  { session }
6940
7348
  );
6941
- if (res.modifiedCount === 0) {
6942
- throw new import_node_server_utils33.InternalServerError(
6943
- "Unable to update hygiene schedule task."
6944
- );
7349
+ if (res.matchedCount === 0) {
7350
+ throw new import_node_server_utils33.NotFoundError("Schedule task not found.");
6945
7351
  }
6946
7352
  delNamespace().then(() => {
6947
7353
  import_node_server_utils33.logger.info(`Cache cleared for namespace: ${namespace_collection}`);
@@ -6956,6 +7362,18 @@ function useScheduleTaskRepository() {
6956
7362
  throw error;
6957
7363
  }
6958
7364
  }
7365
+ async function markScheduleTaskRan(_id, date) {
7366
+ await collection.updateOne(
7367
+ { _id: new import_mongodb19.ObjectId(_id) },
7368
+ { $set: { lastRunDate: date, lastRunAt: /* @__PURE__ */ new Date() } }
7369
+ );
7370
+ delNamespace().catch((err) => {
7371
+ import_node_server_utils33.logger.error(
7372
+ `Failed to clear cache for namespace: ${namespace_collection}`,
7373
+ err
7374
+ );
7375
+ });
7376
+ }
6959
7377
  async function deleteScheduleTask(_id, session) {
6960
7378
  try {
6961
7379
  _id = new import_mongodb19.ObjectId(_id);
@@ -6995,18 +7413,31 @@ function useScheduleTaskRepository() {
6995
7413
  createScheduleTask,
6996
7414
  getScheduleTasks,
6997
7415
  getAllScheduleTask,
7416
+ markScheduleTaskRan,
6998
7417
  getScheduleTaskById,
6999
7418
  updateScheduleTask,
7000
7419
  deleteScheduleTask
7001
7420
  };
7002
7421
  }
7003
7422
 
7423
+ // src/utils/schedule-task-set.util.ts
7424
+ function buildScheduleTaskSets(task, units, startSet, count) {
7425
+ return Array.from({ length: count }, (_, i) => ({
7426
+ set: startSet + i,
7427
+ units,
7428
+ isScheduleTask: true,
7429
+ scheduleTaskId: task._id,
7430
+ scheduleTaskTitle: task.title,
7431
+ scheduleTaskTime: task.time
7432
+ }));
7433
+ }
7434
+
7004
7435
  // src/services/hygiene-schedule-task.service.ts
7005
7436
  var import_mongodb20 = require("mongodb");
7006
7437
  var import_node_server_utils34 = require("@7365admin1/node-server-utils");
7007
7438
  function useScheduleTaskService() {
7008
7439
  const { createParentChecklist } = useParentChecklistRepo();
7009
- const { getAllScheduleTask } = useScheduleTaskRepository();
7440
+ const { getAllScheduleTask, markScheduleTaskRan } = useScheduleTaskRepository();
7010
7441
  const {
7011
7442
  createAreaChecklist,
7012
7443
  getAreaChecklistByAreaAndSchedule,
@@ -7015,73 +7446,7 @@ function useScheduleTaskService() {
7015
7446
  const { getAreaById } = useAreaRepo();
7016
7447
  function checkScheduleConditions(schedule, serviceType, currentDate = /* @__PURE__ */ new Date()) {
7017
7448
  try {
7018
- const now = currentDate;
7019
- const timeString = now.toLocaleTimeString("en-US", {
7020
- hour: "2-digit",
7021
- minute: "2-digit",
7022
- hour12: false,
7023
- timeZone: "Asia/Singapore"
7024
- });
7025
- const [currentHour, currentMinute] = timeString.split(":").map(Number);
7026
- import_node_server_utils34.logger.info(
7027
- `Checking schedule ${schedule._id}: Current time ${currentHour}:${currentMinute}, Schedule time ${schedule.time}, Dates ${JSON.stringify(schedule.dates)}`
7028
- );
7029
- const currentDateFormatted = now.toLocaleDateString("en-CA", {
7030
- timeZone: "Asia/Singapore"
7031
- });
7032
- if (!schedule.dates || !Array.isArray(schedule.dates) || schedule.dates.length === 0) {
7033
- import_node_server_utils34.logger.info(`Schedule ${schedule._id}: No dates configured, skipping`);
7034
- return false;
7035
- }
7036
- if (!schedule.dates.includes(currentDateFormatted)) {
7037
- import_node_server_utils34.logger.info(
7038
- `Schedule ${schedule._id}: Current date ${currentDateFormatted} is not in scheduled dates [${schedule.dates.join(
7039
- ", "
7040
- )}]`
7041
- );
7042
- return false;
7043
- }
7044
- const [scheduleHour, scheduleMinute] = schedule.time.split(":").map(Number);
7045
- const timeMatches = currentHour === scheduleHour && currentMinute === scheduleMinute;
7046
- if (!timeMatches) {
7047
- import_node_server_utils34.logger.info(
7048
- `Schedule ${schedule._id}: Time does not match. Current: ${currentHour}:${currentMinute}, Expected: ${scheduleHour}:${scheduleMinute}`
7049
- );
7050
- return false;
7051
- }
7052
- const relevantTimestamp = schedule.updatedAt || schedule.createdAt;
7053
- if (relevantTimestamp) {
7054
- const savedAt = new Date(relevantTimestamp);
7055
- const savedHour = parseInt(
7056
- savedAt.toLocaleTimeString("en-US", {
7057
- hour: "2-digit",
7058
- hour12: false,
7059
- timeZone: "Asia/Singapore"
7060
- }),
7061
- 10
7062
- );
7063
- const savedMinute = savedAt.toLocaleString("en-US", {
7064
- minute: "2-digit",
7065
- timeZone: "Asia/Singapore"
7066
- });
7067
- const savedDateFormatted = savedAt.toLocaleDateString("en-CA", {
7068
- timeZone: "Asia/Singapore"
7069
- });
7070
- const savedMinNum = parseInt(savedMinute, 10);
7071
- const wasSavedThisMinute = savedDateFormatted === currentDateFormatted && savedHour === currentHour && savedMinNum === currentMinute;
7072
- if (wasSavedThisMinute) {
7073
- import_node_server_utils34.logger.info(
7074
- `Schedule ${schedule._id}: Skipping \u2014 task was saved this minute (${savedHour}:${String(
7075
- savedMinNum
7076
- ).padStart(2, "0")}). Will fire on next occurrence.`
7077
- );
7078
- return false;
7079
- }
7080
- }
7081
- import_node_server_utils34.logger.info(
7082
- `Schedule ${schedule._id}: All conditions matched - Date is within range and time matches`
7083
- );
7084
- return true;
7449
+ return shouldRunNow(schedule, currentDate);
7085
7450
  } catch (error) {
7086
7451
  import_node_server_utils34.logger.error(
7087
7452
  `Error checking schedule conditions for ${schedule._id}:`,
@@ -7090,18 +7455,22 @@ function useScheduleTaskService() {
7090
7455
  return false;
7091
7456
  }
7092
7457
  }
7458
+ const MAX_ATTEMPTS = 5;
7459
+ const attempts = /* @__PURE__ */ new Map();
7093
7460
  async function processScheduledTasks(currentDate) {
7094
7461
  try {
7095
7462
  import_node_server_utils34.logger.info("Starting scheduled task processing...");
7096
- const scheduleTasks = await getAllScheduleTask();
7463
+ const today = sgtParts(currentDate ?? /* @__PURE__ */ new Date()).date;
7464
+ const scheduleTasks = await getAllScheduleTask(today);
7097
7465
  if (!scheduleTasks || scheduleTasks.length === 0) {
7098
7466
  import_node_server_utils34.logger.info("No schedule tasks found to process");
7099
- return { processed: 0, validated: 0 };
7467
+ return { processed: 0, validated: 0, fired: [] };
7100
7468
  }
7101
7469
  import_node_server_utils34.logger.info(`Found ${scheduleTasks.length} schedule tasks to check`);
7102
7470
  let processedCount = 0;
7103
7471
  let validatedCount = 0;
7104
7472
  const validatedTasks = [];
7473
+ const fired = [];
7105
7474
  for (const scheduleTask of scheduleTasks) {
7106
7475
  try {
7107
7476
  const serviceType = scheduleTask.serviceType;
@@ -7144,6 +7513,8 @@ function useScheduleTaskService() {
7144
7513
  import_node_server_utils34.logger.info(
7145
7514
  `Using parent checklist ${parentChecklistId}, now creating/updating area checklists`
7146
7515
  );
7516
+ let failedAreas = 0;
7517
+ const addedAreas = [];
7147
7518
  for (const area of scheduleTask.areas) {
7148
7519
  try {
7149
7520
  const areaId = area.value.toString();
@@ -7206,15 +7577,7 @@ function useScheduleTaskService() {
7206
7577
  )
7207
7578
  ) : 0;
7208
7579
  const startSetNumber = maxSetInChecklist + 1;
7209
- const newSets = Array.from(
7210
- { length: areaSetConfig },
7211
- (_, index) => ({
7212
- set: startSetNumber + index,
7213
- units,
7214
- isScheduleTask: true,
7215
- scheduleTaskId: scheduleTask._id
7216
- })
7217
- );
7580
+ const newSets = buildScheduleTaskSets(scheduleTask, units, startSetNumber, areaSetConfig);
7218
7581
  const modified = await pushScheduleTaskSets(
7219
7582
  parentChecklistId.toString(),
7220
7583
  serviceType,
@@ -7223,6 +7586,7 @@ function useScheduleTaskService() {
7223
7586
  newSets
7224
7587
  );
7225
7588
  if (modified > 0) {
7589
+ addedAreas.push(area.name);
7226
7590
  import_node_server_utils34.logger.info(
7227
7591
  `Appended ${areaSetConfig} set(s) starting from ${startSetNumber} to area checklist for area ${area.name}`
7228
7592
  );
@@ -7233,16 +7597,7 @@ function useScheduleTaskService() {
7233
7597
  }
7234
7598
  } else {
7235
7599
  const areaSetConfig = Number(areaDetails.set) || 1;
7236
- const schedTaskId = scheduleTask._id;
7237
- const schedTaskSets = Array.from(
7238
- { length: areaSetConfig },
7239
- (_, index) => ({
7240
- set: index + 1,
7241
- units,
7242
- isScheduleTask: true,
7243
- scheduleTaskId: schedTaskId
7244
- })
7245
- );
7600
+ const schedTaskSets = buildScheduleTaskSets(scheduleTask, units, 1, areaSetConfig);
7246
7601
  const checklistData = {
7247
7602
  schedule: parentChecklistId.toString(),
7248
7603
  serviceType,
@@ -7255,13 +7610,32 @@ function useScheduleTaskService() {
7255
7610
  `Area ${area.name} (${areaId}): Creating new area checklist with schedule-task sets only`
7256
7611
  );
7257
7612
  await createAreaChecklist(checklistData);
7613
+ addedAreas.push(area.name);
7258
7614
  import_node_server_utils34.logger.info(`Created new area checklist for area ${area.name}`);
7259
7615
  }
7260
7616
  } catch (error) {
7261
7617
  import_node_server_utils34.logger.error(`Error processing area ${area.name}:`, error);
7618
+ failedAreas++;
7262
7619
  continue;
7263
7620
  }
7264
7621
  }
7622
+ const attemptKey = `${scheduleTask._id}:${today}`;
7623
+ const tried = (attempts.get(attemptKey) ?? 0) + 1;
7624
+ attempts.set(attemptKey, tried);
7625
+ if (failedAreas === 0 || tried >= MAX_ATTEMPTS) {
7626
+ await markScheduleTaskRan(scheduleTask._id, today);
7627
+ attempts.delete(attemptKey);
7628
+ }
7629
+ if (addedAreas.length > 0) {
7630
+ fired.push({
7631
+ _id: scheduleTask._id,
7632
+ site: scheduleTask.site,
7633
+ serviceType,
7634
+ title: scheduleTask.title,
7635
+ time: scheduleTask.time,
7636
+ areas: addedAreas
7637
+ });
7638
+ }
7265
7639
  processedCount++;
7266
7640
  validatedCount++;
7267
7641
  validatedTasks.push(scheduleTask);
@@ -7273,6 +7647,17 @@ function useScheduleTaskService() {
7273
7647
  `Error processing schedule task ${scheduleTask._id}:`,
7274
7648
  error
7275
7649
  );
7650
+ try {
7651
+ const key = `${scheduleTask._id}:${today}`;
7652
+ const tried = (attempts.get(key) ?? 0) + 1;
7653
+ attempts.set(key, tried);
7654
+ if (tried >= MAX_ATTEMPTS && scheduleTask._id) {
7655
+ await markScheduleTaskRan(scheduleTask._id, today);
7656
+ attempts.delete(key);
7657
+ }
7658
+ } catch (markError) {
7659
+ import_node_server_utils34.logger.error("Could not mark failing schedule task", markError);
7660
+ }
7276
7661
  continue;
7277
7662
  }
7278
7663
  }
@@ -7282,7 +7667,8 @@ function useScheduleTaskService() {
7282
7667
  return {
7283
7668
  processed: processedCount,
7284
7669
  validated: validatedCount,
7285
- tasks: validatedTasks
7670
+ tasks: validatedTasks,
7671
+ fired
7286
7672
  };
7287
7673
  } catch (error) {
7288
7674
  import_node_server_utils34.logger.error("Error processing scheduled tasks:", error);
@@ -7293,7 +7679,7 @@ function useScheduleTaskService() {
7293
7679
  }
7294
7680
 
7295
7681
  // src/controllers/hygiene-schedule-task.controller.ts
7296
- var import_joi18 = __toESM(require("joi"));
7682
+ var import_joi19 = __toESM(require("joi"));
7297
7683
  var import_node_server_utils35 = require("@7365admin1/node-server-utils");
7298
7684
  var import_core20 = require("@7365admin1/core");
7299
7685
  function useScheduleTaskController() {
@@ -7324,12 +7710,12 @@ function useScheduleTaskController() {
7324
7710
  }
7325
7711
  async function getScheduleTasks(req, res, next) {
7326
7712
  const query = { ...req.query, ...req.params };
7327
- const validation = import_joi18.default.object({
7328
- page: import_joi18.default.number().min(1).optional().allow("", null),
7329
- limit: import_joi18.default.number().min(1).optional().allow("", null),
7330
- search: import_joi18.default.string().optional().allow("", null),
7331
- site: import_joi18.default.string().hex().required(),
7332
- serviceType: import_joi18.default.string().valid(...Object.values(import_core20.AppServiceType)).required()
7713
+ const validation = import_joi19.default.object({
7714
+ page: import_joi19.default.number().min(1).optional().allow("", null),
7715
+ limit: import_joi19.default.number().min(1).optional().allow("", null),
7716
+ search: import_joi19.default.string().optional().allow("", null),
7717
+ site: import_joi19.default.string().hex().required(),
7718
+ serviceType: import_joi19.default.string().valid(...Object.values(import_core20.AppServiceType)).required()
7333
7719
  });
7334
7720
  const { error } = validation.validate(query);
7335
7721
  if (error) {
@@ -7337,8 +7723,8 @@ function useScheduleTaskController() {
7337
7723
  next(new import_node_server_utils35.BadRequestError(error.message));
7338
7724
  return;
7339
7725
  }
7340
- const page = parseInt(req.query.page) ?? 1;
7341
- const limit = parseInt(req.query.limit) ?? 10;
7726
+ const page = parseInt(req.query.page) || 1;
7727
+ const limit = parseInt(req.query.limit) || 10;
7342
7728
  const search = req.query.search ?? "";
7343
7729
  const site = req.params.site ?? "";
7344
7730
  const serviceType = req.query.serviceType ?? "";
@@ -7359,7 +7745,7 @@ function useScheduleTaskController() {
7359
7745
  }
7360
7746
  }
7361
7747
  async function getScheduleTaskById(req, res, next) {
7362
- const validation = import_joi18.default.string().hex().required();
7748
+ const validation = import_joi19.default.string().hex().required();
7363
7749
  const _id = req.params.id;
7364
7750
  const { error, value } = validation.validate(_id);
7365
7751
  if (error) {
@@ -7379,20 +7765,17 @@ function useScheduleTaskController() {
7379
7765
  }
7380
7766
  async function updateScheduleTask(req, res, next) {
7381
7767
  const payload = { id: req.params.id, ...req.body };
7382
- const validation = import_joi18.default.object({
7383
- id: import_joi18.default.string().hex().required(),
7384
- title: import_joi18.default.string().optional().allow("", null),
7385
- time: import_joi18.default.string().pattern(/^([0-1]\d|2[0-3]):([0-5]\d)$/).optional().allow("", null),
7386
- dates: import_joi18.default.array().min(1).items(
7387
- import_joi18.default.string().pattern(/^\d{4}-\d{2}-\d{2}$/).required()
7388
- ).optional(),
7389
- description: import_joi18.default.string().optional().allow("", null),
7390
- areas: import_joi18.default.array().min(1).items(
7391
- import_joi18.default.object({
7392
- name: import_joi18.default.string().required(),
7393
- value: import_joi18.default.any().required()
7394
- })
7395
- ).optional()
7768
+ const optional = Object.fromEntries(
7769
+ Object.entries(scheduleTaskFields).map(([key, rule]) => [
7770
+ key,
7771
+ key === "dates" ? import_joi19.default.array().min(1).items(scheduleDate).optional().messages({
7772
+ "array.min": "Pick at least one date, or choose a repeat."
7773
+ }) : rule.optional()
7774
+ ])
7775
+ );
7776
+ const validation = import_joi19.default.object({
7777
+ id: import_joi19.default.string().hex().required(),
7778
+ ...optional
7396
7779
  });
7397
7780
  const { error } = validation.validate(payload);
7398
7781
  if (error) {
@@ -7400,6 +7783,24 @@ function useScheduleTaskController() {
7400
7783
  next(new import_node_server_utils35.BadRequestError(error.message));
7401
7784
  return;
7402
7785
  }
7786
+ try {
7787
+ const { set, unset } = splitScheduleUpdate(req.body ?? {});
7788
+ if (TIMING_KEYS.some((key) => key in set || unset.includes(key))) {
7789
+ const stored = await _getScheduleTaskById(req.params.id);
7790
+ const merged = { ...stored, ...set };
7791
+ for (const key of unset)
7792
+ delete merged[key];
7793
+ const problem = timingChanged(stored, merged) ? validateScheduleRules(cleanScheduleInput(merged)) : null;
7794
+ if (problem) {
7795
+ next(new import_node_server_utils35.BadRequestError(problem));
7796
+ return;
7797
+ }
7798
+ }
7799
+ } catch (error2) {
7800
+ import_node_server_utils35.logger.log({ level: "error", message: error2.message });
7801
+ next(error2);
7802
+ return;
7803
+ }
7403
7804
  try {
7404
7805
  const { id, ...value } = payload;
7405
7806
  await _updateScheduleTask(id, value);
@@ -7412,7 +7813,7 @@ function useScheduleTaskController() {
7412
7813
  }
7413
7814
  }
7414
7815
  async function deleteScheduleTask(req, res, next) {
7415
- const validation = import_joi18.default.string().hex().required();
7816
+ const validation = import_joi19.default.string().hex().required();
7416
7817
  const _id = req.params.id;
7417
7818
  const { error, value } = validation.validate(_id);
7418
7819
  if (error) {
@@ -7656,17 +8057,17 @@ function useQRService() {
7656
8057
  }
7657
8058
 
7658
8059
  // src/controllers/hygiene-qr.controller.ts
7659
- var import_joi19 = __toESM(require("joi"));
8060
+ var import_joi20 = __toESM(require("joi"));
7660
8061
  var import_node_server_utils37 = require("@7365admin1/node-server-utils");
7661
8062
  function useQRController() {
7662
8063
  const { generateQRImage: _generateQRImage, generateQRPDF: _generateQRPDF } = useQRService();
7663
8064
  async function generateQR(req, res, next) {
7664
- const validation = import_joi19.default.object({
7665
- url: import_joi19.default.string().uri().required(),
7666
- filename: import_joi19.default.string().optional().allow("", null),
7667
- title: import_joi19.default.string().optional().allow("", null),
7668
- subtitle: import_joi19.default.string().optional().allow("", null),
7669
- download: import_joi19.default.boolean().optional().default(false)
8065
+ const validation = import_joi20.default.object({
8066
+ url: import_joi20.default.string().uri().required(),
8067
+ filename: import_joi20.default.string().optional().allow("", null),
8068
+ title: import_joi20.default.string().optional().allow("", null),
8069
+ subtitle: import_joi20.default.string().optional().allow("", null),
8070
+ download: import_joi20.default.boolean().optional().default(false)
7670
8071
  });
7671
8072
  const query = { ...req.query };
7672
8073
  const { error, value } = validation.validate(query);