@7365admin1/layer-common 1.10.1 → 1.10.2
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/AccessCard/AvailableStats.vue +55 -0
- package/components/AccessCardAddForm.vue +185 -8
- package/components/AccessCardAssignToUnitForm.vue +440 -0
- package/components/AccessManagement.vue +106 -56
- package/components/AreaMain.vue +26 -4
- package/components/BulletinBoardManagement.vue +322 -0
- package/components/SignaturePad.vue +17 -5
- package/components/SupplyManagementMain.vue +1 -1
- package/composables/useAccessManagement.ts +73 -0
- package/composables/useBulletin.ts +82 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters align="center" style="gap: 8px">
|
|
3
|
+
<span class="text-caption text-medium-emphasis mr-1">Available:</span>
|
|
4
|
+
<v-chip
|
|
5
|
+
size="small"
|
|
6
|
+
variant="tonal"
|
|
7
|
+
color="primary"
|
|
8
|
+
prepend-icon="mdi-card-account-details"
|
|
9
|
+
:loading="loading"
|
|
10
|
+
>
|
|
11
|
+
NFC: <strong class="ml-1">{{ availablePhysical }}</strong>
|
|
12
|
+
</v-chip>
|
|
13
|
+
<v-chip
|
|
14
|
+
size="small"
|
|
15
|
+
variant="tonal"
|
|
16
|
+
color="secondary"
|
|
17
|
+
prepend-icon="mdi-qrcode"
|
|
18
|
+
:loading="loading"
|
|
19
|
+
>
|
|
20
|
+
QR Code: <strong class="ml-1">{{ availableNonPhysical }}</strong>
|
|
21
|
+
</v-chip>
|
|
22
|
+
</v-row>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
const props = defineProps({
|
|
27
|
+
siteId: {
|
|
28
|
+
type: String,
|
|
29
|
+
required: true,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const { getAllAccessCardsCounts } = useAccessManagement();
|
|
34
|
+
|
|
35
|
+
const availablePhysical = ref(0);
|
|
36
|
+
const availableNonPhysical = ref(0);
|
|
37
|
+
const loading = ref(false);
|
|
38
|
+
|
|
39
|
+
async function refresh() {
|
|
40
|
+
loading.value = true;
|
|
41
|
+
try {
|
|
42
|
+
const res = await getAllAccessCardsCounts({ site: props.siteId, userType: "Visitor/Resident" });
|
|
43
|
+
availablePhysical.value = res?.data?.available_physical ?? 0;
|
|
44
|
+
availableNonPhysical.value = res?.data?.available_non_physical ?? 0;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("Failed to fetch available card counts:", error);
|
|
47
|
+
} finally {
|
|
48
|
+
loading.value = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onMounted(() => refresh());
|
|
53
|
+
|
|
54
|
+
defineExpose({ refresh });
|
|
55
|
+
</script>
|
|
@@ -1,19 +1,127 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-card width="100%">
|
|
3
3
|
<v-toolbar>
|
|
4
|
-
<v-row no-gutters class="fill-height px-6" align="center">
|
|
4
|
+
<v-row no-gutters class="fill-height px-6" align="center" justify="space-between">
|
|
5
5
|
<span class="font-weight-bold text-h5 text-capitalize">
|
|
6
6
|
{{ prop.mode }} Access Card
|
|
7
7
|
</span>
|
|
8
|
+
<v-btn-toggle
|
|
9
|
+
v-if="prop.mode === 'add'"
|
|
10
|
+
v-model="uploadMode"
|
|
11
|
+
mandatory
|
|
12
|
+
color="primary"
|
|
13
|
+
density="compact"
|
|
14
|
+
rounded="pill"
|
|
15
|
+
variant="outlined"
|
|
16
|
+
>
|
|
17
|
+
<v-btn value="single" size="small" class="text-none px-4">Single</v-btn>
|
|
18
|
+
<v-btn value="bulk" size="small" class="text-none px-4">Bulk Upload</v-btn>
|
|
19
|
+
</v-btn-toggle>
|
|
8
20
|
</v-row>
|
|
9
21
|
</v-toolbar>
|
|
10
22
|
<v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-0">
|
|
11
|
-
|
|
23
|
+
<!-- Bulk Upload Mode -->
|
|
24
|
+
<template v-if="uploadMode === 'bulk'">
|
|
25
|
+
<v-row no-gutters class="px-6 pt-4 pb-2">
|
|
26
|
+
<v-col cols="12" class="px-1 mb-2">
|
|
27
|
+
<v-progress-linear v-if="templateLoading" indeterminate color="primary" class="mb-2" />
|
|
28
|
+
<v-alert
|
|
29
|
+
v-else-if="!entryPassTemplate.id"
|
|
30
|
+
type="warning"
|
|
31
|
+
variant="tonal"
|
|
32
|
+
density="compact"
|
|
33
|
+
icon="mdi-alert-outline"
|
|
34
|
+
>
|
|
35
|
+
No upload template has been configured. Please upload a template in
|
|
36
|
+
<strong>Entry Pass Settings</strong> before using bulk upload.
|
|
37
|
+
</v-alert>
|
|
38
|
+
<template v-else-if="entryPassTemplate.id">
|
|
39
|
+
<InputLabel
|
|
40
|
+
class="text-capitalize font-weight-bold"
|
|
41
|
+
title="Download Template"
|
|
42
|
+
/>
|
|
43
|
+
<div class="d-flex align-center mt-1">
|
|
44
|
+
<v-icon size="20" class="mr-2 text-primary">mdi-paperclip</v-icon>
|
|
45
|
+
<a
|
|
46
|
+
href="#"
|
|
47
|
+
class="text-primary text-decoration-none text-body-2"
|
|
48
|
+
@click.prevent="downloadBulkTemplate"
|
|
49
|
+
>
|
|
50
|
+
{{ entryPassTemplate.name }}
|
|
51
|
+
</a>
|
|
52
|
+
<v-btn
|
|
53
|
+
icon
|
|
54
|
+
density="compact"
|
|
55
|
+
variant="text"
|
|
56
|
+
size="small"
|
|
57
|
+
class="ml-1"
|
|
58
|
+
@click.prevent="downloadBulkTemplate"
|
|
59
|
+
>
|
|
60
|
+
<v-icon size="18">mdi-download</v-icon>
|
|
61
|
+
</v-btn>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Column Guide -->
|
|
65
|
+
<div class="mt-3">
|
|
66
|
+
<p class="text-caption font-weight-bold text-grey-darken-2 mb-1 text-uppercase">
|
|
67
|
+
Template Column Guide
|
|
68
|
+
</p>
|
|
69
|
+
<v-table density="compact" class="text-caption rounded border">
|
|
70
|
+
<thead>
|
|
71
|
+
<tr class="bg-grey-lighten-4">
|
|
72
|
+
<th class="text-left text-caption font-weight-bold py-1 px-2">Column</th>
|
|
73
|
+
<th class="text-left text-caption font-weight-bold py-1 px-2">Format / Accepted Values</th>
|
|
74
|
+
<th class="text-left text-caption font-weight-bold py-1 px-2">Example</th>
|
|
75
|
+
</tr>
|
|
76
|
+
</thead>
|
|
77
|
+
<tbody>
|
|
78
|
+
<tr v-for="col in templateColumnGuide" :key="col.name">
|
|
79
|
+
<td class="py-1 px-2 font-weight-medium" style="white-space: nowrap">{{ col.name }}</td>
|
|
80
|
+
<td class="py-1 px-2 text-grey-darken-1">{{ col.format }}</td>
|
|
81
|
+
<td class="py-1 px-2 text-grey-darken-2">{{ col.example }}</td>
|
|
82
|
+
</tr>
|
|
83
|
+
</tbody>
|
|
84
|
+
</v-table>
|
|
85
|
+
</div>
|
|
86
|
+
</template>
|
|
87
|
+
</v-col>
|
|
88
|
+
<v-col cols="12" class="px-1 mt-2">
|
|
89
|
+
<InputLabel
|
|
90
|
+
class="text-capitalize font-weight-bold"
|
|
91
|
+
title="Upload File"
|
|
92
|
+
required
|
|
93
|
+
/>
|
|
94
|
+
<v-file-input
|
|
95
|
+
v-model="bulkFile"
|
|
96
|
+
density="compact"
|
|
97
|
+
hide-details="auto"
|
|
98
|
+
accept=".xlsx,.xls,.csv"
|
|
99
|
+
placeholder="Select file to upload..."
|
|
100
|
+
prepend-icon=""
|
|
101
|
+
prepend-inner-icon="mdi-tray-arrow-up"
|
|
102
|
+
:rules="[requiredRule]"
|
|
103
|
+
:disabled="!entryPassTemplate.id || templateLoading"
|
|
104
|
+
/>
|
|
105
|
+
<v-alert
|
|
106
|
+
v-if="!templateLoading && !entryPassTemplate.id"
|
|
107
|
+
type="info"
|
|
108
|
+
variant="text"
|
|
109
|
+
density="compact"
|
|
110
|
+
class="mt-1 px-0"
|
|
111
|
+
>
|
|
112
|
+
Upload is disabled until a template is configured.
|
|
113
|
+
</v-alert>
|
|
114
|
+
</v-col>
|
|
115
|
+
</v-row>
|
|
116
|
+
</template>
|
|
117
|
+
|
|
118
|
+
<!-- Single Add Mode -->
|
|
119
|
+
<v-form v-if="uploadMode === 'single'" v-model="validForm" :disabled="disable">
|
|
12
120
|
<v-row no-gutters class="px-6 pt-4">
|
|
13
121
|
<v-col cols="12" class="px-1">
|
|
14
122
|
<InputLabel
|
|
15
123
|
class="text-capitalize font-weight-bold"
|
|
16
|
-
title="
|
|
124
|
+
title="Type of Access Card"
|
|
17
125
|
required
|
|
18
126
|
/>
|
|
19
127
|
<v-select
|
|
@@ -298,7 +406,7 @@
|
|
|
298
406
|
color="black"
|
|
299
407
|
class="text-none"
|
|
300
408
|
size="48"
|
|
301
|
-
:disabled="!validForm || disable"
|
|
409
|
+
:disabled="(uploadMode === 'single' ? !validForm : !bulkFile || !entryPassTemplate.id || templateLoading) || disable"
|
|
302
410
|
@click="submit"
|
|
303
411
|
:loading="disable"
|
|
304
412
|
>
|
|
@@ -335,15 +443,38 @@ const {
|
|
|
335
443
|
getAccessGroups: _getAccessGroups,
|
|
336
444
|
addPhysicalCard: _addPhysicalCard,
|
|
337
445
|
addNonPhysicalCard: _addNonPhysicalCard,
|
|
446
|
+
bulkPhysicalAccessCard: _bulkPhysicalAccessCard,
|
|
338
447
|
} = useAccessManagement();
|
|
448
|
+
const { getBySiteId: _getEntryPassSettings } = useSiteEntryPassSettings();
|
|
449
|
+
const { getFileUrl } = useFile();
|
|
339
450
|
|
|
340
451
|
const config = useRuntimeConfig();
|
|
341
|
-
const emit = defineEmits(["cancel", "success"]);
|
|
452
|
+
const emit = defineEmits(["cancel", "success", "error"]);
|
|
342
453
|
|
|
343
454
|
const validForm = ref(false);
|
|
344
455
|
const disable = ref(false);
|
|
345
456
|
const message = ref("");
|
|
346
457
|
|
|
458
|
+
// Bulk upload mode state
|
|
459
|
+
const uploadMode = ref<"single" | "bulk">("single");
|
|
460
|
+
const bulkFile = ref<File | null>(null);
|
|
461
|
+
const entryPassTemplate = ref<{ id: string; name: string }>({ id: "", name: "" });
|
|
462
|
+
|
|
463
|
+
const templateColumnGuide = [
|
|
464
|
+
{ name: "startDate", format: "MM/DD/YYYY", example: "02/26/2026" },
|
|
465
|
+
{ name: "endDate", format: "MM/DD/YYYY", example: "02/26/2036" },
|
|
466
|
+
{ name: "cardNo", format: "Number 0 – 65535", example: "301" },
|
|
467
|
+
{ name: "facilityCode", format: "Number 0 – 255", example: "11" },
|
|
468
|
+
{ name: "pin", format: "6-digit number", example: "123456" },
|
|
469
|
+
{ name: "accessLevel", format: "Number (access level ID)", example: "1" },
|
|
470
|
+
{ name: "userType", format: "Contractor | Visitor | Resident/Tenant | Visitor/Resident", example: "Visitor/Resident" },
|
|
471
|
+
{ name: "accessGroup", format: "Group name (e.g. Full Access, No Access)", example: "Full Access" },
|
|
472
|
+
{ name: "isLiftCard", format: "TRUE or FALSE", example: "FALSE" },
|
|
473
|
+
{ name: "liftAccessLevel", format: "Number (lift level ID) — required if isLiftCard is TRUE", example: "1" },
|
|
474
|
+
{ name: "Door Name", format: "Name of the door", example: "Main Door" },
|
|
475
|
+
{ name: "Lift Name", format: "Name of the lift — required if isLiftCard is TRUE", example: "Main Lift" },
|
|
476
|
+
];
|
|
477
|
+
|
|
347
478
|
const { requiredRule } = useUtils();
|
|
348
479
|
|
|
349
480
|
function formatDate(date: Date): string {
|
|
@@ -443,10 +574,30 @@ const buildingsData = ref<Record<string, any>[]>([]);
|
|
|
443
574
|
const encryptedAcmUrl = ref("");
|
|
444
575
|
|
|
445
576
|
const route = useRoute();
|
|
446
|
-
const siteId = '66ab2f1381856008f1887971' as string;
|
|
447
|
-
|
|
577
|
+
// const siteId = '66ab2f1381856008f1887971' as string;
|
|
578
|
+
//@TODO
|
|
579
|
+
const siteId = route.params.site as string;
|
|
448
580
|
const orgId = route.params.org as string;
|
|
449
581
|
|
|
582
|
+
const templateLoading = ref(false);
|
|
583
|
+
|
|
584
|
+
async function fetchEntryPassTemplate() {
|
|
585
|
+
templateLoading.value = true;
|
|
586
|
+
try {
|
|
587
|
+
const settingsData: any = await _getEntryPassSettings(siteId);
|
|
588
|
+
const template = settingsData?.data?.settings?.template;
|
|
589
|
+
entryPassTemplate.value = template?.id ? template : { id: "", name: "" };
|
|
590
|
+
} catch {
|
|
591
|
+
entryPassTemplate.value = { id: "", name: "" };
|
|
592
|
+
} finally {
|
|
593
|
+
templateLoading.value = false;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
watch(uploadMode, (mode) => {
|
|
598
|
+
if (mode === "bulk") fetchEntryPassTemplate();
|
|
599
|
+
});
|
|
600
|
+
|
|
450
601
|
onMounted(async () => {
|
|
451
602
|
try {
|
|
452
603
|
const { encrypted: acmUrl } = await $fetch<{ encrypted: string }>(
|
|
@@ -463,6 +614,7 @@ onMounted(async () => {
|
|
|
463
614
|
accessGroupItems.value = groups.data ?? [];
|
|
464
615
|
} catch (error) {
|
|
465
616
|
console.error("Failed to fetch access management data:", error);
|
|
617
|
+
emit("error", "EntryPass server is unavailable. Please contact your administrator.");
|
|
466
618
|
}
|
|
467
619
|
});
|
|
468
620
|
|
|
@@ -635,9 +787,33 @@ function buildNonPhysicalCardPayload() {
|
|
|
635
787
|
};
|
|
636
788
|
}
|
|
637
789
|
|
|
790
|
+
async function downloadBulkTemplate() {
|
|
791
|
+
const { id, name } = entryPassTemplate.value;
|
|
792
|
+
try {
|
|
793
|
+
const response = await fetch(getFileUrl(id));
|
|
794
|
+
const blob = await response.blob();
|
|
795
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
796
|
+
const link = document.createElement("a");
|
|
797
|
+
link.href = blobUrl;
|
|
798
|
+
link.download = name || "access-card-template";
|
|
799
|
+
document.body.appendChild(link);
|
|
800
|
+
link.click();
|
|
801
|
+
document.body.removeChild(link);
|
|
802
|
+
URL.revokeObjectURL(blobUrl);
|
|
803
|
+
} catch {
|
|
804
|
+
console.error("Failed to download template");
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
638
808
|
async function submit() {
|
|
639
809
|
disable.value = true;
|
|
640
810
|
try {
|
|
811
|
+
if (uploadMode.value === "bulk") {
|
|
812
|
+
await _bulkPhysicalAccessCard({ site: siteId, file: bulkFile.value! });
|
|
813
|
+
emit("success");
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
641
817
|
if (prop.mode === "add") {
|
|
642
818
|
if (isNonPhysicalCard.value) {
|
|
643
819
|
await _addNonPhysicalCard(buildNonPhysicalCardPayload());
|
|
@@ -651,7 +827,8 @@ async function submit() {
|
|
|
651
827
|
}
|
|
652
828
|
emit("success");
|
|
653
829
|
} catch (error: any) {
|
|
654
|
-
|
|
830
|
+
const msg = error.response?._data?.message || error?.data?.message || "An error occurred. Please try again.";
|
|
831
|
+
emit("error", msg);
|
|
655
832
|
} finally {
|
|
656
833
|
disable.value = false;
|
|
657
834
|
}
|