@7365admin1/layer-common 3.0.15 → 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.
- package/CHANGELOG.md +6 -0
- package/components/ClientDetailForm.vue +831 -0
- package/components/ClientMain.vue +273 -206
- package/components/DashboardMain.vue +904 -12
- package/components/OnlineFormFill.vue +51 -1
- package/composables/useOrg.ts +17 -1
- package/composables/usePDFDownload.ts +23 -3
- package/package.json +1 -1
|
@@ -1,5 +1,207 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
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="
|
|
46
|
-
<v-list-item title="Word" @click="
|
|
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
|
|
978
|
+
<div
|
|
979
|
+
v-if="formattedActivePatrolItems.length"
|
|
980
|
+
class="figma-list mt-2"
|
|
981
|
+
>
|
|
775
982
|
<div
|
|
776
|
-
v-for="item in
|
|
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
|
|
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
|
|
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>(
|
|
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")
|
|
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
|
-
|
|
2985
|
-
|
|
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;
|