@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.
Files changed (283) hide show
  1. package/assets/css/primitives.css +1113 -0
  2. package/assets/css/screens.css +1097 -0
  3. package/assets/css/shell.css +258 -0
  4. package/assets/css/tokens.css +236 -0
  5. package/components/AcceptDialog.vue +17 -18
  6. package/components/AccessCard/AvailableStats.vue +24 -16
  7. package/components/AccessCardAddForm.vue +42 -42
  8. package/components/AccessCardAssignToUnitForm.vue +13 -21
  9. package/components/AccessCardDeleteDialog.vue +41 -39
  10. package/components/AccessCardDetailsDialog.vue +125 -79
  11. package/components/AccessCardPreviewDialog.vue +58 -84
  12. package/components/AccessCardQrTagging.vue +60 -63
  13. package/components/AccessCardReplaceForm.vue +27 -23
  14. package/components/AccessManagement.vue +86 -85
  15. package/components/AddEqupmentForm.vue +18 -22
  16. package/components/AddPassKeyToVisitor.vue +95 -73
  17. package/components/AppButton.vue +62 -0
  18. package/components/AppCard.vue +20 -0
  19. package/components/AppDateField.vue +49 -0
  20. package/components/AppField.vue +76 -0
  21. package/components/AppKpiTile.vue +65 -0
  22. package/components/AppSegmented.vue +38 -0
  23. package/components/AppSelect.vue +247 -0
  24. package/components/AreaChecklistHistoryLogs.vue +51 -44
  25. package/components/AreaChecklistHistoryMain.vue +14 -7
  26. package/components/AreaFormDialog.vue +71 -59
  27. package/components/AreaMain.vue +73 -107
  28. package/components/AttachmentsViewer.vue +84 -22
  29. package/components/AttendanceCheckInOutDialog.vue +85 -88
  30. package/components/AttendanceDetailsDialog.vue +57 -63
  31. package/components/AttendanceMain.vue +4 -3
  32. package/components/AttendanceMapSearchDialog.vue +42 -44
  33. package/components/AttendanceSettingsDialog.vue +79 -63
  34. package/components/Avatar/Main.vue +54 -2
  35. package/components/BillingMain.vue +26 -30
  36. package/components/BtnUploadFile.vue +53 -11
  37. package/components/BuildingForm.vue +34 -27
  38. package/components/BuildingManagement/buildings.vue +39 -39
  39. package/components/BuildingManagement/units.vue +26 -42
  40. package/components/BuildingUnitFormAdd.vue +20 -23
  41. package/components/BuildingUnitFormEdit.vue +8 -4
  42. package/components/BulletinBoardForm.vue +19 -24
  43. package/components/BulletinBoardManagement.vue +49 -57
  44. package/components/BulletinBoardView.vue +9 -6
  45. package/components/BulletinExpirationChip.vue +13 -4
  46. package/components/Button/Close.vue +2 -2
  47. package/components/CameraForm.vue +21 -22
  48. package/components/CameraMain.vue +50 -49
  49. package/components/Card/DeleteConfirmation.vue +24 -22
  50. package/components/Card/MemberInfoSummary.vue +28 -3
  51. package/components/Card/Toggle.vue +15 -3
  52. package/components/CardToolbar.vue +37 -0
  53. package/components/Carousel.vue +56 -36
  54. package/components/Chat/Bubbles.vue +43 -14
  55. package/components/Chat/Information.vue +49 -32
  56. package/components/Chat/ListCard.vue +38 -9
  57. package/components/Chat/Message.vue +41 -15
  58. package/components/Chat/Navigation.vue +27 -15
  59. package/components/Chat/SkeletonLoader.vue +9 -2
  60. package/components/ChecklistItemRow.vue +25 -1
  61. package/components/CleaningScheduleMain.vue +44 -49
  62. package/components/ClientDetailForm.vue +81 -56
  63. package/components/ClientMain.vue +92 -82
  64. package/components/ConfirmDialog.vue +14 -8
  65. package/components/Container/Standard.vue +9 -7
  66. package/components/CreateSubsciptionPlan.vue +148 -89
  67. package/components/DashboardEmptyState.vue +48 -3
  68. package/components/DashboardMain.vue +924 -676
  69. package/components/DashboardPanel.vue +84 -21
  70. package/components/DeliveryCompany.vue +34 -20
  71. package/components/Dialog/DeleteConfirmation.vue +20 -21
  72. package/components/Dialog/ReplaceAutofillPrompt.vue +19 -26
  73. package/components/Dialog/ReusablePrompt.vue +30 -29
  74. package/components/Dialog/UpdateMoreAction.vue +62 -59
  75. package/components/DocumentForm.vue +14 -21
  76. package/components/DocumentManagement.vue +162 -108
  77. package/components/DocumentViewer.vue +29 -12
  78. package/components/DrawImage.vue +29 -21
  79. package/components/EntryPass/QrTemplatePreview.vue +27 -16
  80. package/components/EntryPassInformation.vue +40 -10
  81. package/components/EntryPassMain.vue +49 -27
  82. package/components/EquipmentItemMain.vue +84 -97
  83. package/components/EquipmentManagementMain.vue +81 -103
  84. package/components/Facility/BookingSetup.vue +43 -52
  85. package/components/Feedback/Form.vue +26 -12
  86. package/components/FeedbackMain.vue +35 -49
  87. package/components/FileInput.vue +75 -58
  88. package/components/FileInputWithList.vue +59 -12
  89. package/components/FormDialog.vue +60 -21
  90. package/components/HidAccessLogDashboard.vue +512 -490
  91. package/components/HidCameraCaptureDialog.vue +216 -0
  92. package/components/HidEnabledGate.vue +67 -9
  93. package/components/HidIntercomManagement.vue +1322 -653
  94. package/components/HidQrCodeConfiguration.vue +1030 -258
  95. package/components/HidReaderForm.vue +142 -59
  96. package/components/HidReaderManagement.vue +211 -323
  97. package/components/HidServiceSettingsPanel.vue +44 -13
  98. package/components/HidUserEnrollment.vue +1185 -412
  99. package/components/HygieneUpdateMoreAction.vue +44 -44
  100. package/components/IncidentReport/Authorities.vue +30 -18
  101. package/components/IncidentReport/IncidentInformation.vue +10 -10
  102. package/components/IncidentReport/IncidentInformationDownload.vue +3 -4
  103. package/components/IncidentReport/affectedEntities.vue +41 -20
  104. package/components/Input/Date.vue +3 -0
  105. package/components/Input/DatePicker.vue +1 -2
  106. package/components/Input/DateTimePicker.vue +1 -1
  107. package/components/Input/File.vue +55 -10
  108. package/components/Input/FileV2.vue +39 -6
  109. package/components/Input/InputPhoneNumberV2.vue +2 -1
  110. package/components/Input/ListGroupSelection.vue +56 -8
  111. package/components/Input/NRICNumber.vue +1 -0
  112. package/components/Input/NewDate.vue +1 -0
  113. package/components/Input/Number.vue +1 -0
  114. package/components/Input/Password.vue +1 -0
  115. package/components/Input/PhoneNumber.vue +21 -1
  116. package/components/Input/VehicleNumber.vue +1 -0
  117. package/components/InputLabel.vue +2 -2
  118. package/components/InvitationClientForm.vue +21 -19
  119. package/components/InvitationForm.vue +15 -21
  120. package/components/InvitationMain.vue +46 -61
  121. package/components/Layout/Header.vue +263 -36
  122. package/components/Layout/NavigationDrawer.vue +300 -30
  123. package/components/ListItem.vue +33 -3
  124. package/components/LocalPagination.vue +27 -3
  125. package/components/ManageChecklistMain.vue +136 -86
  126. package/components/MemberInformation.vue +32 -16
  127. package/components/MemberMain.vue +113 -93
  128. package/components/MyAttendanceMain.vue +3 -5
  129. package/components/NavigationItem.vue +83 -3
  130. package/components/Nfc/NFCPatrolReportMain.vue +47 -33
  131. package/components/Nfc/NFCPatrolRouteForm.vue +18 -20
  132. package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
  133. package/components/Nfc/NFCPatrolSettings.vue +14 -4
  134. package/components/Nfc/NFCTagForm.vue +22 -8
  135. package/components/Nfc/NFCTagMain.vue +41 -43
  136. package/components/Nfc/PatrolReport/DailyReportTab.vue +8 -1
  137. package/components/Nfc/PatrolReport/MonthlyReportTab.vue +8 -1
  138. package/components/Nfc/PatrolReport/MonthlyReportTable.vue +5 -3
  139. package/components/Nfc/PatrolReport/PatrolActivityTable.vue +26 -6
  140. package/components/Nfc/PatrolReport/PatrolReportTab.vue +8 -0
  141. package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +41 -9
  142. package/components/Nfc/PatrolReport/RemarksDialog.vue +39 -9
  143. package/components/Nfc/PatrolReport/ReportEmptyState.vue +29 -9
  144. package/components/Nfc/PatrolReport/ReportFilters.vue +19 -10
  145. package/components/Nfc/PatrolReport/ReportLocationHeader.vue +32 -6
  146. package/components/Nfc/PatrolReport/SummaryReportTable.vue +28 -6
  147. package/components/NotificationSettings.vue +373 -0
  148. package/components/NumberSettingField.vue +9 -2
  149. package/components/OnlineFormConfigurationForm.vue +725 -237
  150. package/components/OnlineFormFill.vue +213 -106
  151. package/components/OnlineFormRenderPreview.vue +25 -30
  152. package/components/OnlineFormsConfiguration.vue +113 -109
  153. package/components/OrgViewDialog.vue +34 -16
  154. package/components/OvernightParkingAvailability.vue +49 -26
  155. package/components/OvernightParkingManagement.vue +78 -18
  156. package/components/PageHeader.vue +43 -0
  157. package/components/PassInformation.vue +109 -53
  158. package/components/PasswordConfirmation.vue +44 -26
  159. package/components/PeopleForm.vue +41 -20
  160. package/components/PhotoUpload.vue +110 -48
  161. package/components/PlaceholderComponent.vue +35 -19
  162. package/components/PlanActionsMenu.vue +12 -2
  163. package/components/PlanStatusBadge.vue +7 -8
  164. package/components/PlateNumberDisplay.vue +9 -6
  165. package/components/QrTemplate/PrintDialog.vue +66 -43
  166. package/components/RolePermissionFormCreate.vue +104 -79
  167. package/components/RolePermissionFormPreviewUpdate.vue +111 -109
  168. package/components/RolePermissionMain.vue +183 -81
  169. package/components/ScanVisitorQRCode.vue +99 -35
  170. package/components/ScheduleAreaMain.vue +34 -59
  171. package/components/ScheduleTaskForm.vue +61 -42
  172. package/components/ScheduleTaskMain.vue +56 -69
  173. package/components/SearchVehicleNumberUser.vue +39 -37
  174. package/components/ServiceProviderFormCreate.vue +29 -20
  175. package/components/ServiceProviderInvitationPrompt.vue +155 -0
  176. package/components/ServiceProviderMain.vue +601 -432
  177. package/components/Signature.vue +40 -34
  178. package/components/SignaturePad.vue +5 -29
  179. package/components/SiteSettings.vue +41 -11
  180. package/components/SiteSettingsWorkOrder.vue +61 -30
  181. package/components/SlideCardGroup.vue +35 -11
  182. package/components/Snackbar.vue +36 -3
  183. package/components/SpecificAttr.vue +18 -8
  184. package/components/StatusChip.vue +156 -0
  185. package/components/StockCard.vue +17 -12
  186. package/components/SubscriptionPlanTable.vue +207 -140
  187. package/components/SwitchContext.vue +201 -9
  188. package/components/TableHygiene.vue +36 -46
  189. package/components/TableList.vue +10 -10
  190. package/components/TableListSecondary.vue +105 -54
  191. package/components/TableMain.vue +241 -79
  192. package/components/TableV3.vue +61 -70
  193. package/components/TiptapEditor.vue +33 -40
  194. package/components/TiptapEditorQr.vue +5 -2
  195. package/components/Tooltip/Info.vue +69 -24
  196. package/components/UnitMain.vue +40 -60
  197. package/components/UnitPersonCard.vue +42 -13
  198. package/components/VehicleAddSelection.vue +48 -32
  199. package/components/VehicleForm.vue +112 -129
  200. package/components/VehicleManagement.vue +26 -12
  201. package/components/VehicleUpdateMoreAction.vue +59 -46
  202. package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
  203. package/components/VisitorForm.vue +774 -210
  204. package/components/VisitorFormSelection.vue +40 -37
  205. package/components/VisitorManagement.vue +239 -82
  206. package/components/VisitorPassKeyQRScanner.vue +65 -18
  207. package/components/VisitorSearchResultSummary.vue +60 -16
  208. package/components/VisitorSocketPopUp.vue +22 -14
  209. package/components/VisitorsReportPreview.vue +44 -29
  210. package/components/WorkOrder/Create.vue +26 -17
  211. package/components/WorkOrder/ListView.vue +38 -7
  212. package/components/WorkOrder/Main.vue +62 -59
  213. package/composables/useFeedback.ts +3 -3
  214. package/composables/useHidAmico.ts +269 -0
  215. package/composables/useHidNavigation.ts +2 -9
  216. package/composables/useMember.ts +3 -3
  217. package/composables/useNFCPatrolDailyReport.ts +13 -0
  218. package/composables/useNFCPatrolMonthlyReport.ts +22 -56
  219. package/composables/useNFCPatrolReport.ts +13 -0
  220. package/composables/useNotificationPreference.ts +87 -0
  221. package/composables/useOnlineFormPages.ts +2 -0
  222. package/composables/useRole.ts +22 -0
  223. package/composables/useServiceProvider.ts +3 -3
  224. package/composables/useServiceProviderInvitation.ts +145 -0
  225. package/composables/useSipWebPhone.ts +336 -0
  226. package/composables/useSubscriptionPlan.ts +62 -0
  227. package/composables/useThemePreference.ts +63 -0
  228. package/composables/useVisitor.ts +6 -1
  229. package/composables/useWebUsb.ts +127 -37
  230. package/composables/useWorkOrder.ts +17 -3
  231. package/error.vue +30 -6
  232. package/nuxt.config.ts +36 -0
  233. package/package.json +25 -4
  234. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +2 -2
  235. package/pages/[org]/[site]/access-mgmt/{identity-mapping → hid-cards}/index.vue +2 -2
  236. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  237. package/pages/notification-settings.vue +19 -0
  238. package/pages/payment-method-linked.vue +34 -10
  239. package/pages/require-customer.vue +48 -10
  240. package/pages/require-organization-membership.vue +32 -8
  241. package/pages/unauthorized.vue +29 -4
  242. package/plugins/secure-member.client.ts +21 -56
  243. package/plugins/vuetify.ts +69 -2
  244. package/types/nfc-patrol-report.ts +3 -2
  245. package/types/site.d.ts +71 -0
  246. package/types/subscription.ts +18 -11
  247. package/utils/breadcrumb.test.ts +108 -0
  248. package/utils/breadcrumb.ts +82 -0
  249. package/utils/dashboard.test.ts +118 -0
  250. package/utils/dashboard.ts +53 -0
  251. package/utils/data.test.ts +95 -0
  252. package/utils/data.ts +40 -2
  253. package/utils/hid-discover.test.ts +102 -0
  254. package/utils/list-page.test.ts +60 -0
  255. package/utils/list-page.ts +38 -0
  256. package/utils/materialSymbols.ts +363 -0
  257. package/utils/nav-sections.test.ts +190 -0
  258. package/utils/nav-sections.ts +151 -0
  259. package/utils/nfc-patrol-report.test.ts +130 -0
  260. package/utils/nfc-patrol-report.ts +101 -0
  261. package/utils/role.test.ts +69 -0
  262. package/utils/role.ts +10 -0
  263. package/utils/route-name.test.ts +85 -0
  264. package/utils/route-name.ts +24 -0
  265. package/utils/status.test.ts +110 -0
  266. package/utils/status.ts +135 -0
  267. package/utils/table-card.test.ts +84 -0
  268. package/utils/theme-aa-ledger.ts +148 -0
  269. package/utils/theme.test.ts +485 -7
  270. package/utils/theme.ts +295 -110
  271. package/.changeset/README.md +0 -8
  272. package/.changeset/config.json +0 -11
  273. package/.editorconfig +0 -12
  274. package/.github/workflows/main.yml +0 -17
  275. package/.github/workflows/publish-staging.yml +0 -47
  276. package/.github/workflows/publish.yml +0 -39
  277. package/PUBLISHING.md +0 -269
  278. package/components/HidIdentityMapping.vue +0 -975
  279. package/components/ScheduleTaskAreaFormDialog.vue +0 -144
  280. package/components/ScheduleTaskAreaUpdateMoreAction.vue +0 -109
  281. package/components/ScheduleTastTicketMain.vue +0 -182
  282. package/components/TableWithButton.vue +0 -94
  283. 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 text-subtitle-1">
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>
@@ -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 text-subtitle-1">
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.afterEach(async (to) => {
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 = computed(
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
- console.log('[secure-member-plugin', 'org', org.value)
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 = computed(() => useCookie("user").value ?? "");
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
- watchEffect(() => {
56
- if (userMemberData.value) {
57
- id.value = userMemberData.value.org ?? "";
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
- const { data: getOrgByIdReq } = await useLazyAsyncData(
63
- "plugin-get-org-by-id-" + org.value,
64
- () => getById(org.value),
65
- { watch: [org] }
66
- );
35
+ const [orgResult, roleResult] = await Promise.allSettled([
36
+ getById(org),
37
+ userMemberData.role ? getRoleById(userMemberData.role) : null,
38
+ ]);
67
39
 
68
- watchEffect(() => {
69
- if (getOrgByIdReq.value) {
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
- const { data: getRoleByIdReq } = await useLazyAsyncData(
75
- "plugin-get-role-by-id-" + roleId.value,
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
  });
@@ -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: (props: any) => h(Icon, { icon: props.icon }),
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
- checkedPercentage: number;
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;
@@ -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
- id: string;
9
- name: string;
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
- // Mock catalogue of applications that can be bundled into a plan.
42
+ // Catalogue of applications that can be bundled into a plan.
44
43
  // Replace with data fetched from your backend.
45
- export const AVAILABLE_APPLICATIONS: AppOption[] = [
46
- { id: "crm", name: "CRM", monthlyPrice: 49, yearlyPrice: 490 },
47
- { id: "hrm", name: "HRM", monthlyPrice: 39, yearlyPrice: 390 },
48
- { id: "accounting", name: "Accounting", monthlyPrice: 59, yearlyPrice: 590 },
49
- { id: "inventory", name: "Inventory", monthlyPrice: 45, yearlyPrice: 450 },
50
- { id: "pos", name: "POS", monthlyPrice: 69, yearlyPrice: 690 },
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
+ }