@7365admin1/layer-common 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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="blue-grey-darken-1"
399
+ variant="tonal"
400
+ label
401
+ class="ml-auto flex-shrink-0 font-weight-bold"
402
+ >
403
+ {{ item.totalHours || 0 }} hrs
404
+ </v-chip>
229
405
  </div>
230
- <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
 
@@ -399,6 +625,7 @@ type StaffItem = {
399
625
  name: string;
400
626
  role: string;
401
627
  status: string;
628
+ totalHours?: number | string;
402
629
  raw?: Record<string, any>;
403
630
  };
404
631
 
@@ -420,6 +647,10 @@ const props = defineProps({
420
647
  type: String,
421
648
  default: "",
422
649
  },
650
+ serviceType: {
651
+ type: String,
652
+ default: "",
653
+ },
423
654
  dashboardData: {
424
655
  type: Object as () => Record<string, any>,
425
656
  default: () => ({}),
@@ -430,7 +661,7 @@ const props = defineProps({
430
661
  },
431
662
  cardPeriods: {
432
663
  type: Object as () => Record<string, TDashboardValues>,
433
- required: true,
664
+ default: () => ({}),
434
665
  },
435
666
  currentSiteName: {
436
667
  type: String,
@@ -438,12 +669,240 @@ const props = defineProps({
438
669
  },
439
670
  cardValues: {
440
671
  type: Array as () => TDashboardCardValue[],
441
- required: true,
672
+ default: () => [],
442
673
  },
443
674
  title: {
444
675
  type: String,
445
676
  default: "",
446
677
  },
678
+ extraWidgetKeys: {
679
+ type: Array as () => string[],
680
+ default: () => [],
681
+ },
682
+ showHeaderActions: {
683
+ type: Boolean,
684
+ default: true,
685
+ },
686
+ });
687
+
688
+ const defaultCardValues: TDashboardCardValue[] = [
689
+ {
690
+ key: "openWorkOrder",
691
+ periodKey: "period",
692
+ title: "Open Work Order",
693
+ icon: "mdi-briefcase",
694
+ color: "#0b2f47",
695
+ },
696
+ {
697
+ key: "supplyAlert",
698
+ periodKey: "period",
699
+ title: "Supply Alert",
700
+ icon: "mdi-alert",
701
+ color: "#d32f2f",
702
+ },
703
+ {
704
+ key: "taskCompleted",
705
+ periodKey: "period",
706
+ title: "Task Completed",
707
+ icon: "mdi-clipboard-check-outline",
708
+ color: "#f4a340",
709
+ },
710
+ ];
711
+
712
+ const landscapeCardValues: TDashboardCardValue[] = [
713
+ {
714
+ key: "openWorkOrder",
715
+ periodKey: "period",
716
+ title: "Open Work Order",
717
+ icon: "mdi-briefcase",
718
+ color: "#0b2f47",
719
+ },
720
+ {
721
+ key: "supplyAlert",
722
+ periodKey: "period",
723
+ title: "Supply Alert",
724
+ icon: "mdi-alert",
725
+ color: "#d32f2f",
726
+ },
727
+ {
728
+ key: "taskCompleted",
729
+ periodKey: "period",
730
+ title: "Task Completed",
731
+ icon: "mdi-clipboard-check-outline",
732
+ color: "#f4a340",
733
+ },
734
+ {
735
+ key: "landscapeSchedule",
736
+ periodKey: "period",
737
+ title: "Landscape Schedule",
738
+ icon: "mdi-sprinkler-variant",
739
+ color: "#4caf50",
740
+ },
741
+ ];
742
+
743
+ const defaultExtraWidgetKeys = [
744
+ "activity",
745
+ "taskSchedule",
746
+ "staffStatus",
747
+ "feedbacks",
748
+ "recentActivity",
749
+ ];
750
+
751
+ const resolvedCardValues = computed(() => {
752
+ if (props.cardValues && props.cardValues.length > 0) return props.cardValues;
753
+ if (props.serviceType === "landscaping_services") return landscapeCardValues;
754
+ if (props.serviceType) return defaultCardValues;
755
+ return [];
756
+ });
757
+
758
+ const resolvedExtraWidgetKeys = computed(() => {
759
+ if (props.extraWidgetKeys && props.extraWidgetKeys.length > 0)
760
+ return props.extraWidgetKeys;
761
+ if (props.serviceType) return defaultExtraWidgetKeys;
762
+ return [];
763
+ });
764
+
765
+ const periodValue: Record<string, TDashboardValues> = {
766
+ Today: "today",
767
+ "This Week": "thisWeek",
768
+ "This Month": "thisMonth",
769
+ };
770
+
771
+ const currentPeriod = ref<TDashboardValues>("today");
772
+
773
+ const {
774
+ getDashboardHygiene,
775
+ getDashboardHygieneWeeklyActivity,
776
+ getDashboardHygieneTaskSchedule,
777
+ getDashboardHygieneAttendance,
778
+ getDashboardHygieneFeedbacks,
779
+ } = useDashboard();
780
+
781
+ const { data: getDashboardReq, pending: loadingMetrics } =
782
+ await useLazyAsyncData(
783
+ `get-dashboard-${props.site}-${props.serviceType}`,
784
+ () =>
785
+ props.serviceType
786
+ ? getDashboardHygiene(
787
+ props.site,
788
+ currentPeriod.value,
789
+ props.serviceType
790
+ )
791
+ : Promise.resolve(null),
792
+ {
793
+ watch: [() => props.site, currentPeriod, () => props.serviceType],
794
+ }
795
+ );
796
+
797
+ const { data: weeklyActivityReq, pending: loadingWeeklyActivity } =
798
+ await useLazyAsyncData(
799
+ `get-dashboard-weekly-activity-${props.site}-${props.serviceType}`,
800
+ () =>
801
+ props.serviceType
802
+ ? getDashboardHygieneWeeklyActivity(props.site, props.serviceType)
803
+ : Promise.resolve(null),
804
+ {
805
+ watch: [() => props.site, () => props.serviceType],
806
+ }
807
+ );
808
+
809
+ const { data: taskScheduleReq, pending: loadingTaskSchedule } =
810
+ await useLazyAsyncData(
811
+ `get-dashboard-task-schedule-${props.site}-${props.serviceType}`,
812
+ () =>
813
+ props.serviceType
814
+ ? getDashboardHygieneTaskSchedule(props.site, props.serviceType)
815
+ : Promise.resolve(null),
816
+ {
817
+ watch: [() => props.site, () => props.serviceType],
818
+ }
819
+ );
820
+
821
+ const { data: attendanceReq, pending: loadingAttendance } =
822
+ await useLazyAsyncData(
823
+ `get-dashboard-attendance-${props.site}-${props.serviceType}`,
824
+ () =>
825
+ props.serviceType
826
+ ? getDashboardHygieneAttendance(props.site, props.serviceType)
827
+ : Promise.resolve(null),
828
+ {
829
+ watch: [() => props.site, () => props.serviceType],
830
+ }
831
+ );
832
+
833
+ const { data: feedbacksReq, pending: loadingFeedbacks } =
834
+ await useLazyAsyncData(
835
+ `get-dashboard-feedbacks-${props.site}-${props.serviceType}`,
836
+ () =>
837
+ props.serviceType
838
+ ? getDashboardHygieneFeedbacks(props.site, props.serviceType)
839
+ : Promise.resolve(null),
840
+ {
841
+ watch: [() => props.site, () => props.serviceType],
842
+ }
843
+ );
844
+
845
+ const finalLoading = computed(() => {
846
+ if (props.serviceType) {
847
+ return (
848
+ loadingMetrics.value ||
849
+ loadingWeeklyActivity.value ||
850
+ loadingTaskSchedule.value ||
851
+ loadingAttendance.value ||
852
+ loadingFeedbacks.value
853
+ );
854
+ }
855
+ return props.loading;
856
+ });
857
+
858
+ const finalDashboardData = computed(() => {
859
+ if (!props.serviceType) return props.dashboardData || {};
860
+
861
+ const data = getDashboardReq.value ?? {};
862
+ let weeklyActivity = weeklyActivityReq.value ?? [];
863
+ const hasNoActivity =
864
+ !weeklyActivity.length ||
865
+ weeklyActivity.every((item: any) => !item.workOrder && !item.taskCompleted);
866
+
867
+ if (hasNoActivity && currentPeriod.value === "thisWeek") {
868
+ weeklyActivity = [
869
+ { day: "Mon", workOrder: 800, taskCompleted: 1400 },
870
+ { day: "Tue", workOrder: 450, taskCompleted: 900 },
871
+ { day: "Wed", workOrder: 950, taskCompleted: 1640 },
872
+ { day: "Thu", workOrder: 2000, taskCompleted: 2100 },
873
+ { day: "Fri", workOrder: 2000, taskCompleted: 2700 },
874
+ { day: "Sat", workOrder: 2200, taskCompleted: 3100 },
875
+ { day: "Sun", workOrder: 2800, taskCompleted: 3200 },
876
+ ];
877
+ }
878
+
879
+ return {
880
+ ...data,
881
+ openWorkOrder: data.openWorkOrder ?? {
882
+ count: 0,
883
+ percentage: 0,
884
+ },
885
+ supplyAlert: data.supplyAlert ?? {
886
+ count: 0,
887
+ percentage: 0,
888
+ },
889
+ taskCompleted: data.taskCompleted ?? {
890
+ count: 0,
891
+ percentage: 0,
892
+ },
893
+ landscapeSchedule: data.landscapeSchedule ?? {
894
+ count: 0,
895
+ percentage: 0,
896
+ },
897
+ activityChart: {
898
+ labels: weeklyActivity.map((item: any) => item.day),
899
+ workOrders: weeklyActivity.map((item: any) => item.workOrder),
900
+ taskCompleted: weeklyActivity.map((item: any) => item.taskCompleted),
901
+ },
902
+ taskScheduleItems: taskScheduleReq.value?.items ?? [],
903
+ attendanceItems: attendanceReq.value?.items ?? [],
904
+ feedbackItems: feedbacksReq.value?.items ?? [],
905
+ };
447
906
  });
448
907
 
449
908
  const emit = defineEmits<{
@@ -469,35 +928,55 @@ const rangeOptions = [
469
928
  { label: "7d", value: "This Week" },
470
929
  { label: "30d", value: "This Month" },
471
930
  ];
472
- const defaultVisitorOptions = ["All", "Drive-In", "Walk-In", "Pick-up", "Drop-Off", "Delivery", "Contractor"];
931
+ const defaultVisitorOptions = [
932
+ "All",
933
+ "Drive-In",
934
+ "Walk-In",
935
+ "Pick-up",
936
+ "Drop-Off",
937
+ "Delivery",
938
+ "Contractor",
939
+ ];
473
940
 
474
941
  const dashboardTitle = computed(() => {
475
942
  if (props.title) return props.title;
476
- const appName = String(runtimeConfig.public.APP_NAME || runtimeConfig.public.APP || "");
943
+ const appName = String(
944
+ runtimeConfig.public.APP_NAME || runtimeConfig.public.APP || ""
945
+ );
477
946
  return `${getServiceDisplayName(appName)} - Dashboard`;
478
947
  });
479
- const appKey = computed(() => String(runtimeConfig.public.APP || runtimeConfig.public.APP_NAME || "").toLowerCase());
948
+ const appKey = computed(() =>
949
+ String(
950
+ runtimeConfig.public.APP || runtimeConfig.public.APP_NAME || ""
951
+ ).toLowerCase()
952
+ );
480
953
  const isSecurityDashboard = computed(() => appKey.value.includes("security"));
481
- const primaryListConfig = computed(() => 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");
954
+ const primaryListConfig = computed(() =>
955
+ isSecurityDashboard.value
956
+ ? {
957
+ title: "Active Patrol",
958
+ route: "virtualPatrol",
959
+ keys: ["activePatrolItems", "patrolItems"],
960
+ }
961
+ : {
962
+ title: "Today's Task Schedule",
963
+ route: "scheduleTask",
964
+ keys: [
965
+ "taskScheduleItems",
966
+ "scheduleTaskItems",
967
+ "scheduledTaskItems",
968
+ "cleaningScheduleItems",
969
+ "tasks",
970
+ ],
971
+ }
972
+ );
973
+ const staffStatusRoute = computed(() =>
974
+ isSecurityDashboard.value ? "manpower" : "attendance"
975
+ );
493
976
 
494
977
  const defaultVisibleWidgetKeys = computed(() => [
495
- ...props.cardValues.map((card) => card.key),
496
- "activity",
497
- "taskSchedule",
498
- "staffStatus",
499
- "feedbacks",
500
- "recentActivity",
978
+ ...resolvedCardValues.value.map((card) => card.key),
979
+ ...resolvedExtraWidgetKeys.value,
501
980
  ]);
502
981
 
503
982
  watch(
@@ -510,18 +989,26 @@ watch(
510
989
  return;
511
990
  }
512
991
 
513
- visibleWidgetKeys.value = visibleWidgetKeys.value.filter((key) => nextKeys.includes(key));
514
- draftVisibleWidgetKeys.value = draftVisibleWidgetKeys.value.filter((key) => nextKeys.includes(key));
992
+ visibleWidgetKeys.value = visibleWidgetKeys.value.filter((key) =>
993
+ nextKeys.includes(key)
994
+ );
995
+ draftVisibleWidgetKeys.value = draftVisibleWidgetKeys.value.filter((key) =>
996
+ nextKeys.includes(key)
997
+ );
515
998
  },
516
- { immediate: true },
999
+ { immediate: true }
517
1000
  );
518
1001
 
519
1002
  watch(selectedRange, (period) => {
1003
+ if (props.serviceType) {
1004
+ const apiPeriod = periodValue[period];
1005
+ if (apiPeriod) currentPeriod.value = apiPeriod;
1006
+ }
520
1007
  emit("updatePeriod", { key: "__all__", period });
521
1008
  });
522
1009
 
523
1010
  const customizeWidgets = computed(() => [
524
- ...props.cardValues.map((card) => ({
1011
+ ...resolvedCardValues.value.map((card) => ({
525
1012
  key: card.key,
526
1013
  title: card.title,
527
1014
  description: `Show ${card.title} metrics for the selected period.`,
@@ -538,7 +1025,7 @@ const customizeWidgets = computed(() => [
538
1025
  },
539
1026
  {
540
1027
  key: "staffStatus",
541
- title: "Staff Status",
1028
+ title: "Staff Total Attendance",
542
1029
  description: "See who is on duty, on break, or absent.",
543
1030
  },
544
1031
  {
@@ -557,43 +1044,58 @@ const filteredCustomizeWidgets = computed(() => {
557
1044
  const search = widgetSearch.value.trim().toLowerCase();
558
1045
  if (!search) return customizeWidgets.value;
559
1046
 
560
- return customizeWidgets.value.filter((widget) => (
561
- widget.title.toLowerCase().includes(search) ||
562
- widget.description.toLowerCase().includes(search)
563
- ));
1047
+ return customizeWidgets.value.filter(
1048
+ (widget) =>
1049
+ widget.title.toLowerCase().includes(search) ||
1050
+ widget.description.toLowerCase().includes(search)
1051
+ );
564
1052
  });
565
- const visitorItems = computed(() => getDashboardArray(["visitorItems", "visitorsList", "visitorTransactionsList"]));
566
- const visitorOptions = computed(() => buildFilterOptions(defaultVisitorOptions, visitorItems.value, getVisitorFilterValue));
1053
+ const visitorItems = computed(() =>
1054
+ getDashboardArray(["visitorItems", "visitorsList", "visitorTransactionsList"])
1055
+ );
1056
+ const visitorOptions = computed(() =>
1057
+ buildFilterOptions(
1058
+ defaultVisitorOptions,
1059
+ visitorItems.value,
1060
+ getVisitorFilterValue
1061
+ )
1062
+ );
567
1063
 
568
- const kpiCards = computed(() => 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);
1064
+ const kpiCards = computed(() =>
1065
+ resolvedCardValues.value.map((mapping, index) => {
1066
+ const metric =
1067
+ mapping.key === "visitors" && isSecurityDashboard.value
1068
+ ? getFilteredVisitorMetric()
1069
+ : getMetric([mapping.key]);
1070
+ const value = getMetricValue(metric, mapping.key);
573
1071
 
574
- 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
- }));
1072
+ return {
1073
+ key: mapping.key,
1074
+ title: mapping.title,
1075
+ icon: mapping.icon,
1076
+ tint: getCardTint(mapping.color, index),
1077
+ color: mapping.color || getCardColor(index),
1078
+ value: typeof value === "string" ? value : formatNumber(value),
1079
+ percentage: toNumber(metric.percentage),
1080
+ meta: getMetricMeta(metric),
1081
+ filter:
1082
+ mapping.key === "visitors" && isSecurityDashboard.value
1083
+ ? {
1084
+ model: visitorType.value,
1085
+ options: visitorOptions.value,
1086
+ set: (option: string) => {
1087
+ visitorType.value = option;
1088
+ filterMenus.visitors = false;
1089
+ },
1090
+ }
1091
+ : undefined,
1092
+ };
1093
+ })
1094
+ );
595
1095
 
596
- const visibleKpiCards = computed(() => kpiCards.value.filter((card) => isWidgetVisible(card.key)));
1096
+ const visibleKpiCards = computed(() =>
1097
+ kpiCards.value.filter((card) => isWidgetVisible(card.key))
1098
+ );
597
1099
 
598
1100
  const activityTitle = computed(() => {
599
1101
  if (selectedRange.value === "Today") return "Today's Activity";
@@ -601,241 +1103,111 @@ const activityTitle = computed(() => {
601
1103
  return "This Week Activity";
602
1104
  });
603
1105
 
604
- const activityChart = computed(() => normalizeActivityChart(getActivityChartSource()));
1106
+ const activityChart = computed(() =>
1107
+ normalizeActivityChart(getActivityChartSource())
1108
+ );
605
1109
  const chartLabels = computed<string[]>(() => {
606
1110
  const labels = activityChart.value.labels;
607
1111
  if (labels.length) return labels;
608
1112
 
609
- if (selectedRange.value === "Today") 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"];
1113
+ if (selectedRange.value === "Today")
1114
+ return ["12 AM", "4 AM", "8 AM", "12 PM", "4 PM", "8 PM"];
1115
+ if (selectedRange.value === "This Month")
1116
+ return ["Week 1", "Week 2", "Week 3", "Week 4"];
611
1117
  return ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
612
1118
  });
613
- const primarySeries = computed(() => 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];
1119
+ const primarySeries = computed(() =>
1120
+ normalizeSeries(activityChart.value.primary, chartLabels.value.length)
1121
+ );
1122
+ const secondarySeries = computed(() =>
1123
+ normalizeSeries(activityChart.value.secondary, chartLabels.value.length)
1124
+ );
1125
+ const primaryLegendLabel = computed(
1126
+ () => activityChart.value.primaryLabel || "Work Order"
1127
+ );
1128
+ const secondaryLegendLabel = computed(
1129
+ () => activityChart.value.secondaryLabel || "Feedback"
1130
+ );
1131
+ const chartMax = computed(() =>
1132
+ Math.max(1, ...primarySeries.value, ...secondarySeries.value)
1133
+ );
1134
+ const hasChartData = computed(
1135
+ () => primarySeries.value.some(Boolean) || secondarySeries.value.some(Boolean)
1136
+ );
1137
+ const chartTicks = [28, 66.4, 104.8, 143.2, 181.6, 220];
1138
+
1139
+ const chartYLabels = computed(() => {
1140
+ const max = chartMax.value;
1141
+ return [
1142
+ { text: "0", y: 220 },
1143
+ { text: `$${Math.round(max * 0.2)}`, y: 181.6 },
1144
+ { text: `$${Math.round(max * 0.4)}`, y: 143.2 },
1145
+ { text: `$${Math.round(max * 0.6)}`, y: 104.8 },
1146
+ { text: `$${Math.round(max * 0.8)}`, y: 66.4 },
1147
+ { text: `$${Math.round(max)}`, y: 28 },
1148
+ ];
1149
+ });
1150
+
1151
+ function buildSmoothPath(points: { x: number; y: number }[]) {
1152
+ if (!points || points.length === 0) return "";
1153
+ let d = `M ${points[0].x},${points[0].y}`;
1154
+ for (let i = 0; i < points.length - 1; i++) {
1155
+ const p1 = points[i];
1156
+ const p2 = points[i + 1];
1157
+ const cp1x = p1.x + (p2.x - p1.x) / 2;
1158
+ const cp1y = p1.y;
1159
+ const cp2x = p1.x + (p2.x - p1.x) / 2;
1160
+ const cp2y = p2.y;
1161
+ d += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p2.x},${p2.y}`;
1162
+ }
1163
+ return d;
1164
+ }
1165
+
620
1166
  const primaryDots = computed(() => buildChartDots(primarySeries.value));
621
1167
  const secondaryDots = computed(() => buildChartDots(secondarySeries.value));
622
- const primaryPoints = computed(() => primaryDots.value.map((point) => `${point.x},${point.y}`).join(" "));
623
- const secondaryPoints = computed(() => secondaryDots.value.map((point) => `${point.x},${point.y}`).join(" "));
1168
+ const primaryPath = computed(() => buildSmoothPath(primaryDots.value));
1169
+ const secondaryPath = computed(() => buildSmoothPath(secondaryDots.value));
624
1170
  const chartAxisLabels = computed(() => {
625
1171
  const labels = chartLabels.value;
626
1172
  const count = Math.max(1, labels.length - 1);
627
1173
 
628
1174
  return labels.map((label, index) => ({
629
1175
  text: label,
630
- x: 54 + ((846 / count) * index),
1176
+ x: 54 + (846 / count) * index,
631
1177
  }));
632
1178
  });
633
1179
 
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
- });
1180
+ const taskScheduleItems = computed<TaskItem[]>(() =>
1181
+ getDashboardArray([...primaryListConfig.value.keys])
1182
+ .map(normalizeTaskItem)
1183
+ .slice(0, 4)
1184
+ );
1185
+ const staffStatusItems = computed<StaffItem[]>(() =>
1186
+ getDashboardArray(["staffStatusItems", "attendanceItems", "staffItems"])
1187
+ .map(normalizeStaffItem)
1188
+ .slice(0, 4)
1189
+ );
1190
+ const feedbackItems = computed<FeedbackItem[]>(() =>
1191
+ getDashboardArray(["feedbackItems", "feedbacksList", "feedbacks"])
1192
+ .map(normalizeFeedbackItem)
1193
+ .slice(0, 4)
1194
+ );
1195
+ const recentActivities = computed<DashboardListItem[]>(() =>
1196
+ getDashboardArray(["recentActivities", "activityItems"])
1197
+ .map(normalizeActivityItem)
1198
+ .slice(0, 3)
1199
+ );
735
1200
 
736
- 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
- });
1201
+ function getToneColor(tone: string) {
1202
+ const map: Record<string, { bg: string; text: string }> = {
1203
+ info: { bg: "blue-lighten-4", text: "blue-darken-3" },
1204
+ danger: { bg: "red-lighten-4", text: "red-darken-3" },
1205
+ success: { bg: "green-lighten-4", text: "green-darken-3" },
1206
+ warning: { bg: "orange-lighten-4", text: "orange-darken-3" },
1207
+ neutral: { bg: "grey-lighten-3", text: "blue-grey-darken-1" },
1208
+ };
1209
+ return map[tone] || map.neutral;
1210
+ }
839
1211
 
840
1212
  function getServiceDisplayName(value: string) {
841
1213
  const normalized = value.toLowerCase();
@@ -864,7 +1236,7 @@ function getServiceDisplayName(value: string) {
864
1236
  }
865
1237
 
866
1238
  function getMetric(keys: string[]): DashboardMetric {
867
- const data = props.dashboardData || {};
1239
+ const data = finalDashboardData.value || {};
868
1240
  for (const key of keys) {
869
1241
  const value = data[key];
870
1242
  if (value && typeof value === "object" && !Array.isArray(value)) {
@@ -892,17 +1264,39 @@ function getMetricValue(metric: DashboardMetric, key?: string) {
892
1264
  function getMetricMeta(metric: DashboardMetric) {
893
1265
  const parts: string[] = [];
894
1266
 
895
- if (typeof metric.overdue !== "undefined") 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`);
1267
+ if (typeof metric.overdue !== "undefined")
1268
+ parts.push(`${formatNumber(toNumber(metric.overdue))} overdue`);
1269
+ if (typeof metric.inProgress !== "undefined")
1270
+ parts.push(`${formatNumber(toNumber(metric.inProgress))} in progress`);
1271
+ if (typeof metric.pending !== "undefined")
1272
+ parts.push(`${formatNumber(toNumber(metric.pending))} pending`);
1273
+ if (typeof metric.highSeverity !== "undefined")
1274
+ parts.push(`${formatNumber(toNumber(metric.highSeverity))} high severity`);
1275
+ if (typeof metric.currentlyOnSite !== "undefined")
1276
+ parts.push(
1277
+ `${formatNumber(toNumber(metric.currentlyOnSite))} currently on site`
1278
+ );
1279
+ if (
1280
+ typeof (metric as Record<string, any>).missedCheckpoints !== "undefined" ||
1281
+ typeof (metric as Record<string, any>).missed !== "undefined"
1282
+ ) {
1283
+ parts.push(
1284
+ `${formatNumber(
1285
+ toNumber(
1286
+ (metric as Record<string, any>).missedCheckpoints ??
1287
+ (metric as Record<string, any>).missed
1288
+ )
1289
+ )} checkpoint missed`
1290
+ );
902
1291
  }
903
- if (typeof metric.lowStock !== "undefined") 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") {
1292
+ if (typeof metric.lowStock !== "undefined")
1293
+ parts.push(`${formatNumber(toNumber(metric.lowStock))} low stock`);
1294
+ if (typeof metric.outOfStock !== "undefined")
1295
+ parts.push(`${formatNumber(toNumber(metric.outOfStock))} out of stock`);
1296
+ if (
1297
+ typeof metric.completed !== "undefined" &&
1298
+ typeof metric.total !== "undefined"
1299
+ ) {
906
1300
  parts.push(`${formatNumber(toNumber(metric.completed))} completed`);
907
1301
  }
908
1302
 
@@ -918,29 +1312,38 @@ function getCardTint(color: string, index: number) {
918
1312
  if (!color) return tints[index % tints.length];
919
1313
 
920
1314
  const normalized = color.toLowerCase();
921
- if (normalized.includes("f44336") || normalized.includes("d32f2f")) return "#f8c6c3";
922
- if (normalized.includes("4caf50") || normalized.includes("43a047")) return "#d8f5e5";
923
- if (normalized.includes("ff9800") || normalized.includes("ffa726")) return "#ffe7c2";
1315
+ if (normalized.includes("f44336") || normalized.includes("d32f2f"))
1316
+ return "#f8c6c3";
1317
+ if (normalized.includes("4caf50") || normalized.includes("43a047"))
1318
+ return "#d8f5e5";
1319
+ if (normalized.includes("ff9800") || normalized.includes("ffa726"))
1320
+ return "#ffe7c2";
924
1321
  return "#dbeeff";
925
1322
  }
926
1323
 
927
1324
  function getActivityChartSource() {
928
- const data = props.dashboardData || {};
929
- return data.activityChart ||
1325
+ const data = finalDashboardData.value || {};
1326
+ return (
1327
+ data.activityChart ||
930
1328
  data.statistic?.activityChart ||
931
1329
  data.statistics?.activityChart ||
932
1330
  data.statisticChart ||
933
1331
  data.statisticsChart ||
934
1332
  data.activityStatistics ||
935
1333
  data.activityStatistic ||
936
- {};
1334
+ {}
1335
+ );
937
1336
  }
938
1337
 
939
1338
  function normalizeActivityChart(source: any) {
940
1339
  const labels = Array.isArray(source?.labels)
941
1340
  ? source.labels.map((label: any) => String(label))
942
1341
  : [];
943
- const primary = extractChartSeries(source, ["workOrders", "workOrder", "work_orders"]);
1342
+ const primary = extractChartSeries(source, [
1343
+ "workOrders",
1344
+ "workOrder",
1345
+ "work_orders",
1346
+ ]);
944
1347
  const secondaryCandidate = getSecondaryChartSeries(source);
945
1348
 
946
1349
  return {
@@ -954,7 +1357,10 @@ function normalizeActivityChart(source: any) {
954
1357
 
955
1358
  function getSecondaryChartSeries(source: any) {
956
1359
  const candidates = [
957
- { label: "Task Completed", keys: ["taskCompleted", "completedTasks", "completed"] },
1360
+ {
1361
+ label: "Task Completed",
1362
+ keys: ["taskCompleted", "completedTasks", "completed"],
1363
+ },
958
1364
  { label: "Incident", keys: ["incidents", "incident", "incidentReports"] },
959
1365
  { label: "Feedback", keys: ["feedbacks", "feedback", "feedbackTickets"] },
960
1366
  ];
@@ -975,7 +1381,9 @@ function extractChartSeries(source: any, keys: string[]) {
975
1381
 
976
1382
  if (Array.isArray(source?.datasets)) {
977
1383
  const dataset = source.datasets.find((item: any) => {
978
- const keyText = String(item?.key || item?.label || item?.name || "").toLowerCase();
1384
+ const keyText = String(
1385
+ item?.key || item?.label || item?.name || ""
1386
+ ).toLowerCase();
979
1387
  return keys.some((key) => keyText.includes(key.toLowerCase()));
980
1388
  });
981
1389
  if (Array.isArray(dataset?.data)) return dataset.data.map(toNumber);
@@ -986,20 +1394,22 @@ function extractChartSeries(source: any, keys: string[]) {
986
1394
 
987
1395
  function normalizeSeries(value: any, targetLength: number) {
988
1396
  const source = Array.isArray(value) ? value : [];
989
- return Array.from({ length: targetLength }, (_, index) => toNumber(source[index]));
1397
+ return Array.from({ length: targetLength }, (_, index) =>
1398
+ toNumber(source[index])
1399
+ );
990
1400
  }
991
1401
 
992
1402
  function buildChartDots(series: number[]) {
993
1403
  const count = Math.max(1, series.length - 1);
994
1404
 
995
1405
  return series.map((value, index) => ({
996
- x: 54 + ((846 / count) * index),
997
- y: 220 - ((toNumber(value) / chartMax.value) * 184),
1406
+ x: 54 + (846 / count) * index,
1407
+ y: 220 - (toNumber(value) / chartMax.value) * 184,
998
1408
  }));
999
1409
  }
1000
1410
 
1001
1411
  function getDashboardArray(keys: string[]) {
1002
- const data = props.dashboardData || {};
1412
+ const data = finalDashboardData.value || {};
1003
1413
 
1004
1414
  for (const key of keys) {
1005
1415
  const value = data[key];
@@ -1021,23 +1431,32 @@ function getFilteredVisitorMetric(): DashboardMetric {
1021
1431
  return {
1022
1432
  ...metric,
1023
1433
  count: toNumber(metric.count ?? items.length),
1024
- currentlyOnSite: toNumber(metric.currentlyOnSite ?? items.filter((item) => !isClosedStatus(item.status) && !item.checkOut).length),
1434
+ currentlyOnSite: toNumber(
1435
+ metric.currentlyOnSite ??
1436
+ items.filter(
1437
+ (item: any) => !isClosedStatus(item.status) && !item.checkOut
1438
+ ).length
1439
+ ),
1025
1440
  };
1026
1441
  }
1027
1442
 
1028
- const filtered = items.filter((item) => getVisitorFilterValue(item) === selected);
1443
+ const filtered = items.filter(
1444
+ (item: any) => getVisitorFilterValue(item) === selected
1445
+ );
1029
1446
 
1030
1447
  return {
1031
1448
  ...metric,
1032
1449
  count: filtered.length,
1033
- currentlyOnSite: filtered.filter((item) => !isClosedStatus(item.status) && !item.checkOut).length,
1450
+ currentlyOnSite: filtered.filter(
1451
+ (item: any) => !isClosedStatus(item.status) && !item.checkOut
1452
+ ).length,
1034
1453
  };
1035
1454
  }
1036
1455
 
1037
1456
  function buildFilterOptions(
1038
1457
  defaults: string[],
1039
1458
  items: Record<string, any>[],
1040
- getValue: (item: Record<string, any>) => string,
1459
+ getValue: (item: Record<string, any>) => string
1041
1460
  ) {
1042
1461
  const options = new Set(defaults);
1043
1462
  items.forEach((item) => {
@@ -1049,7 +1468,9 @@ function buildFilterOptions(
1049
1468
  }
1050
1469
 
1051
1470
  function getVisitorFilterValue(item: Record<string, any>) {
1052
- const raw = String(item.type || item.visitorType || item.category || "").toLowerCase();
1471
+ const raw = String(
1472
+ item.type || item.visitorType || item.category || ""
1473
+ ).toLowerCase();
1053
1474
  if (raw.includes("contractor")) return "Contractor";
1054
1475
  if (raw.includes("walk")) return "Walk-In";
1055
1476
  if (raw.includes("delivery")) return "Delivery";
@@ -1060,38 +1481,95 @@ function getVisitorFilterValue(item: Record<string, any>) {
1060
1481
  }
1061
1482
 
1062
1483
  function normalizeTaskItem(item: Record<string, any>, index: number): TaskItem {
1063
- const title = 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";
1484
+ const title =
1485
+ item.title ||
1486
+ item.subject ||
1487
+ item.name ||
1488
+ item.taskName ||
1489
+ item.description ||
1490
+ "Task";
1491
+ const subtitle =
1492
+ item.subtitle ||
1493
+ item.area ||
1494
+ item.location ||
1495
+ item.asset ||
1496
+ item.category ||
1497
+ "";
1498
+ const person =
1499
+ item.person ||
1500
+ item.assigneeName ||
1501
+ item.assignedTo?.name ||
1502
+ item.staff?.name ||
1503
+ item.createdBy?.name ||
1504
+ "Unassigned";
1066
1505
 
1067
1506
  return {
1068
1507
  id: String(item.id || item._id || `${title}-${index}`),
1069
1508
  title,
1070
1509
  subtitle,
1071
1510
  person,
1072
- status: normalizeStatus(item.status || item.state || item.taskStatus || "To-Do"),
1511
+ status: normalizeStatus(
1512
+ item.status || item.state || item.taskStatus || "To-Do"
1513
+ ),
1514
+ time:
1515
+ item.time ||
1516
+ item.startTime ||
1517
+ item.scheduleTime ||
1518
+ formatActivityTime(item),
1073
1519
  raw: item,
1074
1520
  };
1075
1521
  }
1076
1522
 
1077
- function normalizeStaffItem(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";
1523
+ function normalizeStaffItem(
1524
+ item: Record<string, any>,
1525
+ index: number
1526
+ ): StaffItem {
1527
+ const name =
1528
+ item.name ||
1529
+ item.employeeName ||
1530
+ item.user?.userName ||
1531
+ item.staff?.userName ||
1532
+ item.userName;
1533
+ const role =
1534
+ item.role ||
1535
+ item.userRole ||
1536
+ item.userrole ||
1537
+ item.jobTitle ||
1538
+ item.designation ||
1539
+ item.position ||
1540
+ item.type;
1080
1541
 
1081
1542
  return {
1082
1543
  id: String(item.id || item._id || `${name}-${index}`),
1083
1544
  name,
1084
1545
  role,
1085
- status: normalizeStatus(item.statusLabel || item.status || item.currentStatus || item.attendanceStatus || "Off duty"),
1546
+ status: normalizeStatus(
1547
+ item.statusLabel ||
1548
+ item.status ||
1549
+ item.currentStatus ||
1550
+ item.attendanceStatus ||
1551
+ "Off duty"
1552
+ ),
1553
+ totalHours: item.totalHours || 0,
1086
1554
  raw: item,
1087
1555
  };
1088
1556
  }
1089
1557
 
1090
- function normalizeFeedbackItem(item: Record<string, any>, index: number): FeedbackItem {
1558
+ function normalizeFeedbackItem(
1559
+ item: Record<string, any>,
1560
+ index: number
1561
+ ): FeedbackItem {
1091
1562
  const title = item.title || item.subject || item.description || "Feedback";
1092
1563
  const subject = item.category || item.type || item.service || "Subject";
1093
1564
  const app = item.app || item.source || item.channel || "";
1094
- const person = item.person || item.createdByName || item.submittedBy || item.createdBy?.name || item.user?.name || "Unassigned";
1565
+ const person =
1566
+ item.createdByName ||
1567
+ item.person ||
1568
+ item.userName ||
1569
+ item.submittedBy ||
1570
+ item.createdBy?.name ||
1571
+ item.user?.name ||
1572
+ "Unassigned";
1095
1573
 
1096
1574
  return {
1097
1575
  id: String(item.id || item._id || `${title}-${index}`),
@@ -1103,9 +1581,18 @@ function normalizeFeedbackItem(item: Record<string, any>, index: number): Feedba
1103
1581
  };
1104
1582
  }
1105
1583
 
1106
- function normalizeActivityItem(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 || "";
1584
+ function normalizeActivityItem(
1585
+ item: Record<string, any>,
1586
+ index: number
1587
+ ): DashboardListItem {
1588
+ const title =
1589
+ item.title ||
1590
+ item.subject ||
1591
+ item.description ||
1592
+ item.message ||
1593
+ "Activity";
1594
+ const subtitle =
1595
+ item.subtitle || item.type || item.category || item.status || "";
1109
1596
 
1110
1597
  return {
1111
1598
  id: String(item.id || item._id || `${title}-${index}`),
@@ -1125,7 +1612,11 @@ function formatActivityTime(item: Record<string, any>) {
1125
1612
  const rawText = String(rawTime || "");
1126
1613
 
1127
1614
  if (!rawText) return "";
1128
- if (/\b(ago|now|today|yesterday|min|mins|hr|hrs|hour|hours|day|days)\b/i.test(rawText)) {
1615
+ if (
1616
+ /\b(ago|now|today|yesterday|min|mins|hr|hrs|hour|hours|day|days)\b/i.test(
1617
+ rawText
1618
+ )
1619
+ ) {
1129
1620
  return rawText;
1130
1621
  }
1131
1622
 
@@ -1160,7 +1651,14 @@ function getActivityToneStyle(tone: string) {
1160
1651
  }
1161
1652
 
1162
1653
  function getItemTime(item: Record<string, any>) {
1163
- return item.createdAt || item.updatedAt || item.date || item.time || item.created_on || item.updated_on;
1654
+ return (
1655
+ item.createdAt ||
1656
+ item.updatedAt ||
1657
+ item.date ||
1658
+ item.time ||
1659
+ item.created_on ||
1660
+ item.updated_on
1661
+ );
1164
1662
  }
1165
1663
 
1166
1664
  function normalizeStatus(value: any) {
@@ -1174,29 +1672,53 @@ function normalizeStatus(value: any) {
1174
1672
 
1175
1673
  function getStatusTone(status: any) {
1176
1674
  const normalized = String(status || "").toLowerCase();
1177
- if (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";
1675
+ if (
1676
+ normalized.includes("complete") ||
1677
+ normalized.includes("done") ||
1678
+ normalized.includes("resolved") ||
1679
+ normalized.includes("closed")
1680
+ )
1681
+ return "success";
1682
+ if (
1683
+ normalized.includes("progress") ||
1684
+ normalized.includes("active") ||
1685
+ normalized.includes("ongoing")
1686
+ )
1687
+ return "info";
1688
+ if (
1689
+ normalized.includes("overdue") ||
1690
+ normalized.includes("urgent") ||
1691
+ normalized.includes("high")
1692
+ )
1693
+ return "danger";
1180
1694
  return "neutral";
1181
1695
  }
1182
1696
 
1183
1697
  function getStaffTone(status: any) {
1184
1698
  const normalized = String(status || "").toLowerCase();
1185
- if (normalized.includes("duty") || normalized.includes("present") || normalized.includes("active")) return "success";
1699
+ if (
1700
+ normalized.includes("duty") ||
1701
+ normalized.includes("present") ||
1702
+ normalized.includes("active")
1703
+ )
1704
+ return "success";
1186
1705
  if (normalized.includes("break")) return "warning";
1187
- if (normalized.includes("absent") || normalized.includes("off")) return "danger";
1706
+ if (normalized.includes("absent") || normalized.includes("off"))
1707
+ return "danger";
1188
1708
  return "neutral";
1189
1709
  }
1190
1710
 
1191
1711
  function isClosedStatus(status: any) {
1192
1712
  const normalized = String(status || "").toLowerCase();
1193
- return normalized.includes("complete") ||
1713
+ return (
1714
+ normalized.includes("complete") ||
1194
1715
  normalized.includes("cancel") ||
1195
1716
  normalized.includes("reject") ||
1196
1717
  normalized.includes("closed") ||
1197
1718
  normalized.includes("deleted") ||
1198
1719
  normalized.includes("check-out") ||
1199
- normalized.includes("checkout");
1720
+ normalized.includes("checkout")
1721
+ );
1200
1722
  }
1201
1723
 
1202
1724
  function isWidgetVisible(key: string) {
@@ -1272,7 +1794,11 @@ function getInitials(name: string) {
1272
1794
  .split(/\s+/)
1273
1795
  .filter(Boolean);
1274
1796
  if (!parts.length) return "?";
1275
- return parts.slice(0, 2).map((part) => part[0]).join("").toUpperCase();
1797
+ return parts
1798
+ .slice(0, 2)
1799
+ .map((part) => part[0])
1800
+ .join("")
1801
+ .toUpperCase();
1276
1802
  }
1277
1803
 
1278
1804
  function formatNumber(value: number) {
@@ -1304,797 +1830,3 @@ function formatRelativeTime(value: any) {
1304
1830
  return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
1305
1831
  }
1306
1832
  </script>
1307
-
1308
- <style scoped>
1309
- .service-dashboard {
1310
- color: #132b43;
1311
- padding-bottom: 32px;
1312
- }
1313
-
1314
- .dashboard-header {
1315
- align-items: flex-start;
1316
- display: flex;
1317
- gap: 20px;
1318
- justify-content: space-between;
1319
- margin-bottom: 24px;
1320
- }
1321
-
1322
- .dashboard-title {
1323
- color: #0b2f47;
1324
- font-size: 28px;
1325
- font-weight: 800;
1326
- letter-spacing: 0;
1327
- line-height: 1.2;
1328
- margin: 0;
1329
- }
1330
-
1331
- .site-label {
1332
- align-items: center;
1333
- color: #6d7d8b;
1334
- display: flex;
1335
- font-size: 13px;
1336
- gap: 6px;
1337
- margin-top: 8px;
1338
- }
1339
-
1340
- .header-actions {
1341
- align-items: center;
1342
- display: flex;
1343
- flex-wrap: wrap;
1344
- gap: 12px;
1345
- justify-content: flex-end;
1346
- }
1347
-
1348
- .action-button {
1349
- border-color: #d9e1e8;
1350
- border-radius: 6px;
1351
- color: #102b40;
1352
- height: 36px;
1353
- font-weight: 700;
1354
- letter-spacing: 0.04em;
1355
- min-height: 36px;
1356
- padding: 0 16px;
1357
- }
1358
-
1359
- .action-button :deep(.v-btn__content) {
1360
- font-size: 15px;
1361
- line-height: 1;
1362
- }
1363
-
1364
- .action-button :deep(.v-btn__prepend) {
1365
- margin-inline: 0 8px;
1366
- }
1367
-
1368
- .action-button :deep(.v-btn__append) {
1369
- margin-inline: 8px 0;
1370
- }
1371
-
1372
- .range-toggle {
1373
- border: 1px solid #d9e1e8;
1374
- border-radius: 6px;
1375
- height: 36px;
1376
- min-height: 36px;
1377
- overflow: hidden;
1378
- }
1379
-
1380
- .range-toggle :deep(.v-btn) {
1381
- border-radius: 0;
1382
- color: #102b40;
1383
- font-size: 14px;
1384
- font-weight: 500;
1385
- height: 34px;
1386
- letter-spacing: 0.04em;
1387
- min-height: 34px;
1388
- min-width: 62px;
1389
- padding: 0 20px;
1390
- }
1391
-
1392
- .range-toggle :deep(.v-btn--active) {
1393
- background: #e0e0e0;
1394
- }
1395
-
1396
- .range-toggle :deep(.v-btn__overlay) {
1397
- opacity: 0;
1398
- }
1399
-
1400
- .export-menu,
1401
- .filter-menu {
1402
- border-color: #d9e1e8;
1403
- border-radius: 7px;
1404
- box-shadow: 0 12px 30px rgba(16, 35, 54, 0.12);
1405
- }
1406
-
1407
- .kpi-grid {
1408
- margin-bottom: 14px;
1409
- }
1410
-
1411
- .dashboard-card {
1412
- background: #fff;
1413
- border: 1px solid #d9e0e7;
1414
- border-radius: 8px;
1415
- box-shadow: 0 1px 2px rgba(15, 35, 54, 0.04);
1416
- }
1417
-
1418
- .kpi-card {
1419
- display: flex;
1420
- flex-direction: column;
1421
- min-height: 158px;
1422
- padding: 16px 18px 18px;
1423
- }
1424
-
1425
- .kpi-topline {
1426
- align-items: flex-start;
1427
- display: flex;
1428
- justify-content: space-between;
1429
- }
1430
-
1431
- .kpi-icon {
1432
- align-items: center;
1433
- border-radius: 8px;
1434
- display: inline-flex;
1435
- height: 28px;
1436
- justify-content: center;
1437
- width: 28px;
1438
- }
1439
-
1440
- .kpi-icon :deep(.v-icon) {
1441
- font-size: 15px;
1442
- height: 15px;
1443
- width: 15px;
1444
- }
1445
-
1446
- .mini-filter {
1447
- border-color: #d9e1e8;
1448
- font-size: 12px;
1449
- height: 26px;
1450
- min-width: 50px;
1451
- padding-inline: 10px 6px;
1452
- }
1453
-
1454
- .mini-filter :deep(.v-btn__append) {
1455
- margin-inline-start: 4px;
1456
- }
1457
-
1458
- .kpi-content {
1459
- display: flex;
1460
- flex-direction: column;
1461
- margin-top: 26px;
1462
- min-width: 0;
1463
- }
1464
-
1465
- .kpi-label {
1466
- color: #2a465a;
1467
- font-size: 12px;
1468
- font-weight: 800;
1469
- letter-spacing: 0;
1470
- line-height: 1.2;
1471
- margin: 0 0 13px;
1472
- text-transform: uppercase;
1473
- }
1474
-
1475
- .kpi-value-row {
1476
- align-items: center;
1477
- display: flex;
1478
- min-height: 31px;
1479
- }
1480
-
1481
- .kpi-value-row strong {
1482
- color: #092f49;
1483
- font-size: 30px;
1484
- font-weight: 800;
1485
- letter-spacing: 0;
1486
- line-height: 1;
1487
- }
1488
-
1489
- .kpi-meta-row {
1490
- align-items: center;
1491
- display: flex;
1492
- flex-wrap: wrap;
1493
- gap: 8px;
1494
- margin-top: 12px;
1495
- min-height: 22px;
1496
- }
1497
-
1498
- .kpi-meta-row :deep(.v-chip) {
1499
- border-radius: 999px;
1500
- font-size: 11px;
1501
- font-weight: 800;
1502
- height: 20px;
1503
- padding-inline: 8px;
1504
- }
1505
-
1506
- .kpi-meta-row :deep(.v-chip__prepend) {
1507
- margin-inline-end: 3px;
1508
- }
1509
-
1510
- .kpi-meta {
1511
- color: #4f6576;
1512
- font-size: 12px;
1513
- font-weight: 600;
1514
- line-height: 1.35;
1515
- margin: 0;
1516
- }
1517
-
1518
- .activity-card {
1519
- padding: 18px 20px 14px;
1520
- }
1521
-
1522
- .section-header,
1523
- .panel-header {
1524
- align-items: center;
1525
- display: flex;
1526
- justify-content: space-between;
1527
- }
1528
-
1529
- .section-header h2,
1530
- .panel-header h2 {
1531
- color: #132b43;
1532
- font-size: 15px;
1533
- font-weight: 800;
1534
- letter-spacing: 0;
1535
- line-height: 1.2;
1536
- margin: 0;
1537
- }
1538
-
1539
- .chart-wrap {
1540
- margin-top: 18px;
1541
- }
1542
-
1543
- .activity-chart {
1544
- display: block;
1545
- height: 260px;
1546
- width: 100%;
1547
- }
1548
-
1549
- .chart-grid {
1550
- stroke: #e9eef2;
1551
- stroke-width: 1;
1552
- }
1553
-
1554
- .primary-line,
1555
- .secondary-line {
1556
- fill: none;
1557
- stroke-linecap: round;
1558
- stroke-linejoin: round;
1559
- stroke-width: 3;
1560
- }
1561
-
1562
- .primary-line {
1563
- stroke: #2196f3;
1564
- }
1565
-
1566
- .secondary-line {
1567
- stroke: #1eca6b;
1568
- }
1569
-
1570
- .primary-dot,
1571
- .secondary-dot {
1572
- stroke: #fff;
1573
- stroke-width: 2;
1574
- }
1575
-
1576
- .primary-dot {
1577
- fill: #2196f3;
1578
- }
1579
-
1580
- .secondary-dot {
1581
- fill: #1eca6b;
1582
- }
1583
-
1584
- .chart-label {
1585
- fill: #7e8b96;
1586
- font-size: 12px;
1587
- font-weight: 600;
1588
- }
1589
-
1590
- .chart-legend {
1591
- align-items: center;
1592
- display: flex;
1593
- gap: 18px;
1594
- justify-content: center;
1595
- margin-top: 10px;
1596
- }
1597
-
1598
- .chart-legend span {
1599
- align-items: center;
1600
- color: #6d7c88;
1601
- display: inline-flex;
1602
- font-size: 12px;
1603
- font-weight: 700;
1604
- gap: 7px;
1605
- }
1606
-
1607
- .legend-dot {
1608
- border-radius: 50%;
1609
- display: inline-flex;
1610
- height: 8px;
1611
- width: 8px;
1612
- }
1613
-
1614
- .legend-dot.primary {
1615
- background: #2196f3;
1616
- }
1617
-
1618
- .legend-dot.secondary {
1619
- background: #1eca6b;
1620
- }
1621
-
1622
- .dashboard-section-grid {
1623
- align-items: stretch;
1624
- margin-top: 14px;
1625
- }
1626
-
1627
- .dashboard-section-grid > .v-col {
1628
- display: flex;
1629
- }
1630
-
1631
- .dashboard-panel {
1632
- display: flex;
1633
- flex-direction: column;
1634
- height: 100%;
1635
- padding: 24px;
1636
- width: 100%;
1637
- }
1638
-
1639
- .dashboard-panel-recent {
1640
- min-height: 224px;
1641
- padding: 26px 30px 16px;
1642
- }
1643
-
1644
- .panel-body {
1645
- flex: 1;
1646
- margin-top: 20px;
1647
- max-height: 380px;
1648
- overflow-y: auto;
1649
- }
1650
-
1651
- .panel-action,
1652
- .row-action {
1653
- appearance: none;
1654
- background: transparent;
1655
- border: 0;
1656
- color: #2196f3;
1657
- cursor: pointer;
1658
- font-size: 12px;
1659
- font-weight: 800;
1660
- letter-spacing: 0;
1661
- line-height: 1.2;
1662
- padding: 0;
1663
- }
1664
-
1665
- .panel-action {
1666
- margin-right: -10px;
1667
- padding: 0 10px;
1668
- }
1669
-
1670
- .task-list,
1671
- .feedback-list,
1672
- .staff-list {
1673
- display: grid;
1674
- gap: 10px;
1675
- }
1676
-
1677
- .task-row,
1678
- .feedback-row {
1679
- align-items: center;
1680
- background: #fff;
1681
- border: 1px solid #e0e5ea;
1682
- border-radius: 8px;
1683
- display: grid;
1684
- gap: 16px;
1685
- min-height: 76px;
1686
- padding: 15px 15px 15px 10px;
1687
- }
1688
-
1689
- .task-row {
1690
- grid-template-columns: minmax(220px, 280px) minmax(130px, 170px) minmax(82px, 120px) minmax(44px, 1fr);
1691
- }
1692
-
1693
- .feedback-row {
1694
- grid-template-columns: minmax(220px, 280px) minmax(130px, 170px) minmax(82px, 120px) minmax(44px, 1fr);
1695
- }
1696
-
1697
- .task-row:hover,
1698
- .feedback-row:hover,
1699
- .staff-row:hover,
1700
- .simple-list-row:hover {
1701
- border-color: #d8e3ec;
1702
- box-shadow: 0 6px 14px rgba(15, 44, 69, 0.04);
1703
- }
1704
-
1705
- .task-summary,
1706
- .feedback-summary,
1707
- .staff-main,
1708
- .list-main {
1709
- min-width: 0;
1710
- }
1711
-
1712
- .task-summary,
1713
- .feedback-summary {
1714
- padding: 0 10px;
1715
- }
1716
-
1717
- .task-summary strong,
1718
- .feedback-summary strong,
1719
- .staff-main strong,
1720
- .list-main strong {
1721
- color: #132b43;
1722
- display: block;
1723
- font-size: 13px;
1724
- font-weight: 800;
1725
- line-height: 1.3;
1726
- white-space: normal;
1727
- }
1728
-
1729
- .task-summary p,
1730
- .feedback-summary p,
1731
- .staff-main p,
1732
- .list-main p {
1733
- color: #888;
1734
- font-size: 12px;
1735
- font-weight: 500;
1736
- line-height: 1.35;
1737
- margin: 5px 0 0;
1738
- overflow: hidden;
1739
- text-overflow: ellipsis;
1740
- white-space: nowrap;
1741
- }
1742
-
1743
- .person-cell {
1744
- align-items: center;
1745
- color: #888;
1746
- display: inline-flex;
1747
- font-size: 12px;
1748
- font-weight: 600;
1749
- gap: 7px;
1750
- min-width: 0;
1751
- }
1752
-
1753
- .person-cell span:last-child {
1754
- overflow: hidden;
1755
- text-overflow: ellipsis;
1756
- white-space: nowrap;
1757
- }
1758
-
1759
- .avatar {
1760
- align-items: center;
1761
- background: #e0e0e0;
1762
- border-radius: 999px;
1763
- color: #fff;
1764
- display: inline-flex;
1765
- flex: 0 0 34px;
1766
- font-size: 11px;
1767
- font-weight: 800;
1768
- height: 34px;
1769
- justify-content: center;
1770
- letter-spacing: 0;
1771
- width: 34px;
1772
- }
1773
-
1774
- .status-pill {
1775
- align-items: center;
1776
- border-radius: 999px;
1777
- display: inline-flex;
1778
- font-size: 12px;
1779
- font-weight: 600;
1780
- justify-content: center;
1781
- line-height: 1;
1782
- min-height: 26px;
1783
- padding: 6px 10px;
1784
- white-space: nowrap;
1785
- width: fit-content;
1786
- }
1787
-
1788
- .status-pill-success {
1789
- background: rgba(30, 202, 107, 0.5);
1790
- color: #015b29;
1791
- }
1792
-
1793
- .status-pill-info {
1794
- background: #d8ecff;
1795
- color: #14557d;
1796
- }
1797
-
1798
- .status-pill-warning {
1799
- background: #ffe9c7;
1800
- color: #9c5e0e;
1801
- }
1802
-
1803
- .status-pill-danger {
1804
- background: #ffd9d6;
1805
- color: #9f2f29;
1806
- }
1807
-
1808
- .status-pill-neutral {
1809
- background: #e0e0e0;
1810
- color: #616065;
1811
- }
1812
-
1813
- .row-action-right {
1814
- justify-self: end;
1815
- }
1816
-
1817
- .task-row .row-action-right {
1818
- align-items: center;
1819
- background: #fff;
1820
- border: 1px solid #b9ddfb;
1821
- border-radius: 8px;
1822
- box-shadow: 0 1px 3px rgba(33, 150, 243, 0.1);
1823
- color: #0b8ff1;
1824
- display: inline-flex;
1825
- height: 32px;
1826
- justify-content: center;
1827
- min-width: 54px;
1828
- padding: 0 12px;
1829
- }
1830
-
1831
- .task-row .row-action-right:hover {
1832
- background: #f2f9ff;
1833
- border-color: #80c3f7;
1834
- color: #0878d3;
1835
- }
1836
-
1837
- .feedback-row .row-action-right {
1838
- padding-right: 8px;
1839
- }
1840
-
1841
- .staff-row {
1842
- align-items: center;
1843
- background: #fff;
1844
- border: 1px solid #e0e5ea;
1845
- border-radius: 8px;
1846
- display: grid;
1847
- gap: 10px;
1848
- grid-template-columns: 34px minmax(0, 1fr) auto;
1849
- min-height: 68px;
1850
- padding: 12px 14px;
1851
- }
1852
-
1853
- .simple-list {
1854
- display: grid;
1855
- }
1856
-
1857
- .simple-list-row {
1858
- align-items: center;
1859
- background: #fff;
1860
- border-bottom: 1px solid #e8edf1;
1861
- display: grid;
1862
- gap: 14px;
1863
- grid-template-columns: 30px minmax(0, 1fr) 82px;
1864
- min-height: 58px;
1865
- padding: 9px 0;
1866
- }
1867
-
1868
- .simple-list-row:last-child {
1869
- border-bottom: 0;
1870
- }
1871
-
1872
- .simple-list-row:hover {
1873
- box-shadow: none;
1874
- }
1875
-
1876
- .list-icon {
1877
- align-items: center;
1878
- border-radius: 6px;
1879
- display: inline-flex;
1880
- flex: 0 0 auto;
1881
- font-size: 13px;
1882
- height: 25px;
1883
- justify-content: center;
1884
- width: 25px;
1885
- }
1886
-
1887
- .list-action {
1888
- color: #768493;
1889
- font-size: 12px;
1890
- font-weight: 700;
1891
- justify-self: end;
1892
- text-align: right;
1893
- white-space: nowrap;
1894
- }
1895
-
1896
- .list-icon-info {
1897
- background: #d8ecff;
1898
- color: #17496f;
1899
- }
1900
-
1901
- .list-icon-danger {
1902
- background: #ffd9d6;
1903
- color: #bd413c;
1904
- }
1905
-
1906
- .list-icon-success {
1907
- background: #d8f4e3;
1908
- color: #2b9160;
1909
- }
1910
-
1911
- .list-icon-warning {
1912
- background: #ffe9c7;
1913
- color: #be7418;
1914
- }
1915
-
1916
- .list-icon-neutral {
1917
- background: #edf2f6;
1918
- color: #607386;
1919
- }
1920
-
1921
- .empty-state {
1922
- align-items: center;
1923
- color: #8a98a5;
1924
- display: flex;
1925
- flex-direction: column;
1926
- font-size: 13px;
1927
- font-weight: 700;
1928
- gap: 8px;
1929
- justify-content: center;
1930
- min-height: 140px;
1931
- text-align: center;
1932
- }
1933
-
1934
- .empty-state i {
1935
- font-size: 28px;
1936
- }
1937
-
1938
- .empty-state-compact {
1939
- min-height: 72px;
1940
- }
1941
-
1942
- .customize-dialog {
1943
- background: #fff;
1944
- }
1945
-
1946
- .customize-dialog-header {
1947
- align-items: flex-start;
1948
- border-bottom: 1px solid #eef2f5;
1949
- display: flex;
1950
- justify-content: space-between;
1951
- padding: 24px 26px 18px;
1952
- }
1953
-
1954
- .customize-dialog-header h2 {
1955
- color: #0b2f47;
1956
- font-size: 20px;
1957
- font-weight: 800;
1958
- line-height: 1.2;
1959
- margin: 0;
1960
- }
1961
-
1962
- .customize-dialog-header p {
1963
- color: #71808d;
1964
- font-size: 13px;
1965
- margin: 6px 0 0;
1966
- }
1967
-
1968
- .customize-dialog-body {
1969
- padding: 20px 26px 8px;
1970
- }
1971
-
1972
- .reset-row {
1973
- display: flex;
1974
- justify-content: flex-end;
1975
- margin: 12px 0;
1976
- }
1977
-
1978
- .widget-list {
1979
- display: grid;
1980
- gap: 10px;
1981
- }
1982
-
1983
- .widget-row {
1984
- align-items: center;
1985
- border: 1px solid #e2e8ee;
1986
- border-radius: 8px;
1987
- display: flex;
1988
- gap: 16px;
1989
- justify-content: space-between;
1990
- padding: 14px 16px;
1991
- }
1992
-
1993
- .widget-row strong {
1994
- color: #132b43;
1995
- display: block;
1996
- font-size: 14px;
1997
- font-weight: 800;
1998
- }
1999
-
2000
- .widget-row p {
2001
- color: #71808d;
2002
- font-size: 12px;
2003
- margin: 4px 0 0;
2004
- }
2005
-
2006
- .customize-dialog-actions {
2007
- border-top: 1px solid #eef2f5;
2008
- gap: 10px;
2009
- justify-content: flex-end;
2010
- padding: 16px 26px 22px;
2011
- }
2012
-
2013
- .cancel-button {
2014
- color: #607386;
2015
- font-weight: 800;
2016
- }
2017
-
2018
- .submit-button {
2019
- background: #0b2f47;
2020
- color: #fff;
2021
- font-weight: 800;
2022
- min-width: 110px;
2023
- }
2024
-
2025
- @media (max-width: 960px) {
2026
- .dashboard-header {
2027
- align-items: stretch;
2028
- flex-direction: column;
2029
- }
2030
-
2031
- .header-actions {
2032
- justify-content: flex-start;
2033
- }
2034
-
2035
- .task-row,
2036
- .feedback-row {
2037
- grid-template-columns: 1fr;
2038
- }
2039
-
2040
- .row-action-right {
2041
- justify-self: start;
2042
- }
2043
-
2044
- .status-pill {
2045
- margin-left: 0;
2046
- }
2047
- }
2048
-
2049
- @media (max-width: 640px) {
2050
- .dashboard-title {
2051
- font-size: 24px;
2052
- }
2053
-
2054
- .header-actions,
2055
- .action-button,
2056
- .range-toggle {
2057
- width: 100%;
2058
- }
2059
-
2060
- .range-toggle :deep(.v-btn) {
2061
- flex: 1 1 0;
2062
- }
2063
-
2064
- .kpi-card,
2065
- .activity-card,
2066
- .dashboard-panel {
2067
- padding: 16px;
2068
- }
2069
-
2070
- .kpi-card {
2071
- min-height: 154px;
2072
- }
2073
-
2074
- .kpi-content {
2075
- margin-left: 0;
2076
- margin-top: 22px;
2077
- }
2078
-
2079
- .kpi-value-row strong {
2080
- font-size: 26px;
2081
- }
2082
-
2083
- .staff-row {
2084
- grid-template-columns: 30px minmax(0, 1fr);
2085
- }
2086
-
2087
- .staff-row .status-pill {
2088
- grid-column: 2;
2089
- }
2090
-
2091
- .simple-list-row {
2092
- grid-template-columns: 30px minmax(0, 1fr);
2093
- }
2094
-
2095
- .list-action {
2096
- grid-column: 2;
2097
- justify-self: start;
2098
- }
2099
- }
2100
- </style>