@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.
- 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 +251 -78
- 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
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<template
|
|
22
22
|
v-if="card.key === 'supplyAlert' && Array.isArray(card.raw?.items)"
|
|
23
23
|
>
|
|
24
|
-
<div class="d-flex flex-column ga-2 mt-2">
|
|
24
|
+
<div v-if="card.raw.items.length" class="d-flex flex-column ga-2 mt-2">
|
|
25
25
|
<div
|
|
26
26
|
v-for="item in card.raw.items"
|
|
27
27
|
:key="item._id || item.name"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
<span class="text-blue-grey">Balance: {{ item.qty }}</span>
|
|
32
32
|
</div>
|
|
33
33
|
</div>
|
|
34
|
+
<div v-else class="print-kpi-meta mt-2">No supplies recorded</div>
|
|
34
35
|
</template>
|
|
35
36
|
<template v-else>
|
|
36
37
|
<div class="print-kpi-value">{{ card.value }}</div>
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
:y="label.y"
|
|
67
68
|
text-anchor="end"
|
|
68
69
|
alignment-baseline="middle"
|
|
69
|
-
style="fill: #
|
|
70
|
+
style="fill: #64748b; font-size: 11px; font-weight: 500"
|
|
70
71
|
>
|
|
71
72
|
{{ label.text }}
|
|
72
73
|
</text>
|
|
@@ -112,14 +113,15 @@
|
|
|
112
113
|
:x="label.x"
|
|
113
114
|
y="246"
|
|
114
115
|
text-anchor="middle"
|
|
115
|
-
style="fill: #
|
|
116
|
+
style="fill: #64748b; font-size: 12px; font-weight: 600"
|
|
116
117
|
>
|
|
117
118
|
{{ label.text }}
|
|
118
119
|
</text>
|
|
119
120
|
</svg>
|
|
120
121
|
<div class="d-flex align-center justify-center ga-5 mt-2">
|
|
121
122
|
<span
|
|
122
|
-
class="d-inline-flex align-center ga-2 text-caption font-weight-bold
|
|
123
|
+
class="d-inline-flex align-center ga-2 text-caption font-weight-bold"
|
|
124
|
+
style="color: #64748b"
|
|
123
125
|
>
|
|
124
126
|
<span
|
|
125
127
|
class="rounded-circle"
|
|
@@ -133,7 +135,8 @@
|
|
|
133
135
|
{{ primaryLegendLabel }}
|
|
134
136
|
</span>
|
|
135
137
|
<span
|
|
136
|
-
class="d-inline-flex align-center ga-2 text-caption font-weight-bold
|
|
138
|
+
class="d-inline-flex align-center ga-2 text-caption font-weight-bold"
|
|
139
|
+
style="color: #64748b"
|
|
137
140
|
>
|
|
138
141
|
<span
|
|
139
142
|
class="rounded-circle"
|
|
@@ -203,46 +206,36 @@
|
|
|
203
206
|
|
|
204
207
|
<section v-else class="unified-dashboard pb-8">
|
|
205
208
|
<!-- ── Header ──────────────────────────────────────────────────────────── -->
|
|
206
|
-
<
|
|
207
|
-
|
|
209
|
+
<PageHeader
|
|
210
|
+
:title="dashboardTitle"
|
|
211
|
+
:subtitle="currentSiteName || 'Selected site'"
|
|
212
|
+
subtitle-icon="mdi-map-marker"
|
|
208
213
|
>
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<div
|
|
220
|
-
v-if="showHeaderActions"
|
|
221
|
-
class="d-flex flex-wrap align-center justify-md-end ga-3"
|
|
222
|
-
>
|
|
223
|
-
<v-btn
|
|
224
|
-
variant="outlined"
|
|
225
|
-
color="blue-grey-darken-1"
|
|
226
|
-
class="text-none font-weight-bold"
|
|
227
|
-
prepend-icon="mdi-tune-variant"
|
|
214
|
+
<!--
|
|
215
|
+
The design's three actions: two 38px ghost buttons and the accent
|
|
216
|
+
segmented control. Same dialog, same export menu, same range model -
|
|
217
|
+
`Customize Dashboard` and `Export` shorten to the labels the prototype
|
|
218
|
+
prints, and the export menu keeps both of its formats.
|
|
219
|
+
-->
|
|
220
|
+
<template v-if="showHeaderActions" #actions>
|
|
221
|
+
<AppButton
|
|
222
|
+
variant="ghost"
|
|
223
|
+
icon="mdi-tune-variant"
|
|
228
224
|
@click="openCustomizeDialog"
|
|
229
225
|
>
|
|
230
|
-
Customize
|
|
231
|
-
</
|
|
226
|
+
Customize
|
|
227
|
+
</AppButton>
|
|
232
228
|
|
|
233
229
|
<v-menu>
|
|
234
230
|
<template #activator="{ props: menuProps }">
|
|
235
|
-
<
|
|
231
|
+
<AppButton
|
|
236
232
|
v-bind="menuProps"
|
|
237
|
-
variant="
|
|
238
|
-
|
|
239
|
-
class="text-none font-weight-bold"
|
|
240
|
-
append-icon="mdi-chevron-down"
|
|
241
|
-
:loading="isExporting"
|
|
233
|
+
variant="ghost"
|
|
234
|
+
icon="mdi-download-outline"
|
|
242
235
|
:disabled="isExporting"
|
|
243
236
|
>
|
|
244
237
|
Export
|
|
245
|
-
</
|
|
238
|
+
</AppButton>
|
|
246
239
|
</template>
|
|
247
240
|
|
|
248
241
|
<v-list density="compact" border rounded="lg" elevation="3">
|
|
@@ -251,36 +244,9 @@
|
|
|
251
244
|
</v-list>
|
|
252
245
|
</v-menu>
|
|
253
246
|
|
|
254
|
-
<v-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
density="comfortable"
|
|
258
|
-
divided
|
|
259
|
-
variant="outlined"
|
|
260
|
-
class="bg-white"
|
|
261
|
-
style="border-radius: 8px; border-color: #d9e1e8"
|
|
262
|
-
>
|
|
263
|
-
<v-btn
|
|
264
|
-
v-for="range in rangeOptions"
|
|
265
|
-
:key="range.value"
|
|
266
|
-
:value="range.value"
|
|
267
|
-
class="text-none"
|
|
268
|
-
size="small"
|
|
269
|
-
:style="
|
|
270
|
-
selectedRange === range.value
|
|
271
|
-
? {
|
|
272
|
-
backgroundColor: '#e2e8f0',
|
|
273
|
-
color: '#0b2f47',
|
|
274
|
-
fontWeight: '600',
|
|
275
|
-
}
|
|
276
|
-
: { backgroundColor: 'transparent', color: '#64748b' }
|
|
277
|
-
"
|
|
278
|
-
>
|
|
279
|
-
{{ range.label }}
|
|
280
|
-
</v-btn>
|
|
281
|
-
</v-btn-toggle>
|
|
282
|
-
</div>
|
|
283
|
-
</div>
|
|
247
|
+
<AppSegmented v-model="selectedRange" :options="rangeOptions" />
|
|
248
|
+
</template>
|
|
249
|
+
</PageHeader>
|
|
284
250
|
|
|
285
251
|
<v-progress-linear
|
|
286
252
|
v-if="finalLoading"
|
|
@@ -289,6 +255,27 @@
|
|
|
289
255
|
class="mb-4"
|
|
290
256
|
/>
|
|
291
257
|
|
|
258
|
+
<!--
|
|
259
|
+
SAY IT ONCE AT THE TOP, THEN AGAIN WHERE IT HAPPENED. A failed request
|
|
260
|
+
used to be invisible: zeros in the tiles, "No data to display" in the
|
|
261
|
+
lists, nothing else. This names what did not load; each tile and panel
|
|
262
|
+
below carries its own failure too, so nobody has to guess which zero is
|
|
263
|
+
real. `variant="tonal"` on purpose - Vuetify derives a WHITE label for a
|
|
264
|
+
filled error alert, which measures 3.60:1 in these themes.
|
|
265
|
+
-->
|
|
266
|
+
<v-alert
|
|
267
|
+
v-if="failedWidgetLabels.length"
|
|
268
|
+
type="error"
|
|
269
|
+
variant="tonal"
|
|
270
|
+
density="compact"
|
|
271
|
+
icon="mdi-cloud-alert-outline"
|
|
272
|
+
class="mb-4"
|
|
273
|
+
>
|
|
274
|
+
Some of this dashboard could not be loaded ({{
|
|
275
|
+
failedWidgetLabels.join(", ")
|
|
276
|
+
}}). The figures for it are missing, not zero — refresh to try again.
|
|
277
|
+
</v-alert>
|
|
278
|
+
|
|
292
279
|
<!-- ── KPI Cards ───────────────────────────────────────────────────────── -->
|
|
293
280
|
<v-row dense class="mb-1 align-stretch">
|
|
294
281
|
<v-col
|
|
@@ -300,22 +287,24 @@
|
|
|
300
287
|
lg="3"
|
|
301
288
|
class="d-flex"
|
|
302
289
|
>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
290
|
+
<!--
|
|
291
|
+
The same figures the tile has always shown, in the arrangement the
|
|
292
|
+
handoff draws them: icon and label on one row, the number with its
|
|
293
|
+
percentage pill beside it, the sub-line under both. The filter menu
|
|
294
|
+
and the facility "View All" keep their place at the end of the first
|
|
295
|
+
row and do exactly what they did.
|
|
296
|
+
-->
|
|
297
|
+
<AppKpiTile
|
|
298
|
+
:icon="card.icon"
|
|
299
|
+
:tint="card.tint"
|
|
300
|
+
:color="card.color"
|
|
301
|
+
:label="card.title"
|
|
302
|
+
:value="card.value"
|
|
303
|
+
:chip="kpiChipText(card)"
|
|
304
|
+
:down="Number(card.percentage) < 0"
|
|
305
|
+
:sub="card.meta"
|
|
309
306
|
>
|
|
310
|
-
<
|
|
311
|
-
<div
|
|
312
|
-
class="d-inline-flex align-center justify-center rounded-lg"
|
|
313
|
-
style="height: 28px; width: 28px; flex-shrink: 0"
|
|
314
|
-
:style="{ backgroundColor: card.tint }"
|
|
315
|
-
>
|
|
316
|
-
<v-icon :icon="card.icon" :color="card.color" size="15" />
|
|
317
|
-
</div>
|
|
318
|
-
|
|
307
|
+
<template v-if="card.filter || card.key === 'facilityBookings'" #action>
|
|
319
308
|
<v-menu v-if="card.filter" v-model="filterMenus[card.key]">
|
|
320
309
|
<template #activator="{ props: menuProps }">
|
|
321
310
|
<v-btn
|
|
@@ -359,61 +348,29 @@
|
|
|
359
348
|
>
|
|
360
349
|
View All
|
|
361
350
|
</button>
|
|
362
|
-
</
|
|
351
|
+
</template>
|
|
363
352
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
>
|
|
381
|
-
<span>{{ item.name }}</span>
|
|
382
|
-
<span class="text-blue-grey">Balance: {{ item.qty }}</span>
|
|
383
|
-
</div>
|
|
384
|
-
</div>
|
|
385
|
-
</template>
|
|
386
|
-
<template v-else>
|
|
387
|
-
<div class="d-flex align-center mb-3" style="min-height: 31px">
|
|
388
|
-
<span class="text-h5 font-weight-black text-blue-grey-darken-4">
|
|
389
|
-
{{ card.value }}
|
|
390
|
-
</span>
|
|
391
|
-
</div>
|
|
392
|
-
<div class="d-flex align-center flex-wrap ga-2">
|
|
393
|
-
<span v-if="card.meta" class="text-caption text-blue-grey">{{
|
|
394
|
-
card.meta
|
|
395
|
-
}}</span>
|
|
396
|
-
<v-chip
|
|
397
|
-
:color="card.percentage >= 0 ? 'success' : 'error'"
|
|
398
|
-
size="x-small"
|
|
399
|
-
variant="flat"
|
|
400
|
-
class="font-weight-bold"
|
|
401
|
-
>
|
|
402
|
-
<v-icon
|
|
403
|
-
:icon="
|
|
404
|
-
card.percentage >= 0
|
|
405
|
-
? 'mdi-arrow-up-right'
|
|
406
|
-
: 'mdi-arrow-down-right'
|
|
407
|
-
"
|
|
408
|
-
size="13"
|
|
409
|
-
start
|
|
410
|
-
/>
|
|
411
|
-
{{ Math.abs(card.percentage) }}%
|
|
412
|
-
</v-chip>
|
|
353
|
+
<!-- The supply tile lists items instead of counting one figure. -->
|
|
354
|
+
<template
|
|
355
|
+
v-if="card.key === 'supplyAlert' && Array.isArray(card.raw?.items)"
|
|
356
|
+
>
|
|
357
|
+
<!--
|
|
358
|
+
An empty list used to render a title and a blank card - no number,
|
|
359
|
+
no words, nothing to read. The tile now says what it found.
|
|
360
|
+
-->
|
|
361
|
+
<div v-if="card.raw.items.length" class="d-flex flex-column ga-2">
|
|
362
|
+
<div
|
|
363
|
+
v-for="item in card.raw.items"
|
|
364
|
+
:key="item._id || item.name"
|
|
365
|
+
class="d-flex justify-space-between text-body-2 text-high-emphasis font-weight-medium"
|
|
366
|
+
>
|
|
367
|
+
<span>{{ item.name }}</span>
|
|
368
|
+
<span class="text-medium-emphasis">Balance: {{ item.qty }}</span>
|
|
413
369
|
</div>
|
|
414
|
-
</
|
|
415
|
-
|
|
416
|
-
|
|
370
|
+
</div>
|
|
371
|
+
<DashboardEmptyState v-else compact />
|
|
372
|
+
</template>
|
|
373
|
+
</AppKpiTile>
|
|
417
374
|
</v-col>
|
|
418
375
|
</v-row>
|
|
419
376
|
|
|
@@ -421,16 +378,37 @@
|
|
|
421
378
|
<v-card
|
|
422
379
|
v-if="isWidgetVisible('activity')"
|
|
423
380
|
flat
|
|
424
|
-
|
|
425
|
-
rounded="lg"
|
|
426
|
-
class="px-5 pt-4 pb-3 mt-3"
|
|
381
|
+
class="dashboard-card px-5 pt-4 pb-3 mt-3"
|
|
427
382
|
>
|
|
428
383
|
<div class="d-flex align-center justify-space-between">
|
|
429
|
-
<h2 class="
|
|
384
|
+
<h2 class="dashboard-card-title mb-0">
|
|
430
385
|
{{ activityTitle }}
|
|
431
386
|
</h2>
|
|
432
387
|
</div>
|
|
433
388
|
|
|
389
|
+
<!--
|
|
390
|
+
THE TWO SERIES COLOURS STAY LITERAL, AND HERE IS WHY.
|
|
391
|
+
|
|
392
|
+
Everything else in this chart already follows the theme - the gridlines,
|
|
393
|
+
the axis labels and the ring around each dot are all Vuetify theme vars.
|
|
394
|
+
The two series are not, and they are the only chart colours in the file
|
|
395
|
+
that are still hexes.
|
|
396
|
+
|
|
397
|
+
They are left that way deliberately. The handoff names no chart palette
|
|
398
|
+
at all (searched: no "chart", no "legend", no "graph", and neither hex
|
|
399
|
+
appears anywhere in it), so the fallback rule is "use the token the
|
|
400
|
+
literal was already imitating". `#e53935` is plainly `--err`. `#ff9b9b`
|
|
401
|
+
is a second, softer red, and the palette has no second red - the only
|
|
402
|
+
other red token is `--err-bg`, a 10%-alpha FILL that would be invisible
|
|
403
|
+
as a 3px line. Re-pointing just one of them would collapse the pair in
|
|
404
|
+
dark mode, where `--err` is `rgb(229,115,115)` and would sit almost on
|
|
405
|
+
top of `#ff9b9b`'s `rgb(255,155,155)` - two lines the reader could no
|
|
406
|
+
longer tell apart. That is the same "do not merge two series into one
|
|
407
|
+
colour" trap as the Work Order Status bars below.
|
|
408
|
+
|
|
409
|
+
So: a two-series chart palette is missing from the design system, and
|
|
410
|
+
that is a designer question, not something to invent here.
|
|
411
|
+
-->
|
|
434
412
|
<div v-if="hasChartData" class="mt-4" ref="chartContainerRef">
|
|
435
413
|
<svg
|
|
436
414
|
:viewBox="`0 0 ${chartWidth} 260`"
|
|
@@ -444,7 +422,7 @@
|
|
|
444
422
|
:y1="tick"
|
|
445
423
|
:x2="chartWidth - 20"
|
|
446
424
|
:y2="tick"
|
|
447
|
-
style="stroke:
|
|
425
|
+
style="stroke: rgba(var(--v-theme-on-surface), 0.12); stroke-width: 1"
|
|
448
426
|
/>
|
|
449
427
|
<text
|
|
450
428
|
v-for="label in chartYLabels"
|
|
@@ -453,7 +431,7 @@
|
|
|
453
431
|
:y="label.y"
|
|
454
432
|
text-anchor="end"
|
|
455
433
|
alignment-baseline="middle"
|
|
456
|
-
style="fill:
|
|
434
|
+
style="fill: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); font-size: 11px; font-weight: 500"
|
|
457
435
|
>
|
|
458
436
|
{{ label.text }}
|
|
459
437
|
</text>
|
|
@@ -483,7 +461,7 @@
|
|
|
483
461
|
:cx="point.x"
|
|
484
462
|
:cy="point.y"
|
|
485
463
|
r="4"
|
|
486
|
-
style="fill: #ff9b9b; stroke:
|
|
464
|
+
style="fill: #ff9b9b; stroke: rgb(var(--v-theme-surface)); stroke-width: 2"
|
|
487
465
|
/>
|
|
488
466
|
<circle
|
|
489
467
|
v-for="point in secondaryDots"
|
|
@@ -491,7 +469,7 @@
|
|
|
491
469
|
:cx="point.x"
|
|
492
470
|
:cy="point.y"
|
|
493
471
|
r="4"
|
|
494
|
-
style="fill: #e53935; stroke:
|
|
472
|
+
style="fill: #e53935; stroke: rgb(var(--v-theme-surface)); stroke-width: 2"
|
|
495
473
|
/>
|
|
496
474
|
<text
|
|
497
475
|
v-for="label in chartAxisLabels"
|
|
@@ -499,7 +477,7 @@
|
|
|
499
477
|
:x="label.x"
|
|
500
478
|
y="246"
|
|
501
479
|
text-anchor="middle"
|
|
502
|
-
style="fill:
|
|
480
|
+
style="fill: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); font-size: 12px; font-weight: 600"
|
|
503
481
|
>
|
|
504
482
|
{{ label.text }}
|
|
505
483
|
</text>
|
|
@@ -507,7 +485,7 @@
|
|
|
507
485
|
|
|
508
486
|
<div class="d-flex align-center justify-center ga-5 mt-2">
|
|
509
487
|
<span
|
|
510
|
-
class="d-inline-flex align-center ga-2 text-caption font-weight-bold text-
|
|
488
|
+
class="d-inline-flex align-center ga-2 text-caption font-weight-bold text-medium-emphasis"
|
|
511
489
|
>
|
|
512
490
|
<span
|
|
513
491
|
class="rounded-circle"
|
|
@@ -521,7 +499,7 @@
|
|
|
521
499
|
{{ primaryLegendLabel }}
|
|
522
500
|
</span>
|
|
523
501
|
<span
|
|
524
|
-
class="d-inline-flex align-center ga-2 text-caption font-weight-bold text-
|
|
502
|
+
class="d-inline-flex align-center ga-2 text-caption font-weight-bold text-medium-emphasis"
|
|
525
503
|
>
|
|
526
504
|
<span
|
|
527
505
|
class="rounded-circle"
|
|
@@ -537,7 +515,7 @@
|
|
|
537
515
|
</div>
|
|
538
516
|
</div>
|
|
539
517
|
|
|
540
|
-
<DashboardEmptyState v-else />
|
|
518
|
+
<DashboardEmptyState v-else :error="chartFailed" />
|
|
541
519
|
</v-card>
|
|
542
520
|
|
|
543
521
|
<!-- ── Hygiene / M&E / Landscape / Pest / Pool — task + attendance ────── -->
|
|
@@ -553,58 +531,36 @@
|
|
|
553
531
|
action-label="View All"
|
|
554
532
|
@action="goToDashboardRoute(primaryListConfig.route)"
|
|
555
533
|
>
|
|
556
|
-
<div v-if="taskScheduleItems.length" class="
|
|
557
|
-
<
|
|
534
|
+
<div v-if="taskScheduleItems.length" class="figma-list">
|
|
535
|
+
<div
|
|
558
536
|
v-for="item in taskScheduleItems"
|
|
559
537
|
:key="item.id"
|
|
560
|
-
|
|
561
|
-
border
|
|
562
|
-
rounded="lg"
|
|
563
|
-
class="pa-3"
|
|
538
|
+
class="figma-list-row figma-list-row--wide"
|
|
564
539
|
>
|
|
565
|
-
<div class="
|
|
566
|
-
<
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
>
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
{{ item.subtitle
|
|
574
|
-
}}<template v-if="item.subtitle && item.time">
|
|
575
|
-
• </template
|
|
576
|
-
>{{ item.time }}
|
|
577
|
-
</div>
|
|
578
|
-
</div>
|
|
579
|
-
<div
|
|
580
|
-
class="d-flex align-center ga-2 text-caption text-blue-grey"
|
|
581
|
-
>
|
|
582
|
-
<AvatarMain :name="item.person" :size="30" />
|
|
583
|
-
<span class="text-truncate" style="max-width: 100px">{{
|
|
584
|
-
item.person
|
|
585
|
-
}}</span>
|
|
586
|
-
</div>
|
|
587
|
-
<v-chip
|
|
588
|
-
size="small"
|
|
589
|
-
:color="getStatusTone(item.status)"
|
|
590
|
-
variant="tonal"
|
|
591
|
-
label
|
|
592
|
-
>
|
|
593
|
-
{{ item.status }}
|
|
594
|
-
</v-chip>
|
|
595
|
-
<v-btn
|
|
596
|
-
variant="outlined"
|
|
597
|
-
color="primary"
|
|
598
|
-
size="small"
|
|
599
|
-
class="text-none font-weight-bold"
|
|
600
|
-
@click="goToListItem(item, primaryListConfig.route)"
|
|
601
|
-
>
|
|
602
|
-
View
|
|
603
|
-
</v-btn>
|
|
540
|
+
<div class="figma-list-main">
|
|
541
|
+
<strong>{{ item.title }}</strong>
|
|
542
|
+
<p v-if="item.subtitle || item.time">
|
|
543
|
+
{{ item.subtitle
|
|
544
|
+
}}<template v-if="item.subtitle && item.time">
|
|
545
|
+
• </template
|
|
546
|
+
>{{ item.time }}
|
|
547
|
+
</p>
|
|
604
548
|
</div>
|
|
605
|
-
|
|
549
|
+
<div class="figma-list-person">
|
|
550
|
+
<AvatarMain :name="item.person" :size="30" />
|
|
551
|
+
<span>{{ item.person }}</span>
|
|
552
|
+
</div>
|
|
553
|
+
<StatusChip :status="item.status" pill />
|
|
554
|
+
<button
|
|
555
|
+
class="figma-row-action"
|
|
556
|
+
type="button"
|
|
557
|
+
@click="goToListItem(item, primaryListConfig.route)"
|
|
558
|
+
>
|
|
559
|
+
View
|
|
560
|
+
</button>
|
|
561
|
+
</div>
|
|
606
562
|
</div>
|
|
607
|
-
<DashboardEmptyState v-else />
|
|
563
|
+
<DashboardEmptyState v-else :error="taskScheduleFailed" />
|
|
608
564
|
</DashboardPanel>
|
|
609
565
|
</v-col>
|
|
610
566
|
|
|
@@ -619,40 +575,24 @@
|
|
|
619
575
|
action-label="View All"
|
|
620
576
|
@action="goToDashboardRoute('attendance')"
|
|
621
577
|
>
|
|
622
|
-
<div v-if="staffStatusItems.length" class="
|
|
623
|
-
<
|
|
578
|
+
<div v-if="staffStatusItems.length" class="figma-list">
|
|
579
|
+
<div
|
|
624
580
|
v-for="item in staffStatusItems"
|
|
625
581
|
:key="item.id"
|
|
626
|
-
|
|
627
|
-
border
|
|
628
|
-
rounded="lg"
|
|
629
|
-
class="pa-3"
|
|
582
|
+
class="figma-list-row"
|
|
630
583
|
>
|
|
631
|
-
<
|
|
632
|
-
|
|
633
|
-
<
|
|
634
|
-
|
|
635
|
-
class="text-body-2 font-weight-bold text-blue-grey-darken-4 text-truncate"
|
|
636
|
-
>
|
|
637
|
-
{{ item.name }}
|
|
638
|
-
</div>
|
|
639
|
-
<div class="text-caption text-blue-grey mt-1 text-truncate">
|
|
640
|
-
{{ item.role }}
|
|
641
|
-
</div>
|
|
642
|
-
</div>
|
|
643
|
-
<v-chip
|
|
644
|
-
size="small"
|
|
645
|
-
color="blue-grey-darken-1"
|
|
646
|
-
variant="tonal"
|
|
647
|
-
label
|
|
648
|
-
class="ml-auto flex-shrink-0 font-weight-bold"
|
|
649
|
-
>
|
|
650
|
-
{{ item.totalHours || 0 }} hrs
|
|
651
|
-
</v-chip>
|
|
584
|
+
<AvatarMain :name="item.name" :size="32" />
|
|
585
|
+
<div class="figma-list-main">
|
|
586
|
+
<strong>{{ item.name }}</strong>
|
|
587
|
+
<p v-if="item.role">{{ item.role }}</p>
|
|
652
588
|
</div>
|
|
653
|
-
|
|
589
|
+
<!-- Hours are a FIGURE, not a status: no tone to read, so the
|
|
590
|
+
design's neutral pill rather than a StatusChip, which would
|
|
591
|
+
have coloured "8 hrs" as if it were a word it knows. -->
|
|
592
|
+
<span class="figma-row-figure">{{ item.totalHours || 0 }} hrs</span>
|
|
593
|
+
</div>
|
|
654
594
|
</div>
|
|
655
|
-
<DashboardEmptyState v-else />
|
|
595
|
+
<DashboardEmptyState v-else :error="attendanceFailed" />
|
|
656
596
|
</DashboardPanel>
|
|
657
597
|
</v-col>
|
|
658
598
|
|
|
@@ -662,79 +602,31 @@
|
|
|
662
602
|
action-label="View All"
|
|
663
603
|
@action="goToDashboardRoute('feedbacks')"
|
|
664
604
|
>
|
|
665
|
-
<div v-if="feedbackItems.length" class="figma-list
|
|
605
|
+
<div v-if="feedbackItems.length" class="figma-list">
|
|
666
606
|
<div
|
|
667
607
|
v-for="item in feedbackItems"
|
|
668
608
|
:key="item.id"
|
|
669
|
-
class="
|
|
670
|
-
style="
|
|
671
|
-
padding: 16px 20px;
|
|
672
|
-
border: 1px solid #e2e8f0;
|
|
673
|
-
border-radius: 12px;
|
|
674
|
-
margin-bottom: 12px;
|
|
675
|
-
background-color: #ffffff;
|
|
676
|
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
|
677
|
-
"
|
|
609
|
+
class="figma-list-row figma-list-row--wide"
|
|
678
610
|
>
|
|
679
|
-
<div
|
|
680
|
-
<strong
|
|
681
|
-
|
|
682
|
-
display: block;
|
|
683
|
-
font-size: 15px;
|
|
684
|
-
font-weight: 700;
|
|
685
|
-
color: #0b2f47;
|
|
686
|
-
margin-bottom: 4px;
|
|
687
|
-
"
|
|
688
|
-
>{{ item.title }}</strong
|
|
689
|
-
>
|
|
690
|
-
<span style="font-size: 13px; color: #94a3b8">{{
|
|
691
|
-
item.subtitle
|
|
692
|
-
}}</span>
|
|
693
|
-
</div>
|
|
694
|
-
|
|
695
|
-
<div
|
|
696
|
-
style="flex: 1; display: flex; align-items: center; gap: 12px"
|
|
697
|
-
>
|
|
698
|
-
<AvatarMain :name="item.person" :size="36" />
|
|
699
|
-
<span style="font-size: 14px; color: #64748b">{{
|
|
700
|
-
item.person
|
|
701
|
-
}}</span>
|
|
702
|
-
</div>
|
|
703
|
-
|
|
704
|
-
<div style="flex: 1; display: flex; justify-content: center">
|
|
705
|
-
<div
|
|
706
|
-
:style="getStatusPillStyle(item.status)"
|
|
707
|
-
style="
|
|
708
|
-
padding: 6px 16px;
|
|
709
|
-
border-radius: 20px;
|
|
710
|
-
font-size: 13px;
|
|
711
|
-
font-weight: 500;
|
|
712
|
-
text-transform: capitalize;
|
|
713
|
-
"
|
|
714
|
-
>
|
|
715
|
-
{{ item.status || "To-Do" }}
|
|
716
|
-
</div>
|
|
611
|
+
<div class="figma-list-main">
|
|
612
|
+
<strong>{{ item.title }}</strong>
|
|
613
|
+
<p v-if="item.subtitle">{{ item.subtitle }}</p>
|
|
717
614
|
</div>
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
<
|
|
721
|
-
variant="outlined"
|
|
722
|
-
class="text-none font-weight-medium"
|
|
723
|
-
size="small"
|
|
724
|
-
color="#3b82f6"
|
|
725
|
-
style="
|
|
726
|
-
border-color: #e2e8f0;
|
|
727
|
-
border-radius: 8px;
|
|
728
|
-
padding: 0 16px;
|
|
729
|
-
height: 32px;
|
|
730
|
-
"
|
|
731
|
-
@click="goToListItem(item, 'feedbacks')"
|
|
732
|
-
>View</v-btn
|
|
733
|
-
>
|
|
615
|
+
<div class="figma-list-person">
|
|
616
|
+
<AvatarMain :name="item.person" :size="30" />
|
|
617
|
+
<span>{{ item.person }}</span>
|
|
734
618
|
</div>
|
|
619
|
+
<StatusChip :status="item.status || 'To-Do'" pill />
|
|
620
|
+
<button
|
|
621
|
+
class="figma-row-action"
|
|
622
|
+
type="button"
|
|
623
|
+
@click="goToListItem(item, 'feedbacks')"
|
|
624
|
+
>
|
|
625
|
+
View
|
|
626
|
+
</button>
|
|
735
627
|
</div>
|
|
736
628
|
</div>
|
|
737
|
-
<DashboardEmptyState v-else />
|
|
629
|
+
<DashboardEmptyState v-else :error="feedbacksFailed" />
|
|
738
630
|
</DashboardPanel>
|
|
739
631
|
</v-col>
|
|
740
632
|
</v-row>
|
|
@@ -779,7 +671,7 @@
|
|
|
779
671
|
</button>
|
|
780
672
|
</div>
|
|
781
673
|
</div>
|
|
782
|
-
<DashboardEmptyState v-else />
|
|
674
|
+
<DashboardEmptyState v-else :error="metricsFailed" />
|
|
783
675
|
</div>
|
|
784
676
|
</div>
|
|
785
677
|
</v-col>
|
|
@@ -824,7 +716,7 @@
|
|
|
824
716
|
</button>
|
|
825
717
|
</div>
|
|
826
718
|
</div>
|
|
827
|
-
<DashboardEmptyState v-else />
|
|
719
|
+
<DashboardEmptyState v-else :error="metricsFailed" />
|
|
828
720
|
</div>
|
|
829
721
|
</div>
|
|
830
722
|
</v-col>
|
|
@@ -832,7 +724,7 @@
|
|
|
832
724
|
<v-col v-if="isWidgetVisible('todayAttentions')" cols="12" lg="6">
|
|
833
725
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
834
726
|
<div class="figma-panel-header">
|
|
835
|
-
<h2>
|
|
727
|
+
<h2>Needs Attention</h2>
|
|
836
728
|
</div>
|
|
837
729
|
<div class="figma-panel-body">
|
|
838
730
|
<div v-if="todayAttentions.length" class="figma-list">
|
|
@@ -873,7 +765,7 @@
|
|
|
873
765
|
</button>
|
|
874
766
|
</div>
|
|
875
767
|
</div>
|
|
876
|
-
<DashboardEmptyState v-else />
|
|
768
|
+
<DashboardEmptyState v-else :error="metricsFailed" />
|
|
877
769
|
</div>
|
|
878
770
|
</div>
|
|
879
771
|
</v-col>
|
|
@@ -909,7 +801,7 @@
|
|
|
909
801
|
</div>
|
|
910
802
|
</div>
|
|
911
803
|
</div>
|
|
912
|
-
<DashboardEmptyState v-else />
|
|
804
|
+
<DashboardEmptyState v-else :error="metricsFailed" />
|
|
913
805
|
</div>
|
|
914
806
|
</div>
|
|
915
807
|
</v-col>
|
|
@@ -957,7 +849,7 @@
|
|
|
957
849
|
</button>
|
|
958
850
|
</div>
|
|
959
851
|
</div>
|
|
960
|
-
<DashboardEmptyState v-else />
|
|
852
|
+
<DashboardEmptyState v-else :error="feedbacksFailed" />
|
|
961
853
|
</DashboardPanel>
|
|
962
854
|
</v-col>
|
|
963
855
|
</v-row>
|
|
@@ -967,7 +859,7 @@
|
|
|
967
859
|
<v-col v-if="isWidgetVisible('activePatrol')" cols="12" lg="6">
|
|
968
860
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
969
861
|
<div class="figma-panel-header">
|
|
970
|
-
<h2>
|
|
862
|
+
<h2>Active Patrol</h2>
|
|
971
863
|
<button
|
|
972
864
|
class="figma-panel-action"
|
|
973
865
|
type="button"
|
|
@@ -1022,24 +914,19 @@
|
|
|
1022
914
|
</div>
|
|
1023
915
|
|
|
1024
916
|
<div class="active-patrol-action">
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
style="
|
|
1031
|
-
border-color: #e2e8f0;
|
|
1032
|
-
border-radius: 8px;
|
|
1033
|
-
padding: 0 16px;
|
|
1034
|
-
height: 32px;
|
|
1035
|
-
"
|
|
917
|
+
<!-- Chrome only: the route and the Virtual Patrol flow
|
|
918
|
+
behind it are unchanged. -->
|
|
919
|
+
<button
|
|
920
|
+
class="figma-row-action"
|
|
921
|
+
type="button"
|
|
1036
922
|
@click="goToDashboardRoute('virtual-patrol')"
|
|
1037
|
-
>View</v-btn
|
|
1038
923
|
>
|
|
924
|
+
View
|
|
925
|
+
</button>
|
|
1039
926
|
</div>
|
|
1040
927
|
</div>
|
|
1041
928
|
</div>
|
|
1042
|
-
<DashboardEmptyState v-else />
|
|
929
|
+
<DashboardEmptyState v-else :error="metricsFailed" />
|
|
1043
930
|
</div>
|
|
1044
931
|
</div>
|
|
1045
932
|
</v-col>
|
|
@@ -1047,7 +934,7 @@
|
|
|
1047
934
|
<v-col v-if="isWidgetVisible('staffStatus')" cols="12" lg="6">
|
|
1048
935
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
1049
936
|
<div class="figma-panel-header">
|
|
1050
|
-
<h2>
|
|
937
|
+
<h2>Staff Status</h2>
|
|
1051
938
|
<button
|
|
1052
939
|
class="figma-panel-action"
|
|
1053
940
|
type="button"
|
|
@@ -1057,65 +944,31 @@
|
|
|
1057
944
|
</button>
|
|
1058
945
|
</div>
|
|
1059
946
|
<div class="figma-panel-body">
|
|
1060
|
-
<div v-if="securityStaffItems.length" class="figma-list
|
|
947
|
+
<div v-if="securityStaffItems.length" class="figma-list">
|
|
1061
948
|
<div
|
|
1062
949
|
v-for="item in securityStaffItems"
|
|
1063
950
|
:key="item._id || item.id"
|
|
1064
|
-
class="
|
|
1065
|
-
style="
|
|
1066
|
-
padding: 16px 20px;
|
|
1067
|
-
border: 1px solid #e2e8f0;
|
|
1068
|
-
border-radius: 12px;
|
|
1069
|
-
margin-bottom: 12px;
|
|
1070
|
-
background-color: #ffffff;
|
|
1071
|
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
|
1072
|
-
"
|
|
951
|
+
class="figma-list-row"
|
|
1073
952
|
>
|
|
1074
|
-
<
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
<
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
font-size: 15px;
|
|
1089
|
-
font-weight: 700;
|
|
1090
|
-
color: #0b2f47;
|
|
1091
|
-
margin-bottom: 4px;
|
|
1092
|
-
"
|
|
1093
|
-
>{{
|
|
1094
|
-
item.userName || item.personName || "Unassigned"
|
|
1095
|
-
}}</strong
|
|
1096
|
-
>
|
|
1097
|
-
<span style="font-size: 13px; color: #94a3b8">{{
|
|
1098
|
-
item.role || item.designation || "Officer"
|
|
1099
|
-
}}</span>
|
|
1100
|
-
</div>
|
|
1101
|
-
</div>
|
|
1102
|
-
|
|
1103
|
-
<div style="flex: 0 0 auto">
|
|
1104
|
-
<div
|
|
1105
|
-
:style="getStaffStatusPillStyle(item.status)"
|
|
1106
|
-
style="
|
|
1107
|
-
padding: 6px 16px;
|
|
1108
|
-
border-radius: 20px;
|
|
1109
|
-
font-size: 13px;
|
|
1110
|
-
font-weight: 500;
|
|
1111
|
-
"
|
|
1112
|
-
>
|
|
1113
|
-
{{ item.status || "On duty" }}
|
|
1114
|
-
</div>
|
|
953
|
+
<AvatarMain
|
|
954
|
+
:name="
|
|
955
|
+
item.personName ||
|
|
956
|
+
item.employeeName ||
|
|
957
|
+
item.userName ||
|
|
958
|
+
'Unassigned'
|
|
959
|
+
"
|
|
960
|
+
:size="32"
|
|
961
|
+
/>
|
|
962
|
+
<div class="figma-list-main">
|
|
963
|
+
<strong>{{
|
|
964
|
+
item.userName || item.personName || "Unassigned"
|
|
965
|
+
}}</strong>
|
|
966
|
+
<p>{{ item.role || item.designation || "Officer" }}</p>
|
|
1115
967
|
</div>
|
|
968
|
+
<StatusChip :status="item.status || 'On duty'" pill />
|
|
1116
969
|
</div>
|
|
1117
970
|
</div>
|
|
1118
|
-
<DashboardEmptyState v-else />
|
|
971
|
+
<DashboardEmptyState v-else :error="attendanceFailed" />
|
|
1119
972
|
</div>
|
|
1120
973
|
</div>
|
|
1121
974
|
</v-col>
|
|
@@ -1127,79 +980,31 @@
|
|
|
1127
980
|
action-label="View All"
|
|
1128
981
|
@action="goToDashboardRoute('feedbacks')"
|
|
1129
982
|
>
|
|
1130
|
-
<div v-if="feedbackItems.length" class="figma-list
|
|
983
|
+
<div v-if="feedbackItems.length" class="figma-list">
|
|
1131
984
|
<div
|
|
1132
985
|
v-for="item in feedbackItems"
|
|
1133
986
|
:key="item.id"
|
|
1134
|
-
class="
|
|
1135
|
-
style="
|
|
1136
|
-
padding: 16px 20px;
|
|
1137
|
-
border: 1px solid #e2e8f0;
|
|
1138
|
-
border-radius: 12px;
|
|
1139
|
-
margin-bottom: 12px;
|
|
1140
|
-
background-color: #ffffff;
|
|
1141
|
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
|
1142
|
-
"
|
|
987
|
+
class="figma-list-row figma-list-row--wide"
|
|
1143
988
|
>
|
|
1144
|
-
<div
|
|
1145
|
-
<strong
|
|
1146
|
-
|
|
1147
|
-
display: block;
|
|
1148
|
-
font-size: 15px;
|
|
1149
|
-
font-weight: 700;
|
|
1150
|
-
color: #0b2f47;
|
|
1151
|
-
margin-bottom: 4px;
|
|
1152
|
-
"
|
|
1153
|
-
>{{ item.title }}</strong
|
|
1154
|
-
>
|
|
1155
|
-
<span style="font-size: 13px; color: #94a3b8">{{
|
|
1156
|
-
item.subtitle
|
|
1157
|
-
}}</span>
|
|
1158
|
-
</div>
|
|
1159
|
-
|
|
1160
|
-
<div
|
|
1161
|
-
style="flex: 1; display: flex; align-items: center; gap: 12px"
|
|
1162
|
-
>
|
|
1163
|
-
<AvatarMain :name="item.person" :size="36" />
|
|
1164
|
-
<span style="font-size: 14px; color: #64748b">{{
|
|
1165
|
-
item.person
|
|
1166
|
-
}}</span>
|
|
1167
|
-
</div>
|
|
1168
|
-
|
|
1169
|
-
<div style="flex: 1; display: flex; justify-content: center">
|
|
1170
|
-
<div
|
|
1171
|
-
:style="getStatusPillStyle(item.status)"
|
|
1172
|
-
style="
|
|
1173
|
-
padding: 6px 16px;
|
|
1174
|
-
border-radius: 20px;
|
|
1175
|
-
font-size: 13px;
|
|
1176
|
-
font-weight: 500;
|
|
1177
|
-
text-transform: capitalize;
|
|
1178
|
-
"
|
|
1179
|
-
>
|
|
1180
|
-
{{ item.status || "To-Do" }}
|
|
1181
|
-
</div>
|
|
989
|
+
<div class="figma-list-main">
|
|
990
|
+
<strong>{{ item.title }}</strong>
|
|
991
|
+
<p v-if="item.subtitle">{{ item.subtitle }}</p>
|
|
1182
992
|
</div>
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
<
|
|
1186
|
-
variant="outlined"
|
|
1187
|
-
class="text-none font-weight-medium"
|
|
1188
|
-
size="small"
|
|
1189
|
-
color="#3b82f6"
|
|
1190
|
-
style="
|
|
1191
|
-
border-color: #e2e8f0;
|
|
1192
|
-
border-radius: 8px;
|
|
1193
|
-
padding: 0 16px;
|
|
1194
|
-
height: 32px;
|
|
1195
|
-
"
|
|
1196
|
-
@click="goToListItem(item, 'feedbacks')"
|
|
1197
|
-
>View</v-btn
|
|
1198
|
-
>
|
|
993
|
+
<div class="figma-list-person">
|
|
994
|
+
<AvatarMain :name="item.person" :size="30" />
|
|
995
|
+
<span>{{ item.person }}</span>
|
|
1199
996
|
</div>
|
|
997
|
+
<StatusChip :status="item.status || 'To-Do'" pill />
|
|
998
|
+
<button
|
|
999
|
+
class="figma-row-action"
|
|
1000
|
+
type="button"
|
|
1001
|
+
@click="goToListItem(item, 'feedbacks')"
|
|
1002
|
+
>
|
|
1003
|
+
View
|
|
1004
|
+
</button>
|
|
1200
1005
|
</div>
|
|
1201
1006
|
</div>
|
|
1202
|
-
<DashboardEmptyState v-else />
|
|
1007
|
+
<DashboardEmptyState v-else :error="feedbacksFailed" />
|
|
1203
1008
|
</DashboardPanel>
|
|
1204
1009
|
</v-col>
|
|
1205
1010
|
</v-row>
|
|
@@ -1211,10 +1016,10 @@
|
|
|
1211
1016
|
class="d-flex align-start justify-space-between pa-6 pb-4 border-b"
|
|
1212
1017
|
>
|
|
1213
1018
|
<div>
|
|
1214
|
-
<div class="text-h6 font-weight-black text-
|
|
1019
|
+
<div class="text-h6 font-weight-black text-high-emphasis">
|
|
1215
1020
|
Customize Dashboard
|
|
1216
1021
|
</div>
|
|
1217
|
-
<div class="text-caption text-
|
|
1022
|
+
<div class="text-caption text-medium-emphasis mt-1">
|
|
1218
1023
|
Select which widgets to display on your dashboard
|
|
1219
1024
|
</div>
|
|
1220
1025
|
</div>
|
|
@@ -1240,7 +1045,6 @@
|
|
|
1240
1045
|
<div class="d-flex justify-end mt-3 mb-1">
|
|
1241
1046
|
<v-btn
|
|
1242
1047
|
variant="text"
|
|
1243
|
-
color="blue-grey-darken-4"
|
|
1244
1048
|
size="small"
|
|
1245
1049
|
class="text-none font-weight-bold"
|
|
1246
1050
|
prepend-icon="mdi-refresh"
|
|
@@ -1262,11 +1066,11 @@
|
|
|
1262
1066
|
<div class="d-flex align-center justify-space-between ga-4">
|
|
1263
1067
|
<div>
|
|
1264
1068
|
<div
|
|
1265
|
-
class="text-body-2 font-weight-black text-
|
|
1069
|
+
class="text-body-2 font-weight-black text-high-emphasis"
|
|
1266
1070
|
>
|
|
1267
1071
|
{{ widget.title }}
|
|
1268
1072
|
</div>
|
|
1269
|
-
<div class="text-caption text-
|
|
1073
|
+
<div class="text-caption text-medium-emphasis mt-1">
|
|
1270
1074
|
{{ widget.description }}
|
|
1271
1075
|
</div>
|
|
1272
1076
|
</div>
|
|
@@ -1292,7 +1096,7 @@
|
|
|
1292
1096
|
<v-card-actions class="pa-6 pt-4 border-t justify-end ga-3">
|
|
1293
1097
|
<v-btn
|
|
1294
1098
|
variant="text"
|
|
1295
|
-
class="text-none font-weight-bold text-
|
|
1099
|
+
class="text-none font-weight-bold text-medium-emphasis"
|
|
1296
1100
|
@click="cancelCustomize"
|
|
1297
1101
|
>
|
|
1298
1102
|
Cancel
|
|
@@ -1320,7 +1124,7 @@
|
|
|
1320
1124
|
<v-card-title
|
|
1321
1125
|
class="d-flex align-center justify-space-between pa-6 pb-3"
|
|
1322
1126
|
>
|
|
1323
|
-
<div class="text-body-1 font-weight-black text-
|
|
1127
|
+
<div class="text-body-1 font-weight-black text-high-emphasis">
|
|
1324
1128
|
Facility Booking
|
|
1325
1129
|
</div>
|
|
1326
1130
|
<v-btn
|
|
@@ -1381,6 +1185,15 @@
|
|
|
1381
1185
|
</template>
|
|
1382
1186
|
|
|
1383
1187
|
<script setup lang="ts">
|
|
1188
|
+
// The one non-auto-imported thing this file needs: the cookie options type, so
|
|
1189
|
+
// `runtimeConfig.public.cookieConfig` (typed `unknown`) can be narrowed once
|
|
1190
|
+
// rather than at the call site. Same import `useThemePreference.ts` uses.
|
|
1191
|
+
import type { CookieOptions } from "#app";
|
|
1192
|
+
// Explicitly, not by auto-import: every other component in this layer reaches
|
|
1193
|
+
// its utils by relative path (`StatusChip.vue`, `CameraWall.vue`,
|
|
1194
|
+
// `plugins/vuetify.ts`), and a bare call resolved to nothing when driven.
|
|
1195
|
+
import { buildWorkOrderStatus } from "../utils/dashboard";
|
|
1196
|
+
|
|
1384
1197
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
1385
1198
|
|
|
1386
1199
|
type DashboardMetric = {
|
|
@@ -1678,8 +1491,11 @@ const currentVisitorTypeParam = computed(
|
|
|
1678
1491
|
// ─── Data Fetching ────────────────────────────────────────────────────────────
|
|
1679
1492
|
|
|
1680
1493
|
// Hygiene / M&E / Landscape / Pest / Pool main metrics
|
|
1681
|
-
const {
|
|
1682
|
-
|
|
1494
|
+
const {
|
|
1495
|
+
data: getDashboardReq,
|
|
1496
|
+
pending: loadingMetrics,
|
|
1497
|
+
error: metricsError,
|
|
1498
|
+
} = await useLazyAsyncData(
|
|
1683
1499
|
`get-dashboard-${props.site}-${props.serviceType}`,
|
|
1684
1500
|
() =>
|
|
1685
1501
|
props.serviceType && isHygieneMode.value
|
|
@@ -1695,8 +1511,11 @@ const { data: getDashboardReq, pending: loadingMetrics } =
|
|
|
1695
1511
|
);
|
|
1696
1512
|
|
|
1697
1513
|
// Property / Security main metrics (uses generic getDashboard with visitor type)
|
|
1698
|
-
const {
|
|
1699
|
-
|
|
1514
|
+
const {
|
|
1515
|
+
data: getDashboardAltReq,
|
|
1516
|
+
pending: loadingAltMetrics,
|
|
1517
|
+
error: altMetricsError,
|
|
1518
|
+
} = await useLazyAsyncData(
|
|
1700
1519
|
`get-dashboard-alt-${props.site}-${props.serviceType}`,
|
|
1701
1520
|
() =>
|
|
1702
1521
|
props.site && (isPropertyMode.value || isSecurityMode.value)
|
|
@@ -1720,8 +1539,11 @@ const { data: getDashboardAltReq, pending: loadingAltMetrics } =
|
|
|
1720
1539
|
);
|
|
1721
1540
|
|
|
1722
1541
|
// Weekly activity (all modes)
|
|
1723
|
-
const {
|
|
1724
|
-
|
|
1542
|
+
const {
|
|
1543
|
+
data: weeklyActivityReq,
|
|
1544
|
+
pending: loadingWeeklyActivity,
|
|
1545
|
+
error: weeklyActivityError,
|
|
1546
|
+
} = await useLazyAsyncData(
|
|
1725
1547
|
`get-dashboard-weekly-activity-${props.site}-${props.serviceType}`,
|
|
1726
1548
|
() =>
|
|
1727
1549
|
props.serviceType
|
|
@@ -1737,8 +1559,11 @@ const { data: weeklyActivityReq, pending: loadingWeeklyActivity } =
|
|
|
1737
1559
|
);
|
|
1738
1560
|
|
|
1739
1561
|
// Task schedule (hygiene mode only)
|
|
1740
|
-
const {
|
|
1741
|
-
|
|
1562
|
+
const {
|
|
1563
|
+
data: taskScheduleReq,
|
|
1564
|
+
pending: loadingTaskSchedule,
|
|
1565
|
+
error: taskScheduleError,
|
|
1566
|
+
} = await useLazyAsyncData(
|
|
1742
1567
|
`get-dashboard-task-schedule-${props.site}-${props.serviceType}`,
|
|
1743
1568
|
() =>
|
|
1744
1569
|
props.serviceType && isHygieneMode.value
|
|
@@ -1754,8 +1579,11 @@ const { data: taskScheduleReq, pending: loadingTaskSchedule } =
|
|
|
1754
1579
|
);
|
|
1755
1580
|
|
|
1756
1581
|
// Attendance (hygiene + security modes)
|
|
1757
|
-
const {
|
|
1758
|
-
|
|
1582
|
+
const {
|
|
1583
|
+
data: attendanceReq,
|
|
1584
|
+
pending: loadingAttendance,
|
|
1585
|
+
error: attendanceError,
|
|
1586
|
+
} = await useLazyAsyncData(
|
|
1759
1587
|
`get-dashboard-attendance-${props.site}-${props.serviceType}`,
|
|
1760
1588
|
() =>
|
|
1761
1589
|
props.serviceType && !isPropertyMode.value
|
|
@@ -1771,8 +1599,11 @@ const { data: attendanceReq, pending: loadingAttendance } =
|
|
|
1771
1599
|
);
|
|
1772
1600
|
|
|
1773
1601
|
// Feedbacks (all modes)
|
|
1774
|
-
const {
|
|
1775
|
-
|
|
1602
|
+
const {
|
|
1603
|
+
data: feedbacksReq,
|
|
1604
|
+
pending: loadingFeedbacks,
|
|
1605
|
+
error: feedbacksError,
|
|
1606
|
+
} = await useLazyAsyncData(
|
|
1776
1607
|
`get-dashboard-feedbacks-${props.site}-${props.serviceType}`,
|
|
1777
1608
|
() =>
|
|
1778
1609
|
props.serviceType
|
|
@@ -1816,44 +1647,82 @@ const finalLoading = computed(() => {
|
|
|
1816
1647
|
);
|
|
1817
1648
|
});
|
|
1818
1649
|
|
|
1650
|
+
// ─── Request Failures ─────────────────────────────────────────────────────────
|
|
1651
|
+
|
|
1652
|
+
/**
|
|
1653
|
+
* A DEAD API USED TO LOOK EXACTLY LIKE A QUIET DAY.
|
|
1654
|
+
*
|
|
1655
|
+
* Every request's result was read as `x.value ?? <zero>`, so a rejected call
|
|
1656
|
+
* produced the same screen as a site with nothing on it: every tile 0, every
|
|
1657
|
+
* trend +0%, every list "No data to display", and not one word to say the data
|
|
1658
|
+
* never arrived. That ambiguity has cost this estate days before now.
|
|
1659
|
+
*
|
|
1660
|
+
* `useLazyAsyncData` already hands back an `error` ref; these carry it to the
|
|
1661
|
+
* widgets that asked for the data, so a zero on this page is a measurement and
|
|
1662
|
+
* a failure says so.
|
|
1663
|
+
*/
|
|
1664
|
+
const metricsFailed = computed(() =>
|
|
1665
|
+
Boolean(isHygieneMode.value ? metricsError.value : altMetricsError.value)
|
|
1666
|
+
);
|
|
1667
|
+
const chartFailed = computed(() => Boolean(weeklyActivityError.value));
|
|
1668
|
+
const taskScheduleFailed = computed(() => Boolean(taskScheduleError.value));
|
|
1669
|
+
const attendanceFailed = computed(() => Boolean(attendanceError.value));
|
|
1670
|
+
const feedbacksFailed = computed(() => Boolean(feedbacksError.value));
|
|
1671
|
+
|
|
1672
|
+
/** The header banner - one line, whatever combination failed. */
|
|
1673
|
+
const failedWidgetLabels = computed(() => {
|
|
1674
|
+
const labels: string[] = [];
|
|
1675
|
+
if (metricsFailed.value) labels.push("the summary tiles");
|
|
1676
|
+
if (chartFailed.value) labels.push("the activity chart");
|
|
1677
|
+
if (taskScheduleFailed.value && isHygieneMode.value)
|
|
1678
|
+
labels.push(primaryListConfig.value.title.toLowerCase());
|
|
1679
|
+
if (attendanceFailed.value && !isPropertyMode.value) labels.push("staff");
|
|
1680
|
+
if (feedbacksFailed.value) labels.push("feedbacks");
|
|
1681
|
+
return labels;
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
/** The text a tile or panel shows in place of a figure it does not have. */
|
|
1685
|
+
const LOAD_FAILED_TEXT = "Couldn't load";
|
|
1686
|
+
|
|
1819
1687
|
// ─── Merged Dashboard Data ────────────────────────────────────────────────────
|
|
1820
1688
|
|
|
1821
1689
|
const finalDashboardData = computed<Record<string, any>>(() => {
|
|
1822
1690
|
if (!props.serviceType) return props.dashboardData || {};
|
|
1823
1691
|
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1692
|
+
/**
|
|
1693
|
+
* A QUIET WEEK IS NOT A BUSY ONE.
|
|
1694
|
+
*
|
|
1695
|
+
* This used to substitute a hard-coded seven-day curve - Mon 800/1400 through
|
|
1696
|
+
* Sun 2800/3200 - whenever the week's activity came back with nothing in it,
|
|
1697
|
+
* and draw it as real, y-axis to 3,200. The backend zero-fills one row per
|
|
1698
|
+
* day and never returns an empty array, so "nothing in it" is exactly what a
|
|
1699
|
+
* genuinely quiet site looks like; `?? []` made it exactly what a FAILED
|
|
1700
|
+
* request looks like too. And it left the screen: Export -> Word wrote those
|
|
1701
|
+
* invented figures into a document a manager can hand to a client.
|
|
1702
|
+
*
|
|
1703
|
+
* Deleted outright. An empty week now falls through to `hasChartData` and
|
|
1704
|
+
* renders the empty state, a failed one renders the error state, and the Word
|
|
1705
|
+
* export's chart table is already gated on the same `hasChartData`.
|
|
1706
|
+
*/
|
|
1707
|
+
const weeklyActivity = weeklyActivityReq.value ?? [];
|
|
1708
|
+
|
|
1709
|
+
// `weekly-activity` returns each interval twice over: `openWorkOrder` and
|
|
1710
|
+
// `workOrder` are the same not-yet-completed count, `closedWorkOrder` and
|
|
1711
|
+
// `taskCompleted` the same completed count. Carry BOTH names - the Word export
|
|
1712
|
+
// reads `workOrders`/`taskCompleted`, and the open/closed pair is what tells
|
|
1713
|
+
// the chart legend these are work orders and not checklist tasks.
|
|
1714
|
+
const openSeries = weeklyActivity.map(
|
|
1715
|
+
(item: any) => item.workOrder ?? item.openWorkOrder
|
|
1716
|
+
);
|
|
1717
|
+
const closedSeries = weeklyActivity.map(
|
|
1718
|
+
(item: any) => item.taskCompleted ?? item.closedWorkOrder
|
|
1719
|
+
);
|
|
1849
1720
|
const activityChart = {
|
|
1850
1721
|
labels: weeklyActivity.map((item: any) => item.day || item.label),
|
|
1851
|
-
workOrders:
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
(item: any) => item.taskCompleted ?? item.closedWorkOrder
|
|
1856
|
-
),
|
|
1722
|
+
workOrders: openSeries,
|
|
1723
|
+
taskCompleted: closedSeries,
|
|
1724
|
+
openWorkOrder: openSeries,
|
|
1725
|
+
closedWorkOrder: closedSeries,
|
|
1857
1726
|
};
|
|
1858
1727
|
|
|
1859
1728
|
if (isHygieneMode.value) {
|
|
@@ -1931,27 +1800,64 @@ const finalDashboardData = computed<Record<string, any>>(() => {
|
|
|
1931
1800
|
|
|
1932
1801
|
// ─── Card Value Configs ───────────────────────────────────────────────────────
|
|
1933
1802
|
|
|
1803
|
+
/**
|
|
1804
|
+
* THE KPI TILE PALETTE.
|
|
1805
|
+
*
|
|
1806
|
+
* Was eleven hand-picked hexes restated in four places and blind to the theme:
|
|
1807
|
+
* the SAME orange glyph on the SAME pink tint in light and in dark, because a
|
|
1808
|
+
* literal cannot follow a theme. Its worst pairing measured 1.36:1 and the one
|
|
1809
|
+
* that actually shipped measured 1.74:1 - the lowest readings in the package,
|
|
1810
|
+
* on the first screen every account type opens.
|
|
1811
|
+
*
|
|
1812
|
+
* The handoff names no colour for a KPI tile. It DOES name four status tones
|
|
1813
|
+
* and their soft fills, and the hexes that were here were already those four
|
|
1814
|
+
* hues (`#1d5f99` blue, `#b73535` red, `#d28722` amber, `#23824f` green). So
|
|
1815
|
+
* each pair is re-pointed at the token it was imitating rather than given a
|
|
1816
|
+
* new colour. That also makes each tile the design's own chip pairing, which
|
|
1817
|
+
* is the ruling this project already follows.
|
|
1818
|
+
*/
|
|
1819
|
+
const KPI_TONES = {
|
|
1820
|
+
info: { color: "var(--info)", tint: "var(--info-bg)" },
|
|
1821
|
+
err: { color: "var(--err)", tint: "var(--err-bg)" },
|
|
1822
|
+
ok: { color: "var(--ok)", tint: "var(--ok-bg)" },
|
|
1823
|
+
warn: { color: "var(--warn)", tint: "var(--warn-bg)" },
|
|
1824
|
+
} as const;
|
|
1825
|
+
|
|
1934
1826
|
const defaultCardValues: TDashboardCardValue[] = [
|
|
1935
1827
|
{
|
|
1936
1828
|
key: "openWorkOrder",
|
|
1937
1829
|
periodKey: "period",
|
|
1938
1830
|
title: "Open Work Order",
|
|
1939
1831
|
icon: "mdi-briefcase",
|
|
1940
|
-
|
|
1832
|
+
/** The same tile is `#1d5f99` blue in the other two branches - info. */
|
|
1833
|
+
color: KPI_TONES.info.color,
|
|
1941
1834
|
},
|
|
1942
1835
|
{
|
|
1943
1836
|
key: "taskCompleted",
|
|
1944
1837
|
periodKey: "period",
|
|
1945
1838
|
title: "Task Completed",
|
|
1946
1839
|
icon: "mdi-clipboard-check-outline",
|
|
1947
|
-
|
|
1840
|
+
/** `utils/status.ts`: "completed" is `ok`. This was the 1.74:1 orange. */
|
|
1841
|
+
color: KPI_TONES.ok.color,
|
|
1948
1842
|
},
|
|
1949
1843
|
{
|
|
1950
1844
|
key: "supplyAlert",
|
|
1951
1845
|
periodKey: "period",
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1846
|
+
/**
|
|
1847
|
+
* IT IS NOT AN ALERT, SO IT NO LONGER SAYS ALERT.
|
|
1848
|
+
*
|
|
1849
|
+
* The server returns the three lowest-quantity active supplies and NO
|
|
1850
|
+
* threshold of any kind (new-dashboard.repo.ts) - so 900 gloves appeared
|
|
1851
|
+
* under a red "SUPPLY ALERT" heading with a warning triangle, next to
|
|
1852
|
+
* items that genuinely were low, and nothing distinguished them. The title,
|
|
1853
|
+
* the icon and the tone now describe what the data is.
|
|
1854
|
+
*
|
|
1855
|
+
* A real low-stock alert needs a per-supply reorder level the server does
|
|
1856
|
+
* not hold. That is backend work and is deliberately NOT invented here.
|
|
1857
|
+
*/
|
|
1858
|
+
title: "Lowest Stock",
|
|
1859
|
+
icon: "mdi-package-variant",
|
|
1860
|
+
color: KPI_TONES.info.color,
|
|
1955
1861
|
},
|
|
1956
1862
|
];
|
|
1957
1863
|
|
|
@@ -1961,28 +1867,28 @@ const propertyCardValues: TDashboardCardValue[] = [
|
|
|
1961
1867
|
periodKey: "period",
|
|
1962
1868
|
title: "Open Work Order",
|
|
1963
1869
|
icon: "mdi-briefcase",
|
|
1964
|
-
color:
|
|
1870
|
+
color: KPI_TONES.info.color,
|
|
1965
1871
|
},
|
|
1966
1872
|
{
|
|
1967
1873
|
key: "incidents",
|
|
1968
1874
|
periodKey: "period",
|
|
1969
1875
|
title: "Open Incidents",
|
|
1970
1876
|
icon: "mdi-alert",
|
|
1971
|
-
color:
|
|
1877
|
+
color: KPI_TONES.err.color,
|
|
1972
1878
|
},
|
|
1973
1879
|
{
|
|
1974
1880
|
key: "visitors",
|
|
1975
1881
|
periodKey: "period",
|
|
1976
1882
|
title: "Visitors",
|
|
1977
1883
|
icon: "mdi-account-multiple",
|
|
1978
|
-
color:
|
|
1884
|
+
color: KPI_TONES.warn.color,
|
|
1979
1885
|
},
|
|
1980
1886
|
{
|
|
1981
1887
|
key: "facilityBookings",
|
|
1982
1888
|
periodKey: "period",
|
|
1983
1889
|
title: "Facility Booking",
|
|
1984
1890
|
icon: "mdi-office-building",
|
|
1985
|
-
color:
|
|
1891
|
+
color: KPI_TONES.ok.color,
|
|
1986
1892
|
},
|
|
1987
1893
|
];
|
|
1988
1894
|
|
|
@@ -1992,28 +1898,28 @@ const securityCardValues: TDashboardCardValue[] = [
|
|
|
1992
1898
|
periodKey: "period",
|
|
1993
1899
|
title: "Open Work Order",
|
|
1994
1900
|
icon: "mdi-briefcase",
|
|
1995
|
-
color:
|
|
1901
|
+
color: KPI_TONES.info.color,
|
|
1996
1902
|
},
|
|
1997
1903
|
{
|
|
1998
1904
|
key: "incidents",
|
|
1999
1905
|
periodKey: "period",
|
|
2000
1906
|
title: "Open Incidents",
|
|
2001
1907
|
icon: "mdi-alert",
|
|
2002
|
-
color:
|
|
1908
|
+
color: KPI_TONES.err.color,
|
|
2003
1909
|
},
|
|
2004
1910
|
{
|
|
2005
1911
|
key: "visitors",
|
|
2006
1912
|
periodKey: "period",
|
|
2007
1913
|
title: "Visitors",
|
|
2008
1914
|
icon: "mdi-account-multiple",
|
|
2009
|
-
color:
|
|
1915
|
+
color: KPI_TONES.warn.color,
|
|
2010
1916
|
},
|
|
2011
1917
|
{
|
|
2012
1918
|
key: "patrolCompliance",
|
|
2013
1919
|
periodKey: "period",
|
|
2014
1920
|
title: "Patrol Compliance",
|
|
2015
1921
|
icon: "mdi-office-building",
|
|
2016
|
-
color:
|
|
1922
|
+
color: KPI_TONES.ok.color,
|
|
2017
1923
|
},
|
|
2018
1924
|
];
|
|
2019
1925
|
|
|
@@ -2026,12 +1932,16 @@ const resolvedCardValues = computed(() => {
|
|
|
2026
1932
|
|
|
2027
1933
|
// ─── Widget Keys by Mode ──────────────────────────────────────────────────────
|
|
2028
1934
|
|
|
1935
|
+
// "recentActivity" was in this list and in the Customize dialog, and NOTHING
|
|
1936
|
+
// renders it - `isWidgetVisible("recentActivity")` appears nowhere in the
|
|
1937
|
+
// template. Five apps offered a switch that did nothing whichever way it was
|
|
1938
|
+
// set. Removed rather than built: there is no Recent Activity panel anywhere in
|
|
1939
|
+
// the estate to connect it to, and inventing one is a new feature, not a fix.
|
|
2029
1940
|
const defaultExtraWidgetKeys = [
|
|
2030
1941
|
"activity",
|
|
2031
1942
|
"taskSchedule",
|
|
2032
1943
|
"staffStatus",
|
|
2033
1944
|
"feedbacks",
|
|
2034
|
-
"recentActivity",
|
|
2035
1945
|
];
|
|
2036
1946
|
|
|
2037
1947
|
const propertyDefaultWidgetKeys = [
|
|
@@ -2058,6 +1968,36 @@ const securityDefaultWidgetKeys = [
|
|
|
2058
1968
|
"feedbacks",
|
|
2059
1969
|
];
|
|
2060
1970
|
|
|
1971
|
+
/**
|
|
1972
|
+
* THE CUSTOMISE CHOICE, REMEMBERED.
|
|
1973
|
+
*
|
|
1974
|
+
* Hiding a widget worked and then forgot: nothing was written anywhere, so a
|
|
1975
|
+
* reload brought every panel back and the dialog was decoration.
|
|
1976
|
+
*
|
|
1977
|
+
* A COOKIE, and specifically the layer's own `cookieConfig` - the same
|
|
1978
|
+
* mechanism `sid`, `user`, `landing-page` and the light/dark preference already
|
|
1979
|
+
* travel on (see `composables/useThemePreference.ts` for the full reasoning).
|
|
1980
|
+
* localStorage would be per-origin, and this is one component shipped into
|
|
1981
|
+
* seven applications on seven hostnames. No new storage mechanism, and no
|
|
1982
|
+
* invented endpoint - there is no save API on the server for this.
|
|
1983
|
+
*
|
|
1984
|
+
* One cookie per dashboard MODE, because the three modes have different widget
|
|
1985
|
+
* sets and a security member's choice must not decide what a hygiene member
|
|
1986
|
+
* sees.
|
|
1987
|
+
*/
|
|
1988
|
+
const widgetPrefsCookie = useCookie<string[] | null>(
|
|
1989
|
+
`dashboard-widgets-${
|
|
1990
|
+
isPropertyMode.value
|
|
1991
|
+
? "property"
|
|
1992
|
+
: isSecurityMode.value
|
|
1993
|
+
? "security"
|
|
1994
|
+
: "service"
|
|
1995
|
+
}`,
|
|
1996
|
+
runtimeConfig.public.cookieConfig as CookieOptions<string[] | null> & {
|
|
1997
|
+
readonly?: false;
|
|
1998
|
+
}
|
|
1999
|
+
);
|
|
2000
|
+
|
|
2061
2001
|
const defaultVisibleWidgetKeys = computed(() => {
|
|
2062
2002
|
if (props.extraWidgetKeys && props.extraWidgetKeys.length > 0) {
|
|
2063
2003
|
return [
|
|
@@ -2078,8 +2018,16 @@ watch(
|
|
|
2078
2018
|
(keys) => {
|
|
2079
2019
|
const nextKeys = [...keys];
|
|
2080
2020
|
if (!visibleWidgetKeys.value.length) {
|
|
2081
|
-
|
|
2082
|
-
|
|
2021
|
+
// A remembered choice wins on first paint, filtered against the keys this
|
|
2022
|
+
// mode actually has so a stale cookie cannot resurrect a removed widget.
|
|
2023
|
+
// `Array.isArray` rather than a length check: "the user hid everything"
|
|
2024
|
+
// and "there is no cookie" are different answers.
|
|
2025
|
+
const remembered = Array.isArray(widgetPrefsCookie.value)
|
|
2026
|
+
? widgetPrefsCookie.value.filter((k) => nextKeys.includes(k))
|
|
2027
|
+
: null;
|
|
2028
|
+
const initial = remembered ?? nextKeys;
|
|
2029
|
+
visibleWidgetKeys.value = initial;
|
|
2030
|
+
draftVisibleWidgetKeys.value = [...initial];
|
|
2083
2031
|
return;
|
|
2084
2032
|
}
|
|
2085
2033
|
visibleWidgetKeys.value = visibleWidgetKeys.value.filter((k) =>
|
|
@@ -2112,7 +2060,7 @@ const customizeWidgets = computed(() => {
|
|
|
2112
2060
|
key: "visitors",
|
|
2113
2061
|
title: "Visitors",
|
|
2114
2062
|
description:
|
|
2115
|
-
"See today's visitor traffic and
|
|
2063
|
+
"See today's visitor traffic, and how many passes are still not checked out.",
|
|
2116
2064
|
},
|
|
2117
2065
|
{
|
|
2118
2066
|
key: "facilityBookings",
|
|
@@ -2136,9 +2084,9 @@ const customizeWidgets = computed(() => {
|
|
|
2136
2084
|
},
|
|
2137
2085
|
{
|
|
2138
2086
|
key: "todayAttentions",
|
|
2139
|
-
title: "
|
|
2087
|
+
title: "Needs Attention",
|
|
2140
2088
|
description:
|
|
2141
|
-
"
|
|
2089
|
+
"Everything still pending or unacknowledged, not only today's.",
|
|
2142
2090
|
},
|
|
2143
2091
|
{
|
|
2144
2092
|
key: "workOrderStatus",
|
|
@@ -2169,7 +2117,7 @@ const customizeWidgets = computed(() => {
|
|
|
2169
2117
|
key: "visitors",
|
|
2170
2118
|
title: "Visitors",
|
|
2171
2119
|
description:
|
|
2172
|
-
"See today's visitor traffic and
|
|
2120
|
+
"See today's visitor traffic, and how many passes are still not checked out.",
|
|
2173
2121
|
},
|
|
2174
2122
|
{
|
|
2175
2123
|
key: "patrolCompliance",
|
|
@@ -2226,11 +2174,6 @@ const customizeWidgets = computed(() => {
|
|
|
2226
2174
|
title: "Feedbacks",
|
|
2227
2175
|
description: "Show recent feedback and resolution state.",
|
|
2228
2176
|
},
|
|
2229
|
-
{
|
|
2230
|
-
key: "recentActivity",
|
|
2231
|
-
title: "Recent Activity",
|
|
2232
|
-
description: "Latest updates across the service.",
|
|
2233
|
-
},
|
|
2234
2177
|
];
|
|
2235
2178
|
});
|
|
2236
2179
|
|
|
@@ -2342,10 +2285,10 @@ function getPropertyKpiCards() {
|
|
|
2342
2285
|
key: "workOrders",
|
|
2343
2286
|
title: "Open Work Order",
|
|
2344
2287
|
icon: "mdi-briefcase",
|
|
2345
|
-
color:
|
|
2346
|
-
tint:
|
|
2288
|
+
color: KPI_TONES.info.color,
|
|
2289
|
+
tint: KPI_TONES.info.tint,
|
|
2347
2290
|
value: formatNumber(toNumber(workOrderMetric?.count)),
|
|
2348
|
-
percentage:
|
|
2291
|
+
percentage: toNumberOrNaN(workOrderMetric?.percentage),
|
|
2349
2292
|
meta: getMetricMeta(workOrderMetric),
|
|
2350
2293
|
raw: workOrderMetric,
|
|
2351
2294
|
filter: undefined,
|
|
@@ -2354,10 +2297,10 @@ function getPropertyKpiCards() {
|
|
|
2354
2297
|
key: "incidents",
|
|
2355
2298
|
title: "Open Incidents",
|
|
2356
2299
|
icon: "mdi-alert",
|
|
2357
|
-
color:
|
|
2358
|
-
tint:
|
|
2300
|
+
color: KPI_TONES.err.color,
|
|
2301
|
+
tint: KPI_TONES.err.tint,
|
|
2359
2302
|
value: formatNumber(toNumber(incidentMetric?.count)),
|
|
2360
|
-
percentage:
|
|
2303
|
+
percentage: toNumberOrNaN(incidentMetric?.percentage),
|
|
2361
2304
|
meta: getMetricMeta(incidentMetric),
|
|
2362
2305
|
raw: incidentMetric,
|
|
2363
2306
|
filter: undefined,
|
|
@@ -2366,10 +2309,10 @@ function getPropertyKpiCards() {
|
|
|
2366
2309
|
key: "visitors",
|
|
2367
2310
|
title: "Visitors",
|
|
2368
2311
|
icon: "mdi-account-multiple",
|
|
2369
|
-
color:
|
|
2370
|
-
tint:
|
|
2312
|
+
color: KPI_TONES.warn.color,
|
|
2313
|
+
tint: KPI_TONES.warn.tint,
|
|
2371
2314
|
value: formatNumber(toNumber(visitorMetric?.count)),
|
|
2372
|
-
percentage:
|
|
2315
|
+
percentage: toNumberOrNaN(visitorMetric?.percentage),
|
|
2373
2316
|
meta: getMetricMeta(visitorMetric),
|
|
2374
2317
|
raw: visitorMetric,
|
|
2375
2318
|
filter: {
|
|
@@ -2385,10 +2328,10 @@ function getPropertyKpiCards() {
|
|
|
2385
2328
|
key: "facilityBookings",
|
|
2386
2329
|
title: "Facility Booking",
|
|
2387
2330
|
icon: "mdi-office-building",
|
|
2388
|
-
color:
|
|
2389
|
-
tint:
|
|
2331
|
+
color: KPI_TONES.ok.color,
|
|
2332
|
+
tint: KPI_TONES.ok.tint,
|
|
2390
2333
|
value: formatNumber(toNumber(facilityMetric?.count)),
|
|
2391
|
-
percentage:
|
|
2334
|
+
percentage: toNumberOrNaN(facilityMetric?.percentage),
|
|
2392
2335
|
meta: getMetricMeta(facilityMetric),
|
|
2393
2336
|
raw: facilityMetric,
|
|
2394
2337
|
filter: undefined,
|
|
@@ -2420,10 +2363,10 @@ function getSecurityKpiCards() {
|
|
|
2420
2363
|
key: "workOrders",
|
|
2421
2364
|
title: "Open Work Order",
|
|
2422
2365
|
icon: "mdi-briefcase",
|
|
2423
|
-
color:
|
|
2424
|
-
tint:
|
|
2366
|
+
color: KPI_TONES.info.color,
|
|
2367
|
+
tint: KPI_TONES.info.tint,
|
|
2425
2368
|
value: formatNumber(toNumber(workOrderMetric?.count)),
|
|
2426
|
-
percentage:
|
|
2369
|
+
percentage: toNumberOrNaN(workOrderMetric?.percentage),
|
|
2427
2370
|
meta: getMetricMeta(workOrderMetric),
|
|
2428
2371
|
raw: workOrderMetric,
|
|
2429
2372
|
filter: undefined,
|
|
@@ -2432,10 +2375,10 @@ function getSecurityKpiCards() {
|
|
|
2432
2375
|
key: "incidents",
|
|
2433
2376
|
title: "Open Incidents",
|
|
2434
2377
|
icon: "mdi-alert",
|
|
2435
|
-
color:
|
|
2436
|
-
tint:
|
|
2378
|
+
color: KPI_TONES.err.color,
|
|
2379
|
+
tint: KPI_TONES.err.tint,
|
|
2437
2380
|
value: formatNumber(toNumber(incidentMetric?.count)),
|
|
2438
|
-
percentage:
|
|
2381
|
+
percentage: toNumberOrNaN(incidentMetric?.percentage),
|
|
2439
2382
|
meta: getMetricMeta(incidentMetric),
|
|
2440
2383
|
raw: incidentMetric,
|
|
2441
2384
|
filter: undefined,
|
|
@@ -2444,10 +2387,10 @@ function getSecurityKpiCards() {
|
|
|
2444
2387
|
key: "visitors",
|
|
2445
2388
|
title: "Visitors",
|
|
2446
2389
|
icon: "mdi-account-multiple",
|
|
2447
|
-
color:
|
|
2448
|
-
tint:
|
|
2390
|
+
color: KPI_TONES.warn.color,
|
|
2391
|
+
tint: KPI_TONES.warn.tint,
|
|
2449
2392
|
value: formatNumber(toNumber(visitorMetric?.count)),
|
|
2450
|
-
percentage:
|
|
2393
|
+
percentage: toNumberOrNaN(visitorMetric?.percentage),
|
|
2451
2394
|
meta: getMetricMeta(visitorMetric),
|
|
2452
2395
|
raw: visitorMetric,
|
|
2453
2396
|
filter: {
|
|
@@ -2463,10 +2406,12 @@ function getSecurityKpiCards() {
|
|
|
2463
2406
|
key: "patrolCompliance",
|
|
2464
2407
|
title: "Patrol Compliance",
|
|
2465
2408
|
icon: "mdi-office-building",
|
|
2466
|
-
color:
|
|
2467
|
-
tint:
|
|
2468
|
-
|
|
2469
|
-
|
|
2409
|
+
color: KPI_TONES.ok.color,
|
|
2410
|
+
tint: KPI_TONES.ok.tint,
|
|
2411
|
+
// Through the shared helper, so a `null` rate (no checkpoint was due)
|
|
2412
|
+
// reads as not-measured here too, not as 0%.
|
|
2413
|
+
value: getMetricValue(patrolMetric, "patrolCompliance"),
|
|
2414
|
+
percentage: toNumberOrNaN(patrolMetric?.percentage),
|
|
2470
2415
|
meta: getMetricMeta(patrolMetric),
|
|
2471
2416
|
raw: patrolMetric,
|
|
2472
2417
|
filter: undefined,
|
|
@@ -2474,7 +2419,7 @@ function getSecurityKpiCards() {
|
|
|
2474
2419
|
];
|
|
2475
2420
|
}
|
|
2476
2421
|
|
|
2477
|
-
const
|
|
2422
|
+
const rawKpiCards = computed(() => {
|
|
2478
2423
|
if (isPropertyMode.value) return getPropertyKpiCards();
|
|
2479
2424
|
if (isSecurityMode.value) return getSecurityKpiCards();
|
|
2480
2425
|
|
|
@@ -2493,7 +2438,7 @@ const kpiCards = computed(() => {
|
|
|
2493
2438
|
tint: getCardTint(mapping.color, index),
|
|
2494
2439
|
color: mapping.color || getCardColor(index),
|
|
2495
2440
|
value: typeof value === "string" ? value : formatNumber(value),
|
|
2496
|
-
percentage:
|
|
2441
|
+
percentage: toNumberOrNaN(metric.percentage),
|
|
2497
2442
|
meta: getMetricMeta(metric),
|
|
2498
2443
|
raw: metric,
|
|
2499
2444
|
filter: undefined as any,
|
|
@@ -2501,10 +2446,37 @@ const kpiCards = computed(() => {
|
|
|
2501
2446
|
});
|
|
2502
2447
|
});
|
|
2503
2448
|
|
|
2449
|
+
/**
|
|
2450
|
+
* A tile whose request failed shows an em dash and says so, never a zero.
|
|
2451
|
+
* `percentage: NaN` is what suppresses the pill - `kpiChipText()` returns ""
|
|
2452
|
+
* for anything non-finite - and `raw: {}` empties the supply list with it.
|
|
2453
|
+
*/
|
|
2454
|
+
const kpiCards = computed(() => {
|
|
2455
|
+
if (!metricsFailed.value) return rawKpiCards.value;
|
|
2456
|
+
return rawKpiCards.value.map((card) => ({
|
|
2457
|
+
...card,
|
|
2458
|
+
value: "—",
|
|
2459
|
+
percentage: NaN,
|
|
2460
|
+
meta: LOAD_FAILED_TEXT,
|
|
2461
|
+
raw: {} as any,
|
|
2462
|
+
}));
|
|
2463
|
+
});
|
|
2464
|
+
|
|
2504
2465
|
const visibleKpiCards = computed(() =>
|
|
2505
2466
|
kpiCards.value.filter((card) => isWidgetVisible(card.key))
|
|
2506
2467
|
);
|
|
2507
2468
|
|
|
2469
|
+
/**
|
|
2470
|
+
* The percentage pill's label. Same number the chip has always shown, written
|
|
2471
|
+
* the way the handoff prints it - `+0%` / `-6.4%`, the sign carrying what the
|
|
2472
|
+
* arrow icon used to. A card without a percentage gets no pill.
|
|
2473
|
+
*/
|
|
2474
|
+
function kpiChipText(card: Record<string, any>): string {
|
|
2475
|
+
const value = Number(card?.percentage);
|
|
2476
|
+
if (!Number.isFinite(value)) return "";
|
|
2477
|
+
return `${value < 0 ? "-" : "+"}${Math.abs(value)}%`;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2508
2480
|
// ─── Dashboard Title ──────────────────────────────────────────────────────────
|
|
2509
2481
|
|
|
2510
2482
|
const dashboardTitle = computed(() => {
|
|
@@ -2515,6 +2487,12 @@ const dashboardTitle = computed(() => {
|
|
|
2515
2487
|
return `${getServiceDisplayName(props.serviceType || appName)} - Dashboard`;
|
|
2516
2488
|
});
|
|
2517
2489
|
|
|
2490
|
+
const taskScheduleTitle = computed(() => {
|
|
2491
|
+
if (selectedRange.value === "This Month") return "This Month's Task Schedule";
|
|
2492
|
+
if (selectedRange.value === "This Week") return "This Week's Task Schedule";
|
|
2493
|
+
return "Today's Task Schedule";
|
|
2494
|
+
});
|
|
2495
|
+
|
|
2518
2496
|
// ─── Primary list / staff config ─────────────────────────────────────────────
|
|
2519
2497
|
|
|
2520
2498
|
const primaryListConfig = computed(() =>
|
|
@@ -2525,7 +2503,11 @@ const primaryListConfig = computed(() =>
|
|
|
2525
2503
|
keys: ["activePatrolItems", "patrolItems"],
|
|
2526
2504
|
}
|
|
2527
2505
|
: {
|
|
2528
|
-
|
|
2506
|
+
// The endpoint behind this panel takes the selected period
|
|
2507
|
+
// (`getServiceTodayTaskSchedule` -> `getDateRange(period)`), so on the
|
|
2508
|
+
// Week or Month range the list was a week's or a month's work under a
|
|
2509
|
+
// heading that said "Today's".
|
|
2510
|
+
title: taskScheduleTitle.value,
|
|
2529
2511
|
route: "scheduleTask",
|
|
2530
2512
|
keys: [
|
|
2531
2513
|
"taskScheduleItems",
|
|
@@ -2576,8 +2558,12 @@ const primarySeries = computed(() =>
|
|
|
2576
2558
|
const secondarySeries = computed(() =>
|
|
2577
2559
|
normalizeSeries(activityChart.value.secondary, chartLabels.value.length)
|
|
2578
2560
|
);
|
|
2579
|
-
|
|
2580
|
-
|
|
2561
|
+
// The legend must name the series it is drawn beside. These were hard-coded, and
|
|
2562
|
+
// hard-coded the wrong way round: the primary line is the OPEN work orders and
|
|
2563
|
+
// was labelled "Closed Work Order". `normalizeActivityChart()` already works out
|
|
2564
|
+
// what each series is from the payload it found - read that instead of guessing.
|
|
2565
|
+
const primaryLegendLabel = computed(() => activityChart.value.primaryLabel);
|
|
2566
|
+
const secondaryLegendLabel = computed(() => activityChart.value.secondaryLabel);
|
|
2581
2567
|
const chartMax = computed(() =>
|
|
2582
2568
|
Math.max(1, ...primarySeries.value, ...secondarySeries.value)
|
|
2583
2569
|
);
|
|
@@ -2685,42 +2671,38 @@ const todayAttentions = computed(() =>
|
|
|
2685
2671
|
normalizeListItems(["todayAttentions", "attentions"])
|
|
2686
2672
|
);
|
|
2687
2673
|
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
color: "#ff8900",
|
|
2721
|
-
},
|
|
2722
|
-
];
|
|
2723
|
-
});
|
|
2674
|
+
/**
|
|
2675
|
+
* THE WORK ORDER STATUS BARS.
|
|
2676
|
+
*
|
|
2677
|
+
* Was three hand-picked hexes - `#2fb84d` green, `#f12d2d` red, `#ff8900`
|
|
2678
|
+
* orange - and blind to the theme: the SAME three bars in light and in dark,
|
|
2679
|
+
* because a literal cannot follow a theme. Each is re-pointed at the token
|
|
2680
|
+
* whose hue it was already imitating, which is the same treatment the KPI
|
|
2681
|
+
* tiles got, and which keeps all three bars distinguishable.
|
|
2682
|
+
*
|
|
2683
|
+
* ONE OF THEM DOES NOT AGREE WITH `utils/status.ts`, ON PURPOSE.
|
|
2684
|
+
* That file maps `completed -> ok` and `pending -> warn` (both honoured here),
|
|
2685
|
+
* but it also maps `in progress -> warn` - the SAME tone as `pending`. Applying
|
|
2686
|
+
* it literally would paint two of these three bars the same amber and make the
|
|
2687
|
+
* chart unreadable, which is a regression, not a fix. So "In Progress" keeps
|
|
2688
|
+
* the tone it is drawn in today (`--err`, the token `#f12d2d` was imitating)
|
|
2689
|
+
* and this stays a colour change with no behaviour change.
|
|
2690
|
+
*
|
|
2691
|
+
* That leaves a real inconsistency to settle: on this very page the "In
|
|
2692
|
+
* Progress" feedback CHIP is amber (`warn`) while this bar is red. Resolving it
|
|
2693
|
+
* means either giving `in progress` its own tone in `utils/status.ts` - which
|
|
2694
|
+
* is shared by every chip in the estate - or ruling that a chart series is not
|
|
2695
|
+
* a status chip. Both are design decisions, so neither is taken here.
|
|
2696
|
+
*/
|
|
2697
|
+
const workOrderStatus = computed(() =>
|
|
2698
|
+
// The arithmetic and the "no breakdown, no bars" rule live in
|
|
2699
|
+
// `utils/dashboard.ts` so they have a test; the tone -> token mapping stays
|
|
2700
|
+
// here because it is this template's colour question, not the server's.
|
|
2701
|
+
buildWorkOrderStatus(finalDashboardData.value?.workOrderStatus).map((row) => ({
|
|
2702
|
+
...row,
|
|
2703
|
+
color: `var(--${row.tone})`,
|
|
2704
|
+
}))
|
|
2705
|
+
);
|
|
2724
2706
|
|
|
2725
2707
|
// Security-mode lists
|
|
2726
2708
|
const activePatrolItems = computed<any[]>(
|
|
@@ -2808,7 +2790,7 @@ const printTableSections = computed<PrintTableSection[]>(() => {
|
|
|
2808
2790
|
},
|
|
2809
2791
|
{
|
|
2810
2792
|
key: "todayAttentions",
|
|
2811
|
-
title: "
|
|
2793
|
+
title: "Needs Attention",
|
|
2812
2794
|
columns: [
|
|
2813
2795
|
{ key: "title", label: "Attention", width: "40%", strong: true },
|
|
2814
2796
|
{ key: "subtitle", label: "Details / Subtitle", width: "60%" },
|
|
@@ -2913,7 +2895,21 @@ onUnmounted(() => {
|
|
|
2913
2895
|
watch(selectedRange, (period) => {
|
|
2914
2896
|
if (props.serviceType) {
|
|
2915
2897
|
const apiPeriod = periodValue[period];
|
|
2916
|
-
if (apiPeriod)
|
|
2898
|
+
if (apiPeriod) {
|
|
2899
|
+
currentPeriod.value = apiPeriod;
|
|
2900
|
+
/**
|
|
2901
|
+
* The component already READ `?period=` on mount and never wrote it
|
|
2902
|
+
* back, so a refresh - or a link sent to a colleague - silently returned
|
|
2903
|
+
* to Today while the button still said This Month. Writing it closes
|
|
2904
|
+
* both. `replace`, not `push`: changing the range is not a navigation
|
|
2905
|
+
* step the Back button should have to walk through.
|
|
2906
|
+
*
|
|
2907
|
+
* What each range MEANS is untouched here.
|
|
2908
|
+
*/
|
|
2909
|
+
if (route.query.period !== apiPeriod) {
|
|
2910
|
+
router.replace({ query: { ...route.query, period: apiPeriod } });
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2917
2913
|
}
|
|
2918
2914
|
emit("updatePeriod", { key: "__all__", period });
|
|
2919
2915
|
});
|
|
@@ -2933,7 +2929,9 @@ function getMetric(keys: string[]): DashboardMetric {
|
|
|
2933
2929
|
|
|
2934
2930
|
function getMetricValue(metric: DashboardMetric, key?: string) {
|
|
2935
2931
|
if (key === "patrolCompliance") {
|
|
2936
|
-
|
|
2932
|
+
const rate = toNumberOrNaN(metric.count);
|
|
2933
|
+
// `null` = no checkpoint was due in this period. Not 0%, not 100%.
|
|
2934
|
+
return Number.isFinite(rate) ? `${formatNumber(rate)}%` : "—";
|
|
2937
2935
|
}
|
|
2938
2936
|
const completed = toNumber(metric.completed);
|
|
2939
2937
|
const total = toNumber(metric.total);
|
|
@@ -2951,11 +2949,18 @@ function getMetricMeta(metric: DashboardMetric) {
|
|
|
2951
2949
|
parts.push(`${formatNumber(toNumber(metric.inProgress))} in progress`);
|
|
2952
2950
|
if (typeof metric.pending !== "undefined")
|
|
2953
2951
|
parts.push(`${formatNumber(toNumber(metric.pending))} pending`);
|
|
2954
|
-
|
|
2952
|
+
// `null` here is the API saying incident severity is not recorded at all.
|
|
2953
|
+
// Printing "0 high severity" would answer a question nothing measured.
|
|
2954
|
+
if (metric.highSeverity !== undefined && metric.highSeverity !== null)
|
|
2955
2955
|
parts.push(`${formatNumber(toNumber(metric.highSeverity))} high severity`);
|
|
2956
|
-
|
|
2956
|
+
// NOT a live occupancy figure. The server counts every visitor transaction
|
|
2957
|
+
// with a check-in and no check-out, with NO date bound, and nothing in the
|
|
2958
|
+
// product ever closes a stale one - so a pass left open years ago is still
|
|
2959
|
+
// in here. Say what it counts. Same wording the Visitor Management screen
|
|
2960
|
+
// already uses for the same filter ("Not Checked Out").
|
|
2961
|
+
if (metric.currentlyOnSite !== undefined && metric.currentlyOnSite !== null)
|
|
2957
2962
|
parts.push(
|
|
2958
|
-
`${formatNumber(toNumber(metric.currentlyOnSite))}
|
|
2963
|
+
`${formatNumber(toNumber(metric.currentlyOnSite))} not checked out (all time)`
|
|
2959
2964
|
);
|
|
2960
2965
|
if (
|
|
2961
2966
|
typeof (metric as any).missedCheckpoints !== "undefined" ||
|
|
@@ -3251,34 +3256,18 @@ function getInitials(name?: string) {
|
|
|
3251
3256
|
|
|
3252
3257
|
// ─── Status Style Helpers ────────────────────────────────────────────────────
|
|
3253
3258
|
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
error: { backgroundColor: "#fecaca", color: "#b91c1c" },
|
|
3260
|
-
neutral: { backgroundColor: "#e2e8f0", color: "#64748b" },
|
|
3261
|
-
};
|
|
3262
|
-
return map[tone] || map.neutral;
|
|
3263
|
-
}
|
|
3264
|
-
|
|
3259
|
+
/**
|
|
3260
|
+
* The pills are drawn by inline style, so the literal colours lived here rather
|
|
3261
|
+
* than in the stylesheet - which is why they survived the dark-mode repair and
|
|
3262
|
+
* still measured 3.86:1. Same three cases, same words, the design's tones.
|
|
3263
|
+
*/
|
|
3265
3264
|
function getPatrolStatusStyle(status: any) {
|
|
3266
3265
|
const s = String(status || "").toLowerCase();
|
|
3267
3266
|
if (s === "completed" || s === "complete")
|
|
3268
|
-
return { backgroundColor: "
|
|
3267
|
+
return { backgroundColor: "var(--ok-bg)", color: "var(--ok)" };
|
|
3269
3268
|
if (s === "on paused" || s === "paused")
|
|
3270
|
-
return { backgroundColor: "
|
|
3271
|
-
return { backgroundColor: "
|
|
3272
|
-
}
|
|
3273
|
-
|
|
3274
|
-
function getStaffStatusPillStyle(status: any) {
|
|
3275
|
-
const s = String(status || "").toLowerCase();
|
|
3276
|
-
if (s === "on duty" || s === "on-duty")
|
|
3277
|
-
return { backgroundColor: "#72e3a1", color: "#0f5132" };
|
|
3278
|
-
if (s === "on break" || s === "on-break")
|
|
3279
|
-
return { backgroundColor: "#fde68a", color: "#92400e" };
|
|
3280
|
-
if (s === "absent") return { backgroundColor: "#fecaca", color: "#b91c1c" };
|
|
3281
|
-
return { backgroundColor: "#e2e8f0", color: "#64748b" };
|
|
3269
|
+
return { backgroundColor: "var(--hover)", color: "var(--text2)" };
|
|
3270
|
+
return { backgroundColor: "var(--hover)", color: "var(--text2)" };
|
|
3282
3271
|
}
|
|
3283
3272
|
|
|
3284
3273
|
function getStatusProgressStyle(item: { percent: number; color: string }) {
|
|
@@ -3508,6 +3497,9 @@ function resetCustomizeDraft() {
|
|
|
3508
3497
|
|
|
3509
3498
|
function submitCustomize() {
|
|
3510
3499
|
visibleWidgetKeys.value = [...draftVisibleWidgetKeys.value];
|
|
3500
|
+
// The write is the whole point - see `widgetPrefsCookie`. Saving on Submit
|
|
3501
|
+
// rather than on every switch keeps Cancel meaning cancel.
|
|
3502
|
+
widgetPrefsCookie.value = [...draftVisibleWidgetKeys.value];
|
|
3511
3503
|
widgetSearch.value = "";
|
|
3512
3504
|
customizeDialog.value = false;
|
|
3513
3505
|
}
|
|
@@ -3607,6 +3599,13 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
3607
3599
|
<body>
|
|
3608
3600
|
<h1>${props.title || dashboardTitle.value}</h1>
|
|
3609
3601
|
<div class="subtitle">Site: ${siteName} | Period: ${periodLabel}</div>
|
|
3602
|
+
${
|
|
3603
|
+
failedWidgetLabels.value.length
|
|
3604
|
+
? `<div class="subtitle" style="color:#b73535;">Incomplete report: ${failedWidgetLabels.value.join(
|
|
3605
|
+
", "
|
|
3606
|
+
)} could not be loaded. Any figure shown as — is missing, not zero.</div>`
|
|
3607
|
+
: ""
|
|
3608
|
+
}
|
|
3610
3609
|
`;
|
|
3611
3610
|
|
|
3612
3611
|
if (visibleKpiCards.value && visibleKpiCards.value.length > 0) {
|
|
@@ -3619,6 +3618,9 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
3619
3618
|
let kpiValueHtml = `<div class="kpi-value">${card.value}</div>`;
|
|
3620
3619
|
if (card.key === "supplyAlert" && Array.isArray(card.raw?.items)) {
|
|
3621
3620
|
kpiValueHtml = '<div style="font-size: 9pt; margin-top: 4px;">';
|
|
3621
|
+
if (!card.raw.items.length) {
|
|
3622
|
+
kpiValueHtml += "<div>No supplies recorded</div>";
|
|
3623
|
+
}
|
|
3622
3624
|
card.raw.items.forEach((item: any) => {
|
|
3623
3625
|
kpiValueHtml += `<div><strong>${item.name}</strong>: Balance ${item.qty}</div>`;
|
|
3624
3626
|
});
|
|
@@ -3735,7 +3737,7 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
3735
3737
|
}
|
|
3736
3738
|
wordHtml += `</tbody></table>`;
|
|
3737
3739
|
|
|
3738
|
-
wordHtml += `<div class="section-title">
|
|
3740
|
+
wordHtml += `<div class="section-title">Needs Attention</div>`;
|
|
3739
3741
|
wordHtml += `<table class="data-table">`;
|
|
3740
3742
|
wordHtml += `<thead><tr><th>Attention</th><th>Details / Subtitle</th></tr></thead><tbody>`;
|
|
3741
3743
|
if (todayAttentions.value.length > 0) {
|
|
@@ -3860,18 +3862,33 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
3860
3862
|
|
|
3861
3863
|
// ─── Card Color Helpers ───────────────────────────────────────────────────────
|
|
3862
3864
|
|
|
3865
|
+
/** The rotation an unstyled `cardValues` entry falls into - same four tones. */
|
|
3866
|
+
const KPI_ROTATION = [
|
|
3867
|
+
KPI_TONES.info,
|
|
3868
|
+
KPI_TONES.err,
|
|
3869
|
+
KPI_TONES.ok,
|
|
3870
|
+
KPI_TONES.warn,
|
|
3871
|
+
] as const;
|
|
3872
|
+
|
|
3863
3873
|
function getCardColor(index: number) {
|
|
3864
|
-
return [
|
|
3874
|
+
return KPI_ROTATION[index % KPI_ROTATION.length].color;
|
|
3865
3875
|
}
|
|
3866
3876
|
|
|
3877
|
+
/**
|
|
3878
|
+
* A tint for whatever colour the caller gave. `props.cardValues` comes from
|
|
3879
|
+
* eleven applications and still arrives as a raw hex in places, so the hex
|
|
3880
|
+
* matching stays - the token names are matched alongside it, not instead.
|
|
3881
|
+
*/
|
|
3867
3882
|
function getCardTint(color: string, index: number) {
|
|
3868
|
-
|
|
3869
|
-
if (!color) return tints[index % tints.length];
|
|
3883
|
+
if (!color) return KPI_ROTATION[index % KPI_ROTATION.length].tint;
|
|
3870
3884
|
const n = color.toLowerCase();
|
|
3871
|
-
if (n.includes("
|
|
3872
|
-
|
|
3873
|
-
if (n.includes("
|
|
3874
|
-
|
|
3885
|
+
if (n.includes("--err") || n.includes("f44336") || n.includes("d32f2f"))
|
|
3886
|
+
return KPI_TONES.err.tint;
|
|
3887
|
+
if (n.includes("--ok") || n.includes("4caf50") || n.includes("43a047"))
|
|
3888
|
+
return KPI_TONES.ok.tint;
|
|
3889
|
+
if (n.includes("--warn") || n.includes("ff9800") || n.includes("ffa726"))
|
|
3890
|
+
return KPI_TONES.warn.tint;
|
|
3891
|
+
return KPI_TONES.info.tint;
|
|
3875
3892
|
}
|
|
3876
3893
|
|
|
3877
3894
|
// ─── Generic Helpers ─────────────────────────────────────────────────────────
|
|
@@ -3909,6 +3926,21 @@ function toNumber(value: any) {
|
|
|
3909
3926
|
return Number.isFinite(n) ? n : 0;
|
|
3910
3927
|
}
|
|
3911
3928
|
|
|
3929
|
+
/**
|
|
3930
|
+
* A figure the server may report as "not measured".
|
|
3931
|
+
*
|
|
3932
|
+
* `toNumber()` turns `null` into `0`, which is wrong for anything the API
|
|
3933
|
+
* deliberately sends as `null` - a trend with no prior period to compare
|
|
3934
|
+
* against, a compliance rate with no checkpoint due, a severity the incident
|
|
3935
|
+
* form never records. Those must read as absent, not as zero. `NaN` is what
|
|
3936
|
+
* the tile already treats as "no pill" / "no figure".
|
|
3937
|
+
*/
|
|
3938
|
+
function toNumberOrNaN(value: any) {
|
|
3939
|
+
if (value === null || value === undefined || value === "") return NaN;
|
|
3940
|
+
const n = Number(value);
|
|
3941
|
+
return Number.isFinite(n) ? n : NaN;
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3912
3944
|
function normalizeStatus(value: any) {
|
|
3913
3945
|
const text = String(value || "").trim();
|
|
3914
3946
|
if (!text) return "To-Do";
|
|
@@ -4158,6 +4190,29 @@ function formatDashboardTime(value?: string) {
|
|
|
4158
4190
|
text-transform: capitalize;
|
|
4159
4191
|
}
|
|
4160
4192
|
|
|
4193
|
+
/* THESE FIVE ARE LITERAL ON PURPOSE - THE PRINT VIEW IS PAPER.
|
|
4194
|
+
*
|
|
4195
|
+
* `.print-dashboard-view` above pins its surface to `#ffffff` and its ink to
|
|
4196
|
+
* `#1e293b` in BOTH themes, because a printed page is white however the app is
|
|
4197
|
+
* themed. Putting the status tokens on these badges therefore did the opposite
|
|
4198
|
+
* of what tokenising usually does: it let the SCREEN theme reach a surface that
|
|
4199
|
+
* does not follow it. Measured on the print branch at 1440:
|
|
4200
|
+
*
|
|
4201
|
+
* light In Progress rgb(59,111,212) on rgb(235,241,251) 4.20:1
|
|
4202
|
+
* light Completed rgb(15,138,98) on rgb(231,243,239) 3.82:1
|
|
4203
|
+
* dark In Progress rgb(125,166,236) on rgb(239,244,253) 2.23:1
|
|
4204
|
+
* dark Completed rgb(65,199,149) on rgb(232,248,242) 1.93:1
|
|
4205
|
+
*
|
|
4206
|
+
* The dark rows are the defect: the dark-theme INK composited over the white
|
|
4207
|
+
* paper the print view forces, so anyone who exports the dashboard while the
|
|
4208
|
+
* app is in dark mode gets washed-out badges - 1.93:1, the lowest reading taken
|
|
4209
|
+
* on this component. So these go back to fixed pastels, and specifically to the
|
|
4210
|
+
* SAME five pairs the Word export writes into its own stylesheet further up, so
|
|
4211
|
+
* the on-page print view and the exported document finally match.
|
|
4212
|
+
*
|
|
4213
|
+
* Same rule as the signature pad: a surface that does not follow the theme must
|
|
4214
|
+
* not be painted with tokens that do.
|
|
4215
|
+
*/
|
|
4161
4216
|
.badge-success {
|
|
4162
4217
|
background-color: #d1fae5;
|
|
4163
4218
|
color: #065f46;
|
|
@@ -4184,11 +4239,70 @@ function formatDashboardTime(value?: string) {
|
|
|
4184
4239
|
color: #475569;
|
|
4185
4240
|
}
|
|
4186
4241
|
|
|
4242
|
+
/* ------------------------------------------------------------------ */
|
|
4243
|
+
/* PHASE 5 - the design's card, type and semantic tones. */
|
|
4244
|
+
/* */
|
|
4245
|
+
/* The dark-mode repair earlier today moved these surfaces off literal */
|
|
4246
|
+
/* white onto the Vuetify theme; this keeps that and takes the last */
|
|
4247
|
+
/* step, onto the design tokens themselves - so the dashboard's cards, */
|
|
4248
|
+
/* headings and pills are the same 14px card, the same Manrope and the */
|
|
4249
|
+
/* same four status tones as every other screen. */
|
|
4250
|
+
/* ------------------------------------------------------------------ */
|
|
4251
|
+
|
|
4187
4252
|
.dashboard-card {
|
|
4188
|
-
background:
|
|
4189
|
-
border: 1px solid
|
|
4190
|
-
border-radius:
|
|
4191
|
-
box-shadow:
|
|
4253
|
+
background: var(--card);
|
|
4254
|
+
border: 1px solid var(--border);
|
|
4255
|
+
border-radius: var(--r-card);
|
|
4256
|
+
box-shadow: var(--shadow);
|
|
4257
|
+
font-family: var(--font-sans);
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
/* The KPI tile and the rows inside a panel are the same card, one step in. */
|
|
4261
|
+
.dashboard-kpi-card,
|
|
4262
|
+
.dashboard-row-card {
|
|
4263
|
+
background: var(--card);
|
|
4264
|
+
border: 1px solid var(--border);
|
|
4265
|
+
border-radius: var(--r-card);
|
|
4266
|
+
box-shadow: var(--shadow);
|
|
4267
|
+
font-family: var(--font-sans);
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
.dashboard-title {
|
|
4271
|
+
font-family: var(--font-sans);
|
|
4272
|
+
font-size: var(--fs-h1);
|
|
4273
|
+
font-weight: var(--fw-h1);
|
|
4274
|
+
letter-spacing: var(--ls-h1);
|
|
4275
|
+
color: var(--text);
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4278
|
+
.dashboard-card-title {
|
|
4279
|
+
font-family: var(--font-sans);
|
|
4280
|
+
font-size: var(--fs-card-title);
|
|
4281
|
+
font-weight: var(--fw-card-title);
|
|
4282
|
+
letter-spacing: 0;
|
|
4283
|
+
color: var(--text);
|
|
4284
|
+
}
|
|
4285
|
+
|
|
4286
|
+
/* 11px/800 uppercase over a 30px/800 number, tabular so the columns of
|
|
4287
|
+
figures line up - the design's KPI, and the same header type the table
|
|
4288
|
+
card already uses. */
|
|
4289
|
+
.dashboard-kpi-label {
|
|
4290
|
+
font-family: var(--font-sans);
|
|
4291
|
+
font-size: var(--fs-table-head);
|
|
4292
|
+
font-weight: var(--fw-table-head);
|
|
4293
|
+
letter-spacing: var(--ls-table-head);
|
|
4294
|
+
text-transform: uppercase;
|
|
4295
|
+
color: var(--muted);
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4298
|
+
.dashboard-kpi-value {
|
|
4299
|
+
font-family: var(--font-sans);
|
|
4300
|
+
font-size: var(--fs-kpi);
|
|
4301
|
+
font-weight: var(--fw-kpi);
|
|
4302
|
+
letter-spacing: var(--ls-kpi);
|
|
4303
|
+
line-height: 1.1;
|
|
4304
|
+
color: var(--text);
|
|
4305
|
+
font-variant-numeric: tabular-nums;
|
|
4192
4306
|
}
|
|
4193
4307
|
|
|
4194
4308
|
.dashboard-section-grid {
|
|
@@ -4209,7 +4323,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4209
4323
|
}
|
|
4210
4324
|
|
|
4211
4325
|
.figma-panel {
|
|
4212
|
-
border-color:
|
|
4326
|
+
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
|
4213
4327
|
box-shadow: none;
|
|
4214
4328
|
min-height: 276px;
|
|
4215
4329
|
padding: 26px 28px 24px;
|
|
@@ -4223,14 +4337,17 @@ function formatDashboardTime(value?: string) {
|
|
|
4223
4337
|
min-height: 18px;
|
|
4224
4338
|
}
|
|
4225
4339
|
|
|
4340
|
+
/* The panel heading is the design's card title: 15.5px/800, sentence case. It
|
|
4341
|
+
was a 12px/900 uppercase label from the earlier Figma. */
|
|
4226
4342
|
.figma-panel-header h2 {
|
|
4227
|
-
color:
|
|
4228
|
-
font-
|
|
4229
|
-
font-
|
|
4343
|
+
color: var(--text);
|
|
4344
|
+
font-family: var(--font-sans);
|
|
4345
|
+
font-size: var(--fs-card-title);
|
|
4346
|
+
font-weight: var(--fw-card-title);
|
|
4230
4347
|
letter-spacing: 0;
|
|
4231
4348
|
line-height: 1.2;
|
|
4232
4349
|
margin: 0;
|
|
4233
|
-
text-transform:
|
|
4350
|
+
text-transform: none;
|
|
4234
4351
|
}
|
|
4235
4352
|
|
|
4236
4353
|
.figma-panel-action,
|
|
@@ -4238,7 +4355,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4238
4355
|
appearance: none;
|
|
4239
4356
|
background: transparent;
|
|
4240
4357
|
border: 0;
|
|
4241
|
-
color:
|
|
4358
|
+
color: rgb(var(--v-theme-primary));
|
|
4242
4359
|
cursor: pointer;
|
|
4243
4360
|
font-size: 12px;
|
|
4244
4361
|
font-weight: 800;
|
|
@@ -4248,7 +4365,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4248
4365
|
}
|
|
4249
4366
|
|
|
4250
4367
|
.facility-booking-modal {
|
|
4251
|
-
border: 1px solid
|
|
4368
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
4252
4369
|
box-shadow: 0 18px 48px rgba(15, 35, 54, 0.18);
|
|
4253
4370
|
}
|
|
4254
4371
|
|
|
@@ -4269,7 +4386,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4269
4386
|
}
|
|
4270
4387
|
|
|
4271
4388
|
.facility-booking-summary-label {
|
|
4272
|
-
color:
|
|
4389
|
+
color: rgb(var(--v-theme-primary));
|
|
4273
4390
|
font-size: 16px;
|
|
4274
4391
|
font-weight: 500;
|
|
4275
4392
|
line-height: 1.25;
|
|
@@ -4289,7 +4406,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4289
4406
|
}
|
|
4290
4407
|
|
|
4291
4408
|
.facility-booking-summary-dots {
|
|
4292
|
-
border-bottom: 2px dotted
|
|
4409
|
+
border-bottom: 2px dotted rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
4293
4410
|
display: block;
|
|
4294
4411
|
height: 1px;
|
|
4295
4412
|
min-width: 32px;
|
|
@@ -4297,7 +4414,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4297
4414
|
|
|
4298
4415
|
.facility-booking-summary-value,
|
|
4299
4416
|
.facility-booking-total-label {
|
|
4300
|
-
color:
|
|
4417
|
+
color: rgb(var(--v-theme-on-surface));
|
|
4301
4418
|
font-size: 16px;
|
|
4302
4419
|
font-weight: 800;
|
|
4303
4420
|
line-height: 1.25;
|
|
@@ -4320,8 +4437,8 @@ function formatDashboardTime(value?: string) {
|
|
|
4320
4437
|
|
|
4321
4438
|
.figma-list-row {
|
|
4322
4439
|
align-items: center;
|
|
4323
|
-
background:
|
|
4324
|
-
border: 1px solid
|
|
4440
|
+
background: rgb(var(--v-theme-surface-bright));
|
|
4441
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
4325
4442
|
border-radius: 6px;
|
|
4326
4443
|
display: grid;
|
|
4327
4444
|
gap: 10px;
|
|
@@ -4332,7 +4449,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4332
4449
|
}
|
|
4333
4450
|
|
|
4334
4451
|
.figma-list-row:hover {
|
|
4335
|
-
border-color:
|
|
4452
|
+
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
|
4336
4453
|
box-shadow: 0 6px 14px rgba(15, 44, 69, 0.05);
|
|
4337
4454
|
}
|
|
4338
4455
|
|
|
@@ -4341,7 +4458,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4341
4458
|
}
|
|
4342
4459
|
|
|
4343
4460
|
.figma-list-main strong {
|
|
4344
|
-
color:
|
|
4461
|
+
color: rgb(var(--v-theme-on-surface));
|
|
4345
4462
|
display: block;
|
|
4346
4463
|
font-size: 12px;
|
|
4347
4464
|
font-weight: 800;
|
|
@@ -4352,7 +4469,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4352
4469
|
}
|
|
4353
4470
|
|
|
4354
4471
|
.figma-list-main p {
|
|
4355
|
-
color:
|
|
4472
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
4356
4473
|
font-size: 11px;
|
|
4357
4474
|
font-weight: 700;
|
|
4358
4475
|
line-height: 1.3;
|
|
@@ -4366,6 +4483,61 @@ function formatDashboardTime(value?: string) {
|
|
|
4366
4483
|
justify-self: end;
|
|
4367
4484
|
}
|
|
4368
4485
|
|
|
4486
|
+
/* The service-vertical and security rows carry two more cells than the
|
|
4487
|
+
property rows do - who it is assigned to, and the status. Same row, four
|
|
4488
|
+
columns instead of three; there is no second row style. */
|
|
4489
|
+
.figma-list-row--wide {
|
|
4490
|
+
grid-template-columns:
|
|
4491
|
+
minmax(0, 1fr) minmax(0, auto) auto minmax(38px, auto);
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
.figma-list-person {
|
|
4495
|
+
align-items: center;
|
|
4496
|
+
color: var(--muted);
|
|
4497
|
+
display: flex;
|
|
4498
|
+
font-family: var(--font-sans);
|
|
4499
|
+
font-size: 11px;
|
|
4500
|
+
font-weight: 700;
|
|
4501
|
+
gap: 8px;
|
|
4502
|
+
min-width: 0;
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4505
|
+
.figma-list-person span {
|
|
4506
|
+
overflow: hidden;
|
|
4507
|
+
text-overflow: ellipsis;
|
|
4508
|
+
white-space: nowrap;
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
/* A figure, not a status - the neutral pill, so a number is never coloured as
|
|
4512
|
+
if it were a word `utils/status.ts` recognised. */
|
|
4513
|
+
/* The same pill StatusChip draws for `tone-neutral` - `--hover` on `--text2`,
|
|
4514
|
+
3px/10px, `--r-pill` - reproduced rather than reused because StatusChip's
|
|
4515
|
+
rules are scoped to its own file. */
|
|
4516
|
+
.figma-row-figure {
|
|
4517
|
+
background: var(--hover);
|
|
4518
|
+
border-radius: var(--r-pill);
|
|
4519
|
+
color: var(--text2);
|
|
4520
|
+
font-family: var(--font-sans);
|
|
4521
|
+
font-size: var(--fs-chip);
|
|
4522
|
+
font-weight: var(--fw-chip);
|
|
4523
|
+
justify-self: end;
|
|
4524
|
+
line-height: 1.4;
|
|
4525
|
+
padding: 3px 10px;
|
|
4526
|
+
white-space: nowrap;
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
/* Below the design's tablet break the four columns will not hold a readable
|
|
4530
|
+
title, so the row stacks - the same thing the property rows do. */
|
|
4531
|
+
@media (max-width: 700px) {
|
|
4532
|
+
.figma-list-row--wide {
|
|
4533
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
.figma-list-row--wide .figma-list-main {
|
|
4537
|
+
grid-column: 1 / -1;
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4369
4541
|
.figma-event-dot {
|
|
4370
4542
|
align-self: center;
|
|
4371
4543
|
border-radius: 50%;
|
|
@@ -4377,30 +4549,30 @@ function formatDashboardTime(value?: string) {
|
|
|
4377
4549
|
|
|
4378
4550
|
.figma-event-dot-info,
|
|
4379
4551
|
.figma-event-dot-neutral {
|
|
4380
|
-
background:
|
|
4381
|
-
border: 2px solid
|
|
4552
|
+
background: var(--info-bg);
|
|
4553
|
+
border: 2px solid var(--info);
|
|
4382
4554
|
}
|
|
4383
4555
|
|
|
4384
4556
|
.figma-event-dot-success {
|
|
4385
|
-
background:
|
|
4386
|
-
border: 2px solid
|
|
4557
|
+
background: var(--ok-bg);
|
|
4558
|
+
border: 2px solid var(--ok);
|
|
4387
4559
|
}
|
|
4388
4560
|
|
|
4389
4561
|
.figma-event-dot-danger {
|
|
4390
|
-
background:
|
|
4391
|
-
border: 2px solid
|
|
4562
|
+
background: var(--err-bg);
|
|
4563
|
+
border: 2px solid var(--err);
|
|
4392
4564
|
}
|
|
4393
4565
|
|
|
4394
4566
|
.figma-event-dot-warning {
|
|
4395
|
-
background:
|
|
4396
|
-
border: 2px solid
|
|
4567
|
+
background: var(--warn-bg);
|
|
4568
|
+
border: 2px solid var(--warn);
|
|
4397
4569
|
}
|
|
4398
4570
|
|
|
4399
4571
|
.active-patrol-row {
|
|
4400
4572
|
align-items: center;
|
|
4401
|
-
background-color:
|
|
4402
|
-
border: 1px solid
|
|
4403
|
-
border-radius:
|
|
4573
|
+
background-color: var(--card);
|
|
4574
|
+
border: 1px solid var(--border);
|
|
4575
|
+
border-radius: var(--r-inner-lg);
|
|
4404
4576
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
|
4405
4577
|
display: grid;
|
|
4406
4578
|
gap: 10px;
|
|
@@ -4416,7 +4588,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4416
4588
|
}
|
|
4417
4589
|
|
|
4418
4590
|
.active-patrol-main strong {
|
|
4419
|
-
color:
|
|
4591
|
+
color: rgb(var(--v-theme-on-surface));
|
|
4420
4592
|
display: block;
|
|
4421
4593
|
font-size: 13px;
|
|
4422
4594
|
font-weight: 700;
|
|
@@ -4426,7 +4598,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4426
4598
|
}
|
|
4427
4599
|
|
|
4428
4600
|
.active-patrol-main span {
|
|
4429
|
-
color:
|
|
4601
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
4430
4602
|
display: block;
|
|
4431
4603
|
font-size: 11px;
|
|
4432
4604
|
line-height: 1.25;
|
|
@@ -4442,19 +4614,37 @@ function formatDashboardTime(value?: string) {
|
|
|
4442
4614
|
|
|
4443
4615
|
.active-patrol-assignee {
|
|
4444
4616
|
align-items: center;
|
|
4445
|
-
background:
|
|
4446
|
-
border: 1px solid
|
|
4617
|
+
background: rgba(var(--v-theme-on-surface), 0.05);
|
|
4618
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
4447
4619
|
border-radius: 999px;
|
|
4448
4620
|
display: inline-flex;
|
|
4449
|
-
|
|
4621
|
+
/* `1 1 auto` let the chip GROW to fill the column, so one assignee drew as a
|
|
4622
|
+
full-width bordered box that reads as an empty text input. It is a chip -
|
|
4623
|
+
it shrinks, it does not grow. */
|
|
4624
|
+
flex: 0 1 auto;
|
|
4450
4625
|
gap: 6px;
|
|
4451
4626
|
max-width: 100%;
|
|
4452
4627
|
min-width: 0;
|
|
4453
4628
|
padding: 2px 8px 2px 3px;
|
|
4454
4629
|
}
|
|
4455
4630
|
|
|
4631
|
+
/**
|
|
4632
|
+
* Carried from Phase 1: `AvatarMain` colours each person from their NAME, and
|
|
4633
|
+
* writes WHITE initials on whatever tint it picks - 2.34:1 on this dashboard.
|
|
4634
|
+
* That is right in a members list, where the colour tells two people apart,
|
|
4635
|
+
* and wrong on a dashboard, where the same face appears in four panels.
|
|
4636
|
+
*
|
|
4637
|
+
* The component is left alone - Phase 2 took exactly this route for the top
|
|
4638
|
+
* bar's own avatar - and the dashboard paints every avatar it draws with the
|
|
4639
|
+
* design's accent pair instead. One rule, all seven call sites.
|
|
4640
|
+
*/
|
|
4641
|
+
.unified-dashboard :deep(.v-avatar) {
|
|
4642
|
+
background: var(--accent-soft) !important;
|
|
4643
|
+
color: var(--accent-text) !important;
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4456
4646
|
.active-patrol-assignee span {
|
|
4457
|
-
color:
|
|
4647
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
4458
4648
|
font-size: 11px;
|
|
4459
4649
|
font-weight: 600;
|
|
4460
4650
|
line-height: 1.2;
|
|
@@ -4484,10 +4674,10 @@ function formatDashboardTime(value?: string) {
|
|
|
4484
4674
|
|
|
4485
4675
|
.active-patrol-more-pill {
|
|
4486
4676
|
align-items: center;
|
|
4487
|
-
background:
|
|
4488
|
-
border: 1px solid
|
|
4677
|
+
background: rgba(var(--v-theme-on-surface), 0.05);
|
|
4678
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
4489
4679
|
border-radius: 999px;
|
|
4490
|
-
color:
|
|
4680
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
4491
4681
|
display: inline-flex;
|
|
4492
4682
|
flex: 0 0 auto;
|
|
4493
4683
|
font-size: 11px;
|
|
@@ -4507,11 +4697,11 @@ function formatDashboardTime(value?: string) {
|
|
|
4507
4697
|
}
|
|
4508
4698
|
|
|
4509
4699
|
.active-patrol-tooltip::after {
|
|
4510
|
-
background:
|
|
4511
|
-
border-radius:
|
|
4700
|
+
background: var(--text);
|
|
4701
|
+
border-radius: var(--r-inner-sm);
|
|
4512
4702
|
bottom: calc(100% + 8px);
|
|
4513
|
-
box-shadow:
|
|
4514
|
-
color:
|
|
4703
|
+
box-shadow: var(--shadow-menu);
|
|
4704
|
+
color: var(--card);
|
|
4515
4705
|
content: attr(data-tooltip);
|
|
4516
4706
|
font-size: 11px;
|
|
4517
4707
|
font-weight: 600;
|
|
@@ -4541,6 +4731,28 @@ function formatDashboardTime(value?: string) {
|
|
|
4541
4731
|
}
|
|
4542
4732
|
}
|
|
4543
4733
|
|
|
4734
|
+
/**
|
|
4735
|
+
* Carried from Phase 1: the dashboard scrolled sideways at 360. This row is a
|
|
4736
|
+
* four-column grid whose last two columns are `max-content`, so on a phone the
|
|
4737
|
+
* assignee pill, the status pill and the action button cannot fit beside a
|
|
4738
|
+
* 120px title and the row pushes ~39px past the viewport.
|
|
4739
|
+
*
|
|
4740
|
+
* Below 600 the four columns become one. Nothing is hidden, dropped or
|
|
4741
|
+
* reordered - the same four blocks, stacked in the same order.
|
|
4742
|
+
*/
|
|
4743
|
+
@media (max-width: 599.98px) {
|
|
4744
|
+
.active-patrol-row {
|
|
4745
|
+
grid-template-columns: minmax(0, 1fr);
|
|
4746
|
+
}
|
|
4747
|
+
|
|
4748
|
+
.active-patrol-action,
|
|
4749
|
+
.active-patrol-statuses,
|
|
4750
|
+
.active-patrol-assignees {
|
|
4751
|
+
justify-self: start;
|
|
4752
|
+
text-align: left;
|
|
4753
|
+
}
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4544
4756
|
.figma-icon-badge {
|
|
4545
4757
|
align-items: center;
|
|
4546
4758
|
border-radius: 50%;
|
|
@@ -4552,28 +4764,28 @@ function formatDashboardTime(value?: string) {
|
|
|
4552
4764
|
}
|
|
4553
4765
|
|
|
4554
4766
|
.figma-icon-badge-danger {
|
|
4555
|
-
background:
|
|
4556
|
-
color:
|
|
4767
|
+
background: var(--err-bg);
|
|
4768
|
+
color: var(--err);
|
|
4557
4769
|
}
|
|
4558
4770
|
|
|
4559
4771
|
.figma-icon-badge-info {
|
|
4560
|
-
background:
|
|
4561
|
-
color:
|
|
4772
|
+
background: var(--info-bg);
|
|
4773
|
+
color: var(--info);
|
|
4562
4774
|
}
|
|
4563
4775
|
|
|
4564
4776
|
.figma-icon-badge-success {
|
|
4565
|
-
background:
|
|
4566
|
-
color:
|
|
4777
|
+
background: var(--ok-bg);
|
|
4778
|
+
color: var(--ok);
|
|
4567
4779
|
}
|
|
4568
4780
|
|
|
4569
4781
|
.figma-icon-badge-warning {
|
|
4570
|
-
background:
|
|
4571
|
-
color:
|
|
4782
|
+
background: var(--warn-bg);
|
|
4783
|
+
color: var(--warn);
|
|
4572
4784
|
}
|
|
4573
4785
|
|
|
4574
4786
|
.figma-icon-badge-neutral {
|
|
4575
|
-
background:
|
|
4576
|
-
color:
|
|
4787
|
+
background: var(--hover);
|
|
4788
|
+
color: var(--text2);
|
|
4577
4789
|
}
|
|
4578
4790
|
|
|
4579
4791
|
.figma-status-stack {
|
|
@@ -4587,7 +4799,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4587
4799
|
|
|
4588
4800
|
.figma-status-label {
|
|
4589
4801
|
align-items: center;
|
|
4590
|
-
color:
|
|
4802
|
+
color: rgb(var(--v-theme-on-surface));
|
|
4591
4803
|
display: flex;
|
|
4592
4804
|
font-size: 12px;
|
|
4593
4805
|
font-weight: 800;
|
|
@@ -4597,13 +4809,13 @@ function formatDashboardTime(value?: string) {
|
|
|
4597
4809
|
}
|
|
4598
4810
|
|
|
4599
4811
|
.figma-status-label strong {
|
|
4600
|
-
color:
|
|
4812
|
+
color: rgb(var(--v-theme-on-surface));
|
|
4601
4813
|
font-size: 12px;
|
|
4602
4814
|
font-weight: 900;
|
|
4603
4815
|
}
|
|
4604
4816
|
|
|
4605
4817
|
.figma-progress-track {
|
|
4606
|
-
background:
|
|
4818
|
+
background: rgba(var(--v-theme-on-surface), 0.08);
|
|
4607
4819
|
border-radius: 999px;
|
|
4608
4820
|
height: 6px;
|
|
4609
4821
|
overflow: hidden;
|
|
@@ -4624,9 +4836,9 @@ function formatDashboardTime(value?: string) {
|
|
|
4624
4836
|
|
|
4625
4837
|
.feedback-row {
|
|
4626
4838
|
align-items: center;
|
|
4627
|
-
background:
|
|
4628
|
-
border: 1px solid
|
|
4629
|
-
border-radius:
|
|
4839
|
+
background: var(--card);
|
|
4840
|
+
border: 1px solid var(--border);
|
|
4841
|
+
border-radius: var(--r-inner-lg);
|
|
4630
4842
|
display: grid;
|
|
4631
4843
|
gap: 16px;
|
|
4632
4844
|
grid-template-columns: minmax(180px, 225px) minmax(130px, 175px) 100px auto;
|
|
@@ -4635,8 +4847,41 @@ function formatDashboardTime(value?: string) {
|
|
|
4635
4847
|
transition: border-color 0.16s ease, box-shadow 0.16s ease;
|
|
4636
4848
|
}
|
|
4637
4849
|
|
|
4850
|
+
/**
|
|
4851
|
+
* The property-mode feedback row is four flex children that each ask for at
|
|
4852
|
+
* least 140px, so on a 360px phone the status pill and the View button were
|
|
4853
|
+
* clipped by the card edge - visible, but cut in half. It wraps now, and below
|
|
4854
|
+
* 600 each block takes the full width: same four blocks, same order.
|
|
4855
|
+
*/
|
|
4856
|
+
.dashboard-feedback-row {
|
|
4857
|
+
flex-wrap: wrap;
|
|
4858
|
+
gap: 12px;
|
|
4859
|
+
}
|
|
4860
|
+
|
|
4861
|
+
@media (max-width: 599.98px) {
|
|
4862
|
+
.dashboard-feedback-row > div {
|
|
4863
|
+
flex: 1 1 100% !important;
|
|
4864
|
+
justify-content: flex-start !important;
|
|
4865
|
+
text-align: left !important;
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
/* Same story as the patrol row: four fixed-ish columns need ~600px, so on a
|
|
4870
|
+
phone the status pill and the View button were clipped by the card edge.
|
|
4871
|
+
One column below 600, same blocks, same order. */
|
|
4872
|
+
@media (max-width: 599.98px) {
|
|
4873
|
+
.feedback-row {
|
|
4874
|
+
grid-template-columns: minmax(0, 1fr);
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4877
|
+
.feedback-status-pill,
|
|
4878
|
+
.feedback-action {
|
|
4879
|
+
justify-self: start;
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4638
4883
|
.feedback-row:hover {
|
|
4639
|
-
border-color:
|
|
4884
|
+
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
|
4640
4885
|
box-shadow: 0 6px 14px rgba(15, 44, 69, 0.04);
|
|
4641
4886
|
}
|
|
4642
4887
|
|
|
@@ -4645,7 +4890,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4645
4890
|
}
|
|
4646
4891
|
|
|
4647
4892
|
.feedback-summary strong {
|
|
4648
|
-
color:
|
|
4893
|
+
color: rgb(var(--v-theme-on-surface));
|
|
4649
4894
|
display: block;
|
|
4650
4895
|
font-size: 12px;
|
|
4651
4896
|
font-weight: 800;
|
|
@@ -4656,7 +4901,7 @@ function formatDashboardTime(value?: string) {
|
|
|
4656
4901
|
}
|
|
4657
4902
|
|
|
4658
4903
|
.feedback-summary p {
|
|
4659
|
-
color:
|
|
4904
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
4660
4905
|
font-size: 12px;
|
|
4661
4906
|
font-weight: 500;
|
|
4662
4907
|
line-height: 1.35;
|
|
@@ -4679,11 +4924,13 @@ function formatDashboardTime(value?: string) {
|
|
|
4679
4924
|
white-space: nowrap;
|
|
4680
4925
|
}
|
|
4681
4926
|
|
|
4927
|
+
/* Carried from Phase 1: the initials measured 1.26:1 in dark mode, because the
|
|
4928
|
+
tint was a literal light blue whatever the theme. */
|
|
4682
4929
|
.feedback-avatar {
|
|
4683
4930
|
align-items: center;
|
|
4684
|
-
background:
|
|
4931
|
+
background: var(--accent-soft);
|
|
4685
4932
|
border-radius: 50%;
|
|
4686
|
-
color:
|
|
4933
|
+
color: var(--accent-text);
|
|
4687
4934
|
display: inline-flex;
|
|
4688
4935
|
flex-shrink: 0;
|
|
4689
4936
|
font-size: 11px;
|
|
@@ -4702,31 +4949,32 @@ function formatDashboardTime(value?: string) {
|
|
|
4702
4949
|
white-space: nowrap;
|
|
4703
4950
|
}
|
|
4704
4951
|
|
|
4952
|
+
/* The other two carried Phase 1 items: hard-coded chips. */
|
|
4705
4953
|
.feedback-status-pill-completed {
|
|
4706
|
-
background:
|
|
4707
|
-
color:
|
|
4954
|
+
background: var(--ok-bg);
|
|
4955
|
+
color: var(--ok);
|
|
4708
4956
|
}
|
|
4709
4957
|
|
|
4710
4958
|
.feedback-status-pill-progress {
|
|
4711
|
-
background:
|
|
4712
|
-
color:
|
|
4959
|
+
background: var(--warn-bg);
|
|
4960
|
+
color: var(--warn);
|
|
4713
4961
|
}
|
|
4714
4962
|
|
|
4715
4963
|
.feedback-status-pill-danger {
|
|
4716
|
-
background:
|
|
4717
|
-
color:
|
|
4964
|
+
background: var(--err-bg);
|
|
4965
|
+
color: var(--err);
|
|
4718
4966
|
}
|
|
4719
4967
|
|
|
4720
4968
|
.feedback-status-pill-todo {
|
|
4721
|
-
background:
|
|
4722
|
-
color:
|
|
4969
|
+
background: var(--hover);
|
|
4970
|
+
color: var(--text2);
|
|
4723
4971
|
}
|
|
4724
4972
|
|
|
4725
4973
|
.feedback-view-button {
|
|
4726
4974
|
appearance: none;
|
|
4727
4975
|
background: transparent;
|
|
4728
4976
|
border: 0;
|
|
4729
|
-
color:
|
|
4977
|
+
color: rgb(var(--v-theme-primary));
|
|
4730
4978
|
cursor: pointer;
|
|
4731
4979
|
font-size: 12px;
|
|
4732
4980
|
font-weight: 800;
|