@7365admin1/layer-common 1.11.17 → 1.11.18

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.
@@ -12,9 +12,9 @@
12
12
  <div class="w-100 d-flex justify-space-between ga-2">
13
13
  <span class="text-subtitle-1 w-100 font-weight-bold mb-3">{{
14
14
  formTitle
15
- }}</span>
16
- <span v-if="prop.type === 'contractor'" class="text-subtitle-2 font-weight-bold" style="text-wrap: nowrap">Step
17
- <span class="text-primary-button">{{ contractorStep }}</span>/3</span>
15
+ }}</span>
16
+ <span v-if="withStep2 || withStep3" class="text-subtitle-2 font-weight-bold" style="text-wrap: nowrap">Step
17
+ <span class="text-primary-button">{{ contractorStep }}</span>/{{ withStep3 ? 3 : withStep2 ? 2 : 1 }}</span>
18
18
  </div>
19
19
  <v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorMessage = ''">
20
20
  <v-row no-gutters class="pt-4">
@@ -30,7 +30,7 @@
30
30
  <v-list-item-title @click.stop="handleAddNewContractorType" class="d-flex align-center ga-1">
31
31
  <span><v-icon icon="mdi-plus" /></span> Add "<strong>{{
32
32
  contractorTypeInput
33
- }}</strong>" as custom contractor type.
33
+ }}</strong>" as custom contractor type.
34
34
  </v-list-item-title>
35
35
  </v-list-item>
36
36
  </template>
@@ -115,8 +115,7 @@
115
115
  <InputLabel class="text-capitalize" title="Delivery Company" />
116
116
  <v-combobox v-model="deliveryCompany" v-model:search="deliveryCompanyInput" ref="companyCombo"
117
117
  autocomplete="off" :hide-no-data="false" :items="deliveryCompanyList" item-value="value"
118
- :loading="siteDataPending" variant="outlined"
119
- density="comfortable" persistent-hint small-chips>
118
+ :loading="siteDataPending" variant="outlined" density="comfortable" persistent-hint small-chips>
120
119
  <template v-slot:no-data>
121
120
  <v-list-item>
122
121
  <v-list-item-title v-if="fetchCompanyListPending">
@@ -183,22 +182,17 @@
183
182
  <v-textarea v-model="visitor.remarks" density="comfortable" :rows="3" no-resize />
184
183
  </v-col>
185
184
 
186
- <v-col v-if="prop.type === 'contractor' && contractorStep === 2" cols="12">
187
- <PassInformation :site="prop.site" :type="prop.type" :contractor-type="visitor.contractorType" />
188
- <EntryPassInformation
189
- v-if="entryPassSettings?.data?.settings?.nfcPass"
190
- v-model="passType"
191
- v-model:quantity="passQuantity"
192
- v-model:cards="passCards"
193
- :settings="entryPassSettings"
194
- :loading="entryPassSettingsPending"
195
- :site-id="prop.site"
196
- :unit-id="visitor.unit || null"
197
- />
185
+ <v-col v-if="(withStep2 || withStep3) && contractorStep === 2" cols="12">
186
+ <PassInformation v-model:pass="visitor.visitorPass" v-model:keys="visitor.passKeys" :site="prop.site"
187
+ :type="prop.type" :contractor-type="visitor.contractorType" :hide-keys="!showShowKeysField" />
188
+ <EntryPassInformation v-if="entryPassSettings?.data?.settings?.nfcPass" v-model="passType"
189
+ v-model:quantity="passQuantity" v-model:cards="passCards" :settings="entryPassSettings"
190
+ :loading="entryPassSettingsPending" :site-id="prop.site" :unit-id="visitor.unit || null" />
198
191
  </v-col>
199
192
 
200
- <v-col v-if="prop.type === 'contractor' && contractorStep === 3" cols="12">
201
- <MemberInformation v-model="visitor.members" />
193
+ <v-col v-if="withStep3 && contractorStep === 3" cols="12">
194
+ <MemberInformation v-model="visitor.members" :type="prop.type" :contractor-type="visitor.contractorType"
195
+ :site="prop.site" :selected-visitor-pass="visitor.visitorPass" />
202
196
  </v-col>
203
197
 
204
198
  <v-col v-if="prop.mode === 'add'" cols="12" class="mt-2">
@@ -224,7 +218,7 @@
224
218
  <v-col cols="6">
