@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 +12 -0
- package/components/AddPassKeyToVisitor.vue +207 -0
- package/components/AreaChecklistHistoryMain.vue +29 -1
- package/components/BuildingUnitFormEdit.vue +41 -1
- package/components/CleaningScheduleMain.vue +2 -2
- package/components/Input/DateTimePicker.vue +45 -9
- package/components/Input/FileV2.vue +4 -3
- package/components/MemberInformation.vue +8 -2
- package/components/OvernightParkingAvailability.vue +19 -18
- package/components/ScheduleAreaMain.vue +6 -6
- package/components/VisitorManagement.vue +81 -27
- package/composables/useFeedback.ts +1 -1
- package/composables/useSiteSettings.ts +44 -1
- package/package.json +1 -1
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>("
|
|
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 === "
|
|
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="
|
|
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="
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
191
|
-
const name =
|
|
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="
|
|
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="
|
|
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 {
|
|
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
|
|
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
|
-
() =>
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
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>("
|
|
211
|
+
const statusFilter = ref<TScheduleAreaStatus>("all");
|
|
212
212
|
const statusOptions = [
|
|
213
|
-
{ title: "All", value: "
|
|
214
|
-
{ title: "Open", value: "
|
|
215
|
-
{ title: "Ongoing", value: "
|
|
216
|
-
{ title: "Completed", value: "
|
|
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 === "
|
|
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
|
-
<
|
|
24
|
-
<
|
|
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 }}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" :
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
729
|
-
...(keyReturnStatuses.value.length > 0 ? { passKeys:
|
|
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
|
})
|
|
@@ -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
|
}
|