@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.
- package/.changeset/inventory-notification-category.md +5 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +158 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +158 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/scripts/hid-backfill-enrolment-access.mjs +198 -0
- package/test/e2e/dashboard-inventory-low-stock.e2e.test.mjs +59 -0
- package/test/hid-enrolment-grants-access.test.mjs +162 -0
- package/test/notification-category.util.test.mjs +21 -1
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>
|
|
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
|
-
...
|
|
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
|
-
|
|
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:
|
|
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,
|