@7365admin1/layer-common 1.11.2 → 1.11.6

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.
@@ -0,0 +1,272 @@
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
+ const isHydrating = ref(true)
111
+
112
+ const { requiredRule } = useUtils()
113
+ const { generateTimeSlots, generateTimeSlotsFromStart } = useFacilityUtils()
114
+ const { updateOvernightParkingAvailability, getOvernightParkingAvailability } = useSiteSettings()
115
+
116
+ type TDay = Exclude<keyof TOvernightParkingAvailability, 'autoApproveOvernightParking'>
117
+ type TOvernightParkingSlot = TOvernightParkingDay & { day: TDay }
118
+
119
+ const orderedDays: TDay[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
120
+
121
+ const allTimeSlots = computed(() => generateTimeSlotsFromStart(0.5))
122
+
123
+ const isInternalFormValid = ref(false)
124
+ const internalFormRef = ref<InstanceType<typeof import('vuetify/components').VForm>>()
125
+
126
+ const messageSnackbar = ref(false)
127
+ const message = ref('')
128
+ const messageColor = ref<'success' | 'error'>()
129
+
130
+ const loading = reactive({
131
+ updating: false
132
+ })
133
+
134
+ const applyDialog = reactive({
135
+ open: false,
136
+ sourceDay: '' as TDay,
137
+ targetDays: [] as { day: TDay; selected: boolean }[],
138
+ })
139
+
140
+
141
+
142
+ const { data: availabilityDataReq, refresh: refreshAvailability } = await useLazyAsyncData<TOvernightParkingAvailability>(
143
+ `overnight-parking-availability-${props.site}`,
144
+ () => getOvernightParkingAvailability(props.site)
145
+ )
146
+
147
+ watch(availabilityDataReq, (data) => {
148
+ if (!data) return
149
+ orderedDays.forEach((day) => {
150
+ const slot = overnightParkingSlotsArray.value.find((s) => s.day === day)
151
+ if (!slot) return
152
+ slot.isEnabled = data?.[day]?.isEnabled ?? false
153
+ slot.startTime = data?.[day]?.startTime ?? null
154
+ slot.endTime = data?.[day]?.endTime ?? null
155
+ setTimeout(() => {
156
+ isHydrating.value = false
157
+ }, 500)
158
+ })
159
+
160
+ autoApproveOvernightParking.value = data?.autoApproveOvernightParking === true
161
+ }, { immediate: true })
162
+
163
+
164
+ const overnightParkingSlotsArray = ref<TOvernightParkingSlot[]>(
165
+ orderedDays.map((day) => ({
166
+ day,
167
+ isEnabled: model.value?.[day]?.isEnabled ?? false,
168
+ startTime: model.value?.[day]?.startTime ?? null,
169
+ endTime: model.value?.[day]?.endTime ?? null,
170
+ }))
171
+ )
172
+
173
+ watch(overnightParkingSlotsArray, (slots) => {
174
+ const updated = {} as TOvernightParkingAvailability
175
+ slots.forEach((slot) => {
176
+ updated[slot.day] = {
177
+ isEnabled: slot.isEnabled,
178
+ startTime: slot.startTime,
179
+ endTime: slot.endTime,
180
+ }
181
+ })
182
+ model.value = updated
183
+ }, { deep: true })
184
+
185
+ function toggleEnabled(day: TDay) {
186
+ const slot = overnightParkingSlotsArray.value.find((s) => s.day === day)
187
+ if (!slot) return
188
+ internalFormRef.value?.resetValidation()
189
+ slot.isEnabled = !slot.isEnabled
190
+ }
191
+
192
+ function applyToOtherDays(sourceDay: TDay) {
193
+ applyDialog.sourceDay = sourceDay
194
+ applyDialog.targetDays = orderedDays
195
+ .filter((d) => d !== sourceDay)
196
+ .map((d) => ({ day: d, selected: false }))
197
+ applyDialog.open = true
198
+ }
199
+
200
+ function confirmApply() {
201
+ const source = overnightParkingSlotsArray.value.find((s) => s.day === applyDialog.sourceDay)
202
+ if (!source) return
203
+ const selectedDays = applyDialog.targetDays.filter((d) => d.selected).map((d) => d.day)
204
+ overnightParkingSlotsArray.value.forEach((slot) => {
205
+ if (selectedDays.includes(slot.day)) {
206
+ slot.startTime = source.startTime
207
+ slot.endTime = source.endTime
208
+ slot.isEnabled = source.isEnabled
209
+ }
210
+ })
211
+ applyDialog.open = false
212
+ message.value = `Applied ${applyDialog.sourceDay}'s hours to ${selectedDays.join(', ')}.`
213
+ messageColor.value = 'success'
214
+ messageSnackbar.value = true
215
+ }
216
+
217
+ const isValid = computed(() => {
218
+ const slots = overnightParkingSlotsArray.value
219
+ const enabledSlots = slots.filter((s) => s.isEnabled)
220
+ const isAllSlotsValid = slots.every((s) => {
221
+ if (!s.isEnabled) return true
222
+ return !!s.startTime?.trim() && !!s.endTime?.trim()
223
+ })
224
+ return isAllSlotsValid && enabledSlots.length > 0
225
+ })
226
+
227
+ watch(autoApproveOvernightParking, (newValue, oldValue) => {
228
+ if (oldValue === newValue || isHydrating.value) return
229
+ handleSave('toggle')
230
+ }, { immediate: false })
231
+
232
+
233
+ async function handleSave(action: 'toggle' | 'save') {
234
+ if (!isValid.value && action === 'save') {
235
+ message.value = 'Please fill in all required fields.'
236
+ messageColor.value = 'error'
237
+ messageSnackbar.value = true
238
+ return
239
+ }
240
+
241
+ let payload = {}
242
+
243
+ if(action === 'save') {
244
+ payload = {
245
+ ...model.value
246
+ }
247
+ } else if (action === 'toggle') {
248
+ payload = {
249
+ autoApproveOvernightParking: autoApproveOvernightParking.value,
250
+ }
251
+ }
252
+
253
+ try {
254
+ loading.updating = true
255
+ await updateOvernightParkingAvailability(props.site, payload)
256
+ message.value = 'Overnight parking settings updated successfully.'
257
+ messageColor.value = 'success'
258
+ messageSnackbar.value = true
259
+ refreshAvailability()
260
+ } catch (error) {
261
+ message.value = 'Failed to update overnight parking settings. Please try again.'
262
+ messageColor.value = 'error'
263
+ messageSnackbar.value = true
264
+ } finally {
265
+ loading.updating = false
266
+ }
267
+
268
+ }
269
+
270
+ </script>
271
+
272
+ <style scoped></style>
@@ -0,0 +1,240 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <TableMain :headers="headers" :items="items" :loading="loading" :page="page" :pages="pages" :pageRange="pageRange"
4
+ :createLabel="createLabel" :show-header="true" :extension-height="extensionHeight" :offset="offset"
5
+ v-model:search="searchInput" @refresh="emits('refresh')" @create="emits('create')" @row-click="handleRowClick"
6
+ @update:page="handleUpdatePage">
7
+
8
+ <template #extension>
9
+ <v-row no-gutters class="w-100 d-flex align-center justify-end ga-4 pa-2">
10
+ <v-select v-model="statusFilter" label="Filter by types" item-title="label" item-value="value"
11
+ :items="filterOptions" density="compact" clearable max-width="200" hide-details>
12
+ </v-select>
13
+ </v-row>
14
+ </template>
15
+
16
+ <template v-slot:item.name="{ item }">
17
+ <span class="d-flex align-center ga-2">
18
+ <span>
19
+ <AvatarMain :name="item?.name" :size="20" :id="item?._id" />
20
+ </span>
21
+ <span class="text-capitalize">{{ item?.name }}</span>
22
+ </span>
23
+ </template>
24
+
25
+ <template v-slot:item.emailContact="{ item }">
26
+ <span class="d-flex align-center ga-2">
27
+ <v-icon icon="mdi-email" size="15" />
28
+ <span class="text-capitalize">{{ item?.email || "N/A" }}</span>
29
+ </span>
30
+ <span class="d-flex align-center ga-2">
31
+ <v-icon icon="mdi-phone" size="15" />
32
+ <span class="text-capitalize">{{ item?.contact || "N/A" }}</span>
33
+ </span>
34
+ </template>
35
+
36
+ <template #item.createdAt="{ value }">
37
+ {{ formatDateDDMMYYYYLocal(value) }}
38
+ </template>
39
+
40
+ <template #item.status="{ value }">
41
+ <v-chip class="text-capitalize" :color="formatStatus(value).color" density="compact" variant="flat" pill>
42
+ {{ formatStatus(value).label }}
43
+ </v-chip>
44
+ </template>
45
+
46
+ <template #item.action="{ item }">
47
+ <v-menu v-if="item?.status === 'pending'" activator="parent" transition="scale-transition">
48
+ <template #activator="{ props }">
49
+ <v-btn variant="flat" density="compact" icon="mdi-dots-vertical" v-bind="props"></v-btn>
50
+ </template>
51
+ <v-list>
52
+ <v-list-item @click="openActionDialog(item, 'approved')">
53
+ <v-list-item-title>
54
+ <v-icon class="text-success mr-2 text-subtitle-1">mdi-check</v-icon>Approve</v-list-item-title>
55
+ </v-list-item>
56
+ <v-list-item @click="openActionDialog(item, 'rejected')">
57
+ <v-list-item-title>
58
+ <v-icon class="text-error mr-2 text-subtitle-1">mdi-close</v-icon>Reject</v-list-item-title>
59
+ </v-list-item>
60
+ </v-list>
61
+ </v-menu>
62
+ </template>
63
+
64
+ <template v-if="$slots.footer" #footer>
65
+ <slot name="footer" />
66
+ </template>
67
+ </TableMain>
68
+
69
+ <v-dialog v-model="actionDialog.open" max-width="520" persistent>
70
+ <v-card>
71
+ <v-card-title class="text-subtitle-1 font-weight-bold pt-4 px-4">
72
+ {{ actionDialog.action === 'approved' ? 'Approve Request' : 'Reject Request' }}
73
+ </v-card-title>
74
+ <v-card-text class="px-4 pb-2">
75
+ <p class="text-caption text-medium-emphasis mb-3">
76
+ Add remarks before {{ actionDialog.action === 'approved' ? 'approving' : 'rejecting' }} this request.
77
+ </p>
78
+ <v-textarea v-model="remarks" label="Remarks" rows="3" auto-grow :rules="[requiredRule]"
79
+ :disabled="actionLoading" />
80
+ </v-card-text>
81
+ <v-card-actions class="px-4 pb-4">
82
+ <v-spacer />
83
+ <v-btn variant="text" :disabled="actionLoading" @click="closeActionDialog">Cancel</v-btn>
84
+ <v-btn color="primary" variant="flat" :loading="actionLoading" :disabled="!remarks.trim()"
85
+ @click="confirmAction">
86
+ Confirm
87
+ </v-btn>
88
+ </v-card-actions>
89
+ </v-card>
90
+ </v-dialog>
91
+ </v-row>
92
+ </template>
93
+
94
+ <script setup lang="ts">
95
+ import useOvernightParking from "../composables/useOvernightParking";
96
+ import useUtils from "../composables/useUtils";
97
+
98
+ const props = defineProps({
99
+ site: {
100
+ type: String,
101
+ default: "",
102
+ required: false,
103
+ },
104
+ loading: {
105
+ type: Boolean,
106
+ default: false,
107
+ },
108
+
109
+
110
+ });
111
+
112
+ const emits = defineEmits(["refresh", "create", "row-click", "update:page"]);
113
+
114
+ const searchInput = defineModel<string>("search", { default: "" });
115
+
116
+ const { requiredRule, standardFormatDate, formatDateDDMMYYYYLocal } = useUtils();
117
+ const { getOvernightParkingRequests, updateOvernightParkingRequest } = useOvernightParking();
118
+
119
+ const items = ref<TOvernightParkingRequest[]>([])
120
+ const page = ref(1)
121
+ const pages = ref(1)
122
+ const pageRange = ref("-- - -- of --");
123
+ const extensionHeight = ref(0)
124
+ const offset = ref(0)
125
+ const createLabel = "New Overnight Parking Request"
126
+
127
+ const remarks = ref('')
128
+ const actionLoading = ref(false)
129
+ const actionDialog = reactive<{
130
+ open: boolean
131
+ requestId: string
132
+ action: 'approved' | 'rejected'
133
+ }>({
134
+ open: false,
135
+ requestId: '',
136
+ action: 'approved',
137
+ })
138
+
139
+ const statusFilter = ref<TOvernightParkingRequestStatus>("pending")
140
+
141
+ const filterOptions: { label: string; value: TOvernightParkingRequestStatus }[] = [
142
+ { label: "Pending", value: "pending" },
143
+ { label: "Approved", value: "approved" },
144
+ { label: "Rejected", value: "rejected" },
145
+ { label: "Expired", value: "expired" },
146
+ ]
147
+
148
+ const headers = [
149
+ { title: "Name", value: "name" },
150
+ { title: "Email/Contact", value: "emailContact" },
151
+ { title: "Date Requested", value: "createdAt" },
152
+ { title: "Invited By", value: "invitedBy" },
153
+ { title: "Status", value: "status" },
154
+ { title: "Action", value: "action" },
155
+ ];
156
+
157
+
158
+ function formatStatus(status: TOvernightParkingRequest["status"]) {
159
+ switch (String(status || "").toLowerCase()) {
160
+ case "approved":
161
+ return { color: "success", label: "Approved" };
162
+ case "expired":
163
+ return { color: "grey", label: "Expired" };
164
+ case "rejected":
165
+ return { color: "error", label: "Rejected" };
166
+ case "pending":
167
+ return { color: "warning", label: "Pending" };
168
+ default:
169
+ return { color: "secondary", label: status || "N/A" };
170
+ }
171
+ }
172
+
173
+ const { data: overnightParkingRequests, pending: isPendingOvernightParkingRequests, refresh: refreshOvernightParkingRequests } = await useLazyAsyncData<TOvernightParkingRequest[]>(() => getOvernightParkingRequests({ site: props.site, status: statusFilter.value }), { immediate: true })
174
+
175
+
176
+ watch(overnightParkingRequests, (data: any) => {
177
+ // items.value = Array.isArray(data?.items)
178
+ // ? data.items
179
+ // : Array.isArray(data)
180
+ // ? data
181
+ // : []
182
+ pages.value = data?.pages ?? 0;
183
+ pageRange.value = data?.pageRange ?? "-- - -- of --"
184
+ }, { immediate: true })
185
+
186
+
187
+ // function formatTimeValue(value: TOvernightParkingRequest["start"] | TOvernightParkingRequest["end"]) {
188
+ // if (!value) return "N/A";
189
+
190
+ // const date = new Date(value);
191
+ // if (Number.isNaN(date.getTime())) return String(value);
192
+
193
+ // return date.toLocaleTimeString("en-US", {
194
+ // hour: "2-digit",
195
+ // minute: "2-digit",
196
+ // });
197
+ // }
198
+
199
+ function handleRowClick(data: { item?: TOvernightParkingRequest }) {
200
+ emits("row-click", data);
201
+ }
202
+
203
+ function handleUpdatePage(value: number) {
204
+ emits("update:page", value);
205
+ }
206
+
207
+ function openActionDialog(item: TOvernightParkingRequest, action: 'approved' | 'rejected') {
208
+ actionDialog.requestId = item?._id || ''
209
+ actionDialog.action = action
210
+ remarks.value = ''
211
+ actionDialog.open = true
212
+ }
213
+
214
+ function closeActionDialog() {
215
+ if (actionLoading.value) return
216
+ actionDialog.open = false
217
+ }
218
+
219
+ async function confirmAction() {
220
+ if (!actionDialog.requestId || !remarks.value.trim()) return
221
+
222
+ try {
223
+ actionLoading.value = true
224
+ await updateOvernightParkingRequest(actionDialog.requestId, {
225
+ status: actionDialog.action,
226
+ remarks: remarks.value.trim(),
227
+ })
228
+ actionDialog.open = false
229
+ await refreshOvernightParkingRequests()
230
+ refreshOvernightParkingRequests()
231
+ } finally {
232
+ actionLoading.value = false
233
+ }
234
+ }
235
+
236
+ watch([statusFilter], ([newStatus], [oldStatus]) => {
237
+ if (newStatus === oldStatus) return
238
+ refreshOvernightParkingRequests()
239
+ }, { immediate: false })
240
+ </script>
@@ -1,11 +1,12 @@
1
1
  <template>
2
- <v-dialog max-width="700" v-model="isDialogVisible" persistent>
2
+ <v-dialog v-model="dialogModel" max-width="700" persistent>
3
3
  <v-card>
4
4
  <v-toolbar>
5
- <v-toolbar-title>Signature </v-toolbar-title>
5
+ <v-toolbar-title>Signature</v-toolbar-title>
6
6
  <v-spacer />
7
- <v-btn icon="mdi-close" @click="hideModal"></v-btn>
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
- :width="'100%'"
20
- :height="'400px'"
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
- let props = defineProps({
86
+
87
+ const props = defineProps({
89
88
  isShowDialog: {
90
89
  type: Boolean,
91
90
  default: false,
92
91
  },
93
92
  });
94
93
 
95
- const isDialogVisible = computed(() => props.isShowDialog);
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
- file.value = new File([blob], "signature.jpg", { type: "image/jpeg" });
117
+ const base64 = signature.value.saveSignature?.();
118
+ if (!base64) {
119
+ showMessage("Please provide a signature first.", "warning");
120
+ return;
121
+ }
109
122
 
110
- const uploadItem = {
111
- data: file.value,
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(uploadItem.data);
126
+ const response = await addFile(file.value);
119
127
 
120
- if (response && response.length > 0) {
121
- emit("onSubmit", response[0]._id);
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.clearCanvas();
139
+ signature.value?.clearCanvas?.();
132
140
  };
133
141
  </script>
@@ -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;