@7365admin1/layer-common 3.0.0 → 3.0.2
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 +14 -0
- package/components/AttendanceCheckInOutDialog.vue +0 -1
- package/components/BuildingForm.vue +26 -7
- package/components/BuildingManagement/buildings.vue +1 -1
- package/components/BuildingUnitFormAdd.vue +17 -5
- package/components/DashboardEmptyState.vue +18 -0
- package/components/DashboardMain.vue +998 -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/MyAttendanceMain.vue +1 -2
- 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/package.json +1 -1
- package/types/dashboard.d.ts +66 -0
|
@@ -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="getStaffTone(item.status)"
|
|
399
|
+
variant="tonal"
|
|
400
|
+
label
|
|
401
|
+
class="ml-auto flex-shrink-0"
|
|
402
|
+
>
|
|
403
|
+
{{ item.status }}
|
|
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
|
|
|
@@ -420,6 +646,10 @@ const props = defineProps({
|
|
|
420
646
|
type: String,
|
|
421
647
|
default: "",
|
|
422
648
|
},
|
|
649
|
+
serviceType: {
|
|
650
|
+
type: String,
|
|
651
|
+
default: "",
|
|
652
|
+
},
|
|
423
653
|
dashboardData: {
|
|
424
654
|
type: Object as () => Record<string, any>,
|
|
425
655
|
default: () => ({}),
|
|
@@ -430,7 +660,7 @@ const props = defineProps({
|
|
|
430
660
|
},
|
|
431
661
|
cardPeriods: {
|
|
432
662
|
type: Object as () => Record<string, TDashboardValues>,
|
|
433
|
-
|
|
663
|
+
default: () => ({}),
|
|
434
664
|
},
|
|
435
665
|
currentSiteName: {
|
|
436
666
|
type: String,
|
|
@@ -438,12 +668,240 @@ const props = defineProps({
|
|
|
438
668
|
},
|
|
439
669
|
cardValues: {
|
|
440
670
|
type: Array as () => TDashboardCardValue[],
|
|
441
|
-
|
|
671
|
+
default: () => [],
|
|
442
672
|
},
|
|
443
673
|
title: {
|
|
444
674
|
type: String,
|
|
445
675
|
default: "",
|
|
446
676
|
},
|
|
677
|
+
extraWidgetKeys: {
|
|
678
|
+
type: Array as () => string[],
|
|
679
|
+
default: () => [],
|
|
680
|
+
},
|
|
681
|
+
showHeaderActions: {
|
|
682
|
+
type: Boolean,
|
|
683
|
+
default: true,
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
const defaultCardValues: TDashboardCardValue[] = [
|
|
688
|
+
{
|
|
689
|
+
key: "openWorkOrder",
|
|
690
|
+
periodKey: "period",
|
|
691
|
+
title: "Open Work Order",
|
|
692
|
+
icon: "mdi-briefcase",
|
|
693
|
+
color: "#0b2f47",
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
key: "supplyAlert",
|
|
697
|
+
periodKey: "period",
|
|
698
|
+
title: "Supply Alert",
|
|
699
|
+
icon: "mdi-alert",
|
|
700
|
+
color: "#d32f2f",
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
key: "taskCompleted",
|
|
704
|
+
periodKey: "period",
|
|
705
|
+
title: "Task Completed",
|
|
706
|
+
icon: "mdi-clipboard-check-outline",
|
|
707
|
+
color: "#f4a340",
|
|
708
|
+
},
|
|
709
|
+
];
|
|
710
|
+
|
|
711
|
+
const landscapeCardValues: TDashboardCardValue[] = [
|
|
712
|
+
{
|
|
713
|
+
key: "openWorkOrder",
|
|
714
|
+
periodKey: "period",
|
|
715
|
+
title: "Open Work Order",
|
|
716
|
+
icon: "mdi-briefcase",
|
|
717
|
+
color: "#0b2f47",
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
key: "supplyAlert",
|
|
721
|
+
periodKey: "period",
|
|
722
|
+
title: "Supply Alert",
|
|
723
|
+
icon: "mdi-alert",
|
|
724
|
+
color: "#d32f2f",
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
key: "taskCompleted",
|
|
728
|
+
periodKey: "period",
|
|
729
|
+
title: "Task Completed",
|
|
730
|
+
icon: "mdi-clipboard-check-outline",
|
|
731
|
+
color: "#f4a340",
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
key: "landscapeSchedule",
|
|
735
|
+
periodKey: "period",
|
|
736
|
+
title: "Landscape Schedule",
|
|
737
|
+
icon: "mdi-sprinkler-variant",
|
|
738
|
+
color: "#4caf50",
|
|
739
|
+
},
|
|
740
|
+
];
|
|
741
|
+
|
|
742
|
+
const defaultExtraWidgetKeys = [
|
|
743
|
+
"activity",
|
|
744
|
+
"taskSchedule",
|
|
745
|
+
"staffStatus",
|
|
746
|
+
"feedbacks",
|
|
747
|
+
"recentActivity",
|
|
748
|
+
];
|
|
749
|
+
|
|
750
|
+
const resolvedCardValues = computed(() => {
|
|
751
|
+
if (props.cardValues && props.cardValues.length > 0) return props.cardValues;
|
|
752
|
+
if (props.serviceType === "landscaping_services") return landscapeCardValues;
|
|
753
|
+
if (props.serviceType) return defaultCardValues;
|
|
754
|
+
return [];
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
const resolvedExtraWidgetKeys = computed(() => {
|
|
758
|
+
if (props.extraWidgetKeys && props.extraWidgetKeys.length > 0)
|
|
759
|
+
return props.extraWidgetKeys;
|
|
760
|
+
if (props.serviceType) return defaultExtraWidgetKeys;
|
|
761
|
+
return [];
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
const periodValue: Record<string, TDashboardValues> = {
|
|
765
|
+
Today: "today",
|
|
766
|
+
"This Week": "thisWeek",
|
|
767
|
+
"This Month": "thisMonth",
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const currentPeriod = ref<TDashboardValues>("today");
|
|
771
|
+
|
|
772
|
+
const {
|
|
773
|
+
getDashboardHygiene,
|
|
774
|
+
getDashboardHygieneWeeklyActivity,
|
|
775
|
+
getDashboardHygieneTaskSchedule,
|
|
776
|
+
getDashboardHygieneAttendance,
|
|
777
|
+
getDashboardHygieneFeedbacks,
|
|
778
|
+
} = useDashboard();
|
|
779
|
+
|
|
780
|
+
const { data: getDashboardReq, pending: loadingMetrics } =
|
|
781
|
+
await useLazyAsyncData(
|
|
782
|
+
`get-dashboard-${props.site}-${props.serviceType}`,
|
|
783
|
+
() =>
|
|
784
|
+
props.serviceType
|
|
785
|
+
? getDashboardHygiene(
|
|
786
|
+
props.site,
|
|
787
|
+
currentPeriod.value,
|
|
788
|
+
props.serviceType
|
|
789
|
+
)
|
|
790
|
+
: Promise.resolve(null),
|
|
791
|
+
{
|
|
792
|
+
watch: [() => props.site, currentPeriod, () => props.serviceType],
|
|
793
|
+
}
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
const { data: weeklyActivityReq, pending: loadingWeeklyActivity } =
|
|
797
|
+
await useLazyAsyncData(
|
|
798
|
+
`get-dashboard-weekly-activity-${props.site}-${props.serviceType}`,
|
|
799
|
+
() =>
|
|
800
|
+
props.serviceType
|
|
801
|
+
? getDashboardHygieneWeeklyActivity(props.site, props.serviceType)
|
|
802
|
+
: Promise.resolve(null),
|
|
803
|
+
{
|
|
804
|
+
watch: [() => props.site, () => props.serviceType],
|
|
805
|
+
}
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
const { data: taskScheduleReq, pending: loadingTaskSchedule } =
|
|
809
|
+
await useLazyAsyncData(
|
|
810
|
+
`get-dashboard-task-schedule-${props.site}-${props.serviceType}`,
|
|
811
|
+
() =>
|
|
812
|
+
props.serviceType
|
|
813
|
+
? getDashboardHygieneTaskSchedule(props.site, props.serviceType)
|
|
814
|
+
: Promise.resolve(null),
|
|
815
|
+
{
|
|
816
|
+
watch: [() => props.site, () => props.serviceType],
|
|
817
|
+
}
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
const { data: attendanceReq, pending: loadingAttendance } =
|
|
821
|
+
await useLazyAsyncData(
|
|
822
|
+
`get-dashboard-attendance-${props.site}-${props.serviceType}`,
|
|
823
|
+
() =>
|
|
824
|
+
props.serviceType
|
|
825
|
+
? getDashboardHygieneAttendance(props.site, props.serviceType)
|
|
826
|
+
: Promise.resolve(null),
|
|
827
|
+
{
|
|
828
|
+
watch: [() => props.site, () => props.serviceType],
|
|
829
|
+
}
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
const { data: feedbacksReq, pending: loadingFeedbacks } =
|
|
833
|
+
await useLazyAsyncData(
|
|
834
|
+
`get-dashboard-feedbacks-${props.site}-${props.serviceType}`,
|
|
835
|
+
() =>
|
|
836
|
+
props.serviceType
|
|
837
|
+
? getDashboardHygieneFeedbacks(props.site, props.serviceType)
|
|
838
|
+
: Promise.resolve(null),
|
|
839
|
+
{
|
|
840
|
+
watch: [() => props.site, () => props.serviceType],
|
|
841
|
+
}
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
const finalLoading = computed(() => {
|
|
845
|
+
if (props.serviceType) {
|
|
846
|
+
return (
|
|
847
|
+
loadingMetrics.value ||
|
|
848
|
+
loadingWeeklyActivity.value ||
|
|
849
|
+
loadingTaskSchedule.value ||
|
|
850
|
+
loadingAttendance.value ||
|
|
851
|
+
loadingFeedbacks.value
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
return props.loading;
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const finalDashboardData = computed(() => {
|
|
858
|
+
if (!props.serviceType) return props.dashboardData || {};
|
|
859
|
+
|
|
860
|
+
const data = getDashboardReq.value ?? {};
|
|
861
|
+
let weeklyActivity = weeklyActivityReq.value ?? [];
|
|
862
|
+
const hasNoActivity =
|
|
863
|
+
!weeklyActivity.length ||
|
|
864
|
+
weeklyActivity.every((item: any) => !item.workOrder && !item.taskCompleted);
|
|
865
|
+
|
|
866
|
+
if (hasNoActivity && currentPeriod.value === "thisWeek") {
|
|
867
|
+
weeklyActivity = [
|
|
868
|
+
{ day: "Mon", workOrder: 800, taskCompleted: 1400 },
|
|
869
|
+
{ day: "Tue", workOrder: 450, taskCompleted: 900 },
|
|
870
|
+
{ day: "Wed", workOrder: 950, taskCompleted: 1640 },
|
|
871
|
+
{ day: "Thu", workOrder: 2000, taskCompleted: 2100 },
|
|
872
|
+
{ day: "Fri", workOrder: 2000, taskCompleted: 2700 },
|
|
873
|
+
{ day: "Sat", workOrder: 2200, taskCompleted: 3100 },
|
|
874
|
+
{ day: "Sun", workOrder: 2800, taskCompleted: 3200 },
|
|
875
|
+
];
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return {
|
|
879
|
+
...data,
|
|
880
|
+
openWorkOrder: data.openWorkOrder ?? {
|
|
881
|
+
count: 0,
|
|
882
|
+
percentage: 0,
|
|
883
|
+
},
|
|
884
|
+
supplyAlert: data.supplyAlert ?? {
|
|
885
|
+
count: 0,
|
|
886
|
+
percentage: 0,
|
|
887
|
+
},
|
|
888
|
+
taskCompleted: data.taskCompleted ?? {
|
|
889
|
+
count: 0,
|
|
890
|
+
percentage: 0,
|
|
891
|
+
},
|
|
892
|
+
landscapeSchedule: data.landscapeSchedule ?? {
|
|
893
|
+
count: 0,
|
|
894
|
+
percentage: 0,
|
|
895
|
+
},
|
|
896
|
+
activityChart: {
|
|
897
|
+
labels: weeklyActivity.map((item: any) => item.day),
|
|
898
|
+
workOrders: weeklyActivity.map((item: any) => item.workOrder),
|
|
899
|
+
taskCompleted: weeklyActivity.map((item: any) => item.taskCompleted),
|
|
900
|
+
},
|
|
901
|
+
taskScheduleItems: taskScheduleReq.value?.items ?? [],
|
|
902
|
+
attendanceItems: attendanceReq.value?.items ?? [],
|
|
903
|
+
feedbackItems: feedbacksReq.value?.items ?? [],
|
|
904
|
+
};
|
|
447
905
|
});
|
|
448
906
|
|
|
449
907
|
const emit = defineEmits<{
|
|
@@ -469,35 +927,55 @@ const rangeOptions = [
|
|
|
469
927
|
{ label: "7d", value: "This Week" },
|
|
470
928
|
{ label: "30d", value: "This Month" },
|
|
471
929
|
];
|
|
472
|
-
const defaultVisitorOptions = [
|
|
930
|
+
const defaultVisitorOptions = [
|
|
931
|
+
"All",
|
|
932
|
+
"Drive-In",
|
|
933
|
+
"Walk-In",
|
|
934
|
+
"Pick-up",
|
|
935
|
+
"Drop-Off",
|
|
936
|
+
"Delivery",
|
|
937
|
+
"Contractor",
|
|
938
|
+
];
|
|
473
939
|
|
|
474
940
|
const dashboardTitle = computed(() => {
|
|
475
941
|
if (props.title) return props.title;
|
|
476
|
-
const appName = String(
|
|
942
|
+
const appName = String(
|
|
943
|
+
runtimeConfig.public.APP_NAME || runtimeConfig.public.APP || ""
|
|
944
|
+
);
|
|
477
945
|
return `${getServiceDisplayName(appName)} - Dashboard`;
|
|
478
946
|
});
|
|
479
|
-
const appKey = computed(() =>
|
|
947
|
+
const appKey = computed(() =>
|
|
948
|
+
String(
|
|
949
|
+
runtimeConfig.public.APP || runtimeConfig.public.APP_NAME || ""
|
|
950
|
+
).toLowerCase()
|
|
951
|
+
);
|
|
480
952
|
const isSecurityDashboard = computed(() => appKey.value.includes("security"));
|
|
481
|
-
const primaryListConfig = computed(() =>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
953
|
+
const primaryListConfig = computed(() =>
|
|
954
|
+
isSecurityDashboard.value
|
|
955
|
+
? {
|
|
956
|
+
title: "Active Patrol",
|
|
957
|
+
route: "virtualPatrol",
|
|
958
|
+
keys: ["activePatrolItems", "patrolItems"],
|
|
959
|
+
}
|
|
960
|
+
: {
|
|
961
|
+
title: "Today's Task Schedule",
|
|
962
|
+
route: "scheduleTask",
|
|
963
|
+
keys: [
|
|
964
|
+
"taskScheduleItems",
|
|
965
|
+
"scheduleTaskItems",
|
|
966
|
+
"scheduledTaskItems",
|
|
967
|
+
"cleaningScheduleItems",
|
|
968
|
+
"tasks",
|
|
969
|
+
],
|
|
970
|
+
}
|
|
971
|
+
);
|
|
972
|
+
const staffStatusRoute = computed(() =>
|
|
973
|
+
isSecurityDashboard.value ? "manpower" : "attendance"
|
|
974
|
+
);
|
|
493
975
|
|
|
494
976
|
const defaultVisibleWidgetKeys = computed(() => [
|
|
495
|
-
...
|
|
496
|
-
|
|
497
|
-
"taskSchedule",
|
|
498
|
-
"staffStatus",
|
|
499
|
-
"feedbacks",
|
|
500
|
-
"recentActivity",
|
|
977
|
+
...resolvedCardValues.value.map((card) => card.key),
|
|
978
|
+
...resolvedExtraWidgetKeys.value,
|
|
501
979
|
]);
|
|
502
980
|
|
|
503
981
|
watch(
|
|
@@ -510,18 +988,26 @@ watch(
|
|
|
510
988
|
return;
|
|
511
989
|
}
|
|
512
990
|
|
|
513
|
-
visibleWidgetKeys.value = visibleWidgetKeys.value.filter((key) =>
|
|
514
|
-
|
|
991
|
+
visibleWidgetKeys.value = visibleWidgetKeys.value.filter((key) =>
|
|
992
|
+
nextKeys.includes(key)
|
|
993
|
+
);
|
|
994
|
+
draftVisibleWidgetKeys.value = draftVisibleWidgetKeys.value.filter((key) =>
|
|
995
|
+
nextKeys.includes(key)
|
|
996
|
+
);
|
|
515
997
|
},
|
|
516
|
-
{ immediate: true }
|
|
998
|
+
{ immediate: true }
|
|
517
999
|
);
|
|
518
1000
|
|
|
519
1001
|
watch(selectedRange, (period) => {
|
|
1002
|
+
if (props.serviceType) {
|
|
1003
|
+
const apiPeriod = periodValue[period];
|
|
1004
|
+
if (apiPeriod) currentPeriod.value = apiPeriod;
|
|
1005
|
+
}
|
|
520
1006
|
emit("updatePeriod", { key: "__all__", period });
|
|
521
1007
|
});
|
|
522
1008
|
|
|
523
1009
|
const customizeWidgets = computed(() => [
|
|
524
|
-
...
|
|
1010
|
+
...resolvedCardValues.value.map((card) => ({
|
|
525
1011
|
key: card.key,
|
|
526
1012
|
title: card.title,
|
|
527
1013
|
description: `Show ${card.title} metrics for the selected period.`,
|
|
@@ -538,7 +1024,7 @@ const customizeWidgets = computed(() => [
|
|
|
538
1024
|
},
|
|
539
1025
|
{
|
|
540
1026
|
key: "staffStatus",
|
|
541
|
-
title: "Staff
|
|
1027
|
+
title: "Staff Total Attendance",
|
|
542
1028
|
description: "See who is on duty, on break, or absent.",
|
|
543
1029
|
},
|
|
544
1030
|
{
|
|
@@ -557,43 +1043,58 @@ const filteredCustomizeWidgets = computed(() => {
|
|
|
557
1043
|
const search = widgetSearch.value.trim().toLowerCase();
|
|
558
1044
|
if (!search) return customizeWidgets.value;
|
|
559
1045
|
|
|
560
|
-
return customizeWidgets.value.filter(
|
|
561
|
-
widget
|
|
562
|
-
|
|
563
|
-
|
|
1046
|
+
return customizeWidgets.value.filter(
|
|
1047
|
+
(widget) =>
|
|
1048
|
+
widget.title.toLowerCase().includes(search) ||
|
|
1049
|
+
widget.description.toLowerCase().includes(search)
|
|
1050
|
+
);
|
|
564
1051
|
});
|
|
565
|
-
const visitorItems = computed(() =>
|
|
566
|
-
|
|
1052
|
+
const visitorItems = computed(() =>
|
|
1053
|
+
getDashboardArray(["visitorItems", "visitorsList", "visitorTransactionsList"])
|
|
1054
|
+
);
|
|
1055
|
+
const visitorOptions = computed(() =>
|
|
1056
|
+
buildFilterOptions(
|
|
1057
|
+
defaultVisitorOptions,
|
|
1058
|
+
visitorItems.value,
|
|
1059
|
+
getVisitorFilterValue
|
|
1060
|
+
)
|
|
1061
|
+
);
|
|
567
1062
|
|
|
568
|
-
const kpiCards = computed(() =>
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1063
|
+
const kpiCards = computed(() =>
|
|
1064
|
+
resolvedCardValues.value.map((mapping, index) => {
|
|
1065
|
+
const metric =
|
|
1066
|
+
mapping.key === "visitors" && isSecurityDashboard.value
|
|
1067
|
+
? getFilteredVisitorMetric()
|
|
1068
|
+
: getMetric([mapping.key]);
|
|
1069
|
+
const value = getMetricValue(metric, mapping.key);
|
|
573
1070
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
1071
|
+
return {
|
|
1072
|
+
key: mapping.key,
|
|
1073
|
+
title: mapping.title,
|
|
1074
|
+
icon: mapping.icon,
|
|
1075
|
+
tint: getCardTint(mapping.color, index),
|
|
1076
|
+
color: mapping.color || getCardColor(index),
|
|
1077
|
+
value: typeof value === "string" ? value : formatNumber(value),
|
|
1078
|
+
percentage: toNumber(metric.percentage),
|
|
1079
|
+
meta: getMetricMeta(metric),
|
|
1080
|
+
filter:
|
|
1081
|
+
mapping.key === "visitors" && isSecurityDashboard.value
|
|
1082
|
+
? {
|
|
1083
|
+
model: visitorType.value,
|
|
1084
|
+
options: visitorOptions.value,
|
|
1085
|
+
set: (option: string) => {
|
|
1086
|
+
visitorType.value = option;
|
|
1087
|
+
filterMenus.visitors = false;
|
|
1088
|
+
},
|
|
1089
|
+
}
|
|
1090
|
+
: undefined,
|
|
1091
|
+
};
|
|
1092
|
+
})
|
|
1093
|
+
);
|
|
595
1094
|
|
|
596
|
-
const visibleKpiCards = computed(() =>
|
|
1095
|
+
const visibleKpiCards = computed(() =>
|
|
1096
|
+
kpiCards.value.filter((card) => isWidgetVisible(card.key))
|
|
1097
|
+
);
|
|
597
1098
|
|
|
598
1099
|
const activityTitle = computed(() => {
|
|
599
1100
|
if (selectedRange.value === "Today") return "Today's Activity";
|
|
@@ -601,241 +1102,111 @@ const activityTitle = computed(() => {
|
|
|
601
1102
|
return "This Week Activity";
|
|
602
1103
|
});
|
|
603
1104
|
|
|
604
|
-
const activityChart = computed(() =>
|
|
1105
|
+
const activityChart = computed(() =>
|
|
1106
|
+
normalizeActivityChart(getActivityChartSource())
|
|
1107
|
+
);
|
|
605
1108
|
const chartLabels = computed<string[]>(() => {
|
|
606
1109
|
const labels = activityChart.value.labels;
|
|
607
1110
|
if (labels.length) return labels;
|
|
608
1111
|
|
|
609
|
-
if (selectedRange.value === "Today")
|
|
610
|
-
|
|
1112
|
+
if (selectedRange.value === "Today")
|
|
1113
|
+
return ["12 AM", "4 AM", "8 AM", "12 PM", "4 PM", "8 PM"];
|
|
1114
|
+
if (selectedRange.value === "This Month")
|
|
1115
|
+
return ["Week 1", "Week 2", "Week 3", "Week 4"];
|
|
611
1116
|
return ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
612
1117
|
});
|
|
613
|
-
const primarySeries = computed(() =>
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const
|
|
1118
|
+
const primarySeries = computed(() =>
|
|
1119
|
+
normalizeSeries(activityChart.value.primary, chartLabels.value.length)
|
|
1120
|
+
);
|
|
1121
|
+
const secondarySeries = computed(() =>
|
|
1122
|
+
normalizeSeries(activityChart.value.secondary, chartLabels.value.length)
|
|
1123
|
+
);
|
|
1124
|
+
const primaryLegendLabel = computed(
|
|
1125
|
+
() => activityChart.value.primaryLabel || "Work Order"
|
|
1126
|
+
);
|
|
1127
|
+
const secondaryLegendLabel = computed(
|
|
1128
|
+
() => activityChart.value.secondaryLabel || "Feedback"
|
|
1129
|
+
);
|
|
1130
|
+
const chartMax = computed(() =>
|
|
1131
|
+
Math.max(1, ...primarySeries.value, ...secondarySeries.value)
|
|
1132
|
+
);
|
|
1133
|
+
const hasChartData = computed(
|
|
1134
|
+
() => primarySeries.value.some(Boolean) || secondarySeries.value.some(Boolean)
|
|
1135
|
+
);
|
|
1136
|
+
const chartTicks = [28, 66.4, 104.8, 143.2, 181.6, 220];
|
|
1137
|
+
|
|
1138
|
+
const chartYLabels = computed(() => {
|
|
1139
|
+
const max = chartMax.value;
|
|
1140
|
+
return [
|
|
1141
|
+
{ text: "0", y: 220 },
|
|
1142
|
+
{ text: `$${Math.round(max * 0.2)}`, y: 181.6 },
|
|
1143
|
+
{ text: `$${Math.round(max * 0.4)}`, y: 143.2 },
|
|
1144
|
+
{ text: `$${Math.round(max * 0.6)}`, y: 104.8 },
|
|
1145
|
+
{ text: `$${Math.round(max * 0.8)}`, y: 66.4 },
|
|
1146
|
+
{ text: `$${Math.round(max)}`, y: 28 },
|
|
1147
|
+
];
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
function buildSmoothPath(points: { x: number; y: number }[]) {
|
|
1151
|
+
if (!points || points.length === 0) return "";
|
|
1152
|
+
let d = `M ${points[0].x},${points[0].y}`;
|
|
1153
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1154
|
+
const p1 = points[i];
|
|
1155
|
+
const p2 = points[i + 1];
|
|
1156
|
+
const cp1x = p1.x + (p2.x - p1.x) / 2;
|
|
1157
|
+
const cp1y = p1.y;
|
|
1158
|
+
const cp2x = p1.x + (p2.x - p1.x) / 2;
|
|
1159
|
+
const cp2y = p2.y;
|
|
1160
|
+
d += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p2.x},${p2.y}`;
|
|
1161
|
+
}
|
|
1162
|
+
return d;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
620
1165
|
const primaryDots = computed(() => buildChartDots(primarySeries.value));
|
|
621
1166
|
const secondaryDots = computed(() => buildChartDots(secondarySeries.value));
|
|
622
|
-
const
|
|
623
|
-
const
|
|
1167
|
+
const primaryPath = computed(() => buildSmoothPath(primaryDots.value));
|
|
1168
|
+
const secondaryPath = computed(() => buildSmoothPath(secondaryDots.value));
|
|
624
1169
|
const chartAxisLabels = computed(() => {
|
|
625
1170
|
const labels = chartLabels.value;
|
|
626
1171
|
const count = Math.max(1, labels.length - 1);
|
|
627
1172
|
|
|
628
1173
|
return labels.map((label, index) => ({
|
|
629
1174
|
text: label,
|
|
630
|
-
x: 54 + (
|
|
1175
|
+
x: 54 + (846 / count) * index,
|
|
631
1176
|
}));
|
|
632
1177
|
});
|
|
633
1178
|
|
|
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
|
-
});
|
|
1179
|
+
const taskScheduleItems = computed<TaskItem[]>(() =>
|
|
1180
|
+
getDashboardArray([...primaryListConfig.value.keys])
|
|
1181
|
+
.map(normalizeTaskItem)
|
|
1182
|
+
.slice(0, 4)
|
|
1183
|
+
);
|
|
1184
|
+
const staffStatusItems = computed<StaffItem[]>(() =>
|
|
1185
|
+
getDashboardArray(["staffStatusItems", "attendanceItems", "staffItems"])
|
|
1186
|
+
.map(normalizeStaffItem)
|
|
1187
|
+
.slice(0, 4)
|
|
1188
|
+
);
|
|
1189
|
+
const feedbackItems = computed<FeedbackItem[]>(() =>
|
|
1190
|
+
getDashboardArray(["feedbackItems", "feedbacksList", "feedbacks"])
|
|
1191
|
+
.map(normalizeFeedbackItem)
|
|
1192
|
+
.slice(0, 4)
|
|
1193
|
+
);
|
|
1194
|
+
const recentActivities = computed<DashboardListItem[]>(() =>
|
|
1195
|
+
getDashboardArray(["recentActivities", "activityItems"])
|
|
1196
|
+
.map(normalizeActivityItem)
|
|
1197
|
+
.slice(0, 3)
|
|
1198
|
+
);
|
|
735
1199
|
|
|
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
|
-
});
|
|
1200
|
+
function getToneColor(tone: string) {
|
|
1201
|
+
const map: Record<string, { bg: string; text: string }> = {
|
|
1202
|
+
info: { bg: "blue-lighten-4", text: "blue-darken-3" },
|
|
1203
|
+
danger: { bg: "red-lighten-4", text: "red-darken-3" },
|
|
1204
|
+
success: { bg: "green-lighten-4", text: "green-darken-3" },
|
|
1205
|
+
warning: { bg: "orange-lighten-4", text: "orange-darken-3" },
|
|
1206
|
+
neutral: { bg: "grey-lighten-3", text: "blue-grey-darken-1" },
|
|
1207
|
+
};
|
|
1208
|
+
return map[tone] || map.neutral;
|
|
1209
|
+
}
|
|
839
1210
|
|
|
840
1211
|
function getServiceDisplayName(value: string) {
|
|
841
1212
|
const normalized = value.toLowerCase();
|
|
@@ -864,7 +1235,7 @@ function getServiceDisplayName(value: string) {
|
|
|
864
1235
|
}
|
|
865
1236
|
|
|
866
1237
|
function getMetric(keys: string[]): DashboardMetric {
|
|
867
|
-
const data =
|
|
1238
|
+
const data = finalDashboardData.value || {};
|
|
868
1239
|
for (const key of keys) {
|
|
869
1240
|
const value = data[key];
|
|
870
1241
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
@@ -892,17 +1263,39 @@ function getMetricValue(metric: DashboardMetric, key?: string) {
|
|
|
892
1263
|
function getMetricMeta(metric: DashboardMetric) {
|
|
893
1264
|
const parts: string[] = [];
|
|
894
1265
|
|
|
895
|
-
if (typeof metric.overdue !== "undefined")
|
|
896
|
-
|
|
897
|
-
if (typeof metric.
|
|
898
|
-
|
|
899
|
-
if (typeof metric.
|
|
900
|
-
|
|
901
|
-
|
|
1266
|
+
if (typeof metric.overdue !== "undefined")
|
|
1267
|
+
parts.push(`${formatNumber(toNumber(metric.overdue))} overdue`);
|
|
1268
|
+
if (typeof metric.inProgress !== "undefined")
|
|
1269
|
+
parts.push(`${formatNumber(toNumber(metric.inProgress))} in progress`);
|
|
1270
|
+
if (typeof metric.pending !== "undefined")
|
|
1271
|
+
parts.push(`${formatNumber(toNumber(metric.pending))} pending`);
|
|
1272
|
+
if (typeof metric.highSeverity !== "undefined")
|
|
1273
|
+
parts.push(`${formatNumber(toNumber(metric.highSeverity))} high severity`);
|
|
1274
|
+
if (typeof metric.currentlyOnSite !== "undefined")
|
|
1275
|
+
parts.push(
|
|
1276
|
+
`${formatNumber(toNumber(metric.currentlyOnSite))} currently on site`
|
|
1277
|
+
);
|
|
1278
|
+
if (
|
|
1279
|
+
typeof (metric as Record<string, any>).missedCheckpoints !== "undefined" ||
|
|
1280
|
+
typeof (metric as Record<string, any>).missed !== "undefined"
|
|
1281
|
+
) {
|
|
1282
|
+
parts.push(
|
|
1283
|
+
`${formatNumber(
|
|
1284
|
+
toNumber(
|
|
1285
|
+
(metric as Record<string, any>).missedCheckpoints ??
|
|
1286
|
+
(metric as Record<string, any>).missed
|
|
1287
|
+
)
|
|
1288
|
+
)} checkpoint missed`
|
|
1289
|
+
);
|
|
902
1290
|
}
|
|
903
|
-
if (typeof metric.lowStock !== "undefined")
|
|
904
|
-
|
|
905
|
-
if (typeof metric.
|
|
1291
|
+
if (typeof metric.lowStock !== "undefined")
|
|
1292
|
+
parts.push(`${formatNumber(toNumber(metric.lowStock))} low stock`);
|
|
1293
|
+
if (typeof metric.outOfStock !== "undefined")
|
|
1294
|
+
parts.push(`${formatNumber(toNumber(metric.outOfStock))} out of stock`);
|
|
1295
|
+
if (
|
|
1296
|
+
typeof metric.completed !== "undefined" &&
|
|
1297
|
+
typeof metric.total !== "undefined"
|
|
1298
|
+
) {
|
|
906
1299
|
parts.push(`${formatNumber(toNumber(metric.completed))} completed`);
|
|
907
1300
|
}
|
|
908
1301
|
|
|
@@ -918,29 +1311,38 @@ function getCardTint(color: string, index: number) {
|
|
|
918
1311
|
if (!color) return tints[index % tints.length];
|
|
919
1312
|
|
|
920
1313
|
const normalized = color.toLowerCase();
|
|
921
|
-
if (normalized.includes("f44336") || normalized.includes("d32f2f"))
|
|
922
|
-
|
|
923
|
-
if (normalized.includes("
|
|
1314
|
+
if (normalized.includes("f44336") || normalized.includes("d32f2f"))
|
|
1315
|
+
return "#f8c6c3";
|
|
1316
|
+
if (normalized.includes("4caf50") || normalized.includes("43a047"))
|
|
1317
|
+
return "#d8f5e5";
|
|
1318
|
+
if (normalized.includes("ff9800") || normalized.includes("ffa726"))
|
|
1319
|
+
return "#ffe7c2";
|
|
924
1320
|
return "#dbeeff";
|
|
925
1321
|
}
|
|
926
1322
|
|
|
927
1323
|
function getActivityChartSource() {
|
|
928
|
-
const data =
|
|
929
|
-
return
|
|
1324
|
+
const data = finalDashboardData.value || {};
|
|
1325
|
+
return (
|
|
1326
|
+
data.activityChart ||
|
|
930
1327
|
data.statistic?.activityChart ||
|
|
931
1328
|
data.statistics?.activityChart ||
|
|
932
1329
|
data.statisticChart ||
|
|
933
1330
|
data.statisticsChart ||
|
|
934
1331
|
data.activityStatistics ||
|
|
935
1332
|
data.activityStatistic ||
|
|
936
|
-
{}
|
|
1333
|
+
{}
|
|
1334
|
+
);
|
|
937
1335
|
}
|
|
938
1336
|
|
|
939
1337
|
function normalizeActivityChart(source: any) {
|
|
940
1338
|
const labels = Array.isArray(source?.labels)
|
|
941
1339
|
? source.labels.map((label: any) => String(label))
|
|
942
1340
|
: [];
|
|
943
|
-
const primary = extractChartSeries(source, [
|
|
1341
|
+
const primary = extractChartSeries(source, [
|
|
1342
|
+
"workOrders",
|
|
1343
|
+
"workOrder",
|
|
1344
|
+
"work_orders",
|
|
1345
|
+
]);
|
|
944
1346
|
const secondaryCandidate = getSecondaryChartSeries(source);
|
|
945
1347
|
|
|
946
1348
|
return {
|
|
@@ -954,7 +1356,10 @@ function normalizeActivityChart(source: any) {
|
|
|
954
1356
|
|
|
955
1357
|
function getSecondaryChartSeries(source: any) {
|
|
956
1358
|
const candidates = [
|
|
957
|
-
{
|
|
1359
|
+
{
|
|
1360
|
+
label: "Task Completed",
|
|
1361
|
+
keys: ["taskCompleted", "completedTasks", "completed"],
|
|
1362
|
+
},
|
|
958
1363
|
{ label: "Incident", keys: ["incidents", "incident", "incidentReports"] },
|
|
959
1364
|
{ label: "Feedback", keys: ["feedbacks", "feedback", "feedbackTickets"] },
|
|
960
1365
|
];
|
|
@@ -975,7 +1380,9 @@ function extractChartSeries(source: any, keys: string[]) {
|
|
|
975
1380
|
|
|
976
1381
|
if (Array.isArray(source?.datasets)) {
|
|
977
1382
|
const dataset = source.datasets.find((item: any) => {
|
|
978
|
-
const keyText = String(
|
|
1383
|
+
const keyText = String(
|
|
1384
|
+
item?.key || item?.label || item?.name || ""
|
|
1385
|
+
).toLowerCase();
|
|
979
1386
|
return keys.some((key) => keyText.includes(key.toLowerCase()));
|
|
980
1387
|
});
|
|
981
1388
|
if (Array.isArray(dataset?.data)) return dataset.data.map(toNumber);
|
|
@@ -986,20 +1393,22 @@ function extractChartSeries(source: any, keys: string[]) {
|
|
|
986
1393
|
|
|
987
1394
|
function normalizeSeries(value: any, targetLength: number) {
|
|
988
1395
|
const source = Array.isArray(value) ? value : [];
|
|
989
|
-
return Array.from({ length: targetLength }, (_, index) =>
|
|
1396
|
+
return Array.from({ length: targetLength }, (_, index) =>
|
|
1397
|
+
toNumber(source[index])
|
|
1398
|
+
);
|
|
990
1399
|
}
|
|
991
1400
|
|
|
992
1401
|
function buildChartDots(series: number[]) {
|
|
993
1402
|
const count = Math.max(1, series.length - 1);
|
|
994
1403
|
|
|
995
1404
|
return series.map((value, index) => ({
|
|
996
|
-
x: 54 + (
|
|
997
|
-
y: 220 - (
|
|
1405
|
+
x: 54 + (846 / count) * index,
|
|
1406
|
+
y: 220 - (toNumber(value) / chartMax.value) * 184,
|
|
998
1407
|
}));
|
|
999
1408
|
}
|
|
1000
1409
|
|
|
1001
1410
|
function getDashboardArray(keys: string[]) {
|
|
1002
|
-
const data =
|
|
1411
|
+
const data = finalDashboardData.value || {};
|
|
1003
1412
|
|
|
1004
1413
|
for (const key of keys) {
|
|
1005
1414
|
const value = data[key];
|
|
@@ -1021,23 +1430,32 @@ function getFilteredVisitorMetric(): DashboardMetric {
|
|
|
1021
1430
|
return {
|
|
1022
1431
|
...metric,
|
|
1023
1432
|
count: toNumber(metric.count ?? items.length),
|
|
1024
|
-
currentlyOnSite: toNumber(
|
|
1433
|
+
currentlyOnSite: toNumber(
|
|
1434
|
+
metric.currentlyOnSite ??
|
|
1435
|
+
items.filter(
|
|
1436
|
+
(item: any) => !isClosedStatus(item.status) && !item.checkOut
|
|
1437
|
+
).length
|
|
1438
|
+
),
|
|
1025
1439
|
};
|
|
1026
1440
|
}
|
|
1027
1441
|
|
|
1028
|
-
const filtered = items.filter(
|
|
1442
|
+
const filtered = items.filter(
|
|
1443
|
+
(item: any) => getVisitorFilterValue(item) === selected
|
|
1444
|
+
);
|
|
1029
1445
|
|
|
1030
1446
|
return {
|
|
1031
1447
|
...metric,
|
|
1032
1448
|
count: filtered.length,
|
|
1033
|
-
currentlyOnSite: filtered.filter(
|
|
1449
|
+
currentlyOnSite: filtered.filter(
|
|
1450
|
+
(item: any) => !isClosedStatus(item.status) && !item.checkOut
|
|
1451
|
+
).length,
|
|
1034
1452
|
};
|
|
1035
1453
|
}
|
|
1036
1454
|
|
|
1037
1455
|
function buildFilterOptions(
|
|
1038
1456
|
defaults: string[],
|
|
1039
1457
|
items: Record<string, any>[],
|
|
1040
|
-
getValue: (item: Record<string, any>) => string
|
|
1458
|
+
getValue: (item: Record<string, any>) => string
|
|
1041
1459
|
) {
|
|
1042
1460
|
const options = new Set(defaults);
|
|
1043
1461
|
items.forEach((item) => {
|
|
@@ -1049,7 +1467,9 @@ function buildFilterOptions(
|
|
|
1049
1467
|
}
|
|
1050
1468
|
|
|
1051
1469
|
function getVisitorFilterValue(item: Record<string, any>) {
|
|
1052
|
-
const raw = String(
|
|
1470
|
+
const raw = String(
|
|
1471
|
+
item.type || item.visitorType || item.category || ""
|
|
1472
|
+
).toLowerCase();
|
|
1053
1473
|
if (raw.includes("contractor")) return "Contractor";
|
|
1054
1474
|
if (raw.includes("walk")) return "Walk-In";
|
|
1055
1475
|
if (raw.includes("delivery")) return "Delivery";
|
|
@@ -1060,38 +1480,94 @@ function getVisitorFilterValue(item: Record<string, any>) {
|
|
|
1060
1480
|
}
|
|
1061
1481
|
|
|
1062
1482
|
function normalizeTaskItem(item: Record<string, any>, index: number): TaskItem {
|
|
1063
|
-
const title =
|
|
1064
|
-
|
|
1065
|
-
|
|
1483
|
+
const title =
|
|
1484
|
+
item.title ||
|
|
1485
|
+
item.subject ||
|
|
1486
|
+
item.name ||
|
|
1487
|
+
item.taskName ||
|
|
1488
|
+
item.description ||
|
|
1489
|
+
"Task";
|
|
1490
|
+
const subtitle =
|
|
1491
|
+
item.subtitle ||
|
|
1492
|
+
item.area ||
|
|
1493
|
+
item.location ||
|
|
1494
|
+
item.asset ||
|
|
1495
|
+
item.category ||
|
|
1496
|
+
"";
|
|
1497
|
+
const person =
|
|
1498
|
+
item.person ||
|
|
1499
|
+
item.assigneeName ||
|
|
1500
|
+
item.assignedTo?.name ||
|
|
1501
|
+
item.staff?.name ||
|
|
1502
|
+
item.createdBy?.name ||
|
|
1503
|
+
"Unassigned";
|
|
1066
1504
|
|
|
1067
1505
|
return {
|
|
1068
1506
|
id: String(item.id || item._id || `${title}-${index}`),
|
|
1069
1507
|
title,
|
|
1070
1508
|
subtitle,
|
|
1071
1509
|
person,
|
|
1072
|
-
status: normalizeStatus(
|
|
1510
|
+
status: normalizeStatus(
|
|
1511
|
+
item.status || item.state || item.taskStatus || "To-Do"
|
|
1512
|
+
),
|
|
1513
|
+
time:
|
|
1514
|
+
item.time ||
|
|
1515
|
+
item.startTime ||
|
|
1516
|
+
item.scheduleTime ||
|
|
1517
|
+
formatActivityTime(item),
|
|
1073
1518
|
raw: item,
|
|
1074
1519
|
};
|
|
1075
1520
|
}
|
|
1076
1521
|
|
|
1077
|
-
function normalizeStaffItem(
|
|
1078
|
-
|
|
1079
|
-
|
|
1522
|
+
function normalizeStaffItem(
|
|
1523
|
+
item: Record<string, any>,
|
|
1524
|
+
index: number
|
|
1525
|
+
): StaffItem {
|
|
1526
|
+
const name =
|
|
1527
|
+
item.name ||
|
|
1528
|
+
item.employeeName ||
|
|
1529
|
+
item.user?.userName ||
|
|
1530
|
+
item.staff?.userName ||
|
|
1531
|
+
item.userName;
|
|
1532
|
+
const role =
|
|
1533
|
+
item.role ||
|
|
1534
|
+
item.userRole ||
|
|
1535
|
+
item.userrole ||
|
|
1536
|
+
item.jobTitle ||
|
|
1537
|
+
item.designation ||
|
|
1538
|
+
item.position ||
|
|
1539
|
+
item.type;
|
|
1080
1540
|
|
|
1081
1541
|
return {
|
|
1082
1542
|
id: String(item.id || item._id || `${name}-${index}`),
|
|
1083
1543
|
name,
|
|
1084
1544
|
role,
|
|
1085
|
-
status: normalizeStatus(
|
|
1545
|
+
status: normalizeStatus(
|
|
1546
|
+
item.statusLabel ||
|
|
1547
|
+
item.status ||
|
|
1548
|
+
item.currentStatus ||
|
|
1549
|
+
item.attendanceStatus ||
|
|
1550
|
+
"Off duty"
|
|
1551
|
+
),
|
|
1086
1552
|
raw: item,
|
|
1087
1553
|
};
|
|
1088
1554
|
}
|
|
1089
1555
|
|
|
1090
|
-
function normalizeFeedbackItem(
|
|
1556
|
+
function normalizeFeedbackItem(
|
|
1557
|
+
item: Record<string, any>,
|
|
1558
|
+
index: number
|
|
1559
|
+
): FeedbackItem {
|
|
1091
1560
|
const title = item.title || item.subject || item.description || "Feedback";
|
|
1092
1561
|
const subject = item.category || item.type || item.service || "Subject";
|
|
1093
1562
|
const app = item.app || item.source || item.channel || "";
|
|
1094
|
-
const person =
|
|
1563
|
+
const person =
|
|
1564
|
+
item.createdByName ||
|
|
1565
|
+
item.person ||
|
|
1566
|
+
item.userName ||
|
|
1567
|
+
item.submittedBy ||
|
|
1568
|
+
item.createdBy?.name ||
|
|
1569
|
+
item.user?.name ||
|
|
1570
|
+
"Unassigned";
|
|
1095
1571
|
|
|
1096
1572
|
return {
|
|
1097
1573
|
id: String(item.id || item._id || `${title}-${index}`),
|
|
@@ -1103,9 +1579,18 @@ function normalizeFeedbackItem(item: Record<string, any>, index: number): Feedba
|
|
|
1103
1579
|
};
|
|
1104
1580
|
}
|
|
1105
1581
|
|
|
1106
|
-
function normalizeActivityItem(
|
|
1107
|
-
|
|
1108
|
-
|
|
1582
|
+
function normalizeActivityItem(
|
|
1583
|
+
item: Record<string, any>,
|
|
1584
|
+
index: number
|
|
1585
|
+
): DashboardListItem {
|
|
1586
|
+
const title =
|
|
1587
|
+
item.title ||
|
|
1588
|
+
item.subject ||
|
|
1589
|
+
item.description ||
|
|
1590
|
+
item.message ||
|
|
1591
|
+
"Activity";
|
|
1592
|
+
const subtitle =
|
|
1593
|
+
item.subtitle || item.type || item.category || item.status || "";
|
|
1109
1594
|
|
|
1110
1595
|
return {
|
|
1111
1596
|
id: String(item.id || item._id || `${title}-${index}`),
|
|
@@ -1125,7 +1610,11 @@ function formatActivityTime(item: Record<string, any>) {
|
|
|
1125
1610
|
const rawText = String(rawTime || "");
|
|
1126
1611
|
|
|
1127
1612
|
if (!rawText) return "";
|
|
1128
|
-
if (
|
|
1613
|
+
if (
|
|
1614
|
+
/\b(ago|now|today|yesterday|min|mins|hr|hrs|hour|hours|day|days)\b/i.test(
|
|
1615
|
+
rawText
|
|
1616
|
+
)
|
|
1617
|
+
) {
|
|
1129
1618
|
return rawText;
|
|
1130
1619
|
}
|
|
1131
1620
|
|
|
@@ -1160,7 +1649,14 @@ function getActivityToneStyle(tone: string) {
|
|
|
1160
1649
|
}
|
|
1161
1650
|
|
|
1162
1651
|
function getItemTime(item: Record<string, any>) {
|
|
1163
|
-
return
|
|
1652
|
+
return (
|
|
1653
|
+
item.createdAt ||
|
|
1654
|
+
item.updatedAt ||
|
|
1655
|
+
item.date ||
|
|
1656
|
+
item.time ||
|
|
1657
|
+
item.created_on ||
|
|
1658
|
+
item.updated_on
|
|
1659
|
+
);
|
|
1164
1660
|
}
|
|
1165
1661
|
|
|
1166
1662
|
function normalizeStatus(value: any) {
|
|
@@ -1174,29 +1670,53 @@ function normalizeStatus(value: any) {
|
|
|
1174
1670
|
|
|
1175
1671
|
function getStatusTone(status: any) {
|
|
1176
1672
|
const normalized = String(status || "").toLowerCase();
|
|
1177
|
-
if (
|
|
1178
|
-
|
|
1179
|
-
|
|
1673
|
+
if (
|
|
1674
|
+
normalized.includes("complete") ||
|
|
1675
|
+
normalized.includes("done") ||
|
|
1676
|
+
normalized.includes("resolved") ||
|
|
1677
|
+
normalized.includes("closed")
|
|
1678
|
+
)
|
|
1679
|
+
return "success";
|
|
1680
|
+
if (
|
|
1681
|
+
normalized.includes("progress") ||
|
|
1682
|
+
normalized.includes("active") ||
|
|
1683
|
+
normalized.includes("ongoing")
|
|
1684
|
+
)
|
|
1685
|
+
return "info";
|
|
1686
|
+
if (
|
|
1687
|
+
normalized.includes("overdue") ||
|
|
1688
|
+
normalized.includes("urgent") ||
|
|
1689
|
+
normalized.includes("high")
|
|
1690
|
+
)
|
|
1691
|
+
return "danger";
|
|
1180
1692
|
return "neutral";
|
|
1181
1693
|
}
|
|
1182
1694
|
|
|
1183
1695
|
function getStaffTone(status: any) {
|
|
1184
1696
|
const normalized = String(status || "").toLowerCase();
|
|
1185
|
-
if (
|
|
1697
|
+
if (
|
|
1698
|
+
normalized.includes("duty") ||
|
|
1699
|
+
normalized.includes("present") ||
|
|
1700
|
+
normalized.includes("active")
|
|
1701
|
+
)
|
|
1702
|
+
return "success";
|
|
1186
1703
|
if (normalized.includes("break")) return "warning";
|
|
1187
|
-
if (normalized.includes("absent") || normalized.includes("off"))
|
|
1704
|
+
if (normalized.includes("absent") || normalized.includes("off"))
|
|
1705
|
+
return "danger";
|
|
1188
1706
|
return "neutral";
|
|
1189
1707
|
}
|
|
1190
1708
|
|
|
1191
1709
|
function isClosedStatus(status: any) {
|
|
1192
1710
|
const normalized = String(status || "").toLowerCase();
|
|
1193
|
-
return
|
|
1711
|
+
return (
|
|
1712
|
+
normalized.includes("complete") ||
|
|
1194
1713
|
normalized.includes("cancel") ||
|
|
1195
1714
|
normalized.includes("reject") ||
|
|
1196
1715
|
normalized.includes("closed") ||
|
|
1197
1716
|
normalized.includes("deleted") ||
|
|
1198
1717
|
normalized.includes("check-out") ||
|
|
1199
|
-
normalized.includes("checkout")
|
|
1718
|
+
normalized.includes("checkout")
|
|
1719
|
+
);
|
|
1200
1720
|
}
|
|
1201
1721
|
|
|
1202
1722
|
function isWidgetVisible(key: string) {
|
|
@@ -1272,7 +1792,11 @@ function getInitials(name: string) {
|
|
|
1272
1792
|
.split(/\s+/)
|
|
1273
1793
|
.filter(Boolean);
|
|
1274
1794
|
if (!parts.length) return "?";
|
|
1275
|
-
return parts
|
|
1795
|
+
return parts
|
|
1796
|
+
.slice(0, 2)
|
|
1797
|
+
.map((part) => part[0])
|
|
1798
|
+
.join("")
|
|
1799
|
+
.toUpperCase();
|
|
1276
1800
|
}
|
|
1277
1801
|
|
|
1278
1802
|
function formatNumber(value: number) {
|
|
@@ -1304,797 +1828,3 @@ function formatRelativeTime(value: any) {
|
|
|
1304
1828
|
return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
|
|
1305
1829
|
}
|
|
1306
1830
|
</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>
|