@7365admin1/layer-common 4.94.0 → 4.96.0
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 -4
- package/components/InventoryHistoryDialog.vue +10 -3
- package/components/InventoryItemDetail.vue +18 -1
- package/components/InventoryItemDialog.vue +1 -1
- package/components/InventoryLinesDialog.vue +35 -9
- package/components/InventoryMain.vue +107 -5
- package/components/InventoryMyStockTab.vue +167 -12
- package/components/InventoryPoDialog.vue +2 -1
- package/components/InventoryReceiptActions.vue +67 -0
- package/components/InventoryReportsTab.vue +97 -6
- package/components/InventoryRequestsTab.vue +36 -14
- package/components/InventorySettingsTab.vue +27 -1
- package/components/InventoryStockTab.vue +52 -4
- package/components/InventoryTechniciansTab.vue +77 -4
- package/components/InventoryUsageDialog.vue +21 -2
- package/components/InventoryWriteOffDialog.vue +112 -0
- package/components/VehicleQrStickerDialog.vue +83 -16
- package/composables/useInventory.ts +24 -6
- package/package.json +1 -1
- package/types/inventory.d.ts +39 -2
- package/utils/inventory-ack.ts +172 -0
- package/utils/inventory-request-text.ts +5 -3
- package/utils/inventory-usage.ts +16 -1
- package/utils/notification-target.ts +9 -3
- package/utils/vehicle-qr-design.ts +268 -0
- package/utils/vehicle-qr-sticker.ts +26 -15
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vehicle QR sticker designs - the sticker face (frame + QR) as plain shapes.
|
|
3
|
+
*
|
|
4
|
+
* The QR library only gives us the dark/light module grid; we draw it here so
|
|
5
|
+
* the look can change while the code itself does not: same payload, same error
|
|
6
|
+
* correction (Q), a 4-module quiet zone and a 32 mm QR on every design. The
|
|
7
|
+
* shapes are kept as data (`stickerFacePaints`) so the test can draw them to
|
|
8
|
+
* pixels and decode them - every design must still scan. Colours are dark ink
|
|
9
|
+
* on light paper only; the test checks each theme's contrast.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type QrMatrix = { size: number; get(row: number, col: number): number | boolean };
|
|
13
|
+
|
|
14
|
+
export type DotShape = "square" | "rounded" | "dots";
|
|
15
|
+
export type EyeShape = "square" | "rounded" | "circle";
|
|
16
|
+
export type FrameShape = "rounded" | "square" | "soft" | "arch" | "ticket";
|
|
17
|
+
export type LabelChoice = "app" | "verify" | "site" | "none";
|
|
18
|
+
|
|
19
|
+
export type StickerTheme = { id: string; title: string; ink: string; eye: string; paper: string };
|
|
20
|
+
|
|
21
|
+
export type StickerDesign = { dots: DotShape; eyes: EyeShape; theme: string; frame: FrameShape; label: LabelChoice };
|
|
22
|
+
|
|
23
|
+
export const DOT_OPTIONS: { value: DotShape; title: string }[] = [
|
|
24
|
+
{ value: "square", title: "Squares (classic)" },
|
|
25
|
+
{ value: "rounded", title: "Rounded squares" },
|
|
26
|
+
{ value: "dots", title: "Round dots" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export const EYE_OPTIONS: { value: EyeShape; title: string }[] = [
|
|
30
|
+
{ value: "square", title: "Square" },
|
|
31
|
+
{ value: "rounded", title: "Rounded" },
|
|
32
|
+
{ value: "circle", title: "Circle" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export const FRAME_OPTIONS: { value: FrameShape; title: string }[] = [
|
|
36
|
+
{ value: "rounded", title: "Rounded corners" },
|
|
37
|
+
{ value: "square", title: "Square corners" },
|
|
38
|
+
{ value: "soft", title: "Extra round" },
|
|
39
|
+
{ value: "arch", title: "Badge (round top)" },
|
|
40
|
+
{ value: "ticket", title: "Ticket (cut corners)" },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export const LABEL_OPTIONS: { value: LabelChoice; title: string }[] = [
|
|
44
|
+
{ value: "app", title: "Scan with iSecure365 / iMA365" },
|
|
45
|
+
{ value: "verify", title: "Scan to verify vehicle" },
|
|
46
|
+
{ value: "site", title: "Site name" },
|
|
47
|
+
{ value: "none", title: "No extra words" },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Dark ink on light paper only - never inverted, never pale. Checked by test.
|
|
51
|
+
export const STICKER_THEMES: StickerTheme[] = [
|
|
52
|
+
{ id: "black", title: "Black on white", ink: "#000000", eye: "#000000", paper: "#ffffff" },
|
|
53
|
+
{ id: "navy", title: "Navy blue", ink: "#0b2545", eye: "#0b2545", paper: "#ffffff" },
|
|
54
|
+
{ id: "navy-red", title: "Navy with red corners", ink: "#0b2545", eye: "#8b1a1a", paper: "#ffffff" },
|
|
55
|
+
{ id: "green", title: "Dark green on mint", ink: "#14462b", eye: "#14462b", paper: "#f2faf4" },
|
|
56
|
+
{ id: "maroon", title: "Maroon on cream", ink: "#5c0f1d", eye: "#5c0f1d", paper: "#fff8ee" },
|
|
57
|
+
{ id: "purple", title: "Deep purple", ink: "#33105c", eye: "#33105c", paper: "#ffffff" },
|
|
58
|
+
{ id: "charcoal", title: "Charcoal on cream", ink: "#222222", eye: "#222222", paper: "#fffbea" },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export const DEFAULT_STICKER_DESIGN: StickerDesign = {
|
|
62
|
+
dots: "square",
|
|
63
|
+
eyes: "square",
|
|
64
|
+
theme: "black",
|
|
65
|
+
frame: "rounded",
|
|
66
|
+
label: "app",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** A saved or typed-in design with anything unknown replaced by the default. */
|
|
70
|
+
export function normalizeDesign(raw: unknown): StickerDesign {
|
|
71
|
+
const r = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
|
|
72
|
+
const pick = <T extends string>(v: unknown, list: { value: T }[], d: T): T =>
|
|
73
|
+
list.some((o) => o.value === v) ? (v as T) : d;
|
|
74
|
+
const d = DEFAULT_STICKER_DESIGN;
|
|
75
|
+
return {
|
|
76
|
+
dots: pick(r.dots, DOT_OPTIONS, d.dots),
|
|
77
|
+
eyes: pick(r.eyes, EYE_OPTIONS, d.eyes),
|
|
78
|
+
theme: STICKER_THEMES.some((t) => t.id === r.theme) ? String(r.theme) : d.theme,
|
|
79
|
+
frame: pick(r.frame, FRAME_OPTIONS, d.frame),
|
|
80
|
+
label: pick(r.label, LABEL_OPTIONS, d.label),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function stickerTheme(id: string): StickerTheme {
|
|
85
|
+
return STICKER_THEMES.find((t) => t.id === id) ?? STICKER_THEMES[0];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** WCAG relative luminance of "#rrggbb". */
|
|
89
|
+
export function stickerLuminance(hex: string): number {
|
|
90
|
+
const n = parseInt(hex.slice(1), 16);
|
|
91
|
+
const lin = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
92
|
+
return 0.2126 * lin(((n >> 16) & 255) / 255) + 0.7152 * lin(((n >> 8) & 255) / 255) + 0.0722 * lin((n & 255) / 255);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function stickerContrast(a: string, b: string): number {
|
|
96
|
+
const [hi, lo] = [stickerLuminance(a), stickerLuminance(b)].sort((x, y) => y - x);
|
|
97
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---- geometry (all in mm; the card is 47 x 58) ----------------------------
|
|
101
|
+
|
|
102
|
+
export const CARD_W = 47;
|
|
103
|
+
export const CARD_H = 58;
|
|
104
|
+
export const QR_MM = 32; // the whole QR including its quiet zone
|
|
105
|
+
export const QUIET_MODULES = 4;
|
|
106
|
+
/** Error correction on every design (no centre logo, so Q stays enough). */
|
|
107
|
+
export const QR_ECC = "Q";
|
|
108
|
+
const STROKE = 0.35;
|
|
109
|
+
|
|
110
|
+
/** A rectangle with per-corner radii [tl, tr, br, bl], or concave cut corners. */
|
|
111
|
+
export type Box = { t: "box"; x: number; y: number; w: number; h: number; r?: number[]; notch?: number };
|
|
112
|
+
export type Circle = { t: "circle"; cx: number; cy: number; r: number };
|
|
113
|
+
export type Shape = Box | Circle;
|
|
114
|
+
/** A filled shape; `hole` is cut out of it (rings, the frame line). */
|
|
115
|
+
export type Paint = { shape: Shape; hole?: Shape; fill: string };
|
|
116
|
+
|
|
117
|
+
/** Top of the QR on the card: lower on the badge so the round top keeps clear of the code. */
|
|
118
|
+
export function qrTop(frame: FrameShape): number {
|
|
119
|
+
return frame === "arch" ? 7 : 2.5;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cardBox(frame: FrameShape, inset: number): Box {
|
|
123
|
+
const x = inset;
|
|
124
|
+
const w = CARD_W - inset * 2;
|
|
125
|
+
const h = CARD_H - inset * 2;
|
|
126
|
+
const r = (v: number) => Math.max(0, v - inset);
|
|
127
|
+
switch (frame) {
|
|
128
|
+
case "square":
|
|
129
|
+
return { t: "box", x, y: x, w, h, r: [r(0.8), r(0.8), r(0.8), r(0.8)] };
|
|
130
|
+
case "soft":
|
|
131
|
+
return { t: "box", x, y: x, w, h, r: [r(8), r(8), r(8), r(8)] };
|
|
132
|
+
case "arch":
|
|
133
|
+
return { t: "box", x, y: x, w, h, r: [w / 2, w / 2, r(3), r(3)] };
|
|
134
|
+
case "ticket":
|
|
135
|
+
return { t: "box", x, y: x, w, h, notch: 4 + inset };
|
|
136
|
+
default:
|
|
137
|
+
return { t: "box", x, y: x, w, h, r: [r(3), r(3), r(3), r(3)] };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isFinder(n: number, row: number, col: number): boolean {
|
|
142
|
+
return (row < 7 && col < 7) || (row < 7 && col >= n - 7) || (row >= n - 7 && col < 7);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Every shape on the sticker face, painted in order. */
|
|
146
|
+
export function stickerFacePaints(matrix: QrMatrix, design: StickerDesign): Paint[] {
|
|
147
|
+
const theme = stickerTheme(design.theme);
|
|
148
|
+
const n = matrix.size;
|
|
149
|
+
const m = QR_MM / (n + QUIET_MODULES * 2);
|
|
150
|
+
const ox = (CARD_W - QR_MM) / 2 + QUIET_MODULES * m;
|
|
151
|
+
const oy = qrTop(design.frame) + QUIET_MODULES * m;
|
|
152
|
+
const paints: Paint[] = [
|
|
153
|
+
{ shape: cardBox(design.frame, 0), fill: theme.paper },
|
|
154
|
+
{ shape: cardBox(design.frame, 0), hole: cardBox(design.frame, STROKE), fill: theme.ink },
|
|
155
|
+
];
|
|
156
|
+
const box = (x: number, y: number, w: number, h: number, rad = 0): Box => ({ t: "box", x, y, w, h, r: [rad, rad, rad, rad] });
|
|
157
|
+
|
|
158
|
+
for (let row = 0; row < n; row++) {
|
|
159
|
+
for (let col = 0; col < n; col++) {
|
|
160
|
+
if (!matrix.get(row, col) || isFinder(n, row, col)) continue;
|
|
161
|
+
const x = ox + col * m;
|
|
162
|
+
const y = oy + row * m;
|
|
163
|
+
if (design.dots === "dots") {
|
|
164
|
+
paints.push({ shape: { t: "circle", cx: x + m / 2, cy: y + m / 2, r: m * 0.45 }, fill: theme.ink });
|
|
165
|
+
} else if (design.dots === "rounded") {
|
|
166
|
+
paints.push({ shape: box(x, y, m, m, m * 0.3), fill: theme.ink });
|
|
167
|
+
} else {
|
|
168
|
+
// ponytail: one rect per horizontal run, so adjacent squares never show a hairline seam
|
|
169
|
+
let end = col;
|
|
170
|
+
while (end + 1 < n && matrix.get(row, end + 1) && !isFinder(n, row, end + 1)) end++;
|
|
171
|
+
paints.push({ shape: box(x, y, (end - col + 1) * m, m), fill: theme.ink });
|
|
172
|
+
col = end;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const [r0, c0] of [[0, 0], [0, n - 7], [n - 7, 0]]) {
|
|
178
|
+
const x = ox + c0 * m;
|
|
179
|
+
const y = oy + r0 * m;
|
|
180
|
+
if (design.eyes === "circle") {
|
|
181
|
+
const cx = x + 3.5 * m;
|
|
182
|
+
const cy = y + 3.5 * m;
|
|
183
|
+
paints.push({ shape: { t: "circle", cx, cy, r: 3.5 * m }, hole: { t: "circle", cx, cy, r: 2.5 * m }, fill: theme.eye });
|
|
184
|
+
paints.push({ shape: { t: "circle", cx, cy, r: 1.5 * m }, fill: theme.eye });
|
|
185
|
+
} else {
|
|
186
|
+
const round = design.eyes === "rounded";
|
|
187
|
+
paints.push({
|
|
188
|
+
shape: box(x, y, 7 * m, 7 * m, round ? 2 * m : 0),
|
|
189
|
+
hole: box(x + m, y + m, 5 * m, 5 * m, round ? 1.2 * m : 0),
|
|
190
|
+
fill: theme.eye,
|
|
191
|
+
});
|
|
192
|
+
paints.push({ shape: box(x + 2 * m, y + 2 * m, 3 * m, 3 * m, round ? 0.9 * m : 0), fill: theme.eye });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return paints;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---- drawing ---------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
const f = (v: number) => String(Math.round(v * 1000) / 1000);
|
|
201
|
+
|
|
202
|
+
function boxPath(b: Box): string {
|
|
203
|
+
const { x, y, w, h } = b;
|
|
204
|
+
if (b.notch) {
|
|
205
|
+
const k = b.notch;
|
|
206
|
+
return (
|
|
207
|
+
`M${f(x + k)} ${f(y)}H${f(x + w - k)}A${f(k)} ${f(k)} 0 0 0 ${f(x + w)} ${f(y + k)}` +
|
|
208
|
+
`V${f(y + h - k)}A${f(k)} ${f(k)} 0 0 0 ${f(x + w - k)} ${f(y + h)}` +
|
|
209
|
+
`H${f(x + k)}A${f(k)} ${f(k)} 0 0 0 ${f(x)} ${f(y + h - k)}` +
|
|
210
|
+
`V${f(y + k)}A${f(k)} ${f(k)} 0 0 0 ${f(x + k)} ${f(y)}Z`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const [tl, tr, br, bl] = b.r ?? [0, 0, 0, 0];
|
|
214
|
+
const arc = (r: number, ex: number, ey: number) => (r ? `A${f(r)} ${f(r)} 0 0 1 ${f(ex)} ${f(ey)}` : "");
|
|
215
|
+
return (
|
|
216
|
+
`M${f(x + tl)} ${f(y)}H${f(x + w - tr)}${arc(tr, x + w, y + tr)}` +
|
|
217
|
+
`V${f(y + h - br)}${arc(br, x + w - br, y + h)}` +
|
|
218
|
+
`H${f(x + bl)}${arc(bl, x, y + h - bl)}` +
|
|
219
|
+
`V${f(y + tl)}${arc(tl, x + tl, y)}Z`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function shapePath(s: Shape): string {
|
|
224
|
+
if (s.t === "box") return boxPath(s);
|
|
225
|
+
const { cx, cy, r } = s;
|
|
226
|
+
return `M${f(cx - r)} ${f(cy)}A${f(r)} ${f(r)} 0 1 0 ${f(cx + r)} ${f(cy)}A${f(r)} ${f(r)} 0 1 0 ${f(cx - r)} ${f(cy)}Z`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The face as one SVG, 47 x 58 mm. Runs of same-colour shapes share one path. */
|
|
230
|
+
export function paintsToSvg(paints: Paint[]): string {
|
|
231
|
+
const out: string[] = [];
|
|
232
|
+
let fill = "";
|
|
233
|
+
let d = "";
|
|
234
|
+
const flush = () => d && out.push(`<path fill="${fill}" d="${d}"/>`);
|
|
235
|
+
for (const p of paints) {
|
|
236
|
+
if (p.hole) {
|
|
237
|
+
// A ring is its own evenodd path, so its hole never cuts a neighbour.
|
|
238
|
+
flush();
|
|
239
|
+
d = "";
|
|
240
|
+
out.push(`<path fill="${p.fill}" fill-rule="evenodd" d="${shapePath(p.shape)}${shapePath(p.hole)}"/>`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (p.fill !== fill) {
|
|
244
|
+
flush();
|
|
245
|
+
fill = p.fill;
|
|
246
|
+
d = "";
|
|
247
|
+
}
|
|
248
|
+
d += shapePath(p.shape);
|
|
249
|
+
}
|
|
250
|
+
flush();
|
|
251
|
+
return (
|
|
252
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${CARD_W} ${CARD_H}" width="${CARD_W}mm" height="${CARD_H}mm">` +
|
|
253
|
+
out.join("") +
|
|
254
|
+
`</svg>`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function stickerFaceSvg(matrix: QrMatrix, design: StickerDesign): string {
|
|
259
|
+
return paintsToSvg(stickerFacePaints(matrix, design));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The words under the plate for this design ("" = none). */
|
|
263
|
+
export function stickerLabel(design: StickerDesign, siteName = ""): string {
|
|
264
|
+
if (design.label === "none") return "";
|
|
265
|
+
if (design.label === "verify") return "Scan to verify vehicle";
|
|
266
|
+
if (design.label === "site") return String(siteName ?? "").trim();
|
|
267
|
+
return "Scan with iSecure365 / iMA365";
|
|
268
|
+
}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* pasted on the car.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { DEFAULT_STICKER_DESIGN, QR_MM, qrTop, stickerLabel, stickerTheme, type StickerDesign } from "./vehicle-qr-design.ts";
|
|
12
|
+
|
|
11
13
|
/** The server's most vehicles per request (POST /api/vehicles/qr/stickers). */
|
|
12
14
|
export const STICKER_MAX_PER_REQUEST = 500;
|
|
13
15
|
|
|
@@ -146,13 +148,13 @@ html, body { margin: 0; padding: 0; background: #fff; color: #111; font-family:
|
|
|
146
148
|
.page { display: grid; grid-template-columns: repeat(${STICKER_COLUMNS}, 47mm); gap: 2mm; justify-content: center; align-content: start; width: 198mm; height: 285mm; margin: 0 auto; break-after: page; }
|
|
147
149
|
.page:last-child { break-after: auto; }
|
|
148
150
|
.cell { width: 47mm; break-inside: avoid; }
|
|
149
|
-
.card { width: 47mm; height: 58mm;
|
|
150
|
-
.
|
|
151
|
-
.
|
|
151
|
+
.card { position: relative; width: 47mm; height: 58mm; }
|
|
152
|
+
.face, .face svg { position: absolute; inset: 0; width: 47mm; height: 58mm; display: block; }
|
|
153
|
+
.words { position: absolute; left: 3mm; right: 3mm; bottom: 2mm; display: flex; flex-direction: column; align-items: center; justify-content: space-between; text-align: center; }
|
|
152
154
|
.plate { font-size: 13pt; font-weight: 800; letter-spacing: 0.3mm; line-height: 1.1; }
|
|
153
|
-
.hint { font-size: 5.8pt;
|
|
154
|
-
.brand { display: flex; align-items: center; justify-content: center; gap: 1mm; font-size: 6pt;
|
|
155
|
-
.mark { width: 3.2mm; height: 3.2mm; border-radius: 0.8mm;
|
|
155
|
+
.hint { font-size: 5.8pt; line-height: 1.2; opacity: 0.85; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
|
|
156
|
+
.brand { display: flex; align-items: center; justify-content: center; gap: 1mm; font-size: 6pt; border-top: 0.2mm solid #ccc; width: 100%; padding-top: 1mm; }
|
|
157
|
+
.mark { width: 3.2mm; height: 3.2mm; border-radius: 0.8mm; color: #fff; font-weight: 800; font-size: 5pt; line-height: 3.2mm; text-align: center; }
|
|
156
158
|
.cut { margin-top: 1.2mm; border-top: 0.3mm dashed #666; padding-top: 1mm; font-size: 6.5pt; line-height: 1.25; color: #111; min-height: 8mm; }
|
|
157
159
|
.cut .owner { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
158
160
|
.cut .note { color: #666; font-size: 5.5pt; }
|
|
@@ -160,20 +162,29 @@ html, body { margin: 0; padding: 0; background: #fff; color: #111; font-family:
|
|
|
160
162
|
`;
|
|
161
163
|
|
|
162
164
|
/**
|
|
163
|
-
* One full HTML document: A4 sheets of stickers. `
|
|
164
|
-
* `items` (
|
|
165
|
-
* owner name is typed by a person.
|
|
165
|
+
* One full HTML document: A4 sheets of stickers. `faceSvgs` is parallel to
|
|
166
|
+
* `items` (each sticker's frame + QR, already drawn for the chosen design).
|
|
167
|
+
* Every value is HTML-escaped - an owner or site name is typed by a person.
|
|
166
168
|
*/
|
|
167
|
-
export function buildStickerDocument(
|
|
169
|
+
export function buildStickerDocument(
|
|
170
|
+
items: StickerItem[],
|
|
171
|
+
faceSvgs: string[],
|
|
172
|
+
opts: { design?: StickerDesign; siteName?: string } = {},
|
|
173
|
+
): string {
|
|
174
|
+
const design = opts.design ?? DEFAULT_STICKER_DESIGN;
|
|
175
|
+
const { ink } = stickerTheme(design.theme);
|
|
176
|
+
const label = stickerLabel(design, opts.siteName);
|
|
177
|
+
const wordsTop = qrTop(design.frame) + QR_MM + 0.8;
|
|
168
178
|
const cells = items.map((item, i) => {
|
|
169
179
|
const { place, owner } = installerLine(item);
|
|
170
180
|
return (
|
|
171
|
-
`<div class="cell"><div class="card">` +
|
|
172
|
-
`<div class="
|
|
181
|
+
`<div class="cell"><div class="card" style="color:${ink}">` +
|
|
182
|
+
`<div class="face">${faceSvgs[i] ?? ""}</div>` +
|
|
183
|
+
`<div class="words" style="top:${wordsTop}mm">` +
|
|
173
184
|
`<div class="plate">${escapeHtml(item.plateNumber)}</div>` +
|
|
174
|
-
`<div class="hint"
|
|
175
|
-
`<div class="brand"><span class="mark">7</span>Powered by Seven365</div>` +
|
|
176
|
-
`</div>` +
|
|
185
|
+
(label ? `<div class="hint">${escapeHtml(label)}</div>` : "") +
|
|
186
|
+
`<div class="brand"><span class="mark" style="background:${ink}">7</span>Powered by Seven365</div>` +
|
|
187
|
+
`</div></div>` +
|
|
177
188
|
`<div class="cut"><div><b>${escapeHtml(place || "-")}</b></div>` +
|
|
178
189
|
`<div class="owner">Owner: ${escapeHtml(owner || "-")}</div>` +
|
|
179
190
|
`<div class="note">Installer: cut here, then paste on this car</div></div></div>`
|