@7365admin1/core 3.75.1 → 3.77.0

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
@@ -8925,6 +8925,20 @@ var NOTIFICATION_CATEGORIES = [
8925
8925
  vendorDefault: true,
8926
8926
  rendered: true
8927
8927
  },
8928
+ // Inventory (2026-09-17). Sent from `iservice365-API-core`'s inventory
8929
+ // module: a stock request waiting for approval and stock falling to its alert
8930
+ // level go to that store's staff; a decided request goes to the person who
8931
+ // asked. The caller checks `rendered` before it sends, so the sender and this
8932
+ // switch cannot land out of order.
8933
+ {
8934
+ key: "inventory",
8935
+ label: "Inventory",
8936
+ description: "When a stock request needs your approval or has been decided, or stock you look after runs low.",
8937
+ channels: { inApp: true, push: true, email: false },
8938
+ channelNote: { email: NO_EMAIL_RECIPIENT },
8939
+ permissionResources: ["inventory"],
8940
+ rendered: true
8941
+ },
8928
8942
  {
8929
8943
  key: "serviceProviderInvite",
8930
8944
  label: "Service Provider Invitations",
@@ -21240,6 +21254,37 @@ function signPlaybackLink(params) {
21240
21254
  expiresAt: new Date(exp * 1e3).toISOString()
21241
21255
  };
21242
21256
  }
21257
+ function signLiveLink(host, {
21258
+ relays = playbackRelays(),
21259
+ now = /* @__PURE__ */ new Date()
21260
+ } = {}) {
21261
+ const parsed = host ? parseCameraHost(host) : null;
21262
+ if (!parsed || parsed.channel === null)
21263
+ return null;
21264
+ const key = relays[parsed.authority];
21265
+ if (!key)
21266
+ return null;
21267
+ const exp = Math.floor(now.getTime() / 1e3) + PLAYBACK_LINK_SECONDS;
21268
+ const channel = String(parsed.channel);
21269
+ const sig = createHmac("sha256", key).update(`${channel}|${exp}`).digest("hex");
21270
+ const liveAuth = `exp=${exp}&sig=${sig}`;
21271
+ return {
21272
+ /** The player page, ready for an iframe or a WebView. */
21273
+ liveUrl: `https://${parsed.authority}/${channel}?${liveAuth}`,
21274
+ /**
21275
+ * The same signature, on its own, to append to any of the relay's live
21276
+ * routes — the socket, the still, the clip. One signature serves all four
21277
+ * because it covers the CHANNEL and not the path, which is what lets
21278
+ * `insertScreenshot` keep splicing `screenshot` into the path.
21279
+ */
21280
+ liveAuth,
21281
+ liveExpiresAt: new Date(exp * 1e3).toISOString()
21282
+ };
21283
+ }
21284
+ function withLiveLink(fields, options) {
21285
+ const live = signLiveLink(fields.host, options);
21286
+ return live ? { ...fields, ...live } : fields;
21287
+ }
21243
21288
 
21244
21289
  // src/utils/camera-capability.util.ts
