@7365admin1/layer-common 3.2.7 → 3.2.8-staging.200
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/assets/css/primitives.css +1113 -0
- package/assets/css/screens.css +1097 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +236 -0
- package/components/AcceptDialog.vue +17 -18
- package/components/AccessCard/AvailableStats.vue +24 -16
- package/components/AccessCardAddForm.vue +42 -42
- package/components/AccessCardAssignToUnitForm.vue +13 -21
- package/components/AccessCardDeleteDialog.vue +41 -39
- package/components/AccessCardDetailsDialog.vue +125 -79
- package/components/AccessCardPreviewDialog.vue +58 -84
- package/components/AccessCardQrTagging.vue +60 -63
- package/components/AccessCardReplaceForm.vue +27 -23
- package/components/AccessManagement.vue +86 -85
- package/components/AddEqupmentForm.vue +18 -22
- package/components/AddPassKeyToVisitor.vue +95 -73
- package/components/AppButton.vue +62 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +49 -0
- package/components/AppField.vue +76 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +247 -0
- package/components/AreaChecklistHistoryLogs.vue +51 -44
- package/components/AreaChecklistHistoryMain.vue +14 -7
- package/components/AreaFormDialog.vue +71 -59
- package/components/AreaMain.vue +73 -107
- package/components/AttachmentsViewer.vue +84 -22
- package/components/AttendanceCheckInOutDialog.vue +85 -88
- package/components/AttendanceDetailsDialog.vue +57 -63
- package/components/AttendanceMain.vue +4 -3
- package/components/AttendanceMapSearchDialog.vue +42 -44
- package/components/AttendanceSettingsDialog.vue +79 -63
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BtnUploadFile.vue +53 -11
- package/components/BuildingForm.vue +34 -27
- package/components/BuildingManagement/buildings.vue +39 -39
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BuildingUnitFormAdd.vue +20 -23
- package/components/BuildingUnitFormEdit.vue +8 -4
- package/components/BulletinBoardForm.vue +19 -24
- package/components/BulletinBoardManagement.vue +49 -57
- package/components/BulletinBoardView.vue +9 -6
- package/components/BulletinExpirationChip.vue +13 -4
- package/components/Button/Close.vue +2 -2
- package/components/CameraForm.vue +21 -22
- package/components/CameraMain.vue +50 -49
- package/components/Card/DeleteConfirmation.vue +24 -22
- package/components/Card/MemberInfoSummary.vue +28 -3
- package/components/Card/Toggle.vue +15 -3
- package/components/CardToolbar.vue +37 -0
- package/components/Carousel.vue +56 -36
- package/components/Chat/Bubbles.vue +43 -14
- package/components/Chat/Information.vue +49 -32
- package/components/Chat/ListCard.vue +38 -9
- package/components/Chat/Message.vue +41 -15
- package/components/Chat/Navigation.vue +27 -15
- package/components/Chat/SkeletonLoader.vue +9 -2
- package/components/ChecklistItemRow.vue +25 -1
- package/components/CleaningScheduleMain.vue +44 -49
- package/components/ClientDetailForm.vue +81 -56
- package/components/ClientMain.vue +92 -82
- package/components/ConfirmDialog.vue +14 -8
- package/components/Container/Standard.vue +9 -7
- package/components/CreateSubsciptionPlan.vue +148 -89
- package/components/DashboardEmptyState.vue +48 -3
- package/components/DashboardMain.vue +924 -676
- package/components/DashboardPanel.vue +84 -21
- package/components/DeliveryCompany.vue +34 -20
- package/components/Dialog/DeleteConfirmation.vue +20 -21
- package/components/Dialog/ReplaceAutofillPrompt.vue +19 -26
- package/components/Dialog/ReusablePrompt.vue +30 -29
- package/components/Dialog/UpdateMoreAction.vue +62 -59
- package/components/DocumentForm.vue +14 -21
- package/components/DocumentManagement.vue +162 -108
- package/components/DocumentViewer.vue +29 -12
- package/components/DrawImage.vue +29 -21
- package/components/EntryPass/QrTemplatePreview.vue +27 -16
- package/components/EntryPassInformation.vue +40 -10
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +84 -97
- package/components/EquipmentManagementMain.vue +81 -103
- package/components/Facility/BookingSetup.vue +43 -52
- package/components/Feedback/Form.vue +26 -12
- package/components/FeedbackMain.vue +35 -49
- package/components/FileInput.vue +75 -58
- package/components/FileInputWithList.vue +59 -12
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +512 -490
- package/components/HidCameraCaptureDialog.vue +216 -0
- package/components/HidEnabledGate.vue +67 -9
- package/components/HidIntercomManagement.vue +1322 -653
- package/components/HidQrCodeConfiguration.vue +1030 -258
- package/components/HidReaderForm.vue +142 -59
- package/components/HidReaderManagement.vue +211 -323
- package/components/HidServiceSettingsPanel.vue +44 -13
- package/components/HidUserEnrollment.vue +1185 -412
- package/components/HygieneUpdateMoreAction.vue +44 -44
- package/components/IncidentReport/Authorities.vue +30 -18
- package/components/IncidentReport/IncidentInformation.vue +10 -10
- package/components/IncidentReport/IncidentInformationDownload.vue +3 -4
- package/components/IncidentReport/affectedEntities.vue +41 -20
- package/components/Input/Date.vue +3 -0
- package/components/Input/DatePicker.vue +1 -2
- package/components/Input/DateTimePicker.vue +1 -1
- package/components/Input/File.vue +55 -10
- package/components/Input/FileV2.vue +39 -6
- package/components/Input/InputPhoneNumberV2.vue +2 -1
- package/components/Input/ListGroupSelection.vue +56 -8
- package/components/Input/NRICNumber.vue +1 -0
- package/components/Input/NewDate.vue +1 -0
- package/components/Input/Number.vue +1 -0
- package/components/Input/Password.vue +1 -0
- package/components/Input/PhoneNumber.vue +21 -1
- package/components/Input/VehicleNumber.vue +1 -0
- package/components/InputLabel.vue +2 -2
- package/components/InvitationClientForm.vue +21 -19
- package/components/InvitationForm.vue +15 -21
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +263 -36
- package/components/Layout/NavigationDrawer.vue +300 -30
- package/components/ListItem.vue +33 -3
- package/components/LocalPagination.vue +27 -3
- package/components/ManageChecklistMain.vue +136 -86
- package/components/MemberInformation.vue +32 -16
- package/components/MemberMain.vue +113 -93
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +83 -3
- package/components/Nfc/NFCPatrolReportMain.vue +47 -33
- package/components/Nfc/NFCPatrolRouteForm.vue +18 -20
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/Nfc/NFCPatrolSettings.vue +14 -4
- package/components/Nfc/NFCTagForm.vue +22 -8
- package/components/Nfc/NFCTagMain.vue +41 -43
- package/components/Nfc/PatrolReport/DailyReportTab.vue +8 -1
- package/components/Nfc/PatrolReport/MonthlyReportTab.vue +8 -1
- package/components/Nfc/PatrolReport/MonthlyReportTable.vue +5 -3
- package/components/Nfc/PatrolReport/PatrolActivityTable.vue +26 -6
- package/components/Nfc/PatrolReport/PatrolReportTab.vue +8 -0
- package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +41 -9
- package/components/Nfc/PatrolReport/RemarksDialog.vue +39 -9
- package/components/Nfc/PatrolReport/ReportEmptyState.vue +29 -9
- package/components/Nfc/PatrolReport/ReportFilters.vue +19 -10
- package/components/Nfc/PatrolReport/ReportLocationHeader.vue +32 -6
- package/components/Nfc/PatrolReport/SummaryReportTable.vue +28 -6
- package/components/NotificationSettings.vue +373 -0
- package/components/NumberSettingField.vue +9 -2
- package/components/OnlineFormConfigurationForm.vue +725 -237
- package/components/OnlineFormFill.vue +213 -106
- package/components/OnlineFormRenderPreview.vue +25 -30
- package/components/OnlineFormsConfiguration.vue +113 -109
- package/components/OrgViewDialog.vue +34 -16
- package/components/OvernightParkingAvailability.vue +49 -26
- package/components/OvernightParkingManagement.vue +78 -18
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +109 -53
- package/components/PasswordConfirmation.vue +44 -26
- package/components/PeopleForm.vue +41 -20
- package/components/PhotoUpload.vue +110 -48
- package/components/PlaceholderComponent.vue +35 -19
- package/components/PlanActionsMenu.vue +12 -2
- package/components/PlanStatusBadge.vue +7 -8
- package/components/PlateNumberDisplay.vue +9 -6
- package/components/QrTemplate/PrintDialog.vue +66 -43
- package/components/RolePermissionFormCreate.vue +104 -79
- package/components/RolePermissionFormPreviewUpdate.vue +111 -109
- package/components/RolePermissionMain.vue +183 -81
- package/components/ScanVisitorQRCode.vue +99 -35
- package/components/ScheduleAreaMain.vue +34 -59
- package/components/ScheduleTaskForm.vue +61 -42
- package/components/ScheduleTaskMain.vue +56 -69
- package/components/SearchVehicleNumberUser.vue +39 -37
- package/components/ServiceProviderFormCreate.vue +29 -20
- package/components/ServiceProviderInvitationPrompt.vue +155 -0
- package/components/ServiceProviderMain.vue +601 -432
- package/components/Signature.vue +40 -34
- package/components/SignaturePad.vue +5 -29
- package/components/SiteSettings.vue +41 -11
- package/components/SiteSettingsWorkOrder.vue +61 -30
- package/components/SlideCardGroup.vue +35 -11
- package/components/Snackbar.vue +36 -3
- package/components/SpecificAttr.vue +18 -8
- package/components/StatusChip.vue +156 -0
- package/components/StockCard.vue +17 -12
- package/components/SubscriptionPlanTable.vue +207 -140
- package/components/SwitchContext.vue +201 -9
- package/components/TableHygiene.vue +36 -46
- package/components/TableList.vue +10 -10
- package/components/TableListSecondary.vue +105 -54
- package/components/TableMain.vue +241 -79
- package/components/TableV3.vue +61 -70
- package/components/TiptapEditor.vue +33 -40
- package/components/TiptapEditorQr.vue +5 -2
- package/components/Tooltip/Info.vue +69 -24
- package/components/UnitMain.vue +40 -60
- package/components/UnitPersonCard.vue +42 -13
- package/components/VehicleAddSelection.vue +48 -32
- package/components/VehicleForm.vue +112 -129
- package/components/VehicleManagement.vue +26 -12
- package/components/VehicleUpdateMoreAction.vue +59 -46
- package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
- package/components/VisitorForm.vue +774 -210
- package/components/VisitorFormSelection.vue +40 -37
- package/components/VisitorManagement.vue +239 -82
- package/components/VisitorPassKeyQRScanner.vue +65 -18
- package/components/VisitorSearchResultSummary.vue +60 -16
- package/components/VisitorSocketPopUp.vue +22 -14
- package/components/VisitorsReportPreview.vue +44 -29
- package/components/WorkOrder/Create.vue +26 -17
- package/components/WorkOrder/ListView.vue +38 -7
- package/components/WorkOrder/Main.vue +62 -59
- package/composables/useFeedback.ts +3 -3
- package/composables/useHidAmico.ts +269 -0
- package/composables/useHidNavigation.ts +2 -9
- package/composables/useMember.ts +3 -3
- package/composables/useNFCPatrolDailyReport.ts +13 -0
- package/composables/useNFCPatrolMonthlyReport.ts +22 -56
- package/composables/useNFCPatrolReport.ts +13 -0
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProvider.ts +3 -3
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSubscriptionPlan.ts +62 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useWebUsb.ts +127 -37
- package/composables/useWorkOrder.ts +17 -3
- package/error.vue +30 -6
- package/nuxt.config.ts +36 -0
- package/package.json +25 -4
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +2 -2
- package/pages/[org]/[site]/access-mgmt/{identity-mapping → hid-cards}/index.vue +2 -2
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/pages/payment-method-linked.vue +34 -10
- package/pages/require-customer.vue +48 -10
- package/pages/require-organization-membership.vue +32 -8
- package/pages/unauthorized.vue +29 -4
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +69 -2
- package/types/nfc-patrol-report.ts +3 -2
- package/types/site.d.ts +71 -0
- package/types/subscription.ts +18 -11
- package/utils/breadcrumb.test.ts +108 -0
- package/utils/breadcrumb.ts +82 -0
- package/utils/dashboard.test.ts +118 -0
- package/utils/dashboard.ts +53 -0
- package/utils/data.test.ts +95 -0
- package/utils/data.ts +40 -2
- package/utils/hid-discover.test.ts +102 -0
- package/utils/list-page.test.ts +60 -0
- package/utils/list-page.ts +38 -0
- package/utils/materialSymbols.ts +363 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/nfc-patrol-report.test.ts +130 -0
- package/utils/nfc-patrol-report.ts +101 -0
- package/utils/role.test.ts +69 -0
- package/utils/role.ts +10 -0
- package/utils/route-name.test.ts +85 -0
- package/utils/route-name.ts +24 -0
- package/utils/status.test.ts +110 -0
- package/utils/status.ts +135 -0
- package/utils/table-card.test.ts +84 -0
- package/utils/theme-aa-ledger.ts +148 -0
- package/utils/theme.test.ts +485 -7
- package/utils/theme.ts +295 -110
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.editorconfig +0 -12
- package/.github/workflows/main.yml +0 -17
- package/.github/workflows/publish-staging.yml +0 -47
- package/.github/workflows/publish.yml +0 -39
- package/PUBLISHING.md +0 -269
- package/components/HidIdentityMapping.vue +0 -975
- package/components/ScheduleTaskAreaFormDialog.vue +0 -144
- package/components/ScheduleTaskAreaUpdateMoreAction.vue +0 -109
- package/components/ScheduleTastTicketMain.vue +0 -182
- package/components/TableWithButton.vue +0 -94
- package/test/visitor-socket.test.mjs +0 -36
|
@@ -1,23 +1,32 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The "you must be a member of an organization" gate.
|
|
3
|
+
|
|
4
|
+
Was Vuetify's chrome: `text-subtitle-1` for the sentence, `text-subtitle-2`
|
|
5
|
+
on the buttons, and `variant="tonal" rounded="xl"` - a 24px-cornered pill in
|
|
6
|
+
the theme's tinted wash, where the product's action is a 40px, 10px-cornered
|
|
7
|
+
button. The sentence is now `--text2` (9.62:1 on a card) at the design's
|
|
8
|
+
reading size, and the actions are the design's primary and ghost buttons.
|
|
9
|
+
|
|
10
|
+
No behaviour change: same handlers, same destinations, same `plain` layout.
|
|
11
|
+
-->
|
|
1
12
|
<template>
|
|
2
13
|
<v-row no-gutters class="fill-height" align="center" justify="center">
|
|
3
|
-
<v-col cols="12" lg="6" md="6" sm="6" class="text-center
|
|
14
|
+
<v-col cols="12" lg="6" md="6" sm="6" class="text-center gate-page__message">
|
|
4
15
|
You must be a member of an organization to view this page. Please join or
|
|
5
16
|
contact your administrator for access.
|
|
6
17
|
<v-row no-gutters justify="center" align="center" class="mt-2">
|
|
7
18
|
<v-btn
|
|
19
|
+
class="screen-btn-ghost"
|
|
20
|
+
variant="flat"
|
|
8
21
|
@click="goToAccount"
|
|
9
|
-
rounded="xl"
|
|
10
|
-
variant="tonal"
|
|
11
|
-
class="text-none text-subtitle-2"
|
|
12
22
|
>
|
|
13
23
|
Go to Account Page
|
|
14
24
|
</v-btn>
|
|
15
|
-
<span class="mx-2">or</span>
|
|
25
|
+
<span class="mx-2 gate-page__or">or</span>
|
|
16
26
|
<v-btn
|
|
27
|
+
class="screen-btn-primary"
|
|
28
|
+
variant="flat"
|
|
17
29
|
@click="createOrg"
|
|
18
|
-
rounded="xl"
|
|
19
|
-
variant="tonal"
|
|
20
|
-
class="text-none text-subtitle-2"
|
|
21
30
|
>
|
|
22
31
|
Create an Organization
|
|
23
32
|
</v-btn>
|
|
@@ -45,3 +54,18 @@ function createOrg() {
|
|
|
45
54
|
}
|
|
46
55
|
}
|
|
47
56
|
</script>
|
|
57
|
+
|
|
58
|
+
<style scoped>
|
|
59
|
+
.gate-page__message {
|
|
60
|
+
font-family: var(--font-sans);
|
|
61
|
+
font-size: var(--fs-cell);
|
|
62
|
+
color: var(--text2);
|
|
63
|
+
line-height: 1.6;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.gate-page__or {
|
|
67
|
+
font-family: var(--font-sans);
|
|
68
|
+
font-size: var(--fs-cell);
|
|
69
|
+
color: var(--muted);
|
|
70
|
+
}
|
|
71
|
+
</style>
|
package/pages/unauthorized.vue
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The "you are not a member of an organization" gate.
|
|
3
|
+
|
|
4
|
+
Was Vuetify's chrome: `text-subtitle-1` for the sentence, `text-subtitle-2`
|
|
5
|
+
on the buttons, and `variant="tonal" rounded="xl"` - a 24px-cornered pill in
|
|
6
|
+
the theme's tinted wash, where the product's action is a 40px, 10px-cornered
|
|
7
|
+
button. The sentence is now `--text2` (9.62:1 on a card) at the design's
|
|
8
|
+
reading size, and the actions are the design's primary and ghost buttons.
|
|
9
|
+
|
|
10
|
+
No behaviour change: same handlers, same destinations, same `plain` layout.
|
|
11
|
+
-->
|
|
1
12
|
<template>
|
|
2
13
|
<v-row no-gutters class="fill-height" align="center" justify="center">
|
|
3
|
-
<v-col cols="12" lg="6" md="6" sm="6" class="text-center
|
|
14
|
+
<v-col cols="12" lg="6" md="6" sm="6" class="text-center gate-page__message">
|
|
4
15
|
You must be a member of an organization to view this page. Please join or
|
|
5
16
|
contact your administrator for access.
|
|
6
17
|
<v-row no-gutters justify="center" class="mt-2">
|
|
7
18
|
<v-btn
|
|
19
|
+
class="screen-btn-primary"
|
|
20
|
+
variant="flat"
|
|
8
21
|
@click="goToAccount"
|
|
9
|
-
rounded="xl"
|
|
10
|
-
variant="tonal"
|
|
11
|
-
class="text-none text-subtitle-2"
|
|
12
22
|
>
|
|
13
23
|
Go to Account Page
|
|
14
24
|
</v-btn>
|
|
@@ -27,3 +37,18 @@ function goToAccount() {
|
|
|
27
37
|
window.location.href = `${APP_ACCOUNT}/home`;
|
|
28
38
|
}
|
|
29
39
|
</script>
|
|
40
|
+
|
|
41
|
+
<style scoped>
|
|
42
|
+
.gate-page__message {
|
|
43
|
+
font-family: var(--font-sans);
|
|
44
|
+
font-size: var(--fs-cell);
|
|
45
|
+
color: var(--text2);
|
|
46
|
+
line-height: 1.6;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.gate-page__or {
|
|
50
|
+
font-family: var(--font-sans);
|
|
51
|
+
font-size: var(--fs-cell);
|
|
52
|
+
color: var(--muted);
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
@@ -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
|
@@ -43,8 +43,75 @@ import {
|
|
|
43
43
|
import "vuetify-pro-tiptap/style.css";
|
|
44
44
|
|
|
45
45
|
import { LIGHT_THEME, DARK_THEME } from "../utils/theme";
|
|
46
|
+
import { MDI_TO_SYMBOL } from "../utils/materialSymbols";
|
|
47
|
+
import { THEME_COOKIE } from "../composables/useThemePreference";
|
|
48
|
+
import type { CookieOptions } from "#app";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The design tokens. Loaded HERE rather than in `nuxt.config.ts` so they land
|
|
52
|
+
* after `vuetify/styles` in the bundle - `tokens.css` aliases `--v-theme-*`,
|
|
53
|
+
* which Vuetify has to have emitted first.
|
|
54
|
+
*/
|
|
55
|
+
import "../assets/css/tokens.css";
|
|
56
|
+
import "../assets/css/shell.css";
|
|
57
|
+
import "../assets/css/screens.css";
|
|
58
|
+
import "../assets/css/primitives.css";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* THE ICON SET.
|
|
62
|
+
*
|
|
63
|
+
* The design handoff draws icons with Google's `Material Symbols Outlined`
|
|
64
|
+
* variable font - the stylesheet for it is already requested in
|
|
65
|
+
* `nuxt.config.ts` - while the product asks for Material Design Icons names
|
|
66
|
+
* (`mdi-close`, `mdi-magnify`, ...) in roughly 680 places here and again in
|
|
67
|
+
* every application. Translating at the icon set means none of those call sites
|
|
68
|
+
* change: a component still writes `mdi-close`, and this renders the Material
|
|
69
|
+
* Symbols glyph for it.
|
|
70
|
+
*
|
|
71
|
+
* A name with no entry in the table falls through to the previous Iconify
|
|
72
|
+
* rendering, so an unrecognised icon draws its old glyph instead of a blank
|
|
73
|
+
* box. `mdi-blank` maps to an empty string on purpose - it is mdi's deliberate
|
|
74
|
+
* spacer, and `.v-icon` keeps the 1em box.
|
|
75
|
+
*
|
|
76
|
+
* `icon` and `tag` are declared props so Vue does not also emit them as
|
|
77
|
+
* literal attributes; everything else Vuetify passes (the `v-icon` classes,
|
|
78
|
+
* size, colour, click handling) falls through to the root element untouched.
|
|
79
|
+
*/
|
|
80
|
+
const MaterialSymbol = (props: any) => {
|
|
81
|
+
// "mdi-loading mdi-spin" - the name first, any mdi modifier classes after it.
|
|
82
|
+
const [name, ...modifiers] = String(props.icon ?? "").split(/\s+/);
|
|
83
|
+
const symbol = MDI_TO_SYMBOL[name];
|
|
84
|
+
|
|
85
|
+
return symbol === undefined
|
|
86
|
+
? h(Icon, { icon: props.icon })
|
|
87
|
+
: h(
|
|
88
|
+
props.tag ?? "span",
|
|
89
|
+
{ class: ["material-symbols-outlined", ...modifiers] },
|
|
90
|
+
symbol,
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
MaterialSymbol.props = ["icon", "tag"];
|
|
46
94
|
|
|
47
95
|
export default defineNuxtPlugin((app) => {
|
|
96
|
+
/**
|
|
97
|
+
* BOOT ON THE REMEMBERED THEME, DON'T CORRECT IT AFTERWARDS.
|
|
98
|
+
*
|
|
99
|
+
* Reading the cookie here, before `createVuetify`, is what makes the choice
|
|
100
|
+
* survive a reload with no flash: Vuetify's first paint is already the right
|
|
101
|
+
* theme, so there is no moment where the wrong one is on screen. Setting it
|
|
102
|
+
* from a component's `onMounted` would repaint the whole application one
|
|
103
|
+
* frame in, which is the flash this is written to avoid.
|
|
104
|
+
*
|
|
105
|
+
* `useThemePreference()` writes this cookie; nothing else does.
|
|
106
|
+
*/
|
|
107
|
+
const stored = useCookie<string | null>(
|
|
108
|
+
THEME_COOKIE,
|
|
109
|
+
// typed `unknown` in runtimeConfig; same object `sid` and `user` use
|
|
110
|
+
useRuntimeConfig().public.cookieConfig as CookieOptions<string | null> & {
|
|
111
|
+
readonly?: false;
|
|
112
|
+
}
|
|
113
|
+
).value;
|
|
114
|
+
|
|
48
115
|
const vuetify = createVuetify({
|
|
49
116
|
defaults: {
|
|
50
117
|
VTextField: {
|
|
@@ -97,7 +164,7 @@ export default defineNuxtPlugin((app) => {
|
|
|
97
164
|
* `utils/theme.ts`, which `utils/theme.test.ts` measures on every run.
|
|
98
165
|
*/
|
|
99
166
|
theme: {
|
|
100
|
-
defaultTheme: "light",
|
|
167
|
+
defaultTheme: stored === "dark" ? "dark" : "light",
|
|
101
168
|
themes: {
|
|
102
169
|
light: LIGHT_THEME,
|
|
103
170
|
dark: DARK_THEME,
|
|
@@ -114,7 +181,7 @@ export default defineNuxtPlugin((app) => {
|
|
|
114
181
|
defaultSet: "iconify",
|
|
115
182
|
sets: {
|
|
116
183
|
iconify: {
|
|
117
|
-
component:
|
|
184
|
+
component: MaterialSymbol,
|
|
118
185
|
},
|
|
119
186
|
},
|
|
120
187
|
},
|
|
@@ -85,9 +85,10 @@ export interface NFCPatrolSummaryReport {
|
|
|
85
85
|
export interface NFCPatrolMonthlyReportRow {
|
|
86
86
|
month: string;
|
|
87
87
|
checked: number;
|
|
88
|
-
|
|
88
|
+
/** `null` when no checkpoint was due that month - see `utils/nfc-patrol-report.ts`. */
|
|
89
|
+
checkedPercentage: number | null;
|
|
89
90
|
missed: number;
|
|
90
|
-
missedPercentage: number;
|
|
91
|
+
missedPercentage: number | null;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
export interface NFCPatrolMonthlyReport {
|
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;
|
package/types/subscription.ts
CHANGED
|
@@ -5,10 +5,8 @@ export type PlanType = "free_bundle" | "paid_bundle" | "custom_apps";
|
|
|
5
5
|
export type BillingCycle = "monthly" | "annually";
|
|
6
6
|
|
|
7
7
|
export interface AppOption {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
monthlyPrice: number;
|
|
11
|
-
yearlyPrice: number;
|
|
8
|
+
title: string;
|
|
9
|
+
value: string;
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
export interface SubscriptionPlan {
|
|
@@ -32,6 +30,7 @@ export interface PlanFormState {
|
|
|
32
30
|
bundleApps: string[];
|
|
33
31
|
bundlePrice: number | null;
|
|
34
32
|
customApps: string[];
|
|
33
|
+
customAppPrices: Record<string, number>;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
export const PLAN_TYPE_LABEL: Record<PlanType, string> = {
|
|
@@ -40,12 +39,20 @@ export const PLAN_TYPE_LABEL: Record<PlanType, string> = {
|
|
|
40
39
|
custom_apps: "Paid (Custom Apps)",
|
|
41
40
|
};
|
|
42
41
|
|
|
43
|
-
//
|
|
42
|
+
// Catalogue of applications that can be bundled into a plan.
|
|
44
43
|
// Replace with data fetched from your backend.
|
|
45
|
-
export const
|
|
46
|
-
{
|
|
47
|
-
{
|
|
48
|
-
{
|
|
49
|
-
{
|
|
50
|
-
{
|
|
44
|
+
export const APP_LIST: AppOption[] = [
|
|
45
|
+
{ title: "Organization", value: "organization" },
|
|
46
|
+
{ title: "Security Agency", value: "security_agency" },
|
|
47
|
+
{ title: "Cleaning Services", value: "cleaning_services" },
|
|
48
|
+
{ title: "Property Management Agency", value: "property_management_agency" },
|
|
49
|
+
{ title: "Mechanical & Electrical Services", value: "mechanical_electrical_services" },
|
|
50
|
+
{ title: "Pest Control Services", value: "pest_control_services" },
|
|
51
|
+
{ title: "Landscaping Services", value: "landscaping_services" },
|
|
52
|
+
{ title: "Pool Maintenance Services", value: "pool_maintenance_services" },
|
|
51
53
|
];
|
|
54
|
+
|
|
55
|
+
// Static placeholder price applied to every app, for now.
|
|
56
|
+
// Replace with real per-app pricing once that's available.
|
|
57
|
+
export const STATIC_APP_MONTHLY_PRICE = 49;
|
|
58
|
+
export const STATIC_APP_YEARLY_PRICE = 490;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { moduleNameFromRoute } from "./breadcrumb.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Real routes, not invented ones: name, path and parameters exactly as
|
|
8
|
+
* vue-router hands them to the bar in each application.
|
|
9
|
+
*/
|
|
10
|
+
const ID = "68b0c1d2e3f4a5b6c7d8e9f0";
|
|
11
|
+
|
|
12
|
+
test("a site application names the page, not its parameters", () => {
|
|
13
|
+
assert.equal(
|
|
14
|
+
moduleNameFromRoute({
|
|
15
|
+
name: "org-site-work-orders",
|
|
16
|
+
path: `/${ID}/${ID}/work-orders`,
|
|
17
|
+
params: { org: ID, site: ID },
|
|
18
|
+
}),
|
|
19
|
+
"Work Orders"
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("a detail route names its module, not its argument", () => {
|
|
24
|
+
assert.equal(
|
|
25
|
+
moduleNameFromRoute({
|
|
26
|
+
name: "org-site-feedbacks-id",
|
|
27
|
+
path: `/${ID}/${ID}/feedbacks/${ID}`,
|
|
28
|
+
params: { org: ID, site: ID, id: ID },
|
|
29
|
+
}),
|
|
30
|
+
"Feedbacks"
|
|
31
|
+
);
|
|
32
|
+
assert.equal(
|
|
33
|
+
moduleNameFromRoute({
|
|
34
|
+
name: "org-site-members-status",
|
|
35
|
+
path: `/${ID}/${ID}/members/pending`,
|
|
36
|
+
params: { org: ID, site: ID, status: "pending" },
|
|
37
|
+
}),
|
|
38
|
+
"Members"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("a fixed folder in front of the context parameter is not the page", () => {
|
|
43
|
+
// THE DEFECT: every screen of the organization application read "Org".
|
|
44
|
+
assert.equal(
|
|
45
|
+
moduleNameFromRoute({
|
|
46
|
+
name: "org-organization-marketplace",
|
|
47
|
+
path: `/org/${ID}/marketplace`,
|
|
48
|
+
params: { organization: ID },
|
|
49
|
+
}),
|
|
50
|
+
"Marketplace"
|
|
51
|
+
);
|
|
52
|
+
assert.equal(
|
|
53
|
+
moduleNameFromRoute({
|
|
54
|
+
name: "org-organization-payment-methods-linked",
|
|
55
|
+
path: `/org/${ID}/payment-methods/linked`,
|
|
56
|
+
params: { organization: ID },
|
|
57
|
+
}),
|
|
58
|
+
"Payment Methods Linked"
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a literal second segment is NOT a skipped context", () => {
|
|
63
|
+
// `/organizations/status/:status` also begins with a literal folder, but
|
|
64
|
+
// "Organizations" is the page here - only the THIRD segment is a parameter.
|
|
65
|
+
assert.equal(
|
|
66
|
+
moduleNameFromRoute({
|
|
67
|
+
name: "organizations-status-status",
|
|
68
|
+
path: "/organizations/status/pending",
|
|
69
|
+
params: { status: "pending" },
|
|
70
|
+
}),
|
|
71
|
+
"Organizations"
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a route with no parameters is left alone", () => {
|
|
76
|
+
assert.equal(
|
|
77
|
+
moduleNameFromRoute({
|
|
78
|
+
name: "super-admin-client-list",
|
|
79
|
+
path: "/super-admin/client-list",
|
|
80
|
+
params: {},
|
|
81
|
+
}),
|
|
82
|
+
"Super Admin Client List"
|
|
83
|
+
);
|
|
84
|
+
assert.equal(
|
|
85
|
+
moduleNameFromRoute({ name: "personal-info-name", path: "/personal-info/name", params: {} }),
|
|
86
|
+
"Personal Info Name"
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("acronyms keep their case", () => {
|
|
91
|
+
assert.equal(
|
|
92
|
+
moduleNameFromRoute({
|
|
93
|
+
name: "org-site-virtual-patrol-cctv",
|
|
94
|
+
path: `/${ID}/${ID}/virtual-patrol/cctv`,
|
|
95
|
+
params: { org: ID, site: ID },
|
|
96
|
+
}),
|
|
97
|
+
"Virtual Patrol CCTV"
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a context-only route has no page half", () => {
|
|
102
|
+
// The organization dashboard: the bar shows the organization's name alone.
|
|
103
|
+
assert.equal(
|
|
104
|
+
moduleNameFromRoute({ name: "org-organization", path: `/org/${ID}`, params: { organization: ID } }),
|
|
105
|
+
""
|
|
106
|
+
);
|
|
107
|
+
assert.equal(moduleNameFromRoute({ name: "", path: "/", params: {} }), "");
|
|
108
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE TOP BAR'S BREADCRUMB, PAGE HALF.
|
|
3
|
+
*
|
|
4
|
+
* The bar draws `Context / Page`. The page half is derived from the route the
|
|
5
|
+
* router is already on - nothing is fetched and nothing is configured per app,
|
|
6
|
+
* because the applications build their route names from their own folder
|
|
7
|
+
* structure: `org-site-work-orders` is Work Orders.
|
|
8
|
+
*
|
|
9
|
+
* Lives here rather than in the component so the rule is testable: it is the
|
|
10
|
+
* one piece of the bar with real branching, and it named a whole application's
|
|
11
|
+
* screens wrongly for want of a case.
|
|
12
|
+
*/
|
|
13
|
+
const ACRONYMS: Record<string, string> = {
|
|
14
|
+
cctv: "CCTV",
|
|
15
|
+
hid: "HID",
|
|
16
|
+
dob: "DOB",
|
|
17
|
+
anpr: "ANPR",
|
|
18
|
+
nfc: "NFC",
|
|
19
|
+
qr: "QR",
|
|
20
|
+
soa: "SOA",
|
|
21
|
+
sp: "SP",
|
|
22
|
+
id: "ID",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function moduleNameFromRoute(route: {
|
|
26
|
+
name?: unknown;
|
|
27
|
+
path?: unknown;
|
|
28
|
+
params?: Record<string, unknown> | null;
|
|
29
|
+
}): string {
|
|
30
|
+
const segments = String(route.name ?? "").split("-").filter(Boolean);
|
|
31
|
+
const params = new Set(Object.keys(route.params ?? {}));
|
|
32
|
+
|
|
33
|
+
let i = 0;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A FIXED FOLDER IN FRONT OF THE CONTEXT PARAMETER IS NOT THE PAGE.
|
|
37
|
+
*
|
|
38
|
+
* The site applications are `/:org/:site/...`, so skipping the leading
|
|
39
|
+
* parameters was enough. The organization application is
|
|
40
|
+
* `/org/:organization/...` - a literal folder first - and the route NAME
|
|
41
|
+
* cannot tell that "org" is a folder and not a parameter, so every one of
|
|
42
|
+
* that application's screens named itself "Org" instead of the page it is.
|
|
43
|
+
*
|
|
44
|
+
* The PATH can tell: its first segment is that same word, and the segment
|
|
45
|
+
* after it is a parameter's VALUE. Checked against the path rather than
|
|
46
|
+
* assumed, because `/organizations/status/:status` also begins with a
|
|
47
|
+
* literal - and there "Organizations Status" is the right page name, its
|
|
48
|
+
* second path segment being the folder `status` and not the parameter's
|
|
49
|
+
* value.
|
|
50
|
+
*/
|
|
51
|
+
const values = new Set(
|
|
52
|
+
Object.values(route.params ?? {})
|
|
53
|
+
.flat()
|
|
54
|
+
.map((v) => String(v))
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
);
|
|
57
|
+
const pathParts = String(route.path ?? "").split("/").filter(Boolean);
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
segments.length > 1 &&
|
|
61
|
+
!params.has(segments[0]) &&
|
|
62
|
+
pathParts[0] === segments[0] &&
|
|
63
|
+
values.has(String(pathParts[1] ?? ""))
|
|
64
|
+
) {
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Leading parameter segments (`org`, `site`) are skipped and the name stops
|
|
70
|
+
* at the first one that follows, so a detail route (`...-feedbacks-id`) and
|
|
71
|
+
* a status route (`...-members-status`) both name their module rather than
|
|
72
|
+
* their argument.
|
|
73
|
+
*/
|
|
74
|
+
while (i < segments.length && params.has(segments[i])) i++;
|
|
75
|
+
|
|
76
|
+
const words: string[] = [];
|
|
77
|
+
while (i < segments.length && !params.has(segments[i])) words.push(segments[i++]);
|
|
78
|
+
|
|
79
|
+
return words
|
|
80
|
+
.map((w) => ACRONYMS[w] ?? w.charAt(0).toUpperCase() + w.slice(1))
|
|
81
|
+
.join(" ");
|
|
82
|
+
}
|