@7365admin1/layer-common 1.11.1 → 1.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/components/AccessCardAddForm.vue +2 -2
- package/components/AreaMain.vue +4 -0
- package/components/BulletinBoardForm.vue +5 -4
- package/components/BulletinBoardManagement.vue +2 -1
- package/components/DashboardMain.vue +2 -2
- package/components/{SupplyManagementMain.vue → EquipmentManagementMain.vue} +107 -115
- package/components/ManageChecklistMain.vue +68 -13
- package/components/OvernightParkingAvailability.vue +267 -0
- package/components/OvernightParkingManagement.vue +130 -0
- package/components/Signature.vue +36 -28
- package/components/UnitMain.vue +4 -1
- package/components/VehicleForm.vue +5 -4
- package/components/VisitorForm.vue +44 -14
- package/composables/useAccessManagement.ts +11 -0
- package/composables/useAreas.ts +3 -0
- package/composables/useBulletin.ts +3 -3
- package/composables/useComment.ts +1 -1
- package/composables/useFacilityUtils.ts +140 -0
- package/composables/useFeedback.ts +1 -1
- package/composables/useLocalAuth.ts +1 -0
- package/composables/useSiteSettings.ts +21 -0
- package/composables/useUnits.ts +9 -6
- package/composables/useWorkOrder.ts +50 -18
- package/nuxt.config.ts +2 -1
- package/package.json +1 -1
- package/pages/require-organization-membership.vue +3 -3
- package/types/area.d.ts +1 -0
- package/types/dashboard.d.ts +4 -3
- package/types/overnight-parking.d.ts +35 -0
- package/types/user.d.ts +1 -1
- package/types/work-order.d.ts +1 -0
- package/utils/data.ts +17 -0
- package/components/EquipmentManagement.vue +0 -292
|
@@ -117,20 +117,45 @@
|
|
|
117
117
|
</v-col>
|
|
118
118
|
</v-row>
|
|
119
119
|
</template>
|
|
120
|
+
<template #group-header-chips> </template>
|
|
120
121
|
<template #group-header-append="{ group }">
|
|
121
|
-
<v-
|
|
122
|
-
v-if="group
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
122
|
+
<v-row no-gutters align="center" class="ga-2">
|
|
123
|
+
<v-col v-if="isGroupAnyApproved(group)" cols="auto">
|
|
124
|
+
<v-chip
|
|
125
|
+
size="x-small"
|
|
126
|
+
:color="isGroupComplete(group) ? 'success' : 'warning'"
|
|
127
|
+
variant="tonal"
|
|
128
|
+
:prepend-icon="
|
|
129
|
+
isGroupComplete(group)
|
|
130
|
+
? 'mdi-check-circle-outline'
|
|
131
|
+
: 'mdi-progress-clock'
|
|
132
|
+
"
|
|
133
|
+
class="text-none"
|
|
134
|
+
>
|
|
135
|
+
{{ isGroupComplete(group) ? "Completed" : "Ongoing" }}
|
|
136
|
+
<template v-if="getGroupCompletedByName(group)">
|
|
137
|
+
· {{ getGroupCompletedByName(group) }}
|
|
138
|
+
</template>
|
|
139
|
+
</v-chip>
|
|
140
|
+
</v-col>
|
|
141
|
+
<v-col
|
|
142
|
+
v-if="group.attachments && group.attachments.length > 0"
|
|
143
|
+
cols="auto"
|
|
144
|
+
>
|
|
145
|
+
<v-btn
|
|
146
|
+
size="x-small"
|
|
147
|
+
variant="tonal"
|
|
148
|
+
color="primary"
|
|
149
|
+
class="text-none"
|
|
150
|
+
prepend-icon="mdi-paperclip"
|
|
151
|
+
@click.stop="openAttachmentDialog(group.set, group.attachments)"
|
|
152
|
+
>
|
|
153
|
+
{{ group.attachments.length }} attachment{{
|
|
154
|
+
group.attachments.length > 1 ? "s" : ""
|
|
155
|
+
}}
|
|
156
|
+
</v-btn>
|
|
157
|
+
</v-col>
|
|
158
|
+
</v-row>
|
|
134
159
|
</template>
|
|
135
160
|
</TableHygiene>
|
|
136
161
|
</v-col>
|
|
@@ -328,6 +353,36 @@ function getKey(item: any, set?: number): string {
|
|
|
328
353
|
return `${item.unit}_${set ?? ""}`;
|
|
329
354
|
}
|
|
330
355
|
|
|
356
|
+
function isGroupComplete(group: { set: number; items: any[] }): boolean {
|
|
357
|
+
return (
|
|
358
|
+
group.items.length > 0 &&
|
|
359
|
+
group.items.every(
|
|
360
|
+
(item: any) =>
|
|
361
|
+
item.approve === true ||
|
|
362
|
+
activeActions[getKey(item, group.set)] === "approve"
|
|
363
|
+
)
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isGroupAnyApproved(group: { set: number; items: any[] }): boolean {
|
|
368
|
+
return group.items.some(
|
|
369
|
+
(item: any) =>
|
|
370
|
+
item.approve === true ||
|
|
371
|
+
activeActions[getKey(item, group.set)] === "approve"
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function getGroupCompletedByName(group: {
|
|
376
|
+
completedByName: string | null;
|
|
377
|
+
items: any[];
|
|
378
|
+
}): string | null {
|
|
379
|
+
return (
|
|
380
|
+
group.completedByName ??
|
|
381
|
+
group.items.find((item: any) => item.completedByName)?.completedByName ??
|
|
382
|
+
null
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
331
386
|
function isSetFullyApproved(setNumber: number): boolean {
|
|
332
387
|
const group = items.value.find((g: any) => g.set === setNumber);
|
|
333
388
|
if (!group) return false;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-form v-model="isInternalFormValid" ref="internalFormRef" :readonly="viewMode">
|
|
3
|
+
<v-row no-gutters>
|
|
4
|
+
<v-col cols="12">
|
|
5
|
+
<v-row no-gutters class="d-flex align-center ga-2 justify-space-between">
|
|
6
|
+
<InputLabel class="text-capitalize" title="Overnight Parking Approval Hours" />
|
|
7
|
+
<span class="d-flex ga-2 align-center">
|
|
8
|
+
<p>Auto Approve Parking?</p>
|
|
9
|
+
<v-switch v-model="autoApproveOvernightParking" hide-details color="success" size="large" />
|
|
10
|
+
</span>
|
|
11
|
+
</v-row>
|
|
12
|
+
<v-card class="mt-2">
|
|
13
|
+
<v-card-text>
|
|
14
|
+
<template v-for="(slot, index) in overnightParkingSlotsArray" :key="slot.day">
|
|
15
|
+
<v-row no-gutters class="mb-4">
|
|
16
|
+
<v-col cols="12" class="text-capitalize mb-2 text-subtitle-1 d-flex align-center justify-space-between">
|
|
17
|
+
<span class="font-weight-black">{{ slot.day }}</span>
|
|
18
|
+
<v-icon :icon="slot?.isEnabled ? 'mdi-check-circle' : 'mdi-check-circle-outline'" size="20"
|
|
19
|
+
:class="{ 'cursor-pointer': !viewMode }"
|
|
20
|
+
:color="slot.isEnabled ? 'green-darken-2' : 'black-lighten-1'"
|
|
21
|
+
@click="!viewMode && toggleEnabled(slot.day)" />
|
|
22
|
+
</v-col>
|
|
23
|
+
|
|
24
|
+
<v-col cols="6" class="pr-2">
|
|
25
|
+
<InputLabel title="Start" :required="slot.isEnabled" />
|
|
26
|
+
<v-select v-model="slot.startTime" :disabled="!slot.isEnabled" :items="allTimeSlots" item-value="time"
|
|
27
|
+
item-title="time" density="comfortable" :rules="slot.isEnabled ? [requiredRule] : []"
|
|
28
|
+
@update:model-value="slot.endTime = null" />
|
|
29
|
+
</v-col>
|
|
30
|
+
|
|
31
|
+
<v-col cols="6" class="pl-2">
|
|
32
|
+
<InputLabel title="End" :required="slot.isEnabled" />
|
|
33
|
+
<v-select v-model="slot.endTime" :disabled="!slot.isEnabled"
|
|
34
|
+
:items="generateTimeSlots(0.5, slot.startTime ?? undefined)" item-value="time" item-title="time"
|
|
35
|
+
density="comfortable" :rules="slot.isEnabled ? [requiredRule] : []" />
|
|
36
|
+
</v-col>
|
|
37
|
+
|
|
38
|
+
<v-col v-if="!viewMode" cols="12" class="mt-1">
|
|
39
|
+
<div class="d-flex align-center ga-1 cursor-pointer text-caption text-medium-emphasis"
|
|
40
|
+
@click="applyToOtherDays(slot.day)">
|
|
41
|
+
<v-icon icon="mdi-content-copy" size="14" />
|
|
42
|
+
<span>Apply to other days</span>
|
|
43
|
+
</div>
|
|
44
|
+
</v-col>
|
|
45
|
+
</v-row>
|
|
46
|
+
|
|
47
|
+
<v-divider v-if="index < overnightParkingSlotsArray.length - 1" class="mb-4" />
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<v-col cols="12" align="end">
|
|
51
|
+
<v-btn color="primary" class="text-none mt-2" size="large" :loading="loading.updating" variant="flat"
|
|
52
|
+
text="Save" :disabled="viewMode || !isValid" @click="handleSave('save')" />
|
|
53
|
+
</v-col>
|
|
54
|
+
</v-card-text>
|
|
55
|
+
</v-card>
|
|
56
|
+
</v-col>
|
|
57
|
+
</v-row>
|
|
58
|
+
</v-form>
|
|
59
|
+
|
|
60
|
+
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" style="z-index: 3000;" />
|
|
61
|
+
|
|
62
|
+
<v-dialog v-model="applyDialog.open" max-width="360" persistent>
|
|
63
|
+
<v-card>
|
|
64
|
+
<v-card-title class="text-subtitle-1 font-weight-bold pt-4 px-4">Apply to days</v-card-title>
|
|
65
|
+
<v-card-text class="px-4 pb-2">
|
|
66
|
+
<p class="text-caption text-medium-emphasis mb-3">
|
|
67
|
+
Select the days to apply <span class="font-weight-bold text-capitalize">{{ applyDialog.sourceDay }}</span>'s
|
|
68
|
+
hours to.
|
|
69
|
+
</p>
|
|
70
|
+
<v-checkbox v-for="day in applyDialog.targetDays" :key="day.day" v-model="day.selected" :label="day.day"
|
|
71
|
+
density="compact" hide-details class="text-capitalize" />
|
|
72
|
+
</v-card-text>
|
|
73
|
+
<v-card-actions class="px-4 pb-4">
|
|
74
|
+
<v-spacer />
|
|
75
|
+
<v-btn variant="text" @click="applyDialog.open = false">Cancel</v-btn>
|
|
76
|
+
<v-btn color="primary" variant="flat" :disabled="!applyDialog.targetDays.some(d => d.selected)"
|
|
77
|
+
@click="confirmApply">Apply</v-btn>
|
|
78
|
+
</v-card-actions>
|
|
79
|
+
</v-card>
|
|
80
|
+
</v-dialog>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<script setup lang="ts">
|
|
84
|
+
const props = defineProps({
|
|
85
|
+
viewMode: {
|
|
86
|
+
type: Boolean,
|
|
87
|
+
default: false,
|
|
88
|
+
required: false,
|
|
89
|
+
},
|
|
90
|
+
site: {
|
|
91
|
+
type: String,
|
|
92
|
+
default: "",
|
|
93
|
+
required: false,
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
const model = ref<TOvernightParkingAvailability>({
|
|
99
|
+
monday: { isEnabled: false, startTime: null, endTime: null },
|
|
100
|
+
tuesday: { isEnabled: false, startTime: null, endTime: null },
|
|
101
|
+
wednesday: { isEnabled: false, startTime: null, endTime: null },
|
|
102
|
+
thursday: { isEnabled: false, startTime: null, endTime: null },
|
|
103
|
+
friday: { isEnabled: false, startTime: null, endTime: null },
|
|
104
|
+
saturday: { isEnabled: false, startTime: null, endTime: null },
|
|
105
|
+
sunday: { isEnabled: false, startTime: null, endTime: null },
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
const autoApproveOvernightParking = ref(false)
|
|
110
|
+
|
|
111
|
+
const { requiredRule } = useUtils()
|
|
112
|
+
const { generateTimeSlots, generateTimeSlotsFromStart } = useFacilityUtils()
|
|
113
|
+
const { updateOvernightParkingAvailability, getOvernightParkingAvailability } = useSiteSettings()
|
|
114
|
+
|
|
115
|
+
type TDay = Exclude<keyof TOvernightParkingAvailability, 'autoApproveOvernightParking'>
|
|
116
|
+
type TOvernightParkingSlot = TOvernightParkingDay & { day: TDay }
|
|
117
|
+
|
|
118
|
+
const orderedDays: TDay[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
|
|
119
|
+
|
|
120
|
+
const allTimeSlots = computed(() => generateTimeSlotsFromStart(0.5))
|
|
121
|
+
|
|
122
|
+
const isInternalFormValid = ref(false)
|
|
123
|
+
const internalFormRef = ref<InstanceType<typeof import('vuetify/components').VForm>>()
|
|
124
|
+
|
|
125
|
+
const messageSnackbar = ref(false)
|
|
126
|
+
const message = ref('')
|
|
127
|
+
const messageColor = ref<'success' | 'error'>()
|
|
128
|
+
|
|
129
|
+
const loading = reactive({
|
|
130
|
+
updating: false
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const applyDialog = reactive({
|
|
134
|
+
open: false,
|
|
135
|
+
sourceDay: '' as TDay,
|
|
136
|
+
targetDays: [] as { day: TDay; selected: boolean }[],
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
const { data: availabilityDataReq, refresh: refreshAvailability } = await useLazyAsyncData<TOvernightParkingAvailability>(
|
|
142
|
+
`overnight-parking-availability-${props.site}`,
|
|
143
|
+
() => getOvernightParkingAvailability(props.site)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
watch(availabilityDataReq, (data) => {
|
|
147
|
+
if (!data) return
|
|
148
|
+
orderedDays.forEach((day) => {
|
|
149
|
+
const slot = overnightParkingSlotsArray.value.find((s) => s.day === day)
|
|
150
|
+
if (!slot) return
|
|
151
|
+
slot.isEnabled = data?.[day]?.isEnabled ?? false
|
|
152
|
+
slot.startTime = data?.[day]?.startTime ?? null
|
|
153
|
+
slot.endTime = data?.[day]?.endTime ?? null
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
autoApproveOvernightParking.value = data?.autoApproveOvernightParking === true
|
|
157
|
+
}, { immediate: true })
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
const overnightParkingSlotsArray = ref<TOvernightParkingSlot[]>(
|
|
161
|
+
orderedDays.map((day) => ({
|
|
162
|
+
day,
|
|
163
|
+
isEnabled: model.value?.[day]?.isEnabled ?? false,
|
|
164
|
+
startTime: model.value?.[day]?.startTime ?? null,
|
|
165
|
+
endTime: model.value?.[day]?.endTime ?? null,
|
|
166
|
+
}))
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
watch(overnightParkingSlotsArray, (slots) => {
|
|
170
|
+
const updated = {} as TOvernightParkingAvailability
|
|
171
|
+
slots.forEach((slot) => {
|
|
172
|
+
updated[slot.day] = {
|
|
173
|
+
isEnabled: slot.isEnabled,
|
|
174
|
+
startTime: slot.startTime,
|
|
175
|
+
endTime: slot.endTime,
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
model.value = updated
|
|
179
|
+
}, { deep: true })
|
|
180
|
+
|
|
181
|
+
function toggleEnabled(day: TDay) {
|
|
182
|
+
const slot = overnightParkingSlotsArray.value.find((s) => s.day === day)
|
|
183
|
+
if (!slot) return
|
|
184
|
+
internalFormRef.value?.resetValidation()
|
|
185
|
+
slot.isEnabled = !slot.isEnabled
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function applyToOtherDays(sourceDay: TDay) {
|
|
189
|
+
applyDialog.sourceDay = sourceDay
|
|
190
|
+
applyDialog.targetDays = orderedDays
|
|
191
|
+
.filter((d) => d !== sourceDay)
|
|
192
|
+
.map((d) => ({ day: d, selected: false }))
|
|
193
|
+
applyDialog.open = true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function confirmApply() {
|
|
197
|
+
const source = overnightParkingSlotsArray.value.find((s) => s.day === applyDialog.sourceDay)
|
|
198
|
+
if (!source) return
|
|
199
|
+
const selectedDays = applyDialog.targetDays.filter((d) => d.selected).map((d) => d.day)
|
|
200
|
+
overnightParkingSlotsArray.value.forEach((slot) => {
|
|
201
|
+
if (selectedDays.includes(slot.day)) {
|
|
202
|
+
slot.startTime = source.startTime
|
|
203
|
+
slot.endTime = source.endTime
|
|
204
|
+
slot.isEnabled = source.isEnabled
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
applyDialog.open = false
|
|
208
|
+
message.value = `Applied ${applyDialog.sourceDay}'s hours to ${selectedDays.join(', ')}.`
|
|
209
|
+
messageColor.value = 'success'
|
|
210
|
+
messageSnackbar.value = true
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const isValid = computed(() => {
|
|
214
|
+
const slots = overnightParkingSlotsArray.value
|
|
215
|
+
const enabledSlots = slots.filter((s) => s.isEnabled)
|
|
216
|
+
const isAllSlotsValid = slots.every((s) => {
|
|
217
|
+
if (!s.isEnabled) return true
|
|
218
|
+
return !!s.startTime?.trim() && !!s.endTime?.trim()
|
|
219
|
+
})
|
|
220
|
+
return isAllSlotsValid && enabledSlots.length > 0
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
watch(autoApproveOvernightParking, (newValue, oldValue) => {
|
|
224
|
+
if (oldValue === newValue) return
|
|
225
|
+
handleSave('toggle')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
function handleSave(action: 'toggle' | 'save') {
|
|
230
|
+
if (!isValid.value && action === 'save') {
|
|
231
|
+
message.value = 'Please fill in all required fields.'
|
|
232
|
+
messageColor.value = 'error'
|
|
233
|
+
messageSnackbar.value = true
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let payload = {}
|
|
238
|
+
|
|
239
|
+
if(action === 'save') {
|
|
240
|
+
payload = {
|
|
241
|
+
...model.value
|
|
242
|
+
}
|
|
243
|
+
} else if (action === 'toggle') {
|
|
244
|
+
payload = {
|
|
245
|
+
autoApproveOvernightParking: autoApproveOvernightParking.value,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
loading.updating = true
|
|
251
|
+
updateOvernightParkingAvailability(props.site, payload)
|
|
252
|
+
message.value = 'Overnight parking settings updated successfully.'
|
|
253
|
+
messageColor.value = 'success'
|
|
254
|
+
messageSnackbar.value = true
|
|
255
|
+
} catch (error) {
|
|
256
|
+
message.value = 'Failed to update overnight parking settings. Please try again.'
|
|
257
|
+
messageColor.value = 'error'
|
|
258
|
+
messageSnackbar.value = true
|
|
259
|
+
} finally {
|
|
260
|
+
loading.updating = false
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
</script>
|
|
266
|
+
|
|
267
|
+
<style scoped></style>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters>
|
|
3
|
+
<TableMain
|
|
4
|
+
:headers="headers"
|
|
5
|
+
:items="items"
|
|
6
|
+
:loading="loading"
|
|
7
|
+
:page="page"
|
|
8
|
+
:pages="pages"
|
|
9
|
+
:pageRange="pageRange"
|
|
10
|
+
:createLabel="createLabel"
|
|
11
|
+
:show-header="true"
|
|
12
|
+
:extension-height="extensionHeight"
|
|
13
|
+
:offset="offset"
|
|
14
|
+
v-model:search="searchInput"
|
|
15
|
+
@refresh="emits('refresh')"
|
|
16
|
+
@create="emits('create')"
|
|
17
|
+
@row-click="handleRowClick"
|
|
18
|
+
@update:page="handleUpdatePage"
|
|
19
|
+
>
|
|
20
|
+
|
|
21
|
+
<template #item.dateRequested="{ value }">
|
|
22
|
+
{{ formatDateDDMMYYYYLocal(value) }}
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<template #item.status="{ value }">
|
|
26
|
+
<v-chip
|
|
27
|
+
class="text-capitalize"
|
|
28
|
+
:color="formatStatus(value).color"
|
|
29
|
+
variant="flat"
|
|
30
|
+
pill
|
|
31
|
+
>
|
|
32
|
+
{{ formatStatus(value).label }}
|
|
33
|
+
</v-chip>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<template #item.start="{ value }">
|
|
37
|
+
{{ formatTimeValue(value) }}
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<template #item.end="{ value }">
|
|
41
|
+
{{ formatTimeValue(value) }}
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<template #item.action="{ item }">
|
|
45
|
+
<v-btn v-if="item.status === 'pending'" color="success" density="compact">Approve</v-btn>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<template v-if="$slots.footer" #footer>
|
|
49
|
+
<slot name="footer" />
|
|
50
|
+
</template>
|
|
51
|
+
</TableMain>
|
|
52
|
+
</v-row>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<script setup lang="ts">
|
|
56
|
+
import useUtils from "../composables/useUtils";
|
|
57
|
+
|
|
58
|
+
const props = defineProps({
|
|
59
|
+
site: {
|
|
60
|
+
type: String,
|
|
61
|
+
default: "",
|
|
62
|
+
required: false,
|
|
63
|
+
},
|
|
64
|
+
loading: {
|
|
65
|
+
type: Boolean,
|
|
66
|
+
default: false,
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const emits = defineEmits(["refresh", "create", "row-click", "update:page"]);
|
|
73
|
+
|
|
74
|
+
const searchInput = defineModel<string>("search", { default: "" });
|
|
75
|
+
|
|
76
|
+
const { standardFormatDate, formatDateDDMMYYYYLocal } = useUtils();
|
|
77
|
+
|
|
78
|
+
const items = ref<TOvernightParkingRequest[]>([])
|
|
79
|
+
const page = ref(1)
|
|
80
|
+
const pages = ref(1)
|
|
81
|
+
const pageRange = ref("-- - -- of --");
|
|
82
|
+
const extensionHeight = ref(0)
|
|
83
|
+
const offset = ref(0)
|
|
84
|
+
const createLabel = "New Overnight Parking Request"
|
|
85
|
+
|
|
86
|
+
const headers = [
|
|
87
|
+
{ title: "Name", value: "name" },
|
|
88
|
+
{ title: "Plate Number", value: "plateNumber" },
|
|
89
|
+
{ title: "Date Requested", value: "dateTime" },
|
|
90
|
+
{ title: "Invited By", value: "invitedBy" },
|
|
91
|
+
{ title: "Status", value: "status" },
|
|
92
|
+
{ title: "Action", value: "action" },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
function formatStatus(status: TOvernightParkingRequest["status"]) {
|
|
96
|
+
switch (String(status || "").toLowerCase()) {
|
|
97
|
+
case "approved":
|
|
98
|
+
return { color: "success", label: "Approved" };
|
|
99
|
+
case "expired":
|
|
100
|
+
return { color: "grey", label: "Expired" };
|
|
101
|
+
case "rejected":
|
|
102
|
+
return { color: "error", label: "Rejected" };
|
|
103
|
+
case "pending":
|
|
104
|
+
return { color: "warning", label: "Pending" };
|
|
105
|
+
default:
|
|
106
|
+
return { color: "secondary", label: status || "N/A" };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
function formatTimeValue(value: TOvernightParkingRequest["start"] | TOvernightParkingRequest["end"]) {
|
|
112
|
+
if (!value) return "N/A";
|
|
113
|
+
|
|
114
|
+
const date = new Date(value);
|
|
115
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
116
|
+
|
|
117
|
+
return date.toLocaleTimeString("en-US", {
|
|
118
|
+
hour: "2-digit",
|
|
119
|
+
minute: "2-digit",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function handleRowClick(data: { item?: TOvernightParkingRequest }) {
|
|
124
|
+
emits("row-click", data);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function handleUpdatePage(value: number) {
|
|
128
|
+
emits("update:page", value);
|
|
129
|
+
}
|
|
130
|
+
</script>
|
package/components/Signature.vue
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-dialog
|
|
2
|
+
<v-dialog v-model="dialogModel" max-width="700" persistent>
|
|
3
3
|
<v-card>
|
|
4
4
|
<v-toolbar>
|
|
5
|
-
<v-toolbar-title>Signature
|
|
5
|
+
<v-toolbar-title>Signature</v-toolbar-title>
|
|
6
6
|
<v-spacer />
|
|
7
|
-
<v-btn icon="mdi-close" @click="hideModal"
|
|
7
|
+
<v-btn icon="mdi-close" @click="hideModal" />
|
|
8
8
|
</v-toolbar>
|
|
9
|
+
|
|
9
10
|
<v-card-text>
|
|
10
11
|
<v-row no-gutters>
|
|
11
12
|
<v-col cols="12">
|
|
@@ -16,8 +17,8 @@
|
|
|
16
17
|
<NuxtSignaturePad
|
|
17
18
|
ref="signature"
|
|
18
19
|
:options="state.option"
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
width="100%"
|
|
21
|
+
height="400px"
|
|
21
22
|
:disabled="state.disabled"
|
|
22
23
|
class="border"
|
|
23
24
|
/>
|
|
@@ -29,10 +30,9 @@
|
|
|
29
30
|
<v-btn
|
|
30
31
|
text="clear"
|
|
31
32
|
color="warning"
|
|
32
|
-
type="submit"
|
|
33
33
|
class="my-4 w-100 rounded-lg"
|
|
34
34
|
height="40px"
|
|
35
|
-
@click="clear
|
|
35
|
+
@click="clear"
|
|
36
36
|
/>
|
|
37
37
|
</v-col>
|
|
38
38
|
|
|
@@ -40,12 +40,11 @@
|
|
|
40
40
|
<v-btn
|
|
41
41
|
text="submit"
|
|
42
42
|
color="#1867C0"
|
|
43
|
-
type="submit"
|
|
44
43
|
class="my-4 w-100 rounded-lg"
|
|
45
44
|
height="40px"
|
|
46
45
|
:loading="loading"
|
|
47
|
-
@click="submit"
|
|
48
46
|
:disabled="loading"
|
|
47
|
+
@click="submit"
|
|
49
48
|
/>
|
|
50
49
|
</v-col>
|
|
51
50
|
</v-row>
|
|
@@ -58,8 +57,6 @@
|
|
|
58
57
|
|
|
59
58
|
<script setup lang="ts">
|
|
60
59
|
const loading = ref(false);
|
|
61
|
-
// const { isValid } = useAudit();
|
|
62
|
-
// const { uiRequiredInput, uiSetSnackbar } = useUtils();
|
|
63
60
|
|
|
64
61
|
const message = ref("");
|
|
65
62
|
const messageColor = ref("");
|
|
@@ -71,7 +68,8 @@ function showMessage(msg: string, color: string) {
|
|
|
71
68
|
messageSnackbar.value = true;
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
const signature = ref(null);
|
|
71
|
+
const signature = ref<any>(null);
|
|
72
|
+
|
|
75
73
|
const state = ref({
|
|
76
74
|
count: 0,
|
|
77
75
|
option: {
|
|
@@ -85,40 +83,50 @@ const emit = defineEmits<{
|
|
|
85
83
|
(event: "onSubmit", payload: string): void;
|
|
86
84
|
(event: "onCloseDialog"): void;
|
|
87
85
|
}>();
|
|
88
|
-
|
|
86
|
+
|
|
87
|
+
const props = defineProps({
|
|
89
88
|
isShowDialog: {
|
|
90
89
|
type: Boolean,
|
|
91
90
|
default: false,
|
|
92
91
|
},
|
|
93
92
|
});
|
|
94
93
|
|
|
95
|
-
const
|
|
94
|
+
const dialogModel = computed({
|
|
95
|
+
get: () => props.isShowDialog,
|
|
96
|
+
set: (value: boolean) => {
|
|
97
|
+
if (!value) emit("onCloseDialog");
|
|
98
|
+
},
|
|
99
|
+
});
|
|
96
100
|
|
|
97
101
|
const hideModal = () => {
|
|
98
102
|
emit("onCloseDialog");
|
|
99
103
|
};
|
|
104
|
+
|
|
100
105
|
const file = ref<File | null>(null);
|
|
101
106
|
const { addFile } = useFile();
|
|
107
|
+
|
|
102
108
|
const submit = async () => {
|
|
103
109
|
try {
|
|
110
|
+
if (!signature.value) {
|
|
111
|
+
showMessage("Signature pad is not ready.", "error");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
104
115
|
loading.value = true;
|
|
105
|
-
const base64 = signature.value.saveSignature();
|
|
106
|
-
const blob = await (await fetch(base64)).blob();
|
|
107
116
|
|
|
108
|
-
|
|
117
|
+
const base64 = signature.value.saveSignature?.();
|
|
118
|
+
if (!base64) {
|
|
119
|
+
showMessage("Please provide a signature first.", "warning");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
109
122
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
name: file.value.name,
|
|
113
|
-
url: URL.createObjectURL(file.value),
|
|
114
|
-
progress: 0,
|
|
115
|
-
type: file.value.type,
|
|
116
|
-
};
|
|
123
|
+
const blob = await (await fetch(base64)).blob();
|
|
124
|
+
file.value = new File([blob], "signature.jpg", { type: "image/jpeg" });
|
|
117
125
|
|
|
118
|
-
const response = await addFile(
|
|
126
|
+
const response = await addFile(file.value);
|
|
119
127
|
|
|
120
|
-
if (response && response.
|
|
121
|
-
emit("onSubmit", response
|
|
128
|
+
if (response && response.id) {
|
|
129
|
+
emit("onSubmit", response.id);
|
|
122
130
|
}
|
|
123
131
|
} catch (error) {
|
|
124
132
|
showMessage("Error uploading signature. Please try again.", "error");
|
|
@@ -128,6 +136,6 @@ const submit = async () => {
|
|
|
128
136
|
};
|
|
129
137
|
|
|
130
138
|
const clear = () => {
|
|
131
|
-
signature.value
|
|
139
|
+
signature.value?.clearCanvas?.();
|
|
132
140
|
};
|
|
133
141
|
</script>
|
package/components/UnitMain.vue
CHANGED
|
@@ -211,10 +211,12 @@
|
|
|
211
211
|
<script setup lang="ts">
|
|
212
212
|
import { useUnitPermission } from "../composables/useUnitPermission";
|
|
213
213
|
import useUnits from "../composables/useUnits";
|
|
214
|
+
import useUtils from "../composables/useUtils";
|
|
214
215
|
|
|
215
216
|
const props = defineProps({
|
|
216
217
|
orgId: { type: String, default: "" },
|
|
217
218
|
site: { type: String, default: "" },
|
|
219
|
+
serviceType: { type: String, default: "", required: true },
|
|
218
220
|
});
|
|
219
221
|
|
|
220
222
|
const items = ref<Array<Record<string, any>>>([]);
|
|
@@ -252,6 +254,7 @@ const {
|
|
|
252
254
|
page: page.value,
|
|
253
255
|
search: searchInput.value,
|
|
254
256
|
site: props.site,
|
|
257
|
+
serviceType: props.serviceType,
|
|
255
258
|
}),
|
|
256
259
|
{
|
|
257
260
|
watch: [page, () => props.site],
|
|
@@ -389,7 +392,7 @@ async function _createUnit(name: string) {
|
|
|
389
392
|
if (!id) throw new Error("Invalid unit id for update");
|
|
390
393
|
response = await updateUnit(id, name);
|
|
391
394
|
} else {
|
|
392
|
-
response = await createUnit(name, props.site);
|
|
395
|
+
response = await createUnit(name, props.site, props.serviceType);
|
|
393
396
|
}
|
|
394
397
|
showMessage(response?.message, "success");
|
|
395
398
|
dialogShowForm.value = false;
|
|
@@ -622,15 +622,16 @@ function validStartDateRule(value: string) {
|
|
|
622
622
|
return true;
|
|
623
623
|
}
|
|
624
624
|
|
|
625
|
-
function validExpiryDateRule(
|
|
625
|
+
function validExpiryDateRule() {
|
|
626
626
|
const startDateISO = vehicle.start;
|
|
627
|
+
const endDateISO = vehicle.end;
|
|
627
628
|
|
|
628
|
-
if (!
|
|
629
|
+
if (!endDateISO && startDateISO) {
|
|
629
630
|
return 'Expiry Date is required';
|
|
630
631
|
}
|
|
631
632
|
|
|
632
|
-
if (
|
|
633
|
-
const expiry = new Date(
|
|
633
|
+
if (endDateISO && startDateISO) {
|
|
634
|
+
const expiry = new Date(endDateISO);
|
|
634
635
|
const start = new Date(startDateISO as string);
|
|
635
636
|
return expiry > start || 'Expiry date must be later than start date';
|
|
636
637
|
}
|