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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -4436,6 +4436,265 @@ function useAreaChecklistRepo() {
4436
4436
  };
4437
4437
  }
4438
4438
 
4439
+ // src/utils/schedule-task-rules.util.ts
4440
+ import Joi10 from "joi";
4441
+ var REPEATS = ["none", "daily", "weekdays", "weekly", "monthly"];
4442
+ var SGT_MS = 8 * 60 * 60 * 1e3;
4443
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4444
+ var TIME_RE = /^([0-1]\d|2[0-3]):([0-5]\d)$/;
4445
+ var pad = (n) => String(n).padStart(2, "0");
4446
+ function sgtParts(now) {
4447
+ const s = new Date(now.getTime() + SGT_MS);
4448
+ const date = `${s.getUTCFullYear()}-${pad(s.getUTCMonth() + 1)}-${pad(s.getUTCDate())}`;
4449
+ return { date, minutes: s.getUTCHours() * 60 + s.getUTCMinutes(), weekday: s.getUTCDay() };
4450
+ }
4451
+ function isRealDate(value) {
4452
+ if (typeof value !== "string" || !DATE_RE.test(value))
4453
+ return false;
4454
+ const [y, m, d] = value.split("-").map(Number);
4455
+ const t = new Date(Date.UTC(y, m - 1, d));
4456
+ return t.getUTCFullYear() === y && t.getUTCMonth() === m - 1 && t.getUTCDate() === d;
4457
+ }
4458
+ var toMinutes = (time) => {
4459
+ const [h, m] = time.split(":").map(Number);
4460
+ return h * 60 + m;
4461
+ };
4462
+ var weekdayOf = (date) => {
4463
+ const [y, m, d] = date.split("-").map(Number);
4464
+ return new Date(Date.UTC(y, m - 1, d)).getUTCDay();
4465
+ };
4466
+ var daysInMonth = (date) => {
4467
+ const [y, m] = date.split("-").map(Number);
4468
+ return new Date(Date.UTC(y, m, 0)).getUTCDate();
4469
+ };
4470
+ var addDays = (date, n) => {
4471
+ const [y, m, d] = date.split("-").map(Number);
4472
+ const t = new Date(Date.UTC(y, m - 1, d + n));
4473
+ return `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}-${pad(t.getUTCDate())}`;
4474
+ };
4475
+ var isRepeating = (task) => Boolean(task.repeat) && task.repeat !== "none";
4476
+ function occursOn(task, date) {
4477
+ if (task.endDate && date > task.endDate)
4478
+ return false;
4479
+ if (!isRepeating(task))
4480
+ return Array.isArray(task.dates) && task.dates.includes(date);
4481
+ if (task.startDate && date < task.startDate)
4482
+ return false;
4483
+ const weekday = weekdayOf(date);
4484
+ switch (task.repeat) {
4485
+ case "daily":
4486
+ return true;
4487
+ case "weekdays":
4488
+ return weekday >= 1 && weekday <= 5;
4489
+ case "weekly":
4490
+ return Array.isArray(task.daysOfWeek) && task.daysOfWeek.includes(weekday);
4491
+ case "monthly": {
4492
+ const wanted = task.dayOfMonth ?? (task.startDate ? Number(task.startDate.slice(8, 10)) : 1);
4493
+ return Number(date.slice(8, 10)) === Math.min(wanted, daysInMonth(date));
4494
+ }
4495
+ default:
4496
+ return false;
4497
+ }
4498
+ }
4499
+ function savedTodayAt(task, today) {
4500
+ const stamp = task.updatedAt || task.createdAt;
4501
+ if (!stamp)
4502
+ return null;
4503
+ const saved = new Date(stamp);
4504
+ if (Number.isNaN(saved.getTime()))
4505
+ return null;
4506
+ const p = sgtParts(saved);
4507
+ return p.date === today ? p.minutes : null;
4508
+ }
4509
+ function shouldRunNow(task, now = /* @__PURE__ */ new Date()) {
4510
+ if (task.paused || !TIME_RE.test(task.time ?? ""))
4511
+ return false;
4512
+ const { date, minutes } = sgtParts(now);
4513
+ if (task.lastRunDate === date || !occursOn(task, date))
4514
+ return false;
4515
+ const due = toMinutes(task.time);
4516
+ if (minutes < due)
4517
+ return false;
4518
+ const saved = savedTodayAt(task, date);
4519
+ return !(saved !== null && saved >= due);
4520
+ }
4521
+ var iso = (date, time) => `${date}T${time}:00+08:00`;
4522
+ function upcoming(task, now) {
4523
+ if (typeof task.time !== "string" || !TIME_RE.test(task.time))
4524
+ return null;
4525
+ const { date: today, minutes } = sgtParts(now);
4526
+ const due = toMinutes(task.time);
4527
+ if (occursOn(task, today) && (minutes < due || shouldRunNow({ ...task, paused: false }, now))) {
4528
+ return iso(today, task.time);
4529
+ }
4530
+ if (!isRepeating(task)) {
4531
+ const later = (task.dates ?? []).filter((d) => d > today && (!task.endDate || d <= task.endDate)).sort();
4532
+ return later.length ? iso(later[0], task.time) : null;
4533
+ }
4534
+ const from = task.startDate && task.startDate > today ? task.startDate : today;
4535
+ for (let i = from === today ? 1 : 0; i <= 400; i++) {
4536
+ const d = addDays(from, i);
4537
+ if (task.endDate && d > task.endDate)
4538
+ return null;
4539
+ if (occursOn(task, d))
4540
+ return iso(d, task.time);
4541
+ }
4542
+ return null;
4543
+ }
4544
+ function nextRun(task, now = /* @__PURE__ */ new Date()) {
4545
+ return task.paused ? null : upcoming(task, now);
4546
+ }
4547
+ function scheduleState(task, now = /* @__PURE__ */ new Date()) {
4548
+ if (task.paused)
4549
+ return "paused";
4550
+ return upcoming(task, now) ? "active" : "ended";
4551
+ }
4552
+ function lastRun(task) {
4553
+ return task.lastRunDate && TIME_RE.test(task.time ?? "") ? iso(task.lastRunDate, task.time) : null;
4554
+ }
4555
+ function scheduleDisplay(task, now = /* @__PURE__ */ new Date()) {
4556
+ try {
4557
+ return {
4558
+ nextRun: nextRun(task, now),
4559
+ lastRun: lastRun(task),
4560
+ scheduleStatus: scheduleState(task, now)
4561
+ };
4562
+ } catch {
4563
+ return { nextRun: null, lastRun: null, scheduleStatus: null };
4564
+ }
4565
+ }
4566
+ function validateScheduleRules(task, now = /* @__PURE__ */ new Date()) {
4567
+ const repeating = isRepeating(task);
4568
+ const { date: today, minutes } = sgtParts(now);
4569
+ if (repeating) {
4570
+ if (!task.startDate)
4571
+ return "Please choose a start date for a repeating schedule.";
4572
+ if (task.repeat === "weekly" && !(task.daysOfWeek && task.daysOfWeek.length)) {
4573
+ return "Pick at least one weekday for a weekly repeat.";
4574
+ }
4575
+ }
4576
+ const start = repeating ? task.startDate : [...task.dates ?? []].sort()[0];
4577
+ if (task.endDate && start && start > task.endDate) {
4578
+ return "The end date must be on or after the start date.";
4579
+ }
4580
+ if (task.endDate && task.endDate < today)
4581
+ return "The end date has already passed.";
4582
+ if (!repeating) {
4583
+ const dates = (task.dates ?? []).filter((d) => !task.endDate || d <= task.endDate);
4584
+ if (dates.length && dates.every((d) => d < today)) {
4585
+ return "All the dates you picked have already passed. Pick today or a later date.";
4586
+ }
4587
+ if (dates.length && dates.every((d) => d <= today) && dates.includes(today) && TIME_RE.test(task.time) && toMinutes(task.time) <= minutes) {
4588
+ return "That time has already passed today. Pick a later time or another date.";
4589
+ }
4590
+ }
4591
+ if (!upcoming({ ...task, paused: false, updatedAt: now }, now)) {
4592
+ return "This schedule would never run. Check the dates, end date and time.";
4593
+ }
4594
+ return null;
4595
+ }
4596
+ var scheduleDate = Joi10.string().pattern(DATE_RE).custom((value, helpers) => isRealDate(value) ? value : helpers.error("date.invalid")).messages({
4597
+ "string.pattern.base": "Dates must look like 2026-10-21.",
4598
+ "string.empty": "Please choose a date.",
4599
+ "date.invalid": "That date does not exist. Dates must look like 2026-10-21.",
4600
+ "string.base": "Dates must look like 2026-10-21."
4601
+ });
4602
+ var REPEATING = Joi10.valid("daily", "weekdays", "weekly", "monthly").required();
4603
+ var scheduleTaskFields = {
4604
+ title: Joi10.string().trim().required().messages({
4605
+ "any.required": "Please enter a title.",
4606
+ "string.empty": "Please enter a title.",
4607
+ "string.base": "Please enter a title."
4608
+ }),
4609
+ time: Joi10.string().pattern(TIME_RE).required().messages({
4610
+ "any.required": "Please choose a time.",
4611
+ "string.empty": "Please choose a time.",
4612
+ "string.pattern.base": "Time must be in 24-hour format like 08:30.",
4613
+ "string.base": "Time must be in 24-hour format like 08:30."
4614
+ }),
4615
+ dates: Joi10.when("repeat", {
4616
+ is: REPEATING,
4617
+ then: Joi10.array().items(scheduleDate).optional().default([]),
4618
+ otherwise: Joi10.array().min(1).items(scheduleDate).required()
4619
+ }).messages({
4620
+ "any.required": "Pick at least one date, or choose a repeat.",
4621
+ "array.min": "Pick at least one date, or choose a repeat.",
4622
+ "array.base": "Dates must be a list of dates."
4623
+ }),
4624
+ description: Joi10.string().optional().allow("", null),
4625
+ areas: Joi10.array().min(1).items(
4626
+ Joi10.object({
4627
+ name: Joi10.string().required(),
4628
+ value: Joi10.any().required()
4629
+ })
4630
+ ).required().messages({
4631
+ "any.required": "Pick at least one area.",
4632
+ "array.min": "Pick at least one area.",
4633
+ "array.base": "Pick at least one area."
4634
+ }),
4635
+ repeat: Joi10.string().valid(...REPEATS).optional().allow("", null).messages({ "any.only": "Repeat must be one of: none, daily, weekdays, weekly, monthly." }),
4636
+ daysOfWeek: Joi10.array().items(Joi10.number().integer().min(0).max(6)).unique().optional().allow(null).messages({
4637
+ "number.base": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4638
+ "number.min": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4639
+ "number.max": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4640
+ "number.integer": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
4641
+ "array.unique": "Each weekday can only be picked once."
4642
+ }),
4643
+ dayOfMonth: Joi10.number().integer().min(1).max(31).optional().allow(null).messages({ "number.base": "Day of the month must be 1 to 31.", "number.min": "Day of the month must be 1 to 31.", "number.max": "Day of the month must be 1 to 31." }),
4644
+ startDate: scheduleDate.optional().allow("", null),
4645
+ endDate: scheduleDate.optional().allow("", null),
4646
+ paused: Joi10.boolean().optional().allow(null).messages({ "boolean.base": "Paused must be true or false." })
4647
+ };
4648
+ var OPTIONAL_KEYS = ["repeat", "daysOfWeek", "dayOfMonth", "startDate", "endDate", "paused"];
4649
+ var isBlank = (v) => v === "" || v === null;
4650
+ function cleanScheduleInput(value) {
4651
+ const out = { ...value };
4652
+ for (const key of OPTIONAL_KEYS)
4653
+ if (isBlank(out[key]))
4654
+ delete out[key];
4655
+ if (out.repeat === "none") {
4656
+ delete out.daysOfWeek;
4657
+ delete out.dayOfMonth;
4658
+ delete out.startDate;
4659
+ }
4660
+ return out;
4661
+ }
4662
+ var TIMING_KEYS = ["time", "dates", "repeat", "daysOfWeek", "dayOfMonth", "startDate", "endDate"];
4663
+ function splitScheduleUpdate(payload) {
4664
+ const set = {};
4665
+ const unset = [];
4666
+ for (const [key, value] of Object.entries(payload)) {
4667
+ if (OPTIONAL_KEYS.includes(key) && isBlank(value))
4668
+ unset.push(key);
4669
+ else if (value !== void 0)
4670
+ set[key] = value;
4671
+ }
4672
+ if (set.repeat === "none") {
4673
+ for (const key of ["daysOfWeek", "dayOfMonth", "startDate"]) {
4674
+ if (!unset.includes(key))
4675
+ unset.push(key);
4676
+ delete set[key];
4677
+ }
4678
+ }
4679
+ return { set, unset };
4680
+ }
4681
+ function regularSetsMissing(checklist, setCount) {
4682
+ const regular = (checklist ?? []).filter((c) => c.isScheduleTask !== true).length;
4683
+ return Math.max(0, setCount - regular);
4684
+ }
4685
+ var normTiming = (t) => ({
4686
+ time: t.time ?? null,
4687
+ dates: [...Array.isArray(t.dates) ? t.dates : []].sort(),
4688
+ repeat: !t.repeat || t.repeat === "none" ? "none" : t.repeat,
4689
+ daysOfWeek: [...Array.isArray(t.daysOfWeek) ? t.daysOfWeek : []].sort(),
4690
+ dayOfMonth: t.dayOfMonth ?? null,
4691
+ startDate: t.startDate || null,
4692
+ endDate: t.endDate || null
4693
+ });
4694
+ function timingChanged(stored, after) {
4695
+ return JSON.stringify(normTiming(stored)) !== JSON.stringify(normTiming(after));
4696
+ }
4697
+
4439
4698
  // src/services/hygiene-area-checklist.service.ts
4440
4699
  function useAreaChecklistService() {
4441
4700
  const { getAreasForChecklist } = useAreaRepo();
@@ -4503,10 +4762,10 @@ function useAreaChecklistService() {
4503
4762
  `Area checklist for area ${area.name} exists but has no auto-gen sets. Inserting auto-gen sets.`
4504
4763
  );
4505
4764
  const setCount2 = Number(area.set) || 1;
4506
- const totalExistingSets = existing.checklist?.length ?? 0;
4507
- if (totalExistingSets >= setCount2) {
4765
+ const setsToAdd = regularSetsMissing(existing.checklist, setCount2);
4766
+ if (setsToAdd <= 0) {
4508
4767
  logger18.info(
4509
- `Area checklist for area ${area.name} already has ${totalExistingSets} set(s) (configured: ${setCount2}). Skipping auto-gen sets.`
4768
+ `Area checklist for area ${area.name} already has its ${setCount2} regular set(s). Skipping auto-gen sets.`
4510
4769
  );
4511
4770
  return existing._id;
4512
4771
  }
@@ -4514,7 +4773,6 @@ function useAreaChecklistService() {
4514
4773
  (max, c) => Math.max(max, c.set ?? 0),
4515
4774
  0
4516
4775
  ) ?? 0;
4517
- const setsToAdd = setCount2 - totalExistingSets;
4518
4776
  const autoGenSets = Array.from(
4519
4777
  { length: setsToAdd },
4520
4778
  (_, index) => ({
@@ -4675,7 +4933,7 @@ function useAreaChecklistService() {
4675
4933
  }
4676
4934
 
4677
4935
  // src/controllers/hygiene-area-checklist.controller.ts
4678
- import Joi10 from "joi";
4936
+ import Joi11 from "joi";
4679
4937
  import {
4680
4938
  BadRequestError as BadRequestError16,
4681
4939
  InternalServerError as InternalServerError7,
@@ -4920,9 +5178,9 @@ function useAreaChecklistController() {
4920
5178
  site: req.params.site,
4921
5179
  schedule: req.params.schedule
4922
5180
  };
4923
- const validation = Joi10.object({
4924
- site: Joi10.string().hex().required(),
4925
- schedule: Joi10.string().hex().required()
5181
+ const validation = Joi11.object({
5182
+ site: Joi11.string().hex().required(),
5183
+ schedule: Joi11.string().hex().required()
4926
5184
  });
4927
5185
  const { error, value } = validation.validate(payload);
4928
5186
  if (error) {
@@ -4942,14 +5200,14 @@ function useAreaChecklistController() {
4942
5200
  }
4943
5201
  async function getAllAreaChecklist(req, res, next) {
4944
5202
  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()
5203
+ const validation = Joi11.object({
5204
+ page: Joi11.number().min(1).optional().allow("", null),
5205
+ limit: Joi11.number().min(1).optional().allow("", null),
5206
+ search: Joi11.string().optional().allow("", null),
5207
+ type: Joi11.string().valid(...allowedTypes, "all").optional().allow("", null),
5208
+ status: Joi11.string().valid(...allowedStatus, "all").optional().allow("", null),
5209
+ schedule: Joi11.string().hex().required(),
5210
+ serviceType: Joi11.string().valid(...Object.values(AppServiceType10)).required()
4953
5211
  });
4954
5212
  const { error } = validation.validate(query);
4955
5213
  if (error) {
@@ -4984,14 +5242,14 @@ function useAreaChecklistController() {
4984
5242
  }
4985
5243
  async function getAreaChecklistHistory(req, res, next) {
4986
5244
  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)
5245
+ const validation = Joi11.object({
5246
+ page: Joi11.number().min(1).optional().allow("", null),
5247
+ limit: Joi11.number().min(1).optional().allow("", null),
5248
+ search: Joi11.string().optional().allow("", null),
5249
+ type: Joi11.string().optional().allow("", ...allowedTypes),
5250
+ schedule: Joi11.string().hex().required(),
5251
+ status: Joi11.string().allow("", null, ...allowedStatus),
5252
+ createdAt: Joi11.alternatives().try(Joi11.date(), Joi11.string()).optional().allow("", null)
4995
5253
  });
4996
5254
  const { error } = validation.validate(query);
4997
5255
  if (error) {
@@ -5025,7 +5283,7 @@ function useAreaChecklistController() {
5025
5283
  }
5026
5284
  }
5027
5285
  async function getAreaChecklistHistoryDetails(req, res, next) {
5028
- const validation = Joi10.string().hex().required();
5286
+ const validation = Joi11.string().hex().required();
5029
5287
  const _id = req.params.id;
5030
5288
  const { error, value } = validation.validate(_id);
5031
5289
  if (error) {
@@ -5045,12 +5303,12 @@ function useAreaChecklistController() {
5045
5303
  }
5046
5304
  async function getAreaChecklistUnits(req, res, next) {
5047
5305
  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()
5306
+ const validation = Joi11.object({
5307
+ page: Joi11.number().min(1).optional().allow("", null),
5308
+ limit: Joi11.number().min(1).optional().allow("", null),
5309
+ search: Joi11.string().optional().allow("", null),
5310
+ serviceType: Joi11.string().valid(...Object.values(AppServiceType10)).required(),
5311
+ id: Joi11.string().hex().required()
5054
5312
  });
5055
5313
  const { error } = validation.validate(query);
5056
5314
  if (error) {
@@ -5100,21 +5358,21 @@ function useAreaChecklistController() {
5100
5358
  ...decisionValues,
5101
5359
  completedBy
5102
5360
  };
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),
5361
+ const validation = Joi11.object({
5362
+ id: Joi11.string().hex().required(),
5363
+ set: Joi11.number().integer().min(1).required(),
5364
+ unit: Joi11.string().hex().required(),
5365
+ decision: Joi11.string().valid("approve", "reject").required(),
5366
+ approve: Joi11.boolean().optional(),
5367
+ reject: Joi11.boolean().optional(),
5368
+ remarks: Joi11.string().optional().allow("", null),
5111
5369
  /*
5112
5370
  * Shape only. WHETHER a photo is required depends on the set's current
5113
5371
  * state in the database, which Joi cannot see — see the check below.
5114
5372
  * Each entry must still be a real id: a blank string carries no evidence.
5115
5373
  */
5116
- attachment: Joi10.array().items(Joi10.string().trim().min(1)).optional(),
5117
- completedBy: Joi10.string().hex().required()
5374
+ attachment: Joi11.array().items(Joi11.string().trim().min(1)).optional(),
5375
+ completedBy: Joi11.string().hex().required()
5118
5376
  });
5119
5377
  const { error } = validation.validate(payload);
5120
5378
  if (error) {
@@ -5151,7 +5409,7 @@ function useAreaChecklistController() {
5151
5409
  }
5152
5410
  }
5153
5411
  async function downloadChecklistPdf(req, res, next) {
5154
- const validation = Joi10.string().hex().required();
5412
+ const validation = Joi11.string().hex().required();
5155
5413
  const schedule = req.params.schedule;
5156
5414
  const { error, value } = validation.validate(schedule);
5157
5415
  if (error) {
@@ -5197,16 +5455,16 @@ function useAreaChecklistController() {
5197
5455
  }
5198
5456
 
5199
5457
  // src/models/hygiene-supply.model.ts
5200
- import Joi11 from "joi";
5458
+ import Joi12 from "joi";
5201
5459
  import { ObjectId as ObjectId12 } from "mongodb";
5202
5460
  import { BadRequestError as BadRequestError17, logger as logger21 } from "@7365admin1/node-server-utils";
5203
5461
  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()
5462
+ var supplySchema = Joi12.object({
5463
+ site: Joi12.string().hex().required(),
5464
+ serviceType: Joi12.string().valid(...Object.values(AppServiceType11)).required(),
5465
+ name: Joi12.string().required(),
5466
+ unitOfMeasurement: Joi12.string().required(),
5467
+ attachment: Joi12.string().allow(null, "").optional()
5210
5468
  });
5211
5469
  function MSupply(value) {
5212
5470
  const { error } = supplySchema.validate(value);
@@ -5505,7 +5763,7 @@ function useSupplyRepository() {
5505
5763
  }
5506
5764
 
5507
5765
  // src/controllers/hygiene-supply.controller.ts
5508
- import Joi12 from "joi";
5766
+ import Joi13 from "joi";
5509
5767
  import { BadRequestError as BadRequestError19, logger as logger23 } from "@7365admin1/node-server-utils";
5510
5768
  import { AppServiceType as AppServiceType12 } from "@7365admin1/core";
5511
5769
  function useSupplyController() {
@@ -5536,12 +5794,12 @@ function useSupplyController() {
5536
5794
  }
5537
5795
  async function getSupplies(req, res, next) {
5538
5796
  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()
5797
+ const validation = Joi13.object({
5798
+ page: Joi13.number().min(1).optional().allow("", null),
5799
+ limit: Joi13.number().min(1).optional().allow("", null),
5800
+ search: Joi13.string().optional().allow("", null),
5801
+ site: Joi13.string().hex().required(),
5802
+ serviceType: Joi13.string().valid(...Object.values(AppServiceType12)).required()
5545
5803
  });
5546
5804
  const { error } = validation.validate(query);
5547
5805
  if (error) {
@@ -5571,7 +5829,7 @@ function useSupplyController() {
5571
5829
  }
5572
5830
  }
5573
5831
  async function getSupplyById(req, res, next) {
5574
- const validation = Joi12.string().hex().required();
5832
+ const validation = Joi13.string().hex().required();
5575
5833
  const _id = req.params.id;
5576
5834
  const { error, value } = validation.validate(_id);
5577
5835
  if (error) {
@@ -5591,12 +5849,12 @@ function useSupplyController() {
5591
5849
  }
5592
5850
  async function updateSupply(req, res, next) {
5593
5851
  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)
5852
+ const validation = Joi13.object({
5853
+ id: Joi13.string().hex().required(),
5854
+ name: Joi13.string().optional().allow("", null),
5855
+ unitOfMeasurement: Joi13.string().optional().allow("", null),
5856
+ qty: Joi13.number().min(0).optional().allow("", null),
5857
+ attachment: Joi13.string().optional().allow("", null)
5600
5858
  });
5601
5859
  const { error } = validation.validate(payload);
5602
5860
  if (error) {
@@ -5617,7 +5875,7 @@ function useSupplyController() {
5617
5875
  }
5618
5876
  async function deleteSupply(req, res, next) {
5619
5877
  const id = req.params.id;
5620
- const validation = Joi12.string().hex().required();
5878
+ const validation = Joi13.string().hex().required();
5621
5879
  const { error, value } = validation.validate(id);
5622
5880
  if (error) {
5623
5881
  logger23.log({ level: "error", message: error.message });
@@ -5644,18 +5902,18 @@ function useSupplyController() {
5644
5902
  }
5645
5903
 
5646
5904
  // src/models/hygiene-stock.model.ts
5647
- import Joi13 from "joi";
5905
+ import Joi14 from "joi";
5648
5906
  import { ObjectId as ObjectId14 } from "mongodb";
5649
5907
  import { BadRequestError as BadRequestError20, logger as logger24 } from "@7365admin1/node-server-utils";
5650
5908
  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)
5909
+ var stockSchema = Joi14.object({
5910
+ site: Joi14.string().hex().required(),
5911
+ serviceType: Joi14.string().valid(...Object.values(AppServiceType13)).required(),
5912
+ supply: Joi14.string().hex().required(),
5913
+ in: Joi14.number().min(0).optional(),
5914
+ out: Joi14.number().min(0).optional(),
5915
+ balance: Joi14.number().min(0).required(),
5916
+ remarks: Joi14.string().optional().allow("", null)
5659
5917
  });
5660
5918
  function MStock(value) {
5661
5919
  const { error } = stockSchema.validate(value);
@@ -5882,7 +6140,7 @@ function useStockService() {
5882
6140
  }
5883
6141
 
5884
6142
  // src/controllers/hygiene-stock.controller.ts
5885
- import Joi14 from "joi";
6143
+ import Joi15 from "joi";
5886
6144
  import { BadRequestError as BadRequestError23, logger as logger26 } from "@7365admin1/node-server-utils";
5887
6145
  import { AppServiceType as AppServiceType14 } from "@7365admin1/core";
5888
6146
  function useStockController() {
@@ -5890,12 +6148,12 @@ function useStockController() {
5890
6148
  const { createStock: _createStock } = useStockService();
5891
6149
  async function createStock(req, res, next) {
5892
6150
  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)
6151
+ const validation = Joi15.object({
6152
+ site: Joi15.string().hex().required(),
6153
+ serviceType: Joi15.string().valid(...Object.values(AppServiceType14)).required(),
6154
+ supply: Joi15.string().hex().required(),
6155
+ qty: Joi15.number().min(0).required(),
6156
+ remarks: Joi15.string().optional().allow("", null)
5899
6157
  });
5900
6158
  const { error } = validation.validate(payload);
5901
6159
  if (error) {
@@ -5915,13 +6173,13 @@ function useStockController() {
5915
6173
  }
5916
6174
  async function getStocksBySupplyId(req, res, next) {
5917
6175
  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()
6176
+ const validation = Joi15.object({
6177
+ page: Joi15.number().min(1).optional().allow("", null),
6178
+ limit: Joi15.number().min(1).optional().allow("", null),
6179
+ search: Joi15.string().optional().allow("", null),
6180
+ site: Joi15.string().hex().required(),
6181
+ serviceType: Joi15.string().valid(...Object.values(AppServiceType14)).required(),
6182
+ supply: Joi15.string().hex().required()
5925
6183
  });
5926
6184
  const { error } = validation.validate(query);
5927
6185
  if (error) {
@@ -5983,21 +6241,21 @@ async function applyCheckOutDecision(decision, decide, returnStock) {
5983
6241
  }
5984
6242
 
5985
6243
  // src/models/hygiene-checkout-item.model.ts
5986
- import Joi15 from "joi";
6244
+ import Joi16 from "joi";
5987
6245
  import { ObjectId as ObjectId16 } from "mongodb";
5988
6246
  import { BadRequestError as BadRequestError24, logger as logger27 } from "@7365admin1/node-server-utils";
5989
6247
  import { AppServiceType as AppServiceType15 } from "@7365admin1/core";
5990
6248
  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()
6249
+ var checkOutItemSchema = Joi16.object({
6250
+ site: Joi16.string().hex().required(),
6251
+ serviceType: Joi16.string().valid(...Object.values(AppServiceType15)).required(),
6252
+ supply: Joi16.string().hex().required(),
6253
+ supplyName: Joi16.string().required(),
6254
+ supplyQty: Joi16.number().min(0).required(),
6255
+ qty: Joi16.number().min(0).required(),
6256
+ attachment: Joi16.array().items(Joi16.string()).optional().allow(null),
6257
+ createdBy: Joi16.string().hex().required(),
6258
+ createdByName: Joi16.string().required()
6001
6259
  });
6002
6260
  function MCheckOutItem(value) {
6003
6261
  const { error } = checkOutItemSchema.validate(value);
@@ -6486,7 +6744,7 @@ function useCheckOutItemService() {
6486
6744
  }
6487
6745
 
6488
6746
  // src/controllers/hygiene-checkout-item.controller.ts
6489
- import Joi16 from "joi";
6747
+ import Joi17 from "joi";
6490
6748
  import { BadRequestError as BadRequestError27, logger as logger29 } from "@7365admin1/node-server-utils";
6491
6749
  import { AppServiceType as AppServiceType16 } from "@7365admin1/core";
6492
6750
  function useCheckOutItemController() {
@@ -6510,13 +6768,13 @@ function useCheckOutItemController() {
6510
6768
  ...req.params,
6511
6769
  createdBy
6512
6770
  };
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()
6771
+ const validation = Joi17.object({
6772
+ site: Joi17.string().hex().required(),
6773
+ serviceType: Joi17.string().valid(...Object.values(AppServiceType16)).required(),
6774
+ supply: Joi17.string().hex().required(),
6775
+ qty: Joi17.number().min(0).required(),
6776
+ attachment: Joi17.array().items(Joi17.string()).optional().allow(null),
6777
+ createdBy: Joi17.string().hex().required()
6520
6778
  });
6521
6779
  const { error } = validation.validate(payload);
6522
6780
  if (error) {
@@ -6545,15 +6803,15 @@ function useCheckOutItemController() {
6545
6803
  ...req.params,
6546
6804
  createdBy
6547
6805
  };
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)
6806
+ const validation = Joi17.object({
6807
+ site: Joi17.string().hex().required(),
6808
+ serviceType: Joi17.string().valid(...Object.values(AppServiceType16)).required(),
6809
+ createdBy: Joi17.string().hex().required(),
6810
+ items: Joi17.array().items(
6811
+ Joi17.object({
6812
+ supply: Joi17.string().hex().required(),
6813
+ qty: Joi17.number().min(0).required(),
6814
+ attachment: Joi17.array().items(Joi17.string()).optional().allow(null)
6557
6815
  })
6558
6816
  ).min(1).required()
6559
6817
  });
@@ -6575,12 +6833,12 @@ function useCheckOutItemController() {
6575
6833
  }
6576
6834
  async function getCheckOutItems(req, res, next) {
6577
6835
  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()
6836
+ const validation = Joi17.object({
6837
+ page: Joi17.number().min(1).optional().allow("", null),
6838
+ limit: Joi17.number().min(1).optional().allow("", null),
6839
+ search: Joi17.string().optional().allow("", null),
6840
+ site: Joi17.string().hex().required(),
6841
+ serviceType: Joi17.string().valid(...Object.values(AppServiceType16)).required()
6584
6842
  });
6585
6843
  const { error } = validation.validate(query);
6586
6844
  if (error) {
@@ -6610,7 +6868,7 @@ function useCheckOutItemController() {
6610
6868
  }
6611
6869
  }
6612
6870
  async function getCheckOutItemById(req, res, next) {
6613
- const validation = Joi16.string().hex().required();
6871
+ const validation = Joi17.string().hex().required();
6614
6872
  const _id = req.params.id;
6615
6873
  const { error, value } = validation.validate(_id);
6616
6874
  if (error) {
@@ -6649,13 +6907,13 @@ function useCheckOutItemController() {
6649
6907
  ...decisionValues,
6650
6908
  completedBy
6651
6909
  };
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()
6910
+ const validation = Joi17.object({
6911
+ id: Joi17.string().hex().required(),
6912
+ decision: Joi17.string().valid("approve", "disapprove").required(),
6913
+ approve: Joi17.boolean().required(),
6914
+ reject: Joi17.boolean().required(),
6915
+ remarks: Joi17.string().optional().allow("", null),
6916
+ completedBy: Joi17.string().hex().required()
6659
6917
  });
6660
6918
  const { error } = validation.validate(payload);
6661
6919
  if (error) {
@@ -6688,23 +6946,15 @@ function useCheckOutItemController() {
6688
6946
  // src/models/hygiene-schedule-task.model.ts
6689
6947
  import { BadRequestError as BadRequestError28, logger as logger30 } from "@7365admin1/node-server-utils";
6690
6948
  import { AppServiceType as AppServiceType17 } from "@7365admin1/core";
6691
- import Joi17 from "joi";
6949
+ import Joi18 from "joi";
6692
6950
  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()
6951
+ var scheduleTaskSchema = Joi18.object({
6952
+ site: Joi18.string().hex().required(),
6953
+ serviceType: Joi18.string().valid(...Object.values(AppServiceType17)).required(),
6954
+ ...scheduleTaskFields
6955
+ }).custom((value, helpers) => {
6956
+ const message = validateScheduleRules(cleanScheduleInput(value));
6957
+ return message ? helpers.message({ custom: message }) : value;
6708
6958
  });
6709
6959
  function MScheduleTask(value) {
6710
6960
  const { error } = scheduleTaskSchema.validate(value);
@@ -6731,14 +6981,26 @@ function MScheduleTask(value) {
6731
6981
  }
6732
6982
  });
6733
6983
  }
6984
+ const extra = cleanScheduleInput({
6985
+ repeat: value.repeat,
6986
+ daysOfWeek: value.daysOfWeek,
6987
+ dayOfMonth: value.dayOfMonth,
6988
+ startDate: value.startDate,
6989
+ endDate: value.endDate,
6990
+ paused: value.paused
6991
+ });
6992
+ for (const key of Object.keys(extra))
6993
+ if (extra[key] === void 0)
6994
+ delete extra[key];
6734
6995
  return {
6735
6996
  site: value.site,
6736
6997
  serviceType: value.serviceType,
6737
6998
  title: value.title,
6738
6999
  time: value.time,
6739
- dates: value.dates,
7000
+ dates: value.dates ?? [],
6740
7001
  description: value.description,
6741
7002
  areas: value.areas,
7003
+ ...extra,
6742
7004
  status: "active",
6743
7005
  createdAt: /* @__PURE__ */ new Date(),
6744
7006
  updatedAt: "",
@@ -6758,11 +7020,23 @@ import {
6758
7020
  makeCacheKey as makeCacheKey9,
6759
7021
  NotFoundError as NotFoundError7
6760
7022
  } from "@7365admin1/node-server-utils";
7023
+ var SCHEDULE_FIELDS = {
7024
+ time: 1,
7025
+ dates: 1,
7026
+ repeat: 1,
7027
+ daysOfWeek: 1,
7028
+ dayOfMonth: 1,
7029
+ startDate: 1,
7030
+ endDate: 1,
7031
+ paused: 1,
7032
+ lastRunDate: 1
7033
+ };
6761
7034
  function useScheduleTaskRepository() {
6762
7035
  const db = useAtlas14.getDb();
6763
7036
  if (!db) {
6764
7037
  throw new InternalServerError11("Unable to connect to server.");
6765
7038
  }
7039
+ const database = db;
6766
7040
  const namespace_collection = "site.schedule-tasks";
6767
7041
  const collection = db.collection(namespace_collection);
6768
7042
  const { delNamespace, setCache, getCache } = useCache9(namespace_collection);
@@ -6837,7 +7111,7 @@ function useScheduleTaskRepository() {
6837
7111
  const cachedData = await getCache(cacheKey);
6838
7112
  if (cachedData) {
6839
7113
  logger31.info(`Cache hit for key: ${cacheKey}`);
6840
- return cachedData;
7114
+ return enrichList(cachedData, site, serviceType);
6841
7115
  }
6842
7116
  try {
6843
7117
  const items = await collection.aggregate([
@@ -6846,7 +7120,8 @@ function useScheduleTaskRepository() {
6846
7120
  $project: {
6847
7121
  title: 1,
6848
7122
  areas: 1,
6849
- status: 1
7123
+ status: 1,
7124
+ ...SCHEDULE_FIELDS
6850
7125
  }
6851
7126
  },
6852
7127
  { $sort: { _id: -1 } },
@@ -6860,15 +7135,104 @@ function useScheduleTaskRepository() {
6860
7135
  }).catch((err) => {
6861
7136
  logger31.error(`Failed to set cache for key: ${cacheKey}`, err);
6862
7137
  });
6863
- return data;
7138
+ return enrichList(data, site, serviceType);
6864
7139
  } catch (error) {
6865
7140
  throw error;
6866
7141
  }
6867
7142
  }
6868
- async function getAllScheduleTask() {
7143
+ async function enrichList(data, site, serviceType) {
7144
+ const items = data?.items ?? [];
7145
+ const now = /* @__PURE__ */ new Date();
7146
+ let statuses = /* @__PURE__ */ new Map();
7147
+ try {
7148
+ statuses = await getRunStatuses(items, site, serviceType);
7149
+ } catch (error) {
7150
+ logger31.error("Could not read schedule task run status", error);
7151
+ }
7152
+ return {
7153
+ ...data,
7154
+ items: items.map((item) => {
7155
+ try {
7156
+ return {
7157
+ ...item,
7158
+ ...scheduleDisplay(item, now),
7159
+ lastRunStatus: statuses.get(String(item._id)) ?? null
7160
+ };
7161
+ } catch (error) {
7162
+ logger31.error(`Could not enrich schedule task ${item?._id}`, error);
7163
+ return { ...item, nextRun: null, lastRun: null, scheduleStatus: null, lastRunStatus: null };
7164
+ }
7165
+ })
7166
+ };
7167
+ }
7168
+ async function getRunStatuses(items, site, serviceType) {
7169
+ const result = /* @__PURE__ */ new Map();
7170
+ const ran = items.filter((i) => i.lastRunDate);
7171
+ if (!ran.length)
7172
+ return result;
7173
+ const parents = await database.collection("site.cleaning.schedules").find(
7174
+ {
7175
+ site,
7176
+ serviceType,
7177
+ dateStr: { $in: [...new Set(ran.map((i) => i.lastRunDate))] }
7178
+ },
7179
+ { projection: { dateStr: 1 } }
7180
+ ).toArray();
7181
+ if (!parents.length)
7182
+ return result;
7183
+ const dateOf = new Map(parents.map((p) => [String(p._id), p.dateStr]));
7184
+ const taskIds = ran.map((i) => new ObjectId19(i._id));
7185
+ const rows = await database.collection("site.cleaning.schedule.areas").aggregate([
7186
+ {
7187
+ $match: {
7188
+ schedule: { $in: parents.map((p) => p._id) },
7189
+ "checklist.scheduleTaskId": { $in: taskIds }
7190
+ }
7191
+ },
7192
+ { $unwind: "$checklist" },
7193
+ { $match: { "checklist.scheduleTaskId": { $in: taskIds } } },
7194
+ { $unwind: "$checklist.units" },
7195
+ {
7196
+ $group: {
7197
+ _id: { task: "$checklist.scheduleTaskId", schedule: "$schedule" },
7198
+ total: { $sum: 1 },
7199
+ completed: {
7200
+ $sum: { $cond: [{ $eq: ["$checklist.units.status", "completed"] }, 1, 0] }
7201
+ },
7202
+ closed: {
7203
+ $sum: { $cond: [{ $eq: ["$checklist.units.status", "closed"] }, 1, 0] }
7204
+ }
7205
+ }
7206
+ }
7207
+ ]).toArray();
7208
+ const sums = /* @__PURE__ */ new Map();
7209
+ for (const row of rows) {
7210
+ const task = ran.find((i) => String(i._id) === String(row._id.task));
7211
+ if (!task || dateOf.get(String(row._id.schedule)) !== task.lastRunDate)
7212
+ continue;
7213
+ const sum = sums.get(String(task._id)) ?? { total: 0, completed: 0, closed: 0 };
7214
+ sum.total += row.total;
7215
+ sum.completed += row.completed;
7216
+ sum.closed += row.closed;
7217
+ sums.set(String(task._id), sum);
7218
+ }
7219
+ for (const [id, sum] of sums) {
7220
+ result.set(id, sum.completed === sum.total ? "done" : sum.closed > 0 ? "missed" : "pending");
7221
+ }
7222
+ return result;
7223
+ }
7224
+ async function getAllScheduleTask(today) {
6869
7225
  const query = {
6870
7226
  status: { $ne: "deleted" }
6871
7227
  };
7228
+ if (today) {
7229
+ query.paused = { $ne: true };
7230
+ query.lastRunDate = { $ne: today };
7231
+ query.$or = [
7232
+ { dates: today },
7233
+ { repeat: { $in: ["daily", "weekdays", "weekly", "monthly"] } }
7234
+ ];
7235
+ }
6872
7236
  try {
6873
7237
  const items = await collection.aggregate([{ $match: query }, { $sort: { _id: -1 } }]).toArray();
6874
7238
  return items;
@@ -6876,6 +7240,9 @@ function useScheduleTaskRepository() {
6876
7240
  throw error;
6877
7241
  }
6878
7242
  }
7243
+ function withScheduleDisplay(row) {
7244
+ return { ...row, ...scheduleDisplay(row, /* @__PURE__ */ new Date()) };
7245
+ }
6879
7246
  async function getScheduleTaskById(_id, session) {
6880
7247
  try {
6881
7248
  _id = new ObjectId19(_id);
@@ -6893,7 +7260,7 @@ function useScheduleTaskRepository() {
6893
7260
  const cachedData = await getCache(cacheKey);
6894
7261
  if (cachedData) {
6895
7262
  logger31.info(`Cache hit for key: ${cacheKey}`);
6896
- return cachedData;
7263
+ return withScheduleDisplay(cachedData);
6897
7264
  }
6898
7265
  } else {
6899
7266
  logger31.info(`Skipping cache during transaction for key: ${cacheKey}`);
@@ -6904,12 +7271,13 @@ function useScheduleTaskRepository() {
6904
7271
  {
6905
7272
  $project: {
6906
7273
  title: 1,
6907
- time: 1,
6908
- dates: 1,
6909
7274
  description: 1,
6910
7275
  areas: 1,
6911
7276
  status: 1,
6912
- createdAt: 1
7277
+ createdAt: 1,
7278
+ site: 1,
7279
+ serviceType: 1,
7280
+ ...SCHEDULE_FIELDS
6913
7281
  }
6914
7282
  }
6915
7283
  ]).toArray();
@@ -6921,7 +7289,7 @@ function useScheduleTaskRepository() {
6921
7289
  }).catch((err) => {
6922
7290
  logger31.error(`Failed to set cache for key: ${cacheKey}`, err);
6923
7291
  });
6924
- return data[0];
7292
+ return withScheduleDisplay(data[0]);
6925
7293
  } catch (error) {
6926
7294
  throw error;
6927
7295
  }
@@ -6945,16 +7313,20 @@ function useScheduleTaskRepository() {
6945
7313
  });
6946
7314
  }
6947
7315
  try {
6948
- const updateValue = { ...value, updatedAt: /* @__PURE__ */ new Date() };
7316
+ const { set, unset } = splitScheduleUpdate(value);
7317
+ const update = {
7318
+ $set: { ...set, updatedAt: /* @__PURE__ */ new Date() }
7319
+ };
7320
+ if (unset.length) {
7321
+ update.$unset = Object.fromEntries(unset.map((key) => [key, ""]));
7322
+ }
6949
7323
  const res = await collection.updateOne(
6950
- { _id },
6951
- { $set: updateValue },
7324
+ { _id, status: { $ne: "deleted" } },
7325
+ update,
6952
7326
  { session }
6953
7327
  );
6954
- if (res.modifiedCount === 0) {
6955
- throw new InternalServerError11(
6956
- "Unable to update hygiene schedule task."
6957
- );
7328
+ if (res.matchedCount === 0) {
7329
+ throw new NotFoundError7("Schedule task not found.");
6958
7330
  }
6959
7331
  delNamespace().then(() => {
6960
7332
  logger31.info(`Cache cleared for namespace: ${namespace_collection}`);
@@ -6969,6 +7341,18 @@ function useScheduleTaskRepository() {
6969
7341
  throw error;
6970
7342
  }
6971
7343
  }
7344
+ async function markScheduleTaskRan(_id, date) {
7345
+ await collection.updateOne(
7346
+ { _id: new ObjectId19(_id) },
7347
+ { $set: { lastRunDate: date, lastRunAt: /* @__PURE__ */ new Date() } }
7348
+ );
7349
+ delNamespace().catch((err) => {
7350
+ logger31.error(
7351
+ `Failed to clear cache for namespace: ${namespace_collection}`,
7352
+ err
7353
+ );
7354
+ });
7355
+ }
6972
7356
  async function deleteScheduleTask(_id, session) {
6973
7357
  try {
6974
7358
  _id = new ObjectId19(_id);
@@ -7008,6 +7392,7 @@ function useScheduleTaskRepository() {
7008
7392
  createScheduleTask,
7009
7393
  getScheduleTasks,
7010
7394
  getAllScheduleTask,
7395
+ markScheduleTaskRan,
7011
7396
  getScheduleTaskById,
7012
7397
  updateScheduleTask,
7013
7398
  deleteScheduleTask
@@ -7019,7 +7404,7 @@ import { ObjectId as ObjectId20 } from "mongodb";
7019
7404
  import { logger as logger32 } from "@7365admin1/node-server-utils";
7020
7405
  function useScheduleTaskService() {
7021
7406
  const { createParentChecklist } = useParentChecklistRepo();
7022
- const { getAllScheduleTask } = useScheduleTaskRepository();
7407
+ const { getAllScheduleTask, markScheduleTaskRan } = useScheduleTaskRepository();
7023
7408
  const {
7024
7409
  createAreaChecklist,
7025
7410
  getAreaChecklistByAreaAndSchedule,
@@ -7028,73 +7413,7 @@ function useScheduleTaskService() {
7028
7413
  const { getAreaById } = useAreaRepo();
7029
7414
  function checkScheduleConditions(schedule, serviceType, currentDate = /* @__PURE__ */ new Date()) {
7030
7415
  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;
7416
+ return shouldRunNow(schedule, currentDate);
7098
7417
  } catch (error) {
7099
7418
  logger32.error(
7100
7419
  `Error checking schedule conditions for ${schedule._id}:`,
@@ -7103,18 +7422,22 @@ function useScheduleTaskService() {
7103
7422
  return false;
7104
7423
  }
7105
7424
  }
7425
+ const MAX_ATTEMPTS = 5;
7426
+ const attempts = /* @__PURE__ */ new Map();
7106
7427
  async function processScheduledTasks(currentDate) {
7107
7428
  try {
7108
7429
  logger32.info("Starting scheduled task processing...");
7109
- const scheduleTasks = await getAllScheduleTask();
7430
+ const today = sgtParts(currentDate ?? /* @__PURE__ */ new Date()).date;
7431
+ const scheduleTasks = await getAllScheduleTask(today);
7110
7432
  if (!scheduleTasks || scheduleTasks.length === 0) {
7111
7433
  logger32.info("No schedule tasks found to process");
7112
- return { processed: 0, validated: 0 };
7434
+ return { processed: 0, validated: 0, fired: [] };
7113
7435
  }
7114
7436
  logger32.info(`Found ${scheduleTasks.length} schedule tasks to check`);
7115
7437
  let processedCount = 0;
7116
7438
  let validatedCount = 0;
7117
7439
  const validatedTasks = [];
7440
+ const fired = [];
7118
7441
  for (const scheduleTask of scheduleTasks) {
7119
7442
  try {
7120
7443
  const serviceType = scheduleTask.serviceType;
@@ -7157,6 +7480,8 @@ function useScheduleTaskService() {
7157
7480
  logger32.info(
7158
7481
  `Using parent checklist ${parentChecklistId}, now creating/updating area checklists`
7159
7482
  );
7483
+ let failedAreas = 0;
7484
+ const addedAreas = [];
7160
7485
  for (const area of scheduleTask.areas) {
7161
7486
  try {
7162
7487
  const areaId = area.value.toString();
@@ -7236,6 +7561,7 @@ function useScheduleTaskService() {
7236
7561
  newSets
7237
7562
  );
7238
7563
  if (modified > 0) {
7564
+ addedAreas.push(area.name);
7239
7565
  logger32.info(
7240
7566
  `Appended ${areaSetConfig} set(s) starting from ${startSetNumber} to area checklist for area ${area.name}`
7241
7567
  );
@@ -7268,13 +7594,32 @@ function useScheduleTaskService() {
7268
7594
  `Area ${area.name} (${areaId}): Creating new area checklist with schedule-task sets only`
7269
7595
  );
7270
7596
  await createAreaChecklist(checklistData);
7597
+ addedAreas.push(area.name);
7271
7598
  logger32.info(`Created new area checklist for area ${area.name}`);
7272
7599
  }
7273
7600
  } catch (error) {
7274
7601
  logger32.error(`Error processing area ${area.name}:`, error);
7602
+ failedAreas++;
7275
7603
  continue;
7276
7604
  }
7277
7605
  }
7606
+ const attemptKey = `${scheduleTask._id}:${today}`;
7607
+ const tried = (attempts.get(attemptKey) ?? 0) + 1;
7608
+ attempts.set(attemptKey, tried);
7609
+ if (failedAreas === 0 || tried >= MAX_ATTEMPTS) {
7610
+ await markScheduleTaskRan(scheduleTask._id, today);
7611
+ attempts.delete(attemptKey);
7612
+ }
7613
+ if (addedAreas.length > 0) {
7614
+ fired.push({
7615
+ _id: scheduleTask._id,
7616
+ site: scheduleTask.site,
7617
+ serviceType,
7618
+ title: scheduleTask.title,
7619
+ time: scheduleTask.time,
7620
+ areas: addedAreas
7621
+ });
7622
+ }
7278
7623
  processedCount++;
7279
7624
  validatedCount++;
7280
7625
  validatedTasks.push(scheduleTask);
@@ -7286,6 +7631,17 @@ function useScheduleTaskService() {
7286
7631
  `Error processing schedule task ${scheduleTask._id}:`,
7287
7632
  error
7288
7633
  );
7634
+ try {
7635
+ const key = `${scheduleTask._id}:${today}`;
7636
+ const tried = (attempts.get(key) ?? 0) + 1;
7637
+ attempts.set(key, tried);
7638
+ if (tried >= MAX_ATTEMPTS && scheduleTask._id) {
7639
+ await markScheduleTaskRan(scheduleTask._id, today);
7640
+ attempts.delete(key);
7641
+ }
7642
+ } catch (markError) {
7643
+ logger32.error("Could not mark failing schedule task", markError);
7644
+ }
7289
7645
  continue;
7290
7646
  }
7291
7647
  }
@@ -7295,7 +7651,8 @@ function useScheduleTaskService() {
7295
7651
  return {
7296
7652
  processed: processedCount,
7297
7653
  validated: validatedCount,
7298
- tasks: validatedTasks
7654
+ tasks: validatedTasks,
7655
+ fired
7299
7656
  };
7300
7657
  } catch (error) {
7301
7658
  logger32.error("Error processing scheduled tasks:", error);
@@ -7306,7 +7663,7 @@ function useScheduleTaskService() {
7306
7663
  }
7307
7664
 
7308
7665
  // src/controllers/hygiene-schedule-task.controller.ts
7309
- import Joi18 from "joi";
7666
+ import Joi19 from "joi";
7310
7667
  import { BadRequestError as BadRequestError30, logger as logger33 } from "@7365admin1/node-server-utils";
7311
7668
  import { AppServiceType as AppServiceType18 } from "@7365admin1/core";
7312
7669
  function useScheduleTaskController() {
@@ -7337,12 +7694,12 @@ function useScheduleTaskController() {
7337
7694
  }
7338
7695
  async function getScheduleTasks(req, res, next) {
7339
7696
  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()
7697
+ const validation = Joi19.object({
7698
+ page: Joi19.number().min(1).optional().allow("", null),
7699
+ limit: Joi19.number().min(1).optional().allow("", null),
7700
+ search: Joi19.string().optional().allow("", null),
7701
+ site: Joi19.string().hex().required(),
7702
+ serviceType: Joi19.string().valid(...Object.values(AppServiceType18)).required()
7346
7703
  });
7347
7704
  const { error } = validation.validate(query);
7348
7705
  if (error) {
@@ -7350,8 +7707,8 @@ function useScheduleTaskController() {
7350
7707
  next(new BadRequestError30(error.message));
7351
7708
  return;
7352
7709
  }
7353
- const page = parseInt(req.query.page) ?? 1;
7354
- const limit = parseInt(req.query.limit) ?? 10;
7710
+ const page = parseInt(req.query.page) || 1;
7711
+ const limit = parseInt(req.query.limit) || 10;
7355
7712
  const search = req.query.search ?? "";
7356
7713
  const site = req.params.site ?? "";
7357
7714
  const serviceType = req.query.serviceType ?? "";
@@ -7372,7 +7729,7 @@ function useScheduleTaskController() {
7372
7729
  }
7373
7730
  }
7374
7731
  async function getScheduleTaskById(req, res, next) {
7375
- const validation = Joi18.string().hex().required();
7732
+ const validation = Joi19.string().hex().required();
7376
7733
  const _id = req.params.id;
7377
7734
  const { error, value } = validation.validate(_id);
7378
7735
  if (error) {
@@ -7392,20 +7749,17 @@ function useScheduleTaskController() {
7392
7749
  }
7393
7750
  async function updateScheduleTask(req, res, next) {
7394
7751
  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()
7752
+ const optional = Object.fromEntries(
7753
+ Object.entries(scheduleTaskFields).map(([key, rule]) => [
7754
+ key,
7755
+ key === "dates" ? Joi19.array().min(1).items(scheduleDate).optional().messages({
7756
+ "array.min": "Pick at least one date, or choose a repeat."
7757
+ }) : rule.optional()
7758
+ ])
7759
+ );
7760
+ const validation = Joi19.object({
7761
+ id: Joi19.string().hex().required(),
7762
+ ...optional
7409
7763
  });
7410
7764
  const { error } = validation.validate(payload);
7411
7765
  if (error) {
@@ -7413,6 +7767,24 @@ function useScheduleTaskController() {
7413
7767
  next(new BadRequestError30(error.message));
7414
7768
  return;
7415
7769
  }
7770
+ try {
7771
+ const { set, unset } = splitScheduleUpdate(req.body ?? {});
7772
+ if (TIMING_KEYS.some((key) => key in set || unset.includes(key))) {
7773
+ const stored = await _getScheduleTaskById(req.params.id);
7774
+ const merged = { ...stored, ...set };
7775
+ for (const key of unset)
7776
+ delete merged[key];
7777
+ const problem = timingChanged(stored, merged) ? validateScheduleRules(cleanScheduleInput(merged)) : null;
7778
+ if (problem) {
7779
+ next(new BadRequestError30(problem));
7780
+ return;
7781
+ }
7782
+ }
7783
+ } catch (error2) {
7784
+ logger33.log({ level: "error", message: error2.message });
7785
+ next(error2);
7786
+ return;
7787
+ }
7416
7788
  try {
7417
7789
  const { id, ...value } = payload;
7418
7790
  await _updateScheduleTask(id, value);
@@ -7425,7 +7797,7 @@ function useScheduleTaskController() {
7425
7797
  }
7426
7798
  }
7427
7799
  async function deleteScheduleTask(req, res, next) {
7428
- const validation = Joi18.string().hex().required();
7800
+ const validation = Joi19.string().hex().required();
7429
7801
  const _id = req.params.id;
7430
7802
  const { error, value } = validation.validate(_id);
7431
7803
  if (error) {
@@ -7669,17 +8041,17 @@ function useQRService() {
7669
8041
  }
7670
8042
 
7671
8043
  // src/controllers/hygiene-qr.controller.ts
7672
- import Joi19 from "joi";
8044
+ import Joi20 from "joi";
7673
8045
  import { BadRequestError as BadRequestError31, logger as logger35 } from "@7365admin1/node-server-utils";
7674
8046
  function useQRController() {
7675
8047
  const { generateQRImage: _generateQRImage, generateQRPDF: _generateQRPDF } = useQRService();
7676
8048
  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)
8049
+ const validation = Joi20.object({
8050
+ url: Joi20.string().uri().required(),
8051
+ filename: Joi20.string().optional().allow("", null),
8052
+ title: Joi20.string().optional().allow("", null),
8053
+ subtitle: Joi20.string().optional().allow("", null),
8054
+ download: Joi20.boolean().optional().default(false)
7683
8055
  });
7684
8056
  const query = { ...req.query };
7685
8057
  const { error, value } = validation.validate(query);