@7365admin1/layer-common 3.0.8 → 3.0.10

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