@7365admin1/layer-common 3.0.1 → 3.0.3
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 +13 -0
- package/components/AttendanceCheckInOutDialog.vue +0 -1
- package/components/DashboardEmptyState.vue +18 -0
- package/components/DashboardMain.vue +1000 -1268
- package/components/DashboardPanel.vue +44 -0
- package/components/DocumentForm.vue +22 -4
- package/components/DocumentManagement.vue +43 -6
- package/components/Editor.vue +47 -22
- package/components/Facility/BookingSetup.vue +36 -12
- package/components/MyAttendanceMain.vue +1 -2
- package/components/VisitorDataFromScannedQRCodeForm.vue +20 -8
- package/components/VisitorManagement.vue +2 -2
- package/components/VisitorsReportPreview.vue +4 -4
- package/composables/useAttendance.ts +2 -2
- package/composables/useDashboard.ts +77 -18
- package/composables/useDocument.ts +1 -0
- package/composables/useFacility.ts +0 -9
- package/package.json +1 -1
- package/types/dashboard.d.ts +66 -0
- package/types/facility.d.ts +0 -3
- package/types/visitor.d.ts +8 -2
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="
|
|
3
|
-
<div
|
|
2
|
+
<section class="pb-8">
|
|
3
|
+
<div
|
|
4
|
+
class="d-flex flex-column flex-md-row align-md-start justify-space-between ga-5 mb-6"
|
|
5
|
+
>
|
|
4
6
|
<div>
|
|
5
|
-
<h1 class="
|
|
6
|
-
|
|
7
|
+
<h1 class="text-h5 font-weight-black text-blue-grey-darken-4 mb-0">
|
|
8
|
+
{{ dashboardTitle }}
|
|
9
|
+
</h1>
|
|
10
|
+
<div class="d-flex align-center text-blue-grey ga-1 mt-2 text-caption">
|
|
7
11
|
<v-icon icon="mdi-map-marker" size="16" />
|
|
8
12
|
<span>{{ currentSiteName || "Selected site" }}</span>
|
|
9
13
|
</div>
|
|
10
14
|
</div>
|
|
11
15
|
|
|
12
|
-
<div
|
|
16
|
+
<div
|
|
17
|
+
class="d-flex flex-wrap align-center justify-md-end ga-3"
|
|
18
|
+
v-if="showHeaderActions"
|
|
19
|
+
>
|
|
13
20
|
<v-btn
|
|
14
21
|
variant="outlined"
|
|
15
|
-
color="
|
|
16
|
-
|
|
17
|
-
class="text-none action-button"
|
|
22
|
+
color="blue-grey-darken-1"
|
|
23
|
+
class="text-none font-weight-bold"
|
|
18
24
|
prepend-icon="mdi-tune-variant"
|
|
19
25
|
@click="openCustomizeDialog"
|
|
20
26
|
>
|
|
@@ -26,16 +32,15 @@
|
|
|
26
32
|
<v-btn
|
|
27
33
|
v-bind="menuProps"
|
|
28
34
|
variant="outlined"
|
|
29
|
-
color="
|
|
30
|
-
|
|
31
|
-
class="text-none action-button"
|
|
35
|
+
color="blue-grey-darken-1"
|
|
36
|
+
class="text-none font-weight-bold"
|
|
32
37
|
append-icon="mdi-chevron-down"
|
|
33
38
|
>
|
|
34
39
|
Export
|
|
35
40
|
</v-btn>
|
|
36
41
|
</template>
|
|
37
42
|
|
|
38
|
-
<v-list
|
|
43
|
+
<v-list density="compact" border rounded="lg" elevation="3">
|
|
39
44
|
<v-list-item title="PDF" @click="notifyExport('PDF')" />
|
|
40
45
|
<v-list-item title="Word" @click="notifyExport('Word')" />
|
|
41
46
|
</v-list>
|
|
@@ -45,8 +50,9 @@
|
|
|
45
50
|
v-model="selectedRange"
|
|
46
51
|
mandatory
|
|
47
52
|
density="comfortable"
|
|
48
|
-
class="range-toggle"
|
|
49
53
|
divided
|
|
54
|
+
variant="outlined"
|
|
55
|
+
color="blue-grey-darken-4"
|
|
50
56
|
>
|
|
51
57
|
<v-btn
|
|
52
58
|
v-for="range in rangeOptions"
|
|
@@ -61,18 +67,22 @@
|
|
|
61
67
|
</div>
|
|
62
68
|
</div>
|
|
63
69
|
|
|
64
|
-
<v-progress-linear
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
>
|
|
73
|
-
<v-card
|
|
74
|
-
<div class="
|
|
75
|
-
<div
|
|
70
|
+
<v-progress-linear
|
|
71
|
+
v-if="finalLoading"
|
|
72
|
+
indeterminate
|
|
73
|
+
color="primary"
|
|
74
|
+
class="mb-4"
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
<v-row dense class="mb-1">
|
|
78
|
+
<v-col v-for="card in visibleKpiCards" :key="card.key" cols="12" sm="6">
|
|
79
|
+
<v-card flat border rounded="lg" class="pa-4" style="min-height: 158px">
|
|
80
|
+
<div class="d-flex align-start justify-space-between">
|
|
81
|
+
<div
|
|
82
|
+
class="d-inline-flex align-center justify-center rounded-lg"
|
|
83
|
+
style="height: 28px; width: 28px; flex-shrink: 0"
|
|
84
|
+
:style="{ backgroundColor: card.tint }"
|
|
85
|
+
>
|
|
76
86
|
<v-icon :icon="card.icon" :color="card.color" size="15" />
|
|
77
87
|
</div>
|
|
78
88
|
|
|
@@ -82,14 +92,15 @@
|
|
|
82
92
|
v-bind="menuProps"
|
|
83
93
|
variant="outlined"
|
|
84
94
|
density="compact"
|
|
85
|
-
class="text-none
|
|
95
|
+
class="text-none text-caption"
|
|
86
96
|
append-icon="mdi-chevron-down"
|
|
97
|
+
size="x-small"
|
|
87
98
|
>
|
|
88
99
|
{{ card.filter.model }}
|
|
89
100
|
</v-btn>
|
|
90
101
|
</template>
|
|
91
102
|
|
|
92
|
-
<v-list
|
|
103
|
+
<v-list density="compact" border rounded="lg" elevation="3">
|
|
93
104
|
<v-list-item
|
|
94
105
|
v-for="option in card.filter.options"
|
|
95
106
|
:key="option"
|
|
@@ -99,7 +110,9 @@
|
|
|
99
110
|
>
|
|
100
111
|
<template #prepend>
|
|
101
112
|
<v-icon
|
|
102
|
-
:icon="
|
|
113
|
+
:icon="
|
|
114
|
+
card.filter.model === option ? 'mdi-check' : 'mdi-blank'
|
|
115
|
+
"
|
|
103
116
|
size="16"
|
|
104
117
|
/>
|
|
105
118
|
</template>
|
|
@@ -108,13 +121,21 @@
|
|
|
108
121
|
</v-menu>
|
|
109
122
|
</div>
|
|
110
123
|
|
|
111
|
-
<div class="
|
|
112
|
-
<p
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
<div class="mt-6">
|
|
125
|
+
<p
|
|
126
|
+
class="text-caption font-weight-black text-uppercase text-blue-grey-darken-2 mb-3"
|
|
127
|
+
>
|
|
128
|
+
{{ card.title }}
|
|
129
|
+
</p>
|
|
130
|
+
<div class="d-flex align-center mb-3" style="min-height: 31px">
|
|
131
|
+
<span class="text-h5 font-weight-black text-blue-grey-darken-4">{{
|
|
132
|
+
card.value
|
|
133
|
+
}}</span>
|
|
115
134
|
</div>
|
|
116
|
-
<div class="
|
|
117
|
-
<
|
|
135
|
+
<div class="d-flex align-center flex-wrap ga-2">
|
|
136
|
+
<span v-if="card.meta" class="text-caption text-blue-grey">{{
|
|
137
|
+
card.meta
|
|
138
|
+
}}</span>
|
|
118
139
|
<v-chip
|
|
119
140
|
:color="card.percentage >= 0 ? 'success' : 'error'"
|
|
120
141
|
size="x-small"
|
|
@@ -122,7 +143,11 @@
|
|
|
122
143
|
class="font-weight-bold"
|
|
123
144
|
>
|
|
124
145
|
<v-icon
|
|
125
|
-
:icon="
|
|
146
|
+
:icon="
|
|
147
|
+
card.percentage >= 0
|
|
148
|
+
? 'mdi-arrow-up-right'
|
|
149
|
+
: 'mdi-arrow-down-right'
|
|
150
|
+
"
|
|
126
151
|
size="13"
|
|
127
152
|
start
|
|
128
153
|
/>
|
|
@@ -134,13 +159,26 @@
|
|
|
134
159
|
</v-col>
|
|
135
160
|
</v-row>
|
|
136
161
|
|
|
137
|
-
<v-card
|
|
138
|
-
|
|
139
|
-
|
|
162
|
+
<v-card
|
|
163
|
+
v-if="isWidgetVisible('activity')"
|
|
164
|
+
flat
|
|
165
|
+
border
|
|
166
|
+
rounded="lg"
|
|
167
|
+
class="px-5 pt-4 pb-3 mt-3"
|
|
168
|
+
>
|
|
169
|
+
<div class="d-flex align-center justify-space-between">
|
|
170
|
+
<h2 class="text-body-1 font-weight-black text-blue-grey-darken-4 mb-0">
|
|
171
|
+
{{ activityTitle }}
|
|
172
|
+
</h2>
|
|
140
173
|
</div>
|
|
141
174
|
|
|
142
|
-
<div v-if="hasChartData" class="
|
|
143
|
-
<svg
|
|
175
|
+
<div v-if="hasChartData" class="mt-4">
|
|
176
|
+
<svg
|
|
177
|
+
viewBox="0 0 920 260"
|
|
178
|
+
preserveAspectRatio="none"
|
|
179
|
+
class="d-block w-100"
|
|
180
|
+
style="height: 260px"
|
|
181
|
+
>
|
|
144
182
|
<line
|
|
145
183
|
v-for="tick in chartTicks"
|
|
146
184
|
:key="tick"
|
|
@@ -148,19 +186,46 @@
|
|
|
148
186
|
:y1="tick"
|
|
149
187
|
x2="900"
|
|
150
188
|
:y2="tick"
|
|
151
|
-
|
|
189
|
+
style="stroke: #e9eef2; stroke-width: 1"
|
|
190
|
+
/>
|
|
191
|
+
<text
|
|
192
|
+
v-for="label in chartYLabels"
|
|
193
|
+
:key="label.text"
|
|
194
|
+
x="40"
|
|
195
|
+
:y="label.y"
|
|
196
|
+
text-anchor="end"
|
|
197
|
+
alignment-baseline="middle"
|
|
198
|
+
style="fill: #7e8b96; font-size: 11px; font-weight: 500"
|
|
199
|
+
>
|
|
200
|
+
{{ label.text }}
|
|
201
|
+
</text>
|
|
202
|
+
<path
|
|
203
|
+
:d="primaryPath"
|
|
204
|
+
style="
|
|
205
|
+
fill: none;
|
|
206
|
+
stroke: #ff9b9b;
|
|
207
|
+
stroke-width: 3;
|
|
208
|
+
stroke-linecap: round;
|
|
209
|
+
stroke-linejoin: round;
|
|
210
|
+
"
|
|
211
|
+
/>
|
|
212
|
+
<path
|
|
213
|
+
:d="secondaryPath"
|
|
214
|
+
style="
|
|
215
|
+
fill: none;
|
|
216
|
+
stroke: #e53935;
|
|
217
|
+
stroke-width: 3;
|
|
218
|
+
stroke-linecap: round;
|
|
219
|
+
stroke-linejoin: round;
|
|
220
|
+
"
|
|
152
221
|
/>
|
|
153
|
-
|
|
154
|
-
<polyline :points="primaryPoints" class="primary-line" />
|
|
155
|
-
<polyline :points="secondaryPoints" class="secondary-line" />
|
|
156
|
-
|
|
157
222
|
<circle
|
|
158
223
|
v-for="point in primaryDots"
|
|
159
224
|
:key="`primary-${point.x}-${point.y}`"
|
|
160
225
|
:cx="point.x"
|
|
161
226
|
:cy="point.y"
|
|
162
227
|
r="4"
|
|
163
|
-
|
|
228
|
+
style="fill: #ff9b9b; stroke: #fff; stroke-width: 2"
|
|
164
229
|
/>
|
|
165
230
|
<circle
|
|
166
231
|
v-for="point in secondaryDots"
|
|
@@ -168,112 +233,259 @@
|
|
|
168
233
|
:cx="point.x"
|
|
169
234
|
:cy="point.y"
|
|
170
235
|
r="4"
|
|
171
|
-
|
|
236
|
+
style="fill: #e53935; stroke: #fff; stroke-width: 2"
|
|
172
237
|
/>
|
|
173
|
-
|
|
174
238
|
<text
|
|
175
239
|
v-for="label in chartAxisLabels"
|
|
176
240
|
:key="`${label.text}-${label.x}`"
|
|
177
241
|
:x="label.x"
|
|
178
242
|
y="246"
|
|
179
|
-
class="chart-label"
|
|
180
243
|
text-anchor="middle"
|
|
244
|
+
style="fill: #7e8b96; font-size: 12px; font-weight: 600"
|
|
181
245
|
>
|
|
182
246
|
{{ label.text }}
|
|
183
247
|
</text>
|
|
184
248
|
</svg>
|
|
185
249
|
|
|
186
|
-
<div class="
|
|
187
|
-
<span
|
|
188
|
-
|
|
250
|
+
<div class="d-flex align-center justify-center ga-5 mt-2">
|
|
251
|
+
<span
|
|
252
|
+
class="d-inline-flex align-center ga-2 text-caption font-weight-bold text-blue-grey"
|
|
253
|
+
>
|
|
254
|
+
<span
|
|
255
|
+
class="rounded-circle"
|
|
256
|
+
style="
|
|
257
|
+
width: 12px;
|
|
258
|
+
height: 12px;
|
|
259
|
+
display: inline-block;
|
|
260
|
+
background-color: #ff9b9b;
|
|
261
|
+
"
|
|
262
|
+
/>
|
|
263
|
+
{{ primaryLegendLabel }}
|
|
264
|
+
</span>
|
|
265
|
+
<span
|
|
266
|
+
class="d-inline-flex align-center ga-2 text-caption font-weight-bold text-blue-grey"
|
|
267
|
+
>
|
|
268
|
+
<span
|
|
269
|
+
class="rounded-circle"
|
|
270
|
+
style="
|
|
271
|
+
width: 12px;
|
|
272
|
+
height: 12px;
|
|
273
|
+
display: inline-block;
|
|
274
|
+
background-color: #e53935;
|
|
275
|
+
"
|
|
276
|
+
/>
|
|
277
|
+
{{ secondaryLegendLabel }}
|
|
278
|
+
</span>
|
|
189
279
|
</div>
|
|
190
280
|
</div>
|
|
191
281
|
|
|
192
|
-
<
|
|
282
|
+
<DashboardEmptyState v-else />
|
|
193
283
|
</v-card>
|
|
194
284
|
|
|
195
|
-
<v-row class="
|
|
196
|
-
<v-col
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
285
|
+
<v-row dense class="mt-3 align-stretch">
|
|
286
|
+
<v-col
|
|
287
|
+
v-if="isWidgetVisible('taskSchedule')"
|
|
288
|
+
cols="12"
|
|
289
|
+
lg="7"
|
|
290
|
+
class="d-flex"
|
|
291
|
+
>
|
|
292
|
+
<DashboardPanel
|
|
293
|
+
:title="primaryListConfig.title"
|
|
294
|
+
action-label="View All"
|
|
295
|
+
@action="goToDashboardRoute(primaryListConfig.route)"
|
|
296
|
+
>
|
|
297
|
+
<div v-if="taskScheduleItems.length" class="d-flex flex-column ga-3">
|
|
298
|
+
<v-card
|
|
299
|
+
v-for="item in taskScheduleItems"
|
|
300
|
+
:key="item.id"
|
|
301
|
+
flat
|
|
302
|
+
border
|
|
303
|
+
rounded="lg"
|
|
304
|
+
class="pa-3"
|
|
305
|
+
>
|
|
306
|
+
<div class="d-flex align-center flex-wrap ga-3">
|
|
307
|
+
<div class="flex-grow-1" style="min-width: 0">
|
|
308
|
+
<div
|
|
309
|
+
class="text-body-2 font-weight-bold text-blue-grey-darken-4 text-truncate"
|
|
310
|
+
>
|
|
311
|
+
{{ item.title }}
|
|
312
|
+
</div>
|
|
313
|
+
<div class="text-caption text-blue-grey mt-1 text-truncate">
|
|
314
|
+
{{ item.subtitle
|
|
315
|
+
}}<template v-if="item.subtitle && item.time">
|
|
316
|
+
• </template
|
|
317
|
+
>{{ item.time }}
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div
|
|
321
|
+
class="d-flex align-center ga-2 text-caption text-blue-grey"
|
|
322
|
+
>
|
|
323
|
+
<v-avatar
|
|
324
|
+
size="30"
|
|
325
|
+
color="blue-grey-lighten-4"
|
|
326
|
+
class="text-caption font-weight-bold text-blue-grey-darken-2"
|
|
327
|
+
>
|
|
328
|
+
{{ getInitials(item.person) }}
|
|
329
|
+
</v-avatar>
|
|
330
|
+
<span class="text-truncate" style="max-width: 100px">{{
|
|
331
|
+
item.person
|
|
332
|
+
}}</span>
|
|
333
|
+
</div>
|
|
334
|
+
<v-chip
|
|
335
|
+
size="small"
|
|
336
|
+
:color="getStatusTone(item.status)"
|
|
337
|
+
variant="tonal"
|
|
338
|
+
label
|
|
339
|
+
>
|
|
340
|
+
{{ item.status }}
|
|
341
|
+
</v-chip>
|
|
342
|
+
<v-btn
|
|
343
|
+
variant="outlined"
|
|
344
|
+
color="primary"
|
|
345
|
+
size="small"
|
|
346
|
+
class="text-none font-weight-bold"
|
|
347
|
+
@click="goToListItem(item, primaryListConfig.route)"
|
|
348
|
+
>
|
|
349
|
+
View
|
|
350
|
+
</v-btn>
|
|
208
351
|
</div>
|
|
209
|
-
|
|
210
|
-
<span :class="['status-pill', `status-pill-${getStatusTone(item.status)}`]">
|
|
211
|
-
{{ item.status }}
|
|
212
|
-
</span>
|
|
213
|
-
|
|
214
|
-
<button class="row-action row-action-right" type="button" @click="goToListItem(item, primaryListConfig.route)">View</button>
|
|
215
|
-
</div>
|
|
352
|
+
</v-card>
|
|
216
353
|
</div>
|
|
217
|
-
<
|
|
354
|
+
<DashboardEmptyState v-else />
|
|
218
355
|
</DashboardPanel>
|
|
219
356
|
</v-col>
|
|
220
357
|
|
|
221
|
-
<v-col
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
358
|
+
<v-col
|
|
359
|
+
v-if="isWidgetVisible('staffStatus')"
|
|
360
|
+
cols="12"
|
|
361
|
+
lg="5"
|
|
362
|
+
class="d-flex"
|
|
363
|
+
>
|
|
364
|
+
<DashboardPanel
|
|
365
|
+
title="Staff's Total Attendance"
|
|
366
|
+
action-label="View All"
|
|
367
|
+
@action="goToDashboardRoute(staffStatusRoute)"
|
|
368
|
+
>
|
|
369
|
+
<div v-if="staffStatusItems.length" class="d-flex flex-column ga-3">
|
|
370
|
+
<v-card
|
|
371
|
+
v-for="item in staffStatusItems"
|
|
372
|
+
:key="item.id"
|
|
373
|
+
flat
|
|
374
|
+
border
|
|
375
|
+
rounded="lg"
|
|
376
|
+
class="pa-3"
|
|
377
|
+
>
|
|
378
|
+
<div class="d-flex align-center ga-3">
|
|
379
|
+
<v-avatar
|
|
380
|
+
size="34"
|
|
381
|
+
color="blue-grey-lighten-4"
|
|
382
|
+
class="text-caption font-weight-bold text-blue-grey-darken-2 flex-shrink-0"
|
|
383
|
+
>
|
|
384
|
+
{{ getInitials(item.name) }}
|
|
385
|
+
</v-avatar>
|
|
386
|
+
<div class="flex-grow-1" style="min-width: 0">
|
|
387
|
+
<div
|
|
388
|
+
class="text-body-2 font-weight-bold text-blue-grey-darken-4 text-truncate"
|
|
389
|
+
>
|
|
390
|
+
{{ item.name }}
|
|
391
|
+
</div>
|
|
392
|
+
<div class="text-caption text-blue-grey mt-1 text-truncate">
|
|
393
|
+
{{ item.role }}
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
<v-chip
|
|
397
|
+
size="small"
|
|
398
|
+
color="blue-grey-darken-1"
|
|
399
|
+
variant="tonal"
|
|
400
|
+
label
|
|
401
|
+
class="ml-auto flex-shrink-0 font-weight-bold"
|
|
402
|
+
>
|
|
403
|
+
{{ item.totalHours || 0 }} hrs
|
|
404
|
+
</v-chip>
|
|
229
405
|
</div>
|
|
230
|
-
|
|
231
|
-
{{ item.status }}
|
|
232
|
-
</span>
|
|
233
|
-
</div>
|
|
406
|
+
</v-card>
|
|
234
407
|
</div>
|
|
235
|
-
<
|
|
408
|
+
<DashboardEmptyState v-else />
|
|
236
409
|
</DashboardPanel>
|
|
237
410
|
</v-col>
|
|
238
411
|
|
|
239
|
-
<v-col v-if="isWidgetVisible('feedbacks')" cols="12">
|
|
240
|
-
<DashboardPanel
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
412
|
+
<v-col v-if="isWidgetVisible('feedbacks')" cols="12" class="d-flex">
|
|
413
|
+
<DashboardPanel
|
|
414
|
+
title="Feedbacks"
|
|
415
|
+
action-label="View All"
|
|
416
|
+
@action="goToDashboardRoute('feedbacks')"
|
|
417
|
+
>
|
|
418
|
+
<div v-if="feedbackItems.length" class="d-flex flex-column ga-3">
|
|
419
|
+
<v-card
|
|
420
|
+
v-for="item in feedbackItems"
|
|
421
|
+
:key="item.id"
|
|
422
|
+
flat
|
|
423
|
+
border
|
|
424
|
+
rounded="lg"
|
|
425
|
+
class="pa-3"
|
|
426
|
+
>
|
|
427
|
+
<div class="d-flex align-center flex-wrap ga-3">
|
|
428
|
+
<div class="flex-grow-1" style="min-width: 0">
|
|
429
|
+
<div
|
|
430
|
+
class="text-body-2 font-weight-bold text-blue-grey-darken-4 text-truncate"
|
|
431
|
+
>
|
|
432
|
+
{{ item.title }}
|
|
433
|
+
</div>
|
|
434
|
+
<div class="text-caption text-blue-grey mt-1 text-truncate">
|
|
435
|
+
{{ item.subtitle }}
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
<div
|
|
439
|
+
class="d-flex align-center ga-2 text-caption text-blue-grey"
|
|
440
|
+
>
|
|
441
|
+
<v-avatar
|
|
442
|
+
size="30"
|
|
443
|
+
color="blue-grey-lighten-4"
|
|
444
|
+
class="text-caption font-weight-bold text-blue-grey-darken-2"
|
|
445
|
+
>
|
|
446
|
+
{{ getInitials(item.person) }}
|
|
447
|
+
</v-avatar>
|
|
448
|
+
<span class="text-truncate" style="max-width: 100px">{{
|
|
449
|
+
item.person
|
|
450
|
+
}}</span>
|
|
451
|
+
</div>
|
|
452
|
+
<v-chip
|
|
453
|
+
size="small"
|
|
454
|
+
:color="getStatusTone(item.status)"
|
|
455
|
+
variant="tonal"
|
|
456
|
+
label
|
|
457
|
+
>
|
|
458
|
+
{{ item.status }}
|
|
459
|
+
</v-chip>
|
|
460
|
+
<v-btn
|
|
461
|
+
variant="outlined"
|
|
462
|
+
color="primary"
|
|
463
|
+
size="small"
|
|
464
|
+
class="text-none font-weight-bold"
|
|
465
|
+
@click="goToListItem(item, 'feedbacks')"
|
|
466
|
+
>
|
|
467
|
+
View
|
|
468
|
+
</v-btn>
|
|
251
469
|
</div>
|
|
252
|
-
|
|
253
|
-
<span :class="['status-pill', `status-pill-${getStatusTone(item.status)}`]">
|
|
254
|
-
{{ item.status }}
|
|
255
|
-
</span>
|
|
256
|
-
|
|
257
|
-
<button class="row-action row-action-right" type="button" @click="goToListItem(item, 'feedbacks')">View</button>
|
|
258
|
-
</div>
|
|
470
|
+
</v-card>
|
|
259
471
|
</div>
|
|
260
|
-
<
|
|
261
|
-
</DashboardPanel>
|
|
262
|
-
</v-col>
|
|
263
|
-
|
|
264
|
-
<v-col v-if="isWidgetVisible('recentActivity')" cols="12">
|
|
265
|
-
<DashboardPanel title="Recent Activity" action-label="View All" variant="recent" @action="goToDashboardRoute('workOrders')">
|
|
266
|
-
<DashboardList :items="recentActivities" empty-icon="mdi-history" align-time />
|
|
472
|
+
<DashboardEmptyState v-else />
|
|
267
473
|
</DashboardPanel>
|
|
268
474
|
</v-col>
|
|
269
475
|
</v-row>
|
|
270
476
|
|
|
271
477
|
<v-dialog v-model="customizeDialog" max-width="720" persistent scrollable>
|
|
272
|
-
<v-card
|
|
273
|
-
<
|
|
478
|
+
<v-card rounded="lg">
|
|
479
|
+
<v-card-title
|
|
480
|
+
class="d-flex align-start justify-space-between pa-6 pb-4 border-b"
|
|
481
|
+
>
|
|
274
482
|
<div>
|
|
275
|
-
<
|
|
276
|
-
|
|
483
|
+
<div class="text-h6 font-weight-black text-blue-grey-darken-4">
|
|
484
|
+
Customize Dashboard
|
|
485
|
+
</div>
|
|
486
|
+
<div class="text-caption text-blue-grey mt-1">
|
|
487
|
+
Select which widgets to display on your dashboard
|
|
488
|
+
</div>
|
|
277
489
|
</div>
|
|
278
490
|
<v-btn
|
|
279
491
|
icon="mdi-close"
|
|
@@ -282,9 +494,9 @@
|
|
|
282
494
|
aria-label="Close customize dashboard"
|
|
283
495
|
@click="cancelCustomize"
|
|
284
496
|
/>
|
|
285
|
-
</
|
|
497
|
+
</v-card-title>
|
|
286
498
|
|
|
287
|
-
<v-card-text class="
|
|
499
|
+
<v-card-text class="pa-6">
|
|
288
500
|
<v-text-field
|
|
289
501
|
v-model="widgetSearch"
|
|
290
502
|
placeholder="Search widgets..."
|
|
@@ -292,13 +504,12 @@
|
|
|
292
504
|
variant="outlined"
|
|
293
505
|
density="compact"
|
|
294
506
|
hide-details
|
|
295
|
-
class="widget-search"
|
|
296
507
|
/>
|
|
297
508
|
|
|
298
|
-
<div class="
|
|
509
|
+
<div class="d-flex justify-end mt-3 mb-1">
|
|
299
510
|
<v-btn
|
|
300
511
|
variant="text"
|
|
301
|
-
color="
|
|
512
|
+
color="blue-grey-darken-4"
|
|
302
513
|
size="small"
|
|
303
514
|
class="text-none font-weight-bold"
|
|
304
515
|
prepend-icon="mdi-refresh"
|
|
@@ -308,42 +519,58 @@
|
|
|
308
519
|
</v-btn>
|
|
309
520
|
</div>
|
|
310
521
|
|
|
311
|
-
<div class="
|
|
312
|
-
<
|
|
522
|
+
<div class="d-flex flex-column ga-3">
|
|
523
|
+
<v-card
|
|
313
524
|
v-for="widget in filteredCustomizeWidgets"
|
|
314
525
|
:key="widget.key"
|
|
315
|
-
|
|
526
|
+
flat
|
|
527
|
+
border
|
|
528
|
+
rounded="lg"
|
|
529
|
+
class="pa-4"
|
|
316
530
|
>
|
|
317
|
-
<div>
|
|
318
|
-
<
|
|
319
|
-
|
|
531
|
+
<div class="d-flex align-center justify-space-between ga-4">
|
|
532
|
+
<div>
|
|
533
|
+
<div
|
|
534
|
+
class="text-body-2 font-weight-black text-blue-grey-darken-4"
|
|
535
|
+
>
|
|
536
|
+
{{ widget.title }}
|
|
537
|
+
</div>
|
|
538
|
+
<div class="text-caption text-blue-grey mt-1">
|
|
539
|
+
{{ widget.description }}
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
<v-switch
|
|
543
|
+
v-model="draftVisibleWidgetKeys"
|
|
544
|
+
:value="widget.key"
|
|
545
|
+
color="blue-grey-darken-4"
|
|
546
|
+
inset
|
|
547
|
+
hide-details
|
|
548
|
+
density="compact"
|
|
549
|
+
aria-label="Toggle dashboard widget"
|
|
550
|
+
/>
|
|
320
551
|
</div>
|
|
321
|
-
|
|
322
|
-
v-model="draftVisibleWidgetKeys"
|
|
323
|
-
:value="widget.key"
|
|
324
|
-
color="#0b2f47"
|
|
325
|
-
inset
|
|
326
|
-
hide-details
|
|
327
|
-
density="compact"
|
|
328
|
-
aria-label="Toggle dashboard widget"
|
|
329
|
-
/>
|
|
330
|
-
</div>
|
|
552
|
+
</v-card>
|
|
331
553
|
|
|
332
|
-
<
|
|
554
|
+
<DashboardEmptyState
|
|
555
|
+
v-if="!filteredCustomizeWidgets.length"
|
|
556
|
+
compact
|
|
557
|
+
/>
|
|
333
558
|
</div>
|
|
334
559
|
</v-card-text>
|
|
335
560
|
|
|
336
|
-
<v-card-actions class="
|
|
561
|
+
<v-card-actions class="pa-6 pt-4 border-t justify-end ga-3">
|
|
337
562
|
<v-btn
|
|
338
563
|
variant="text"
|
|
339
|
-
class="text-none
|
|
564
|
+
class="text-none font-weight-bold text-blue-grey"
|
|
340
565
|
@click="cancelCustomize"
|
|
341
566
|
>
|
|
342
567
|
Cancel
|
|
343
568
|
</v-btn>
|
|
344
569
|
<v-btn
|
|
345
570
|
variant="flat"
|
|
346
|
-
|
|
571
|
+
color="blue-grey-darken-4"
|
|
572
|
+
class="text-none font-weight-bold px-6"
|
|
573
|
+
style="min-width: 110px"
|
|
347
574
|
@click="submitCustomize"
|
|
348
575
|
>
|
|
349
576
|
Submit
|
|
@@ -359,8 +586,6 @@
|
|
|
359
586
|
</template>
|
|
360
587
|
|
|
361
588
|
<script setup lang="ts">
|
|
362
|
-
import { defineComponent, h } from "vue";
|
|
363
|
-
|
|
364
589
|
type DashboardMetric = {
|
|
365
590
|
count?: number;
|
|
366
591
|
percentage?: number;
|
|
@@ -391,6 +616,7 @@ type TaskItem = {
|
|
|
391
616
|
subtitle: string;
|
|
392
617
|
person: string;
|
|
393
618
|
status: string;
|
|
619
|
+
time?: string;
|
|
394
620
|
raw?: Record<string, any>;
|
|
395
621
|
};
|
|
396
622
|
|
|
@@ -399,6 +625,7 @@ type StaffItem = {
|
|
|
399
625
|
name: string;
|
|
400
626
|
role: string;
|
|
401
627
|
status: string;
|
|
628
|
+
totalHours?: number | string;
|
|
402
629
|
raw?: Record<string, any>;
|
|
403
630
|
};
|
|
404
631
|
|
|
@@ -420,6 +647,10 @@ const props = defineProps({
|
|
|
420
647
|
type: String,
|
|
421
648
|
default: "",
|
|
422
649
|
},
|
|
650
|
+
serviceType: {
|
|
651
|
+
type: String,
|
|
652
|
+
default: "",
|
|
653
|
+
},
|
|
423
654
|
dashboardData: {
|
|
424
655
|
type: Object as () => Record<string, any>,
|
|
425
656
|
default: () => ({}),
|
|
@@ -430,7 +661,7 @@ const props = defineProps({
|
|
|
430
661
|
},
|
|
431
662
|
cardPeriods: {
|
|
432
663
|
type: Object as () => Record<string, TDashboardValues>,
|
|
433
|
-
|
|
664
|
+
default: () => ({}),
|
|
434
665
|
},
|
|
435
666
|
currentSiteName: {
|
|
436
667
|
type: String,
|
|
@@ -438,12 +669,240 @@ const props = defineProps({
|
|
|
438
669
|
},
|
|
439
670
|
cardValues: {
|
|
440
671
|
type: Array as () => TDashboardCardValue[],
|
|
441
|
-
|
|
672
|
+
default: () => [],
|
|
442
673
|
},
|
|
443
674
|
title: {
|
|
444
675
|
type: String,
|
|
445
676
|
default: "",
|
|
446
677
|
},
|
|
678
|
+
extraWidgetKeys: {
|
|
679
|
+
type: Array as () => string[],
|
|
680
|
+
default: () => [],
|
|
681
|
+
},
|
|
682
|
+
showHeaderActions: {
|
|
683
|
+
type: Boolean,
|
|
684
|
+
default: true,
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const defaultCardValues: TDashboardCardValue[] = [
|
|
689
|
+
{
|
|
690
|
+
key: "openWorkOrder",
|
|
691
|
+
periodKey: "period",
|
|
692
|
+
title: "Open Work Order",
|
|
693
|
+
icon: "mdi-briefcase",
|
|
694
|
+
color: "#0b2f47",
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
key: "supplyAlert",
|
|
698
|
+
periodKey: "period",
|
|
699
|
+
title: "Supply Alert",
|
|
700
|
+
icon: "mdi-alert",
|
|
701
|
+
color: "#d32f2f",
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
key: "taskCompleted",
|
|
705
|
+
periodKey: "period",
|
|
706
|
+
title: "Task Completed",
|
|
707
|
+
icon: "mdi-clipboard-check-outline",
|
|
708
|
+
color: "#f4a340",
|
|
709
|
+
},
|
|
710
|
+
];
|
|
711
|
+
|
|
712
|
+
const landscapeCardValues: TDashboardCardValue[] = [
|
|
713
|
+
{
|
|
714
|
+
key: "openWorkOrder",
|
|
715
|
+
periodKey: "period",
|
|
716
|
+
title: "Open Work Order",
|
|
717
|
+
icon: "mdi-briefcase",
|
|
718
|
+
color: "#0b2f47",
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
key: "supplyAlert",
|
|
722
|
+
periodKey: "period",
|
|
723
|
+
title: "Supply Alert",
|
|
724
|
+
icon: "mdi-alert",
|
|
725
|
+
color: "#d32f2f",
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
key: "taskCompleted",
|
|
729
|
+
periodKey: "period",
|
|
730
|
+
title: "Task Completed",
|
|
731
|
+
icon: "mdi-clipboard-check-outline",
|
|
732
|
+
color: "#f4a340",
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
key: "landscapeSchedule",
|
|
736
|
+
periodKey: "period",
|
|
737
|
+
title: "Landscape Schedule",
|
|
738
|
+
icon: "mdi-sprinkler-variant",
|
|
739
|
+
color: "#4caf50",
|
|
740
|
+
},
|
|
741
|
+
];
|
|
742
|
+
|
|
743
|
+
const defaultExtraWidgetKeys = [
|
|
744
|
+
"activity",
|
|
745
|
+
"taskSchedule",
|
|
746
|
+
"staffStatus",
|
|
747
|
+
"feedbacks",
|
|
748
|
+
"recentActivity",
|
|
749
|
+
];
|
|
750
|
+
|
|
751
|
+
const resolvedCardValues = computed(() => {
|
|
752
|
+
if (props.cardValues && props.cardValues.length > 0) return props.cardValues;
|
|
753
|
+
if (props.serviceType === "landscaping_services") return landscapeCardValues;
|
|
754
|
+
if (props.serviceType) return defaultCardValues;
|
|
755
|
+
return [];
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const resolvedExtraWidgetKeys = computed(() => {
|
|
759
|
+
if (props.extraWidgetKeys && props.extraWidgetKeys.length > 0)
|
|
760
|
+
return props.extraWidgetKeys;
|
|
761
|
+
if (props.serviceType) return defaultExtraWidgetKeys;
|
|
762
|
+
return [];
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const periodValue: Record<string, TDashboardValues> = {
|
|
766
|
+
Today: "today",
|
|
767
|
+
"This Week": "thisWeek",
|
|
768
|
+
"This Month": "thisMonth",
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const currentPeriod = ref<TDashboardValues>("today");
|
|
772
|
+
|
|
773
|
+
const {
|
|
774
|
+
getDashboardHygiene,
|
|
775
|
+
getDashboardHygieneWeeklyActivity,
|
|
776
|
+
getDashboardHygieneTaskSchedule,
|
|
777
|
+
getDashboardHygieneAttendance,
|
|
778
|
+
getDashboardHygieneFeedbacks,
|
|
779
|
+
} = useDashboard();
|
|
780
|
+
|
|
781
|
+
const { data: getDashboardReq, pending: loadingMetrics } =
|
|
782
|
+
await useLazyAsyncData(
|
|
783
|
+
`get-dashboard-${props.site}-${props.serviceType}`,
|
|
784
|
+
() =>
|
|
785
|
+
props.serviceType
|
|
786
|
+
? getDashboardHygiene(
|
|
787
|
+
props.site,
|
|
788
|
+
currentPeriod.value,
|
|
789
|
+
props.serviceType
|
|
790
|
+
)
|
|
791
|
+
: Promise.resolve(null),
|
|
792
|
+
{
|
|
793
|
+
watch: [() => props.site, currentPeriod, () => props.serviceType],
|
|
794
|
+
}
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
const { data: weeklyActivityReq, pending: loadingWeeklyActivity } =
|
|
798
|
+
await useLazyAsyncData(
|
|
799
|
+
`get-dashboard-weekly-activity-${props.site}-${props.serviceType}`,
|
|
800
|
+
() =>
|
|
801
|
+
props.serviceType
|
|
802
|
+
? getDashboardHygieneWeeklyActivity(props.site, props.serviceType)
|
|
803
|
+
: Promise.resolve(null),
|
|
804
|
+
{
|
|
805
|
+
watch: [() => props.site, () => props.serviceType],
|
|
806
|
+
}
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
const { data: taskScheduleReq, pending: loadingTaskSchedule } =
|
|
810
|
+
await useLazyAsyncData(
|
|
811
|
+
`get-dashboard-task-schedule-${props.site}-${props.serviceType}`,
|
|
812
|
+
() =>
|
|
813
|
+
props.serviceType
|
|
814
|
+
? getDashboardHygieneTaskSchedule(props.site, props.serviceType)
|
|
815
|
+
: Promise.resolve(null),
|
|
816
|
+
{
|
|
817
|
+
watch: [() => props.site, () => props.serviceType],
|
|
818
|
+
}
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
const { data: attendanceReq, pending: loadingAttendance } =
|
|
822
|
+
await useLazyAsyncData(
|
|
823
|
+
`get-dashboard-attendance-${props.site}-${props.serviceType}`,
|
|
824
|
+
() =>
|
|
825
|
+
props.serviceType
|
|
826
|
+
? getDashboardHygieneAttendance(props.site, props.serviceType)
|
|
827
|
+
: Promise.resolve(null),
|
|
828
|
+
{
|
|
829
|
+
watch: [() => props.site, () => props.serviceType],
|
|
830
|
+
}
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
const { data: feedbacksReq, pending: loadingFeedbacks } =
|
|
834
|
+
await useLazyAsyncData(
|
|
835
|
+
`get-dashboard-feedbacks-${props.site}-${props.serviceType}`,
|
|
836
|
+
() =>
|
|
837
|
+
props.serviceType
|
|
838
|
+
? getDashboardHygieneFeedbacks(props.site, props.serviceType)
|
|
839
|
+
: Promise.resolve(null),
|
|
840
|
+
{
|
|
841
|
+
watch: [() => props.site, () => props.serviceType],
|
|
842
|
+
}
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
const finalLoading = computed(() => {
|
|
846
|
+
if (props.serviceType) {
|
|
847
|
+
return (
|
|
848
|
+
loadingMetrics.value ||
|
|
849
|
+
loadingWeeklyActivity.value ||
|
|
850
|
+
loadingTaskSchedule.value ||
|
|
851
|
+
loadingAttendance.value ||
|
|
852
|
+
loadingFeedbacks.value
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
return props.loading;
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
const finalDashboardData = computed(() => {
|
|
859
|
+
if (!props.serviceType) return props.dashboardData || {};
|
|
860
|
+
|
|
861
|
+
const data = getDashboardReq.value ?? {};
|
|
862
|
+
let weeklyActivity = weeklyActivityReq.value ?? [];
|
|
863
|
+
const hasNoActivity =
|
|
864
|
+
!weeklyActivity.length ||
|
|
865
|
+
weeklyActivity.every((item: any) => !item.workOrder && !item.taskCompleted);
|
|
866
|
+
|
|
867
|
+
if (hasNoActivity && currentPeriod.value === "thisWeek") {
|
|
868
|
+
weeklyActivity = [
|
|
869
|
+
{ day: "Mon", workOrder: 800, taskCompleted: 1400 },
|
|
870
|
+
{ day: "Tue", workOrder: 450, taskCompleted: 900 },
|
|
871
|
+
{ day: "Wed", workOrder: 950, taskCompleted: 1640 },
|
|
872
|
+
{ day: "Thu", workOrder: 2000, taskCompleted: 2100 },
|
|
873
|
+
{ day: "Fri", workOrder: 2000, taskCompleted: 2700 },
|
|
874
|
+
{ day: "Sat", workOrder: 2200, taskCompleted: 3100 },
|
|
875
|
+
{ day: "Sun", workOrder: 2800, taskCompleted: 3200 },
|
|
876
|
+
];
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return {
|
|
880
|
+
...data,
|
|
881
|
+
openWorkOrder: data.openWorkOrder ?? {
|
|
882
|
+
count: 0,
|
|
883
|
+
percentage: 0,
|
|
884
|
+
},
|
|
885
|
+
supplyAlert: data.supplyAlert ?? {
|
|
886
|
+
count: 0,
|
|
887
|
+
percentage: 0,
|
|
888
|
+
},
|
|
889
|
+
taskCompleted: data.taskCompleted ?? {
|
|
890
|
+
count: 0,
|
|
891
|
+
percentage: 0,
|
|
892
|
+
},
|
|
893
|
+
landscapeSchedule: data.landscapeSchedule ?? {
|
|
894
|
+
count: 0,
|
|
895
|
+
percentage: 0,
|
|
896
|
+
},
|
|
897
|
+
activityChart: {
|
|
898
|
+
labels: weeklyActivity.map((item: any) => item.day),
|
|
899
|
+
workOrders: weeklyActivity.map((item: any) => item.workOrder),
|
|
900
|
+
taskCompleted: weeklyActivity.map((item: any) => item.taskCompleted),
|
|
901
|
+
},
|
|
902
|
+
taskScheduleItems: taskScheduleReq.value?.items ?? [],
|
|
903
|
+
attendanceItems: attendanceReq.value?.items ?? [],
|
|
904
|
+
feedbackItems: feedbacksReq.value?.items ?? [],
|
|
905
|
+
};
|
|
447
906
|
});
|
|
448
907
|
|
|
449
908
|
const emit = defineEmits<{
|
|
@@ -469,35 +928,55 @@ const rangeOptions = [
|
|
|
469
928
|
{ label: "7d", value: "This Week" },
|
|
470
929
|
{ label: "30d", value: "This Month" },
|
|
471
930
|
];
|
|
472
|
-
const defaultVisitorOptions = [
|
|
931
|
+
const defaultVisitorOptions = [
|
|
932
|
+
"All",
|
|
933
|
+
"Drive-In",
|
|
934
|
+
"Walk-In",
|
|
935
|
+
"Pick-up",
|
|
936
|
+
"Drop-Off",
|
|
937
|
+
"Delivery",
|
|
938
|
+
"Contractor",
|
|
939
|
+
];
|
|
473
940
|
|
|
474
941
|
const dashboardTitle = computed(() => {
|
|
475
942
|
if (props.title) return props.title;
|
|
476
|
-
const appName = String(
|
|
943
|
+
const appName = String(
|
|
944
|
+
runtimeConfig.public.APP_NAME || runtimeConfig.public.APP || ""
|
|
945
|
+
);
|
|
477
946
|
return `${getServiceDisplayName(appName)} - Dashboard`;
|
|
478
947
|
});
|
|
479
|
-
const appKey = computed(() =>
|
|
948
|
+
const appKey = computed(() =>
|
|
949
|
+
String(
|
|
950
|
+
runtimeConfig.public.APP || runtimeConfig.public.APP_NAME || ""
|
|
951
|
+
).toLowerCase()
|
|
952
|
+
);
|
|
480
953
|
const isSecurityDashboard = computed(() => appKey.value.includes("security"));
|
|
481
|
-
const primaryListConfig = computed(() =>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
954
|
+
const primaryListConfig = computed(() =>
|
|
955
|
+
isSecurityDashboard.value
|
|
956
|
+
? {
|
|
957
|
+
title: "Active Patrol",
|
|
958
|
+
route: "virtualPatrol",
|
|
959
|
+
keys: ["activePatrolItems", "patrolItems"],
|
|
960
|
+
}
|
|
961
|
+
: {
|
|
962
|
+
title: "Today's Task Schedule",
|
|
963
|
+
route: "scheduleTask",
|
|
964
|
+
keys: [
|
|
965
|
+
"taskScheduleItems",
|
|
966
|
+
"scheduleTaskItems",
|
|
967
|
+
"scheduledTaskItems",
|
|
968
|
+
"cleaningScheduleItems",
|
|
969
|
+
"tasks",
|
|
970
|
+
],
|
|
971
|
+
}
|
|
972
|
+
);
|
|
973
|
+
const staffStatusRoute = computed(() =>
|
|
974
|
+
isSecurityDashboard.value ? "manpower" : "attendance"
|
|
975
|
+
);
|
|
493
976
|
|
|
494
977
|
const defaultVisibleWidgetKeys = computed(() => [
|
|
495
|
-
...
|
|
496
|
-
|
|
497
|
-
"taskSchedule",
|
|
498
|
-
"staffStatus",
|
|
499
|
-
"feedbacks",
|
|
500
|
-
"recentActivity",
|
|
978
|
+
...resolvedCardValues.value.map((card) => card.key),
|
|
979
|
+
...resolvedExtraWidgetKeys.value,
|
|
501
980
|
]);
|
|
502
981
|
|
|
503
982
|
watch(
|
|
@@ -510,18 +989,26 @@ watch(
|
|
|
510
989
|
return;
|
|
511
990
|
}
|
|
512
991
|
|
|
513
|
-
visibleWidgetKeys.value = visibleWidgetKeys.value.filter((key) =>
|
|
514
|
-
|
|
992
|
+
visibleWidgetKeys.value = visibleWidgetKeys.value.filter((key) =>
|
|
993
|
+
nextKeys.includes(key)
|
|
994
|
+
);
|
|
995
|
+
draftVisibleWidgetKeys.value = draftVisibleWidgetKeys.value.filter((key) =>
|
|
996
|
+
nextKeys.includes(key)
|
|
997
|
+
);
|
|
515
998
|
},
|
|
516
|
-
{ immediate: true }
|
|
999
|
+
{ immediate: true }
|
|
517
1000
|
);
|
|
518
1001
|
|
|
519
1002
|
watch(selectedRange, (period) => {
|
|
1003
|
+
if (props.serviceType) {
|
|
1004
|
+
const apiPeriod = periodValue[period];
|
|
1005
|
+
if (apiPeriod) currentPeriod.value = apiPeriod;
|
|
1006
|
+
}
|
|
520
1007
|
emit("updatePeriod", { key: "__all__", period });
|
|
521
1008
|
});
|
|
522
1009
|
|
|
523
1010
|
const customizeWidgets = computed(() => [
|
|
524
|
-
...
|
|
1011
|
+
...resolvedCardValues.value.map((card) => ({
|
|
525
1012
|
key: card.key,
|
|
526
1013
|
title: card.title,
|
|
527
1014
|
description: `Show ${card.title} metrics for the selected period.`,
|
|
@@ -538,7 +1025,7 @@ const customizeWidgets = computed(() => [
|
|
|
538
1025
|
},
|
|
539
1026
|
{
|
|
540
1027
|
key: "staffStatus",
|
|
541
|
-
title: "Staff
|
|
1028
|
+
title: "Staff Total Attendance",
|
|
542
1029
|
description: "See who is on duty, on break, or absent.",
|
|
543
1030
|
},
|
|
544
1031
|
{
|
|
@@ -557,43 +1044,58 @@ const filteredCustomizeWidgets = computed(() => {
|
|
|
557
1044
|
const search = widgetSearch.value.trim().toLowerCase();
|
|
558
1045
|
if (!search) return customizeWidgets.value;
|
|
559
1046
|
|
|
560
|
-
return customizeWidgets.value.filter(
|
|
561
|
-
widget
|
|
562
|
-
|
|
563
|
-
|
|
1047
|
+
return customizeWidgets.value.filter(
|
|
1048
|
+
(widget) =>
|
|
1049
|
+
widget.title.toLowerCase().includes(search) ||
|
|
1050
|
+
widget.description.toLowerCase().includes(search)
|
|
1051
|
+
);
|
|
564
1052
|
});
|
|
565
|
-
const visitorItems = computed(() =>
|
|
566
|
-
|
|
1053
|
+
const visitorItems = computed(() =>
|
|
1054
|
+
getDashboardArray(["visitorItems", "visitorsList", "visitorTransactionsList"])
|
|
1055
|
+
);
|
|
1056
|
+
const visitorOptions = computed(() =>
|
|
1057
|
+
buildFilterOptions(
|
|
1058
|
+
defaultVisitorOptions,
|
|
1059
|
+
visitorItems.value,
|
|
1060
|
+
getVisitorFilterValue
|
|
1061
|
+
)
|
|
1062
|
+
);
|
|
567
1063
|
|
|
568
|
-
const kpiCards = computed(() =>
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1064
|
+
const kpiCards = computed(() =>
|
|
1065
|
+
resolvedCardValues.value.map((mapping, index) => {
|
|
1066
|
+
const metric =
|
|
1067
|
+
mapping.key === "visitors" && isSecurityDashboard.value
|
|
1068
|
+
? getFilteredVisitorMetric()
|
|
1069
|
+
: getMetric([mapping.key]);
|
|
1070
|
+
const value = getMetricValue(metric, mapping.key);
|
|
573
1071
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
1072
|
+
return {
|
|
1073
|
+
key: mapping.key,
|
|
1074
|
+
title: mapping.title,
|
|
1075
|
+
icon: mapping.icon,
|
|
1076
|
+
tint: getCardTint(mapping.color, index),
|
|
1077
|
+
color: mapping.color || getCardColor(index),
|
|
1078
|
+
value: typeof value === "string" ? value : formatNumber(value),
|
|
1079
|
+
percentage: toNumber(metric.percentage),
|
|
1080
|
+
meta: getMetricMeta(metric),
|
|
1081
|
+
filter:
|
|
1082
|
+
mapping.key === "visitors" && isSecurityDashboard.value
|
|
1083
|
+
? {
|
|
1084
|
+
model: visitorType.value,
|
|
1085
|
+
options: visitorOptions.value,
|
|
1086
|
+
set: (option: string) => {
|
|
1087
|
+
visitorType.value = option;
|
|
1088
|
+
filterMenus.visitors = false;
|
|
1089
|
+
},
|
|
1090
|
+
}
|
|
1091
|
+
: undefined,
|
|
1092
|
+
};
|
|
1093
|
+
})
|
|
1094
|
+
);
|
|
595
1095
|
|
|
596
|
-
const visibleKpiCards = computed(() =>
|
|
1096
|
+
const visibleKpiCards = computed(() =>
|
|
1097
|
+
kpiCards.value.filter((card) => isWidgetVisible(card.key))
|
|
1098
|
+
);
|
|
597
1099
|
|
|
598
1100
|
const activityTitle = computed(() => {
|
|
599
1101
|
if (selectedRange.value === "Today") return "Today's Activity";
|
|
@@ -601,241 +1103,111 @@ const activityTitle = computed(() => {
|
|
|
601
1103
|
return "This Week Activity";
|
|
602
1104
|
});
|
|
603
1105
|
|
|
604
|
-
const activityChart = computed(() =>
|
|
1106
|
+
const activityChart = computed(() =>
|
|
1107
|
+
normalizeActivityChart(getActivityChartSource())
|
|
1108
|
+
);
|
|
605
1109
|
const chartLabels = computed<string[]>(() => {
|
|
606
1110
|
const labels = activityChart.value.labels;
|
|
607
1111
|
if (labels.length) return labels;
|
|
608
1112
|
|
|
609
|
-
if (selectedRange.value === "Today")
|
|
610
|
-
|
|
1113
|
+
if (selectedRange.value === "Today")
|
|
1114
|
+
return ["12 AM", "4 AM", "8 AM", "12 PM", "4 PM", "8 PM"];
|
|
1115
|
+
if (selectedRange.value === "This Month")
|
|
1116
|
+
return ["Week 1", "Week 2", "Week 3", "Week 4"];
|
|
611
1117
|
return ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
612
1118
|
});
|
|
613
|
-
const primarySeries = computed(() =>
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const
|
|
1119
|
+
const primarySeries = computed(() =>
|
|
1120
|
+
normalizeSeries(activityChart.value.primary, chartLabels.value.length)
|
|
1121
|
+
);
|
|
1122
|
+
const secondarySeries = computed(() =>
|
|
1123
|
+
normalizeSeries(activityChart.value.secondary, chartLabels.value.length)
|
|
1124
|
+
);
|
|
1125
|
+
const primaryLegendLabel = computed(
|
|
1126
|
+
() => activityChart.value.primaryLabel || "Work Order"
|
|
1127
|
+
);
|
|
1128
|
+
const secondaryLegendLabel = computed(
|
|
1129
|
+
() => activityChart.value.secondaryLabel || "Feedback"
|
|
1130
|
+
);
|
|
1131
|
+
const chartMax = computed(() =>
|
|
1132
|
+
Math.max(1, ...primarySeries.value, ...secondarySeries.value)
|
|
1133
|
+
);
|
|
1134
|
+
const hasChartData = computed(
|
|
1135
|
+
() => primarySeries.value.some(Boolean) || secondarySeries.value.some(Boolean)
|
|
1136
|
+
);
|
|
1137
|
+
const chartTicks = [28, 66.4, 104.8, 143.2, 181.6, 220];
|
|
1138
|
+
|
|
1139
|
+
const chartYLabels = computed(() => {
|
|
1140
|
+
const max = chartMax.value;
|
|
1141
|
+
return [
|
|
1142
|
+
{ text: "0", y: 220 },
|
|
1143
|
+
{ text: `$${Math.round(max * 0.2)}`, y: 181.6 },
|
|
1144
|
+
{ text: `$${Math.round(max * 0.4)}`, y: 143.2 },
|
|
1145
|
+
{ text: `$${Math.round(max * 0.6)}`, y: 104.8 },
|
|
1146
|
+
{ text: `$${Math.round(max * 0.8)}`, y: 66.4 },
|
|
1147
|
+
{ text: `$${Math.round(max)}`, y: 28 },
|
|
1148
|
+
];
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
function buildSmoothPath(points: { x: number; y: number }[]) {
|
|
1152
|
+
if (!points || points.length === 0) return "";
|
|
1153
|
+
let d = `M ${points[0].x},${points[0].y}`;
|
|
1154
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1155
|
+
const p1 = points[i];
|
|
1156
|
+
const p2 = points[i + 1];
|
|
1157
|
+
const cp1x = p1.x + (p2.x - p1.x) / 2;
|
|
1158
|
+
const cp1y = p1.y;
|
|
1159
|
+
const cp2x = p1.x + (p2.x - p1.x) / 2;
|
|
1160
|
+
const cp2y = p2.y;
|
|
1161
|
+
d += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p2.x},${p2.y}`;
|
|
1162
|
+
}
|
|
1163
|
+
return d;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
620
1166
|
const primaryDots = computed(() => buildChartDots(primarySeries.value));
|
|
621
1167
|
const secondaryDots = computed(() => buildChartDots(secondarySeries.value));
|
|
622
|
-
const
|
|
623
|
-
const
|
|
1168
|
+
const primaryPath = computed(() => buildSmoothPath(primaryDots.value));
|
|
1169
|
+
const secondaryPath = computed(() => buildSmoothPath(secondaryDots.value));
|
|
624
1170
|
const chartAxisLabels = computed(() => {
|
|
625
1171
|
const labels = chartLabels.value;
|
|
626
1172
|
const count = Math.max(1, labels.length - 1);
|
|
627
1173
|
|
|
628
1174
|
return labels.map((label, index) => ({
|
|
629
1175
|
text: label,
|
|
630
|
-
x: 54 + (
|
|
1176
|
+
x: 54 + (846 / count) * index,
|
|
631
1177
|
}));
|
|
632
1178
|
});
|
|
633
1179
|
|
|
634
|
-
const taskScheduleItems = computed<TaskItem[]>(() =>
|
|
635
|
-
...primaryListConfig.value.keys
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
"staffItems"
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
"feedbacks"
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
props: {
|
|
655
|
-
compact: Boolean,
|
|
656
|
-
},
|
|
657
|
-
setup(componentProps) {
|
|
658
|
-
return () => h("div", { class: ["empty-state", componentProps.compact ? "empty-state-compact" : ""] }, [
|
|
659
|
-
h("i", { class: "mdi mdi-database-search-outline" }),
|
|
660
|
-
h("span", "No data to display"),
|
|
661
|
-
]);
|
|
662
|
-
},
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
const DashboardPanel = defineComponent({
|
|
666
|
-
name: "DashboardPanel",
|
|
667
|
-
props: {
|
|
668
|
-
title: {
|
|
669
|
-
type: String,
|
|
670
|
-
required: true,
|
|
671
|
-
},
|
|
672
|
-
actionLabel: {
|
|
673
|
-
type: String,
|
|
674
|
-
default: "",
|
|
675
|
-
},
|
|
676
|
-
variant: {
|
|
677
|
-
type: String,
|
|
678
|
-
default: "",
|
|
679
|
-
},
|
|
680
|
-
},
|
|
681
|
-
emits: ["action"],
|
|
682
|
-
setup(componentProps, { slots, emit: componentEmit }) {
|
|
683
|
-
return () => h("div", {
|
|
684
|
-
class: [
|
|
685
|
-
"dashboard-card",
|
|
686
|
-
"dashboard-panel",
|
|
687
|
-
componentProps.variant ? `dashboard-panel-${componentProps.variant}` : "",
|
|
688
|
-
],
|
|
689
|
-
}, [
|
|
690
|
-
h("div", {
|
|
691
|
-
class: "panel-header",
|
|
692
|
-
style: {
|
|
693
|
-
alignItems: "center",
|
|
694
|
-
display: "flex",
|
|
695
|
-
flexDirection: "row",
|
|
696
|
-
justifyContent: "space-between",
|
|
697
|
-
width: "100%",
|
|
698
|
-
},
|
|
699
|
-
}, [
|
|
700
|
-
h("h2", {
|
|
701
|
-
style: {
|
|
702
|
-
color: "#132b43",
|
|
703
|
-
fontSize: componentProps.variant === "recent" ? "15px" : "15px",
|
|
704
|
-
fontWeight: "800",
|
|
705
|
-
letterSpacing: "0",
|
|
706
|
-
lineHeight: "1.2",
|
|
707
|
-
margin: "0",
|
|
708
|
-
textTransform: componentProps.variant === "recent" ? "uppercase" : "none",
|
|
709
|
-
},
|
|
710
|
-
}, componentProps.title),
|
|
711
|
-
componentProps.actionLabel
|
|
712
|
-
? h("button", {
|
|
713
|
-
class: "panel-action",
|
|
714
|
-
style: {
|
|
715
|
-
appearance: "none",
|
|
716
|
-
background: "transparent",
|
|
717
|
-
border: "0",
|
|
718
|
-
color: "#2196f3",
|
|
719
|
-
cursor: "pointer",
|
|
720
|
-
fontSize: "12px",
|
|
721
|
-
fontWeight: "800",
|
|
722
|
-
lineHeight: "1.2",
|
|
723
|
-
marginLeft: "auto",
|
|
724
|
-
padding: "0",
|
|
725
|
-
},
|
|
726
|
-
type: "button",
|
|
727
|
-
onClick: () => componentEmit("action"),
|
|
728
|
-
}, componentProps.actionLabel)
|
|
729
|
-
: null,
|
|
730
|
-
]),
|
|
731
|
-
h("div", { class: "panel-body" }, slots.default?.()),
|
|
732
|
-
]);
|
|
733
|
-
},
|
|
734
|
-
});
|
|
1180
|
+
const taskScheduleItems = computed<TaskItem[]>(() =>
|
|
1181
|
+
getDashboardArray([...primaryListConfig.value.keys])
|
|
1182
|
+
.map(normalizeTaskItem)
|
|
1183
|
+
.slice(0, 4)
|
|
1184
|
+
);
|
|
1185
|
+
const staffStatusItems = computed<StaffItem[]>(() =>
|
|
1186
|
+
getDashboardArray(["staffStatusItems", "attendanceItems", "staffItems"])
|
|
1187
|
+
.map(normalizeStaffItem)
|
|
1188
|
+
.slice(0, 4)
|
|
1189
|
+
);
|
|
1190
|
+
const feedbackItems = computed<FeedbackItem[]>(() =>
|
|
1191
|
+
getDashboardArray(["feedbackItems", "feedbacksList", "feedbacks"])
|
|
1192
|
+
.map(normalizeFeedbackItem)
|
|
1193
|
+
.slice(0, 4)
|
|
1194
|
+
);
|
|
1195
|
+
const recentActivities = computed<DashboardListItem[]>(() =>
|
|
1196
|
+
getDashboardArray(["recentActivities", "activityItems"])
|
|
1197
|
+
.map(normalizeActivityItem)
|
|
1198
|
+
.slice(0, 3)
|
|
1199
|
+
);
|
|
735
1200
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
},
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
},
|
|
747
|
-
alignTime: Boolean,
|
|
748
|
-
},
|
|
749
|
-
setup(componentProps) {
|
|
750
|
-
return () => componentProps.items.length
|
|
751
|
-
? h("div", {
|
|
752
|
-
class: "simple-list",
|
|
753
|
-
style: {
|
|
754
|
-
display: "grid",
|
|
755
|
-
width: "100%",
|
|
756
|
-
},
|
|
757
|
-
}, componentProps.items.map((item) => {
|
|
758
|
-
const tone = getActivityToneStyle(item.tone || "neutral");
|
|
759
|
-
|
|
760
|
-
return h("div", {
|
|
761
|
-
class: "simple-list-row",
|
|
762
|
-
key: item.id,
|
|
763
|
-
style: {
|
|
764
|
-
alignItems: "center",
|
|
765
|
-
background: "#fff",
|
|
766
|
-
borderBottom: "1px solid #e0e0e0",
|
|
767
|
-
display: "grid",
|
|
768
|
-
gap: "14px",
|
|
769
|
-
gridTemplateColumns: componentProps.alignTime ? "34px minmax(0, 1fr) 96px" : "34px minmax(0, 1fr)",
|
|
770
|
-
minHeight: "58px",
|
|
771
|
-
padding: "10px 0",
|
|
772
|
-
},
|
|
773
|
-
}, [
|
|
774
|
-
h("span", {
|
|
775
|
-
class: ["list-icon", `list-icon-${item.tone || "neutral"}`],
|
|
776
|
-
style: {
|
|
777
|
-
alignItems: "center",
|
|
778
|
-
background: tone.background,
|
|
779
|
-
borderRadius: "6px",
|
|
780
|
-
color: tone.color,
|
|
781
|
-
display: "inline-flex",
|
|
782
|
-
fontSize: "13px",
|
|
783
|
-
height: "25px",
|
|
784
|
-
justifyContent: "center",
|
|
785
|
-
width: "25px",
|
|
786
|
-
},
|
|
787
|
-
}, [
|
|
788
|
-
h("i", { class: `mdi ${item.icon || componentProps.emptyIcon}` }),
|
|
789
|
-
]),
|
|
790
|
-
h("div", {
|
|
791
|
-
class: "list-main",
|
|
792
|
-
style: {
|
|
793
|
-
minWidth: "0",
|
|
794
|
-
},
|
|
795
|
-
}, [
|
|
796
|
-
h("strong", {
|
|
797
|
-
style: {
|
|
798
|
-
color: "#132b43",
|
|
799
|
-
display: "block",
|
|
800
|
-
fontSize: "13px",
|
|
801
|
-
fontWeight: "700",
|
|
802
|
-
lineHeight: "1.3",
|
|
803
|
-
overflow: "hidden",
|
|
804
|
-
textOverflow: "ellipsis",
|
|
805
|
-
whiteSpace: "nowrap",
|
|
806
|
-
},
|
|
807
|
-
}, item.title),
|
|
808
|
-
item.subtitle ? h("p", {
|
|
809
|
-
style: {
|
|
810
|
-
color: "#888",
|
|
811
|
-
fontSize: "12px",
|
|
812
|
-
fontWeight: "500",
|
|
813
|
-
lineHeight: "1.35",
|
|
814
|
-
margin: "5px 0 0",
|
|
815
|
-
overflow: "hidden",
|
|
816
|
-
textOverflow: "ellipsis",
|
|
817
|
-
whiteSpace: "nowrap",
|
|
818
|
-
},
|
|
819
|
-
}, item.subtitle) : null,
|
|
820
|
-
]),
|
|
821
|
-
componentProps.alignTime
|
|
822
|
-
? h("span", {
|
|
823
|
-
class: "list-action",
|
|
824
|
-
style: {
|
|
825
|
-
color: "#888",
|
|
826
|
-
fontSize: "12px",
|
|
827
|
-
fontWeight: "500",
|
|
828
|
-
justifySelf: "end",
|
|
829
|
-
textAlign: "right",
|
|
830
|
-
whiteSpace: "nowrap",
|
|
831
|
-
},
|
|
832
|
-
}, item.time || "")
|
|
833
|
-
: null,
|
|
834
|
-
]);
|
|
835
|
-
}))
|
|
836
|
-
: h(EmptyDashboardState);
|
|
837
|
-
},
|
|
838
|
-
});
|
|
1201
|
+
function getToneColor(tone: string) {
|
|
1202
|
+
const map: Record<string, { bg: string; text: string }> = {
|
|
1203
|
+
info: { bg: "blue-lighten-4", text: "blue-darken-3" },
|
|
1204
|
+
danger: { bg: "red-lighten-4", text: "red-darken-3" },
|
|
1205
|
+
success: { bg: "green-lighten-4", text: "green-darken-3" },
|
|
1206
|
+
warning: { bg: "orange-lighten-4", text: "orange-darken-3" },
|
|
1207
|
+
neutral: { bg: "grey-lighten-3", text: "blue-grey-darken-1" },
|
|
1208
|
+
};
|
|
1209
|
+
return map[tone] || map.neutral;
|
|
1210
|
+
}
|
|
839
1211
|
|
|
840
1212
|
function getServiceDisplayName(value: string) {
|
|
841
1213
|
const normalized = value.toLowerCase();
|
|
@@ -864,7 +1236,7 @@ function getServiceDisplayName(value: string) {
|
|
|
864
1236
|
}
|
|
865
1237
|
|
|
866
1238
|
function getMetric(keys: string[]): DashboardMetric {
|
|
867
|
-
const data =
|
|
1239
|
+
const data = finalDashboardData.value || {};
|
|
868
1240
|
for (const key of keys) {
|
|
869
1241
|
const value = data[key];
|
|
870
1242
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
@@ -892,17 +1264,39 @@ function getMetricValue(metric: DashboardMetric, key?: string) {
|
|
|
892
1264
|
function getMetricMeta(metric: DashboardMetric) {
|
|
893
1265
|
const parts: string[] = [];
|
|
894
1266
|
|
|
895
|
-
if (typeof metric.overdue !== "undefined")
|
|
896
|
-
|
|
897
|
-
if (typeof metric.
|
|
898
|
-
|
|
899
|
-
if (typeof metric.
|
|
900
|
-
|
|
901
|
-
|
|
1267
|
+
if (typeof metric.overdue !== "undefined")
|
|
1268
|
+
parts.push(`${formatNumber(toNumber(metric.overdue))} overdue`);
|
|
1269
|
+
if (typeof metric.inProgress !== "undefined")
|
|
1270
|
+
parts.push(`${formatNumber(toNumber(metric.inProgress))} in progress`);
|
|
1271
|
+
if (typeof metric.pending !== "undefined")
|
|
1272
|
+
parts.push(`${formatNumber(toNumber(metric.pending))} pending`);
|
|
1273
|
+
if (typeof metric.highSeverity !== "undefined")
|
|
1274
|
+
parts.push(`${formatNumber(toNumber(metric.highSeverity))} high severity`);
|
|
1275
|
+
if (typeof metric.currentlyOnSite !== "undefined")
|
|
1276
|
+
parts.push(
|
|
1277
|
+
`${formatNumber(toNumber(metric.currentlyOnSite))} currently on site`
|
|
1278
|
+
);
|
|
1279
|
+
if (
|
|
1280
|
+
typeof (metric as Record<string, any>).missedCheckpoints !== "undefined" ||
|
|
1281
|
+
typeof (metric as Record<string, any>).missed !== "undefined"
|
|
1282
|
+
) {
|
|
1283
|
+
parts.push(
|
|
1284
|
+
`${formatNumber(
|
|
1285
|
+
toNumber(
|
|
1286
|
+
(metric as Record<string, any>).missedCheckpoints ??
|
|
1287
|
+
(metric as Record<string, any>).missed
|
|
1288
|
+
)
|
|
1289
|
+
)} checkpoint missed`
|
|
1290
|
+
);
|
|
902
1291
|
}
|
|
903
|
-
if (typeof metric.lowStock !== "undefined")
|
|
904
|
-
|
|
905
|
-
if (typeof metric.
|
|
1292
|
+
if (typeof metric.lowStock !== "undefined")
|
|
1293
|
+
parts.push(`${formatNumber(toNumber(metric.lowStock))} low stock`);
|
|
1294
|
+
if (typeof metric.outOfStock !== "undefined")
|
|
1295
|
+
parts.push(`${formatNumber(toNumber(metric.outOfStock))} out of stock`);
|
|
1296
|
+
if (
|
|
1297
|
+
typeof metric.completed !== "undefined" &&
|
|
1298
|
+
typeof metric.total !== "undefined"
|
|
1299
|
+
) {
|
|
906
1300
|
parts.push(`${formatNumber(toNumber(metric.completed))} completed`);
|
|
907
1301
|
}
|
|
908
1302
|
|
|
@@ -918,29 +1312,38 @@ function getCardTint(color: string, index: number) {
|
|
|
918
1312
|
if (!color) return tints[index % tints.length];
|
|
919
1313
|
|
|
920
1314
|
const normalized = color.toLowerCase();
|
|
921
|
-
if (normalized.includes("f44336") || normalized.includes("d32f2f"))
|
|
922
|
-
|
|
923
|
-
if (normalized.includes("
|
|
1315
|
+
if (normalized.includes("f44336") || normalized.includes("d32f2f"))
|
|
1316
|
+
return "#f8c6c3";
|
|
1317
|
+
if (normalized.includes("4caf50") || normalized.includes("43a047"))
|
|
1318
|
+
return "#d8f5e5";
|
|
1319
|
+
if (normalized.includes("ff9800") || normalized.includes("ffa726"))
|
|
1320
|
+
return "#ffe7c2";
|
|
924
1321
|
return "#dbeeff";
|
|
925
1322
|
}
|
|
926
1323
|
|
|
927
1324
|
function getActivityChartSource() {
|
|
928
|
-
const data =
|
|
929
|
-
return
|
|
1325
|
+
const data = finalDashboardData.value || {};
|
|
1326
|
+
return (
|
|
1327
|
+
data.activityChart ||
|
|
930
1328
|
data.statistic?.activityChart ||
|
|
931
1329
|
data.statistics?.activityChart ||
|
|
932
1330
|
data.statisticChart ||
|
|
933
1331
|
data.statisticsChart ||
|
|
934
1332
|
data.activityStatistics ||
|
|
935
1333
|
data.activityStatistic ||
|
|
936
|
-
{}
|
|
1334
|
+
{}
|
|
1335
|
+
);
|
|
937
1336
|
}
|
|
938
1337
|
|
|
939
1338
|
function normalizeActivityChart(source: any) {
|
|
940
1339
|
const labels = Array.isArray(source?.labels)
|
|
941
1340
|
? source.labels.map((label: any) => String(label))
|
|
942
1341
|
: [];
|
|
943
|
-
const primary = extractChartSeries(source, [
|
|
1342
|
+
const primary = extractChartSeries(source, [
|
|
1343
|
+
"workOrders",
|
|
1344
|
+
"workOrder",
|
|
1345
|
+
"work_orders",
|
|
1346
|
+
]);
|
|
944
1347
|
const secondaryCandidate = getSecondaryChartSeries(source);
|
|
945
1348
|
|
|
946
1349
|
return {
|
|
@@ -954,7 +1357,10 @@ function normalizeActivityChart(source: any) {
|
|
|
954
1357
|
|
|
955
1358
|
function getSecondaryChartSeries(source: any) {
|
|
956
1359
|
const candidates = [
|
|
957
|
-
{
|
|
1360
|
+
{
|
|
1361
|
+
label: "Task Completed",
|
|
1362
|
+
keys: ["taskCompleted", "completedTasks", "completed"],
|
|
1363
|
+
},
|
|
958
1364
|
{ label: "Incident", keys: ["incidents", "incident", "incidentReports"] },
|
|
959
1365
|
{ label: "Feedback", keys: ["feedbacks", "feedback", "feedbackTickets"] },
|
|
960
1366
|
];
|
|
@@ -975,7 +1381,9 @@ function extractChartSeries(source: any, keys: string[]) {
|
|
|
975
1381
|
|
|
976
1382
|
if (Array.isArray(source?.datasets)) {
|
|
977
1383
|
const dataset = source.datasets.find((item: any) => {
|
|
978
|
-
const keyText = String(
|
|
1384
|
+
const keyText = String(
|
|
1385
|
+
item?.key || item?.label || item?.name || ""
|
|
1386
|
+
).toLowerCase();
|
|
979
1387
|
return keys.some((key) => keyText.includes(key.toLowerCase()));
|
|
980
1388
|
});
|
|
981
1389
|
if (Array.isArray(dataset?.data)) return dataset.data.map(toNumber);
|
|
@@ -986,20 +1394,22 @@ function extractChartSeries(source: any, keys: string[]) {
|
|
|
986
1394
|
|
|
987
1395
|
function normalizeSeries(value: any, targetLength: number) {
|
|
988
1396
|
const source = Array.isArray(value) ? value : [];
|
|
989
|
-
return Array.from({ length: targetLength }, (_, index) =>
|
|
1397
|
+
return Array.from({ length: targetLength }, (_, index) =>
|
|
1398
|
+
toNumber(source[index])
|
|
1399
|
+
);
|
|
990
1400
|
}
|
|
991
1401
|
|
|
992
1402
|
function buildChartDots(series: number[]) {
|
|
993
1403
|
const count = Math.max(1, series.length - 1);
|
|
994
1404
|
|
|
995
1405
|
return series.map((value, index) => ({
|
|
996
|
-
x: 54 + (
|
|
997
|
-
y: 220 - (
|
|
1406
|
+
x: 54 + (846 / count) * index,
|
|
1407
|
+
y: 220 - (toNumber(value) / chartMax.value) * 184,
|
|
998
1408
|
}));
|
|
999
1409
|
}
|
|
1000
1410
|
|
|
1001
1411
|
function getDashboardArray(keys: string[]) {
|
|
1002
|
-
const data =
|
|
1412
|
+
const data = finalDashboardData.value || {};
|
|
1003
1413
|
|
|
1004
1414
|
for (const key of keys) {
|
|
1005
1415
|
const value = data[key];
|
|
@@ -1021,23 +1431,32 @@ function getFilteredVisitorMetric(): DashboardMetric {
|
|
|
1021
1431
|
return {
|
|
1022
1432
|
...metric,
|
|
1023
1433
|
count: toNumber(metric.count ?? items.length),
|
|
1024
|
-
currentlyOnSite: toNumber(
|
|
1434
|
+
currentlyOnSite: toNumber(
|
|
1435
|
+
metric.currentlyOnSite ??
|
|
1436
|
+
items.filter(
|
|
1437
|
+
(item: any) => !isClosedStatus(item.status) && !item.checkOut
|
|
1438
|
+
).length
|
|
1439
|
+
),
|
|
1025
1440
|
};
|
|
1026
1441
|
}
|
|
1027
1442
|
|
|
1028
|
-
const filtered = items.filter(
|
|
1443
|
+
const filtered = items.filter(
|
|
1444
|
+
(item: any) => getVisitorFilterValue(item) === selected
|
|
1445
|
+
);
|
|
1029
1446
|
|
|
1030
1447
|
return {
|
|
1031
1448
|
...metric,
|
|
1032
1449
|
count: filtered.length,
|
|
1033
|
-
currentlyOnSite: filtered.filter(
|
|
1450
|
+
currentlyOnSite: filtered.filter(
|
|
1451
|
+
(item: any) => !isClosedStatus(item.status) && !item.checkOut
|
|
1452
|
+
).length,
|
|
1034
1453
|
};
|
|
1035
1454
|
}
|
|
1036
1455
|
|
|
1037
1456
|
function buildFilterOptions(
|
|
1038
1457
|
defaults: string[],
|
|
1039
1458
|
items: Record<string, any>[],
|
|
1040
|
-
getValue: (item: Record<string, any>) => string
|
|
1459
|
+
getValue: (item: Record<string, any>) => string
|
|
1041
1460
|
) {
|
|
1042
1461
|
const options = new Set(defaults);
|
|
1043
1462
|
items.forEach((item) => {
|
|
@@ -1049,7 +1468,9 @@ function buildFilterOptions(
|
|
|
1049
1468
|
}
|
|
1050
1469
|
|
|
1051
1470
|
function getVisitorFilterValue(item: Record<string, any>) {
|
|
1052
|
-
const raw = String(
|
|
1471
|
+
const raw = String(
|
|
1472
|
+
item.type || item.visitorType || item.category || ""
|
|
1473
|
+
).toLowerCase();
|
|
1053
1474
|
if (raw.includes("contractor")) return "Contractor";
|
|
1054
1475
|
if (raw.includes("walk")) return "Walk-In";
|
|
1055
1476
|
if (raw.includes("delivery")) return "Delivery";
|
|
@@ -1060,38 +1481,95 @@ function getVisitorFilterValue(item: Record<string, any>) {
|
|
|
1060
1481
|
}
|
|
1061
1482
|
|
|
1062
1483
|
function normalizeTaskItem(item: Record<string, any>, index: number): TaskItem {
|
|
1063
|
-
const title =
|
|
1064
|
-
|
|
1065
|
-
|
|
1484
|
+
const title =
|
|
1485
|
+
item.title ||
|
|
1486
|
+
item.subject ||
|
|
1487
|
+
item.name ||
|
|
1488
|
+
item.taskName ||
|
|
1489
|
+
item.description ||
|
|
1490
|
+
"Task";
|
|
1491
|
+
const subtitle =
|
|
1492
|
+
item.subtitle ||
|
|
1493
|
+
item.area ||
|
|
1494
|
+
item.location ||
|
|
1495
|
+
item.asset ||
|
|
1496
|
+
item.category ||
|
|
1497
|
+
"";
|
|
1498
|
+
const person =
|
|
1499
|
+
item.person ||
|
|
1500
|
+
item.assigneeName ||
|
|
1501
|
+
item.assignedTo?.name ||
|
|
1502
|
+
item.staff?.name ||
|
|
1503
|
+
item.createdBy?.name ||
|
|
1504
|
+
"Unassigned";
|
|
1066
1505
|
|
|
1067
1506
|
return {
|
|
1068
1507
|
id: String(item.id || item._id || `${title}-${index}`),
|
|
1069
1508
|
title,
|
|
1070
1509
|
subtitle,
|
|
1071
1510
|
person,
|
|
1072
|
-
status: normalizeStatus(
|
|
1511
|
+
status: normalizeStatus(
|
|
1512
|
+
item.status || item.state || item.taskStatus || "To-Do"
|
|
1513
|
+
),
|
|
1514
|
+
time:
|
|
1515
|
+
item.time ||
|
|
1516
|
+
item.startTime ||
|
|
1517
|
+
item.scheduleTime ||
|
|
1518
|
+
formatActivityTime(item),
|
|
1073
1519
|
raw: item,
|
|
1074
1520
|
};
|
|
1075
1521
|
}
|
|
1076
1522
|
|
|
1077
|
-
function normalizeStaffItem(
|
|
1078
|
-
|
|
1079
|
-
|
|
1523
|
+
function normalizeStaffItem(
|
|
1524
|
+
item: Record<string, any>,
|
|
1525
|
+
index: number
|
|
1526
|
+
): StaffItem {
|
|
1527
|
+
const name =
|
|
1528
|
+
item.name ||
|
|
1529
|
+
item.employeeName ||
|
|
1530
|
+
item.user?.userName ||
|
|
1531
|
+
item.staff?.userName ||
|
|
1532
|
+
item.userName;
|
|
1533
|
+
const role =
|
|
1534
|
+
item.role ||
|
|
1535
|
+
item.userRole ||
|
|
1536
|
+
item.userrole ||
|
|
1537
|
+
item.jobTitle ||
|
|
1538
|
+
item.designation ||
|
|
1539
|
+
item.position ||
|
|
1540
|
+
item.type;
|
|
1080
1541
|
|
|
1081
1542
|
return {
|
|
1082
1543
|
id: String(item.id || item._id || `${name}-${index}`),
|
|
1083
1544
|
name,
|
|
1084
1545
|
role,
|
|
1085
|
-
status: normalizeStatus(
|
|
1546
|
+
status: normalizeStatus(
|
|
1547
|
+
item.statusLabel ||
|
|
1548
|
+
item.status ||
|
|
1549
|
+
item.currentStatus ||
|
|
1550
|
+
item.attendanceStatus ||
|
|
1551
|
+
"Off duty"
|
|
1552
|
+
),
|
|
1553
|
+
totalHours: item.totalHours || 0,
|
|
1086
1554
|
raw: item,
|
|
1087
1555
|
};
|
|
1088
1556
|
}
|
|
1089
1557
|
|
|
1090
|
-
function normalizeFeedbackItem(
|
|
1558
|
+
function normalizeFeedbackItem(
|
|
1559
|
+
item: Record<string, any>,
|
|
1560
|
+
index: number
|
|
1561
|
+
): FeedbackItem {
|
|
1091
1562
|
const title = item.title || item.subject || item.description || "Feedback";
|
|
1092
1563
|
const subject = item.category || item.type || item.service || "Subject";
|
|
1093
1564
|
const app = item.app || item.source || item.channel || "";
|
|
1094
|
-
const person =
|
|
1565
|
+
const person =
|
|
1566
|
+
item.createdByName ||
|
|
1567
|
+
item.person ||
|
|
1568
|
+
item.userName ||
|
|
1569
|
+
item.submittedBy ||
|
|
1570
|
+
item.createdBy?.name ||
|
|
1571
|
+
item.user?.name ||
|
|
1572
|
+
"Unassigned";
|
|
1095
1573
|
|
|
1096
1574
|
return {
|
|
1097
1575
|
id: String(item.id || item._id || `${title}-${index}`),
|
|
@@ -1103,9 +1581,18 @@ function normalizeFeedbackItem(item: Record<string, any>, index: number): Feedba
|
|
|
1103
1581
|
};
|
|
1104
1582
|
}
|
|
1105
1583
|
|
|
1106
|
-
function normalizeActivityItem(
|
|
1107
|
-
|
|
1108
|
-
|
|
1584
|
+
function normalizeActivityItem(
|
|
1585
|
+
item: Record<string, any>,
|
|
1586
|
+
index: number
|
|
1587
|
+
): DashboardListItem {
|
|
1588
|
+
const title =
|
|
1589
|
+
item.title ||
|
|
1590
|
+
item.subject ||
|
|
1591
|
+
item.description ||
|
|
1592
|
+
item.message ||
|
|
1593
|
+
"Activity";
|
|
1594
|
+
const subtitle =
|
|
1595
|
+
item.subtitle || item.type || item.category || item.status || "";
|
|
1109
1596
|
|
|
1110
1597
|
return {
|
|
1111
1598
|
id: String(item.id || item._id || `${title}-${index}`),
|
|
@@ -1125,7 +1612,11 @@ function formatActivityTime(item: Record<string, any>) {
|
|
|
1125
1612
|
const rawText = String(rawTime || "");
|
|
1126
1613
|
|
|
1127
1614
|
if (!rawText) return "";
|
|
1128
|
-
if (
|
|
1615
|
+
if (
|
|
1616
|
+
/\b(ago|now|today|yesterday|min|mins|hr|hrs|hour|hours|day|days)\b/i.test(
|
|
1617
|
+
rawText
|
|
1618
|
+
)
|
|
1619
|
+
) {
|
|
1129
1620
|
return rawText;
|
|
1130
1621
|
}
|
|
1131
1622
|
|
|
@@ -1160,7 +1651,14 @@ function getActivityToneStyle(tone: string) {
|
|
|
1160
1651
|
}
|
|
1161
1652
|
|
|
1162
1653
|
function getItemTime(item: Record<string, any>) {
|
|
1163
|
-
return
|
|
1654
|
+
return (
|
|
1655
|
+
item.createdAt ||
|
|
1656
|
+
item.updatedAt ||
|
|
1657
|
+
item.date ||
|
|
1658
|
+
item.time ||
|
|
1659
|
+
item.created_on ||
|
|
1660
|
+
item.updated_on
|
|
1661
|
+
);
|
|
1164
1662
|
}
|
|
1165
1663
|
|
|
1166
1664
|
function normalizeStatus(value: any) {
|
|
@@ -1174,29 +1672,53 @@ function normalizeStatus(value: any) {
|
|
|
1174
1672
|
|
|
1175
1673
|
function getStatusTone(status: any) {
|
|
1176
1674
|
const normalized = String(status || "").toLowerCase();
|
|
1177
|
-
if (
|
|
1178
|
-
|
|
1179
|
-
|
|
1675
|
+
if (
|
|
1676
|
+
normalized.includes("complete") ||
|
|
1677
|
+
normalized.includes("done") ||
|
|
1678
|
+
normalized.includes("resolved") ||
|
|
1679
|
+
normalized.includes("closed")
|
|
1680
|
+
)
|
|
1681
|
+
return "success";
|
|
1682
|
+
if (
|
|
1683
|
+
normalized.includes("progress") ||
|
|
1684
|
+
normalized.includes("active") ||
|
|
1685
|
+
normalized.includes("ongoing")
|
|
1686
|
+
)
|
|
1687
|
+
return "info";
|
|
1688
|
+
if (
|
|
1689
|
+
normalized.includes("overdue") ||
|
|
1690
|
+
normalized.includes("urgent") ||
|
|
1691
|
+
normalized.includes("high")
|
|
1692
|
+
)
|
|
1693
|
+
return "danger";
|
|
1180
1694
|
return "neutral";
|
|
1181
1695
|
}
|
|
1182
1696
|
|
|
1183
1697
|
function getStaffTone(status: any) {
|
|
1184
1698
|
const normalized = String(status || "").toLowerCase();
|
|
1185
|
-
if (
|
|
1699
|
+
if (
|
|
1700
|
+
normalized.includes("duty") ||
|
|
1701
|
+
normalized.includes("present") ||
|
|
1702
|
+
normalized.includes("active")
|
|
1703
|
+
)
|
|
1704
|
+
return "success";
|
|
1186
1705
|
if (normalized.includes("break")) return "warning";
|
|
1187
|
-
if (normalized.includes("absent") || normalized.includes("off"))
|
|
1706
|
+
if (normalized.includes("absent") || normalized.includes("off"))
|
|
1707
|
+
return "danger";
|
|
1188
1708
|
return "neutral";
|
|
1189
1709
|
}
|
|
1190
1710
|
|
|
1191
1711
|
function isClosedStatus(status: any) {
|
|
1192
1712
|
const normalized = String(status || "").toLowerCase();
|
|
1193
|
-
return
|
|
1713
|
+
return (
|
|
1714
|
+
normalized.includes("complete") ||
|
|
1194
1715
|
normalized.includes("cancel") ||
|
|
1195
1716
|
normalized.includes("reject") ||
|
|
1196
1717
|
normalized.includes("closed") ||
|
|
1197
1718
|
normalized.includes("deleted") ||
|
|
1198
1719
|
normalized.includes("check-out") ||
|
|
1199
|
-
normalized.includes("checkout")
|
|
1720
|
+
normalized.includes("checkout")
|
|
1721
|
+
);
|
|
1200
1722
|
}
|
|
1201
1723
|
|
|
1202
1724
|
function isWidgetVisible(key: string) {
|
|
@@ -1272,7 +1794,11 @@ function getInitials(name: string) {
|
|
|
1272
1794
|
.split(/\s+/)
|
|
1273
1795
|
.filter(Boolean);
|
|
1274
1796
|
if (!parts.length) return "?";
|
|
1275
|
-
return parts
|
|
1797
|
+
return parts
|
|
1798
|
+
.slice(0, 2)
|
|
1799
|
+
.map((part) => part[0])
|
|
1800
|
+
.join("")
|
|
1801
|
+
.toUpperCase();
|
|
1276
1802
|
}
|
|
1277
1803
|
|
|
1278
1804
|
function formatNumber(value: number) {
|
|
@@ -1304,797 +1830,3 @@ function formatRelativeTime(value: any) {
|
|
|
1304
1830
|
return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
|
|
1305
1831
|
}
|
|
1306
1832
|
</script>
|
|
1307
|
-
|
|
1308
|
-
<style scoped>
|
|
1309
|
-
.service-dashboard {
|
|
1310
|
-
color: #132b43;
|
|
1311
|
-
padding-bottom: 32px;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
.dashboard-header {
|
|
1315
|
-
align-items: flex-start;
|
|
1316
|
-
display: flex;
|
|
1317
|
-
gap: 20px;
|
|
1318
|
-
justify-content: space-between;
|
|
1319
|
-
margin-bottom: 24px;
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
.dashboard-title {
|
|
1323
|
-
color: #0b2f47;
|
|
1324
|
-
font-size: 28px;
|
|
1325
|
-
font-weight: 800;
|
|
1326
|
-
letter-spacing: 0;
|
|
1327
|
-
line-height: 1.2;
|
|
1328
|
-
margin: 0;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
.site-label {
|
|
1332
|
-
align-items: center;
|
|
1333
|
-
color: #6d7d8b;
|
|
1334
|
-
display: flex;
|
|
1335
|
-
font-size: 13px;
|
|
1336
|
-
gap: 6px;
|
|
1337
|
-
margin-top: 8px;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
.header-actions {
|
|
1341
|
-
align-items: center;
|
|
1342
|
-
display: flex;
|
|
1343
|
-
flex-wrap: wrap;
|
|
1344
|
-
gap: 12px;
|
|
1345
|
-
justify-content: flex-end;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
.action-button {
|
|
1349
|
-
border-color: #d9e1e8;
|
|
1350
|
-
border-radius: 6px;
|
|
1351
|
-
color: #102b40;
|
|
1352
|
-
height: 36px;
|
|
1353
|
-
font-weight: 700;
|
|
1354
|
-
letter-spacing: 0.04em;
|
|
1355
|
-
min-height: 36px;
|
|
1356
|
-
padding: 0 16px;
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
.action-button :deep(.v-btn__content) {
|
|
1360
|
-
font-size: 15px;
|
|
1361
|
-
line-height: 1;
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
.action-button :deep(.v-btn__prepend) {
|
|
1365
|
-
margin-inline: 0 8px;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
.action-button :deep(.v-btn__append) {
|
|
1369
|
-
margin-inline: 8px 0;
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
.range-toggle {
|
|
1373
|
-
border: 1px solid #d9e1e8;
|
|
1374
|
-
border-radius: 6px;
|
|
1375
|
-
height: 36px;
|
|
1376
|
-
min-height: 36px;
|
|
1377
|
-
overflow: hidden;
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
.range-toggle :deep(.v-btn) {
|
|
1381
|
-
border-radius: 0;
|
|
1382
|
-
color: #102b40;
|
|
1383
|
-
font-size: 14px;
|
|
1384
|
-
font-weight: 500;
|
|
1385
|
-
height: 34px;
|
|
1386
|
-
letter-spacing: 0.04em;
|
|
1387
|
-
min-height: 34px;
|
|
1388
|
-
min-width: 62px;
|
|
1389
|
-
padding: 0 20px;
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
.range-toggle :deep(.v-btn--active) {
|
|
1393
|
-
background: #e0e0e0;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
.range-toggle :deep(.v-btn__overlay) {
|
|
1397
|
-
opacity: 0;
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
.export-menu,
|
|
1401
|
-
.filter-menu {
|
|
1402
|
-
border-color: #d9e1e8;
|
|
1403
|
-
border-radius: 7px;
|
|
1404
|
-
box-shadow: 0 12px 30px rgba(16, 35, 54, 0.12);
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
.kpi-grid {
|
|
1408
|
-
margin-bottom: 14px;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
.dashboard-card {
|
|
1412
|
-
background: #fff;
|
|
1413
|
-
border: 1px solid #d9e0e7;
|
|
1414
|
-
border-radius: 8px;
|
|
1415
|
-
box-shadow: 0 1px 2px rgba(15, 35, 54, 0.04);
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
.kpi-card {
|
|
1419
|
-
display: flex;
|
|
1420
|
-
flex-direction: column;
|
|
1421
|
-
min-height: 158px;
|
|
1422
|
-
padding: 16px 18px 18px;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
.kpi-topline {
|
|
1426
|
-
align-items: flex-start;
|
|
1427
|
-
display: flex;
|
|
1428
|
-
justify-content: space-between;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
.kpi-icon {
|
|
1432
|
-
align-items: center;
|
|
1433
|
-
border-radius: 8px;
|
|
1434
|
-
display: inline-flex;
|
|
1435
|
-
height: 28px;
|
|
1436
|
-
justify-content: center;
|
|
1437
|
-
width: 28px;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
.kpi-icon :deep(.v-icon) {
|
|
1441
|
-
font-size: 15px;
|
|
1442
|
-
height: 15px;
|
|
1443
|
-
width: 15px;
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
.mini-filter {
|
|
1447
|
-
border-color: #d9e1e8;
|
|
1448
|
-
font-size: 12px;
|
|
1449
|
-
height: 26px;
|
|
1450
|
-
min-width: 50px;
|
|
1451
|
-
padding-inline: 10px 6px;
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
.mini-filter :deep(.v-btn__append) {
|
|
1455
|
-
margin-inline-start: 4px;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
.kpi-content {
|
|
1459
|
-
display: flex;
|
|
1460
|
-
flex-direction: column;
|
|
1461
|
-
margin-top: 26px;
|
|
1462
|
-
min-width: 0;
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
.kpi-label {
|
|
1466
|
-
color: #2a465a;
|
|
1467
|
-
font-size: 12px;
|
|
1468
|
-
font-weight: 800;
|
|
1469
|
-
letter-spacing: 0;
|
|
1470
|
-
line-height: 1.2;
|
|
1471
|
-
margin: 0 0 13px;
|
|
1472
|
-
text-transform: uppercase;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
.kpi-value-row {
|
|
1476
|
-
align-items: center;
|
|
1477
|
-
display: flex;
|
|
1478
|
-
min-height: 31px;
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
.kpi-value-row strong {
|
|
1482
|
-
color: #092f49;
|
|
1483
|
-
font-size: 30px;
|
|
1484
|
-
font-weight: 800;
|
|
1485
|
-
letter-spacing: 0;
|
|
1486
|
-
line-height: 1;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
.kpi-meta-row {
|
|
1490
|
-
align-items: center;
|
|
1491
|
-
display: flex;
|
|
1492
|
-
flex-wrap: wrap;
|
|
1493
|
-
gap: 8px;
|
|
1494
|
-
margin-top: 12px;
|
|
1495
|
-
min-height: 22px;
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
.kpi-meta-row :deep(.v-chip) {
|
|
1499
|
-
border-radius: 999px;
|
|
1500
|
-
font-size: 11px;
|
|
1501
|
-
font-weight: 800;
|
|
1502
|
-
height: 20px;
|
|
1503
|
-
padding-inline: 8px;
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
.kpi-meta-row :deep(.v-chip__prepend) {
|
|
1507
|
-
margin-inline-end: 3px;
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
.kpi-meta {
|
|
1511
|
-
color: #4f6576;
|
|
1512
|
-
font-size: 12px;
|
|
1513
|
-
font-weight: 600;
|
|
1514
|
-
line-height: 1.35;
|
|
1515
|
-
margin: 0;
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
.activity-card {
|
|
1519
|
-
padding: 18px 20px 14px;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
.section-header,
|
|
1523
|
-
.panel-header {
|
|
1524
|
-
align-items: center;
|
|
1525
|
-
display: flex;
|
|
1526
|
-
justify-content: space-between;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
.section-header h2,
|
|
1530
|
-
.panel-header h2 {
|
|
1531
|
-
color: #132b43;
|
|
1532
|
-
font-size: 15px;
|
|
1533
|
-
font-weight: 800;
|
|
1534
|
-
letter-spacing: 0;
|
|
1535
|
-
line-height: 1.2;
|
|
1536
|
-
margin: 0;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
.chart-wrap {
|
|
1540
|
-
margin-top: 18px;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
.activity-chart {
|
|
1544
|
-
display: block;
|
|
1545
|
-
height: 260px;
|
|
1546
|
-
width: 100%;
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
.chart-grid {
|
|
1550
|
-
stroke: #e9eef2;
|
|
1551
|
-
stroke-width: 1;
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
.primary-line,
|
|
1555
|
-
.secondary-line {
|
|
1556
|
-
fill: none;
|
|
1557
|
-
stroke-linecap: round;
|
|
1558
|
-
stroke-linejoin: round;
|
|
1559
|
-
stroke-width: 3;
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
.primary-line {
|
|
1563
|
-
stroke: #2196f3;
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
.secondary-line {
|
|
1567
|
-
stroke: #1eca6b;
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
.primary-dot,
|
|
1571
|
-
.secondary-dot {
|
|
1572
|
-
stroke: #fff;
|
|
1573
|
-
stroke-width: 2;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
.primary-dot {
|
|
1577
|
-
fill: #2196f3;
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
.secondary-dot {
|
|
1581
|
-
fill: #1eca6b;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
.chart-label {
|
|
1585
|
-
fill: #7e8b96;
|
|
1586
|
-
font-size: 12px;
|
|
1587
|
-
font-weight: 600;
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
.chart-legend {
|
|
1591
|
-
align-items: center;
|
|
1592
|
-
display: flex;
|
|
1593
|
-
gap: 18px;
|
|
1594
|
-
justify-content: center;
|
|
1595
|
-
margin-top: 10px;
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
.chart-legend span {
|
|
1599
|
-
align-items: center;
|
|
1600
|
-
color: #6d7c88;
|
|
1601
|
-
display: inline-flex;
|
|
1602
|
-
font-size: 12px;
|
|
1603
|
-
font-weight: 700;
|
|
1604
|
-
gap: 7px;
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
.legend-dot {
|
|
1608
|
-
border-radius: 50%;
|
|
1609
|
-
display: inline-flex;
|
|
1610
|
-
height: 8px;
|
|
1611
|
-
width: 8px;
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
.legend-dot.primary {
|
|
1615
|
-
background: #2196f3;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
.legend-dot.secondary {
|
|
1619
|
-
background: #1eca6b;
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
.dashboard-section-grid {
|
|
1623
|
-
align-items: stretch;
|
|
1624
|
-
margin-top: 14px;
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
.dashboard-section-grid > .v-col {
|
|
1628
|
-
display: flex;
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
.dashboard-panel {
|
|
1632
|
-
display: flex;
|
|
1633
|
-
flex-direction: column;
|
|
1634
|
-
height: 100%;
|
|
1635
|
-
padding: 24px;
|
|
1636
|
-
width: 100%;
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
.dashboard-panel-recent {
|
|
1640
|
-
min-height: 224px;
|
|
1641
|
-
padding: 26px 30px 16px;
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
.panel-body {
|
|
1645
|
-
flex: 1;
|
|
1646
|
-
margin-top: 20px;
|
|
1647
|
-
max-height: 380px;
|
|
1648
|
-
overflow-y: auto;
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
.panel-action,
|
|
1652
|
-
.row-action {
|
|
1653
|
-
appearance: none;
|
|
1654
|
-
background: transparent;
|
|
1655
|
-
border: 0;
|
|
1656
|
-
color: #2196f3;
|
|
1657
|
-
cursor: pointer;
|
|
1658
|
-
font-size: 12px;
|
|
1659
|
-
font-weight: 800;
|
|
1660
|
-
letter-spacing: 0;
|
|
1661
|
-
line-height: 1.2;
|
|
1662
|
-
padding: 0;
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
.panel-action {
|
|
1666
|
-
margin-right: -10px;
|
|
1667
|
-
padding: 0 10px;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
.task-list,
|
|
1671
|
-
.feedback-list,
|
|
1672
|
-
.staff-list {
|
|
1673
|
-
display: grid;
|
|
1674
|
-
gap: 10px;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
.task-row,
|
|
1678
|
-
.feedback-row {
|
|
1679
|
-
align-items: center;
|
|
1680
|
-
background: #fff;
|
|
1681
|
-
border: 1px solid #e0e5ea;
|
|
1682
|
-
border-radius: 8px;
|
|
1683
|
-
display: grid;
|
|
1684
|
-
gap: 16px;
|
|
1685
|
-
min-height: 76px;
|
|
1686
|
-
padding: 15px 15px 15px 10px;
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
.task-row {
|
|
1690
|
-
grid-template-columns: minmax(220px, 280px) minmax(130px, 170px) minmax(82px, 120px) minmax(44px, 1fr);
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
.feedback-row {
|
|
1694
|
-
grid-template-columns: minmax(220px, 280px) minmax(130px, 170px) minmax(82px, 120px) minmax(44px, 1fr);
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
.task-row:hover,
|
|
1698
|
-
.feedback-row:hover,
|
|
1699
|
-
.staff-row:hover,
|
|
1700
|
-
.simple-list-row:hover {
|
|
1701
|
-
border-color: #d8e3ec;
|
|
1702
|
-
box-shadow: 0 6px 14px rgba(15, 44, 69, 0.04);
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
.task-summary,
|
|
1706
|
-
.feedback-summary,
|
|
1707
|
-
.staff-main,
|
|
1708
|
-
.list-main {
|
|
1709
|
-
min-width: 0;
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
.task-summary,
|
|
1713
|
-
.feedback-summary {
|
|
1714
|
-
padding: 0 10px;
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
.task-summary strong,
|
|
1718
|
-
.feedback-summary strong,
|
|
1719
|
-
.staff-main strong,
|
|
1720
|
-
.list-main strong {
|
|
1721
|
-
color: #132b43;
|
|
1722
|
-
display: block;
|
|
1723
|
-
font-size: 13px;
|
|
1724
|
-
font-weight: 800;
|
|
1725
|
-
line-height: 1.3;
|
|
1726
|
-
white-space: normal;
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
.task-summary p,
|
|
1730
|
-
.feedback-summary p,
|
|
1731
|
-
.staff-main p,
|
|
1732
|
-
.list-main p {
|
|
1733
|
-
color: #888;
|
|
1734
|
-
font-size: 12px;
|
|
1735
|
-
font-weight: 500;
|
|
1736
|
-
line-height: 1.35;
|
|
1737
|
-
margin: 5px 0 0;
|
|
1738
|
-
overflow: hidden;
|
|
1739
|
-
text-overflow: ellipsis;
|
|
1740
|
-
white-space: nowrap;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
.person-cell {
|
|
1744
|
-
align-items: center;
|
|
1745
|
-
color: #888;
|
|
1746
|
-
display: inline-flex;
|
|
1747
|
-
font-size: 12px;
|
|
1748
|
-
font-weight: 600;
|
|
1749
|
-
gap: 7px;
|
|
1750
|
-
min-width: 0;
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
.person-cell span:last-child {
|
|
1754
|
-
overflow: hidden;
|
|
1755
|
-
text-overflow: ellipsis;
|
|
1756
|
-
white-space: nowrap;
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
.avatar {
|
|
1760
|
-
align-items: center;
|
|
1761
|
-
background: #e0e0e0;
|
|
1762
|
-
border-radius: 999px;
|
|
1763
|
-
color: #fff;
|
|
1764
|
-
display: inline-flex;
|
|
1765
|
-
flex: 0 0 34px;
|
|
1766
|
-
font-size: 11px;
|
|
1767
|
-
font-weight: 800;
|
|
1768
|
-
height: 34px;
|
|
1769
|
-
justify-content: center;
|
|
1770
|
-
letter-spacing: 0;
|
|
1771
|
-
width: 34px;
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
.status-pill {
|
|
1775
|
-
align-items: center;
|
|
1776
|
-
border-radius: 999px;
|
|
1777
|
-
display: inline-flex;
|
|
1778
|
-
font-size: 12px;
|
|
1779
|
-
font-weight: 600;
|
|
1780
|
-
justify-content: center;
|
|
1781
|
-
line-height: 1;
|
|
1782
|
-
min-height: 26px;
|
|
1783
|
-
padding: 6px 10px;
|
|
1784
|
-
white-space: nowrap;
|
|
1785
|
-
width: fit-content;
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
.status-pill-success {
|
|
1789
|
-
background: rgba(30, 202, 107, 0.5);
|
|
1790
|
-
color: #015b29;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
.status-pill-info {
|
|
1794
|
-
background: #d8ecff;
|
|
1795
|
-
color: #14557d;
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
.status-pill-warning {
|
|
1799
|
-
background: #ffe9c7;
|
|
1800
|
-
color: #9c5e0e;
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
.status-pill-danger {
|
|
1804
|
-
background: #ffd9d6;
|
|
1805
|
-
color: #9f2f29;
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
.status-pill-neutral {
|
|
1809
|
-
background: #e0e0e0;
|
|
1810
|
-
color: #616065;
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
.row-action-right {
|
|
1814
|
-
justify-self: end;
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
.task-row .row-action-right {
|
|
1818
|
-
align-items: center;
|
|
1819
|
-
background: #fff;
|
|
1820
|
-
border: 1px solid #b9ddfb;
|
|
1821
|
-
border-radius: 8px;
|
|
1822
|
-
box-shadow: 0 1px 3px rgba(33, 150, 243, 0.1);
|
|
1823
|
-
color: #0b8ff1;
|
|
1824
|
-
display: inline-flex;
|
|
1825
|
-
height: 32px;
|
|
1826
|
-
justify-content: center;
|
|
1827
|
-
min-width: 54px;
|
|
1828
|
-
padding: 0 12px;
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
.task-row .row-action-right:hover {
|
|
1832
|
-
background: #f2f9ff;
|
|
1833
|
-
border-color: #80c3f7;
|
|
1834
|
-
color: #0878d3;
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
.feedback-row .row-action-right {
|
|
1838
|
-
padding-right: 8px;
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
.staff-row {
|
|
1842
|
-
align-items: center;
|
|
1843
|
-
background: #fff;
|
|
1844
|
-
border: 1px solid #e0e5ea;
|
|
1845
|
-
border-radius: 8px;
|
|
1846
|
-
display: grid;
|
|
1847
|
-
gap: 10px;
|
|
1848
|
-
grid-template-columns: 34px minmax(0, 1fr) auto;
|
|
1849
|
-
min-height: 68px;
|
|
1850
|
-
padding: 12px 14px;
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
.simple-list {
|
|
1854
|
-
display: grid;
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
.simple-list-row {
|
|
1858
|
-
align-items: center;
|
|
1859
|
-
background: #fff;
|
|
1860
|
-
border-bottom: 1px solid #e8edf1;
|
|
1861
|
-
display: grid;
|
|
1862
|
-
gap: 14px;
|
|
1863
|
-
grid-template-columns: 30px minmax(0, 1fr) 82px;
|
|
1864
|
-
min-height: 58px;
|
|
1865
|
-
padding: 9px 0;
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
.simple-list-row:last-child {
|
|
1869
|
-
border-bottom: 0;
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
.simple-list-row:hover {
|
|
1873
|
-
box-shadow: none;
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
.list-icon {
|
|
1877
|
-
align-items: center;
|
|
1878
|
-
border-radius: 6px;
|
|
1879
|
-
display: inline-flex;
|
|
1880
|
-
flex: 0 0 auto;
|
|
1881
|
-
font-size: 13px;
|
|
1882
|
-
height: 25px;
|
|
1883
|
-
justify-content: center;
|
|
1884
|
-
width: 25px;
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
.list-action {
|
|
1888
|
-
color: #768493;
|
|
1889
|
-
font-size: 12px;
|
|
1890
|
-
font-weight: 700;
|
|
1891
|
-
justify-self: end;
|
|
1892
|
-
text-align: right;
|
|
1893
|
-
white-space: nowrap;
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
.list-icon-info {
|
|
1897
|
-
background: #d8ecff;
|
|
1898
|
-
color: #17496f;
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
.list-icon-danger {
|
|
1902
|
-
background: #ffd9d6;
|
|
1903
|
-
color: #bd413c;
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
.list-icon-success {
|
|
1907
|
-
background: #d8f4e3;
|
|
1908
|
-
color: #2b9160;
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
.list-icon-warning {
|
|
1912
|
-
background: #ffe9c7;
|
|
1913
|
-
color: #be7418;
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
.list-icon-neutral {
|
|
1917
|
-
background: #edf2f6;
|
|
1918
|
-
color: #607386;
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
.empty-state {
|
|
1922
|
-
align-items: center;
|
|
1923
|
-
color: #8a98a5;
|
|
1924
|
-
display: flex;
|
|
1925
|
-
flex-direction: column;
|
|
1926
|
-
font-size: 13px;
|
|
1927
|
-
font-weight: 700;
|
|
1928
|
-
gap: 8px;
|
|
1929
|
-
justify-content: center;
|
|
1930
|
-
min-height: 140px;
|
|
1931
|
-
text-align: center;
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
.empty-state i {
|
|
1935
|
-
font-size: 28px;
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
.empty-state-compact {
|
|
1939
|
-
min-height: 72px;
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
.customize-dialog {
|
|
1943
|
-
background: #fff;
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
.customize-dialog-header {
|
|
1947
|
-
align-items: flex-start;
|
|
1948
|
-
border-bottom: 1px solid #eef2f5;
|
|
1949
|
-
display: flex;
|
|
1950
|
-
justify-content: space-between;
|
|
1951
|
-
padding: 24px 26px 18px;
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
.customize-dialog-header h2 {
|
|
1955
|
-
color: #0b2f47;
|
|
1956
|
-
font-size: 20px;
|
|
1957
|
-
font-weight: 800;
|
|
1958
|
-
line-height: 1.2;
|
|
1959
|
-
margin: 0;
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
.customize-dialog-header p {
|
|
1963
|
-
color: #71808d;
|
|
1964
|
-
font-size: 13px;
|
|
1965
|
-
margin: 6px 0 0;
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
.customize-dialog-body {
|
|
1969
|
-
padding: 20px 26px 8px;
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
.reset-row {
|
|
1973
|
-
display: flex;
|
|
1974
|
-
justify-content: flex-end;
|
|
1975
|
-
margin: 12px 0;
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
.widget-list {
|
|
1979
|
-
display: grid;
|
|
1980
|
-
gap: 10px;
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
.widget-row {
|
|
1984
|
-
align-items: center;
|
|
1985
|
-
border: 1px solid #e2e8ee;
|
|
1986
|
-
border-radius: 8px;
|
|
1987
|
-
display: flex;
|
|
1988
|
-
gap: 16px;
|
|
1989
|
-
justify-content: space-between;
|
|
1990
|
-
padding: 14px 16px;
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
.widget-row strong {
|
|
1994
|
-
color: #132b43;
|
|
1995
|
-
display: block;
|
|
1996
|
-
font-size: 14px;
|
|
1997
|
-
font-weight: 800;
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
.widget-row p {
|
|
2001
|
-
color: #71808d;
|
|
2002
|
-
font-size: 12px;
|
|
2003
|
-
margin: 4px 0 0;
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
.customize-dialog-actions {
|
|
2007
|
-
border-top: 1px solid #eef2f5;
|
|
2008
|
-
gap: 10px;
|
|
2009
|
-
justify-content: flex-end;
|
|
2010
|
-
padding: 16px 26px 22px;
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
.cancel-button {
|
|
2014
|
-
color: #607386;
|
|
2015
|
-
font-weight: 800;
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
.submit-button {
|
|
2019
|
-
background: #0b2f47;
|
|
2020
|
-
color: #fff;
|
|
2021
|
-
font-weight: 800;
|
|
2022
|
-
min-width: 110px;
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
@media (max-width: 960px) {
|
|
2026
|
-
.dashboard-header {
|
|
2027
|
-
align-items: stretch;
|
|
2028
|
-
flex-direction: column;
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
.header-actions {
|
|
2032
|
-
justify-content: flex-start;
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
.task-row,
|
|
2036
|
-
.feedback-row {
|
|
2037
|
-
grid-template-columns: 1fr;
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
.row-action-right {
|
|
2041
|
-
justify-self: start;
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
.status-pill {
|
|
2045
|
-
margin-left: 0;
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
@media (max-width: 640px) {
|
|
2050
|
-
.dashboard-title {
|
|
2051
|
-
font-size: 24px;
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
.header-actions,
|
|
2055
|
-
.action-button,
|
|
2056
|
-
.range-toggle {
|
|
2057
|
-
width: 100%;
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
.range-toggle :deep(.v-btn) {
|
|
2061
|
-
flex: 1 1 0;
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
.kpi-card,
|
|
2065
|
-
.activity-card,
|
|
2066
|
-
.dashboard-panel {
|
|
2067
|
-
padding: 16px;
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
.kpi-card {
|
|
2071
|
-
min-height: 154px;
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
.kpi-content {
|
|
2075
|
-
margin-left: 0;
|
|
2076
|
-
margin-top: 22px;
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
.kpi-value-row strong {
|
|
2080
|
-
font-size: 26px;
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
.staff-row {
|
|
2084
|
-
grid-template-columns: 30px minmax(0, 1fr);
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
.staff-row .status-pill {
|
|
2088
|
-
grid-column: 2;
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
.simple-list-row {
|
|
2092
|
-
grid-template-columns: 30px minmax(0, 1fr);
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
.list-action {
|
|
2096
|
-
grid-column: 2;
|
|
2097
|
-
justify-self: start;
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
</style>
|