@7365admin1/layer-common 3.2.7 → 3.2.8-staging.201

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 +251 -78
  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,13 +1,11 @@
1
1
  <template>
2
2
  <v-dialog v-model="showDialog" max-width="600" persistent>
3
- <v-card>
4
- <v-toolbar>
5
- <v-row no-gutters class="fill-height px-6" align="center">
6
- <span class="font-weight-bold text-h6">Attendance Settings</span>
7
- </v-row>
8
- </v-toolbar>
9
-
10
- <v-card-text class="pa-6">
3
+ <v-card class="screen-modal attendance-settings">
4
+ <div class="attendance-settings__head">
5
+ <v-card-title>Attendance Settings</v-card-title>
6
+ </div>
7
+
8
+ <div class="screen-modal__body attendance-settings__body filter-field">
11
9
  <v-alert
12
10
  v-if="error"
13
11
  type="error"
@@ -26,12 +24,13 @@
26
24
  <v-checkbox
27
25
  v-model="isGeofencingEnabled"
28
26
  label="Enable Geofencing"
27
+ color="accent"
29
28
  density="comfortable"
30
29
  hide-details
31
30
  />
32
31
  <span
33
32
  v-if="isGeofencingEnabled"
34
- class="text-body-2 text-medium-emphasis"
33
+ class="attendance-settings__hint"
35
34
  >
36
35
  Enabling this setting will require users to take attendance
37
36
  within a certain radius via QR code or within the site location.
@@ -43,44 +42,32 @@
43
42
  <v-checkbox
44
43
  v-model="isQrEnabled"
45
44
  label="Enable QR Code"
45
+ color="accent"
46
46
  density="comfortable"
47
47
  hide-details
48
48
  />
49
49
  </v-col>
50
50
 
51
51
  <v-col v-if="isQrEnabled" cols="12">
52
- <v-card
53
- variant="flat"
54
- class="pa-4 d-flex flex-column align-center"
55
- >
52
+ <div class="attendance-settings__qr">
56
53
  <QrcodeVue
57
54
  :value="site"
58
55
  :level="'M'"
59
56
  :render-as="'svg'"
60
57
  :size="300"
61
58
  />
62
- <v-btn
63
- variant="text"
64
- color="primary"
65
- class="mt-2"
66
- @click="downloadQrCode"
67
- >
68
- <v-icon class="mr-2">mdi-download</v-icon>
59
+ <AppButton variant="ghost" icon="mdi-download" class="mt-2" @click="downloadQrCode">
69
60
  Download QR Code
70
- </v-btn>
71
- </v-card>
61
+ </AppButton>
62
+ </div>
72
63
  </v-col>
73
64
 
74
65
  <v-col cols="12" class="mt-4">
75
66
  <InputLabel title="Site Location" />
76
- <v-btn
77
- variant="outlined"
78
- class="mt-2"
79
- @click="isMapSearchVisible = true"
80
- >
81
- <v-icon class="mr-2">mdi-map-search</v-icon>
67
+ <AppButton variant="ghost" icon="mdi-map-search" class="mt-2"
68
+ @click="isMapSearchVisible = true">
82
69
  Search Location
83
- </v-btn>
70
+ </AppButton>
84
71
  </v-col>
85
72
 
86
73
  <v-col v-if="geolocation" cols="12">
@@ -125,40 +112,12 @@
125
112
  </template>
126
113
  </v-row>
127
114
  </v-form>
128
- </v-card-text>
129
-
130
- <v-toolbar class="pa-0" density="compact">
131
- <v-row no-gutters>
132
- <v-col cols="6" class="pa-0">
133
- <v-btn
134
- block
135
- variant="text"
136
- class="text-none"
137
- size="large"
138
- @click="close"
139
- height="56"
140
- :disabled="submitting"
141
- >
142
- Cancel
143
- </v-btn>
144
- </v-col>
145
- <v-col cols="6" class="pa-0">
146
- <v-btn
147
- block
148
- variant="flat"
149
- color="black"
150
- class="text-none font-weight-bold rounded-0"
151
- height="56"
152
- size="large"
153
- :loading="submitting"
154
- @click="onSubmit"
155
- >
156
- <v-icon class="mr-2">mdi-check</v-icon>
157
- Update
158
- </v-btn>
159
- </v-col>
160
- </v-row>
161
- </v-toolbar>
115
+ </div>
116
+
117
+ <div class="attendance-settings__footer">
118
+ <AppButton variant="ghost" :disabled="submitting" @click="close">Cancel</AppButton>
119
+ <AppButton icon="mdi-check" :disabled="submitting" @click="onSubmit">Update</AppButton>
120
+ </div>
162
121
  </v-card>
