@7365admin1/layer-common 3.0.8 → 3.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/components/DashboardMain.vue +1894 -844
  3. package/components/EntryPassInformation.vue +60 -0
  4. package/components/HidAccessLogDashboard.vue +1691 -0
  5. package/components/HidEnabledGate.vue +51 -0
  6. package/components/HidIdentityMapping.vue +975 -0
  7. package/components/HidQrCodeConfiguration.vue +618 -0
  8. package/components/HidReaderForm.vue +246 -0
  9. package/components/HidReaderManagement.vue +1233 -0
  10. package/components/HidServiceSettingsPanel.vue +150 -0
  11. package/components/HidUserEnrollment.vue +1274 -0
  12. package/components/Layout/NavigationDrawer.vue +31 -1
  13. package/components/NavigationItem.vue +11 -4
  14. package/components/SiteSettings.vue +24 -1
  15. package/components/VisitorForm.vue +195 -4
  16. package/components/VisitorFormSelection.vue +12 -2
  17. package/composables/useDashboard.ts +29 -2
  18. package/composables/useHidAmico.ts +152 -0
  19. package/composables/useHidNavigation.ts +106 -0
  20. package/composables/useMember.ts +7 -1
  21. package/composables/useOptionalServices.ts +118 -0
  22. package/composables/useSiteSettings.ts +1 -1
  23. package/composables/useUser.ts +1 -2
  24. package/nuxt.config.ts +1 -1
  25. package/package.json +2 -1
  26. package/pages/[org]/[site]/access-mgmt/access-logs/index.vue +21 -0
  27. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +21 -0
  28. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +21 -0
  29. package/pages/[org]/[site]/access-mgmt/hid-users/index.vue +21 -0
  30. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +21 -0
  31. 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
- size="x-small"
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
- class="text-h5 font-weight-black text-blue-grey-darken-4"
163
- >{{ card.value }}</span
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
- <v-row dense class="mt-3 align-stretch">
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
- &bull; </template
337
+ }}<template v-if="item.subtitle && item.time"> &bull; </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(staffStatusRoute)"
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 v-for="item in feedbackItems" :key="item.id" class="d-flex align-center justify-space-between" 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);">
439
- <div style="flex: 1; min-width: 140px;">
440
- <strong style="display: block; font-size: 15px; font-weight: 700; color: #0b2f47; margin-bottom: 4px;">{{ item.title }}</strong>
441
- <span style="font-size: 13px; color: #94a3b8;">{{ item.subtitle }}</span>
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;">{{ item.person }}</span>
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 v-if="getStatusTone(item.status) === 'success'" style="background-color: #72e3a1; color: #0f5132; padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; text-transform: capitalize;">
451
- {{ item.status }}
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
- <div v-else-if="getStatusTone(item.status) === 'warning'" style="background-color: #fde68a; color: #92400e; padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; text-transform: capitalize;">
454
- {{ item.status }}
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
- <div v-else-if="getStatusTone(item.status) === 'error'" style="background-color: #fecaca; color: #b91c1c; padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; text-transform: capitalize;">
457
- {{ item.status }}
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
- <div v-else style="background-color: #e2e8f0; color: #64748b; padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; text-transform: capitalize;">
460
- {{ item.status || 'To-Do' }}
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 variant="outlined" class="text-none font-weight-medium" size="small" color="#3b82f6" style="border-color: #e2e8f0; border-radius: 8px; padding: 0 16px; height: 32px;" @click="goToListItem(item, 'feedbacks')">View</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
- raw?: Record<string, any>;
1006
+ color?: string;
1007
+ actionLabel?: string;
609
1008
  };
610
1009
 
