@iservice365/layer-common 1.1.0 → 1.3.0

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 (47) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/components/CameraForm.vue +264 -0
  3. package/components/CameraMain.vue +352 -0
  4. package/components/Card/DeleteConfirmation.vue +51 -0
  5. package/components/Card/MemberInfoSummary.vue +44 -0
  6. package/components/Chat/Information.vue +28 -11
  7. package/components/Dialog/DeleteConfirmation.vue +51 -0
  8. package/components/Dialog/UpdateMoreAction.vue +99 -0
  9. package/components/Feedback/Form.vue +17 -3
  10. package/components/FeedbackDetail.vue +0 -11
  11. package/components/FeedbackMain.vue +21 -11
  12. package/components/Input/DateTimePicker.vue +10 -5
  13. package/components/Input/File.vue +1 -1
  14. package/components/Input/FileV2.vue +106 -63
  15. package/components/Input/InputPhoneNumberV2.vue +114 -0
  16. package/components/Input/NRICNumber.vue +41 -0
  17. package/components/Input/PhoneNumber.vue +1 -0
  18. package/components/Input/VehicleNumber.vue +49 -0
  19. package/components/NumberSettingField.vue +107 -0
  20. package/components/PeopleForm.vue +452 -0
  21. package/components/TableMain.vue +2 -1
  22. package/components/VehicleUpdateMoreAction.vue +84 -0
  23. package/components/VisitorForm.vue +712 -0
  24. package/components/VisitorFormSelection.vue +53 -0
  25. package/components/VisitorManagement.vue +569 -0
  26. package/components/WorkOrder/Create.vue +87 -49
  27. package/components/WorkOrder/Main.vue +17 -12
  28. package/composables/useBuilding.ts +250 -0
  29. package/composables/useBuildingUnit.ts +116 -0
  30. package/composables/useFeedback.ts +3 -3
  31. package/composables/useFile.ts +7 -9
  32. package/composables/useLocal.ts +67 -0
  33. package/composables/usePeople.ts +48 -0
  34. package/composables/useSecurityUtils.ts +18 -0
  35. package/composables/useSiteSettings.ts +111 -0
  36. package/composables/useUtils.ts +30 -1
  37. package/composables/useVisitor.ts +80 -0
  38. package/composables/useWorkOrder.ts +3 -3
  39. package/package.json +1 -1
  40. package/plugins/vuetify.ts +6 -1
  41. package/types/building.d.ts +19 -0
  42. package/types/camera.d.ts +31 -0
  43. package/types/people.d.ts +22 -0
  44. package/types/select.d.ts +4 -0
  45. package/types/site.d.ts +10 -7
  46. package/types/visitor.d.ts +42 -0
  47. package/utils/phoneMasks.ts +1703 -0
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <v-card flat border="sm grey-lighten-2" color="grey-lighten-3" class="w-100">
3
+ <v-card-text>
4
+ <v-row no-gutters>
5
+ <v-col cols="10" no-gutters>
6
+ <v-row no-gutters class="ga-1">
7
+ <template v-for="(label, key) in displayFields" :key="key">
8
+ <v-col v-if="member[key]" cols="12">
9
+ <span class="d-flex ga-3 align-center"><strong>{{ label }}:</strong> {{ member[key]
10
+ }}</span>
11
+ </v-col>
12
+ </template>
13
+ </v-row>
14
+ </v-col>
15
+ <v-col cols="2" class="d-flex align-center justify-center">
16
+ <v-btn icon="mdi-trash-outline" size="small" color="red" @click="emit('remove')" />
17
+ </v-col>
18
+ </v-row>
19
+ </v-card-text>
20
+ </v-card>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+
25
+ const prop = defineProps({
26
+ member: {
27
+ type: Object as PropType<TMemberInfo>,
28
+ required: true
29
+ }
30
+ })
31
+
32
+ const emit = defineEmits(['remove'])
33
+
34
+
35
+ const displayFields: Record<keyof TMemberInfo, string> = {
36
+ name: "Name",
37
+ nric: "NRIC",
38
+ visitorPass: "Pass",
39
+ contact: "Contact"
40
+ } as const
41
+
42
+ </script>
43
+
44
+ <style scoped></style>
@@ -62,7 +62,7 @@
62
62
  <v-row dense class="my-1 pr-4">
63
63
  <v-col cols="6" class="py-1"><strong>Category:</strong></v-col>
