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