21245
21290
  var CAMERA_CAPABILITIES = [
@@ -22595,7 +22640,9 @@ function useCameraViewService() {
22595
22640
  // player-page URL, which is the one picture path proven to work, and the
22596
22641
  // plain `GET /site-cameras` already returns it to every authenticated
22597
22642
  // caller. The credential fields are still never included.
22598
- ...publicCameraFields({ ...camera, siteName: site?.name }),
22643
+ ...withLiveLink(
22644
+ publicCameraFields({ ...camera, siteName: site?.name })
22645
+ ),
22599
22646
  // Answered before anything is requested, so a camera with no address or
22600
22647
  // an inactive one renders with a reason and costs zero device requests.
22601
22648
  // (An ANPR unit never reaches here — it is filtered out above.)
@@ -22638,7 +22685,7 @@ function useCameraViewService() {
22638
22685
  async function probeCamera(camera, cacheSeconds) {
22639
22686
  const id = camera?._id?.toString?.() ?? "";
22640
22687
  const identity = {
22641
- ...publicCameraFields(camera),
22688
+ ...withLiveLink(publicCameraFields(camera)),
22642
22689
  unavailableReason: snapshotRefusalReason(camera)
22643
22690
  };
22644
22691
  const lastFrameAt = await getCache(`lastframe:${id}`) || null;
@@ -22772,7 +22819,7 @@ function useCameraViewService() {
22772
22819
  );
22773
22820
  const { capabilities } = capabilitiesOf(camera, probe);
22774
22821
  const base = {
22775
- camera: publicCameraFields(camera),
22822
+ camera: withLiveLink(publicCameraFields(camera)),
22776
22823
  snapshotSupported: refusal === null,
22777
22824
  capabilities,
22778
22825
  // Filled in from the probe when the device answers. Today it does not, so
@@ -37981,6 +38028,87 @@ function useVehicleController() {
37981
38028
  next(error);
37982
38029
  }
37983
38030
  }
38031
+ const MAX_BULK_VEHICLE_ROWS = 100;
38032
+ async function addBulk(req, res, next) {
38033
+ const rowSchema = Joi57.object({
38034
+ // The sheet's own row id. Opaque to the server, handed straight back.
38035
+ id: Joi57.string().trim().max(64).required(),
38036
+ block: Joi57.string().hex().length(24).required(),
38037
+ level: Joi57.string().hex().length(24).required(),
38038
+ unit: Joi57.string().hex().length(24).required(),
38039
+ name: Joi57.string().trim().min(1).required(),
38040
+ plateNumber: Joi57.string().trim().min(1).required(),
38041
+ nric: Joi57.string().trim().optional().allow("", null),
38042
+ phoneNumber: Joi57.string().trim().optional().allow("", null)
38043
+ });
38044
+ const schema2 = Joi57.object({
38045
+ site: Joi57.string().hex().length(24).required(),
38046
+ org: Joi57.string().hex().length(24).required(),
38047
+ category: Joi57.string().optional().valid(...Object.values(VehicleCategory)).default("resident" /* RESIDENT */),
38048
+ rows: Joi57.array().items(rowSchema).min(1).max(MAX_BULK_VEHICLE_ROWS).required()
38049
+ });
38050
+ const { error, value } = schema2.validate(req.body, { abortEarly: false });
38051
+ if (error) {
38052
+ const messages = error.details.map((d) => d.message).join(", ");
38053
+ logger79.log({ level: "error", message: messages });
38054
+ next(new BadRequestError102(messages));
38055
+ return;
38056
+ }
38057
+ try {
38058
+ await requireSiteReach(req, value.site);
38059
+ const user = req.user?.user || req.user?._id;
38060
+ const results = [];
38061
+ for (const row of value.rows) {
38062
+ try {
38063
+ const message = await _add(
38064
+ {
38065
+ site: value.site,
38066
+ org: value.org,
38067
+ type: "whitelist" /* WHITELIST */,
38068
+ category: value.category,
38069
+ name: row.name,
38070
+ nric: row.nric || "",
38071
+ phoneNumber: row.phoneNumber || "",
38072
+ block: row.block,
38073
+ level: row.level,
38074
+ unit: row.unit,
38075
+ plateNumber: [row.plateNumber]
38076
+ },
38077
+ void 0,
38078
+ user
38079
+ );
38080
+ results.push({
38081
+ id: row.id,
38082
+ ok: true,
38083
+ message: String(message ?? "Vehicle added successfully.")
38084
+ });
38085
+ } catch (rowError) {
38086
+ logger79.log({
38087
+ level: "error",
38088
+ message: `bulk vehicle row ${row.id}: ${rowError?.message ?? rowError}`
38089
+ });
38090
+ results.push({
38091
+ id: row.id,
38092
+ ok: false,
38093
+ message: String(
38094
+ rowError?.message ?? "The vehicle could not be added."
38095
+ )
38096
+ });
38097
+ }
38098
+ }
38099
+ const added = results.filter((result) => result.ok).length;
38100
+ const failed = results.length - added;
38101
+ res.status(200).json({
38102
+ message: failed === 0 ? `${added} vehicle${added === 1 ? "" : "s"} added.` : `${added} of ${results.length} vehicles added; ${failed} could not be added.`,
38103
+ added,
38104
+ failed,
38105
+ results
38106
+ });
38107
+ } catch (error2) {
38108
+ logger79.log({ level: "error", message: error2.message });
38109
+ next(error2);
38110
+ }
38111
+ }
37984
38112
  async function add(req, res, next) {
37985
38113
  const { error, value } = vehicleSchema.validate(req.body, {
37986
38114
  abortEarly: false
@@ -38456,6 +38584,7 @@ function useVehicleController() {
38456
38584
  }
38457
38585
  return {
38458
38586
  add,
38587
+ addBulk,
38459
38588
  getVehicles,
38460
38589
  getSeasonPassTypes,
38461
38590
  getVehicleById,
@@ -76363,6 +76492,7 @@ function highSeverityIncidentCount(severities) {
76363
76492
  ).length;
76364
76493
  }
76365
76494
  var SUPPLY_BASIS_LOWEST_QUANTITY = "lowest-quantity";
76495
+ var SUPPLY_BASIS_BELOW_ALERT_LEVEL = "below-alert-level";
76366
76496
  var SUPPLY_THRESHOLD_NOT_IN_MODEL = "Supplies do not record a stock threshold, so these are the lowest-quantity items rather than items below a level.";
76367
76497
 
76368
76498
  // src/repositories/new-dashboard.repo.ts
@@ -77221,6 +77351,32 @@ function useNewDashboardRepo() {
77221
77351
  { $unwind: { path: "$scheduleDoc", preserveNullAndEmptyArrays: false } }
77222
77352
  ];
77223
77353
  }
77354
+ async function lowStockItems(site, serviceType) {
77355
+ const stores = await db2.collection("inventory.stores").find({ sites: site, status: "active" }, { projection: { _id: 1 } }).toArray();
77356
+ if (stores.length) {
77357
+ const items2 = await db2.collection("inventory.items").aggregate([
77358
+ {
77359
+ $match: {
77360
+ store: { $in: stores.map((store) => store._id) },
77361
+ status: "active",
77362
+ reorderLevel: { $gt: 0 },
77363
+ $expr: { $lte: ["$onHand", "$reorderLevel"] }
77364
+ }
77365
+ },
77366
+ { $sort: { onHand: 1 } },
77367
+ { $limit: 5 },
77368
+ { $project: { _id: 1, name: 1, qty: "$onHand" } }
77369
+ ]).toArray();
77370
+ return { items: items2, basis: SUPPLY_BASIS_BELOW_ALERT_LEVEL };
77371
+ }
77372
+ const items = await supplyCollection.aggregate([
77373
+ { $match: { site, serviceType, status: "active" } },
77374
+ { $sort: { qty: 1 } },
77375
+ { $limit: 3 },
77376
+ { $project: { _id: 1, name: 1, qty: 1 } }
77377
+ ]).toArray();
77378
+ return { items, basis: SUPPLY_BASIS_LOWEST_QUANTITY };
77379
+ }
77224
77380
  async function getServiceDashboard({
77225
77381
  site,
77226
77382
  serviceType,
@@ -77278,12 +77434,7 @@ function useNewDashboardRepo() {
77278
77434
  }
77279
77435
  }
77280
77436
  ]).toArray(),
77281
- supplyCollection.aggregate([
77282
- { $match: { site, serviceType, status: "active" } },
77283
- { $sort: { qty: 1 } },
77284
- { $limit: 3 },
77285
- { $project: { _id: 1, name: 1, qty: 1 } }
77286
- ]).toArray(),
77437
+ lowStockItems(site, serviceType),
77287
77438
  areaChecklistCollection.aggregate([
77288
77439
  ...areaChecklistScheduleLookup(site, serviceType, periodRange),
77289
77440
  {
@@ -77345,7 +77496,7 @@ function useNewDashboardRepo() {
77345
77496
  )
77346
77497
  },
77347
77498
  supplyAlert: {
77348
- items: supplyAlertReport.map((item) => ({
77499
+ items: supplyAlertReport.items.map((item) => ({
77349
77500
  name: item.name,
77350
77501
  qty: item.qty
77351
77502
  })),
@@ -77353,8 +77504,8 @@ function useNewDashboardRepo() {
77353
77504
  // with no threshold, because `site.supplies` records no stock level to
77354
77505
  // compare against. Naming the ordering stops 900 gloves being drawn as
77355
77506
  // a warning. No threshold is invented here.
77356
- basis: SUPPLY_BASIS_LOWEST_QUANTITY,
77357
- thresholdUnavailableReason: SUPPLY_THRESHOLD_NOT_IN_MODEL
77507
+ basis: supplyAlertReport.basis,
77508
+ ...supplyAlertReport.basis === SUPPLY_BASIS_LOWEST_QUANTITY ? { thresholdUnavailableReason: SUPPLY_THRESHOLD_NOT_IN_MODEL } : {}
77358
77509
  },
77359
77510
  taskCompleted: {
77360
77511
  completed: tFacet.completed[0]?.count ?? 0,
@@ -91783,6 +91934,7 @@ export {
91783
91934
  sessionSchema,
91784
91935
  setIO,
91785
91936
  shiftSchema,
91937
+ signLiveLink,
91786
91938
  signPlaybackLink,
91787
91939
  siteDayBounds,
91788
91940
  siteModulesHandlers,
@@ -92033,6 +92185,7 @@ export {
92033
92185
  vehicles_namespace_collection,
92034
92186
  visitors_namespace_collection,
92035
92187
  wallConfig,
92188
+ withLiveLink,
92036
92189
  workOrderSchema,
92037
92190
  work_orders2_namespace_collection,
92038
92191
  work_orders_namespace_collection