64
64
  <v-col cols="6" class="py-1 text-right text-capitalize">{{
65
- item.categoryInfo
65
+ item.category
66
66
  }}</v-col>
67
67
  </v-row>
68
68
 
@@ -186,7 +186,7 @@
186
186
 
187
187
  <WorkOrderCreate
188
188
  v-model="showCreateDialog"
189
- :created-from="'feedback'"
189
+ created-from="feedback"
190
190
  :work-order="_workOrder"
191
191
  @update:work-order="(val: TWorkOrderCreate) => (_workOrder = val)"
192
192
  :is-edit-mode="isEditMode"
@@ -269,20 +269,37 @@ const _workOrder = ref<TWorkOrderCreate>({
269
269
  const serviceProviders = ref<
270
270
  Array<{ title: string; value: string; subtitle: string }>
271
271
  >([]);
272
- const { getAll: getAllServiceProvider } = useServiceProvider();
273
-
274
- const { data: getAllReq } = useLazyAsyncData("get-all-service-providers", () =>
275
- getAllServiceProvider({
276
- siteId: useRoute().params.site as string,
277
- })
272
+ // const { getAll: getAllServiceProvider } = useServiceProvider();
273
+
274
+ // const { data: getAllReq } = useLazyAsyncData("get-all-service-providers", () =>
275
+ // getAllServiceProvider({
276
+ // siteId: useRoute().params.site as string,
277
+ // })
278
+ // );
279
+
280
+ // watchEffect(() => {
281
+ // if (getAllReq.value) {
282
+ // serviceProviders.value = getAllReq.value.items.map((i: any) => ({
283
+ // title: i.nature.replace(/_/g, " "),
284
+ // value: i.serviceProviderOrgId,
285
+ // subtitle: i.name,
286
+ // }));
287
+ // }
288
+ // });
289
+
290
+ const { getBySiteAsServiceProvider } = useCustomerSite();
291
+
292
+ const { data: getAllReq } = useLazyAsyncData(
293
+ "get-by-site-as-service-provider",
294
+ () => getBySiteAsServiceProvider(useRoute().params.site as string)
278
295
  );
279
296
 
280
297
  watchEffect(() => {
281
298
  if (getAllReq.value) {
282
- serviceProviders.value = getAllReq.value.items.map((i: any) => ({
299
+ serviceProviders.value = getAllReq.value.map((i: any) => ({
283
300
  title: i.nature.replace(/_/g, " "),
284
- value: i.serviceProviderOrgId,
285
- subtitle: i.name,
301
+ subtitle: i.title,
302
+ value: i._id.org,
286
303
  }));
287
304
  }
288
305
  });
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <v-card width="100%" :disabled="loading" :loading="loading">
3
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-5 px-7 text-center">
4
+ <span> {{ promptTitle }}</span>
5
+
6
+ <span v-if="message" class="text-error mt-2">
7
+ {{ message }} Do you want to delete anyway?
8
+ </span>
9
+ </v-card-text>
10
+
11
+ <v-toolbar class="pa-0" density="compact">
12
+ <v-row no-gutters>
13
+ <v-col cols="6" class="pa-0">
14
+ <v-btn block variant="text" class="text-none" size="large" tile @click="emit('close')"
15
+ height="48">
16
+ Cancel
17
+ </v-btn>
18
+ </v-col>
19
+
20
+ <v-col cols="6" class="pa-0">
21
+ <v-btn block tile variant="flat" class="text-none" size="large" height="48" color="black"
22
+ @click="emit('delete')">
23
+ Delete
24
+ </v-btn>
25
+ </v-col>
26
+ </v-row>
27
+ </v-toolbar>
28
+ </v-card>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ const props = defineProps({
33
+ message: {
34
+ type: String,
35
+ default: ""
36
+ },
37
+ promptTitle: {
38
+ type: String,
39
+ default: "Are you sure want to delete this? "
40
+ },
41
+ loading: {
42
+ type: Boolean,
43
+ default: false
44
+ }
45
+ })
46
+
47
+ const emit = defineEmits(["close", "delete"])
48
+
49
+ </script>
50
+
51
+ <style scoped></style>
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <v-card width="100%">
3
+ <v-toolbar>
4
+ <template v-if="$slots.header">
5
+ <slot name="header" :title="title" :onClose="onClose" />
6
+ </template>
7
+ <template v-else>
8
+ <v-row no-gutters class="fill-height px-6" align="center">
9
+ <span class="font-weight-bold text-h5 text-capitalize">{{
10
+ title
11
+ }}</span>
12
+ </v-row>
13
+ </template>
14
+ </v-toolbar>
15
+
16
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0">
17
+ <slot name="content" />
18
+ </v-card-text>
19
+
20
+ <v-toolbar class="pa-0" density="compact">
21
+ <v-row no-gutters>
22
+ <v-col cols="6" class="pa-0">
23
+ <v-btn
24
+ block
25
+ variant="text"
26
+ class="text-none"
27
+ size="large"
28
+ @click="emit('close')"
29
+ height="48"
30
+ >
31
+ Close
32
+ </v-btn>
33
+ </v-col>
34
+
35
+ <v-col cols="6" class="pa-0" >
36
+ <v-menu contained>
37
+ <template #activator="{ props }">
38
+ <v-btn
39
+ block
40
+ variant="flat"
41
+ color="black"
42
+ class="text-none"
43
+ height="48"
44
+ v-bind="props"
45
+ tile
46
+ >
47
+ More actions
48
+ </v-btn>
49
+ </template>
50
+
51
+ <v-list class="pa-0">
52
+ <v-list-item v-if="canUpdate" @click="emit('edit')">
53
+ <v-list-item-title class="text-subtitle-2">
54
+ {{ editButtonLabel }}
55
+ </v-list-item-title>
56
+ </v-list-item>
57
+
58
+ <v-list-item v-if="canDelete" @click="emit('delete')" class="text-red">
59
+ <v-list-item-title class="text-subtitle-2">
60
+ {{ deleteButtonLabel }}
61
+ </v-list-item-title>
62
+ </v-list-item>
63
+ </v-list>
64
+ </v-menu>
65
+ </v-col>
66
+ </v-row>
67
+ </v-toolbar>
68
+ </v-card>
69
+ </template>
70
+
71
+ <script setup lang="ts">
72
+ const prop = defineProps({
73
+ canUpdate: {
74
+ type: Boolean,
75
+ default: true,
76
+ },
77
+ canDelete: {
78
+ type: Boolean,
79
+ default: true,
80
+ },
81
+ editButtonLabel: {
82
+ type: String,
83
+ default: "Edit",
84
+ },
85
+ deleteButtonLabel: {
86
+ type: String,
87
+ default: "Delete",
88
+ },
89
+ title: {
90
+ type: String,
91
+ default: "Details",
92
+ },
93
+ });
94
+
95
+ const emit = defineEmits(["close", "edit", "delete"]);
96
+ const { canUpdate, editButtonLabel, deleteButtonLabel, title } = prop;
97
+ </script>
98
+
99
+ <style scoped></style>
@@ -15,13 +15,18 @@
15
15
  @errored="onImageError"
16
16
  />
17
17
 
18
- <v-text-field
18
+ <v-autocomplete
19
+ v-model="localFeedback.subject"
20
+ :items="subjects"
21
+ item-title="title"
22
+ item-value="value"
23
+ item-props
19
24
  label="Subject"
20
25
  variant="outlined"
21
26
  density="compact"
22
- v-model="localFeedback.subject"
23
27
  class="mb-2"
24
- dense
28
+ clearable
29
+ :custom-filter="customFilter"
25
30
  />
26
31
 
27
32
  <v-autocomplete
@@ -133,4 +138,13 @@ function closeDialog() {
133
138
  function submitFeedback() {
134
139
  emit("submit", { ...localFeedback.value });
135
140
  }
141
+
142
+ const { subjects } = useLocal();
143
+
144
+ const customFilter = (value: string, query: string, item: any) => {
145
+ const title = item?.raw?.title?.toLowerCase() || "";
146
+ const subtitle = item?.raw?.subtitle?.toLowerCase() || "";
147
+ const search = query.toLowerCase();
148
+ return title.includes(search) || subtitle.includes(search);
149
+ };
136
150
  </script>
@@ -1,16 +1,6 @@
1
1
  <template>
2
2
  <div class="feedback-detail-wrapper">
3
3
  <v-row no-gutters class="fill-height">
4
- <!-- <v-col cols="12" xl="3" lg="4" md="4" class="fill-height">
5
- <div class="panel-container border-e">
6
- <ChatNavigation
7
- :title="'Feedbacks'"
8
- :items="items"
9
- @select="handleSelectFeedback"
10
- @search="_getFeedbacks"
11
- />
12
- </div>
13
- </v-col> -->
14
4
 
15
5
  <v-col cols="12" xl="7" lg="7" md="7" class="fill-height">
16
6
  <div class="panel-container border-e">
@@ -179,7 +169,6 @@ const {
179
169
  } = useLazyAsyncData("get-all-feedbacks", () =>
180
170
  _getFeedbacks({
181
171
  page: page.value,
182
- organization: route.params.org as string,
183
172
  site: route.params.site as string,
184
173
  })
185
174
  );
@@ -164,6 +164,10 @@ const props = defineProps({
164
164
  type: Boolean,
165
165
  default: false,
166
166
  },
167
+ category: {
168
+ type: String,
169
+ default: "",
170
+ },
167
171
  });
168
172
 
169
173
  const isEditMode = ref(false);
@@ -179,20 +183,27 @@ const { getUserFromCookie } = useLocal();
179
183
  const serviceProviders = ref<
180
184
  Array<{ title: string; value: string; subtitle: string }>
181
185
  >([]);
182
- const { getAll: getAllServiceProvider } = useServiceProvider();
186
+ // const { getAll: getAllServiceProvider } = useServiceProvider();
183
187
 
184
- const { data: getAllReq } = useLazyAsyncData("get-all-service-providers", () =>
185
- getAllServiceProvider({
186
- siteId: useRoute().params.site as string,
187
- })
188
+ // const { data: getAllReq } = useLazyAsyncData("get-all-service-providers", () =>
189
+ // getAllServiceProvider({
190
+ // siteId: useRoute().params.site as string,
191
+ // })
192
+ // );
193
+
194
+ const { getBySiteAsServiceProvider } = useCustomerSite();
195
+
196
+ const { data: getAllReq } = useLazyAsyncData(
197
+ "get-by-site-as-service-provider",
198
+ () => getBySiteAsServiceProvider(useRoute().params.site as string)
188
199
  );
189
200
 
190
201
  watchEffect(() => {
191
202
  if (getAllReq.value) {
192
- serviceProviders.value = getAllReq.value.items.map((i: any) => ({
203
+ serviceProviders.value = getAllReq.value.map((i: any) => ({
193
204
  title: i.nature.replace(/_/g, " "),
194
- value: i.serviceProviderOrgId,
195
- subtitle: i.name,
205
+ subtitle: i.title,
206
+ value: i.nature,
196
207
  }));
197
208
  }
198
209
  });
@@ -244,11 +255,11 @@ const {
244
255
  data: getAllFeedbackReq,
245
256
  status: getAllReqStatus,
246
257
  refresh: getAllReqRefresh,
247
- } = useLazyAsyncData("get-all-feedbacks", () =>
258
+ } = useLazyAsyncData("get-all-feedbacks-" + props.category, () =>
248
259
  _getFeedbacks({
249
260
  page: page.value,
250
- organization: route.params.org as string,
251
261
  site: route.params.site as string,
262
+ category: props.category,
252
263
  })
253
264
  );
254
265
 
@@ -268,7 +279,6 @@ async function updatePage(pageVal: any) {
268
279
  page.value = pageVal;
269
280
  const response = await _getFeedbacks({
270
281
  page: page.value,
271
- organization: route.params.org as string,
272
282
  site: route.params.site as string,
273
283
  });
274
284
  if (response) {
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="d-flex flex-column">
3
3
  <v-text-field v-bind="$attrs" ref="dateTimePickerRef" :model-value="dateTimeFormattedReadOnly"
4
- placeholder="MM/DD/YYYY, HH:MM AM/PM" :rules="rules" style="z-index: 10" @click="openDatePicker">
4
+ :placeholder="placeholder" :rules="rules" style="z-index: 10" @click="openDatePicker">
5
5
  <template #append-inner>
6
6
  <v-icon icon="mdi-calendar" @click.stop="openDatePicker" />
7
7
  </template>
@@ -20,6 +20,10 @@ const prop = defineProps({
20
20
  type: Array as PropType<Array<any>>,
21
21
  default: () => []
22
22
  },
23
+ placeholder: {
24
+ type: String,
25
+ default: 'MM/DD/YYYY, HH:MM AM/PM'
26
+ }
23
27
  })
24
28
 
25
29
  const { formatDateISO8601 } = useUtils()
@@ -93,15 +97,16 @@ watch(dateTime, (dateVal) => {
93
97
 
94
98
  }, { immediate: false })
95
99
 
100
+ watch(dateTimeUTC, () => {
101
+ handleInitialDate()
102
+ }, { immediate: true})
96
103
 
97
- onMounted(async () => {
98
104
 
99
- handleInitialDate()
105
+ onMounted(async () => {
100
106
  await nextTick()
101
107
  isInitialLoad.value = false
102
-
103
108
  // Wait until Vuetify renders its internal input
104
- const nativeInput = dateTimePickerRef.value?.$el?.querySelector('input')
109
+ const nativeInput = (dateTimePickerRef.value as any)?.$el?.querySelector('input')
105
110
  if (nativeInput) {
106
111
  nativeInput.addEventListener('click', (e: MouseEvent) => {
107
112
  e.stopPropagation()
@@ -128,7 +128,7 @@ const props = defineProps<{
128
128
  attachments: string[];
129
129
  erroredImages?: string[];
130
130
  maxFiles?: number;
131
- createdFrom: string;
131
+ createdFrom?: string;
132
132
  }>();
133
133
 
134
134
  const emit = defineEmits<{
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <v-row no-gutters class="w-100 pb-5" @click="resetErrorMessage">
3
3
  <v-file-upload v-model="uploadFiles" density="compact" @update:model-value="handleUpdateValue"
4
- :loading="processing" :disabled="processing" :height="height" title="Upload Images" accept="image/*"
4
+ :loading="processing" :disabled="processing" :height="height" :title="title" :accept="accept"
5
5
  name="upload_images" class="text-caption w-100" clearable :multiple="multiple">
6
6
  <template v-slot:item="{ props: itemProps, file }">
7
7
  <v-file-upload-item v-bind="itemProps" lines="one" nav>
@@ -22,93 +22,136 @@
22
22
  </template>
23
23
 
24
24
  <script setup lang="ts">
25
-
26
- const prop = defineProps({
27
- height: {
28
- type: Number || String,
29
- default: 68
30
- },
31
- multiple: {
32
- type: Boolean,
33
- default: false
34
- },
35
- maxLength: {
36
- type: Number,
37
- default: 10
38
- }
39
-
40
-
25
+ import { nextTick, ref, onMounted, watch } from 'vue'
26
+
27
+ const props = defineProps({
28
+ height: {
29
+ type: [Number, String],
30
+ default: 68,
31
+ },
32
+ multiple: {
33
+ type: Boolean,
34
+ default: false,
35
+ },
36
+ maxLength: {
37
+ type: Number,
38
+ default: 10,
39
+ },
40
+ title: {
41
+ type: String,
42
+ default: 'Upload Images'
43
+ },
44
+ accept: {
45
+ type: String,
46
+ default: "image/*"
47
+ }
41
48
  })
42
49
 
43
50
  const { addFile, deleteFile, getFileUrl, urlToFile } = useFile()
44
51
 
52
+ // The parent v-model binding
53
+ const idsArray = defineModel<string[]>({ default: [] })
54
+
45
55
  const uploadFiles = ref<File[]>([])
46
- const filesCollection = defineModel<{ file: File, id: string }[]>({required: true, default: []}) // files collection array
47
- const showCameraDialog = ref(false)
48
- const errorMessage = ref('');
56
+ const filesCollection = ref<{ file: File; id: string }[]>([])
57
+ const errorMessage = ref('')
49
58
  const processing = ref(false)
50
59
 
51
- const video = ref<HTMLVideoElement>()
52
- const canvas = ref<HTMLCanvasElement>()
53
- const cameraFacingMode = ref('environment')
54
-
60
+ function fileKey(f: File) {
61
+ return `${f.name}_${f.size}_${f.lastModified}`
62
+ }
55
63
 
56
64
  async function handleRemove(removedFile: File) {
57
- const fileKey = (f: File) => `${f.name}_${f.size}_${f.lastModified}`
58
- const arr = filesCollection.value
59
- const fileId = arr.find(item => fileKey(item.file) === fileKey(removedFile))?.id
65
+ const key = fileKey(removedFile)
66
+ const arr = [...filesCollection.value]
67
+
68
+ const removedItem = arr.find((item) => fileKey(item.file) === key)
69
+ if (!removedItem) return
70
+
71
+ filesCollection.value = arr.filter((item) => item.id !== removedItem.id)
72
+ uploadFiles.value = uploadFiles.value.filter((f) => fileKey(f) !== key)
73
+
74
+
75
+ idsArray.value = filesCollection.value.map((x) => x.id)
60
76
 
61
- uploadFiles.value = uploadFiles.value.filter(f => fileKey(f) !== fileKey(removedFile))
62
- filesCollection.value = arr.filter(item => item.id !== fileId)
63
77
  }
64
78
 
65
79
  async function handleUpdateValue(value: File[]) {
66
- await nextTick()
67
- const max = prop.maxLength
68
- const existingLength = filesCollection.value.length
69
- errorMessage.value = ''
70
- if ((existingLength + value.length) > max) {
71
- value = value.slice(0, (max - existingLength))
72
- errorMessage.value = `Max value of allowed image is ${max}`
73
- }
74
- uploadFiles.value = []
75
- processing.value = true
76
- // Determine which files are newly added or removed
77
- const fileKey = (f: File) => `${f.name}_${f.size}_${f.lastModified}`
80
+ await nextTick()
81
+ const max = props.maxLength
82
+ const existingLength = filesCollection.value.length
83
+ errorMessage.value = ''
78
84
 
79
- const arr = filesCollection.value
80
- const collectionKeys = arr.map(x => fileKey(x.file))
81
- const addedFiles = value.filter(f => !collectionKeys.includes(fileKey(f)))
85
+ if (existingLength + value.length > max) {
86
+ value = value.slice(0, max - existingLength)
87
+ errorMessage.value = `Max allowed images is ${max}`
88
+ }
82
89
 
83
- // Upload new files
84
- processing.value = true
90
+ const collectionKeys = filesCollection.value.map((x) => fileKey(x.file))
91
+ const addedFiles = value.filter((f) => !collectionKeys.includes(fileKey(f)))
92
+
93
+ processing.value = true
94
+ try {
85
95
  for (const file of addedFiles) {
86
- try {
87
- const res = await addFile(file) // expected to return { id, url } or similar
88
- if (res?.id) {
89
- filesCollection.value.push({ file, id: res?.id })
90
- }
91
- } catch (err) {
92
- console.error("Upload failed", err)
93
- errorMessage.value = `Failed to upload ${file.name}`
94
- } finally {
95
- processing.value = false
96
- uploadFiles.value = filesCollection.value.map(x => x.file)
97
- }
96
+ const res = await addFile(file) // should return { id, url }
97
+ if (res?.id) {
98
+ filesCollection.value.push({ file, id: res.id })
99
+ }
98
100
  }
99
101
 
102
+ uploadFiles.value = filesCollection.value.map((x) => x.file)
103
+ idsArray.value = filesCollection.value.map((x) => x.id)
104
+ } catch (err) {
105
+ console.error('Upload failed', err)
106
+ errorMessage.value = 'Failed to upload some files.'
107
+ } finally {
108
+ processing.value = false
109
+ }
110
+ }
111
+
112
+
113
+ async function loadFilesFromIds(ids: string[]) {
114
+ const result: { file: File; id: string }[] = []
115
+ for (const id of ids) {
116
+ try {
117
+ const url = await getFileUrl(id)
118
+ const name = decodeURIComponent(url.split('/').pop() || `file_${id}`)
119
+ const file = await urlToFile(url, name)
120
+ result.push({ file, id })
121
+ } catch (err) {
122
+ console.warn('Failed to load file from ID:', id, err)
123
+ }
124
+ }
125
+ return result
100
126
  }
101
127
 
102
- function resetErrorMessage(){
103
- errorMessage.value = '';
104
-
128
+
129
+ function resetErrorMessage() {
130
+ errorMessage.value = ''
105
131
  }
106
132
 
107
- onMounted(() => {
108
- filesCollection.value = []
133
+
134
+ onMounted(async () => {
135
+ if (idsArray.value.length > 0) {
136
+ processing.value = true
137
+ const loaded = await loadFilesFromIds(idsArray.value)
138
+ filesCollection.value = loaded
139
+ uploadFiles.value = loaded.map((x) => x.file)
140
+ processing.value = false
141
+ }
109
142
  })
143
+
144
+
145
+ watch(
146
+ filesCollection,
147
+ (newVal) => {
148
+ idsArray.value = newVal.map((x) => x.id)
149
+ },
150
+ { deep: true }
151
+ )
110
152
  </script>
111
153
 
154
+
112
155
  <style scoped>
113
156
  * :deep(.v-file-upload-title) {
114
157
  font-size: 1rem;