@7365admin1/core 3.72.1-staging.326 → 3.72.1-staging.328

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.
@@ -0,0 +1,5 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Add the `inventory` notification category (in-app and push), offered to roles holding any `inventory:*` permission, for API-core's inventory alerts: a request waiting for approval, a decided request, and low stock.
package/dist/index.d.ts CHANGED
@@ -10567,12 +10567,12 @@ declare function useNewDashboardRepo(): {
10567
10567
  percentage: number | null;
10568
10568
  };
10569
10569
  supplyAlert: {
10570
+ thresholdUnavailableReason?: string | undefined;
10570
10571
  items: {
10571
10572
  name: any;
10572
10573
  qty: any;
10573
10574
  }[];
10574
- basis: "lowest-quantity";
10575
- thresholdUnavailableReason: string;
10575
+ basis: "lowest-quantity" | "below-alert-level";
10576
10576
  };
10577
10577
  taskCompleted: {
10578
10578
  completed: any;
@@ -13237,6 +13237,8 @@ declare function useHidAmicoRepo(): {
13237
13237
  findIdentitiesByHidUserIds: (reader: string | ObjectId, hidUserIds: Array<string | number>) => Promise<mongodb.WithId<bson.Document>[]>;
13238
13238
  acquireEnrollmentLock: (reader: string | ObjectId, owner: string, ttlSeconds: number) => Promise<boolean>;
13239
13239
  releaseEnrollmentLock: (reader: string | ObjectId, owner?: string) => Promise<boolean>;
13240
+ setEnrollmentSession: (reader: string | ObjectId, owner: string, session: string) => Promise<boolean>;
13241
+ getEnrollmentSession: (reader: string | ObjectId) => Promise<string>;
13240
13242
  assertOrganizationSiteAccess: (site: string | ObjectId, org: string | ObjectId) => Promise<{
13241
13243
  siteId: ObjectId;
13242
13244
  orgId: ObjectId;
package/dist/index.js CHANGED
@@ -9680,6 +9680,20 @@ var NOTIFICATION_CATEGORIES = [
9680
9680
  vendorDefault: true,
9681
9681
  rendered: true
9682
9682
  },
9683
+ // Inventory (2026-09-17). Sent from `iservice365-API-core`'s inventory
9684
+ // module: a stock request waiting for approval and stock falling to its alert
9685
+ // level go to that store's staff; a decided request goes to the person who
9686
+ // asked. The caller checks `rendered` before it sends, so the sender and this
9687
+ // switch cannot land out of order.
9688
+ {
9689
+ key: "inventory",
9690
+ label: "Inventory",
9691
+ description: "When a stock request needs your approval or has been decided, or stock you look after runs low.",
9692
+ channels: { inApp: true, push: true, email: false },
9693
+ channelNote: { email: NO_EMAIL_RECIPIENT },
9694
+ permissionResources: ["inventory"],
9695
+ rendered: true
9696
+ },
9683
9697
  {
9684
9698
  key: "serviceProviderInvite",
9685
9699
  label: "Service Provider Invitations",
@@ -45115,6 +45129,20 @@ function useHidAmicoRepo() {
45115
45129
  throw error;
45116
45130
  }
45117
45131
  }
45132
+ async function setEnrollmentSession(reader, owner, session) {
45133
+ const result = await enrollmentLocks().updateOne(
45134
+ { _id: toId3(reader, "reader ID"), owner },
45135
+ { $set: { session, sessionAt: /* @__PURE__ */ new Date() } }
45136
+ );
45137
+ return result.matchedCount > 0;
45138
+ }
45139
+ async function getEnrollmentSession(reader) {
45140
+ const lock = await enrollmentLocks().findOne({
45141
+ _id: toId3(reader, "reader ID"),
45142
+ expiresAt: { $gt: /* @__PURE__ */ new Date() }
45143
+ });
45144
+ return String(lock?.session ?? "");
45145
+ }
45118
45146
  async function releaseEnrollmentLock(reader, owner) {
45119
45147
  const result = await enrollmentLocks().deleteOne({
45120
45148
  _id: toId3(reader, "reader ID"),
@@ -45673,6 +45701,8 @@ function useHidAmicoRepo() {
45673
45701
  findIdentitiesByHidUserIds,
45674
45702
  acquireEnrollmentLock,
45675
45703
  releaseEnrollmentLock,
45704
+ setEnrollmentSession,
45705
+ getEnrollmentSession,
45676
45706
  assertOrganizationSiteAccess,
45677
45707
  getSitePermissions,
45678
45708
  listSitePermissionDocuments,
@@ -46921,6 +46951,8 @@ var HidAmicoClient = class {
46921
46951
  constructor(reader) {
46922
46952
  this.reader = reader;
46923
46953
  this.session = "";
46954
+ /** True while `session` belongs to another request. See `adoptSession`. */
46955
+ this.borrowedSession = false;
46924
46956
  const relayAuth = getRelayAuth();
46925
46957
  this.http = import_axios.default.create({
46926
46958
  baseURL: normalizeHidReaderBaseUrl(reader.baseUrl),
@@ -46980,6 +47012,7 @@ var HidAmicoClient = class {
46980
47012
  throw new import_node_server_utils141.BadRequestError("HID Amico login failed: missing session.");
46981
47013
  }
46982
47014
  this.session = res.data.session;
47015
+ this.borrowedSession = false;
46983
47016
  return this.session;
46984
47017
  }
46985
47018
  async sessionIsValid() {
@@ -47132,10 +47165,34 @@ var HidAmicoClient = class {
47132
47165
  verified: true
47133
47166
  };
47134
47167
  }
47168
+ /**
47169
+ * Take over a session somebody else opened, without logging in.
47170
+ *
47171
+ * For the cancel path only. While an enrolment is listening the reader will
47172
+ * not serve a second `hidlogin.fcgi` inside the client's 15 second timeout, so
47173
+ * the cancel has to ride the enrolment's session or it never reaches the
47174
+ * device at all.
47175
+ *
47176
+ * A borrowed session is NOT this client's to close - see `logout`.
47177
+ */
47178
+ adoptSession(session) {
47179
+ this.session = String(session ?? "");
47180
+ this.borrowedSession = Boolean(this.session);
47181
+ return this.session;
47182
+ }
47183
+ /** Whether this client may end the session it is holding. */
47184
+ get ownsSession() {
47185
+ return Boolean(this.session) && !this.borrowedSession;
47186
+ }
47135
47187
  async logout() {
47136
47188
  if (!this.session) {
47137
47189
  return;
47138
47190
  }
47191
+ if (this.borrowedSession) {
47192
+ this.session = "";
47193
+ this.borrowedSession = false;
47194
+ return;
47195
+ }
47139
47196
  const logoutUrl = this.url("/logout.fcgi");
47140
47197
  try {
47141
47198
  this.session = "";
@@ -49852,7 +49909,8 @@ function useHidAmicoService() {
49852
49909
  throw new import_node_server_utils141.BadRequestError("Another physical HID card enrollment is already running on this reader.");
49853
49910
  }
49854
49911
  try {
49855
- await client.login();
49912
+ const session = await client.login();
49913
+ await repo.setEnrollmentSession(readerId, lockOwner, session);
49856
49914
  await assertHidUserExists(client, hidUserId);
49857
49915
  await ensurePhysicalCardIdentification(client);
49858
49916
  const beforeRows = await loadPhysicalCardRows(client, hidUserId);
@@ -49933,15 +49991,20 @@ function useHidAmicoService() {
49933
49991
  async function cancelUserCardEnrollment(readerId) {
49934
49992
  const reader = await getActiveReader(readerId);
49935
49993
  const client = new HidAmicoClient(reader);
49994
+ const runningSession = await repo.getEnrollmentSession(readerId);
49936
49995
  try {
49937
- await client.login();
49996
+ if (runningSession) {
49997
+ client.adoptSession(runningSession);
49998
+ } else {
49999
+ await client.login();
50000
+ }
49938
50001
  await client.cancelRemoteEnrollment();
49939
50002
  const enrollmentWasRunning = await repo.releaseEnrollmentLock(readerId);
49940
50003
  await repo.addEvent({
49941
50004
  reader: readerId,
49942
50005
  site: reader.site,
49943
50006
  type: "user_card_enrollment_cancelled",
49944
- payload: { enrollmentWasRunning }
50007
+ payload: { enrollmentWasRunning, borrowedSession: Boolean(runningSession) }
49945
50008
  });
49946
50009
  return { readerId, cancelled: true };
49947
50010
  } finally {
@@ -77824,6 +77887,7 @@ function highSeverityIncidentCount(severities) {
77824
77887
  ).length;
77825
77888
  }
77826
77889
  var SUPPLY_BASIS_LOWEST_QUANTITY = "lowest-quantity";
77890
+ var SUPPLY_BASIS_BELOW_ALERT_LEVEL = "below-alert-level";
77827
77891
  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.";
77828
77892
 
77829
77893
  // src/repositories/new-dashboard.repo.ts
@@ -78682,6 +78746,32 @@ function useNewDashboardRepo() {
78682
78746
  { $unwind: { path: "$scheduleDoc", preserveNullAndEmptyArrays: false } }
78683
78747
  ];
78684
78748
  }
78749
+ async function lowStockItems(site, serviceType) {
78750
+ const stores = await db2.collection("inventory.stores").find({ sites: site, status: "active" }, { projection: { _id: 1 } }).toArray();
78751
+ if (stores.length) {
78752
+ const items2 = await db2.collection("inventory.items").aggregate([
78753
+ {
78754
+ $match: {
78755
+ store: { $in: stores.map((store) => store._id) },
78756
+ status: "active",
78757
+ reorderLevel: { $gt: 0 },
78758
+ $expr: { $lte: ["$onHand", "$reorderLevel"] }
78759
+ }
78760
+ },
78761
+ { $sort: { onHand: 1 } },
78762
+ { $limit: 5 },
78763
+ { $project: { _id: 1, name: 1, qty: "$onHand" } }
78764
+ ]).toArray();
78765
+ return { items: items2, basis: SUPPLY_BASIS_BELOW_ALERT_LEVEL };
78766
+ }
78767
+ const items = await supplyCollection.aggregate([
78768
+ { $match: { site, serviceType, status: "active" } },
78769
+ { $sort: { qty: 1 } },
78770
+ { $limit: 3 },
78771
+ { $project: { _id: 1, name: 1, qty: 1 } }
78772
+ ]).toArray();
78773
+ return { items, basis: SUPPLY_BASIS_LOWEST_QUANTITY };
78774
+ }
78685
78775
  async function getServiceDashboard({
78686
78776
  site,
78687
78777
  serviceType,
@@ -78739,12 +78829,7 @@ function useNewDashboardRepo() {
78739
78829
  }
78740
78830
  }
78741
78831
  ]).toArray(),
78742
- supplyCollection.aggregate([
78743
- { $match: { site, serviceType, status: "active" } },
78744
- { $sort: { qty: 1 } },
78745
- { $limit: 3 },
78746
- { $project: { _id: 1, name: 1, qty: 1 } }
78747
- ]).toArray(),
78832
+ lowStockItems(site, serviceType),
78748
78833
  areaChecklistCollection.aggregate([
78749
78834
  ...areaChecklistScheduleLookup(site, serviceType, periodRange),
78750
78835
  {
@@ -78806,7 +78891,7 @@ function useNewDashboardRepo() {
78806
78891
  )
78807
78892
  },
78808
78893
  supplyAlert: {
78809
- items: supplyAlertReport.map((item) => ({
78894
+ items: supplyAlertReport.items.map((item) => ({
78810
78895
  name: item.name,
78811
78896
  qty: item.qty
78812
78897
  })),
@@ -78814,8 +78899,8 @@ function useNewDashboardRepo() {
78814
78899
  // with no threshold, because `site.supplies` records no stock level to
78815
78900
  // compare against. Naming the ordering stops 900 gloves being drawn as
78816
78901
  // a warning. No threshold is invented here.
78817
- basis: SUPPLY_BASIS_LOWEST_QUANTITY,
78818
- thresholdUnavailableReason: SUPPLY_THRESHOLD_NOT_IN_MODEL
78902
+ basis: supplyAlertReport.basis,
78903
+ ...supplyAlertReport.basis === SUPPLY_BASIS_LOWEST_QUANTITY ? { thresholdUnavailableReason: SUPPLY_THRESHOLD_NOT_IN_MODEL } : {}
78819
78904
  },
78820
78905
  taskCompleted: {
78821
78906
  completed: tFacet.completed[0]?.count ?? 0,