@7365admin1/layer-common 1.10.0 → 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 +12 -0
- package/components/AcceptDialog.vue +44 -0
- package/components/AccessCard/AvailableStats.vue +55 -0
- package/components/AccessCardAddForm.vue +284 -19
- package/components/AccessCardAssignToUnitForm.vue +440 -0
- package/components/AccessManagement.vue +218 -85
- package/components/AddSupplyForm.vue +165 -0
- package/components/AreaChecklistHistoryLogs.vue +235 -0
- package/components/AreaChecklistHistoryMain.vue +176 -0
- package/components/AreaFormDialog.vue +266 -0
- package/components/AreaMain.vue +863 -0
- package/components/AttendanceCheckInOutDialog.vue +416 -0
- package/components/AttendanceDetailsDialog.vue +184 -0
- package/components/AttendanceMain.vue +155 -0
- package/components/AttendanceMapSearchDialog.vue +393 -0
- package/components/AttendanceSettingsDialog.vue +398 -0
- package/components/BuildingManagement/buildings.vue +5 -5
- package/components/BuildingManagement/units.vue +5 -5
- package/components/BulletinBoardManagement.vue +322 -0
- package/components/ChecklistItemRow.vue +54 -0
- package/components/CheckoutItemMain.vue +705 -0
- package/components/CleaningScheduleMain.vue +271 -0
- package/components/DocumentManagement.vue +4 -0
- package/components/EntryPass/QrTemplatePreview.vue +104 -0
- package/components/EntryPassMain.vue +252 -200
- package/components/HygieneUpdateMoreAction.vue +238 -0
- package/components/ManageChecklistMain.vue +384 -0
- package/components/MemberMain.vue +48 -20
- package/components/MyAttendanceMain.vue +224 -0
- package/components/OnlineFormsConfiguration.vue +9 -2
- package/components/PhotoUpload.vue +410 -0
- package/components/ScheduleAreaMain.vue +313 -0
- package/components/ScheduleTaskAreaFormDialog.vue +144 -0
- package/components/ScheduleTaskAreaUpdateMoreAction.vue +109 -0
- package/components/ScheduleTaskForm.vue +471 -0
- package/components/ScheduleTaskMain.vue +345 -0
- package/components/ScheduleTastTicketMain.vue +182 -0
- package/components/SignaturePad.vue +17 -5
- package/components/StockCard.vue +191 -0
- package/components/SupplyManagementMain.vue +557 -0
- package/components/TableHygiene.vue +617 -0
- package/components/UnitMain.vue +451 -0
- package/components/VisitorManagement.vue +28 -15
- package/composables/useAccessManagement.ts +163 -0
- package/composables/useAreaPermission.ts +51 -0
- package/composables/useAreas.ts +99 -0
- package/composables/useAttendance.ts +89 -0
- package/composables/useAttendancePermission.ts +68 -0
- package/composables/useBuilding.ts +2 -2
- package/composables/useBuildingUnit.ts +2 -2
- package/composables/useBulletin.ts +82 -0
- package/composables/useCard.ts +2 -0
- package/composables/useCheckout.ts +61 -0
- package/composables/useCheckoutPermission.ts +80 -0
- package/composables/useCleaningPermission.ts +229 -0
- package/composables/useCleaningSchedulePermission.ts +58 -0
- package/composables/useCleaningSchedules.ts +233 -0
- package/composables/useCountry.ts +8 -0
- package/composables/useDashboardData.ts +2 -2
- package/composables/useFeedback.ts +1 -1
- package/composables/useLocation.ts +78 -0
- package/composables/useOnlineForm.ts +16 -9
- package/composables/usePeople.ts +87 -72
- package/composables/useQR.ts +29 -0
- package/composables/useScheduleTask.ts +89 -0
- package/composables/useScheduleTaskArea.ts +85 -0
- package/composables/useScheduleTaskPermission.ts +68 -0
- package/composables/useSiteEntryPassSettings.ts +4 -15
- package/composables/useStock.ts +45 -0
- package/composables/useSupply.ts +63 -0
- package/composables/useSupplyPermission.ts +92 -0
- package/composables/useUnitPermission.ts +51 -0
- package/composables/useUnits.ts +82 -0
- package/composables/useWebUsb.ts +389 -0
- package/composables/useWorkOrder.ts +1 -1
- package/nuxt.config.ts +3 -0
- package/package.json +4 -1
- package/types/area.d.ts +22 -0
- package/types/attendance.d.ts +38 -0
- package/types/checkout-item.d.ts +27 -0
- package/types/cleaner-schedule.d.ts +54 -0
- package/types/location.d.ts +42 -0
- package/types/schedule-task.d.ts +18 -0
- package/types/stock.d.ts +16 -0
- package/types/supply.d.ts +11 -0
- package/utils/acm-crypto.ts +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog
|
|
3
|
+
:model-value="modelValue"
|
|
4
|
+
@update:model-value="val => $emit('update:modelValue', val)"
|
|
5
|
+
max-width="420"
|
|
6
|
+
persistent
|
|
7
|
+
>
|
|
8
|
+
<v-card class="pa-4" style="border-radius:12px;">
|
|
9
|
+
<v-card-title class="headline" style="font-size:1.3rem; font-weight:500;">
|
|
10
|
+
{{ title }}
|
|
11
|
+
</v-card-title>
|
|
12
|
+
<v-card-text style="font-size:1.05rem; margin-bottom:24px;">
|
|
13
|
+
{{ message }}
|
|
14
|
+
</v-card-text>
|
|
15
|
+
<v-card-actions class="justify-end" style="gap:12px;">
|
|
16
|
+
<v-btn
|
|
17
|
+
color="primary"
|
|
18
|
+
variant="outlined"
|
|
19
|
+
style="min-width:70px; background:#e3f2fd; color:#1976d2; font-weight:500;"
|
|
20
|
+
@click="$emit('confirm')"
|
|
21
|
+
>
|
|
22
|
+
Yes
|
|
23
|
+
</v-btn>
|
|
24
|
+
<v-btn
|
|
25
|
+
color="error"
|
|
26
|
+
variant="outlined"
|
|
27
|
+
style="min-width:70px; background:#ffebee; color:#d32f2f; font-weight:500;"
|
|
28
|
+
@click="$emit('cancel')"
|
|
29
|
+
>
|
|
30
|
+
No
|
|
31
|
+
</v-btn>
|
|
32
|
+
</v-card-actions>
|
|
33
|
+
</v-card>
|
|
34
|
+
</v-dialog>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
defineProps<{
|
|
39
|
+
modelValue: boolean,
|
|
40
|
+
title?: string,
|
|
41
|
+
message?: string
|
|
42
|
+
}>();
|
|
43
|
+
defineEmits(['confirm', 'cancel', 'update:modelValue']);
|
|
44
|
+
</script>
|
|
@@ -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
|
|
@@ -91,7 +199,7 @@
|
|
|
91
199
|
<v-col cols="12">
|
|
92
200
|
<InputLabel
|
|
93
201
|
class="text-capitalize font-weight-bold"
|
|
94
|
-
title="
|
|
202
|
+
title="Door Location"
|
|
95
203
|
required
|
|
96
204
|
/>
|
|
97
205
|
<v-select
|
|
@@ -115,6 +223,8 @@
|
|
|
115
223
|
v-model="card.accessGroup"
|
|
116
224
|
density="compact"
|
|
117
225
|
:items="accessGroupItems"
|
|
226
|
+
item-title="name"
|
|
227
|
+
item-value="no"
|
|
118
228
|
hide-details
|
|
119
229
|
:rules="[requiredRule]"
|
|
120
230
|
multiple
|
|
@@ -179,7 +289,7 @@
|
|
|
179
289
|
hide-details
|
|
180
290
|
:rules="card.useAsLiftCard ? [requiredRule] : []"
|
|
181
291
|
item-title="name"
|
|
182
|
-
item-value="
|
|
292
|
+
item-value="no"
|
|
183
293
|
/>
|
|
184
294
|
</v-col>
|
|
185
295
|
<v-col cols="12">
|
|
@@ -296,7 +406,7 @@
|
|
|
296
406
|
color="black"
|
|
297
407
|
class="text-none"
|
|
298
408
|
size="48"
|
|
299
|
-
:disabled="!validForm || disable"
|
|
409
|
+
:disabled="(uploadMode === 'single' ? !validForm : !bulkFile || !entryPassTemplate.id || templateLoading) || disable"
|
|
300
410
|
@click="submit"
|
|
301
411
|
:loading="disable"
|
|
302
412
|
>
|
|
@@ -327,12 +437,44 @@ const prop = defineProps({
|
|
|
327
437
|
const { add: _addCard, updateById: _updateCardById } = useCard();
|
|
328
438
|
const { getAll: _getBuildings } = useBuilding();
|
|
329
439
|
const { getAllUnits: _getUnits } = useBuildingUnit();
|
|
330
|
-
const
|
|
440
|
+
const {
|
|
441
|
+
getDoorAccessLevels: _getDoorAccessLevels,
|
|
442
|
+
getLiftAccessLevels: _getLiftAccessLevels,
|
|
443
|
+
getAccessGroups: _getAccessGroups,
|
|
444
|
+
addPhysicalCard: _addPhysicalCard,
|
|
445
|
+
addNonPhysicalCard: _addNonPhysicalCard,
|
|
446
|
+
bulkPhysicalAccessCard: _bulkPhysicalAccessCard,
|
|
447
|
+
} = useAccessManagement();
|
|
448
|
+
const { getBySiteId: _getEntryPassSettings } = useSiteEntryPassSettings();
|
|
449
|
+
const { getFileUrl } = useFile();
|
|
450
|
+
|
|
451
|
+
const config = useRuntimeConfig();
|
|
452
|
+
const emit = defineEmits(["cancel", "success", "error"]);
|
|
331
453
|
|
|
332
454
|
const validForm = ref(false);
|
|
333
455
|
const disable = ref(false);
|
|
334
456
|
const message = ref("");
|
|
335
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
|
+
|
|
336
478
|
const { requiredRule } = useUtils();
|
|
337
479
|
|
|
338
480
|
function formatDate(date: Date): string {
|
|
@@ -429,11 +571,53 @@ const buildingItems = ref<{ name: string; value: string }[]>([]);
|
|
|
429
571
|
const levelItems = ref<{ name: string; value: string }[]>([]);
|
|
430
572
|
const unitItems = ref<{ name: string; value: string }[]>([]);
|
|
431
573
|
const buildingsData = ref<Record<string, any>[]>([]);
|
|
574
|
+
const encryptedAcmUrl = ref("");
|
|
432
575
|
|
|
433
576
|
const route = useRoute();
|
|
434
|
-
const siteId =
|
|
577
|
+
// const siteId = '66ab2f1381856008f1887971' as string;
|
|
578
|
+
//@TODO
|
|
579
|
+
const siteId = route.params.site as string;
|
|
435
580
|
const orgId = route.params.org as string;
|
|
436
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
|
+
|
|
601
|
+
onMounted(async () => {
|
|
602
|
+
try {
|
|
603
|
+
const { encrypted: acmUrl } = await $fetch<{ encrypted: string }>(
|
|
604
|
+
"/api/encrypt-acm-url"
|
|
605
|
+
);
|
|
606
|
+
encryptedAcmUrl.value = acmUrl;
|
|
607
|
+
const [doorLevels, liftLevels, groups] = await Promise.all([
|
|
608
|
+
_getDoorAccessLevels(acmUrl),
|
|
609
|
+
_getLiftAccessLevels(acmUrl),
|
|
610
|
+
_getAccessGroups(acmUrl),
|
|
611
|
+
]);
|
|
612
|
+
accessLevelItems.value = doorLevels.data ?? [];
|
|
613
|
+
liftAccessLevelItems.value = liftLevels.data ?? [];
|
|
614
|
+
accessGroupItems.value = groups.data ?? [];
|
|
615
|
+
} catch (error) {
|
|
616
|
+
console.error("Failed to fetch access management data:", error);
|
|
617
|
+
emit("error", "EntryPass server is unavailable. Please contact your administrator.");
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
437
621
|
function limitCardNumber() {
|
|
438
622
|
if (card.value.cardNumber && card.value.cardNumber.toString().length > cardNoMaxLength.value) {
|
|
439
623
|
card.value.cardNumber = card.value.cardNumber.toString().slice(0, cardNoMaxLength.value);
|
|
@@ -543,27 +727,108 @@ function cancel() {
|
|
|
543
727
|
emit("cancel");
|
|
544
728
|
}
|
|
545
729
|
|
|
730
|
+
function buildPhysicalCardPayload() {
|
|
731
|
+
const now = new Date().toISOString();
|
|
732
|
+
const doorItem = accessLevelItems.value.find((i: any) => i.no === card.value.door);
|
|
733
|
+
const liftItem = liftAccessLevelItems.value.find((i: any) => i.no === card.value.liftAccessLevel);
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
site: siteId,
|
|
737
|
+
cardNo: card.value.cardNumber,
|
|
738
|
+
isActivated: card.value.isActivate,
|
|
739
|
+
isAntiPassBack: card.value.isAntiPassBack,
|
|
740
|
+
isLiftCard: card.value.useAsLiftCard,
|
|
741
|
+
userType: "Visitor/Resident",
|
|
742
|
+
accessLevel: card.value.door,
|
|
743
|
+
accessGroup: card.value.accessGroup,
|
|
744
|
+
accessType: card.value.cardType,
|
|
745
|
+
startDate: card.value.startDate,
|
|
746
|
+
endDate: card.value.endDate,
|
|
747
|
+
doorName: doorItem?.name ?? "",
|
|
748
|
+
createdAt: now,
|
|
749
|
+
updatedAt: now,
|
|
750
|
+
...(card.value.pinNo ? { pin: card.value.pinNo } : {}),
|
|
751
|
+
...(card.value.useAsLiftCard
|
|
752
|
+
? {
|
|
753
|
+
liftAccessLevel: card.value.liftAccessLevel,
|
|
754
|
+
liftName: liftItem?.name ?? "",
|
|
755
|
+
}
|
|
756
|
+
: {}),
|
|
757
|
+
...(card.value.assignUnit ? { unit: [card.value.assignUnit] } : {}),
|
|
758
|
+
...(card.value.isWinsland !== undefined ? { isWinsland: card.value.isWinsland } : {}),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function buildNonPhysicalCardPayload() {
|
|
763
|
+
const now = new Date().toISOString();
|
|
764
|
+
const doorItem = accessLevelItems.value.find((i: any) => i.no === card.value.door);
|
|
765
|
+
const liftItem = liftAccessLevelItems.value.find((i: any) => i.no === card.value.liftAccessLevel);
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
site: siteId,
|
|
769
|
+
quantity: card.value.quantity,
|
|
770
|
+
accessLevel: card.value.door,
|
|
771
|
+
isLiftCard: card.value.useAsLiftCard,
|
|
772
|
+
accessGroup: card.value.accessGroup,
|
|
773
|
+
userType: "Visitor/Resident",
|
|
774
|
+
doorName: doorItem?.name ?? "",
|
|
775
|
+
startDate: card.value.startDate,
|
|
776
|
+
endDate: card.value.endDate,
|
|
777
|
+
createdAt: now,
|
|
778
|
+
updatedAt: now,
|
|
779
|
+
...(card.value.useAsLiftCard
|
|
780
|
+
? {
|
|
781
|
+
liftAccessLevel: card.value.liftAccessLevel,
|
|
782
|
+
liftName: liftItem?.name ?? "",
|
|
783
|
+
}
|
|
784
|
+
: {}),
|
|
785
|
+
...(card.value.assignUnit ? { unit: [card.value.assignUnit] } : {}),
|
|
786
|
+
...(card.value.isWinsland !== undefined ? { isWinsland: card.value.isWinsland } : {}),
|
|
787
|
+
};
|
|
788
|
+
}
|
|
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
|
+
|
|
546
808
|
async function submit() {
|
|
547
809
|
disable.value = true;
|
|
548
810
|
try {
|
|
811
|
+
if (uploadMode.value === "bulk") {
|
|
812
|
+
await _bulkPhysicalAccessCard({ site: siteId, file: bulkFile.value! });
|
|
813
|
+
emit("success");
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
549
817
|
if (prop.mode === "add") {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
await _addCard(payload);
|
|
818
|
+
if (isNonPhysicalCard.value) {
|
|
819
|
+
await _addNonPhysicalCard(buildNonPhysicalCardPayload());
|
|
820
|
+
} else {
|
|
821
|
+
await _addPhysicalCard(buildPhysicalCardPayload());
|
|
822
|
+
}
|
|
556
823
|
}
|
|
557
824
|
|
|
558
825
|
if (prop.mode === "edit") {
|
|
559
|
-
|
|
560
|
-
...card.value,
|
|
561
|
-
};
|
|
562
|
-
await _updateCardById(prop.card._id ?? "", payload);
|
|
826
|
+
await _updateCardById(prop.card._id ?? "", { ...card.value });
|
|
563
827
|
}
|
|
564
828
|
emit("success");
|
|
565
829
|
} catch (error: any) {
|
|
566
|
-
|
|
830
|
+
const msg = error.response?._data?.message || error?.data?.message || "An error occurred. Please try again.";
|
|
831
|
+
emit("error", msg);
|
|
567
832
|
} finally {
|
|
568
833
|
disable.value = false;
|
|
569
834
|
}
|