611
1010
  type TaskItem = {
@@ -636,132 +1035,49 @@ type FeedbackItem = {
636
1035
  raw?: Record<string, any>;
637
1036
  };
638
1037
 
1038
+ // ─── Props ────────────────────────────────────────────────────────────────────
1039
+
639
1040
  const props = defineProps({
640
- orgId: {
641
- type: String,
642
- default: "",
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
- type: String,
674
- default: "",
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 defaultCardValues: TDashboardCardValue[] = [
687
- {
688
- key: "openWorkOrder",
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
- const resolvedExtraWidgetKeys = computed(() => {
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 periodValue: Record<string, TDashboardValues> = {
757
- Today: "today",
758
- "This Week": "thisWeek",
759
- "This Month": "thisMonth",
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
- const currentPeriod = ref<TDashboardValues>("today");
1077
+ // ─── Composables ──────────────────────────────────────────────────────────────
763
1078
 
764
1079
  const {
1080
+ getDashboard,
765
1081
  getDashboardHygiene,
766
1082
  getDashboardHygieneWeeklyActivity,
767
1083
  getDashboardHygieneTaskSchedule,
@@ -769,30 +1085,99 @@ const {
769
1085
  getDashboardHygieneFeedbacks,
770
1086
  } = useDashboard();
771
1087
 
772
- const { data: getDashboardReq, pending: loadingMetrics } =
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 { data: weeklyActivityReq, pending: loadingWeeklyActivity } =
789
- await useLazyAsyncData(
790
- `get-dashboard-weekly-activity-${props.site}-${props.serviceType}`,
791
- () =>
792
- props.serviceType
793
- ? getDashboardHygieneWeeklyActivity(
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 (hygiene mode only)
1173
+ const { data: weeklyActivityReq, pending: loadingWeeklyActivity } =
1174
+ await useLazyAsyncData(
1175
+ `get-dashboard-weekly-activity-${props.site}-${props.serviceType}`,
1176
+ () =>
1177
+ props.serviceType && isHygieneMode.value
1178
+ ? getDashboardHygieneWeeklyActivity(
794
1179
  props.site,
795
- props.serviceType,
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(props.site, props.serviceType)
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(props.site, props.serviceType)
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(props.site, props.serviceType)
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,174 +1250,251 @@ const finalLoading = computed(() => {
847
1250
  loadingFeedbacks.value
848
1251
  );
849
1252
  }
850
- return props.loading;
1253
+ if (isPropertyMode.value) {
1254
+ return loadingAltMetrics.value || loadingFeedbacks.value;
1255
+ }
1256
+ // security
1257
+ return loadingAltMetrics.value || loadingAttendance.value || loadingFeedbacks.value;
851
1258
  });
852
1259
 
853
- const finalDashboardData = computed(() => {
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
- let weeklyActivity = weeklyActivityReq.value ?? [];
858
- const hasNoActivity =
859
- !weeklyActivity.length ||
860
- weeklyActivity.every(
861
- (item: any) =>
862
- !item.workOrder &&
863
- !item.taskCompleted &&
864
- !item.openWorkOrder &&
865
- !item.closedWorkOrder
866
- );
1265
+ if (isHygieneMode.value) {
1266
+ const data = getDashboardReq.value ?? {};
1267
+ let weeklyActivity = weeklyActivityReq.value ?? [];
1268
+ const hasNoActivity =
1269
+ !weeklyActivity.length ||
1270
+ weeklyActivity.every(
1271
+ (item: any) =>
1272
+ !item.workOrder &&
1273
+ !item.taskCompleted &&
1274
+ !item.openWorkOrder &&
1275
+ !item.closedWorkOrder
1276
+ );
1277
+
1278
+ if (hasNoActivity && currentPeriod.value === "thisWeek") {
1279
+ weeklyActivity = [
1280
+ { day: "Mon", workOrder: 800, taskCompleted: 1400 },
1281
+ { day: "Tue", workOrder: 450, taskCompleted: 900 },
1282
+ { day: "Wed", workOrder: 950, taskCompleted: 1640 },
1283
+ { day: "Thu", workOrder: 2000, taskCompleted: 2100 },
1284
+ { day: "Fri", workOrder: 2000, taskCompleted: 2700 },
1285
+ { day: "Sat", workOrder: 2200, taskCompleted: 3100 },
1286
+ { day: "Sun", workOrder: 2800, taskCompleted: 3200 },
1287
+ ];
1288
+ }
867
1289
 
868
- if (hasNoActivity && currentPeriod.value === "thisWeek") {
869
- weeklyActivity = [
870
- { day: "Mon", workOrder: 800, taskCompleted: 1400 },
871
- { day: "Tue", workOrder: 450, taskCompleted: 900 },
872
- { day: "Wed", workOrder: 950, taskCompleted: 1640 },
873
- { day: "Thu", workOrder: 2000, taskCompleted: 2100 },
874
- { day: "Fri", workOrder: 2000, taskCompleted: 2700 },
875
- { day: "Sat", workOrder: 2200, taskCompleted: 3100 },
876
- { day: "Sun", workOrder: 2800, taskCompleted: 3200 },
877
- ];
1290
+ return {
1291
+ ...data,
1292
+ openWorkOrder: data.openWorkOrder ?? { count: 0, percentage: 0 },
1293
+ supplyAlert: data.supplyAlert ?? { count: 0, percentage: 0 },
1294
+ taskCompleted: data.taskCompleted ?? { count: 0, percentage: 0 },
1295
+ activityChart: {
1296
+ labels: weeklyActivity.map((item: any) => item.day || item.label),
1297
+ workOrders: weeklyActivity.map(
1298
+ (item: any) => item.workOrder ?? item.openWorkOrder
1299
+ ),
1300
+ taskCompleted: weeklyActivity.map(
1301
+ (item: any) => item.taskCompleted ?? item.closedWorkOrder
1302
+ ),
1303
+ },
1304
+ taskScheduleItems: taskScheduleReq.value?.items ?? [],
1305
+ attendanceItems: attendanceReq.value?.items ?? [],
1306
+ feedbackItems: feedbacksReq.value?.items ?? [],
1307
+ };
1308
+ }
1309
+
1310
+ if (isPropertyMode.value) {
1311
+ const data = getDashboardAltReq.value ?? {};
1312
+ return {
1313
+ ...data,
1314
+ feedbackItems: feedbacksReq.value?.items ?? [],
1315
+ openWorkOrder: data.openWorkOrder ?? { count: 0, inProgress: 0, percentage: 0 },
1316
+ openIncidents: data.openIncidents ?? { count: 0, highSeverity: 0, percentage: 0 },
1317
+ visitors: data.visitors ?? { count: 0, currentlyOnSite: 0, percentage: 0 },
1318
+ facilityBooking: data.facilityBooking ?? {
1319
+ count: 0,
1320
+ ongoing: 0,
1321
+ waitingApproval: 0,
1322
+ percentage: 0,
1323
+ },
1324
+ };
878
1325
  }
879
1326
 
1327
+ // Security
1328
+ const data = getDashboardAltReq.value ?? {};
880
1329
  return {
881
1330
  ...data,
1331
+ feedbackItems: feedbacksReq.value?.items ?? [],
1332
+ attendanceItems: attendanceReq.value?.items ?? [],
882
1333
  openWorkOrder: data.openWorkOrder ?? {
883
1334
  count: 0,
1335
+ inProgress: 0,
884
1336
  percentage: 0,
1337
+ overdue: 0,
885
1338
  },
886
- supplyAlert: data.supplyAlert ?? {
1339
+ openIncidents: data.openIncidents ?? {
887
1340
  count: 0,
1341
+ highSeverity: 0,
888
1342
  percentage: 0,
889
1343
  },
890
- taskCompleted: data.taskCompleted ?? {
1344
+ visitors: data.visitors ?? { count: 0, currentlyOnSite: 0, percentage: 0 },
1345
+ patrolCompliance: data.patrolCompliance ?? {
891
1346
  count: 0,
1347
+ missedCheckpoints: 0,
1348
+ missed: 0,
892
1349
  percentage: 0,
893
1350
  },
894
- landscapeSchedule: data.landscapeSchedule ?? {
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 ?? [],
1351
+ activePatrolItems: data.activePatrolItems ?? [],
920
1352
  };
921
1353
  });
922
1354
 
923
- const emit = defineEmits<{
924
- updatePeriod: [payload: { key: string; period: string }];
925
- }>();
1355
+ // ─── Card Value Configs ───────────────────────────────────────────────────────
926
1356
 
927
- const router = useRouter();
928
- const runtimeConfig = useRuntimeConfig();
929
- const customizeDialog = ref(false);
930
- const widgetSearch = ref("");
931
- const selectedRange = ref("Today");
932
- const visitorType = ref("All");
933
- const visibleWidgetKeys = ref<string[]>([]);
934
- const draftVisibleWidgetKeys = ref<string[]>([]);
935
- const filterMenus = reactive<Record<string, boolean>>({});
936
- const snackbar = reactive({
937
- visible: false,
938
- text: "",
939
- });
1357
+ const defaultCardValues: TDashboardCardValue[] = [
1358
+ {
1359
+ key: "openWorkOrder",
1360
+ periodKey: "period",
1361
+ title: "Open Work Order",
1362
+ icon: "mdi-briefcase",
1363
+ color: "#0b2f47",
1364
+ },
1365
+ {
1366
+ key: "taskCompleted",
1367
+ periodKey: "period",
1368
+ title: "Task Completed",
1369
+ icon: "mdi-clipboard-check-outline",
1370
+ color: "#f4a340",
1371
+ },
1372
+ {
1373
+ key: "supplyAlert",
1374
+ periodKey: "period",
1375
+ title: "Supply Alert",
1376
+ icon: "mdi-alert",
1377
+ color: "#d32f2f",
1378
+ },
1379
+ ];
940
1380
 
941
- const chartContainerRef = ref<HTMLElement | null>(null);
942
- const chartWidth = ref(920);
943
- let resizeObserver: ResizeObserver | null = null;
1381
+ const propertyCardValues: TDashboardCardValue[] = [
1382
+ {
1383
+ key: "workOrders",
1384
+ periodKey: "period",
1385
+ title: "Open Work Order",
1386
+ icon: "mdi-briefcase",
1387
+ color: "#1d5f99",
1388
+ },
1389
+ {
1390
+ key: "incidents",
1391
+ periodKey: "period",
1392
+ title: "Open Incidents",
1393
+ icon: "mdi-alert",
1394
+ color: "#b73535",
1395
+ },
1396
+ {
1397
+ key: "visitors",
1398
+ periodKey: "period",
1399
+ title: "Visitors",
1400
+ icon: "mdi-account-group",
1401
+ color: "#d28722",
1402
+ },
1403
+ {
1404
+ key: "facilityBookings",
1405
+ periodKey: "period",
1406
+ title: "Facility Booking",
1407
+ icon: "mdi-domain",
1408
+ color: "#23824f",
1409
+ },
1410
+ ];
944
1411
 
945
- watch(chartContainerRef, (el) => {
946
- if (resizeObserver) {
947
- resizeObserver.disconnect();
948
- }
949
- if (el && window.ResizeObserver) {
950
- resizeObserver = new ResizeObserver((entries) => {
951
- if (entries[0] && entries[0].contentRect.width > 0) {
952
- chartWidth.value = entries[0].contentRect.width;
953
- }
954
- });
955
- resizeObserver.observe(el);
956
- }
957
- });
1412
+ const securityCardValues: TDashboardCardValue[] = [
1413
+ {
1414
+ key: "workOrders",
1415
+ periodKey: "period",
1416
+ title: "Open Work Order",
1417
+ icon: "mdi-briefcase",
1418
+ color: "#1d5f99",
1419
+ },
1420
+ {
1421
+ key: "incidents",
1422
+ periodKey: "period",
1423
+ title: "Open Incidents",
1424
+ icon: "mdi-alert",
1425
+ color: "#b73535",
1426
+ },
1427
+ {
1428
+ key: "visitors",
1429
+ periodKey: "period",
1430
+ title: "Visitors",
1431
+ icon: "mdi-account-group",
1432
+ color: "#d28722",
1433
+ },
1434
+ {
1435
+ key: "patrolCompliance",
1436
+ periodKey: "period",
1437
+ title: "Patrol Compliance",
1438
+ icon: "mdi-calculator-variant",
1439
+ color: "#23824f",
1440
+ },
1441
+ ];
958
1442
 
959
- onUnmounted(() => {
960
- if (resizeObserver) resizeObserver.disconnect();
1443
+ const resolvedCardValues = computed(() => {
1444
+ if (props.cardValues && props.cardValues.length > 0) return props.cardValues;
1445
+ if (isPropertyMode.value) return propertyCardValues;
1446
+ if (isSecurityMode.value) return securityCardValues;
1447
+ return defaultCardValues;
961
1448
  });
962
1449
 
963
- const rangeOptions = [
964
- { label: "Today", value: "Today" },
965
- { label: "7d", value: "This Week" },
966
- { label: "30d", value: "This Month" },
1450
+ // ─── Widget Keys by Mode ──────────────────────────────────────────────────────
1451
+
1452
+ const defaultExtraWidgetKeys = [
1453
+ "activity",
1454
+ "taskSchedule",
1455
+ "staffStatus",
1456
+ "feedbacks",
1457
+ "recentActivity",
967
1458
  ];
968
- const defaultVisitorOptions = [
969
- "All",
970
- "Drive-In",
971
- "Walk-In",
972
- "Pick-up",
973
- "Drop-Off",
974
- "Delivery",
975
- "Contractor",
1459
+
1460
+ const propertyDefaultWidgetKeys = [
1461
+ "workOrders",
1462
+ "incidents",
1463
+ "visitors",
1464
+ "facilityBookings",
1465
+ "activity",
1466
+ "upcomingEvents",
1467
+ "todayReminders",
1468
+ "todayAttentions",
1469
+ "workOrderStatus",
1470
+ "feedbacks",
976
1471
  ];
977
1472
 
978
- const dashboardTitle = computed(() => {
979
- if (props.title) return props.title;
980
- const appName = String(
981
- runtimeConfig.public.APP_NAME || runtimeConfig.public.APP || ""
982
- );
983
- return `${getServiceDisplayName(appName)} - Dashboard`;
984
- });
985
- const appKey = computed(() =>
986
- String(
987
- runtimeConfig.public.APP || runtimeConfig.public.APP_NAME || ""
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
- );
1473
+ const securityDefaultWidgetKeys = [
1474
+ "workOrders",
1475
+ "incidents",
1476
+ "visitors",
1477
+ "patrolCompliance",
1478
+ "activity",
1479
+ "activePatrol",
1480
+ "staffStatus",
1481
+ "feedbacks",
1482
+ ];
1013
1483
 
1014
- const defaultVisibleWidgetKeys = computed(() => [
1015
- ...resolvedCardValues.value.map((card) => card.key),
1016
- ...resolvedExtraWidgetKeys.value,
1017
- ]);
1484
+ const defaultVisibleWidgetKeys = computed(() => {
1485
+ if (props.extraWidgetKeys && props.extraWidgetKeys.length > 0) {
1486
+ return [
1487
+ ...resolvedCardValues.value.map((c) => c.key),
1488
+ ...props.extraWidgetKeys,
1489
+ ];
1490
+ }
1491
+ if (isPropertyMode.value) return propertyDefaultWidgetKeys;
1492
+ if (isSecurityMode.value) return securityDefaultWidgetKeys;
1493
+ return [
1494
+ ...resolvedCardValues.value.map((c) => c.key),
1495
+ ...defaultExtraWidgetKeys,
1496
+ ];
1497
+ });
1018
1498
 
1019
1499
  watch(
1020
1500
  defaultVisibleWidgetKeys,
@@ -1025,83 +1505,269 @@ watch(
1025
1505
  draftVisibleWidgetKeys.value = nextKeys;
1026
1506
  return;
1027
1507
  }
1028
-
1029
- visibleWidgetKeys.value = visibleWidgetKeys.value.filter((key) =>
1030
- nextKeys.includes(key)
1508
+ visibleWidgetKeys.value = visibleWidgetKeys.value.filter((k) =>
1509
+ nextKeys.includes(k)
1031
1510
  );
1032
- draftVisibleWidgetKeys.value = draftVisibleWidgetKeys.value.filter((key) =>
1033
- nextKeys.includes(key)
1511
+ draftVisibleWidgetKeys.value = draftVisibleWidgetKeys.value.filter((k) =>
1512
+ nextKeys.includes(k)
1034
1513
  );
1035
1514
  },
1036
1515
  { immediate: true }
1037
1516
  );
1038
1517
 
1039
- watch(selectedRange, (period) => {
1040
- if (props.serviceType) {
1041
- const apiPeriod = periodValue[period];
1042
- if (apiPeriod) currentPeriod.value = apiPeriod;
1518
+ // ─── Customize Widgets Config ─────────────────────────────────────────────────
1519
+
1520
+ const customizeWidgets = computed(() => {
1521
+ if (isPropertyMode.value) {
1522
+ return [
1523
+ { key: "workOrders", title: "Open Work Orders", description: "Track active work orders, overdue items, and what's in progress." },
1524
+ { key: "incidents", title: "Open Incidents", description: "Monitor reported incidents and flag high severity cases." },
1525
+ { key: "visitors", title: "Visitors", description: "See today's visitor traffic and who is currently on site." },
1526
+ { key: "facilityBookings", title: "Facility Bookings", description: "View booking volume and approvals waiting for action." },
1527
+ { key: "activity", title: "This Week Activity (Line Chart)", description: "Compare incidents and work orders over time." },
1528
+ { key: "upcomingEvents", title: "Upcoming Events", description: "Quick view of scheduled events happening soon." },
1529
+ { key: "todayReminders", title: "Today's Reminder", description: "Your next tasks and time sensitive alerts for today." },
1530
+ { key: "todayAttentions", title: "Today's Attentions", description: "Items that require review overdue, pending, or unacknowledged." },
1531
+ { key: "workOrderStatus", title: "Work Order Status (Progress Bar)", description: "Breakdown of work orders by status." },
1532
+ { key: "feedbacks", title: "Feedbacks", description: "Recent feedback items and current resolution status." },
1533
+ ];
1043
1534
  }
1044
- emit("updatePeriod", { key: "__all__", period });
1535
+ if (isSecurityMode.value) {
1536
+ return [
1537
+ { key: "workOrders", title: "Open Work Orders", description: "Track active work orders, overdue items, and what's in progress." },
1538
+ { key: "incidents", title: "Open Incidents", description: "Monitor reported incidents and flag high severity cases." },
1539
+ { key: "visitors", title: "Visitors", description: "See today's visitor traffic and who is currently on site." },
1540
+ { key: "patrolCompliance", title: "Patrol Compliance", description: "View missed checkpoints and ongoing patrols." },
1541
+ { key: "activity", title: "This Week Activity (Line Chart)", description: "Compare incidents and work orders over time." },
1542
+ { key: "activePatrol", title: "Active Patrol", description: "Quick view of scheduled active patrol routes happening soon." },
1543
+ { key: "staffStatus", title: "Staff Status", description: "Current attendance and status of staff." },
1544
+ { key: "feedbacks", title: "Feedbacks", description: "Recent feedback and resolution status." },
1545
+ ];
1546
+ }
1547
+ // Hygiene / others
1548
+ return [
1549
+ ...resolvedCardValues.value.map((card) => ({
1550
+ key: card.key,
1551
+ title: card.title,
1552
+ description: `Show ${card.title} metrics for the selected period.`,
1553
+ })),
1554
+ { key: "activity", title: "Activity Chart", description: "Compare activity trends over time." },
1555
+ { key: "taskSchedule", title: primaryListConfig.value.title, description: "Show current tasks and their latest status." },
1556
+ { key: "staffStatus", title: "Staff Total Attendance", description: "See who is on duty, on break, or absent." },
1557
+ { key: "feedbacks", title: "Feedbacks", description: "Show recent feedback and resolution state." },
1558
+ { key: "recentActivity", title: "Recent Activity", description: "Latest updates across the service." },
1559
+ ];
1045
1560
  });
1046
1561
 
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
1562
  const filteredCustomizeWidgets = computed(() => {
1081
1563
  const search = widgetSearch.value.trim().toLowerCase();
1082
1564
  if (!search) return customizeWidgets.value;
1083
-
1084
1565
  return customizeWidgets.value.filter(
1085
- (widget) =>
1086
- widget.title.toLowerCase().includes(search) ||
1087
- widget.description.toLowerCase().includes(search)
1566
+ (w) =>
1567
+ w.title.toLowerCase().includes(search) ||
1568
+ w.description.toLowerCase().includes(search)
1088
1569
  );
1089
1570
  });
1571
+
1572
+ // ─── KPI Metrics ─────────────────────────────────────────────────────────────
1573
+
1574
+ const defaultVisitorOptions = [
1575
+ "All", "Drive-In", "Walk-In", "Pick-up", "Drop-Off", "Delivery", "Contractor",
1576
+ ];
1577
+
1090
1578
  const visitorItems = computed(() =>
1091
1579
  getDashboardArray(["visitorItems", "visitorsList", "visitorTransactionsList"])
1092
1580
  );
1093
1581
  const visitorOptions = computed(() =>
1094
- buildFilterOptions(
1095
- defaultVisitorOptions,
1096
- visitorItems.value,
1097
- getVisitorFilterValue
1098
- )
1582
+ buildFilterOptions(defaultVisitorOptions, visitorItems.value, getVisitorFilterValue)
1099
1583
  );
1100
1584
 
1101
- const kpiCards = computed(() =>
1102
- resolvedCardValues.value.map((mapping, index) => {
1585
+ // Property-specific: facility booking metric with client-side filter
1586
+ const facilityBookingItems = computed(() =>
1587
+ getDashboardArray([
1588
+ "facilityBookingItems",
1589
+ "facilityBookingsList",
1590
+ "bookingsList",
1591
+ ])
1592
+ );
1593
+ const defaultFacilityOptions = ["All"];
1594
+ const facilityOptions = computed(() =>
1595
+ buildFilterOptions(
1596
+ defaultFacilityOptions,
1597
+ facilityBookingItems.value,
1598
+ (item: any) =>
1599
+ item.facilityName || item.facility?.name || item.facilityInfo?.name || item.name || ""
1600
+ )
1601
+ );
1602
+
1603
+ function getPropertyKpiCards() {
1604
+ const workOrderMetric = getMetric(["openWorkOrder", "workOrders", "openWorkOrders", "workOrder"]);
1605
+ const incidentMetric = getMetric(["openIncidents", "incidents", "incidentReports"]);
1606
+ const visitorMetric = getMetric(["visitors", "visitorTransactions"]);
1607
+ const facilityMetric = computeFacilityMetric();
1608
+
1609
+ return [
1610
+ {
1611
+ key: "workOrders",
1612
+ title: "Open Work Order",
1613
+ icon: "mdi-briefcase",
1614
+ color: "#1d5f99",
1615
+ tint: "#d7ecff",
1616
+ value: formatNumber(toNumber(workOrderMetric?.count)),
1617
+ percentage: toNumber(workOrderMetric?.percentage),
1618
+ meta: `${toNumber(workOrderMetric?.inProgress)} in progress`,
1619
+ raw: workOrderMetric,
1620
+ filter: undefined,
1621
+ },
1622
+ {
1623
+ key: "incidents",
1624
+ title: "Open Incidents",
1625
+ icon: "mdi-alert",
1626
+ color: "#b73535",
1627
+ tint: "#ffdede",
1628
+ value: formatNumber(toNumber(incidentMetric?.count)),
1629
+ percentage: toNumber(incidentMetric?.percentage),
1630
+ meta: `${toNumber(incidentMetric?.highSeverity)} high severity`,
1631
+ raw: incidentMetric,
1632
+ filter: undefined,
1633
+ },
1634
+ {
1635
+ key: "visitors",
1636
+ title: "Visitors",
1637
+ icon: "mdi-account-group",
1638
+ color: "#d28722",
1639
+ tint: "#ffe4bd",
1640
+ value: formatNumber(toNumber(visitorMetric?.count)),
1641
+ percentage: toNumber(visitorMetric?.percentage),
1642
+ meta: `${toNumber(visitorMetric?.currentlyOnSite)} currently on site`,
1643
+ raw: visitorMetric,
1644
+ filter: {
1645
+ model: visitorType.value,
1646
+ options: visitorOptions.value,
1647
+ set: (v: string) => {
1648
+ visitorType.value = v;
1649
+ filterMenus.visitors = false;
1650
+ },
1651
+ },
1652
+ },
1653
+ {
1654
+ key: "facilityBookings",
1655
+ title: "Facility Booking",
1656
+ icon: "mdi-domain",
1657
+ color: "#23824f",
1658
+ tint: "#c8f3da",
1659
+ value: formatNumber(toNumber(facilityMetric?.count)),
1660
+ percentage: toNumber(facilityMetric?.percentage),
1661
+ meta: `${toNumber(facilityMetric?.ongoing)} ongoing - ${toNumber(facilityMetric?.waitingApproval)} waiting approval`,
1662
+ raw: facilityMetric,
1663
+ filter: {
1664
+ model: facilityType.value,
1665
+ options: facilityOptions.value,
1666
+ set: (v: string) => {
1667
+ facilityType.value = v;
1668
+ filterMenus.facilityBookings = false;
1669
+ },
1670
+ },
1671
+ },
1672
+ ];
1673
+ }
1674
+
1675
+ function computeFacilityMetric(): DashboardMetric {
1676
+ const metric = getMetric(["facilityBooking", "facilityBookings", "bookings"]);
1677
+ const selected = facilityType.value;
1678
+ if (selected === "All") return metric;
1679
+ const items = facilityBookingItems.value.filter((item: any) => {
1680
+ const val = String(
1681
+ item.facilityName || item.facility?.name || item.facilityInfo?.name || item.name || ""
1682
+ ).toLowerCase();
1683
+ return val === selected.toLowerCase();
1684
+ });
1685
+ return {
1686
+ ...metric,
1687
+ count: items.length,
1688
+ ongoing: items.filter((i: any) => {
1689
+ const s = String(i.status || "").toLowerCase();
1690
+ return s.includes("ongoing") || s.includes("approved") || s.includes("in-progress");
1691
+ }).length,
1692
+ waitingApproval: items.filter((i: any) => {
1693
+ const s = String(i.status || "").toLowerCase();
1694
+ return s.includes("pending") || s.includes("review") || s.includes("approval");
1695
+ }).length,
1696
+ };
1697
+ }
1698
+
1699
+ function getSecurityKpiCards() {
1700
+ const workOrderMetric = getMetric(["openWorkOrder", "workOrders", "openWorkOrders", "workOrder"]);
1701
+ const incidentMetric = getMetric(["openIncidents", "incidents", "incidentReports"]);
1702
+ const patrolMetric = getMetric(["patrolCompliance", "patrol"]);
1703
+
1704
+ return [
1705
+ {
1706
+ key: "workOrders",
1707
+ title: "Open Work Order",
1708
+ icon: "mdi-briefcase",
1709
+ color: "#1d5f99",
1710
+ tint: "#d7ecff",
1711
+ value: formatNumber(toNumber(workOrderMetric?.count)),
1712
+ percentage: toNumber(workOrderMetric?.percentage),
1713
+ meta: `${toNumber(workOrderMetric?.overdue)} overdue · ${toNumber(workOrderMetric?.inProgress)} in progress`,
1714
+ raw: workOrderMetric,
1715
+ filter: undefined,
1716
+ },
1717
+ {
1718
+ key: "incidents",
1719
+ title: "Open Incidents",
1720
+ icon: "mdi-alert",
1721
+ color: "#b73535",
1722
+ tint: "#ffdede",
1723
+ value: formatNumber(toNumber(incidentMetric?.count)),
1724
+ percentage: toNumber(incidentMetric?.percentage),
1725
+ meta: `${toNumber(incidentMetric?.highSeverity)} high severity`,
1726
+ raw: incidentMetric,
1727
+ filter: undefined,
1728
+ },
1729
+ {
1730
+ key: "visitors",
1731
+ title: "Visitors",
1732
+ icon: "mdi-account-group",
1733
+ color: "#d28722",
1734
+ tint: "#ffe4bd",
1735
+ value: formatNumber(toNumber(getFilteredVisitorMetric()?.count)),
1736
+ percentage: toNumber(getFilteredVisitorMetric()?.percentage),
1737
+ meta: `${toNumber(getFilteredVisitorMetric()?.currentlyOnSite)} currently on site`,
1738
+ raw: getFilteredVisitorMetric(),
1739
+ filter: {
1740
+ model: visitorType.value,
1741
+ options: visitorOptions.value,
1742
+ set: (v: string) => {
1743
+ visitorType.value = v;
1744
+ filterMenus.visitors = false;
1745
+ },
1746
+ },
1747
+ },
1748
+ {
1749
+ key: "patrolCompliance",
1750
+ title: "Patrol Compliance",
1751
+ icon: "mdi-calculator-variant",
1752
+ color: "#23824f",
1753
+ tint: "#c8f3da",
1754
+ value: `${toNumber(patrolMetric?.count)}%`,
1755
+ percentage: toNumber(patrolMetric?.percentage),
1756
+ meta: `${toNumber(patrolMetric?.missed ?? patrolMetric?.missedCheckpoints)} checkpoint missed · ${toNumber(patrolMetric?.ongoing)} ongoing`,
1757
+ raw: patrolMetric,
1758
+ filter: undefined,
1759
+ },
1760
+ ];
1761
+ }
1762
+
1763
+ const kpiCards = computed(() => {
1764
+ if (isPropertyMode.value) return getPropertyKpiCards();
1765
+ if (isSecurityMode.value) return getSecurityKpiCards();
1766
+
1767
+ // Hygiene / others — use generic resolvedCardValues
1768
+ return resolvedCardValues.value.map((mapping, index) => {
1103
1769
  const metric =
1104
- mapping.key === "visitors" && isSecurityDashboard.value
1770
+ mapping.key === "visitors" && isSecurityMode.value
1105
1771
  ? getFilteredVisitorMetric()
1106
1772
  : getMetric([mapping.key]);
1107
1773
  const value = getMetricValue(metric, mapping.key);
@@ -1116,25 +1782,49 @@ const kpiCards = computed(() =>
1116
1782
  percentage: toNumber(metric.percentage),
1117
1783
  meta: getMetricMeta(metric),
1118
1784
  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,
1785
+ filter: undefined as any,
1130
1786
  };
1131
- })
1132
- );
1787
+ });
1788
+ });
1133
1789
 
1134
1790
  const visibleKpiCards = computed(() =>
1135
1791
  kpiCards.value.filter((card) => isWidgetVisible(card.key))
1136
1792
  );
1137
1793
 
1794
+ // ─── Dashboard Title ──────────────────────────────────────────────────────────
1795
+
1796
+ const dashboardTitle = computed(() => {
1797
+ if (props.title) return props.title;
1798
+ const appName = String(
1799
+ runtimeConfig.public.APP_NAME || runtimeConfig.public.APP || ""
1800
+ );
1801
+ return `${getServiceDisplayName(props.serviceType || appName)} - Dashboard`;
1802
+ });
1803
+
1804
+ // ─── Primary list / staff config ─────────────────────────────────────────────
1805
+
1806
+ const primaryListConfig = computed(() =>
1807
+ isSecurityMode.value
1808
+ ? {
1809
+ title: "Active Patrol",
1810
+ route: "virtualPatrol",
1811
+ keys: ["activePatrolItems", "patrolItems"],
1812
+ }
1813
+ : {
1814
+ title: "Today's Task Schedule",
1815
+ route: "scheduleTask",
1816
+ keys: [
1817
+ "taskScheduleItems",
1818
+ "scheduleTaskItems",
1819
+ "scheduledTaskItems",
1820
+ "cleaningScheduleItems",
1821
+ "tasks",
1822
+ ],
1823
+ }
1824
+ );
1825
+
1826
+ // ─── Activity Chart ───────────────────────────────────────────────────────────
1827
+
1138
1828
  const activityTitle = computed(() => {
1139
1829
  if (selectedRange.value === "Today") return "Today's Activity";
1140
1830
  if (selectedRange.value === "This Month") return "This Month Activity";
@@ -1147,7 +1837,6 @@ const activityChart = computed(() =>
1147
1837
  const chartLabels = computed<string[]>(() => {
1148
1838
  const labels = activityChart.value.labels;
1149
1839
  if (labels.length) return labels;
1150
-
1151
1840
  if (selectedRange.value === "Today")
1152
1841
  return ["12 AM", "4 AM", "8 AM", "12 PM", "4 PM", "8 PM"];
1153
1842
  if (selectedRange.value === "This Month") {
@@ -1166,12 +1855,8 @@ const primarySeries = computed(() =>
1166
1855
  const secondarySeries = computed(() =>
1167
1856
  normalizeSeries(activityChart.value.secondary, chartLabels.value.length)
1168
1857
  );
1169
- const primaryLegendLabel = computed(
1170
- () => "Closed Work Order"
1171
- );
1172
- const secondaryLegendLabel = computed(
1173
- () => "Open Work Order"
1174
- );
1858
+ const primaryLegendLabel = computed(() => "Closed Work Order");
1859
+ const secondaryLegendLabel = computed(() => "Open Work Order");
1175
1860
  const chartMax = computed(() =>
1176
1861
  Math.max(1, ...primarySeries.value, ...secondarySeries.value)
1177
1862
  );
@@ -1192,21 +1877,6 @@ const chartYLabels = computed(() => {
1192
1877
  ];
1193
1878
  });
1194
1879
 
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
1880
  const primaryDots = computed(() => buildChartDots(primarySeries.value));
1211
1881
  const secondaryDots = computed(() => buildChartDots(secondarySeries.value));
1212
1882
  const primaryPath = computed(() => buildSmoothPath(primaryDots.value));
@@ -1215,13 +1885,14 @@ const chartAxisLabels = computed(() => {
1215
1885
  const labels = chartLabels.value;
1216
1886
  const count = Math.max(1, labels.length - 1);
1217
1887
  const width = chartWidth.value;
1218
-
1219
1888
  return labels.map((label, index) => ({
1220
1889
  text: label,
1221
1890
  x: 54 + ((width - 74) / count) * index,
1222
1891
  }));
1223
1892
  });
1224
1893
 
1894
+ // ─── List Data ────────────────────────────────────────────────────────────────
1895
+
1225
1896
  const taskScheduleItems = computed<TaskItem[]>(() =>
1226
1897
  getDashboardArray([...primaryListConfig.value.keys])
1227
1898
  .map(normalizeTaskItem)
@@ -1237,48 +1908,76 @@ const feedbackItems = computed<FeedbackItem[]>(() =>
1237
1908
  .map(normalizeFeedbackItem)
1238
1909
  .slice(0, 4)
1239
1910
  );
1240
- const recentActivities = computed<DashboardListItem[]>(() =>
1241
- getDashboardArray(["recentActivities", "activityItems"])
1242
- .map(normalizeActivityItem)
1243
- .slice(0, 3)
1911
+
1912
+ // Property-mode lists
1913
+ const upcomingEvents = computed(() =>
1914
+ normalizeListItems(["upcomingEvents", "eventsList", "events"])
1915
+ );
1916
+ const todayReminders = computed(() =>
1917
+ normalizeListItems(["todayReminders", "todaysReminders", "reminders"])
1918
+ );
1919
+ const todayAttentions = computed(() =>
1920
+ normalizeListItems(["todayAttentions", "attentions"])
1244
1921
  );
1245
1922
 
1246
- function getToneColor(tone: string) {
1247
- const map: Record<string, { bg: string; text: string }> = {
1248
- info: { bg: "blue-lighten-4", text: "blue-darken-3" },
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
- }
1923
+ const workOrderStatus = computed(() => {
1924
+ const summary = finalDashboardData.value?.workOrderStatusSummary;
1925
+ const metric = getMetric(["openWorkOrder", "workOrders", "workOrder"]);
1256
1926
 
1257
- function getServiceDisplayName(value: string) {
1258
- const normalized = value.toLowerCase();
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
- };
1927
+ const src = summary || metric;
1928
+ if (!src) return [];
1270
1929
 
1271
- if (titleMap[normalized]) return titleMap[normalized];
1930
+ const completed = Number(src.completed ?? 0);
1931
+ const inProgress = Number(src.inProgress ?? 0);
1932
+ const pending =
1933
+ src.pending !== undefined
1934
+ ? Number(src.pending)
1935
+ : Math.max(0, Number(src.count ?? 0) - inProgress);
1936
+ const max = Math.max(completed, inProgress, pending, 1);
1272
1937
 
1273
- const cleaned = normalized
1274
- .replace(/_services|_agency/g, "")
1275
- .replace(/[_-]+/g, " ")
1276
- .trim();
1938
+ return [
1939
+ { label: "Completed", value: completed, percent: (completed / max) * 100, color: "#2fb84d" },
1940
+ { label: "In Progress", value: inProgress, percent: (inProgress / max) * 100, color: "#f12d2d" },
1941
+ { label: "Pending", value: pending, percent: (pending / max) * 100, color: "#ff8900" },
1942
+ ];
1943
+ });
1277
1944
 
1278
- return cleaned
1279
- ? cleaned.replace(/\b\w/g, (char) => char.toUpperCase())
1280
- : "Service";
1281
- }
1945
+ // Security-mode lists
1946
+ const activePatrolItems = computed<any[]>(() =>
1947
+ finalDashboardData.value?.activePatrolItems ?? []
1948
+ );
1949
+ const securityStaffItems = computed<any[]>(() =>
1950
+ attendanceReq.value?.items ?? []
1951
+ );
1952
+
1953
+ // ─── ResizeObserver ───────────────────────────────────────────────────────────
1954
+
1955
+ watch(chartContainerRef, (el) => {
1956
+ if (resizeObserver) resizeObserver.disconnect();
1957
+ if (el && window.ResizeObserver) {
1958
+ resizeObserver = new ResizeObserver((entries) => {
1959
+ if (entries[0] && entries[0].contentRect.width > 0) {
1960
+ chartWidth.value = entries[0].contentRect.width;
1961
+ }
1962
+ });
1963
+ resizeObserver.observe(el);
1964
+ }
1965
+ });
1966
+ onUnmounted(() => {
1967
+ if (resizeObserver) resizeObserver.disconnect();
1968
+ });
1969
+
1970
+ // ─── Range watcher ────────────────────────────────────────────────────────────
1971
+
1972
+ watch(selectedRange, (period) => {
1973
+ if (props.serviceType) {
1974
+ const apiPeriod = periodValue[period];
1975
+ if (apiPeriod) currentPeriod.value = apiPeriod;
1976
+ }
1977
+ emit("updatePeriod", { key: "__all__", period });
1978
+ });
1979
+
1980
+ // ─── Metric Helpers ───────────────────────────────────────────────────────────
1282
1981
 
1283
1982
  function getMetric(keys: string[]): DashboardMetric {
1284
1983
  const data = finalDashboardData.value || {};
@@ -1295,20 +1994,16 @@ function getMetricValue(metric: DashboardMetric, key?: string) {
1295
1994
  if (key === "patrolCompliance") {
1296
1995
  return `${formatNumber(toNumber(metric.count))}%`;
1297
1996
  }
1298
-
1299
1997
  const completed = toNumber(metric.completed);
1300
1998
  const total = toNumber(metric.total);
1301
-
1302
1999
  if (total > 0 && typeof metric.completed !== "undefined") {
1303
2000
  return `${formatNumber(completed)}/${formatNumber(total)}`;
1304
2001
  }
1305
-
1306
2002
  return toNumber(metric.count);
1307
2003
  }
1308
2004
 
1309
2005
  function getMetricMeta(metric: DashboardMetric) {
1310
2006
  const parts: string[] = [];
1311
-
1312
2007
  if (typeof metric.overdue !== "undefined")
1313
2008
  parts.push(`${formatNumber(toNumber(metric.overdue))} overdue`);
1314
2009
  if (typeof metric.inProgress !== "undefined")
@@ -1318,20 +2013,13 @@ function getMetricMeta(metric: DashboardMetric) {
1318
2013
  if (typeof metric.highSeverity !== "undefined")
1319
2014
  parts.push(`${formatNumber(toNumber(metric.highSeverity))} high severity`);
1320
2015
  if (typeof metric.currentlyOnSite !== "undefined")
1321
- parts.push(
1322
- `${formatNumber(toNumber(metric.currentlyOnSite))} currently on site`
1323
- );
2016
+ parts.push(`${formatNumber(toNumber(metric.currentlyOnSite))} currently on site`);
1324
2017
  if (
1325
- typeof (metric as Record<string, any>).missedCheckpoints !== "undefined" ||
1326
- typeof (metric as Record<string, any>).missed !== "undefined"
2018
+ typeof (metric as any).missedCheckpoints !== "undefined" ||
2019
+ typeof (metric as any).missed !== "undefined"
1327
2020
  ) {
1328
2021
  parts.push(
1329
- `${formatNumber(
1330
- toNumber(
1331
- (metric as Record<string, any>).missedCheckpoints ??
1332
- (metric as Record<string, any>).missed
1333
- )
1334
- )} checkpoint missed`
2022
+ `${formatNumber(toNumber((metric as any).missedCheckpoints ?? (metric as any).missed))} checkpoint missed`
1335
2023
  );
1336
2024
  }
1337
2025
  if (typeof metric.lowStock !== "undefined")
@@ -1344,247 +2032,144 @@ function getMetricMeta(metric: DashboardMetric) {
1344
2032
  ) {
1345
2033
  parts.push(`${formatNumber(toNumber(metric.completed))} completed`);
1346
2034
  }
1347
-
1348
2035
  return parts.join(" - ");
1349
2036
  }
1350
2037
 
1351
- function getCardColor(index: number) {
1352
- return ["#155178", "#9f2f29", "#1f8a56", "#b96d17"][index % 4];
2038
+ function getFilteredVisitorMetric(): DashboardMetric {
2039
+ const metric = getMetric(["visitors", "visitor"]);
2040
+ const selected = visitorType.value;
2041
+ const items = visitorItems.value;
2042
+
2043
+ if (selected === "All") {
2044
+ return {
2045
+ ...metric,
2046
+ count: toNumber(metric.count ?? items.length),
2047
+ currentlyOnSite: toNumber(
2048
+ metric.currentlyOnSite ??
2049
+ items.filter((item: any) => !isClosedStatus(item.status) && !item.checkOut)
2050
+ .length
2051
+ ),
2052
+ };
2053
+ }
2054
+
2055
+ const filtered = items.filter(
2056
+ (item: any) => getVisitorFilterValue(item) === selected
2057
+ );
2058
+ return {
2059
+ ...metric,
2060
+ count: filtered.length,
2061
+ currentlyOnSite: filtered.filter(
2062
+ (item: any) => !isClosedStatus(item.status) && !item.checkOut
2063
+ ).length,
2064
+ };
1353
2065
  }
1354
2066
 
1355
- function getCardTint(color: string, index: number) {
1356
- const tints = ["#dbeeff", "#f8c6c3", "#d8f5e5", "#ffe7c2"];
1357
- if (!color) return tints[index % tints.length];
2067
+ // ─── Array/List Helpers ───────────────────────────────────────────────────────
1358
2068
 
1359
- const normalized = color.toLowerCase();
1360
- if (normalized.includes("f44336") || normalized.includes("d32f2f"))
1361
- return "#f8c6c3";
1362
- if (normalized.includes("4caf50") || normalized.includes("43a047"))
1363
- return "#d8f5e5";
1364
- if (normalized.includes("ff9800") || normalized.includes("ffa726"))
1365
- return "#ffe7c2";
1366
- return "#dbeeff";
2069
+ function getDashboardArray(keys: string[]) {
2070
+ const data = finalDashboardData.value || {};
2071
+ for (const key of keys) {
2072
+ const value = data[key];
2073
+ if (Array.isArray(value)) return value;
2074
+ if (Array.isArray(value?.items)) return value.items;
2075
+ if (Array.isArray(value?.docs)) return value.docs;
2076
+ if (Array.isArray(value?.data)) return value.data;
2077
+ }
2078
+ return [];
1367
2079
  }
1368
2080
 
1369
- function getActivityChartSource() {
2081
+ function normalizeListItems(keys: string[]): DashboardListItem[] {
1370
2082
  const data = finalDashboardData.value || {};
2083
+ const source = keys.map((k) => data[k]).find(Array.isArray);
2084
+ if (!source) return [];
2085
+ const context = keys[0];
2086
+
2087
+ return source.slice(0, 4).map((item: any, index: number) => ({
2088
+ id: item._id || item.id || `${keys[0]}-${index}`,
2089
+ title: item.title || item.name || item.subject || "Untitled",
2090
+ subtitle: getListSubtitle(context, item),
2091
+ time: getListTime(item),
2092
+ color: item.color || item.statusColor || "primary",
2093
+ icon: item.icon || getListIcon(context),
2094
+ tone: item.tone,
2095
+ actionLabel: item.actionLabel,
2096
+ }));
2097
+ }
2098
+
2099
+ function getListSubtitle(context: string, item: any) {
2100
+ if (item.subtitle) return item.subtitle;
2101
+ const time = getListTime(item);
2102
+ if (context === "upcomingEvents") {
2103
+ const type = item.type || item.category || item.status || "Event";
2104
+ return [type, formatDashboardDateTime(time)].filter(Boolean).join(" - ");
2105
+ }
2106
+ if (context === "todayReminders" || context === "todayAttentions") {
2107
+ return formatDashboardTime(time) || item.description || item.status || "";
2108
+ }
2109
+ return item.description || item.category || item.location || formatDashboardTime(time);
2110
+ }
2111
+
2112
+ function getListTime(item: any) {
1371
2113
  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
- {}
2114
+ item.time ||
2115
+ item.dateTime ||
2116
+ item.start_datetime ||
2117
+ item.startDate ||
2118
+ item.startAt ||
2119
+ item.date ||
2120
+ item.createdAt ||
2121
+ item.updatedAt ||
2122
+ ""
1380
2123
  );
1381
2124
  }
1382
2125
 
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
- ]);
2126
+ function getListIcon(context: string) {
2127
+ if (context === "upcomingEvents") return "mdi-circle";
2128
+ if (context === "todayReminders") return "mdi-alert";
2129
+ if (context === "todayAttentions") return "mdi-alert-circle-outline";
2130
+ return "mdi-circle";
2131
+ }
1393
2132
 
1394
- const hasClosedSeries = secondary.length > 0;
2133
+ function getListTone(item: DashboardListItem) {
2134
+ if (item.tone) return item.tone;
2135
+ const text = `${item.title} ${item.subtitle || ""} ${item.icon || ""} ${item.color || ""}`.toLowerCase();
2136
+ if (text.includes("alert") || text.includes("incident") || text.includes("overdue") || text.includes("pending"))
2137
+ return "danger";
2138
+ if (text.includes("booking") || text.includes("facility") || text.includes("approved") || text.includes("complete"))
2139
+ return "success";
2140
+ if (text.includes("visitor") || text.includes("check")) return "warning";
2141
+ if (text.includes("calendar") || text.includes("event") || text.includes("reminder"))
2142
+ return "info";
2143
+ return "neutral";
2144
+ }
1395
2145
 
1396
- const primary = extractChartSeries(source, [
1397
- "openWorkOrder",
1398
- "openWorkOrders",
1399
- "open_work_orders",
1400
- "workOrders",
1401
- "workOrder",
1402
- "work_orders",
1403
- ]);
2146
+ // ─── Normalizers ─────────────────────────────────────────────────────────────
1404
2147
 
1405
- const secondaryCandidate = hasClosedSeries
1406
- ? { label: "Closed Work Order", series: secondary }
1407
- : getSecondaryChartSeries(source);
2148
+ function normalizeTaskItem(item: Record<string, any>, index: number): TaskItem {
2149
+ const title =
2150
+ item.title || item.subject || item.name || item.taskName || item.description || "Task";
2151
+ const subtitle = item.subtitle || item.area || item.location || item.asset || item.category || "";
2152
+ const person =
2153
+ item.person ||
2154
+ item.assigneeName ||
2155
+ item.assignedTo?.name ||
2156
+ item.staff?.name ||
2157
+ item.createdBy?.name ||
2158
+ "Unassigned";
1408
2159
 
1409
2160
  return {
1410
- labels,
1411
- primary,
1412
- secondary: secondaryCandidate.series,
1413
- primaryLabel: hasClosedSeries ? "Open Work Order" : "Work Order",
1414
- secondaryLabel: secondaryCandidate.label,
2161
+ id: String(item.id || item._id || `${title}-${index}`),
2162
+ title,
2163
+ subtitle,
2164
+ person,
2165
+ status: normalizeStatus(item.status || item.state || item.taskStatus || "To-Do"),
2166
+ time:
2167
+ item.time || item.startTime || item.scheduleTime || formatActivityTime(item),
2168
+ raw: item,
1415
2169
  };
1416
2170
  }
1417
2171
 
1418
- function getSecondaryChartSeries(source: any) {
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
- ];
1427
-
1428
- for (const candidate of candidates) {
1429
- const series = extractChartSeries(source, candidate.keys);
1430
- if (series.length) return { label: candidate.label, series };
1431
- }
1432
-
1433
- return { label: "Feedback", series: [] as number[] };
1434
- }
1435
-
1436
- function extractChartSeries(source: any, keys: string[]) {
1437
- for (const key of keys) {
1438
- const value = source?.[key];
1439
- if (Array.isArray(value)) return value.map(toNumber);
1440
- }
1441
-
1442
- if (Array.isArray(source?.datasets)) {
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);
1450
- }
1451
-
1452
- return [];
1453
- }
1454
-
1455
- function normalizeSeries(value: any, targetLength: number) {
1456
- const source = Array.isArray(value) ? value : [];
1457
- return Array.from({ length: targetLength }, (_, index) =>
1458
- toNumber(source[index])
1459
- );
1460
- }
1461
-
1462
- function buildChartDots(series: number[]) {
1463
- const count = Math.max(1, series.length - 1);
1464
- const width = chartWidth.value;
1465
-
1466
- return series.map((value, index) => ({
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
- };
1515
- }
1516
-
1517
- function buildFilterOptions(
1518
- defaults: string[],
1519
- items: Record<string, any>[],
1520
- getValue: (item: Record<string, any>) => string
1521
- ) {
1522
- const options = new Set(defaults);
1523
- items.forEach((item) => {
1524
- const value = getValue(item);
1525
- if (value) options.add(value);
1526
- });
1527
-
1528
- return Array.from(options);
1529
- }
1530
-
1531
- function getVisitorFilterValue(item: Record<string, any>) {
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
- }
1543
-
1544
- function normalizeTaskItem(item: Record<string, any>, index: number): TaskItem {
1545
- const title =
1546
- item.title ||
1547
- item.subject ||
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
- "";
1559
- const person =
1560
- item.person ||
1561
- item.assigneeName ||
1562
- item.assignedTo?.name ||
1563
- item.staff?.name ||
1564
- item.createdBy?.name ||
1565
- "Unassigned";
1566
-
1567
- return {
1568
- id: String(item.id || item._id || `${title}-${index}`),
1569
- title,
1570
- subtitle,
1571
- person,
1572
- status: normalizeStatus(
1573
- item.status || item.state || item.taskStatus || "To-Do"
1574
- ),
1575
- time:
1576
- item.time ||
1577
- item.startTime ||
1578
- item.scheduleTime ||
1579
- formatActivityTime(item),
1580
- raw: item,
1581
- };
1582
- }
1583
-
1584
- function normalizeStaffItem(
1585
- item: Record<string, any>,
1586
- index: number
1587
- ): StaffItem {
2172
+ function normalizeStaffItem(item: Record<string, any>, index: number): StaffItem {
1588
2173
  const name =
1589
2174
  item.name ||
1590
2175
  item.employeeName ||
@@ -1605,21 +2190,14 @@ function normalizeStaffItem(
1605
2190
  name,
1606
2191
  role,
1607
2192
  status: normalizeStatus(
1608
- item.statusLabel ||
1609
- item.status ||
1610
- item.currentStatus ||
1611
- item.attendanceStatus ||
1612
- "Off duty"
2193
+ item.statusLabel || item.status || item.currentStatus || item.attendanceStatus || "Off duty"
1613
2194
  ),
1614
2195
  totalHours: item.totalHours || 0,
1615
2196
  raw: item,
1616
2197
  };
1617
2198
  }
1618
2199
 
1619
- function normalizeFeedbackItem(
1620
- item: Record<string, any>,
1621
- index: number
1622
- ): FeedbackItem {
2200
+ function normalizeFeedbackItem(item: Record<string, any>, index: number): FeedbackItem {
1623
2201
  const title = item.title || item.subject || item.description || "Feedback";
1624
2202
  const subject = item.category || item.type || item.service || "Subject";
1625
2203
  const app = item.app || item.source || item.channel || "";
@@ -1642,179 +2220,190 @@ function normalizeFeedbackItem(
1642
2220
  };
1643
2221
  }
1644
2222
 
1645
- function normalizeActivityItem(
1646
- item: Record<string, any>,
1647
- index: number
1648
- ): DashboardListItem {
1649
- const title =
1650
- item.title ||
1651
- item.subject ||
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
- };
2223
+ // Feedback tone helper (for property-mode pill classes)
2224
+ function getFeedbackTone(status: any) {
2225
+ const s = String(status || "").toLowerCase();
2226
+ if (s.includes("complete") || s.includes("resolved") || s.includes("done")) return "completed";
2227
+ if (s.includes("progress") || s.includes("review")) return "progress";
2228
+ if (s.includes("reject") || s.includes("cancel")) return "danger";
2229
+ return "todo";
1667
2230
  }
1668
2231
 
1669
- function formatActivityTime(item: Record<string, any>) {
1670
- if (item.displayTime) return String(item.displayTime);
1671
-
1672
- const rawTime = item.time || getItemTime(item);
1673
- const rawText = String(rawTime || "");
1674
-
1675
- if (!rawText) return "";
1676
- if (
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);
2232
+ function getInitials(name?: string) {
2233
+ return String(name || "")
2234
+ .trim()
2235
+ .split(/\s+/)
2236
+ .filter(Boolean)
2237
+ .slice(0, 2)
2238
+ .map((w) => w.charAt(0).toUpperCase())
2239
+ .join("") || "-";
1685
2240
  }
1686
2241
 
1687
- function getActivityToneStyle(tone: string) {
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
- };
2242
+ // ─── Status Style Helpers ────────────────────────────────────────────────────
1710
2243
 
1711
- return toneMap[tone] || toneMap.neutral;
2244
+ function getStatusPillStyle(status: any) {
2245
+ const tone = getStatusTone(status);
2246
+ const map: Record<string, { backgroundColor: string; color: string }> = {
2247
+ success: { backgroundColor: "#72e3a1", color: "#0f5132" },
2248
+ warning: { backgroundColor: "#fde68a", color: "#92400e" },
2249
+ error: { backgroundColor: "#fecaca", color: "#b91c1c" },
2250
+ neutral: { backgroundColor: "#e2e8f0", color: "#64748b" },
2251
+ };
2252
+ return map[tone] || map.neutral;
1712
2253
  }
1713
2254
 
1714
- function getItemTime(item: Record<string, any>) {
1715
- return (
1716
- item.createdAt ||
1717
- item.updatedAt ||
1718
- item.date ||
1719
- item.time ||
1720
- item.created_on ||
1721
- item.updated_on
1722
- );
2255
+ function getPatrolStatusStyle(status: any) {
2256
+ const s = String(status || "").toLowerCase();
2257
+ if (s === "completed" || s === "complete")
2258
+ return { backgroundColor: "#72e3a1", color: "#0f5132" };
2259
+ if (s === "on paused" || s === "paused")
2260
+ return { backgroundColor: "#dcdcdc", color: "#64748b" };
2261
+ return { backgroundColor: "#e2e8f0", color: "#64748b" };
1723
2262
  }
1724
2263
 
1725
- function normalizeStatus(value: any) {
1726
- const text = String(value || "").trim();
1727
- if (!text) return "To-Do";
2264
+ function getStaffStatusPillStyle(status: any) {
2265
+ const s = String(status || "").toLowerCase();
2266
+ if (s === "on duty" || s === "on-duty")
2267
+ return { backgroundColor: "#72e3a1", color: "#0f5132" };
2268
+ if (s === "on break" || s === "on-break")
2269
+ return { backgroundColor: "#fde68a", color: "#92400e" };
2270
+ if (s === "absent")
2271
+ return { backgroundColor: "#fecaca", color: "#b91c1c" };
2272
+ return { backgroundColor: "#e2e8f0", color: "#64748b" };
2273
+ }
1728
2274
 
1729
- return text
1730
- .replace(/[_-]+/g, " ")
1731
- .replace(/\b\w/g, (char) => char.toUpperCase());
2275
+ function getStatusProgressStyle(item: { percent: number; color: string }) {
2276
+ const percent = Math.min(100, Math.max(0, Number(item.percent) || 0));
2277
+ return { backgroundColor: item.color, width: `${percent}%` };
1732
2278
  }
1733
2279
 
1734
- function getStatusTone(status: any) {
1735
- const normalized = String(status || "").toLowerCase();
1736
- if (
1737
- normalized.includes("complete") ||
1738
- normalized.includes("done") ||
1739
- normalized.includes("resolved") ||
1740
- normalized.includes("closed")
1741
- )
1742
- return "success";
1743
- if (
1744
- normalized.includes("progress") ||
1745
- normalized.includes("active") ||
1746
- normalized.includes("ongoing")
1747
- )
1748
- return "info";
1749
- if (
1750
- normalized.includes("overdue") ||
1751
- normalized.includes("urgent") ||
1752
- normalized.includes("high")
1753
- )
1754
- return "danger";
1755
- return "neutral";
2280
+ // ─── Chart Helpers ────────────────────────────────────────────────────────────
2281
+
2282
+ function buildSmoothPath(points: { x: number; y: number }[]) {
2283
+ if (!points || points.length === 0) return "";
2284
+ let d = `M ${points[0].x},${points[0].y}`;
2285
+ for (let i = 0; i < points.length - 1; i++) {
2286
+ const p1 = points[i];
2287
+ const p2 = points[i + 1];
2288
+ const cp1x = p1.x + (p2.x - p1.x) / 2;
2289
+ const cp1y = p1.y;
2290
+ const cp2x = p1.x + (p2.x - p1.x) / 2;
2291
+ const cp2y = p2.y;
2292
+ d += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p2.x},${p2.y}`;
2293
+ }
2294
+ return d;
1756
2295
  }
1757
2296
 
1758
- function getStaffTone(status: any) {
1759
- const normalized = String(status || "").toLowerCase();
1760
- if (
1761
- normalized.includes("duty") ||
1762
- normalized.includes("present") ||
1763
- normalized.includes("active")
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";
2297
+ function buildChartDots(series: number[]) {
2298
+ const count = Math.max(1, series.length - 1);
2299
+ const width = chartWidth.value;
2300
+ return series.map((value, index) => ({
2301
+ x: 54 + ((width - 74) / count) * index,
2302
+ y: 220 - (toNumber(value) / chartMax.value) * 184,
2303
+ }));
1770
2304
  }
1771
2305
 
1772
- function isClosedStatus(status: any) {
1773
- const normalized = String(status || "").toLowerCase();
2306
+ function getActivityChartSource() {
2307
+ const data = finalDashboardData.value || {};
1774
2308
  return (
1775
- normalized.includes("complete") ||
1776
- normalized.includes("cancel") ||
1777
- normalized.includes("reject") ||
1778
- normalized.includes("closed") ||
1779
- normalized.includes("deleted") ||
1780
- normalized.includes("check-out") ||
1781
- normalized.includes("checkout")
2309
+ data.activityChart ||
2310
+ data.statistic?.activityChart ||
2311
+ data.statistics?.activityChart ||
2312
+ data.statisticChart ||
2313
+ data.statisticsChart ||
2314
+ data.activityStatistics ||
2315
+ data.activityStatistic ||
2316
+ {}
1782
2317
  );
1783
2318
  }
1784
2319
 
1785
- function isWidgetVisible(key: string) {
1786
- return visibleWidgetKeys.value.includes(key);
2320
+ function normalizeActivityChart(source: any) {
2321
+ const labels = Array.isArray(source?.labels)
2322
+ ? source.labels.map((l: any) => String(l))
2323
+ : [];
2324
+ const secondary = extractChartSeries(source, ["closedWorkOrder", "closedWorkOrders", "closed_work_orders"]);
2325
+ const hasClosedSeries = secondary.length > 0;
2326
+ const primary = extractChartSeries(source, ["openWorkOrder", "openWorkOrders", "open_work_orders", "workOrders", "workOrder", "work_orders"]);
2327
+ const secondaryCandidate = hasClosedSeries
2328
+ ? { label: "Closed Work Order", series: secondary }
2329
+ : getSecondaryChartSeries(source);
2330
+
2331
+ return {
2332
+ labels,
2333
+ primary,
2334
+ secondary: secondaryCandidate.series,
2335
+ primaryLabel: hasClosedSeries ? "Open Work Order" : "Work Order",
2336
+ secondaryLabel: secondaryCandidate.label,
2337
+ };
1787
2338
  }
1788
2339
 
1789
- function openCustomizeDialog() {
1790
- draftVisibleWidgetKeys.value = [...visibleWidgetKeys.value];
1791
- customizeDialog.value = true;
2340
+ function getSecondaryChartSeries(source: any) {
2341
+ const candidates = [
2342
+ { label: "Task Completed", keys: ["taskCompleted", "completedTasks", "completed"] },
2343
+ { label: "Incident", keys: ["incidents", "incident", "incidentReports"] },
2344
+ { label: "Feedback", keys: ["feedbacks", "feedback", "feedbackTickets"] },
2345
+ ];
2346
+ for (const candidate of candidates) {
2347
+ const series = extractChartSeries(source, candidate.keys);
2348
+ if (series.length) return { label: candidate.label, series };
2349
+ }
2350
+ return { label: "Feedback", series: [] as number[] };
1792
2351
  }
1793
2352
 
1794
- function cancelCustomize() {
1795
- draftVisibleWidgetKeys.value = [...visibleWidgetKeys.value];
1796
- widgetSearch.value = "";
1797
- customizeDialog.value = false;
2353
+ function extractChartSeries(source: any, keys: string[]) {
2354
+ for (const key of keys) {
2355
+ const value = source?.[key];
2356
+ if (Array.isArray(value)) return value.map(toNumber);
2357
+ }
2358
+ if (Array.isArray(source?.datasets)) {
2359
+ const dataset = source.datasets.find((item: any) => {
2360
+ const keyText = String(item?.key || item?.label || item?.name || "").toLowerCase();
2361
+ return keys.some((key) => keyText.includes(key.toLowerCase()));
2362
+ });
2363
+ if (Array.isArray(dataset?.data)) return dataset.data.map(toNumber);
2364
+ }
2365
+ return [];
1798
2366
  }
1799
2367
 
1800
- function resetCustomizeDraft() {
1801
- draftVisibleWidgetKeys.value = [...defaultVisibleWidgetKeys.value];
2368
+ function normalizeSeries(value: any, targetLength: number) {
2369
+ const source = Array.isArray(value) ? value : [];
2370
+ return Array.from({ length: targetLength }, (_, index) => toNumber(source[index]));
1802
2371
  }
1803
2372
 
1804
- function submitCustomize() {
1805
- visibleWidgetKeys.value = [...draftVisibleWidgetKeys.value];
1806
- widgetSearch.value = "";
1807
- customizeDialog.value = false;
2373
+ // ─── Filter Helpers ───────────────────────────────────────────────────────────
2374
+
2375
+ function buildFilterOptions(
2376
+ defaults: string[],
2377
+ items: Record<string, any>[],
2378
+ getValue: (item: Record<string, any>) => string
2379
+ ) {
2380
+ const options = new Set(defaults);
2381
+ items.forEach((item) => {
2382
+ const value = getValue(item);
2383
+ if (value) options.add(value);
2384
+ });
2385
+ return Array.from(options);
1808
2386
  }
1809
2387
 
1810
- function notifyExport(format: string) {
1811
- snackbar.text = `${format} export is being prepared.`;
1812
- snackbar.visible = true;
2388
+ function getVisitorFilterValue(item: Record<string, any>) {
2389
+ const raw = String(item.type || item.visitorType || item.category || "").toLowerCase();
2390
+ if (raw.includes("contractor")) return "Contractor";
2391
+ if (raw.includes("walk")) return "Walk-In";
2392
+ if (raw.includes("delivery")) return "Delivery";
2393
+ if (raw.includes("pick")) return "Pick-up";
2394
+ if (raw.includes("drop")) return "Drop-Off";
2395
+ if (raw.includes("guest") || raw.includes("drive")) return "Drive-In";
2396
+ return "All";
2397
+ }
2398
+
2399
+ // ─── Routing ─────────────────────────────────────────────────────────────────
2400
+
2401
+ function dashboardPath(path: string) {
2402
+ return `/${props.orgId}/${props.site}/${path}`;
1813
2403
  }
1814
2404
 
1815
2405
  function goToDashboardRoute(routeKey: string) {
1816
2406
  if (!props.orgId || !props.site) return;
1817
-
1818
2407
  const pathMap: Record<string, string> = {
1819
2408
  attendance: "attendance",
1820
2409
  feedbacks: "feedbacks",
@@ -1822,8 +2411,11 @@ function goToDashboardRoute(routeKey: string) {
1822
2411
  virtualPatrol: "virtual-patrol/logs",
1823
2412
  manpower: "manpower-monitoring/attendance",
1824
2413
  incidentReports: "incident-reports",
1825
- visitorManagement: "visitor-management",
1826
- workOrders: "work-orders",
2414
+ "virtual-patrol": "virtual-patrol",
2415
+ "work-orders": "work-orders",
2416
+ "event-mgmt": "event-mgmt",
2417
+ calendar: "calendar",
2418
+ "incident-reports": "incident-reports",
1827
2419
  };
1828
2420
  const path = pathMap[routeKey] || routeKey;
1829
2421
  router.push(`/${props.orgId}/${props.site}/${path}`);
@@ -1834,13 +2426,11 @@ function goToListItem(item: TaskItem | FeedbackItem, routeKey: string) {
1834
2426
  goToDashboardRoute(routeKey);
1835
2427
  return;
1836
2428
  }
1837
-
1838
2429
  const id = item.raw?._id || item.raw?.id || item.id;
1839
2430
  if (!id || String(id).includes("-")) {
1840
2431
  goToDashboardRoute(routeKey);
1841
2432
  return;
1842
2433
  }
1843
-
1844
2434
  const pathMap: Record<string, string> = {
1845
2435
  feedbacks: "feedbacks",
1846
2436
  scheduleTask: "schedule-task",
@@ -1849,34 +2439,494 @@ function goToListItem(item: TaskItem | FeedbackItem, routeKey: string) {
1849
2439
  router.push(`/${props.orgId}/${props.site}/${path}/${id}`);
1850
2440
  }
1851
2441
 
2442
+ // ─── Widget Management ────────────────────────────────────────────────────────
1852
2443
 
2444
+ function isWidgetVisible(key: string) {
2445
+ return visibleWidgetKeys.value.includes(key);
2446
+ }
2447
+
2448
+ function openCustomizeDialog() {
2449
+ draftVisibleWidgetKeys.value = [...visibleWidgetKeys.value];
2450
+ customizeDialog.value = true;
2451
+ }
2452
+
2453
+ function cancelCustomize() {
2454
+ draftVisibleWidgetKeys.value = [...visibleWidgetKeys.value];
2455
+ widgetSearch.value = "";
2456
+ customizeDialog.value = false;
2457
+ }
2458
+
2459
+ function resetCustomizeDraft() {
2460
+ draftVisibleWidgetKeys.value = [...defaultVisibleWidgetKeys.value];
2461
+ }
1853
2462
 
1854
- function formatNumber(value: number) {
1855
- return new Intl.NumberFormat("en-US").format(toNumber(value));
2463
+ function submitCustomize() {
2464
+ visibleWidgetKeys.value = [...draftVisibleWidgetKeys.value];
2465
+ widgetSearch.value = "";
2466
+ customizeDialog.value = false;
2467
+ }
2468
+
2469
+ function notifyExport(format: string) {
2470
+ snackbar.text = `${format} export is being prepared.`;
2471
+ snackbar.visible = true;
2472
+ }
2473
+
2474
+ // ─── Card Color Helpers ───────────────────────────────────────────────────────
2475
+
2476
+ function getCardColor(index: number) {
2477
+ return ["#155178", "#9f2f29", "#1f8a56", "#b96d17"][index % 4];
2478
+ }
2479
+
2480
+ function getCardTint(color: string, index: number) {
2481
+ const tints = ["#dbeeff", "#f8c6c3", "#d8f5e5", "#ffe7c2"];
2482
+ if (!color) return tints[index % tints.length];
2483
+ const n = color.toLowerCase();
2484
+ if (n.includes("f44336") || n.includes("d32f2f")) return "#f8c6c3";
2485
+ if (n.includes("4caf50") || n.includes("43a047")) return "#d8f5e5";
2486
+ if (n.includes("ff9800") || n.includes("ffa726")) return "#ffe7c2";
2487
+ return "#dbeeff";
2488
+ }
2489
+
2490
+ // ─── Generic Helpers ─────────────────────────────────────────────────────────
2491
+
2492
+ function getServiceDisplayName(value: string) {
2493
+ const normalized = value.toLowerCase();
2494
+ const titleMap: Record<string, string> = {
2495
+ security_agency: "Security",
2496
+ cleaning_services: "Hygiene",
2497
+ hygiene: "Hygiene",
2498
+ mechanical_electrical: "Mechanical & Electrical",
2499
+ mechanical_electrical_services: "Mechanical & Electrical",
2500
+ pest_control_services: "Pest Control",
2501
+ pool_maintenance_services: "Pool Maintenance",
2502
+ landscaping_services: "Landscaping",
2503
+ property_management_agency: "Property Management",
2504
+ };
2505
+ if (titleMap[normalized]) return titleMap[normalized];
2506
+ const cleaned = normalized
2507
+ .replace(/_services|_agency/g, "")
2508
+ .replace(/[_-]+/g, " ")
2509
+ .trim();
2510
+ return cleaned
2511
+ ? cleaned.replace(/\b\w/g, (char) => char.toUpperCase())
2512
+ : "Service";
2513
+ }
2514
+
2515
+ function formatNumber(value: any) {
2516
+ const n = Number(value ?? 0);
2517
+ return new Intl.NumberFormat("en-US").format(Number.isFinite(n) ? n : 0);
1856
2518
  }
1857
2519
 
1858
2520
  function toNumber(value: any) {
1859
- const numberValue = Number(value ?? 0);
1860
- return Number.isFinite(numberValue) ? numberValue : 0;
2521
+ const n = Number(value ?? 0);
2522
+ return Number.isFinite(n) ? n : 0;
2523
+ }
2524
+
2525
+ function normalizeStatus(value: any) {
2526
+ const text = String(value || "").trim();
2527
+ if (!text) return "To-Do";
2528
+ return text.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2529
+ }
2530
+
2531
+ function getStatusTone(status: any) {
2532
+ const s = String(status || "").toLowerCase();
2533
+ if (s.includes("complete") || s.includes("done") || s.includes("resolved") || s.includes("closed"))
2534
+ return "success";
2535
+ if (s.includes("progress") || s.includes("active") || s.includes("ongoing"))
2536
+ return "info";
2537
+ if (s.includes("overdue") || s.includes("urgent") || s.includes("high"))
2538
+ return "danger";
2539
+ if (s.includes("warning") || s.includes("reject") || s.includes("cancel"))
2540
+ return "error";
2541
+ return "neutral";
2542
+ }
2543
+
2544
+ function isClosedStatus(status: any) {
2545
+ const s = String(status || "").toLowerCase();
2546
+ return (
2547
+ s.includes("complete") ||
2548
+ s.includes("cancel") ||
2549
+ s.includes("reject") ||
2550
+ s.includes("closed") ||
2551
+ s.includes("deleted") ||
2552
+ s.includes("check-out") ||
2553
+ s.includes("checkout")
2554
+ );
2555
+ }
2556
+
2557
+ function formatActivityTime(item: Record<string, any>) {
2558
+ const rawTime =
2559
+ item.time ||
2560
+ item.createdAt ||
2561
+ item.updatedAt ||
2562
+ item.date ||
2563
+ item.startTime ||
2564
+ item.scheduleTime;
2565
+ return formatRelativeTime(rawTime);
1861
2566
  }
1862
2567
 
1863
2568
  function formatRelativeTime(value: any) {
1864
2569
  if (!value) return "";
1865
-
1866
2570
  const date = new Date(value);
1867
2571
  if (Number.isNaN(date.getTime())) return "";
1868
-
1869
2572
  const diffMs = Date.now() - date.getTime();
1870
2573
  const diffMinutes = Math.max(0, Math.round(diffMs / 60000));
1871
2574
  if (diffMinutes < 60) {
1872
2575
  const minutes = diffMinutes || 1;
1873
2576
  return `${minutes} min${minutes === 1 ? "" : "s"} ago`;
1874
2577
  }
1875
-
1876
2578
  const diffHours = Math.round(diffMinutes / 60);
1877
2579
  if (diffHours < 24) return `${diffHours} hr${diffHours === 1 ? "" : "s"} ago`;
1878
-
1879
2580
  const diffDays = Math.round(diffHours / 24);
1880
2581
  return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
1881
2582
  }
2583
+
2584
+ function formatDashboardDateTime(value?: string) {
2585
+ if (!value) return "";
2586
+ const date = new Date(value);
2587
+ if (Number.isNaN(date.getTime())) return value;
2588
+ return `${date.toLocaleDateString("en-GB", {
2589
+ day: "2-digit",
2590
+ month: "2-digit",
2591
+ year: "numeric",
2592
+ })}, ${date.toLocaleTimeString("en-GB", {
2593
+ hour: "2-digit",
2594
+ minute: "2-digit",
2595
+ hour12: false,
2596
+ })}`;
2597
+ }
2598
+
2599
+ function formatDashboardTime(value?: string) {
2600
+ if (!value) return "";
2601
+ const date = new Date(value);
2602
+ if (Number.isNaN(date.getTime())) return value;
2603
+ const now = Date.now();
2604
+ const diff = date.getTime() - now;
2605
+ const abs = Math.abs(diff);
2606
+ const minute = 60 * 1000;
2607
+ const hour = 60 * minute;
2608
+ const day = 24 * hour;
2609
+ if (abs < minute) return diff >= 0 ? "In a moment" : "Just now";
2610
+ if (abs < hour) {
2611
+ const minutes = Math.max(1, Math.round(abs / minute));
2612
+ return diff >= 0 ? `In ${minutes} minutes` : `${minutes} min ago`;
2613
+ }
2614
+ if (abs < day) {
2615
+ const hours = Math.max(1, Math.round(abs / hour));
2616
+ return diff >= 0 ? `In ${hours} hours` : `${hours} hr ago`;
2617
+ }
2618
+ return formatDashboardDateTime(value);
2619
+ }
1882
2620
  </script>
2621
+
2622
+ <style scoped>
2623
+ /* ── Figma Panel Styles (Property & Security modes) ────────────────────────── */
2624
+ .dashboard-card {
2625
+ background: #fff;
2626
+ border: 1px solid #d9e0e7;
2627
+ border-radius: 8px;
2628
+ box-shadow: 0 1px 2px rgba(15, 35, 54, 0.04);
2629
+ }
2630
+
2631
+ .dashboard-section-grid {
2632
+ margin-top: 14px;
2633
+ }
2634
+
2635
+ .dashboard-section-grid > .v-col {
2636
+ display: flex;
2637
+ }
2638
+
2639
+ .dashboard-panel {
2640
+ display: flex;
2641
+ flex-direction: column;
2642
+ height: 100%;
2643
+ min-height: 274px;
2644
+ padding: 28px 28px 26px;
2645
+ width: 100%;
2646
+ }
2647
+
2648
+ .figma-panel {
2649
+ border-color: #dde4ea;
2650
+ box-shadow: none;
2651
+ min-height: 276px;
2652
+ padding: 26px 28px 24px;
2653
+ }
2654
+
2655
+ .figma-panel-header {
2656
+ align-items: center;
2657
+ display: flex;
2658
+ gap: 16px;
2659
+ justify-content: space-between;
2660
+ min-height: 18px;
2661
+ }
2662
+
2663
+ .figma-panel-header h2 {
2664
+ color: #18324a;
2665
+ font-size: 12px;
2666
+ font-weight: 900;
2667
+ letter-spacing: 0;
2668
+ line-height: 1.2;
2669
+ margin: 0;
2670
+ text-transform: uppercase;
2671
+ }
2672
+
2673
+ .figma-panel-action,
2674
+ .figma-row-action {
2675
+ appearance: none;
2676
+ background: transparent;
2677
+ border: 0;
2678
+ color: #1976d2;
2679
+ cursor: pointer;
2680
+ font-size: 12px;
2681
+ font-weight: 800;
2682
+ line-height: 1;
2683
+ padding: 0;
2684
+ white-space: nowrap;
2685
+ }
2686
+
2687
+ .figma-panel-body {
2688
+ flex: 1;
2689
+ margin-top: 20px;
2690
+ }
2691
+
2692
+ .figma-list {
2693
+ display: grid;
2694
+ gap: 10px;
2695
+ }
2696
+
2697
+ .figma-list-row {
2698
+ align-items: center;
2699
+ background: #fff;
2700
+ border: 1px solid #e1e7ed;
2701
+ border-radius: 6px;
2702
+ display: grid;
2703
+ gap: 10px;
2704
+ grid-template-columns: 34px minmax(0, 1fr) minmax(38px, auto);
2705
+ min-height: 52px;
2706
+ padding: 8px 12px;
2707
+ transition: border-color 0.16s ease, box-shadow 0.16s ease;
2708
+ }
2709
+
2710
+ .figma-list-row:hover {
2711
+ border-color: #d5e0ea;
2712
+ box-shadow: 0 6px 14px rgba(15, 44, 69, 0.05);
2713
+ }
2714
+
2715
+ .figma-list-main {
2716
+ min-width: 0;
2717
+ }
2718
+
2719
+ .figma-list-main strong {
2720
+ color: #152f47;
2721
+ display: block;
2722
+ font-size: 12px;
2723
+ font-weight: 800;
2724
+ line-height: 1.25;
2725
+ overflow: hidden;
2726
+ text-overflow: ellipsis;
2727
+ white-space: nowrap;
2728
+ }
2729
+
2730
+ .figma-list-main p {
2731
+ color: #7b8895;
2732
+ font-size: 11px;
2733
+ font-weight: 700;
2734
+ line-height: 1.3;
2735
+ margin: 3px 0 0;
2736
+ overflow: hidden;
2737
+ text-overflow: ellipsis;
2738
+ white-space: nowrap;
2739
+ }
2740
+
2741
+ .figma-row-action {
2742
+ justify-self: end;
2743
+ }
2744
+
2745
+ .figma-event-dot {
2746
+ align-self: center;
2747
+ border-radius: 50%;
2748
+ display: inline-block;
2749
+ height: 20px;
2750
+ justify-self: center;
2751
+ width: 20px;
2752
+ }
2753
+
2754
+ .figma-event-dot-info,
2755
+ .figma-event-dot-neutral {
2756
+ background: #c9e6ff;
2757
+ border: 2px solid #77bef9;
2758
+ }
2759
+
2760
+ .figma-event-dot-success {
2761
+ background: #2fb84d;
2762
+ border: 2px solid #c9efd4;
2763
+ }
2764
+
2765
+ .figma-event-dot-danger {
2766
+ background: #ffd9d6;
2767
+ border: 2px solid #f9a6a0;
2768
+ }
2769
+
2770
+ .figma-event-dot-warning {
2771
+ background: #ffe9c7;
2772
+ border: 2px solid #f9c97c;
2773
+ }
2774
+
2775
+ .figma-icon-badge {
2776
+ align-items: center;
2777
+ border-radius: 50%;
2778
+ display: inline-flex;
2779
+ height: 28px;
2780
+ justify-content: center;
2781
+ justify-self: center;
2782
+ width: 28px;
2783
+ }
2784
+
2785
+ .figma-icon-badge-danger { background: #ffe0de; color: #d72f2f; }
2786
+ .figma-icon-badge-info { background: #e8e2ff; color: #6d4bd8; }
2787
+ .figma-icon-badge-success { background: #ddf7e6; color: #229852; }
2788
+ .figma-icon-badge-warning { background: #ffeed4; color: #c57916; }
2789
+ .figma-icon-badge-neutral { background: #edf2f6; color: #607386; }
2790
+
2791
+ .figma-status-stack {
2792
+ display: grid;
2793
+ gap: 22px;
2794
+ padding: 6px 2px 0;
2795
+ }
2796
+
2797
+ .figma-status-row {}
2798
+
2799
+ .figma-status-label {
2800
+ align-items: center;
2801
+ color: #263d53;
2802
+ display: flex;
2803
+ font-size: 12px;
2804
+ font-weight: 800;
2805
+ justify-content: space-between;
2806
+ line-height: 1.25;
2807
+ margin-bottom: 10px;
2808
+ }
2809
+
2810
+ .figma-status-label strong {
2811
+ color: #18324a;
2812
+ font-size: 12px;
2813
+ font-weight: 900;
2814
+ }
2815
+
2816
+ .figma-progress-track {
2817
+ background: #edf2f6;
2818
+ border-radius: 999px;
2819
+ height: 6px;
2820
+ overflow: hidden;
2821
+ width: 100%;
2822
+ }
2823
+
2824
+ .figma-progress-fill {
2825
+ border-radius: inherit;
2826
+ display: block;
2827
+ height: 100%;
2828
+ }
2829
+
2830
+ /* ── Property Feedback List ────────────────────────────────────────────────── */
2831
+ .feedback-list {
2832
+ display: grid;
2833
+ gap: 9px;
2834
+ }
2835
+
2836
+ .feedback-row {
2837
+ align-items: center;
2838
+ background: #fff;
2839
+ border: 1px solid #e0e5ea;
2840
+ border-radius: 8px;
2841
+ display: grid;
2842
+ gap: 16px;
2843
+ grid-template-columns: minmax(180px, 225px) minmax(130px, 175px) 100px auto;
2844
+ min-height: 66px;
2845
+ padding: 12px 14px;
2846
+ transition: border-color 0.16s ease, box-shadow 0.16s ease;
2847
+ }
2848
+
2849
+ .feedback-row:hover {
2850
+ border-color: #d8e3ec;
2851
+ box-shadow: 0 6px 14px rgba(15, 44, 69, 0.04);
2852
+ }
2853
+
2854
+ .feedback-summary { min-width: 0; }
2855
+
2856
+ .feedback-summary strong {
2857
+ color: #132b43;
2858
+ display: block;
2859
+ font-size: 12px;
2860
+ font-weight: 800;
2861
+ line-height: 1.25;
2862
+ overflow: hidden;
2863
+ text-overflow: ellipsis;
2864
+ white-space: nowrap;
2865
+ }
2866
+
2867
+ .feedback-summary p {
2868
+ color: #888;
2869
+ font-size: 12px;
2870
+ font-weight: 500;
2871
+ line-height: 1.35;
2872
+ margin: 5px 0 0;
2873
+ overflow: hidden;
2874
+ text-overflow: ellipsis;
2875
+ }
2876
+
2877
+ .feedback-person {
2878
+ align-items: center;
2879
+ display: flex;
2880
+ gap: 8px;
2881
+ min-width: 0;
2882
+ }
2883
+
2884
+ .feedback-person span {
2885
+ font-size: 13px;
2886
+ overflow: hidden;
2887
+ text-overflow: ellipsis;
2888
+ white-space: nowrap;
2889
+ }
2890
+
2891
+ .feedback-avatar {
2892
+ align-items: center;
2893
+ background: #d1e8ff;
2894
+ border-radius: 50%;
2895
+ color: #1d5f99;
2896
+ display: inline-flex;
2897
+ flex-shrink: 0;
2898
+ font-size: 11px;
2899
+ font-weight: 800;
2900
+ height: 30px;
2901
+ justify-content: center;
2902
+ width: 30px;
2903
+ }
2904
+
2905
+ .feedback-status-pill {
2906
+ border-radius: 20px;
2907
+ font-size: 12px;
2908
+ font-weight: 600;
2909
+ padding: 4px 10px;
2910
+ text-align: center;
2911
+ white-space: nowrap;
2912
+ }
2913
+
2914
+ .feedback-status-pill-completed { background: #72e3a1; color: #0f5132; }
2915
+ .feedback-status-pill-progress { background: #fde68a; color: #92400e; }
2916
+ .feedback-status-pill-danger { background: #fecaca; color: #b91c1c; }
2917
+ .feedback-status-pill-todo { background: #e2e8f0; color: #64748b; }
2918
+
2919
+ .feedback-view-button {
2920
+ appearance: none;
2921
+ background: transparent;
2922
+ border: 0;
2923
+ color: #2196f3;
2924
+ cursor: pointer;
2925
+ font-size: 12px;
2926
+ font-weight: 800;
2927
+ justify-self: end;
2928
+ line-height: 1;
2929
+ padding: 0;
2930
+ white-space: nowrap;
2931
+ }
2932
+ </style>