@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/components/AccessCardAddForm.vue +2 -2
  3. package/components/AreaMain.vue +4 -0
  4. package/components/BulletinBoardForm.vue +5 -4
  5. package/components/BulletinBoardManagement.vue +2 -1
  6. package/components/DashboardMain.vue +2 -2
  7. package/components/{SupplyManagementMain.vue → EquipmentManagementMain.vue} +107 -115
  8. package/components/ManageChecklistMain.vue +68 -13
  9. package/components/OvernightParkingAvailability.vue +267 -0
  10. package/components/OvernightParkingManagement.vue +130 -0
  11. package/components/Signature.vue +36 -28
  12. package/components/UnitMain.vue +4 -1
  13. package/components/VehicleForm.vue +5 -4
  14. package/components/VisitorForm.vue +44 -14
  15. package/composables/useAccessManagement.ts +11 -0
  16. package/composables/useAreas.ts +3 -0
  17. package/composables/useBulletin.ts +3 -3
  18. package/composables/useComment.ts +1 -1
  19. package/composables/useFacilityUtils.ts +140 -0
  20. package/composables/useFeedback.ts +1 -1
  21. package/composables/useLocalAuth.ts +1 -0
  22. package/composables/useSiteSettings.ts +21 -0
  23. package/composables/useUnits.ts +9 -6
  24. package/composables/useWorkOrder.ts +50 -18
  25. package/nuxt.config.ts +2 -1
  26. package/package.json +1 -1
  27. package/pages/require-organization-membership.vue +3 -3
  28. package/types/area.d.ts +1 -0
  29. package/types/dashboard.d.ts +4 -3
  30. package/types/overnight-parking.d.ts +35 -0
  31. package/types/user.d.ts +1 -1
  32. package/types/work-order.d.ts +1 -0
  33. package/utils/data.ts +17 -0
  34. 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-btn
122
- v-if="group.attachments && group.attachments.length > 0"
123
- size="x-small"
124
- variant="tonal"
125
- color="primary"
126
- class="text-none"
127
- prepend-icon="mdi-paperclip"
128
- @click.stop="openAttachmentDialog(group.set, group.attachments)"
129
- >
130
- {{ group.attachments.length }} attachment{{
131
- group.attachments.length > 1 ? "s" : ""
132
- }}
133
- </v-btn>
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>
@@ -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;
@@ -622,15 +622,16 @@ function validStartDateRule(value: string) {
622
622
  return true;
623
623
  }
624
624
 
625
- function validExpiryDateRule(value: string) {
625
+ function validExpiryDateRule() {
626
626
  const startDateISO = vehicle.start;
627
+ const endDateISO = vehicle.end;
627
628
 
628
- if (!value && startDateISO) {
629
+ if (!endDateISO && startDateISO) {
629
630
  return 'Expiry Date is required';
630
631
  }
631
632
 
632
- if (value && startDateISO) {
633
- const expiry = new Date(value);
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
  }