@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.js
CHANGED
|
@@ -93,9 +93,57 @@ var allowedStatus = [
|
|
|
93
93
|
var allowedPeriods = ["today", "thisWeek", "thisMonth"];
|
|
94
94
|
|
|
95
95
|
// src/repositories/hygiene-dashboard.repository.ts
|
|
96
|
-
var
|
|
96
|
+
var import_moment_timezone2 = __toESM(require("moment-timezone"));
|
|
97
97
|
var import_mongodb = require("mongodb");
|
|
98
98
|
var import_node_server_utils = require("@7365admin1/node-server-utils");
|
|
99
|
+
|
|
100
|
+
// src/utils/hygiene-dashboard-metrics.util.ts
|
|
101
|
+
var import_moment_timezone = __toESM(require("moment-timezone"));
|
|
102
|
+
var DASHBOARD_TIMEZONE = "Asia/Singapore";
|
|
103
|
+
var PERIOD_UNITS = {
|
|
104
|
+
thisWeek: { boundary: "isoWeek", step: "week" },
|
|
105
|
+
thisMonth: { boundary: "month", step: "month" }
|
|
106
|
+
};
|
|
107
|
+
var DAY_UNIT = {
|
|
108
|
+
boundary: "day",
|
|
109
|
+
step: "day"
|
|
110
|
+
};
|
|
111
|
+
function unitsOf(period) {
|
|
112
|
+
return PERIOD_UNITS[period] ?? DAY_UNIT;
|
|
113
|
+
}
|
|
114
|
+
function at(now) {
|
|
115
|
+
return now === void 0 || now === null ? import_moment_timezone.default.tz(DASHBOARD_TIMEZONE) : import_moment_timezone.default.tz(now, DASHBOARD_TIMEZONE);
|
|
116
|
+
}
|
|
117
|
+
function getPeriodRange(period, now) {
|
|
118
|
+
const { boundary } = unitsOf(period);
|
|
119
|
+
const point = at(now);
|
|
120
|
+
return {
|
|
121
|
+
$gte: point.clone().startOf(boundary).toDate(),
|
|
122
|
+
$lte: point.clone().endOf(boundary).toDate()
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function getPreviousPeriodRange(period, now) {
|
|
126
|
+
const { boundary, step } = unitsOf(period);
|
|
127
|
+
const point = at(now).subtract(1, step);
|
|
128
|
+
return {
|
|
129
|
+
$gte: point.clone().startOf(boundary).toDate(),
|
|
130
|
+
$lte: point.clone().endOf(boundary).toDate()
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function calculatePercentageChange(currentCount, previousCount) {
|
|
134
|
+
if (typeof currentCount !== "number" || !Number.isFinite(currentCount)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (typeof previousCount !== "number" || !Number.isFinite(previousCount)) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
if (previousCount === 0)
|
|
141
|
+
return null;
|
|
142
|
+
return Math.round((currentCount - previousCount) / previousCount * 100 * 100) / 100;
|
|
143
|
+
}
|
|
144
|
+
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.";
|
|
145
|
+
|
|
146
|
+
// src/repositories/hygiene-dashboard.repository.ts
|
|
99
147
|
function useHygieneDashboardRepository() {
|
|
100
148
|
const db = import_node_server_utils.useAtlas.getDb();
|
|
101
149
|
if (!db) {
|
|
@@ -166,59 +214,16 @@ function useHygieneDashboardRepository() {
|
|
|
166
214
|
import_node_server_utils.logger.info(`Cache hit for dashboard: ${dashboardCacheKey}`);
|
|
167
215
|
return cachedDashboard;
|
|
168
216
|
}
|
|
169
|
-
function formatEndDate(date) {
|
|
170
|
-
if (typeof date === "string") {
|
|
171
|
-
const dateWithoutTimezone = date.replace(/\s*\+\d{2}:\d{2}$/, "");
|
|
172
|
-
date = new Date(dateWithoutTimezone);
|
|
173
|
-
}
|
|
174
|
-
const singaporeMoment = import_moment_timezone.default.tz(date, "Asia/Singapore");
|
|
175
|
-
singaporeMoment.set({
|
|
176
|
-
hour: 23,
|
|
177
|
-
minute: 59,
|
|
178
|
-
second: 59,
|
|
179
|
-
millisecond: 999
|
|
180
|
-
});
|
|
181
|
-
return singaporeMoment.toDate();
|
|
182
|
-
}
|
|
183
|
-
function getDateRange(p) {
|
|
184
|
-
const start = /* @__PURE__ */ new Date();
|
|
185
|
-
start.setHours(0, 0, 0, 0);
|
|
186
|
-
if (p === "today") {
|
|
187
|
-
return { $gte: start, $lte: formatEndDate(start) };
|
|
188
|
-
}
|
|
189
|
-
const days = p === "thisWeek" ? 7 : 30;
|
|
190
|
-
const rangeStart = /* @__PURE__ */ new Date();
|
|
191
|
-
rangeStart.setDate(start.getDate() - days);
|
|
192
|
-
rangeStart.setHours(0, 0, 0, 0);
|
|
193
|
-
return { $gte: rangeStart, $lte: formatEndDate(/* @__PURE__ */ new Date()) };
|
|
194
|
-
}
|
|
195
|
-
const calculatePercentageChange = (currentCount, previousCount) => {
|
|
196
|
-
if (previousCount === 0) {
|
|
197
|
-
return currentCount > 0 ? 100 : 0;
|
|
198
|
-
}
|
|
199
|
-
return Math.round(
|
|
200
|
-
(currentCount - previousCount) / previousCount * 100 * 100
|
|
201
|
-
) / 100;
|
|
202
|
-
};
|
|
203
217
|
const workOrderService = workOrderServiceMap[serviceType] ?? serviceType;
|
|
204
218
|
try {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
const yesterday = /* @__PURE__ */ new Date();
|
|
208
|
-
yesterday.setDate(today.getDate() - 1);
|
|
209
|
-
yesterday.setHours(0, 0, 0, 0);
|
|
210
|
-
const yesterdayEnd = new Date(yesterday);
|
|
211
|
-
yesterdayEnd.setHours(23, 59, 59, 999);
|
|
212
|
-
const todayEnd = formatEndDate(/* @__PURE__ */ new Date());
|
|
213
|
-
const periodRange = getDateRange(period);
|
|
219
|
+
const periodRange = getPeriodRange(period);
|
|
220
|
+
const priorPeriodRange = getPreviousPeriodRange(period);
|
|
214
221
|
const [
|
|
215
222
|
workOrderReport,
|
|
216
223
|
supplyAlertReport,
|
|
217
224
|
taskCompletedReport,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
yesterdayTaskCompletedReport,
|
|
221
|
-
todayTaskCompletedReport
|
|
225
|
+
priorWorkOrderReport,
|
|
226
|
+
priorTaskCompletedReport
|
|
222
227
|
] = await Promise.all([
|
|
223
228
|
// 1. Open work orders (period-filtered) with inProgress breakdown
|
|
224
229
|
workOrderCollection.aggregate([
|
|
@@ -262,45 +267,28 @@ function useHygieneDashboardRepository() {
|
|
|
262
267
|
}
|
|
263
268
|
}
|
|
264
269
|
]).toArray(),
|
|
265
|
-
|
|
270
|
+
/*
|
|
271
|
+
4. Open work orders in the PREVIOUS period (for % change).
|
|
272
|
+
|
|
273
|
+
The current side of the comparison is no longer queried separately:
|
|
274
|
+
it is now the same range as tile 1, so `wFacet.total` below IS the
|
|
275
|
+
current count. Two aggregations dropped, not by cutting a measure but
|
|
276
|
+
because the fix made them duplicates.
|
|
277
|
+
*/
|
|
266
278
|
workOrderCollection.aggregate([
|
|
267
279
|
{
|
|
268
280
|
$match: {
|
|
269
281
|
site,
|
|
270
282
|
service: workOrderService,
|
|
271
|
-
createdAt:
|
|
283
|
+
createdAt: priorPeriodRange,
|
|
272
284
|
status: { $nin: ["completed"] }
|
|
273
285
|
}
|
|
274
286
|
},
|
|
275
287
|
{ $count: "count" }
|
|
276
288
|
]).toArray(),
|
|
277
|
-
// 5.
|
|
278
|
-
workOrderCollection.aggregate([
|
|
279
|
-
{
|
|
280
|
-
$match: {
|
|
281
|
-
site,
|
|
282
|
-
service: workOrderService,
|
|
283
|
-
createdAt: { $gte: today, $lte: todayEnd },
|
|
284
|
-
status: { $nin: ["completed"] }
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
{ $count: "count" }
|
|
288
|
-
]).toArray(),
|
|
289
|
-
// 6. Yesterday task completed — area checklists (for % change)
|
|
290
|
-
areaChecklistCollection.aggregate([
|
|
291
|
-
...areaChecklistScheduleLookup(site, serviceType, {
|
|
292
|
-
$gte: yesterday,
|
|
293
|
-
$lte: yesterdayEnd
|
|
294
|
-
}),
|
|
295
|
-
{ $match: { status: "completed" } },
|
|
296
|
-
{ $count: "count" }
|
|
297
|
-
]).toArray(),
|
|
298
|
-
// 7. Today task completed — area checklists (for % change)
|
|
289
|
+
// 5. Task completed in the PREVIOUS period — area checklists (for % change)
|
|
299
290
|
areaChecklistCollection.aggregate([
|
|
300
|
-
...areaChecklistScheduleLookup(site, serviceType,
|
|
301
|
-
$gte: today,
|
|
302
|
-
$lte: todayEnd
|
|
303
|
-
}),
|
|
291
|
+
...areaChecklistScheduleLookup(site, serviceType, priorPeriodRange),
|
|
304
292
|
{ $match: { status: "completed" } },
|
|
305
293
|
{ $count: "count" }
|
|
306
294
|
]).toArray()
|
|
@@ -316,21 +304,26 @@ function useHygieneDashboardRepository() {
|
|
|
316
304
|
count: wFacet.total[0]?.count ?? 0,
|
|
317
305
|
inProgress: wFacet.inProgress[0]?.count ?? 0,
|
|
318
306
|
percentage: calculatePercentageChange(
|
|
319
|
-
|
|
320
|
-
|
|
307
|
+
wFacet.total[0]?.count ?? 0,
|
|
308
|
+
priorWorkOrderReport[0]?.count ?? 0
|
|
321
309
|
)
|
|
322
310
|
},
|
|
323
311
|
supplyAlert: {
|
|
312
|
+
// The count is real: active supplies at qty 0 for this site and
|
|
313
|
+
// service. The percentage beside it was the literal 0 and had never
|
|
314
|
+
// been computed from anything — see the util for why there is no
|
|
315
|
+
// prior period to compute one from.
|
|
324
316
|
count: supplyAlertReport[0]?.count ?? 0,
|
|
325
|
-
percentage:
|
|
317
|
+
percentage: null,
|
|
318
|
+
percentageUnavailableReason: SUPPLY_ALERT_HAS_NO_PRIOR_PERIOD
|
|
326
319
|
},
|
|
327
320
|
taskCompleted: {
|
|
328
321
|
completed: tFacet.completed[0]?.count ?? 0,
|
|
329
322
|
total: tFacet.total[0]?.count ?? 0,
|
|
330
323
|
inProgress: tFacet.inProgress[0]?.count ?? 0,
|
|
331
324
|
percentage: calculatePercentageChange(
|
|
332
|
-
|
|
333
|
-
|
|
325
|
+
tFacet.completed[0]?.count ?? 0,
|
|
326
|
+
priorTaskCompletedReport[0]?.count ?? 0
|
|
334
327
|
)
|
|
335
328
|
}
|
|
336
329
|
};
|
|
@@ -368,8 +361,8 @@ function useHygieneDashboardRepository() {
|
|
|
368
361
|
return cachedData;
|
|
369
362
|
}
|
|
370
363
|
const workOrderService = workOrderServiceMap[serviceType] ?? serviceType;
|
|
371
|
-
const weekStart =
|
|
372
|
-
const weekEnd =
|
|
364
|
+
const weekStart = import_moment_timezone2.default.tz("Asia/Singapore").startOf("isoWeek").toDate();
|
|
365
|
+
const weekEnd = import_moment_timezone2.default.tz("Asia/Singapore").endOf("isoWeek").toDate();
|
|
373
366
|
const weekRange = { $gte: weekStart, $lte: weekEnd };
|
|
374
367
|
const dayLabels = {
|
|
375
368
|
1: "Sun",
|
|
@@ -449,8 +442,8 @@ function useHygieneDashboardRepository() {
|
|
|
449
442
|
import_node_server_utils.logger.info(`Cache hit for todayTaskSchedule: ${cacheKey}`);
|
|
450
443
|
return cachedData;
|
|
451
444
|
}
|
|
452
|
-
const todayStart =
|
|
453
|
-
const todayEnd =
|
|
445
|
+
const todayStart = import_moment_timezone2.default.tz("Asia/Singapore").startOf("day").toDate();
|
|
446
|
+
const todayEnd = import_moment_timezone2.default.tz("Asia/Singapore").endOf("day").toDate();
|
|
454
447
|
const todayRange = { $gte: todayStart, $lte: todayEnd };
|
|
455
448
|
try {
|
|
456
449
|
const [items, countResult] = await Promise.all([
|
|
@@ -554,8 +547,8 @@ function useHygieneDashboardRepository() {
|
|
|
554
547
|
import_node_server_utils.logger.info(`Cache hit for staffAttendance: ${cacheKey}`);
|
|
555
548
|
return cachedData;
|
|
556
549
|
}
|
|
557
|
-
const todayStart =
|
|
558
|
-
const todayEnd =
|
|
550
|
+
const todayStart = import_moment_timezone2.default.tz("Asia/Singapore").startOf("day").toDate();
|
|
551
|
+
const todayEnd = import_moment_timezone2.default.tz("Asia/Singapore").endOf("day").toDate();
|
|
559
552
|
const todayStartStr = todayStart.toISOString();
|
|
560
553
|
const todayEndStr = todayEnd.toISOString();
|
|
561
554
|
try {
|
|
@@ -2578,7 +2571,7 @@ function MParentChecklist(value) {
|
|
|
2578
2571
|
|
|
2579
2572
|
// src/repositories/hygiene-parent-checklist.repository.ts
|
|
2580
2573
|
var import_mongodb7 = require("mongodb");
|
|
2581
|
-
var
|
|
2574
|
+
var import_moment_timezone3 = __toESM(require("moment-timezone"));
|
|
2582
2575
|
var import_node_server_utils14 = require("@7365admin1/node-server-utils");
|
|
2583
2576
|
var import_core7 = require("@7365admin1/core");
|
|
2584
2577
|
function useParentChecklistRepo() {
|
|
@@ -2874,11 +2867,11 @@ function useParentChecklistRepo() {
|
|
|
2874
2867
|
cacheOptions.search = search;
|
|
2875
2868
|
}
|
|
2876
2869
|
const singaporeTz = "Asia/Singapore";
|
|
2877
|
-
const normalizedStartDate = typeof startDate === "string" ? startDate : (0,
|
|
2878
|
-
const normalizedEndDate = typeof endDate === "string" ? endDate : (0,
|
|
2870
|
+
const normalizedStartDate = typeof startDate === "string" ? startDate : (0, import_moment_timezone3.default)(startDate).tz(singaporeTz).format("YYYY-MM-DD");
|
|
2871
|
+
const normalizedEndDate = typeof endDate === "string" ? endDate : (0, import_moment_timezone3.default)(endDate).tz(singaporeTz).format("YYYY-MM-DD");
|
|
2879
2872
|
if (startDate && endDate) {
|
|
2880
|
-
const startOfDay =
|
|
2881
|
-
const endOfDay =
|
|
2873
|
+
const startOfDay = import_moment_timezone3.default.tz(normalizedStartDate, "YYYY-MM-DD", singaporeTz).startOf("day").toDate();
|
|
2874
|
+
const endOfDay = import_moment_timezone3.default.tz(normalizedEndDate, "YYYY-MM-DD", singaporeTz).endOf("day").toDate();
|
|
2882
2875
|
query.createdAt = {
|
|
2883
2876
|
$gte: startOfDay,
|
|
2884
2877
|
$lte: endOfDay
|
|
@@ -2887,12 +2880,12 @@ function useParentChecklistRepo() {
|
|
|
2887
2880
|
cacheOptions.endDate = normalizedEndDate;
|
|
2888
2881
|
} else if (startDate) {
|
|
2889
2882
|
query.createdAt = {
|
|
2890
|
-
$gte:
|
|
2883
|
+
$gte: import_moment_timezone3.default.tz(normalizedStartDate, "YYYY-MM-DD", singaporeTz).startOf("day").toDate()
|
|
2891
2884
|
};
|
|
2892
2885
|
cacheOptions.startDate = normalizedStartDate;
|
|
2893
2886
|
} else if (endDate) {
|
|
2894
2887
|
query.createdAt = {
|
|
2895
|
-
$lte:
|
|
2888
|
+
$lte: import_moment_timezone3.default.tz(normalizedEndDate, "YYYY-MM-DD", singaporeTz).endOf("day").toDate()
|
|
2896
2889
|
};
|
|
2897
2890
|
cacheOptions.endDate = normalizedEndDate;
|
|
2898
2891
|
}
|