@7365admin1/layer-common 3.0.14 → 3.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,207 @@
1
1
  <template>
2
- <section class="unified-dashboard pb-8">
2
+ <div v-if="printMode" class="print-dashboard-view">
3
+ <!-- Header -->
4
+ <div class="print-header mb-6">
5
+ <h1 class="print-title">{{ title || dashboardTitle }}</h1>
6
+ <div class="print-subtitle">
7
+ <span>Site: {{ currentSiteName || "Selected site" }}</span>
8
+ <span class="ml-4">Period: {{ selectedRange }}</span>
9
+ </div>
10
+ </div>
11
+
12
+ <!-- KPI Cards (Statistics) -->
13
+ <div class="print-section-title">Overview Statistics</div>
14
+ <div class="print-kpi-grid">
15
+ <div
16
+ v-for="card in visibleKpiCards"
17
+ :key="card.key"
18
+ class="print-kpi-card"
19
+ >
20
+ <span class="print-kpi-title">{{ card.title }}</span>
21
+ <template
22
+ v-if="card.key === 'supplyAlert' && Array.isArray(card.raw?.items)"
23
+ >
24
+ <div class="d-flex flex-column ga-2 mt-2">
25
+ <div
26
+ v-for="item in card.raw.items"
27
+ :key="item._id || item.name"
28
+ class="d-flex justify-space-between text-body-2 text-blue-grey-darken-4 font-weight-medium"
29
+ >
30
+ <span>{{ item.name }}</span>
31
+ <span class="text-blue-grey">Balance: {{ item.qty }}</span>
32
+ </div>
33
+ </div>
34
+ </template>
35
+ <template v-else>
36
+ <div class="print-kpi-value">{{ card.value }}</div>
37
+ <div class="print-kpi-meta" v-if="card.meta || card.percentage">
38
+ {{ card.meta || "" }}
39
+ <span v-if="card.percentage"
40
+ >({{ card.percentage >= 0 ? "+" : ""
41
+ }}{{ card.percentage }}%)</span
42
+ >
43
+ </div>
44
+ </template>
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Activity Chart (Statistics) -->
49
+ <div v-if="hasChartData" class="print-chart-section mt-6">
50
+ <div class="print-section-title">{{ activityTitle }}</div>
51
+ <div class="print-chart-container">
52
+ <svg viewBox="0 0 920 260" class="d-block w-100" style="height: 260px">
53
+ <line
54
+ v-for="tick in chartTicks"
55
+ :key="tick"
56
+ x1="48"
57
+ :y1="tick"
58
+ x2="900"
59
+ :y2="tick"
60
+ style="stroke: #e9eef2; stroke-width: 1"
61
+ />
62
+ <text
63
+ v-for="label in chartYLabels"
64
+ :key="label.text"
65
+ x="40"
66
+ :y="label.y"
67
+ text-anchor="end"
68
+ alignment-baseline="middle"
69
+ style="fill: #7e8b96; font-size: 11px; font-weight: 500"
70
+ >
71
+ {{ label.text }}
72
+ </text>
73
+ <path
74
+ :d="primaryPath"
75
+ style="
76
+ fill: none;
77
+ stroke: #ff9b9b;
78
+ stroke-width: 3;
79
+ stroke-linecap: round;
80
+ stroke-linejoin: round;
81
+ "
82
+ />
83
+ <path
84
+ :d="secondaryPath"
85
+ style="
86
+ fill: none;
87
+ stroke: #e53935;
88
+ stroke-width: 3;
89
+ stroke-linecap: round;
90
+ stroke-linejoin: round;
91
+ "
92
+ />
93
+ <circle
94
+ v-for="point in primaryDots"
95
+ :key="`primary-${point.x}-${point.y}`"
96
+ :cx="point.x"
97
+ :cy="point.y"
98
+ r="4"
99
+ style="fill: #ff9b9b; stroke: #fff; stroke-width: 2"
100
+ />
101
+ <circle
102
+ v-for="point in secondaryDots"
103
+ :key="`secondary-${point.x}-${point.y}`"
104
+ :cx="point.x"
105
+ :cy="point.y"
106
+ r="4"
107
+ style="fill: #e53935; stroke: #fff; stroke-width: 2"
108
+ />
109
+ <text
110
+ v-for="label in chartAxisLabels"
111
+ :key="`${label.text}-${label.x}`"
112
+ :x="label.x"
113
+ y="246"
114
+ text-anchor="middle"
115
+ style="fill: #7e8b96; font-size: 12px; font-weight: 600"
116
+ >
117
+ {{ label.text }}
118
+ </text>
119
+ </svg>
120
+ <div class="d-flex align-center justify-center ga-5 mt-2">
121
+ <span
122
+ class="d-inline-flex align-center ga-2 text-caption font-weight-bold text-blue-grey"
123
+ >
124
+ <span
125
+ class="rounded-circle"
126
+ style="
127
+ width: 12px;
128
+ height: 12px;
129
+ display: inline-block;
130
+ background-color: #ff9b9b;
131
+ "
132
+ />
133
+ {{ primaryLegendLabel }}
134
+ </span>
135
+ <span
136
+ class="d-inline-flex align-center ga-2 text-caption font-weight-bold text-blue-grey"
137
+ >
138
+ <span
139
+ class="rounded-circle"
140
+ style="
141
+ width: 12px;
142
+ height: 12px;
143
+ display: inline-block;
144
+ background-color: #e53935;
145
+ "
146
+ />
147
+ {{ secondaryLegendLabel }}
148
+ </span>
149
+ </div>
150
+ </div>
151
+ </div>
152
+
153
+ <!-- Tables for List Widgets (Other data) -->
154
+ <div
155
+ v-for="section in printTableSections"
156
+ :key="section.key"
157
+ class="print-table-section mt-6"
158
+ >
159
+ <div class="print-section-title">{{ section.title }}</div>
160
+ <table class="print-table">
161
+ <thead>
162
+ <tr>
163
+ <th
164
+ v-for="column in section.columns"
165
+ :key="column.key"
166
+ :style="{ width: column.width }"
167
+ >
168
+ {{ column.label }}
169
+ </th>
170
+ </tr>
171
+ </thead>
172
+ <tbody>
173
+ <tr v-for="row in section.rows" :key="row.id">
174
+ <td
175
+ v-for="column in section.columns"
176
+ :key="`${row.id}-${column.key}`"
177
+ >
178
+ <template v-if="column.status">
179
+ <span
180
+ :class="[
181
+ 'print-status-badge',
182
+ `badge-${getStatusTone(row[column.key])}`,
183
+ ]"
184
+ >
185
+ {{ row[column.key] ?? column.fallback ?? "-" }}
186
+ </span>
187
+ </template>
188
+ <strong v-else-if="column.strong">{{
189
+ row[column.key] ?? "-"
190
+ }}</strong>
191
+ <template v-else>{{ row[column.key] ?? "-" }}</template>
192
+ </td>
193
+ </tr>
194
+ <tr v-if="!section.rows.length">
195
+ <td :colspan="section.columns.length" class="print-empty-cell">
196
+ No data available
197
+ </td>
198
+ </tr>
199
+ </tbody>
200
+ </table>
201
+ </div>
202
+ </div>
203
+
204
+ <section v-else class="unified-dashboard pb-8">
3
205
  <!-- ── Header ──────────────────────────────────────────────────────────── -->
