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