@7365admin1/layer-common 3.0.7 → 3.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/components/CameraMain.vue +17 -1
- package/components/DashboardMain.vue +1854 -848
- package/components/EntryPassInformation.vue +60 -0
- package/components/HidAccessLogDashboard.vue +1691 -0
- package/components/HidEnabledGate.vue +51 -0
- package/components/HidIdentityMapping.vue +975 -0
- package/components/HidQrCodeConfiguration.vue +618 -0
- package/components/HidReaderForm.vue +246 -0
- package/components/HidReaderManagement.vue +1233 -0
- package/components/HidServiceSettingsPanel.vue +150 -0
- package/components/HidUserEnrollment.vue +1274 -0
- package/components/Layout/NavigationDrawer.vue +31 -1
- package/components/MemberInformation.vue +23 -15
- package/components/NavigationItem.vue +11 -4
- package/components/OnlineFormFill.vue +34 -0
- package/components/PassInformation.vue +73 -34
- package/components/SiteSettings.vue +60 -116
- package/components/VisitorForm.vue +815 -290
- package/components/VisitorFormSelection.vue +12 -2
- package/components/VisitorManagement.vue +652 -205
- package/composables/useCommonPermission.ts +59 -0
- package/composables/useDashboard.ts +46 -1
- package/composables/useHidAmico.ts +152 -0
- package/composables/useHidNavigation.ts +106 -0
- package/composables/useMember.ts +7 -1
- package/composables/useOptionalServices.ts +118 -0
- package/composables/useSettingsPermission.ts +138 -0
- package/composables/useSiteSettings.ts +1 -1
- package/composables/useUser.ts +1 -2
- package/nuxt.config.ts +1 -1
- package/package.json +3 -2
- package/pages/[org]/[site]/access-mgmt/access-logs/index.vue +21 -0
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +21 -0
- package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +21 -0
- package/pages/[org]/[site]/access-mgmt/hid-users/index.vue +21 -0
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +21 -0
- package/types/local.d.ts +2 -1
- package/types/visitor.d.ts +4 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="pb-8">
|
|
2
|
+
<section class="unified-dashboard pb-8">
|
|
3
|
+
<!-- ── Header ──────────────────────────────────────────────────────────── -->
|
|
3
4
|
<div
|
|
4
5
|
class="d-flex flex-column flex-md-row align-md-start justify-space-between ga-5 mb-6"
|
|
5
6
|
>
|
|
@@ -14,8 +15,8 @@
|
|
|
14
15
|
</div>
|
|
15
16
|
|
|
16
17
|
<div
|
|
17
|
-
class="d-flex flex-wrap align-center justify-md-end ga-3"
|
|
18
18
|
v-if="showHeaderActions"
|
|
19
|
+
class="d-flex flex-wrap align-center justify-md-end ga-3"
|
|
19
20
|
>
|
|
20
21
|
<v-btn
|
|
21
22
|
variant="outlined"
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
divided
|
|
54
55
|
variant="outlined"
|
|
55
56
|
class="bg-white"
|
|
57
|
+
style="border-radius: 8px; border-color: #d9e1e8"
|
|
56
58
|
>
|
|
57
59
|
<v-btn
|
|
58
60
|
v-for="range in rangeOptions"
|
|
@@ -62,8 +64,8 @@
|
|
|
62
64
|
size="small"
|
|
63
65
|
:style="
|
|
64
66
|
selectedRange === range.value
|
|
65
|
-
? { backgroundColor: '#
|
|
66
|
-
: {}
|
|
67
|
+
? { backgroundColor: '#e2e8f0', color: '#0b2f47', fontWeight: '600' }
|
|
68
|
+
: { backgroundColor: 'transparent', color: '#64748b' }
|
|
67
69
|
"
|
|
68
70
|
>
|
|
69
71
|
{{ range.label }}
|
|
@@ -79,6 +81,7 @@
|
|
|
79
81
|
class="mb-4"
|
|
80
82
|
/>
|
|
81
83
|
|
|
84
|
+
<!-- ── KPI Cards ───────────────────────────────────────────────────────── -->
|
|
82
85
|
<v-row dense class="mb-1 align-stretch">
|
|
83
86
|
<v-col
|
|
84
87
|
v-for="card in visibleKpiCards"
|
|
@@ -86,6 +89,7 @@
|
|
|
86
89
|
cols="12"
|
|
87
90
|
sm="6"
|
|
88
91
|
md="4"
|
|
92
|
+
lg="3"
|
|
89
93
|
class="d-flex"
|
|
90
94
|
>
|
|
91
95
|
<v-card flat border rounded="lg" class="pa-4 flex-grow-1" style="min-height: 158px">
|
|
@@ -104,9 +108,10 @@
|
|
|
104
108
|
v-bind="menuProps"
|
|
105
109
|
variant="outlined"
|
|
106
110
|
density="compact"
|
|
107
|
-
class="text-none text-caption"
|
|
111
|
+
class="text-none text-caption px-2"
|
|
108
112
|
append-icon="mdi-chevron-down"
|
|
109
|
-
|
|
113
|
+
height="26"
|
|
114
|
+
rounded="pill"
|
|
110
115
|
>
|
|
111
116
|
{{ card.filter.model }}
|
|
112
117
|
</v-btn>
|
|
@@ -122,9 +127,7 @@
|
|
|
122
127
|
>
|
|
123
128
|
<template #prepend>
|
|
124
129
|
<v-icon
|
|
125
|
-
:icon="
|
|
126
|
-
card.filter.model === option ? 'mdi-check' : 'mdi-blank'
|
|
127
|
-
"
|
|
130
|
+
:icon="card.filter.model === option ? 'mdi-check' : 'mdi-blank'"
|
|
128
131
|
size="16"
|
|
129
132
|
/>
|
|
130
133
|
</template>
|
|
@@ -140,9 +143,7 @@
|
|
|
140
143
|
{{ card.title }}
|
|
141
144
|
</p>
|
|
142
145
|
<template
|
|
143
|
-
v-if="
|
|
144
|
-
card.key === 'supplyAlert' && Array.isArray(card.raw?.items)
|
|
145
|
-
"
|
|
146
|
+
v-if="card.key === 'supplyAlert' && Array.isArray(card.raw?.items)"
|
|
146
147
|
>
|
|
147
148
|
<div class="d-flex flex-column ga-2 mt-2">
|
|
148
149
|
<div
|
|
@@ -157,10 +158,9 @@
|
|
|
157
158
|
</template>
|
|
158
159
|
<template v-else>
|
|
159
160
|
<div class="d-flex align-center mb-3" style="min-height: 31px">
|
|
160
|
-
<span
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
>
|
|
161
|
+
<span class="text-h5 font-weight-black text-blue-grey-darken-4">
|
|
162
|
+
{{ card.value }}
|
|
163
|
+
</span>
|
|
164
164
|
</div>
|
|
165
165
|
<div class="d-flex align-center flex-wrap ga-2">
|
|
166
166
|
<span v-if="card.meta" class="text-caption text-blue-grey">{{
|
|
@@ -190,6 +190,7 @@
|
|
|
190
190
|
</v-col>
|
|
191
191
|
</v-row>
|
|
192
192
|
|
|
193
|
+
<!-- ── Activity Chart (all modes) ────────────────────────────────────── -->
|
|
193
194
|
<v-card
|
|
194
195
|
v-if="isWidgetVisible('activity')"
|
|
195
196
|
flat
|
|
@@ -283,12 +284,7 @@
|
|
|
283
284
|
>
|
|
284
285
|
<span
|
|
285
286
|
class="rounded-circle"
|
|
286
|
-
style="
|
|
287
|
-
width: 12px;
|
|
288
|
-
height: 12px;
|
|
289
|
-
display: inline-block;
|
|
290
|
-
background-color: #ff9b9b;
|
|
291
|
-
"
|
|
287
|
+
style="width: 12px; height: 12px; display: inline-block; background-color: #ff9b9b"
|
|
292
288
|
/>
|
|
293
289
|
{{ primaryLegendLabel }}
|
|
294
290
|
</span>
|
|
@@ -297,12 +293,7 @@
|
|
|
297
293
|
>
|
|
298
294
|
<span
|
|
299
295
|
class="rounded-circle"
|
|
300
|
-
style="
|
|
301
|
-
width: 12px;
|
|
302
|
-
height: 12px;
|
|
303
|
-
display: inline-block;
|
|
304
|
-
background-color: #e53935;
|
|
305
|
-
"
|
|
296
|
+
style="width: 12px; height: 12px; display: inline-block; background-color: #e53935"
|
|
306
297
|
/>
|
|
307
298
|
{{ secondaryLegendLabel }}
|
|
308
299
|
</span>
|
|
@@ -312,7 +303,8 @@
|
|
|
312
303
|
<DashboardEmptyState v-else />
|
|
313
304
|
</v-card>
|
|
314
305
|
|
|
315
|
-
|
|
306
|
+
<!-- ── Hygiene / M&E / Landscape / Pest / Pool — task + attendance ────── -->
|
|
307
|
+
<v-row v-if="isHygieneMode" dense class="mt-3 align-stretch">
|
|
316
308
|
<v-col
|
|
317
309
|
v-if="isWidgetVisible('taskSchedule')"
|
|
318
310
|
cols="12"
|
|
@@ -342,21 +334,14 @@
|
|
|
342
334
|
</div>
|
|
343
335
|
<div class="text-caption text-blue-grey mt-1 text-truncate">
|
|
344
336
|
{{ item.subtitle
|
|
345
|
-
}}<template v-if="item.subtitle && item.time">
|
|
346
|
-
• </template
|
|
337
|
+
}}<template v-if="item.subtitle && item.time"> • </template
|
|
347
338
|
>{{ item.time }}
|
|
348
339
|
</div>
|
|
349
340
|
</div>
|
|
350
341
|
<div
|
|
351
342
|
class="d-flex align-center ga-2 text-caption text-blue-grey"
|
|
352
343
|
>
|
|
353
|
-
<
|
|
354
|
-
size="30"
|
|
355
|
-
color="blue-grey-lighten-4"
|
|
356
|
-
class="text-caption font-weight-bold text-blue-grey-darken-2"
|
|
357
|
-
>
|
|
358
|
-
{{ getInitials(item.person) }}
|
|
359
|
-
</v-avatar>
|
|
344
|
+
<AvatarMain :name="item.person" :size="30" />
|
|
360
345
|
<span class="text-truncate" style="max-width: 100px">{{
|
|
361
346
|
item.person
|
|
362
347
|
}}</span>
|
|
@@ -394,7 +379,7 @@
|
|
|
394
379
|
<DashboardPanel
|
|
395
380
|
title="Staff's Total Attendance"
|
|
396
381
|
action-label="View All"
|
|
397
|
-
@action="goToDashboardRoute(
|
|
382
|
+
@action="goToDashboardRoute('attendance')"
|
|
398
383
|
>
|
|
399
384
|
<div v-if="staffStatusItems.length" class="d-flex flex-column ga-3">
|
|
400
385
|
<v-card
|
|
@@ -405,14 +390,8 @@
|
|
|
405
390
|
rounded="lg"
|
|
406
391
|
class="pa-3"
|
|
407
392
|
>
|
|
408
|
-
<div class="d-flex align-
|
|
409
|
-
<
|
|
410
|
-
size="34"
|
|
411
|
-
color="blue-grey-lighten-4"
|
|
412
|
-
class="text-caption font-weight-bold text-blue-grey-darken-2 flex-shrink-0"
|
|
413
|
-
>
|
|
414
|
-
{{ getInitials(item.name) }}
|
|
415
|
-
</v-avatar>
|
|
393
|
+
<div class="d-flex align-start ga-3">
|
|
394
|
+
<AvatarMain :name="item.name" :size="32" />
|
|
416
395
|
<div class="flex-grow-1" style="min-width: 0">
|
|
417
396
|
<div
|
|
418
397
|
class="text-body-2 font-weight-bold text-blue-grey-darken-4 text-truncate"
|
|
@@ -445,65 +424,445 @@
|
|
|
445
424
|
action-label="View All"
|
|
446
425
|
@action="goToDashboardRoute('feedbacks')"
|
|
447
426
|
>
|
|
448
|
-
<div v-if="feedbackItems.length" class="
|
|
449
|
-
<
|
|
427
|
+
<div v-if="feedbackItems.length" class="figma-list mt-2">
|
|
428
|
+
<div
|
|
450
429
|
v-for="item in feedbackItems"
|
|
451
430
|
:key="item.id"
|
|
452
|
-
|
|
453
|
-
border
|
|
454
|
-
rounded="lg"
|
|
455
|
-
class="pa-3"
|
|
431
|
+
class="d-flex align-center justify-space-between"
|
|
432
|
+
style="padding: 16px 20px; border: 1px solid #e2e8f0; border-radius: 12px; margin-bottom: 12px; background-color: #ffffff; box-shadow: 0 1px 2px rgba(0,0,0,0.02)"
|
|
456
433
|
>
|
|
457
|
-
<div
|
|
458
|
-
<
|
|
434
|
+
<div style="flex: 1; min-width: 140px">
|
|
435
|
+
<strong
|
|
436
|
+
style="display: block; font-size: 15px; font-weight: 700; color: #0b2f47; margin-bottom: 4px"
|
|
437
|
+
>{{ item.title }}</strong
|
|
438
|
+
>
|
|
439
|
+
<span style="font-size: 13px; color: #94a3b8">{{ item.subtitle }}</span>
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
<div style="flex: 1; display: flex; align-items: center; gap: 12px">
|
|
443
|
+
<AvatarMain :name="item.person" :size="36" />
|
|
444
|
+
<span style="font-size: 14px; color: #64748b">{{ item.person }}</span>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div style="flex: 1; display: flex; justify-content: center">
|
|
448
|
+
<div
|
|
449
|
+
:style="getStatusPillStyle(item.status)"
|
|
450
|
+
style="padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; text-transform: capitalize"
|
|
451
|
+
>
|
|
452
|
+
{{ item.status || "To-Do" }}
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
<div style="flex: 0 0 auto; text-align: right">
|
|
457
|
+
<v-btn
|
|
458
|
+
variant="outlined"
|
|
459
|
+
class="text-none font-weight-medium"
|
|
460
|
+
size="small"
|
|
461
|
+
color="#3b82f6"
|
|
462
|
+
style="border-color: #e2e8f0; border-radius: 8px; padding: 0 16px; height: 32px"
|
|
463
|
+
@click="goToListItem(item, 'feedbacks')"
|
|
464
|
+
>View</v-btn
|
|
465
|
+
>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
<DashboardEmptyState v-else />
|
|
470
|
+
</DashboardPanel>
|
|
471
|
+
</v-col>
|
|
472
|
+
</v-row>
|
|
473
|
+
|
|
474
|
+
<!-- ── Property Management mode panels ───────────────────────────────── -->
|
|
475
|
+
<v-row v-if="isPropertyMode" class="dashboard-section-grid" dense>
|
|
476
|
+
<v-col v-if="isWidgetVisible('upcomingEvents')" cols="12" lg="6">
|
|
477
|
+
<div class="dashboard-card dashboard-panel figma-panel">
|
|
478
|
+
<div class="figma-panel-header">
|
|
479
|
+
<h2>Upcoming Events</h2>
|
|
480
|
+
<button
|
|
481
|
+
class="figma-panel-action"
|
|
482
|
+
type="button"
|
|
483
|
+
@click="goToDashboardRoute('event-mgmt')"
|
|
484
|
+
>
|
|
485
|
+
View All
|
|
486
|
+
</button>
|
|
487
|
+
</div>
|
|
488
|
+
<div class="figma-panel-body">
|
|
489
|
+
<div v-if="upcomingEvents.length" class="figma-list">
|
|
490
|
+
<div
|
|
491
|
+
v-for="item in upcomingEvents"
|
|
492
|
+
:key="item.id"
|
|
493
|
+
class="figma-list-row"
|
|
494
|
+
>
|
|
495
|
+
<span
|
|
496
|
+
:class="[
|
|
497
|
+
'figma-event-dot',
|
|
498
|
+
`figma-event-dot-${getListTone(item)}`,
|
|
499
|
+
]"
|
|
500
|
+
/>
|
|
501
|
+
<div class="figma-list-main">
|
|
502
|
+
<strong>{{ item.title }}</strong>
|
|
503
|
+
<p v-if="item.subtitle">{{ item.subtitle }}</p>
|
|
504
|
+
</div>
|
|
505
|
+
<button
|
|
506
|
+
class="figma-row-action"
|
|
507
|
+
type="button"
|
|
508
|
+
@click="navigateTo(dashboardPath('event-mgmt'))"
|
|
509
|
+
>
|
|
510
|
+
{{ item.actionLabel || "View" }}
|
|
511
|
+
</button>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
<DashboardEmptyState v-else />
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
</v-col>
|
|
518
|
+
|
|
519
|
+
<v-col v-if="isWidgetVisible('todayReminders')" cols="12" lg="6">
|
|
520
|
+
<div class="dashboard-card dashboard-panel figma-panel">
|
|
521
|
+
<div class="figma-panel-header">
|
|
522
|
+
<h2>Today's Reminder</h2>
|
|
523
|
+
<button
|
|
524
|
+
class="figma-panel-action"
|
|
525
|
+
type="button"
|
|
526
|
+
@click="goToDashboardRoute('calendar')"
|
|
527
|
+
>
|
|
528
|
+
View All
|
|
529
|
+
</button>
|
|
530
|
+
</div>
|
|
531
|
+
<div class="figma-panel-body">
|
|
532
|
+
<div v-if="todayReminders.length" class="figma-list">
|
|
533
|
+
<div
|
|
534
|
+
v-for="item in todayReminders"
|
|
535
|
+
:key="item.id"
|
|
536
|
+
class="figma-list-row"
|
|
537
|
+
>
|
|
538
|
+
<span
|
|
539
|
+
:class="[
|
|
540
|
+
'figma-icon-badge',
|
|
541
|
+
`figma-icon-badge-${getListTone(item)}`,
|
|
542
|
+
]"
|
|
543
|
+
>
|
|
544
|
+
<v-icon :icon="item.icon || 'mdi-alert'" size="15" />
|
|
545
|
+
</span>
|
|
546
|
+
<div class="figma-list-main">
|
|
547
|
+
<strong>{{ item.title }}</strong>
|
|
548
|
+
<p v-if="item.subtitle">{{ item.subtitle }}</p>
|
|
549
|
+
</div>
|
|
550
|
+
<button
|
|
551
|
+
class="figma-row-action"
|
|
552
|
+
type="button"
|
|
553
|
+
@click="navigateTo(dashboardPath('calendar'))"
|
|
554
|
+
>
|
|
555
|
+
{{ item.actionLabel || "View" }}
|
|
556
|
+
</button>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
<DashboardEmptyState v-else />
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
</v-col>
|
|
563
|
+
|
|
564
|
+
<v-col v-if="isWidgetVisible('todayAttentions')" cols="12" lg="6">
|
|
565
|
+
<div class="dashboard-card dashboard-panel figma-panel">
|
|
566
|
+
<div class="figma-panel-header">
|
|
567
|
+
<h2>Today's Attentions</h2>
|
|
568
|
+
<button
|
|
569
|
+
class="figma-panel-action"
|
|
570
|
+
type="button"
|
|
571
|
+
@click="goToDashboardRoute('incident-reports')"
|
|
572
|
+
>
|
|
573
|
+
View All
|
|
574
|
+
</button>
|
|
575
|
+
</div>
|
|
576
|
+
<div class="figma-panel-body">
|
|
577
|
+
<div v-if="todayAttentions.length" class="figma-list">
|
|
578
|
+
<div
|
|
579
|
+
v-for="item in todayAttentions"
|
|
580
|
+
:key="item.id"
|
|
581
|
+
class="figma-list-row"
|
|
582
|
+
>
|
|
583
|
+
<span
|
|
584
|
+
:class="[
|
|
585
|
+
'figma-icon-badge',
|
|
586
|
+
`figma-icon-badge-${getListTone(item)}`,
|
|
587
|
+
]"
|
|
588
|
+
>
|
|
589
|
+
<v-icon :icon="item.icon || 'mdi-alert-circle-outline'" size="15" />
|
|
590
|
+
</span>
|
|
591
|
+
<div class="figma-list-main">
|
|
592
|
+
<strong>{{ item.title }}</strong>
|
|
593
|
+
<p v-if="item.subtitle">{{ item.subtitle }}</p>
|
|
594
|
+
</div>
|
|
595
|
+
<button
|
|
596
|
+
class="figma-row-action"
|
|
597
|
+
type="button"
|
|
598
|
+
@click="navigateTo(dashboardPath('incident-reports'))"
|
|
599
|
+
>
|
|
600
|
+
{{ item.actionLabel || "Review" }}
|
|
601
|
+
</button>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
<DashboardEmptyState v-else />
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
</v-col>
|
|
608
|
+
|
|
609
|
+
<v-col v-if="isWidgetVisible('workOrderStatus')" cols="12" lg="6">
|
|
610
|
+
<div class="dashboard-card dashboard-panel figma-panel">
|
|
611
|
+
<div class="figma-panel-header">
|
|
612
|
+
<h2>Work Order Status</h2>
|
|
613
|
+
<button
|
|
614
|
+
class="figma-panel-action"
|
|
615
|
+
type="button"
|
|
616
|
+
@click="goToDashboardRoute('work-orders')"
|
|
617
|
+
>
|
|
618
|
+
View All
|
|
619
|
+
</button>
|
|
620
|
+
</div>
|
|
621
|
+
<div class="figma-panel-body">
|
|
622
|
+
<div v-if="workOrderStatus.length" class="figma-status-stack">
|
|
623
|
+
<div
|
|
624
|
+
v-for="item in workOrderStatus"
|
|
625
|
+
:key="item.label"
|
|
626
|
+
class="figma-status-row"
|
|
627
|
+
>
|
|
628
|
+
<div class="figma-status-label">
|
|
629
|
+
<span>{{ item.label }}</span>
|
|
630
|
+
<strong>{{ item.value }}</strong>
|
|
631
|
+
</div>
|
|
632
|
+
<div class="figma-progress-track">
|
|
633
|
+
<span
|
|
634
|
+
class="figma-progress-fill"
|
|
635
|
+
:style="getStatusProgressStyle(item)"
|
|
636
|
+
/>
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
<DashboardEmptyState v-else />
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
</v-col>
|
|
644
|
+
|
|
645
|
+
<!-- Property feedbacks -->
|
|
646
|
+
<v-col v-if="isWidgetVisible('feedbacks')" cols="12">
|
|
647
|
+
<DashboardPanel
|
|
648
|
+
title="Feedbacks"
|
|
649
|
+
action-label="View All"
|
|
650
|
+
@action="goToDashboardRoute('feedbacks')"
|
|
651
|
+
>
|
|
652
|
+
<div v-if="feedbackItems.length" class="feedback-list">
|
|
653
|
+
<div
|
|
654
|
+
v-for="item in feedbackItems"
|
|
655
|
+
:key="item.id"
|
|
656
|
+
class="feedback-row"
|
|
657
|
+
>
|
|
658
|
+
<div class="feedback-summary">
|
|
659
|
+
<strong>{{ item.title }}</strong>
|
|
660
|
+
<p>{{ item.subtitle }}</p>
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<div class="feedback-person">
|
|
664
|
+
<span class="feedback-avatar">{{ getInitials(item.person) }}</span>
|
|
665
|
+
<span>{{ item.person }}</span>
|
|
666
|
+
</div>
|
|
667
|
+
|
|
668
|
+
<span
|
|
669
|
+
:class="[
|
|
670
|
+
'feedback-status-pill',
|
|
671
|
+
`feedback-status-pill-${getFeedbackTone(item.status)}`,
|
|
672
|
+
]"
|
|
673
|
+
>
|
|
674
|
+
{{ item.status }}
|
|
675
|
+
</span>
|
|
676
|
+
|
|
677
|
+
<button
|
|
678
|
+
class="feedback-view-button"
|
|
679
|
+
type="button"
|
|
680
|
+
@click="goToListItem(item, 'feedbacks')"
|
|
681
|
+
>
|
|
682
|
+
View
|
|
683
|
+
</button>
|
|
684
|
+
</div>
|
|
685
|
+
</div>
|
|
686
|
+
<DashboardEmptyState v-else />
|
|
687
|
+
</DashboardPanel>
|
|
688
|
+
</v-col>
|
|
689
|
+
</v-row>
|
|
690
|
+
|
|
691
|
+
<!-- ── Security mode panels ───────────────────────────────────────────── -->
|
|
692
|
+
<v-row v-if="isSecurityMode" class="dashboard-section-grid" dense>
|
|
693
|
+
<v-col v-if="isWidgetVisible('activePatrol')" cols="12" lg="6">
|
|
694
|
+
<div class="dashboard-card dashboard-panel figma-panel">
|
|
695
|
+
<div class="figma-panel-header">
|
|
696
|
+
<h2>ACTIVE PATROL</h2>
|
|
697
|
+
<button
|
|
698
|
+
class="figma-panel-action"
|
|
699
|
+
type="button"
|
|
700
|
+
@click="goToDashboardRoute('virtual-patrol')"
|
|
701
|
+
>
|
|
702
|
+
View All
|
|
703
|
+
</button>
|
|
704
|
+
</div>
|
|
705
|
+
<div class="figma-panel-body">
|
|
706
|
+
<div
|
|
707
|
+
v-if="activePatrolItems.length"
|
|
708
|
+
class="figma-list mt-2"
|
|
709
|
+
>
|
|
710
|
+
<div
|
|
711
|
+
v-for="item in activePatrolItems"
|
|
712
|
+
:key="item.id || item._id"
|
|
713
|
+
class="d-flex align-center justify-space-between"
|
|
714
|
+
style="padding: 16px 20px; border: 1px solid #e2e8f0; border-radius: 12px; margin-bottom: 12px; background-color: #ffffff; box-shadow: 0 1px 2px rgba(0,0,0,0.02)"
|
|
715
|
+
>
|
|
716
|
+
<div style="flex: 1; min-width: 140px">
|
|
717
|
+
<strong
|
|
718
|
+
style="display: block; font-size: 15px; font-weight: 700; color: #0b2f47; margin-bottom: 4px"
|
|
719
|
+
>{{ item.title || item.name }}</strong
|
|
720
|
+
>
|
|
721
|
+
<span style="font-size: 13px; color: #94a3b8">{{
|
|
722
|
+
item.subtitle || item.location || ""
|
|
723
|
+
}}</span>
|
|
724
|
+
</div>
|
|
725
|
+
|
|
726
|
+
<div style="flex: 1; display: flex; align-items: center; gap: 12px">
|
|
727
|
+
<AvatarMain :name="item.person || item.assignedTo" :size="36" />
|
|
728
|
+
<span style="font-size: 14px; color: #64748b">{{
|
|
729
|
+
item.person || item.assignedTo
|
|
730
|
+
}}</span>
|
|
731
|
+
</div>
|
|
732
|
+
|
|
733
|
+
<div style="flex: 1; display: flex; justify-content: center">
|
|
459
734
|
<div
|
|
460
|
-
|
|
735
|
+
:style="getPatrolStatusStyle(item.status)"
|
|
736
|
+
style="padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; text-transform: capitalize"
|
|
461
737
|
>
|
|
462
|
-
{{ item.
|
|
738
|
+
{{ item.status }}
|
|
463
739
|
</div>
|
|
464
|
-
|
|
465
|
-
|
|
740
|
+
</div>
|
|
741
|
+
|
|
742
|
+
<div style="flex: 0 0 auto; text-align: right">
|
|
743
|
+
<v-btn
|
|
744
|
+
variant="outlined"
|
|
745
|
+
class="text-none font-weight-medium"
|
|
746
|
+
size="small"
|
|
747
|
+
color="#3b82f6"
|
|
748
|
+
style="border-color: #e2e8f0; border-radius: 8px; padding: 0 16px; height: 32px"
|
|
749
|
+
@click="goToDashboardRoute('virtual-patrol')"
|
|
750
|
+
>View</v-btn
|
|
751
|
+
>
|
|
752
|
+
</div>
|
|
753
|
+
</div>
|
|
754
|
+
</div>
|
|
755
|
+
<DashboardEmptyState v-else />
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
</v-col>
|
|
759
|
+
|
|
760
|
+
<v-col v-if="isWidgetVisible('staffStatus')" cols="12" lg="6">
|
|
761
|
+
<div class="dashboard-card dashboard-panel figma-panel">
|
|
762
|
+
<div class="figma-panel-header">
|
|
763
|
+
<h2>STAFF STATUS</h2>
|
|
764
|
+
<button
|
|
765
|
+
class="figma-panel-action"
|
|
766
|
+
type="button"
|
|
767
|
+
@click="goToDashboardRoute('attendance')"
|
|
768
|
+
>
|
|
769
|
+
View All
|
|
770
|
+
</button>
|
|
771
|
+
</div>
|
|
772
|
+
<div class="figma-panel-body">
|
|
773
|
+
<div v-if="securityStaffItems.length" class="figma-list mt-2">
|
|
774
|
+
<div
|
|
775
|
+
v-for="item in securityStaffItems"
|
|
776
|
+
:key="item._id || item.id"
|
|
777
|
+
class="d-flex align-center justify-space-between"
|
|
778
|
+
style="padding: 16px 20px; border: 1px solid #e2e8f0; border-radius: 12px; margin-bottom: 12px; background-color: #ffffff; box-shadow: 0 1px 2px rgba(0,0,0,0.02)"
|
|
779
|
+
>
|
|
780
|
+
<div style="display: flex; align-items: center; gap: 16px">
|
|
781
|
+
<AvatarMain
|
|
782
|
+
:name="item.personName || item.employeeName || item.userName || 'Unassigned'"
|
|
783
|
+
:size="40"
|
|
784
|
+
/>
|
|
785
|
+
<div>
|
|
786
|
+
<strong
|
|
787
|
+
style="display: block; font-size: 15px; font-weight: 700; color: #0b2f47; margin-bottom: 4px"
|
|
788
|
+
>{{ item.userName || item.personName || "Unassigned" }}</strong
|
|
789
|
+
>
|
|
790
|
+
<span style="font-size: 13px; color: #94a3b8">{{
|
|
791
|
+
item.role || item.designation || "Officer"
|
|
792
|
+
}}</span>
|
|
466
793
|
</div>
|
|
467
794
|
</div>
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
color="blue-grey-lighten-4"
|
|
474
|
-
class="text-caption font-weight-bold text-blue-grey-darken-2"
|
|
795
|
+
|
|
796
|
+
<div style="flex: 0 0 auto">
|
|
797
|
+
<div
|
|
798
|
+
:style="getStaffStatusPillStyle(item.status)"
|
|
799
|
+
style="padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500"
|
|
475
800
|
>
|
|
476
|
-
{{
|
|
477
|
-
</
|
|
478
|
-
<span class="text-truncate" style="max-width: 100px">{{
|
|
479
|
-
item.person
|
|
480
|
-
}}</span>
|
|
801
|
+
{{ item.status || "On duty" }}
|
|
802
|
+
</div>
|
|
481
803
|
</div>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
804
|
+
</div>
|
|
805
|
+
</div>
|
|
806
|
+
<DashboardEmptyState v-else />
|
|
807
|
+
</div>
|
|
808
|
+
</div>
|
|
809
|
+
</v-col>
|
|
810
|
+
|
|
811
|
+
<!-- Security feedbacks -->
|
|
812
|
+
<v-col v-if="isWidgetVisible('feedbacks')" cols="12">
|
|
813
|
+
<DashboardPanel
|
|
814
|
+
title="Feedbacks"
|
|
815
|
+
action-label="View All"
|
|
816
|
+
@action="goToDashboardRoute('feedbacks')"
|
|
817
|
+
>
|
|
818
|
+
<div v-if="feedbackItems.length" class="figma-list mt-2">
|
|
819
|
+
<div
|
|
820
|
+
v-for="item in feedbackItems"
|
|
821
|
+
:key="item.id"
|
|
822
|
+
class="d-flex align-center justify-space-between"
|
|
823
|
+
style="padding: 16px 20px; border: 1px solid #e2e8f0; border-radius: 12px; margin-bottom: 12px; background-color: #ffffff; box-shadow: 0 1px 2px rgba(0,0,0,0.02)"
|
|
824
|
+
>
|
|
825
|
+
<div style="flex: 1; min-width: 140px">
|
|
826
|
+
<strong
|
|
827
|
+
style="display: block; font-size: 15px; font-weight: 700; color: #0b2f47; margin-bottom: 4px"
|
|
828
|
+
>{{ item.title }}</strong
|
|
487
829
|
>
|
|
488
|
-
|
|
489
|
-
|
|
830
|
+
<span style="font-size: 13px; color: #94a3b8">{{ item.subtitle }}</span>
|
|
831
|
+
</div>
|
|
832
|
+
|
|
833
|
+
<div style="flex: 1; display: flex; align-items: center; gap: 12px">
|
|
834
|
+
<AvatarMain :name="item.person" :size="36" />
|
|
835
|
+
<span style="font-size: 14px; color: #64748b">{{ item.person }}</span>
|
|
836
|
+
</div>
|
|
837
|
+
|
|
838
|
+
<div style="flex: 1; display: flex; justify-content: center">
|
|
839
|
+
<div
|
|
840
|
+
:style="getStatusPillStyle(item.status)"
|
|
841
|
+
style="padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; text-transform: capitalize"
|
|
842
|
+
>
|
|
843
|
+
{{ item.status || "To-Do" }}
|
|
844
|
+
</div>
|
|
845
|
+
</div>
|
|
846
|
+
|
|
847
|
+
<div style="flex: 0 0 auto; text-align: right">
|
|
490
848
|
<v-btn
|
|
491
849
|
variant="outlined"
|
|
492
|
-
|
|
850
|
+
class="text-none font-weight-medium"
|
|
493
851
|
size="small"
|
|
494
|
-
|
|
852
|
+
color="#3b82f6"
|
|
853
|
+
style="border-color: #e2e8f0; border-radius: 8px; padding: 0 16px; height: 32px"
|
|
495
854
|
@click="goToListItem(item, 'feedbacks')"
|
|
855
|
+
>View</v-btn
|
|
496
856
|
>
|
|
497
|
-
View
|
|
498
|
-
</v-btn>
|
|
499
857
|
</div>
|
|
500
|
-
</
|
|
858
|
+
</div>
|
|
501
859
|
</div>
|
|
502
860
|
<DashboardEmptyState v-else />
|
|
503
861
|
</DashboardPanel>
|
|
504
862
|
</v-col>
|
|
505
863
|
</v-row>
|
|
506
864
|
|
|
865
|
+
<!-- ── Customize Dialog ───────────────────────────────────────────────── -->
|
|
507
866
|
<v-dialog v-model="customizeDialog" max-width="720" persistent scrollable>
|
|
508
867
|
<v-card rounded="lg">
|
|
509
868
|
<v-card-title
|
|
@@ -616,6 +975,8 @@
|
|
|
616
975
|
</template>
|
|
617
976
|
|
|
618
977
|
<script setup lang="ts">
|
|
978
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
979
|
+
|
|
619
980
|
type DashboardMetric = {
|
|
620
981
|
count?: number;
|
|
621
982
|
percentage?: number;
|
|
@@ -624,6 +985,10 @@ type DashboardMetric = {
|
|
|
624
985
|
pending?: number;
|
|
625
986
|
highSeverity?: number;
|
|
626
987
|
currentlyOnSite?: number;
|
|
988
|
+
ongoing?: number;
|
|
989
|
+
waitingApproval?: number;
|
|
990
|
+
missed?: number;
|
|
991
|
+
missedCheckpoints?: number;
|
|
627
992
|
lowStock?: number;
|
|
628
993
|
outOfStock?: number;
|
|
629
994
|
completed?: number;
|
|
@@ -638,7 +1003,8 @@ type DashboardListItem = {
|
|
|
638
1003
|
time?: string;
|
|
639
1004
|
icon?: string;
|
|
640
1005
|
tone?: string;
|
|
641
|
-
|
|
1006
|
+
color?: string;
|
|
1007
|
+
actionLabel?: string;
|
|
642
1008
|
};
|
|
643
1009
|
|
|
644
1010
|
type TaskItem = {
|
|
@@ -669,132 +1035,49 @@ type FeedbackItem = {
|
|
|
669
1035
|
raw?: Record<string, any>;
|
|
670
1036
|
};
|
|
671
1037
|
|
|
1038
|
+
// ─── Props ────────────────────────────────────────────────────────────────────
|
|
1039
|
+
|
|
672
1040
|
const props = defineProps({
|
|
673
|
-
orgId: {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
},
|
|
677
|
-
site: {
|
|
678
|
-
type: String,
|
|
679
|
-
default: "",
|
|
680
|
-
},
|
|
681
|
-
serviceType: {
|
|
682
|
-
type: String,
|
|
683
|
-
default: "",
|
|
684
|
-
},
|
|
1041
|
+
orgId: { type: String, default: "" },
|
|
1042
|
+
site: { type: String, default: "" },
|
|
1043
|
+
serviceType: { type: String, default: "" },
|
|
685
1044
|
dashboardData: {
|
|
686
1045
|
type: Object as () => Record<string, any>,
|
|
687
1046
|
default: () => ({}),
|
|
688
1047
|
},
|
|
689
|
-
loading: {
|
|
690
|
-
type: Boolean,
|
|
691
|
-
default: false,
|
|
692
|
-
},
|
|
1048
|
+
loading: { type: Boolean, default: false },
|
|
693
1049
|
cardPeriods: {
|
|
694
1050
|
type: Object as () => Record<string, TDashboardValues>,
|
|
695
1051
|
default: () => ({}),
|
|
696
1052
|
},
|
|
697
|
-
currentSiteName: {
|
|
698
|
-
type: String,
|
|
699
|
-
default: "",
|
|
700
|
-
},
|
|
1053
|
+
currentSiteName: { type: String, default: "" },
|
|
701
1054
|
cardValues: {
|
|
702
1055
|
type: Array as () => TDashboardCardValue[],
|
|
703
1056
|
default: () => [],
|
|
704
1057
|
},
|
|
705
|
-
title: {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
},
|
|
709
|
-
extraWidgetKeys: {
|
|
710
|
-
type: Array as () => string[],
|
|
711
|
-
default: () => [],
|
|
712
|
-
},
|
|
713
|
-
showHeaderActions: {
|
|
714
|
-
type: Boolean,
|
|
715
|
-
default: true,
|
|
716
|
-
},
|
|
1058
|
+
title: { type: String, default: "" },
|
|
1059
|
+
extraWidgetKeys: { type: Array as () => string[], default: () => [] },
|
|
1060
|
+
showHeaderActions: { type: Boolean, default: true },
|
|
717
1061
|
});
|
|
718
1062
|
|
|
719
|
-
const
|
|
720
|
-
{
|
|
721
|
-
|
|
722
|
-
periodKey: "period",
|
|
723
|
-
title: "Open Work Order",
|
|
724
|
-
icon: "mdi-briefcase",
|
|
725
|
-
color: "#0b2f47",
|
|
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: "supplyAlert",
|
|
736
|
-
periodKey: "period",
|
|
737
|
-
title: "Supply Alert",
|
|
738
|
-
icon: "mdi-alert",
|
|
739
|
-
color: "#d32f2f",
|
|
740
|
-
},
|
|
741
|
-
];
|
|
742
|
-
|
|
743
|
-
const landscapeCardValues: TDashboardCardValue[] = [
|
|
744
|
-
{
|
|
745
|
-
key: "openWorkOrder",
|
|
746
|
-
periodKey: "period",
|
|
747
|
-
title: "Open Work Order",
|
|
748
|
-
icon: "mdi-briefcase",
|
|
749
|
-
color: "#0b2f47",
|
|
750
|
-
},
|
|
751
|
-
{
|
|
752
|
-
key: "taskCompleted",
|
|
753
|
-
periodKey: "period",
|
|
754
|
-
title: "Task Completed",
|
|
755
|
-
icon: "mdi-clipboard-check-outline",
|
|
756
|
-
color: "#f4a340",
|
|
757
|
-
},
|
|
758
|
-
{
|
|
759
|
-
key: "supplyAlert",
|
|
760
|
-
periodKey: "period",
|
|
761
|
-
title: "Supply Alert",
|
|
762
|
-
icon: "mdi-alert",
|
|
763
|
-
color: "#d32f2f",
|
|
764
|
-
},
|
|
765
|
-
];
|
|
1063
|
+
const emit = defineEmits<{
|
|
1064
|
+
updatePeriod: [payload: { key: string; period: string }];
|
|
1065
|
+
}>();
|
|
766
1066
|
|
|
767
|
-
|
|
768
|
-
"activity",
|
|
769
|
-
"taskSchedule",
|
|
770
|
-
"staffStatus",
|
|
771
|
-
"feedbacks",
|
|
772
|
-
"recentActivity",
|
|
773
|
-
];
|
|
1067
|
+
// ─── Mode Detection ───────────────────────────────────────────────────────────
|
|
774
1068
|
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1069
|
+
const isPropertyMode = computed(
|
|
1070
|
+
() => props.serviceType === "property_management_agency"
|
|
1071
|
+
);
|
|
1072
|
+
const isSecurityMode = computed(() => props.serviceType === "security_agency");
|
|
1073
|
+
const isHygieneMode = computed(
|
|
1074
|
+
() => !isPropertyMode.value && !isSecurityMode.value
|
|
1075
|
+
);
|
|
781
1076
|
|
|
782
|
-
|
|
783
|
-
if (props.extraWidgetKeys && props.extraWidgetKeys.length > 0)
|
|
784
|
-
return props.extraWidgetKeys;
|
|
785
|
-
if (props.serviceType) return defaultExtraWidgetKeys;
|
|
786
|
-
return [];
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
const periodValue: Record<string, TDashboardValues> = {
|
|
790
|
-
Today: "today",
|
|
791
|
-
"This Week": "thisWeek",
|
|
792
|
-
"This Month": "thisMonth",
|
|
793
|
-
};
|
|
794
|
-
|
|
795
|
-
const currentPeriod = ref<TDashboardValues>("today");
|
|
1077
|
+
// ─── Composables ──────────────────────────────────────────────────────────────
|
|
796
1078
|
|
|
797
1079
|
const {
|
|
1080
|
+
getDashboard,
|
|
798
1081
|
getDashboardHygiene,
|
|
799
1082
|
getDashboardHygieneWeeklyActivity,
|
|
800
1083
|
getDashboardHygieneTaskSchedule,
|
|
@@ -802,30 +1085,99 @@ const {
|
|
|
802
1085
|
getDashboardHygieneFeedbacks,
|
|
803
1086
|
} = useDashboard();
|
|
804
1087
|
|
|
1088
|
+
// ─── State ────────────────────────────────────────────────────────────────────
|
|
1089
|
+
|
|
1090
|
+
const router = useRouter();
|
|
1091
|
+
const runtimeConfig = useRuntimeConfig();
|
|
1092
|
+
const customizeDialog = ref(false);
|
|
1093
|
+
const widgetSearch = ref("");
|
|
1094
|
+
const selectedRange = ref("Today");
|
|
1095
|
+
const visitorType = ref("All");
|
|
1096
|
+
const facilityType = ref("All");
|
|
1097
|
+
const visibleWidgetKeys = ref<string[]>([]);
|
|
1098
|
+
const draftVisibleWidgetKeys = ref<string[]>([]);
|
|
1099
|
+
const filterMenus = reactive<Record<string, boolean>>({});
|
|
1100
|
+
const snackbar = reactive({ visible: false, text: "" });
|
|
1101
|
+
const chartContainerRef = ref<HTMLElement | null>(null);
|
|
1102
|
+
const chartWidth = ref(920);
|
|
1103
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
1104
|
+
|
|
1105
|
+
const periodValue: Record<string, TDashboardValues> = {
|
|
1106
|
+
Today: "today",
|
|
1107
|
+
"This Week": "thisWeek",
|
|
1108
|
+
"This Month": "thisMonth",
|
|
1109
|
+
};
|
|
1110
|
+
const currentPeriod = ref<TDashboardValues>("today");
|
|
1111
|
+
|
|
1112
|
+
const rangeOptions = [
|
|
1113
|
+
{ label: "Today", value: "Today" },
|
|
1114
|
+
{ label: "Week", value: "This Week" },
|
|
1115
|
+
{ label: "Month", value: "This Month" },
|
|
1116
|
+
];
|
|
1117
|
+
|
|
1118
|
+
// ─── Visitor / Facility filter mappings ───────────────────────────────────────
|
|
1119
|
+
|
|
1120
|
+
const visitorTypeApiMap: Record<string, string> = {
|
|
1121
|
+
All: "",
|
|
1122
|
+
"Drive-In": "guest",
|
|
1123
|
+
Contractor: "contractor",
|
|
1124
|
+
"Walk-In": "walk-in",
|
|
1125
|
+
Delivery: "delivery",
|
|
1126
|
+
"Pick-up": "pick-up",
|
|
1127
|
+
"Drop-Off": "drop-off",
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
const currentVisitorTypeParam = computed(
|
|
1131
|
+
() => visitorTypeApiMap[visitorType.value] ?? ""
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
// ─── Data Fetching ────────────────────────────────────────────────────────────
|
|
1135
|
+
|
|
1136
|
+
// Hygiene / M&E / Landscape / Pest / Pool main metrics
|
|
805
1137
|
const { data: getDashboardReq, pending: loadingMetrics } =
|
|
806
1138
|
await useLazyAsyncData(
|
|
807
1139
|
`get-dashboard-${props.site}-${props.serviceType}`,
|
|
808
1140
|
() =>
|
|
809
|
-
props.serviceType
|
|
810
|
-
? getDashboardHygiene(
|
|
1141
|
+
props.serviceType && isHygieneMode.value
|
|
1142
|
+
? getDashboardHygiene(props.site, currentPeriod.value, props.serviceType as TServiceType)
|
|
1143
|
+
: Promise.resolve(null),
|
|
1144
|
+
{
|
|
1145
|
+
watch: [() => props.site, currentPeriod, () => props.serviceType],
|
|
1146
|
+
}
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1149
|
+
// Property / Security main metrics (uses generic getDashboard with visitor type)
|
|
1150
|
+
const { data: getDashboardAltReq, pending: loadingAltMetrics } =
|
|
1151
|
+
await useLazyAsyncData(
|
|
1152
|
+
`get-dashboard-alt-${props.site}-${props.serviceType}`,
|
|
1153
|
+
() =>
|
|
1154
|
+
props.site && (isPropertyMode.value || isSecurityMode.value)
|
|
1155
|
+
? getDashboard(
|
|
811
1156
|
props.site,
|
|
1157
|
+
props.serviceType as TServiceType,
|
|
812
1158
|
currentPeriod.value,
|
|
813
|
-
|
|
1159
|
+
currentVisitorTypeParam.value ? { type: currentVisitorTypeParam.value } : {}
|
|
814
1160
|
)
|
|
815
1161
|
: Promise.resolve(null),
|
|
816
1162
|
{
|
|
817
|
-
watch: [
|
|
1163
|
+
watch: [
|
|
1164
|
+
() => props.site,
|
|
1165
|
+
() => props.serviceType,
|
|
1166
|
+
currentPeriod,
|
|
1167
|
+
currentVisitorTypeParam,
|
|
1168
|
+
],
|
|
818
1169
|
}
|
|
819
1170
|
);
|
|
820
1171
|
|
|
1172
|
+
// Weekly activity (hygiene mode only)
|
|
821
1173
|
const { data: weeklyActivityReq, pending: loadingWeeklyActivity } =
|
|
822
1174
|
await useLazyAsyncData(
|
|
823
1175
|
`get-dashboard-weekly-activity-${props.site}-${props.serviceType}`,
|
|
824
1176
|
() =>
|
|
825
|
-
props.serviceType
|
|
1177
|
+
props.serviceType && isHygieneMode.value
|
|
826
1178
|
? getDashboardHygieneWeeklyActivity(
|
|
827
1179
|
props.site,
|
|
828
|
-
props.serviceType,
|
|
1180
|
+
props.serviceType as TServiceType,
|
|
829
1181
|
currentPeriod.value
|
|
830
1182
|
)
|
|
831
1183
|
: Promise.resolve(null),
|
|
@@ -834,44 +1186,62 @@ const { data: weeklyActivityReq, pending: loadingWeeklyActivity } =
|
|
|
834
1186
|
}
|
|
835
1187
|
);
|
|
836
1188
|
|
|
1189
|
+
// Task schedule (hygiene mode only)
|
|
837
1190
|
const { data: taskScheduleReq, pending: loadingTaskSchedule } =
|
|
838
1191
|
await useLazyAsyncData(
|
|
839
1192
|
`get-dashboard-task-schedule-${props.site}-${props.serviceType}`,
|
|
840
1193
|
() =>
|
|
841
|
-
props.serviceType
|
|
842
|
-
? getDashboardHygieneTaskSchedule(
|
|
1194
|
+
props.serviceType && isHygieneMode.value
|
|
1195
|
+
? getDashboardHygieneTaskSchedule(
|
|
1196
|
+
props.site,
|
|
1197
|
+
props.serviceType as TServiceType,
|
|
1198
|
+
currentPeriod.value
|
|
1199
|
+
)
|
|
843
1200
|
: Promise.resolve(null),
|
|
844
1201
|
{
|
|
845
|
-
watch: [() => props.site, () => props.serviceType],
|
|
1202
|
+
watch: [() => props.site, () => props.serviceType, currentPeriod],
|
|
846
1203
|
}
|
|
847
1204
|
);
|
|
848
1205
|
|
|
1206
|
+
// Attendance (hygiene + security modes)
|
|
849
1207
|
const { data: attendanceReq, pending: loadingAttendance } =
|
|
850
1208
|
await useLazyAsyncData(
|
|
851
1209
|
`get-dashboard-attendance-${props.site}-${props.serviceType}`,
|
|
852
1210
|
() =>
|
|
853
|
-
props.serviceType
|
|
854
|
-
? getDashboardHygieneAttendance(
|
|
1211
|
+
props.serviceType && !isPropertyMode.value
|
|
1212
|
+
? getDashboardHygieneAttendance(
|
|
1213
|
+
props.site,
|
|
1214
|
+
props.serviceType as TServiceType,
|
|
1215
|
+
currentPeriod.value
|
|
1216
|
+
)
|
|
855
1217
|
: Promise.resolve(null),
|
|
856
1218
|
{
|
|
857
|
-
watch: [() => props.site, () => props.serviceType],
|
|
1219
|
+
watch: [() => props.site, () => props.serviceType, currentPeriod],
|
|
858
1220
|
}
|
|
859
1221
|
);
|
|
860
1222
|
|
|
1223
|
+
// Feedbacks (all modes)
|
|
861
1224
|
const { data: feedbacksReq, pending: loadingFeedbacks } =
|
|
862
1225
|
await useLazyAsyncData(
|
|
863
1226
|
`get-dashboard-feedbacks-${props.site}-${props.serviceType}`,
|
|
864
1227
|
() =>
|
|
865
1228
|
props.serviceType
|
|
866
|
-
? getDashboardHygieneFeedbacks(
|
|
1229
|
+
? getDashboardHygieneFeedbacks(
|
|
1230
|
+
props.site,
|
|
1231
|
+
props.serviceType as TServiceType,
|
|
1232
|
+
currentPeriod.value
|
|
1233
|
+
)
|
|
867
1234
|
: Promise.resolve(null),
|
|
868
1235
|
{
|
|
869
|
-
watch: [() => props.site, () => props.serviceType],
|
|
1236
|
+
watch: [() => props.site, () => props.serviceType, currentPeriod],
|
|
870
1237
|
}
|
|
871
1238
|
);
|
|
872
1239
|
|
|
1240
|
+
// ─── Loading State ────────────────────────────────────────────────────────────
|
|
1241
|
+
|
|
873
1242
|
const finalLoading = computed(() => {
|
|
874
|
-
if (props.serviceType)
|
|
1243
|
+
if (!props.serviceType) return props.loading;
|
|
1244
|
+
if (isHygieneMode.value) {
|
|
875
1245
|
return (
|
|
876
1246
|
loadingMetrics.value ||
|
|
877
1247
|
loadingWeeklyActivity.value ||
|
|
@@ -880,174 +1250,251 @@ const finalLoading = computed(() => {
|
|
|
880
1250
|
loadingFeedbacks.value
|
|
881
1251
|
);
|
|
882
1252
|
}
|
|
883
|
-
|
|
1253
|
+
if (isPropertyMode.value) {
|
|
1254
|
+
return loadingAltMetrics.value || loadingFeedbacks.value;
|
|
1255
|
+
}
|
|
1256
|
+
// security
|
|
1257
|
+
return loadingAltMetrics.value || loadingAttendance.value || loadingFeedbacks.value;
|
|
884
1258
|
});
|
|
885
1259
|
|
|
886
|
-
|
|
1260
|
+
// ─── Merged Dashboard Data ────────────────────────────────────────────────────
|
|
1261
|
+
|
|
1262
|
+
const finalDashboardData = computed<Record<string, any>>(() => {
|
|
887
1263
|
if (!props.serviceType) return props.dashboardData || {};
|
|
888
1264
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
(
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1265
|
+
if (isHygieneMode.value) {
|
|
1266
|
+
const data = getDashboardReq.value ?? {};
|
|
1267
|
+
let weeklyActivity = weeklyActivityReq.value ?? [];
|
|
1268
|
+
const hasNoActivity =
|
|
1269
|
+
!weeklyActivity.length ||
|
|
1270
|
+
weeklyActivity.every(
|
|
1271
|
+
(item: any) =>
|
|
1272
|
+
!item.workOrder &&
|
|
1273
|
+
!item.taskCompleted &&
|
|
1274
|
+
!item.openWorkOrder &&
|
|
1275
|
+
!item.closedWorkOrder
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
if (hasNoActivity && currentPeriod.value === "thisWeek") {
|
|
1279
|
+
weeklyActivity = [
|
|
1280
|
+
{ day: "Mon", workOrder: 800, taskCompleted: 1400 },
|
|
1281
|
+
{ day: "Tue", workOrder: 450, taskCompleted: 900 },
|
|
1282
|
+
{ day: "Wed", workOrder: 950, taskCompleted: 1640 },
|
|
1283
|
+
{ day: "Thu", workOrder: 2000, taskCompleted: 2100 },
|
|
1284
|
+
{ day: "Fri", workOrder: 2000, taskCompleted: 2700 },
|
|
1285
|
+
{ day: "Sat", workOrder: 2200, taskCompleted: 3100 },
|
|
1286
|
+
{ day: "Sun", workOrder: 2800, taskCompleted: 3200 },
|
|
1287
|
+
];
|
|
1288
|
+
}
|
|
900
1289
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1290
|
+
return {
|
|
1291
|
+
...data,
|
|
1292
|
+
openWorkOrder: data.openWorkOrder ?? { count: 0, percentage: 0 },
|
|
1293
|
+
supplyAlert: data.supplyAlert ?? { count: 0, percentage: 0 },
|
|
1294
|
+
taskCompleted: data.taskCompleted ?? { count: 0, percentage: 0 },
|
|
1295
|
+
activityChart: {
|
|
1296
|
+
labels: weeklyActivity.map((item: any) => item.day || item.label),
|
|
1297
|
+
workOrders: weeklyActivity.map(
|
|
1298
|
+
(item: any) => item.workOrder ?? item.openWorkOrder
|
|
1299
|
+
),
|
|
1300
|
+
taskCompleted: weeklyActivity.map(
|
|
1301
|
+
(item: any) => item.taskCompleted ?? item.closedWorkOrder
|
|
1302
|
+
),
|
|
1303
|
+
},
|
|
1304
|
+
taskScheduleItems: taskScheduleReq.value?.items ?? [],
|
|
1305
|
+
attendanceItems: attendanceReq.value?.items ?? [],
|
|
1306
|
+
feedbackItems: feedbacksReq.value?.items ?? [],
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if (isPropertyMode.value) {
|
|
1311
|
+
const data = getDashboardAltReq.value ?? {};
|
|
1312
|
+
return {
|
|
1313
|
+
...data,
|
|
1314
|
+
feedbackItems: feedbacksReq.value?.items ?? [],
|
|
1315
|
+
openWorkOrder: data.openWorkOrder ?? { count: 0, inProgress: 0, percentage: 0 },
|
|
1316
|
+
openIncidents: data.openIncidents ?? { count: 0, highSeverity: 0, percentage: 0 },
|
|
1317
|
+
visitors: data.visitors ?? { count: 0, currentlyOnSite: 0, percentage: 0 },
|
|
1318
|
+
facilityBooking: data.facilityBooking ?? {
|
|
1319
|
+
count: 0,
|
|
1320
|
+
ongoing: 0,
|
|
1321
|
+
waitingApproval: 0,
|
|
1322
|
+
percentage: 0,
|
|
1323
|
+
},
|
|
1324
|
+
};
|
|
911
1325
|
}
|
|
912
1326
|
|
|
1327
|
+
// Security
|
|
1328
|
+
const data = getDashboardAltReq.value ?? {};
|
|
913
1329
|
return {
|
|
914
1330
|
...data,
|
|
1331
|
+
feedbackItems: feedbacksReq.value?.items ?? [],
|
|
1332
|
+
attendanceItems: attendanceReq.value?.items ?? [],
|
|
915
1333
|
openWorkOrder: data.openWorkOrder ?? {
|
|
916
1334
|
count: 0,
|
|
1335
|
+
inProgress: 0,
|
|
917
1336
|
percentage: 0,
|
|
1337
|
+
overdue: 0,
|
|
918
1338
|
},
|
|
919
|
-
|
|
920
|
-
count: 0,
|
|
921
|
-
percentage: 0,
|
|
922
|
-
},
|
|
923
|
-
taskCompleted: data.taskCompleted ?? {
|
|
1339
|
+
openIncidents: data.openIncidents ?? {
|
|
924
1340
|
count: 0,
|
|
1341
|
+
highSeverity: 0,
|
|
925
1342
|
percentage: 0,
|
|
926
1343
|
},
|
|
927
|
-
|
|
1344
|
+
visitors: data.visitors ?? { count: 0, currentlyOnSite: 0, percentage: 0 },
|
|
1345
|
+
patrolCompliance: data.patrolCompliance ?? {
|
|
928
1346
|
count: 0,
|
|
1347
|
+
missedCheckpoints: 0,
|
|
1348
|
+
missed: 0,
|
|
929
1349
|
percentage: 0,
|
|
930
1350
|
},
|
|
931
|
-
|
|
932
|
-
labels: weeklyActivity.map((item: any) => item.day || item.label),
|
|
933
|
-
workOrders: weeklyActivity.map(
|
|
934
|
-
(item: any) => item.workOrder ?? item.openWorkOrder
|
|
935
|
-
),
|
|
936
|
-
taskCompleted: weeklyActivity.map(
|
|
937
|
-
(item: any) => item.taskCompleted ?? item.closedWorkOrder
|
|
938
|
-
),
|
|
939
|
-
openWorkOrder: weeklyActivity.some(
|
|
940
|
-
(item: any) => item.openWorkOrder !== undefined
|
|
941
|
-
)
|
|
942
|
-
? weeklyActivity.map((item: any) => item.openWorkOrder)
|
|
943
|
-
: undefined,
|
|
944
|
-
closedWorkOrder: weeklyActivity.some(
|
|
945
|
-
(item: any) => item.closedWorkOrder !== undefined
|
|
946
|
-
)
|
|
947
|
-
? weeklyActivity.map((item: any) => item.closedWorkOrder)
|
|
948
|
-
: undefined,
|
|
949
|
-
},
|
|
950
|
-
taskScheduleItems: taskScheduleReq.value?.items ?? [],
|
|
951
|
-
attendanceItems: attendanceReq.value?.items ?? [],
|
|
952
|
-
feedbackItems: feedbacksReq.value?.items ?? [],
|
|
1351
|
+
activePatrolItems: data.activePatrolItems ?? [],
|
|
953
1352
|
};
|
|
954
1353
|
});
|
|
955
1354
|
|
|
956
|
-
|
|
957
|
-
updatePeriod: [payload: { key: string; period: string }];
|
|
958
|
-
}>();
|
|
1355
|
+
// ─── Card Value Configs ───────────────────────────────────────────────────────
|
|
959
1356
|
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1357
|
+
const defaultCardValues: TDashboardCardValue[] = [
|
|
1358
|
+
{
|
|
1359
|
+
key: "openWorkOrder",
|
|
1360
|
+
periodKey: "period",
|
|
1361
|
+
title: "Open Work Order",
|
|
1362
|
+
icon: "mdi-briefcase",
|
|
1363
|
+
color: "#0b2f47",
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
key: "taskCompleted",
|
|
1367
|
+
periodKey: "period",
|
|
1368
|
+
title: "Task Completed",
|
|
1369
|
+
icon: "mdi-clipboard-check-outline",
|
|
1370
|
+
color: "#f4a340",
|
|
1371
|
+
},
|
|
1372
|
+
{
|
|
1373
|
+
key: "supplyAlert",
|
|
1374
|
+
periodKey: "period",
|
|
1375
|
+
title: "Supply Alert",
|
|
1376
|
+
icon: "mdi-alert",
|
|
1377
|
+
color: "#d32f2f",
|
|
1378
|
+
},
|
|
1379
|
+
];
|
|
973
1380
|
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
1381
|
+
const propertyCardValues: TDashboardCardValue[] = [
|
|
1382
|
+
{
|
|
1383
|
+
key: "workOrders",
|
|
1384
|
+
periodKey: "period",
|
|
1385
|
+
title: "Open Work Order",
|
|
1386
|
+
icon: "mdi-briefcase",
|
|
1387
|
+
color: "#1d5f99",
|
|
1388
|
+
},
|
|
1389
|
+
{
|
|
1390
|
+
key: "incidents",
|
|
1391
|
+
periodKey: "period",
|
|
1392
|
+
title: "Open Incidents",
|
|
1393
|
+
icon: "mdi-alert",
|
|
1394
|
+
color: "#b73535",
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
key: "visitors",
|
|
1398
|
+
periodKey: "period",
|
|
1399
|
+
title: "Visitors",
|
|
1400
|
+
icon: "mdi-account-group",
|
|
1401
|
+
color: "#d28722",
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
key: "facilityBookings",
|
|
1405
|
+
periodKey: "period",
|
|
1406
|
+
title: "Facility Booking",
|
|
1407
|
+
icon: "mdi-domain",
|
|
1408
|
+
color: "#23824f",
|
|
1409
|
+
},
|
|
1410
|
+
];
|
|
977
1411
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1412
|
+
const securityCardValues: TDashboardCardValue[] = [
|
|
1413
|
+
{
|
|
1414
|
+
key: "workOrders",
|
|
1415
|
+
periodKey: "period",
|
|
1416
|
+
title: "Open Work Order",
|
|
1417
|
+
icon: "mdi-briefcase",
|
|
1418
|
+
color: "#1d5f99",
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
key: "incidents",
|
|
1422
|
+
periodKey: "period",
|
|
1423
|
+
title: "Open Incidents",
|
|
1424
|
+
icon: "mdi-alert",
|
|
1425
|
+
color: "#b73535",
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
key: "visitors",
|
|
1429
|
+
periodKey: "period",
|
|
1430
|
+
title: "Visitors",
|
|
1431
|
+
icon: "mdi-account-group",
|
|
1432
|
+
color: "#d28722",
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
key: "patrolCompliance",
|
|
1436
|
+
periodKey: "period",
|
|
1437
|
+
title: "Patrol Compliance",
|
|
1438
|
+
icon: "mdi-calculator-variant",
|
|
1439
|
+
color: "#23824f",
|
|
1440
|
+
},
|
|
1441
|
+
];
|
|
991
1442
|
|
|
992
|
-
|
|
993
|
-
if (
|
|
1443
|
+
const resolvedCardValues = computed(() => {
|
|
1444
|
+
if (props.cardValues && props.cardValues.length > 0) return props.cardValues;
|
|
1445
|
+
if (isPropertyMode.value) return propertyCardValues;
|
|
1446
|
+
if (isSecurityMode.value) return securityCardValues;
|
|
1447
|
+
return defaultCardValues;
|
|
994
1448
|
});
|
|
995
1449
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1450
|
+
// ─── Widget Keys by Mode ──────────────────────────────────────────────────────
|
|
1451
|
+
|
|
1452
|
+
const defaultExtraWidgetKeys = [
|
|
1453
|
+
"activity",
|
|
1454
|
+
"taskSchedule",
|
|
1455
|
+
"staffStatus",
|
|
1456
|
+
"feedbacks",
|
|
1457
|
+
"recentActivity",
|
|
1000
1458
|
];
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
"
|
|
1004
|
-
"
|
|
1005
|
-
"
|
|
1006
|
-
"
|
|
1007
|
-
"
|
|
1008
|
-
"
|
|
1459
|
+
|
|
1460
|
+
const propertyDefaultWidgetKeys = [
|
|
1461
|
+
"workOrders",
|
|
1462
|
+
"incidents",
|
|
1463
|
+
"visitors",
|
|
1464
|
+
"facilityBookings",
|
|
1465
|
+
"activity",
|
|
1466
|
+
"upcomingEvents",
|
|
1467
|
+
"todayReminders",
|
|
1468
|
+
"todayAttentions",
|
|
1469
|
+
"workOrderStatus",
|
|
1470
|
+
"feedbacks",
|
|
1009
1471
|
];
|
|
1010
1472
|
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
).toLowerCase()
|
|
1022
|
-
);
|
|
1023
|
-
const isSecurityDashboard = computed(() => appKey.value.includes("security"));
|
|
1024
|
-
const primaryListConfig = computed(() =>
|
|
1025
|
-
isSecurityDashboard.value
|
|
1026
|
-
? {
|
|
1027
|
-
title: "Active Patrol",
|
|
1028
|
-
route: "virtualPatrol",
|
|
1029
|
-
keys: ["activePatrolItems", "patrolItems"],
|
|
1030
|
-
}
|
|
1031
|
-
: {
|
|
1032
|
-
title: "Today's Task Schedule",
|
|
1033
|
-
route: "scheduleTask",
|
|
1034
|
-
keys: [
|
|
1035
|
-
"taskScheduleItems",
|
|
1036
|
-
"scheduleTaskItems",
|
|
1037
|
-
"scheduledTaskItems",
|
|
1038
|
-
"cleaningScheduleItems",
|
|
1039
|
-
"tasks",
|
|
1040
|
-
],
|
|
1041
|
-
}
|
|
1042
|
-
);
|
|
1043
|
-
const staffStatusRoute = computed(() =>
|
|
1044
|
-
isSecurityDashboard.value ? "manpower" : "attendance"
|
|
1045
|
-
);
|
|
1473
|
+
const securityDefaultWidgetKeys = [
|
|
1474
|
+
"workOrders",
|
|
1475
|
+
"incidents",
|
|
1476
|
+
"visitors",
|
|
1477
|
+
"patrolCompliance",
|
|
1478
|
+
"activity",
|
|
1479
|
+
"activePatrol",
|
|
1480
|
+
"staffStatus",
|
|
1481
|
+
"feedbacks",
|
|
1482
|
+
];
|
|
1046
1483
|
|
|
1047
|
-
const defaultVisibleWidgetKeys = computed(() =>
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1484
|
+
const defaultVisibleWidgetKeys = computed(() => {
|
|
1485
|
+
if (props.extraWidgetKeys && props.extraWidgetKeys.length > 0) {
|
|
1486
|
+
return [
|
|
1487
|
+
...resolvedCardValues.value.map((c) => c.key),
|
|
1488
|
+
...props.extraWidgetKeys,
|
|
1489
|
+
];
|
|
1490
|
+
}
|
|
1491
|
+
if (isPropertyMode.value) return propertyDefaultWidgetKeys;
|
|
1492
|
+
if (isSecurityMode.value) return securityDefaultWidgetKeys;
|
|
1493
|
+
return [
|
|
1494
|
+
...resolvedCardValues.value.map((c) => c.key),
|
|
1495
|
+
...defaultExtraWidgetKeys,
|
|
1496
|
+
];
|
|
1497
|
+
});
|
|
1051
1498
|
|
|
1052
1499
|
watch(
|
|
1053
1500
|
defaultVisibleWidgetKeys,
|
|
@@ -1058,84 +1505,270 @@ watch(
|
|
|
1058
1505
|
draftVisibleWidgetKeys.value = nextKeys;
|
|
1059
1506
|
return;
|
|
1060
1507
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
nextKeys.includes(key)
|
|
1508
|
+
visibleWidgetKeys.value = visibleWidgetKeys.value.filter((k) =>
|
|
1509
|
+
nextKeys.includes(k)
|
|
1064
1510
|
);
|
|
1065
|
-
draftVisibleWidgetKeys.value = draftVisibleWidgetKeys.value.filter((
|
|
1066
|
-
nextKeys.includes(
|
|
1511
|
+
draftVisibleWidgetKeys.value = draftVisibleWidgetKeys.value.filter((k) =>
|
|
1512
|
+
nextKeys.includes(k)
|
|
1067
1513
|
);
|
|
1068
1514
|
},
|
|
1069
1515
|
{ immediate: true }
|
|
1070
1516
|
);
|
|
1071
1517
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1518
|
+
// ─── Customize Widgets Config ─────────────────────────────────────────────────
|
|
1519
|
+
|
|
1520
|
+
const customizeWidgets = computed(() => {
|
|
1521
|
+
if (isPropertyMode.value) {
|
|
1522
|
+
return [
|
|
1523
|
+
{ key: "workOrders", title: "Open Work Orders", description: "Track active work orders, overdue items, and what's in progress." },
|
|
1524
|
+
{ key: "incidents", title: "Open Incidents", description: "Monitor reported incidents and flag high severity cases." },
|
|
1525
|
+
{ key: "visitors", title: "Visitors", description: "See today's visitor traffic and who is currently on site." },
|
|
1526
|
+
{ key: "facilityBookings", title: "Facility Bookings", description: "View booking volume and approvals waiting for action." },
|
|
1527
|
+
{ key: "activity", title: "This Week Activity (Line Chart)", description: "Compare incidents and work orders over time." },
|
|
1528
|
+
{ key: "upcomingEvents", title: "Upcoming Events", description: "Quick view of scheduled events happening soon." },
|
|
1529
|
+
{ key: "todayReminders", title: "Today's Reminder", description: "Your next tasks and time sensitive alerts for today." },
|
|
1530
|
+
{ key: "todayAttentions", title: "Today's Attentions", description: "Items that require review overdue, pending, or unacknowledged." },
|
|
1531
|
+
{ key: "workOrderStatus", title: "Work Order Status (Progress Bar)", description: "Breakdown of work orders by status." },
|
|
1532
|
+
{ key: "feedbacks", title: "Feedbacks", description: "Recent feedback items and current resolution status." },
|
|
1533
|
+
];
|
|
1076
1534
|
}
|
|
1077
|
-
|
|
1535
|
+
if (isSecurityMode.value) {
|
|
1536
|
+
return [
|
|
1537
|
+
{ key: "workOrders", title: "Open Work Orders", description: "Track active work orders, overdue items, and what's in progress." },
|
|
1538
|
+
{ key: "incidents", title: "Open Incidents", description: "Monitor reported incidents and flag high severity cases." },
|
|
1539
|
+
{ key: "visitors", title: "Visitors", description: "See today's visitor traffic and who is currently on site." },
|
|
1540
|
+
{ key: "patrolCompliance", title: "Patrol Compliance", description: "View missed checkpoints and ongoing patrols." },
|
|
1541
|
+
{ key: "activity", title: "This Week Activity (Line Chart)", description: "Compare incidents and work orders over time." },
|
|
1542
|
+
{ key: "activePatrol", title: "Active Patrol", description: "Quick view of scheduled active patrol routes happening soon." },
|
|
1543
|
+
{ key: "staffStatus", title: "Staff Status", description: "Current attendance and status of staff." },
|
|
1544
|
+
{ key: "feedbacks", title: "Feedbacks", description: "Recent feedback and resolution status." },
|
|
1545
|
+
];
|
|
1546
|
+
}
|
|
1547
|
+
// Hygiene / others
|
|
1548
|
+
return [
|
|
1549
|
+
...resolvedCardValues.value.map((card) => ({
|
|
1550
|
+
key: card.key,
|
|
1551
|
+
title: card.title,
|
|
1552
|
+
description: `Show ${card.title} metrics for the selected period.`,
|
|
1553
|
+
})),
|
|
1554
|
+
{ key: "activity", title: "Activity Chart", description: "Compare activity trends over time." },
|
|
1555
|
+
{ key: "taskSchedule", title: primaryListConfig.value.title, description: "Show current tasks and their latest status." },
|
|
1556
|
+
{ key: "staffStatus", title: "Staff Total Attendance", description: "See who is on duty, on break, or absent." },
|
|
1557
|
+
{ key: "feedbacks", title: "Feedbacks", description: "Show recent feedback and resolution state." },
|
|
1558
|
+
{ key: "recentActivity", title: "Recent Activity", description: "Latest updates across the service." },
|
|
1559
|
+
];
|
|
1078
1560
|
});
|
|
1079
1561
|
|
|
1080
|
-
const customizeWidgets = computed(() => [
|
|
1081
|
-
...resolvedCardValues.value.map((card) => ({
|
|
1082
|
-
key: card.key,
|
|
1083
|
-
title: card.title,
|
|
1084
|
-
description: `Show ${card.title} metrics for the selected period.`,
|
|
1085
|
-
})),
|
|
1086
|
-
{
|
|
1087
|
-
key: "activity",
|
|
1088
|
-
title: "Activity Chart",
|
|
1089
|
-
description: "Compare activity trends over time.",
|
|
1090
|
-
},
|
|
1091
|
-
{
|
|
1092
|
-
key: "taskSchedule",
|
|
1093
|
-
title: primaryListConfig.value.title,
|
|
1094
|
-
description: "Show current tasks and their latest status.",
|
|
1095
|
-
},
|
|
1096
|
-
{
|
|
1097
|
-
key: "staffStatus",
|
|
1098
|
-
title: "Staff Total Attendance",
|
|
1099
|
-
description: "See who is on duty, on break, or absent.",
|
|
1100
|
-
},
|
|
1101
|
-
{
|
|
1102
|
-
key: "feedbacks",
|
|
1103
|
-
title: "Feedbacks",
|
|
1104
|
-
description: "Show recent feedback and resolution state.",
|
|
1105
|
-
},
|
|
1106
|
-
{
|
|
1107
|
-
key: "recentActivity",
|
|
1108
|
-
title: "Recent Activity",
|
|
1109
|
-
description: "Latest updates across the service.",
|
|
1110
|
-
},
|
|
1111
|
-
]);
|
|
1112
|
-
|
|
1113
1562
|
const filteredCustomizeWidgets = computed(() => {
|
|
1114
1563
|
const search = widgetSearch.value.trim().toLowerCase();
|
|
1115
1564
|
if (!search) return customizeWidgets.value;
|
|
1116
|
-
|
|
1117
1565
|
return customizeWidgets.value.filter(
|
|
1118
|
-
(
|
|
1119
|
-
|
|
1120
|
-
|
|
1566
|
+
(w) =>
|
|
1567
|
+
w.title.toLowerCase().includes(search) ||
|
|
1568
|
+
w.description.toLowerCase().includes(search)
|
|
1121
1569
|
);
|
|
1122
1570
|
});
|
|
1571
|
+
|
|
1572
|
+
// ─── KPI Metrics ─────────────────────────────────────────────────────────────
|
|
1573
|
+
|
|
1574
|
+
const defaultVisitorOptions = [
|
|
1575
|
+
"All", "Drive-In", "Walk-In", "Pick-up", "Drop-Off", "Delivery", "Contractor",
|
|
1576
|
+
];
|
|
1577
|
+
|
|
1123
1578
|
const visitorItems = computed(() =>
|
|
1124
1579
|
getDashboardArray(["visitorItems", "visitorsList", "visitorTransactionsList"])
|
|
1125
1580
|
);
|
|
1126
1581
|
const visitorOptions = computed(() =>
|
|
1582
|
+
buildFilterOptions(defaultVisitorOptions, visitorItems.value, getVisitorFilterValue)
|
|
1583
|
+
);
|
|
1584
|
+
|
|
1585
|
+
// Property-specific: facility booking metric with client-side filter
|
|
1586
|
+
const facilityBookingItems = computed(() =>
|
|
1587
|
+
getDashboardArray([
|
|
1588
|
+
"facilityBookingItems",
|
|
1589
|
+
"facilityBookingsList",
|
|
1590
|
+
"bookingsList",
|
|
1591
|
+
])
|
|
1592
|
+
);
|
|
1593
|
+
const defaultFacilityOptions = ["All"];
|
|
1594
|
+
const facilityOptions = computed(() =>
|
|
1127
1595
|
buildFilterOptions(
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1596
|
+
defaultFacilityOptions,
|
|
1597
|
+
facilityBookingItems.value,
|
|
1598
|
+
(item: any) =>
|
|
1599
|
+
item.facilityName || item.facility?.name || item.facilityInfo?.name || item.name || ""
|
|
1131
1600
|
)
|
|
1132
1601
|
);
|
|
1133
1602
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1603
|
+
function getPropertyKpiCards() {
|
|
1604
|
+
const workOrderMetric = getMetric(["openWorkOrder", "workOrders", "openWorkOrders", "workOrder"]);
|
|
1605
|
+
const incidentMetric = getMetric(["openIncidents", "incidents", "incidentReports"]);
|
|
1606
|
+
const visitorMetric = getMetric(["visitors", "visitorTransactions"]);
|
|
1607
|
+
const facilityMetric = computeFacilityMetric();
|
|
1608
|
+
|
|
1609
|
+
return [
|
|
1610
|
+
{
|
|
1611
|
+
key: "workOrders",
|
|
1612
|
+
title: "Open Work Order",
|
|
1613
|
+
icon: "mdi-briefcase",
|
|
1614
|
+
color: "#1d5f99",
|
|
1615
|
+
tint: "#d7ecff",
|
|
1616
|
+
value: formatNumber(toNumber(workOrderMetric?.count)),
|
|
1617
|
+
percentage: toNumber(workOrderMetric?.percentage),
|
|
1618
|
+
meta: `${toNumber(workOrderMetric?.inProgress)} in progress`,
|
|
1619
|
+
raw: workOrderMetric,
|
|
1620
|
+
filter: undefined,
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
key: "incidents",
|
|
1624
|
+
title: "Open Incidents",
|
|
1625
|
+
icon: "mdi-alert",
|
|
1626
|
+
color: "#b73535",
|
|
1627
|
+
tint: "#ffdede",
|
|
1628
|
+
value: formatNumber(toNumber(incidentMetric?.count)),
|
|
1629
|
+
percentage: toNumber(incidentMetric?.percentage),
|
|
1630
|
+
meta: `${toNumber(incidentMetric?.highSeverity)} high severity`,
|
|
1631
|
+
raw: incidentMetric,
|
|
1632
|
+
filter: undefined,
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
key: "visitors",
|
|
1636
|
+
title: "Visitors",
|
|
1637
|
+
icon: "mdi-account-group",
|
|
1638
|
+
color: "#d28722",
|
|
1639
|
+
tint: "#ffe4bd",
|
|
1640
|
+
value: formatNumber(toNumber(visitorMetric?.count)),
|
|
1641
|
+
percentage: toNumber(visitorMetric?.percentage),
|
|
1642
|
+
meta: `${toNumber(visitorMetric?.currentlyOnSite)} currently on site`,
|
|
1643
|
+
raw: visitorMetric,
|
|
1644
|
+
filter: {
|
|
1645
|
+
model: visitorType.value,
|
|
1646
|
+
options: visitorOptions.value,
|
|
1647
|
+
set: (v: string) => {
|
|
1648
|
+
visitorType.value = v;
|
|
1649
|
+
filterMenus.visitors = false;
|
|
1650
|
+
},
|
|
1651
|
+
},
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
key: "facilityBookings",
|
|
1655
|
+
title: "Facility Booking",
|
|
1656
|
+
icon: "mdi-domain",
|
|
1657
|
+
color: "#23824f",
|
|
1658
|
+
tint: "#c8f3da",
|
|
1659
|
+
value: formatNumber(toNumber(facilityMetric?.count)),
|
|
1660
|
+
percentage: toNumber(facilityMetric?.percentage),
|
|
1661
|
+
meta: `${toNumber(facilityMetric?.ongoing)} ongoing - ${toNumber(facilityMetric?.waitingApproval)} waiting approval`,
|
|
1662
|
+
raw: facilityMetric,
|
|
1663
|
+
filter: {
|
|
1664
|
+
model: facilityType.value,
|
|
1665
|
+
options: facilityOptions.value,
|
|
1666
|
+
set: (v: string) => {
|
|
1667
|
+
facilityType.value = v;
|
|
1668
|
+
filterMenus.facilityBookings = false;
|
|
1669
|
+
},
|
|
1670
|
+
},
|
|
1671
|
+
},
|
|
1672
|
+
];
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function computeFacilityMetric(): DashboardMetric {
|
|
1676
|
+
const metric = getMetric(["facilityBooking", "facilityBookings", "bookings"]);
|
|
1677
|
+
const selected = facilityType.value;
|
|
1678
|
+
if (selected === "All") return metric;
|
|
1679
|
+
const items = facilityBookingItems.value.filter((item: any) => {
|
|
1680
|
+
const val = String(
|
|
1681
|
+
item.facilityName || item.facility?.name || item.facilityInfo?.name || item.name || ""
|
|
1682
|
+
).toLowerCase();
|
|
1683
|
+
return val === selected.toLowerCase();
|
|
1684
|
+
});
|
|
1685
|
+
return {
|
|
1686
|
+
...metric,
|
|
1687
|
+
count: items.length,
|
|
1688
|
+
ongoing: items.filter((i: any) => {
|
|
1689
|
+
const s = String(i.status || "").toLowerCase();
|
|
1690
|
+
return s.includes("ongoing") || s.includes("approved") || s.includes("in-progress");
|
|
1691
|
+
}).length,
|
|
1692
|
+
waitingApproval: items.filter((i: any) => {
|
|
1693
|
+
const s = String(i.status || "").toLowerCase();
|
|
1694
|
+
return s.includes("pending") || s.includes("review") || s.includes("approval");
|
|
1695
|
+
}).length,
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function getSecurityKpiCards() {
|
|
1700
|
+
const workOrderMetric = getMetric(["openWorkOrder", "workOrders", "openWorkOrders", "workOrder"]);
|
|
1701
|
+
const incidentMetric = getMetric(["openIncidents", "incidents", "incidentReports"]);
|
|
1702
|
+
const patrolMetric = getMetric(["patrolCompliance", "patrol"]);
|
|
1703
|
+
|
|
1704
|
+
return [
|
|
1705
|
+
{
|
|
1706
|
+
key: "workOrders",
|
|
1707
|
+
title: "Open Work Order",
|
|
1708
|
+
icon: "mdi-briefcase",
|
|
1709
|
+
color: "#1d5f99",
|
|
1710
|
+
tint: "#d7ecff",
|
|
1711
|
+
value: formatNumber(toNumber(workOrderMetric?.count)),
|
|
1712
|
+
percentage: toNumber(workOrderMetric?.percentage),
|
|
1713
|
+
meta: `${toNumber(workOrderMetric?.overdue)} overdue · ${toNumber(workOrderMetric?.inProgress)} in progress`,
|
|
1714
|
+
raw: workOrderMetric,
|
|
1715
|
+
filter: undefined,
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
key: "incidents",
|
|
1719
|
+
title: "Open Incidents",
|
|
1720
|
+
icon: "mdi-alert",
|
|
1721
|
+
color: "#b73535",
|
|
1722
|
+
tint: "#ffdede",
|
|
1723
|
+
value: formatNumber(toNumber(incidentMetric?.count)),
|
|
1724
|
+
percentage: toNumber(incidentMetric?.percentage),
|
|
1725
|
+
meta: `${toNumber(incidentMetric?.highSeverity)} high severity`,
|
|
1726
|
+
raw: incidentMetric,
|
|
1727
|
+
filter: undefined,
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
key: "visitors",
|
|
1731
|
+
title: "Visitors",
|
|
1732
|
+
icon: "mdi-account-group",
|
|
1733
|
+
color: "#d28722",
|
|
1734
|
+
tint: "#ffe4bd",
|
|
1735
|
+
value: formatNumber(toNumber(getFilteredVisitorMetric()?.count)),
|
|
1736
|
+
percentage: toNumber(getFilteredVisitorMetric()?.percentage),
|
|
1737
|
+
meta: `${toNumber(getFilteredVisitorMetric()?.currentlyOnSite)} currently on site`,
|
|
1738
|
+
raw: getFilteredVisitorMetric(),
|
|
1739
|
+
filter: {
|
|
1740
|
+
model: visitorType.value,
|
|
1741
|
+
options: visitorOptions.value,
|
|
1742
|
+
set: (v: string) => {
|
|
1743
|
+
visitorType.value = v;
|
|
1744
|
+
filterMenus.visitors = false;
|
|
1745
|
+
},
|
|
1746
|
+
},
|
|
1747
|
+
},
|
|
1748
|
+
{
|
|
1749
|
+
key: "patrolCompliance",
|
|
1750
|
+
title: "Patrol Compliance",
|
|
1751
|
+
icon: "mdi-calculator-variant",
|
|
1752
|
+
color: "#23824f",
|
|
1753
|
+
tint: "#c8f3da",
|
|
1754
|
+
value: `${toNumber(patrolMetric?.count)}%`,
|
|
1755
|
+
percentage: toNumber(patrolMetric?.percentage),
|
|
1756
|
+
meta: `${toNumber(patrolMetric?.missed ?? patrolMetric?.missedCheckpoints)} checkpoint missed · ${toNumber(patrolMetric?.ongoing)} ongoing`,
|
|
1757
|
+
raw: patrolMetric,
|
|
1758
|
+
filter: undefined,
|
|
1759
|
+
},
|
|
1760
|
+
];
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
const kpiCards = computed(() => {
|
|
1764
|
+
if (isPropertyMode.value) return getPropertyKpiCards();
|
|
1765
|
+
if (isSecurityMode.value) return getSecurityKpiCards();
|
|
1766
|
+
|
|
1767
|
+
// Hygiene / others — use generic resolvedCardValues
|
|
1768
|
+
return resolvedCardValues.value.map((mapping, index) => {
|
|
1769
|
+
const metric =
|
|
1770
|
+
mapping.key === "visitors" && isSecurityMode.value
|
|
1771
|
+
? getFilteredVisitorMetric()
|
|
1139
1772
|
: getMetric([mapping.key]);
|
|
1140
1773
|
const value = getMetricValue(metric, mapping.key);
|
|
1141
1774
|
|
|
@@ -1149,25 +1782,49 @@ const kpiCards = computed(() =>
|
|
|
1149
1782
|
percentage: toNumber(metric.percentage),
|
|
1150
1783
|
meta: getMetricMeta(metric),
|
|
1151
1784
|
raw: metric,
|
|
1152
|
-
filter:
|
|
1153
|
-
mapping.key === "visitors" && isSecurityDashboard.value
|
|
1154
|
-
? {
|
|
1155
|
-
model: visitorType.value,
|
|
1156
|
-
options: visitorOptions.value,
|
|
1157
|
-
set: (option: string) => {
|
|
1158
|
-
visitorType.value = option;
|
|
1159
|
-
filterMenus.visitors = false;
|
|
1160
|
-
},
|
|
1161
|
-
}
|
|
1162
|
-
: undefined,
|
|
1785
|
+
filter: undefined as any,
|
|
1163
1786
|
};
|
|
1164
|
-
})
|
|
1165
|
-
);
|
|
1787
|
+
});
|
|
1788
|
+
});
|
|
1166
1789
|
|
|
1167
1790
|
const visibleKpiCards = computed(() =>
|
|
1168
1791
|
kpiCards.value.filter((card) => isWidgetVisible(card.key))
|
|
1169
1792
|
);
|
|
1170
1793
|
|
|
1794
|
+
// ─── Dashboard Title ──────────────────────────────────────────────────────────
|
|
1795
|
+
|
|
1796
|
+
const dashboardTitle = computed(() => {
|
|
1797
|
+
if (props.title) return props.title;
|
|
1798
|
+
const appName = String(
|
|
1799
|
+
runtimeConfig.public.APP_NAME || runtimeConfig.public.APP || ""
|
|
1800
|
+
);
|
|
1801
|
+
return `${getServiceDisplayName(props.serviceType || appName)} - Dashboard`;
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
// ─── Primary list / staff config ─────────────────────────────────────────────
|
|
1805
|
+
|
|
1806
|
+
const primaryListConfig = computed(() =>
|
|
1807
|
+
isSecurityMode.value
|
|
1808
|
+
? {
|
|
1809
|
+
title: "Active Patrol",
|
|
1810
|
+
route: "virtualPatrol",
|
|
1811
|
+
keys: ["activePatrolItems", "patrolItems"],
|
|
1812
|
+
}
|
|
1813
|
+
: {
|
|
1814
|
+
title: "Today's Task Schedule",
|
|
1815
|
+
route: "scheduleTask",
|
|
1816
|
+
keys: [
|
|
1817
|
+
"taskScheduleItems",
|
|
1818
|
+
"scheduleTaskItems",
|
|
1819
|
+
"scheduledTaskItems",
|
|
1820
|
+
"cleaningScheduleItems",
|
|
1821
|
+
"tasks",
|
|
1822
|
+
],
|
|
1823
|
+
}
|
|
1824
|
+
);
|
|
1825
|
+
|
|
1826
|
+
// ─── Activity Chart ───────────────────────────────────────────────────────────
|
|
1827
|
+
|
|
1171
1828
|
const activityTitle = computed(() => {
|
|
1172
1829
|
if (selectedRange.value === "Today") return "Today's Activity";
|
|
1173
1830
|
if (selectedRange.value === "This Month") return "This Month Activity";
|
|
@@ -1180,7 +1837,6 @@ const activityChart = computed(() =>
|
|
|
1180
1837
|
const chartLabels = computed<string[]>(() => {
|
|
1181
1838
|
const labels = activityChart.value.labels;
|
|
1182
1839
|
if (labels.length) return labels;
|
|
1183
|
-
|
|
1184
1840
|
if (selectedRange.value === "Today")
|
|
1185
1841
|
return ["12 AM", "4 AM", "8 AM", "12 PM", "4 PM", "8 PM"];
|
|
1186
1842
|
if (selectedRange.value === "This Month") {
|
|
@@ -1199,12 +1855,8 @@ const primarySeries = computed(() =>
|
|
|
1199
1855
|
const secondarySeries = computed(() =>
|
|
1200
1856
|
normalizeSeries(activityChart.value.secondary, chartLabels.value.length)
|
|
1201
1857
|
);
|
|
1202
|
-
const primaryLegendLabel = computed(
|
|
1203
|
-
|
|
1204
|
-
);
|
|
1205
|
-
const secondaryLegendLabel = computed(
|
|
1206
|
-
() => activityChart.value.secondaryLabel || "Feedback"
|
|
1207
|
-
);
|
|
1858
|
+
const primaryLegendLabel = computed(() => "Closed Work Order");
|
|
1859
|
+
const secondaryLegendLabel = computed(() => "Open Work Order");
|
|
1208
1860
|
const chartMax = computed(() =>
|
|
1209
1861
|
Math.max(1, ...primarySeries.value, ...secondarySeries.value)
|
|
1210
1862
|
);
|
|
@@ -1225,21 +1877,6 @@ const chartYLabels = computed(() => {
|
|
|
1225
1877
|
];
|
|
1226
1878
|
});
|
|
1227
1879
|
|
|
1228
|
-
function buildSmoothPath(points: { x: number; y: number }[]) {
|
|
1229
|
-
if (!points || points.length === 0) return "";
|
|
1230
|
-
let d = `M ${points[0].x},${points[0].y}`;
|
|
1231
|
-
for (let i = 0; i < points.length - 1; i++) {
|
|
1232
|
-
const p1 = points[i];
|
|
1233
|
-
const p2 = points[i + 1];
|
|
1234
|
-
const cp1x = p1.x + (p2.x - p1.x) / 2;
|
|
1235
|
-
const cp1y = p1.y;
|
|
1236
|
-
const cp2x = p1.x + (p2.x - p1.x) / 2;
|
|
1237
|
-
const cp2y = p2.y;
|
|
1238
|
-
d += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p2.x},${p2.y}`;
|
|
1239
|
-
}
|
|
1240
|
-
return d;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
1880
|
const primaryDots = computed(() => buildChartDots(primarySeries.value));
|
|
1244
1881
|
const secondaryDots = computed(() => buildChartDots(secondarySeries.value));
|
|
1245
1882
|
const primaryPath = computed(() => buildSmoothPath(primaryDots.value));
|
|
@@ -1248,13 +1885,14 @@ const chartAxisLabels = computed(() => {
|
|
|
1248
1885
|
const labels = chartLabels.value;
|
|
1249
1886
|
const count = Math.max(1, labels.length - 1);
|
|
1250
1887
|
const width = chartWidth.value;
|
|
1251
|
-
|
|
1252
1888
|
return labels.map((label, index) => ({
|
|
1253
1889
|
text: label,
|
|
1254
1890
|
x: 54 + ((width - 74) / count) * index,
|
|
1255
1891
|
}));
|
|
1256
1892
|
});
|
|
1257
1893
|
|
|
1894
|
+
// ─── List Data ────────────────────────────────────────────────────────────────
|
|
1895
|
+
|
|
1258
1896
|
const taskScheduleItems = computed<TaskItem[]>(() =>
|
|
1259
1897
|
getDashboardArray([...primaryListConfig.value.keys])
|
|
1260
1898
|
.map(normalizeTaskItem)
|
|
@@ -1270,48 +1908,76 @@ const feedbackItems = computed<FeedbackItem[]>(() =>
|
|
|
1270
1908
|
.map(normalizeFeedbackItem)
|
|
1271
1909
|
.slice(0, 4)
|
|
1272
1910
|
);
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1911
|
+
|
|
1912
|
+
// Property-mode lists
|
|
1913
|
+
const upcomingEvents = computed(() =>
|
|
1914
|
+
normalizeListItems(["upcomingEvents", "eventsList", "events"])
|
|
1915
|
+
);
|
|
1916
|
+
const todayReminders = computed(() =>
|
|
1917
|
+
normalizeListItems(["todayReminders", "todaysReminders", "reminders"])
|
|
1918
|
+
);
|
|
1919
|
+
const todayAttentions = computed(() =>
|
|
1920
|
+
normalizeListItems(["todayAttentions", "attentions"])
|
|
1277
1921
|
);
|
|
1278
1922
|
|
|
1279
|
-
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
danger: { bg: "red-lighten-4", text: "red-darken-3" },
|
|
1283
|
-
success: { bg: "green-lighten-4", text: "green-darken-3" },
|
|
1284
|
-
warning: { bg: "orange-lighten-4", text: "orange-darken-3" },
|
|
1285
|
-
neutral: { bg: "grey-lighten-3", text: "blue-grey-darken-1" },
|
|
1286
|
-
};
|
|
1287
|
-
return map[tone] || map.neutral;
|
|
1288
|
-
}
|
|
1923
|
+
const workOrderStatus = computed(() => {
|
|
1924
|
+
const summary = finalDashboardData.value?.workOrderStatusSummary;
|
|
1925
|
+
const metric = getMetric(["openWorkOrder", "workOrders", "workOrder"]);
|
|
1289
1926
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const titleMap: Record<string, string> = {
|
|
1293
|
-
security_agency: "Security",
|
|
1294
|
-
cleaning_services: "Hygiene",
|
|
1295
|
-
hygiene: "Hygiene",
|
|
1296
|
-
mechanical_electrical: "Mechanical & Electrical",
|
|
1297
|
-
mechanical_electrical_services: "Mechanical & Electrical",
|
|
1298
|
-
pest_control_services: "Pest Control",
|
|
1299
|
-
pool_maintenance_services: "Pool Maintenance",
|
|
1300
|
-
landscaping_services: "Landscaping",
|
|
1301
|
-
property_management_agency: "Property Management",
|
|
1302
|
-
};
|
|
1927
|
+
const src = summary || metric;
|
|
1928
|
+
if (!src) return [];
|
|
1303
1929
|
|
|
1304
|
-
|
|
1930
|
+
const completed = Number(src.completed ?? 0);
|
|
1931
|
+
const inProgress = Number(src.inProgress ?? 0);
|
|
1932
|
+
const pending =
|
|
1933
|
+
src.pending !== undefined
|
|
1934
|
+
? Number(src.pending)
|
|
1935
|
+
: Math.max(0, Number(src.count ?? 0) - inProgress);
|
|
1936
|
+
const max = Math.max(completed, inProgress, pending, 1);
|
|
1305
1937
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1938
|
+
return [
|
|
1939
|
+
{ label: "Completed", value: completed, percent: (completed / max) * 100, color: "#2fb84d" },
|
|
1940
|
+
{ label: "In Progress", value: inProgress, percent: (inProgress / max) * 100, color: "#f12d2d" },
|
|
1941
|
+
{ label: "Pending", value: pending, percent: (pending / max) * 100, color: "#ff8900" },
|
|
1942
|
+
];
|
|
1943
|
+
});
|
|
1310
1944
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1945
|
+
// Security-mode lists
|
|
1946
|
+
const activePatrolItems = computed<any[]>(() =>
|
|
1947
|
+
finalDashboardData.value?.activePatrolItems ?? []
|
|
1948
|
+
);
|
|
1949
|
+
const securityStaffItems = computed<any[]>(() =>
|
|
1950
|
+
attendanceReq.value?.items ?? []
|
|
1951
|
+
);
|
|
1952
|
+
|
|
1953
|
+
// ─── ResizeObserver ───────────────────────────────────────────────────────────
|
|
1954
|
+
|
|
1955
|
+
watch(chartContainerRef, (el) => {
|
|
1956
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
1957
|
+
if (el && window.ResizeObserver) {
|
|
1958
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
1959
|
+
if (entries[0] && entries[0].contentRect.width > 0) {
|
|
1960
|
+
chartWidth.value = entries[0].contentRect.width;
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
resizeObserver.observe(el);
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
onUnmounted(() => {
|
|
1967
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
// ─── Range watcher ────────────────────────────────────────────────────────────
|
|
1971
|
+
|
|
1972
|
+
watch(selectedRange, (period) => {
|
|
1973
|
+
if (props.serviceType) {
|
|
1974
|
+
const apiPeriod = periodValue[period];
|
|
1975
|
+
if (apiPeriod) currentPeriod.value = apiPeriod;
|
|
1976
|
+
}
|
|
1977
|
+
emit("updatePeriod", { key: "__all__", period });
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
// ─── Metric Helpers ───────────────────────────────────────────────────────────
|
|
1315
1981
|
|
|
1316
1982
|
function getMetric(keys: string[]): DashboardMetric {
|
|
1317
1983
|
const data = finalDashboardData.value || {};
|
|
@@ -1328,20 +1994,16 @@ function getMetricValue(metric: DashboardMetric, key?: string) {
|
|
|
1328
1994
|
if (key === "patrolCompliance") {
|
|
1329
1995
|
return `${formatNumber(toNumber(metric.count))}%`;
|
|
1330
1996
|
}
|
|
1331
|
-
|
|
1332
1997
|
const completed = toNumber(metric.completed);
|
|
1333
1998
|
const total = toNumber(metric.total);
|
|
1334
|
-
|
|
1335
1999
|
if (total > 0 && typeof metric.completed !== "undefined") {
|
|
1336
2000
|
return `${formatNumber(completed)}/${formatNumber(total)}`;
|
|
1337
2001
|
}
|
|
1338
|
-
|
|
1339
2002
|
return toNumber(metric.count);
|
|
1340
2003
|
}
|
|
1341
2004
|
|
|
1342
2005
|
function getMetricMeta(metric: DashboardMetric) {
|
|
1343
2006
|
const parts: string[] = [];
|
|
1344
|
-
|
|
1345
2007
|
if (typeof metric.overdue !== "undefined")
|
|
1346
2008
|
parts.push(`${formatNumber(toNumber(metric.overdue))} overdue`);
|
|
1347
2009
|
if (typeof metric.inProgress !== "undefined")
|
|
@@ -1351,20 +2013,13 @@ function getMetricMeta(metric: DashboardMetric) {
|
|
|
1351
2013
|
if (typeof metric.highSeverity !== "undefined")
|
|
1352
2014
|
parts.push(`${formatNumber(toNumber(metric.highSeverity))} high severity`);
|
|
1353
2015
|
if (typeof metric.currentlyOnSite !== "undefined")
|
|
1354
|
-
parts.push(
|
|
1355
|
-
`${formatNumber(toNumber(metric.currentlyOnSite))} currently on site`
|
|
1356
|
-
);
|
|
2016
|
+
parts.push(`${formatNumber(toNumber(metric.currentlyOnSite))} currently on site`);
|
|
1357
2017
|
if (
|
|
1358
|
-
typeof (metric as
|
|
1359
|
-
typeof (metric as
|
|
2018
|
+
typeof (metric as any).missedCheckpoints !== "undefined" ||
|
|
2019
|
+
typeof (metric as any).missed !== "undefined"
|
|
1360
2020
|
) {
|
|
1361
2021
|
parts.push(
|
|
1362
|
-
`${formatNumber(
|
|
1363
|
-
toNumber(
|
|
1364
|
-
(metric as Record<string, any>).missedCheckpoints ??
|
|
1365
|
-
(metric as Record<string, any>).missed
|
|
1366
|
-
)
|
|
1367
|
-
)} checkpoint missed`
|
|
2022
|
+
`${formatNumber(toNumber((metric as any).missedCheckpoints ?? (metric as any).missed))} checkpoint missed`
|
|
1368
2023
|
);
|
|
1369
2024
|
}
|
|
1370
2025
|
if (typeof metric.lowStock !== "undefined")
|
|
@@ -1377,218 +2032,123 @@ function getMetricMeta(metric: DashboardMetric) {
|
|
|
1377
2032
|
) {
|
|
1378
2033
|
parts.push(`${formatNumber(toNumber(metric.completed))} completed`);
|
|
1379
2034
|
}
|
|
1380
|
-
|
|
1381
2035
|
return parts.join(" - ");
|
|
1382
2036
|
}
|
|
1383
2037
|
|
|
1384
|
-
function
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
function getCardTint(color: string, index: number) {
|
|
1389
|
-
const tints = ["#dbeeff", "#f8c6c3", "#d8f5e5", "#ffe7c2"];
|
|
1390
|
-
if (!color) return tints[index % tints.length];
|
|
2038
|
+
function getFilteredVisitorMetric(): DashboardMetric {
|
|
2039
|
+
const metric = getMetric(["visitors", "visitor"]);
|
|
2040
|
+
const selected = visitorType.value;
|
|
2041
|
+
const items = visitorItems.value;
|
|
1391
2042
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
2043
|
+
if (selected === "All") {
|
|
2044
|
+
return {
|
|
2045
|
+
...metric,
|
|
2046
|
+
count: toNumber(metric.count ?? items.length),
|
|
2047
|
+
currentlyOnSite: toNumber(
|
|
2048
|
+
metric.currentlyOnSite ??
|
|
2049
|
+
items.filter((item: any) => !isClosedStatus(item.status) && !item.checkOut)
|
|
2050
|
+
.length
|
|
2051
|
+
),
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
1401
2054
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
return (
|
|
1405
|
-
data.activityChart ||
|
|
1406
|
-
data.statistic?.activityChart ||
|
|
1407
|
-
data.statistics?.activityChart ||
|
|
1408
|
-
data.statisticChart ||
|
|
1409
|
-
data.statisticsChart ||
|
|
1410
|
-
data.activityStatistics ||
|
|
1411
|
-
data.activityStatistic ||
|
|
1412
|
-
{}
|
|
2055
|
+
const filtered = items.filter(
|
|
2056
|
+
(item: any) => getVisitorFilterValue(item) === selected
|
|
1413
2057
|
);
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
function normalizeActivityChart(source: any) {
|
|
1417
|
-
const labels = Array.isArray(source?.labels)
|
|
1418
|
-
? source.labels.map((label: any) => String(label))
|
|
1419
|
-
: [];
|
|
1420
|
-
|
|
1421
|
-
const secondary = extractChartSeries(source, [
|
|
1422
|
-
"closedWorkOrder",
|
|
1423
|
-
"closedWorkOrders",
|
|
1424
|
-
"closed_work_orders",
|
|
1425
|
-
]);
|
|
1426
|
-
|
|
1427
|
-
const hasClosedSeries = secondary.length > 0;
|
|
1428
|
-
|
|
1429
|
-
const primary = extractChartSeries(source, [
|
|
1430
|
-
"openWorkOrder",
|
|
1431
|
-
"openWorkOrders",
|
|
1432
|
-
"open_work_orders",
|
|
1433
|
-
"workOrders",
|
|
1434
|
-
"workOrder",
|
|
1435
|
-
"work_orders",
|
|
1436
|
-
]);
|
|
1437
|
-
|
|
1438
|
-
const secondaryCandidate = hasClosedSeries
|
|
1439
|
-
? { label: "Closed Work Order", series: secondary }
|
|
1440
|
-
: getSecondaryChartSeries(source);
|
|
1441
|
-
|
|
1442
2058
|
return {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
2059
|
+
...metric,
|
|
2060
|
+
count: filtered.length,
|
|
2061
|
+
currentlyOnSite: filtered.filter(
|
|
2062
|
+
(item: any) => !isClosedStatus(item.status) && !item.checkOut
|
|
2063
|
+
).length,
|
|
1448
2064
|
};
|
|
1449
2065
|
}
|
|
1450
2066
|
|
|
1451
|
-
|
|
1452
|
-
const candidates = [
|
|
1453
|
-
{
|
|
1454
|
-
label: "Task Completed",
|
|
1455
|
-
keys: ["taskCompleted", "completedTasks", "completed"],
|
|
1456
|
-
},
|
|
1457
|
-
{ label: "Incident", keys: ["incidents", "incident", "incidentReports"] },
|
|
1458
|
-
{ label: "Feedback", keys: ["feedbacks", "feedback", "feedbackTickets"] },
|
|
1459
|
-
];
|
|
2067
|
+
// ─── Array/List Helpers ───────────────────────────────────────────────────────
|
|
1460
2068
|
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
2069
|
+
function getDashboardArray(keys: string[]) {
|
|
2070
|
+
const data = finalDashboardData.value || {};
|
|
2071
|
+
for (const key of keys) {
|
|
2072
|
+
const value = data[key];
|
|
2073
|
+
if (Array.isArray(value)) return value;
|
|
2074
|
+
if (Array.isArray(value?.items)) return value.items;
|
|
2075
|
+
if (Array.isArray(value?.docs)) return value.docs;
|
|
2076
|
+
if (Array.isArray(value?.data)) return value.data;
|
|
1464
2077
|
}
|
|
2078
|
+
return [];
|
|
2079
|
+
}
|
|
1465
2080
|
|
|
1466
|
-
|
|
2081
|
+
function normalizeListItems(keys: string[]): DashboardListItem[] {
|
|
2082
|
+
const data = finalDashboardData.value || {};
|
|
2083
|
+
const source = keys.map((k) => data[k]).find(Array.isArray);
|
|
2084
|
+
if (!source) return [];
|
|
2085
|
+
const context = keys[0];
|
|
2086
|
+
|
|
2087
|
+
return source.slice(0, 4).map((item: any, index: number) => ({
|
|
2088
|
+
id: item._id || item.id || `${keys[0]}-${index}`,
|
|
2089
|
+
title: item.title || item.name || item.subject || "Untitled",
|
|
2090
|
+
subtitle: getListSubtitle(context, item),
|
|
2091
|
+
time: getListTime(item),
|
|
2092
|
+
color: item.color || item.statusColor || "primary",
|
|
2093
|
+
icon: item.icon || getListIcon(context),
|
|
2094
|
+
tone: item.tone,
|
|
2095
|
+
actionLabel: item.actionLabel,
|
|
2096
|
+
}));
|
|
1467
2097
|
}
|
|
1468
2098
|
|
|
1469
|
-
function
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
2099
|
+
function getListSubtitle(context: string, item: any) {
|
|
2100
|
+
if (item.subtitle) return item.subtitle;
|
|
2101
|
+
const time = getListTime(item);
|
|
2102
|
+
if (context === "upcomingEvents") {
|
|
2103
|
+
const type = item.type || item.category || item.status || "Event";
|
|
2104
|
+
return [type, formatDashboardDateTime(time)].filter(Boolean).join(" - ");
|
|
1473
2105
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
const dataset = source.datasets.find((item: any) => {
|
|
1477
|
-
const keyText = String(
|
|
1478
|
-
item?.key || item?.label || item?.name || ""
|
|
1479
|
-
).toLowerCase();
|
|
1480
|
-
return keys.some((key) => keyText.includes(key.toLowerCase()));
|
|
1481
|
-
});
|
|
1482
|
-
if (Array.isArray(dataset?.data)) return dataset.data.map(toNumber);
|
|
2106
|
+
if (context === "todayReminders" || context === "todayAttentions") {
|
|
2107
|
+
return formatDashboardTime(time) || item.description || item.status || "";
|
|
1483
2108
|
}
|
|
1484
|
-
|
|
1485
|
-
return [];
|
|
2109
|
+
return item.description || item.category || item.location || formatDashboardTime(time);
|
|
1486
2110
|
}
|
|
1487
2111
|
|
|
1488
|
-
function
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
2112
|
+
function getListTime(item: any) {
|
|
2113
|
+
return (
|
|
2114
|
+
item.time ||
|
|
2115
|
+
item.dateTime ||
|
|
2116
|
+
item.start_datetime ||
|
|
2117
|
+
item.startDate ||
|
|
2118
|
+
item.startAt ||
|
|
2119
|
+
item.date ||
|
|
2120
|
+
item.createdAt ||
|
|
2121
|
+
item.updatedAt ||
|
|
2122
|
+
""
|
|
1492
2123
|
);
|
|
1493
2124
|
}
|
|
1494
2125
|
|
|
1495
|
-
function
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
return
|
|
1500
|
-
x: 54 + ((width - 74) / count) * index,
|
|
1501
|
-
y: 220 - (toNumber(value) / chartMax.value) * 184,
|
|
1502
|
-
}));
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
function getDashboardArray(keys: string[]) {
|
|
1506
|
-
const data = finalDashboardData.value || {};
|
|
1507
|
-
|
|
1508
|
-
for (const key of keys) {
|
|
1509
|
-
const value = data[key];
|
|
1510
|
-
if (Array.isArray(value)) return value;
|
|
1511
|
-
if (Array.isArray(value?.items)) return value.items;
|
|
1512
|
-
if (Array.isArray(value?.docs)) return value.docs;
|
|
1513
|
-
if (Array.isArray(value?.data)) return value.data;
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
return [];
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
function getFilteredVisitorMetric(): DashboardMetric {
|
|
1520
|
-
const metric = getMetric(["visitors", "visitor"]);
|
|
1521
|
-
const selected = visitorType.value;
|
|
1522
|
-
const items = visitorItems.value;
|
|
1523
|
-
|
|
1524
|
-
if (selected === "All") {
|
|
1525
|
-
return {
|
|
1526
|
-
...metric,
|
|
1527
|
-
count: toNumber(metric.count ?? items.length),
|
|
1528
|
-
currentlyOnSite: toNumber(
|
|
1529
|
-
metric.currentlyOnSite ??
|
|
1530
|
-
items.filter(
|
|
1531
|
-
(item: any) => !isClosedStatus(item.status) && !item.checkOut
|
|
1532
|
-
).length
|
|
1533
|
-
),
|
|
1534
|
-
};
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
const filtered = items.filter(
|
|
1538
|
-
(item: any) => getVisitorFilterValue(item) === selected
|
|
1539
|
-
);
|
|
1540
|
-
|
|
1541
|
-
return {
|
|
1542
|
-
...metric,
|
|
1543
|
-
count: filtered.length,
|
|
1544
|
-
currentlyOnSite: filtered.filter(
|
|
1545
|
-
(item: any) => !isClosedStatus(item.status) && !item.checkOut
|
|
1546
|
-
).length,
|
|
1547
|
-
};
|
|
2126
|
+
function getListIcon(context: string) {
|
|
2127
|
+
if (context === "upcomingEvents") return "mdi-circle";
|
|
2128
|
+
if (context === "todayReminders") return "mdi-alert";
|
|
2129
|
+
if (context === "todayAttentions") return "mdi-alert-circle-outline";
|
|
2130
|
+
return "mdi-circle";
|
|
1548
2131
|
}
|
|
1549
2132
|
|
|
1550
|
-
function
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
return Array.from(options);
|
|
2133
|
+
function getListTone(item: DashboardListItem) {
|
|
2134
|
+
if (item.tone) return item.tone;
|
|
2135
|
+
const text = `${item.title} ${item.subtitle || ""} ${item.icon || ""} ${item.color || ""}`.toLowerCase();
|
|
2136
|
+
if (text.includes("alert") || text.includes("incident") || text.includes("overdue") || text.includes("pending"))
|
|
2137
|
+
return "danger";
|
|
2138
|
+
if (text.includes("booking") || text.includes("facility") || text.includes("approved") || text.includes("complete"))
|
|
2139
|
+
return "success";
|
|
2140
|
+
if (text.includes("visitor") || text.includes("check")) return "warning";
|
|
2141
|
+
if (text.includes("calendar") || text.includes("event") || text.includes("reminder"))
|
|
2142
|
+
return "info";
|
|
2143
|
+
return "neutral";
|
|
1562
2144
|
}
|
|
1563
2145
|
|
|
1564
|
-
|
|
1565
|
-
const raw = String(
|
|
1566
|
-
item.type || item.visitorType || item.category || ""
|
|
1567
|
-
).toLowerCase();
|
|
1568
|
-
if (raw.includes("contractor")) return "Contractor";
|
|
1569
|
-
if (raw.includes("walk")) return "Walk-In";
|
|
1570
|
-
if (raw.includes("delivery")) return "Delivery";
|
|
1571
|
-
if (raw.includes("pick")) return "Pick-up";
|
|
1572
|
-
if (raw.includes("drop")) return "Drop-Off";
|
|
1573
|
-
if (raw.includes("guest") || raw.includes("drive")) return "Drive-In";
|
|
1574
|
-
return "All";
|
|
1575
|
-
}
|
|
2146
|
+
// ─── Normalizers ─────────────────────────────────────────────────────────────
|
|
1576
2147
|
|
|
1577
2148
|
function normalizeTaskItem(item: Record<string, any>, index: number): TaskItem {
|
|
1578
2149
|
const title =
|
|
1579
|
-
item.title ||
|
|
1580
|
-
|
|
1581
|
-
item.name ||
|
|
1582
|
-
item.taskName ||
|
|
1583
|
-
item.description ||
|
|
1584
|
-
"Task";
|
|
1585
|
-
const subtitle =
|
|
1586
|
-
item.subtitle ||
|
|
1587
|
-
item.area ||
|
|
1588
|
-
item.location ||
|
|
1589
|
-
item.asset ||
|
|
1590
|
-
item.category ||
|
|
1591
|
-
"";
|
|
2150
|
+
item.title || item.subject || item.name || item.taskName || item.description || "Task";
|
|
2151
|
+
const subtitle = item.subtitle || item.area || item.location || item.asset || item.category || "";
|
|
1592
2152
|
const person =
|
|
1593
2153
|
item.person ||
|
|
1594
2154
|
item.assigneeName ||
|
|
@@ -1602,22 +2162,14 @@ function normalizeTaskItem(item: Record<string, any>, index: number): TaskItem {
|
|
|
1602
2162
|
title,
|
|
1603
2163
|
subtitle,
|
|
1604
2164
|
person,
|
|
1605
|
-
status: normalizeStatus(
|
|
1606
|
-
item.status || item.state || item.taskStatus || "To-Do"
|
|
1607
|
-
),
|
|
2165
|
+
status: normalizeStatus(item.status || item.state || item.taskStatus || "To-Do"),
|
|
1608
2166
|
time:
|
|
1609
|
-
item.time ||
|
|
1610
|
-
item.startTime ||
|
|
1611
|
-
item.scheduleTime ||
|
|
1612
|
-
formatActivityTime(item),
|
|
2167
|
+
item.time || item.startTime || item.scheduleTime || formatActivityTime(item),
|
|
1613
2168
|
raw: item,
|
|
1614
2169
|
};
|
|
1615
2170
|
}
|
|
1616
2171
|
|
|
1617
|
-
function normalizeStaffItem(
|
|
1618
|
-
item: Record<string, any>,
|
|
1619
|
-
index: number
|
|
1620
|
-
): StaffItem {
|
|
2172
|
+
function normalizeStaffItem(item: Record<string, any>, index: number): StaffItem {
|
|
1621
2173
|
const name =
|
|
1622
2174
|
item.name ||
|
|
1623
2175
|
item.employeeName ||
|
|
@@ -1638,21 +2190,14 @@ function normalizeStaffItem(
|
|
|
1638
2190
|
name,
|
|
1639
2191
|
role,
|
|
1640
2192
|
status: normalizeStatus(
|
|
1641
|
-
item.statusLabel ||
|
|
1642
|
-
item.status ||
|
|
1643
|
-
item.currentStatus ||
|
|
1644
|
-
item.attendanceStatus ||
|
|
1645
|
-
"Off duty"
|
|
2193
|
+
item.statusLabel || item.status || item.currentStatus || item.attendanceStatus || "Off duty"
|
|
1646
2194
|
),
|
|
1647
2195
|
totalHours: item.totalHours || 0,
|
|
1648
2196
|
raw: item,
|
|
1649
2197
|
};
|
|
1650
2198
|
}
|
|
1651
2199
|
|
|
1652
|
-
function normalizeFeedbackItem(
|
|
1653
|
-
item: Record<string, any>,
|
|
1654
|
-
index: number
|
|
1655
|
-
): FeedbackItem {
|
|
2200
|
+
function normalizeFeedbackItem(item: Record<string, any>, index: number): FeedbackItem {
|
|
1656
2201
|
const title = item.title || item.subject || item.description || "Feedback";
|
|
1657
2202
|
const subject = item.category || item.type || item.service || "Subject";
|
|
1658
2203
|
const app = item.app || item.source || item.channel || "";
|
|
@@ -1675,179 +2220,190 @@ function normalizeFeedbackItem(
|
|
|
1675
2220
|
};
|
|
1676
2221
|
}
|
|
1677
2222
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
)
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
item.description ||
|
|
1686
|
-
item.message ||
|
|
1687
|
-
"Activity";
|
|
1688
|
-
const subtitle =
|
|
1689
|
-
item.subtitle || item.type || item.category || item.status || "";
|
|
1690
|
-
|
|
1691
|
-
return {
|
|
1692
|
-
id: String(item.id || item._id || `${title}-${index}`),
|
|
1693
|
-
title,
|
|
1694
|
-
subtitle,
|
|
1695
|
-
time: formatActivityTime(item),
|
|
1696
|
-
icon: item.icon || "mdi-history",
|
|
1697
|
-
tone: item.tone || getStatusTone(item.status || item.type),
|
|
1698
|
-
raw: item,
|
|
1699
|
-
};
|
|
2223
|
+
// Feedback tone helper (for property-mode pill classes)
|
|
2224
|
+
function getFeedbackTone(status: any) {
|
|
2225
|
+
const s = String(status || "").toLowerCase();
|
|
2226
|
+
if (s.includes("complete") || s.includes("resolved") || s.includes("done")) return "completed";
|
|
2227
|
+
if (s.includes("progress") || s.includes("review")) return "progress";
|
|
2228
|
+
if (s.includes("reject") || s.includes("cancel")) return "danger";
|
|
2229
|
+
return "todo";
|
|
1700
2230
|
}
|
|
1701
2231
|
|
|
1702
|
-
function
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
/\b(ago|now|today|yesterday|min|mins|hr|hrs|hour|hours|day|days)\b/i.test(
|
|
1711
|
-
rawText
|
|
1712
|
-
)
|
|
1713
|
-
) {
|
|
1714
|
-
return rawText;
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
return formatRelativeTime(rawTime);
|
|
2232
|
+
function getInitials(name?: string) {
|
|
2233
|
+
return String(name || "")
|
|
2234
|
+
.trim()
|
|
2235
|
+
.split(/\s+/)
|
|
2236
|
+
.filter(Boolean)
|
|
2237
|
+
.slice(0, 2)
|
|
2238
|
+
.map((w) => w.charAt(0).toUpperCase())
|
|
2239
|
+
.join("") || "-";
|
|
1718
2240
|
}
|
|
1719
2241
|
|
|
1720
|
-
|
|
1721
|
-
const toneMap: Record<string, { background: string; color: string }> = {
|
|
1722
|
-
info: {
|
|
1723
|
-
background: "#d8ecff",
|
|
1724
|
-
color: "#17496f",
|
|
1725
|
-
},
|
|
1726
|
-
danger: {
|
|
1727
|
-
background: "#ffd9d6",
|
|
1728
|
-
color: "#bd413c",
|
|
1729
|
-
},
|
|
1730
|
-
success: {
|
|
1731
|
-
background: "#d8f4e3",
|
|
1732
|
-
color: "#2b9160",
|
|
1733
|
-
},
|
|
1734
|
-
warning: {
|
|
1735
|
-
background: "#ffe0ad",
|
|
1736
|
-
color: "#d88717",
|
|
1737
|
-
},
|
|
1738
|
-
neutral: {
|
|
1739
|
-
background: "#edf2f6",
|
|
1740
|
-
color: "#607386",
|
|
1741
|
-
},
|
|
1742
|
-
};
|
|
2242
|
+
// ─── Status Style Helpers ────────────────────────────────────────────────────
|
|
1743
2243
|
|
|
1744
|
-
|
|
2244
|
+
function getStatusPillStyle(status: any) {
|
|
2245
|
+
const tone = getStatusTone(status);
|
|
2246
|
+
const map: Record<string, { backgroundColor: string; color: string }> = {
|
|
2247
|
+
success: { backgroundColor: "#72e3a1", color: "#0f5132" },
|
|
2248
|
+
warning: { backgroundColor: "#fde68a", color: "#92400e" },
|
|
2249
|
+
error: { backgroundColor: "#fecaca", color: "#b91c1c" },
|
|
2250
|
+
neutral: { backgroundColor: "#e2e8f0", color: "#64748b" },
|
|
2251
|
+
};
|
|
2252
|
+
return map[tone] || map.neutral;
|
|
1745
2253
|
}
|
|
1746
2254
|
|
|
1747
|
-
function
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
item.updated_on
|
|
1755
|
-
);
|
|
2255
|
+
function getPatrolStatusStyle(status: any) {
|
|
2256
|
+
const s = String(status || "").toLowerCase();
|
|
2257
|
+
if (s === "completed" || s === "complete")
|
|
2258
|
+
return { backgroundColor: "#72e3a1", color: "#0f5132" };
|
|
2259
|
+
if (s === "on paused" || s === "paused")
|
|
2260
|
+
return { backgroundColor: "#dcdcdc", color: "#64748b" };
|
|
2261
|
+
return { backgroundColor: "#e2e8f0", color: "#64748b" };
|
|
1756
2262
|
}
|
|
1757
2263
|
|
|
1758
|
-
function
|
|
1759
|
-
const
|
|
1760
|
-
if (
|
|
2264
|
+
function getStaffStatusPillStyle(status: any) {
|
|
2265
|
+
const s = String(status || "").toLowerCase();
|
|
2266
|
+
if (s === "on duty" || s === "on-duty")
|
|
2267
|
+
return { backgroundColor: "#72e3a1", color: "#0f5132" };
|
|
2268
|
+
if (s === "on break" || s === "on-break")
|
|
2269
|
+
return { backgroundColor: "#fde68a", color: "#92400e" };
|
|
2270
|
+
if (s === "absent")
|
|
2271
|
+
return { backgroundColor: "#fecaca", color: "#b91c1c" };
|
|
2272
|
+
return { backgroundColor: "#e2e8f0", color: "#64748b" };
|
|
2273
|
+
}
|
|
1761
2274
|
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
2275
|
+
function getStatusProgressStyle(item: { percent: number; color: string }) {
|
|
2276
|
+
const percent = Math.min(100, Math.max(0, Number(item.percent) || 0));
|
|
2277
|
+
return { backgroundColor: item.color, width: `${percent}%` };
|
|
1765
2278
|
}
|
|
1766
2279
|
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
if (
|
|
1783
|
-
normalized.includes("overdue") ||
|
|
1784
|
-
normalized.includes("urgent") ||
|
|
1785
|
-
normalized.includes("high")
|
|
1786
|
-
)
|
|
1787
|
-
return "danger";
|
|
1788
|
-
return "neutral";
|
|
2280
|
+
// ─── Chart Helpers ────────────────────────────────────────────────────────────
|
|
2281
|
+
|
|
2282
|
+
function buildSmoothPath(points: { x: number; y: number }[]) {
|
|
2283
|
+
if (!points || points.length === 0) return "";
|
|
2284
|
+
let d = `M ${points[0].x},${points[0].y}`;
|
|
2285
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
2286
|
+
const p1 = points[i];
|
|
2287
|
+
const p2 = points[i + 1];
|
|
2288
|
+
const cp1x = p1.x + (p2.x - p1.x) / 2;
|
|
2289
|
+
const cp1y = p1.y;
|
|
2290
|
+
const cp2x = p1.x + (p2.x - p1.x) / 2;
|
|
2291
|
+
const cp2y = p2.y;
|
|
2292
|
+
d += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p2.x},${p2.y}`;
|
|
2293
|
+
}
|
|
2294
|
+
return d;
|
|
1789
2295
|
}
|
|
1790
2296
|
|
|
1791
|
-
function
|
|
1792
|
-
const
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
)
|
|
1798
|
-
return "success";
|
|
1799
|
-
if (normalized.includes("break")) return "warning";
|
|
1800
|
-
if (normalized.includes("absent") || normalized.includes("off"))
|
|
1801
|
-
return "danger";
|
|
1802
|
-
return "neutral";
|
|
2297
|
+
function buildChartDots(series: number[]) {
|
|
2298
|
+
const count = Math.max(1, series.length - 1);
|
|
2299
|
+
const width = chartWidth.value;
|
|
2300
|
+
return series.map((value, index) => ({
|
|
2301
|
+
x: 54 + ((width - 74) / count) * index,
|
|
2302
|
+
y: 220 - (toNumber(value) / chartMax.value) * 184,
|
|
2303
|
+
}));
|
|
1803
2304
|
}
|
|
1804
2305
|
|
|
1805
|
-
function
|
|
1806
|
-
const
|
|
2306
|
+
function getActivityChartSource() {
|
|
2307
|
+
const data = finalDashboardData.value || {};
|
|
1807
2308
|
return (
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
2309
|
+
data.activityChart ||
|
|
2310
|
+
data.statistic?.activityChart ||
|
|
2311
|
+
data.statistics?.activityChart ||
|
|
2312
|
+
data.statisticChart ||
|
|
2313
|
+
data.statisticsChart ||
|
|
2314
|
+
data.activityStatistics ||
|
|
2315
|
+
data.activityStatistic ||
|
|
2316
|
+
{}
|
|
1815
2317
|
);
|
|
1816
2318
|
}
|
|
1817
2319
|
|
|
1818
|
-
function
|
|
1819
|
-
|
|
2320
|
+
function normalizeActivityChart(source: any) {
|
|
2321
|
+
const labels = Array.isArray(source?.labels)
|
|
2322
|
+
? source.labels.map((l: any) => String(l))
|
|
2323
|
+
: [];
|
|
2324
|
+
const secondary = extractChartSeries(source, ["closedWorkOrder", "closedWorkOrders", "closed_work_orders"]);
|
|
2325
|
+
const hasClosedSeries = secondary.length > 0;
|
|
2326
|
+
const primary = extractChartSeries(source, ["openWorkOrder", "openWorkOrders", "open_work_orders", "workOrders", "workOrder", "work_orders"]);
|
|
2327
|
+
const secondaryCandidate = hasClosedSeries
|
|
2328
|
+
? { label: "Closed Work Order", series: secondary }
|
|
2329
|
+
: getSecondaryChartSeries(source);
|
|
2330
|
+
|
|
2331
|
+
return {
|
|
2332
|
+
labels,
|
|
2333
|
+
primary,
|
|
2334
|
+
secondary: secondaryCandidate.series,
|
|
2335
|
+
primaryLabel: hasClosedSeries ? "Open Work Order" : "Work Order",
|
|
2336
|
+
secondaryLabel: secondaryCandidate.label,
|
|
2337
|
+
};
|
|
1820
2338
|
}
|
|
1821
2339
|
|
|
1822
|
-
function
|
|
1823
|
-
|
|
1824
|
-
|
|
2340
|
+
function getSecondaryChartSeries(source: any) {
|
|
2341
|
+
const candidates = [
|
|
2342
|
+
{ label: "Task Completed", keys: ["taskCompleted", "completedTasks", "completed"] },
|
|
2343
|
+
{ label: "Incident", keys: ["incidents", "incident", "incidentReports"] },
|
|
2344
|
+
{ label: "Feedback", keys: ["feedbacks", "feedback", "feedbackTickets"] },
|
|
2345
|
+
];
|
|
2346
|
+
for (const candidate of candidates) {
|
|
2347
|
+
const series = extractChartSeries(source, candidate.keys);
|
|
2348
|
+
if (series.length) return { label: candidate.label, series };
|
|
2349
|
+
}
|
|
2350
|
+
return { label: "Feedback", series: [] as number[] };
|
|
1825
2351
|
}
|
|
1826
2352
|
|
|
1827
|
-
function
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2353
|
+
function extractChartSeries(source: any, keys: string[]) {
|
|
2354
|
+
for (const key of keys) {
|
|
2355
|
+
const value = source?.[key];
|
|
2356
|
+
if (Array.isArray(value)) return value.map(toNumber);
|
|
2357
|
+
}
|
|
2358
|
+
if (Array.isArray(source?.datasets)) {
|
|
2359
|
+
const dataset = source.datasets.find((item: any) => {
|
|
2360
|
+
const keyText = String(item?.key || item?.label || item?.name || "").toLowerCase();
|
|
2361
|
+
return keys.some((key) => keyText.includes(key.toLowerCase()));
|
|
2362
|
+
});
|
|
2363
|
+
if (Array.isArray(dataset?.data)) return dataset.data.map(toNumber);
|
|
2364
|
+
}
|
|
2365
|
+
return [];
|
|
1831
2366
|
}
|
|
1832
2367
|
|
|
1833
|
-
function
|
|
1834
|
-
|
|
2368
|
+
function normalizeSeries(value: any, targetLength: number) {
|
|
2369
|
+
const source = Array.isArray(value) ? value : [];
|
|
2370
|
+
return Array.from({ length: targetLength }, (_, index) => toNumber(source[index]));
|
|
1835
2371
|
}
|
|
1836
2372
|
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2373
|
+
// ─── Filter Helpers ───────────────────────────────────────────────────────────
|
|
2374
|
+
|
|
2375
|
+
function buildFilterOptions(
|
|
2376
|
+
defaults: string[],
|
|
2377
|
+
items: Record<string, any>[],
|
|
2378
|
+
getValue: (item: Record<string, any>) => string
|
|
2379
|
+
) {
|
|
2380
|
+
const options = new Set(defaults);
|
|
2381
|
+
items.forEach((item) => {
|
|
2382
|
+
const value = getValue(item);
|
|
2383
|
+
if (value) options.add(value);
|
|
2384
|
+
});
|
|
2385
|
+
return Array.from(options);
|
|
1841
2386
|
}
|
|
1842
2387
|
|
|
1843
|
-
function
|
|
1844
|
-
|
|
1845
|
-
|
|
2388
|
+
function getVisitorFilterValue(item: Record<string, any>) {
|
|
2389
|
+
const raw = String(item.type || item.visitorType || item.category || "").toLowerCase();
|
|
2390
|
+
if (raw.includes("contractor")) return "Contractor";
|
|
2391
|
+
if (raw.includes("walk")) return "Walk-In";
|
|
2392
|
+
if (raw.includes("delivery")) return "Delivery";
|
|
2393
|
+
if (raw.includes("pick")) return "Pick-up";
|
|
2394
|
+
if (raw.includes("drop")) return "Drop-Off";
|
|
2395
|
+
if (raw.includes("guest") || raw.includes("drive")) return "Drive-In";
|
|
2396
|
+
return "All";
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
// ─── Routing ─────────────────────────────────────────────────────────────────
|
|
2400
|
+
|
|
2401
|
+
function dashboardPath(path: string) {
|
|
2402
|
+
return `/${props.orgId}/${props.site}/${path}`;
|
|
1846
2403
|
}
|
|
1847
2404
|
|
|
1848
2405
|
function goToDashboardRoute(routeKey: string) {
|
|
1849
2406
|
if (!props.orgId || !props.site) return;
|
|
1850
|
-
|
|
1851
2407
|
const pathMap: Record<string, string> = {
|
|
1852
2408
|
attendance: "attendance",
|
|
1853
2409
|
feedbacks: "feedbacks",
|
|
@@ -1855,8 +2411,11 @@ function goToDashboardRoute(routeKey: string) {
|
|
|
1855
2411
|
virtualPatrol: "virtual-patrol/logs",
|
|
1856
2412
|
manpower: "manpower-monitoring/attendance",
|
|
1857
2413
|
incidentReports: "incident-reports",
|
|
1858
|
-
|
|
1859
|
-
|
|
2414
|
+
"virtual-patrol": "virtual-patrol",
|
|
2415
|
+
"work-orders": "work-orders",
|
|
2416
|
+
"event-mgmt": "event-mgmt",
|
|
2417
|
+
calendar: "calendar",
|
|
2418
|
+
"incident-reports": "incident-reports",
|
|
1860
2419
|
};
|
|
1861
2420
|
const path = pathMap[routeKey] || routeKey;
|
|
1862
2421
|
router.push(`/${props.orgId}/${props.site}/${path}`);
|
|
@@ -1867,13 +2426,11 @@ function goToListItem(item: TaskItem | FeedbackItem, routeKey: string) {
|
|
|
1867
2426
|
goToDashboardRoute(routeKey);
|
|
1868
2427
|
return;
|
|
1869
2428
|
}
|
|
1870
|
-
|
|
1871
2429
|
const id = item.raw?._id || item.raw?.id || item.id;
|
|
1872
2430
|
if (!id || String(id).includes("-")) {
|
|
1873
2431
|
goToDashboardRoute(routeKey);
|
|
1874
2432
|
return;
|
|
1875
2433
|
}
|
|
1876
|
-
|
|
1877
2434
|
const pathMap: Record<string, string> = {
|
|
1878
2435
|
feedbacks: "feedbacks",
|
|
1879
2436
|
scheduleTask: "schedule-task",
|
|
@@ -1882,45 +2439,494 @@ function goToListItem(item: TaskItem | FeedbackItem, routeKey: string) {
|
|
|
1882
2439
|
router.push(`/${props.orgId}/${props.site}/${path}/${id}`);
|
|
1883
2440
|
}
|
|
1884
2441
|
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
.filter(Boolean);
|
|
1890
|
-
if (!parts.length) return "?";
|
|
1891
|
-
return parts
|
|
1892
|
-
.slice(0, 2)
|
|
1893
|
-
.map((part) => part[0])
|
|
1894
|
-
.join("")
|
|
1895
|
-
.toUpperCase();
|
|
2442
|
+
// ─── Widget Management ────────────────────────────────────────────────────────
|
|
2443
|
+
|
|
2444
|
+
function isWidgetVisible(key: string) {
|
|
2445
|
+
return visibleWidgetKeys.value.includes(key);
|
|
1896
2446
|
}
|
|
1897
2447
|
|
|
1898
|
-
function
|
|
1899
|
-
|
|
2448
|
+
function openCustomizeDialog() {
|
|
2449
|
+
draftVisibleWidgetKeys.value = [...visibleWidgetKeys.value];
|
|
2450
|
+
customizeDialog.value = true;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
function cancelCustomize() {
|
|
2454
|
+
draftVisibleWidgetKeys.value = [...visibleWidgetKeys.value];
|
|
2455
|
+
widgetSearch.value = "";
|
|
2456
|
+
customizeDialog.value = false;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function resetCustomizeDraft() {
|
|
2460
|
+
draftVisibleWidgetKeys.value = [...defaultVisibleWidgetKeys.value];
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
function submitCustomize() {
|
|
2464
|
+
visibleWidgetKeys.value = [...draftVisibleWidgetKeys.value];
|
|
2465
|
+
widgetSearch.value = "";
|
|
2466
|
+
customizeDialog.value = false;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
function notifyExport(format: string) {
|
|
2470
|
+
snackbar.text = `${format} export is being prepared.`;
|
|
2471
|
+
snackbar.visible = true;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// ─── Card Color Helpers ───────────────────────────────────────────────────────
|
|
2475
|
+
|
|
2476
|
+
function getCardColor(index: number) {
|
|
2477
|
+
return ["#155178", "#9f2f29", "#1f8a56", "#b96d17"][index % 4];
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
function getCardTint(color: string, index: number) {
|
|
2481
|
+
const tints = ["#dbeeff", "#f8c6c3", "#d8f5e5", "#ffe7c2"];
|
|
2482
|
+
if (!color) return tints[index % tints.length];
|
|
2483
|
+
const n = color.toLowerCase();
|
|
2484
|
+
if (n.includes("f44336") || n.includes("d32f2f")) return "#f8c6c3";
|
|
2485
|
+
if (n.includes("4caf50") || n.includes("43a047")) return "#d8f5e5";
|
|
2486
|
+
if (n.includes("ff9800") || n.includes("ffa726")) return "#ffe7c2";
|
|
2487
|
+
return "#dbeeff";
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// ─── Generic Helpers ─────────────────────────────────────────────────────────
|
|
2491
|
+
|
|
2492
|
+
function getServiceDisplayName(value: string) {
|
|
2493
|
+
const normalized = value.toLowerCase();
|
|
2494
|
+
const titleMap: Record<string, string> = {
|
|
2495
|
+
security_agency: "Security",
|
|
2496
|
+
cleaning_services: "Hygiene",
|
|
2497
|
+
hygiene: "Hygiene",
|
|
2498
|
+
mechanical_electrical: "Mechanical & Electrical",
|
|
2499
|
+
mechanical_electrical_services: "Mechanical & Electrical",
|
|
2500
|
+
pest_control_services: "Pest Control",
|
|
2501
|
+
pool_maintenance_services: "Pool Maintenance",
|
|
2502
|
+
landscaping_services: "Landscaping",
|
|
2503
|
+
property_management_agency: "Property Management",
|
|
2504
|
+
};
|
|
2505
|
+
if (titleMap[normalized]) return titleMap[normalized];
|
|
2506
|
+
const cleaned = normalized
|
|
2507
|
+
.replace(/_services|_agency/g, "")
|
|
2508
|
+
.replace(/[_-]+/g, " ")
|
|
2509
|
+
.trim();
|
|
2510
|
+
return cleaned
|
|
2511
|
+
? cleaned.replace(/\b\w/g, (char) => char.toUpperCase())
|
|
2512
|
+
: "Service";
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
function formatNumber(value: any) {
|
|
2516
|
+
const n = Number(value ?? 0);
|
|
2517
|
+
return new Intl.NumberFormat("en-US").format(Number.isFinite(n) ? n : 0);
|
|
1900
2518
|
}
|
|
1901
2519
|
|
|
1902
2520
|
function toNumber(value: any) {
|
|
1903
|
-
const
|
|
1904
|
-
return Number.isFinite(
|
|
2521
|
+
const n = Number(value ?? 0);
|
|
2522
|
+
return Number.isFinite(n) ? n : 0;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
function normalizeStatus(value: any) {
|
|
2526
|
+
const text = String(value || "").trim();
|
|
2527
|
+
if (!text) return "To-Do";
|
|
2528
|
+
return text.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function getStatusTone(status: any) {
|
|
2532
|
+
const s = String(status || "").toLowerCase();
|
|
2533
|
+
if (s.includes("complete") || s.includes("done") || s.includes("resolved") || s.includes("closed"))
|
|
2534
|
+
return "success";
|
|
2535
|
+
if (s.includes("progress") || s.includes("active") || s.includes("ongoing"))
|
|
2536
|
+
return "info";
|
|
2537
|
+
if (s.includes("overdue") || s.includes("urgent") || s.includes("high"))
|
|
2538
|
+
return "danger";
|
|
2539
|
+
if (s.includes("warning") || s.includes("reject") || s.includes("cancel"))
|
|
2540
|
+
return "error";
|
|
2541
|
+
return "neutral";
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
function isClosedStatus(status: any) {
|
|
2545
|
+
const s = String(status || "").toLowerCase();
|
|
2546
|
+
return (
|
|
2547
|
+
s.includes("complete") ||
|
|
2548
|
+
s.includes("cancel") ||
|
|
2549
|
+
s.includes("reject") ||
|
|
2550
|
+
s.includes("closed") ||
|
|
2551
|
+
s.includes("deleted") ||
|
|
2552
|
+
s.includes("check-out") ||
|
|
2553
|
+
s.includes("checkout")
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
function formatActivityTime(item: Record<string, any>) {
|
|
2558
|
+
const rawTime =
|
|
2559
|
+
item.time ||
|
|
2560
|
+
item.createdAt ||
|
|
2561
|
+
item.updatedAt ||
|
|
2562
|
+
item.date ||
|
|
2563
|
+
item.startTime ||
|
|
2564
|
+
item.scheduleTime;
|
|
2565
|
+
return formatRelativeTime(rawTime);
|
|
1905
2566
|
}
|
|
1906
2567
|
|
|
1907
2568
|
function formatRelativeTime(value: any) {
|
|
1908
2569
|
if (!value) return "";
|
|
1909
|
-
|
|
1910
2570
|
const date = new Date(value);
|
|
1911
2571
|
if (Number.isNaN(date.getTime())) return "";
|
|
1912
|
-
|
|
1913
2572
|
const diffMs = Date.now() - date.getTime();
|
|
1914
2573
|
const diffMinutes = Math.max(0, Math.round(diffMs / 60000));
|
|
1915
2574
|
if (diffMinutes < 60) {
|
|
1916
2575
|
const minutes = diffMinutes || 1;
|
|
1917
2576
|
return `${minutes} min${minutes === 1 ? "" : "s"} ago`;
|
|
1918
2577
|
}
|
|
1919
|
-
|
|
1920
2578
|
const diffHours = Math.round(diffMinutes / 60);
|
|
1921
2579
|
if (diffHours < 24) return `${diffHours} hr${diffHours === 1 ? "" : "s"} ago`;
|
|
1922
|
-
|
|
1923
2580
|
const diffDays = Math.round(diffHours / 24);
|
|
1924
2581
|
return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
|
|
1925
2582
|
}
|
|
2583
|
+
|
|
2584
|
+
function formatDashboardDateTime(value?: string) {
|
|
2585
|
+
if (!value) return "";
|
|
2586
|
+
const date = new Date(value);
|
|
2587
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
2588
|
+
return `${date.toLocaleDateString("en-GB", {
|
|
2589
|
+
day: "2-digit",
|
|
2590
|
+
month: "2-digit",
|
|
2591
|
+
year: "numeric",
|
|
2592
|
+
})}, ${date.toLocaleTimeString("en-GB", {
|
|
2593
|
+
hour: "2-digit",
|
|
2594
|
+
minute: "2-digit",
|
|
2595
|
+
hour12: false,
|
|
2596
|
+
})}`;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
function formatDashboardTime(value?: string) {
|
|
2600
|
+
if (!value) return "";
|
|
2601
|
+
const date = new Date(value);
|
|
2602
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
2603
|
+
const now = Date.now();
|
|
2604
|
+
const diff = date.getTime() - now;
|
|
2605
|
+
const abs = Math.abs(diff);
|
|
2606
|
+
const minute = 60 * 1000;
|
|
2607
|
+
const hour = 60 * minute;
|
|
2608
|
+
const day = 24 * hour;
|
|
2609
|
+
if (abs < minute) return diff >= 0 ? "In a moment" : "Just now";
|
|
2610
|
+
if (abs < hour) {
|
|
2611
|
+
const minutes = Math.max(1, Math.round(abs / minute));
|
|
2612
|
+
return diff >= 0 ? `In ${minutes} minutes` : `${minutes} min ago`;
|
|
2613
|
+
}
|
|
2614
|
+
if (abs < day) {
|
|
2615
|
+
const hours = Math.max(1, Math.round(abs / hour));
|
|
2616
|
+
return diff >= 0 ? `In ${hours} hours` : `${hours} hr ago`;
|
|
2617
|
+
}
|
|
2618
|
+
return formatDashboardDateTime(value);
|
|
2619
|
+
}
|
|
1926
2620
|
</script>
|
|
2621
|
+
|
|
2622
|
+
<style scoped>
|
|
2623
|
+
/* ── Figma Panel Styles (Property & Security modes) ────────────────────────── */
|
|
2624
|
+
.dashboard-card {
|
|
2625
|
+
background: #fff;
|
|
2626
|
+
border: 1px solid #d9e0e7;
|
|
2627
|
+
border-radius: 8px;
|
|
2628
|
+
box-shadow: 0 1px 2px rgba(15, 35, 54, 0.04);
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
.dashboard-section-grid {
|
|
2632
|
+
margin-top: 14px;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
.dashboard-section-grid > .v-col {
|
|
2636
|
+
display: flex;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
.dashboard-panel {
|
|
2640
|
+
display: flex;
|
|
2641
|
+
flex-direction: column;
|
|
2642
|
+
height: 100%;
|
|
2643
|
+
min-height: 274px;
|
|
2644
|
+
padding: 28px 28px 26px;
|
|
2645
|
+
width: 100%;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
.figma-panel {
|
|
2649
|
+
border-color: #dde4ea;
|
|
2650
|
+
box-shadow: none;
|
|
2651
|
+
min-height: 276px;
|
|
2652
|
+
padding: 26px 28px 24px;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
.figma-panel-header {
|
|
2656
|
+
align-items: center;
|
|
2657
|
+
display: flex;
|
|
2658
|
+
gap: 16px;
|
|
2659
|
+
justify-content: space-between;
|
|
2660
|
+
min-height: 18px;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
.figma-panel-header h2 {
|
|
2664
|
+
color: #18324a;
|
|
2665
|
+
font-size: 12px;
|
|
2666
|
+
font-weight: 900;
|
|
2667
|
+
letter-spacing: 0;
|
|
2668
|
+
line-height: 1.2;
|
|
2669
|
+
margin: 0;
|
|
2670
|
+
text-transform: uppercase;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
.figma-panel-action,
|
|
2674
|
+
.figma-row-action {
|
|
2675
|
+
appearance: none;
|
|
2676
|
+
background: transparent;
|
|
2677
|
+
border: 0;
|
|
2678
|
+
color: #1976d2;
|
|
2679
|
+
cursor: pointer;
|
|
2680
|
+
font-size: 12px;
|
|
2681
|
+
font-weight: 800;
|
|
2682
|
+
line-height: 1;
|
|
2683
|
+
padding: 0;
|
|
2684
|
+
white-space: nowrap;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
.figma-panel-body {
|
|
2688
|
+
flex: 1;
|
|
2689
|
+
margin-top: 20px;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
.figma-list {
|
|
2693
|
+
display: grid;
|
|
2694
|
+
gap: 10px;
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
.figma-list-row {
|
|
2698
|
+
align-items: center;
|
|
2699
|
+
background: #fff;
|
|
2700
|
+
border: 1px solid #e1e7ed;
|
|
2701
|
+
border-radius: 6px;
|
|
2702
|
+
display: grid;
|
|
2703
|
+
gap: 10px;
|
|
2704
|
+
grid-template-columns: 34px minmax(0, 1fr) minmax(38px, auto);
|
|
2705
|
+
min-height: 52px;
|
|
2706
|
+
padding: 8px 12px;
|
|
2707
|
+
transition: border-color 0.16s ease, box-shadow 0.16s ease;
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
.figma-list-row:hover {
|
|
2711
|
+
border-color: #d5e0ea;
|
|
2712
|
+
box-shadow: 0 6px 14px rgba(15, 44, 69, 0.05);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
.figma-list-main {
|
|
2716
|
+
min-width: 0;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
.figma-list-main strong {
|
|
2720
|
+
color: #152f47;
|
|
2721
|
+
display: block;
|
|
2722
|
+
font-size: 12px;
|
|
2723
|
+
font-weight: 800;
|
|
2724
|
+
line-height: 1.25;
|
|
2725
|
+
overflow: hidden;
|
|
2726
|
+
text-overflow: ellipsis;
|
|
2727
|
+
white-space: nowrap;
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
.figma-list-main p {
|
|
2731
|
+
color: #7b8895;
|
|
2732
|
+
font-size: 11px;
|
|
2733
|
+
font-weight: 700;
|
|
2734
|
+
line-height: 1.3;
|
|
2735
|
+
margin: 3px 0 0;
|
|
2736
|
+
overflow: hidden;
|
|
2737
|
+
text-overflow: ellipsis;
|
|
2738
|
+
white-space: nowrap;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
.figma-row-action {
|
|
2742
|
+
justify-self: end;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
.figma-event-dot {
|
|
2746
|
+
align-self: center;
|
|
2747
|
+
border-radius: 50%;
|
|
2748
|
+
display: inline-block;
|
|
2749
|
+
height: 20px;
|
|
2750
|
+
justify-self: center;
|
|
2751
|
+
width: 20px;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
.figma-event-dot-info,
|
|
2755
|
+
.figma-event-dot-neutral {
|
|
2756
|
+
background: #c9e6ff;
|
|
2757
|
+
border: 2px solid #77bef9;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
.figma-event-dot-success {
|
|
2761
|
+
background: #2fb84d;
|
|
2762
|
+
border: 2px solid #c9efd4;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
.figma-event-dot-danger {
|
|
2766
|
+
background: #ffd9d6;
|
|
2767
|
+
border: 2px solid #f9a6a0;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
.figma-event-dot-warning {
|
|
2771
|
+
background: #ffe9c7;
|
|
2772
|
+
border: 2px solid #f9c97c;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
.figma-icon-badge {
|
|
2776
|
+
align-items: center;
|
|
2777
|
+
border-radius: 50%;
|
|
2778
|
+
display: inline-flex;
|
|
2779
|
+
height: 28px;
|
|
2780
|
+
justify-content: center;
|
|
2781
|
+
justify-self: center;
|
|
2782
|
+
width: 28px;
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
.figma-icon-badge-danger { background: #ffe0de; color: #d72f2f; }
|
|
2786
|
+
.figma-icon-badge-info { background: #e8e2ff; color: #6d4bd8; }
|
|
2787
|
+
.figma-icon-badge-success { background: #ddf7e6; color: #229852; }
|
|
2788
|
+
.figma-icon-badge-warning { background: #ffeed4; color: #c57916; }
|
|
2789
|
+
.figma-icon-badge-neutral { background: #edf2f6; color: #607386; }
|
|
2790
|
+
|
|
2791
|
+
.figma-status-stack {
|
|
2792
|
+
display: grid;
|
|
2793
|
+
gap: 22px;
|
|
2794
|
+
padding: 6px 2px 0;
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
.figma-status-row {}
|
|
2798
|
+
|
|
2799
|
+
.figma-status-label {
|
|
2800
|
+
align-items: center;
|
|
2801
|
+
color: #263d53;
|
|
2802
|
+
display: flex;
|
|
2803
|
+
font-size: 12px;
|
|
2804
|
+
font-weight: 800;
|
|
2805
|
+
justify-content: space-between;
|
|
2806
|
+
line-height: 1.25;
|
|
2807
|
+
margin-bottom: 10px;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
.figma-status-label strong {
|
|
2811
|
+
color: #18324a;
|
|
2812
|
+
font-size: 12px;
|
|
2813
|
+
font-weight: 900;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
.figma-progress-track {
|
|
2817
|
+
background: #edf2f6;
|
|
2818
|
+
border-radius: 999px;
|
|
2819
|
+
height: 6px;
|
|
2820
|
+
overflow: hidden;
|
|
2821
|
+
width: 100%;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
.figma-progress-fill {
|
|
2825
|
+
border-radius: inherit;
|
|
2826
|
+
display: block;
|
|
2827
|
+
height: 100%;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
/* ── Property Feedback List ────────────────────────────────────────────────── */
|
|
2831
|
+
.feedback-list {
|
|
2832
|
+
display: grid;
|
|
2833
|
+
gap: 9px;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
.feedback-row {
|
|
2837
|
+
align-items: center;
|
|
2838
|
+
background: #fff;
|
|
2839
|
+
border: 1px solid #e0e5ea;
|
|
2840
|
+
border-radius: 8px;
|
|
2841
|
+
display: grid;
|
|
2842
|
+
gap: 16px;
|
|
2843
|
+
grid-template-columns: minmax(180px, 225px) minmax(130px, 175px) 100px auto;
|
|
2844
|
+
min-height: 66px;
|
|
2845
|
+
padding: 12px 14px;
|
|
2846
|
+
transition: border-color 0.16s ease, box-shadow 0.16s ease;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
.feedback-row:hover {
|
|
2850
|
+
border-color: #d8e3ec;
|
|
2851
|
+
box-shadow: 0 6px 14px rgba(15, 44, 69, 0.04);
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
.feedback-summary { min-width: 0; }
|
|
2855
|
+
|
|
2856
|
+
.feedback-summary strong {
|
|
2857
|
+
color: #132b43;
|
|
2858
|
+
display: block;
|
|
2859
|
+
font-size: 12px;
|
|
2860
|
+
font-weight: 800;
|
|
2861
|
+
line-height: 1.25;
|
|
2862
|
+
overflow: hidden;
|
|
2863
|
+
text-overflow: ellipsis;
|
|
2864
|
+
white-space: nowrap;
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
.feedback-summary p {
|
|
2868
|
+
color: #888;
|
|
2869
|
+
font-size: 12px;
|
|
2870
|
+
font-weight: 500;
|
|
2871
|
+
line-height: 1.35;
|
|
2872
|
+
margin: 5px 0 0;
|
|
2873
|
+
overflow: hidden;
|
|
2874
|
+
text-overflow: ellipsis;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
.feedback-person {
|
|
2878
|
+
align-items: center;
|
|
2879
|
+
display: flex;
|
|
2880
|
+
gap: 8px;
|
|
2881
|
+
min-width: 0;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
.feedback-person span {
|
|
2885
|
+
font-size: 13px;
|
|
2886
|
+
overflow: hidden;
|
|
2887
|
+
text-overflow: ellipsis;
|
|
2888
|
+
white-space: nowrap;
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
.feedback-avatar {
|
|
2892
|
+
align-items: center;
|
|
2893
|
+
background: #d1e8ff;
|
|
2894
|
+
border-radius: 50%;
|
|
2895
|
+
color: #1d5f99;
|
|
2896
|
+
display: inline-flex;
|
|
2897
|
+
flex-shrink: 0;
|
|
2898
|
+
font-size: 11px;
|
|
2899
|
+
font-weight: 800;
|
|
2900
|
+
height: 30px;
|
|
2901
|
+
justify-content: center;
|
|
2902
|
+
width: 30px;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
.feedback-status-pill {
|
|
2906
|
+
border-radius: 20px;
|
|
2907
|
+
font-size: 12px;
|
|
2908
|
+
font-weight: 600;
|
|
2909
|
+
padding: 4px 10px;
|
|
2910
|
+
text-align: center;
|
|
2911
|
+
white-space: nowrap;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
.feedback-status-pill-completed { background: #72e3a1; color: #0f5132; }
|
|
2915
|
+
.feedback-status-pill-progress { background: #fde68a; color: #92400e; }
|
|
2916
|
+
.feedback-status-pill-danger { background: #fecaca; color: #b91c1c; }
|
|
2917
|
+
.feedback-status-pill-todo { background: #e2e8f0; color: #64748b; }
|
|
2918
|
+
|
|
2919
|
+
.feedback-view-button {
|
|
2920
|
+
appearance: none;
|
|
2921
|
+
background: transparent;
|
|
2922
|
+
border: 0;
|
|
2923
|
+
color: #2196f3;
|
|
2924
|
+
cursor: pointer;
|
|
2925
|
+
font-size: 12px;
|
|
2926
|
+
font-weight: 800;
|
|
2927
|
+
justify-self: end;
|
|
2928
|
+
line-height: 1;
|
|
2929
|
+
padding: 0;
|
|
2930
|
+
white-space: nowrap;
|
|
2931
|
+
}
|
|
2932
|
+
</style>
|