@7365admin1/layer-common 4.108.0 → 4.109.1
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 +20 -0
- package/assets/css/screens.css +11 -0
- package/components/BulletinBoardForm.vue +112 -0
- package/components/BulletinBoardView.vue +2 -0
- package/components/BulletinEventRsvp.vue +126 -0
- package/components/DashboardMain.vue +7 -7
- package/components/Nfc/NFCPatrolReportMain.vue +61 -72
- package/components/Nfc/NFCPatrolRouteMain.vue +21 -31
- package/components/Nfc/NFCRouteScheduleTimes.vue +83 -0
- package/components/Nfc/PatrolReport/DailyReportTab.vue +24 -3
- package/components/Nfc/PatrolReport/MonthlyReportTab.vue +32 -3
- package/components/Nfc/PatrolReport/MonthlyReportTable.vue +29 -1
- package/components/Nfc/PatrolReport/PatrolActivityTable.vue +24 -2
- package/components/Nfc/PatrolReport/PatrolMonthDialog.vue +195 -0
- package/components/Nfc/PatrolReport/PatrolReportTab.vue +11 -2
- package/components/Nfc/PatrolReport/PatrolRouteDetailsDialog.vue +214 -0
- package/components/Nfc/PatrolReport/ReportLocationHeader.vue +4 -1
- package/components/Nfc/PatrolReport/SkipPhotoDetails.vue +137 -0
- package/components/Nfc/PatrolReport/SkipPhotoThumb.vue +132 -0
- package/components/Nfc/PatrolReport/SummaryReportTable.vue +42 -1
- package/components/SiteSettings.vue +4 -0
- package/components/VehicleManagement.vue +4 -1
- package/composables/useNFCPatrolDailyReport.ts +4 -18
- package/composables/useNFCPatrolMonthlyReport.ts +34 -5
- package/composables/useNFCPatrolReport.ts +5 -74
- package/composables/useReportPdf.ts +220 -0
- package/package.json +1 -1
- package/types/bulletin-board.d.ts +11 -1
- package/types/nfc-patrol-report.ts +14 -0
- package/utils/nav-sections.ts +1 -0
- package/utils/nfc-patrol-report.ts +239 -1
- package/utils/nfc-patrol-schedule.ts +73 -0
- package/utils/report-export.ts +120 -0
|
@@ -6,7 +6,12 @@ import type {
|
|
|
6
6
|
} from "../types/nfc-patrol-report";
|
|
7
7
|
import useNFCPatrolLog from "./useNFCPatrolLog";
|
|
8
8
|
import useSite from "./useSite";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
collectAllPages,
|
|
11
|
+
mapLogsToMonthlyRows,
|
|
12
|
+
mapLogsToMonthRuns,
|
|
13
|
+
monthIndexOf,
|
|
14
|
+
} from "../utils/nfc-patrol-report";
|
|
10
15
|
|
|
11
16
|
interface NFCPatrolLogListResponse {
|
|
12
17
|
items?: Record<string, any>[];
|
|
@@ -48,9 +53,15 @@ export function useNFCPatrolMonthlyReport(
|
|
|
48
53
|
*/
|
|
49
54
|
const failed = ref(false);
|
|
50
55
|
|
|
56
|
+
// The year's patrol logs, kept so a month's row can open its list without
|
|
57
|
+
// asking the server again. Not deep-reactive: nothing edits them.
|
|
58
|
+
const logs = shallowRef<Record<string, any>[]>([]);
|
|
59
|
+
let siteTimezone: string | undefined;
|
|
60
|
+
|
|
51
61
|
async function fetchReport() {
|
|
52
62
|
if (!site || !filters.route) {
|
|
53
63
|
report.value = null;
|
|
64
|
+
logs.value = [];
|
|
54
65
|
notFound.value = false;
|
|
55
66
|
failed.value = false;
|
|
56
67
|
return;
|
|
@@ -63,7 +74,7 @@ export function useNFCPatrolMonthlyReport(
|
|
|
63
74
|
try {
|
|
64
75
|
// The whole year, not page one of it. `collectAllPages` reads the page
|
|
65
76
|
// count out of the first reply and walks the rest.
|
|
66
|
-
const [
|
|
77
|
+
const [yearLogs, siteInfo] = await Promise.all([
|
|
67
78
|
collectAllPages((page) =>
|
|
68
79
|
getPatrolLogs({
|
|
69
80
|
page,
|
|
@@ -77,8 +88,16 @@ export function useNFCPatrolMonthlyReport(
|
|
|
77
88
|
getSiteById(site),
|
|
78
89
|
]);
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
logs.value = yearLogs;
|
|
92
|
+
siteTimezone = (siteInfo as { timezone?: string }).timezone;
|
|
93
|
+
|
|
94
|
+
if (yearLogs.length) {
|
|
95
|
+
const routeName = yearLogs[0]?.route?.name ?? "-";
|
|
96
|
+
const runs = Array<number>(12).fill(0);
|
|
97
|
+
for (const log of yearLogs) {
|
|
98
|
+
const month = monthIndexOf(log);
|
|
99
|
+
if (!Number.isNaN(month)) runs[month]++;
|
|
100
|
+
}
|
|
82
101
|
|
|
83
102
|
report.value = {
|
|
84
103
|
location: {
|
|
@@ -86,7 +105,10 @@ export function useNFCPatrolMonthlyReport(
|
|
|
86
105
|
address: buildAddress(siteInfo.address),
|
|
87
106
|
},
|
|
88
107
|
title: `Patrol Route Summary: ${routeName}`,
|
|
89
|
-
rows: mapLogsToMonthlyRows(
|
|
108
|
+
rows: mapLogsToMonthlyRows(yearLogs).map((row, month) => ({
|
|
109
|
+
...row,
|
|
110
|
+
runs: runs[month],
|
|
111
|
+
})),
|
|
90
112
|
};
|
|
91
113
|
} else {
|
|
92
114
|
report.value = null;
|
|
@@ -94,6 +116,7 @@ export function useNFCPatrolMonthlyReport(
|
|
|
94
116
|
}
|
|
95
117
|
} catch {
|
|
96
118
|
report.value = null;
|
|
119
|
+
logs.value = [];
|
|
97
120
|
notFound.value = false;
|
|
98
121
|
failed.value = true;
|
|
99
122
|
} finally {
|
|
@@ -101,8 +124,14 @@ export function useNFCPatrolMonthlyReport(
|
|
|
101
124
|
}
|
|
102
125
|
}
|
|
103
126
|
|
|
127
|
+
/** The patrol runs of one month (0-11), in the order they ran. */
|
|
128
|
+
function monthRuns(monthIndex: number) {
|
|
129
|
+
return mapLogsToMonthRuns(logs.value, monthIndex, siteTimezone);
|
|
130
|
+
}
|
|
131
|
+
|
|
104
132
|
return {
|
|
105
133
|
report,
|
|
134
|
+
monthRuns,
|
|
106
135
|
loading,
|
|
107
136
|
notFound,
|
|
108
137
|
failed,
|
|
@@ -5,12 +5,7 @@ import type {
|
|
|
5
5
|
} from "../types/nfc-patrol-report";
|
|
6
6
|
import useNFCPatrolLog from "./useNFCPatrolLog";
|
|
7
7
|
import useSite from "./useSite";
|
|
8
|
-
import {
|
|
9
|
-
actualPatrolSpan,
|
|
10
|
-
checkpointClock,
|
|
11
|
-
clockTime,
|
|
12
|
-
DEFAULT_PATROL_TIMEZONE,
|
|
13
|
-
} from "../utils/nfc-patrol-time";
|
|
8
|
+
import { mapLogToPatrolRun } from "../utils/nfc-patrol-report";
|
|
14
9
|
|
|
15
10
|
interface NFCPatrolLogListResponse {
|
|
16
11
|
items?: Record<string, any>[];
|
|
@@ -18,69 +13,6 @@ interface NFCPatrolLogListResponse {
|
|
|
18
13
|
pageRange?: string;
|
|
19
14
|
}
|
|
20
15
|
|
|
21
|
-
function formatDuration(totalMinutes: number) {
|
|
22
|
-
if (totalMinutes < 60) return `${totalMinutes} mins`;
|
|
23
|
-
|
|
24
|
-
const hrs = Math.floor(totalMinutes / 60);
|
|
25
|
-
const mins = totalMinutes % 60;
|
|
26
|
-
|
|
27
|
-
if (mins === 0) return `${hrs} hrs`;
|
|
28
|
-
return `${hrs} hrs ${mins} mins`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function diffMinutes(startIso?: string | Date, endIso?: string | Date) {
|
|
32
|
-
if (!startIso || !endIso) return "-";
|
|
33
|
-
const totalMinutes = Math.max(
|
|
34
|
-
0,
|
|
35
|
-
Math.round((new Date(endIso).getTime() - new Date(startIso).getTime()) / 60000),
|
|
36
|
-
);
|
|
37
|
-
return formatDuration(totalMinutes);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
function mapLogToPatrolReport(
|
|
42
|
-
log: Record<string, any>,
|
|
43
|
-
location: NFCPatrolReport["location"],
|
|
44
|
-
siteTimezone?: string,
|
|
45
|
-
): NFCPatrolReport {
|
|
46
|
-
const checkpoints: Record<string, any>[] = log.checkPoints ?? [];
|
|
47
|
-
const timezone = log.timezone || siteTimezone || DEFAULT_PATROL_TIMEZONE;
|
|
48
|
-
const actual = actualPatrolSpan(log);
|
|
49
|
-
|
|
50
|
-
const activities: NFCPatrolActivity[] = checkpoints.map((cp) => {
|
|
51
|
-
const clock = checkpointClock(log, cp, siteTimezone);
|
|
52
|
-
return {
|
|
53
|
-
id: cp.nfcTag_id,
|
|
54
|
-
checkpoint: cp.nfcTag_name,
|
|
55
|
-
guardName: log.sign?.guardName,
|
|
56
|
-
guardSignatureFileId: log.sign?.guardSignatureFileId,
|
|
57
|
-
startTime: clock.start,
|
|
58
|
-
endTime: clock.end,
|
|
59
|
-
status: cp.status,
|
|
60
|
-
remarks: cp.skippedRemarks && cp.skippedRemarks !== "N/A" ? cp.skippedRemarks : undefined,
|
|
61
|
-
};
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
location,
|
|
66
|
-
routeSummary: {
|
|
67
|
-
routeName: log.route?.name,
|
|
68
|
-
tolerance: Number(log.route?.tolerance) || 0,
|
|
69
|
-
schedule: {
|
|
70
|
-
start: log.route?.startTime,
|
|
71
|
-
end: log.route?.endTime,
|
|
72
|
-
totalTime: diffMinutes(log.startDateTime, log.endDateTime),
|
|
73
|
-
},
|
|
74
|
-
actual: {
|
|
75
|
-
start: clockTime(actual.start, timezone),
|
|
76
|
-
end: clockTime(actual.end, timezone),
|
|
77
|
-
totalTime: diffMinutes(actual.start, actual.end),
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
activities,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
16
|
export function useNFCPatrolReport(
|
|
85
17
|
filters: NFCPatrolReportFilters,
|
|
86
18
|
site: string,
|
|
@@ -128,14 +60,13 @@ export function useNFCPatrolReport(
|
|
|
128
60
|
const log = logsRes.items?.[0];
|
|
129
61
|
|
|
130
62
|
if (log) {
|
|
131
|
-
report.value =
|
|
132
|
-
|
|
133
|
-
{
|
|
63
|
+
report.value = {
|
|
64
|
+
location: {
|
|
134
65
|
name: siteInfo.name,
|
|
135
66
|
address: buildAddress(siteInfo.address),
|
|
136
67
|
},
|
|
137
|
-
(siteInfo as { timezone?: string }).timezone,
|
|
138
|
-
|
|
68
|
+
...mapLogToPatrolRun(log, (siteInfo as { timezone?: string }).timezone),
|
|
69
|
+
};
|
|
139
70
|
} else {
|
|
140
71
|
report.value = null;
|
|
141
72
|
notFound.value = true;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import html2canvas from "html2canvas";
|
|
2
|
+
import jsPDF from "jspdf";
|
|
3
|
+
import { formatGeneratedAt, planPdfPages, type PdfTable } from "../utils/report-export";
|
|
4
|
+
|
|
5
|
+
export type ReportPdfMeta = {
|
|
6
|
+
/** "Patrol Report" */
|
|
7
|
+
title: string;
|
|
8
|
+
site?: string;
|
|
9
|
+
address?: string;
|
|
10
|
+
/** Shown top right of every page: Route, Date, Time... */
|
|
11
|
+
details: Array<{ label: string; value: string }>;
|
|
12
|
+
fileName: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A report area (`.report-export`) as an A4 PDF.
|
|
17
|
+
*
|
|
18
|
+
* The area is drawn the way the old export drew it - html2canvas, one tall
|
|
19
|
+
* image - but onto a page rather than across it:
|
|
20
|
+
*
|
|
21
|
+
* - laid out at one width whatever the screen, so a PDF from a laptop and
|
|
22
|
+
* one from a 4K monitor read the same, and always in the light theme;
|
|
23
|
+
* - inside margins, under a header (report, site, route, date) and over a
|
|
24
|
+
* footer (when it was made, page X of Y) drawn as real text on every page;
|
|
25
|
+
* - cut only between rows, photos and sections (`planPdfPages`), repeating a
|
|
26
|
+
* table's header row on the page it carries on to;
|
|
27
|
+
* - without the on-screen site card and dividers: the header says it.
|
|
28
|
+
*
|
|
29
|
+
* Everything PDF-only happens on html2canvas's clone of the page, so the
|
|
30
|
+
* screen never changes. `report-export--pdf` marks that clone: under it
|
|
31
|
+
* `data-pdf-only` shows (hidden on screen by screens.css) and `data-pdf-hide`
|
|
32
|
+
* goes. A row marked `data-pdf-keep-with-next` is never the last on a page,
|
|
33
|
+
* so a checkpoint stays with the skip photo drawn under it.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const RENDER_WIDTH = 1000; // CSS px the report is laid out at for the PDF
|
|
37
|
+
const SCALE = 2;
|
|
38
|
+
|
|
39
|
+
// A4 portrait, in mm.
|
|
40
|
+
const PAGE_W = 210;
|
|
41
|
+
const PAGE_H = 297;
|
|
42
|
+
const MARGIN = 12;
|
|
43
|
+
const CONTENT_TOP = 38;
|
|
44
|
+
const CONTENT_BOTTOM = PAGE_H - MARGIN - 9;
|
|
45
|
+
const CONTENT_W = PAGE_W - MARGIN * 2;
|
|
46
|
+
const DETAILS_LABEL_X = 134;
|
|
47
|
+
const DETAILS_VALUE_X = 150;
|
|
48
|
+
|
|
49
|
+
const INK: [number, number, number] = [20, 22, 26];
|
|
50
|
+
const MUTED: [number, number, number] = [100, 104, 112];
|
|
51
|
+
const RULE: [number, number, number] = [220, 222, 226];
|
|
52
|
+
|
|
53
|
+
const PDF_CSS = `
|
|
54
|
+
.report-export--pdf { width: ${RENDER_WIDTH}px !important; max-width: none !important; background: #fff; }
|
|
55
|
+
.report-export--pdf > .v-divider, .report-export--pdf [data-pdf-hide] { display: none !important; }
|
|
56
|
+
.report-export--pdf tr[data-pdf-only] { display: table-row !important; }
|
|
57
|
+
.report-export--pdf [data-pdf-only]:not(tr) { display: block !important; }
|
|
58
|
+
.report-export--pdf tr[data-pdf-keep-with-next] > td { border-bottom-color: transparent !important; }
|
|
59
|
+
.report-export--pdf .table-card, .report-export--pdf .app-card { box-shadow: none !important; }
|
|
60
|
+
.report-export--pdf .v-table__wrapper { overflow: visible !important; }
|
|
61
|
+
.report-export--pdf tbody tr:hover > td { background: transparent !important; }
|
|
62
|
+
/* html2canvas draws digits at their plain widths but places them where the
|
|
63
|
+
page laid out tabular ones, so "10:33" came out "10 :33". */
|
|
64
|
+
.report-export--pdf * { font-variant-numeric: normal !important; }
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
/** Where the report can be cut, and where its tables are, measured on the clone. */
|
|
68
|
+
function measure(clone: HTMLElement) {
|
|
69
|
+
const origin = clone.getBoundingClientRect().top;
|
|
70
|
+
const top = (el: Element) => el.getBoundingClientRect().top - origin;
|
|
71
|
+
const bottom = (el: Element) => el.getBoundingClientRect().bottom - origin;
|
|
72
|
+
|
|
73
|
+
// Between sections, between table rows (the last row ends with its card,
|
|
74
|
+
// and a row stays with the skip photos under it), and between lines of
|
|
75
|
+
// skip photos. Never right under a heading.
|
|
76
|
+
const breaks = [
|
|
77
|
+
...Array.from(clone.children),
|
|
78
|
+
...Array.from(clone.querySelectorAll("tbody tr:not(:last-child):not([data-pdf-keep-with-next])")),
|
|
79
|
+
...Array.from(clone.querySelectorAll(".skip-details__item")),
|
|
80
|
+
].map(bottom);
|
|
81
|
+
|
|
82
|
+
const tables: PdfTable[] = Array.from(clone.querySelectorAll("table")).flatMap((table) => {
|
|
83
|
+
const head = table.querySelector("thead");
|
|
84
|
+
return head
|
|
85
|
+
? [{ top: top(table), bottom: bottom(table), head: { top: top(head), bottom: bottom(head) } }]
|
|
86
|
+
: [];
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return { height: clone.getBoundingClientRect().height, breaks, tables };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function drawHeader(pdf: jsPDF, meta: ReportPdfMeta) {
|
|
93
|
+
pdf.setTextColor(...INK);
|
|
94
|
+
pdf.setFont("helvetica", "bold");
|
|
95
|
+
pdf.setFontSize(15);
|
|
96
|
+
pdf.text(meta.title, MARGIN, MARGIN + 5);
|
|
97
|
+
|
|
98
|
+
let y = MARGIN + 11.5;
|
|
99
|
+
if (meta.site) {
|
|
100
|
+
pdf.setFontSize(10);
|
|
101
|
+
pdf.text(pdf.splitTextToSize(meta.site, DETAILS_LABEL_X - MARGIN - 6)[0], MARGIN, y);
|
|
102
|
+
y += 4.5;
|
|
103
|
+
}
|
|
104
|
+
if (meta.address) {
|
|
105
|
+
pdf.setFont("helvetica", "normal");
|
|
106
|
+
pdf.setFontSize(8.5);
|
|
107
|
+
pdf.setTextColor(...MUTED);
|
|
108
|
+
pdf.text(pdf.splitTextToSize(meta.address, DETAILS_LABEL_X - MARGIN - 6).slice(0, 2), MARGIN, y);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let dy = MARGIN + 4;
|
|
112
|
+
for (const { label, value } of meta.details) {
|
|
113
|
+
pdf.setFont("helvetica", "normal");
|
|
114
|
+
pdf.setFontSize(8.5);
|
|
115
|
+
pdf.setTextColor(...MUTED);
|
|
116
|
+
pdf.text(label, DETAILS_LABEL_X, dy);
|
|
117
|
+
|
|
118
|
+
const lines: string[] = pdf.splitTextToSize(value || "-", PAGE_W - MARGIN - DETAILS_VALUE_X);
|
|
119
|
+
pdf.setFont("helvetica", "bold");
|
|
120
|
+
pdf.setTextColor(...INK);
|
|
121
|
+
pdf.text(lines.length > 1 ? `${lines[0].replace(/\s+$/, "")}…` : lines[0], DETAILS_VALUE_X, dy);
|
|
122
|
+
dy += 5;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pdf.setDrawColor(...RULE);
|
|
126
|
+
pdf.setLineWidth(0.3);
|
|
127
|
+
pdf.line(MARGIN, CONTENT_TOP - 4, PAGE_W - MARGIN, CONTENT_TOP - 4);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function drawFooter(pdf: jsPDF, page: number, pages: number, generatedAt: string) {
|
|
131
|
+
const y = PAGE_H - MARGIN;
|
|
132
|
+
pdf.setDrawColor(...RULE);
|
|
133
|
+
pdf.setLineWidth(0.3);
|
|
134
|
+
pdf.line(MARGIN, y - 5, PAGE_W - MARGIN, y - 5);
|
|
135
|
+
|
|
136
|
+
pdf.setFont("helvetica", "normal");
|
|
137
|
+
pdf.setFontSize(8);
|
|
138
|
+
pdf.setTextColor(...MUTED);
|
|
139
|
+
pdf.text(`Generated ${generatedAt}`, MARGIN, y);
|
|
140
|
+
pdf.text(`Page ${page} of ${pages}`, PAGE_W - MARGIN, y, { align: "right" });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default function useReportPdf() {
|
|
144
|
+
async function exportReportPdf(report: HTMLElement, meta: ReportPdfMeta) {
|
|
145
|
+
let layout = { height: 0, breaks: [] as number[], tables: [] as PdfTable[] };
|
|
146
|
+
|
|
147
|
+
const canvas = await html2canvas(report, {
|
|
148
|
+
scale: SCALE,
|
|
149
|
+
useCORS: true,
|
|
150
|
+
backgroundColor: "#fff",
|
|
151
|
+
windowWidth: Math.max(window.innerWidth, RENDER_WIDTH + 200),
|
|
152
|
+
onclone: (doc, clone) => {
|
|
153
|
+
// A PDF is read on white: the dark theme's tokens hang off this class.
|
|
154
|
+
doc.querySelectorAll(".v-theme--dark").forEach((el) => {
|
|
155
|
+
el.classList.replace("v-theme--dark", "v-theme--light");
|
|
156
|
+
});
|
|
157
|
+
const style = doc.createElement("style");
|
|
158
|
+
style.textContent = PDF_CSS;
|
|
159
|
+
doc.head.appendChild(style);
|
|
160
|
+
clone.classList.add("report-export--pdf");
|
|
161
|
+
layout = measure(clone);
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Canvas px per report px, and report px per mm on the page.
|
|
166
|
+
const k = canvas.height / layout.height;
|
|
167
|
+
const pxPerMm = canvas.width / k / CONTENT_W;
|
|
168
|
+
const pages = planPdfPages(
|
|
169
|
+
layout.height,
|
|
170
|
+
(CONTENT_BOTTOM - CONTENT_TOP) * pxPerMm,
|
|
171
|
+
layout.breaks,
|
|
172
|
+
layout.tables,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const pdf = new jsPDF({ unit: "mm", format: "a4", orientation: "portrait" });
|
|
176
|
+
|
|
177
|
+
pages.forEach((page, index) => {
|
|
178
|
+
if (index > 0) pdf.addPage();
|
|
179
|
+
|
|
180
|
+
const headPx = page.head ? page.head.bottom - page.head.top : 0;
|
|
181
|
+
const bodyPx = page.bottom - page.top;
|
|
182
|
+
const part = document.createElement("canvas");
|
|
183
|
+
part.width = canvas.width;
|
|
184
|
+
part.height = Math.max(1, Math.round((headPx + bodyPx) * k));
|
|
185
|
+
|
|
186
|
+
const ctx = part.getContext("2d");
|
|
187
|
+
if (!ctx) return;
|
|
188
|
+
ctx.fillStyle = "#fff";
|
|
189
|
+
ctx.fillRect(0, 0, part.width, part.height);
|
|
190
|
+
if (page.head) {
|
|
191
|
+
const h = Math.round(headPx * k);
|
|
192
|
+
ctx.drawImage(canvas, 0, Math.round(page.head.top * k), canvas.width, h, 0, 0, canvas.width, h);
|
|
193
|
+
}
|
|
194
|
+
const bodyTop = Math.round(page.top * k);
|
|
195
|
+
const bodyH = Math.round(page.bottom * k) - bodyTop;
|
|
196
|
+
ctx.drawImage(canvas, 0, bodyTop, canvas.width, bodyH, 0, Math.round(headPx * k), canvas.width, bodyH);
|
|
197
|
+
|
|
198
|
+
pdf.addImage(
|
|
199
|
+
part.toDataURL("image/jpeg", 0.92),
|
|
200
|
+
"JPEG",
|
|
201
|
+
MARGIN,
|
|
202
|
+
CONTENT_TOP,
|
|
203
|
+
CONTENT_W,
|
|
204
|
+
part.height / k / pxPerMm,
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const generatedAt = formatGeneratedAt();
|
|
209
|
+
const total = pdf.getNumberOfPages();
|
|
210
|
+
for (let page = 1; page <= total; page++) {
|
|
211
|
+
pdf.setPage(page);
|
|
212
|
+
drawHeader(pdf, meta);
|
|
213
|
+
drawFooter(pdf, page, total, generatedAt);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
pdf.save(meta.fileName);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { exportReportPdf };
|
|
220
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.109.1",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
|
@@ -12,6 +12,16 @@ declare interface TAnnouncement {
|
|
|
12
12
|
startDate: string;
|
|
13
13
|
endDate: string;
|
|
14
14
|
status: TAnnouncementStatus
|
|
15
|
+
event?: TAnnouncementEvent | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declare interface TAnnouncementEvent {
|
|
19
|
+
startAt: string;
|
|
20
|
+
endAt?: string | null;
|
|
21
|
+
venue?: string;
|
|
22
|
+
rsvp?: boolean;
|
|
23
|
+
rsvpClosesAt?: string | null;
|
|
24
|
+
capacity?: number | null;
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
declare interface TAnnouncementFile {
|
|
@@ -24,6 +34,6 @@ declare type TAnnouncementRecipients = "resident" | "security_agency" | "cleanin
|
|
|
24
34
|
|
|
25
35
|
|
|
26
36
|
|
|
27
|
-
declare type TCreateAnnouncementPayload = Pick<TAnnouncement, "recipients" | "file" | "site" | "noExpiration" | "startDate" | "endDate" | "content" | "title">
|
|
37
|
+
declare type TCreateAnnouncementPayload = Pick<TAnnouncement, "recipients" | "file" | "site" | "noExpiration" | "startDate" | "endDate" | "content" | "title"> & { event?: TAnnouncementEvent | null }
|
|
28
38
|
|
|
29
39
|
declare type TAnnouncementStatus = "active" | "expired"
|
|
@@ -53,6 +53,14 @@ export interface NFCPatrolActivity {
|
|
|
53
53
|
endTime: string;
|
|
54
54
|
status: NFCPatrolActivityStatus;
|
|
55
55
|
remarks?: string;
|
|
56
|
+
/** Photo the guard attached when skipping (an uploaded file's id). */
|
|
57
|
+
skippedPhotoFileId?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One patrol run: its schedule-vs-actual and its checkpoints. */
|
|
61
|
+
export interface NFCPatrolRunDetail {
|
|
62
|
+
routeSummary: NFCPatrolRouteSummary;
|
|
63
|
+
activities: NFCPatrolActivity[];
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
export interface NFCPatrolReport {
|
|
@@ -65,6 +73,8 @@ export type NFCPatrolReportSummaryStatus = "Completed" | "Incomplete";
|
|
|
65
73
|
|
|
66
74
|
export interface NFCPatrolReportSummaryRow {
|
|
67
75
|
id: string;
|
|
76
|
+
/** The patrol's day, "YYYY-MM-DD". */
|
|
77
|
+
date?: string;
|
|
68
78
|
routeName: string;
|
|
69
79
|
securityCheckSchedule: string;
|
|
70
80
|
guardName?: string;
|
|
@@ -72,6 +82,8 @@ export interface NFCPatrolReportSummaryRow {
|
|
|
72
82
|
checked: number;
|
|
73
83
|
missed: number;
|
|
74
84
|
status: NFCPatrolReportSummaryStatus;
|
|
85
|
+
/** The run behind the row, opened in the route details dialog. */
|
|
86
|
+
detail?: NFCPatrolRunDetail;
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
export interface NFCPatrolSummaryReport {
|
|
@@ -82,6 +94,8 @@ export interface NFCPatrolSummaryReport {
|
|
|
82
94
|
|
|
83
95
|
export interface NFCPatrolMonthlyReportRow {
|
|
84
96
|
month: string;
|
|
97
|
+
/** Patrol runs that month; a month with none has no list to open. */
|
|
98
|
+
runs?: number;
|
|
85
99
|
checked: number;
|
|
86
100
|
/** `null` when no checkpoint was due that month - see `utils/nfc-patrol-report.ts`. */
|
|
87
101
|
checkedPercentage: number | null;
|
package/utils/nav-sections.ts
CHANGED
|
@@ -65,6 +65,7 @@ export const NAV_SECTIONS: Record<string, string> = {
|
|
|
65
65
|
"Document Mgmt": "PROPERTY",
|
|
66
66
|
"Access Mgmt": "PROPERTY",
|
|
67
67
|
"Event Mgmt": "PROPERTY",
|
|
68
|
+
"Task Calendar": "PROPERTY",
|
|
68
69
|
"Emergency Contacts": "PROPERTY",
|
|
69
70
|
"Online Billing": "PROPERTY",
|
|
70
71
|
"SOA Mgmt": "PROPERTY",
|