@7365admin1/layer-common 1.11.18 → 1.11.20

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,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.11.20
4
+
5
+ ### Patch Changes
6
+
7
+ - e9bd022: fix feedbacks page when refreshing or reloading the page
8
+
9
+ ## 1.11.19
10
+
11
+ ### Patch Changes
12
+
13
+ - e340731: Update Layer-common
14
+
3
15
  ## 1.11.18
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1,207 @@
1
+ <template>
2
+ <v-card width="100%" :loading="processing">
3
+ <v-toolbar>
4
+ <v-row no-gutters class="fill-height px-6 d-flex justify-space-between align-center" align="center">
5
+ <span class="font-weight-bold text-subtitle-1">Assign Pass & Keys</span>
6
+ <ButtonClose @click="emit('close')" />
7
+ </v-row>
8
+ </v-toolbar>
9
+
10
+ <v-card-text>
11
+ <v-row no-gutters class="ga-1">
12
+ <v-col cols="12">
13
+ <span class="text-subtitle-2 text-medium-emphasis">
14
+ Name: {{ prop.visitor.name }}
15
+ </span>
16
+ </v-col>
17
+
18
+ <v-col cols="12" class="mt-3">
19
+ <InputLabel title="Pass" />
20
+ <v-autocomplete
21
+ v-model="selectedPass"
22
+ v-model:search="passInput"
23
+ :hide-no-data="false"
24
+ :items="passItems"
25
+ item-title="prefixAndName"
26
+ item-value="_id"
27
+ variant="outlined"
28
+ hide-details
29
+ density="compact"
30
+ small-chips
31
+ :loading="fetchPassesPending"
32
+ >
33
+ <template v-slot:chip="{ props: chipProps, item }">
34
+ <v-chip v-if="selectedPass" v-bind="chipProps" prepend-icon="mdi-card-bulleted-outline"
35
+ :text="item.raw?.prefixAndName" />
36
+ </template>
37
+ <template v-slot:no-data>
38
+ <v-list-item density="compact">
39
+ <v-list-item-title>No available passes</v-list-item-title>
40
+ </v-list-item>
41
+ </template>
42
+ </v-autocomplete>
43
+ </v-col>
44
+
45
+ <v-col v-if="showKeys" cols="12" class="mt-3">
46
+ <InputLabel title="Keys" />
47
+ <v-autocomplete
48
+ v-model="selectedKeys"
49
+ v-model:search="keyInput"
50
+ :hide-no-data="false"
51
+ :items="keyItems"
52
+ item-title="prefixAndName"
53
+ item-value="_id"
54
+ multiple
55
+ variant="outlined"
56
+ hide-details
57
+ density="compact"
58
+ small-chips
59
+ :loading="fetchKeysPending"
60
+ >
61
+ <template v-slot:chip="{ props: chipProps, item }">
62
+ <v-chip v-if="selectedKeys.length > 0" v-bind="chipProps" prepend-icon="mdi-key"
63
+ :text="item.raw?.prefixAndName" />
64
+ </template>
65
+ <template v-slot:no-data>
66
+ <v-list-item density="compact">
67
+ <v-list-item-title>No available keys</v-list-item-title>
68
+ </v-list-item>
69
+ </template>
70
+ </v-autocomplete>
71
+ </v-col>
72
+ </v-row>
73
+
74
+ <v-row v-if="errorMessage" no-gutters class="mt-2">
75
+ <p class="text-error text-subtitle-2 w-100 text-center">{{ errorMessage }}</p>
76
+ </v-row>
77
+ </v-card-text>
78
+
79
+ <v-toolbar density="compact">
80
+ <v-row no-gutters>
81
+ <v-col cols="6">
82
+ <v-btn tile block variant="text" class="text-none" size="48" text="Cancel" @click="emit('close')" />
83
+ </v-col>
84
+ <v-col cols="6">
85
+ <v-btn
86
+ tile block variant="flat" color="black" class="text-none" size="48"
87
+ text="Assign"
88
+ :disabled="!selectedPass && selectedKeys.length === 0"
89
+ :loading="processing"
90
+ @click="handleSubmit"
91
+ />
92
+ </v-col>
93
+ </v-row>
94
+ </v-toolbar>
95
+ </v-card>
96
+ </template>
97
+
98
+ <script setup lang="ts">
99
+ import type { PropType } from 'vue'
100
+ import usePassKey from '../composables/usePassKey'
101
+ import useVisitor from '../composables/useVisitor'
102
+
103
+ const prop = defineProps({
104
+ visitor: {
105
+ type: Object as PropType<TVisitor>,
106
+ required: true,
107
+ },
108
+ site: {
109
+ type: String,
110
+ required: true,
111
+ },
112
+ type: {
113
+ type: String as PropType<TVisitorType>,
114
+ required: true,
115
+ },
116
+ contractorType: {
117
+ type: String,
118
+ default: '',
119
+ },
120
+ })
121
+
122
+ const emit = defineEmits<{
123
+ (e: 'done'): void
124
+ (e: 'close'): void
125
+ }>()
126
+
127
+ const { getPassKeysByPageSearch } = usePassKey()
128
+ const { updateVisitor } = useVisitor()
129
+
130
+ const processing = ref(false)
131
+ const errorMessage = ref('')
132
+
133
+ const selectedPass = ref<string>('')
134
+ const selectedKeys = ref<string[]>([])
135
+ const passInput = ref('')
136
+ const keyInput = ref('')
137
+ const passItems = ref<TPassKey[]>([])
138
+ const keyItems = ref<TPassKey[]>([])
139
+
140
+ const showKeys = computed(() => prop.visitor.type === 'contractor')
141
+
142
+ const passTypesComputed = computed(() => {
143
+ if (prop.type === 'contractor') {
144
+ return prop.contractorType === 'property-agent' ? ['agent-pass'] : ['contractor-pass']
145
+ }
146
+ return ['visitor-pass']
147
+ })
148
+
149
+ const { data: passesData, pending: fetchPassesPending } = await useLazyAsyncData(
150
+ `add-pass-key-visitor-passes-${prop.visitor._id}`,
151
+ () => getPassKeysByPageSearch({
152
+ search: passInput.value,
153
+ page: 1,
154
+ limit: 500,
155
+ passTypes: passTypesComputed.value,
156
+ sites: [prop.site],
157
+ statuses: ['Available'],
158
+ })
159
+ )
160
+
161
+ const { data: keysData, pending: fetchKeysPending } = await useLazyAsyncData(
162
+ `add-pass-key-visitor-keys-${prop.visitor._id}`,
163
+ () => getPassKeysByPageSearch({
164
+ search: keyInput.value,
165
+ page: 1,
166
+ limit: 500,
167
+ passTypes: ['pass-key'],
168
+ sites: [prop.site],
169
+ statuses: ['Available'],
170
+ })
171
+ )
172
+
173
+ watch(passesData, (data: any) => {
174
+ passItems.value = Array.isArray(data?.items) ? data.items : []
175
+ })
176
+
177
+ watch(keysData, (data: any) => {
178
+ keyItems.value = Array.isArray(data?.items) ? data.items : []
179
+ })
180
+
181
+ async function handleSubmit() {
182
+ if (!prop.visitor._id) return
183
+ errorMessage.value = ''
184
+ processing.value = true
185
+
186
+ try {
187
+ const payload: Partial<TVisitorPayload> = {}
188
+
189
+ if (selectedPass.value) {
190
+ payload.visitorPass = [{ keyId: selectedPass.value, status: "In Use" }]
191
+ }
192
+
193
+ if (selectedKeys.value.length > 0) {
194
+ payload.passKeys = selectedKeys.value.map(keyId => ({ keyId, status: "In Use" }))
195
+ }
196
+
197
+ await updateVisitor(prop.visitor._id, payload)
198
+ emit('done')
199
+ } catch (error: any) {
200
+ errorMessage.value = error?.data?.message || 'Failed to assign pass & keys. Please try again.'
201
+ } finally {
202
+ processing.value = false
203
+ }
204
+ }
205
+ </script>
206
+
207
+ <style scoped></style>
@@ -14,6 +14,22 @@
14
14
  @refresh="getAreaChecklistHistoryRefresh"
