@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/CHANGELOG.md +19 -0
- package/dist/index.d.ts +66 -5
- package/dist/index.js +167 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +165 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/camera-live-link.test.mjs +163 -0
- package/test/e2e/dashboard-inventory-low-stock.e2e.test.mjs +59 -0
- package/test/notification-category.util.test.mjs +21 -1
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
|
-
...
|
|
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
|
-
|
|
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:
|
|
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
|