@7365admin1/module-hygiene 4.28.1-staging.7 → 4.28.1-staging.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +87 -94
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +87 -94
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/test/hygiene-dashboard-metrics.util.test.mjs +146 -0
package/dist/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ var allowedStatus = [
|
|
|
9
9
|
var allowedPeriods = ["today", "thisWeek", "thisMonth"];
|
|
10
10
|
|
|
11
11
|
// src/repositories/hygiene-dashboard.repository.ts
|
|
12
|
-
import
|
|
12
|
+
import moment2 from "moment-timezone";
|
|
13
13
|
import { ObjectId } from "mongodb";
|
|
14
14
|
import {
|
|
15
15
|
useAtlas,
|
|
@@ -20,6 +20,54 @@ import {
|
|
|
20
20
|
paginate,
|
|
21
21
|
logger
|
|
22
22
|
} from "@7365admin1/node-server-utils";
|
|
23
|
+
|
|
24
|
+
// src/utils/hygiene-dashboard-metrics.util.ts
|
|
25
|
+
import moment from "moment-timezone";
|
|
26
|
+
var DASHBOARD_TIMEZONE = "Asia/Singapore";
|
|
27
|
+
var PERIOD_UNITS = {
|
|
28
|
+
thisWeek: { boundary: "isoWeek", step: "week" },
|
|
29
|
+
thisMonth: { boundary: "month", step: "month" }
|
|
30
|
+
};
|
|
31
|
+
var DAY_UNIT = {
|
|
32
|
+
boundary: "day",
|
|
33
|
+
step: "day"
|
|
34
|
+
};
|
|
35
|
+
function unitsOf(period) {
|
|
36
|
+
return PERIOD_UNITS[period] ?? DAY_UNIT;
|
|
37
|
+
}
|
|
38
|
+
function at(now) {
|
|
39
|
+
return now === void 0 || now === null ? moment.tz(DASHBOARD_TIMEZONE) : moment.tz(now, DASHBOARD_TIMEZONE);
|
|
40
|
+
}
|
|
41
|
+
function getPeriodRange(period, now) {
|
|
42
|
+
const { boundary } = unitsOf(period);
|
|
43
|
+
const point = at(now);
|
|
44
|
+
return {
|
|
45
|
+
$gte: point.clone().startOf(boundary).toDate(),
|
|
46
|
+
$lte: point.clone().endOf(boundary).toDate()
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function getPreviousPeriodRange(period, now) {
|
|
50
|
+
const { boundary, step } = unitsOf(period);
|
|
51
|
+
const point = at(now).subtract(1, step);
|
|
52
|
+
return {
|
|
53
|
+
$gte: point.clone().startOf(boundary).toDate(),
|
|
54
|
+
$lte: point.clone().endOf(boundary).toDate()
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function calculatePercentageChange(currentCount, previousCount) {
|
|
58
|
+
if (typeof currentCount !== "number" || !Number.isFinite(currentCount)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (typeof previousCount !== "number" || !Number.isFinite(previousCount)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (previousCount === 0)
|
|
65
|
+
return null;
|
|
66
|
+
return Math.round((currentCount - previousCount) / previousCount * 100 * 100) / 100;
|
|
67
|
+
}
|
|
68
|
+
var SUPPLY_ALERT_HAS_NO_PRIOR_PERIOD = "Supply alerts count what is out of stock right now, and the supply record keeps no history, so there is no earlier period to compare against.";
|
|
69
|
+
|
|
70
|
+
// src/repositories/hygiene-dashboard.repository.ts
|
|
23
71
|
function useHygieneDashboardRepository() {
|
|
24
72
|
const db = useAtlas.getDb();
|
|
25
73
|
if (!db) {
|
|
@@ -90,59 +138,16 @@ function useHygieneDashboardRepository() {
|
|
|
90
138
|
logger.info(`Cache hit for dashboard: ${dashboardCacheKey}`);
|
|
91
139
|
return cachedDashboard;
|
|
92
140
|
}
|
|
93
|
-
function formatEndDate(date) {
|
|
94
|
-
if (typeof date === "string") {
|
|
95
|
-
const dateWithoutTimezone = date.replace(/\s*\+\d{2}:\d{2}$/, "");
|
|
96
|
-
date = new Date(dateWithoutTimezone);
|
|
97
|
-
}
|
|
98
|
-
const singaporeMoment = moment.tz(date, "Asia/Singapore");
|
|
99
|
-
singaporeMoment.set({
|
|
100
|
-
hour: 23,
|
|
101
|
-
minute: 59,
|
|
102
|
-
second: 59,
|
|
103
|
-
millisecond: 999
|
|
104
|
-
});
|
|
105
|
-
return singaporeMoment.toDate();
|
|
106
|
-
}
|
|
107
|
-
function getDateRange(p) {
|
|
108
|
-
const start = /* @__PURE__ */ new Date();
|
|
109
|
-
start.setHours(0, 0, 0, 0);
|
|
110
|
-
if (p === "today") {
|
|
111
|
-
return { $gte: start, $lte: formatEndDate(start) };
|
|
112
|
-
}
|
|
113
|
-
const days = p === "thisWeek" ? 7 : 30;
|
|
114
|
-
const rangeStart = /* @__PURE__ */ new Date();
|
|
115
|
-
rangeStart.setDate(start.getDate() - days);
|
|
116
|
-
rangeStart.setHours(0, 0, 0, 0);
|
|
117
|
-
return { $gte: rangeStart, $lte: formatEndDate(/* @__PURE__ */ new Date()) };
|
|
118
|
-
}
|
|
119
|
-
const calculatePercentageChange = (currentCount, previousCount) => {
|
|
120
|
-
if (previousCount === 0) {
|
|
121
|
-
return currentCount > 0 ? 100 : 0;
|
|
122
|
-
}
|
|
123
|
-
return Math.round(
|
|
124
|
-
(currentCount - previousCount) / previousCount * 100 * 100
|
|
125
|
-
) / 100;
|
|
126
|
-
};
|
|
127
141
|
const workOrderService = workOrderServiceMap[serviceType] ?? serviceType;
|
|
128
142
|
try {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const yesterday = /* @__PURE__ */ new Date();
|
|
132
|
-
yesterday.setDate(today.getDate() - 1);
|
|
133
|
-
yesterday.setHours(0, 0, 0, 0);
|
|
134
|
-
const yesterdayEnd = new Date(yesterday);
|
|
135
|
-
yesterdayEnd.setHours(23, 59, 59, 999);
|
|
136
|
-
const todayEnd = formatEndDate(/* @__PURE__ */ new Date());
|
|
137
|
-
const periodRange = getDateRange(period);
|
|
143
|
+
const periodRange = getPeriodRange(period);
|
|
144
|
+
const priorPeriodRange = getPreviousPeriodRange(period);
|
|
138
145
|
const [
|
|
139
146
|
workOrderReport,
|
|
140
147
|
supplyAlertReport,
|
|
141
148
|
taskCompletedReport,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
yesterdayTaskCompletedReport,
|
|
145
|
-
todayTaskCompletedReport
|
|
149
|
+
priorWorkOrderReport,
|
|
150
|
+
priorTaskCompletedReport
|
|
146
151
|
] = await Promise.all([
|
|
147
152
|
// 1. Open work orders (period-filtered) with inProgress breakdown
|
|
148
153
|
workOrderCollection.aggregate([
|
|
@@ -186,45 +191,28 @@ function useHygieneDashboardRepository() {
|
|
|
186
191
|
}
|
|
187
192
|
}
|
|
188
193
|
]).toArray(),
|
|
189
|
-
|
|
194
|
+
/*
|
|
195
|
+
4. Open work orders in the PREVIOUS period (for % change).
|
|
196
|
+
|
|
197
|
+
The current side of the comparison is no longer queried separately:
|
|
198
|
+
it is now the same range as tile 1, so `wFacet.total` below IS the
|
|
199
|
+
current count. Two aggregations dropped, not by cutting a measure but
|
|
200
|
+
because the fix made them duplicates.
|
|
201
|
+
*/
|
|
190
202
|
workOrderCollection.aggregate([
|
|
191
203
|
{
|
|
192
204
|
$match: {
|
|
193
205
|
site,
|
|
194
206
|
service: workOrderService,
|
|
195
|
-
createdAt:
|
|
207
|
+
createdAt: priorPeriodRange,
|
|
196
208
|
status: { $nin: ["completed"] }
|
|
197
209
|
}
|
|
198
210
|
},
|
|
199
211
|
{ $count: "count" }
|
|
200
212
|
]).toArray(),
|
|
201
|
-
// 5.
|
|
202
|
-
workOrderCollection.aggregate([
|
|
203
|
-
{
|
|
204
|
-
$match: {
|
|
205
|
-
site,
|
|
206
|
-
service: workOrderService,
|
|
207
|
-
createdAt: { $gte: today, $lte: todayEnd },
|
|
208
|
-
status: { $nin: ["completed"] }
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
{ $count: "count" }
|
|
212
|
-
]).toArray(),
|
|
213
|
-
// 6. Yesterday task completed — area checklists (for % change)
|
|
214
|
-
areaChecklistCollection.aggregate([
|
|
215
|
-
...areaChecklistScheduleLookup(site, serviceType, {
|
|
216
|
-
$gte: yesterday,
|
|
217
|
-
$lte: yesterdayEnd
|
|
218
|
-
}),
|
|
219
|
-
{ $match: { status: "completed" } },
|
|
220
|
-
{ $count: "count" }
|
|
221
|
-
]).toArray(),
|
|
222
|
-
// 7. Today task completed — area checklists (for % change)
|
|
213
|
+
// 5. Task completed in the PREVIOUS period — area checklists (for % change)
|
|
223
214
|
areaChecklistCollection.aggregate([
|
|
224
|
-
...areaChecklistScheduleLookup(site, serviceType,
|
|
225
|
-
$gte: today,
|
|
226
|
-
$lte: todayEnd
|
|
227
|
-
}),
|
|
215
|
+
...areaChecklistScheduleLookup(site, serviceType, priorPeriodRange),
|
|
228
216
|
{ $match: { status: "completed" } },
|
|
229
217
|
{ $count: "count" }
|
|
230
218
|
]).toArray()
|
|
@@ -240,21 +228,26 @@ function useHygieneDashboardRepository() {
|
|
|
240
228
|
count: wFacet.total[0]?.count ?? 0,
|
|
241
229
|
inProgress: wFacet.inProgress[0]?.count ?? 0,
|
|
242
230
|
percentage: calculatePercentageChange(
|
|
243
|
-
|
|
244
|
-
|
|
231
|
+
wFacet.total[0]?.count ?? 0,
|
|
232
|
+
priorWorkOrderReport[0]?.count ?? 0
|
|
245
233
|
)
|
|
246
234
|
},
|
|
247
235
|
supplyAlert: {
|
|
236
|
+
// The count is real: active supplies at qty 0 for this site and
|
|
237
|
+
// service. The percentage beside it was the literal 0 and had never
|
|
238
|
+
// been computed from anything — see the util for why there is no
|
|
239
|
+
// prior period to compute one from.
|
|
248
240
|
count: supplyAlertReport[0]?.count ?? 0,
|
|
249
|
-
percentage:
|
|
241
|
+
percentage: null,
|
|
242
|
+
percentageUnavailableReason: SUPPLY_ALERT_HAS_NO_PRIOR_PERIOD
|
|
250
243
|
},
|
|
251
244
|
taskCompleted: {
|
|
252
245
|
completed: tFacet.completed[0]?.count ?? 0,
|
|
253
246
|
total: tFacet.total[0]?.count ?? 0,
|
|
254
247
|
inProgress: tFacet.inProgress[0]?.count ?? 0,
|
|
255
248
|
percentage: calculatePercentageChange(
|
|
256
|
-
|
|
257
|
-
|
|
249
|
+
tFacet.completed[0]?.count ?? 0,
|
|
250
|
+
priorTaskCompletedReport[0]?.count ?? 0
|
|
258
251
|
)
|
|
259
252
|
}
|
|
260
253
|
};
|
|
@@ -292,8 +285,8 @@ function useHygieneDashboardRepository() {
|
|
|
292
285
|
return cachedData;
|
|
293
286
|
}
|
|
294
287
|
const workOrderService = workOrderServiceMap[serviceType] ?? serviceType;
|
|
295
|
-
const weekStart =
|
|
296
|
-
const weekEnd =
|
|
288
|
+
const weekStart = moment2.tz("Asia/Singapore").startOf("isoWeek").toDate();
|
|
289
|
+
const weekEnd = moment2.tz("Asia/Singapore").endOf("isoWeek").toDate();
|
|
297
290
|
const weekRange = { $gte: weekStart, $lte: weekEnd };
|
|
298
291
|
const dayLabels = {
|
|
299
292
|
1: "Sun",
|
|
@@ -373,8 +366,8 @@ function useHygieneDashboardRepository() {
|
|
|
373
366
|
logger.info(`Cache hit for todayTaskSchedule: ${cacheKey}`);
|
|
374
367
|
return cachedData;
|
|
375
368
|
}
|
|
376
|
-
const todayStart =
|
|
377
|
-
const todayEnd =
|
|
369
|
+
const todayStart = moment2.tz("Asia/Singapore").startOf("day").toDate();
|
|
370
|
+
const todayEnd = moment2.tz("Asia/Singapore").endOf("day").toDate();
|
|
378
371
|
const todayRange = { $gte: todayStart, $lte: todayEnd };
|
|
379
372
|
try {
|
|
380
373
|
const [items, countResult] = await Promise.all([
|
|
@@ -478,8 +471,8 @@ function useHygieneDashboardRepository() {
|
|
|
478
471
|
logger.info(`Cache hit for staffAttendance: ${cacheKey}`);
|
|
479
472
|
return cachedData;
|
|
480
473
|
}
|
|
481
|
-
const todayStart =
|
|
482
|
-
const todayEnd =
|
|
474
|
+
const todayStart = moment2.tz("Asia/Singapore").startOf("day").toDate();
|
|
475
|
+
const todayEnd = moment2.tz("Asia/Singapore").endOf("day").toDate();
|
|
483
476
|
const todayStartStr = todayStart.toISOString();
|
|
484
477
|
const todayEndStr = todayEnd.toISOString();
|
|
485
478
|
try {
|
|
@@ -2528,7 +2521,7 @@ function MParentChecklist(value) {
|
|
|
2528
2521
|
|
|
2529
2522
|
// src/repositories/hygiene-parent-checklist.repository.ts
|
|
2530
2523
|
import { ObjectId as ObjectId7 } from "mongodb";
|
|
2531
|
-
import
|
|
2524
|
+
import moment3 from "moment-timezone";
|
|
2532
2525
|
import {
|
|
2533
2526
|
useAtlas as useAtlas5,
|
|
2534
2527
|
InternalServerError as InternalServerError4,
|
|
@@ -2832,11 +2825,11 @@ function useParentChecklistRepo() {
|
|
|
2832
2825
|
cacheOptions.search = search;
|
|
2833
2826
|
}
|
|
2834
2827
|
const singaporeTz = "Asia/Singapore";
|
|
2835
|
-
const normalizedStartDate = typeof startDate === "string" ? startDate :
|
|
2836
|
-
const normalizedEndDate = typeof endDate === "string" ? endDate :
|
|
2828
|
+
const normalizedStartDate = typeof startDate === "string" ? startDate : moment3(startDate).tz(singaporeTz).format("YYYY-MM-DD");
|
|
2829
|
+
const normalizedEndDate = typeof endDate === "string" ? endDate : moment3(endDate).tz(singaporeTz).format("YYYY-MM-DD");
|
|
2837
2830
|
if (startDate && endDate) {
|
|
2838
|
-
const startOfDay =
|
|
2839
|
-
const endOfDay =
|
|
2831
|
+
const startOfDay = moment3.tz(normalizedStartDate, "YYYY-MM-DD", singaporeTz).startOf("day").toDate();
|
|
2832
|
+
const endOfDay = moment3.tz(normalizedEndDate, "YYYY-MM-DD", singaporeTz).endOf("day").toDate();
|
|
2840
2833
|
query.createdAt = {
|
|
2841
2834
|
$gte: startOfDay,
|
|
2842
2835
|
$lte: endOfDay
|
|
@@ -2845,12 +2838,12 @@ function useParentChecklistRepo() {
|
|
|
2845
2838
|
cacheOptions.endDate = normalizedEndDate;
|
|
2846
2839
|
} else if (startDate) {
|
|
2847
2840
|
query.createdAt = {
|
|
2848
|
-
$gte:
|
|
2841
|
+
$gte: moment3.tz(normalizedStartDate, "YYYY-MM-DD", singaporeTz).startOf("day").toDate()
|
|
2849
2842
|
};
|
|
2850
2843
|
cacheOptions.startDate = normalizedStartDate;
|
|
2851
2844
|
} else if (endDate) {
|
|
2852
2845
|
query.createdAt = {
|
|
2853
|
-
$lte:
|
|
2846
|
+
$lte: moment3.tz(normalizedEndDate, "YYYY-MM-DD", singaporeTz).endOf("day").toDate()
|
|
2854
2847
|
};
|
|
2855
2848
|
cacheOptions.endDate = normalizedEndDate;
|
|
2856
2849
|
}
|