@7365admin1/layer-common 3.2.1 → 3.2.2-staging.101
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/camera-wall-gated-endpoint.md +47 -0
- package/.changeset/camera-wall-plain-english.md +60 -0
- package/.changeset/camera-wall-selection-and-guidance.md +65 -0
- package/.changeset/dark-primary-contrast.md +39 -0
- package/.changeset/dashboard-dark-theme-contrast.md +15 -0
- package/.changeset/notification-preferences.md +17 -0
- package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
- package/.changeset/service-provider-invitation-actions.md +28 -0
- package/assets/css/primitives.css +680 -0
- package/assets/css/screens.css +541 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +220 -0
- package/components/AccessCardQrTagging.vue +1 -1
- package/components/AccessManagement.vue +53 -72
- package/components/AppButton.vue +52 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +35 -0
- package/components/AppField.vue +51 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +203 -0
- package/components/AreaChecklistHistoryLogs.vue +51 -44
- package/components/AreaChecklistHistoryMain.vue +6 -2
- package/components/AreaMain.vue +9 -17
- package/components/AttendanceMain.vue +4 -3
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BuildingManagement/buildings.vue +27 -38
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BulletinBoardManagement.vue +23 -10
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/CardToolbar.vue +37 -0
- package/components/CleaningScheduleMain.vue +29 -46
- package/components/ClientMain.vue +90 -78
- package/components/DashboardEmptyState.vue +11 -1
- package/components/DashboardMain.vue +349 -275
- package/components/DashboardPanel.vue +1 -1
- package/components/DocumentManagement.vue +129 -93
- package/components/EntryPassInformation.vue +4 -4
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +4 -6
- package/components/EquipmentManagementMain.vue +9 -7
- package/components/FeedbackMain.vue +42 -56
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +825 -202
- package/components/HidQrCodeConfiguration.vue +927 -93
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +454 -106
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +289 -37
- package/components/Layout/NavigationDrawer.vue +340 -30
- package/components/ManageChecklistMain.vue +17 -17
- package/components/MemberMain.vue +79 -60
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +9 -0
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/NotificationSettings.vue +361 -0
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +7 -3
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +104 -45
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/RolePermissionMain.vue +184 -91
- package/components/ScanVisitorQRCode.vue +35 -7
- package/components/ScheduleAreaMain.vue +15 -49
- package/components/ScheduleTaskMain.vue +23 -26
- package/components/ScheduleTastTicketMain.vue +18 -43
- package/components/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +346 -157
- package/components/SiteSettings.vue +25 -10
- package/components/SiteSettingsWorkOrder.vue +61 -30
- package/components/SpecificAttr.vue +10 -2
- package/components/StatusChip.vue +93 -0
- package/components/StockCard.vue +17 -12
- package/components/SwitchContext.vue +33 -1
- package/components/TableHygiene.vue +29 -46
- package/components/TableList.vue +1 -1
- package/components/TableListSecondary.vue +1 -1
- package/components/TableMain.vue +161 -77
- package/components/TableV3.vue +61 -66
- package/components/UnitMain.vue +9 -17
- package/components/VehicleManagement.vue +20 -9
- package/components/VisitorForm.vue +294 -78
- package/components/VisitorManagement.vue +221 -71
- package/components/VisitorSocketPopUp.vue +56 -2
- package/components/WorkOrder/Main.vue +42 -51
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +151 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useOrg.ts +1 -1
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/nuxt.config.ts +31 -0
- package/package.json +4 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +68 -9
- package/test/visitor-socket.test.mjs +36 -0
- package/types/org.d.ts +2 -0
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +571 -0
- package/utils/camera-wall.ts +926 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/status.test.ts +77 -0
- package/utils/status.ts +90 -0
- package/utils/theme.test.ts +534 -0
- package/utils/theme.ts +294 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/middleware/member.ts +0 -4
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
package/composables/useWebUsb.ts
CHANGED
|
@@ -11,6 +11,10 @@ interface USBDevice {
|
|
|
11
11
|
close(): Promise<void>;
|
|
12
12
|
selectConfiguration(configurationValue: number): Promise<void>;
|
|
13
13
|
claimInterface(interfaceNumber: number): Promise<void>;
|
|
14
|
+
selectAlternateInterface(
|
|
15
|
+
interfaceNumber: number,
|
|
16
|
+
alternateSetting: number,
|
|
17
|
+
): Promise<void>;
|
|
14
18
|
releaseInterface(interfaceNumber: number): Promise<void>;
|
|
15
19
|
transferOut(
|
|
16
20
|
endpointNumber: number,
|
|
@@ -19,14 +23,27 @@ interface USBDevice {
|
|
|
19
23
|
configuration: {
|
|
20
24
|
interfaces: Array<{
|
|
21
25
|
interfaceNumber: number;
|
|
26
|
+
alternate: USBAlternateInterface;
|
|
22
27
|
alternates: Array<{
|
|
28
|
+
alternateSetting: number;
|
|
29
|
+
interfaceClass: number;
|
|
23
30
|
endpoints: Array<{
|
|
24
31
|
endpointNumber: number;
|
|
25
32
|
direction: "in" | "out";
|
|
26
33
|
}>;
|
|
27
34
|
}>;
|
|
28
35
|
}>;
|
|
29
|
-
};
|
|
36
|
+
} | null;
|
|
37
|
+
configurations: Array<{ configurationValue: number }>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface USBAlternateInterface {
|
|
41
|
+
alternateSetting: number;
|
|
42
|
+
interfaceClass: number;
|
|
43
|
+
endpoints: Array<{
|
|
44
|
+
endpointNumber: number;
|
|
45
|
+
direction: "in" | "out";
|
|
46
|
+
}>;
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
interface USBOutTransferResult {
|
|
@@ -39,13 +56,86 @@ declare global {
|
|
|
39
56
|
usb: {
|
|
40
57
|
getDevices(): Promise<USBDevice[]>;
|
|
41
58
|
requestDevice(options: {
|
|
42
|
-
filters: Array<{
|
|
59
|
+
filters: Array<{
|
|
60
|
+
vendorId?: number;
|
|
61
|
+
productId?: number;
|
|
62
|
+
classCode?: number;
|
|
63
|
+
}>;
|
|
43
64
|
}): Promise<USBDevice>;
|
|
44
65
|
};
|
|
45
66
|
}
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
export default function useWebUsb() {
|
|
70
|
+
const printerConnections = new WeakMap<
|
|
71
|
+
USBDevice,
|
|
72
|
+
{ interfaceNumber: number; endpointNumber: number }
|
|
73
|
+
>();
|
|
74
|
+
|
|
75
|
+
const protectedInterfaceClasses = new Set([0x01, 0x03, 0x08, 0x0b, 0x0e, 0x10, 0xe0]);
|
|
76
|
+
|
|
77
|
+
function getErrorMessage(error: unknown, fallback: string) {
|
|
78
|
+
return error instanceof Error && error.message ? error.message : fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findPrinterInterface(device: USBDevice) {
|
|
82
|
+
const candidates = (device.configuration?.interfaces || []).flatMap((usbInterface) =>
|
|
83
|
+
usbInterface.alternates.flatMap((alternate) => {
|
|
84
|
+
const outputEndpoint = alternate.endpoints.find((endpoint) => endpoint.direction === "out");
|
|
85
|
+
if (!outputEndpoint || protectedInterfaceClasses.has(alternate.interfaceClass)) return [];
|
|
86
|
+
|
|
87
|
+
return [{
|
|
88
|
+
interfaceNumber: usbInterface.interfaceNumber,
|
|
89
|
+
alternateSetting: alternate.alternateSetting,
|
|
90
|
+
activeAlternateSetting: usbInterface.alternate.alternateSetting,
|
|
91
|
+
interfaceClass: alternate.interfaceClass,
|
|
92
|
+
endpointNumber: outputEndpoint.endpointNumber,
|
|
93
|
+
}];
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
candidates.sort((left, right) => {
|
|
98
|
+
const priority = (interfaceClass: number) => interfaceClass === 0x07 ? 0 : interfaceClass === 0xff ? 1 : 2;
|
|
99
|
+
return priority(left.interfaceClass) - priority(right.interfaceClass);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return candidates[0] || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function preparePrinterDevice(device: USBDevice) {
|
|
106
|
+
await device.open();
|
|
107
|
+
if (!device.configuration) {
|
|
108
|
+
await device.selectConfiguration(device.configurations[0]?.configurationValue || 1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const printerInterface = findPrinterInterface(device);
|
|
112
|
+
if (!printerInterface) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
"The selected USB device is not a compatible printer. Select the receipt printer instead of a Bluetooth, camera, keyboard, or other system device.",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await device.claimInterface(printerInterface.interfaceNumber);
|
|
119
|
+
if (printerInterface.alternateSetting !== printerInterface.activeAlternateSetting) {
|
|
120
|
+
await device.selectAlternateInterface(
|
|
121
|
+
printerInterface.interfaceNumber,
|
|
122
|
+
printerInterface.alternateSetting,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
printerConnections.set(device, {
|
|
127
|
+
interfaceNumber: printerInterface.interfaceNumber,
|
|
128
|
+
endpointNumber: printerInterface.endpointNumber,
|
|
129
|
+
});
|
|
130
|
+
return device;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getPrinterConnection(device: USBDevice) {
|
|
134
|
+
const connection = printerConnections.get(device);
|
|
135
|
+
if (!connection) throw new Error("Printer interface is not connected");
|
|
136
|
+
return connection;
|
|
137
|
+
}
|
|
138
|
+
|
|
49
139
|
const isWebUsbSupported = computed(() => {
|
|
50
140
|
return typeof navigator !== "undefined" && "usb" in navigator;
|
|
51
141
|
});
|
|
@@ -75,7 +165,9 @@ export default function useWebUsb() {
|
|
|
75
165
|
throw new Error("Web USB is not supported in this browser");
|
|
76
166
|
}
|
|
77
167
|
try {
|
|
78
|
-
const device = await navigator.usb.requestDevice({
|
|
168
|
+
const device = await navigator.usb.requestDevice({
|
|
169
|
+
filters: [{ classCode: 0x07 }, { classCode: 0xff }],
|
|
170
|
+
});
|
|
79
171
|
return {
|
|
80
172
|
vendorId: device.vendorId,
|
|
81
173
|
productId: device.productId,
|
|
@@ -84,12 +176,12 @@ export default function useWebUsb() {
|
|
|
84
176
|
serialNumber: device.serialNumber || "Unknown",
|
|
85
177
|
deviceId: `${device.vendorId}:${device.productId}`,
|
|
86
178
|
};
|
|
87
|
-
} catch (error:
|
|
88
|
-
if (error.name === "NotFoundError") {
|
|
179
|
+
} catch (error: unknown) {
|
|
180
|
+
if (error instanceof DOMException && error.name === "NotFoundError") {
|
|
89
181
|
throw new Error("No device selected");
|
|
90
182
|
}
|
|
91
183
|
console.error("Error requesting USB device:", error);
|
|
92
|
-
throw new Error("Failed to access USB
|
|
184
|
+
throw new Error(getErrorMessage(error, "Failed to access USB printer"));
|
|
93
185
|
}
|
|
94
186
|
};
|
|
95
187
|
|
|
@@ -110,14 +202,17 @@ export default function useWebUsb() {
|
|
|
110
202
|
});
|
|
111
203
|
}
|
|
112
204
|
console.log("[WebUSB] Opening device:", device.vendorId, device.productId);
|
|
113
|
-
await device
|
|
114
|
-
await device.selectConfiguration(1);
|
|
115
|
-
await device.claimInterface(0);
|
|
205
|
+
await preparePrinterDevice(device);
|
|
116
206
|
console.log("[WebUSB] Device opened and interface claimed.");
|
|
117
207
|
return device;
|
|
118
|
-
} catch (error) {
|
|
208
|
+
} catch (error: unknown) {
|
|
119
209
|
console.error("Error connecting to USB device:", error);
|
|
120
|
-
|
|
210
|
+
try {
|
|
211
|
+
await device?.close();
|
|
212
|
+
} catch (closeError) {
|
|
213
|
+
console.error("Error closing rejected USB device:", closeError);
|
|
214
|
+
}
|
|
215
|
+
throw new Error(getErrorMessage(error, "Failed to connect to USB printer"));
|
|
121
216
|
}
|
|
122
217
|
};
|
|
123
218
|
|
|
@@ -173,15 +268,20 @@ export default function useWebUsb() {
|
|
|
173
268
|
? "Connection and print test successful"
|
|
174
269
|
: "Connection successful but print test failed",
|
|
175
270
|
};
|
|
176
|
-
} catch (error:
|
|
271
|
+
} catch (error: unknown) {
|
|
177
272
|
return {
|
|
178
273
|
success: false,
|
|
179
|
-
error: error
|
|
274
|
+
error: getErrorMessage(error, "Failed to connect to USB printer"),
|
|
180
275
|
message: "Connection failed",
|
|
181
276
|
};
|
|
182
277
|
} finally {
|
|
183
278
|
if (device) {
|
|
184
279
|
try {
|
|
280
|
+
const connection = printerConnections.get(device);
|
|
281
|
+
if (connection) {
|
|
282
|
+
await device.releaseInterface(connection.interfaceNumber);
|
|
283
|
+
printerConnections.delete(device);
|
|
284
|
+
}
|
|
185
285
|
await device.close();
|
|
186
286
|
} catch (closeError) {
|
|
187
287
|
console.error("Error closing device:", closeError);
|
|
@@ -205,21 +305,16 @@ export default function useWebUsb() {
|
|
|
205
305
|
];
|
|
206
306
|
|
|
207
307
|
const testData = new Uint8Array(testCommands);
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
const outputEndpoint = alternate.endpoints.find(
|
|
211
|
-
(ep: any) => ep.direction === "out",
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
if (!outputEndpoint) {
|
|
215
|
-
throw new Error("No output endpoint found");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
await device.transferOut(outputEndpoint.endpointNumber, testData);
|
|
308
|
+
const { endpointNumber } = getPrinterConnection(device);
|
|
309
|
+
await device.transferOut(endpointNumber, testData);
|
|
219
310
|
return { success: true, message: "Test print sent successfully" };
|
|
220
|
-
} catch (error:
|
|
311
|
+
} catch (error: unknown) {
|
|
221
312
|
console.error("Test print error:", error);
|
|
222
|
-
return {
|
|
313
|
+
return {
|
|
314
|
+
success: false,
|
|
315
|
+
error: getErrorMessage(error, "Test print failed"),
|
|
316
|
+
message: "Test print failed",
|
|
317
|
+
};
|
|
223
318
|
}
|
|
224
319
|
};
|
|
225
320
|
|
|
@@ -354,13 +449,8 @@ export default function useWebUsb() {
|
|
|
354
449
|
) => {
|
|
355
450
|
try {
|
|
356
451
|
console.log("[WebUSB] printQrCode start — doorLevel:", doorLevel, "liftLevel:", liftLevel, "company:", companyName, "address:", address);
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
const outputEndpoint = alternate.endpoints.find(
|
|
360
|
-
(ep: any) => ep.direction === "out",
|
|
361
|
-
);
|
|
362
|
-
if (!outputEndpoint) throw new Error("No output endpoint found");
|
|
363
|
-
console.log("[WebUSB] Using endpoint:", outputEndpoint.endpointNumber);
|
|
452
|
+
const { endpointNumber } = getPrinterConnection(device);
|
|
453
|
+
console.log("[WebUSB] Using endpoint:", endpointNumber);
|
|
364
454
|
|
|
365
455
|
const canvas = await createReceiptLayout(
|
|
366
456
|
urlImage,
|
|
@@ -375,17 +465,17 @@ export default function useWebUsb() {
|
|
|
375
465
|
const rasterData = canvasToRaster(canvas);
|
|
376
466
|
console.log("[WebUSB] Raster data size:", rasterData.byteLength, "bytes");
|
|
377
467
|
|
|
378
|
-
await device.transferOut(
|
|
468
|
+
await device.transferOut(endpointNumber, rasterData.buffer);
|
|
379
469
|
console.log("[WebUSB] Raster data sent.");
|
|
380
470
|
|
|
381
471
|
const CUT = new Uint8Array([0x1d, 0x56, 0x41, 0x10]);
|
|
382
|
-
await device.transferOut(
|
|
472
|
+
await device.transferOut(endpointNumber, CUT.buffer);
|
|
383
473
|
console.log("[WebUSB] Cut command sent.");
|
|
384
474
|
|
|
385
475
|
return { success: true, message: "QR print sent successfully" };
|
|
386
|
-
} catch (error:
|
|
476
|
+
} catch (error: unknown) {
|
|
387
477
|
console.error("[WebUSB] printQrCode error:", error);
|
|
388
|
-
return { success: false, error: error
|
|
478
|
+
return { success: false, error: getErrorMessage(error, "Unable to print QR code") };
|
|
389
479
|
}
|
|
390
480
|
};
|
|
391
481
|
|
package/nuxt.config.ts
CHANGED
|
@@ -7,6 +7,37 @@ export default defineNuxtConfig({
|
|
|
7
7
|
transpile: ["vuetify"],
|
|
8
8
|
},
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* THE REDESIGN'S TYPEFACE - LOADED, DELIBERATELY NOT APPLIED YET.
|
|
12
|
+
*
|
|
13
|
+
* `--font-sans` in `assets/css/tokens.css` names Manrope, and a token that
|
|
14
|
+
* names a font nobody loaded is a trap: the next phase writes
|
|
15
|
+
* `font-family: var(--font-sans)`, silently falls back to Arial, and looks
|
|
16
|
+
* fine to whoever wrote it. So the stylesheet is requested here.
|
|
17
|
+
*
|
|
18
|
+
* Nothing sets `font-family` on the page in Phase 1. Switching eleven
|
|
19
|
+
* applications to a new typeface before one screen has been rebuilt would
|
|
20
|
+
* leave the whole product looking half-migrated; each phase adopts the token
|
|
21
|
+
* as it rebuilds its own surfaces. Browsers do not download a webfont no
|
|
22
|
+
* element uses, so until then this costs one small CSS request.
|
|
23
|
+
*/
|
|
24
|
+
app: {
|
|
25
|
+
head: {
|
|
26
|
+
link: [
|
|
27
|
+
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
|
28
|
+
{
|
|
29
|
+
rel: "preconnect",
|
|
30
|
+
href: "https://fonts.gstatic.com",
|
|
31
|
+
crossorigin: "",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
rel: "stylesheet",
|
|
35
|
+
href: "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
10
41
|
runtimeConfig: {
|
|
11
42
|
public: {
|
|
12
43
|
cookieConfig: {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "3.2.
|
|
5
|
+
"version": "3.2.2-staging.101",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"publishConfig": {
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"build": "nuxt build .playground",
|
|
15
15
|
"generate": "nuxt generate .playground",
|
|
16
16
|
"preview": "nuxt preview .playground",
|
|
17
|
+
"test": "esbuild composables/useVisitorSocket.ts --format=esm --outfile=test/.build/useVisitorSocket.mjs --log-level=error && node --test \"test/*.test.mjs\" && yarn test:units",
|
|
18
|
+
"test:units": "node --experimental-strip-types --test \"utils/*.test.ts\"",
|
|
17
19
|
"release": "yarn run build && changeset publish"
|
|
18
20
|
},
|
|
19
21
|
"devDependencies": {
|
|
@@ -40,6 +42,7 @@
|
|
|
40
42
|
"qrcode": "^1.5.4",
|
|
41
43
|
"qrcode.vue": "^3.4.1",
|
|
42
44
|
"sass": "^1.80.6",
|
|
45
|
+
"sip.js": "0.21.2",
|
|
43
46
|
"socket.io-client": "^4.8.3",
|
|
44
47
|
"vue-draggable-next": "^2.3.0",
|
|
45
48
|
"vue3-signature": "^0.2.4",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NotificationSettings />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
/**
|
|
7
|
+
* Notification settings, reachable from Profile.
|
|
8
|
+
*
|
|
9
|
+
* The page lives in the shared layer so every web app has the route and there
|
|
10
|
+
* is only one screen to keep correct; the link to it sits in Profile, in the
|
|
11
|
+
* account app, which is where "Manage Account" already sends everybody.
|
|
12
|
+
*/
|
|
13
|
+
definePageMeta({
|
|
14
|
+
middleware: ["01-auth"],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const { authenticate } = useLocalAuth();
|
|
18
|
+
authenticate();
|
|
19
|
+
</script>
|
|
@@ -12,75 +12,40 @@ export default defineNuxtPlugin(() => {
|
|
|
12
12
|
|
|
13
13
|
const { userAppRole, id, orgNature } = useLocalSetup();
|
|
14
14
|
|
|
15
|
-
router.
|
|
15
|
+
router.beforeEach(async (to) => {
|
|
16
16
|
const isMember = to.meta?.memberOnly;
|
|
17
17
|
|
|
18
18
|
if (!isMember) return;
|
|
19
19
|
|
|
20
20
|
const APP = useRuntimeConfig().public.APP;
|
|
21
|
-
const org =
|
|
22
|
-
()
|
|
23
|
-
(to.params.org as string) || (to.params.organization as string) || ""
|
|
24
|
-
);
|
|
21
|
+
const org =
|
|
22
|
+
(to.params.org as string) || (to.params.organization as string) || "";
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (!hexSchema.safeParse(org.value).success) {
|
|
29
|
-
return router.replace({ name: "require-organization-membership" });
|
|
24
|
+
if (!hexSchema.safeParse(org).success) {
|
|
25
|
+
return { name: "require-organization-membership" };
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
const userId =
|
|
33
|
-
|
|
34
|
-
const { data: userMemberData, error: userMemberError } =
|
|
35
|
-
await useLazyAsyncData(
|
|
36
|
-
"plugin-get-member-by-id-" + userId.value + "-" + APP + "-" + org.value,
|
|
37
|
-
() => getByUserType(userId.value, APP, org.value),
|
|
38
|
-
{ watch: [userId] }
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
watchEffect(() => {
|
|
42
|
-
if (userMemberError.value) {
|
|
43
|
-
console.log('running-secure-member-redirect-plugin')
|
|
44
|
-
navigateTo(
|
|
45
|
-
{
|
|
46
|
-
name: "index",
|
|
47
|
-
},
|
|
48
|
-
{ replace: true }
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const roleId = ref("roleId");
|
|
28
|
+
const userId = useCookie("user").value ?? "";
|
|
29
|
+
if (!userId) return { name: "index" };
|
|
54
30
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
roleId.value = userMemberData.value.role ?? "roleId";
|
|
59
|
-
}
|
|
60
|
-
});
|
|
31
|
+
try {
|
|
32
|
+
const userMemberData = await getByUserType(userId, APP, org);
|
|
33
|
+
id.value = userMemberData.org ?? "";
|
|
61
34
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
35
|
+
const [orgResult, roleResult] = await Promise.allSettled([
|
|
36
|
+
getById(org),
|
|
37
|
+
userMemberData.role ? getRoleById(userMemberData.role) : null,
|
|
38
|
+
]);
|
|
67
39
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
orgNature.value = getOrgByIdReq.value.nature ?? "";
|
|
40
|
+
if (orgResult.status === "fulfilled" && orgResult.value) {
|
|
41
|
+
orgNature.value = orgResult.value.nature ?? "";
|
|
71
42
|
}
|
|
72
|
-
});
|
|
73
43
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
() => getRoleById(roleId.value),
|
|
77
|
-
{ watch: [roleId] }
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
watchEffect(() => {
|
|
81
|
-
if (getRoleByIdReq.value) {
|
|
82
|
-
userAppRole.value = getRoleByIdReq.value;
|
|
44
|
+
if (roleResult.status === "fulfilled" && roleResult.value) {
|
|
45
|
+
userAppRole.value = roleResult.value;
|
|
83
46
|
}
|
|
84
|
-
})
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return { name: "index" };
|
|
49
|
+
}
|
|
85
50
|
});
|
|
86
51
|
});
|
package/plugins/vuetify.ts
CHANGED
|
@@ -42,7 +42,40 @@ import {
|
|
|
42
42
|
} from "vuetify-pro-tiptap";
|
|
43
43
|
import "vuetify-pro-tiptap/style.css";
|
|
44
44
|
|
|
45
|
+
import { LIGHT_THEME, DARK_THEME } from "../utils/theme";
|
|
46
|
+
import { THEME_COOKIE } from "../composables/useThemePreference";
|
|
47
|
+
import type { CookieOptions } from "#app";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The design tokens. Loaded HERE rather than in `nuxt.config.ts` so they land
|
|
51
|
+
* after `vuetify/styles` in the bundle - `tokens.css` aliases `--v-theme-*`,
|
|
52
|
+
* which Vuetify has to have emitted first.
|
|
53
|
+
*/
|
|
54
|
+
import "../assets/css/tokens.css";
|
|
55
|
+
import "../assets/css/shell.css";
|
|
56
|
+
import "../assets/css/screens.css";
|
|
57
|
+
import "../assets/css/primitives.css";
|
|
58
|
+
|
|
45
59
|
export default defineNuxtPlugin((app) => {
|
|
60
|
+
/**
|
|
61
|
+
* BOOT ON THE REMEMBERED THEME, DON'T CORRECT IT AFTERWARDS.
|
|
62
|
+
*
|
|
63
|
+
* Reading the cookie here, before `createVuetify`, is what makes the choice
|
|
64
|
+
* survive a reload with no flash: Vuetify's first paint is already the right
|
|
65
|
+
* theme, so there is no moment where the wrong one is on screen. Setting it
|
|
66
|
+
* from a component's `onMounted` would repaint the whole application one
|
|
67
|
+
* frame in, which is the flash this is written to avoid.
|
|
68
|
+
*
|
|
69
|
+
* `useThemePreference()` writes this cookie; nothing else does.
|
|
70
|
+
*/
|
|
71
|
+
const stored = useCookie<string | null>(
|
|
72
|
+
THEME_COOKIE,
|
|
73
|
+
// typed `unknown` in runtimeConfig; same object `sid` and `user` use
|
|
74
|
+
useRuntimeConfig().public.cookieConfig as CookieOptions<string | null> & {
|
|
75
|
+
readonly?: false;
|
|
76
|
+
}
|
|
77
|
+
).value;
|
|
78
|
+
|
|
46
79
|
const vuetify = createVuetify({
|
|
47
80
|
defaults: {
|
|
48
81
|
VTextField: {
|
|
@@ -69,17 +102,43 @@ export default defineNuxtPlugin((app) => {
|
|
|
69
102
|
// hint: "This field is required",
|
|
70
103
|
// },
|
|
71
104
|
},
|
|
105
|
+
/**
|
|
106
|
+
* THE PRODUCT HAS TWO THEMES, AND UNTIL NOW ONLY ONE OF THEM WAS OURS.
|
|
107
|
+
*
|
|
108
|
+
* Every app shows a light/dark toggle in the app bar (Layout/Header.vue),
|
|
109
|
+
* and it sets Vuetify's built-in "light" and "dark" themes. Those were left
|
|
110
|
+
* at Vuetify's factory values, so one click on any screen in any of the
|
|
111
|
+
* eleven apps threw the brand away: `primary` stopped being the Seven365
|
|
112
|
+
* navy and became Material blue #2196F3 - and `primary` is the navigation
|
|
113
|
+
* drawer's fill - while `primary-button` and `text-primary`, used in around
|
|
114
|
+
* eighty places across the layer and the apps, do not exist in a stock
|
|
115
|
+
* theme at all, so those components lost their colour to an undefined CSS
|
|
116
|
+
* variable. There was also no way back to the brand: the toggle only ever
|
|
117
|
+
* flipped between the two stock themes.
|
|
118
|
+
*
|
|
119
|
+
* The fix is to make the stock names OURS rather than to add new names.
|
|
120
|
+
* Several components already branch on `theme.global.name === "dark"` (this
|
|
121
|
+
* layer's FormDialog, property-management's facility icons), so a
|
|
122
|
+
* differently-named dark theme would have left every one of them silently
|
|
123
|
+
* on their light branch inside a dark app. Overriding `light` and `dark`
|
|
124
|
+
* means the toggle, `v-theme-provider theme="light"`, the `plain-dark`
|
|
125
|
+
* layout and all of those comparisons keep working, and start being right.
|
|
126
|
+
*
|
|
127
|
+
* The colours themselves, and every ratio they were chosen for, are in
|
|
128
|
+
* `utils/theme.ts`, which `utils/theme.test.ts` measures on every run.
|
|
129
|
+
*/
|
|
72
130
|
theme: {
|
|
73
|
-
defaultTheme: "
|
|
131
|
+
defaultTheme: stored === "dark" ? "dark" : "light",
|
|
74
132
|
themes: {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
133
|
+
light: LIGHT_THEME,
|
|
134
|
+
dark: DARK_THEME,
|
|
135
|
+
/**
|
|
136
|
+
* The name this product booted on before the two above carried the
|
|
137
|
+
* brand. Kept, and identical to `light`, so anything that asks for it
|
|
138
|
+
* by name - in an app that is not in this repository - still gets a
|
|
139
|
+
* theme rather than a blank one.
|
|
140
|
+
*/
|
|
141
|
+
iservice365: LIGHT_THEME,
|
|
83
142
|
},
|
|
84
143
|
},
|
|
85
144
|
icons: {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { cameraAlertMessage } from "./.build/useVisitorSocket.mjs";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* The reported symptom was one camera raising a red toast every 10 seconds on
|
|
8
|
+
* an unrelated page. API-core now paces the alert; this side has to make sure
|
|
9
|
+
* several cameras failing at once still cannot become several snackbars.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
test("nothing is shown when no camera is down", () => {
|
|
13
|
+
assert.equal(cameraAlertMessage([]), "");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("one failing camera shows the server's own message, which names it", () => {
|
|
17
|
+
const text = "The entry and exit ANPR camera at Seventh Condominium is not responding.";
|
|
18
|
+
assert.equal(cameraAlertMessage([text]), text);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("several failing cameras collapse into a single line", () => {
|
|
22
|
+
const many = ["camera one is down", "camera two is down", "camera three is down"];
|
|
23
|
+
const text = cameraAlertMessage(many);
|
|
24
|
+
assert.match(text, /^3 cameras at this site are not responding/);
|
|
25
|
+
for (const message of many) assert.doesNotMatch(text, new RegExp(message));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("the collapsed line points at the page that lists them", () => {
|
|
29
|
+
assert.match(cameraAlertMessage(["a", "b"]), /Site Settings > Cameras/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("no camera message tells the operator to deactivate anything", () => {
|
|
33
|
+
for (const messages of [["a camera is down"], ["a", "b"]]) {
|
|
34
|
+
assert.doesNotMatch(cameraAlertMessage(messages), /inactive|deactivat/i);
|
|
35
|
+
}
|
|
36
|
+
});
|
package/types/org.d.ts
CHANGED
package/types/site.d.ts
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
declare type TSiteCreate = Pick<TSite, "name" | "description" | "orgId">;
|
|
2
2
|
|
|
3
|
+
declare type THidQrCodeFormat = "0" | "1" | "2";
|
|
4
|
+
|
|
5
|
+
declare type THidPermissionCategory =
|
|
6
|
+
| "resident"
|
|
7
|
+
| "property_management"
|
|
8
|
+
| "service_provider";
|
|
9
|
+
|
|
10
|
+
declare type THidPermissionAssignment = {
|
|
11
|
+
subjectId: string;
|
|
12
|
+
category: THidPermissionCategory;
|
|
13
|
+
name: string;
|
|
14
|
+
subtitle?: string;
|
|
15
|
+
intercom: boolean;
|
|
16
|
+
readerId?: string;
|
|
17
|
+
location?: string;
|
|
18
|
+
locationKey?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
declare type THidSitePermissions = {
|
|
22
|
+
orgId: string;
|
|
23
|
+
site: string;
|
|
24
|
+
readerId: string;
|
|
25
|
+
location: string;
|
|
26
|
+
assignments: THidPermissionAssignment[];
|
|
27
|
+
counts: {
|
|
28
|
+
resident: number;
|
|
29
|
+
propertyManagement: number;
|
|
30
|
+
serviceProvider: number;
|
|
31
|
+
intercom: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
declare type THidPermissionCandidate = THidPermissionAssignment & {
|
|
36
|
+
selected: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
declare type THidQrCodePassConfig = {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
onlineMode?: boolean;
|
|
42
|
+
readerId: string;
|
|
43
|
+
qrFormat: THidQrCodeFormat;
|
|
44
|
+
identificationMethods?: {
|
|
45
|
+
facial: boolean;
|
|
46
|
+
card: boolean;
|
|
47
|
+
qrCode: boolean;
|
|
48
|
+
idPassword: boolean;
|
|
49
|
+
pin: boolean;
|
|
50
|
+
bluetooth: boolean;
|
|
51
|
+
};
|
|
52
|
+
printer: {
|
|
53
|
+
vendorId: string;
|
|
54
|
+
productId: string;
|
|
55
|
+
};
|
|
56
|
+
template: {
|
|
57
|
+
header: string;
|
|
58
|
+
subtext: string;
|
|
59
|
+
};
|
|
60
|
+
validityMinutes: number | null;
|
|
61
|
+
updatedAt?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
declare type TSiteMetadata = {
|
|
65
|
+
block?: number;
|
|
66
|
+
guardPosts?: number;
|
|
67
|
+
gracePeriod?: number;
|
|
68
|
+
incidentCounter?: number;
|
|
69
|
+
incidentLogo?: string;
|
|
70
|
+
services?: Record<string, unknown>[];
|
|
71
|
+
hidQrCodePass?: THidQrCodePassConfig;
|
|
72
|
+
};
|
|
73
|
+
|
|
3
74
|
|
|
4
75
|
declare type TSite = {
|
|
5
76
|
_id?: string;
|