@7365admin1/layer-common 1.11.2 → 1.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/components/AccessCardAddForm.vue +2 -2
- package/components/AreaMain.vue +4 -0
- 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/VisitorForm.vue +44 -14
- package/composables/useAccessManagement.ts +11 -0
- package/composables/useAreas.ts +3 -0
- 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
|
@@ -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;
|
|
@@ -289,6 +289,7 @@
|
|
|
289
289
|
|
|
290
290
|
<script setup lang="ts">
|
|
291
291
|
import useBuildingUnit from '../composables/useBuildingUnit';
|
|
292
|
+
import useWebUsb from '../composables/useWebUsb';
|
|
292
293
|
|
|
293
294
|
|
|
294
295
|
const prop = defineProps({
|
|
@@ -322,7 +323,8 @@ const { requiredRule, debounce, UTCToLocalTIme } = useUtils();
|
|
|
322
323
|
const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
|
|
323
324
|
const { createVisitor, typeFieldMap, contractorTypes, getVisitors, updateVisitor } = useVisitor();
|
|
324
325
|
const { getBySiteId: getEntryPassSettingsBySiteId } = useSiteEntryPassSettings();
|
|
325
|
-
const { createVisitorPass } = useAccessManagement();
|
|
326
|
+
const { createVisitorPass, signQr } = useAccessManagement();
|
|
327
|
+
const { testConnection } = useWebUsb();
|
|
326
328
|
const { findPersonByNRIC, findPersonByContact, searchCompanyList, findUsersByPlateNumber } = usePeople()
|
|
327
329
|
const { getById: getUnitDataById } = useBuildingUnit()
|
|
328
330
|
|
|
@@ -353,7 +355,7 @@ const visitor = reactive<Partial<TVisitorPayload>>({
|
|
|
353
355
|
members: [],
|
|
354
356
|
});
|
|
355
357
|
|
|
356
|
-
const passType = ref(
|
|
358
|
+
const passType = ref<string | null>(null);
|
|
357
359
|
const passQuantity = ref<number | null>(1);
|
|
358
360
|
const passCards = ref<{ _id: string; cardNo: string }[]>([]);
|
|
359
361
|
|
|
@@ -655,15 +657,9 @@ function handleAutofillDataViaVehicleNumber(item: TPeople) {
|
|
|
655
657
|
const {
|
|
656
658
|
data: entryPassSettings,
|
|
657
659
|
pending: entryPassSettingsPending,
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
return getEntryPassSettingsBySiteId(prop.site);
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
watch(contractorStep, (step) => {
|
|
665
|
-
if (step === 2) refreshEntryPassSettings();
|
|
666
|
-
});
|
|
660
|
+
} = useLazyAsyncData(`fetch-entrypass-settings-${prop.site}`, () =>
|
|
661
|
+
getEntryPassSettingsBySiteId(prop.site)
|
|
662
|
+
);
|
|
667
663
|
|
|
668
664
|
const {
|
|
669
665
|
data: siteData,
|
|
@@ -908,10 +904,13 @@ async function submit() {
|
|
|
908
904
|
const res = await createVisitor(payload);
|
|
909
905
|
if (res) {
|
|
910
906
|
if (prop.type === "contractor" && passType.value) {
|
|
911
|
-
const visitorId = res
|
|
912
|
-
const
|
|
907
|
+
const visitorId = res as unknown as string;
|
|
908
|
+
const rawUrl = entryPassSettings.value?.data?.settings?.url;
|
|
909
|
+
const { encrypted: acmUrl } = await $fetch<{ encrypted: string }>(
|
|
910
|
+
`/api/encrypt-acm-url${rawUrl ? `?url=${encodeURIComponent(rawUrl)}` : ""}`
|
|
911
|
+
);
|
|
913
912
|
if (visitorId && acmUrl) {
|
|
914
|
-
await createVisitorPass({
|
|
913
|
+
const passRes = await createVisitorPass({
|
|
915
914
|
site: prop.site,
|
|
916
915
|
unitId: visitor.unit!,
|
|
917
916
|
quantity: passQuantity.value ?? 1,
|
|
@@ -920,6 +919,37 @@ async function submit() {
|
|
|
920
919
|
acm_url: acmUrl,
|
|
921
920
|
visitorId,
|
|
922
921
|
});
|
|
922
|
+
|
|
923
|
+
if (passType.value === "QR") {
|
|
924
|
+
const accessCards: any[] = (passRes as any)?.accessCards ?? [];
|
|
925
|
+
const printer = entryPassSettings.value?.data?.settings?.printer;
|
|
926
|
+
if (printer?.vendorId && printer?.productId && accessCards.length) {
|
|
927
|
+
const vendorId = parseInt(printer.vendorId);
|
|
928
|
+
const productId = parseInt(printer.productId);
|
|
929
|
+
const companyName = visitor.company || "";
|
|
930
|
+
const blockLabel = blocksArray.value.find((b: any) => b.value === visitor.block)?.title || "";
|
|
931
|
+
const levelLabel = levelsArray.value.find((l: any) => l.value === visitor.level)?.title || "";
|
|
932
|
+
const unitLabel = unitsArray.value.find((u: any) => u.value === visitor.unit)?.title || "";
|
|
933
|
+
const address = [blockLabel, levelLabel, unitLabel].filter(Boolean).join("/");
|
|
934
|
+
|
|
935
|
+
await nextTick();
|
|
936
|
+
for (const qrCode of accessCards) {
|
|
937
|
+
const signed = await signQr({ cardId: qrCode._id, purpose: "vms" });
|
|
938
|
+
const qrData = signed?.data;
|
|
939
|
+
if (!qrData) continue;
|
|
940
|
+
await testConnection(
|
|
941
|
+
vendorId,
|
|
942
|
+
productId,
|
|
943
|
+
true,
|
|
944
|
+
qrData,
|
|
945
|
+
qrCode.accessLevel,
|
|
946
|
+
qrCode.liftAccessLevel,
|
|
947
|
+
companyName,
|
|
948
|
+
address,
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
923
953
|
}
|
|
924
954
|
}
|
|
925
955
|
if (createMore.value) {
|
|
@@ -279,6 +279,16 @@ export default function useAccessManagement() {
|
|
|
279
279
|
);
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
function signQr(payload: { cardId: string; purpose: string }) {
|
|
283
|
+
return useNuxtApp().$api<{ message: string; data: string }>(
|
|
284
|
+
`/api/access-management/sign-qr`,
|
|
285
|
+
{
|
|
286
|
+
method: "POST",
|
|
287
|
+
body: payload,
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
282
292
|
return {
|
|
283
293
|
getDoorAccessLevels,
|
|
284
294
|
getLiftAccessLevels,
|
|
@@ -299,5 +309,6 @@ export default function useAccessManagement() {
|
|
|
299
309
|
getAvailableContractorCards,
|
|
300
310
|
generateQrVms,
|
|
301
311
|
createVisitorPass,
|
|
312
|
+
signQr,
|
|
302
313
|
};
|
|
303
314
|
}
|