4
206
  <div
5
207
  class="d-flex flex-column flex-md-row align-md-start justify-space-between ga-5 mb-6"
@@ -36,14 +238,16 @@
36
238
  color="blue-grey-darken-1"
37
239
  class="text-none font-weight-bold"
38
240
  append-icon="mdi-chevron-down"
241
+ :loading="isExporting"
242
+ :disabled="isExporting"
39
243
  >
40
244
  Export
41
245
  </v-btn>
42
246
  </template>
43
247
 
44
248
  <v-list density="compact" border rounded="lg" elevation="3">
45
- <v-list-item title="PDF" @click="notifyExport('PDF')" />
46
- <v-list-item title="Word" @click="notifyExport('Word')" />
249
+ <v-list-item title="PDF" @click="exportDashboard('PDF')" />
250
+ <v-list-item title="Word" @click="exportDashboard('Word')" />
47
251
  </v-list>
48
252
  </v-menu>
49
253
 
@@ -653,7 +857,15 @@
653
857
  <button
654
858
  class="figma-row-action"
655
859
  type="button"
656
- @click="navigateTo(dashboardPath('incident-reports'))"
860
+ @click="
861
+ navigateTo(
862
+ item.type === 'facility-booking'
863
+ ? `${dashboardPath('facility/bookings')}?booking=${
864
+ item.id
865
+ }`
866
+ : dashboardPath(`incident-reports/${item.id}`)
867
+ )
868
+ "
657
869
  >
658
870
  {{ item.actionLabel || "Review" }}
659
871
  </button>
@@ -763,9 +975,12 @@
763
975
  </button>
764
976
  </div>
765
977
  <div class="figma-panel-body">
766
- <div v-if="activePatrolItems.length" class="figma-list mt-2">
978
+ <div
979
+ v-if="formattedActivePatrolItems.length"
980
+ class="figma-list mt-2"
981
+ >
767
982
  <div
768
- v-for="item in activePatrolItems"
983
+ v-for="item in formattedActivePatrolItems"
769
984
  :key="item.id || item._id"
770
985
  class="d-flex align-center justify-space-between"
771
986
  style="
@@ -1109,7 +1324,11 @@
1109
1324
  </v-card>
1110
1325
  </v-dialog>
1111
1326
 
1112
- <v-snackbar v-model="snackbar.visible" :timeout="2200" color="primary">
1327
+ <v-snackbar
1328
+ v-model="snackbar.visible"
1329
+ :timeout="2200"
1330
+ :color="snackbar.color || 'primary'"
1331
+ >
1113
1332
  {{ snackbar.text }}
1114
1333
  </v-snackbar>
1115
1334
  </section>
@@ -1146,6 +1365,8 @@ type DashboardListItem = {
1146
1365
  tone?: string;
1147
1366
  color?: string;
1148
1367
  actionLabel?: string;
1368
+ type?: string;
1369
+ raw?: Record<string, any>;
1149
1370
  };
1150
1371
 
