@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AcceptDialog.vue +44 -0
  3. package/components/AccessCard/AvailableStats.vue +55 -0
  4. package/components/AccessCardAddForm.vue +284 -19
  5. package/components/AccessCardAssignToUnitForm.vue +440 -0
  6. package/components/AccessManagement.vue +218 -85
  7. package/components/AddSupplyForm.vue +165 -0
  8. package/components/AreaChecklistHistoryLogs.vue +235 -0
  9. package/components/AreaChecklistHistoryMain.vue +176 -0
  10. package/components/AreaFormDialog.vue +266 -0
  11. package/components/AreaMain.vue +863 -0
  12. package/components/AttendanceCheckInOutDialog.vue +416 -0
  13. package/components/AttendanceDetailsDialog.vue +184 -0
  14. package/components/AttendanceMain.vue +155 -0
  15. package/components/AttendanceMapSearchDialog.vue +393 -0
  16. package/components/AttendanceSettingsDialog.vue +398 -0
  17. package/components/BuildingManagement/buildings.vue +5 -5
  18. package/components/BuildingManagement/units.vue +5 -5
  19. package/components/BulletinBoardManagement.vue +322 -0
  20. package/components/ChecklistItemRow.vue +54 -0
  21. package/components/CheckoutItemMain.vue +705 -0
  22. package/components/CleaningScheduleMain.vue +271 -0
  23. package/components/DocumentManagement.vue +4 -0
  24. package/components/EntryPass/QrTemplatePreview.vue +104 -0
  25. package/components/EntryPassMain.vue +252 -200
  26. package/components/HygieneUpdateMoreAction.vue +238 -0
  27. package/components/ManageChecklistMain.vue +384 -0
  28. package/components/MemberMain.vue +48 -20
  29. package/components/MyAttendanceMain.vue +224 -0
  30. package/components/OnlineFormsConfiguration.vue +9 -2
  31. package/components/PhotoUpload.vue +410 -0
  32. package/components/ScheduleAreaMain.vue +313 -0
  33. package/components/ScheduleTaskAreaFormDialog.vue +144 -0
  34. package/components/ScheduleTaskAreaUpdateMoreAction.vue +109 -0
  35. package/components/ScheduleTaskForm.vue +471 -0
  36. package/components/ScheduleTaskMain.vue +345 -0
  37. package/components/ScheduleTastTicketMain.vue +182 -0
  38. package/components/SignaturePad.vue +17 -5
  39. package/components/StockCard.vue +191 -0
  40. package/components/SupplyManagementMain.vue +557 -0
  41. package/components/TableHygiene.vue +617 -0
  42. package/components/UnitMain.vue +451 -0
  43. package/components/VisitorManagement.vue +28 -15
  44. package/composables/useAccessManagement.ts +163 -0
  45. package/composables/useAreaPermission.ts +51 -0
  46. package/composables/useAreas.ts +99 -0
  47. package/composables/useAttendance.ts +89 -0
  48. package/composables/useAttendancePermission.ts +68 -0
  49. package/composables/useBuilding.ts +2 -2
  50. package/composables/useBuildingUnit.ts +2 -2
  51. package/composables/useBulletin.ts +82 -0
  52. package/composables/useCard.ts +2 -0
  53. package/composables/useCheckout.ts +61 -0
  54. package/composables/useCheckoutPermission.ts +80 -0
  55. package/composables/useCleaningPermission.ts +229 -0
  56. package/composables/useCleaningSchedulePermission.ts +58 -0
  57. package/composables/useCleaningSchedules.ts +233 -0
  58. package/composables/useCountry.ts +8 -0
  59. package/composables/useDashboardData.ts +2 -2
  60. package/composables/useFeedback.ts +1 -1
  61. package/composables/useLocation.ts +78 -0
  62. package/composables/useOnlineForm.ts +16 -9
  63. package/composables/usePeople.ts +87 -72
  64. package/composables/useQR.ts +29 -0
  65. package/composables/useScheduleTask.ts +89 -0
  66. package/composables/useScheduleTaskArea.ts +85 -0
  67. package/composables/useScheduleTaskPermission.ts +68 -0
  68. package/composables/useSiteEntryPassSettings.ts +4 -15
  69. package/composables/useStock.ts +45 -0
  70. package/composables/useSupply.ts +63 -0
  71. package/composables/useSupplyPermission.ts +92 -0
  72. package/composables/useUnitPermission.ts +51 -0
  73. package/composables/useUnits.ts +82 -0
  74. package/composables/useWebUsb.ts +389 -0
  75. package/composables/useWorkOrder.ts +1 -1
  76. package/nuxt.config.ts +3 -0
  77. package/package.json +4 -1
  78. package/types/area.d.ts +22 -0
  79. package/types/attendance.d.ts +38 -0
  80. package/types/checkout-item.d.ts +27 -0
  81. package/types/cleaner-schedule.d.ts +54 -0
  82. package/types/location.d.ts +42 -0
  83. package/types/schedule-task.d.ts +18 -0
  84. package/types/stock.d.ts +16 -0
  85. package/types/supply.d.ts +11 -0
  86. package/utils/acm-crypto.ts +30 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.10.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 451e46f: Move Bulletin Board Components
8
+
9
+ ## 1.10.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 72d3443: Update Changes
14
+
3
15
  ## 1.10.0
4
16
 
5
17
  ### Minor Changes
@@ -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
- <v-form v-model="validForm" :disabled="disable">
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="Types of Access Card"
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="Doors Location"
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="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 emit = defineEmits(["cancel", "success"]);
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 = route.params.site as string;
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
- const payload = {
551
- ...card.value,
552
- site: siteId,
553
- org: orgId,
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
- const payload = {
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
- message.value = error.response?._data?.message || "Failed to create card";
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
  }