@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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.10.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 451e46f: Move Bulletin Board Components
8
+
3
9
  ## 1.10.1
4
10
 
5
11
  ### Patch Changes
@@ -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
@@ -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
- // const siteId = route.params.site as string; @TODO
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
- 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);
655
832
  } finally {
656
833
  disable.value = false;
657
834
  }