1151
1372
  type TaskItem = {
@@ -1176,6 +1397,22 @@ type FeedbackItem = {
1176
1397
  raw?: Record<string, any>;
1177
1398
  };
1178
1399
 
1400
+ type PrintTableColumn = {
1401
+ key: string;
1402
+ label: string;
1403
+ width: string;
1404
+ strong?: boolean;
1405
+ status?: boolean;
1406
+ fallback?: string;
1407
+ };
1408
+
1409
+ type PrintTableSection = {
1410
+ key: string;
1411
+ title: string;
1412
+ columns: PrintTableColumn[];
1413
+ rows: Array<Record<string, any>>;
1414
+ };
1415
+
1179
1416
  // ─── Props ────────────────────────────────────────────────────────────────────
1180
1417
 
1181
1418
  const props = defineProps({
@@ -1199,6 +1436,20 @@ const props = defineProps({
1199
1436
  title: { type: String, default: "" },
1200
1437
  extraWidgetKeys: { type: Array as () => string[], default: () => [] },
1201
1438
  showHeaderActions: { type: Boolean, default: true },
1439
+ appKey: {
1440
+ type: String as () =>
1441
+ | "PROPERTY"
1442
+ | "SECURITY"
1443
+ | "HYGIENE"
1444
+ | "MECHANICAL_ELECTRICAL"
1445
+ | "PEST_CONTROL"
1446
+ | "LANDSCAPING"
1447
+ | "POOL_MAINTENANCE",
1448
+ default: "",
1449
+ },
1450
+ orgSlug: { type: String, default: "" },
1451
+ siteSlug: { type: String, default: "" },
1452
+ printMode: { type: Boolean, default: false },
1202
1453
  });
1203
1454
 
1204
1455
  const emit = defineEmits<{
@@ -1229,6 +1480,8 @@ const {
1229
1480
  const { userAppRole } = useLocalSetup();
1230
1481
  const { hasPermission } = usePermission();
1231
1482
  const { dashboardPermissions } = useCommonPermissions();
1483
+ const { downloadPDF } = usePDFDownload();
1484
+ const route = useRoute();
1232
1485
 
1233
1486
  function canViewWidget(key: string) {
1234
1487
  if (!userAppRole.value) return true;
@@ -1236,21 +1489,36 @@ function canViewWidget(key: string) {
1236
1489
 
1237
1490
  const widgetToModulePermissionMap: Record<string, string[]> = {
1238
1491
  workOrders: ["work-orders:see-all-work-orders"],
1239
- incidents: ["incident-reports:see-incident-reports", "incident-reports:see-all-incident-reports"],
1492
+ incidents: [
1493
+ "incident-reports:see-incident-reports",
1494
+ "incident-reports:see-all-incident-reports",
1495
+ ],
1240
1496
  visitors: ["visitor:see-all-visitor"],
1241
1497
  facilityBookings: ["facility-booking:see-all-facility-booking"],
1242
- patrolCompliance: ["virtual-patrol:see-all-virtual-patrol-logs", "virtual-patrol:see-all-virtual-patrol-route"],
1498
+ patrolCompliance: [
1499
+ "virtual-patrol:see-all-virtual-patrol-logs",
1500
+ "virtual-patrol:see-all-virtual-patrol-route",
1501
+ ],
1243
1502
  staffStatus: ["attendance:see-all-attendance"],
1244
1503
  attendance: ["attendance:see-all-attendance"],
1245
1504
  feedbacks: ["feedbacks:see-all-feedbacks"],
1246
- upcomingEvents: ["bulletin-boards:see-all-bulletin-boards", "bulletin-board:see-all-bulletin-board"],
1505
+ upcomingEvents: [
1506
+ "bulletin-boards:see-all-bulletin-boards",
1507
+ "bulletin-board:see-all-bulletin-board",
1508
+ ],
1247
1509
  workOrderStatus: ["work-orders:see-all-work-orders"],
1248
- activePatrol: ["virtual-patrol:see-all-virtual-patrol-logs", "virtual-patrol:see-all-virtual-patrol-route"],
1249
- taskSchedule: ["cleaning-schedule-mgmt:see-all-schedules"]
1510
+ activePatrol: [
1511
+ "virtual-patrol:see-all-virtual-patrol-logs",
1512
+ "virtual-patrol:see-all-virtual-patrol-route",
1513
+ ],
1514
+ taskSchedule: ["cleaning-schedule-mgmt:see-all-schedules"],
1250
1515
  };
1251
1516
 
1252
1517
  const modulePerms = widgetToModulePermissionMap[key];
1253
- if (modulePerms && modulePerms.some(p => userAppRole.value.permissions?.includes(p))) {
1518
+ if (
1519
+ modulePerms &&
1520
+ modulePerms.some((p) => userAppRole.value.permissions?.includes(p))
1521
+ ) {
1254
1522
  return true;
1255
1523
  }
1256
1524
 
@@ -1284,13 +1552,24 @@ const router = useRouter();
1284
1552
  const runtimeConfig = useRuntimeConfig();
1285
1553
  const customizeDialog = ref(false);
1286
1554
  const widgetSearch = ref("");
1287
- const selectedRange = ref("Today");
1555
+ const periodLabelValue: Record<TDashboardValues, string> = {
1556
+ today: "Today",
1557
+ thisWeek: "This Week",
1558
+ thisMonth: "This Month",
1559
+ };
1560
+ const initialPeriod = (
1561
+ ["today", "thisWeek", "thisMonth"].includes(String(route.query.period))
1562
+ ? route.query.period
1563
+ : "today"
1564
+ ) as TDashboardValues;
1565
+ const selectedRange = ref(periodLabelValue[initialPeriod]);
1288
1566
  const visitorType = ref("All");
1289
1567
  const facilityType = ref("All");
1290
1568
  const visibleWidgetKeys = ref<string[]>([]);
1291
1569
  const draftVisibleWidgetKeys = ref<string[]>([]);
1292
1570
  const filterMenus = reactive<Record<string, boolean>>({});
1293
- const snackbar = reactive({ visible: false, text: "" });
1571
+ const snackbar = reactive({ visible: false, text: "", color: "" });
1572
+ const isExporting = ref(false);
1294
1573
  const chartContainerRef = ref<HTMLElement | null>(null);
1295
1574
  const chartWidth = ref(920);
1296
1575
  let resizeObserver: ResizeObserver | null = null;
@@ -1300,7 +1579,7 @@ const periodValue: Record<string, TDashboardValues> = {
1300
1579
  "This Week": "thisWeek",
1301
1580
  "This Month": "thisMonth",
1302
1581
  };
1303
- const currentPeriod = ref<TDashboardValues>("today");
1582
+ const currentPeriod = ref<TDashboardValues>(initialPeriod);
1304
1583
 
1305
1584
  const rangeOptions = [
1306
1585
  { label: "Today", value: "Today" },
@@ -2034,23 +2313,26 @@ function computeFacilityMetric(): DashboardMetric {
2034
2313
  ).toLowerCase();
2035
2314
  return val === selected.toLowerCase();
2036
2315
  });
2316
+ const ongoing = items.filter((i: any) => {
2317
+ const s = String(i.status || "").toLowerCase();
2318
+ return (
2319
+ s.includes("ongoing") ||
2320
+ s.includes("approved") ||
2321
+ s.includes("in-progress")
2322
+ );
2323
+ }).length;
2324
+ const waitingApproval = items.filter((i: any) => {
2325
+ const s = String(i.status || "").toLowerCase();
2326
+ return (
2327
+ s.includes("pending") || s.includes("review") || s.includes("approval")
2328
+ );
2329
+ }).length;
2330
+
2037
2331
  return {
2038
2332
  ...metric,
2039
- count: items.length,
2040
- ongoing: items.filter((i: any) => {
2041
- const s = String(i.status || "").toLowerCase();
2042
- return (
2043
- s.includes("ongoing") ||
2044
- s.includes("approved") ||
2045
- s.includes("in-progress")
2046
- );
2047
- }).length,
2048
- waitingApproval: items.filter((i: any) => {
2049
- const s = String(i.status || "").toLowerCase();
2050
- return (
2051
- s.includes("pending") || s.includes("review") || s.includes("approval")
2052
- );
2053
- }).length,
2333
+ count: ongoing + waitingApproval,
2334
+ ongoing,
2335
+ waitingApproval,
2054
2336
  };
2055
2337
  }
2056
2338
 
@@ -2329,10 +2611,166 @@ const workOrderStatus = computed(() => {
2329
2611
  const activePatrolItems = computed<any[]>(
2330
2612
  () => finalDashboardData.value?.activePatrolItems ?? []
2331
2613
  );
2614
+
2615
+ const formattedActivePatrolItems = computed<any[]>(() => {
2616
+ return activePatrolItems.value.map((item) => {
2617
+ let status = item.status;
2618
+ if (Array.isArray(status)) {
2619
+ status = status.join(", ");
2620
+ }
2621
+ let person = item.person || item.assignedTo || "Unassigned";
2622
+ if (Array.isArray(person)) {
2623
+ person = person.join(", ");
2624
+ }
2625
+ return {
2626
+ ...item,
2627
+ status,
2628
+ person,
2629
+ };
2630
+ });
2631
+ });
2332
2632
  const securityStaffItems = computed<any[]>(
2333
2633
  () => attendanceReq.value?.items ?? []
2334
2634
  );
2335
2635
 
2636
+ const printTableSections = computed<PrintTableSection[]>(() => {
2637
+ const sections: PrintTableSection[] = [];
2638
+
2639
+ if (isHygieneMode.value) {
2640
+ sections.push(
2641
+ {
2642
+ key: "taskSchedule",
2643
+ title: primaryListConfig.value.title,
2644
+ columns: [
2645
+ { key: "title", label: "Task Title", width: "25%", strong: true },
2646
+ { key: "subtitle", label: "Description / Subtitle", width: "30%" },
2647
+ { key: "person", label: "Assigned Staff", width: "20%" },
2648
+ { key: "time", label: "Time", width: "15%" },
2649
+ { key: "status", label: "Status", width: "10%", status: true },
2650
+ ],
2651
+ rows: taskScheduleItems.value,
2652
+ },
2653
+ {
2654
+ key: "staffAttendance",
2655
+ title: "Staff's Total Attendance",
2656
+ columns: [
2657
+ { key: "name", label: "Staff Name", width: "40%", strong: true },
2658
+ { key: "role", label: "Role", width: "40%" },
2659
+ { key: "totalHoursLabel", label: "Total Hours", width: "20%" },
2660
+ ],
2661
+ rows: staffStatusItems.value.map((item) => ({
2662
+ ...item,
2663
+ totalHoursLabel: `${item.totalHours || 0} hrs`,
2664
+ })),
2665
+ }
2666
+ );
2667
+ }
2668
+
2669
+ if (isPropertyMode.value) {
2670
+ sections.push(
2671
+ {
2672
+ key: "upcomingEvents",
2673
+ title: "Upcoming Events",
2674
+ columns: [
2675
+ { key: "title", label: "Event Title", width: "40%", strong: true },
2676
+ { key: "subtitle", label: "Description / Subtitle", width: "60%" },
2677
+ ],
2678
+ rows: upcomingEvents.value,
2679
+ },
2680
+ {
2681
+ key: "todayReminders",
2682
+ title: "Today's Reminder",
2683
+ columns: [
2684
+ { key: "title", label: "Reminder", width: "40%", strong: true },
2685
+ { key: "subtitle", label: "Details / Subtitle", width: "60%" },
2686
+ ],
2687
+ rows: todayReminders.value,
2688
+ },
2689
+ {
2690
+ key: "todayAttentions",
2691
+ title: "Today's Attentions",
2692
+ columns: [
2693
+ { key: "title", label: "Attention", width: "40%", strong: true },
2694
+ { key: "subtitle", label: "Details / Subtitle", width: "60%" },
2695
+ ],
2696
+ rows: todayAttentions.value,
2697
+ },
2698
+ {
2699
+ key: "workOrderStatus",
2700
+ title: "Work Order Status",
2701
+ columns: [
2702
+ { key: "label", label: "Status", width: "50%", strong: true },
2703
+ { key: "value", label: "Count / Value", width: "50%" },
2704
+ ],
2705
+ rows: workOrderStatus.value,
2706
+ }
2707
+ );
2708
+ }
2709
+
2710
+ if (isSecurityMode.value) {
2711
+ sections.push(
2712
+ {
2713
+ key: "activePatrol",
2714
+ title: "Active Patrol",
2715
+ columns: [
2716
+ { key: "title", label: "Patrol Route", width: "30%", strong: true },
2717
+ { key: "subtitle", label: "Location", width: "30%" },
2718
+ { key: "person", label: "Assigned Officer", width: "25%" },
2719
+ { key: "status", label: "Status", width: "15%", status: true },
2720
+ ],
2721
+ rows: activePatrolItems.value.map((item, index) => ({
2722
+ id: item.id || item._id || `active-patrol-${index}`,
2723
+ title: item.title || item.name || "Patrol",
2724
+ subtitle: item.subtitle || item.location || "",
2725
+ person: item.person || item.assignedTo || "Unassigned",
2726
+ status: item.status || "To-Do",
2727
+ })),
2728
+ },
2729
+ {
2730
+ key: "staffStatus",
2731
+ title: "Staff Status",
2732
+ columns: [
2733
+ { key: "name", label: "Officer Name", width: "40%", strong: true },
2734
+ { key: "role", label: "Role / Designation", width: "40%" },
2735
+ {
2736
+ key: "status",
2737
+ label: "Status",
2738
+ width: "20%",
2739
+ status: true,
2740
+ fallback: "On duty",
2741
+ },
2742
+ ],
2743
+ rows: securityStaffItems.value.map((item, index) => ({
2744
+ id: item.id || item._id || `security-staff-${index}`,
2745
+ name: item.userName || item.personName || "Unassigned",
2746
+ role: item.role || item.designation || "Officer",
2747
+ status: item.status || "On duty",
2748
+ })),
2749
+ }
2750
+ );
2751
+ }
2752
+
2753
+ sections.push({
2754
+ key: "feedbacks",
2755
+ title: "Feedbacks",
2756
+ columns: [
2757
+ { key: "title", label: "Title", width: "30%", strong: true },
2758
+ { key: "subtitle", label: "Description / Category", width: "30%" },
2759
+ { key: "person", label: "Reporter / Person", width: "25%" },
2760
+ {
2761
+ key: "status",
2762
+ label: "Status",
2763
+ width: "15%",
2764
+ status: true,
2765
+ fallback: "To-Do",
2766
+ },
2767
+ ],
2768
+ rows: feedbackItems.value,
2769
+ });
2770
+
2771
+ return sections;
2772
+ });
2773
+
2336
2774
  // ─── ResizeObserver ───────────────────────────────────────────────────────────
2337
2775
 
2338
2776
  watch(chartContainerRef, (el) => {
@@ -2413,7 +2851,9 @@ function getMetricMeta(metric: DashboardMetric) {
2413
2851
  parts.push(`${formatNumber(toNumber(metric.ongoing))} ongoing`);
2414
2852
  if (typeof metric.waitingApproval !== "undefined")
2415
2853
  parts.push(
2416
- `${formatNumber(toNumber(metric.waitingApproval))} waiting approval`
2854
+ `${formatNumber(toNumber(metric.waitingApproval))} ${
2855
+ isPropertyMode.value ? "for approval" : "waiting approval"
2856
+ }`
2417
2857
  );
2418
2858
  if (typeof metric.lowStock !== "undefined")
2419
2859
  parts.push(`${formatNumber(toNumber(metric.lowStock))} low stock`);
@@ -2446,6 +2886,10 @@ function getFilteredVisitorMetric(): DashboardMetric {
2446
2886
  };
2447
2887
  }
2448
2888
 
2889
+ if (!items || !items.length) {
2890
+ return metric;
2891
+ }
2892
+
2449
2893
  const filtered = items.filter(
2450
2894
  (item: any) => getVisitorFilterValue(item) === selected
2451
2895
  );
@@ -2487,6 +2931,8 @@ function normalizeListItems(keys: string[]): DashboardListItem[] {
2487
2931
  icon: item.icon || getListIcon(context),
2488
2932
  tone: item.tone,
2489
2933
  actionLabel: item.actionLabel,
2934
+ type: item.type,
2935
+ raw: item,
2490
2936
  }));
2491
2937
  }
2492
2938
 
@@ -2865,16 +3311,27 @@ function dashboardPath(path: string) {
2865
3311
  return `/${props.orgId}/${props.site}/${path}`;
2866
3312
  }
2867
3313
 
3314
+ function getSchedulePath() {
3315
+ const service = props.serviceType;
3316
+ if (service === "cleaning_services") return "cleaning-schedule";
3317
+ if (service === "landscaping_services") return "landscape-schedule";
3318
+ if (service === "pest_control_services") return "pest-schedule";
3319
+ if (service === "pool_maintenance_services") return "pool-schedule";
3320
+ if (service === "mechanical_electrical_services")
3321
+ return "technician-schedule";
3322
+ return "schedule-task";
3323
+ }
3324
+
2868
3325
  function goToDashboardRoute(routeKey: string) {
2869
3326
  if (!props.orgId || !props.site) return;
2870
3327
  const pathMap: Record<string, string> = {
2871
3328
  attendance: "attendance",
2872
3329
  feedbacks: "feedbacks",
2873
- scheduleTask: "schedule-task",
3330
+ scheduleTask: getSchedulePath(),
2874
3331
  virtualPatrol: "virtual-patrol/logs",
2875
3332
  manpower: "manpower-monitoring/attendance",
2876
3333
  incidentReports: "incident-reports",
2877
- "virtual-patrol": "virtual-patrol",
3334
+ "virtual-patrol": "virtual-patrol/logs",
2878
3335
  "work-orders": "work-orders",
2879
3336
  "event-mgmt": "event-mgmt",
2880
3337
  calendar: "calendar",
@@ -2896,10 +3353,15 @@ function goToListItem(item: TaskItem | FeedbackItem, routeKey: string) {
2896
3353
  }
2897
3354
  const pathMap: Record<string, string> = {
2898
3355
  feedbacks: "feedbacks",
2899
- scheduleTask: "schedule-task",
3356
+ scheduleTask: getSchedulePath(),
2900
3357
  };
2901
3358
  const path = pathMap[routeKey] || routeKey;
2902
- router.push(`/${props.orgId}/${props.site}/${path}/${id}`);
3359
+ const scheduleId = item.raw?.schedule?._id || item.raw?.schedule;
3360
+ if (routeKey === "scheduleTask" && scheduleId) {
3361
+ router.push(`/${props.orgId}/${props.site}/${path}/${scheduleId}/${id}`);
3362
+ } else {
3363
+ router.push(`/${props.orgId}/${props.site}/${path}/${id}`);
3364
+ }
2903
3365
  }
2904
3366
 
2905
3367
  // ─── Widget Management ────────────────────────────────────────────────────────
@@ -2930,9 +3392,332 @@ function submitCustomize() {
2930
3392
  customizeDialog.value = false;
2931
3393
  }
2932
3394
 
2933
- function notifyExport(format: string) {
2934
- snackbar.text = `${format} export is being prepared.`;
3395
+ // ─── Dashboard Export ─────────────────────────────────────────────────────────
3396
+
3397
+ const appKeyMap: Record<
3398
+ string,
3399
+ | "PROPERTY"
3400
+ | "SECURITY"
3401
+ | "HYGIENE"
3402
+ | "MECHANICAL_ELECTRICAL"
3403
+ | "PEST_CONTROL"
3404
+ | "LANDSCAPING"
3405
+ | "POOL_MAINTENANCE"
3406
+ > = {
3407
+ property_management_agency: "PROPERTY",
3408
+ security_agency: "SECURITY",
3409
+ cleaning_services: "HYGIENE",
3410
+ mechanical_electrical_services: "MECHANICAL_ELECTRICAL",
3411
+ landscaping_services: "LANDSCAPING",
3412
+ pest_control_services: "PEST_CONTROL",
3413
+ pool_maintenance_services: "POOL_MAINTENANCE",
3414
+ };
3415
+
3416
+ async function exportDashboard(format: "PDF" | "Word") {
3417
+ if (isExporting.value) return;
3418
+ isExporting.value = true;
3419
+
3420
+ snackbar.text = `Generating ${format} report, please wait...`;
3421
+ snackbar.color = "info";
2935
3422
  snackbar.visible = true;
3423
+
3424
+ const periodLabel =
3425
+ selectedRange.value === "Today"
3426
+ ? "Today"
3427
+ : selectedRange.value === "This Week"
3428
+ ? "This Week"
3429
+ : "This Month";
3430
+
3431
+ const reportTitle = `${props.title || dashboardTitle.value} - ${periodLabel}`;
3432
+
3433
+ try {
3434
+ if (format === "PDF") {
3435
+ const org = props.orgSlug || props.orgId || "";
3436
+ const site = props.siteSlug || props.site || "";
3437
+ const printUrl = `/${org}/${site}/print-dashboard?period=${currentPeriod.value}`;
3438
+
3439
+ const targetAppKey =
3440
+ props.appKey || appKeyMap[props.serviceType] || "PROPERTY";
3441
+
3442
+ await downloadPDF({
3443
+ url: printUrl,
3444
+ title: reportTitle,
3445
+ orientation: "L",
3446
+ appKey: targetAppKey,
3447
+ });
3448
+
3449
+ snackbar.text = "PDF exported successfully.";
3450
+ snackbar.color = "success";
3451
+ snackbar.visible = true;
3452
+ } else {
3453
+ const org = props.orgSlug || props.orgId || "";
3454
+ const site = props.siteSlug || props.site || "";
3455
+ const siteName = props.currentSiteName || "Selected Site";
3456
+
3457
+ let wordHtml = `
3458
+ <html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'>
3459
+ <head>
3460
+ <meta charset="utf-8">
3461
+ <title>${reportTitle}</title>
3462
+ <style>
3463
+ body { font-family: 'Segoe UI', Arial, sans-serif; color: #1e293b; line-height: 1.5; padding: 20px; }
3464
+ h1 { color: #0b2f47; font-size: 20pt; margin-bottom: 5px; font-weight: bold; }
3465
+ .subtitle { color: #64748b; font-size: 11pt; margin-bottom: 25px; border-bottom: 1px solid #e2e8f0; padding-bottom: 15px; }
3466
+ .section-title { color: #0b2f47; font-size: 14pt; font-weight: bold; margin-top: 25px; margin-bottom: 12px; border-bottom: 2px solid #0b2f47; padding-bottom: 3px; }
3467
+
3468
+ .kpi-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
3469
+ .kpi-table td { border: 1px solid #cbd5e1; padding: 12px; vertical-align: top; width: 25%; background-color: #f8fafc; }
3470
+ .kpi-title { font-size: 8.5pt; text-transform: uppercase; color: #64748b; font-weight: bold; margin-bottom: 6px; }
3471
+ .kpi-value { font-size: 16pt; font-weight: bold; color: #0b2f47; }
3472
+ .kpi-meta { font-size: 8.5pt; color: #64748b; margin-top: 4px; }
3473
+
3474
+ .data-table { width: 100%; border-collapse: collapse; margin-bottom: 25px; }
3475
+ .data-table th { background-color: #0b2f47; color: #ffffff; padding: 10px; font-size: 9.5pt; font-weight: bold; text-align: left; border: 1px solid #0b2f47; }
3476
+ .data-table td { padding: 10px; font-size: 9.5pt; border: 1px solid #cbd5e1; text-align: left; vertical-align: top; }
3477
+ .data-table tr:nth-child(even) { background-color: #f8fafc; }
3478
+
3479
+ .status-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 8.5pt; font-weight: bold; }
3480
+ .badge-success { background-color: #d1fae5; color: #065f46; }
3481
+ .badge-info { background-color: #e0f2fe; color: #075985; }
3482
+ .badge-danger, .badge-error { background-color: #fee2e2; color: #991b1b; }
3483
+ .badge-warning { background-color: #fef3c7; color: #92400e; }
3484
+ .badge-neutral { background-color: #f1f5f9; color: #475569; }
3485
+ </style>
3486
+ </head>
3487
+ <body>
3488
+ <h1>${props.title || dashboardTitle.value}</h1>
3489
+ <div class="subtitle">Site: ${siteName} | Period: ${periodLabel}</div>
3490
+ `;
3491
+
3492
+ if (visibleKpiCards.value && visibleKpiCards.value.length > 0) {
3493
+ wordHtml += `<div class="section-title">Overview Statistics</div>`;
3494
+ wordHtml += `<table class="kpi-table"><tr>`;
3495
+ visibleKpiCards.value.forEach((card, index) => {
3496
+ if (index > 0 && index % 4 === 0) {
3497
+ wordHtml += `</tr><tr>`;
3498
+ }
3499
+ let kpiValueHtml = `<div class="kpi-value">${card.value}</div>`;
3500
+ if (card.key === "supplyAlert" && Array.isArray(card.raw?.items)) {
3501
+ kpiValueHtml = '<div style="font-size: 9pt; margin-top: 4px;">';
3502
+ card.raw.items.forEach((item: any) => {
3503
+ kpiValueHtml += `<div><strong>${item.name}</strong>: Balance ${item.qty}</div>`;
3504
+ });
3505
+ kpiValueHtml += "</div>";
3506
+ }
3507
+ wordHtml += `
3508
+ <td>
3509
+ <div class="kpi-title">${card.title}</div>
3510
+ ${kpiValueHtml}
3511
+ <div class="kpi-meta">${card.meta || ""} ${
3512
+ card.percentage
3513
+ ? `(${card.percentage >= 0 ? "+" : ""}${card.percentage}%)`
3514
+ : ""
3515
+ }</div>
3516
+ </td>
3517
+ `;
3518
+ });
3519
+ const remaining = 4 - (visibleKpiCards.value.length % 4);
3520
+ if (remaining < 4) {
3521
+ for (let i = 0; i < remaining; i++) {
3522
+ wordHtml += `<td></td>`;
3523
+ }
3524
+ }
3525
+ wordHtml += `</tr></table>`;
3526
+ }
3527
+
3528
+ if (hasChartData.value) {
3529
+ wordHtml += `<div class="section-title">${activityTitle.value}</div>`;
3530
+ wordHtml += `<table class="data-table">`;
3531
+ wordHtml += `<thead><tr><th>Period / Day</th><th>${primaryLegendLabel.value}</th><th>${secondaryLegendLabel.value}</th></tr></thead><tbody>`;
3532
+
3533
+ const labels = chartAxisLabels.value || [];
3534
+ const workOrders =
3535
+ finalDashboardData.value.activityChart?.workOrders || [];
3536
+ const taskCompleted =
3537
+ finalDashboardData.value.activityChart?.taskCompleted || [];
3538
+
3539
+ labels.forEach((labelObj, idx) => {
3540
+ const lbl = labelObj.text;
3541
+ const woVal = workOrders[idx] !== undefined ? workOrders[idx] : 0;
3542
+ const tcVal =
3543
+ taskCompleted[idx] !== undefined ? taskCompleted[idx] : 0;
3544
+ wordHtml += `<tr><td><strong>${lbl}</strong></td><td>${woVal}</td><td>${tcVal}</td></tr>`;
3545
+ });
3546
+ wordHtml += `</tbody></table>`;
3547
+ }
3548
+
3549
+ if (isHygieneMode.value) {
3550
+ if (taskScheduleItems.value.length > 0) {
3551
+ wordHtml += `<div class="section-title">${primaryListConfig.value.title}</div>`;
3552
+ wordHtml += `<table class="data-table">`;
3553
+ wordHtml += `<thead><tr><th>Task Title</th><th>Description / Subtitle</th><th>Assigned Staff</th><th>Time</th><th>Status</th></tr></thead><tbody>`;
3554
+ taskScheduleItems.value.forEach((item) => {
3555
+ wordHtml += `<tr>
3556
+ <td><strong>${item.title}</strong></td>
3557
+ <td>${item.subtitle || ""}</td>
3558
+ <td>${item.person || ""}</td>
3559
+ <td>${item.time || ""}</td>
3560
+ <td><span class="status-badge badge-${getStatusTone(
3561
+ item.status
3562
+ )}">${item.status || ""}</span></td>
3563
+ </tr>`;
3564
+ });
3565
+ wordHtml += `</tbody></table>`;
3566
+ }
3567
+
3568
+ if (staffStatusItems.value.length > 0) {
3569
+ wordHtml += `<div class="section-title">Staff's Total Attendance</div>`;
3570
+ wordHtml += `<table class="data-table">`;
3571
+ wordHtml += `<thead><tr><th>Staff Name</th><th>Role</th><th>Total Hours</th></tr></thead><tbody>`;
3572
+ staffStatusItems.value.forEach((item) => {
3573
+ wordHtml += `<tr>
3574
+ <td><strong>${item.name}</strong></td>
3575
+ <td>${item.role || ""}</td>
3576
+ <td>${item.totalHours || 0} hrs</td>
3577
+ </tr>`;
3578
+ });
3579
+ wordHtml += `</tbody></table>`;
3580
+ }
3581
+ }
3582
+
3583
+ if (isPropertyMode.value) {
3584
+ if (upcomingEvents.value.length > 0) {
3585
+ wordHtml += `<div class="section-title">Upcoming Events</div>`;
3586
+ wordHtml += `<table class="data-table">`;
3587
+ wordHtml += `<thead><tr><th>Event Title</th><th>Description / Subtitle</th></tr></thead><tbody>`;
3588
+ upcomingEvents.value.forEach((item) => {
3589
+ wordHtml += `<tr>
3590
+ <td><strong>${item.title}</strong></td>
3591
+ <td>${item.subtitle || ""}</td>
3592
+ </tr>`;
3593
+ });
3594
+ wordHtml += `</tbody></table>`;
3595
+ }
3596
+
3597
+ if (todayReminders.value.length > 0) {
3598
+ wordHtml += `<div class="section-title">Today's Reminder</div>`;
3599
+ wordHtml += `<table class="data-table">`;
3600
+ wordHtml += `<thead><tr><th>Reminder</th><th>Details / Subtitle</th></tr></thead><tbody>`;
3601
+ todayReminders.value.forEach((item) => {
3602
+ wordHtml += `<tr>
3603
+ <td><strong>${item.title}</strong></td>
3604
+ <td>${item.subtitle || ""}</td>
3605
+ </tr>`;
3606
+ });
3607
+ wordHtml += `</tbody></table>`;
3608
+ }
3609
+
3610
+ if (todayAttentions.value.length > 0) {
3611
+ wordHtml += `<div class="section-title">Today's Attentions</div>`;
3612
+ wordHtml += `<table class="data-table">`;
3613
+ wordHtml += `<thead><tr><th>Attention</th><th>Details / Subtitle</th></tr></thead><tbody>`;
3614
+ todayAttentions.value.forEach((item) => {
3615
+ wordHtml += `<tr>
3616
+ <td><strong>${item.title}</strong></td>
3617
+ <td>${item.subtitle || ""}</td>
3618
+ </tr>`;
3619
+ });
3620
+ wordHtml += `</tbody></table>`;
3621
+ }
3622
+
3623
+ if (workOrderStatus.value.length > 0) {
3624
+ wordHtml += `<div class="section-title">Work Order Status</div>`;
3625
+ wordHtml += `<table class="data-table">`;
3626
+ wordHtml += `<thead><tr><th>Status</th><th>Count / Value</th></tr></thead><tbody>`;
3627
+ workOrderStatus.value.forEach((item) => {
3628
+ wordHtml += `<tr>
3629
+ <td><strong>${item.label}</strong></td>
3630
+ <td>${item.value || 0}</td>
3631
+ </tr>`;
3632
+ });
3633
+ wordHtml += `</tbody></table>`;
3634
+ }
3635
+ }
3636
+
3637
+ if (isSecurityMode.value) {
3638
+ if (activePatrolItems.value.length > 0) {
3639
+ wordHtml += `<div class="section-title">Active Patrol</div>`;
3640
+ wordHtml += `<table class="data-table">`;
3641
+ wordHtml += `<thead><tr><th>Patrol Route</th><th>Location</th><th>Assigned Officer</th><th>Status</th></tr></thead><tbody>`;
3642
+ activePatrolItems.value.forEach((item) => {
3643
+ wordHtml += `<tr>
3644
+ <td><strong>${item.title || item.name}</strong></td>
3645
+ <td>${item.subtitle || item.location || ""}</td>
3646
+ <td>${item.person || item.assignedTo || ""}</td>
3647
+ <td><span class="status-badge badge-${getStatusTone(
3648
+ item.status
3649
+ )}">${item.status || ""}</span></td>
3650
+ </tr>`;
3651
+ });
3652
+ wordHtml += `</tbody></table>`;
3653
+ }
3654
+
3655
+ if (securityStaffItems.value.length > 0) {
3656
+ wordHtml += `<div class="section-title">Staff Status</div>`;
3657
+ wordHtml += `<table class="data-table">`;
3658
+ wordHtml += `<thead><tr><th>Officer Name</th><th>Role / Designation</th><th>Status</th></tr></thead><tbody>`;
3659
+ securityStaffItems.value.forEach((item) => {
3660
+ wordHtml += `<tr>
3661
+ <td><strong>${
3662
+ item.userName || item.personName || "Unassigned"
3663
+ }</strong></td>
3664
+ <td>${item.role || item.designation || "Officer"}</td>
3665
+ <td><span class="status-badge badge-${getStatusTone(
3666
+ item.status
3667
+ )}">${item.status || "On duty"}</span></td>
3668
+ </tr>`;
3669
+ });
3670
+ wordHtml += `</tbody></table>`;
3671
+ }
3672
+ }
3673
+
3674
+ if (feedbackItems.value.length > 0) {
3675
+ wordHtml += `<div class="section-title">Feedbacks</div>`;
3676
+ wordHtml += `<table class="data-table">`;
3677
+ wordHtml += `<thead><tr><th>Title</th><th>Description / Category</th><th>Reporter / Person</th><th>Status</th></tr></thead><tbody>`;
3678
+ feedbackItems.value.forEach((item) => {
3679
+ wordHtml += `<tr>
3680
+ <td><strong>${item.title}</strong></td>
3681
+ <td>${item.subtitle || ""}</td>
3682
+ <td>${item.person || ""}</td>
3683
+ <td><span class="status-badge badge-${getStatusTone(
3684
+ item.status
3685
+ )}">${item.status || "To-Do"}</span></td>
3686
+ </tr>`;
3687
+ });
3688
+ wordHtml += `</tbody></table>`;
3689
+ }
3690
+
3691
+ wordHtml += `
3692
+ </body>
3693
+ </html>
3694
+ `;
3695
+
3696
+ const blob = new Blob([wordHtml], {
3697
+ type: "application/msword;charset=utf-8",
3698
+ });
3699
+ const downloadUrl = window.URL.createObjectURL(blob);
3700
+ const a = document.createElement("a");
3701
+ a.href = downloadUrl;
3702
+ a.download = `${reportTitle}.doc`;
3703
+ document.body.appendChild(a);
3704
+ a.click();
3705
+ document.body.removeChild(a);
3706
+ window.URL.revokeObjectURL(downloadUrl);
3707
+
3708
+ snackbar.text = "Word export downloaded successfully.";
3709
+ snackbar.color = "success";
3710
+ snackbar.visible = true;
3711
+ }
3712
+ } catch (err: any) {
3713
+ snackbar.text = `Export failed: ${
3714
+ err?.message || "Unknown error"
3715
+ }. Please try again.`;
3716
+ snackbar.color = "error";
3717
+ snackbar.visible = true;
3718
+ } finally {
3719
+ isExporting.value = false;
3720
+ }
2936
3721
  }
2937
3722
 
2938
3723
  // ─── Card Color Helpers ───────────────────────────────────────────────────────
@@ -3089,6 +3874,164 @@ function formatDashboardTime(value?: string) {
3089
3874
  </script>
3090
3875
 
3091
3876
  <style scoped>
3877
+ /* Print Dashboard View Styles */
3878
+ .print-dashboard-view {
3879
+ background-color: #ffffff;
3880
+ color: #1e293b;
3881
+ font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
3882
+ line-height: 1.5;
3883
+ width: 100%;
3884
+ }
3885
+
3886
+ .print-header {
3887
+ border-bottom: 2px solid #e2e8f0;
3888
+ padding-bottom: 16px;
3889
+ margin-bottom: 24px;
3890
+ }
3891
+
3892
+ .print-title {
3893
+ color: #0b2f47;
3894
+ font-size: 24px;
3895
+ font-weight: 800;
3896
+ margin: 0;
3897
+ }
3898
+
3899
+ .print-subtitle {
3900
+ color: #64748b;
3901
+ font-size: 14px;
3902
+ margin-top: 6px;
3903
+ display: flex;
3904
+ gap: 16px;
3905
+ }
3906
+
3907
+ .print-section-title {
3908
+ color: #0b2f47;
3909
+ font-size: 16px;
3910
+ font-weight: 700;
3911
+ margin-top: 28px;
3912
+ margin-bottom: 12px;
3913
+ text-transform: uppercase;
3914
+ letter-spacing: 0.5px;
3915
+ border-left: 4px solid #0b2f47;
3916
+ padding-left: 10px;
3917
+ }
3918
+
3919
+ .print-kpi-grid {
3920
+ display: grid;
3921
+ grid-template-columns: repeat(4, 1fr);
3922
+ gap: 16px;
3923
+ margin-bottom: 24px;
3924
+ }
3925
+
3926
+ .print-kpi-card {
3927
+ background-color: #f8fafc;
3928
+ border: 1px solid #e2e8f0;
3929
+ border-radius: 8px;
3930
+ padding: 16px;
3931
+ min-height: 100px;
3932
+ }
3933
+
3934
+ .print-kpi-title {
3935
+ color: #64748b;
3936
+ font-size: 11px;
3937
+ font-weight: 700;
3938
+ text-transform: uppercase;
3939
+ margin-bottom: 8px;
3940
+ display: block;
3941
+ }
3942
+
3943
+ .print-kpi-value {
3944
+ color: #0b2f47;
3945
+ font-size: 24px;
3946
+ font-weight: 800;
3947
+ line-height: 1.2;
3948
+ }
3949
+
3950
+ .print-kpi-meta {
3951
+ color: #64748b;
3952
+ font-size: 11px;
3953
+ margin-top: 6px;
3954
+ }
3955
+
3956
+ .print-chart-container {
3957
+ background-color: #f8fafc;
3958
+ border: 1px solid #e2e8f0;
3959
+ border-radius: 8px;
3960
+ padding: 16px;
3961
+ margin-bottom: 24px;
3962
+ }
3963
+
3964
+ .print-table {
3965
+ width: 100%;
3966
+ border-collapse: collapse;
3967
+ margin-bottom: 24px;
3968
+ }
3969
+
3970
+ .print-table th {
3971
+ background-color: #0b2f47;
3972
+ color: #ffffff;
3973
+ -webkit-print-color-adjust: exact;
3974
+ print-color-adjust: exact;
3975
+ font-size: 12px;
3976
+ font-weight: 700;
3977
+ text-align: left;
3978
+ padding: 10px 12px;
3979
+ border: 1px solid #0b2f47;
3980
+ }
3981
+
3982
+ .print-table td {
3983
+ font-size: 12px;
3984
+ padding: 10px 12px;
3985
+ border: 1px solid #e2e8f0;
3986
+ color: #334155;
3987
+ vertical-align: top;
3988
+ }
3989
+
3990
+ .print-table .print-empty-cell {
3991
+ color: #64748b;
3992
+ font-weight: 600;
3993
+ text-align: center;
3994
+ }
3995
+
3996
+ .print-table tr:nth-child(even) td {
3997
+ background-color: #f8fafc;
3998
+ }
3999
+
4000
+ .print-status-badge {
4001
+ display: inline-block;
4002
+ font-size: 11px;
4003
+ font-weight: 600;
4004
+ padding: 2px 8px;
4005
+ border-radius: 4px;
4006
+ text-transform: capitalize;
4007
+ }
4008
+
4009
+ .badge-success {
4010
+ background-color: #d1fae5;
4011
+ color: #065f46;
4012
+ }
4013
+
4014
+ .badge-info {
4015
+ background-color: #e0f2fe;
4016
+ color: #075985;
4017
+ }
4018
+
4019
+ .badge-danger,
4020
+ .badge-error {
4021
+ background-color: #fee2e2;
4022
+ color: #991b1b;
4023
+ }
4024
+
4025
+ .badge-warning {
4026
+ background-color: #fef3c7;
4027
+ color: #92400e;
4028
+ }
4029
+
4030
+ .badge-neutral {
4031
+ background-color: #f1f5f9;
4032
+ color: #475569;
4033
+ }
4034
+
3092
4035
  .dashboard-card {
3093
4036
  background: #fff;
3094
4037
  border: 1px solid #d9e0e7;