@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.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 import_moment_timezone = __toESM(require("moment-timezone"));
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 today = /* @__PURE__ */ new Date();
206
- today.setHours(0, 0, 0, 0);
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
- yesterdayWorkOrderReport,
219
- todayWorkOrderReport,
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
- // 4. Yesterday open work orders (for % change)
266
- workOrderCollection.aggregate([
267
- {
268
- $match: {
269
- site,
270
- service: workOrderService,
271
- createdAt: { $gte: yesterday, $lte: yesterdayEnd },
272
- status: { $nin: ["completed"] }
273
- }
274
- },
275
- { $count: "count" }
276
- ]).toArray(),
277
- // 5. Today open work orders (for % change)
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
+ */
278
278
  workOrderCollection.aggregate([
279
279
  {
280
280
  $match: {
281
281
  site,
282
282
  service: workOrderService,
283
- createdAt: { $gte: today, $lte: todayEnd },
283
+ createdAt: priorPeriodRange,
284
284
  status: { $nin: ["completed"] }
285
285
  }
286
286
  },
287
287
  { $count: "count" }
288
288
  ]).toArray(),
289
- // 6. Yesterday task completed — area checklists (for % change)
289
+ // 5. Task completed in the PREVIOUS period — area checklists (for % change)
290
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)
299
- 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
- todayWorkOrderReport[0]?.count ?? 0,
320
- yesterdayWorkOrderReport[0]?.count ?? 0
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: 0
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
- todayTaskCompletedReport[0]?.count ?? 0,
333
- yesterdayTaskCompletedReport[0]?.count ?? 0
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 = import_moment_timezone.default.tz("Asia/Singapore").startOf("isoWeek").toDate();
372
- const weekEnd = import_moment_timezone.default.tz("Asia/Singapore").endOf("isoWeek").toDate();
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 = import_moment_timezone.default.tz("Asia/Singapore").startOf("day").toDate();
453
- const todayEnd = import_moment_timezone.default.tz("Asia/Singapore").endOf("day").toDate();
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 = import_moment_timezone.default.tz("Asia/Singapore").startOf("day").toDate();
558
- const todayEnd = import_moment_timezone.default.tz("Asia/Singapore").endOf("day").toDate();
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 {
@@ -1663,7 +1656,7 @@ function useUnitRepository() {
1663
1656
  function useAreaService() {
1664
1657
  const { createArea: _createArea, getAreasForChecklist } = useAreaRepo();
1665
1658
  const { generateAreaExcel } = useAreaExportService();
1666
- const { getUnits: _getUnits } = useUnitRepository();
1659
+ const { createUnit: _createUnit, getUnits: _getUnits } = useUnitRepository();
1667
1660
  async function importArea({
1668
1661
  dataJson,
1669
1662
  site,
@@ -1745,23 +1738,40 @@ function useAreaService() {
1745
1738
  areaData.set = setNumber;
1746
1739
  }
1747
1740
  }
1748
- if (row.UNITS && availableUnits.length > 0) {
1741
+ if (row.UNITS) {
1749
1742
  const unitNames = String(row.UNITS).split(",").map((u) => u.trim()).filter((u) => u);
1750
1743
  if (unitNames.length > 0) {
1751
1744
  const areaUnits = [];
1752
1745
  for (const unitName of unitNames) {
1753
- const foundUnit = availableUnits.find(
1746
+ let foundUnit = availableUnits.find(
1754
1747
  (u) => u.name.toLowerCase() === unitName.toLowerCase()
1755
1748
  );
1749
+ if (!foundUnit) {
1750
+ try {
1751
+ const insertedUnitId = await _createUnit({
1752
+ name: unitName,
1753
+ site,
1754
+ serviceType
1755
+ });
1756
+ foundUnit = {
1757
+ _id: insertedUnitId,
1758
+ name: unitName
1759
+ };
1760
+ availableUnits.push(foundUnit);
1761
+ import_node_server_utils8.logger.info(
1762
+ `Successfully created missing unit "${unitName}" for area "${areaName}"`
1763
+ );
1764
+ } catch (error) {
1765
+ import_node_server_utils8.logger.warn(
1766
+ `Unit "${unitName}" could not be created for area "${areaName}": ${error.message}`
1767
+ );
1768
+ }
1769
+ }
1756
1770
  if (foundUnit) {
1757
1771
  areaUnits.push({
1758
1772
  unit: foundUnit._id,
1759
1773
  name: foundUnit.name
1760
1774
  });
1761
- } else {
1762
- import_node_server_utils8.logger.warn(
1763
- `Unit "${unitName}" not found in site for area "${areaName}"`
1764
- );
1765
1775
  }
1766
1776
  }
1767
1777
  if (areaUnits.length > 0) {
@@ -2148,6 +2158,13 @@ function useUnitService() {
2148
2158
  updateUnit: _updateUnit,
2149
2159
  deleteUnit: _deleteUnit
2150
2160
  } = useUnitRepository();
2161
+ function getUnitNamesFromRow(row) {
2162
+ const unitValue = row?.UNIT ?? row?.UNITS;
2163
+ if (!unitValue) {
2164
+ return [];
2165
+ }
2166
+ return String(unitValue).split(",").map((unit) => unit.trim()).filter((unit) => unit && !unit.startsWith("Sample:"));
2167
+ }
2151
2168
  async function importUnit({
2152
2169
  dataJson,
2153
2170
  site,
@@ -2173,36 +2190,35 @@ function useUnitService() {
2173
2190
  try {
2174
2191
  for (let i = 0; i < dataArray.length; i++) {
2175
2192
  const row = dataArray[i];
2176
- if (!row?.UNIT) {
2177
- import_node_server_utils11.logger.warn(`Skipping row ${i + 1} with missing UNIT:`, row);
2178
- skippedRows.push(i + 1);
2179
- continue;
2180
- }
2181
- const unitName = String(row.UNIT).trim();
2182
- if (!unitName) {
2183
- import_node_server_utils11.logger.warn(`Skipping row ${i + 1} with empty unit name`);
2193
+ const unitNames = getUnitNamesFromRow(row);
2194
+ if (unitNames.length === 0) {
2195
+ import_node_server_utils11.logger.warn(`Skipping row ${i + 1} with missing UNIT/UNITS:`, row);
2184
2196
  skippedRows.push(i + 1);
2185
2197
  continue;
2186
2198
  }
2187
- if (unitName.startsWith("Sample:")) {
2188
- import_node_server_utils11.logger.warn(`Skipping row ${i + 1} with sample unit: ${unitName}`);
2189
- skippedRows.push(i + 1);
2190
- continue;
2191
- }
2192
- try {
2193
- const insertedId = await _createUnit({
2194
- name: unitName,
2195
- site,
2196
- serviceType
2197
- });
2198
- insertedUnitIds.push(insertedId);
2199
- import_node_server_utils11.logger.info(`Successfully created unit: ${unitName}`);
2200
- } catch (error) {
2201
- import_node_server_utils11.logger.error(`Error creating unit "${unitName}": ${error.message}`);
2202
- if (error.message.includes("Unit already exists")) {
2203
- duplicateUnits.push(unitName);
2204
- } else {
2205
- failedUnits.push(unitName);
2199
+ const uniqueUnitNames = Array.from(
2200
+ new Set(unitNames.map((unitName) => unitName.toLowerCase()))
2201
+ ).map(
2202
+ (lowerUnitName) => unitNames.find(
2203
+ (unitName) => unitName.toLowerCase() === lowerUnitName
2204
+ )
2205
+ );
2206
+ for (const unitName of uniqueUnitNames) {
2207
+ try {
2208
+ const insertedId = await _createUnit({
2209
+ name: unitName,
2210
+ site,
2211
+ serviceType
2212
+ });
2213
+ insertedUnitIds.push(insertedId);
2214
+ import_node_server_utils11.logger.info(`Successfully created unit: ${unitName}`);
2215
+ } catch (error) {
2216
+ import_node_server_utils11.logger.error(`Error creating unit "${unitName}": ${error.message}`);
2217
+ if (error.message.includes("Unit already exists")) {
2218
+ duplicateUnits.push(unitName);
2219
+ } else {
2220
+ failedUnits.push(unitName);
2221
+ }
2206
2222
  }
2207
2223
  }
2208
2224
  }
@@ -2555,7 +2571,7 @@ function MParentChecklist(value) {
2555
2571
 
2556
2572
  // src/repositories/hygiene-parent-checklist.repository.ts
2557
2573
  var import_mongodb7 = require("mongodb");
2558
- var import_moment_timezone2 = __toESM(require("moment-timezone"));
2574
+ var import_moment_timezone3 = __toESM(require("moment-timezone"));
2559
2575
  var import_node_server_utils14 = require("@7365admin1/node-server-utils");
2560
2576
  var import_core7 = require("@7365admin1/core");
2561
2577
  function useParentChecklistRepo() {
@@ -2851,11 +2867,11 @@ function useParentChecklistRepo() {
2851
2867
  cacheOptions.search = search;
2852
2868
  }
2853
2869
  const singaporeTz = "Asia/Singapore";
2854
- const normalizedStartDate = typeof startDate === "string" ? startDate : (0, import_moment_timezone2.default)(startDate).tz(singaporeTz).format("YYYY-MM-DD");
2855
- const normalizedEndDate = typeof endDate === "string" ? endDate : (0, import_moment_timezone2.default)(endDate).tz(singaporeTz).format("YYYY-MM-DD");
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");
2856
2872
  if (startDate && endDate) {
2857
- const startOfDay = import_moment_timezone2.default.tz(normalizedStartDate, "YYYY-MM-DD", singaporeTz).startOf("day").toDate();
2858
- const endOfDay = import_moment_timezone2.default.tz(normalizedEndDate, "YYYY-MM-DD", singaporeTz).endOf("day").toDate();
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();
2859
2875
  query.createdAt = {
2860
2876
  $gte: startOfDay,
2861
2877
  $lte: endOfDay
@@ -2864,12 +2880,12 @@ function useParentChecklistRepo() {
2864
2880
  cacheOptions.endDate = normalizedEndDate;
2865
2881
  } else if (startDate) {
2866
2882
  query.createdAt = {
2867
- $gte: import_moment_timezone2.default.tz(normalizedStartDate, "YYYY-MM-DD", singaporeTz).startOf("day").toDate()
2883
+ $gte: import_moment_timezone3.default.tz(normalizedStartDate, "YYYY-MM-DD", singaporeTz).startOf("day").toDate()
2868
2884
  };
2869
2885
  cacheOptions.startDate = normalizedStartDate;
2870
2886
  } else if (endDate) {
2871
2887
  query.createdAt = {
2872
- $lte: import_moment_timezone2.default.tz(normalizedEndDate, "YYYY-MM-DD", singaporeTz).endOf("day").toDate()
2888
+ $lte: import_moment_timezone3.default.tz(normalizedEndDate, "YYYY-MM-DD", singaporeTz).endOf("day").toDate()
2873
2889
  };
2874
2890
  cacheOptions.endDate = normalizedEndDate;
2875
2891
  }