225
219
  <v-btn v-if="
226
220
  prop.mode === 'add' &&
227
- prop.type === 'contractor' &&
221
+ (withStep2 || withStep3) &&
228
222
  contractorStep > 1
229
223
  " tile block variant="text" class="text-none" size="48" @click="handleGoToPreviousPage" text="Back" />
230
224
  <v-btn v-else-if="prop.mode === 'add' || prop.mode === 'register'" tile block variant="text" class="text-none"
@@ -232,10 +226,11 @@
232
226
  <v-btn v-else tile block variant="text" class="text-none" size="48" @click="emit('close:all')" text="Close" />
233
227
  </v-col>
234
228
  <v-col cols="6">
235
- <v-btn v-if="prop.type === 'contractor'" tile block variant="flat" color="primary-button" class="text-none"
236
- size="48" :disabled="processing" @click="handleNextPage" :text="contractorStep === 3 ? 'Submit' : 'Next'" />
229
+ <v-btn v-if="showNextButton" tile block variant="flat" color="primary-button" class="text-none" size="48"
230
+ :disabled="processing" @click="handleNextPage" :text="'Next'" />
237
231
  <v-btn v-else tile block variant="flat" color="black" class="text-none" size="48"
238
- :disabled="!validForm || processing" @click="submit" :text="prop.mode == 'add' ? 'Submit' : 'Update'" />
232
+ :disabled="!validForm || processing || membersFieldIncomplete" @click="submit"
233
+ :text="prop.mode == 'add' ? 'Submit' : 'Update'" />
239
234
  </v-col>
240
235
  </v-row>
241
236
  </v-toolbar>
@@ -353,6 +348,8 @@ const visitor = reactive<Partial<TVisitorPayload>>({
353
348
  remarks: "",
354
349
  attachments: [],
355
350
  members: [],
351
+ visitorPass: [],
352
+ passKeys: []
356
353
  });
357
354
 
358
355
  const passType = ref<string | null>(null);
@@ -400,12 +397,34 @@ const vehicleNumberUserItems = ref<TPeople[]>([])
400
397
 
401
398
 
