@7365admin1/layer-common 3.0.22 → 3.0.24
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/CHANGELOG.md +12 -0
- package/components/DashboardMain.vue +981 -346
- package/components/Nfc/NFCPatrolReportMain.vue +48 -519
- package/components/Nfc/PatrolReport/DailyReportTab.vue +59 -0
- package/components/Nfc/PatrolReport/MonthlyReportTab.vue +59 -0
- package/components/Nfc/PatrolReport/PatrolActivityTable.vue +81 -0
- package/components/Nfc/PatrolReport/PatrolReportTab.vue +73 -0
- package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +63 -0
- package/components/Nfc/PatrolReport/RemarksDialog.vue +37 -0
- package/components/Nfc/PatrolReport/ReportEmptyState.vue +24 -0
- package/components/Nfc/PatrolReport/ReportFilters.vue +88 -0
- package/components/Nfc/PatrolReport/ReportLocationHeader.vue +17 -0
- package/components/Nfc/PatrolReport/SummaryReportTable.vue +51 -0
- package/components/ServiceProviderMain.vue +183 -184
- package/components/VisitorManagement.vue +8 -0
- package/components/VisitorSocketPopUp.vue +10 -30
- package/composables/useNFCPatrolDailyReport.ts +27 -0
- package/composables/useNFCPatrolMonthlyReport.ts +27 -0
- package/composables/useNFCPatrolReport.ts +38 -0
- package/composables/useNFCPatrolReportExport.ts +31 -0
- package/composables/useNFCPatrolReportFilters.ts +39 -0
- package/composables/useServiceProvider.ts +2 -1
- package/package.json +1 -1
- package/services/nfcPatrolReport.ts +172 -0
- package/types/nfc-patrol-report.ts +73 -0
|
@@ -12,12 +12,21 @@
|
|
|
12
12
|
<!-- KPI Cards (Statistics) -->
|
|
13
13
|
<div class="print-section-title">Overview Statistics</div>
|
|
14
14
|
<div class="print-kpi-grid">
|
|
15
|
-
<div
|
|
15
|
+
<div
|
|
16
|
+
v-for="card in visibleKpiCards"
|
|
17
|
+
:key="card.key"
|
|
18
|
+
class="print-kpi-card"
|
|
19
|
+
>
|
|
16
20
|
<span class="print-kpi-title">{{ card.title }}</span>
|
|
17
|
-
<template
|
|
21
|
+
<template
|
|
22
|
+
v-if="card.key === 'supplyAlert' && Array.isArray(card.raw?.items)"
|
|
23
|
+
>
|
|
18
24
|
<div class="d-flex flex-column ga-2 mt-2">
|
|
19
|
-
<div
|
|
20
|
-
|
|
25
|
+
<div
|
|
26
|
+
v-for="item in card.raw.items"
|
|
27
|
+
:key="item._id || item.name"
|
|
28
|
+
class="d-flex justify-space-between text-body-2 text-blue-grey-darken-4 font-weight-medium"
|
|
29
|
+
>
|
|
21
30
|
<span>{{ item.name }}</span>
|
|
22
31
|
<span class="text-blue-grey">Balance: {{ item.qty }}</span>
|
|
23
32
|
</div>
|
|
@@ -27,8 +36,10 @@
|
|
|
27
36
|
<div class="print-kpi-value">{{ card.value }}</div>
|
|
28
37
|
<div class="print-kpi-meta" v-if="card.meta || card.percentage">
|
|
29
38
|
{{ card.meta || "" }}
|
|
30
|
-
<span v-if="card.percentage"
|
|
31
|
-
|
|
39
|
+
<span v-if="card.percentage"
|
|
40
|
+
>({{ card.percentage >= 0 ? "+" : ""
|
|
41
|
+
}}{{ card.percentage }}%)</span
|
|
42
|
+
>
|
|
32
43
|
</div>
|
|
33
44
|
</template>
|
|
34
45
|
</div>
|
|
@@ -39,52 +50,100 @@
|
|
|
39
50
|
<div class="print-section-title">{{ activityTitle }}</div>
|
|
40
51
|
<div class="print-chart-container">
|
|
41
52
|
<svg viewBox="0 0 920 260" class="d-block w-100" style="height: 260px">
|
|
42
|
-
<line
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
<line
|
|
54
|
+
v-for="tick in chartTicks"
|
|
55
|
+
:key="tick"
|
|
56
|
+
x1="48"
|
|
57
|
+
:y1="tick"
|
|
58
|
+
x2="900"
|
|
59
|
+
:y2="tick"
|
|
60
|
+
style="stroke: #e9eef2; stroke-width: 1"
|
|
61
|
+
/>
|
|
62
|
+
<text
|
|
63
|
+
v-for="label in chartYLabels"
|
|
64
|
+
:key="label.text"
|
|
65
|
+
x="40"
|
|
66
|
+
:y="label.y"
|
|
67
|
+
text-anchor="end"
|
|
68
|
+
alignment-baseline="middle"
|
|
69
|
+
style="fill: #7e8b96; font-size: 11px; font-weight: 500"
|
|
70
|
+
>
|
|
46
71
|
{{ label.text }}
|
|
47
72
|
</text>
|
|
48
|
-
<path
|
|
73
|
+
<path
|
|
74
|
+
:d="primaryPath"
|
|
75
|
+
style="
|
|
49
76
|
fill: none;
|
|
50
77
|
stroke: #ff9b9b;
|
|
51
78
|
stroke-width: 3;
|
|
52
79
|
stroke-linecap: round;
|
|
53
80
|
stroke-linejoin: round;
|
|
54
|
-
"
|
|
55
|
-
|
|
81
|
+
"
|
|
82
|
+
/>
|
|
83
|
+
<path
|
|
84
|
+
:d="secondaryPath"
|
|
85
|
+
style="
|
|
56
86
|
fill: none;
|
|
57
87
|
stroke: #e53935;
|
|
58
88
|
stroke-width: 3;
|
|
59
89
|
stroke-linecap: round;
|
|
60
90
|
stroke-linejoin: round;
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
"
|
|
92
|
+
/>
|
|
93
|
+
<circle
|
|
94
|
+
v-for="point in primaryDots"
|
|
95
|
+
:key="`primary-${point.x}-${point.y}`"
|
|
96
|
+
:cx="point.x"
|
|
97
|
+
:cy="point.y"
|
|
98
|
+
r="4"
|
|
99
|
+
style="fill: #ff9b9b; stroke: #fff; stroke-width: 2"
|
|
100
|
+
/>
|
|
101
|
+
<circle
|
|
102
|
+
v-for="point in secondaryDots"
|
|
103
|
+
:key="`secondary-${point.x}-${point.y}`"
|
|
104
|
+
:cx="point.x"
|
|
105
|
+
:cy="point.y"
|
|
106
|
+
r="4"
|
|
107
|
+
style="fill: #e53935; stroke: #fff; stroke-width: 2"
|
|
108
|
+
/>
|
|
109
|
+
<text
|
|
110
|
+
v-for="label in chartAxisLabels"
|
|
111
|
+
:key="`${label.text}-${label.x}`"
|
|
112
|
+
:x="label.x"
|
|
113
|
+
y="246"
|
|
114
|
+
text-anchor="middle"
|
|
115
|
+
style="fill: #7e8b96; font-size: 12px; font-weight: 600"
|
|
116
|
+
>
|
|
68
117
|
{{ label.text }}
|
|
69
118
|
</text>
|
|
70
119
|
</svg>
|
|
71
120
|
<div class="d-flex align-center justify-center ga-5 mt-2">
|
|
72
|
-
<span
|
|
73
|
-
|
|
121
|
+
<span
|
|
122
|
+
class="d-inline-flex align-center ga-2 text-caption font-weight-bold text-blue-grey"
|
|
123
|
+
>
|
|
124
|
+
<span
|
|
125
|
+
class="rounded-circle"
|
|
126
|
+
style="
|
|
74
127
|
width: 12px;
|
|
75
128
|
height: 12px;
|
|
76
129
|
display: inline-block;
|
|
77
130
|
background-color: #ff9b9b;
|
|
78
|
-
"
|
|
131
|
+
"
|
|
132
|
+
/>
|
|
79
133
|
{{ primaryLegendLabel }}
|
|
80
134
|
</span>
|
|
81
|
-
<span
|
|
82
|
-
|
|
135
|
+
<span
|
|
136
|
+
class="d-inline-flex align-center ga-2 text-caption font-weight-bold text-blue-grey"
|
|
137
|
+
>
|
|
138
|
+
<span
|
|
139
|
+
class="rounded-circle"
|
|
140
|
+
style="
|
|
83
141
|
width: 12px;
|
|
84
142
|
height: 12px;
|
|
85
143
|
display: inline-block;
|
|
86
144
|
background-color: #e53935;
|
|
87
|
-
"
|
|
145
|
+
"
|
|
146
|
+
/>
|
|
88
147
|
{{ secondaryLegendLabel }}
|
|
89
148
|
</span>
|
|
90
149
|
</div>
|
|
@@ -92,30 +151,43 @@
|
|
|
92
151
|
</div>
|
|
93
152
|
|
|
94
153
|
<!-- Tables for List Widgets (Other data) -->
|
|
95
|
-
<div
|
|
154
|
+
<div
|
|
155
|
+
v-for="section in printTableSections"
|
|
156
|
+
:key="section.key"
|
|
157
|
+
class="print-table-section mt-6"
|
|
158
|
+
>
|
|
96
159
|
<div class="print-section-title">{{ section.title }}</div>
|
|
97
160
|
<table class="print-table">
|
|
98
161
|
<thead>
|
|
99
162
|
<tr>
|
|
100
|
-
<th
|
|
163
|
+
<th
|
|
164
|
+
v-for="column in section.columns"
|
|
165
|
+
:key="column.key"
|
|
166
|
+
:style="{ width: column.width }"
|
|
167
|
+
>
|
|
101
168
|
{{ column.label }}
|
|
102
169
|
</th>
|
|
103
170
|
</tr>
|
|
104
171
|
</thead>
|
|
105
172
|
<tbody>
|
|
106
173
|
<tr v-for="row in section.rows" :key="row.id">
|
|
107
|
-
<td
|
|
174
|
+
<td
|
|
175
|
+
v-for="column in section.columns"
|
|
176
|
+
:key="`${row.id}-${column.key}`"
|
|
177
|
+
>
|
|
108
178
|
<template v-if="column.status">
|
|
109
|
-
<span
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
179
|
+
<span
|
|
180
|
+
:class="[
|
|
181
|
+
'print-status-badge',
|
|
182
|
+
`badge-${getStatusTone(row[column.key])}`,
|
|
183
|
+
]"
|
|
184
|
+
>
|
|
113
185
|
{{ row[column.key] ?? column.fallback ?? "-" }}
|
|
114
186
|
</span>
|
|
115
187
|
</template>
|
|
116
188
|
<strong v-else-if="column.strong">{{
|
|
117
189
|
row[column.key] ?? "-"
|
|
118
|
-
|
|
190
|
+
}}</strong>
|
|
119
191
|
<template v-else>{{ row[column.key] ?? "-" }}</template>
|
|
120
192
|
</td>
|
|
121
193
|
</tr>
|
|
@@ -131,7 +203,9 @@
|
|
|
131
203
|
|
|
132
204
|
<section v-else class="unified-dashboard pb-8">
|
|
133
205
|
<!-- ── Header ──────────────────────────────────────────────────────────── -->
|
|
134
|
-
<div
|
|
206
|
+
<div
|
|
207
|
+
class="d-flex flex-column flex-md-row align-md-start justify-space-between ga-5 mb-6"
|
|
208
|
+
>
|
|
135
209
|
<div>
|
|
136
210
|
<h1 class="text-h5 font-weight-black text-blue-grey-darken-4 mb-0">
|
|
137
211
|
{{ dashboardTitle }}
|
|
@@ -142,16 +216,31 @@
|
|
|
142
216
|
</div>
|
|
143
217
|
</div>
|
|
144
218
|
|
|
145
|
-
<div
|
|
146
|
-
|
|
147
|
-
|
|
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"
|
|
228
|
+
@click="openCustomizeDialog"
|
|
229
|
+
>
|
|
148
230
|
Customize Dashboard
|
|
149
231
|
</v-btn>
|
|
150
232
|
|
|
151
233
|
<v-menu>
|
|
152
234
|
<template #activator="{ props: menuProps }">
|
|
153
|
-
<v-btn
|
|
154
|
-
|
|
235
|
+
<v-btn
|
|
236
|
+
v-bind="menuProps"
|
|
237
|
+
variant="outlined"
|
|
238
|
+
color="blue-grey-darken-1"
|
|
239
|
+
class="text-none font-weight-bold"
|
|
240
|
+
append-icon="mdi-chevron-down"
|
|
241
|
+
:loading="isExporting"
|
|
242
|
+
:disabled="isExporting"
|
|
243
|
+
>
|
|
155
244
|
Export
|
|
156
245
|
</v-btn>
|
|
157
246
|
</template>
|
|
@@ -162,65 +251,133 @@
|
|
|
162
251
|
</v-list>
|
|
163
252
|
</v-menu>
|
|
164
253
|
|
|
165
|
-
<v-btn-toggle
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
254
|
+
<v-btn-toggle
|
|
255
|
+
v-model="selectedRange"
|
|
256
|
+
mandatory
|
|
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
|
|
169
271
|
? {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
272
|
+
backgroundColor: '#e2e8f0',
|
|
273
|
+
color: '#0b2f47',
|
|
274
|
+
fontWeight: '600',
|
|
275
|
+
}
|
|
174
276
|
: { backgroundColor: 'transparent', color: '#64748b' }
|
|
175
|
-
|
|
277
|
+
"
|
|
278
|
+
>
|
|
176
279
|
{{ range.label }}
|
|
177
280
|
</v-btn>
|
|
178
281
|
</v-btn-toggle>
|
|
179
282
|
</div>
|
|
180
283
|
</div>
|
|
181
284
|
|
|
182
|
-
<v-progress-linear
|
|
285
|
+
<v-progress-linear
|
|
286
|
+
v-if="finalLoading"
|
|
287
|
+
indeterminate
|
|
288
|
+
color="primary"
|
|
289
|
+
class="mb-4"
|
|
290
|
+
/>
|
|
183
291
|
|
|
184
292
|
<!-- ── KPI Cards ───────────────────────────────────────────────────────── -->
|
|
185
293
|
<v-row dense class="mb-1 align-stretch">
|
|
186
|
-
<v-col
|
|
187
|
-
|
|
294
|
+
<v-col
|
|
295
|
+
v-for="card in visibleKpiCards"
|
|
296
|
+
:key="card.key"
|
|
297
|
+
cols="12"
|
|
298
|
+
sm="6"
|
|
299
|
+
md="4"
|
|
300
|
+
lg="3"
|
|
301
|
+
class="d-flex"
|
|
302
|
+
>
|
|
303
|
+
<v-card
|
|
304
|
+
flat
|
|
305
|
+
border
|
|
306
|
+
rounded="lg"
|
|
307
|
+
class="pa-4 flex-grow-1"
|
|
308
|
+
style="min-height: 158px"
|
|
309
|
+
>
|
|
188
310
|
<div class="d-flex align-start justify-space-between">
|
|
189
|
-
<div
|
|
190
|
-
|
|
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
|
+
>
|
|
191
316
|
<v-icon :icon="card.icon" :color="card.color" size="15" />
|
|
192
317
|
</div>
|
|
193
318
|
|
|
194
319
|
<v-menu v-if="card.filter" v-model="filterMenus[card.key]">
|
|
195
320
|
<template #activator="{ props: menuProps }">
|
|
196
|
-
<v-btn
|
|
197
|
-
|
|
321
|
+
<v-btn
|
|
322
|
+
v-bind="menuProps"
|
|
323
|
+
variant="outlined"
|
|
324
|
+
density="compact"
|
|
325
|
+
class="text-none text-caption px-2"
|
|
326
|
+
append-icon="mdi-chevron-down"
|
|
327
|
+
height="26"
|
|
328
|
+
rounded="pill"
|
|
329
|
+
>
|
|
198
330
|
{{ card.filter.model }}
|
|
199
331
|
</v-btn>
|
|
200
332
|
</template>
|
|
201
333
|
|
|
202
334
|
<v-list density="compact" border rounded="lg" elevation="3">
|
|
203
|
-
<v-list-item
|
|
204
|
-
|
|
335
|
+
<v-list-item
|
|
336
|
+
v-for="option in card.filter.options"
|
|
337
|
+
:key="option"
|
|
338
|
+
:active="card.filter.model === option"
|
|
339
|
+
:title="option"
|
|
340
|
+
@click="card.filter.set(option)"
|
|
341
|
+
>
|
|
205
342
|
<template #prepend>
|
|
206
|
-
<v-icon
|
|
207
|
-
|
|
343
|
+
<v-icon
|
|
344
|
+
:icon="
|
|
345
|
+
card.filter.model === option ? 'mdi-check' : 'mdi-blank'
|
|
346
|
+
"
|
|
347
|
+
size="16"
|
|
348
|
+
/>
|
|
208
349
|
</template>
|
|
209
350
|
</v-list-item>
|
|
210
351
|
</v-list>
|
|
211
352
|
</v-menu>
|
|
353
|
+
|
|
354
|
+
<button
|
|
355
|
+
v-if="card.key === 'facilityBookings'"
|
|
356
|
+
class="figma-panel-action"
|
|
357
|
+
type="button"
|
|
358
|
+
@click="openFacilityBookingDialog"
|
|
359
|
+
>
|
|
360
|
+
View All
|
|
361
|
+
</button>
|
|
212
362
|
</div>
|
|
213
363
|
|
|
214
364
|
<div class="mt-6 w-100">
|
|
215
|
-
<p
|
|
365
|
+
<p
|
|
366
|
+
class="text-caption font-weight-black text-uppercase text-blue-grey-darken-2 mb-3"
|
|
367
|
+
>
|
|
216
368
|
{{ card.title }}
|
|
217
369
|
</p>
|
|
218
|
-
<template
|
|
219
|
-
|
|
220
|
-
|
|
370
|
+
<template
|
|
371
|
+
v-if="
|
|
372
|
+
card.key === 'supplyAlert' && Array.isArray(card.raw?.items)
|
|
373
|
+
"
|
|
374
|
+
>
|
|
221
375
|
<div class="d-flex flex-column ga-2 mt-2">
|
|
222
|
-
<div
|
|
223
|
-
|
|
376
|
+
<div
|
|
377
|
+
v-for="item in card.raw.items"
|
|
378
|
+
:key="item._id || item.name"
|
|
379
|
+
class="d-flex justify-space-between text-body-2 text-blue-grey-darken-4 font-weight-medium"
|
|
380
|
+
>
|
|
224
381
|
<span>{{ item.name }}</span>
|
|
225
382
|
<span class="text-blue-grey">Balance: {{ item.qty }}</span>
|
|
226
383
|
</div>
|
|
@@ -235,13 +392,22 @@
|
|
|
235
392
|
<div class="d-flex align-center flex-wrap ga-2">
|
|
236
393
|
<span v-if="card.meta" class="text-caption text-blue-grey">{{
|
|
237
394
|
card.meta
|
|
238
|
-
|
|
239
|
-
<v-chip
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
/>
|
|
245
411
|
{{ Math.abs(card.percentage) }}%
|
|
246
412
|
</v-chip>
|
|
247
413
|
</div>
|
|
@@ -252,7 +418,13 @@
|
|
|
252
418
|
</v-row>
|
|
253
419
|
|
|
254
420
|
<!-- ── Activity Chart (all modes) ────────────────────────────────────── -->
|
|
255
|
-
<v-card
|
|
421
|
+
<v-card
|
|
422
|
+
v-if="isWidgetVisible('activity')"
|
|
423
|
+
flat
|
|
424
|
+
border
|
|
425
|
+
rounded="lg"
|
|
426
|
+
class="px-5 pt-4 pb-3 mt-3"
|
|
427
|
+
>
|
|
256
428
|
<div class="d-flex align-center justify-space-between">
|
|
257
429
|
<h2 class="text-body-1 font-weight-black text-blue-grey-darken-4 mb-0">
|
|
258
430
|
{{ activityTitle }}
|
|
@@ -260,54 +432,106 @@
|
|
|
260
432
|
</div>
|
|
261
433
|
|
|
262
434
|
<div v-if="hasChartData" class="mt-4" ref="chartContainerRef">
|
|
263
|
-
<svg
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
435
|
+
<svg
|
|
436
|
+
:viewBox="`0 0 ${chartWidth} 260`"
|
|
437
|
+
class="d-block w-100"
|
|
438
|
+
style="height: auto; max-height: 260px"
|
|
439
|
+
>
|
|
440
|
+
<line
|
|
441
|
+
v-for="tick in chartTicks"
|
|
442
|
+
:key="tick"
|
|
443
|
+
x1="48"
|
|
444
|
+
:y1="tick"
|
|
445
|
+
:x2="chartWidth - 20"
|
|
446
|
+
:y2="tick"
|
|
447
|
+
style="stroke: #e9eef2; stroke-width: 1"
|
|
448
|
+
/>
|
|
449
|
+
<text
|
|
450
|
+
v-for="label in chartYLabels"
|
|
451
|
+
:key="label.text"
|
|
452
|
+
x="40"
|
|
453
|
+
:y="label.y"
|
|
454
|
+
text-anchor="end"
|
|
455
|
+
alignment-baseline="middle"
|
|
456
|
+
style="fill: #7e8b96; font-size: 11px; font-weight: 500"
|
|
457
|
+
>
|
|
268
458
|
{{ label.text }}
|
|
269
459
|
</text>
|
|
270
|
-
<path
|
|
460
|
+
<path
|
|
461
|
+
:d="primaryPath"
|
|
462
|
+
style="
|
|
271
463
|
fill: none;
|
|
272
464
|
stroke: #ff9b9b;
|
|
273
465
|
stroke-width: 3;
|
|
274
466
|
stroke-linecap: round;
|
|
275
467
|
stroke-linejoin: round;
|
|
276
|
-
"
|
|
277
|
-
|
|
468
|
+
"
|
|
469
|
+
/>
|
|
470
|
+
<path
|
|
471
|
+
:d="secondaryPath"
|
|
472
|
+
style="
|
|
278
473
|
fill: none;
|
|
279
474
|
stroke: #e53935;
|
|
280
475
|
stroke-width: 3;
|
|
281
476
|
stroke-linecap: round;
|
|
282
477
|
stroke-linejoin: round;
|
|
283
|
-
"
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
478
|
+
"
|
|
479
|
+
/>
|
|
480
|
+
<circle
|
|
481
|
+
v-for="point in primaryDots"
|
|
482
|
+
:key="`primary-${point.x}-${point.y}`"
|
|
483
|
+
:cx="point.x"
|
|
484
|
+
:cy="point.y"
|
|
485
|
+
r="4"
|
|
486
|
+
style="fill: #ff9b9b; stroke: #fff; stroke-width: 2"
|
|
487
|
+
/>
|
|
488
|
+
<circle
|
|
489
|
+
v-for="point in secondaryDots"
|
|
490
|
+
:key="`secondary-${point.x}-${point.y}`"
|
|
491
|
+
:cx="point.x"
|
|
492
|
+
:cy="point.y"
|
|
493
|
+
r="4"
|
|
494
|
+
style="fill: #e53935; stroke: #fff; stroke-width: 2"
|
|
495
|
+
/>
|
|
496
|
+
<text
|
|
497
|
+
v-for="label in chartAxisLabels"
|
|
498
|
+
:key="`${label.text}-${label.x}`"
|
|
499
|
+
:x="label.x"
|
|
500
|
+
y="246"
|
|
501
|
+
text-anchor="middle"
|
|
502
|
+
style="fill: #7e8b96; font-size: 12px; font-weight: 600"
|
|
503
|
+
>
|
|
290
504
|
{{ label.text }}
|
|
291
505
|
</text>
|
|
292
506
|
</svg>
|
|
293
507
|
|
|
294
508
|
<div class="d-flex align-center justify-center ga-5 mt-2">
|
|
295
|
-
<span
|
|
296
|
-
|
|
509
|
+
<span
|
|
510
|
+
class="d-inline-flex align-center ga-2 text-caption font-weight-bold text-blue-grey"
|
|
511
|
+
>
|
|
512
|
+
<span
|
|
513
|
+
class="rounded-circle"
|
|
514
|
+
style="
|
|
297
515
|
width: 12px;
|
|
298
516
|
height: 12px;
|
|
299
517
|
display: inline-block;
|
|
300
518
|
background-color: #ff9b9b;
|
|
301
|
-
"
|
|
519
|
+
"
|
|
520
|
+
/>
|
|
302
521
|
{{ primaryLegendLabel }}
|
|
303
522
|
</span>
|
|
304
|
-
<span
|
|
305
|
-
|
|
523
|
+
<span
|
|
524
|
+
class="d-inline-flex align-center ga-2 text-caption font-weight-bold text-blue-grey"
|
|
525
|
+
>
|
|
526
|
+
<span
|
|
527
|
+
class="rounded-circle"
|
|
528
|
+
style="
|
|
306
529
|
width: 12px;
|
|
307
530
|
height: 12px;
|
|
308
531
|
display: inline-block;
|
|
309
532
|
background-color: #e53935;
|
|
310
|
-
"
|
|
533
|
+
"
|
|
534
|
+
/>
|
|
311
535
|
{{ secondaryLegendLabel }}
|
|
312
536
|
</span>
|
|
313
537
|
</div>
|
|
@@ -318,33 +542,63 @@
|
|
|
318
542
|
|
|
319
543
|
<!-- ── Hygiene / M&E / Landscape / Pest / Pool — task + attendance ────── -->
|
|
320
544
|
<v-row v-if="isHygieneMode" dense class="mt-3 align-stretch">
|
|
321
|
-
<v-col
|
|
322
|
-
|
|
323
|
-
|
|
545
|
+
<v-col
|
|
546
|
+
v-if="isWidgetVisible('taskSchedule')"
|
|
547
|
+
cols="12"
|
|
548
|
+
lg="7"
|
|
549
|
+
class="d-flex"
|
|
550
|
+
>
|
|
551
|
+
<DashboardPanel
|
|
552
|
+
:title="primaryListConfig.title"
|
|
553
|
+
action-label="View All"
|
|
554
|
+
@action="goToDashboardRoute(primaryListConfig.route)"
|
|
555
|
+
>
|
|
324
556
|
<div v-if="taskScheduleItems.length" class="d-flex flex-column ga-3">
|
|
325
|
-
<v-card
|
|
557
|
+
<v-card
|
|
558
|
+
v-for="item in taskScheduleItems"
|
|
559
|
+
:key="item.id"
|
|
560
|
+
flat
|
|
561
|
+
border
|
|
562
|
+
rounded="lg"
|
|
563
|
+
class="pa-3"
|
|
564
|
+
>
|
|
326
565
|
<div class="d-flex align-center flex-wrap ga-3">
|
|
327
566
|
<div class="flex-grow-1" style="min-width: 0">
|
|
328
|
-
<div
|
|
567
|
+
<div
|
|
568
|
+
class="text-body-2 font-weight-bold text-blue-grey-darken-4 text-truncate"
|
|
569
|
+
>
|
|
329
570
|
{{ item.title }}
|
|
330
571
|
</div>
|
|
331
572
|
<div class="text-caption text-blue-grey mt-1 text-truncate">
|
|
332
573
|
{{ item.subtitle
|
|
333
574
|
}}<template v-if="item.subtitle && item.time">
|
|
334
|
-
• </template
|
|
575
|
+
• </template
|
|
576
|
+
>{{ item.time }}
|
|
335
577
|
</div>
|
|
336
578
|
</div>
|
|
337
|
-
<div
|
|
579
|
+
<div
|
|
580
|
+
class="d-flex align-center ga-2 text-caption text-blue-grey"
|
|
581
|
+
>
|
|
338
582
|
<AvatarMain :name="item.person" :size="30" />
|
|
339
583
|
<span class="text-truncate" style="max-width: 100px">{{
|
|
340
584
|
item.person
|
|
341
|
-
|
|
585
|
+
}}</span>
|
|
342
586
|
</div>
|
|
343
|
-
<v-chip
|
|
587
|
+
<v-chip
|
|
588
|
+
size="small"
|
|
589
|
+
:color="getStatusTone(item.status)"
|
|
590
|
+
variant="tonal"
|
|
591
|
+
label
|
|
592
|
+
>
|
|
344
593
|
{{ item.status }}
|
|
345
594
|
</v-chip>
|
|
346
|
-
<v-btn
|
|
347
|
-
|
|
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
|
+
>
|
|
348
602
|
View
|
|
349
603
|
</v-btn>
|
|
350
604
|
</div>
|
|
@@ -354,23 +608,45 @@
|
|
|
354
608
|
</DashboardPanel>
|
|
355
609
|
</v-col>
|
|
356
610
|
|
|
357
|
-
<v-col
|
|
358
|
-
|
|
359
|
-
|
|
611
|
+
<v-col
|
|
612
|
+
v-if="isWidgetVisible('staffStatus')"
|
|
613
|
+
cols="12"
|
|
614
|
+
lg="5"
|
|
615
|
+
class="d-flex"
|
|
616
|
+
>
|
|
617
|
+
<DashboardPanel
|
|
618
|
+
title="Staff's Total Attendance"
|
|
619
|
+
action-label="View All"
|
|
620
|
+
@action="goToDashboardRoute('attendance')"
|
|
621
|
+
>
|
|
360
622
|
<div v-if="staffStatusItems.length" class="d-flex flex-column ga-3">
|
|
361
|
-
<v-card
|
|
623
|
+
<v-card
|
|
624
|
+
v-for="item in staffStatusItems"
|
|
625
|
+
:key="item.id"
|
|
626
|
+
flat
|
|
627
|
+
border
|
|
628
|
+
rounded="lg"
|
|
629
|
+
class="pa-3"
|
|
630
|
+
>
|
|
362
631
|
<div class="d-flex align-start ga-3">
|
|
363
632
|
<AvatarMain :name="item.name" :size="32" />
|
|
364
633
|
<div class="flex-grow-1" style="min-width: 0">
|
|
365
|
-
<div
|
|
634
|
+
<div
|
|
635
|
+
class="text-body-2 font-weight-bold text-blue-grey-darken-4 text-truncate"
|
|
636
|
+
>
|
|
366
637
|
{{ item.name }}
|
|
367
638
|
</div>
|
|
368
639
|
<div class="text-caption text-blue-grey mt-1 text-truncate">
|
|
369
640
|
{{ item.role }}
|
|
370
641
|
</div>
|
|
371
642
|
</div>
|
|
372
|
-
<v-chip
|
|
373
|
-
|
|
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
|
+
>
|
|
374
650
|
{{ item.totalHours || 0 }} hrs
|
|
375
651
|
</v-chip>
|
|
376
652
|
</div>
|
|
@@ -381,55 +657,80 @@
|
|
|
381
657
|
</v-col>
|
|
382
658
|
|
|
383
659
|
<v-col v-if="isWidgetVisible('feedbacks')" cols="12" class="d-flex">
|
|
384
|
-
<DashboardPanel
|
|
660
|
+
<DashboardPanel
|
|
661
|
+
title="Feedbacks"
|
|
662
|
+
action-label="View All"
|
|
663
|
+
@action="goToDashboardRoute('feedbacks')"
|
|
664
|
+
>
|
|
385
665
|
<div v-if="feedbackItems.length" class="figma-list mt-2">
|
|
386
|
-
<div
|
|
666
|
+
<div
|
|
667
|
+
v-for="item in feedbackItems"
|
|
668
|
+
:key="item.id"
|
|
669
|
+
class="d-flex align-center justify-space-between"
|
|
670
|
+
style="
|
|
387
671
|
padding: 16px 20px;
|
|
388
672
|
border: 1px solid #e2e8f0;
|
|
389
673
|
border-radius: 12px;
|
|
390
674
|
margin-bottom: 12px;
|
|
391
675
|
background-color: #ffffff;
|
|
392
676
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
|
393
|
-
"
|
|
677
|
+
"
|
|
678
|
+
>
|
|
394
679
|
<div style="flex: 1; min-width: 140px">
|
|
395
|
-
<strong
|
|
680
|
+
<strong
|
|
681
|
+
style="
|
|
396
682
|
display: block;
|
|
397
683
|
font-size: 15px;
|
|
398
684
|
font-weight: 700;
|
|
399
685
|
color: #0b2f47;
|
|
400
686
|
margin-bottom: 4px;
|
|
401
|
-
"
|
|
687
|
+
"
|
|
688
|
+
>{{ item.title }}</strong
|
|
689
|
+
>
|
|
402
690
|
<span style="font-size: 13px; color: #94a3b8">{{
|
|
403
691
|
item.subtitle
|
|
404
|
-
|
|
692
|
+
}}</span>
|
|
405
693
|
</div>
|
|
406
694
|
|
|
407
|
-
<div
|
|
695
|
+
<div
|
|
696
|
+
style="flex: 1; display: flex; align-items: center; gap: 12px"
|
|
697
|
+
>
|
|
408
698
|
<AvatarMain :name="item.person" :size="36" />
|
|
409
699
|
<span style="font-size: 14px; color: #64748b">{{
|
|
410
700
|
item.person
|
|
411
|
-
|
|
701
|
+
}}</span>
|
|
412
702
|
</div>
|
|
413
703
|
|
|
414
704
|
<div style="flex: 1; display: flex; justify-content: center">
|
|
415
|
-
<div
|
|
705
|
+
<div
|
|
706
|
+
:style="getStatusPillStyle(item.status)"
|
|
707
|
+
style="
|
|
416
708
|
padding: 6px 16px;
|
|
417
709
|
border-radius: 20px;
|
|
418
710
|
font-size: 13px;
|
|
419
711
|
font-weight: 500;
|
|
420
712
|
text-transform: capitalize;
|
|
421
|
-
"
|
|
713
|
+
"
|
|
714
|
+
>
|
|
422
715
|
{{ item.status || "To-Do" }}
|
|
423
716
|
</div>
|
|
424
717
|
</div>
|
|
425
718
|
|
|
426
719
|
<div style="flex: 0 0 auto; text-align: right">
|
|
427
|
-
<v-btn
|
|
720
|
+
<v-btn
|
|
721
|
+
variant="outlined"
|
|
722
|
+
class="text-none font-weight-medium"
|
|
723
|
+
size="small"
|
|
724
|
+
color="#3b82f6"
|
|
725
|
+
style="
|
|
428
726
|
border-color: #e2e8f0;
|
|
429
727
|
border-radius: 8px;
|
|
430
728
|
padding: 0 16px;
|
|
431
729
|
height: 32px;
|
|
432
|
-
"
|
|
730
|
+
"
|
|
731
|
+
@click="goToListItem(item, 'feedbacks')"
|
|
732
|
+
>View</v-btn
|
|
733
|
+
>
|
|
433
734
|
</div>
|
|
434
735
|
</div>
|
|
435
736
|
</div>
|
|
@@ -444,22 +745,36 @@
|
|
|
444
745
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
445
746
|
<div class="figma-panel-header">
|
|
446
747
|
<h2>Upcoming Events</h2>
|
|
447
|
-
<button
|
|
748
|
+
<button
|
|
749
|
+
class="figma-panel-action"
|
|
750
|
+
type="button"
|
|
751
|
+
@click="goToDashboardRoute('event-mgmt')"
|
|
752
|
+
>
|
|
448
753
|
View All
|
|
449
754
|
</button>
|
|
450
755
|
</div>
|
|
451
756
|
<div class="figma-panel-body">
|
|
452
757
|
<div v-if="upcomingEvents.length" class="figma-list">
|
|
453
|
-
<div
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
758
|
+
<div
|
|
759
|
+
v-for="item in upcomingEvents"
|
|
760
|
+
:key="item.id"
|
|
761
|
+
class="figma-list-row"
|
|
762
|
+
>
|
|
763
|
+
<span
|
|
764
|
+
:class="[
|
|
765
|
+
'figma-event-dot',
|
|
766
|
+
`figma-event-dot-${getListTone(item)}`,
|
|
767
|
+
]"
|
|
768
|
+
/>
|
|
458
769
|
<div class="figma-list-main">
|
|
459
770
|
<strong>{{ item.title }}</strong>
|
|
460
771
|
<p v-if="item.subtitle">{{ item.subtitle }}</p>
|
|
461
772
|
</div>
|
|
462
|
-
<button
|
|
773
|
+
<button
|
|
774
|
+
class="figma-row-action"
|
|
775
|
+
type="button"
|
|
776
|
+
@click="navigateTo(dashboardPath('event-mgmt'))"
|
|
777
|
+
>
|
|
463
778
|
{{ item.actionLabel || "View" }}
|
|
464
779
|
</button>
|
|
465
780
|
</div>
|
|
@@ -473,24 +788,38 @@
|
|
|
473
788
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
474
789
|
<div class="figma-panel-header">
|
|
475
790
|
<h2>Today's Reminder</h2>
|
|
476
|
-
<button
|
|
791
|
+
<button
|
|
792
|
+
class="figma-panel-action"
|
|
793
|
+
type="button"
|
|
794
|
+
@click="goToDashboardRoute('calendar')"
|
|
795
|
+
>
|
|
477
796
|
View All
|
|
478
797
|
</button>
|
|
479
798
|
</div>
|
|
480
799
|
<div class="figma-panel-body">
|
|
481
800
|
<div v-if="todayReminders.length" class="figma-list">
|
|
482
|
-
<div
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
801
|
+
<div
|
|
802
|
+
v-for="item in todayReminders"
|
|
803
|
+
:key="item.id"
|
|
804
|
+
class="figma-list-row"
|
|
805
|
+
>
|
|
806
|
+
<span
|
|
807
|
+
:class="[
|
|
808
|
+
'figma-icon-badge',
|
|
809
|
+
`figma-icon-badge-${getListTone(item)}`,
|
|
810
|
+
]"
|
|
811
|
+
>
|
|
487
812
|
<v-icon :icon="item.icon || 'mdi-alert'" size="15" />
|
|
488
813
|
</span>
|
|
489
814
|
<div class="figma-list-main">
|
|
490
815
|
<strong>{{ item.title }}</strong>
|
|
491
816
|
<p v-if="item.subtitle">{{ item.subtitle }}</p>
|
|
492
817
|
</div>
|
|
493
|
-
<button
|
|
818
|
+
<button
|
|
819
|
+
class="figma-row-action"
|
|
820
|
+
type="button"
|
|
821
|
+
@click="navigateTo(dashboardPath('calendar'))"
|
|
822
|
+
>
|
|
494
823
|
{{ item.actionLabel || "View" }}
|
|
495
824
|
</button>
|
|
496
825
|
</div>
|
|
@@ -504,29 +833,42 @@
|
|
|
504
833
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
505
834
|
<div class="figma-panel-header">
|
|
506
835
|
<h2>Today's Attentions</h2>
|
|
507
|
-
|
|
508
836
|
</div>
|
|
509
837
|
<div class="figma-panel-body">
|
|
510
838
|
<div v-if="todayAttentions.length" class="figma-list">
|
|
511
|
-
<div
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
839
|
+
<div
|
|
840
|
+
v-for="item in todayAttentions"
|
|
841
|
+
:key="item.id"
|
|
842
|
+
class="figma-list-row"
|
|
843
|
+
>
|
|
844
|
+
<span
|
|
845
|
+
:class="[
|
|
846
|
+
'figma-icon-badge',
|
|
847
|
+
`figma-icon-badge-${getListTone(item)}`,
|
|
848
|
+
]"
|
|
849
|
+
>
|
|
850
|
+
<v-icon
|
|
851
|
+
:icon="item.icon || 'mdi-alert-circle-outline'"
|
|
852
|
+
size="15"
|
|
853
|
+
/>
|
|
517
854
|
</span>
|
|
518
855
|
<div class="figma-list-main">
|
|
519
856
|
<strong>{{ item.title }}</strong>
|
|
520
857
|
<p v-if="item.subtitle">{{ item.subtitle }}</p>
|
|
521
858
|
</div>
|
|
522
|
-
<button
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
859
|
+
<button
|
|
860
|
+
class="figma-row-action"
|
|
861
|
+
type="button"
|
|
862
|
+
@click="
|
|
863
|
+
navigateTo(
|
|
864
|
+
item.type === 'facility-booking'
|
|
865
|
+
? `${dashboardPath('facility/bookings')}?booking=${
|
|
866
|
+
item.id
|
|
867
|
+
}`
|
|
868
|
+
: dashboardPath(`incident-reports/${item.id}`)
|
|
869
|
+
)
|
|
870
|
+
"
|
|
871
|
+
>
|
|
530
872
|
{{ item.actionLabel || "Review" }}
|
|
531
873
|
</button>
|
|
532
874
|
</div>
|
|
@@ -540,19 +882,30 @@
|
|
|
540
882
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
541
883
|
<div class="figma-panel-header">
|
|
542
884
|
<h2>Work Order Status</h2>
|
|
543
|
-
<button
|
|
885
|
+
<button
|
|
886
|
+
class="figma-panel-action"
|
|
887
|
+
type="button"
|
|
888
|
+
@click="goToDashboardRoute('work-orders')"
|
|
889
|
+
>
|
|
544
890
|
View All
|
|
545
891
|
</button>
|
|
546
892
|
</div>
|
|
547
893
|
<div class="figma-panel-body">
|
|
548
894
|
<div v-if="workOrderStatus.length" class="figma-status-stack">
|
|
549
|
-
<div
|
|
895
|
+
<div
|
|
896
|
+
v-for="item in workOrderStatus"
|
|
897
|
+
:key="item.label"
|
|
898
|
+
class="figma-status-row"
|
|
899
|
+
>
|
|
550
900
|
<div class="figma-status-label">
|
|
551
901
|
<span>{{ item.label }}</span>
|
|
552
902
|
<strong>{{ item.value }}</strong>
|
|
553
903
|
</div>
|
|
554
904
|
<div class="figma-progress-track">
|
|
555
|
-
<span
|
|
905
|
+
<span
|
|
906
|
+
class="figma-progress-fill"
|
|
907
|
+
:style="getStatusProgressStyle(item)"
|
|
908
|
+
/>
|
|
556
909
|
</div>
|
|
557
910
|
</div>
|
|
558
911
|
</div>
|
|
@@ -563,9 +916,17 @@
|
|
|
563
916
|
|
|
564
917
|
<!-- Property feedbacks -->
|
|
565
918
|
<v-col v-if="isWidgetVisible('feedbacks')" cols="12">
|
|
566
|
-
<DashboardPanel
|
|
919
|
+
<DashboardPanel
|
|
920
|
+
title="Feedbacks"
|
|
921
|
+
action-label="View All"
|
|
922
|
+
@action="goToDashboardRoute('feedbacks')"
|
|
923
|
+
>
|
|
567
924
|
<div v-if="feedbackItems.length" class="feedback-list">
|
|
568
|
-
<div
|
|
925
|
+
<div
|
|
926
|
+
v-for="item in feedbackItems"
|
|
927
|
+
:key="item.id"
|
|
928
|
+
class="feedback-row"
|
|
929
|
+
>
|
|
569
930
|
<div class="feedback-summary">
|
|
570
931
|
<strong>{{ item.title }}</strong>
|
|
571
932
|
<p>{{ item.subtitle }}</p>
|
|
@@ -574,18 +935,24 @@
|
|
|
574
935
|
<div class="feedback-person">
|
|
575
936
|
<span class="feedback-avatar">{{
|
|
576
937
|
getInitials(item.person)
|
|
577
|
-
|
|
938
|
+
}}</span>
|
|
578
939
|
<span>{{ item.person }}</span>
|
|
579
940
|
</div>
|
|
580
941
|
|
|
581
|
-
<span
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
942
|
+
<span
|
|
943
|
+
:class="[
|
|
944
|
+
'feedback-status-pill',
|
|
945
|
+
`feedback-status-pill-${getFeedbackTone(item.status)}`,
|
|
946
|
+
]"
|
|
947
|
+
>
|
|
585
948
|
{{ item.status }}
|
|
586
949
|
</span>
|
|
587
950
|
|
|
588
|
-
<button
|
|
951
|
+
<button
|
|
952
|
+
class="feedback-view-button"
|
|
953
|
+
type="button"
|
|
954
|
+
@click="goToListItem(item, 'feedbacks')"
|
|
955
|
+
>
|
|
589
956
|
View
|
|
590
957
|
</button>
|
|
591
958
|
</div>
|
|
@@ -601,60 +968,90 @@
|
|
|
601
968
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
602
969
|
<div class="figma-panel-header">
|
|
603
970
|
<h2>ACTIVE PATROL</h2>
|
|
604
|
-
<button
|
|
971
|
+
<button
|
|
972
|
+
class="figma-panel-action"
|
|
973
|
+
type="button"
|
|
974
|
+
@click="goToDashboardRoute('virtual-patrol')"
|
|
975
|
+
>
|
|
605
976
|
View All
|
|
606
977
|
</button>
|
|
607
978
|
</div>
|
|
608
979
|
<div class="figma-panel-body">
|
|
609
|
-
<div
|
|
610
|
-
|
|
611
|
-
|
|
980
|
+
<div
|
|
981
|
+
v-if="formattedActivePatrolItems.length"
|
|
982
|
+
class="figma-list mt-2"
|
|
983
|
+
>
|
|
984
|
+
<div
|
|
985
|
+
v-for="item in formattedActivePatrolItems"
|
|
986
|
+
:key="item.id || item._id"
|
|
987
|
+
class="d-flex align-center justify-space-between"
|
|
988
|
+
style="
|
|
612
989
|
padding: 16px 20px;
|
|
613
990
|
border: 1px solid #e2e8f0;
|
|
614
991
|
border-radius: 12px;
|
|
615
992
|
margin-bottom: 12px;
|
|
616
993
|
background-color: #ffffff;
|
|
617
994
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
|
618
|
-
"
|
|
995
|
+
"
|
|
996
|
+
>
|
|
619
997
|
<div style="flex: 1; min-width: 140px">
|
|
620
|
-
<strong
|
|
998
|
+
<strong
|
|
999
|
+
style="
|
|
621
1000
|
display: block;
|
|
622
1001
|
font-size: 15px;
|
|
623
1002
|
font-weight: 700;
|
|
624
1003
|
color: #0b2f47;
|
|
625
1004
|
margin-bottom: 4px;
|
|
626
|
-
"
|
|
1005
|
+
"
|
|
1006
|
+
>{{ item.title || item.name }}</strong
|
|
1007
|
+
>
|
|
627
1008
|
<span style="font-size: 13px; color: #94a3b8">{{
|
|
628
1009
|
item.subtitle || item.location || ""
|
|
629
|
-
|
|
1010
|
+
}}</span>
|
|
630
1011
|
</div>
|
|
631
1012
|
|
|
632
|
-
<div
|
|
633
|
-
|
|
1013
|
+
<div
|
|
1014
|
+
style="flex: 1; display: flex; align-items: center; gap: 12px"
|
|
1015
|
+
>
|
|
1016
|
+
<AvatarMain
|
|
1017
|
+
:name="item.person || item.assignedTo"
|
|
1018
|
+
:size="36"
|
|
1019
|
+
/>
|
|
634
1020
|
<span style="font-size: 14px; color: #64748b">{{
|
|
635
1021
|
item.person || item.assignedTo
|
|
636
|
-
|
|
1022
|
+
}}</span>
|
|
637
1023
|
</div>
|
|
638
1024
|
|
|
639
1025
|
<div style="flex: 1; display: flex; justify-content: center">
|
|
640
|
-
<div
|
|
1026
|
+
<div
|
|
1027
|
+
:style="getPatrolStatusStyle(item.status)"
|
|
1028
|
+
style="
|
|
641
1029
|
padding: 6px 16px;
|
|
642
1030
|
border-radius: 20px;
|
|
643
1031
|
font-size: 13px;
|
|
644
1032
|
font-weight: 500;
|
|
645
1033
|
text-transform: capitalize;
|
|
646
|
-
"
|
|
1034
|
+
"
|
|
1035
|
+
>
|
|
647
1036
|
{{ item.status }}
|
|
648
1037
|
</div>
|
|
649
1038
|
</div>
|
|
650
1039
|
|
|
651
1040
|
<div style="flex: 0 0 auto; text-align: right">
|
|
652
|
-
<v-btn
|
|
1041
|
+
<v-btn
|
|
1042
|
+
variant="outlined"
|
|
1043
|
+
class="text-none font-weight-medium"
|
|
1044
|
+
size="small"
|
|
1045
|
+
color="#3b82f6"
|
|
1046
|
+
style="
|
|
653
1047
|
border-color: #e2e8f0;
|
|
654
1048
|
border-radius: 8px;
|
|
655
1049
|
padding: 0 16px;
|
|
656
1050
|
height: 32px;
|
|
657
|
-
"
|
|
1051
|
+
"
|
|
1052
|
+
@click="goToDashboardRoute('virtual-patrol')"
|
|
1053
|
+
>View</v-btn
|
|
1054
|
+
>
|
|
658
1055
|
</div>
|
|
659
1056
|
</div>
|
|
660
1057
|
</div>
|
|
@@ -667,50 +1064,68 @@
|
|
|
667
1064
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
668
1065
|
<div class="figma-panel-header">
|
|
669
1066
|
<h2>STAFF STATUS</h2>
|
|
670
|
-
<button
|
|
1067
|
+
<button
|
|
1068
|
+
class="figma-panel-action"
|
|
1069
|
+
type="button"
|
|
1070
|
+
@click="goToDashboardRoute('attendance')"
|
|
1071
|
+
>
|
|
671
1072
|
View All
|
|
672
1073
|
</button>
|
|
673
1074
|
</div>
|
|
674
1075
|
<div class="figma-panel-body">
|
|
675
1076
|
<div v-if="securityStaffItems.length" class="figma-list mt-2">
|
|
676
|
-
<div
|
|
677
|
-
|
|
1077
|
+
<div
|
|
1078
|
+
v-for="item in securityStaffItems"
|
|
1079
|
+
:key="item._id || item.id"
|
|
1080
|
+
class="d-flex align-center justify-space-between"
|
|
1081
|
+
style="
|
|
678
1082
|
padding: 16px 20px;
|
|
679
1083
|
border: 1px solid #e2e8f0;
|
|
680
1084
|
border-radius: 12px;
|
|
681
1085
|
margin-bottom: 12px;
|
|
682
1086
|
background-color: #ffffff;
|
|
683
1087
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
|
684
|
-
"
|
|
1088
|
+
"
|
|
1089
|
+
>
|
|
685
1090
|
<div style="display: flex; align-items: center; gap: 16px">
|
|
686
|
-
<AvatarMain
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1091
|
+
<AvatarMain
|
|
1092
|
+
:name="
|
|
1093
|
+
item.personName ||
|
|
1094
|
+
item.employeeName ||
|
|
1095
|
+
item.userName ||
|
|
1096
|
+
'Unassigned'
|
|
1097
|
+
"
|
|
1098
|
+
:size="40"
|
|
1099
|
+
/>
|
|
691
1100
|
<div>
|
|
692
|
-
<strong
|
|
1101
|
+
<strong
|
|
1102
|
+
style="
|
|
693
1103
|
display: block;
|
|
694
1104
|
font-size: 15px;
|
|
695
1105
|
font-weight: 700;
|
|
696
1106
|
color: #0b2f47;
|
|
697
1107
|
margin-bottom: 4px;
|
|
698
|
-
"
|
|
1108
|
+
"
|
|
1109
|
+
>{{
|
|
699
1110
|
item.userName || item.personName || "Unassigned"
|
|
700
|
-
}}</strong
|
|
1111
|
+
}}</strong
|
|
1112
|
+
>
|
|
701
1113
|
<span style="font-size: 13px; color: #94a3b8">{{
|
|
702
1114
|
item.role || item.designation || "Officer"
|
|
703
|
-
|
|
1115
|
+
}}</span>
|
|
704
1116
|
</div>
|
|
705
1117
|
</div>
|
|
706
1118
|
|
|
707
1119
|
<div style="flex: 0 0 auto">
|
|
708
|
-
<div
|
|
1120
|
+
<div
|
|
1121
|
+
:style="getStaffStatusPillStyle(item.status)"
|
|
1122
|
+
style="
|
|
709
1123
|
padding: 6px 16px;
|
|
710
1124
|
border-radius: 20px;
|
|
711
1125
|
font-size: 13px;
|
|
712
1126
|
font-weight: 500;
|
|
713
|
-
"
|
|
1127
|
+
"
|
|
1128
|
+
>
|
|
714
1129
|
{{ item.status || "On duty" }}
|
|
715
1130
|
</div>
|
|
716
1131
|
</div>
|
|
@@ -723,55 +1138,80 @@
|
|
|
723
1138
|
|
|
724
1139
|
<!-- Security feedbacks -->
|
|
725
1140
|
<v-col v-if="isWidgetVisible('feedbacks')" cols="12">
|
|
726
|
-
<DashboardPanel
|
|
1141
|
+
<DashboardPanel
|
|
1142
|
+
title="Feedbacks"
|
|
1143
|
+
action-label="View All"
|
|
1144
|
+
@action="goToDashboardRoute('feedbacks')"
|
|
1145
|
+
>
|
|
727
1146
|
<div v-if="feedbackItems.length" class="figma-list mt-2">
|
|
728
|
-
<div
|
|
1147
|
+
<div
|
|
1148
|
+
v-for="item in feedbackItems"
|
|
1149
|
+
:key="item.id"
|
|
1150
|
+
class="d-flex align-center justify-space-between"
|
|
1151
|
+
style="
|
|
729
1152
|
padding: 16px 20px;
|
|
730
1153
|
border: 1px solid #e2e8f0;
|
|
731
1154
|
border-radius: 12px;
|
|
732
1155
|
margin-bottom: 12px;
|
|
733
1156
|
background-color: #ffffff;
|
|
734
1157
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
|
735
|
-
"
|
|
1158
|
+
"
|
|
1159
|
+
>
|
|
736
1160
|
<div style="flex: 1; min-width: 140px">
|
|
737
|
-
<strong
|
|
1161
|
+
<strong
|
|
1162
|
+
style="
|
|
738
1163
|
display: block;
|
|
739
1164
|
font-size: 15px;
|
|
740
1165
|
font-weight: 700;
|
|
741
1166
|
color: #0b2f47;
|
|
742
1167
|
margin-bottom: 4px;
|
|
743
|
-
"
|
|
1168
|
+
"
|
|
1169
|
+
>{{ item.title }}</strong
|
|
1170
|
+
>
|
|
744
1171
|
<span style="font-size: 13px; color: #94a3b8">{{
|
|
745
1172
|
item.subtitle
|
|
746
|
-
|
|
1173
|
+
}}</span>
|
|
747
1174
|
</div>
|
|
748
1175
|
|
|
749
|
-
<div
|
|
1176
|
+
<div
|
|
1177
|
+
style="flex: 1; display: flex; align-items: center; gap: 12px"
|
|
1178
|
+
>
|
|
750
1179
|
<AvatarMain :name="item.person" :size="36" />
|
|
751
1180
|
<span style="font-size: 14px; color: #64748b">{{
|
|
752
1181
|
item.person
|
|
753
|
-
|
|
1182
|
+
}}</span>
|
|
754
1183
|
</div>
|
|
755
1184
|
|
|
756
1185
|
<div style="flex: 1; display: flex; justify-content: center">
|
|
757
|
-
<div
|
|
1186
|
+
<div
|
|
1187
|
+
:style="getStatusPillStyle(item.status)"
|
|
1188
|
+
style="
|
|
758
1189
|
padding: 6px 16px;
|
|
759
1190
|
border-radius: 20px;
|
|
760
1191
|
font-size: 13px;
|
|
761
1192
|
font-weight: 500;
|
|
762
1193
|
text-transform: capitalize;
|
|
763
|
-
"
|
|
1194
|
+
"
|
|
1195
|
+
>
|
|
764
1196
|
{{ item.status || "To-Do" }}
|
|
765
1197
|
</div>
|
|
766
1198
|
</div>
|
|
767
1199
|
|
|
768
1200
|
<div style="flex: 0 0 auto; text-align: right">
|
|
769
|
-
<v-btn
|
|
1201
|
+
<v-btn
|
|
1202
|
+
variant="outlined"
|
|
1203
|
+
class="text-none font-weight-medium"
|
|
1204
|
+
size="small"
|
|
1205
|
+
color="#3b82f6"
|
|
1206
|
+
style="
|
|
770
1207
|
border-color: #e2e8f0;
|
|
771
1208
|
border-radius: 8px;
|
|
772
1209
|
padding: 0 16px;
|
|
773
1210
|
height: 32px;
|
|
774
|
-
"
|
|
1211
|
+
"
|
|
1212
|
+
@click="goToListItem(item, 'feedbacks')"
|
|
1213
|
+
>View</v-btn
|
|
1214
|
+
>
|
|
775
1215
|
</div>
|
|
776
1216
|
</div>
|
|
777
1217
|
</div>
|
|
@@ -783,7 +1223,9 @@
|
|
|
783
1223
|
<!-- ── Customize Dialog ───────────────────────────────────────────────── -->
|
|
784
1224
|
<v-dialog v-model="customizeDialog" max-width="720" persistent scrollable>
|
|
785
1225
|
<v-card rounded="lg">
|
|
786
|
-
<v-card-title
|
|
1226
|
+
<v-card-title
|
|
1227
|
+
class="d-flex align-start justify-space-between pa-6 pb-4 border-b"
|
|
1228
|
+
>
|
|
787
1229
|
<div>
|
|
788
1230
|
<div class="text-h6 font-weight-black text-blue-grey-darken-4">
|
|
789
1231
|
Customize Dashboard
|
|
@@ -792,54 +1234,154 @@
|
|
|
792
1234
|
Select which widgets to display on your dashboard
|
|
793
1235
|
</div>
|
|
794
1236
|
</div>
|
|
795
|
-
<v-btn
|
|
796
|
-
|
|
1237
|
+
<v-btn
|
|
1238
|
+
icon="mdi-close"
|
|
1239
|
+
variant="text"
|
|
1240
|
+
density="comfortable"
|
|
1241
|
+
aria-label="Close customize dashboard"
|
|
1242
|
+
@click="cancelCustomize"
|
|
1243
|
+
/>
|
|
797
1244
|
</v-card-title>
|
|
798
1245
|
|
|
799
1246
|
<v-card-text class="pa-6">
|
|
800
|
-
<v-text-field
|
|
801
|
-
|
|
1247
|
+
<v-text-field
|
|
1248
|
+
v-model="widgetSearch"
|
|
1249
|
+
placeholder="Search widgets..."
|
|
1250
|
+
prepend-inner-icon="mdi-magnify"
|
|
1251
|
+
variant="outlined"
|
|
1252
|
+
density="compact"
|
|
1253
|
+
hide-details
|
|
1254
|
+
/>
|
|
802
1255
|
|
|
803
1256
|
<div class="d-flex justify-end mt-3 mb-1">
|
|
804
|
-
<v-btn
|
|
805
|
-
|
|
1257
|
+
<v-btn
|
|
1258
|
+
variant="text"
|
|
1259
|
+
color="blue-grey-darken-4"
|
|
1260
|
+
size="small"
|
|
1261
|
+
class="text-none font-weight-bold"
|
|
1262
|
+
prepend-icon="mdi-refresh"
|
|
1263
|
+
@click="resetCustomizeDraft"
|
|
1264
|
+
>
|
|
806
1265
|
Reset to default
|
|
807
1266
|
</v-btn>
|
|
808
1267
|
</div>
|
|
809
1268
|
|
|
810
1269
|
<div class="d-flex flex-column ga-3">
|
|
811
|
-
<v-card
|
|
1270
|
+
<v-card
|
|
1271
|
+
v-for="widget in filteredCustomizeWidgets"
|
|
1272
|
+
:key="widget.key"
|
|
1273
|
+
flat
|
|
1274
|
+
border
|
|
1275
|
+
rounded="lg"
|
|
1276
|
+
class="pa-4"
|
|
1277
|
+
>
|
|
812
1278
|
<div class="d-flex align-center justify-space-between ga-4">
|
|
813
1279
|
<div>
|
|
814
|
-
<div
|
|
1280
|
+
<div
|
|
1281
|
+
class="text-body-2 font-weight-black text-blue-grey-darken-4"
|
|
1282
|
+
>
|
|
815
1283
|
{{ widget.title }}
|
|
816
1284
|
</div>
|
|
817
1285
|
<div class="text-caption text-blue-grey mt-1">
|
|
818
1286
|
{{ widget.description }}
|
|
819
1287
|
</div>
|
|
820
1288
|
</div>
|
|
821
|
-
<v-switch
|
|
822
|
-
|
|
1289
|
+
<v-switch
|
|
1290
|
+
v-model="draftVisibleWidgetKeys"
|
|
1291
|
+
:value="widget.key"
|
|
1292
|
+
color="blue-grey-darken-4"
|
|
1293
|
+
inset
|
|
1294
|
+
hide-details
|
|
1295
|
+
density="compact"
|
|
1296
|
+
aria-label="Toggle dashboard widget"
|
|
1297
|
+
/>
|
|
823
1298
|
</div>
|
|
824
1299
|
</v-card>
|
|
825
1300
|
|
|
826
|
-
<DashboardEmptyState
|
|
1301
|
+
<DashboardEmptyState
|
|
1302
|
+
v-if="!filteredCustomizeWidgets.length"
|
|
1303
|
+
compact
|
|
1304
|
+
/>
|
|
827
1305
|
</div>
|
|
828
1306
|
</v-card-text>
|
|
829
1307
|
|
|
830
1308
|
<v-card-actions class="pa-6 pt-4 border-t justify-end ga-3">
|
|
831
|
-
<v-btn
|
|
1309
|
+
<v-btn
|
|
1310
|
+
variant="text"
|
|
1311
|
+
class="text-none font-weight-bold text-blue-grey"
|
|
1312
|
+
@click="cancelCustomize"
|
|
1313
|
+
>
|
|
832
1314
|
Cancel
|
|
833
1315
|
</v-btn>
|
|
834
|
-
<v-btn
|
|
835
|
-
|
|
1316
|
+
<v-btn
|
|
1317
|
+
variant="flat"
|
|
1318
|
+
color="blue-grey-darken-4"
|
|
1319
|
+
class="text-none font-weight-bold px-6"
|
|
1320
|
+
style="min-width: 110px"
|
|
1321
|
+
@click="submitCustomize"
|
|
1322
|
+
>
|
|
836
1323
|
Submit
|
|
837
1324
|
</v-btn>
|
|
838
1325
|
</v-card-actions>
|
|
839
1326
|
</v-card>
|
|
840
1327
|
</v-dialog>
|
|
841
1328
|
|
|
842
|
-
<v-
|
|
1329
|
+
<v-dialog
|
|
1330
|
+
v-model="facilityBookingDialog"
|
|
1331
|
+
max-width="650"
|
|
1332
|
+
persistent
|
|
1333
|
+
scrollable
|
|
1334
|
+
>
|
|
1335
|
+
<v-card rounded="lg" class="facility-booking-modal">
|
|
1336
|
+
<v-card-title
|
|
1337
|
+
class="d-flex align-center justify-space-between pa-6 pb-3"
|
|
1338
|
+
>
|
|
1339
|
+
<div class="text-body-1 font-weight-black text-blue-grey-darken-4">
|
|
1340
|
+
Facility Booking
|
|
1341
|
+
</div>
|
|
1342
|
+
<v-btn
|
|
1343
|
+
icon="mdi-close"
|
|
1344
|
+
variant="text"
|
|
1345
|
+
density="comfortable"
|
|
1346
|
+
aria-label="Close facility booking summary"
|
|
1347
|
+
@click="facilityBookingDialog = false"
|
|
1348
|
+
/>
|
|
1349
|
+
</v-card-title>
|
|
1350
|
+
|
|
1351
|
+
<v-card-text class="px-6 pb-6 pt-3">
|
|
1352
|
+
<div class="facility-booking-summary-list">
|
|
1353
|
+
<div
|
|
1354
|
+
v-for="item in facilityBookingBreakdown"
|
|
1355
|
+
:key="item.status"
|
|
1356
|
+
class="facility-booking-summary-row"
|
|
1357
|
+
>
|
|
1358
|
+
<span class="facility-booking-summary-label">
|
|
1359
|
+
{{ item.status }}
|
|
1360
|
+
</span>
|
|
1361
|
+
<span class="facility-booking-summary-dots" />
|
|
1362
|
+
<strong class="facility-booking-summary-value">
|
|
1363
|
+
{{ formatNumber(item.count) }}
|
|
1364
|
+
</strong>
|
|
1365
|
+
</div>
|
|
1366
|
+
</div>
|
|
1367
|
+
|
|
1368
|
+
<v-divider class="my-5" />
|
|
1369
|
+
|
|
1370
|
+
<div class="facility-booking-summary-row facility-booking-total-row">
|
|
1371
|
+
<strong class="facility-booking-total-label">Total</strong>
|
|
1372
|
+
<strong class="facility-booking-summary-value">
|
|
1373
|
+
{{ formatNumber(facilityBookingTotal) }}
|
|
1374
|
+
</strong>
|
|
1375
|
+
</div>
|
|
1376
|
+
</v-card-text>
|
|
1377
|
+
</v-card>
|
|
1378
|
+
</v-dialog>
|
|
1379
|
+
|
|
1380
|
+
<v-snackbar
|
|
1381
|
+
v-model="snackbar.visible"
|
|
1382
|
+
:timeout="2200"
|
|
1383
|
+
:color="snackbar.color || 'primary'"
|
|
1384
|
+
>
|
|
843
1385
|
{{ snackbar.text }}
|
|
844
1386
|
</v-snackbar>
|
|
845
1387
|
</section>
|
|
@@ -864,6 +1406,8 @@ type DashboardMetric = {
|
|
|
864
1406
|
outOfStock?: number;
|
|
865
1407
|
completed?: number;
|
|
866
1408
|
total?: number;
|
|
1409
|
+
byStatus?: Record<string, number>;
|
|
1410
|
+
statusCounts?: Record<string, number>;
|
|
867
1411
|
items?: { name: string; qty: number }[];
|
|
868
1412
|
};
|
|
869
1413
|
|
|
@@ -1075,7 +1619,7 @@ const initialPeriod = (
|
|
|
1075
1619
|
) as TDashboardValues;
|
|
1076
1620
|
const selectedRange = ref(periodLabelValue[initialPeriod]);
|
|
1077
1621
|
const visitorType = ref("All");
|
|
1078
|
-
const
|
|
1622
|
+
const facilityBookingDialog = ref(false);
|
|
1079
1623
|
const visibleWidgetKeys = ref<string[]>([]);
|
|
1080
1624
|
const draftVisibleWidgetKeys = ref<string[]>([]);
|
|
1081
1625
|
const filterMenus = reactive<Record<string, boolean>>({});
|
|
@@ -1123,10 +1667,10 @@ const { data: getDashboardReq, pending: loadingMetrics } =
|
|
|
1123
1667
|
() =>
|
|
1124
1668
|
props.serviceType && isHygieneMode.value
|
|
1125
1669
|
? getDashboardHygiene(
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1670
|
+
props.site,
|
|
1671
|
+
currentPeriod.value,
|
|
1672
|
+
props.serviceType as TServiceType
|
|
1673
|
+
)
|
|
1130
1674
|
: Promise.resolve(null),
|
|
1131
1675
|
{
|
|
1132
1676
|
watch: [() => props.site, currentPeriod, () => props.serviceType],
|
|
@@ -1140,13 +1684,13 @@ const { data: getDashboardAltReq, pending: loadingAltMetrics } =
|
|
|
1140
1684
|
() =>
|
|
1141
1685
|
props.site && (isPropertyMode.value || isSecurityMode.value)
|
|
1142
1686
|
? getDashboard(
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1687
|
+
props.site,
|
|
1688
|
+
props.serviceType as TServiceType,
|
|
1689
|
+
currentPeriod.value,
|
|
1690
|
+
currentVisitorTypeParam.value
|
|
1691
|
+
? { type: currentVisitorTypeParam.value }
|
|
1692
|
+
: {}
|
|
1693
|
+
)
|
|
1150
1694
|
: Promise.resolve(null),
|
|
1151
1695
|
{
|
|
1152
1696
|
watch: [
|
|
@@ -1165,10 +1709,10 @@ const { data: weeklyActivityReq, pending: loadingWeeklyActivity } =
|
|
|
1165
1709
|
() =>
|
|
1166
1710
|
props.serviceType
|
|
1167
1711
|
? getDashboardWeeklyActivity(
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1712
|
+
props.site,
|
|
1713
|
+
props.serviceType as TServiceType,
|
|
1714
|
+
currentPeriod.value
|
|
1715
|
+
)
|
|
1172
1716
|
: Promise.resolve(null),
|
|
1173
1717
|
{
|
|
1174
1718
|
watch: [() => props.site, () => props.serviceType, currentPeriod],
|
|
@@ -1182,10 +1726,10 @@ const { data: taskScheduleReq, pending: loadingTaskSchedule } =
|
|
|
1182
1726
|
() =>
|
|
1183
1727
|
props.serviceType && isHygieneMode.value
|
|
1184
1728
|
? getDashboardHygieneTaskSchedule(
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1729
|
+
props.site,
|
|
1730
|
+
props.serviceType as TServiceType,
|
|
1731
|
+
currentPeriod.value
|
|
1732
|
+
)
|
|
1189
1733
|
: Promise.resolve(null),
|
|
1190
1734
|
{
|
|
1191
1735
|
watch: [() => props.site, () => props.serviceType, currentPeriod],
|
|
@@ -1199,10 +1743,10 @@ const { data: attendanceReq, pending: loadingAttendance } =
|
|
|
1199
1743
|
() =>
|
|
1200
1744
|
props.serviceType && !isPropertyMode.value
|
|
1201
1745
|
? getDashboardHygieneAttendance(
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1746
|
+
props.site,
|
|
1747
|
+
props.serviceType as TServiceType,
|
|
1748
|
+
currentPeriod.value
|
|
1749
|
+
)
|
|
1206
1750
|
: Promise.resolve(null),
|
|
1207
1751
|
{
|
|
1208
1752
|
watch: [() => props.site, () => props.serviceType, currentPeriod],
|
|
@@ -1216,10 +1760,10 @@ const { data: feedbacksReq, pending: loadingFeedbacks } =
|
|
|
1216
1760
|
() =>
|
|
1217
1761
|
props.serviceType
|
|
1218
1762
|
? getDashboardHygieneFeedbacks(
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1763
|
+
props.site,
|
|
1764
|
+
props.serviceType as TServiceType,
|
|
1765
|
+
currentPeriod.value
|
|
1766
|
+
)
|
|
1223
1767
|
: Promise.resolve(null),
|
|
1224
1768
|
{
|
|
1225
1769
|
watch: [() => props.site, () => props.serviceType, currentPeriod],
|
|
@@ -1707,7 +2251,6 @@ const visitorOptions = computed(() =>
|
|
|
1707
2251
|
)
|
|
1708
2252
|
);
|
|
1709
2253
|
|
|
1710
|
-
// Property-specific: facility booking metric with client-side filter
|
|
1711
2254
|
const facilityBookingItems = computed(() =>
|
|
1712
2255
|
getDashboardArray([
|
|
1713
2256
|
"facilityBookingItems",
|
|
@@ -1715,20 +2258,47 @@ const facilityBookingItems = computed(() =>
|
|
|
1715
2258
|
"bookingsList",
|
|
1716
2259
|
])
|
|
1717
2260
|
);
|
|
1718
|
-
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
2261
|
+
|
|
2262
|
+
const facilityBookingStatuses = [
|
|
2263
|
+
"Pending",
|
|
2264
|
+
"Approved",
|
|
2265
|
+
"Ongoing",
|
|
2266
|
+
"For Review",
|
|
2267
|
+
"With Balance",
|
|
2268
|
+
"With Refund",
|
|
2269
|
+
"Completed",
|
|
2270
|
+
"Rejected",
|
|
2271
|
+
"Cancelled",
|
|
2272
|
+
];
|
|
2273
|
+
|
|
2274
|
+
const facilityBookingBreakdown = computed(() => {
|
|
2275
|
+
const metric = getMetric(["facilityBooking", "facilityBookings", "bookings"]);
|
|
2276
|
+
const sourceCounts = metric.byStatus ?? metric.statusCounts ?? {};
|
|
2277
|
+
const itemCounts: Record<string, number> = {};
|
|
2278
|
+
facilityBookingItems.value.forEach((item: any) => {
|
|
2279
|
+
const status = normalizeFacilityBookingStatus(item.status);
|
|
2280
|
+
if (status) itemCounts[status] = (itemCounts[status] || 0) + 1;
|
|
2281
|
+
});
|
|
2282
|
+
|
|
2283
|
+
return facilityBookingStatuses.map((status) => ({
|
|
2284
|
+
status,
|
|
2285
|
+
count: toNumber(sourceCounts[status] ?? itemCounts[status]),
|
|
2286
|
+
}));
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2289
|
+
const facilityBookingTotal = computed(() =>
|
|
2290
|
+
facilityBookingBreakdown.value.reduce((total, item) => total + item.count, 0)
|
|
1730
2291
|
);
|
|
1731
2292
|
|
|
2293
|
+
function openFacilityBookingDialog() {
|
|
2294
|
+
facilityBookingDialog.value = true;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
function normalizeFacilityBookingStatus(status: unknown) {
|
|
2298
|
+
const value = String(status || "").trim();
|
|
2299
|
+
return value === "For Refund" ? "With Refund" : value;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
1732
2302
|
function getPropertyKpiCards() {
|
|
1733
2303
|
const workOrderMetric = getMetric([
|
|
1734
2304
|
"openWorkOrder",
|
|
@@ -1798,53 +2368,13 @@ function getPropertyKpiCards() {
|
|
|
1798
2368
|
percentage: toNumber(facilityMetric?.percentage),
|
|
1799
2369
|
meta: getMetricMeta(facilityMetric),
|
|
1800
2370
|
raw: facilityMetric,
|
|
1801
|
-
filter:
|
|
1802
|
-
model: facilityType.value,
|
|
1803
|
-
options: facilityOptions.value,
|
|
1804
|
-
set: (v: string) => {
|
|
1805
|
-
facilityType.value = v;
|
|
1806
|
-
filterMenus.facilityBookings = false;
|
|
1807
|
-
},
|
|
1808
|
-
},
|
|
2371
|
+
filter: undefined,
|
|
1809
2372
|
},
|
|
1810
2373
|
];
|
|
1811
2374
|
}
|
|
1812
2375
|
|
|
1813
2376
|
function computeFacilityMetric(): DashboardMetric {
|
|
1814
|
-
|
|
1815
|
-
const selected = facilityType.value;
|
|
1816
|
-
if (selected === "All") return metric;
|
|
1817
|
-
const items = facilityBookingItems.value.filter((item: any) => {
|
|
1818
|
-
const val = String(
|
|
1819
|
-
item.facilityName ||
|
|
1820
|
-
item.facility?.name ||
|
|
1821
|
-
item.facilityInfo?.name ||
|
|
1822
|
-
item.name ||
|
|
1823
|
-
""
|
|
1824
|
-
).toLowerCase();
|
|
1825
|
-
return val === selected.toLowerCase();
|
|
1826
|
-
});
|
|
1827
|
-
const ongoing = items.filter((i: any) => {
|
|
1828
|
-
const s = String(i.status || "").toLowerCase();
|
|
1829
|
-
return (
|
|
1830
|
-
s.includes("ongoing") ||
|
|
1831
|
-
s.includes("approved") ||
|
|
1832
|
-
s.includes("in-progress")
|
|
1833
|
-
);
|
|
1834
|
-
}).length;
|
|
1835
|
-
const waitingApproval = items.filter((i: any) => {
|
|
1836
|
-
const s = String(i.status || "").toLowerCase();
|
|
1837
|
-
return (
|
|
1838
|
-
s.includes("pending") || s.includes("review") || s.includes("approval")
|
|
1839
|
-
);
|
|
1840
|
-
}).length;
|
|
1841
|
-
|
|
1842
|
-
return {
|
|
1843
|
-
...metric,
|
|
1844
|
-
count: ongoing + waitingApproval,
|
|
1845
|
-
ongoing,
|
|
1846
|
-
waitingApproval,
|
|
1847
|
-
};
|
|
2377
|
+
return getMetric(["facilityBooking", "facilityBookings", "bookings"]);
|
|
1848
2378
|
}
|
|
1849
2379
|
|
|
1850
2380
|
function getSecurityKpiCards() {
|
|
@@ -1967,21 +2497,21 @@ const dashboardTitle = computed(() => {
|
|
|
1967
2497
|
const primaryListConfig = computed(() =>
|
|
1968
2498
|
isSecurityMode.value
|
|
1969
2499
|
? {
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
2500
|
+
title: "Active Patrol",
|
|
2501
|
+
route: "virtualPatrol",
|
|
2502
|
+
keys: ["activePatrolItems", "patrolItems"],
|
|
2503
|
+
}
|
|
1974
2504
|
: {
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2505
|
+
title: "Today's Task Schedule",
|
|
2506
|
+
route: "scheduleTask",
|
|
2507
|
+
keys: [
|
|
2508
|
+
"taskScheduleItems",
|
|
2509
|
+
"scheduleTaskItems",
|
|
2510
|
+
"scheduledTaskItems",
|
|
2511
|
+
"cleaningScheduleItems",
|
|
2512
|
+
"tasks",
|
|
2513
|
+
],
|
|
2514
|
+
}
|
|
1985
2515
|
);
|
|
1986
2516
|
|
|
1987
2517
|
// ─── Activity Chart ───────────────────────────────────────────────────────────
|
|
@@ -1989,7 +2519,14 @@ const primaryListConfig = computed(() =>
|
|
|
1989
2519
|
const activityTitle = computed(() => {
|
|
1990
2520
|
if (selectedRange.value === "Today") return "Today's Activity";
|
|
1991
2521
|
if (selectedRange.value === "This Month") return "This Month Activity";
|
|
1992
|
-
return
|
|
2522
|
+
return `This Week Activity (${currentWeekRangeLabel.value})`;
|
|
2523
|
+
});
|
|
2524
|
+
|
|
2525
|
+
const currentWeekRangeLabel = computed(() => {
|
|
2526
|
+
const today = new Date();
|
|
2527
|
+
const start = getMondayWeekStart(today);
|
|
2528
|
+
const end = addDays(start, 6);
|
|
2529
|
+
return `Week ${getWeekOfMonth(today)}: ${formatWeekRange(start, end)}`;
|
|
1993
2530
|
});
|
|
1994
2531
|
|
|
1995
2532
|
const activityChart = computed(() =>
|
|
@@ -2026,6 +2563,50 @@ const hasChartData = computed(
|
|
|
2026
2563
|
);
|
|
2027
2564
|
const chartTicks = [28, 66.4, 104.8, 143.2, 181.6, 220];
|
|
2028
2565
|
|
|
2566
|
+
function getMondayWeekStart(date: Date) {
|
|
2567
|
+
const start = new Date(date);
|
|
2568
|
+
start.setHours(0, 0, 0, 0);
|
|
2569
|
+
start.setDate(start.getDate() - ((start.getDay() + 6) % 7));
|
|
2570
|
+
return start;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
function addDays(date: Date, days: number) {
|
|
2574
|
+
const next = new Date(date);
|
|
2575
|
+
next.setDate(next.getDate() + days);
|
|
2576
|
+
return next;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
function getWeekOfMonth(date: Date) {
|
|
2580
|
+
const monthStart = new Date(date.getFullYear(), date.getMonth(), 1);
|
|
2581
|
+
const firstWeekStart = getMondayWeekStart(monthStart);
|
|
2582
|
+
const currentWeekStart = getMondayWeekStart(date);
|
|
2583
|
+
const diffMs = currentWeekStart.getTime() - firstWeekStart.getTime();
|
|
2584
|
+
return Math.floor(diffMs / (7 * 24 * 60 * 60 * 1000)) + 1;
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
function formatWeekRange(start: Date, end: Date) {
|
|
2588
|
+
const sameMonth = start.getMonth() === end.getMonth();
|
|
2589
|
+
const sameYear = start.getFullYear() === end.getFullYear();
|
|
2590
|
+
const monthDayFormatter = new Intl.DateTimeFormat("en-US", {
|
|
2591
|
+
month: "long",
|
|
2592
|
+
day: "numeric",
|
|
2593
|
+
});
|
|
2594
|
+
const dayFormatter = new Intl.DateTimeFormat("en-US", { day: "numeric" });
|
|
2595
|
+
const fullFormatter = new Intl.DateTimeFormat("en-US", {
|
|
2596
|
+
month: "long",
|
|
2597
|
+
day: "numeric",
|
|
2598
|
+
year: "numeric",
|
|
2599
|
+
});
|
|
2600
|
+
|
|
2601
|
+
if (sameMonth && sameYear) {
|
|
2602
|
+
return `${monthDayFormatter.format(start)} - ${dayFormatter.format(
|
|
2603
|
+
end
|
|
2604
|
+
)}, ${end.getFullYear()}`;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
return `${fullFormatter.format(start)} - ${fullFormatter.format(end)}`;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2029
2610
|
const chartYLabels = computed(() => {
|
|
2030
2611
|
const max = chartMax.value;
|
|
2031
2612
|
return [
|
|
@@ -2362,7 +2943,8 @@ function getMetricMeta(metric: DashboardMetric) {
|
|
|
2362
2943
|
parts.push(`${formatNumber(toNumber(metric.ongoing))} ongoing`);
|
|
2363
2944
|
if (typeof metric.waitingApproval !== "undefined")
|
|
2364
2945
|
parts.push(
|
|
2365
|
-
`${formatNumber(toNumber(metric.waitingApproval))} ${
|
|
2946
|
+
`${formatNumber(toNumber(metric.waitingApproval))} ${
|
|
2947
|
+
isPropertyMode.value ? "for approval" : "waiting approval"
|
|
2366
2948
|
}`
|
|
2367
2949
|
);
|
|
2368
2950
|
if (typeof metric.lowStock !== "undefined")
|
|
@@ -2389,9 +2971,9 @@ function getFilteredVisitorMetric(): DashboardMetric {
|
|
|
2389
2971
|
count: toNumber(metric.count ?? items.length),
|
|
2390
2972
|
currentlyOnSite: toNumber(
|
|
2391
2973
|
metric.currentlyOnSite ??
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2974
|
+
items.filter(
|
|
2975
|
+
(item: any) => !isClosedStatus(item.status) && !item.checkOut
|
|
2976
|
+
).length
|
|
2395
2977
|
),
|
|
2396
2978
|
};
|
|
2397
2979
|
}
|
|
@@ -2487,8 +3069,9 @@ function getListIcon(context: string) {
|
|
|
2487
3069
|
|
|
2488
3070
|
function getListTone(item: DashboardListItem) {
|
|
2489
3071
|
if (item.tone) return item.tone;
|
|
2490
|
-
const text = `${item.title} ${item.subtitle || ""} ${item.icon || ""} ${
|
|
2491
|
-
|
|
3072
|
+
const text = `${item.title} ${item.subtitle || ""} ${item.icon || ""} ${
|
|
3073
|
+
item.color || ""
|
|
3074
|
+
}`.toLowerCase();
|
|
2492
3075
|
if (
|
|
2493
3076
|
text.includes("alert") ||
|
|
2494
3077
|
text.includes("incident") ||
|
|
@@ -2580,10 +3163,10 @@ function normalizeStaffItem(
|
|
|
2580
3163
|
role,
|
|
2581
3164
|
status: normalizeStatus(
|
|
2582
3165
|
item.statusLabel ||
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
3166
|
+
item.status ||
|
|
3167
|
+
item.currentStatus ||
|
|
3168
|
+
item.attendanceStatus ||
|
|
3169
|
+
"Off duty"
|
|
2587
3170
|
),
|
|
2588
3171
|
totalHours: item.totalHours || 0,
|
|
2589
3172
|
raw: item,
|
|
@@ -2934,8 +3517,8 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
2934
3517
|
selectedRange.value === "Today"
|
|
2935
3518
|
? "Today"
|
|
2936
3519
|
: selectedRange.value === "This Week"
|
|
2937
|
-
|
|
2938
|
-
|
|
3520
|
+
? "This Week"
|
|
3521
|
+
: "This Month";
|
|
2939
3522
|
|
|
2940
3523
|
const reportTitle = `${props.title || dashboardTitle.value} - ${periodLabel}`;
|
|
2941
3524
|
|
|
@@ -3017,10 +3600,11 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
3017
3600
|
<td>
|
|
3018
3601
|
<div class="kpi-title">${card.title}</div>
|
|
3019
3602
|
${kpiValueHtml}
|
|
3020
|
-
<div class="kpi-meta">${card.meta || ""} ${
|
|
3603
|
+
<div class="kpi-meta">${card.meta || ""} ${
|
|
3604
|
+
card.percentage
|
|
3021
3605
|
? `(${card.percentage >= 0 ? "+" : ""}${card.percentage}%)`
|
|
3022
3606
|
: ""
|
|
3023
|
-
|
|
3607
|
+
}</div>
|
|
3024
3608
|
</td>
|
|
3025
3609
|
`;
|
|
3026
3610
|
});
|
|
@@ -3066,8 +3650,8 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
3066
3650
|
<td>${item.person || ""}</td>
|
|
3067
3651
|
<td>${item.time || ""}</td>
|
|
3068
3652
|
<td><span class="status-badge badge-${getStatusTone(
|
|
3069
|
-
|
|
3070
|
-
|
|
3653
|
+
item.status
|
|
3654
|
+
)}">${item.status || ""}</span></td>
|
|
3071
3655
|
</tr>`;
|
|
3072
3656
|
});
|
|
3073
3657
|
} else {
|
|
@@ -3165,8 +3749,8 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
3165
3749
|
<td>${item.subtitle || item.location || ""}</td>
|
|
3166
3750
|
<td>${item.person || item.assignedTo || ""}</td>
|
|
3167
3751
|
<td><span class="status-badge badge-${getStatusTone(
|
|
3168
|
-
|
|
3169
|
-
|
|
3752
|
+
item.status
|
|
3753
|
+
)}">${item.status || ""}</span></td>
|
|
3170
3754
|
</tr>`;
|
|
3171
3755
|
});
|
|
3172
3756
|
} else {
|
|
@@ -3180,7 +3764,8 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
3180
3764
|
if (securityStaffItems.value.length > 0) {
|
|
3181
3765
|
securityStaffItems.value.forEach((item) => {
|
|
3182
3766
|
wordHtml += `<tr>
|
|
3183
|
-
<td><strong>${
|
|
3767
|
+
<td><strong>${
|
|
3768
|
+
item.userName || item.personName || "Unassigned"
|
|
3184
3769
|
}</strong></td>
|
|
3185
3770
|
<td>${item.role || item.designation || "Officer"}</td>
|
|
3186
3771
|
<td><span class="status-badge badge-${getStatusTone(
|
|
@@ -3204,8 +3789,8 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
3204
3789
|
<td>${item.subtitle || ""}</td>
|
|
3205
3790
|
<td>${item.person || ""}</td>
|
|
3206
3791
|
<td><span class="status-badge badge-${getStatusTone(
|
|
3207
|
-
|
|
3208
|
-
|
|
3792
|
+
item.status
|
|
3793
|
+
)}">${item.status || "To-Do"}</span></td>
|
|
3209
3794
|
</tr>`;
|
|
3210
3795
|
});
|
|
3211
3796
|
} else {
|
|
@@ -3235,8 +3820,9 @@ async function exportDashboard(format: "PDF" | "Word") {
|
|
|
3235
3820
|
snackbar.visible = true;
|
|
3236
3821
|
}
|
|
3237
3822
|
} catch (err: any) {
|
|
3238
|
-
snackbar.text = `Export failed: ${
|
|
3239
|
-
|
|
3823
|
+
snackbar.text = `Export failed: ${
|
|
3824
|
+
err?.message || "Unknown error"
|
|
3825
|
+
}. Please try again.`;
|
|
3240
3826
|
snackbar.color = "error";
|
|
3241
3827
|
snackbar.visible = true;
|
|
3242
3828
|
} finally {
|
|
@@ -3567,7 +4153,7 @@ function formatDashboardTime(value?: string) {
|
|
|
3567
4153
|
margin-top: 14px;
|
|
3568
4154
|
}
|
|
3569
4155
|
|
|
3570
|
-
.dashboard-section-grid
|
|
4156
|
+
.dashboard-section-grid > .v-col {
|
|
3571
4157
|
display: flex;
|
|
3572
4158
|
}
|
|
3573
4159
|
|
|
@@ -3619,6 +4205,54 @@ function formatDashboardTime(value?: string) {
|
|
|
3619
4205
|
white-space: nowrap;
|
|
3620
4206
|
}
|
|
3621
4207
|
|
|
4208
|
+
.facility-booking-modal {
|
|
4209
|
+
border: 1px solid #d9e0e7;
|
|
4210
|
+
box-shadow: 0 18px 48px rgba(15, 35, 54, 0.18);
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
.facility-booking-summary-list {
|
|
4214
|
+
display: grid;
|
|
4215
|
+
gap: 24px;
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
.facility-booking-summary-row {
|
|
4219
|
+
align-items: center;
|
|
4220
|
+
display: grid;
|
|
4221
|
+
gap: 16px;
|
|
4222
|
+
grid-template-columns: minmax(112px, max-content) minmax(40px, 1fr) minmax(
|
|
4223
|
+
32px,
|
|
4224
|
+
max-content
|
|
4225
|
+
);
|
|
4226
|
+
min-height: 22px;
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
.facility-booking-summary-label {
|
|
4230
|
+
color: #1565c0;
|
|
4231
|
+
font-size: 16px;
|
|
4232
|
+
font-weight: 500;
|
|
4233
|
+
line-height: 1.25;
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
.facility-booking-summary-dots {
|
|
4237
|
+
border-bottom: 2px dotted #9aa3ad;
|
|
4238
|
+
display: block;
|
|
4239
|
+
height: 1px;
|
|
4240
|
+
min-width: 32px;
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
.facility-booking-summary-value,
|
|
4244
|
+
.facility-booking-total-label {
|
|
4245
|
+
color: #0b2f47;
|
|
4246
|
+
font-size: 16px;
|
|
4247
|
+
font-weight: 800;
|
|
4248
|
+
line-height: 1.25;
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
.facility-booking-total-row {
|
|
4252
|
+
grid-template-columns: minmax(112px, max-content) minmax(32px, max-content);
|
|
4253
|
+
justify-content: space-between;
|
|
4254
|
+
}
|
|
4255
|
+
|
|
3622
4256
|
.figma-panel-body {
|
|
3623
4257
|
flex: 1;
|
|
3624
4258
|
margin-top: 20px;
|
|
@@ -3748,7 +4382,8 @@ function formatDashboardTime(value?: string) {
|
|
|
3748
4382
|
padding: 6px 2px 0;
|
|
3749
4383
|
}
|
|
3750
4384
|
|
|
3751
|
-
.figma-status-row {
|
|
4385
|
+
.figma-status-row {
|
|
4386
|
+
}
|
|
3752
4387
|
|
|
3753
4388
|
.figma-status-label {
|
|
3754
4389
|
align-items: center;
|