15
15
  @row-click="handleRowClick"
16
16
  >
17
+ <template #actions>
18
+ <v-row class="w-100" align="center" no-gutters>
19
+ <v-col cols="auto">
20
+ <v-btn
21
+ variant="text"
22
+ color="primary"
23
+ class="text-none"
24
+ @click="back"
25
+ >
26
+ <v-icon left>mdi-arrow-left</v-icon>
27
+ Back
28
+ </v-btn>
29
+ </v-col>
30
+ </v-row>
31
+ </template>
32
+
17
33
  <template #extension>
18
34
  <v-row no-gutters class="w-100 d-flex flex-column">
19
35
  <v-card
@@ -84,7 +100,7 @@ const props = defineProps({
84
100
  scheduleRoute: { type: String, default: "cleaning-schedule" },
85
101
  });
86
102
 
87
- const { formatDate } = useUtils();
103
+ const { formatDate, back, debounce } = useUtils();
88
104
 
89
105
  const loading = ref(false);
90
106
  const message = ref("");
@@ -160,6 +176,18 @@ watchEffect(() => {
160
176
  }
161
177
  });
162
178
 
179
+ const debouncedSearchRefresh = debounce(() => {
180
+ const wasPage = page.value;
181
+ page.value = 1;
182
+ if (wasPage === 1) {
183
+ getAreaChecklistHistoryRefresh();
184
+ }
185
+ }, 450);
186
+
187
+ watch(searchInput, () => {
188
+ debouncedSearchRefresh();
189
+ });
190
+
163
191
  function handleRowClick(data: any) {
164
192
  const item = data?.item ?? data;
165
193
  const id = item?._id || item?.id || item?.areaId;
@@ -227,6 +227,17 @@
227
227
 
228
228
  </v-row>
229
229
  </v-col>
230
+
231
+ <v-col cols="12" class="mt-2">
232
+ <InputLabel class="text-capitalize d-block mb-1" title="Unit Documents" />
233
+ <InputFileV2
234
+ v-model="buildingUnit.buildingUnitFiles"
235
+ :multiple="true"
236
+ accept="*/*"
237
+ title="Upload documents"
238
+ :height="104"
239
+ />
240
+ </v-col>
230
241
  </v-row>
231
242
  </template>
232
243
  </v-card-text>
@@ -331,6 +342,35 @@ const buildingUnit = ref({
331
342
 
332
343
  buildingUnit.value = JSON.parse(JSON.stringify(prop.roomFacility));
333
344
 
345
+ // Normalize buildingUnitFiles: extract IDs for InputFileV2, keep names in a separate map
346
+ const buildingUnitFilesNames = ref<Record<string, string>>({});
347
+ const rawFiles = buildingUnit.value.buildingUnitFiles as any[];
348
+ if (rawFiles?.length && typeof rawFiles[0] === 'object') {
349
+ rawFiles.forEach((f: { id: string; name: string }) => {
350
+ buildingUnitFilesNames.value[f.id] = f.name ?? "";
351
+ });
352
+ buildingUnit.value.buildingUnitFiles = rawFiles.map((f: { id: string }) => f.id);
353
+ }
354
+
355
+ const { getFileById } = useFile();
356
+
357
+ watch(
358
+ () => buildingUnit.value.buildingUnitFiles as string[],
359
+ async (ids) => {
360
+ for (const id of ids) {
361
+ if (!buildingUnitFilesNames.value[id]) {
362
+ try {
363
+ const meta = await getFileById(id) as any;
364
+ buildingUnitFilesNames.value[id] = meta?.data?.name ?? meta?.name ?? "";
365
+ } catch {
366
+ buildingUnitFilesNames.value[id] = "";
367
+ }
368
+ }
369
+ }
370
+ },
371
+ { deep: true }
372
+ );
373
+
334
374
  const emit = defineEmits(["cancel", "success", "success:create-more", "delete-unit"]);
335
375
 
336
376
 
@@ -462,7 +502,7 @@ async function submit() {
462
502
  name: buildingUnit.value.name,
463
503
  level: buildingUnit.value.level,
464
504
  // category: buildingUnit.value.category,
465
- buildingUnitFiles: buildingUnit.value.buildingUnitFiles || [],
505
+ buildingUnitFiles: (buildingUnit.value.buildingUnitFiles as string[] || []).map((id) => ({ id, name: buildingUnitFilesNames.value[id] ?? "" })),
466
506
  companyName: buildingUnit.value.companyName,
467
507
  companyRegistrationNumber: buildingUnit.value.companyRegistrationNumber || "",
468
508
  leaseStart: buildingUnit.value.leaseStart,
@@ -116,7 +116,7 @@ const props = defineProps({
116
116
 
117
117
  const startDate = ref("");
118
118
  const endDate = ref("");
119
- const status = ref<TScheduleAreaStatus>("All");
119
+ const status = ref<TScheduleAreaStatus>("all");
120
120
  const statusOptions = [
121
121
  { title: "All", value: "all" },
122
122
  { title: "Open", value: "open" },
@@ -191,7 +191,7 @@ const {
191
191
  site: props.site,
192
192
  startDate: startDate.value,
193
193
  endDate: endDate.value,
194
- status: status.value === "All" ? undefined : status.value,
194
+ status: status.value === "all" ? undefined : status.value,
195
195
  serviceType: props.serviceType,
196
196
  }),
197
197
  {
@@ -1,13 +1,13 @@
1
1
  <template>
2
2
  <div class="d-flex flex-column">
3
3
  <v-text-field v-bind="$attrs" ref="dateTimePickerRef" :model-value="dateTimeFormattedReadOnly" autocomplete="off"
4
- :placeholder="placeholder" :rules="rules" style="z-index: 10" @click="openDatePicker">
4
+ :placeholder="inputPlaceholder" :rules="rules" style="z-index: 10" @click="openDatePicker">
5
5
  <template #append-inner>
6
6
  <v-icon icon="mdi-calendar" @click.stop="openDatePicker" />
7
7
  </template>
8
8
  </v-text-field>
9
9
  <div class="w-100 d-flex align-end ga-3 hidden-input">
10
- <input ref="dateInput" type="datetime-local" v-model="dateTime" />
10
+ <input ref="dateInput" :type="inputType" v-model="dateTime" />
11
11
  </div>
12
12
  </div>
13
13
  </template>
@@ -23,6 +23,10 @@ const prop = defineProps({
23
23
  placeholder: {
24
24
  type: String,
25
25
  default: 'DD/MM/YYYY, HH:MM AM/PM'
26
+ },
27
+ dateOnly: {
28
+ type: Boolean,
29
+ default: false
26
30
  }
27
31
  })
28
32
 
@@ -31,6 +35,10 @@ const dateTime = defineModel<string | null>({ default: null }) //2025-10-10T13:0
31
35
  const dateTimeUTC = defineModel<string | null>('utc', { default: null }) // UTC format
32
36
 
33
37
  const dateTimeFormattedReadOnly = ref<string | null>(null)
38
+ const inputType = computed(() => (prop.dateOnly ? 'date' : 'datetime-local'))
39
+ const inputPlaceholder = computed(() => (
40
+ prop.placeholder || (prop.dateOnly ? 'MM/DD/YYYY' : 'DD/MM/YYYY, HH:MM AM/PM')
41
+ ))
34
42
 
35
43
 
36
44
 
@@ -55,11 +63,16 @@ function convertToReadableFormat(dateStr: string): string {
55
63
  if (!dateStr) return "";
56
64
 
57
65
  const date = new Date(dateStr)
66
+ if (Number.isNaN(date.getTime())) return "";
58
67
 
59
- const day = String(date.getDate()).padStart(2, '0')
60
68
  const month = String(date.getMonth() + 1).padStart(2, '0')
69
+ const day = String(date.getDate()).padStart(2, '0')
61
70
  const year = date.getFullYear()
62
71
 
72
+ if (prop.dateOnly) {
73
+ return `${month}/${day}/${year}`
74
+ }
75
+
63
76
  let hours = date.getHours()
64
77
  const minutes = String(date.getMinutes()).padStart(2, '0')
65
78
 
@@ -72,17 +85,36 @@ function convertToReadableFormat(dateStr: string): string {
72
85
  return `${day}/${month}/${year}, ${formattedTime}`
73
86
  }
74
87
 
88
+ function toDateOnlyInputValue(dateStr: string): string {
89
+ if (!dateStr) return ''
90
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return dateStr
91
+ const date = new Date(dateStr)
92
+ if (Number.isNaN(date.getTime())) return ''
93
+ const year = date.getFullYear()
94
+ const month = String(date.getMonth() + 1).padStart(2, '0')
95
+ const day = String(date.getDate()).padStart(2, '0')
96
+ return `${year}-${month}-${day}`
97
+ }
98
+
75
99
  function handleInitialDate(){
76
100
  const dateDefault = dateTime.value
77
101
  const dateUTC = dateTimeUTC.value
78
102
  if(dateDefault){
79
103
  dateTimeFormattedReadOnly.value = convertToReadableFormat(dateDefault)
80
- const localDate = new Date(dateDefault)
81
- dateTimeUTC.value = localDate.toISOString()
104
+ if (prop.dateOnly) {
105
+ dateTimeUTC.value = toDateOnlyInputValue(dateDefault)
106
+ } else {
107
+ const localDate = new Date(dateDefault)
108
+ dateTimeUTC.value = localDate.toISOString()
109
+ }
82
110
  } else if (dateUTC){
83
111
  dateTimeFormattedReadOnly.value = convertToReadableFormat(dateUTC)
84
- const localDate = new Date(dateUTC)
85
- dateTime.value = formatDateISO8601(localDate)
112
+ if (prop.dateOnly) {
113
+ dateTime.value = toDateOnlyInputValue(dateUTC)
114
+ } else {
115
+ const localDate = new Date(dateUTC)
116
+ dateTime.value = formatDateISO8601(localDate)
117
+ }
86
118
  } else {
87
119
  dateTimeFormattedReadOnly.value = null
88
120
  }
@@ -98,8 +130,12 @@ watch(dateTime, (dateVal) => {
98
130
  }
99
131
 
100
132
  dateTimeFormattedReadOnly.value = convertToReadableFormat(dateVal)
101
- const localDate = new Date(dateVal)
102
- dateTimeUTC.value = localDate.toISOString()
133
+ if (prop.dateOnly) {
134
+ dateTimeUTC.value = toDateOnlyInputValue(dateVal)
135
+ } else {
136
+ const localDate = new Date(dateVal)
137
+ dateTimeUTC.value = localDate.toISOString()
138
+ }
103
139
 
104
140
  }, { immediate: false })
105
141
 
@@ -77,7 +77,7 @@ const props = defineProps({
77
77
  }
78
78
  })
79
79
 
80
- const { addFile, deleteFile, getFileUrl, urlToFile } = useFile()
80
+ const { addFile, deleteFile, getFileUrl, getFileById, urlToFile } = useFile()
81
81
 
82
82
  const showImageCarousel = ref(false)
83
83
  const activeImageId = ref("")
@@ -187,8 +187,9 @@ async function downloadFile(id: string, filename: string) {
187
187
  const result: { file: File; id: string }[] = []
188
188
  for (const id of ids) {
189
189
  try {
190
- const url = await getFileUrl(id)
191
- const name = decodeURIComponent(url.split('/').pop() || `file_${id}`)
190
+ const meta = await getFileById(id) as any
191
+ const name: string = meta?.data?.name || meta?.name || `file_${id}`
192
+ const url = getFileUrl(id)
192
193
  const file = await urlToFile(url, name)
193
194
  result.push({ file, id })
194
195
  } catch (err) {
@@ -19,7 +19,7 @@
19
19
  </v-col>
20
20
 
21
21
  <v-col cols="12">
22
- <v-autocomplete v-model="memberForm.visitorPass" v-model:search="passInput"
22
+ <v-autocomplete v-model="selectedPass" v-model:search="passInput"
23
23
  :hide-no-data="false" class="mt-3" :items="passItemsFilteredFinal"
24
24
  item-title="prefixAndName" item-value="_id" label="Pass (optional)" variant="outlined"
25
25
  hide-details density="compact" persistent-hint small-chips>
@@ -35,7 +35,7 @@
35
35
  </template>
36
36
 
37
37
  <template v-slot:chip="{ props, item }">
38
- <v-chip v-if="memberForm.visitorPass" v-bind="props"
38
+ <v-chip v-if="selectedPass" v-bind="props"
39
39
  prepend-icon="mdi-card-bulleted-outline"
40
40
  :text="item.raw?.prefixAndName"></v-chip>
41
41
  </template>
@@ -116,6 +116,8 @@ const errorMessage = ref('')
116
116
  const passInput = ref('')
117
117
  const passItems = ref<TPassKey[]>([])
118
118
 
119
+ const selectedPass = ref<string>('')
120
+
119
121
  const members = defineModel<TMemberInfo[]>({ required: true, default: [] })
120
122
  const committedMembers = ref<TMemberInfo[]>([])
121
123
 
@@ -171,6 +173,10 @@ const membersDisplayed = computed(() => {
171
173
  })
172
174
  })
173
175
 
176
+ watch(selectedPass, (newVal) => {
177
+ memberForm.visitorPass = [{ keyId: newVal }]
178
+ })
179
+
174
180
 
175
181
 
176
182
  function handleClearForm() {
@@ -111,7 +111,8 @@ const isHydrating = ref(true)
111
111
 
112
112
  const { requiredRule } = useUtils()
113
113
  const { generateTimeSlots, generateTimeSlotsFromStart } = useFacilityUtils()
114
- const { updateOvernightParkingAvailability, getOvernightParkingAvailability } = useSiteSettings()
114
+ const { currentUser } = useLocalAuth()
115
+ const { getOvernightParkingAvailabilityV2, createOrUpdateOvernightParkingAvailabilityV2 } = useSiteSettings()
115
116
 
116
117
  type TDay = Exclude<keyof TOvernightParkingAvailability, 'autoApproveOvernightParking'>
117
118
  type TOvernightParkingSlot = TOvernightParkingDay & { day: TDay }
@@ -139,9 +140,11 @@ const applyDialog = reactive({
139
140
 
140
141
 
141
142
 
142
- const { data: availabilityDataReq, refresh: refreshAvailability } = await useLazyAsyncData<TOvernightParkingAvailability>(
143
+ const userType = computed(() => currentUser.value?.type ?? "site")
144
+ const updatedByValue = computed(() => currentUser.value?._id ?? currentUser.value?.email ?? "system")
145
+ const { data: availabilityDataReq, refresh: refreshAvailability } = await useLazyAsyncData<Record<string, any>>(
143
146
  `overnight-parking-availability-${props.site}`,
144
- () => getOvernightParkingAvailability(props.site)
147
+ () => getOvernightParkingAvailabilityV2(props.site, userType.value)
145
148
  )
146
149
 
147
150
  watch(availabilityDataReq, (data) => {
@@ -152,15 +155,16 @@ watch(availabilityDataReq, (data) => {
152
155
  slot.isEnabled = data?.[day]?.isEnabled ?? false
153
156
  slot.startTime = data?.[day]?.startTime ?? null
154
157
  slot.endTime = data?.[day]?.endTime ?? null
155
- setTimeout(() => {
156
- isHydrating.value = false
157
- }, 500)
158
158
  })
159
-
160
- autoApproveOvernightParking.value = data?.autoApproveOvernightParking === true
159
+ autoApproveOvernightParking.value =
160
+ data?.autoApproveOvernightParking === true || data?.isAutoApproved === true
161
+ setTimeout(() => {
162
+ isHydrating.value = false
163
+ }, 500)
161
164
  }, { immediate: true })
162
165
 
163
166
 
167
+
164
168
  const overnightParkingSlotsArray = ref<TOvernightParkingSlot[]>(
165
169
  orderedDays.map((day) => ({
166
170
  day,
@@ -240,23 +244,20 @@ async function handleSave(action: 'toggle' | 'save') {
240
244
 
241
245
  let payload = {}
242
246
 
243
- if(action === 'save') {
244
- payload = {
245
- ...model.value
246
- }
247
- } else if (action === 'toggle') {
248
- payload = {
249
- autoApproveOvernightParking: autoApproveOvernightParking.value,
250
- }
247
+ payload = {
248
+ ...model.value,
249
+ autoApproveOvernightParking: autoApproveOvernightParking.value,
250
+ isAutoApproved: autoApproveOvernightParking.value,
251
+ updatedBy: updatedByValue.value,
251
252
  }
252
253
 
253
254
  try {
254
255
  loading.updating = true
255
- await updateOvernightParkingAvailability(props.site, payload)
256
+ await createOrUpdateOvernightParkingAvailabilityV2(props.site, payload, userType.value)
257
+ refreshAvailability()
256
258
  message.value = 'Overnight parking settings updated successfully.'
257
259
  messageColor.value = 'success'
258
260
  messageSnackbar.value = true
259
- refreshAvailability()
260
261
  } catch (error) {
261
262
  message.value = 'Failed to update overnight parking settings. Please try again.'
262
263
  messageColor.value = 'error'
@@ -208,12 +208,12 @@ const isScheduleClosed = computed(
208
208
 
209
209
  const items = ref<Array<Record<string, any>>>([]);
210
210
 
211
- const statusFilter = ref<TScheduleAreaStatus>("All");
211
+ const statusFilter = ref<TScheduleAreaStatus>("all");
212
212
  const statusOptions = [
213
- { title: "All", value: "All" },
214
- { title: "Open", value: "Open" },
215
- { title: "Ongoing", value: "Ongoing" },
216
- { title: "Completed", value: "Completed" },
213
+ { title: "All", value: "all" },
214
+ { title: "Open", value: "open" },
215
+ { title: "Ongoing", value: "ongoing" },
216
+ { title: "Completed", value: "completed" },
217
217
  ];
218
218
  const areaTypeFilter = ref<TAreaType>("all");
219
219
  const typeOptions = [
@@ -235,7 +235,7 @@ const {
235
235
  getScheduleAreas({
236
236
  page: page.value,
237
237
  scheduleAreaId: props.scheduleAreaId,
238
- status: statusFilter.value === "All" ? undefined : statusFilter.value,
238
+ status: statusFilter.value === "all" ? undefined : statusFilter.value,
239
239
  type: areaTypeFilter.value === "all" ? undefined : areaTypeFilter.value,
240
240
  serviceType: props.serviceType,
241
241
  }),
@@ -20,8 +20,8 @@
20
20
  <span class="text-caption">Not Checked Out</span>
21
21
  </template>
22
22
  </v-checkbox>
23
- <InputDateTimePicker v-model:utc="dateFrom" density="compact" hide-details />
24
- <InputDateTimePicker v-model:utc="dateTo" density="compact" hide-details />
23
+ <InputDatePicker v-model="dateFrom" density="compact" hide-details />
24
+ <InputDatePicker v-model="dateTo" density="compact" hide-details />
25
25
  <v-select v-if="activeTab == 'registered'" v-model="filterTypes" label="Filter by types" item-title="label"
26
26
  item-value="value" :items="visitorSelection" density="compact" clearable multiple max-width="200"
27
27
  hide-details>
@@ -43,7 +43,8 @@
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
+ <span class="text-grey text-caption" v-if="item?.members?.length > 0">( +{{ item?.members?.length }}
47
+ members)</span>
47
48
  </template>
48
49
 
49
50
  <template v-slot:item.type-company="{ item }">
@@ -51,7 +52,7 @@
51
52
  <v-icon icon="mdi-user" size="15" />
52
53
  <span v-if="item.type === 'contractor'" class="text-capitalize">{{
53
54
  formatCamelCaseToWords(item.contractorType)
54
- }}</span>
55
+ }}</span>
55
56
  <span v-else class="text-capitalize">{{ formatType(item) }}</span>
56
57
  </span>
57
58
  <span class="d-flex align-center ga-2">
@@ -90,7 +91,7 @@
90
91
  <v-icon icon="mdi-clock-time-four-outline" color="green" size="20" />
91
92
  <span class="text-capitalize">{{
92
93
  UTCToLocalTIme(item.checkIn) || "-"
93
- }}</span>
94
+ }}</span>
94
95
  <span>
95
96
  <v-icon v-if="item?.snapshotEntryImage" size="17" icon="mdi-image"
96
97
  @click.stop="handleViewImage(item.snapshotEntryImage)" />
@@ -101,7 +102,7 @@
101
102
  <template v-if="item.checkOut">
102
103
  <span class="text-capitalize">{{
103
104
  UTCToLocalTIme(item.checkOut) || "-"
104
- }}</span>
105
+ }}</span>
105
106
  <span>
106
107
  <v-icon v-if="item?.snapshotExitImage" size="17" icon="mdi-image"
107
108
  @click.stop="handleViewImage(item.snapshotExitImage)" />
@@ -127,6 +128,13 @@
127
128
  {{ (key as any)?.prefixAndName }}
128
129
  </v-chip>
129
130
  </div>
131
+ <div v-if="showAddPassKeyButton(item)" class="d-flex flex-wrap ga-1 mt-1 mt-2"
132
+ @click.stop="handleCheckout(item._id)">
133
+ <v-chip size="x-small" variant="tonal" prepend-icon="mdi-key" @click.stop="handleOpenAddPassKey(item)">
134
+ Add Pass/Key
135
+ </v-chip>
136
+
137
+ </div>
130
138
  </v-row>
131
139
  </template>
132
140
 
@@ -180,7 +188,7 @@
180
188
  </span>
181
189
 
182
190
  <span v-else-if="selectedVisitorObject[key]" class="d-flex ga-3 align-center"><strong>{{ label
183
- }}:</strong>
191
+ }}:</strong>
184
192
  {{ formatValues(key, selectedVisitorObject[key]) }}
185
193
  <TooltipInfo v-if="key === 'checkOut'" text="Manual Checkout" density="compact" size="x-small" />
186
194
  </span>
@@ -251,9 +259,9 @@
251
259
  {{ (pass as any)?.prefixAndName }}
252
260
  </v-chip>
253
261
  <v-select hide-details max-width="200px" density="compact" :items="passStatusOptions" item-title="label"
254
- item-value="value" v-model="pass.status"></v-select>
262
+ item-value="value" v-model="pass.status" :disabled="selectedVisitorObject.checkOut"></v-select>
255
263
  <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>
264
+ density="compact" v-model="pass.remarks" :disabled="selectedVisitorObject.checkOut"></v-textarea>
257
265
  </v-row>
258
266
  </div>
259
267
  <div v-if="(keyReturnStatuses.length > 0)" class="mb-2">
@@ -264,32 +272,37 @@
264
272
  {{ (key as any)?.prefixAndName }}
265
273
  </v-chip>
266
274
  <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>
275
+ item-value="value" v-model="key.status" :disabled="selectedVisitorObject.checkOut"></v-select>
269
276
  <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>
277
+ density="compact" v-model="key.remarks" :disabled="selectedVisitorObject.checkOut"></v-textarea>
271
278
  </v-row>
272
279
  </div>
273
280
 
274
281
  <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>
282
+ <v-btn variant="flat" color="blue" density="comfortable" class="text-capitalize" :disabled="selectedVisitorObject.checkOut"
283
+ :loading="loading.updatingPassKeys" @click.stop="handleUpdatePassKeys" >Update Pass/Keys</v-btn>
276
284
  </v-row>
277
285
  </v-card-text>
278
286
  <v-toolbar class="pa-0" density="compact">
279
287
  <v-row no-gutters>
280
288
  <v-col cols="6">
281
- <v-btn variant="text" block :disabled="loading.checkingOut"
289
+ <v-btn variant="text" block
282
290
  @click="dialog.returnPassesKeys = false">Close</v-btn>
283
291
  </v-col>
284
292
  <v-col cols="6">
285
293
  <v-btn color="red" variant="flat" height="48" rounded="0" block :loading="loading.checkingOut"
286
- :disabled="!canConfirmCheckout" @click="proceedCheckout">Confirm Checkout</v-btn>
294
+ :disabled="!canConfirmCheckout || selectedVisitorObject.checkOut" @click="proceedCheckout">Confirm Checkout</v-btn>
287
295
  </v-col>
288
296
  </v-row>
289
297
  </v-toolbar>
290
298
  </v-card>
291
299
  </v-dialog>
292
300
 
301
+ <v-dialog v-model="dialog.addPassKey" width="450" persistent>
302
+ <AddPassKeyToVisitor :visitor="selectedVisitorDataObject" :site="siteId" @close="dialog.addPassKey = false"
303
+ @update="getVisitorRefresh" />
304
+ </v-dialog>
305
+
293
306
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
294
307
  </v-row>
295
308
  </template>
@@ -379,6 +392,7 @@ const dialog = reactive({
379
392
  snapshotImage: false,
380
393
  remarks: false,
381
394
  returnPassesKeys: false,
395
+ addPassKey: false
382
396
  });
383
397
 
384
398
  const snapshotImageUrl = ref("");
@@ -404,6 +418,19 @@ const tabOptions = [
404
418
  { name: "Resident Transactions", value: "resident-transactions" },
405
419
  ];
406
420
 
421
+ function normalizeDateOnly(value: string) {
422
+ if (!value) return "";
423
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
424
+
425
+ const parsedDate = new Date(value);
426
+ if (Number.isNaN(parsedDate.getTime())) return "";
427
+
428
+ const year = parsedDate.getFullYear();
429
+ const month = String(parsedDate.getMonth() + 1).padStart(2, "0");
430
+ const day = String(parsedDate.getDate()).padStart(2, "0");
431
+ return `${year}-${month}-${day}`;
432
+ }
433
+
407
434
  const selectedVisitorDataObject = computed(() => {
408
435
  return items.value.find((x: any) => x?._id === selectedVisitorId.value) || {};
409
436
  });
@@ -472,8 +499,8 @@ const {
472
499
  page: page.value,
473
500
  site: siteId,
474
501
  search: searchInput.value,
475
- dateTo: dateTo.value,
476
- dateFrom: dateFrom.value,
502
+ dateTo: normalizeDateOnly(dateTo.value),
503
+ dateFrom: normalizeDateOnly(dateFrom.value),
477
504
  type: (filterTypes.value.length === 0 && activeTab.value === 'registered' ? "contractor,delivery,walk-in,pick-up,drop-off,guest" : filterTypes.value.filter(Boolean).join(",")),
478
505
  checkedOut: displayNotCheckedOut.value ? false : undefined
479
506
  }
@@ -507,8 +534,8 @@ const {
507
534
 
508
535
  watch(getVisitorReq, (newData: any) => {
509
536
  if (newData) {
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" }] }];
537
+ items.value = newData.items ?? [];
538
+ // 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" }] }];
512
539
  pages.value = newData.pages ?? 0;
513
540
  pageRange.value = newData?.pageRange ?? "-- - -- of --";
514
541
  }
@@ -540,6 +567,8 @@ function formatValues(key: string, value: any) {
540
567
  return value;
541
568
  }
542
569
 
570
+
571
+
543
572
  function handleAddNew() {
544
573
  dialog.showSelection = true;
545
574
  activeVisitorFormType.value = null;
@@ -555,6 +584,15 @@ function handleUpdatePage(newPageNum: number) {
555
584
  page.value = newPageNum;
556
585
  }
557
586
 
587
+ function showAddPassKeyButton(item: any) {
588
+ const hasPasses = (item?.visitorPass?.length ?? 0) > 0;
589
+ const hasKeys = (item?.passKeys?.length ?? 0) > 0;
590
+
591
+ const isTypeWithPassKey = ["contractor", "guest", "walk-in"].includes(item?.type);
592
+
593
+ return !hasPasses && !hasKeys && isTypeWithPassKey && item?.status === "registered" && !item?.checkOut;
594
+ }
595
+
558
596
  function handleSelectVisitorType(type: TVisitorType) {
559
597
  dialog.showSelection = false;
560
598
  dialog.showForm = true;
@@ -607,6 +645,11 @@ function handleViewImage(imageId: string) {
607
645
  dialog.snapshotImage = true;
608
646
  }
609
647
 
648
+ function handleOpenAddPassKey(item: any) {
649
+ selectedVisitorId.value = item?._id;
650
+ dialog.addPassKey = true;
651
+ }
652
+
610
653
  function handleOpenRemarks(item: any, type: 'checkIn' | 'checkOut') {
611
654
  selectedVisitorId.value = item?._id;
612
655
  remarksType.value = type;
@@ -635,16 +678,18 @@ async function handleSaveRemarks() {
635
678
  }
636
679
  }
637
680
 
638
- async function handleUpdatePassKeys(){
681
+ async function handleUpdatePassKeys() {
639
682
  if (!selectedVisitorId.value) return;
640
683
  try {
641
684
  loading.updatingPassKeys = true;
642
685
  const payload: any = {};
643
686
  if (passReturnStatuses.value.length > 0) {
644
- payload.visitorPass = passReturnStatuses.value;
687
+ const passReturnStatusesPayload = passReturnStatuses.value.map(p => ({ keyId: p.keyId, status: p.status, remarks: p.remarks }));
688
+ payload.visitorPass = passReturnStatusesPayload;
645
689
  }
646
690
  if (keyReturnStatuses.value.length > 0) {
647
- payload.passKeys = keyReturnStatuses.value;
691
+ const keyReturnStatusesPayload = keyReturnStatuses.value.map(k => ({ keyId: k.keyId, status: k.status, remarks: k.remarks }));
692
+ payload.passKeys = keyReturnStatusesPayload;
648
693
  }
649
694
  await updateVisitor(selectedVisitorId.value, payload);
650
695
  showMessage("Pass/Key statuses updated successfully!", "info");
@@ -723,10 +768,14 @@ async function proceedCheckout() {
723
768
 
724
769
  try {
725
770
  loading.checkingOut = true;
771
+
772
+ const passReturnStatusesPayload = passReturnStatuses.value.map(p => ({ keyId: p.keyId, status: p.status, remarks: p.remarks }));
773
+ const keyReturnStatusesPayload = keyReturnStatuses.value.map(k => ({ keyId: k.keyId, status: k.status, remarks: k.remarks }));
774
+
726
775
  const res = await updateVisitor(userId as string, {
727
776
  checkOut: new Date().toISOString(),
728
- ...(passReturnStatuses.value.length > 0 ? { visitorPass: passReturnStatuses.value } : {}),
729
- ...(keyReturnStatuses.value.length > 0 ? { passKeys: keyReturnStatuses.value } : {}),
777
+ ...(passReturnStatuses.value.length > 0 ? { visitorPass: passReturnStatusesPayload } : {}),
778
+ ...(keyReturnStatuses.value.length > 0 ? { passKeys: keyReturnStatusesPayload } : {}),
730
779
  });
731
780
  if (res) {
732
781
  showMessage("Visitor successfully checked-out!", "info");
@@ -769,8 +818,13 @@ const updateRouteQuery = debounce(
769
818
  watch(
770
819
  [searchInput, dateFrom, dateTo, filterTypes, activeTab, displayNotCheckedOut],
771
820
  ([search, from, to, types, tab, checkedOut]) => {
821
+ const normalizedFrom = normalizeDateOnly(from);
822
+ const normalizedTo = normalizeDateOnly(to);
823
+ dateFrom.value = normalizedFrom;
824
+ dateTo.value = normalizedTo;
825
+
772
826
  // updateRouteQuery(search, from, to, types, status, checkedOut)
773
- updateRouteQuery({ search, from, to, types, tab, checkedOut })
827
+ updateRouteQuery({ search, from: normalizedFrom, to: normalizedTo, types, tab, checkedOut })
774
828
  getVisitorRefresh();
775
829
  },
776
830
  { deep: true }
@@ -781,8 +835,8 @@ watch(
781
835
  onMounted(() => {
782
836
  activeTab.value = (route.query.tab as string) || "unregistered"
783
837
  searchInput.value = (route.query.search as string) || "";
784
- dateFrom.value = (route.query.dateFrom as string) || "";
785
- dateTo.value = (route.query.dateTo as string) || "";
838
+ dateFrom.value = normalizeDateOnly((route.query.dateFrom as string) || "");
839
+ dateTo.value = normalizeDateOnly((route.query.dateTo as string) || "");
786
840
  filterTypes.value = ((route.query.type as string)?.split(",") || []).filter(Boolean) as TVisitorType[];
787
841
  displayNotCheckedOut.value = (route.query.checkedOut as string) == 'true' || false
788
842
  })
@@ -100,7 +100,7 @@ export default function useFeedback() {
100
100
  status,
101
101
  provider,
102
102
  service,
103
- ...(currentUser.value.type != "site" && {
103
+ ...(currentUser.value?.type != "site" && {
104
104
  userId,
105
105
  }),
106
106
  },
@@ -98,6 +98,19 @@ export default function () {
98
98
  });
99
99
  }
100
100
 
101
+ async function updateSiteInformation(
102
+ siteId: string,
103
+ payload: { bgImage: string; description: string; docs: { id: string; name: string }[] }
104
+ ) {
105
+ return await useNuxtApp().$api<Record<string, any>>(
106
+ `/api/sites/information/id/${siteId}`,
107
+ {
108
+ method: "PATCH",
109
+ body: payload,
110
+ }
111
+ );
112
+ }
113
+
101
114
  async function setSiteGuardPosts(siteId: string, value: number) {
102
115
  return await useNuxtApp().$api<Record<string, any>>(
103
116
  `/api/sites/guard-post/id/${siteId}`,
@@ -127,6 +140,33 @@ export default function () {
127
140
  );
128
141
  }
129
142
 
143
+ async function getOvernightParkingAvailabilityV2(
144
+ siteId: string,
145
+ userType = "site"
146
+ ) {
147
+ return await useNuxtApp().$api<Record<string, any>>(
148
+ `/api/overnight-parking-approval-settings2/v1`,
149
+ {
150
+ method: "PATCH",
151
+ body: JSON.stringify({ site: siteId, userType }),
152
+ }
153
+ );
154
+ }
155
+
156
+ async function createOrUpdateOvernightParkingAvailabilityV2(
157
+ siteId: string,
158
+ payload: Record<string, any>,
159
+ userType = "site"
160
+ ) {
161
+ return await useNuxtApp().$api<Record<string, any>>(
162
+ `/api/overnight-parking-approval-settings2/v1`,
163
+ {
164
+ method: "POST",
165
+ body: JSON.stringify({ site: siteId, userType, ...payload }),
166
+ }
167
+ );
168
+ }
169
+
130
170
  return {
131
171
  getSiteById,
132
172
  getSiteLevels,
@@ -138,7 +178,10 @@ export default function () {
138
178
  updateSiteCamera,
139
179
  deleteSiteCameraById,
140
180
  updateSitebyId,
181
+ updateSiteInformation,
141
182
  getOvernightParkingAvailability,
142
- updateOvernightParkingAvailability
183
+ updateOvernightParkingAvailability,
184
+ getOvernightParkingAvailabilityV2,
185
+ createOrUpdateOvernightParkingAvailabilityV2,
143
186
  };
144
187
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "1.11.18",
5
+ "version": "1.11.20",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {