@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/.changeset/schedule-task-repeat-pause.md +5 -0
- package/.changeset/schedule-task-set-fields.md +5 -0
- package/dist/index.d.ts +42 -10
- package/dist/index.js +682 -286
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +682 -286
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/schedule-followups.test.mjs +24 -0
- package/test/schedule-task-rules.test.mjs +185 -0
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
|
|
4507
|
-
if (
|
|
4784
|
+
const setsToAdd = regularSetsMissing(existing.checklist, setCount2);
|
|
4785
|
+
if (setsToAdd <= 0) {
|
|
4508
4786
|
logger18.info(
|
|
4509
|
-
`Area checklist for area ${area.name} already has ${
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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">✗</span>' : ""}</td>
|
|
4704
4983
|
<td class="icon-cell">${unit.approve ? '<span class="approve-icon">✓</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 =
|
|
4924
|
-
site:
|
|
4925
|
-
schedule:
|
|
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 =
|
|
4946
|
-
page:
|
|
4947
|
-
limit:
|
|
4948
|
-
search:
|
|
4949
|
-
type:
|
|
4950
|
-
status:
|
|
4951
|
-
schedule:
|
|
4952
|
-
serviceType:
|
|
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 =
|
|
4988
|
-
page:
|
|
4989
|
-
limit:
|
|
4990
|
-
search:
|
|
4991
|
-
type:
|
|
4992
|
-
schedule:
|
|
4993
|
-
status:
|
|
4994
|
-
createdAt:
|
|
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 =
|
|
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 =
|
|
5049
|
-
page:
|
|
5050
|
-
limit:
|
|
5051
|
-
search:
|
|
5052
|
-
serviceType:
|
|
5053
|
-
id:
|
|
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 =
|
|
5104
|
-
id:
|
|
5105
|
-
set:
|
|
5106
|
-
unit:
|
|
5107
|
-
decision:
|
|
5108
|
-
approve:
|
|
5109
|
-
reject:
|
|
5110
|
-
remarks:
|
|
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:
|
|
5117
|
-
completedBy:
|
|
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 =
|
|
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
|
|
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 =
|
|
5205
|
-
site:
|
|
5206
|
-
serviceType:
|
|
5207
|
-
name:
|
|
5208
|
-
unitOfMeasurement:
|
|
5209
|
-
attachment:
|
|
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
|
|
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 =
|
|
5540
|
-
page:
|
|
5541
|
-
limit:
|
|
5542
|
-
search:
|
|
5543
|
-
site:
|
|
5544
|
-
serviceType:
|
|
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 =
|
|
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 =
|
|
5595
|
-
id:
|
|
5596
|
-
name:
|
|
5597
|
-
unitOfMeasurement:
|
|
5598
|
-
qty:
|
|
5599
|
-
attachment:
|
|
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 =
|
|
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
|
|
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 =
|
|
5652
|
-
site:
|
|
5653
|
-
serviceType:
|
|
5654
|
-
supply:
|
|
5655
|
-
in:
|
|
5656
|
-
out:
|
|
5657
|
-
balance:
|
|
5658
|
-
remarks:
|
|
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
|
|
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 =
|
|
5894
|
-
site:
|
|
5895
|
-
serviceType:
|
|
5896
|
-
supply:
|
|
5897
|
-
qty:
|
|
5898
|
-
remarks:
|
|
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 =
|
|
5919
|
-
page:
|
|
5920
|
-
limit:
|
|
5921
|
-
search:
|
|
5922
|
-
site:
|
|
5923
|
-
serviceType:
|
|
5924
|
-
supply:
|
|
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
|
|
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 =
|
|
5992
|
-
site:
|
|
5993
|
-
serviceType:
|
|
5994
|
-
supply:
|
|
5995
|
-
supplyName:
|
|
5996
|
-
supplyQty:
|
|
5997
|
-
qty:
|
|
5998
|
-
attachment:
|
|
5999
|
-
createdBy:
|
|
6000
|
-
createdByName:
|
|
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
|
|
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 =
|
|
6514
|
-
site:
|
|
6515
|
-
serviceType:
|
|
6516
|
-
supply:
|
|
6517
|
-
qty:
|
|
6518
|
-
attachment:
|
|
6519
|
-
createdBy:
|
|
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 =
|
|
6549
|
-
site:
|
|
6550
|
-
serviceType:
|
|
6551
|
-
createdBy:
|
|
6552
|
-
items:
|
|
6553
|
-
|
|
6554
|
-
supply:
|
|
6555
|
-
qty:
|
|
6556
|
-
attachment:
|
|
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 =
|
|
6579
|
-
page:
|
|
6580
|
-
limit:
|
|
6581
|
-
search:
|
|
6582
|
-
site:
|
|
6583
|
-
serviceType:
|
|
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 =
|
|
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 =
|
|
6653
|
-
id:
|
|
6654
|
-
decision:
|
|
6655
|
-
approve:
|
|
6656
|
-
reject:
|
|
6657
|
-
remarks:
|
|
6658
|
-
completedBy:
|
|
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
|
|
6978
|
+
import Joi18 from "joi";
|
|
6692
6979
|
import { ObjectId as ObjectId18 } from "mongodb";
|
|
6693
|
-
var scheduleTaskSchema =
|
|
6694
|
-
site:
|
|
6695
|
-
serviceType:
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
7353
|
+
{ _id, status: { $ne: "deleted" } },
|
|
7354
|
+
update,
|
|
6952
7355
|
{ session }
|
|
6953
7356
|
);
|
|
6954
|
-
if (res.
|
|
6955
|
-
throw new
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
7341
|
-
page:
|
|
7342
|
-
limit:
|
|
7343
|
-
search:
|
|
7344
|
-
site:
|
|
7345
|
-
serviceType:
|
|
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)
|
|
7354
|
-
const limit = parseInt(req.query.limit)
|
|
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 =
|
|
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
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
)
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
7678
|
-
url:
|
|
7679
|
-
filename:
|
|
7680
|
-
title:
|
|
7681
|
-
subtitle:
|
|
7682
|
-
download:
|
|
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);
|