@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.
@@ -1,20 +1,26 @@
1
1
  <template>
2
- <section class="service-dashboard">
3
- <div class="dashboard-header">
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="dashboard-title">{{ dashboardTitle }}</h1>
6
- <div class="site-label">
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 class="header-actions">
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="default"
16
- density="comfortable"
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="default"
30
- density="comfortable"
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 class="export-menu" density="compact" border>
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 v-if="loading" indeterminate color="primary" class="mb-4" />
65
-
66
- <v-row class="kpi-grid" dense>
67
- <v-col
68
- v-for="card in visibleKpiCards"
69
- :key="card.key"
70
- cols="12"
71
- md="6"
72
- >
73
- <v-card :class="['dashboard-card', 'kpi-card', `kpi-card-${card.key}`]" flat border>
74
- <div class="kpi-topline">
75
- <div :class="['kpi-icon', `kpi-icon-${card.key}`]" :style="{ backgroundColor: card.tint }">
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 mini-filter"
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 class="filter-menu" density="compact" border>
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="card.filter.model === option ? 'mdi-check' : 'mdi-blank'"
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="kpi-content">
112
- <p class="kpi-label">{{ card.title }}</p>
113
- <div class="kpi-value-row">
114
- <strong>{{ card.value }}</strong>
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="kpi-meta-row">
117
- <p v-if="card.meta" class="kpi-meta">{{ card.meta }}</p>
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="card.percentage >= 0 ? 'mdi-arrow-up-right' : 'mdi-arrow-down-right'"
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 v-if="isWidgetVisible('activity')" class="dashboard-card activity-card" flat border>
138
- <div class="section-header">
139
- <h2>{{ activityTitle }}</h2>
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="chart-wrap">
143
- <svg viewBox="0 0 920 260" preserveAspectRatio="none" class="activity-chart">
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
- class="chart-grid"
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
- class="primary-dot"
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
- class="secondary-dot"
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="chart-legend">
187
- <span><i class="legend-dot primary" />{{ primaryLegendLabel }}</span>
188
- <span><i class="legend-dot secondary" />{{ secondaryLegendLabel }}</span>
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
- <EmptyDashboardState v-else />
282
+ <DashboardEmptyState v-else />
193
283
  </v-card>
194
284
 
195
- <v-row class="dashboard-section-grid" dense>
196
- <v-col v-if="isWidgetVisible('taskSchedule')" cols="12" lg="7">
197
- <DashboardPanel :title="primaryListConfig.title" action-label="View All" @action="goToDashboardRoute(primaryListConfig.route)">
198
- <div v-if="taskScheduleItems.length" class="task-list">
199
- <div v-for="item in taskScheduleItems" :key="item.id" class="task-row">
200
- <div class="task-summary">
201
- <strong>{{ item.title }}</strong>
202
- <p>{{ item.subtitle }}</p>
203
- </div>
204
-
205
- <div class="person-cell">
206
- <span class="avatar">{{ getInitials(item.person) }}</span>
207
- <span>{{ item.person }}</span>
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
+ &bull; </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
- <EmptyDashboardState v-else />
354
+ <DashboardEmptyState v-else />
218
355
  </DashboardPanel>
219
356
  </v-col>
220
357
 
221
- <v-col v-if="isWidgetVisible('staffStatus')" cols="12" lg="5">
222
- <DashboardPanel title="Staff Status" action-label="View All" @action="goToDashboardRoute(staffStatusRoute)">
223
- <div v-if="staffStatusItems.length" class="staff-list">
224
- <div v-for="item in staffStatusItems" :key="item.id" class="staff-row">
225
- <span class="avatar">{{ getInitials(item.name) }}</span>
226
- <div class="staff-main">
227
- <strong>{{ item.name }}</strong>
228
- <p>{{ item.role }}</p>
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
- <span :class="['status-pill', `status-pill-${getStaffTone(item.status)}`]">
231
- {{ item.status }}
232
- </span>
233
- </div>
406
+ </v-card>
234
407
  </div>