163
122
  </v-dialog>
164
123
 
@@ -210,6 +169,7 @@ const {
210
169
  data: getAttendanceSettingsReq,
211
170
  refresh: getAttendanceSettingsRefresh,
212
171
  pending: loading,
172
+ error: getAttendanceSettingsError,
213
173
  } = await useLazyAsyncData(
214
174
  `attendance-settings-${props.site}`,
215
175
  () => getAttendanceSettings(props.site, props.serviceType),
@@ -220,6 +180,22 @@ const {
220
180
  );
221
181
 
222
182
  watchEffect(() => {
183
+ // A failed read used to be indistinguishable from a site with no settings
184
+ // saved: the dialog just opened with geofencing off and blank fields, and
185
+ // the operator had no way to tell it was showing them nothing rather than
186
+ // the truth. Worse, a request answered by the SPA fallback resolves with a
187
+ // 200 and a STRING, so nothing threw - every field read off it was simply
188
+ // undefined. Anything that is not an object is a failure, and it is said.
189
+ const res: any = getAttendanceSettingsReq.value;
190
+ if (
191
+ getAttendanceSettingsError.value ||
192
+ (res !== null && res !== undefined && typeof res !== "object")
193
+ ) {
194
+ error.value =
195
+ "Could not load the saved settings - what you see below is not what is saved. Close and reopen to try again.";
196
+ return;
197
+ }
198
+
223
199
  if (getAttendanceSettingsReq.value) {
224
200
  isGeofencingEnabled.value =
225
201
  getAttendanceSettingsReq.value.isGeofencingEnabled || false;
@@ -397,3 +373,43 @@ watch(showDialog, (newVal) => {
397
373
  }
398
374
  });
399
375
  </script>
376
+
377
+ <style scoped>
378
+ .attendance-settings__head {
379
+ display: flex;
380
+ align-items: center;
381
+ justify-content: space-between;
382
+ padding-right: 12px;
383
+ }
384
+
385
+ .attendance-settings__body {
386
+ padding: 0 20px;
387
+ }
388
+
389
+ .attendance-settings__hint {
390
+ font-size: var(--fs-cell);
391
+ color: var(--text2);
392
+ }
393
+
394
+ /* Was a `variant="flat"` v-card, i.e. a second card inside the modal card. It
395
+ is a frame around the QR, so it takes the design's inner surface. */
396
+ .attendance-settings__qr {
397
+ display: flex;
398
+ flex-direction: column;
399
+ align-items: center;
400
+ padding: 16px;
401
+ border: 1px solid var(--border);
402
+ border-radius: var(--r-inner);
403
+ background: var(--thead);
404
+ }
405
+
406
+ /* The design's dialog footer. Deliberately not the shared
407
+ `.screen-modal__footer` - that class is on the batch-D branch (#1150), not
408
+ on this base, and a second copy in `screens.css` would collide with it. */
409
+ .attendance-settings__footer {
410
+ display: flex;
411
+ justify-content: flex-end;
412
+ gap: 8px;
413
+ padding: 16px 20px 20px;
414
+ }
415
+ </style>
@@ -1,6 +1,18 @@
1
1
  <template>
2
2
  <v-avatar :color="getColor(name || id)" :size="size">
3
- <v-img v-if="imageSrc" alt="John" :src="imageUrl" />
3
+ <!--
4
+ A stored profile photo that 404s left the circle EMPTY - `v-img`
5
+ draws nothing when the source fails, and there was no fallback under
6
+ it. The initials the avatar already computes stand in, so the circle
7
+ is never blank.
8
+ -->
9
+ <v-img v-if="imageSrc" :alt="name" :src="imageUrl">
10
+ <template #error>
11
+ <div class="d-flex align-center justify-center fill-height">
12
+ <span :style="{ fontSize: `${fontSize}px` }">{{ initials }}</span>
13
+ </div>
14
+ </template>
15
+ </v-img>
4
16
  <span v-else :style="{
5
17
  fontSize: `${fontSize}px`,
6
18
  lineHeight: `${size}px`
@@ -43,6 +55,39 @@ const initials = computed(() => {
43
55
 
44
56
  const fontSize = computed(() => Math.round(props.size * 0.5))
45
57
 
58
+ /**
59
+ * WHITE INITIALS ON A HASHED COLOUR - THE ESTATE'S WORST CONTRAST DEFECT.
60
+ *
61
+ * The circle's fill is derived from the name, and Vuetify then picks a WHITE
62
+ * label for it. Measured across the whole space this generator can produce
63
+ * (360 hues x 20 saturations x 15 lightnesses): **70.6% of names fail AA**,
64
+ * and the worst - a yellow, `hsl(60,79%,59%)` - reads **1.30:1**. The 3.12:1
65
+ * seen on one screen was a mild sample, not the floor.
66
+ *
67
+ * The name's colour is its identity, so the HUE and SATURATION are kept
68
+ * exactly; only the lightness is walked down until white clears 4.5:1 on it.
69
+ * A yellow therefore darkens a long way and a blue barely moves, which is what
70
+ * makes a single fixed lightness impossible here.
71
+ */
72
+ const AA = 4.5
73
+
74
+ const srgb = (c: number) => {
75
+ const v = c / 255
76
+ return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4
77
+ }
78
+
79
+ /** Relative luminance of an `hsl()` triple, without going through the DOM. */
80
+ function luminance(h: number, s: number, l: number): number {
81
+ const sat = s / 100
82
+ const light = l / 100
83
+ const a = sat * Math.min(light, 1 - light)
84
+ const f = (n: number) => {
85
+ const k = (n + h / 30) % 12
86
+ return light - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)))
87
+ }
88
+ return 0.2126 * srgb(f(0) * 255) + 0.7152 * srgb(f(8) * 255) + 0.0722 * srgb(f(4) * 255)
89
+ }
90
+
46
91
  function getColor(str?: string): string {
47
92
  if(!str) return "grey"
48
93
 
@@ -54,7 +99,14 @@ function getColor(str?: string): string {
54
99
 
55
100
  const hue = Math.abs(hash) % 360
56
101
  const saturation = 60 + (Math.abs((hash >> 3) % 20)) // 60–80%
57
- const lightness = 45 + (Math.abs((hash >> 5) % 15)) // 45–60%
102
+ let lightness = 45 + (Math.abs((hash >> 5) % 15)) // 45–60%
103
+
104
+ // ponytail: linear 1% walk, not a solve - the range is at most 45 steps and
105
+ // this runs once per avatar. Lightness bottoms out at 0, where every hue is
106
+ // black and white is 21:1, so the loop always terminates on a pass.
107
+ while (lightness > 0 && 1.05 / (luminance(hue, saturation, lightness) + 0.05) < AA) {
108
+ lightness -= 1
109
+ }
58
110
 
59
111
  return `hsl(${hue}, ${saturation}%, ${lightness}%)`
60
112
  }
@@ -1,30 +1,27 @@
1
1
  <template>
2
- <v-row no-gutters>
3
- <v-col cols="12">
4
- <v-card
5
- width="100%"
6
- variant="outlined"
7
- border="thin"
8
- rounded="lg"
9
- :loading="false"
10
- >
11
- <v-toolbar density="compact" color="grey-lighten-4">
12
- <template #prepend>
13
- <v-btn fab icon density="comfortable">
14
- <v-icon>mdi-refresh</v-icon>
15
- </v-btn>
16
- </template>
17
- <template #append>
18
- <v-row no-gutters justify="end" align="center">
19
- <span class="mr-2 text-caption text-fontgray">
20
- {{ pageRange }}
21
- </span>
22
- <local-pagination v-model="page" :length="pages" />
23
- </v-row>
24
- </template>
25
- </v-toolbar>
2
+ <div>
3
+ <!--
4
+ THE DESIGN'S PAGE SCAFFOLD (same shape as Service Providers).
5
+
6
+ Placement only: the same refresh control, the same range text, the same
7
+ pager bound to the same `page`, and the table, its columns and its cell
8
+ templates are untouched. The screen simply gains the page title it has
9
+ never had, and its card + toolbar are the design's rather than Vuetify's.
10
+ -->
11
+ <PageHeader title="Billing" />
12
+
13
+ <AppCard class="table-card">
14
+ <CardToolbar :count="pageRange">
15
+ <template #left>
16
+ <AppButton variant="icon" icon="mdi-refresh" aria-label="Refresh" />
17
+ </template>
18
+
19
+ <template #right>
20
+ <local-pagination v-model="page" :length="pages" />
21
+ </template>
22
+ </CardToolbar>
26
23
 
27
- <v-data-table
24
+ <v-data-table
28
25
  :headers="headers"
29
26
  :items="items"
30
27
  item-value="_id"
@@ -38,7 +35,7 @@
38
35
  <span class="text-caption font-weight-bold text-capitalize">
39
36
  permissions
40
37
  </span>
41
- <v-chip>{{ value.length }}</v-chip>
38
+ <StatusChip :dot="false" tone="neutral" :label="`${value.length}`" />
42
39
  </template>
43
40
 
44
41
  <template #item.nature="{ item }">
@@ -46,10 +43,9 @@
46
43
  {{ item.nature }}
47
44
  </span>
48
45
  </template>
49
- </v-data-table>
50
- </v-card>
51
- </v-col>
52
- </v-row>
46
+ </v-data-table>
47
+ </AppCard>
48
+ </div>
53
49
  </template>
54
50
 
55
51
  <script setup lang="ts">
@@ -10,12 +10,17 @@
10
10
  <v-row no-gutters>
11
11
  <v-col v-if="value || readOnly" cols="12">
12
12
  <v-row no-gutters>
13
+ <!-- The strip is one control split in two: the file name, and the
14
+ cross that clears it. `rounded=""` on the read-only card was
15
+ not "no radius" - Vuetify's `useRounded` treats an empty string
16
+ as `true` and adds `.rounded`, i.e. its own 4px. The design's
17
+ radius is stated here instead, on both ends. -->
13
18
  <v-card
14
19
  variant="outlined"
15
20
  border="thin"
16
- class="px-4 text-caption font-weight-medium"
17
- :rounded="readOnly ? '' : '0 s'"
18
- height="36"
21
+ class="btn-input-file__name"
22
+ :class="{ 'btn-input-file__name--solo': readOnly }"
23
+ height="40"
19
24
  @click="emit('click:view')"
20
25
  >
21
26
  <v-row no-gutters class="fill-height" align="center">
@@ -26,24 +31,28 @@
26
31
  v-if="!readOnly"
27
32
  variant="outlined"
28
33
  border="s-0 thin"
29
- rounded="0 e"
30
- class="px-4 d-inline"
31
- height="36"
34
+ class="btn-input-file__clear"
35
+ height="40"
32
36
  @click="emit('click:close')"
33
37
  >
34
38
  <v-row no-gutters class="fill-height" align="center">
35
- <v-icon size="small"> mdi-close </v-icon>
39
+ <v-icon size="18"> mdi-close </v-icon>
36
40
  </v-row>
37
41
  </v-card>
38
42
  </v-row>
39
43
  </v-col>
40
44
 
41
45
  <v-col v-else cols="12">
46
+ <!-- This arm is the `v-else` of `v-if="value || readOnly"`, so
47
+ `value` is always falsy by the time it renders: the old
48
+ `:prepend-icon="value ? '' : ..."` had an arm that could never be
49
+ taken, and the icon is unconditional. `screen-btn-ghost` is the
50
+ design's 40px bordered button while keeping Vuetify's `loading`
51
+ spinner, which the upload needs and `AppButton` does not have. -->
42
52
  <v-btn
43
- variant="outlined"
44
- border="thin"
45
- class="text-none"
46
- :prepend-icon="value ? '' : 'mdi-tray-arrow-up'"
53
+ variant="flat"
54
+ class="text-none screen-btn-ghost"
55
+ prepend-icon="mdi-tray-arrow-up"
47
56
  :disabled="loading"
48
57
  :loading="loading"
49
58
  @click="selectAttachment"
@@ -137,3 +146,36 @@ const errorMessage = computed(() => {
137
146
  return null;
138
147
  });
139
148
  </script>
149
+
150
+ <style scoped>
151
+ /* The design's control box on the attachment strip: 40px, `--r-inner` on the
152
+ outer corners only (the two cards butt together), a `--line` edge and the
153
+ 13.5px/500 cell type - not Vuetify's `text-caption` scale and 4px radius.
154
+ `:deep` because the box being styled is the `v-card` root Vuetify renders. */
155
+ .btn-input-file__name,
156
+ .btn-input-file__clear {
157
+ font-family: var(--font-sans);
158
+ font-size: var(--fs-cell);
159
+ font-weight: var(--fw-cell);
160
+ color: var(--text2);
161
+ border-color: var(--border);
162
+ }
163
+
164
+ .btn-input-file__name {
165
+ padding-inline: 14px;
166
+ border-start-start-radius: var(--r-inner);
167
+ border-end-start-radius: var(--r-inner);
168
+ }
169
+
170
+ /* Read-only: there is no cross beside it, so it is a single rounded box. */
171
+ .btn-input-file__name--solo {
172
+ border-radius: var(--r-inner);
173
+ }
174
+
175
+ .btn-input-file__clear {
176
+ display: inline;
177
+ padding-inline: 12px;
178
+ border-start-end-radius: var(--r-inner);
179
+ border-end-end-radius: var(--r-inner);
180
+ }
181
+ </style>
@@ -1,20 +1,17 @@
1
1
  <template>
2
- <v-card width="100%">
3
- <v-toolbar>
4
- <v-row no-gutters class="fill-height px-6" align="center">
5
- <span class="font-weight-bold text-h5 text-capitalize">
6
- {{ prop.mode }} Block {{ (prop.mode === "edit" && building.block) ? `- ${building.block}` : "" }}
7
- </span>
8
- </v-row>
9
- </v-toolbar>
10
- <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-0">
2
+ <v-card width="100%" class="screen-modal">
3
+ <v-card-title class="text-capitalize">
4
+ {{ prop.mode }} Block {{ (prop.mode === "edit" && building.block) ? `- ${building.block}` : "" }}
5
+ </v-card-title>
6
+
7
+ <v-card-text class="screen-modal__body pa-0">
11
8
  <v-form v-model="validForm" :disabled="disable">
12
9
  <v-row no-gutters class="px-6 pt-4">
13
10
  <v-col cols="12" class="mt-2">
14
11
  <v-row no-gutters>
15
12
  <InputLabel class="text-capitalize font-weight-bold" title="Block Name" required />
16
13
  <v-col cols="12">
17
- <v-text-field v-model="building.name" density="comfortable" :rules="[requiredRule]"></v-text-field>
14
+ <v-text-field v-model="building.name" class="filter-field" density="comfortable" :rules="[requiredRule]"></v-text-field>
18
15
  </v-col>
19
16
  </v-row>
20
17
  </v-col>
@@ -26,7 +23,7 @@
26
23
  <InputLabel class="text-capitalize font-weight-bold" title="Block No." required />
27
24
  <v-col cols="12">
28
25
  <v-select v-model.number="building.block" :items="blocks" item-title="label" item-value="value"
29
- density="comfortable" :rules="[
26
+ class="filter-field" density="comfortable" :rules="[
30
27
  requiredRule,
31
28
  () =>
32
29
  (building.block && building.block > 0) ||
@@ -45,7 +42,7 @@
45
42
  <v-row no-gutters>
46
43
  <InputLabel class="text-capitalize font-weight-bold" title="No. of Levels" required />
47
44
  <v-col cols="12">
48
- <v-text-field v-model.number="buildingLevels" density="comfortable" :rules="[requiredRule]"
45
+ <v-text-field v-model.number="buildingLevels" class="filter-field" density="comfortable" :rules="[requiredRule]"
49
46
  type="number" @update:model-value="setBuildingLevels()"></v-text-field>
50
47
  </v-col>
51
48
  </v-row>
@@ -73,7 +70,7 @@
73
70
  <v-col cols="12">
74
71
  <v-row no-gutters>
75
72
  <v-col :cols="prop.mode === 'edit' ? 10 : 12">
76
- <v-text-field v-model.trim="building.levels[levelIndex].name" density="comfortable"></v-text-field>
73
+ <v-text-field v-model.trim="building.levels[levelIndex].name" class="filter-field" density="comfortable"></v-text-field>
77
74
  </v-col>
78
75
  <v-col v-if="prop.mode === 'edit'" cols="2" class="d-flex align-center pb-5 justify-end">
79
76
  <v-btn icon variant="text" color="error" :disabled="disable"
@@ -92,7 +89,7 @@
92
89
  <v-row no-gutters class="px-6 w-100">
93
90
  <v-row no-gutters v-for="(newLevelsItem, index) in newLevelsArray" :key="index" :cols="12" class="w-100">
94
91
  <v-col :cols="prop.mode === 'edit' ? 10 : 12">
95
- <v-text-field v-model.trim="newLevelsArray[index]" density="comfortable"></v-text-field>
92
+ <v-text-field v-model.trim="newLevelsArray[index]" class="filter-field" density="comfortable"></v-text-field>
96
93
  </v-col>
97
94
  <v-col v-if="prop.mode === 'edit'" cols="2" class="d-flex align-center pb-5 justify-end">
98
95
  <v-btn icon variant="text" color="error" @click="newLevelsArray.splice(index, 1)">
@@ -104,7 +101,7 @@
104
101
 
105
102
  <v-row v-if="prop.mode === 'edit'" no-gutters class="px-6 w-100">
106
103
  <v-col cols="10">
107
- <v-text-field v-model.trim="activeNewLevelInput" density="comfortable" class="w-100"
104
+ <v-text-field v-model.trim="activeNewLevelInput" class="filter-field w-100" density="comfortable"
108
105
  placeholder="Add new level" @click="addNewLevelErrorMessage = ''"></v-text-field>
109
106
  </v-col>
110
107
  <v-col cols="2" class="d-flex align-center pb-5 justify-end">
@@ -150,22 +147,22 @@
150
147
  </v-form>
151
148
  </v-card-text>
152
149
 
153
- <v-toolbar density="compact">
150
+ <v-card-actions class="screen-modal__footer">
154
151
  <v-row no-gutters>
155
- <v-col cols="6">
156
- <v-btn tile block variant="text" class="text-none" size="48" @click="cancel" :disabled="disable">
152
+ <v-col cols="6" class="pr-2">
153
+ <v-btn block variant="outlined" class="text-none screen-btn-ghost" @click="cancel" :disabled="disable">
157
154
  Cancel
158
155
  </v-btn>
159
156
  </v-col>
160
157
 
161
- <v-col cols="6">
162
- <v-btn tile block variant="flat" color="black" class="text-none" size="48" :disabled="!validForm || disable"
158
+ <v-col cols="6" class="pl-2">
159
+ <v-btn block variant="flat" class="text-none screen-btn-primary" :disabled="!validForm || disable"
163
160
  @click="submit" :loading="disable">
164
161
  Submit
165
162
  </v-btn>
166
163
  </v-col>
167
164
  </v-row>
168
- </v-toolbar>
165
+ </v-card-actions>
169
166
 
170
167
  <ConfirmDialog v-model="confirmDeleteLevelDialog" :loading="loading.deletingLevel" width="450" persistent>
171
168
  <template #title>
@@ -178,16 +175,24 @@
178
175
 
179
176
  <template #footer>
180
177
  <v-row no-gutters class="w-100">
181
- <v-col cols="6">
182
- <v-btn tile block size="48" variant="text" class="text-none" :disabled="loading.deletingLevel"
178
+ <v-col cols="6" class="pr-2">
179
+ <v-btn block variant="outlined" class="text-none screen-btn-ghost" :disabled="loading.deletingLevel"
183
180
  @click="closeDeleteLevelPrompt">
184
181
  Cancel
185
182
  </v-btn>
186
183
  </v-col>
187
184
 
188
- <v-col cols="6">
189
- <v-btn tile block size="48" variant="flat" color="error" class="text-none" :loading="loading.deletingLevel"
190
- :disabled="loading.deletingLevel" @click="confirmDeleteLevel">
185
+ <v-col cols="6" class="pl-2">
186
+ <!-- Left on Vuetify's own error fill, deliberately. It must NOT
187
+ take `.screen-btn-primary` - that class sets `background` and
188
+ would repaint a delete button accent blue. And the handoff
189
+ draws no modal, so it specifies no destructive button: making
190
+ one out of `--err` puts white on a token that LIGHTENS in dark
191
+ theme, which measured 2.99:1 - the same fill-used-as-ink trap
192
+ as the Upload button. So this keeps today's appearance and only
193
+ loses the 40px height. A destructive button needs a spec. -->
194
+ <v-btn block variant="flat" color="error" class="text-none"
195
+ :loading="loading.deletingLevel" :disabled="loading.deletingLevel" @click="confirmDeleteLevel">
191
196
  Delete
192
197
  </v-btn>
193
198
  </v-col>
@@ -348,7 +353,9 @@ const buildingFilesNames = ref<Record<string, string>>({});
348
353
  building.value.site = prop.site;
349
354
  if (prop.mode === "edit") {
350
355
  building.value = JSON.parse(JSON.stringify(prop.building));
351
- building.value.levels = building.value.levels.map((level: any) => ({ _id: level._id, name: level.name })); //temporary
356
+ // Same missing field as the blocks table: a block record without `levels`
357
+ // made this `.map` throw, which killed the edit dialog before it opened.
358
+ building.value.levels = (building.value.levels ?? []).map((level: any) => ({ _id: level._id, name: level.name })); //temporary
352
359
  buildingLevels.value = building.value.levels.length;
353
360
  initialBuildingLevels.value = JSON.parse(JSON.stringify(building.value.levels));
354
361