@7365admin1/core 3.72.1-staging.325 → 3.72.1-staging.327

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;
@@ -13592,7 +13592,9 @@ declare function useHidAmicoService(): {
13592
13592
  hasMore: boolean;
13593
13593
  pageRange: string;
13594
13594
  }>;
13595
- createIdentity: (readerId: string, value: Partial<THidAmicoIdentity>) => Promise<{
13595
+ createIdentity: (readerId: string, value: Partial<THidAmicoIdentity> & {
13596
+ orgId?: string;
13597
+ }) => Promise<{
13596
13598
  _id: bson.ObjectId;
13597
13599
  reader: bson.ObjectId;
13598
13600
  site: bson.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",
@@ -44051,6 +44065,15 @@ var schemaHidAmicoIdentity = import_joi66.default.object({
44051
44065
  }).or("hidUserId", "registration", "cardNo");
44052
44066
  var schemaCreateHidAmicoIdentity = import_joi66.default.object({
44053
44067
  site: objectIdSchema2.optional(),
44068
+ /*
44069
+ * Which organisation's HID permission document this enrolment's access grant
44070
+ * belongs in. Optional so existing callers keep working - the service falls
44071
+ * back to the site's own organisation - but a managing agent should send its
44072
+ * own org, because a site reached through `customer.sites` would otherwise
44073
+ * get a second document and the permissions screen would show the person as
44074
+ * unassigned while the door let them through.
44075
+ */
44076
+ orgId: objectIdSchema2.optional(),
44054
44077
  hidUserId: import_joi66.default.alternatives(import_joi66.default.string(), import_joi66.default.number()).optional().allow(null, ""),
44055
44078
  registration: import_joi66.default.string().trim().optional().allow(""),
44056
44079
  cardNo: import_joi66.default.string().optional().allow(null, ""),
@@ -45811,6 +45834,27 @@ function resolveHidPhysicalCardValue(input) {
45811
45834
  return normalizeHidCardValue(input.cardValue);
45812
45835
  }
45813
45836
 
45837
+ // src/utils/hid-assignment.util.ts
45838
+ function permissionSubjectOfIdentity(value) {
45839
+ if (value.person) {
45840
+ return { subjectId: String(value.person), category: "resident" };
45841
+ }
45842
+ if (value.member) {
45843
+ return { subjectId: String(value.member), category: "property_management" };
45844
+ }
45845
+ if (value.serviceProvider) {
45846
+ return { subjectId: String(value.serviceProvider), category: "service_provider" };
45847
+ }
45848
+ return null;
45849
+ }
45850
+ function assignmentNamesSubject(item, subject, scope) {
45851
+ if (String(item.subjectId ?? "") !== subject.subjectId)
45852
+ return false;
45853
+ if (item.category !== subject.category)
45854
+ return false;
45855
+ return item.readerId ? String(item.readerId) === scope.readerId : item.locationKey === scope.locationKey;
45856
+ }
45857
+
45814
45858
  // src/utils/hid-json.util.ts
45815
45859
  var import_node_crypto5 = __toESM(require("crypto"));
45816
45860
  var UINT64_MAX = 2n ** 64n - 1n;
@@ -48584,6 +48628,71 @@ function useHidAmicoService() {
48584
48628
  await client.logout();
48585
48629
  }
48586
48630
  }
48631
+ async function resolvePermissionOrgId(reader, requested) {
48632
+ if (requested)
48633
+ return String(requested);
48634
+ const site = await siteRepo.getSiteById(String(reader.site));
48635
+ const orgId = String(site?.org ?? "");
48636
+ if (!orgId) {
48637
+ throw new import_node_server_utils141.BadRequestError("This site has no organisation, so HID access cannot be granted.");
48638
+ }
48639
+ return orgId;
48640
+ }
48641
+ async function addEnrolmentAssignment(reader, orgId, subject) {
48642
+ const siteId = String(reader.site);
48643
+ const readerId = String(reader._id);
48644
+ const scope = await getPermissionScope(orgId, siteId, readerId);
48645
+ const current = await repo.getSitePermissions(orgId, siteId);
48646
+ const assignments = current.assignments ?? [];
48647
+ if (assignments.some((item) => assignmentNamesSubject(item, subject, scope))) {
48648
+ return null;
48649
+ }
48650
+ const [resolved] = await repo.resolvePermissionAssignments(siteId, [
48651
+ { ...subject, intercom: false }
48652
+ ]);
48653
+ if (!resolved) {
48654
+ throw new import_node_server_utils141.BadRequestError("This person cannot be granted access at this site.");
48655
+ }
48656
+ const next = [...assignments, {
48657
+ ...resolved,
48658
+ readerId: scope.readerId,
48659
+ location: scope.location,
48660
+ locationKey: scope.locationKey
48661
+ }];
48662
+ await repo.saveSitePermissions(orgId, siteId, next);
48663
+ return async () => {
48664
+ await repo.saveSitePermissions(orgId, siteId, assignments);
48665
+ };
48666
+ }
48667
+ async function removeEnrolmentAssignment(reader, subject) {
48668
+ const siteId = String(reader.site);
48669
+ const readerId = String(reader._id);
48670
+ const scope = {
48671
+ readerId,
48672
+ locationKey: normalizePermissionLocation(String(reader.location ?? ""))
48673
+ };
48674
+ const documents = await repo.listSitePermissionDocuments(siteId);
48675
+ const undos = [];
48676
+ for (const document2 of documents) {
48677
+ const assignments = document2.assignments ?? [];
48678
+ const next = assignments.filter((item) => !assignmentNamesSubject(item, subject, scope));
48679
+ if (next.length === assignments.length)
48680
+ continue;
48681
+ const orgId = String(document2.orgId ?? "");
48682
+ if (!orgId)
48683
+ continue;
48684
+ await repo.saveSitePermissions(orgId, siteId, next);
48685
+ undos.push(async () => {
48686
+ await repo.saveSitePermissions(orgId, siteId, assignments);
48687
+ });
48688
+ }
48689
+ if (!undos.length)
48690
+ return null;
48691
+ return async () => {
48692
+ for (const undo of undos)
48693
+ await undo();
48694
+ };
48695
+ }
48587
48696
  async function resolveSubjectUser(site, value) {
48588
48697
  const subject = value.person ? { subjectId: String(value.person), category: "resident" } : value.member ? { subjectId: String(value.member), category: "property_management" } : value.serviceProvider ? { subjectId: String(value.serviceProvider), category: "service_provider" } : null;
48589
48698
  if (!subject) {
@@ -48615,6 +48724,7 @@ function useHidAmicoService() {
48615
48724
  );
48616
48725
  }
48617
48726
  async function createIdentity(readerId, value) {
48727
+ const { orgId: requestedOrgId, ...identityValue } = value;
48618
48728
  const reader = await repo.getById(readerId);
48619
48729
  const linkedSubjects = [value.person, value.member, value.serviceProvider, value.visitor].filter(Boolean);
48620
48730
  if (linkedSubjects.length !== 1) {
@@ -48635,7 +48745,7 @@ function useHidAmicoService() {
48635
48745
  const subjectUser = await resolveSubjectUser(String(reader.site), value);
48636
48746
  await assertSubjectNotEnrolled(readerId, value, subjectUser);
48637
48747
  const identity = await repo.addIdentity({
48638
- ...value,
48748
+ ...identityValue,
48639
48749
  // Stored, not derived on read. It is how this row is recognised as the
48640
48750
  // same human as one written by a different screen, and a `site.people`
48641
48751
  // row can be re-pointed at another account after the fact.
@@ -48643,9 +48753,17 @@ function useHidAmicoService() {
48643
48753
  reader: readerId,
48644
48754
  site: value.site || reader?.site?.toString()
48645
48755
  });
48756
+ let undoAssignment = null;
48646
48757
  try {
48758
+ const subject = permissionSubjectOfIdentity(identityValue);
48759
+ if (subject) {
48760
+ const orgId = await resolvePermissionOrgId(reader, requestedOrgId);
48761
+ undoAssignment = await addEnrolmentAssignment(reader, orgId, subject);
48762
+ }
48647
48763
  await reconcileStoredReaderPermissions(readerId);
48648
48764
  } catch (error) {
48765
+ if (undoAssignment)
48766
+ await undoAssignment();
48649
48767
  await repo.deleteIdentity(identity._id);
48650
48768
  throw error;
48651
48769
  }
@@ -48706,9 +48824,17 @@ function useHidAmicoService() {
48706
48824
  async function deleteIdentity(identityId) {
48707
48825
  const current = await repo.getIdentityById(identityId);
48708
48826
  const identity = await repo.deleteIdentity(identityId);
48827
+ let undoAssignment = null;
48709
48828
  try {
48829
+ const subject = permissionSubjectOfIdentity(current);
48830
+ if (subject) {
48831
+ const reader = await repo.getById(String(current.reader));
48832
+ undoAssignment = await removeEnrolmentAssignment(reader, subject);
48833
+ }
48710
48834
  await reconcileStoredReaderPermissions(String(current.reader));
48711
48835
  } catch (error) {
48836
+ if (undoAssignment)
48837
+ await undoAssignment();
48712
48838
  await repo.updateIdentity(identityId, { status: current.status || "active", deletedAt: "" });
48713
48839
  throw error;
48714
48840
  }
@@ -77712,6 +77838,7 @@ function highSeverityIncidentCount(severities) {
77712
77838
  ).length;
77713
77839
  }
77714
77840
  var SUPPLY_BASIS_LOWEST_QUANTITY = "lowest-quantity";
77841
+ var SUPPLY_BASIS_BELOW_ALERT_LEVEL = "below-alert-level";
77715
77842
  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.";
77716
77843
 
77717
77844
  // src/repositories/new-dashboard.repo.ts
@@ -78570,6 +78697,32 @@ function useNewDashboardRepo() {
78570
78697
  { $unwind: { path: "$scheduleDoc", preserveNullAndEmptyArrays: false } }
78571
78698
  ];
78572
78699
  }
78700
+ async function lowStockItems(site, serviceType) {
78701
+ const stores = await db2.collection("inventory.stores").find({ sites: site, status: "active" }, { projection: { _id: 1 } }).toArray();
78702
+ if (stores.length) {
78703
+ const items2 = await db2.collection("inventory.items").aggregate([
78704
+ {
78705
+ $match: {
78706
+ store: { $in: stores.map((store) => store._id) },
78707
+ status: "active",
78708
+ reorderLevel: { $gt: 0 },
78709
+ $expr: { $lte: ["$onHand", "$reorderLevel"] }
78710
+ }
78711
+ },
78712
+ { $sort: { onHand: 1 } },
78713
+ { $limit: 5 },
78714
+ { $project: { _id: 1, name: 1, qty: "$onHand" } }
78715
+ ]).toArray();
78716
+ return { items: items2, basis: SUPPLY_BASIS_BELOW_ALERT_LEVEL };
78717
+ }
78718
+ const items = await supplyCollection.aggregate([
78719
+ { $match: { site, serviceType, status: "active" } },
78720
+ { $sort: { qty: 1 } },
78721
+ { $limit: 3 },
78722
+ { $project: { _id: 1, name: 1, qty: 1 } }
78723
+ ]).toArray();
78724
+ return { items, basis: SUPPLY_BASIS_LOWEST_QUANTITY };
78725
+ }
78573
78726
  async function getServiceDashboard({
78574
78727
  site,
78575
78728
  serviceType,
@@ -78627,12 +78780,7 @@ function useNewDashboardRepo() {
78627
78780
  }
78628
78781
  }
78629
78782
  ]).toArray(),
78630
- supplyCollection.aggregate([
78631
- { $match: { site, serviceType, status: "active" } },
78632
- { $sort: { qty: 1 } },
78633
- { $limit: 3 },
78634
- { $project: { _id: 1, name: 1, qty: 1 } }
78635
- ]).toArray(),
78783
+ lowStockItems(site, serviceType),
78636
78784
  areaChecklistCollection.aggregate([
78637
78785
  ...areaChecklistScheduleLookup(site, serviceType, periodRange),
78638
78786
  {
@@ -78694,7 +78842,7 @@ function useNewDashboardRepo() {
78694
78842
  )
78695
78843
  },
78696
78844
  supplyAlert: {
78697
- items: supplyAlertReport.map((item) => ({
78845
+ items: supplyAlertReport.items.map((item) => ({
78698
78846
  name: item.name,
78699
78847
  qty: item.qty
78700
78848
  })),
@@ -78702,8 +78850,8 @@ function useNewDashboardRepo() {
78702
78850
  // with no threshold, because `site.supplies` records no stock level to
78703
78851
  // compare against. Naming the ordering stops 900 gloves being drawn as
78704
78852
  // a warning. No threshold is invented here.
78705
- basis: SUPPLY_BASIS_LOWEST_QUANTITY,
78706
- thresholdUnavailableReason: SUPPLY_THRESHOLD_NOT_IN_MODEL
78853
+ basis: supplyAlertReport.basis,
78854
+ ...supplyAlertReport.basis === SUPPLY_BASIS_LOWEST_QUANTITY ? { thresholdUnavailableReason: SUPPLY_THRESHOLD_NOT_IN_MODEL } : {}
78707
78855
  },
78708
78856
  taskCompleted: {
78709
78857
  completed: tFacet.completed[0]?.count ?? 0,