235
- <EmptyDashboardState v-else />
408
+ <DashboardEmptyState v-else />
236
409
  </DashboardPanel>
237
410
  </v-col>
238
411
 
239
- <v-col v-if="isWidgetVisible('feedbacks')" cols="12">
240
- <DashboardPanel title="Feedbacks" action-label="View All" @action="goToDashboardRoute('feedbacks')">
241
- <div v-if="feedbackItems.length" class="feedback-list">
242
- <div v-for="item in feedbackItems" :key="item.id" class="feedback-row">
243
- <div class="feedback-summary">
244
- <strong>{{ item.title }}</strong>
245
- <p>{{ item.subtitle }}</p>
246
- </div>
247
-
248
- <div class="person-cell">
249
- <span class="avatar">{{ getInitials(item.person) }}</span>
250
- <span>{{ item.person }}</span>
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
- <EmptyDashboardState v-else />
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 class="customize-dialog" rounded="lg">
273
- <div class="customize-dialog-header">
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
- <h2>Customize Dashboard</h2>
276
- <p>Select which widgets to display on your dashboard</p>
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
- </div>
497
+ </v-card-title>
286
498
 
287
- <v-card-text class="customize-dialog-body">
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="reset-row">
509
+ <div class="d-flex justify-end mt-3 mb-1">
299
510
  <v-btn
300
511
  variant="text"
301
- color="#0b2f47"
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="widget-list">
312
- <div
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
- class="widget-row"
526
+ flat
527
+ border
528
+ rounded="lg"
529
+ class="pa-4"
316
530
  >
