@7365admin1/module-hygiene 4.29.1-staging.15 → 4.29.1-staging.17

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
@@ -3186,7 +3186,9 @@ var areaChecklistSchema = Joi9.object({
3186
3186
  }).required()
3187
3187
  ).min(1).required(),
3188
3188
  isScheduleTask: Joi9.boolean().optional().default(false),
3189
- 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)
3190
3192
  }).required()
3191
3193
  ).optional().default([])
3192
3194
  });
@@ -3216,6 +3218,8 @@ function MAreaChecklist(value) {
3216
3218
  set: checklistItem.set,
3217
3219
  isScheduleTask: checklistItem.isScheduleTask ?? false,
3218
3220
  scheduleTaskId: checklistItem.scheduleTaskId ?? void 0,
3221
+ scheduleTaskTitle: checklistItem.scheduleTaskTitle ?? void 0,
3222
+ scheduleTaskTime: checklistItem.scheduleTaskTime ?? void 0,
3219
3223
  units: checklistItem.units.map((unit) => ({
3220
3224
  unit: new ObjectId8(unit.unit),
3221
3225
  name: unit.name,
@@ -3710,6 +3714,9 @@ function useAreaChecklistRepo() {
3710
3714
  isScheduleTask: {
3711
3715
  $ifNull: ["$checklist.isScheduleTask", false]
3712
3716
  },
3717
+ scheduleTaskId: { $ifNull: ["$checklist.scheduleTaskId", null] },
3718
+ scheduleTaskTitle: { $ifNull: ["$checklist.scheduleTaskTitle", null] },
3719
+ scheduleTaskTime: { $ifNull: ["$checklist.scheduleTaskTime", null] },
3713
3720
  unit: "$checklist.units.unit",
3714
3721
  name: "$checklist.units.name",
3715
3722
  approve: "$checklist.units.approve",
@@ -3746,6 +3753,9 @@ function useAreaChecklistRepo() {
3746
3753
  $group: {
3747
3754
  _id: "$set",
3748
3755
  isScheduleTask: { $first: "$isScheduleTask" },
3756
+ scheduleTaskId: { $first: "$scheduleTaskId" },
3757
+ scheduleTaskTitle: { $first: "$scheduleTaskTitle" },
3758
+ scheduleTaskTime: { $first: "$scheduleTaskTime" },
3749
3759
  units: {
3750
3760
  $push: {
3751
3761
  unit: "$unit",
@@ -3766,6 +3776,9 @@ function useAreaChecklistRepo() {
3766
3776
  _id: 0,
3767
3777
  set: "$_id",
3768
3778
  isScheduleTask: 1,
3779
+ scheduleTaskId: 1,
3780
+ scheduleTaskTitle: 1,
3781
+ scheduleTaskTime: 1,
3769
3782
  units: 1
3770
3783
  }
3771
3784
  },
@@ -3897,6 +3910,8 @@ function useAreaChecklistRepo() {
3897
3910
  isScheduleTask: {
3898
3911
  $ifNull: ["$checklist.isScheduleTask", false]
3899
3912
  },
3913
+ scheduleTaskTitle: { $ifNull: ["$checklist.scheduleTaskTitle", null] },
3914
+ scheduleTaskTime: { $ifNull: ["$checklist.scheduleTaskTime", null] },
3900
3915
  scheduleTaskId: {
3901
3916
  $ifNull: ["$checklist.scheduleTaskId", null]
3902
3917
  },
@@ -3937,6 +3952,8 @@ function useAreaChecklistRepo() {
3937
3952
  $group: {
3938
3953
  _id: "$set",
3939
3954
  isScheduleTask: { $first: "$isScheduleTask" },
3955
+ scheduleTaskTitle: { $first: "$scheduleTaskTitle" },
3956
+ scheduleTaskTime: { $first: "$scheduleTaskTime" },
3940
3957
  scheduleTaskId: { $first: "$scheduleTaskId" },
3941
3958
  remarks: { $first: "$remarks" },
3942
3959
  attachment: { $first: "$attachment" },
@@ -3961,6 +3978,8 @@ function useAreaChecklistRepo() {
3961
3978
  _id: 0,
3962
3979
  set: "$_id",
3963
3980
  isScheduleTask: 1,
3981
+ scheduleTaskTitle: 1,
3982
+ scheduleTaskTime: 1,
3964
3983
  scheduleTaskId: 1,
3965
3984
  remarks: "$remarks",
3966
3985
  attachment: "$attachment",
@@ -4436,6 +4455,265 @@ function useAreaChecklistRepo() {
4436
4455
  };
4437
4456
  }
4438
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
+
4439
4717
  // src/services/hygiene-area-checklist.service.ts
4440
4718
  function useAreaChecklistService() {
4441
4719
  const { getAreasForChecklist } = useAreaRepo();
@@ -4503,10 +4781,10 @@ function useAreaChecklistService() {
4503
4781
  `Area checklist for area ${area.name} exists but has no auto-gen sets. Inserting auto-gen sets.`
4504
4782
  );
4505
4783
  const setCount2 = Number(area.set) || 1;
4506
- const totalExistingSets = existing.checklist?.length ?? 0;
4507
- if (totalExistingSets >= setCount2) {
4784
+ const setsToAdd = regularSetsMissing(existing.checklist, setCount2);
4785
+ if (setsToAdd <= 0) {
4508
4786
  logger18.info(
4509
- `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.`
4510
4788
  );
4511
4789
  return existing._id;
4512
4790
  }
@@ -4514,7 +4792,6 @@ function useAreaChecklistService() {
4514
4792
  (max, c) => Math.max(max, c.set ?? 0),
4515
4793
  0
4516
4794
  ) ?? 0;
4517
- const setsToAdd = setCount2 - totalExistingSets;
4518
4795
  const autoGenSets = Array.from(
4519
4796
  { length: setsToAdd },
4520
4797
  (_, index) => ({
@@ -4675,7 +4952,7 @@ function useAreaChecklistService() {
4675
4952
  }
4676
4953
 
4677
4954
  // src/controllers/hygiene-area-checklist.controller.ts
4678
- import Joi10 from "joi";
4955
+ import Joi11 from "joi";
4679
4956
  import {
4680
4957
  BadRequestError as BadRequestError16,
4681
4958
  InternalServerError as InternalServerError7,
@@ -4687,6 +4964,8 @@ import { AppServiceType as AppServiceType10 } from "@7365admin1/core";
4687
4964
  import { launch } from "puppeteer";
4688
4965
  import { InternalServerError as InternalServerError6, useAtlas as useAtlas8 } from "@7365admin1/node-server-utils";
4689
4966
  import { ObjectId as ObjectId11 } from "mongodb";
4967
+
4968
+ // src/utils/checklist-pdf-html.util.ts
4690
4969
  function escapeHtml(text) {
4691
4970
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
4692
4971
  }
@@ -4699,7 +4978,7 @@ function buildChecklistHtml(areaChecklists) {
4699
4978
  const unitRows = (setItem.units || []).map(
4700
4979
  (unit) => `
4701
4980
  <tr>
4702
- <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>
4703
4982
  <td class="icon-cell">${unit.reject ? '<span class="reject-icon">&#10007;</span>' : ""}</td>
4704
4983
  <td class="icon-cell">${unit.approve ? '<span class="approve-icon">&#10003;</span>' : ""}</td>
4705
4984
  </tr>`
@@ -4836,6 +5115,12 @@ function buildChecklistHtml(areaChecklists) {
4836
5115
  font-size: 16px;
4837
5116
  font-weight: bold;
4838
5117
  }
5118
+ .missed-label {
5119
+ color: #b3261e;
5120
+ font-size: 11px;
5121
+ font-weight: bold;
5122
+ margin-left: 6px;
5123
+ }
4839
5124
  .reject-icon {
4840
5125
  color: #e74c3c;
4841
5126
  font-size: 16px;
@@ -4851,6 +5136,8 @@ function buildChecklistHtml(areaChecklists) {
4851
5136
  </body>
4852
5137
  </html>`;
4853
5138
  }
5139
+
5140
+ // src/services/hygiene-checklist-pdf.service.ts
4854
5141
  function useChecklistPdfService() {
4855
5142
  async function generateChecklistPdf(scheduleId) {
4856
5143
  const db = useAtlas8.getDb();
@@ -4920,9 +5207,9 @@ function useAreaChecklistController() {
4920
5207
  site: req.params.site,
4921
5208
  schedule: req.params.schedule
4922
5209
  };
4923
- const validation = Joi10.object({
4924
- site: Joi10.string().hex().required(),
4925
- schedule: Joi10.string().hex().required()
5210
+ const validation = Joi11.object({
5211
+ site: Joi11.string().hex().required(),
5212
+ schedule: Joi11.string().hex().required()
4926
5213
  });
4927
5214
  const { error, value } = validation.validate(payload);
4928
5215
  if (error) {
@@ -4942,14 +5229,14 @@ function useAreaChecklistController() {
4942
5229
  }
4943
5230
  async function getAllAreaChecklist(req, res, next) {
4944
5231
  const query = { ...req.query, ...req.params };
4945
- const validation = Joi10.object({
4946
- page: Joi10.number().min(1).optional().allow("", null),
4947
- limit: Joi10.number().min(1).optional().allow("", null),
4948
- search: Joi10.string().optional().allow("", null),
4949
- type: Joi10.string().valid(...allowedTypes, "all").optional().allow("", null),
4950
- status: Joi10.string().valid(...allowedStatus, "all").optional().allow("", null),
4951
- schedule: Joi10.string().hex().required(),
4952
- 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()
4953
5240
  });
4954
5241
  const { error } = validation.validate(query);
4955
5242
  if (error) {
@@ -4984,14 +5271,14 @@ function useAreaChecklistController() {
4984
5271
  }
4985
5272
  async function getAreaChecklistHistory(req, res, next) {
4986
5273
  const query = { ...req.query, ...req.params };
4987
- const validation = Joi10.object({
4988
- page: Joi10.number().min(1).optional().allow("", null),
4989
- limit: Joi10.number().min(1).optional().allow("", null),
4990
- search: Joi10.string().optional().allow("", null),
4991
- type: Joi10.string().optional().allow("", ...allowedTypes),
4992
- schedule: Joi10.string().hex().required(),
4993
- status: Joi10.string().allow("", null, ...allowedStatus),
4994
- 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)
4995
5282
  });
4996
5283
  const { error } = validation.validate(query);
4997
5284
  if (error) {
@@ -5025,7 +5312,7 @@ function useAreaChecklistController() {
5025
5312
  }
5026
5313
  }
5027
5314
  async function getAreaChecklistHistoryDetails(req, res, next) {
5028
- const validation = Joi10.string().hex().required();
5315
+ const validation = Joi11.string().hex().required();
5029
5316
  const _id = req.params.id;
5030
5317
  const { error, value } = validation.validate(_id);
5031
5318
  if (error) {
@@ -5045,12 +5332,12 @@ function useAreaChecklistController() {
5045
5332
  }
5046
5333
  async function getAreaChecklistUnits(req, res, next) {
5047
5334
  const query = { ...req.query, ...req.params };
5048
- const validation = Joi10.object({
5049
- page: Joi10.number().min(1).optional().allow("", null),
5050
- limit: Joi10.number().min(1).optional().allow("", null),
5051
- search: Joi10.string().optional().allow("", null),
5052
- serviceType: Joi10.string().valid(...Object.values(AppServiceType10)).required(),
5053
- 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()
5054
5341
  });
5055
5342
  const { error } = validation.validate(query);
5056
5343
  if (error) {
@@ -5100,21 +5387,21 @@ function useAreaChecklistController() {
5100
5387
  ...decisionValues,
5101
5388
  completedBy
5102
5389
  };
5103
- const validation = Joi10.object({
5104
- id: Joi10.string().hex().required(),
5105
- set: Joi10.number().integer().min(1).required(),
5106
- unit: Joi10.string().hex().required(),
5107
- decision: Joi10.string().valid("approve", "reject").required(),
5108
- approve: Joi10.boolean().optional(),
5109
- reject: Joi10.boolean().optional(),
5110
- 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),
5111
5398
  /*
5112
5399
  * Shape only. WHETHER a photo is required depends on the set's current
5113
5400
  * state in the database, which Joi cannot see — see the check below.
5114
5401
  * Each entry must still be a real id: a blank string carries no evidence.
5115
5402
  */
5116
- attachment: Joi10.array().items(Joi10.string().trim().min(1)).optional(),
5117
- completedBy: Joi10.string().hex().required()
5403
+ attachment: Joi11.array().items(Joi11.string().trim().min(1)).optional(),
5404
+ completedBy: Joi11.string().hex().required()
5118
5405
  });
5119
5406
  const { error } = validation.validate(payload);
5120
5407
  if (error) {
@@ -5151,7 +5438,7 @@ function useAreaChecklistController() {
5151
5438
  }
5152
5439
  }
5153
5440
  async function downloadChecklistPdf(req, res, next) {
5154
- const validation = Joi10.string().hex().required();
5441
+ const validation = Joi11.string().hex().required();
5155
5442
  const schedule = req.params.schedule;
5156
5443
  const { error, value } = validation.validate(schedule);
5157
5444
  if (error) {
@@ -5197,16 +5484,16 @@ function useAreaChecklistController() {
5197
5484
  }
5198
5485
 
5199
5486
  // src/models/hygiene-supply.model.ts
5200
- import Joi11 from "joi";
5487
+ import Joi12 from "joi";
5201
5488
  import { ObjectId as ObjectId12 } from "mongodb";
5202
5489
  import { BadRequestError as BadRequestError17, logger as logger21 } from "@7365admin1/node-server-utils";
5203
5490
  import { AppServiceType as AppServiceType11 } from "@7365admin1/core";
5204
- var supplySchema = Joi11.object({
5205
- site: Joi11.string().hex().required(),
5206
- serviceType: Joi11.string().valid(...Object.values(AppServiceType11)).required(),
5207
- name: Joi11.string().required(),
5208
- unitOfMeasurement: Joi11.string().required(),
5209
- 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()
5210
5497
  });
5211
5498
  function MSupply(value) {
5212
5499
  const { error } = supplySchema.validate(value);
@@ -5505,7 +5792,7 @@ function useSupplyRepository() {
5505
5792
  }
5506
5793
 
5507
5794
  // src/controllers/hygiene-supply.controller.ts
5508
- import Joi12 from "joi";
5795
+ import Joi13 from "joi";
5509
5796
  import { BadRequestError as BadRequestError19, logger as logger23 } from "@7365admin1/node-server-utils";
5510
5797
  import { AppServiceType as AppServiceType12 } from "@7365admin1/core";
5511
5798
  function useSupplyController() {
@@ -5536,12 +5823,12 @@ function useSupplyController() {
5536
5823
  }
5537
5824
  async function getSupplies(req, res, next) {
5538
5825
  const query = { ...req.query, ...req.params };
5539
- const validation = Joi12.object({
5540
- page: Joi12.number().min(1).optional().allow("", null),
5541
- limit: Joi12.number().min(1).optional().allow("", null),
5542
- search: Joi12.string().optional().allow("", null),
5543
- site: Joi12.string().hex().required(),
5544
- 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()
5545
5832
  });
5546
5833
  const { error } = validation.validate(query);
5547
5834
  if (error) {
@@ -5571,7 +5858,7 @@ function useSupplyController() {
5571
5858
  }
5572
5859
  }
5573
5860
  async function getSupplyById(req, res, next) {
5574
- const validation = Joi12.string().hex().required();
5861
+ const validation = Joi13.string().hex().required();
5575
5862
  const _id = req.params.id;
5576
5863
  const { error, value } = validation.validate(_id);
5577
5864
  if (error) {
@@ -5591,12 +5878,12 @@ function useSupplyController() {
5591
5878
  }
5592
5879
  async function updateSupply(req, res, next) {
5593
5880
  const payload = { id: req.params.id, ...req.body };
5594
- const validation = Joi12.object({
5595
- id: Joi12.string().hex().required(),
5596
- name: Joi12.string().optional().allow("", null),
5597
- unitOfMeasurement: Joi12.string().optional().allow("", null),
5598
- qty: Joi12.number().min(0).optional().allow("", null),
5599
- 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)
5600
5887
  });
5601
5888
  const { error } = validation.validate(payload);
5602
5889
  if (error) {
@@ -5617,7 +5904,7 @@ function useSupplyController() {
5617
5904
  }
5618
5905
  async function deleteSupply(req, res, next) {
5619
5906
  const id = req.params.id;
5620
- const validation = Joi12.string().hex().required();
5907
+ const validation = Joi13.string().hex().required();
5621
5908
  const { error, value } = validation.validate(id);
5622
5909
  if (error) {
5623
5910
  logger23.log({ level: "error", message: error.message });
@@ -5644,18 +5931,18 @@ function useSupplyController() {
5644
5931
  }
5645
5932
 
5646
5933
  // src/models/hygiene-stock.model.ts
5647
- import Joi13 from "joi";
5934
+ import Joi14 from "joi";
5648
5935
  import { ObjectId as ObjectId14 } from "mongodb";
5649
5936
  import { BadRequestError as BadRequestError20, logger as logger24 } from "@7365admin1/node-server-utils";
5650
5937
  import { AppServiceType as AppServiceType13 } from "@7365admin1/core";
5651
- var stockSchema = Joi13.object({
5652
- site: Joi13.string().hex().required(),
5653
- serviceType: Joi13.string().valid(...Object.values(AppServiceType13)).required(),
5654
- supply: Joi13.string().hex().required(),
5655
- in: Joi13.number().min(0).optional(),
5656
- out: Joi13.number().min(0).optional(),
5657
- balance: Joi13.number().min(0).required(),
5658
- 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)
5659
5946
  });
5660
5947
  function MStock(value) {
5661
5948
  const { error } = stockSchema.validate(value);
@@ -5882,7 +6169,7 @@ function useStockService() {
5882
6169
  }
5883
6170
 
5884
6171
  // src/controllers/hygiene-stock.controller.ts
5885
- import Joi14 from "joi";
6172
+ import Joi15 from "joi";
5886
6173
  import { BadRequestError as BadRequestError23, logger as logger26 } from "@7365admin1/node-server-utils";
5887
6174
  import { AppServiceType as AppServiceType14 } from "@7365admin1/core";
5888
6175
  function useStockController() {
@@ -5890,12 +6177,12 @@ function useStockController() {
5890
6177
  const { createStock: _createStock } = useStockService();
5891
6178
  async function createStock(req, res, next) {
5892
6179
  const payload = { ...req.body, ...req.params };
5893
- const validation = Joi14.object({
5894
- site: Joi14.string().hex().required(),
5895
- serviceType: Joi14.string().valid(...Object.values(AppServiceType14)).required(),
5896
- supply: Joi14.string().hex().required(),
5897
- qty: Joi14.number().min(0).required(),
5898
- 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)
5899
6186
  });
5900
6187
  const { error } = validation.validate(payload);
5901
6188
  if (error) {
@@ -5915,13 +6202,13 @@ function useStockController() {
5915
6202
  }
5916
6203
  async function getStocksBySupplyId(req, res, next) {
5917
6204
  const query = { ...req.query, ...req.params };
5918
- const validation = Joi14.object({
5919
- page: Joi14.number().min(1).optional().allow("", null),
5920
- limit: Joi14.number().min(1).optional().allow("", null),
5921
- search: Joi14.string().optional().allow("", null),
5922
- site: Joi14.string().hex().required(),
5923
- serviceType: Joi14.string().valid(...Object.values(AppServiceType14)).required(),
5924
- 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()
5925
6212
  });
5926
6213
  const { error } = validation.validate(query);
5927
6214
  if (error) {
@@ -5983,21 +6270,21 @@ async function applyCheckOutDecision(decision, decide, returnStock) {
5983
6270
  }
5984
6271
 
5985
6272
  // src/models/hygiene-checkout-item.model.ts
5986
- import Joi15 from "joi";
6273
+ import Joi16 from "joi";
5987
6274
  import { ObjectId as ObjectId16 } from "mongodb";
5988
6275
  import { BadRequestError as BadRequestError24, logger as logger27 } from "@7365admin1/node-server-utils";
5989
6276
  import { AppServiceType as AppServiceType15 } from "@7365admin1/core";
5990
6277
  var allowedCheckOutItemStatus = ["pending", "completed"];
5991
- var checkOutItemSchema = Joi15.object({
5992
- site: Joi15.string().hex().required(),
5993
- serviceType: Joi15.string().valid(...Object.values(AppServiceType15)).required(),
5994
- supply: Joi15.string().hex().required(),
5995
- supplyName: Joi15.string().required(),
5996
- supplyQty: Joi15.number().min(0).required(),
5997
- qty: Joi15.number().min(0).required(),
5998
- attachment: Joi15.array().items(Joi15.string()).optional().allow(null),
5999
- createdBy: Joi15.string().hex().required(),
6000
- 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()
6001
6288
  });
6002
6289
  function MCheckOutItem(value) {
6003
6290
  const { error } = checkOutItemSchema.validate(value);
@@ -6486,7 +6773,7 @@ function useCheckOutItemService() {
6486
6773
  }
6487
6774
 
6488
6775
  // src/controllers/hygiene-checkout-item.controller.ts
6489
- import Joi16 from "joi";
6776
+ import Joi17 from "joi";
6490
6777
  import { BadRequestError as BadRequestError27, logger as logger29 } from "@7365admin1/node-server-utils";
6491
6778
  import { AppServiceType as AppServiceType16 } from "@7365admin1/core";
6492
6779
  function useCheckOutItemController() {
@@ -6510,13 +6797,13 @@ function useCheckOutItemController() {
6510
6797
  ...req.params,
6511
6798
  createdBy
6512
6799
  };
6513
- const validation = Joi16.object({
6514
- site: Joi16.string().hex().required(),
6515
- serviceType: Joi16.string().valid(...Object.values(AppServiceType16)).required(),
6516
- supply: Joi16.string().hex().required(),
6517
- qty: Joi16.number().min(0).required(),
6518
- attachment: Joi16.array().items(Joi16.string()).optional().allow(null),
6519
- 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()
6520
6807
  });
6521
6808
  const { error } = validation.validate(payload);
6522
6809
  if (error) {
@@ -6545,15 +6832,15 @@ function useCheckOutItemController() {
6545
6832
  ...req.params,
6546
6833
  createdBy
6547
6834
  };
6548
- const validation = Joi16.object({
6549
- site: Joi16.string().hex().required(),
6550
- serviceType: Joi16.string().valid(...Object.values(AppServiceType16)).required(),
6551
- createdBy: Joi16.string().hex().required(),
6552
- items: Joi16.array().items(
6553
- Joi16.object({
6554
- supply: Joi16.string().hex().required(),
6555
- qty: Joi16.number().min(0).required(),
6556
- 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)
6557
6844
  })
6558
6845
  ).min(1).required()
6559
6846
  });
@@ -6575,12 +6862,12 @@ function useCheckOutItemController() {
6575
6862
  }
6576
6863
  async function getCheckOutItems(req, res, next) {
6577
6864
  const query = { ...req.query, ...req.params };
6578
- const validation = Joi16.object({
6579
- page: Joi16.number().min(1).optional().allow("", null),
6580
- limit: Joi16.number().min(1).optional().allow("", null),
6581
- search: Joi16.string().optional().allow("", null),
6582
- site: Joi16.string().hex().required(),
6583
- 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()
6584
6871
  });
6585
6872
  const { error } = validation.validate(query);
6586
6873
  if (error) {
@@ -6610,7 +6897,7 @@ function useCheckOutItemController() {
6610
6897
  }
6611
6898
  }
6612
6899
  async function getCheckOutItemById(req, res, next) {
6613
- const validation = Joi16.string().hex().required();
6900
+ const validation = Joi17.string().hex().required();
6614
6901
  const _id = req.params.id;
6615
6902
  const { error, value } = validation.validate(_id);
6616
6903
  if (error) {
@@ -6649,13 +6936,13 @@ function useCheckOutItemController() {
6649
6936
  ...decisionValues,
6650
6937
  completedBy
6651
6938
  };
6652
- const validation = Joi16.object({
6653
- id: Joi16.string().hex().required(),
6654
- decision: Joi16.string().valid("approve", "disapprove").required(),
6655
- approve: Joi16.boolean().required(),
6656
- reject: Joi16.boolean().required(),
6657
- remarks: Joi16.string().optional().allow("", null),
6658
- 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()
6659
6946
  });
6660
6947
  const { error } = validation.validate(payload);
6661
6948
  if (error) {
@@ -6688,23 +6975,15 @@ function useCheckOutItemController() {
6688
6975
  // src/models/hygiene-schedule-task.model.ts
6689
6976
  import { BadRequestError as BadRequestError28, logger as logger30 } from "@7365admin1/node-server-utils";
6690
6977
  import { AppServiceType as AppServiceType17 } from "@7365admin1/core";
6691
- import Joi17 from "joi";
6978
+ import Joi18 from "joi";
6692
6979
  import { ObjectId as ObjectId18 } from "mongodb";
6693
- var scheduleTaskSchema = Joi17.object({
6694
- site: Joi17.string().hex().required(),
6695
- serviceType: Joi17.string().valid(...Object.values(AppServiceType17)).required(),
6696
- title: Joi17.string().required(),
6697
- time: Joi17.string().pattern(/^([0-1]\d|2[0-3]):([0-5]\d)$/).required(),
6698
- dates: Joi17.array().min(1).items(
6699
- Joi17.string().pattern(/^\d{4}-\d{2}-\d{2}$/).required()
6700
- ).required(),
6701
- description: Joi17.string().optional().allow("", null),
6702
- areas: Joi17.array().min(1).items(
6703
- Joi17.object({
6704
- name: Joi17.string().required(),
6705
- value: Joi17.any().required()
6706
- })
6707
- ).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;
6708
6987
  });
6709
6988
  function MScheduleTask(value) {
6710
6989
  const { error } = scheduleTaskSchema.validate(value);
@@ -6731,14 +7010,26 @@ function MScheduleTask(value) {
6731
7010
  }
6732
7011
  });
6733
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];
6734
7024
  return {
6735
7025
  site: value.site,
6736
7026
  serviceType: value.serviceType,
6737
7027
  title: value.title,
6738
7028
  time: value.time,
6739
- dates: value.dates,
7029
+ dates: value.dates ?? [],
6740
7030
  description: value.description,
6741
7031
  areas: value.areas,
7032
+ ...extra,
6742
7033
  status: "active",
6743
7034
  createdAt: /* @__PURE__ */ new Date(),
6744
7035
  updatedAt: "",
@@ -6758,11 +7049,23 @@ import {
6758
7049
  makeCacheKey as makeCacheKey9,
6759
7050
  NotFoundError as NotFoundError7
6760
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
+ };
6761
7063
  function useScheduleTaskRepository() {
6762
7064
  const db = useAtlas14.getDb();
6763
7065
  if (!db) {
6764
7066
  throw new InternalServerError11("Unable to connect to server.");
6765
7067
  }
7068
+ const database = db;
6766
7069
  const namespace_collection = "site.schedule-tasks";
6767
7070
  const collection = db.collection(namespace_collection);
6768
7071
  const { delNamespace, setCache, getCache } = useCache9(namespace_collection);
@@ -6837,7 +7140,7 @@ function useScheduleTaskRepository() {
6837
7140
  const cachedData = await getCache(cacheKey);
6838
7141
  if (cachedData) {
6839
7142
  logger31.info(`Cache hit for key: ${cacheKey}`);
6840
- return cachedData;
7143
+ return enrichList(cachedData, site, serviceType);
6841
7144
  }
6842
7145
  try {
6843
7146
  const items = await collection.aggregate([
@@ -6846,7 +7149,8 @@ function useScheduleTaskRepository() {
6846
7149
  $project: {
6847
7150
  title: 1,
6848
7151
  areas: 1,
6849
- status: 1
7152
+ status: 1,
7153
+ ...SCHEDULE_FIELDS
6850
7154
  }
6851
7155
  },
6852
7156
  { $sort: { _id: -1 } },
@@ -6860,15 +7164,104 @@ function useScheduleTaskRepository() {
6860
7164
  }).catch((err) => {
6861
7165
  logger31.error(`Failed to set cache for key: ${cacheKey}`, err);
6862
7166
  });
6863
- return data;
7167
+ return enrichList(data, site, serviceType);
6864
7168
  } catch (error) {
6865
7169
  throw error;
6866
7170
  }
6867
7171
  }
6868
- 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) {
6869
7254
  const query = {
6870
7255
  status: { $ne: "deleted" }
6871
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
+ }
6872
7265
  try {
6873
7266
  const items = await collection.aggregate([{ $match: query }, { $sort: { _id: -1 } }]).toArray();
6874
7267
  return items;
@@ -6876,6 +7269,9 @@ function useScheduleTaskRepository() {
6876
7269
  throw error;
6877
7270
  }
6878
7271
  }
7272
+ function withScheduleDisplay(row) {
7273
+ return { ...row, ...scheduleDisplay(row, /* @__PURE__ */ new Date()) };
7274
+ }
6879
7275
  async function getScheduleTaskById(_id, session) {
6880
7276
  try {
6881
7277
  _id = new ObjectId19(_id);
@@ -6893,7 +7289,7 @@ function useScheduleTaskRepository() {
6893
7289
  const cachedData = await getCache(cacheKey);
6894
7290
  if (cachedData) {
6895
7291
  logger31.info(`Cache hit for key: ${cacheKey}`);
6896
- return cachedData;
7292
+ return withScheduleDisplay(cachedData);
6897
7293
  }
6898
7294
  } else {
6899
7295
  logger31.info(`Skipping cache during transaction for key: ${cacheKey}`);
@@ -6904,12 +7300,13 @@ function useScheduleTaskRepository() {
6904
7300
  {
6905
7301
  $project: {
6906
7302
  title: 1,
6907
- time: 1,
6908
- dates: 1,
6909
7303
  description: 1,
6910
7304
  areas: 1,
6911
7305
  status: 1,
6912
- createdAt: 1
7306
+ createdAt: 1,
7307
+ site: 1,
7308
+ serviceType: 1,
7309
+ ...SCHEDULE_FIELDS
6913
7310
  }
6914
7311
  }
6915
7312
  ]).toArray();
@@ -6921,7 +7318,7 @@ function useScheduleTaskRepository() {
6921
7318
  }).catch((err) => {
6922
7319
  logger31.error(`Failed to set cache for key: ${cacheKey}`, err);
6923
7320
  });
6924
- return data[0];
7321
+ return withScheduleDisplay(data[0]);
6925
7322
  } catch (error) {
6926
7323
  throw error;
6927
7324
  }
@@ -6945,16 +7342,20 @@ function useScheduleTaskRepository() {
6945
7342
  });
6946
7343
  }
6947
7344
  try {
6948
- 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
+ }
6949
7352
  const res = await collection.updateOne(
6950
- { _id },
6951
- { $set: updateValue },
7353
+ { _id, status: { $ne: "deleted" } },
7354
+ update,
6952
7355
  { session }
6953
7356
  );
6954
- if (res.modifiedCount === 0) {
6955
- throw new InternalServerError11(
6956
- "Unable to update hygiene schedule task."
6957
- );
7357
+ if (res.matchedCount === 0) {
7358
+ throw new NotFoundError7("Schedule task not found.");
6958
7359
  }
6959
7360
  delNamespace().then(() => {
6960
7361
  logger31.info(`Cache cleared for namespace: ${namespace_collection}`);
@@ -6969,6 +7370,18 @@ function useScheduleTaskRepository() {
6969
7370
  throw error;
6970
7371
  }
6971
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
+ }
6972
7385
  async function deleteScheduleTask(_id, session) {
6973
7386
  try {
6974
7387
  _id = new ObjectId19(_id);
@@ -7008,18 +7421,31 @@ function useScheduleTaskRepository() {
7008
7421
  createScheduleTask,
7009
7422
  getScheduleTasks,
7010
7423
  getAllScheduleTask,
7424
+ markScheduleTaskRan,
7011
7425
  getScheduleTaskById,
7012
7426
  updateScheduleTask,
7013
7427
  deleteScheduleTask
7014
7428
  };
7015
7429
  }
7016
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
+
7017
7443
  // src/services/hygiene-schedule-task.service.ts
7018
7444
  import { ObjectId as ObjectId20 } from "mongodb";
7019
7445
  import { logger as logger32 } from "@7365admin1/node-server-utils";
7020
7446
  function useScheduleTaskService() {
7021
7447
  const { createParentChecklist } = useParentChecklistRepo();
7022
- const { getAllScheduleTask } = useScheduleTaskRepository();
7448
+ const { getAllScheduleTask, markScheduleTaskRan } = useScheduleTaskRepository();
7023
7449
  const {
7024
7450
  createAreaChecklist,
7025
7451
  getAreaChecklistByAreaAndSchedule,
@@ -7028,73 +7454,7 @@ function useScheduleTaskService() {
7028
7454
  const { getAreaById } = useAreaRepo();
7029
7455
  function checkScheduleConditions(schedule, serviceType, currentDate = /* @__PURE__ */ new Date()) {
7030
7456
  try {
7031
- const now = currentDate;
7032
- const timeString = now.toLocaleTimeString("en-US", {
7033
- hour: "2-digit",
7034
- minute: "2-digit",
7035
- hour12: false,
7036
- timeZone: "Asia/Singapore"
7037
- });
7038
- const [currentHour, currentMinute] = timeString.split(":").map(Number);
7039
- logger32.info(
7040
- `Checking schedule ${schedule._id}: Current time ${currentHour}:${currentMinute}, Schedule time ${schedule.time}, Dates ${JSON.stringify(schedule.dates)}`
7041
- );
7042
- const currentDateFormatted = now.toLocaleDateString("en-CA", {
7043
- timeZone: "Asia/Singapore"
7044
- });
7045
- if (!schedule.dates || !Array.isArray(schedule.dates) || schedule.dates.length === 0) {
7046
- logger32.info(`Schedule ${schedule._id}: No dates configured, skipping`);
7047
- return false;
7048
- }
7049
- if (!schedule.dates.includes(currentDateFormatted)) {
7050
- logger32.info(
7051
- `Schedule ${schedule._id}: Current date ${currentDateFormatted} is not in scheduled dates [${schedule.dates.join(
7052
- ", "
7053
- )}]`
7054
- );
7055
- return false;
7056
- }
7057
- const [scheduleHour, scheduleMinute] = schedule.time.split(":").map(Number);
7058
- const timeMatches = currentHour === scheduleHour && currentMinute === scheduleMinute;
7059
- if (!timeMatches) {
7060
- logger32.info(
7061
- `Schedule ${schedule._id}: Time does not match. Current: ${currentHour}:${currentMinute}, Expected: ${scheduleHour}:${scheduleMinute}`
7062
- );
7063
- return false;
7064
- }
7065
- const relevantTimestamp = schedule.updatedAt || schedule.createdAt;
7066
- if (relevantTimestamp) {
7067
- const savedAt = new Date(relevantTimestamp);
7068
- const savedHour = parseInt(
7069
- savedAt.toLocaleTimeString("en-US", {
7070
- hour: "2-digit",
7071
- hour12: false,
7072
- timeZone: "Asia/Singapore"
7073
- }),
7074
- 10
7075
- );
7076
- const savedMinute = savedAt.toLocaleString("en-US", {
7077
- minute: "2-digit",
7078
- timeZone: "Asia/Singapore"
7079
- });
7080
- const savedDateFormatted = savedAt.toLocaleDateString("en-CA", {
7081
- timeZone: "Asia/Singapore"
7082
- });
7083
- const savedMinNum = parseInt(savedMinute, 10);
7084
- const wasSavedThisMinute = savedDateFormatted === currentDateFormatted && savedHour === currentHour && savedMinNum === currentMinute;
7085
- if (wasSavedThisMinute) {
7086
- logger32.info(
7087
- `Schedule ${schedule._id}: Skipping \u2014 task was saved this minute (${savedHour}:${String(
7088
- savedMinNum
7089
- ).padStart(2, "0")}). Will fire on next occurrence.`
7090
- );
7091
- return false;
7092
- }
7093
- }
7094
- logger32.info(
7095
- `Schedule ${schedule._id}: All conditions matched - Date is within range and time matches`
7096
- );
7097
- return true;
7457
+ return shouldRunNow(schedule, currentDate);
7098
7458
  } catch (error) {
7099
7459
  logger32.error(
7100
7460
  `Error checking schedule conditions for ${schedule._id}:`,
@@ -7103,18 +7463,22 @@ function useScheduleTaskService() {
7103
7463
  return false;
7104
7464
  }
7105
7465
  }
7466
+ const MAX_ATTEMPTS = 5;
7467
+ const attempts = /* @__PURE__ */ new Map();
7106
7468
  async function processScheduledTasks(currentDate) {
7107
7469
  try {
7108
7470
  logger32.info("Starting scheduled task processing...");
7109
- const scheduleTasks = await getAllScheduleTask();
7471
+ const today = sgtParts(currentDate ?? /* @__PURE__ */ new Date()).date;
7472
+ const scheduleTasks = await getAllScheduleTask(today);
7110
7473
  if (!scheduleTasks || scheduleTasks.length === 0) {
7111
7474
  logger32.info("No schedule tasks found to process");
7112
- return { processed: 0, validated: 0 };
7475
+ return { processed: 0, validated: 0, fired: [] };
7113
7476
  }
7114
7477
  logger32.info(`Found ${scheduleTasks.length} schedule tasks to check`);
7115
7478
  let processedCount = 0;
7116
7479
  let validatedCount = 0;
7117
7480
  const validatedTasks = [];
7481
+ const fired = [];
7118
7482
  for (const scheduleTask of scheduleTasks) {
7119
7483
  try {
7120
7484
  const serviceType = scheduleTask.serviceType;
@@ -7157,6 +7521,8 @@ function useScheduleTaskService() {
7157
7521
  logger32.info(
7158
7522
  `Using parent checklist ${parentChecklistId}, now creating/updating area checklists`
7159
7523
  );
7524
+ let failedAreas = 0;
7525
+ const addedAreas = [];
7160
7526
  for (const area of scheduleTask.areas) {
7161
7527
  try {
7162
7528
  const areaId = area.value.toString();
@@ -7219,15 +7585,7 @@ function useScheduleTaskService() {
7219
7585
  )
7220
7586
  ) : 0;
7221
7587
  const startSetNumber = maxSetInChecklist + 1;
7222
- const newSets = Array.from(
7223
- { length: areaSetConfig },
7224
- (_, index) => ({
7225
- set: startSetNumber + index,
7226
- units,
7227
- isScheduleTask: true,
7228
- scheduleTaskId: scheduleTask._id
7229
- })
7230
- );
7588
+ const newSets = buildScheduleTaskSets(scheduleTask, units, startSetNumber, areaSetConfig);
7231
7589
  const modified = await pushScheduleTaskSets(
7232
7590
  parentChecklistId.toString(),
7233
7591
  serviceType,
@@ -7236,6 +7594,7 @@ function useScheduleTaskService() {
7236
7594
  newSets
7237
7595
  );
7238
7596
  if (modified > 0) {
7597
+ addedAreas.push(area.name);
7239
7598
  logger32.info(
7240
7599
  `Appended ${areaSetConfig} set(s) starting from ${startSetNumber} to area checklist for area ${area.name}`
7241
7600
  );
@@ -7246,16 +7605,7 @@ function useScheduleTaskService() {
7246
7605
  }
7247
7606
  } else {
7248
7607
  const areaSetConfig = Number(areaDetails.set) || 1;
7249
- const schedTaskId = scheduleTask._id;
7250
- const schedTaskSets = Array.from(
7251
- { length: areaSetConfig },
7252
- (_, index) => ({
7253
- set: index + 1,
7254
- units,
7255
- isScheduleTask: true,
7256
- scheduleTaskId: schedTaskId
7257
- })
7258
- );
7608
+ const schedTaskSets = buildScheduleTaskSets(scheduleTask, units, 1, areaSetConfig);
7259
7609
  const checklistData = {
7260
7610
  schedule: parentChecklistId.toString(),
7261
7611
  serviceType,
@@ -7268,13 +7618,32 @@ function useScheduleTaskService() {
7268
7618
  `Area ${area.name} (${areaId}): Creating new area checklist with schedule-task sets only`
7269
7619
  );
7270
7620
  await createAreaChecklist(checklistData);
7621
+ addedAreas.push(area.name);
7271
7622
  logger32.info(`Created new area checklist for area ${area.name}`);
7272
7623
  }
7273
7624
  } catch (error) {
7274
7625
  logger32.error(`Error processing area ${area.name}:`, error);
7626
+ failedAreas++;
7275
7627
  continue;
7276
7628
  }
7277
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
+ }
7278
7647
  processedCount++;
7279
7648
  validatedCount++;
7280
7649
  validatedTasks.push(scheduleTask);
@@ -7286,6 +7655,17 @@ function useScheduleTaskService() {
7286
7655
  `Error processing schedule task ${scheduleTask._id}:`,
7287
7656
  error
7288
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
+ }
7289
7669
  continue;
7290
7670
  }
7291
7671
  }
@@ -7295,7 +7675,8 @@ function useScheduleTaskService() {
7295
7675
  return {
7296
7676
  processed: processedCount,
7297
7677
  validated: validatedCount,
7298
- tasks: validatedTasks
7678
+ tasks: validatedTasks,
7679
+ fired
7299
7680
  };
7300
7681
  } catch (error) {
7301
7682
  logger32.error("Error processing scheduled tasks:", error);
@@ -7306,7 +7687,7 @@ function useScheduleTaskService() {
7306
7687
  }
7307
7688
 
7308
7689
  // src/controllers/hygiene-schedule-task.controller.ts
7309
- import Joi18 from "joi";
7690
+ import Joi19 from "joi";
7310
7691
  import { BadRequestError as BadRequestError30, logger as logger33 } from "@7365admin1/node-server-utils";
7311
7692
  import { AppServiceType as AppServiceType18 } from "@7365admin1/core";
7312
7693
  function useScheduleTaskController() {
@@ -7337,12 +7718,12 @@ function useScheduleTaskController() {
7337
7718
  }
7338
7719
  async function getScheduleTasks(req, res, next) {
7339
7720
  const query = { ...req.query, ...req.params };
7340
- const validation = Joi18.object({
7341
- page: Joi18.number().min(1).optional().allow("", null),
7342
- limit: Joi18.number().min(1).optional().allow("", null),
7343
- search: Joi18.string().optional().allow("", null),
7344
- site: Joi18.string().hex().required(),
7345
- 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()
7346
7727
  });
7347
7728
  const { error } = validation.validate(query);
7348
7729
  if (error) {
@@ -7350,8 +7731,8 @@ function useScheduleTaskController() {
7350
7731
  next(new BadRequestError30(error.message));
7351
7732
  return;
7352
7733
  }
7353
- const page = parseInt(req.query.page) ?? 1;
7354
- const limit = parseInt(req.query.limit) ?? 10;
7734
+ const page = parseInt(req.query.page) || 1;
7735
+ const limit = parseInt(req.query.limit) || 10;
7355
7736
  const search = req.query.search ?? "";
7356
7737
  const site = req.params.site ?? "";
7357
7738
  const serviceType = req.query.serviceType ?? "";
@@ -7372,7 +7753,7 @@ function useScheduleTaskController() {
7372
7753
  }
7373
7754
  }
7374
7755
  async function getScheduleTaskById(req, res, next) {
7375
- const validation = Joi18.string().hex().required();
7756
+ const validation = Joi19.string().hex().required();
7376
7757
  const _id = req.params.id;
7377
7758
  const { error, value } = validation.validate(_id);
7378
7759
  if (error) {
@@ -7392,20 +7773,17 @@ function useScheduleTaskController() {
7392
7773
  }
7393
7774
  async function updateScheduleTask(req, res, next) {
7394
7775
  const payload = { id: req.params.id, ...req.body };
7395
- const validation = Joi18.object({
7396
- id: Joi18.string().hex().required(),
7397
- title: Joi18.string().optional().allow("", null),
7398
- time: Joi18.string().pattern(/^([0-1]\d|2[0-3]):([0-5]\d)$/).optional().allow("", null),
7399
- dates: Joi18.array().min(1).items(
7400
- Joi18.string().pattern(/^\d{4}-\d{2}-\d{2}$/).required()
7401
- ).optional(),
7402
- description: Joi18.string().optional().allow("", null),
7403
- areas: Joi18.array().min(1).items(
7404
- Joi18.object({
7405
- name: Joi18.string().required(),
7406
- value: Joi18.any().required()
7407
- })
7408
- ).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
7409
7787
  });
7410
7788
  const { error } = validation.validate(payload);
7411
7789
  if (error) {
@@ -7413,6 +7791,24 @@ function useScheduleTaskController() {
7413
7791
  next(new BadRequestError30(error.message));
7414
7792
  return;
7415
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
+ }
7416
7812
  try {
7417
7813
  const { id, ...value } = payload;
7418
7814
  await _updateScheduleTask(id, value);
@@ -7425,7 +7821,7 @@ function useScheduleTaskController() {
7425
7821
  }
7426
7822
  }
7427
7823
  async function deleteScheduleTask(req, res, next) {
7428
- const validation = Joi18.string().hex().required();
7824
+ const validation = Joi19.string().hex().required();
7429
7825
  const _id = req.params.id;
7430
7826
  const { error, value } = validation.validate(_id);
7431
7827
  if (error) {
@@ -7669,17 +8065,17 @@ function useQRService() {
7669
8065
  }
7670
8066
 
7671
8067
  // src/controllers/hygiene-qr.controller.ts
7672
- import Joi19 from "joi";
8068
+ import Joi20 from "joi";
7673
8069
  import { BadRequestError as BadRequestError31, logger as logger35 } from "@7365admin1/node-server-utils";
7674
8070
  function useQRController() {
7675
8071
  const { generateQRImage: _generateQRImage, generateQRPDF: _generateQRPDF } = useQRService();
7676
8072
  async function generateQR(req, res, next) {
7677
- const validation = Joi19.object({
7678
- url: Joi19.string().uri().required(),
7679
- filename: Joi19.string().optional().allow("", null),
7680
- title: Joi19.string().optional().allow("", null),
7681
- subtitle: Joi19.string().optional().allow("", null),
7682
- 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)
7683
8079
  });
7684
8080
  const query = { ...req.query };
7685
8081
  const { error, value } = validation.validate(query);