@7365admin1/layer-common 3.0.8 → 3.0.9
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/DashboardMain.vue +1894 -844
- package/components/EntryPassInformation.vue +60 -0
- package/components/HidAccessLogDashboard.vue +1691 -0
- package/components/HidEnabledGate.vue +51 -0
- package/components/HidIdentityMapping.vue +975 -0
- package/components/HidQrCodeConfiguration.vue +618 -0
- package/components/HidReaderForm.vue +246 -0
- package/components/HidReaderManagement.vue +1233 -0
- package/components/HidServiceSettingsPanel.vue +150 -0
- package/components/HidUserEnrollment.vue +1274 -0
- package/components/Layout/NavigationDrawer.vue +31 -1
- package/components/NavigationItem.vue +11 -4
- package/components/SiteSettings.vue +24 -1
- package/components/VisitorForm.vue +195 -4
- package/components/VisitorFormSelection.vue +12 -2
- package/composables/useDashboard.ts +29 -2
- package/composables/useHidAmico.ts +152 -0
- package/composables/useHidNavigation.ts +106 -0
- package/composables/useMember.ts +7 -1
- package/composables/useOptionalServices.ts +118 -0
- package/composables/useSiteSettings.ts +1 -1
- package/composables/useUser.ts +1 -2
- package/nuxt.config.ts +1 -1
- package/package.json +2 -1
- package/pages/[org]/[site]/access-mgmt/access-logs/index.vue +21 -0
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +21 -0
- package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +21 -0
- package/pages/[org]/[site]/access-mgmt/hid-users/index.vue +21 -0
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +21 -0
- package/types/local.d.ts +2 -1
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="hid-qr-config">
|
|
3
|
+
<v-card flat class="config-panel">
|
|
4
|
+
<div class="panel-header">
|
|
5
|
+
<div>
|
|
6
|
+
<h2>HID Configuration</h2>
|
|
7
|
+
</div>
|
|
8
|
+
<v-btn icon="mdi-chevron-up" variant="text" density="comfortable" />
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="config-body">
|
|
12
|
+
<div class="section-head">
|
|
13
|
+
<div>
|
|
14
|
+
<h3>HID QR Code Pass</h3>
|
|
15
|
+
<p>Allow users to generate and scan digital QR codes at compatible HID readers for touchless access control.</p>
|
|
16
|
+
</div>
|
|
17
|
+
<v-switch
|
|
18
|
+
v-model="form.enabled"
|
|
19
|
+
color="success"
|
|
20
|
+
hide-details
|
|
21
|
+
inset
|
|
22
|
+
:loading="savingSection === 'enabled'"
|
|
23
|
+
@update:model-value="saveSection('enabled')"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="form-section">
|
|
28
|
+
<div class="section-label">HID Reader</div>
|
|
29
|
+
<v-select
|
|
30
|
+
v-model="form.readerId"
|
|
31
|
+
:items="readerOptions"
|
|
32
|
+
item-title="title"
|
|
33
|
+
item-value="value"
|
|
34
|
+
placeholder="Select HID reader"
|
|
35
|
+
variant="outlined"
|
|
36
|
+
density="compact"
|
|
37
|
+
hide-details
|
|
38
|
+
class="reader-select"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<v-divider />
|
|
43
|
+
|
|
44
|
+
<div class="form-section">
|
|
45
|
+
<div class="section-label">QR Code Format</div>
|
|
46
|
+
<div class="validity-grid">
|
|
47
|
+
<v-select
|
|
48
|
+
v-model="form.qrFormat"
|
|
49
|
+
:items="qrFormatOptions"
|
|
50
|
+
item-title="title"
|
|
51
|
+
item-value="value"
|
|
52
|
+
placeholder="Select format"
|
|
53
|
+
variant="outlined"
|
|
54
|
+
density="compact"
|
|
55
|
+
hide-details
|
|
56
|
+
/>
|
|
57
|
+
<v-btn
|
|
58
|
+
class="text-none save-button"
|
|
59
|
+
variant="flat"
|
|
60
|
+
prepend-icon="mdi-qrcode-edit"
|
|
61
|
+
height="40"
|
|
62
|
+
:loading="savingSection === 'qr-format'"
|
|
63
|
+
:disabled="!form.readerId"
|
|
64
|
+
@click="saveSection('qr-format')"
|
|
65
|
+
>
|
|
66
|
+
Save Format
|
|
67
|
+
</v-btn>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="form-section">
|
|
72
|
+
<div class="section-label">Printer Setup</div>
|
|
73
|
+
<div class="printer-grid">
|
|
74
|
+
<v-text-field
|
|
75
|
+
v-model="form.printer.vendorId"
|
|
76
|
+
placeholder="Vendor"
|
|
77
|
+
variant="outlined"
|
|
78
|
+
density="compact"
|
|
79
|
+
hide-details
|
|
80
|
+
/>
|
|
81
|
+
<v-text-field
|
|
82
|
+
v-model="form.printer.productId"
|
|
83
|
+
placeholder="Product Id"
|
|
84
|
+
variant="outlined"
|
|
85
|
+
density="compact"
|
|
86
|
+
hide-details
|
|
87
|
+
/>
|
|
88
|
+
<v-btn
|
|
89
|
+
class="text-none utility-button"
|
|
90
|
+
variant="outlined"
|
|
91
|
+
prepend-icon="mdi-magnify"
|
|
92
|
+
height="40"
|
|
93
|
+
@click="scanPrinter"
|
|
94
|
+
>
|
|
95
|
+
Scan Printer
|
|
96
|
+
</v-btn>
|
|
97
|
+
<v-btn
|
|
98
|
+
class="text-none test-button"
|
|
99
|
+
variant="outlined"
|
|
100
|
+
prepend-icon="mdi-printer-check"
|
|
101
|
+
height="40"
|
|
102
|
+
:disabled="!hasPrinterConfig"
|
|
103
|
+
@click="testPrint"
|
|
104
|
+
>
|
|
105
|
+
Test & Print
|
|
106
|
+
</v-btn>
|
|
107
|
+
<v-btn
|
|
108
|
+
class="text-none save-button"
|
|
109
|
+
variant="flat"
|
|
110
|
+
prepend-icon="mdi-printer"
|
|
111
|
+
height="40"
|
|
112
|
+
:loading="savingSection === 'printer'"
|
|
113
|
+
@click="saveSection('printer')"
|
|
114
|
+
>
|
|
115
|
+
Save Settings
|
|
116
|
+
</v-btn>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<v-divider />
|
|
121
|
+
|
|
122
|
+
<div class="form-section">
|
|
123
|
+
<div class="section-label">HID QR Code Template</div>
|
|
124
|
+
<div class="template-grid">
|
|
125
|
+
<v-text-field
|
|
126
|
+
v-model="form.template.header"
|
|
127
|
+
placeholder="Header"
|
|
128
|
+
variant="outlined"
|
|
129
|
+
density="compact"
|
|
130
|
+
hide-details
|
|
131
|
+
/>
|
|
132
|
+
<v-text-field
|
|
133
|
+
v-model="form.template.subtext"
|
|
134
|
+
placeholder="Subtext"
|
|
135
|
+
variant="outlined"
|
|
136
|
+
density="compact"
|
|
137
|
+
hide-details
|
|
138
|
+
/>
|
|
139
|
+
<v-btn
|
|
140
|
+
class="text-none preview-button"
|
|
141
|
+
variant="flat"
|
|
142
|
+
prepend-icon="mdi-eye"
|
|
143
|
+
height="40"
|
|
144
|
+
@click="previewDialog = true"
|
|
145
|
+
>
|
|
146
|
+
Preview
|
|
147
|
+
</v-btn>
|
|
148
|
+
<v-btn
|
|
149
|
+
class="text-none save-button"
|
|
150
|
+
variant="flat"
|
|
151
|
+
prepend-icon="mdi-qrcode-scan"
|
|
152
|
+
height="40"
|
|
153
|
+
:loading="savingSection === 'template'"
|
|
154
|
+
@click="saveSection('template')"
|
|
155
|
+
>
|
|
156
|
+
Save
|
|
157
|
+
</v-btn>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<v-divider />
|
|
162
|
+
|
|
163
|
+
<div class="form-section">
|
|
164
|
+
<div class="section-label">HID QR Code Validity</div>
|
|
165
|
+
<div class="validity-grid">
|
|
166
|
+
<v-text-field
|
|
167
|
+
v-model.number="form.validityMinutes"
|
|
168
|
+
placeholder="Validity (minutes)"
|
|
169
|
+
variant="outlined"
|
|
170
|
+
density="compact"
|
|
171
|
+
hide-details
|
|
172
|
+
min="1"
|
|
173
|
+
type="number"
|
|
174
|
+
/>
|
|
175
|
+
<v-btn
|
|
176
|
+
class="text-none save-button"
|
|
177
|
+
variant="flat"
|
|
178
|
+
prepend-icon="mdi-timer-outline"
|
|
179
|
+
height="40"
|
|
180
|
+
:loading="savingSection === 'validity'"
|
|
181
|
+
@click="saveSection('validity')"
|
|
182
|
+
>
|
|
183
|
+
Save
|
|
184
|
+
</v-btn>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</v-card>
|
|
189
|
+
|
|
190
|
+
<v-dialog v-model="previewDialog" max-width="420">
|
|
191
|
+
<v-card class="preview-shell">
|
|
192
|
+
<v-btn
|
|
193
|
+
class="preview-close"
|
|
194
|
+
icon="mdi-close"
|
|
195
|
+
variant="text"
|
|
196
|
+
density="comfortable"
|
|
197
|
+
@click="previewDialog = false"
|
|
198
|
+
/>
|
|
199
|
+
<v-card-text>
|
|
200
|
+
<div class="preview-title">QR code Preview</div>
|
|
201
|
+
<div class="pass-preview">
|
|
202
|
+
<div class="pass-heading">{{ form.template.header || "Header" }}</div>
|
|
203
|
+
<div class="pass-subtext">{{ form.template.subtext || "Subtext" }}</div>
|
|
204
|
+
|
|
205
|
+
<div class="qr-grid">
|
|
206
|
+
<div class="qr-placeholder small"><span>No QR</span></div>
|
|
207
|
+
<div class="qr-placeholder small"><span>No QR</span></div>
|
|
208
|
+
<div class="qr-placeholder large"><span>No QR payload</span></div>
|
|
209
|
+
<div class="qr-placeholder small"><span>No QR</span></div>
|
|
210
|
+
<div class="qr-placeholder small"><span>No QR</span></div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div class="pass-footer">
|
|
214
|
+
<strong>{{ siteName }}</strong>
|
|
215
|
+
<strong>{{ siteLabel }}</strong>
|
|
216
|
+
<span>Validity: {{ validityText }}</span>
|
|
217
|
+
<span>Access: Door</span>
|
|
218
|
+
<span>Generated QR payload will render here.</span>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</v-card-text>
|
|
222
|
+
</v-card>
|
|
223
|
+
</v-dialog>
|
|
224
|
+
|
|
225
|
+
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
226
|
+
</section>
|
|
227
|
+
</template>
|
|
228
|
+
|
|
229
|
+
<script setup lang="ts">
|
|
230
|
+
type HidQrConfig = {
|
|
231
|
+
enabled: boolean;
|
|
232
|
+
readerId: string;
|
|
233
|
+
qrFormat: "0" | "1" | "2";
|
|
234
|
+
printer: {
|
|
235
|
+
vendorId: string;
|
|
236
|
+
productId: string;
|
|
237
|
+
};
|
|
238
|
+
template: {
|
|
239
|
+
header: string;
|
|
240
|
+
subtext: string;
|
|
241
|
+
};
|
|
242
|
+
validityMinutes: number | null;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const props = defineProps<{
|
|
246
|
+
site: string;
|
|
247
|
+
}>();
|
|
248
|
+
|
|
249
|
+
const { getSiteById, updateSitebyId } = useSiteSettings();
|
|
250
|
+
const { getReaders, setReaderConfiguration } = useHidAmico();
|
|
251
|
+
|
|
252
|
+
const defaultConfig: HidQrConfig = {
|
|
253
|
+
enabled: false,
|
|
254
|
+
readerId: "",
|
|
255
|
+
qrFormat: "0",
|
|
256
|
+
printer: {
|
|
257
|
+
vendorId: "",
|
|
258
|
+
productId: "",
|
|
259
|
+
},
|
|
260
|
+
template: {
|
|
261
|
+
header: "",
|
|
262
|
+
subtext: "",
|
|
263
|
+
},
|
|
264
|
+
validityMinutes: null,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const siteData = ref<Record<string, any> | null>(null);
|
|
268
|
+
const readers = ref<Record<string, any>[]>([]);
|
|
269
|
+
const form = reactive<HidQrConfig>(structuredClone(defaultConfig));
|
|
270
|
+
const previewDialog = ref(false);
|
|
271
|
+
const savingSection = ref("");
|
|
272
|
+
const message = ref("");
|
|
273
|
+
const messageColor = ref("success");
|
|
274
|
+
const messageSnackbar = ref(false);
|
|
275
|
+
|
|
276
|
+
const hasPrinterConfig = computed(() =>
|
|
277
|
+
Boolean(form.printer.vendorId && form.printer.productId),
|
|
278
|
+
);
|
|
279
|
+
const readerOptions = computed(() => readers.value.map((reader) => ({
|
|
280
|
+
title: reader.name || reader.deviceId || reader._id,
|
|
281
|
+
value: reader._id,
|
|
282
|
+
})));
|
|
283
|
+
const qrFormatOptions = [
|
|
284
|
+
{ title: "Alphanumeric", value: "0" },
|
|
285
|
+
{ title: "Numeric only", value: "1" },
|
|
286
|
+
{ title: "Hexadecimal numeric", value: "2" },
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
const siteName = computed(() => siteData.value?.name || "Site");
|
|
290
|
+
const siteLabel = computed(() => siteData.value?.code || siteData.value?.block || props.site);
|
|
291
|
+
const validityText = computed(() =>
|
|
292
|
+
form.validityMinutes ? `${form.validityMinutes} minutes` : "Not set",
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
watch(
|
|
296
|
+
() => props.site,
|
|
297
|
+
() => loadConfig(),
|
|
298
|
+
{ immediate: true },
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
async function loadConfig() {
|
|
302
|
+
if (!props.site) return;
|
|
303
|
+
|
|
304
|
+
const [site, readerResponse] = await Promise.all([
|
|
305
|
+
getSiteById(props.site),
|
|
306
|
+
getReaders({ site: props.site, page: 1, limit: 100 }),
|
|
307
|
+
]);
|
|
308
|
+
siteData.value = site;
|
|
309
|
+
readers.value = readerResponse?.items ?? readerResponse?.data?.items ?? readerResponse?.data?.readers ?? [];
|
|
310
|
+
const config = siteData.value?.metadata?.hidQrCodePass || {};
|
|
311
|
+
Object.assign(form, {
|
|
312
|
+
enabled: Boolean(config.enabled),
|
|
313
|
+
readerId: String(config.readerId || readers.value[0]?._id || ""),
|
|
314
|
+
qrFormat: ["0", "1", "2"].includes(String(config.qrFormat)) ? String(config.qrFormat) as "0" | "1" | "2" : "0",
|
|
315
|
+
printer: {
|
|
316
|
+
vendorId: String(config.printer?.vendorId || ""),
|
|
317
|
+
productId: String(config.printer?.productId || ""),
|
|
318
|
+
},
|
|
319
|
+
template: {
|
|
320
|
+
header: String(config.template?.header || ""),
|
|
321
|
+
subtext: String(config.template?.subtext || ""),
|
|
322
|
+
},
|
|
323
|
+
validityMinutes: config.validityMinutes ? Number(config.validityMinutes) : null,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function buildPayload() {
|
|
328
|
+
const metadata = {
|
|
329
|
+
...(siteData.value?.metadata || {}),
|
|
330
|
+
hidQrCodePass: {
|
|
331
|
+
enabled: form.enabled,
|
|
332
|
+
readerId: form.readerId,
|
|
333
|
+
qrFormat: form.qrFormat,
|
|
334
|
+
printer: {
|
|
335
|
+
vendorId: form.printer.vendorId.trim(),
|
|
336
|
+
productId: form.printer.productId.trim(),
|
|
337
|
+
},
|
|
338
|
+
template: {
|
|
339
|
+
header: form.template.header.trim(),
|
|
340
|
+
subtext: form.template.subtext.trim(),
|
|
341
|
+
},
|
|
342
|
+
validityMinutes: form.validityMinutes ? Number(form.validityMinutes) : null,
|
|
343
|
+
updatedAt: new Date().toISOString(),
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
return { metadata };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function saveSection(section: string) {
|
|
351
|
+
if (!props.site) return;
|
|
352
|
+
|
|
353
|
+
savingSection.value = section;
|
|
354
|
+
try {
|
|
355
|
+
if (section === "qr-format") {
|
|
356
|
+
await setReaderConfiguration(form.readerId, {
|
|
357
|
+
face_id: {
|
|
358
|
+
qrcode_legacy_mode_enabled: form.qrFormat,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
await updateSitebyId(props.site, buildPayload());
|
|
363
|
+
await loadConfig();
|
|
364
|
+
showMessage("HID QR code settings saved.", "success");
|
|
365
|
+
} catch (error: any) {
|
|
366
|
+
showMessage(errorConverter(error), "error");
|
|
367
|
+
} finally {
|
|
368
|
+
savingSection.value = "";
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function scanPrinter() {
|
|
373
|
+
if (!("usb" in navigator)) {
|
|
374
|
+
showMessage("USB printer scan is not available in this browser.", "error");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const device = await (navigator as any).usb.requestDevice({ filters: [] });
|
|
380
|
+
form.printer.vendorId = `0x${Number(device.vendorId).toString(16).padStart(4, "0")}`;
|
|
381
|
+
form.printer.productId = `0x${Number(device.productId).toString(16).padStart(4, "0")}`;
|
|
382
|
+
showMessage("Printer selected. Save settings to keep it.", "success");
|
|
383
|
+
} catch (error: any) {
|
|
384
|
+
if (error?.name !== "NotFoundError") {
|
|
385
|
+
showMessage(error?.message || "Unable to scan printer.", "error");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function testPrint() {
|
|
391
|
+
showMessage("Printer settings are ready. Connect print command when HID QR payload generation is available.", "success");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function showMessage(text: string, color: string) {
|
|
395
|
+
message.value = text;
|
|
396
|
+
messageColor.value = color;
|
|
397
|
+
messageSnackbar.value = true;
|
|
398
|
+
}
|
|
399
|
+
</script>
|
|
400
|
+
|
|
401
|
+
<style scoped>
|
|
402
|
+
.hid-qr-config {
|
|
403
|
+
width: 100%;
|
|
404
|
+
min-width: 0;
|
|
405
|
+
padding: 8px 0 28px;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.config-panel {
|
|
409
|
+
border: 1px solid #e6eaee;
|
|
410
|
+
border-radius: 6px;
|
|
411
|
+
overflow: hidden;
|
|
412
|
+
background: #ffffff;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.panel-header {
|
|
416
|
+
display: flex;
|
|
417
|
+
align-items: center;
|
|
418
|
+
justify-content: space-between;
|
|
419
|
+
min-height: 48px;
|
|
420
|
+
padding: 0 18px;
|
|
421
|
+
background: #082e43;
|
|
422
|
+
color: #ffffff;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.panel-header h2 {
|
|
426
|
+
margin: 0;
|
|
427
|
+
font-size: 13px;
|
|
428
|
+
font-weight: 700;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.config-body {
|
|
432
|
+
padding: 30px 38px;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.section-head {
|
|
436
|
+
display: flex;
|
|
437
|
+
align-items: flex-start;
|
|
438
|
+
justify-content: space-between;
|
|
439
|
+
gap: 24px;
|
|
440
|
+
margin-bottom: 24px;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.section-head h3,
|
|
444
|
+
.section-label {
|
|
445
|
+
margin: 0 0 6px;
|
|
446
|
+
color: #233545;
|
|
447
|
+
font-size: 13px;
|
|
448
|
+
font-weight: 700;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.section-head p {
|
|
452
|
+
margin: 0;
|
|
453
|
+
color: #677280;
|
|
454
|
+
font-size: 12px;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.form-section {
|
|
458
|
+
padding: 16px 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.printer-grid {
|
|
462
|
+
display: grid;
|
|
463
|
+
grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr);
|
|
464
|
+
gap: 14px;
|
|
465
|
+
align-items: center;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.template-grid {
|
|
469
|
+
display: grid;
|
|
470
|
+
grid-template-columns: minmax(220px, 1fr) minmax(220px, 1fr);
|
|
471
|
+
gap: 14px;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.template-grid .preview-button,
|
|
475
|
+
.template-grid .save-button {
|
|
476
|
+
width: 100%;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.validity-grid {
|
|
480
|
+
display: grid;
|
|
481
|
+
grid-template-columns: minmax(180px, 260px) minmax(140px, 260px);
|
|
482
|
+
gap: 14px;
|
|
483
|
+
align-items: center;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.save-button {
|
|
487
|
+
background: #082e43;
|
|
488
|
+
color: #ffffff;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.preview-button {
|
|
492
|
+
background: #35b84b;
|
|
493
|
+
color: #ffffff;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.test-button {
|
|
497
|
+
border-color: #34b84a;
|
|
498
|
+
color: #2da643;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.utility-button {
|
|
502
|
+
color: #1f3448;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.preview-shell {
|
|
506
|
+
position: relative;
|
|
507
|
+
overflow: visible;
|
|
508
|
+
background: transparent;
|
|
509
|
+
box-shadow: none;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.preview-close {
|
|
513
|
+
position: absolute;
|
|
514
|
+
top: 38px;
|
|
515
|
+
right: 18px;
|
|
516
|
+
z-index: 2;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.preview-title {
|
|
520
|
+
margin: 0 0 12px;
|
|
521
|
+
color: #ffffff;
|
|
522
|
+
font-size: 18px;
|
|
523
|
+
font-weight: 700;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.pass-preview {
|
|
527
|
+
width: 244px;
|
|
528
|
+
min-height: 360px;
|
|
529
|
+
margin: 0 auto;
|
|
530
|
+
padding: 22px 28px;
|
|
531
|
+
background: #ffffff;
|
|
532
|
+
color: #252f3b;
|
|
533
|
+
text-align: center;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.pass-heading {
|
|
537
|
+
min-height: 20px;
|
|
538
|
+
font-size: 16px;
|
|
539
|
+
font-weight: 700;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.pass-subtext {
|
|
543
|
+
min-height: 18px;
|
|
544
|
+
color: #6b7280;
|
|
545
|
+
font-size: 11px;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.qr-grid {
|
|
549
|
+
display: grid;
|
|
550
|
+
grid-template-columns: 1fr 1fr;
|
|
551
|
+
gap: 14px 36px;
|
|
552
|
+
justify-items: center;
|
|
553
|
+
margin: 16px 0;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.qr-placeholder {
|
|
557
|
+
display: grid;
|
|
558
|
+
place-items: center;
|
|
559
|
+
border: 1px dashed #aab3bd;
|
|
560
|
+
background:
|
|
561
|
+
linear-gradient(45deg, #f4f6f8 25%, transparent 25%),
|
|
562
|
+
linear-gradient(-45deg, #f4f6f8 25%, transparent 25%),
|
|
563
|
+
linear-gradient(45deg, transparent 75%, #f4f6f8 75%),
|
|
564
|
+
linear-gradient(-45deg, transparent 75%, #f4f6f8 75%);
|
|
565
|
+
background-size: 10px 10px;
|
|
566
|
+
background-position: 0 0, 0 5px, 5px -5px, -5px 0;
|
|
567
|
+
color: #6b7280;
|
|
568
|
+
font-size: 9px;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.qr-placeholder.small {
|
|
572
|
+
width: 54px;
|
|
573
|
+
height: 54px;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.qr-placeholder.large {
|
|
577
|
+
grid-column: 1 / -1;
|
|
578
|
+
width: 96px;
|
|
579
|
+
height: 96px;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.pass-footer {
|
|
583
|
+
display: grid;
|
|
584
|
+
gap: 4px;
|
|
585
|
+
color: #4b5563;
|
|
586
|
+
font-size: 10px;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.pass-footer strong {
|
|
590
|
+
color: #263241;
|
|
591
|
+
font-size: 15px;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
@media (max-width: 1100px) {
|
|
595
|
+
.printer-grid,
|
|
596
|
+
.template-grid,
|
|
597
|
+
.validity-grid {
|
|
598
|
+
grid-template-columns: 1fr 1fr;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
@media (max-width: 700px) {
|
|
603
|
+
.config-body {
|
|
604
|
+
padding: 22px 16px;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.section-head,
|
|
608
|
+
.printer-grid,
|
|
609
|
+
.template-grid,
|
|
610
|
+
.validity-grid {
|
|
611
|
+
grid-template-columns: 1fr;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.section-head {
|
|
615
|
+
display: grid;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
</style>
|