@7365admin1/module-hygiene 4.28.1-staging.6 → 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.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 moment from "moment-timezone";
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 today = /* @__PURE__ */ new Date();
130
- today.setHours(0, 0, 0, 0);
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
- yesterdayWorkOrderReport,
143
- todayWorkOrderReport,
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
- // 4. Yesterday open work orders (for % change)
190
- workOrderCollection.aggregate([
191
- {
192
- $match: {
193
- site,
194
- service: workOrderService,
195
- createdAt: { $gte: yesterday, $lte: yesterdayEnd },
196
- status: { $nin: ["completed"] }
197
- }
198
- },
199
- { $count: "count" }
200
- ]).toArray(),
201
- // 5. Today open work orders (for % change)
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
+ */
202
202
  workOrderCollection.aggregate([
203
203
  {
204
204
  $match: {
205
205
  site,
206
206
  service: workOrderService,
207
- createdAt: { $gte: today, $lte: todayEnd },
207
+ createdAt: priorPeriodRange,
208
208
  status: { $nin: ["completed"] }
209
209
  }
210
210
  },
211
211
  { $count: "count" }
212
212
  ]).toArray(),
213
- // 6. Yesterday task completed — area checklists (for % change)
213
+ // 5. Task completed in the PREVIOUS period — area checklists (for % change)
214
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)
223
- 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
- todayWorkOrderReport[0]?.count ?? 0,
244
- yesterdayWorkOrderReport[0]?.count ?? 0
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: 0
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
- todayTaskCompletedReport[0]?.count ?? 0,
257
- yesterdayTaskCompletedReport[0]?.count ?? 0
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 = moment.tz("Asia/Singapore").startOf("isoWeek").toDate();
296
- const weekEnd = moment.tz("Asia/Singapore").endOf("isoWeek").toDate();
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 = moment.tz("Asia/Singapore").startOf("day").toDate();
377
- const todayEnd = moment.tz("Asia/Singapore").endOf("day").toDate();
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 = moment.tz("Asia/Singapore").startOf("day").toDate();
482
- const todayEnd = moment.tz("Asia/Singapore").endOf("day").toDate();
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 {
@@ -1608,7 +1601,7 @@ function useUnitRepository() {
1608
1601
  function useAreaService() {
1609
1602
  const { createArea: _createArea, getAreasForChecklist } = useAreaRepo();
1610
1603
  const { generateAreaExcel } = useAreaExportService();
1611
- const { getUnits: _getUnits } = useUnitRepository();
1604
+ const { createUnit: _createUnit, getUnits: _getUnits } = useUnitRepository();
1612
1605
  async function importArea({
1613
1606
  dataJson,
1614
1607
  site,
@@ -1690,23 +1683,40 @@ function useAreaService() {
1690
1683
  areaData.set = setNumber;
1691
1684
  }
1692
1685
  }
1693
- if (row.UNITS && availableUnits.length > 0) {
1686
+ if (row.UNITS) {
1694
1687
  const unitNames = String(row.UNITS).split(",").map((u) => u.trim()).filter((u) => u);
1695
1688
  if (unitNames.length > 0) {
1696
1689
  const areaUnits = [];
1697
1690
  for (const unitName of unitNames) {
1698
- const foundUnit = availableUnits.find(
1691
+ let foundUnit = availableUnits.find(
1699
1692
  (u) => u.name.toLowerCase() === unitName.toLowerCase()
1700
1693
  );
1694
+ if (!foundUnit) {
1695
+ try {
1696
+ const insertedUnitId = await _createUnit({
1697
+ name: unitName,
1698
+ site,
1699
+ serviceType
1700
+ });
1701
+ foundUnit = {
1702
+ _id: insertedUnitId,
1703
+ name: unitName
1704
+ };
1705
+ availableUnits.push(foundUnit);
1706
+ logger8.info(
1707
+ `Successfully created missing unit "${unitName}" for area "${areaName}"`
1708
+ );
1709
+ } catch (error) {
1710
+ logger8.warn(
1711
+ `Unit "${unitName}" could not be created for area "${areaName}": ${error.message}`
1712
+ );
1713
+ }
1714
+ }
1701
1715
  if (foundUnit) {
1702
1716
  areaUnits.push({
1703
1717
  unit: foundUnit._id,
1704
1718
  name: foundUnit.name
1705
1719
  });
1706
- } else {
1707
- logger8.warn(
1708
- `Unit "${unitName}" not found in site for area "${areaName}"`
1709
- );
1710
1720
  }
1711
1721
  }
1712
1722
  if (areaUnits.length > 0) {
@@ -2098,6 +2108,13 @@ function useUnitService() {
2098
2108
  updateUnit: _updateUnit,
2099
2109
  deleteUnit: _deleteUnit
2100
2110
  } = useUnitRepository();
2111
+ function getUnitNamesFromRow(row) {
2112
+ const unitValue = row?.UNIT ?? row?.UNITS;
2113
+ if (!unitValue) {
2114
+ return [];
2115
+ }
2116
+ return String(unitValue).split(",").map((unit) => unit.trim()).filter((unit) => unit && !unit.startsWith("Sample:"));
2117
+ }
2101
2118
  async function importUnit({
2102
2119
  dataJson,
2103
2120
  site,
@@ -2123,36 +2140,35 @@ function useUnitService() {
2123
2140
  try {
2124
2141
  for (let i = 0; i < dataArray.length; i++) {
2125
2142
  const row = dataArray[i];
2126
- if (!row?.UNIT) {
2127
- logger11.warn(`Skipping row ${i + 1} with missing UNIT:`, row);
2128
- skippedRows.push(i + 1);
2129
- continue;
2130
- }
2131
- const unitName = String(row.UNIT).trim();
2132
- if (!unitName) {
2133
- logger11.warn(`Skipping row ${i + 1} with empty unit name`);
2143
+ const unitNames = getUnitNamesFromRow(row);
2144
+ if (unitNames.length === 0) {
2145
+ logger11.warn(`Skipping row ${i + 1} with missing UNIT/UNITS:`, row);
2134
2146
  skippedRows.push(i + 1);
2135
2147
  continue;
2136
2148
  }
2137
- if (unitName.startsWith("Sample:")) {
2138
- logger11.warn(`Skipping row ${i + 1} with sample unit: ${unitName}`);
2139
- skippedRows.push(i + 1);
2140
- continue;
2141
- }
2142
- try {
2143
- const insertedId = await _createUnit({
2144
- name: unitName,
2145
- site,
2146
- serviceType
2147
- });
2148
- insertedUnitIds.push(insertedId);
2149
- logger11.info(`Successfully created unit: ${unitName}`);
2150
- } catch (error) {
2151
- logger11.error(`Error creating unit "${unitName}": ${error.message}`);
2152
- if (error.message.includes("Unit already exists")) {
2153
- duplicateUnits.push(unitName);
2154
- } else {
2155
- failedUnits.push(unitName);
2149
+ const uniqueUnitNames = Array.from(
2150
+ new Set(unitNames.map((unitName) => unitName.toLowerCase()))
2151
+ ).map(
2152
+ (lowerUnitName) => unitNames.find(
2153
+ (unitName) => unitName.toLowerCase() === lowerUnitName
2154
+ )
2155
+ );
2156
+ for (const unitName of uniqueUnitNames) {
2157
+ try {
2158
+ const insertedId = await _createUnit({
2159
+ name: unitName,
2160
+ site,
2161
+ serviceType
2162
+ });
2163
+ insertedUnitIds.push(insertedId);
2164
+ logger11.info(`Successfully created unit: ${unitName}`);
2165
+ } catch (error) {
2166
+ logger11.error(`Error creating unit "${unitName}": ${error.message}`);
2167
+ if (error.message.includes("Unit already exists")) {
2168
+ duplicateUnits.push(unitName);
2169
+ } else {
2170
+ failedUnits.push(unitName);
2171
+ }
2156
2172
  }
2157
2173
  }
2158
2174
  }
@@ -2505,7 +2521,7 @@ function MParentChecklist(value) {
2505
2521
 
2506
2522
  // src/repositories/hygiene-parent-checklist.repository.ts
2507
2523
  import { ObjectId as ObjectId7 } from "mongodb";
2508
- import moment2 from "moment-timezone";
2524
+ import moment3 from "moment-timezone";
2509
2525
  import {
2510
2526
  useAtlas as useAtlas5,
2511
2527
  InternalServerError as InternalServerError4,
@@ -2809,11 +2825,11 @@ function useParentChecklistRepo() {
2809
2825
  cacheOptions.search = search;
2810
2826
  }
2811
2827
  const singaporeTz = "Asia/Singapore";
2812
- const normalizedStartDate = typeof startDate === "string" ? startDate : moment2(startDate).tz(singaporeTz).format("YYYY-MM-DD");
2813
- const normalizedEndDate = typeof endDate === "string" ? endDate : moment2(endDate).tz(singaporeTz).format("YYYY-MM-DD");
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");
2814
2830
  if (startDate && endDate) {
2815
- const startOfDay = moment2.tz(normalizedStartDate, "YYYY-MM-DD", singaporeTz).startOf("day").toDate();
2816
- const endOfDay = moment2.tz(normalizedEndDate, "YYYY-MM-DD", singaporeTz).endOf("day").toDate();
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();
2817
2833
  query.createdAt = {
2818
2834
  $gte: startOfDay,
2819
2835
  $lte: endOfDay
@@ -2822,12 +2838,12 @@ function useParentChecklistRepo() {
2822
2838
  cacheOptions.endDate = normalizedEndDate;
2823
2839
  } else if (startDate) {
2824
2840
  query.createdAt = {
2825
- $gte: moment2.tz(normalizedStartDate, "YYYY-MM-DD", singaporeTz).startOf("day").toDate()
2841
+ $gte: moment3.tz(normalizedStartDate, "YYYY-MM-DD", singaporeTz).startOf("day").toDate()
2826
2842
  };
2827
2843
  cacheOptions.startDate = normalizedStartDate;
2828
2844
  } else if (endDate) {
2829
2845
  query.createdAt = {
2830
- $lte: moment2.tz(normalizedEndDate, "YYYY-MM-DD", singaporeTz).endOf("day").toDate()
2846
+ $lte: moment3.tz(normalizedEndDate, "YYYY-MM-DD", singaporeTz).endOf("day").toDate()
2831
2847
  };
2832
2848
  cacheOptions.endDate = normalizedEndDate;
2833
2849
  }