@7365admin1/module-hygiene 4.29.1-staging.14 → 4.29.1-staging.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/schedule-task-repeat-pause.md +5 -0
- package/.changeset/search-input-bounds.md +5 -0
- package/dist/index.d.ts +40 -10
- package/dist/index.js +647 -270
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +647 -270
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/escape-regex.util.test.mjs +26 -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);
|
|
@@ -4431,6 +4436,265 @@ function useAreaChecklistRepo() {
|
|
|
4431
4436
|
};
|
|
4432
4437
|
}
|
|
4433
4438
|
|
|
4439
|
+
// src/utils/schedule-task-rules.util.ts
|
|
4440
|
+
import Joi10 from "joi";
|
|
4441
|
+
var REPEATS = ["none", "daily", "weekdays", "weekly", "monthly"];
|
|
4442
|
+
var SGT_MS = 8 * 60 * 60 * 1e3;
|
|
4443
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4444
|
+
var TIME_RE = /^([0-1]\d|2[0-3]):([0-5]\d)$/;
|
|
4445
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
4446
|
+
function sgtParts(now) {
|
|
4447
|
+
const s = new Date(now.getTime() + SGT_MS);
|
|
4448
|
+
const date = `${s.getUTCFullYear()}-${pad(s.getUTCMonth() + 1)}-${pad(s.getUTCDate())}`;
|
|
4449
|
+
return { date, minutes: s.getUTCHours() * 60 + s.getUTCMinutes(), weekday: s.getUTCDay() };
|
|
4450
|
+
}
|
|
4451
|
+
function isRealDate(value) {
|
|
4452
|
+
if (typeof value !== "string" || !DATE_RE.test(value))
|
|
4453
|
+
return false;
|
|
4454
|
+
const [y, m, d] = value.split("-").map(Number);
|
|
4455
|
+
const t = new Date(Date.UTC(y, m - 1, d));
|
|
4456
|
+
return t.getUTCFullYear() === y && t.getUTCMonth() === m - 1 && t.getUTCDate() === d;
|
|
4457
|
+
}
|
|
4458
|
+
var toMinutes = (time) => {
|
|
4459
|
+
const [h, m] = time.split(":").map(Number);
|
|
4460
|
+
return h * 60 + m;
|
|
4461
|
+
};
|
|
4462
|
+
var weekdayOf = (date) => {
|
|
4463
|
+
const [y, m, d] = date.split("-").map(Number);
|
|
4464
|
+
return new Date(Date.UTC(y, m - 1, d)).getUTCDay();
|
|
4465
|
+
};
|
|
4466
|
+
var daysInMonth = (date) => {
|
|
4467
|
+
const [y, m] = date.split("-").map(Number);
|
|
4468
|
+
return new Date(Date.UTC(y, m, 0)).getUTCDate();
|
|
4469
|
+
};
|
|
4470
|
+
var addDays = (date, n) => {
|
|
4471
|
+
const [y, m, d] = date.split("-").map(Number);
|
|
4472
|
+
const t = new Date(Date.UTC(y, m - 1, d + n));
|
|
4473
|
+
return `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}-${pad(t.getUTCDate())}`;
|
|
4474
|
+
};
|
|
4475
|
+
var isRepeating = (task) => Boolean(task.repeat) && task.repeat !== "none";
|
|
4476
|
+
function occursOn(task, date) {
|
|
4477
|
+
if (task.endDate && date > task.endDate)
|
|
4478
|
+
return false;
|
|
4479
|
+
if (!isRepeating(task))
|
|
4480
|
+
return Array.isArray(task.dates) && task.dates.includes(date);
|
|
4481
|
+
if (task.startDate && date < task.startDate)
|
|
4482
|
+
return false;
|
|
4483
|
+
const weekday = weekdayOf(date);
|
|
4484
|
+
switch (task.repeat) {
|
|
4485
|
+
case "daily":
|
|
4486
|
+
return true;
|
|
4487
|
+
case "weekdays":
|
|
4488
|
+
return weekday >= 1 && weekday <= 5;
|
|
4489
|
+
case "weekly":
|
|
4490
|
+
return Array.isArray(task.daysOfWeek) && task.daysOfWeek.includes(weekday);
|
|
4491
|
+
case "monthly": {
|
|
4492
|
+
const wanted = task.dayOfMonth ?? (task.startDate ? Number(task.startDate.slice(8, 10)) : 1);
|
|
4493
|
+
return Number(date.slice(8, 10)) === Math.min(wanted, daysInMonth(date));
|
|
4494
|
+
}
|
|
4495
|
+
default:
|
|
4496
|
+
return false;
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
function savedTodayAt(task, today) {
|
|
4500
|
+
const stamp = task.updatedAt || task.createdAt;
|
|
4501
|
+
if (!stamp)
|
|
4502
|
+
return null;
|
|
4503
|
+
const saved = new Date(stamp);
|
|
4504
|
+
if (Number.isNaN(saved.getTime()))
|
|
4505
|
+
return null;
|
|
4506
|
+
const p = sgtParts(saved);
|
|
4507
|
+
return p.date === today ? p.minutes : null;
|
|
4508
|
+
}
|
|
4509
|
+
function shouldRunNow(task, now = /* @__PURE__ */ new Date()) {
|
|
4510
|
+
if (task.paused || !TIME_RE.test(task.time ?? ""))
|
|
4511
|
+
return false;
|
|
4512
|
+
const { date, minutes } = sgtParts(now);
|
|
4513
|
+
if (task.lastRunDate === date || !occursOn(task, date))
|
|
4514
|
+
return false;
|
|
4515
|
+
const due = toMinutes(task.time);
|
|
4516
|
+
if (minutes < due)
|
|
4517
|
+
return false;
|
|
4518
|
+
const saved = savedTodayAt(task, date);
|
|
4519
|
+
return !(saved !== null && saved >= due);
|
|
4520
|
+
}
|
|
4521
|
+
var iso = (date, time) => `${date}T${time}:00+08:00`;
|
|
4522
|
+
function upcoming(task, now) {
|
|
4523
|
+
if (typeof task.time !== "string" || !TIME_RE.test(task.time))
|
|
4524
|
+
return null;
|
|
4525
|
+
const { date: today, minutes } = sgtParts(now);
|
|
4526
|
+
const due = toMinutes(task.time);
|
|
4527
|
+
if (occursOn(task, today) && (minutes < due || shouldRunNow({ ...task, paused: false }, now))) {
|
|
4528
|
+
return iso(today, task.time);
|
|
4529
|
+
}
|
|
4530
|
+
if (!isRepeating(task)) {
|
|
4531
|
+
const later = (task.dates ?? []).filter((d) => d > today && (!task.endDate || d <= task.endDate)).sort();
|
|
4532
|
+
return later.length ? iso(later[0], task.time) : null;
|
|
4533
|
+
}
|
|
4534
|
+
const from = task.startDate && task.startDate > today ? task.startDate : today;
|
|
4535
|
+
for (let i = from === today ? 1 : 0; i <= 400; i++) {
|
|
4536
|
+
const d = addDays(from, i);
|
|
4537
|
+
if (task.endDate && d > task.endDate)
|
|
4538
|
+
return null;
|
|
4539
|
+
if (occursOn(task, d))
|
|
4540
|
+
return iso(d, task.time);
|
|
4541
|
+
}
|
|
4542
|
+
return null;
|
|
4543
|
+
}
|
|
4544
|
+
function nextRun(task, now = /* @__PURE__ */ new Date()) {
|
|
4545
|
+
return task.paused ? null : upcoming(task, now);
|
|
4546
|
+
}
|
|
4547
|
+
function scheduleState(task, now = /* @__PURE__ */ new Date()) {
|
|
4548
|
+
if (task.paused)
|
|
4549
|
+
return "paused";
|
|
4550
|
+
return upcoming(task, now) ? "active" : "ended";
|
|
4551
|
+
}
|
|
4552
|
+
function lastRun(task) {
|
|
4553
|
+
return task.lastRunDate && TIME_RE.test(task.time ?? "") ? iso(task.lastRunDate, task.time) : null;
|
|
4554
|
+
}
|
|
4555
|
+
function scheduleDisplay(task, now = /* @__PURE__ */ new Date()) {
|
|
4556
|
+
try {
|
|
4557
|
+
return {
|
|
4558
|
+
nextRun: nextRun(task, now),
|
|
4559
|
+
lastRun: lastRun(task),
|
|
4560
|
+
scheduleStatus: scheduleState(task, now)
|
|
4561
|
+
};
|
|
4562
|
+
} catch {
|
|
4563
|
+
return { nextRun: null, lastRun: null, scheduleStatus: null };
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
function validateScheduleRules(task, now = /* @__PURE__ */ new Date()) {
|
|
4567
|
+
const repeating = isRepeating(task);
|
|
4568
|
+
const { date: today, minutes } = sgtParts(now);
|
|
4569
|
+
if (repeating) {
|
|
4570
|
+
if (!task.startDate)
|
|
4571
|
+
return "Please choose a start date for a repeating schedule.";
|
|
4572
|
+
if (task.repeat === "weekly" && !(task.daysOfWeek && task.daysOfWeek.length)) {
|
|
4573
|
+
return "Pick at least one weekday for a weekly repeat.";
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
const start = repeating ? task.startDate : [...task.dates ?? []].sort()[0];
|
|
4577
|
+
if (task.endDate && start && start > task.endDate) {
|
|
4578
|
+
return "The end date must be on or after the start date.";
|
|
4579
|
+
}
|
|
4580
|
+
if (task.endDate && task.endDate < today)
|
|
4581
|
+
return "The end date has already passed.";
|
|
4582
|
+
if (!repeating) {
|
|
4583
|
+
const dates = (task.dates ?? []).filter((d) => !task.endDate || d <= task.endDate);
|
|
4584
|
+
if (dates.length && dates.every((d) => d < today)) {
|
|
4585
|
+
return "All the dates you picked have already passed. Pick today or a later date.";
|
|
4586
|
+
}
|
|
4587
|
+
if (dates.length && dates.every((d) => d <= today) && dates.includes(today) && TIME_RE.test(task.time) && toMinutes(task.time) <= minutes) {
|
|
4588
|
+
return "That time has already passed today. Pick a later time or another date.";
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
if (!upcoming({ ...task, paused: false, updatedAt: now }, now)) {
|
|
4592
|
+
return "This schedule would never run. Check the dates, end date and time.";
|
|
4593
|
+
}
|
|
4594
|
+
return null;
|
|
4595
|
+
}
|
|
4596
|
+
var scheduleDate = Joi10.string().pattern(DATE_RE).custom((value, helpers) => isRealDate(value) ? value : helpers.error("date.invalid")).messages({
|
|
4597
|
+
"string.pattern.base": "Dates must look like 2026-10-21.",
|
|
4598
|
+
"string.empty": "Please choose a date.",
|
|
4599
|
+
"date.invalid": "That date does not exist. Dates must look like 2026-10-21.",
|
|
4600
|
+
"string.base": "Dates must look like 2026-10-21."
|
|
4601
|
+
});
|
|
4602
|
+
var REPEATING = Joi10.valid("daily", "weekdays", "weekly", "monthly").required();
|
|
4603
|
+
var scheduleTaskFields = {
|
|
4604
|
+
title: Joi10.string().trim().required().messages({
|
|
4605
|
+
"any.required": "Please enter a title.",
|
|
4606
|
+
"string.empty": "Please enter a title.",
|
|
4607
|
+
"string.base": "Please enter a title."
|
|
4608
|
+
}),
|
|
4609
|
+
time: Joi10.string().pattern(TIME_RE).required().messages({
|
|
4610
|
+
"any.required": "Please choose a time.",
|
|
4611
|
+
"string.empty": "Please choose a time.",
|
|
4612
|
+
"string.pattern.base": "Time must be in 24-hour format like 08:30.",
|
|
4613
|
+
"string.base": "Time must be in 24-hour format like 08:30."
|
|
4614
|
+
}),
|
|
4615
|
+
dates: Joi10.when("repeat", {
|
|
4616
|
+
is: REPEATING,
|
|
4617
|
+
then: Joi10.array().items(scheduleDate).optional().default([]),
|
|
4618
|
+
otherwise: Joi10.array().min(1).items(scheduleDate).required()
|
|
4619
|
+
}).messages({
|
|
4620
|
+
"any.required": "Pick at least one date, or choose a repeat.",
|
|
4621
|
+
"array.min": "Pick at least one date, or choose a repeat.",
|
|
4622
|
+
"array.base": "Dates must be a list of dates."
|
|
4623
|
+
}),
|
|
4624
|
+
description: Joi10.string().optional().allow("", null),
|
|
4625
|
+
areas: Joi10.array().min(1).items(
|
|
4626
|
+
Joi10.object({
|
|
4627
|
+
name: Joi10.string().required(),
|
|
4628
|
+
value: Joi10.any().required()
|
|
4629
|
+
})
|
|
4630
|
+
).required().messages({
|
|
4631
|
+
"any.required": "Pick at least one area.",
|
|
4632
|
+
"array.min": "Pick at least one area.",
|
|
4633
|
+
"array.base": "Pick at least one area."
|
|
4634
|
+
}),
|
|
4635
|
+
repeat: Joi10.string().valid(...REPEATS).optional().allow("", null).messages({ "any.only": "Repeat must be one of: none, daily, weekdays, weekly, monthly." }),
|
|
4636
|
+
daysOfWeek: Joi10.array().items(Joi10.number().integer().min(0).max(6)).unique().optional().allow(null).messages({
|
|
4637
|
+
"number.base": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
|
|
4638
|
+
"number.min": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
|
|
4639
|
+
"number.max": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
|
|
4640
|
+
"number.integer": "Weekdays must be numbers from 0 (Sunday) to 6 (Saturday).",
|
|
4641
|
+
"array.unique": "Each weekday can only be picked once."
|
|
4642
|
+
}),
|
|
4643
|
+
dayOfMonth: Joi10.number().integer().min(1).max(31).optional().allow(null).messages({ "number.base": "Day of the month must be 1 to 31.", "number.min": "Day of the month must be 1 to 31.", "number.max": "Day of the month must be 1 to 31." }),
|
|
4644
|
+
startDate: scheduleDate.optional().allow("", null),
|
|
4645
|
+
endDate: scheduleDate.optional().allow("", null),
|
|
4646
|
+
paused: Joi10.boolean().optional().allow(null).messages({ "boolean.base": "Paused must be true or false." })
|
|
4647
|
+
};
|
|
4648
|
+
var OPTIONAL_KEYS = ["repeat", "daysOfWeek", "dayOfMonth", "startDate", "endDate", "paused"];
|
|
4649
|
+
var isBlank = (v) => v === "" || v === null;
|
|
4650
|
+
function cleanScheduleInput(value) {
|
|
4651
|
+
const out = { ...value };
|
|
4652
|
+
for (const key of OPTIONAL_KEYS)
|
|
4653
|
+
if (isBlank(out[key]))
|
|
4654
|
+
delete out[key];
|
|
4655
|
+
if (out.repeat === "none") {
|
|
4656
|
+
delete out.daysOfWeek;
|
|
4657
|
+
delete out.dayOfMonth;
|
|
4658
|
+
delete out.startDate;
|
|
4659
|
+
}
|
|
4660
|
+
return out;
|
|
4661
|
+
}
|
|
4662
|
+
var TIMING_KEYS = ["time", "dates", "repeat", "daysOfWeek", "dayOfMonth", "startDate", "endDate"];
|
|
4663
|
+
function splitScheduleUpdate(payload) {
|
|
4664
|
+
const set = {};
|
|
4665
|
+
const unset = [];
|
|
4666
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
4667
|
+
if (OPTIONAL_KEYS.includes(key) && isBlank(value))
|
|
4668
|
+
unset.push(key);
|
|
4669
|
+
else if (value !== void 0)
|
|
4670
|
+
set[key] = value;
|
|
4671
|
+
}
|
|
4672
|
+
if (set.repeat === "none") {
|
|
4673
|
+
for (const key of ["daysOfWeek", "dayOfMonth", "startDate"]) {
|
|
4674
|
+
if (!unset.includes(key))
|
|
4675
|
+
unset.push(key);
|
|
4676
|
+
delete set[key];
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
return { set, unset };
|
|
4680
|
+
}
|
|
4681
|
+
function regularSetsMissing(checklist, setCount) {
|
|
4682
|
+
const regular = (checklist ?? []).filter((c) => c.isScheduleTask !== true).length;
|
|
4683
|
+
return Math.max(0, setCount - regular);
|
|
4684
|
+
}
|
|
4685
|
+
var normTiming = (t) => ({
|
|
4686
|
+
time: t.time ?? null,
|
|
4687
|
+
dates: [...Array.isArray(t.dates) ? t.dates : []].sort(),
|
|
4688
|
+
repeat: !t.repeat || t.repeat === "none" ? "none" : t.repeat,
|
|
4689
|
+
daysOfWeek: [...Array.isArray(t.daysOfWeek) ? t.daysOfWeek : []].sort(),
|
|
4690
|
+
dayOfMonth: t.dayOfMonth ?? null,
|
|
4691
|
+
startDate: t.startDate || null,
|
|
4692
|
+
endDate: t.endDate || null
|
|
4693
|
+
});
|
|
4694
|
+
function timingChanged(stored, after) {
|
|
4695
|
+
return JSON.stringify(normTiming(stored)) !== JSON.stringify(normTiming(after));
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4434
4698
|
// src/services/hygiene-area-checklist.service.ts
|
|
4435
4699
|
function useAreaChecklistService() {
|
|
4436
4700
|
const { getAreasForChecklist } = useAreaRepo();
|
|
@@ -4498,10 +4762,10 @@ function useAreaChecklistService() {
|
|
|
4498
4762
|
`Area checklist for area ${area.name} exists but has no auto-gen sets. Inserting auto-gen sets.`
|
|
4499
4763
|
);
|
|
4500
4764
|
const setCount2 = Number(area.set) || 1;
|
|
4501
|
-
const
|
|
4502
|
-
if (
|
|
4765
|
+
const setsToAdd = regularSetsMissing(existing.checklist, setCount2);
|
|
4766
|
+
if (setsToAdd <= 0) {
|
|
4503
4767
|
logger18.info(
|
|
4504
|
-
`Area checklist for area ${area.name} already has ${
|
|
4768
|
+
`Area checklist for area ${area.name} already has its ${setCount2} regular set(s). Skipping auto-gen sets.`
|
|
4505
4769
|
);
|
|
4506
4770
|
return existing._id;
|
|
4507
4771
|
}
|
|
@@ -4509,7 +4773,6 @@ function useAreaChecklistService() {
|
|
|
4509
4773
|
(max, c) => Math.max(max, c.set ?? 0),
|
|
4510
4774
|
0
|
|
4511
4775
|
) ?? 0;
|
|
4512
|
-
const setsToAdd = setCount2 - totalExistingSets;
|
|
4513
4776
|
const autoGenSets = Array.from(
|
|
4514
4777
|
{ length: setsToAdd },
|
|
4515
4778
|
(_, index) => ({
|
|
@@ -4670,7 +4933,7 @@ function useAreaChecklistService() {
|
|
|
4670
4933
|
}
|
|
4671
4934
|
|
|
4672
4935
|
// src/controllers/hygiene-area-checklist.controller.ts
|
|
4673
|
-
import
|
|
4936
|
+
import Joi11 from "joi";
|
|
4674
4937
|
import {
|
|
4675
4938
|
BadRequestError as BadRequestError16,
|
|
4676
4939
|
InternalServerError as InternalServerError7,
|
|
@@ -4915,9 +5178,9 @@ function useAreaChecklistController() {
|
|
|
4915
5178
|
site: req.params.site,
|
|
4916
5179
|
schedule: req.params.schedule
|
|
4917
5180
|
};
|
|
4918
|
-
const validation =
|
|
4919
|
-
site:
|
|
4920
|
-
schedule:
|
|
5181
|
+
const validation = Joi11.object({
|
|
5182
|
+
site: Joi11.string().hex().required(),
|
|
5183
|
+
schedule: Joi11.string().hex().required()
|
|
4921
5184
|
});
|
|
4922
5185
|
const { error, value } = validation.validate(payload);
|
|
4923
5186
|
if (error) {
|
|
@@ -4937,14 +5200,14 @@ function useAreaChecklistController() {
|
|
|
4937
5200
|
}
|
|
4938
5201
|
async function getAllAreaChecklist(req, res, next) {
|
|
4939
5202
|
const query = { ...req.query, ...req.params };
|
|
4940
|
-
const validation =
|
|
4941
|
-
page:
|
|
4942
|
-
limit:
|
|
4943
|
-
search:
|
|
4944
|
-
type:
|
|
4945
|
-
status:
|
|
4946
|
-
schedule:
|
|
4947
|
-
serviceType:
|
|
5203
|
+
const validation = Joi11.object({
|
|
5204
|
+
page: Joi11.number().min(1).optional().allow("", null),
|
|
5205
|
+
limit: Joi11.number().min(1).optional().allow("", null),
|
|
5206
|
+
search: Joi11.string().optional().allow("", null),
|
|
5207
|
+
type: Joi11.string().valid(...allowedTypes, "all").optional().allow("", null),
|
|
5208
|
+
status: Joi11.string().valid(...allowedStatus, "all").optional().allow("", null),
|
|
5209
|
+
schedule: Joi11.string().hex().required(),
|
|
5210
|
+
serviceType: Joi11.string().valid(...Object.values(AppServiceType10)).required()
|
|
4948
5211
|
});
|
|
4949
5212
|
const { error } = validation.validate(query);
|
|
4950
5213
|
if (error) {
|
|
@@ -4979,14 +5242,14 @@ function useAreaChecklistController() {
|
|
|
4979
5242
|
}
|
|
4980
5243
|
async function getAreaChecklistHistory(req, res, next) {
|
|
4981
5244
|
const query = { ...req.query, ...req.params };
|
|
4982
|
-
const validation =
|
|
4983
|
-
page:
|
|
4984
|
-
limit:
|
|
4985
|
-
search:
|
|
4986
|
-
type:
|
|
4987
|
-
schedule:
|
|
4988
|
-
status:
|
|
4989
|
-
createdAt:
|
|
5245
|
+
const validation = Joi11.object({
|
|
5246
|
+
page: Joi11.number().min(1).optional().allow("", null),
|
|
5247
|
+
limit: Joi11.number().min(1).optional().allow("", null),
|
|
5248
|
+
search: Joi11.string().optional().allow("", null),
|
|
5249
|
+
type: Joi11.string().optional().allow("", ...allowedTypes),
|
|
5250
|
+
schedule: Joi11.string().hex().required(),
|
|
5251
|
+
status: Joi11.string().allow("", null, ...allowedStatus),
|
|
5252
|
+
createdAt: Joi11.alternatives().try(Joi11.date(), Joi11.string()).optional().allow("", null)
|
|
4990
5253
|
});
|
|
4991
5254
|
const { error } = validation.validate(query);
|
|
4992
5255
|
if (error) {
|
|
@@ -5020,7 +5283,7 @@ function useAreaChecklistController() {
|
|
|
5020
5283
|
}
|
|
5021
5284
|
}
|
|
5022
5285
|
async function getAreaChecklistHistoryDetails(req, res, next) {
|
|
5023
|
-
const validation =
|
|
5286
|
+
const validation = Joi11.string().hex().required();
|
|
5024
5287
|
const _id = req.params.id;
|
|
5025
5288
|
const { error, value } = validation.validate(_id);
|
|
5026
5289
|
if (error) {
|
|
@@ -5040,12 +5303,12 @@ function useAreaChecklistController() {
|
|
|
5040
5303
|
}
|
|
5041
5304
|
async function getAreaChecklistUnits(req, res, next) {
|
|
5042
5305
|
const query = { ...req.query, ...req.params };
|
|
5043
|
-
const validation =
|
|
5044
|
-
page:
|
|
5045
|
-
limit:
|
|
5046
|
-
search:
|
|
5047
|
-
serviceType:
|
|
5048
|
-
id:
|
|
5306
|
+
const validation = Joi11.object({
|
|
5307
|
+
page: Joi11.number().min(1).optional().allow("", null),
|
|
5308
|
+
limit: Joi11.number().min(1).optional().allow("", null),
|
|
5309
|
+
search: Joi11.string().optional().allow("", null),
|
|
5310
|
+
serviceType: Joi11.string().valid(...Object.values(AppServiceType10)).required(),
|
|
5311
|
+
id: Joi11.string().hex().required()
|
|
5049
5312
|
});
|
|
5050
5313
|
const { error } = validation.validate(query);
|
|
5051
5314
|
if (error) {
|
|
@@ -5095,21 +5358,21 @@ function useAreaChecklistController() {
|
|
|
5095
5358
|
...decisionValues,
|
|
5096
5359
|
completedBy
|
|
5097
5360
|
};
|
|
5098
|
-
const validation =
|
|
5099
|
-
id:
|
|
5100
|
-
set:
|
|
5101
|
-
unit:
|
|
5102
|
-
decision:
|
|
5103
|
-
approve:
|
|
5104
|
-
reject:
|
|
5105
|
-
remarks:
|
|
5361
|
+
const validation = Joi11.object({
|
|
5362
|
+
id: Joi11.string().hex().required(),
|
|
5363
|
+
set: Joi11.number().integer().min(1).required(),
|
|
5364
|
+
unit: Joi11.string().hex().required(),
|
|
5365
|
+
decision: Joi11.string().valid("approve", "reject").required(),
|
|
5366
|
+
approve: Joi11.boolean().optional(),
|
|
5367
|
+
reject: Joi11.boolean().optional(),
|
|
5368
|
+
remarks: Joi11.string().optional().allow("", null),
|
|
5106
5369
|
/*
|
|
5107
5370
|
* Shape only. WHETHER a photo is required depends on the set's current
|
|
5108
5371
|
* state in the database, which Joi cannot see — see the check below.
|
|
5109
5372
|
* Each entry must still be a real id: a blank string carries no evidence.
|
|
5110
5373
|
*/
|
|
5111
|
-
attachment:
|
|
5112
|
-
completedBy:
|
|
5374
|
+
attachment: Joi11.array().items(Joi11.string().trim().min(1)).optional(),
|
|
5375
|
+
completedBy: Joi11.string().hex().required()
|
|
5113
5376
|
});
|
|
5114
5377
|
const { error } = validation.validate(payload);
|
|
5115
5378
|
if (error) {
|
|
@@ -5146,7 +5409,7 @@ function useAreaChecklistController() {
|
|
|
5146
5409
|
}
|
|
5147
5410
|
}
|
|
5148
5411
|
async function downloadChecklistPdf(req, res, next) {
|
|
5149
|
-
const validation =
|
|
5412
|
+
const validation = Joi11.string().hex().required();
|
|
5150
5413
|
const schedule = req.params.schedule;
|
|
5151
5414
|
const { error, value } = validation.validate(schedule);
|
|
5152
5415
|
if (error) {
|
|
@@ -5192,16 +5455,16 @@ function useAreaChecklistController() {
|
|
|
5192
5455
|
}
|
|
5193
5456
|
|
|
5194
5457
|
// src/models/hygiene-supply.model.ts
|
|
5195
|
-
import
|
|
5458
|
+
import Joi12 from "joi";
|
|
5196
5459
|
import { ObjectId as ObjectId12 } from "mongodb";
|
|
5197
5460
|
import { BadRequestError as BadRequestError17, logger as logger21 } from "@7365admin1/node-server-utils";
|
|
5198
5461
|
import { AppServiceType as AppServiceType11 } from "@7365admin1/core";
|
|
5199
|
-
var supplySchema =
|
|
5200
|
-
site:
|
|
5201
|
-
serviceType:
|
|
5202
|
-
name:
|
|
5203
|
-
unitOfMeasurement:
|
|
5204
|
-
attachment:
|
|
5462
|
+
var supplySchema = Joi12.object({
|
|
5463
|
+
site: Joi12.string().hex().required(),
|
|
5464
|
+
serviceType: Joi12.string().valid(...Object.values(AppServiceType11)).required(),
|
|
5465
|
+
name: Joi12.string().required(),
|
|
5466
|
+
unitOfMeasurement: Joi12.string().required(),
|
|
5467
|
+
attachment: Joi12.string().allow(null, "").optional()
|
|
5205
5468
|
});
|
|
5206
5469
|
function MSupply(value) {
|
|
5207
5470
|
const { error } = supplySchema.validate(value);
|
|
@@ -5338,7 +5601,7 @@ function useSupplyRepository() {
|
|
|
5338
5601
|
throw new BadRequestError18("Invalid site ID format.");
|
|
5339
5602
|
}
|
|
5340
5603
|
if (search) {
|
|
5341
|
-
query.name = { $regex: search, $options: "i" };
|
|
5604
|
+
query.name = { $regex: escapeRegex(search), $options: "i" };
|
|
5342
5605
|
cacheOptions.search = search;
|
|
5343
5606
|
}
|
|
5344
5607
|
const cacheKey = makeCacheKey6(namespace_collection, cacheOptions);
|
|
@@ -5500,7 +5763,7 @@ function useSupplyRepository() {
|
|
|
5500
5763
|
}
|
|
5501
5764
|
|
|
5502
5765
|
// src/controllers/hygiene-supply.controller.ts
|
|
5503
|
-
import
|
|
5766
|
+
import Joi13 from "joi";
|
|
5504
5767
|
import { BadRequestError as BadRequestError19, logger as logger23 } from "@7365admin1/node-server-utils";
|
|
5505
5768
|
import { AppServiceType as AppServiceType12 } from "@7365admin1/core";
|
|
5506
5769
|
function useSupplyController() {
|
|
@@ -5531,12 +5794,12 @@ function useSupplyController() {
|
|
|
5531
5794
|
}
|
|
5532
5795
|
async function getSupplies(req, res, next) {
|
|
5533
5796
|
const query = { ...req.query, ...req.params };
|
|
5534
|
-
const validation =
|
|
5535
|
-
page:
|
|
5536
|
-
limit:
|
|
5537
|
-
search:
|
|
5538
|
-
site:
|
|
5539
|
-
serviceType:
|
|
5797
|
+
const validation = Joi13.object({
|
|
5798
|
+
page: Joi13.number().min(1).optional().allow("", null),
|
|
5799
|
+
limit: Joi13.number().min(1).optional().allow("", null),
|
|
5800
|
+
search: Joi13.string().optional().allow("", null),
|
|
5801
|
+
site: Joi13.string().hex().required(),
|
|
5802
|
+
serviceType: Joi13.string().valid(...Object.values(AppServiceType12)).required()
|
|
5540
5803
|
});
|
|
5541
5804
|
const { error } = validation.validate(query);
|
|
5542
5805
|
if (error) {
|
|
@@ -5566,7 +5829,7 @@ function useSupplyController() {
|
|
|
5566
5829
|
}
|
|
5567
5830
|
}
|
|
5568
5831
|
async function getSupplyById(req, res, next) {
|
|
5569
|
-
const validation =
|
|
5832
|
+
const validation = Joi13.string().hex().required();
|
|
5570
5833
|
const _id = req.params.id;
|
|
5571
5834
|
const { error, value } = validation.validate(_id);
|
|
5572
5835
|
if (error) {
|
|
@@ -5586,12 +5849,12 @@ function useSupplyController() {
|
|
|
5586
5849
|
}
|
|
5587
5850
|
async function updateSupply(req, res, next) {
|
|
5588
5851
|
const payload = { id: req.params.id, ...req.body };
|
|
5589
|
-
const validation =
|
|
5590
|
-
id:
|
|
5591
|
-
name:
|
|
5592
|
-
unitOfMeasurement:
|
|
5593
|
-
qty:
|
|
5594
|
-
attachment:
|
|
5852
|
+
const validation = Joi13.object({
|
|
5853
|
+
id: Joi13.string().hex().required(),
|
|
5854
|
+
name: Joi13.string().optional().allow("", null),
|
|
5855
|
+
unitOfMeasurement: Joi13.string().optional().allow("", null),
|
|
5856
|
+
qty: Joi13.number().min(0).optional().allow("", null),
|
|
5857
|
+
attachment: Joi13.string().optional().allow("", null)
|
|
5595
5858
|
});
|
|
5596
5859
|
const { error } = validation.validate(payload);
|
|
5597
5860
|
if (error) {
|
|
@@ -5612,7 +5875,7 @@ function useSupplyController() {
|
|
|
5612
5875
|
}
|
|
5613
5876
|
async function deleteSupply(req, res, next) {
|
|
5614
5877
|
const id = req.params.id;
|
|
5615
|
-
const validation =
|
|
5878
|
+
const validation = Joi13.string().hex().required();
|
|
5616
5879
|
const { error, value } = validation.validate(id);
|
|
5617
5880
|
if (error) {
|
|
5618
5881
|
logger23.log({ level: "error", message: error.message });
|
|
@@ -5639,18 +5902,18 @@ function useSupplyController() {
|
|
|
5639
5902
|
}
|
|
5640
5903
|
|
|
5641
5904
|
// src/models/hygiene-stock.model.ts
|
|
5642
|
-
import
|
|
5905
|
+
import Joi14 from "joi";
|
|
5643
5906
|
import { ObjectId as ObjectId14 } from "mongodb";
|
|
5644
5907
|
import { BadRequestError as BadRequestError20, logger as logger24 } from "@7365admin1/node-server-utils";
|
|
5645
5908
|
import { AppServiceType as AppServiceType13 } from "@7365admin1/core";
|
|
5646
|
-
var stockSchema =
|
|
5647
|
-
site:
|
|
5648
|
-
serviceType:
|
|
5649
|
-
supply:
|
|
5650
|
-
in:
|
|
5651
|
-
out:
|
|
5652
|
-
balance:
|
|
5653
|
-
remarks:
|
|
5909
|
+
var stockSchema = Joi14.object({
|
|
5910
|
+
site: Joi14.string().hex().required(),
|
|
5911
|
+
serviceType: Joi14.string().valid(...Object.values(AppServiceType13)).required(),
|
|
5912
|
+
supply: Joi14.string().hex().required(),
|
|
5913
|
+
in: Joi14.number().min(0).optional(),
|
|
5914
|
+
out: Joi14.number().min(0).optional(),
|
|
5915
|
+
balance: Joi14.number().min(0).required(),
|
|
5916
|
+
remarks: Joi14.string().optional().allow("", null)
|
|
5654
5917
|
});
|
|
5655
5918
|
function MStock(value) {
|
|
5656
5919
|
const { error } = stockSchema.validate(value);
|
|
@@ -5877,7 +6140,7 @@ function useStockService() {
|
|
|
5877
6140
|
}
|
|
5878
6141
|
|
|
5879
6142
|
// src/controllers/hygiene-stock.controller.ts
|
|
5880
|
-
import
|
|
6143
|
+
import Joi15 from "joi";
|
|
5881
6144
|
import { BadRequestError as BadRequestError23, logger as logger26 } from "@7365admin1/node-server-utils";
|
|
5882
6145
|
import { AppServiceType as AppServiceType14 } from "@7365admin1/core";
|
|
5883
6146
|
function useStockController() {
|
|
@@ -5885,12 +6148,12 @@ function useStockController() {
|
|
|
5885
6148
|
const { createStock: _createStock } = useStockService();
|
|
5886
6149
|
async function createStock(req, res, next) {
|
|
5887
6150
|
const payload = { ...req.body, ...req.params };
|
|
5888
|
-
const validation =
|
|
5889
|
-
site:
|
|
5890
|
-
serviceType:
|
|
5891
|
-
supply:
|
|
5892
|
-
qty:
|
|
5893
|
-
remarks:
|
|
6151
|
+
const validation = Joi15.object({
|
|
6152
|
+
site: Joi15.string().hex().required(),
|
|
6153
|
+
serviceType: Joi15.string().valid(...Object.values(AppServiceType14)).required(),
|
|
6154
|
+
supply: Joi15.string().hex().required(),
|
|
6155
|
+
qty: Joi15.number().min(0).required(),
|
|
6156
|
+
remarks: Joi15.string().optional().allow("", null)
|
|
5894
6157
|
});
|
|
5895
6158
|
const { error } = validation.validate(payload);
|
|
5896
6159
|
if (error) {
|
|
@@ -5910,13 +6173,13 @@ function useStockController() {
|
|
|
5910
6173
|
}
|
|
5911
6174
|
async function getStocksBySupplyId(req, res, next) {
|
|
5912
6175
|
const query = { ...req.query, ...req.params };
|
|
5913
|
-
const validation =
|
|
5914
|
-
page:
|
|
5915
|
-
limit:
|
|
5916
|
-
search:
|
|
5917
|
-
site:
|
|
5918
|
-
serviceType:
|
|
5919
|
-
supply:
|
|
6176
|
+
const validation = Joi15.object({
|
|
6177
|
+
page: Joi15.number().min(1).optional().allow("", null),
|
|
6178
|
+
limit: Joi15.number().min(1).optional().allow("", null),
|
|
6179
|
+
search: Joi15.string().optional().allow("", null),
|
|
6180
|
+
site: Joi15.string().hex().required(),
|
|
6181
|
+
serviceType: Joi15.string().valid(...Object.values(AppServiceType14)).required(),
|
|
6182
|
+
supply: Joi15.string().hex().required()
|
|
5920
6183
|
});
|
|
5921
6184
|
const { error } = validation.validate(query);
|
|
5922
6185
|
if (error) {
|
|
@@ -5978,21 +6241,21 @@ async function applyCheckOutDecision(decision, decide, returnStock) {
|
|
|
5978
6241
|
}
|
|
5979
6242
|
|
|
5980
6243
|
// src/models/hygiene-checkout-item.model.ts
|
|
5981
|
-
import
|
|
6244
|
+
import Joi16 from "joi";
|
|
5982
6245
|
import { ObjectId as ObjectId16 } from "mongodb";
|
|
5983
6246
|
import { BadRequestError as BadRequestError24, logger as logger27 } from "@7365admin1/node-server-utils";
|
|
5984
6247
|
import { AppServiceType as AppServiceType15 } from "@7365admin1/core";
|
|
5985
6248
|
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:
|
|
6249
|
+
var checkOutItemSchema = Joi16.object({
|
|
6250
|
+
site: Joi16.string().hex().required(),
|
|
6251
|
+
serviceType: Joi16.string().valid(...Object.values(AppServiceType15)).required(),
|
|
6252
|
+
supply: Joi16.string().hex().required(),
|
|
6253
|
+
supplyName: Joi16.string().required(),
|
|
6254
|
+
supplyQty: Joi16.number().min(0).required(),
|
|
6255
|
+
qty: Joi16.number().min(0).required(),
|
|
6256
|
+
attachment: Joi16.array().items(Joi16.string()).optional().allow(null),
|
|
6257
|
+
createdBy: Joi16.string().hex().required(),
|
|
6258
|
+
createdByName: Joi16.string().required()
|
|
5996
6259
|
});
|
|
5997
6260
|
function MCheckOutItem(value) {
|
|
5998
6261
|
const { error } = checkOutItemSchema.validate(value);
|
|
@@ -6125,7 +6388,7 @@ function useCheckOutItemRepository() {
|
|
|
6125
6388
|
throw new BadRequestError25("Invalid site ID format.");
|
|
6126
6389
|
}
|
|
6127
6390
|
if (search) {
|
|
6128
|
-
query.supplyName = { $regex: search, $options: "i" };
|
|
6391
|
+
query.supplyName = { $regex: escapeRegex(search), $options: "i" };
|
|
6129
6392
|
cacheOptions.search = search;
|
|
6130
6393
|
}
|
|
6131
6394
|
const cacheKey = makeCacheKey8(namespace_collection, cacheOptions);
|
|
@@ -6481,7 +6744,7 @@ function useCheckOutItemService() {
|
|
|
6481
6744
|
}
|
|
6482
6745
|
|
|
6483
6746
|
// src/controllers/hygiene-checkout-item.controller.ts
|
|
6484
|
-
import
|
|
6747
|
+
import Joi17 from "joi";
|
|
6485
6748
|
import { BadRequestError as BadRequestError27, logger as logger29 } from "@7365admin1/node-server-utils";
|
|
6486
6749
|
import { AppServiceType as AppServiceType16 } from "@7365admin1/core";
|
|
6487
6750
|
function useCheckOutItemController() {
|
|
@@ -6505,13 +6768,13 @@ function useCheckOutItemController() {
|
|
|
6505
6768
|
...req.params,
|
|
6506
6769
|
createdBy
|
|
6507
6770
|
};
|
|
6508
|
-
const validation =
|
|
6509
|
-
site:
|
|
6510
|
-
serviceType:
|
|
6511
|
-
supply:
|
|
6512
|
-
qty:
|
|
6513
|
-
attachment:
|
|
6514
|
-
createdBy:
|
|
6771
|
+
const validation = Joi17.object({
|
|
6772
|
+
site: Joi17.string().hex().required(),
|
|
6773
|
+
serviceType: Joi17.string().valid(...Object.values(AppServiceType16)).required(),
|
|
6774
|
+
supply: Joi17.string().hex().required(),
|
|
6775
|
+
qty: Joi17.number().min(0).required(),
|
|
6776
|
+
attachment: Joi17.array().items(Joi17.string()).optional().allow(null),
|
|
6777
|
+
createdBy: Joi17.string().hex().required()
|
|
6515
6778
|
});
|
|
6516
6779
|
const { error } = validation.validate(payload);
|
|
6517
6780
|
if (error) {
|
|
@@ -6540,15 +6803,15 @@ function useCheckOutItemController() {
|
|
|
6540
6803
|
...req.params,
|
|
6541
6804
|
createdBy
|
|
6542
6805
|
};
|
|
6543
|
-
const validation =
|
|
6544
|
-
site:
|
|
6545
|
-
serviceType:
|
|
6546
|
-
createdBy:
|
|
6547
|
-
items:
|
|
6548
|
-
|
|
6549
|
-
supply:
|
|
6550
|
-
qty:
|
|
6551
|
-
attachment:
|
|
6806
|
+
const validation = Joi17.object({
|
|
6807
|
+
site: Joi17.string().hex().required(),
|
|
6808
|
+
serviceType: Joi17.string().valid(...Object.values(AppServiceType16)).required(),
|
|
6809
|
+
createdBy: Joi17.string().hex().required(),
|
|
6810
|
+
items: Joi17.array().items(
|
|
6811
|
+
Joi17.object({
|
|
6812
|
+
supply: Joi17.string().hex().required(),
|
|
6813
|
+
qty: Joi17.number().min(0).required(),
|
|
6814
|
+
attachment: Joi17.array().items(Joi17.string()).optional().allow(null)
|
|
6552
6815
|
})
|
|
6553
6816
|
).min(1).required()
|
|
6554
6817
|
});
|
|
@@ -6570,12 +6833,12 @@ function useCheckOutItemController() {
|
|
|
6570
6833
|
}
|
|
6571
6834
|
async function getCheckOutItems(req, res, next) {
|
|
6572
6835
|
const query = { ...req.query, ...req.params };
|
|
6573
|
-
const validation =
|
|
6574
|
-
page:
|
|
6575
|
-
limit:
|
|
6576
|
-
search:
|
|
6577
|
-
site:
|
|
6578
|
-
serviceType:
|
|
6836
|
+
const validation = Joi17.object({
|
|
6837
|
+
page: Joi17.number().min(1).optional().allow("", null),
|
|
6838
|
+
limit: Joi17.number().min(1).optional().allow("", null),
|
|
6839
|
+
search: Joi17.string().optional().allow("", null),
|
|
6840
|
+
site: Joi17.string().hex().required(),
|
|
6841
|
+
serviceType: Joi17.string().valid(...Object.values(AppServiceType16)).required()
|
|
6579
6842
|
});
|
|
6580
6843
|
const { error } = validation.validate(query);
|
|
6581
6844
|
if (error) {
|
|
@@ -6605,7 +6868,7 @@ function useCheckOutItemController() {
|
|
|
6605
6868
|
}
|
|
6606
6869
|
}
|
|
6607
6870
|
async function getCheckOutItemById(req, res, next) {
|
|
6608
|
-
const validation =
|
|
6871
|
+
const validation = Joi17.string().hex().required();
|
|
6609
6872
|
const _id = req.params.id;
|
|
6610
6873
|
const { error, value } = validation.validate(_id);
|
|
6611
6874
|
if (error) {
|
|
@@ -6644,13 +6907,13 @@ function useCheckOutItemController() {
|
|
|
6644
6907
|
...decisionValues,
|
|
6645
6908
|
completedBy
|
|
6646
6909
|
};
|
|
6647
|
-
const validation =
|
|
6648
|
-
id:
|
|
6649
|
-
decision:
|
|
6650
|
-
approve:
|
|
6651
|
-
reject:
|
|
6652
|
-
remarks:
|
|
6653
|
-
completedBy:
|
|
6910
|
+
const validation = Joi17.object({
|
|
6911
|
+
id: Joi17.string().hex().required(),
|
|
6912
|
+
decision: Joi17.string().valid("approve", "disapprove").required(),
|
|
6913
|
+
approve: Joi17.boolean().required(),
|
|
6914
|
+
reject: Joi17.boolean().required(),
|
|
6915
|
+
remarks: Joi17.string().optional().allow("", null),
|
|
6916
|
+
completedBy: Joi17.string().hex().required()
|
|
6654
6917
|
});
|
|
6655
6918
|
const { error } = validation.validate(payload);
|
|
6656
6919
|
if (error) {
|
|
@@ -6683,23 +6946,15 @@ function useCheckOutItemController() {
|
|
|
6683
6946
|
// src/models/hygiene-schedule-task.model.ts
|
|
6684
6947
|
import { BadRequestError as BadRequestError28, logger as logger30 } from "@7365admin1/node-server-utils";
|
|
6685
6948
|
import { AppServiceType as AppServiceType17 } from "@7365admin1/core";
|
|
6686
|
-
import
|
|
6949
|
+
import Joi18 from "joi";
|
|
6687
6950
|
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()
|
|
6951
|
+
var scheduleTaskSchema = Joi18.object({
|
|
6952
|
+
site: Joi18.string().hex().required(),
|
|
6953
|
+
serviceType: Joi18.string().valid(...Object.values(AppServiceType17)).required(),
|
|
6954
|
+
...scheduleTaskFields
|
|
6955
|
+
}).custom((value, helpers) => {
|
|
6956
|
+
const message = validateScheduleRules(cleanScheduleInput(value));
|
|
6957
|
+
return message ? helpers.message({ custom: message }) : value;
|
|
6703
6958
|
});
|
|
6704
6959
|
function MScheduleTask(value) {
|
|
6705
6960
|
const { error } = scheduleTaskSchema.validate(value);
|
|
@@ -6726,14 +6981,26 @@ function MScheduleTask(value) {
|
|
|
6726
6981
|
}
|
|
6727
6982
|
});
|
|
6728
6983
|
}
|
|
6984
|
+
const extra = cleanScheduleInput({
|
|
6985
|
+
repeat: value.repeat,
|
|
6986
|
+
daysOfWeek: value.daysOfWeek,
|
|
6987
|
+
dayOfMonth: value.dayOfMonth,
|
|
6988
|
+
startDate: value.startDate,
|
|
6989
|
+
endDate: value.endDate,
|
|
6990
|
+
paused: value.paused
|
|
6991
|
+
});
|
|
6992
|
+
for (const key of Object.keys(extra))
|
|
6993
|
+
if (extra[key] === void 0)
|
|
6994
|
+
delete extra[key];
|
|
6729
6995
|
return {
|
|
6730
6996
|
site: value.site,
|
|
6731
6997
|
serviceType: value.serviceType,
|
|
6732
6998
|
title: value.title,
|
|
6733
6999
|
time: value.time,
|
|
6734
|
-
dates: value.dates,
|
|
7000
|
+
dates: value.dates ?? [],
|
|
6735
7001
|
description: value.description,
|
|
6736
7002
|
areas: value.areas,
|
|
7003
|
+
...extra,
|
|
6737
7004
|
status: "active",
|
|
6738
7005
|
createdAt: /* @__PURE__ */ new Date(),
|
|
6739
7006
|
updatedAt: "",
|
|
@@ -6753,11 +7020,23 @@ import {
|
|
|
6753
7020
|
makeCacheKey as makeCacheKey9,
|
|
6754
7021
|
NotFoundError as NotFoundError7
|
|
6755
7022
|
} from "@7365admin1/node-server-utils";
|
|
7023
|
+
var SCHEDULE_FIELDS = {
|
|
7024
|
+
time: 1,
|
|
7025
|
+
dates: 1,
|
|
7026
|
+
repeat: 1,
|
|
7027
|
+
daysOfWeek: 1,
|
|
7028
|
+
dayOfMonth: 1,
|
|
7029
|
+
startDate: 1,
|
|
7030
|
+
endDate: 1,
|
|
7031
|
+
paused: 1,
|
|
7032
|
+
lastRunDate: 1
|
|
7033
|
+
};
|
|
6756
7034
|
function useScheduleTaskRepository() {
|
|
6757
7035
|
const db = useAtlas14.getDb();
|
|
6758
7036
|
if (!db) {
|
|
6759
7037
|
throw new InternalServerError11("Unable to connect to server.");
|
|
6760
7038
|
}
|
|
7039
|
+
const database = db;
|
|
6761
7040
|
const namespace_collection = "site.schedule-tasks";
|
|
6762
7041
|
const collection = db.collection(namespace_collection);
|
|
6763
7042
|
const { delNamespace, setCache, getCache } = useCache9(namespace_collection);
|
|
@@ -6825,14 +7104,14 @@ function useScheduleTaskRepository() {
|
|
|
6825
7104
|
throw new BadRequestError29("Invalid site ID format.");
|
|
6826
7105
|
}
|
|
6827
7106
|
if (search) {
|
|
6828
|
-
query.$or = [{ title: { $regex: search, $options: "i" } }];
|
|
7107
|
+
query.$or = [{ title: { $regex: escapeRegex(search), $options: "i" } }];
|
|
6829
7108
|
cacheOptions.search = search;
|
|
6830
7109
|
}
|
|
6831
7110
|
const cacheKey = makeCacheKey9(namespace_collection, cacheOptions);
|
|
6832
7111
|
const cachedData = await getCache(cacheKey);
|
|
6833
7112
|
if (cachedData) {
|
|
6834
7113
|
logger31.info(`Cache hit for key: ${cacheKey}`);
|
|
6835
|
-
return cachedData;
|
|
7114
|
+
return enrichList(cachedData, site, serviceType);
|
|
6836
7115
|
}
|
|
6837
7116
|
try {
|
|
6838
7117
|
const items = await collection.aggregate([
|
|
@@ -6841,7 +7120,8 @@ function useScheduleTaskRepository() {
|
|
|
6841
7120
|
$project: {
|
|
6842
7121
|
title: 1,
|
|
6843
7122
|
areas: 1,
|
|
6844
|
-
status: 1
|
|
7123
|
+
status: 1,
|
|
7124
|
+
...SCHEDULE_FIELDS
|
|
6845
7125
|
}
|
|
6846
7126
|
},
|
|
6847
7127
|
{ $sort: { _id: -1 } },
|
|
@@ -6855,15 +7135,104 @@ function useScheduleTaskRepository() {
|
|
|
6855
7135
|
}).catch((err) => {
|
|
6856
7136
|
logger31.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
6857
7137
|
});
|
|
6858
|
-
return data;
|
|
7138
|
+
return enrichList(data, site, serviceType);
|
|
6859
7139
|
} catch (error) {
|
|
6860
7140
|
throw error;
|
|
6861
7141
|
}
|
|
6862
7142
|
}
|
|
6863
|
-
async function
|
|
7143
|
+
async function enrichList(data, site, serviceType) {
|
|
7144
|
+
const items = data?.items ?? [];
|
|
7145
|
+
const now = /* @__PURE__ */ new Date();
|
|
7146
|
+
let statuses = /* @__PURE__ */ new Map();
|
|
7147
|
+
try {
|
|
7148
|
+
statuses = await getRunStatuses(items, site, serviceType);
|
|
7149
|
+
} catch (error) {
|
|
7150
|
+
logger31.error("Could not read schedule task run status", error);
|
|
7151
|
+
}
|
|
7152
|
+
return {
|
|
7153
|
+
...data,
|
|
7154
|
+
items: items.map((item) => {
|
|
7155
|
+
try {
|
|
7156
|
+
return {
|
|
7157
|
+
...item,
|
|
7158
|
+
...scheduleDisplay(item, now),
|
|
7159
|
+
lastRunStatus: statuses.get(String(item._id)) ?? null
|
|
7160
|
+
};
|
|
7161
|
+
} catch (error) {
|
|
7162
|
+
logger31.error(`Could not enrich schedule task ${item?._id}`, error);
|
|
7163
|
+
return { ...item, nextRun: null, lastRun: null, scheduleStatus: null, lastRunStatus: null };
|
|
7164
|
+
}
|
|
7165
|
+
})
|
|
7166
|
+
};
|
|
7167
|
+
}
|
|
7168
|
+
async function getRunStatuses(items, site, serviceType) {
|
|
7169
|
+
const result = /* @__PURE__ */ new Map();
|
|
7170
|
+
const ran = items.filter((i) => i.lastRunDate);
|
|
7171
|
+
if (!ran.length)
|
|
7172
|
+
return result;
|
|
7173
|
+
const parents = await database.collection("site.cleaning.schedules").find(
|
|
7174
|
+
{
|
|
7175
|
+
site,
|
|
7176
|
+
serviceType,
|
|
7177
|
+
dateStr: { $in: [...new Set(ran.map((i) => i.lastRunDate))] }
|
|
7178
|
+
},
|
|
7179
|
+
{ projection: { dateStr: 1 } }
|
|
7180
|
+
).toArray();
|
|
7181
|
+
if (!parents.length)
|
|
7182
|
+
return result;
|
|
7183
|
+
const dateOf = new Map(parents.map((p) => [String(p._id), p.dateStr]));
|
|
7184
|
+
const taskIds = ran.map((i) => new ObjectId19(i._id));
|
|
7185
|
+
const rows = await database.collection("site.cleaning.schedule.areas").aggregate([
|
|
7186
|
+
{
|
|
7187
|
+
$match: {
|
|
7188
|
+
schedule: { $in: parents.map((p) => p._id) },
|
|
7189
|
+
"checklist.scheduleTaskId": { $in: taskIds }
|
|
7190
|
+
}
|
|
7191
|
+
},
|
|
7192
|
+
{ $unwind: "$checklist" },
|
|
7193
|
+
{ $match: { "checklist.scheduleTaskId": { $in: taskIds } } },
|
|
7194
|
+
{ $unwind: "$checklist.units" },
|
|
7195
|
+
{
|
|
7196
|
+
$group: {
|
|
7197
|
+
_id: { task: "$checklist.scheduleTaskId", schedule: "$schedule" },
|
|
7198
|
+
total: { $sum: 1 },
|
|
7199
|
+
completed: {
|
|
7200
|
+
$sum: { $cond: [{ $eq: ["$checklist.units.status", "completed"] }, 1, 0] }
|
|
7201
|
+
},
|
|
7202
|
+
closed: {
|
|
7203
|
+
$sum: { $cond: [{ $eq: ["$checklist.units.status", "closed"] }, 1, 0] }
|
|
7204
|
+
}
|
|
7205
|
+
}
|
|
7206
|
+
}
|
|
7207
|
+
]).toArray();
|
|
7208
|
+
const sums = /* @__PURE__ */ new Map();
|
|
7209
|
+
for (const row of rows) {
|
|
7210
|
+
const task = ran.find((i) => String(i._id) === String(row._id.task));
|
|
7211
|
+
if (!task || dateOf.get(String(row._id.schedule)) !== task.lastRunDate)
|
|
7212
|
+
continue;
|
|
7213
|
+
const sum = sums.get(String(task._id)) ?? { total: 0, completed: 0, closed: 0 };
|
|
7214
|
+
sum.total += row.total;
|
|
7215
|
+
sum.completed += row.completed;
|
|
7216
|
+
sum.closed += row.closed;
|
|
7217
|
+
sums.set(String(task._id), sum);
|
|
7218
|
+
}
|
|
7219
|
+
for (const [id, sum] of sums) {
|
|
7220
|
+
result.set(id, sum.completed === sum.total ? "done" : sum.closed > 0 ? "missed" : "pending");
|
|
7221
|
+
}
|
|
7222
|
+
return result;
|
|
7223
|
+
}
|
|
7224
|
+
async function getAllScheduleTask(today) {
|
|
6864
7225
|
const query = {
|
|
6865
7226
|
status: { $ne: "deleted" }
|
|
6866
7227
|
};
|
|
7228
|
+
if (today) {
|
|
7229
|
+
query.paused = { $ne: true };
|
|
7230
|
+
query.lastRunDate = { $ne: today };
|
|
7231
|
+
query.$or = [
|
|
7232
|
+
{ dates: today },
|
|
7233
|
+
{ repeat: { $in: ["daily", "weekdays", "weekly", "monthly"] } }
|
|
7234
|
+
];
|
|
7235
|
+
}
|
|
6867
7236
|
try {
|
|
6868
7237
|
const items = await collection.aggregate([{ $match: query }, { $sort: { _id: -1 } }]).toArray();
|
|
6869
7238
|
return items;
|
|
@@ -6871,6 +7240,9 @@ function useScheduleTaskRepository() {
|
|
|
6871
7240
|
throw error;
|
|
6872
7241
|
}
|
|
6873
7242
|
}
|
|
7243
|
+
function withScheduleDisplay(row) {
|
|
7244
|
+
return { ...row, ...scheduleDisplay(row, /* @__PURE__ */ new Date()) };
|
|
7245
|
+
}
|
|
6874
7246
|
async function getScheduleTaskById(_id, session) {
|
|
6875
7247
|
try {
|
|
6876
7248
|
_id = new ObjectId19(_id);
|
|
@@ -6888,7 +7260,7 @@ function useScheduleTaskRepository() {
|
|
|
6888
7260
|
const cachedData = await getCache(cacheKey);
|
|
6889
7261
|
if (cachedData) {
|
|
6890
7262
|
logger31.info(`Cache hit for key: ${cacheKey}`);
|
|
6891
|
-
return cachedData;
|
|
7263
|
+
return withScheduleDisplay(cachedData);
|
|
6892
7264
|
}
|
|
6893
7265
|
} else {
|
|
6894
7266
|
logger31.info(`Skipping cache during transaction for key: ${cacheKey}`);
|
|
@@ -6899,12 +7271,13 @@ function useScheduleTaskRepository() {
|
|
|
6899
7271
|
{
|
|
6900
7272
|
$project: {
|
|
6901
7273
|
title: 1,
|
|
6902
|
-
time: 1,
|
|
6903
|
-
dates: 1,
|
|
6904
7274
|
description: 1,
|
|
6905
7275
|
areas: 1,
|
|
6906
7276
|
status: 1,
|
|
6907
|
-
createdAt: 1
|
|
7277
|
+
createdAt: 1,
|
|
7278
|
+
site: 1,
|
|
7279
|
+
serviceType: 1,
|
|
7280
|
+
...SCHEDULE_FIELDS
|
|
6908
7281
|
}
|
|
6909
7282
|
}
|
|
6910
7283
|
]).toArray();
|
|
@@ -6916,7 +7289,7 @@ function useScheduleTaskRepository() {
|
|
|
6916
7289
|
}).catch((err) => {
|
|
6917
7290
|
logger31.error(`Failed to set cache for key: ${cacheKey}`, err);
|
|
6918
7291
|
});
|
|
6919
|
-
return data[0];
|
|
7292
|
+
return withScheduleDisplay(data[0]);
|
|
6920
7293
|
} catch (error) {
|
|
6921
7294
|
throw error;
|
|
6922
7295
|
}
|
|
@@ -6940,16 +7313,20 @@ function useScheduleTaskRepository() {
|
|
|
6940
7313
|
});
|
|
6941
7314
|
}
|
|
6942
7315
|
try {
|
|
6943
|
-
const
|
|
7316
|
+
const { set, unset } = splitScheduleUpdate(value);
|
|
7317
|
+
const update = {
|
|
7318
|
+
$set: { ...set, updatedAt: /* @__PURE__ */ new Date() }
|
|
7319
|
+
};
|
|
7320
|
+
if (unset.length) {
|
|
7321
|
+
update.$unset = Object.fromEntries(unset.map((key) => [key, ""]));
|
|
7322
|
+
}
|
|
6944
7323
|
const res = await collection.updateOne(
|
|
6945
|
-
{ _id },
|
|
6946
|
-
|
|
7324
|
+
{ _id, status: { $ne: "deleted" } },
|
|
7325
|
+
update,
|
|
6947
7326
|
{ session }
|
|
6948
7327
|
);
|
|
6949
|
-
if (res.
|
|
6950
|
-
throw new
|
|
6951
|
-
"Unable to update hygiene schedule task."
|
|
6952
|
-
);
|
|
7328
|
+
if (res.matchedCount === 0) {
|
|
7329
|
+
throw new NotFoundError7("Schedule task not found.");
|
|
6953
7330
|
}
|
|
6954
7331
|
delNamespace().then(() => {
|
|
6955
7332
|
logger31.info(`Cache cleared for namespace: ${namespace_collection}`);
|
|
@@ -6964,6 +7341,18 @@ function useScheduleTaskRepository() {
|
|
|
6964
7341
|
throw error;
|
|
6965
7342
|
}
|
|
6966
7343
|
}
|
|
7344
|
+
async function markScheduleTaskRan(_id, date) {
|
|
7345
|
+
await collection.updateOne(
|
|
7346
|
+
{ _id: new ObjectId19(_id) },
|
|
7347
|
+
{ $set: { lastRunDate: date, lastRunAt: /* @__PURE__ */ new Date() } }
|
|
7348
|
+
);
|
|
7349
|
+
delNamespace().catch((err) => {
|
|
7350
|
+
logger31.error(
|
|
7351
|
+
`Failed to clear cache for namespace: ${namespace_collection}`,
|
|
7352
|
+
err
|
|
7353
|
+
);
|
|
7354
|
+
});
|
|
7355
|
+
}
|
|
6967
7356
|
async function deleteScheduleTask(_id, session) {
|
|
6968
7357
|
try {
|
|
6969
7358
|
_id = new ObjectId19(_id);
|
|
@@ -7003,6 +7392,7 @@ function useScheduleTaskRepository() {
|
|
|
7003
7392
|
createScheduleTask,
|
|
7004
7393
|
getScheduleTasks,
|
|
7005
7394
|
getAllScheduleTask,
|
|
7395
|
+
markScheduleTaskRan,
|
|
7006
7396
|
getScheduleTaskById,
|
|
7007
7397
|
updateScheduleTask,
|
|
7008
7398
|
deleteScheduleTask
|
|
@@ -7014,7 +7404,7 @@ import { ObjectId as ObjectId20 } from "mongodb";
|
|
|
7014
7404
|
import { logger as logger32 } from "@7365admin1/node-server-utils";
|
|
7015
7405
|
function useScheduleTaskService() {
|
|
7016
7406
|
const { createParentChecklist } = useParentChecklistRepo();
|
|
7017
|
-
const { getAllScheduleTask } = useScheduleTaskRepository();
|
|
7407
|
+
const { getAllScheduleTask, markScheduleTaskRan } = useScheduleTaskRepository();
|
|
7018
7408
|
const {
|
|
7019
7409
|
createAreaChecklist,
|
|
7020
7410
|
getAreaChecklistByAreaAndSchedule,
|
|
@@ -7023,73 +7413,7 @@ function useScheduleTaskService() {
|
|
|
7023
7413
|
const { getAreaById } = useAreaRepo();
|
|
7024
7414
|
function checkScheduleConditions(schedule, serviceType, currentDate = /* @__PURE__ */ new Date()) {
|
|
7025
7415
|
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;
|
|
7416
|
+
return shouldRunNow(schedule, currentDate);
|
|
7093
7417
|
} catch (error) {
|
|
7094
7418
|
logger32.error(
|
|
7095
7419
|
`Error checking schedule conditions for ${schedule._id}:`,
|
|
@@ -7098,18 +7422,22 @@ function useScheduleTaskService() {
|
|
|
7098
7422
|
return false;
|
|
7099
7423
|
}
|
|
7100
7424
|
}
|
|
7425
|
+
const MAX_ATTEMPTS = 5;
|
|
7426
|
+
const attempts = /* @__PURE__ */ new Map();
|
|
7101
7427
|
async function processScheduledTasks(currentDate) {
|
|
7102
7428
|
try {
|
|
7103
7429
|
logger32.info("Starting scheduled task processing...");
|
|
7104
|
-
const
|
|
7430
|
+
const today = sgtParts(currentDate ?? /* @__PURE__ */ new Date()).date;
|
|
7431
|
+
const scheduleTasks = await getAllScheduleTask(today);
|
|
7105
7432
|
if (!scheduleTasks || scheduleTasks.length === 0) {
|
|
7106
7433
|
logger32.info("No schedule tasks found to process");
|
|
7107
|
-
return { processed: 0, validated: 0 };
|
|
7434
|
+
return { processed: 0, validated: 0, fired: [] };
|
|
7108
7435
|
}
|
|
7109
7436
|
logger32.info(`Found ${scheduleTasks.length} schedule tasks to check`);
|
|
7110
7437
|
let processedCount = 0;
|
|
7111
7438
|
let validatedCount = 0;
|
|
7112
7439
|
const validatedTasks = [];
|
|
7440
|
+
const fired = [];
|
|
7113
7441
|
for (const scheduleTask of scheduleTasks) {
|
|
7114
7442
|
try {
|
|
7115
7443
|
const serviceType = scheduleTask.serviceType;
|
|
@@ -7152,6 +7480,8 @@ function useScheduleTaskService() {
|
|
|
7152
7480
|
logger32.info(
|
|
7153
7481
|
`Using parent checklist ${parentChecklistId}, now creating/updating area checklists`
|
|
7154
7482
|
);
|
|
7483
|
+
let failedAreas = 0;
|
|
7484
|
+
const addedAreas = [];
|
|
7155
7485
|
for (const area of scheduleTask.areas) {
|
|
7156
7486
|
try {
|
|
7157
7487
|
const areaId = area.value.toString();
|
|
@@ -7231,6 +7561,7 @@ function useScheduleTaskService() {
|
|
|
7231
7561
|
newSets
|
|
7232
7562
|
);
|
|
7233
7563
|
if (modified > 0) {
|
|
7564
|
+
addedAreas.push(area.name);
|
|
7234
7565
|
logger32.info(
|
|
7235
7566
|
`Appended ${areaSetConfig} set(s) starting from ${startSetNumber} to area checklist for area ${area.name}`
|
|
7236
7567
|
);
|
|
@@ -7263,13 +7594,32 @@ function useScheduleTaskService() {
|
|
|
7263
7594
|
`Area ${area.name} (${areaId}): Creating new area checklist with schedule-task sets only`
|
|
7264
7595
|
);
|
|
7265
7596
|
await createAreaChecklist(checklistData);
|
|
7597
|
+
addedAreas.push(area.name);
|
|
7266
7598
|
logger32.info(`Created new area checklist for area ${area.name}`);
|
|
7267
7599
|
}
|
|
7268
7600
|
} catch (error) {
|
|
7269
7601
|
logger32.error(`Error processing area ${area.name}:`, error);
|
|
7602
|
+
failedAreas++;
|
|
7270
7603
|
continue;
|
|
7271
7604
|
}
|
|
7272
7605
|
}
|
|
7606
|
+
const attemptKey = `${scheduleTask._id}:${today}`;
|
|
7607
|
+
const tried = (attempts.get(attemptKey) ?? 0) + 1;
|
|
7608
|
+
attempts.set(attemptKey, tried);
|
|
7609
|
+
if (failedAreas === 0 || tried >= MAX_ATTEMPTS) {
|
|
7610
|
+
await markScheduleTaskRan(scheduleTask._id, today);
|
|
7611
|
+
attempts.delete(attemptKey);
|
|
7612
|
+
}
|
|
7613
|
+
if (addedAreas.length > 0) {
|
|
7614
|
+
fired.push({
|
|
7615
|
+
_id: scheduleTask._id,
|
|
7616
|
+
site: scheduleTask.site,
|
|
7617
|
+
serviceType,
|
|
7618
|
+
title: scheduleTask.title,
|
|
7619
|
+
time: scheduleTask.time,
|
|
7620
|
+
areas: addedAreas
|
|
7621
|
+
});
|
|
7622
|
+
}
|
|
7273
7623
|
processedCount++;
|
|
7274
7624
|
validatedCount++;
|
|
7275
7625
|
validatedTasks.push(scheduleTask);
|
|
@@ -7281,6 +7631,17 @@ function useScheduleTaskService() {
|
|
|
7281
7631
|
`Error processing schedule task ${scheduleTask._id}:`,
|
|
7282
7632
|
error
|
|
7283
7633
|
);
|
|
7634
|
+
try {
|
|
7635
|
+
const key = `${scheduleTask._id}:${today}`;
|
|
7636
|
+
const tried = (attempts.get(key) ?? 0) + 1;
|
|
7637
|
+
attempts.set(key, tried);
|
|
7638
|
+
if (tried >= MAX_ATTEMPTS && scheduleTask._id) {
|
|
7639
|
+
await markScheduleTaskRan(scheduleTask._id, today);
|
|
7640
|
+
attempts.delete(key);
|
|
7641
|
+
}
|
|
7642
|
+
} catch (markError) {
|
|
7643
|
+
logger32.error("Could not mark failing schedule task", markError);
|
|
7644
|
+
}
|
|
7284
7645
|
continue;
|
|
7285
7646
|
}
|
|
7286
7647
|
}
|
|
@@ -7290,7 +7651,8 @@ function useScheduleTaskService() {
|
|
|
7290
7651
|
return {
|
|
7291
7652
|
processed: processedCount,
|
|
7292
7653
|
validated: validatedCount,
|
|
7293
|
-
tasks: validatedTasks
|
|
7654
|
+
tasks: validatedTasks,
|
|
7655
|
+
fired
|
|
7294
7656
|
};
|
|
7295
7657
|
} catch (error) {
|
|
7296
7658
|
logger32.error("Error processing scheduled tasks:", error);
|
|
@@ -7301,7 +7663,7 @@ function useScheduleTaskService() {
|
|
|
7301
7663
|
}
|
|
7302
7664
|
|
|
7303
7665
|
// src/controllers/hygiene-schedule-task.controller.ts
|
|
7304
|
-
import
|
|
7666
|
+
import Joi19 from "joi";
|
|
7305
7667
|
import { BadRequestError as BadRequestError30, logger as logger33 } from "@7365admin1/node-server-utils";
|
|
7306
7668
|
import { AppServiceType as AppServiceType18 } from "@7365admin1/core";
|
|
7307
7669
|
function useScheduleTaskController() {
|
|
@@ -7332,12 +7694,12 @@ function useScheduleTaskController() {
|
|
|
7332
7694
|
}
|
|
7333
7695
|
async function getScheduleTasks(req, res, next) {
|
|
7334
7696
|
const query = { ...req.query, ...req.params };
|
|
7335
|
-
const validation =
|
|
7336
|
-
page:
|
|
7337
|
-
limit:
|
|
7338
|
-
search:
|
|
7339
|
-
site:
|
|
7340
|
-
serviceType:
|
|
7697
|
+
const validation = Joi19.object({
|
|
7698
|
+
page: Joi19.number().min(1).optional().allow("", null),
|
|
7699
|
+
limit: Joi19.number().min(1).optional().allow("", null),
|
|
7700
|
+
search: Joi19.string().optional().allow("", null),
|
|
7701
|
+
site: Joi19.string().hex().required(),
|
|
7702
|
+
serviceType: Joi19.string().valid(...Object.values(AppServiceType18)).required()
|
|
7341
7703
|
});
|
|
7342
7704
|
const { error } = validation.validate(query);
|
|
7343
7705
|
if (error) {
|
|
@@ -7345,8 +7707,8 @@ function useScheduleTaskController() {
|
|
|
7345
7707
|
next(new BadRequestError30(error.message));
|
|
7346
7708
|
return;
|
|
7347
7709
|
}
|
|
7348
|
-
const page = parseInt(req.query.page)
|
|
7349
|
-
const limit = parseInt(req.query.limit)
|
|
7710
|
+
const page = parseInt(req.query.page) || 1;
|
|
7711
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
7350
7712
|
const search = req.query.search ?? "";
|
|
7351
7713
|
const site = req.params.site ?? "";
|
|
7352
7714
|
const serviceType = req.query.serviceType ?? "";
|
|
@@ -7367,7 +7729,7 @@ function useScheduleTaskController() {
|
|
|
7367
7729
|
}
|
|
7368
7730
|
}
|
|
7369
7731
|
async function getScheduleTaskById(req, res, next) {
|
|
7370
|
-
const validation =
|
|
7732
|
+
const validation = Joi19.string().hex().required();
|
|
7371
7733
|
const _id = req.params.id;
|
|
7372
7734
|
const { error, value } = validation.validate(_id);
|
|
7373
7735
|
if (error) {
|
|
@@ -7387,20 +7749,17 @@ function useScheduleTaskController() {
|
|
|
7387
7749
|
}
|
|
7388
7750
|
async function updateScheduleTask(req, res, next) {
|
|
7389
7751
|
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()
|
|
7752
|
+
const optional = Object.fromEntries(
|
|
7753
|
+
Object.entries(scheduleTaskFields).map(([key, rule]) => [
|
|
7754
|
+
key,
|
|
7755
|
+
key === "dates" ? Joi19.array().min(1).items(scheduleDate).optional().messages({
|
|
7756
|
+
"array.min": "Pick at least one date, or choose a repeat."
|
|
7757
|
+
}) : rule.optional()
|
|
7758
|
+
])
|
|
7759
|
+
);
|
|
7760
|
+
const validation = Joi19.object({
|
|
7761
|
+
id: Joi19.string().hex().required(),
|
|
7762
|
+
...optional
|
|
7404
7763
|
});
|
|
7405
7764
|
const { error } = validation.validate(payload);
|
|
7406
7765
|
if (error) {
|
|
@@ -7408,6 +7767,24 @@ function useScheduleTaskController() {
|
|
|
7408
7767
|
next(new BadRequestError30(error.message));
|
|
7409
7768
|
return;
|
|
7410
7769
|
}
|
|
7770
|
+
try {
|
|
7771
|
+
const { set, unset } = splitScheduleUpdate(req.body ?? {});
|
|
7772
|
+
if (TIMING_KEYS.some((key) => key in set || unset.includes(key))) {
|
|
7773
|
+
const stored = await _getScheduleTaskById(req.params.id);
|
|
7774
|
+
const merged = { ...stored, ...set };
|
|
7775
|
+
for (const key of unset)
|
|
7776
|
+
delete merged[key];
|
|
7777
|
+
const problem = timingChanged(stored, merged) ? validateScheduleRules(cleanScheduleInput(merged)) : null;
|
|
7778
|
+
if (problem) {
|
|
7779
|
+
next(new BadRequestError30(problem));
|
|
7780
|
+
return;
|
|
7781
|
+
}
|
|
7782
|
+
}
|
|
7783
|
+
} catch (error2) {
|
|
7784
|
+
logger33.log({ level: "error", message: error2.message });
|
|
7785
|
+
next(error2);
|
|
7786
|
+
return;
|
|
7787
|
+
}
|
|
7411
7788
|
try {
|
|
7412
7789
|
const { id, ...value } = payload;
|
|
7413
7790
|
await _updateScheduleTask(id, value);
|
|
@@ -7420,7 +7797,7 @@ function useScheduleTaskController() {
|
|
|
7420
7797
|
}
|
|
7421
7798
|
}
|
|
7422
7799
|
async function deleteScheduleTask(req, res, next) {
|
|
7423
|
-
const validation =
|
|
7800
|
+
const validation = Joi19.string().hex().required();
|
|
7424
7801
|
const _id = req.params.id;
|
|
7425
7802
|
const { error, value } = validation.validate(_id);
|
|
7426
7803
|
if (error) {
|
|
@@ -7664,17 +8041,17 @@ function useQRService() {
|
|
|
7664
8041
|
}
|
|
7665
8042
|
|
|
7666
8043
|
// src/controllers/hygiene-qr.controller.ts
|
|
7667
|
-
import
|
|
8044
|
+
import Joi20 from "joi";
|
|
7668
8045
|
import { BadRequestError as BadRequestError31, logger as logger35 } from "@7365admin1/node-server-utils";
|
|
7669
8046
|
function useQRController() {
|
|
7670
8047
|
const { generateQRImage: _generateQRImage, generateQRPDF: _generateQRPDF } = useQRService();
|
|
7671
8048
|
async function generateQR(req, res, next) {
|
|
7672
|
-
const validation =
|
|
7673
|
-
url:
|
|
7674
|
-
filename:
|
|
7675
|
-
title:
|
|
7676
|
-
subtitle:
|
|
7677
|
-
download:
|
|
8049
|
+
const validation = Joi20.object({
|
|
8050
|
+
url: Joi20.string().uri().required(),
|
|
8051
|
+
filename: Joi20.string().optional().allow("", null),
|
|
8052
|
+
title: Joi20.string().optional().allow("", null),
|
|
8053
|
+
subtitle: Joi20.string().optional().allow("", null),
|
|
8054
|
+
download: Joi20.boolean().optional().default(false)
|
|
7678
8055
|
});
|
|
7679
8056
|
const query = { ...req.query };
|
|
7680
8057
|
const { error, value } = validation.validate(query);
|