402
399
  const shouldShowField = (fieldKey: keyof TVisitorPayload | 'delivery-company') => {
403
- if (prop.type !== "contractor" || contractorStep.value === 1) {
400
+ if ((!withStep2.value && !withStep3.value) || contractorStep.value === 1) {
404
401
  const visibleFields = typeFieldMap[prop.type];
405
402
  return visibleFields?.includes(fieldKey);
406
403
  }
407
404
  };
408
405
 
406
+ const withStep2 = computed(() => {
407
+ const step2Types = ["guest", "walk-in"];
408
+ return step2Types.includes(prop.type);
409
+ })
410
+
411
+ const withStep3 = computed(() => {
412
+ const step3Types = ["contractor"];
413
+ return step3Types.includes(prop.type);
414
+ })
415
+
416
+ const showNextButton = computed(() => {
417
+ if (withStep3.value) {
418
+ return contractorStep.value === 1 || contractorStep.value === 2
419
+ } else if (withStep2.value) {
420
+ return contractorStep.value === 1
421
+ } else return false
422
+ })
423
+
424
+ const showShowKeysField = computed(() => {
425
+ return prop.type === "contractor"
426
+ })
427
+
409
428
  const requireNRIC = computed(() => {
410
429
  return prop.type !== 'walk-in' && prop.type !== 'guest';
411
430
  })
@@ -415,8 +434,10 @@ const companyAutofillDataArray = ref<{ companyName: string[], unit: string, bloc
415
434
 
416
435
  const formTitle = computed(() => {
417
436
  const isContractorForm = prop.type === "contractor";
437
+ const isDriveInForm = prop.type === "guest";
438
+ const isWalkInForm = prop.type === "walk-in";
418
439
  const step = contractorStep.value;
419
- if (isContractorForm && step === 2) {
440
+ if ((isContractorForm || isDriveInForm || isWalkInForm) && step === 2) {
420
441
  return "Pass & Keys Information";
421
442
  } else if (isContractorForm && step === 3) {
422
443
  return "Members Information";
@@ -519,7 +540,7 @@ const {
519
540
  }
520
541
  );
521
542
 
522
- watch(fetchPersonByContactReq, (obj) => {
543
+ watch(fetchPersonByContactReq, (obj: Partial<TPerson>) => {
523
544
  if (obj) {
524
545
  currentAutofillSource.value = "contact";
525
546
  companyNames.value = obj.companyName ?? []
@@ -568,7 +589,7 @@ watch(fetchVisitorListByVehicleNumberReq, (arr: any) => {
568
589
  const itemsArray = arr?.items || []
569
590
  const isValidArray = Array.isArray(itemsArray)
570
591
  matchingPlateNumberNonCheckedOutArr.value = isValidArray ? itemsArray : []
571
- if(matchingPlateNumberNonCheckedOutArr.value.length > 0) {
592
+ if (matchingPlateNumberNonCheckedOutArr.value.length > 0) {
572
593
  dialog.showNonCheckedOutDialog = true;
573
594
  } else {
574
595
  dialog.showNonCheckedOutDialog = false;
@@ -603,7 +624,7 @@ async function handleCheckout(visitorId: string) {
603
624
  checkOut: new Date().toISOString(),
604
625
  });
605
626
  if (res) {
606
- await fetchVisitorListByVehicleNumberRefresh();
627
+ await fetchVisitorListByVehicleNumberRefresh();
607
628
 
608
629
  }
609
630
  } catch (error: any) {
@@ -876,6 +897,15 @@ async function submit() {
876
897
  };
877
898
  }
878
899
  }
900
+
901
+ const isGuest = prop.type === "guest";
902
+ const isWalkIn = prop.type === "walk-in";
903
+ const isContractor = prop.type === "contractor";
904
+ if (isGuest || isWalkIn || isContractor) {
905
+ payload.visitorPass = visitor.visitorPass;
906
+ payload.passKeys = visitor.passKeys;
907
+ }
908
+
879
909
  }
880
910
 
881
911
 
@@ -888,7 +918,7 @@ async function submit() {
888
918
  };
889
919
  }
890
920
 
891
- if(prop.type === "delivery"){
921
+ if (prop.type === "delivery") {
892
922
  payload = {
893
923
  ...payload,
894
924
  company: deliveryCompany.value
@@ -969,6 +999,18 @@ watch(
969
999
  }
970
1000
  );
971
1001
 
1002
+ const membersFieldIncomplete = computed(() => {
1003
+ if (!visitor.members || visitor.members.length === 0) return false;
1004
+
1005
+ return visitor.members.some((member) => {
1006
+ const hasName = !!member.name;
1007
+ const hasNric = !!member.nric;
1008
+ const hasContact = !!member.contact;
1009
+
1010
+ return !hasName || !hasNric || !hasContact;
1011
+ });
1012
+ });
1013
+
972
1014
  onMounted(() => {
973
1015
  contractorStep.value = 2;
974
1016
  currentAutofillSource.value = null;
@@ -43,6 +43,7 @@
43
43
  </span>
44
44
  <span class="text-capitalize">{{ item?.name }}</span>
45
45
  </span>
46
+ <span class="text-grey text-caption" v-if="item?.members?.length > 0">( +{{ item?.members?.length }} members)</span>
46
47
  </template>
47
48
 
48
49
  <template v-slot:item.type-company="{ item }">
@@ -50,7 +51,7 @@
50
51
  <v-icon icon="mdi-user" size="15" />
51
52
  <span v-if="item.type === 'contractor'" class="text-capitalize">{{
52
53
  formatCamelCaseToWords(item.contractorType)
53
- }}</span>
54
+ }}</span>
54
55
  <span v-else class="text-capitalize">{{ formatType(item) }}</span>
55
56
  </span>
56
57
  <span class="d-flex align-center ga-2">
@@ -84,36 +85,49 @@
84
85
  </template>
85
86
 
86
87
  <template v-slot:item.checkin-out="{ item }">
87
- <span class="d-flex align-center ga-2">
88
- <v-icon icon="mdi-clock-time-four-outline" color="green" size="20" />
89
- <span class="text-capitalize">{{
90
- UTCToLocalTIme(item.checkIn) || "-"
91
- }}</span>
92
- <span>
93
- <v-icon v-if="item?.snapshotEntryImage" size="17" icon="mdi-image"
94
- @click.stop="handleViewImage(item.snapshotEntryImage)" />
95
- </span>
96
- </span>
97
- <span class="d-flex align-center ga-2">
98
- <v-icon icon="mdi-clock-time-eight-outline" color="red" size="20" />
99
- <template v-if="item.checkOut">
88
+ <v-row no-gutters class="d-flex flex-column py-2">
89
+ <span class="d-flex align-center ga-2">
90
+ <v-icon icon="mdi-clock-time-four-outline" color="green" size="20" />
100
91
  <span class="text-capitalize">{{
101
- UTCToLocalTIme(item.checkOut) || "-"
92
+ UTCToLocalTIme(item.checkIn) || "-"
102
93
  }}</span>
103
94
  <span>
104
- <v-icon v-if="item?.snapshotExitImage" size="17" icon="mdi-image"
105
- @click.stop="handleViewImage(item.snapshotExitImage)" />
95
+ <v-icon v-if="item?.snapshotEntryImage" size="17" icon="mdi-image"
96
+ @click.stop="handleViewImage(item.snapshotEntryImage)" />
106
97
  </span>
107
- <span v-if="item?.manualCheckout">
108
- <TooltipInfo text="Manual Checkout" density="compact" size="x-small" />
98
+ </span>
99
+ <span class="d-flex align-center ga-2">
100
+ <v-icon icon="mdi-clock-time-eight-outline" color="red" size="20" />
101
+ <template v-if="item.checkOut">
102
+ <span class="text-capitalize">{{
103
+ UTCToLocalTIme(item.checkOut) || "-"
104
+ }}</span>
105
+ <span>
106
+ <v-icon v-if="item?.snapshotExitImage" size="17" icon="mdi-image"
107
+ @click.stop="handleViewImage(item.snapshotExitImage)" />
108
+ </span>
109
+ <span v-if="item?.manualCheckout">
110
+ <TooltipInfo text="Manual Checkout" density="compact" size="x-small" />
111
+ </span>
112
+ </template>
113
+ <span v-else-if="!item?.checkOut && (item?.status === 'registered' || item?.status === 'unregistered')">
114
+ <v-btn size="x-small" class="text-capitalize" color="red" text="Checkout"
115
+ :loading="loading.checkingOut && item?._id === selectedVisitorId" @click.stop="handleCheckout(item._id)"
116
+ v-if="canCheckoutVisitor" />
109
117
  </span>
110
- </template>
111
- <span v-else-if="!item?.checkOut && (item?.status === 'registered' || item?.status === 'unregistered')">
112
- <v-btn size="x-small" class="text-capitalize" color="red" text="Checkout"
113
- :loading="loading.checkingOut && item?._id === selectedVisitorId" @click.stop="handleCheckout(item._id)"
114
- v-if="canCheckoutVisitor" />
115
118
  </span>
116
- </span>
119
+ <div v-if="(item.visitorPass?.length ?? 0) > 0 || (item.passKeys?.length ?? 0) > 0"
120
+ class="d-flex flex-wrap ga-1 mt-1" @click.stop="handleCheckout(item._id)">
121
+ <v-chip v-for="pass in item.visitorPass" :key="(pass as any)._id ?? (pass as any).keyId"
122
+ prepend-icon="mdi-card-bulleted-outline" size="x-small" variant="tonal" color="blue">
123
+ {{ (pass as any)?.prefixAndName }}
124
+ </v-chip>
125
+ <v-chip v-for="key in item.passKeys" :key="(key as any)._id ?? (key as any).keyId" prepend-icon="mdi-key"
126
+ size="x-small" variant="tonal" color="orange">
127
+ {{ (key as any)?.prefixAndName }}
128
+ </v-chip>
129
+ </div>
130
+ </v-row>
117
131
  </template>
118
132
 
119
133
  <template v-slot:item.action="{ item }">
@@ -121,6 +135,21 @@
121
135
  @click.stop="handleRegistrationUnregisteredVisitor(item)" />
122
136
  </template>
123
137
 
138
+ <template v-slot:item.checkInRemarks="{ item }">
139
+ <span>
140
+ <v-btn variant="text" color="blue-darken-2" density="compact" size="small"
141
+ @click.stop="handleOpenRemarks(item, 'checkIn')">{{ item.checkInRemarks ? "View Remarks" : "Add Remarks"
142
+ }}</v-btn>
143
+ </span>
144
+ </template>
145
+ <template v-slot:item.checkOutRemarks="{ item }">
146
+ <span>
147
+ <v-btn variant="text" color="blue-darken-2" density="compact" size="small"
148
+ @click.stop="handleOpenRemarks(item, 'checkOut')">{{ item.checkOutRemarks ? "View Remarks" : "Add Remarks"
149
+ }}</v-btn>
150
+ </span>
151
+ </template>
152
+
124
153
  </TableMain>
125
154
 
126
155
  <v-dialog v-model="dialog.showSelection" width="450" persistent>
@@ -151,7 +180,7 @@
151
180
  </span>
152
181
 
153
182
  <span v-else-if="selectedVisitorObject[key]" class="d-flex ga-3 align-center"><strong>{{ label
154
- }}:</strong>
183
+ }}:</strong>
155
184
  {{ formatValues(key, selectedVisitorObject[key]) }}
156
185
  <TooltipInfo v-if="key === 'checkOut'" text="Manual Checkout" density="compact" size="x-small" />
157
186
  </span>
@@ -179,6 +208,88 @@
179
208
  </v-card>
180
209
  </v-dialog>
181
210
 
211
+ <v-dialog v-model="dialog.remarks" max-width="450" persistent>
212
+ <v-card>
213
+ <v-card-title class="d-flex justify-space-between align-center pt-4 px-4">
214
+ <span>{{ remarksType === 'checkIn' ? 'Check-In Remarks' : 'Check-Out Remarks' }}</span>
215
+ <v-btn icon="mdi-close" variant="text" @click="dialog.remarks = false" />
216
+ </v-card-title>
217
+ <v-card-text class="px-4 pb-2">
218
+ <v-textarea v-model="remarksInput" label="Remarks" rows="4" auto-grow variant="outlined"
219
+ :readonly="remarksViewOnly" />
220
+ </v-card-text>
221
+ <v-toolbar class="pa-0" density="compact">
222
+ <v-row no-gutters>
223
+ <v-col cols="6">
224
+ <v-btn variant="text" block :disabled="loading.savingRemarks"
225
+ @click="dialog.remarks = false">Cancel</v-btn>
226
+ </v-col>
227
+ <v-col cols="6">
228
+ <v-btn color="primary" variant="flat" height="48" rounded="0" block :loading="loading.savingRemarks"
229
+ :disabled="!remarksInput.trim()" @click="handleSaveRemarks">Save</v-btn>
230
+ </v-col>
231
+ </v-row>
232
+ </v-toolbar>
233
+ </v-card>
234
+ </v-dialog>
235
+
236
+ <v-dialog v-model="dialog.returnPassesKeys" max-width="450" persistent>
237
+ <v-card>
238
+ <v-toolbar density="compact" color="">
239
+ <v-row no-gutters class="d-flex fill-height justify-space-between align-center px-4">
240
+ <span class="font-weight-bold">Return Passes &amp; Keys</span>
241
+ <v-btn icon="mdi-close" variant="text" @click="dialog.returnPassesKeys = false" />
242
+ </v-row>
243
+ </v-toolbar>
244
+ <v-card-text class="px-4 pb-2">
245
+ <p class="text-body-2 mb-3">Please ensure all passes and keys are returned before checking out.</p>
246
+ <div v-if="passReturnStatuses.length > 0" class="mb-2">
247
+ <p class="text-caption text-medium-emphasis mb-1">Passes</p>
248
+ <v-row no-gutters v-for="pass in passReturnStatuses" :key="(pass as any)._id ?? (pass as any).keyId"
249
+ class="d-flex flex-wrap justify-space-between align-center ga-5 mb-2">
250
+ <v-chip prepend-icon="mdi-card-bulleted-outline" size="small" variant="tonal" color="blue">
251
+ {{ (pass as any)?.prefixAndName }}
252
+ </v-chip>
253
+ <v-select hide-details max-width="200px" density="compact" :items="passStatusOptions" item-title="label"
254
+ item-value="value" v-model="pass.status"></v-select>
255
+ <v-textarea v-if="pass.status === 'Lost' || pass.status === 'Damaged'" no-resize rows="3" class="w-100"
256
+ density="compact" v-model="pass.remarks"></v-textarea>
257
+ </v-row>
258
+ </div>
259
+ <div v-if="(keyReturnStatuses.length > 0)" class="mb-2">
260
+ <p class="text-caption text-medium-emphasis mb-1">Keys</p>
261
+ <v-row no-gutters v-for="key in keyReturnStatuses" :key="(key as any)._id ?? (key as any).keyId"
262
+ class="d-flex flex-wrap justify-space-between align-center ga-5 mb-2">
263
+ <v-chip prepend-icon="mdi-key" size="small" variant="tonal" color="orange">
264
+ {{ (key as any)?.prefixAndName }}
265
+ </v-chip>
266
+ <v-select hide-details max-width="200px" density="compact" :items="passStatusOptions" item-title="label"
267
+ item-value="value" v-model="key.status">
268
+ </v-select>
269
+ <v-textarea v-if="key.status === 'Lost' || key.status === 'Damaged'" no-resize rows="3" class="w-100"
270
+ density="compact" v-model="key.remarks"></v-textarea>
271
+ </v-row>
272
+ </div>
273
+
274
+ <v-row no-gutters class="my-5">
275
+ <v-btn variant="flat" color="blue" density="comfortable" class="text-capitalize" :loading="loading.updatingPassKeys" @click.stop="handleUpdatePassKeys">Update Pass/Keys</v-btn>
276
+ </v-row>
277
+ </v-card-text>
278
+ <v-toolbar class="pa-0" density="compact">
279
+ <v-row no-gutters>
280
+ <v-col cols="6">
281
+ <v-btn variant="text" block :disabled="loading.checkingOut"
282
+ @click="dialog.returnPassesKeys = false">Close</v-btn>
283
+ </v-col>
284
+ <v-col cols="6">
285
+ <v-btn color="red" variant="flat" height="48" rounded="0" block :loading="loading.checkingOut"
286
+ :disabled="!canConfirmCheckout" @click="proceedCheckout">Confirm Checkout</v-btn>
287
+ </v-col>
288
+ </v-row>
289
+ </v-toolbar>
290
+ </v-card>
291
+ </v-dialog>
292
+
182
293
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
183
294
  </v-row>
184
295
  </template>
@@ -256,6 +367,8 @@ const loading = reactive({
256
367
  deletingVisitor: false,
257
368
  fetchingVisitors: false,
258
369
  checkingOut: false,
370
+ savingRemarks: false,
371
+ updatingPassKeys: false,
259
372
  });
260
373
 
261
374
  const dialog = reactive({
@@ -264,9 +377,14 @@ const dialog = reactive({
264
377
  viewVisitor: false,
265
378
  deleteConfirmation: false,
266
379
  snapshotImage: false,
380
+ remarks: false,
381
+ returnPassesKeys: false,
267
382
  });
268
383
 
269
384
  const snapshotImageUrl = ref("");
385
+ const remarksType = ref<'checkIn' | 'checkOut'>('checkIn');
386
+ const remarksInput = ref("");
387
+ const remarksViewOnly = ref(false);
270
388
 
271
389
  const headers = computed(() => [
272
390
  { title: "Name", value: "name" },
@@ -274,6 +392,7 @@ const headers = computed(() => [
274
392
  { title: "Location", value: "location" },
275
393
  { title: "Contact/Vehicle No.", value: "contact-vehicleNumber" },
276
394
  { title: "Check In/Out", value: "checkin-out" },
395
+ ...(activeTab.value === "resident-transactions" ? [{ title: "Check-in Remarks", value: "checkInRemarks" }, { title: "Check-out Remarks", value: "checkOutRemarks" }] : []),
277
396
  ...(activeTab.value === 'unregistered' ? [{ title: "Action", value: "action" }] : [])
278
397
  ])
279
398
 
@@ -314,7 +433,15 @@ function filterTypeSelectionLabel() {
314
433
  return `${length} selected ${length === 1 ? "type" : "types"}` as string;
315
434
  }
316
435
 
436
+ const passStatusOptions = [
437
+ { label: "Returned", value: "Returned" },
438
+ { label: "Not Returned", value: "In Use" },
439
+ { label: "Damaged", value: "Damaged" },
440
+ { label: "Lost", value: "Lost" },
441
+ ]
442
+
317
443
  function toRoute(tab: any) {
444
+ items.value = []
318
445
 
319
446
  const obj = tabOptions.find((x) => x.value === tab);
320
447
  if (!obj) return;
@@ -358,7 +485,7 @@ const {
358
485
  // params.status = "pending"
359
486
  // }
360
487
 
361
- if(activeTab.value === "guests"){
488
+ if (activeTab.value === "guests") {
362
489
  params.type = "guest"
363
490
  params.status = "pending"
364
491
  } else if (activeTab.value === "resident-transactions") {
@@ -380,7 +507,8 @@ const {
380
507
 
381
508
  watch(getVisitorReq, (newData: any) => {
382
509
  if (newData) {
383
- items.value = newData.items ?? [];
510
+ // items.value = newData.items ?? [];
511
+ items.value = [{ _id: "testid", name: "John Doe", type: "Contractor", company: "ABC Corp", location: "Block A, Level 1, Unit 101", contact: "12345678", plateNumber: "SGX1234A", checkIn: new Date().toISOString(), checkOut: null, status: "registered", visitorPass: [{ _id: "1", keyId: "pass1", prefixAndName: "VIP Pass" }, { _id: "2", keyId: "pass2", prefixAndName: "VIP Pass 2" }], passKeys: [{ _id: "1", keyId: "key2", prefixAndName: "Master Key" }] }];
384
512
  pages.value = newData.pages ?? 0;
385
513
  pageRange.value = newData?.pageRange ?? "-- - -- of --";
386
514
  }
@@ -479,6 +607,56 @@ function handleViewImage(imageId: string) {
479
607
  dialog.snapshotImage = true;
480
608
  }
481
609
 
610
+ function handleOpenRemarks(item: any, type: 'checkIn' | 'checkOut') {
611
+ selectedVisitorId.value = item?._id;
612
+ remarksType.value = type;
613
+ const existingRemarks = type === 'checkIn' ? item?.checkInRemarks : item?.checkOutRemarks;
614
+ remarksInput.value = existingRemarks || "";
615
+ remarksViewOnly.value = !!existingRemarks;
616
+ dialog.remarks = true;
617
+ }
618
+
619
+ async function handleSaveRemarks() {
620
+ if (!remarksInput.value.trim() || !selectedVisitorId.value) return;
621
+ try {
622
+ loading.savingRemarks = true;
623
+ const payload = remarksType.value === 'checkIn'
624
+ ? { checkInRemarks: remarksInput.value.trim() }
625
+ : { checkOutRemarks: remarksInput.value.trim() };
626
+ await updateVisitor(selectedVisitorId.value, payload);
627
+ showMessage("Remarks saved successfully!", "info");
628
+ await getVisitorRefresh();
629
+ dialog.remarks = false;
630
+ } catch (error: any) {
631
+ const errorMessage = error?.response?._data?.message;
632
+ showMessage(errorMessage || "Something went wrong. Please try again later.", "error");
633
+ } finally {
634
+ loading.savingRemarks = false;
635
+ }
636
+ }
637
+
638
+ async function handleUpdatePassKeys(){
639
+ if (!selectedVisitorId.value) return;
640
+ try {
641
+ loading.updatingPassKeys = true;
642
+ const payload: any = {};
643
+ if (passReturnStatuses.value.length > 0) {
644
+ payload.visitorPass = passReturnStatuses.value;
645
+ }
646
+ if (keyReturnStatuses.value.length > 0) {
647
+ payload.passKeys = keyReturnStatuses.value;
648
+ }
649
+ await updateVisitor(selectedVisitorId.value, payload);
650
+ showMessage("Pass/Key statuses updated successfully!", "info");
651
+ await getVisitorRefresh();
652
+ } catch (error: any) {
653
+ const errorMessage = error?.response?._data?.message;
654
+ showMessage(errorMessage || "Something went wrong. Please try again later.", "error");
655
+ } finally {
656
+ loading.updatingPassKeys = false;
657
+ }
658
+ }
659
+
482
660
 
483
661
  async function handleProceedDeleteVisitor() {
484
662
  try {
@@ -502,22 +680,59 @@ async function handleProceedDeleteVisitor() {
502
680
  }
503
681
  }
504
682
 
505
- async function handleCheckout(userId: string) {
683
+
684
+ const passReturnStatuses = ref<{ keyId: string, status: string, prefixAndName: string, remarks: string }[]>([]);
685
+ const keyReturnStatuses = ref<{ keyId: string, status: string, prefixAndName: string, remarks: string }[]>([]);
686
+
687
+ const canConfirmCheckout = computed(() => {
688
+ const allEntries = [...passReturnStatuses.value, ...keyReturnStatuses.value];
689
+ return allEntries.every((entry) => {
690
+ if (!entry.status || entry.status === 'In Use') return false;
691
+ if ((entry.status === 'Lost' || entry.status === 'Damaged') && !entry.remarks.trim()) return false;
692
+ return true;
693
+ });
694
+ });
695
+
696
+
697
+
698
+ function handleCheckout(userId: string) {
506
699
  if (!userId) {
507
700
  showMessage("Invalid userId", "error");
508
701
  return;
509
702
  }
510
703
  selectedVisitorId.value = userId;
511
704
 
705
+ const visitor = items.value.find((x: any) => x?._id === userId);
706
+ const hasPasses = (visitor?.visitorPass?.length ?? 0) > 0;
707
+ const hasKeys = (visitor?.passKeys?.length ?? 0) > 0;
708
+
709
+ if (hasPasses || hasKeys) {
710
+ const visitor = items.value.find((x: any) => x?._id === userId);
711
+ passReturnStatuses.value = ((visitor?.visitorPass as any[]) || []).map((p: any) => ({ keyId: p.keyId, status: p.status ?? "In Use", remarks: '', prefixAndName: p.prefixAndName }));
712
+ keyReturnStatuses.value = ((visitor?.passKeys as any[]) || []).map((k: any) => ({ keyId: k.keyId, status: k.status ?? "In Use", remarks: '', prefixAndName: k.prefixAndName }));
713
+ dialog.returnPassesKeys = true;
714
+ return;
715
+ }
716
+
717
+ proceedCheckout();
718
+ }
719
+
720
+ async function proceedCheckout() {
721
+ const userId = selectedVisitorId.value;
722
+ if (!userId) return;
723
+
512
724
  try {
513
725
  loading.checkingOut = true;
514
726
  const res = await updateVisitor(userId as string, {
515
727
  checkOut: new Date().toISOString(),
728
+ ...(passReturnStatuses.value.length > 0 ? { visitorPass: passReturnStatuses.value } : {}),
729
+ ...(keyReturnStatuses.value.length > 0 ? { passKeys: keyReturnStatuses.value } : {}),
516
730
  });
517
731
  if (res) {
518
732
  showMessage("Visitor successfully checked-out!", "info");
519
733
  await getVisitorRefresh();
520
734
  dialog.viewVisitor = false;
735
+ dialog.returnPassesKeys = false;
521
736
  }
522
737
  } catch (error: any) {
523
738
  const errorMessage = error?.response?._data?.message;
@@ -195,6 +195,7 @@ export default function useKey() {
195
195
  organization,
196
196
  startDateTime,
197
197
  endDateTime,
198
+ statuses
198
199
  }: {
199
200
  page?: number;
200
201
  search?: string | ISearch;
@@ -204,6 +205,7 @@ export default function useKey() {
204
205
  organization?: string;
205
206
  startDateTime?: string;
206
207
  endDateTime?: string;
208
+ statuses?: string[];
207
209
  } = {}) {
208
210
  /*
209
211
  search here should accept
@@ -220,6 +222,7 @@ export default function useKey() {
220
222
  organization,
221
223
  startDateTime,
222
224
  endDateTime,
225
+ statuses
223
226
  },
224
227
  });
225
228
  }
@@ -381,5 +384,6 @@ export default function useKey() {
381
384
  passTypesStatus,
382
385
  getCountTotalKeys,
383
386
  getKeyWithSequence,
387
+ getKeysByKeyPageSearch
384
388
  };
385
389
  }
@@ -29,7 +29,7 @@ export default function useLocalAuth() {
29
29
 
30
30
  const currentUser = useState(
31
31
  "currentUser",
32
- (): TUser => JSON.parse(JSON.stringify(user))
32
+ (): TUser | null => JSON.parse(JSON.stringify(user))
33
33
  );
34
34
 
35
35
  async function authenticate() {
@@ -41,9 +41,7 @@ export default function useLocalAuth() {
41
41
  );
42
42
 
43
43
  watchEffect(() => {
44
- if (getCurrentUserReq.value) {
45
- currentUser.value = getCurrentUserReq.value;
46
- }
44
+ currentUser.value = getCurrentUserReq.value as TUser | null;
47
45
  });
48
46
 
49
47
  watchEffect(() => {