@7365admin1/layer-common 1.11.2 → 1.11.4

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.
@@ -0,0 +1,267 @@
1
+ <template>
2
+ <v-form v-model="isInternalFormValid" ref="internalFormRef" :readonly="viewMode">
3
+ <v-row no-gutters>
4
+ <v-col cols="12">
5
+ <v-row no-gutters class="d-flex align-center ga-2 justify-space-between">
6
+ <InputLabel class="text-capitalize" title="Overnight Parking Approval Hours" />
7
+ <span class="d-flex ga-2 align-center">
8
+ <p>Auto Approve Parking?</p>
9
+ <v-switch v-model="autoApproveOvernightParking" hide-details color="success" size="large" />
10
+ </span>
11
+ </v-row>
12
+ <v-card class="mt-2">
13
+ <v-card-text>
14
+ <template v-for="(slot, index) in overnightParkingSlotsArray" :key="slot.day">
15
+ <v-row no-gutters class="mb-4">
16
+ <v-col cols="12" class="text-capitalize mb-2 text-subtitle-1 d-flex align-center justify-space-between">
17
+ <span class="font-weight-black">{{ slot.day }}</span>
18
+ <v-icon :icon="slot?.isEnabled ? 'mdi-check-circle' : 'mdi-check-circle-outline'" size="20"
19
+ :class="{ 'cursor-pointer': !viewMode }"
20
+ :color="slot.isEnabled ? 'green-darken-2' : 'black-lighten-1'"
21
+ @click="!viewMode && toggleEnabled(slot.day)" />
22
+ </v-col>
23
+
24
+ <v-col cols="6" class="pr-2">
25
+ <InputLabel title="Start" :required="slot.isEnabled" />
26
+ <v-select v-model="slot.startTime" :disabled="!slot.isEnabled" :items="allTimeSlots" item-value="time"
27
+ item-title="time" density="comfortable" :rules="slot.isEnabled ? [requiredRule] : []"
28
+ @update:model-value="slot.endTime = null" />
29
+ </v-col>
30
+
31
+ <v-col cols="6" class="pl-2">
32
+ <InputLabel title="End" :required="slot.isEnabled" />
33
+ <v-select v-model="slot.endTime" :disabled="!slot.isEnabled"
34
+ :items="generateTimeSlots(0.5, slot.startTime ?? undefined)" item-value="time" item-title="time"
35
+ density="comfortable" :rules="slot.isEnabled ? [requiredRule] : []" />
36
+ </v-col>
37
+
38
+ <v-col v-if="!viewMode" cols="12" class="mt-1">
39
+ <div class="d-flex align-center ga-1 cursor-pointer text-caption text-medium-emphasis"
40
+ @click="applyToOtherDays(slot.day)">
41
+ <v-icon icon="mdi-content-copy" size="14" />
42
+ <span>Apply to other days</span>
43
+ </div>
44
+ </v-col>
45
+ </v-row>
46
+
47
+ <v-divider v-if="index < overnightParkingSlotsArray.length - 1" class="mb-4" />
48
+ </template>
49
+
50
+ <v-col cols="12" align="end">
51
+ <v-btn color="primary" class="text-none mt-2" size="large" :loading="loading.updating" variant="flat"
52
+ text="Save" :disabled="viewMode || !isValid" @click="handleSave('save')" />
53
+ </v-col>
54
+ </v-card-text>
55
+ </v-card>
56
+ </v-col>
57
+ </v-row>
58
+ </v-form>
59
+
60
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" style="z-index: 3000;" />
61
+
62
+ <v-dialog v-model="applyDialog.open" max-width="360" persistent>
63
+ <v-card>
64
+ <v-card-title class="text-subtitle-1 font-weight-bold pt-4 px-4">Apply to days</v-card-title>
65
+ <v-card-text class="px-4 pb-2">
66
+ <p class="text-caption text-medium-emphasis mb-3">
67
+ Select the days to apply <span class="font-weight-bold text-capitalize">{{ applyDialog.sourceDay }}</span>'s
68
+ hours to.
69
+ </p>
70
+ <v-checkbox v-for="day in applyDialog.targetDays" :key="day.day" v-model="day.selected" :label="day.day"
71
+ density="compact" hide-details class="text-capitalize" />
72
+ </v-card-text>
73
+ <v-card-actions class="px-4 pb-4">
74
+ <v-spacer />
75
+ <v-btn variant="text" @click="applyDialog.open = false">Cancel</v-btn>
76
+ <v-btn color="primary" variant="flat" :disabled="!applyDialog.targetDays.some(d => d.selected)"
77
+ @click="confirmApply">Apply</v-btn>
78
+ </v-card-actions>
79
+ </v-card>
80
+ </v-dialog>
81
+ </template>
82
+
83
+ <script setup lang="ts">
84
+ const props = defineProps({
85
+ viewMode: {
86
+ type: Boolean,
87
+ default: false,
88
+ required: false,
89
+ },
90
+ site: {
91
+ type: String,
92
+ default: "",
93
+ required: false,
94
+ }
95
+ })
96
+
97
+
98
+ const model = ref<TOvernightParkingAvailability>({
99
+ monday: { isEnabled: false, startTime: null, endTime: null },
100
+ tuesday: { isEnabled: false, startTime: null, endTime: null },
101
+ wednesday: { isEnabled: false, startTime: null, endTime: null },
102
+ thursday: { isEnabled: false, startTime: null, endTime: null },
103
+ friday: { isEnabled: false, startTime: null, endTime: null },
104
+ saturday: { isEnabled: false, startTime: null, endTime: null },
105
+ sunday: { isEnabled: false, startTime: null, endTime: null },
106
+ })
107
+
108
+
109
+ const autoApproveOvernightParking = ref(false)
110
+
111
+ const { requiredRule } = useUtils()
112
+ const { generateTimeSlots, generateTimeSlotsFromStart } = useFacilityUtils()
113
+ const { updateOvernightParkingAvailability, getOvernightParkingAvailability } = useSiteSettings()
114
+
115
+ type TDay = Exclude<keyof TOvernightParkingAvailability, 'autoApproveOvernightParking'>
116
+ type TOvernightParkingSlot = TOvernightParkingDay & { day: TDay }
117
+
118
+ const orderedDays: TDay[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
119
+
120
+ const allTimeSlots = computed(() => generateTimeSlotsFromStart(0.5))
121
+
122
+ const isInternalFormValid = ref(false)
123
+ const internalFormRef = ref<InstanceType<typeof import('vuetify/components').VForm>>()
124
+
125
+ const messageSnackbar = ref(false)
126
+ const message = ref('')
127
+ const messageColor = ref<'success' | 'error'>()
128
+
129
+ const loading = reactive({
130
+ updating: false
131
+ })
132
+
133
+ const applyDialog = reactive({
134
+ open: false,
135
+ sourceDay: '' as TDay,
136
+ targetDays: [] as { day: TDay; selected: boolean }[],
137
+ })
138
+
139
+
140
+
141
+ const { data: availabilityDataReq, refresh: refreshAvailability } = await useLazyAsyncData<TOvernightParkingAvailability>(
142
+ `overnight-parking-availability-${props.site}`,
143
+ () => getOvernightParkingAvailability(props.site)
144
+ )
145
+
146
+ watch(availabilityDataReq, (data) => {
147
+ if (!data) return
148
+ orderedDays.forEach((day) => {
149
+ const slot = overnightParkingSlotsArray.value.find((s) => s.day === day)
150
+ if (!slot) return
151
+ slot.isEnabled = data?.[day]?.isEnabled ?? false
152
+ slot.startTime = data?.[day]?.startTime ?? null
153
+ slot.endTime = data?.[day]?.endTime ?? null
154
+ })
155
+
156
+ autoApproveOvernightParking.value = data?.autoApproveOvernightParking === true
157
+ }, { immediate: true })
158
+
159
+
160
+ const overnightParkingSlotsArray = ref<TOvernightParkingSlot[]>(
161
+ orderedDays.map((day) => ({
162
+ day,
163
+ isEnabled: model.value?.[day]?.isEnabled ?? false,
164
+ startTime: model.value?.[day]?.startTime ?? null,
165
+ endTime: model.value?.[day]?.endTime ?? null,
166
+ }))
167
+ )
168
+
169
+ watch(overnightParkingSlotsArray, (slots) => {
170
+ const updated = {} as TOvernightParkingAvailability
171
+ slots.forEach((slot) => {
172
+ updated[slot.day] = {
173
+ isEnabled: slot.isEnabled,
174
+ startTime: slot.startTime,
175
+ endTime: slot.endTime,
176
+ }
177
+ })
178
+ model.value = updated
179
+ }, { deep: true })
180
+
181
+ function toggleEnabled(day: TDay) {
182
+ const slot = overnightParkingSlotsArray.value.find((s) => s.day === day)
183
+ if (!slot) return
184
+ internalFormRef.value?.resetValidation()
185
+ slot.isEnabled = !slot.isEnabled
186
+ }
187
+
188
+ function applyToOtherDays(sourceDay: TDay) {
189
+ applyDialog.sourceDay = sourceDay
190
+ applyDialog.targetDays = orderedDays
191
+ .filter((d) => d !== sourceDay)
192
+ .map((d) => ({ day: d, selected: false }))
193
+ applyDialog.open = true
194
+ }
195
+
196
+ function confirmApply() {
197
+ const source = overnightParkingSlotsArray.value.find((s) => s.day === applyDialog.sourceDay)
198
+ if (!source) return
199
+ const selectedDays = applyDialog.targetDays.filter((d) => d.selected).map((d) => d.day)
200
+ overnightParkingSlotsArray.value.forEach((slot) => {
201
+ if (selectedDays.includes(slot.day)) {
202
+ slot.startTime = source.startTime
203
+ slot.endTime = source.endTime
204
+ slot.isEnabled = source.isEnabled
205
+ }
206
+ })
207
+ applyDialog.open = false
208
+ message.value = `Applied ${applyDialog.sourceDay}'s hours to ${selectedDays.join(', ')}.`
209
+ messageColor.value = 'success'
210
+ messageSnackbar.value = true
211
+ }
212
+
213
+ const isValid = computed(() => {
214
+ const slots = overnightParkingSlotsArray.value
215
+ const enabledSlots = slots.filter((s) => s.isEnabled)
216
+ const isAllSlotsValid = slots.every((s) => {
217
+ if (!s.isEnabled) return true
218
+ return !!s.startTime?.trim() && !!s.endTime?.trim()
219
+ })
220
+ return isAllSlotsValid && enabledSlots.length > 0
221
+ })
222
+
223
+ watch(autoApproveOvernightParking, (newValue, oldValue) => {
224
+ if (oldValue === newValue) return
225
+ handleSave('toggle')
226
+ })
227
+
228
+
229
+ function handleSave(action: 'toggle' | 'save') {
230
+ if (!isValid.value && action === 'save') {
231
+ message.value = 'Please fill in all required fields.'
232
+ messageColor.value = 'error'
233
+ messageSnackbar.value = true
234
+ return
235
+ }
236
+
237
+ let payload = {}
238
+
239
+ if(action === 'save') {
240
+ payload = {
241
+ ...model.value
242
+ }
243
+ } else if (action === 'toggle') {
244
+ payload = {
245
+ autoApproveOvernightParking: autoApproveOvernightParking.value,
246
+ }
247
+ }
248
+
249
+ try {
250
+ loading.updating = true
251
+ updateOvernightParkingAvailability(props.site, payload)
252
+ message.value = 'Overnight parking settings updated successfully.'
253
+ messageColor.value = 'success'
254
+ messageSnackbar.value = true
255
+ } catch (error) {
256
+ message.value = 'Failed to update overnight parking settings. Please try again.'
257
+ messageColor.value = 'error'
258
+ messageSnackbar.value = true
259
+ } finally {
260
+ loading.updating = false
261
+ }
262
+
263
+ }
264
+
265
+ </script>
266
+
267
+ <style scoped></style>
@@ -0,0 +1,130 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <TableMain
4
+ :headers="headers"
5
+ :items="items"
6
+ :loading="loading"
7
+ :page="page"
8
+ :pages="pages"
9
+ :pageRange="pageRange"
10
+ :createLabel="createLabel"
11
+ :show-header="true"
12
+ :extension-height="extensionHeight"
13
+ :offset="offset"
14
+ v-model:search="searchInput"
15
+ @refresh="emits('refresh')"
16
+ @create="emits('create')"
17
+ @row-click="handleRowClick"
18
+ @update:page="handleUpdatePage"
19
+ >
20
+
21
+ <template #item.dateRequested="{ value }">
22
+ {{ formatDateDDMMYYYYLocal(value) }}
23
+ </template>
24
+
25
+ <template #item.status="{ value }">
26
+ <v-chip
27
+ class="text-capitalize"
28
+ :color="formatStatus(value).color"
29
+ variant="flat"
30
+ pill
31
+ >
32
+ {{ formatStatus(value).label }}
33
+ </v-chip>
34
+ </template>
35
+
36
+ <template #item.start="{ value }">
37
+ {{ formatTimeValue(value) }}
38
+ </template>
39
+
40
+ <template #item.end="{ value }">
41
+ {{ formatTimeValue(value) }}
42
+ </template>
43
+
44
+ <template #item.action="{ item }">
45
+ <v-btn v-if="item.status === 'pending'" color="success" density="compact">Approve</v-btn>
46
+ </template>
47
+
48
+ <template v-if="$slots.footer" #footer>
49
+ <slot name="footer" />
50
+ </template>
51
+ </TableMain>
52
+ </v-row>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import useUtils from "../composables/useUtils";
57
+
58
+ const props = defineProps({
59
+ site: {
60
+ type: String,
61
+ default: "",
62
+ required: false,
63
+ },
64
+ loading: {
65
+ type: Boolean,
66
+ default: false,
67
+ },
68
+
69
+
70
+ });
71
+
72
+ const emits = defineEmits(["refresh", "create", "row-click", "update:page"]);
73
+
74
+ const searchInput = defineModel<string>("search", { default: "" });
75
+
76
+ const { standardFormatDate, formatDateDDMMYYYYLocal } = useUtils();
77
+
78
+ const items = ref<TOvernightParkingRequest[]>([])
79
+ const page = ref(1)
80
+ const pages = ref(1)
81
+ const pageRange = ref("-- - -- of --");
82
+ const extensionHeight = ref(0)
83
+ const offset = ref(0)
84
+ const createLabel = "New Overnight Parking Request"
85
+
86
+ const headers = [
87
+ { title: "Name", value: "name" },
88
+ { title: "Plate Number", value: "plateNumber" },
89
+ { title: "Date Requested", value: "dateTime" },
90
+ { title: "Invited By", value: "invitedBy" },
91
+ { title: "Status", value: "status" },
92
+ { title: "Action", value: "action" },
93
+ ];
94
+
95
+ function formatStatus(status: TOvernightParkingRequest["status"]) {
96
+ switch (String(status || "").toLowerCase()) {
97
+ case "approved":
98
+ return { color: "success", label: "Approved" };
99
+ case "expired":
100
+ return { color: "grey", label: "Expired" };
101
+ case "rejected":
102
+ return { color: "error", label: "Rejected" };
103
+ case "pending":
104
+ return { color: "warning", label: "Pending" };
105
+ default:
106
+ return { color: "secondary", label: status || "N/A" };
107
+ }
108
+ }
109
+
110
+
111
+ function formatTimeValue(value: TOvernightParkingRequest["start"] | TOvernightParkingRequest["end"]) {
112
+ if (!value) return "N/A";
113
+
114
+ const date = new Date(value);
115
+ if (Number.isNaN(date.getTime())) return String(value);
116
+
117
+ return date.toLocaleTimeString("en-US", {
118
+ hour: "2-digit",
119
+ minute: "2-digit",
120
+ });
121
+ }
122
+
123
+ function handleRowClick(data: { item?: TOvernightParkingRequest }) {
124
+ emits("row-click", data);
125
+ }
126
+
127
+ function handleUpdatePage(value: number) {
128
+ emits("update:page", value);
129
+ }
130
+ </script>
@@ -1,11 +1,12 @@
1
1
  <template>
2
- <v-dialog max-width="700" v-model="isDialogVisible" persistent>
2
+ <v-dialog v-model="dialogModel" max-width="700" persistent>
3
3
  <v-card>
4
4
  <v-toolbar>
5
- <v-toolbar-title>Signature </v-toolbar-title>
5
+ <v-toolbar-title>Signature</v-toolbar-title>
6
6
  <v-spacer />
7
- <v-btn icon="mdi-close" @click="hideModal"></v-btn>
7
+ <v-btn icon="mdi-close" @click="hideModal" />
8
8
  </v-toolbar>
9
+
9
10
  <v-card-text>
10
11
  <v-row no-gutters>
11
12
  <v-col cols="12">
@@ -16,8 +17,8 @@
16
17
  <NuxtSignaturePad
17
18
  ref="signature"
18
19
  :options="state.option"
19
- :width="'100%'"
20
- :height="'400px'"
20
+ width="100%"
21
+ height="400px"
21
22
  :disabled="state.disabled"
22
23
  class="border"
23
24
  />
@@ -29,10 +30,9 @@
29
30
  <v-btn
30
31
  text="clear"
31
32
  color="warning"
32
- type="submit"
33
33
  class="my-4 w-100 rounded-lg"
34
34
  height="40px"
35
- @click="clear()"
35
+ @click="clear"
36
36
  />
37
37
  </v-col>
38
38
 
@@ -40,12 +40,11 @@
40
40
  <v-btn
41
41
  text="submit"
42
42
  color="#1867C0"
43
- type="submit"
44
43
  class="my-4 w-100 rounded-lg"
45
44
  height="40px"
46
45
  :loading="loading"
47
- @click="submit"
48
46
  :disabled="loading"
47
+ @click="submit"
49
48
  />
50
49
  </v-col>
51
50
  </v-row>
@@ -58,8 +57,6 @@
58
57
 
59
58
  <script setup lang="ts">
60
59
  const loading = ref(false);
61
- // const { isValid } = useAudit();
62
- // const { uiRequiredInput, uiSetSnackbar } = useUtils();
63
60
 
64
61
  const message = ref("");
65
62
  const messageColor = ref("");
@@ -71,7 +68,8 @@ function showMessage(msg: string, color: string) {
71
68
  messageSnackbar.value = true;
72
69
  }
73
70
 
74
- const signature = ref(null);
71
+ const signature = ref<any>(null);
72
+
75
73
  const state = ref({
76
74
  count: 0,
77
75
  option: {
@@ -85,40 +83,50 @@ const emit = defineEmits<{
85
83
  (event: "onSubmit", payload: string): void;
86
84
  (event: "onCloseDialog"): void;
87
85
  }>();
88
- let props = defineProps({
86
+
87
+ const props = defineProps({
89
88
  isShowDialog: {
90
89
  type: Boolean,
91
90
  default: false,
92
91
  },
93
92
  });
94
93
 
95
- const isDialogVisible = computed(() => props.isShowDialog);
94
+ const dialogModel = computed({
95
+ get: () => props.isShowDialog,
96
+ set: (value: boolean) => {
97
+ if (!value) emit("onCloseDialog");
98
+ },
99
+ });
96
100
 
97
101
  const hideModal = () => {
98
102
  emit("onCloseDialog");
99
103
  };
104
+
100
105
  const file = ref<File | null>(null);
101
106
  const { addFile } = useFile();
107
+
102
108
  const submit = async () => {
103
109
  try {
110
+ if (!signature.value) {
111
+ showMessage("Signature pad is not ready.", "error");
112
+ return;
113
+ }
114
+
104
115
  loading.value = true;
105
- const base64 = signature.value.saveSignature();
106
- const blob = await (await fetch(base64)).blob();
107
116
 
108
- file.value = new File([blob], "signature.jpg", { type: "image/jpeg" });
117
+ const base64 = signature.value.saveSignature?.();
118
+ if (!base64) {
119
+ showMessage("Please provide a signature first.", "warning");
120
+ return;
121
+ }
109
122
 
110
- const uploadItem = {
111
- data: file.value,
112
- name: file.value.name,
113
- url: URL.createObjectURL(file.value),
114
- progress: 0,
115
- type: file.value.type,
116
- };
123
+ const blob = await (await fetch(base64)).blob();
124
+ file.value = new File([blob], "signature.jpg", { type: "image/jpeg" });
117
125
 
118
- const response = await addFile(uploadItem.data);
126
+ const response = await addFile(file.value);
119
127
 
120
- if (response && response.length > 0) {
121
- emit("onSubmit", response[0]._id);
128
+ if (response && response.id) {
129
+ emit("onSubmit", response.id);
122
130
  }
123
131
  } catch (error) {
124
132
  showMessage("Error uploading signature. Please try again.", "error");
@@ -128,6 +136,6 @@ const submit = async () => {
128
136
  };
129
137
 
130
138
  const clear = () => {
131
- signature.value.clearCanvas();
139
+ signature.value?.clearCanvas?.();
132
140
  };
133
141
  </script>
@@ -211,10 +211,12 @@
211
211
  <script setup lang="ts">
212
212
  import { useUnitPermission } from "../composables/useUnitPermission";
213
213
  import useUnits from "../composables/useUnits";
214
+ import useUtils from "../composables/useUtils";
214
215
 
215
216
  const props = defineProps({
216
217
  orgId: { type: String, default: "" },
217
218
  site: { type: String, default: "" },
219
+ serviceType: { type: String, default: "", required: true },
218
220
  });
219
221
 
220
222
  const items = ref<Array<Record<string, any>>>([]);
@@ -252,6 +254,7 @@ const {
252
254
  page: page.value,
253
255
  search: searchInput.value,
254
256
  site: props.site,
257
+ serviceType: props.serviceType,
255
258
  }),
256
259
  {
257
260
  watch: [page, () => props.site],
@@ -389,7 +392,7 @@ async function _createUnit(name: string) {
389
392
  if (!id) throw new Error("Invalid unit id for update");
390
393
  response = await updateUnit(id, name);
391
394
  } else {
392
- response = await createUnit(name, props.site);
395
+ response = await createUnit(name, props.site, props.serviceType);
393
396
  }
394
397
  showMessage(response?.message, "success");
395
398
  dialogShowForm.value = false;
@@ -289,6 +289,7 @@
289
289
 
290
290
  <script setup lang="ts">
291
291
  import useBuildingUnit from '../composables/useBuildingUnit';
292
+ import useWebUsb from '../composables/useWebUsb';
292
293
 
293
294
 
294
295
  const prop = defineProps({
@@ -322,7 +323,8 @@ const { requiredRule, debounce, UTCToLocalTIme } = useUtils();
322
323
  const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
323
324
  const { createVisitor, typeFieldMap, contractorTypes, getVisitors, updateVisitor } = useVisitor();
324
325
  const { getBySiteId: getEntryPassSettingsBySiteId } = useSiteEntryPassSettings();
325
- const { createVisitorPass } = useAccessManagement();
326
+ const { createVisitorPass, signQr } = useAccessManagement();
327
+ const { testConnection } = useWebUsb();
326
328
  const { findPersonByNRIC, findPersonByContact, searchCompanyList, findUsersByPlateNumber } = usePeople()
327
329
  const { getById: getUnitDataById } = useBuildingUnit()
328
330
 
@@ -353,7 +355,7 @@ const visitor = reactive<Partial<TVisitorPayload>>({
353
355
  members: [],
354
356
  });
355
357
 
356
- const passType = ref("");
358
+ const passType = ref<string | null>(null);
357
359
  const passQuantity = ref<number | null>(1);
358
360
  const passCards = ref<{ _id: string; cardNo: string }[]>([]);
359
361
 
@@ -655,15 +657,9 @@ function handleAutofillDataViaVehicleNumber(item: TPeople) {
655
657
  const {
656
658
  data: entryPassSettings,
657
659
  pending: entryPassSettingsPending,
658
- refresh: refreshEntryPassSettings,
659
- } = useLazyAsyncData(`fetch-entrypass-settings-${prop.site}`, () => {
660
- if (contractorStep.value !== 2) return Promise.resolve(null);
661
- return getEntryPassSettingsBySiteId(prop.site);
662
- });
663
-
664
- watch(contractorStep, (step) => {
665
- if (step === 2) refreshEntryPassSettings();
666
- });
660
+ } = useLazyAsyncData(`fetch-entrypass-settings-${prop.site}`, () =>
661
+ getEntryPassSettingsBySiteId(prop.site)
662
+ );
667
663
 
668
664
  const {
669
665
  data: siteData,
@@ -908,10 +904,13 @@ async function submit() {
908
904
  const res = await createVisitor(payload);
909
905
  if (res) {
910
906
  if (prop.type === "contractor" && passType.value) {
911
- const visitorId = res?._id;
912
- const acmUrl = entryPassSettings.value?.data?.settings?.acm_url;
907
+ const visitorId = res as unknown as string;
908
+ const rawUrl = entryPassSettings.value?.data?.settings?.url;
909
+ const { encrypted: acmUrl } = await $fetch<{ encrypted: string }>(
910
+ `/api/encrypt-acm-url${rawUrl ? `?url=${encodeURIComponent(rawUrl)}` : ""}`
911
+ );
913
912
  if (visitorId && acmUrl) {
914
- await createVisitorPass({
913
+ const passRes = await createVisitorPass({
915
914
  site: prop.site,
916
915
  unitId: visitor.unit!,
917
916
  quantity: passQuantity.value ?? 1,
@@ -920,6 +919,37 @@ async function submit() {
920
919
  acm_url: acmUrl,
921
920
  visitorId,
922
921
  });
922
+
923
+ if (passType.value === "QR") {
924
+ const accessCards: any[] = (passRes as any)?.accessCards ?? [];
925
+ const printer = entryPassSettings.value?.data?.settings?.printer;
926
+ if (printer?.vendorId && printer?.productId && accessCards.length) {
927
+ const vendorId = parseInt(printer.vendorId);
928
+ const productId = parseInt(printer.productId);
929
+ const companyName = visitor.company || "";
930
+ const blockLabel = blocksArray.value.find((b: any) => b.value === visitor.block)?.title || "";
931
+ const levelLabel = levelsArray.value.find((l: any) => l.value === visitor.level)?.title || "";
932
+ const unitLabel = unitsArray.value.find((u: any) => u.value === visitor.unit)?.title || "";
933
+ const address = [blockLabel, levelLabel, unitLabel].filter(Boolean).join("/");
934
+
935
+ await nextTick();
936
+ for (const qrCode of accessCards) {
937
+ const signed = await signQr({ cardId: qrCode._id, purpose: "vms" });
938
+ const qrData = signed?.data;
939
+ if (!qrData) continue;
940
+ await testConnection(
941
+ vendorId,
942
+ productId,
943
+ true,
944
+ qrData,
945
+ qrCode.accessLevel,
946
+ qrCode.liftAccessLevel,
947
+ companyName,
948
+ address,
949
+ );
950
+ }
951
+ }
952
+ }
923
953
  }
924
954
  }
925
955
  if (createMore.value) {
@@ -279,6 +279,16 @@ export default function useAccessManagement() {
279
279
  );
280
280
  }
281
281
 
282
+ function signQr(payload: { cardId: string; purpose: string }) {
283
+ return useNuxtApp().$api<{ message: string; data: string }>(
284
+ `/api/access-management/sign-qr`,
285
+ {
286
+ method: "POST",
287
+ body: payload,
288
+ }
289
+ );
290
+ }
291
+
282
292
  return {
283
293
  getDoorAccessLevels,
284
294
  getLiftAccessLevels,
@@ -299,5 +309,6 @@ export default function useAccessManagement() {
299
309
  getAvailableContractorCards,
300
310
  generateQrVms,
301
311
  createVisitorPass,
312
+ signQr,
302
313
  };
303
314
  }