@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.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);
@@ -3181,7 +3186,9 @@ var areaChecklistSchema = Joi9.object({
3181
3186
  }).required()
3182
3187
  ).min(1).required(),
3183
3188
  isScheduleTask: Joi9.boolean().optional().default(false),
3184
- scheduleTaskId: Joi9.string().hex().optional().allow(null)
3189
+ scheduleTaskId: Joi9.string().hex().optional().allow(null),
3190
+ scheduleTaskTitle: Joi9.string().optional().allow("", null),
3191
+ scheduleTaskTime: Joi9.string().optional().allow("", null)
3185
3192
  }).required()
3186
3193
  ).optional().default([])
3187
3194
  });
@@ -3211,6 +3218,8 @@ function MAreaChecklist(value) {
3211
3218
  set: checklistItem.set,
3212
3219
  isScheduleTask: checklistItem.isScheduleTask ?? false,
3213
3220
  scheduleTaskId: checklistItem.scheduleTaskId ?? void 0,
3221
+ scheduleTaskTitle: checklistItem.scheduleTaskTitle ?? void 0,
3222
+ scheduleTaskTime: checklistItem.scheduleTaskTime ?? void 0,
3214
3223
  units: checklistItem.units.map((unit) => ({
3215
3224
  unit: new ObjectId8(unit.unit),
3216
3225
  name: unit.name,
@@ -3705,6 +3714,9 @@ function useAreaChecklistRepo() {
3705
3714
  isScheduleTask: {
3706
3715
  $ifNull: ["$checklist.isScheduleTask", false]
3707
3716
  },
3717
+ scheduleTaskId: { $ifNull: ["$checklist.scheduleTaskId", null] },
3718
+ scheduleTaskTitle: { $ifNull: ["$checklist.scheduleTaskTitle", null] },
3719
+ scheduleTaskTime: { $ifNull: ["$checklist.scheduleTaskTime", null] },
3708
3720
  unit: "$checklist.units.unit",
3709
3721
  name: "$checklist.units.name",
3710
3722
  approve: "$checklist.units.approve",
@@ -3741,6 +3753,9 @@ function useAreaChecklistRepo() {
3741
3753
  $group: {
3742
3754
  _id: "$set",
3743
3755
  isScheduleTask: { $first: "$isScheduleTask" },
3756
+ scheduleTaskId: { $first: "$scheduleTaskId" },
3757
+ scheduleTaskTitle: { $first: "$scheduleTaskTitle" },
3758
+ scheduleTaskTime: { $first: "$scheduleTaskTime" },
3744
3759
  units: {
3745
3760
  $push: {
3746
3761
  unit: "$unit",
@@ -3761,6 +3776,9 @@ function useAreaChecklistRepo() {
3761
3776
  _id: 0,
3762
3777
  set: "$_id",
3763
3778
  isScheduleTask: 1,
3779
+ scheduleTaskId: 1,
3780
+ scheduleTaskTitle: 1,
3781
+ scheduleTaskTime: 1,
3764
3782
  units: 1
3765
3783
  }
3766
3784
  },
@@ -3892,6 +3910,8 @@ function useAreaChecklistRepo() {
3892
3910
  isScheduleTask: {
3893
3911
  $ifNull: ["$checklist.isScheduleTask", false]
3894
3912
  },
3913
+ scheduleTaskTitle: { $ifNull: ["$checklist.scheduleTaskTitle", null] },
3914
+ scheduleTaskTime: { $ifNull: ["$checklist.scheduleTaskTime", null] },
3895
3915
  scheduleTaskId: {
3896
3916
  $ifNull: ["$checklist.scheduleTaskId", null]
3897
3917
  },
@@ -3932,6 +3952,8 @@ function useAreaChecklistRepo() {
3932
3952
  $group: {
3933
3953
  _id: "$set",
3934
3954
  isScheduleTask: { $first: "$isScheduleTask" },
3955
+ scheduleTaskTitle: { $first: "$scheduleTaskTitle" },
3956
+ scheduleTaskTime: { $first: "$scheduleTaskTime" },
3935
3957
  scheduleTaskId: { $first: "$scheduleTaskId" },
3936
3958
  remarks: { $first: "$remarks" },
3937
3959
  attachment: { $first: "$attachment" },
@@ -3956,6 +3978,8 @@ function useAreaChecklistRepo() {
3956
3978
  _id: 0,
3957
3979
  set: "$_id",
3958
3980
  isScheduleTask: 1,
3981
+ scheduleTaskTitle: 1,
3982
+ scheduleTaskTime: 1,
3959
3983
  scheduleTaskId: 1,
3960
3984
  remarks: "$remarks",
3961
3985
  attachment: "$attachment",
@@ -4431,6 +4455,265 @@ function useAreaChecklistRepo() {
4431
4455
  };
4432
4456
  }
4433
4457
 
4458
+ // src/utils/schedule-task-rules.util.ts
4459
+ import Joi10 from "joi";
4460
+ var REPEATS = ["none", "daily", "weekdays", "weekly", "monthly"];
4461
+ var SGT_MS = 8 * 60 * 60 * 1e3;
4462
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4463
+ var TIME_RE = /^([0-1]\d|2[0-3]):([0-5]\d)$/;
4464
+ var pad = (n) => String(n).padStart(2, "0");
4465
+ function sgtParts(now) {
4466
+ const s = new Date(now.getTime() + SGT_MS);
4467
+ const date = `${s.getUTCFullYear()}-${pad(s.getUTCMonth() + 1)}-${pad(s.getUTCDate())}`;
4468
+ return { date, minutes: s.getUTCHours() * 60 + s.getUTCMinutes(), weekday: s.getUTCDay() };
4469
+ }
4470
+ function isRealDate(value) {
4471
+ if (typeof value !== "string" || !DATE_RE.test(value))
4472
+ return false;
4473
+ const [y, m, d] = value.split("-").map(Number);
4474
+ const t = new Date(Date.UTC(y, m - 1, d));
4475
+ return t.getUTCFullYear() === y && t.getUTCMonth() === m - 1 && t.getUTCDate() === d;
4476
+ }
4477
+ var toMinutes = (time) => {
4478
+ const [h, m] = time.split(":").map(Number);
4479
+ return h * 60 + m;
4480
+ };
4481
+ var weekdayOf = (date) => {
4482
+ const [y, m, d] = date.split("-").map(Number);
4483
+ return new Date(Date.UTC(y, m - 1, d)).getUTCDay();
4484
+ };
4485
+ var daysInMonth = (date) => {
4486
+ const [y, m] = date.split("-").map(Number);
4487
+ return new Date(Date.UTC(y, m, 0)).getUTCDate();
4488
+ };
4489
+ var addDays = (date, n) => {
4490
+ const [y, m, d] = date.split("-").map(Number);
4491
+ const t = new Date(Date.UTC(y, m - 1, d + n));
4492
+ return `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}-${pad(t.getUTCDate())}`;
4493
+ };
4494
+ var isRepeating = (task) => Boolean(task.repeat) && task.repeat !== "none";
4495
+ function occursOn(task, date) {
4496
+ if (task.endDate && date > task.endDate)
4497
+ return false;
4498
+ if (!isRepeating(task))
4499
+ return Array.isArray(task.dates) && task.dates.includes(date);
4500
+ if (task.startDate && date < task.startDate)
4501
+ return false;
4502
+ const weekday = weekdayOf(date);
4503
+ switch (task.repeat) {
4504
+ case "daily":
4505
+ return true;
4506
+ case "weekdays":
4507
+ return weekday >= 1 && weekday <= 5;
4508
+ case "weekly":
4509
+ return Array.isArray(task.daysOfWeek) && task.daysOfWeek.includes(weekday);
4510
+ case "monthly": {
4511
+ const wanted = task.dayOfMonth ?? (task.startDate ? Number(task.startDate.slice(8, 10)) : 1);
4512
+ return Number(date.slice(8, 10)) === Math.min(wanted, daysInMonth(date));
4513
+ }
4514
+ default:
4515
+ return false;
4516
+ }
4517
+ }
4518
+ function savedTodayAt(task, today) {
4519
+ const stamp = task.updatedAt || task.createdAt;
4520
+ if (!stamp)
4521
+ return null;
4522
+ const saved = new Date(stamp);
4523
+ if (Number.isNaN(saved.getTime()))
4524
+ return null;
4525
+ const p = sgtParts(saved);
4526
+ return p.date === today ? p.minutes : null;
4527
+ }
4528
+ function shouldRunNow(task, now = /* @__PURE__ */ new Date()) {
4529
+ if (task.paused || !TIME_RE.test(task.time ?? ""))
4530
+ return false;
4531
+ const { date, minutes } = sgtParts(now);
4532
+ if (task.lastRunDate === date || !occursOn(task, date))
4533
+ return false;
4534
+ const due = toMinutes(task.time);
4535
+ if (minutes < due)
4536
+ return false;
4537
+ const saved = savedTodayAt(task, date);
4538
+ return !(saved !== null && saved >= due);
4539
+ }
4540
+ var iso = (date, time) => `${date}T${time}:00+08:00`;
4541
+ function upcoming(task, now) {
4542
+ if (typeof task.time !== "string" || !TIME_RE.test(task.time))
4543
+ return null;
4544
+ const { date: today, minutes } = sgtParts(now);
4545
+ const due = toMinutes(task.time);
4546
+ if (occursOn(task, today) && (minutes < due || shouldRunNow({ ...task, paused: false }, now))) {
4547
+ return iso(today, task.time);
4548
+ }
4549
+ if (!isRepeating(task)) {
4550
+ const later = (task.dates ?? []).filter((d) => d > today && (!task.endDate || d <= task.endDate)).sort();
4551
+ return later.length ? iso(later[0], task.time) : null;
4552
+ }
4553
+ const from = task.startDate && task.startDate > today ? task.startDate : today;
4554
+ for (let i = from === today ? 1 : 0; i <= 400; i++) {
4555
+ const d = addDays(from, i);
4556
+ if (task.endDate && d > task.endDate)
4557
+ return null;
4558
+ if (occursOn(task, d))
4559
+ return iso(d, task.time);
4560
+ }
4561
+ return null;
4562
+ }
4563
+ function nextRun(task, now = /* @__PURE__ */ new Date()) {
4564
+ return task.paused ? null : upcoming(task, now);
4565
+ }
4566
+ function scheduleState(task, now = /* @__PURE__ */ new Date()) {
4567
+ if (task.paused)
4568
+ return "paused";
4569
+ return upcoming(task, now) ? "active" : "ended";
4570
+ }
4571
+ function lastRun(task) {
4572
+ return task.lastRunDate && TIME_RE.test(task.time ?? "") ? iso(task.lastRunDate, task.time) : null;
4573
+ }
4574
+ function scheduleDisplay(task, now = /* @__PURE__ */ new Date()) {
4575
+ try {
4576
+ return {
4577
+ nextRun: nextRun(task, now),
4578
+ lastRun: lastRun(task),
4579
+ scheduleStatus: scheduleState(task, now)
4580
+ };
4581
+ } catch {
4582
+ return { nextRun: null, lastRun: null, scheduleStatus: null };
4583
+ }
4584
+ }
4585
+ function validateScheduleRules(task, now = /* @__PURE__ */ new Date()) {
4586
+ const repeating = isRepeating(task);
4587
+ const { date: today, minutes } = sgtParts(now);
4588
+ if (repeating) {
4589
+ if (!task.startDate)
4590
+ return "Please choose a start date for a repeating schedule.";
4591
+ if (task.repeat === "weekly" && !(task.daysOfWeek && task.daysOfWeek.length)) {
4592
+ return "Pick at least one weekday for a weekly repeat.";
4593
+ }
4594
+ }
4595
+ const start = repeating ? task.startDate : [...task.dates ?? []].sort()[0];
4596
+ if (task.endDate && start && start > task.endDate) {
4597
+ return "The end date must be on or after the start date.";
4598
+ }
4599
+ if (task.endDate && task.endDate < today)
4600
+ return "The end date has already passed.";
4601
+ if (!repeating) {
4602
+ const dates = (task.dates ?? []).filter((d) => !task.endDate || d <= task.endDate);
4603
+ if (dates.length && dates.every((d) => d < today)) {
4604
+ return "All the dates you picked have already passed. Pick today or a later date.";
4605
+ }
4606
+ if (dates.length && dates.every((d) => d <= today) && dates.includes(today) && TIME_RE.test(task.time) && toMinutes(task.time) <= minutes) {
4607
+ return "That time has already passed today. Pick a later time or another date.";
4608
+ }
4609
+ }
4610
+ if (!upcoming({ ...task, paused: false, updatedAt: now }, now)) {
4611
+ return "This schedule would never run. Check the dates, end date and time.";
4612
+ }
4613
+ return null;
4614
+ }
4615
+ var scheduleDate = Joi10.string().pattern(DATE_RE).custom((value, helpers) => isRealDate(value) ? value : helpers.error("date.invalid")).messages({
4616
+ "string.pattern.base": "Dates must look like 2026-10-21.",
4617
+ "string.empty": "Please choose a date.",
4618
+ "date.invalid": "That date does not exist. Dates must look like 2026-10-21.",
4619
+ "string.base": "Dates must look like 2026-10-21."
4620
+ });
4621
+ var REPEATING = Joi10.valid("daily", "weekdays", "weekly", "monthly").required();
4622
+ var scheduleTaskFields = {
4623
+ title: Joi10.string().trim().required().messages({
4624
+ "any.required": "Please enter a title.",
4625
+ "string.empty": "Please enter a title.",
4626
+ "string.base": "Please enter a title."
4627
+ }),
4628
+ time: Joi10.string().pattern(TIME_RE).required().messages({
4629
+ "any.required": "Please choose a time.",
4630
+ "string.empty": "Please choose a time.",
4631
+ "string.pattern.base": "Time must be in 24-hour format like 08:30.",
4632
+ "string.base": "Time must be in 24-hour format like 08:30."
4633
+ }),
4634
+ dates: Joi10.when("repeat", {
4635
+ is: REPEATING,
4636
+ then: Joi10.array().items(scheduleDate).optional().default([]),
4637
+ otherwise: Joi10.array().min(1).items(scheduleDate).required()
4638
+ }).messages({
4639
+ "any.required": "Pick at least one date, or choose a repeat.",
4640
+ "array.min": "Pick at least one date, or choose a repeat.",
4641
+ "array.base": "Dates must be a list of dates."
4642
+ }),
4643
+ description: Joi10.string().optional().allow("", null),
4644
+ areas: Joi10.array().min(1).items(
4645
+ Joi10.object({
4646
+ name: Joi10.string().required(),
4647
+ value: Joi10.any().required()
4648
+ })
4649
+ ).required().messages({
4650
+ "any.required": "Pick at least one area.",
4651
+ "array.min": "Pick at least one area.",
4652
+ "array.base": "Pick at least one area."
4653
+ }),
4654
+ repeat: Joi10.string().valid(...REPEATS).optional().allow("", null).messages({ "any.only": "Repeat must be one of: none, daily, weekdays, weekly, monthly." }),
4655
+ daysOfWeek: Joi10.array().items(Joi10.number().integer().min(0).max(6)).unique().optional().allow(null).messages({
4656
+ "number.base": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4657
+ "number.min": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4658
+ "number.max": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4659
+ "number.integer": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4660
+ "array.unique": "Each weekday can only be picked once."
4661
+ }),
4662
+ 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." }),
4663
+ startDate: scheduleDate.optional().allow("", null),
4664
+ endDate: scheduleDate.optional().allow("", null),
4665
+ paused: Joi10.boolean().optional().allow(null).messages({ "boolean.base": "Paused must be true or false." })
4666
+ };
4667
+ var OPTIONAL_KEYS = ["repeat", "daysOfWeek", "dayOfMonth", "startDate", "endDate", "paused"];
4668
+ var isBlank = (v) => v === "" || v === null;
4669
+ function cleanScheduleInput(value) {
4670
+ const out = { ...value };
4671
+ for (const key of OPTIONAL_KEYS)
4672
+ if (isBlank(out[key]))
4673
+ delete out[key];
4674
+ if (out.repeat === "none") {
4675
+ delete out.daysOfWeek;
4676
+ delete out.dayOfMonth;
4677
+ delete out.startDate;
4678
+ }
4679
+ return out;
4680
+ }
4681
+ var TIMING_KEYS = ["time", "dates", "repeat", "daysOfWeek", "dayOfMonth", "startDate", "endDate"];
4682
+ function splitScheduleUpdate(payload) {
4683
+ const set = {};
4684
+ const unset = [];
4685
+ for (const [key, value] of Object.entries(payload)) {
4686
+ if (OPTIONAL_KEYS.includes(key) && isBlank(value))
4687
+ unset.push(key);
4688
+ else if (value !== void 0)
4689
+ set[key] = value;
4690
+ }
4691
+ if (set.repeat === "none") {
4692
+ for (const key of ["daysOfWeek", "dayOfMonth", "startDate"]) {
4693
+ if (!unset.includes(key))
4694
+ unset.push(key);
4695
+ delete set[key];
4696
+ }
4697
+ }
4698
+ return { set, unset };
4699
+ }
4700
+ function regularSetsMissing(checklist, setCount) {
4701
+ const regular = (checklist ?? []).filter((c) => c.isScheduleTask !== true).length;
4702
+ return Math.max(0, setCount - regular);
4703
+ }
4704
+ var normTiming = (t) => ({
4705
+ time: t.time ?? null,
4706
+ dates: [...Array.isArray(t.dates) ? t.dates : []].sort(),
4707
+ repeat: !t.repeat || t.repeat === "none" ? "none" : t.repeat,
4708
+ daysOfWeek: [...Array.isArray(t.daysOfWeek) ? t.daysOfWeek : []].sort(),
4709
+ dayOfMonth: t.dayOfMonth ?? null,
4710
+ startDate: t.startDate || null,
4711
+ endDate: t.endDate || null
4712
+ });
4713
+ function timingChanged(stored, after) {
4714
+ return JSON.stringify(normTiming(stored)) !== JSON.stringify(normTiming(after));
4715
+ }
4716
+
4434
4717
  // src/services/hygiene-area-checklist.service.ts
4435
4718
  function useAreaChecklistService() {
4436
4719
  const { getAreasForChecklist } = useAreaRepo();
@@ -4498,10 +4781,10 @@ function useAreaChecklistService() {
4498
4781
  `Area checklist for area ${area.name} exists but has no auto-gen sets. Inserting auto-gen sets.`
4499
4782
  );
4500
4783
  const setCount2 = Number(area.set) || 1;
4501
- const totalExistingSets = existing.checklist?.length ?? 0;
4502
- if (totalExistingSets >= setCount2) {
4784
+ const setsToAdd = regularSetsMissing(existing.checklist, setCount2);
4785
+ if (setsToAdd <= 0) {
4503
4786
  logger18.info(
4504
- `Area checklist for area ${area.name} already has ${totalExistingSets} set(s) (configured: ${setCount2}). Skipping auto-gen sets.`
4787
+ `Area checklist for area ${area.name} already has its ${setCount2} regular set(s). Skipping auto-gen sets.`
4505
4788
  );
4506
4789
  return existing._id;
4507
4790
  }
@@ -4509,7 +4792,6 @@ function useAreaChecklistService() {
4509
4792
  (max, c) => Math.max(max, c.set ?? 0),
4510
4793
  0
4511
4794
  ) ?? 0;
4512
- const setsToAdd = setCount2 - totalExistingSets;
4513
4795
  const autoGenSets = Array.from(
4514
4796
  { length: setsToAdd },
4515
4797
  (_, index) => ({
@@ -4670,7 +4952,7 @@ function useAreaChecklistService() {
4670
4952
  }
4671
4953
 
4672
4954
  // src/controllers/hygiene-area-checklist.controller.ts
4673
- import Joi10 from "joi";
4955
+ import Joi11 from "joi";
4674
4956
  import {
4675
4957
  BadRequestError as BadRequestError16,
4676
4958
  InternalServerError as InternalServerError7,
@@ -4682,6 +4964,8 @@ import { AppServiceType as AppServiceType10 } from "@7365admin1/core";
4682
4964
  import { launch } from "puppeteer";
4683
4965
  import { InternalServerError as InternalServerError6, useAtlas as useAtlas8 } from "@7365admin1/node-server-utils";
4684
4966
  import { ObjectId as ObjectId11 } from "mongodb";
4967
+
4968
+ // src/utils/checklist-pdf-html.util.ts
4685
4969
  function escapeHtml(text) {
4686
4970
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
4687
4971
  }
@@ -4694,7 +4978,7 @@ function buildChecklistHtml(areaChecklists) {
4694
4978
  const unitRows = (setItem.units || []).map(
4695
4979
  (unit) => `
4696
4980
  <tr>
4697
- <td class="unit-name">${escapeHtml(unit.name || "")}</td>
4981
+ <td class="unit-name">${escapeHtml(unit.name || "")}${unit.approve || unit.reject ? "" : ' <span class="missed-label">Missed</span>'}</td>
4698
4982
  <td class="icon-cell">${unit.reject ? '<span class="reject-icon">&#10007;</span>' : ""}</td>
4699
4983
  <td class="icon-cell">${unit.approve ? '<span class="approve-icon">&#10003;</span>' : ""}</td>
4700
4984
  </tr>`
@@ -4831,6 +5115,12 @@ function buildChecklistHtml(areaChecklists) {
4831
5115
  font-size: 16px;
4832
5116
  font-weight: bold;
4833
5117
  }
5118
+ .missed-label {
5119
+ color: #b3261e;
5120
+ font-size: 11px;
5121
+ font-weight: bold;
5122
+ margin-left: 6px;
5123
+ }
4834
5124
  .reject-icon {
4835
5125
  color: #e74c3c;
4836
5126
  font-size: 16px;
@@ -4846,6 +5136,8 @@ function buildChecklistHtml(areaChecklists) {
4846
5136
  </body>
4847
5137
  </html>`;
4848
5138
  }
5139
+
5140
+ // src/services/hygiene-checklist-pdf.service.ts
4849
5141
  function useChecklistPdfService() {
4850
5142
  async function generateChecklistPdf(scheduleId) {
4851
5143
  const db = useAtlas8.getDb();
@@ -4915,9 +5207,9 @@ function useAreaChecklistController() {
4915
5207
  site: req.params.site,
4916
5208
  schedule: req.params.schedule
4917
5209
  };
4918
- const validation = Joi10.object({
4919
- site: Joi10.string().hex().required(),
4920
- schedule: Joi10.string().hex().required()
5210
+ const validation = Joi11.object({
5211
+ site: Joi11.string().hex().required(),
5212
+ schedule: Joi11.string().hex().required()
4921
5213
  });
4922
5214
  const { error, value } = validation.validate(payload);
4923
5215
  if (error) {
@@ -4937,14 +5229,14 @@ function useAreaChecklistController() {
4937
5229
  }
4938
5230
  async function getAllAreaChecklist(req, res, next) {
4939
5231
  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()
5232
+ const validation = Joi11.object({
5233
+ page: Joi11.number().min(1).optional().allow("", null),
5234
+ limit: Joi11.number().min(1).optional().allow("", null),
5235
+ search: Joi11.string().optional().allow("", null),
5236
+ type: Joi11.string().valid(...allowedTypes, "all").optional().allow("", null),
5237
+ status: Joi11.string().valid(...allowedStatus, "all").optional().allow("", null),
5238
+ schedule: Joi11.string().hex().required(),
5239
+ serviceType: Joi11.string().valid(...Object.values(AppServiceType10)).required()
4948
5240
  });
4949
5241
  const { error } = validation.validate(query);
4950
5242
  if (error) {
@@ -4979,14 +5271,14 @@ function useAreaChecklistController() {
4979
5271
  }
4980
5272
  async function getAreaChecklistHistory(req, res, next) {
4981
5273
  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)
5274
+ const validation = Joi11.object({
5275
+ page: Joi11.number().min(1).optional().allow("", null),
5276
+ limit: Joi11.number().min(1).optional().allow("", null),
5277
+ search: Joi11.string().optional().allow("", null),
5278
+ type: Joi11.string().optional().allow("", ...allowedTypes),
5279
+ schedule: Joi11.string().hex().required(),
5280
+ status: Joi11.string().allow("", null, ...allowedStatus),
5281
+ createdAt: Joi11.alternatives().try(Joi11.date(), Joi11.string()).optional().allow("", null)
4990
5282
  });
4991
5283
  const { error } = validation.validate(query);
4992
5284
  if (error) {
@@ -5020,7 +5312,7 @@ function useAreaChecklistController() {
5020
5312
  }
5021
5313
  }
5022
5314
  async function getAreaChecklistHistoryDetails(req, res, next) {
5023
- const validation = Joi10.string().hex().required();
5315
+ const validation = Joi11.string().hex().required();
5024
5316
  const _id = req.params.id;
5025
5317
  const { error, value } = validation.validate(_id);
5026
5318
  if (error) {
@@ -5040,12 +5332,12 @@ function useAreaChecklistController() {
5040
5332
  }
5041
5333
  async function getAreaChecklistUnits(req, res, next) {
5042
5334
  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()
5335
+ const validation = Joi11.object({
5336
+ page: Joi11.number().min(1).optional().allow("", null),
5337
+ limit: Joi11.number().min(1).optional().allow("", null),
5338
+ search: Joi11.string().optional().allow("", null),
5339
+ serviceType: Joi11.string().valid(...Object.values(AppServiceType10)).required(),
5340
+ id: Joi11.string().hex().required()
5049
5341
  });
5050
5342
  const { error } = validation.validate(query);
5051
5343
  if (error) {
@@ -5095,21 +5387,21 @@ function useAreaChecklistController() {
5095
5387
  ...decisionValues,
5096
5388
  completedBy
5097
5389
  };
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),
5390
+ const validation = Joi11.object({
5391
+ id: Joi11.string().hex().required(),
5392
+ set: Joi11.number().integer().min(1).required(),
5393
+ unit: Joi11.string().hex().required(),
5394
+ decision: Joi11.string().valid("approve", "reject").required(),
5395
+ approve: Joi11.boolean().optional(),
5396
+ reject: Joi11.boolean().optional(),
5397
+ remarks: Joi11.string().optional().allow("", null),
5106
5398
  /*
5107
5399
  * Shape only. WHETHER a photo is required depends on the set's current
5108
5400
  * state in the database, which Joi cannot see — see the check below.
5109
5401
  * Each entry must still be a real id: a blank string carries no evidence.
5110
5402
  */
5111
- attachment: Joi10.array().items(Joi10.string().trim().min(1)).optional(),
5112
- completedBy: Joi10.string().hex().required()
5403
+ attachment: Joi11.array().items(Joi11.string().trim().min(1)).optional(),
5404
+ completedBy: Joi11.string().hex().required()
5113
5405
  });
5114
5406
  const { error } = validation.validate(payload);
5115
5407
  if (error) {
@@ -5146,7 +5438,7 @@ function useAreaChecklistController() {
5146
5438
  }
5147
5439
  }
5148
5440
  async function downloadChecklistPdf(req, res, next) {
5149
- const validation = Joi10.string().hex().required();
5441
+ const validation = Joi11.string().hex().required();
5150
5442
  const schedule = req.params.schedule;
5151
5443
  const { error, value } = validation.validate(schedule);
5152
5444
  if (error) {
@@ -5192,16 +5484,16 @@ function useAreaChecklistController() {
5192
5484
  }
5193
5485
 
5194
5486
  // src/models/hygiene-supply.model.ts
5195
- import Joi11 from "joi";
5487
+ import Joi12 from "joi";
5196
5488
  import { ObjectId as ObjectId12 } from "mongodb";
5197
5489
  import { BadRequestError as BadRequestError17, logger as logger21 } from "@7365admin1/node-server-utils";
5198
5490
  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()
5491
+ var supplySchema = Joi12.object({
5492
+ site: Joi12.string().hex().required(),
5493
+ serviceType: Joi12.string().valid(...Object.values(AppServiceType11)).required(),
5494
+ name: Joi12.string().required(),
5495
+ unitOfMeasurement: Joi12.string().required(),
5496
+ attachment: Joi12.string().allow(null, "").optional()
5205
5497
  });
5206
5498
  function MSupply(value) {
5207
5499
  const { error } = supplySchema.validate(value);
@@ -5338,7 +5630,7 @@ function useSupplyRepository() {
5338
5630
  throw new BadRequestError18("Invalid site ID format.");
5339
5631
  }
5340
5632
  if (search) {
5341
- query.name = { $regex: search, $options: "i" };
5633
+ query.name = { $regex: escapeRegex(search), $options: "i" };
5342
5634
  cacheOptions.search = search;
5343
5635
  }
5344
5636
  const cacheKey = makeCacheKey6(namespace_collection, cacheOptions);
@@ -5500,7 +5792,7 @@ function useSupplyRepository() {
5500
5792
  }
5501
5793
 
5502
5794
  // src/controllers/hygiene-supply.controller.ts
5503
- import Joi12 from "joi";
5795
+ import Joi13 from "joi";
5504
5796
  import { BadRequestError as BadRequestError19, logger as logger23 } from "@7365admin1/node-server-utils";
5505
5797
  import { AppServiceType as AppServiceType12 } from "@7365admin1/core";
5506
5798
  function useSupplyController() {
@@ -5531,12 +5823,12 @@ function useSupplyController() {
5531
5823
  }
5532
5824
  async function getSupplies(req, res, next) {
5533
5825
  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()
5826
+ const validation = Joi13.object({
5827
+ page: Joi13.number().min(1).optional().allow("", null),
5828
+ limit: Joi13.number().min(1).optional().allow("", null),
5829
+ search: Joi13.string().optional().allow("", null),
5830
+ site: Joi13.string().hex().required(),
5831
+ serviceType: Joi13.string().valid(...Object.values(AppServiceType12)).required()
5540
5832
  });
5541
5833
  const { error } = validation.validate(query);
5542
5834
  if (error) {
@@ -5566,7 +5858,7 @@ function useSupplyController() {
5566
5858
  }
5567
5859
  }
5568
5860
  async function getSupplyById(req, res, next) {
5569
- const validation = Joi12.string().hex().required();
5861
+ const validation = Joi13.string().hex().required();
5570
5862
  const _id = req.params.id;
5571
5863
  const { error, value } = validation.validate(_id);
5572
5864
  if (error) {
@@ -5586,12 +5878,12 @@ function useSupplyController() {
5586
5878
  }
5587
5879
  async function updateSupply(req, res, next) {
5588
5880
  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)
5881
+ const validation = Joi13.object({
5882
+ id: Joi13.string().hex().required(),
5883
+ name: Joi13.string().optional().allow("", null),
5884
+ unitOfMeasurement: Joi13.string().optional().allow("", null),
5885
+ qty: Joi13.number().min(0).optional().allow("", null),
5886
+ attachment: Joi13.string().optional().allow("", null)
5595
5887
  });
5596
5888
  const { error } = validation.validate(payload);
5597
5889
  if (error) {
@@ -5612,7 +5904,7 @@ function useSupplyController() {
5612
5904
  }
5613
5905
  async function deleteSupply(req, res, next) {
5614
5906
  const id = req.params.id;
5615
- const validation = Joi12.string().hex().required();
5907
+ const validation = Joi13.string().hex().required();
5616
5908
  const { error, value } = validation.validate(id);
5617
5909
  if (error) {
5618
5910
  logger23.log({ level: "error", message: error.message });
@@ -5639,18 +5931,18 @@ function useSupplyController() {
5639
5931
  }
5640
5932
 
5641
5933
  // src/models/hygiene-stock.model.ts
5642
- import Joi13 from "joi";
5934
+ import Joi14 from "joi";
5643
5935
  import { ObjectId as ObjectId14 } from "mongodb";
5644
5936
  import { BadRequestError as BadRequestError20, logger as logger24 } from "@7365admin1/node-server-utils";
5645
5937
  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)
5938
+ var stockSchema = Joi14.object({
5939
+ site: Joi14.string().hex().required(),
5940
+ serviceType: Joi14.string().valid(...Object.values(AppServiceType13)).required(),
5941
+ supply: Joi14.string().hex().required(),
5942
+ in: Joi14.number().min(0).optional(),
5943
+ out: Joi14.number().min(0).optional(),
5944
+ balance: Joi14.number().min(0).required(),
5945
+ remarks: Joi14.string().optional().allow("", null)
5654
5946
  });
5655
5947
  function MStock(value) {
5656
5948
  const { error } = stockSchema.validate(value);
@@ -5877,7 +6169,7 @@ function useStockService() {
5877
6169
  }
5878
6170
 
5879
6171
  // src/controllers/hygiene-stock.controller.ts
5880
- import Joi14 from "joi";
6172
+ import Joi15 from "joi";
5881
6173
  import { BadRequestError as BadRequestError23, logger as logger26 } from "@7365admin1/node-server-utils";
5882
6174
  import { AppServiceType as AppServiceType14 } from "@7365admin1/core";
5883
6175
  function useStockController() {
@@ -5885,12 +6177,12 @@ function useStockController() {
5885
6177
  const { createStock: _createStock } = useStockService();
5886
6178
  async function createStock(req, res, next) {
5887
6179
  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)
6180
+ const validation = Joi15.object({
6181
+ site: Joi15.string().hex().required(),
6182
+ serviceType: Joi15.string().valid(...Object.values(AppServiceType14)).required(),
6183
+ supply: Joi15.string().hex().required(),
6184
+ qty: Joi15.number().min(0).required(),
6185
+ remarks: Joi15.string().optional().allow("", null)
5894
6186
  });
5895
6187
  const { error } = validation.validate(payload);
5896
6188
  if (error) {
@@ -5910,13 +6202,13 @@ function useStockController() {
5910
6202
  }
5911
6203
  async function getStocksBySupplyId(req, res, next) {
5912
6204
  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()
6205
+ const validation = Joi15.object({
6206
+ page: Joi15.number().min(1).optional().allow("", null),
6207
+ limit: Joi15.number().min(1).optional().allow("", null),
6208
+ search: Joi15.string().optional().allow("", null),
6209
+ site: Joi15.string().hex().required(),
6210
+ serviceType: Joi15.string().valid(...Object.values(AppServiceType14)).required(),
6211
+ supply: Joi15.string().hex().required()
5920
6212
  });
5921
6213
  const { error } = validation.validate(query);
5922
6214
  if (error) {
@@ -5978,21 +6270,21 @@ async function applyCheckOutDecision(decision, decide, returnStock) {
5978
6270
  }
5979
6271
 
5980
6272
  // src/models/hygiene-checkout-item.model.ts
5981
- import Joi15 from "joi";
6273
+ import Joi16 from "joi";
5982
6274
  import { ObjectId as ObjectId16 } from "mongodb";
5983
6275
  import { BadRequestError as BadRequestError24, logger as logger27 } from "@7365admin1/node-server-utils";
5984
6276
  import { AppServiceType as AppServiceType15 } from "@7365admin1/core";
5985
6277
  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()
6278
+ var checkOutItemSchema = Joi16.object({
6279
+ site: Joi16.string().hex().required(),
6280
+ serviceType: Joi16.string().valid(...Object.values(AppServiceType15)).required(),
6281
+ supply: Joi16.string().hex().required(),
6282
+ supplyName: Joi16.string().required(),
6283
+ supplyQty: Joi16.number().min(0).required(),
6284
+ qty: Joi16.number().min(0).required(),
6285
+ attachment: Joi16.array().items(Joi16.string()).optional().allow(null),
6286
+ createdBy: Joi16.string().hex().required(),
6287
+ createdByName: Joi16.string().required()
5996
6288
  });
5997
6289
  function MCheckOutItem(value) {
5998
6290
  const { error } = checkOutItemSchema.validate(value);
@@ -6125,7 +6417,7 @@ function useCheckOutItemRepository() {
6125
6417
  throw new BadRequestError25("Invalid site ID format.");
6126
6418
  }
6127
6419
  if (search) {
6128
- query.supplyName = { $regex: search, $options: "i" };
6420
+ query.supplyName = { $regex: escapeRegex(search), $options: "i" };
6129
6421
  cacheOptions.search = search;
6130
6422
  }
6131
6423
  const cacheKey = makeCacheKey8(namespace_collection, cacheOptions);
@@ -6481,7 +6773,7 @@ function useCheckOutItemService() {
6481
6773
  }
6482
6774
 
6483
6775
  // src/controllers/hygiene-checkout-item.controller.ts
6484
- import Joi16 from "joi";
6776
+ import Joi17 from "joi";
6485
6777
  import { BadRequestError as BadRequestError27, logger as logger29 } from "@7365admin1/node-server-utils";
6486
6778
  import { AppServiceType as AppServiceType16 } from "@7365admin1/core";
6487
6779
  function useCheckOutItemController() {
@@ -6505,13 +6797,13 @@ function useCheckOutItemController() {
6505
6797
  ...req.params,
6506
6798
  createdBy
6507
6799
  };
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()
6800
+ const validation = Joi17.object({
6801
+ site: Joi17.string().hex().required(),
6802
+ serviceType: Joi17.string().valid(...Object.values(AppServiceType16)).required(),
6803
+ supply: Joi17.string().hex().required(),
6804
+ qty: Joi17.number().min(0).required(),
6805
+ attachment: Joi17.array().items(Joi17.string()).optional().allow(null),
6806
+ createdBy: Joi17.string().hex().required()
6515
6807
  });
6516
6808
  const { error } = validation.validate(payload);
6517
6809
  if (error) {
@@ -6540,15 +6832,15 @@ function useCheckOutItemController() {
6540
6832
  ...req.params,
6541
6833
  createdBy
6542
6834
  };
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)
6835
+ const validation = Joi17.object({
6836
+ site: Joi17.string().hex().required(),
6837
+ serviceType: Joi17.string().valid(...Object.values(AppServiceType16)).required(),
6838
+ createdBy: Joi17.string().hex().required(),
6839
+ items: Joi17.array().items(
6840
+ Joi17.object({
6841
+ supply: Joi17.string().hex().required(),
6842
+ qty: Joi17.number().min(0).required(),
6843
+ attachment: Joi17.array().items(Joi17.string()).optional().allow(null)
6552
6844
  })
6553
6845
  ).min(1).required()
6554
6846
  });
@@ -6570,12 +6862,12 @@ function useCheckOutItemController() {
6570
6862
  }
6571
6863
  async function getCheckOutItems(req, res, next) {
6572
6864
  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()
6865
+ const validation = Joi17.object({
6866
+ page: Joi17.number().min(1).optional().allow("", null),
6867
+ limit: Joi17.number().min(1).optional().allow("", null),
6868
+ search: Joi17.string().optional().allow("", null),
6869
+ site: Joi17.string().hex().required(),
6870
+ serviceType: Joi17.string().valid(...Object.values(AppServiceType16)).required()
6579
6871
  });
6580
6872
  const { error } = validation.validate(query);
6581
6873
  if (error) {
@@ -6605,7 +6897,7 @@ function useCheckOutItemController() {
6605
6897
  }
6606
6898
  }
6607
6899
  async function getCheckOutItemById(req, res, next) {
6608
- const validation = Joi16.string().hex().required();
6900
+ const validation = Joi17.string().hex().required();
6609
6901
  const _id = req.params.id;
6610
6902
  const { error, value } = validation.validate(_id);
6611
6903
  if (error) {
@@ -6644,13 +6936,13 @@ function useCheckOutItemController() {
6644
6936
  ...decisionValues,
6645
6937
  completedBy
6646
6938
  };
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()
6939
+ const validation = Joi17.object({
6940
+ id: Joi17.string().hex().required(),
6941
+ decision: Joi17.string().valid("approve", "disapprove").required(),
6942
+ approve: Joi17.boolean().required(),
6943
+ reject: Joi17.boolean().required(),
6944
+ remarks: Joi17.string().optional().allow("", null),
6945
+ completedBy: Joi17.string().hex().required()
6654
6946
  });
6655
6947
  const { error } = validation.validate(payload);
6656
6948
  if (error) {
@@ -6683,23 +6975,15 @@ function useCheckOutItemController() {
6683
6975
  // src/models/hygiene-schedule-task.model.ts
6684
6976
  import { BadRequestError as BadRequestError28, logger as logger30 } from "@7365admin1/node-server-utils";
6685
6977
  import { AppServiceType as AppServiceType17 } from "@7365admin1/core";
6686
- import Joi17 from "joi";
6978
+ import Joi18 from "joi";
6687
6979
  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()
6980
+ var scheduleTaskSchema = Joi18.object({
6981
+ site: Joi18.string().hex().required(),
6982
+ serviceType: Joi18.string().valid(...Object.values(AppServiceType17)).required(),
6983
+ ...scheduleTaskFields
6984
+ }).custom((value, helpers) => {
6985
+ const message = validateScheduleRules(cleanScheduleInput(value));
6986
+ return message ? helpers.message({ custom: message }) : value;
6703
6987
  });
6704
6988
  function MScheduleTask(value) {
6705
6989
  const { error } = scheduleTaskSchema.validate(value);
@@ -6726,14 +7010,26 @@ function MScheduleTask(value) {
6726
7010
  }
6727
7011
  });
6728
7012
  }
7013
+ const extra = cleanScheduleInput({
7014
+ repeat: value.repeat,
7015
+ daysOfWeek: value.daysOfWeek,
7016
+ dayOfMonth: value.dayOfMonth,
7017
+ startDate: value.startDate,
7018
+ endDate: value.endDate,
7019
+ paused: value.paused
7020
+ });
7021
+ for (const key of Object.keys(extra))
7022
+ if (extra[key] === void 0)
7023
+ delete extra[key];
6729
7024
  return {
6730
7025
  site: value.site,
6731
7026
  serviceType: value.serviceType,
6732
7027
  title: value.title,
6733
7028
  time: value.time,
6734
- dates: value.dates,
7029
+ dates: value.dates ?? [],
6735
7030
  description: value.description,
6736
7031
  areas: value.areas,
7032
+ ...extra,
6737
7033
  status: "active",
6738
7034
  createdAt: /* @__PURE__ */ new Date(),
6739
7035
  updatedAt: "",
@@ -6753,11 +7049,23 @@ import {
6753
7049
  makeCacheKey as makeCacheKey9,
6754
7050
  NotFoundError as NotFoundError7
6755
7051
  } from "@7365admin1/node-server-utils";
7052
+ var SCHEDULE_FIELDS = {
7053
+ time: 1,
7054
+ dates: 1,
7055
+ repeat: 1,
7056
+ daysOfWeek: 1,
7057
+ dayOfMonth: 1,
7058
+ startDate: 1,
7059
+ endDate: 1,
7060
+ paused: 1,
7061
+ lastRunDate: 1
7062
+ };
6756
7063
  function useScheduleTaskRepository() {
6757
7064
  const db = useAtlas14.getDb();
6758
7065
  if (!db) {
6759
7066
  throw new InternalServerError11("Unable to connect to server.");
6760
7067
  }
7068
+ const database = db;
6761
7069
  const namespace_collection = "site.schedule-tasks";
6762
7070
  const collection = db.collection(namespace_collection);
6763
7071
  const { delNamespace, setCache, getCache } = useCache9(namespace_collection);
@@ -6825,14 +7133,14 @@ function useScheduleTaskRepository() {
6825
7133
  throw new BadRequestError29("Invalid site ID format.");
6826
7134
  }
6827
7135
  if (search) {
6828
- query.$or = [{ title: { $regex: search, $options: "i" } }];
7136
+ query.$or = [{ title: { $regex: escapeRegex(search), $options: "i" } }];
6829
7137
  cacheOptions.search = search;
6830
7138
  }
6831
7139
  const cacheKey = makeCacheKey9(namespace_collection, cacheOptions);
6832
7140
  const cachedData = await getCache(cacheKey);
6833
7141
  if (cachedData) {
6834
7142
  logger31.info(`Cache hit for key: ${cacheKey}`);
6835
- return cachedData;
7143
+ return enrichList(cachedData, site, serviceType);
6836
7144
  }
6837
7145
  try {
6838
7146
  const items = await collection.aggregate([
@@ -6841,7 +7149,8 @@ function useScheduleTaskRepository() {
6841
7149
  $project: {
6842
7150
  title: 1,
6843
7151
  areas: 1,
6844
- status: 1
7152
+ status: 1,
7153
+ ...SCHEDULE_FIELDS
6845
7154
  }
6846
7155
  },
6847
7156
  { $sort: { _id: -1 } },
@@ -6855,15 +7164,104 @@ function useScheduleTaskRepository() {
6855
7164
  }).catch((err) => {
6856
7165
  logger31.error(`Failed to set cache for key: ${cacheKey}`, err);
6857
7166
  });
6858
- return data;
7167
+ return enrichList(data, site, serviceType);
6859
7168
  } catch (error) {
6860
7169
  throw error;
6861
7170
  }
6862
7171
  }
6863
- async function getAllScheduleTask() {
7172
+ async function enrichList(data, site, serviceType) {
7173
+ const items = data?.items ?? [];
7174
+ const now = /* @__PURE__ */ new Date();
7175
+ let statuses = /* @__PURE__ */ new Map();
7176
+ try {
7177
+ statuses = await getRunStatuses(items, site, serviceType);
7178
+ } catch (error) {
7179
+ logger31.error("Could not read schedule task run status", error);
7180
+ }
7181
+ return {
7182
+ ...data,
7183
+ items: items.map((item) => {
7184
+ try {
7185
+ return {
7186
+ ...item,
7187
+ ...scheduleDisplay(item, now),
7188
+ lastRunStatus: statuses.get(String(item._id)) ?? null
7189
+ };
7190
+ } catch (error) {
7191
+ logger31.error(`Could not enrich schedule task ${item?._id}`, error);
7192
+ return { ...item, nextRun: null, lastRun: null, scheduleStatus: null, lastRunStatus: null };
7193
+ }
7194
+ })
7195
+ };
7196
+ }
7197
+ async function getRunStatuses(items, site, serviceType) {
7198
+ const result = /* @__PURE__ */ new Map();
7199
+ const ran = items.filter((i) => i.lastRunDate);
7200
+ if (!ran.length)
7201
+ return result;
7202
+ const parents = await database.collection("site.cleaning.schedules").find(
7203
+ {
7204
+ site,
7205
+ serviceType,
7206
+ dateStr: { $in: [...new Set(ran.map((i) => i.lastRunDate))] }
7207
+ },
7208
+ { projection: { dateStr: 1 } }
7209
+ ).toArray();
7210
+ if (!parents.length)
7211
+ return result;
7212
+ const dateOf = new Map(parents.map((p) => [String(p._id), p.dateStr]));
7213
+ const taskIds = ran.map((i) => new ObjectId19(i._id));
7214
+ const rows = await database.collection("site.cleaning.schedule.areas").aggregate([
7215
+ {
7216
+ $match: {
7217
+ schedule: { $in: parents.map((p) => p._id) },
7218
+ "checklist.scheduleTaskId": { $in: taskIds }
7219
+ }
7220
+ },
7221
+ { $unwind: "$checklist" },
7222
+ { $match: { "checklist.scheduleTaskId": { $in: taskIds } } },
7223
+ { $unwind: "$checklist.units" },
7224
+ {
7225
+ $group: {
7226
+ _id: { task: "$checklist.scheduleTaskId", schedule: "$schedule" },
7227
+ total: { $sum: 1 },
7228
+ completed: {
7229
+ $sum: { $cond: [{ $eq: ["$checklist.units.status", "completed"] }, 1, 0] }
7230
+ },
7231
+ closed: {
7232
+ $sum: { $cond: [{ $eq: ["$checklist.units.status", "closed"] }, 1, 0] }
7233
+ }
7234
+ }
7235
+ }
7236
+ ]).toArray();
7237
+ const sums = /* @__PURE__ */ new Map();
7238
+ for (const row of rows) {
7239
+ const task = ran.find((i) => String(i._id) === String(row._id.task));
7240
+ if (!task || dateOf.get(String(row._id.schedule)) !== task.lastRunDate)
7241
+ continue;
7242
+ const sum = sums.get(String(task._id)) ?? { total: 0, completed: 0, closed: 0 };
7243
+ sum.total += row.total;
7244
+ sum.completed += row.completed;
7245
+ sum.closed += row.closed;
7246
+ sums.set(String(task._id), sum);
7247
+ }
7248
+ for (const [id, sum] of sums) {
7249
+ result.set(id, sum.completed === sum.total ? "done" : sum.closed > 0 ? "missed" : "pending");
7250
+ }
7251
+ return result;
7252
+ }
7253
+ async function getAllScheduleTask(today) {
6864
7254
  const query = {
6865
7255
  status: { $ne: "deleted" }
6866
7256
  };
7257
+ if (today) {
7258
+ query.paused = { $ne: true };
7259
+ query.lastRunDate = { $ne: today };
7260
+ query.$or = [
7261
+ { dates: today },
7262
+ { repeat: { $in: ["daily", "weekdays", "weekly", "monthly"] } }
7263
+ ];
7264
+ }
6867
7265
  try {
6868
7266
  const items = await collection.aggregate([{ $match: query }, { $sort: { _id: -1 } }]).toArray();
6869
7267
  return items;
@@ -6871,6 +7269,9 @@ function useScheduleTaskRepository() {
6871
7269
  throw error;
6872
7270
  }
6873
7271
  }
7272
+ function withScheduleDisplay(row) {
7273
+ return { ...row, ...scheduleDisplay(row, /* @__PURE__ */ new Date()) };
7274
+ }
6874
7275
  async function getScheduleTaskById(_id, session) {
6875
7276
  try {
6876
7277
  _id = new ObjectId19(_id);
@@ -6888,7 +7289,7 @@ function useScheduleTaskRepository() {
6888
7289
  const cachedData = await getCache(cacheKey);
6889
7290
  if (cachedData) {
6890
7291
  logger31.info(`Cache hit for key: ${cacheKey}`);
6891
- return cachedData;
7292
+ return withScheduleDisplay(cachedData);
6892
7293
  }
6893
7294
  } else {
6894
7295
  logger31.info(`Skipping cache during transaction for key: ${cacheKey}`);
@@ -6899,12 +7300,13 @@ function useScheduleTaskRepository() {
6899
7300
  {
6900
7301
  $project: {
6901
7302
  title: 1,
6902
- time: 1,
6903
- dates: 1,
6904
7303
  description: 1,
6905
7304
  areas: 1,
6906
7305
  status: 1,
6907
- createdAt: 1
7306
+ createdAt: 1,
7307
+ site: 1,
7308
+ serviceType: 1,
7309
+ ...SCHEDULE_FIELDS
6908
7310
  }
6909
7311
  }
6910
7312
  ]).toArray();
@@ -6916,7 +7318,7 @@ function useScheduleTaskRepository() {
6916
7318
  }).catch((err) => {
6917
7319
  logger31.error(`Failed to set cache for key: ${cacheKey}`, err);
6918
7320
  });
6919
- return data[0];
7321
+ return withScheduleDisplay(data[0]);
6920
7322
  } catch (error) {
6921
7323
  throw error;
6922
7324
  }
@@ -6940,16 +7342,20 @@ function useScheduleTaskRepository() {
6940
7342
  });
6941
7343
  }
6942
7344
  try {
6943
- const updateValue = { ...value, updatedAt: /* @__PURE__ */ new Date() };
7345
+ const { set, unset } = splitScheduleUpdate(value);
7346
+ const update = {
7347
+ $set: { ...set, updatedAt: /* @__PURE__ */ new Date() }
7348
+ };
7349
+ if (unset.length) {
7350
+ update.$unset = Object.fromEntries(unset.map((key) => [key, ""]));
7351
+ }
6944
7352
  const res = await collection.updateOne(
6945
- { _id },
6946
- { $set: updateValue },
7353
+ { _id, status: { $ne: "deleted" } },
7354
+ update,
6947
7355
  { session }
6948
7356
  );
6949
- if (res.modifiedCount === 0) {
6950
- throw new InternalServerError11(
6951
- "Unable to update hygiene schedule task."
6952
- );
7357
+ if (res.matchedCount === 0) {
7358
+ throw new NotFoundError7("Schedule task not found.");
6953
7359
  }
6954
7360
  delNamespace().then(() => {
6955
7361
  logger31.info(`Cache cleared for namespace: ${namespace_collection}`);
@@ -6964,6 +7370,18 @@ function useScheduleTaskRepository() {
6964
7370
  throw error;
6965
7371
  }
6966
7372
  }
7373
+ async function markScheduleTaskRan(_id, date) {
7374
+ await collection.updateOne(
7375
+ { _id: new ObjectId19(_id) },
7376
+ { $set: { lastRunDate: date, lastRunAt: /* @__PURE__ */ new Date() } }
7377
+ );
7378
+ delNamespace().catch((err) => {
7379
+ logger31.error(
7380
+ `Failed to clear cache for namespace: ${namespace_collection}`,
7381
+ err
7382
+ );
7383
+ });
7384
+ }
6967
7385
  async function deleteScheduleTask(_id, session) {
6968
7386
  try {
6969
7387
  _id = new ObjectId19(_id);
@@ -7003,18 +7421,31 @@ function useScheduleTaskRepository() {
7003
7421
  createScheduleTask,
7004
7422
  getScheduleTasks,
7005
7423
  getAllScheduleTask,
7424
+ markScheduleTaskRan,
7006
7425
  getScheduleTaskById,
7007
7426
  updateScheduleTask,
7008
7427
  deleteScheduleTask
7009
7428
  };
7010
7429
  }
7011
7430
 
7431
+ // src/utils/schedule-task-set.util.ts
7432
+ function buildScheduleTaskSets(task, units, startSet, count) {
7433
+ return Array.from({ length: count }, (_, i) => ({
7434
+ set: startSet + i,
7435
+ units,
7436
+ isScheduleTask: true,
7437
+ scheduleTaskId: task._id,
7438
+ scheduleTaskTitle: task.title,
7439
+ scheduleTaskTime: task.time
7440
+ }));
7441
+ }
7442
+
7012
7443
  // src/services/hygiene-schedule-task.service.ts
7013
7444
  import { ObjectId as ObjectId20 } from "mongodb";
7014
7445
  import { logger as logger32 } from "@7365admin1/node-server-utils";
7015
7446
  function useScheduleTaskService() {
7016
7447
  const { createParentChecklist } = useParentChecklistRepo();
7017
- const { getAllScheduleTask } = useScheduleTaskRepository();
7448
+ const { getAllScheduleTask, markScheduleTaskRan } = useScheduleTaskRepository();
7018
7449
  const {
7019
7450
  createAreaChecklist,
7020
7451
  getAreaChecklistByAreaAndSchedule,
@@ -7023,73 +7454,7 @@ function useScheduleTaskService() {
7023
7454
  const { getAreaById } = useAreaRepo();
7024
7455
  function checkScheduleConditions(schedule, serviceType, currentDate = /* @__PURE__ */ new Date()) {
7025
7456
  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;
7457
+ return shouldRunNow(schedule, currentDate);
7093
7458
  } catch (error) {
7094
7459
  logger32.error(
7095
7460
  `Error checking schedule conditions for ${schedule._id}:`,
@@ -7098,18 +7463,22 @@ function useScheduleTaskService() {
7098
7463
  return false;
7099
7464
  }
7100
7465
  }
7466
+ const MAX_ATTEMPTS = 5;
7467
+ const attempts = /* @__PURE__ */ new Map();
7101
7468
  async function processScheduledTasks(currentDate) {
7102
7469
  try {
7103
7470
  logger32.info("Starting scheduled task processing...");
7104
- const scheduleTasks = await getAllScheduleTask();
7471
+ const today = sgtParts(currentDate ?? /* @__PURE__ */ new Date()).date;
7472
+ const scheduleTasks = await getAllScheduleTask(today);
7105
7473
  if (!scheduleTasks || scheduleTasks.length === 0) {
7106
7474
  logger32.info("No schedule tasks found to process");
7107
- return { processed: 0, validated: 0 };
7475
+ return { processed: 0, validated: 0, fired: [] };
7108
7476
  }
7109
7477
  logger32.info(`Found ${scheduleTasks.length} schedule tasks to check`);
7110
7478
  let processedCount = 0;
7111
7479
  let validatedCount = 0;
7112
7480
  const validatedTasks = [];
7481
+ const fired = [];
7113
7482
  for (const scheduleTask of scheduleTasks) {
7114
7483
  try {
7115
7484
  const serviceType = scheduleTask.serviceType;
@@ -7152,6 +7521,8 @@ function useScheduleTaskService() {
7152
7521
  logger32.info(
7153
7522
  `Using parent checklist ${parentChecklistId}, now creating/updating area checklists`
7154
7523
  );
7524
+ let failedAreas = 0;
7525
+ const addedAreas = [];
7155
7526
  for (const area of scheduleTask.areas) {
7156
7527
  try {
7157
7528
  const areaId = area.value.toString();
@@ -7214,15 +7585,7 @@ function useScheduleTaskService() {
7214
7585
  )
7215
7586
  ) : 0;
7216
7587
  const startSetNumber = maxSetInChecklist + 1;
7217
- const newSets = Array.from(
7218
- { length: areaSetConfig },
7219
- (_, index) => ({
7220
- set: startSetNumber + index,
7221
- units,
7222
- isScheduleTask: true,
7223
- scheduleTaskId: scheduleTask._id
7224
- })
7225
- );
7588
+ const newSets = buildScheduleTaskSets(scheduleTask, units, startSetNumber, areaSetConfig);
7226
7589
  const modified = await pushScheduleTaskSets(
7227
7590
  parentChecklistId.toString(),
7228
7591
  serviceType,
@@ -7231,6 +7594,7 @@ function useScheduleTaskService() {
7231
7594
  newSets
7232
7595
  );
7233
7596
  if (modified > 0) {
7597
+ addedAreas.push(area.name);
7234
7598
  logger32.info(
7235
7599
  `Appended ${areaSetConfig} set(s) starting from ${startSetNumber} to area checklist for area ${area.name}`
7236
7600
  );
@@ -7241,16 +7605,7 @@ function useScheduleTaskService() {
7241
7605
  }
7242
7606
  } else {
7243
7607
  const areaSetConfig = Number(areaDetails.set) || 1;
7244
- const schedTaskId = scheduleTask._id;
7245
- const schedTaskSets = Array.from(
7246
- { length: areaSetConfig },
7247
- (_, index) => ({
7248
- set: index + 1,
7249
- units,
7250
- isScheduleTask: true,
7251
- scheduleTaskId: schedTaskId
7252
- })
7253
- );
7608
+ const schedTaskSets = buildScheduleTaskSets(scheduleTask, units, 1, areaSetConfig);
7254
7609
  const checklistData = {
7255
7610
  schedule: parentChecklistId.toString(),
7256
7611
  serviceType,
@@ -7263,13 +7618,32 @@ function useScheduleTaskService() {
7263
7618
  `Area ${area.name} (${areaId}): Creating new area checklist with schedule-task sets only`
7264
7619
  );
7265
7620
  await createAreaChecklist(checklistData);
7621
+ addedAreas.push(area.name);
7266
7622
  logger32.info(`Created new area checklist for area ${area.name}`);
7267
7623
  }
7268
7624
  } catch (error) {
7269
7625
  logger32.error(`Error processing area ${area.name}:`, error);
7626
+ failedAreas++;
7270
7627
  continue;
7271
7628
  }
7272
7629
  }
7630
+ const attemptKey = `${scheduleTask._id}:${today}`;
7631
+ const tried = (attempts.get(attemptKey) ?? 0) + 1;
7632
+ attempts.set(attemptKey, tried);
7633
+ if (failedAreas === 0 || tried >= MAX_ATTEMPTS) {
7634
+ await markScheduleTaskRan(scheduleTask._id, today);
7635
+ attempts.delete(attemptKey);
7636
+ }
7637
+ if (addedAreas.length > 0) {
7638
+ fired.push({
7639
+ _id: scheduleTask._id,
7640
+ site: scheduleTask.site,
7641
+ serviceType,
7642
+ title: scheduleTask.title,
7643
+ time: scheduleTask.time,
7644
+ areas: addedAreas
7645
+ });
7646
+ }
7273
7647
  processedCount++;
7274
7648
  validatedCount++;
7275
7649
  validatedTasks.push(scheduleTask);
@@ -7281,6 +7655,17 @@ function useScheduleTaskService() {
7281
7655
  `Error processing schedule task ${scheduleTask._id}:`,
7282
7656
  error
7283
7657
  );
7658
+ try {
7659
+ const key = `${scheduleTask._id}:${today}`;
7660
+ const tried = (attempts.get(key) ?? 0) + 1;
7661
+ attempts.set(key, tried);
7662
+ if (tried >= MAX_ATTEMPTS && scheduleTask._id) {
7663
+ await markScheduleTaskRan(scheduleTask._id, today);
7664
+ attempts.delete(key);
7665
+ }
7666
+ } catch (markError) {
7667
+ logger32.error("Could not mark failing schedule task", markError);
7668
+ }
7284
7669
  continue;
7285
7670
  }
7286
7671
  }
@@ -7290,7 +7675,8 @@ function useScheduleTaskService() {
7290
7675
  return {
7291
7676
  processed: processedCount,
7292
7677
  validated: validatedCount,
7293
- tasks: validatedTasks
7678
+ tasks: validatedTasks,
7679
+ fired
7294
7680
  };
7295
7681
  } catch (error) {
7296
7682
  logger32.error("Error processing scheduled tasks:", error);
@@ -7301,7 +7687,7 @@ function useScheduleTaskService() {
7301
7687
  }
7302
7688
 
7303
7689
  // src/controllers/hygiene-schedule-task.controller.ts
7304
- import Joi18 from "joi";
7690
+ import Joi19 from "joi";
7305
7691
  import { BadRequestError as BadRequestError30, logger as logger33 } from "@7365admin1/node-server-utils";
7306
7692
  import { AppServiceType as AppServiceType18 } from "@7365admin1/core";
7307
7693
  function useScheduleTaskController() {
@@ -7332,12 +7718,12 @@ function useScheduleTaskController() {
7332
7718
  }
7333
7719
  async function getScheduleTasks(req, res, next) {
7334
7720
  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()
7721
+ const validation = Joi19.object({
7722
+ page: Joi19.number().min(1).optional().allow("", null),
7723
+ limit: Joi19.number().min(1).optional().allow("", null),
7724
+ search: Joi19.string().optional().allow("", null),
7725
+ site: Joi19.string().hex().required(),
7726
+ serviceType: Joi19.string().valid(...Object.values(AppServiceType18)).required()
7341
7727
  });
7342
7728
  const { error } = validation.validate(query);
7343
7729
  if (error) {
@@ -7345,8 +7731,8 @@ function useScheduleTaskController() {
7345
7731
  next(new BadRequestError30(error.message));
7346
7732
  return;
7347
7733
  }
7348
- const page = parseInt(req.query.page) ?? 1;
7349
- const limit = parseInt(req.query.limit) ?? 10;
7734
+ const page = parseInt(req.query.page) || 1;
7735
+ const limit = parseInt(req.query.limit) || 10;
7350
7736
  const search = req.query.search ?? "";
7351
7737
  const site = req.params.site ?? "";
7352
7738
  const serviceType = req.query.serviceType ?? "";
@@ -7367,7 +7753,7 @@ function useScheduleTaskController() {
7367
7753
  }
7368
7754
  }
7369
7755
  async function getScheduleTaskById(req, res, next) {
7370
- const validation = Joi18.string().hex().required();
7756
+ const validation = Joi19.string().hex().required();
7371
7757
  const _id = req.params.id;
7372
7758
  const { error, value } = validation.validate(_id);
7373
7759
  if (error) {
@@ -7387,20 +7773,17 @@ function useScheduleTaskController() {
7387
7773
  }
7388
7774
  async function updateScheduleTask(req, res, next) {
7389
7775
  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()
7776
+ const optional = Object.fromEntries(
7777
+ Object.entries(scheduleTaskFields).map(([key, rule]) => [
7778
+ key,
7779
+ key === "dates" ? Joi19.array().min(1).items(scheduleDate).optional().messages({
7780
+ "array.min": "Pick at least one date, or choose a repeat."
7781
+ }) : rule.optional()
7782
+ ])
7783
+ );
7784
+ const validation = Joi19.object({
7785
+ id: Joi19.string().hex().required(),
7786
+ ...optional
7404
7787
  });
7405
7788
  const { error } = validation.validate(payload);
7406
7789
  if (error) {
@@ -7408,6 +7791,24 @@ function useScheduleTaskController() {
7408
7791
  next(new BadRequestError30(error.message));
7409
7792
  return;
7410
7793
  }
7794
+ try {
7795
+ const { set, unset } = splitScheduleUpdate(req.body ?? {});
7796
+ if (TIMING_KEYS.some((key) => key in set || unset.includes(key))) {
7797
+ const stored = await _getScheduleTaskById(req.params.id);
7798
+ const merged = { ...stored, ...set };
7799
+ for (const key of unset)
7800
+ delete merged[key];
7801
+ const problem = timingChanged(stored, merged) ? validateScheduleRules(cleanScheduleInput(merged)) : null;
7802
+ if (problem) {
7803
+ next(new BadRequestError30(problem));
7804
+ return;
7805
+ }
7806
+ }
7807
+ } catch (error2) {
7808
+ logger33.log({ level: "error", message: error2.message });
7809
+ next(error2);
7810
+ return;
7811
+ }
7411
7812
  try {
7412
7813
  const { id, ...value } = payload;
7413
7814
  await _updateScheduleTask(id, value);
@@ -7420,7 +7821,7 @@ function useScheduleTaskController() {
7420
7821
  }
7421
7822
  }
7422
7823
  async function deleteScheduleTask(req, res, next) {
7423
- const validation = Joi18.string().hex().required();
7824
+ const validation = Joi19.string().hex().required();
7424
7825
  const _id = req.params.id;
7425
7826
  const { error, value } = validation.validate(_id);
7426
7827
  if (error) {
@@ -7664,17 +8065,17 @@ function useQRService() {
7664
8065
  }
7665
8066
 
7666
8067
  // src/controllers/hygiene-qr.controller.ts
7667
- import Joi19 from "joi";
8068
+ import Joi20 from "joi";
7668
8069
  import { BadRequestError as BadRequestError31, logger as logger35 } from "@7365admin1/node-server-utils";
7669
8070
  function useQRController() {
7670
8071
  const { generateQRImage: _generateQRImage, generateQRPDF: _generateQRPDF } = useQRService();
7671
8072
  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)
8073
+ const validation = Joi20.object({
8074
+ url: Joi20.string().uri().required(),
8075
+ filename: Joi20.string().optional().allow("", null),
8076
+ title: Joi20.string().optional().allow("", null),
8077
+ subtitle: Joi20.string().optional().allow("", null),
8078
+ download: Joi20.boolean().optional().default(false)
7678
8079
  });
7679
8080
  const query = { ...req.query };
7680
8081
  const { error, value } = validation.validate(query);