@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.
- package/CHANGELOG.md +12 -0
- package/components/ClientDetailForm.vue +831 -0
- package/components/ClientMain.vue +273 -206
- package/components/DashboardMain.vue +981 -38
- 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
|
|
|
@@ -653,7 +857,15 @@
|
|
|
653
857
|
<button
|
|
654
858
|
class="figma-row-action"
|
|
655
859
|
type="button"
|
|
656
|
-
@click="
|
|
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
|
|
978
|
+
<div
|
|
979
|
+
v-if="formattedActivePatrolItems.length"
|
|
980
|
+
class="figma-list mt-2"
|
|
981
|
+
>
|
|
767
982
|
<div
|
|
768
|
-
v-for="item in
|
|
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
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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: [
|
|
1249
|
-
|
|
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 (
|
|
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
|
|
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>(
|
|
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:
|
|
2040
|
-
ongoing
|
|
2041
|
-
|
|
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))}
|
|
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:
|
|
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:
|
|
3356
|
+
scheduleTask: getSchedulePath(),
|
|
2900
3357
|
};
|
|
2901
3358
|
const path = pathMap[routeKey] || routeKey;
|
|
2902
|
-
|
|
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
|
-
|
|
2934
|
-
|
|
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;
|