@7365admin1/layer-common 3.0.15 → 3.0.17

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
 
@@ -771,9 +975,12 @@
771
975
  </button>
772
976
  </div>
773
977
  <div class="figma-panel-body">
774
- <div v-if="activePatrolItems.length" class="figma-list mt-2">
978
+ <div
979
+ v-if="formattedActivePatrolItems.length"
980
+ class="figma-list mt-2"
981
+ >
775
982
  <div
776
- v-for="item in activePatrolItems"
983
+ v-for="item in formattedActivePatrolItems"
777
984
  :key="item.id || item._id"
778
985
  class="d-flex align-center justify-space-between"
779
986
  style="
@@ -1117,7 +1324,11 @@
1117
1324
  </v-card>
1118
1325
  </v-dialog>
1119
1326
 
1120
- <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
+ >
1121
1332
  {{ snackbar.text }}
1122
1333
  </v-snackbar>
1123
1334
  </section>
@@ -1186,6 +1397,22 @@ type FeedbackItem = {
1186
1397
  raw?: Record<string, any>;
1187
1398
  };
1188
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
+
1189
1416
  // ─── Props ────────────────────────────────────────────────────────────────────
1190
1417
 
1191
1418
  const props = defineProps({
@@ -1209,6 +1436,20 @@ const props = defineProps({
1209
1436
  title: { type: String, default: "" },
1210
1437
  extraWidgetKeys: { type: Array as () => string[], default: () => [] },
1211
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 },
1212
1453
  });
1213
1454
 
1214
1455
  const emit = defineEmits<{
@@ -1239,6 +1480,8 @@ const {
1239
1480
  const { userAppRole } = useLocalSetup();
1240
1481
  const { hasPermission } = usePermission();
1241
1482
  const { dashboardPermissions } = useCommonPermissions();
1483
+ const { downloadPDF } = usePDFDownload();
1484
+ const route = useRoute();
1242
1485
 
1243
1486
  function canViewWidget(key: string) {
1244
1487
  if (!userAppRole.value) return true;
@@ -1309,13 +1552,24 @@ const router = useRouter();
1309
1552
  const runtimeConfig = useRuntimeConfig();
1310
1553
  const customizeDialog = ref(false);
1311
1554
  const widgetSearch = ref("");
1312
- 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]);
1313
1566
  const visitorType = ref("All");
1314
1567
  const facilityType = ref("All");
1315
1568
  const visibleWidgetKeys = ref<string[]>([]);
1316
1569
  const draftVisibleWidgetKeys = ref<string[]>([]);
1317
1570
  const filterMenus = reactive<Record<string, boolean>>({});
1318
- const snackbar = reactive({ visible: false, text: "" });
1571
+ const snackbar = reactive({ visible: false, text: "", color: "" });
1572
+ const isExporting = ref(false);
1319
1573
  const chartContainerRef = ref<HTMLElement | null>(null);
1320
1574
  const chartWidth = ref(920);
1321
1575
  let resizeObserver: ResizeObserver | null = null;
@@ -1325,7 +1579,7 @@ const periodValue: Record<string, TDashboardValues> = {
1325
1579
  "This Week": "thisWeek",
1326
1580
  "This Month": "thisMonth",
1327
1581
  };
1328
- const currentPeriod = ref<TDashboardValues>("today");
1582
+ const currentPeriod = ref<TDashboardValues>(initialPeriod);
1329
1583
 
1330
1584
  const rangeOptions = [
1331
1585
  { label: "Today", value: "Today" },
@@ -2357,10 +2611,166 @@ const workOrderStatus = computed(() => {
2357
2611
  const activePatrolItems = computed<any[]>(
2358
2612
  () => finalDashboardData.value?.activePatrolItems ?? []
2359
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
+ });
2360
2632
  const securityStaffItems = computed<any[]>(
2361
2633
  () => attendanceReq.value?.items ?? []
2362
2634
  );
2363
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
+
2364
2774
  // ─── ResizeObserver ───────────────────────────────────────────────────────────
2365
2775
 
2366
2776
  watch(chartContainerRef, (el) => {
@@ -2907,7 +3317,8 @@ function getSchedulePath() {
2907
3317
  if (service === "landscaping_services") return "landscape-schedule";
2908
3318
  if (service === "pest_control_services") return "pest-schedule";
2909
3319
  if (service === "pool_maintenance_services") return "pool-schedule";
2910
- if (service === "mechanical_electrical_services") return "technician-schedule";
3320
+ if (service === "mechanical_electrical_services")
3321
+ return "technician-schedule";
2911
3322
  return "schedule-task";
2912
3323
  }
2913
3324
 
@@ -2981,9 +3392,332 @@ function submitCustomize() {
2981
3392
  customizeDialog.value = false;
2982
3393
  }
2983
3394
 
2984
- function notifyExport(format: string) {
2985
- 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";
2986
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
+ }
2987
3721
  }
2988
3722
 
2989
3723
  // ─── Card Color Helpers ───────────────────────────────────────────────────────
@@ -3140,6 +3874,164 @@ function formatDashboardTime(value?: string) {
3140
3874
  </script>
3141
3875
 
3142
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
+
3143
4035
  .dashboard-card {
3144
4036
  background: #fff;
3145
4037
  border: 1px solid #d9e0e7;