317
- <div>
318
- <strong>{{ widget.title }}</strong>
319
- <p>{{ widget.description }}</p>
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
- <v-switch
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
- <EmptyDashboardState v-if="!filteredCustomizeWidgets.length" compact />
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="customize-dialog-actions">
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 cancel-button"
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
- class="text-none submit-button"
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
- required: true,
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
- required: true,
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 = ["All", "Drive-In", "Walk-In", "Pick-up", "Drop-Off", "Delivery", "Contractor"];
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(runtimeConfig.public.APP_NAME || runtimeConfig.public.APP || "");
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(() => String(runtimeConfig.public.APP || runtimeConfig.public.APP_NAME || "").toLowerCase());
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(() => isSecurityDashboard.value
482
- ? {
483
- title: "Active Patrol",
484
- route: "virtualPatrol",
485
- keys: ["activePatrolItems", "patrolItems"],
486
- }
487
- : {
488
- title: "Today's Task Schedule",
489
- route: "scheduleTask",
490
- keys: ["taskScheduleItems", "scheduleTaskItems", "scheduledTaskItems", "cleaningScheduleItems", "tasks"],
491
- });
492
- const staffStatusRoute = computed(() => isSecurityDashboard.value ? "manpower" : "attendance");
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
- ...props.cardValues.map((card) => card.key),
496
- "activity",
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) => nextKeys.includes(key));
514
- draftVisibleWidgetKeys.value = draftVisibleWidgetKeys.value.filter((key) => nextKeys.includes(key));
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
- ...props.cardValues.map((card) => ({
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 Status",
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((widget) => (
561
- widget.title.toLowerCase().includes(search) ||
562
- widget.description.toLowerCase().includes(search)
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(() => getDashboardArray(["visitorItems", "visitorsList", "visitorTransactionsList"]));
566
- const visitorOptions = computed(() => buildFilterOptions(defaultVisitorOptions, visitorItems.value, getVisitorFilterValue));
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(() => props.cardValues.map((mapping, index) => {
569
- const metric = mapping.key === "visitors" && isSecurityDashboard.value
570
- ? getFilteredVisitorMetric()
571
- : getMetric([mapping.key]);
572
- const value = getMetricValue(metric, mapping.key);
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
- return {
575
- key: mapping.key,
576
- title: mapping.title,
577
- icon: mapping.icon,
578
- tint: getCardTint(mapping.color, index),
579
- color: mapping.color || getCardColor(index),
580
- value: typeof value === "string" ? value : formatNumber(value),
581
- percentage: toNumber(metric.percentage),
582
- meta: getMetricMeta(metric),
583
- filter: mapping.key === "visitors" && isSecurityDashboard.value
584
- ? {
585
- model: visitorType.value,
586
- options: visitorOptions.value,
587
- set: (option: string) => {
588
- visitorType.value = option;
589
- filterMenus.visitors = false;
590
- },
591
- }
592
- : undefined,
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(() => kpiCards.value.filter((card) => isWidgetVisible(card.key)));
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(() => normalizeActivityChart(getActivityChartSource()));
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") return ["12 AM", "4 AM", "8 AM", "12 PM", "4 PM", "8 PM"];
610
- if (selectedRange.value === "This Month") return ["Week 1", "Week 2", "Week 3", "Week 4"];
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(() => normalizeSeries(activityChart.value.primary, chartLabels.value.length));
614
- const secondarySeries = computed(() => normalizeSeries(activityChart.value.secondary, chartLabels.value.length));
615
- const primaryLegendLabel = computed(() => activityChart.value.primaryLabel || "Work Order");
616
- const secondaryLegendLabel = computed(() => activityChart.value.secondaryLabel || "Feedback");
617
- const chartMax = computed(() => Math.max(1, ...primarySeries.value, ...secondarySeries.value));
618
- const hasChartData = computed(() => primarySeries.value.some(Boolean) || secondarySeries.value.some(Boolean));
619
- const chartTicks = [28, 76, 124, 172, 220];
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 primaryPoints = computed(() => primaryDots.value.map((point) => `${point.x},${point.y}`).join(" "));
623
- const secondaryPoints = computed(() => secondaryDots.value.map((point) => `${point.x},${point.y}`).join(" "));
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 + ((846 / count) * index),
1175
+ x: 54 + (846 / count) * index,
631
1176
  }));
632
1177
  });
633
1178
 
634
- const taskScheduleItems = computed<TaskItem[]>(() => getDashboardArray([
635
- ...primaryListConfig.value.keys,
636
- ]).map(normalizeTaskItem).slice(0, 4));
637
- const staffStatusItems = computed<StaffItem[]>(() => getDashboardArray([
638
- "staffStatusItems",
639
- "attendanceItems",
640
- "staffItems",
641
- ]).map(normalizeStaffItem).slice(0, 4));
642
- const feedbackItems = computed<FeedbackItem[]>(() => getDashboardArray([
643
- "feedbackItems",
644
- "feedbacksList",
645
- "feedbacks",
646
- ]).map(normalizeFeedbackItem).slice(0, 4));
647
- const recentActivities = computed<DashboardListItem[]>(() => getDashboardArray([
648
- "recentActivities",
649
- "activityItems",
650
- ]).map(normalizeActivityItem).slice(0, 3));
651
-
652
- const EmptyDashboardState = defineComponent({
653
- name: "EmptyDashboardState",
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
- const DashboardList = defineComponent({
737
- name: "DashboardList",
738
- props: {
739
- items: {
740
- type: Array as () => DashboardListItem[],
741
- default: () => [],
742
- },
743
- emptyIcon: {
744
- type: String,
745
- default: "mdi-database-search-outline",
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 = props.dashboardData || {};
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") parts.push(`${formatNumber(toNumber(metric.overdue))} overdue`);
896
- if (typeof metric.inProgress !== "undefined") parts.push(`${formatNumber(toNumber(metric.inProgress))} in progress`);
897
- if (typeof metric.pending !== "undefined") parts.push(`${formatNumber(toNumber(metric.pending))} pending`);
898
- if (typeof metric.highSeverity !== "undefined") parts.push(`${formatNumber(toNumber(metric.highSeverity))} high severity`);
899
- if (typeof metric.currentlyOnSite !== "undefined") parts.push(`${formatNumber(toNumber(metric.currentlyOnSite))} currently on site`);
900
- if (typeof (metric as Record<string, any>).missedCheckpoints !== "undefined" || typeof (metric as Record<string, any>).missed !== "undefined") {
901
- parts.push(`${formatNumber(toNumber((metric as Record<string, any>).missedCheckpoints ?? (metric as Record<string, any>).missed))} checkpoint missed`);
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") parts.push(`${formatNumber(toNumber(metric.lowStock))} low stock`);
904
- if (typeof metric.outOfStock !== "undefined") parts.push(`${formatNumber(toNumber(metric.outOfStock))} out of stock`);
905
- if (typeof metric.completed !== "undefined" && typeof metric.total !== "undefined") {
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")) return "#f8c6c3";
922
- if (normalized.includes("4caf50") || normalized.includes("43a047")) return "#d8f5e5";
923
- if (normalized.includes("ff9800") || normalized.includes("ffa726")) return "#ffe7c2";
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 = props.dashboardData || {};
929
- return data.activityChart ||
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, ["workOrders", "workOrder", "work_orders"]);
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
- { label: "Task Completed", keys: ["taskCompleted", "completedTasks", "completed"] },
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(item?.key || item?.label || item?.name || "").toLowerCase();
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) => toNumber(source[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 + ((846 / count) * index),
997
- y: 220 - ((toNumber(value) / chartMax.value) * 184),
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 = props.dashboardData || {};
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(metric.currentlyOnSite ?? items.filter((item) => !isClosedStatus(item.status) && !item.checkOut).length),
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((item) => getVisitorFilterValue(item) === selected);
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((item) => !isClosedStatus(item.status) && !item.checkOut).length,
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(item.type || item.visitorType || item.category || "").toLowerCase();
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 = item.title || item.subject || item.name || item.taskName || item.description || "Task";
1064
- const subtitle = item.subtitle || item.area || item.location || item.asset || item.category || "";
1065
- const person = item.person || item.assigneeName || item.assignedTo?.name || item.staff?.name || item.createdBy?.name || "Unassigned";
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(item.status || item.state || item.taskStatus || "To-Do"),
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(item: Record<string, any>, index: number): StaffItem {
1078
- const name = item.name || item.employeeName || item.user?.name || item.staff?.name || item.createdByName || "Unassigned";
1079
- const role = item.role || item.jobTitle || item.designation || item.position || "Officer";
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(item.statusLabel || item.status || item.currentStatus || item.attendanceStatus || "Off duty"),
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(item: Record<string, any>, index: number): FeedbackItem {
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 = item.person || item.createdByName || item.submittedBy || item.createdBy?.name || item.user?.name || "Unassigned";
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(item: Record<string, any>, index: number): DashboardListItem {
1107
- const title = item.title || item.subject || item.description || item.message || "Activity";
1108
- const subtitle = item.subtitle || item.type || item.category || item.status || "";
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 (/\b(ago|now|today|yesterday|min|mins|hr|hrs|hour|hours|day|days)\b/i.test(rawText)) {
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 item.createdAt || item.updatedAt || item.date || item.time || item.created_on || item.updated_on;
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 (normalized.includes("complete") || normalized.includes("done") || normalized.includes("resolved") || normalized.includes("closed")) return "success";
1178
- if (normalized.includes("progress") || normalized.includes("active") || normalized.includes("ongoing")) return "info";
1179
- if (normalized.includes("overdue") || normalized.includes("urgent") || normalized.includes("high")) return "danger";
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 (normalized.includes("duty") || normalized.includes("present") || normalized.includes("active")) return "success";
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")) return "danger";
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 normalized.includes("complete") ||
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.slice(0, 2).map((part) => part[0]).join("").toUpperCase();
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>