@iservice365/layer-common 0.1.0 → 0.2.1

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 (93) hide show
  1. package/.playground/app.vue +7 -2
  2. package/.playground/pages/feedback.vue +30 -0
  3. package/CHANGELOG.md +12 -0
  4. package/components/Chat/Bubbles.vue +53 -0
  5. package/components/Chat/Information.vue +187 -0
  6. package/components/Chat/ListCard.vue +62 -0
  7. package/components/Chat/Message.vue +149 -0
  8. package/components/Chat/Navigation.vue +150 -0
  9. package/components/ConfirmDialog.vue +66 -0
  10. package/components/Container/Standard.vue +33 -0
  11. package/components/Feedback/Form.vue +136 -0
  12. package/components/FeedbackDetail.vue +465 -0
  13. package/components/FeedbackMain.vue +454 -0
  14. package/components/FormDialog.vue +65 -0
  15. package/components/Input/File.vue +203 -0
  16. package/components/Input/ListGroupSelection.vue +96 -0
  17. package/components/Input/NewDate.vue +123 -0
  18. package/components/Input/Number.vue +124 -0
  19. package/components/InvitationMain.vue +284 -0
  20. package/components/Layout/Header.vue +14 -4
  21. package/components/ListView.vue +87 -0
  22. package/components/MemberMain.vue +459 -0
  23. package/components/RolePermissionFormCreate.vue +161 -0
  24. package/components/RolePermissionFormPreviewUpdate.vue +183 -0
  25. package/components/RolePermissionMain.vue +361 -0
  26. package/components/ServiceProviderFormCreate.vue +154 -0
  27. package/components/ServiceProviderMain.vue +195 -0
  28. package/components/SignaturePad.vue +73 -0
  29. package/components/SpecificAttr.vue +53 -0
  30. package/components/SwitchContext.vue +26 -5
  31. package/components/TableList.vue +150 -0
  32. package/components/TableListSecondary.vue +164 -0
  33. package/components/WorkOrder/Create.vue +197 -0
  34. package/components/WorkOrder/ListView.vue +96 -0
  35. package/components/WorkOrder/Main.vue +308 -0
  36. package/components/Workorder.vue +1 -0
  37. package/composables/useAddress.ts +107 -0
  38. package/composables/useCommonPermission.ts +130 -0
  39. package/composables/useCustomer.ts +113 -0
  40. package/composables/useFeedback.ts +117 -0
  41. package/composables/useFile.ts +40 -0
  42. package/composables/useInvoice.ts +18 -0
  43. package/composables/useLocal.ts +24 -4
  44. package/composables/useLocalAuth.ts +62 -20
  45. package/composables/useLocalSetup.ts +13 -0
  46. package/composables/useMember.ts +111 -0
  47. package/composables/useOrg.ts +76 -92
  48. package/composables/usePaymentMethod.ts +101 -0
  49. package/composables/usePrice.ts +15 -0
  50. package/composables/usePromoCode.ts +36 -0
  51. package/composables/useRole.ts +38 -7
  52. package/composables/useServiceProvider.ts +218 -0
  53. package/composables/useSite.ts +108 -0
  54. package/composables/useSubscription.ts +149 -0
  55. package/composables/useUser.ts +38 -14
  56. package/composables/useUtils.ts +218 -6
  57. package/composables/useVerification.ts +33 -0
  58. package/composables/useWorkOrder.ts +68 -0
  59. package/middleware/01.auth.ts +11 -0
  60. package/middleware/02.org.ts +18 -0
  61. package/middleware/03.customer.ts +13 -0
  62. package/middleware/member.ts +4 -0
  63. package/nuxt.config.ts +3 -1
  64. package/package.json +7 -3
  65. package/pages/index.vue +3 -0
  66. package/pages/payment-method-linked.vue +31 -0
  67. package/pages/require-customer.vue +56 -0
  68. package/pages/require-organization-membership.vue +47 -0
  69. package/pages/unauthorized.vue +29 -0
  70. package/plugins/API.ts +2 -25
  71. package/plugins/iconify.client.ts +5 -0
  72. package/plugins/secure-member.client.ts +54 -0
  73. package/plugins/vuetify.ts +2 -0
  74. package/public/bg-camera.jpg +0 -0
  75. package/public/bg-city.jpg +0 -0
  76. package/public/bg-condo.jpg +0 -0
  77. package/public/images/icons/delete-icon.png +0 -0
  78. package/public/sprite.svg +1 -0
  79. package/types/address.d.ts +13 -0
  80. package/types/customer.d.ts +15 -0
  81. package/types/feedback.d.ts +63 -0
  82. package/types/local.d.ts +47 -38
  83. package/types/member.d.ts +21 -0
  84. package/types/org.d.ts +13 -0
  85. package/types/permission.d.ts +1 -0
  86. package/types/price.d.ts +17 -0
  87. package/types/promo-code.d.ts +19 -0
  88. package/types/service-provider.d.ts +15 -0
  89. package/types/site.d.ts +13 -0
  90. package/types/subscription.d.ts +23 -0
  91. package/types/user.d.ts +19 -0
  92. package/types/verification.d.ts +20 -0
  93. package/types/work-order.d.ts +40 -0
@@ -0,0 +1,454 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <v-col cols="12" class="mb-2">
4
+ <v-row no-gutters>
5
+ <v-btn
6
+ class="text-none text-capitalize"
7
+ rounded="pill"
8
+ variant="tonal"
9
+ @click="showCreateDialog = true"
10
+ size="large"
11
+ v-if="canCreateFeedback"
12
+ >
13
+ Create Feedback
14
+ </v-btn>
15
+ </v-row>
16
+ </v-col>
17
+
18
+ <v-col cols="12">
19
+ <ListView
20
+ :headers="headers"
21
+ :items="items"
22
+ :pages="pages"
23
+ :page-range="pageRange"
24
+ :loading="loading"
25
+ :height="'calc(100vh - 175px)'"
26
+ v-model:page="page"
27
+ :selected="selected"
28
+ @update:value="getAllReqRefresh"
29
+ @update:selected="onSelectedUpdate"
30
+ @click:create="showCreateDialog = true"
31
+ :viewPage="{ name: 'org-site-feedbacks-id' }"
32
+ :clickable-rows="true"
33
+ :length="pages"
34
+ >
35
+ <template #title>
36
+ <span class="text-h6 font-weight-regular">Feedbacks</span>
37
+ </template>
38
+
39
+ <template #category="{ item }">
40
+ <span class="text-capitalize">{{ item.category }}</span>
41
+ </template>
42
+
43
+ <template #status="{ item }">
44
+ <v-chip
45
+ :color="getColorStatus(item.status || 'No Status')"
46
+ text-color="white"
47
+ variant="flat"
48
+ >
49
+ {{ item.status || "No Status" }}
50
+ </v-chip>
51
+ </template>
52
+
53
+ <template #action-table="{ item }">
54
+ <v-menu
55
+ v-model="item.menuOpen"
56
+ :close-on-content-click="false"
57
+ offset-y
58
+ width="150"
59
+ >
60
+ <template v-slot:activator="{ props }">
61
+ <v-icon v-bind="props">mdi-dots-horizontal-circle-outline</v-icon>
62
+ </template>
63
+ <v-list>
64
+ <v-list-item @click="onViewFeedback(item)">View</v-list-item>
65
+ <v-list-item @click="editFeedback(item)">Edit</v-list-item>
66
+ <v-list-item
67
+ @click="confirmDeleteFeedback(item)"
68
+ v-if="canDeleteFeedback"
69
+ >Delete</v-list-item
70
+ >
71
+ </v-list>
72
+ </v-menu>
73
+ </template>
74
+ </ListView>
75
+ </v-col>
76
+ </v-row>
77
+
78
+ <FeedbackForm
79
+ v-model="showCreateDialog"
80
+ :feedback="_feedback"
81
+ :is-edit-mode="isEditMode"
82
+ :loading="submitting"
83
+ :categories="serviceProviders"
84
+ :errored-images="erroredImages"
85
+ :max-files="5"
86
+ :message-fn="showMessage"
87
+ @close="handleCloseDialog"
88
+ @submit="submitFeedback"
89
+ @file-added="handleFileAdded"
90
+ @file-deleted="deleteFile"
91
+ />
92
+
93
+ <ConfirmDialog
94
+ v-model="showDeleteDialog"
95
+ :loading="submitting"
96
+ @submit="submitDelete"
97
+ :image-src="'/images/icons/delete-icon.png'"
98
+ >
99
+ <template #image>
100
+ <v-img
101
+ height="120"
102
+ src="/images/icons/delete-icon.png"
103
+ alt="Delete Icon"
104
+ contain
105
+ />
106
+ </template>
107
+
108
+ <template #title> Are you sure you want to delete this feedback? </template>
109
+
110
+ <template #footer>
111
+ <v-btn
112
+ color="primary-button"
113
+ variant="flat"
114
+ class="font-weight-bold py-5 d-flex align-center justify-center"
115
+ @click="submitDelete"
116
+ :loading="submitting"
117
+ block
118
+ >
119
+ Confirm
120
+ </v-btn>
121
+ </template>
122
+ </ConfirmDialog>
123
+
124
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
125
+ </template>
126
+
127
+ <script setup lang="ts">
128
+ const props = defineProps({
129
+ detailRoute: {
130
+ type: String,
131
+ default: "index",
132
+ },
133
+ permissions: {
134
+ type: Object,
135
+ required: true,
136
+ default: () => ({}),
137
+ },
138
+ orgId: {
139
+ type: String,
140
+ default: "",
141
+ },
142
+ customerId: {
143
+ type: String,
144
+ default: "",
145
+ },
146
+ siteId: {
147
+ type: String,
148
+ default: "",
149
+ },
150
+ canCreateFeedback: {
151
+ type: Boolean,
152
+ default: false,
153
+ },
154
+ canViewFeedback: {
155
+ type: Boolean,
156
+ default: false,
157
+ },
158
+ canViewFeedbackDetails: {
159
+ type: Boolean,
160
+ default: false,
161
+ },
162
+ canDeleteFeedback: {
163
+ type: Boolean,
164
+ default: false,
165
+ },
166
+ });
167
+
168
+ const isEditMode = ref(false);
169
+ const showCreateDialog = ref(false);
170
+ const showDeleteDialog = ref(false);
171
+ const submitting = ref(false);
172
+ const message = ref("");
173
+ const messageColor = ref("");
174
+ const messageSnackbar = ref(false);
175
+
176
+ const serviceProviders = ref<
177
+ Array<{ title: string; value: string; subtitle: string }>
178
+ >([]);
179
+ const { getAll: getAllServiceProvider } = useServiceProvider();
180
+
181
+ const { data: getAllReq } = useLazyAsyncData("get-all-service-providers", () =>
182
+ getAllServiceProvider({
183
+ siteId: useRoute().params.site as string,
184
+ })
185
+ );
186
+
187
+ watchEffect(() => {
188
+ if (getAllReq.value) {
189
+ serviceProviders.value = getAllReq.value.items.map((i: any) => ({
190
+ title: i.nature.replace(/_/g, " "),
191
+ value: i.serviceProviderOrgId,
192
+ subtitle: i.name,
193
+ }));
194
+ }
195
+ });
196
+
197
+ const _feedback = ref({
198
+ subject: "",
199
+ category: "",
200
+ location: "",
201
+ description: "",
202
+ highPriority: false,
203
+ attachments: [] as string[],
204
+ serviceProvider: "",
205
+ assignee: "",
206
+ organization: "",
207
+ site: "",
208
+ });
209
+
210
+ const selected = ref<string[]>([]);
211
+
212
+ const { getColorStatus } = useUtils();
213
+
214
+ const headers = [
215
+ { title: "Name", value: "createdByName", align: "start" },
216
+ { title: "Subject", value: "subject", align: "start" },
217
+ { title: "Category", value: "category", align: "start" },
218
+ { title: "Status", value: "status", align: "start" },
219
+ { title: "", value: "action-table", align: "end" },
220
+ ];
221
+
222
+ // const loading = ref(false);
223
+ const erroredImages = ref<string[]>([]);
224
+ const route = useRoute();
225
+ const { customers } = useCustomer();
226
+
227
+ const {
228
+ getFeedbacks: _getFeedbacks,
229
+ deleteFeedback,
230
+ createFeedback,
231
+ getFeedbackById,
232
+ updateFeedback,
233
+ } = useFeedback();
234
+
235
+ const page = ref(1);
236
+ const pages = ref(0);
237
+ const pageRange = ref("-- - -- of --");
238
+ const items = ref<Array<Record<string, any>>>([]);
239
+
240
+ const {
241
+ data: getAllFeedbackReq,
242
+ status: getAllReqStatus,
243
+ refresh: getAllReqRefresh,
244
+ } = useLazyAsyncData("get-all-feedbacks", () =>
245
+ _getFeedbacks({
246
+ page: page.value,
247
+ organization: route.params.org as string,
248
+ site: route.params.site as string,
249
+ })
250
+ );
251
+
252
+ const loading = computed(() => getAllReqStatus.value === "pending");
253
+
254
+ watchEffect(() => {
255
+ if (getAllFeedbackReq.value) {
256
+ items.value = getAllFeedbackReq.value.items;
257
+ pages.value = getAllFeedbackReq.value.pages;
258
+ pageRange.value = getAllFeedbackReq.value.pageRange;
259
+ }
260
+ });
261
+
262
+ function onSelectedUpdate(newSelected: string[]) {
263
+ selected.value = newSelected;
264
+ }
265
+
266
+ async function editFeedback(item: any) {
267
+ try {
268
+ const _feedbacks = await getFeedbackById(item._id);
269
+ _feedback.value = {
270
+ attachments: (_feedbacks.attachments || []) as string[],
271
+ category: _feedbacks.category || "",
272
+ subject: _feedbacks.subject || "",
273
+ location: _feedbacks.location || "",
274
+ description: _feedbacks.description || "",
275
+ organization: _feedbacks.organization || "",
276
+ site: _feedbacks.site || "",
277
+ highPriority: _feedbacks.highPriority || false,
278
+ serviceProvider: _feedbacks.serviceProvider || "",
279
+ assignee: _feedbacks.assignee || "",
280
+ };
281
+ _feedbackId.value = item._id;
282
+ isEditMode.value = true;
283
+ showCreateDialog.value = true;
284
+ } catch (error) {
285
+ showMessage("Failed to load full feedback", "error");
286
+ }
287
+ }
288
+
289
+ function onViewFeedback(item: any) {
290
+ const route = useRoute();
291
+ const org = route.params.org;
292
+ const customer = route.params.customer;
293
+ const site = route.params.site;
294
+ const id = item._id;
295
+ useRouter().push({
296
+ name: props.detailRoute,
297
+ params: { org, site, id },
298
+ });
299
+ }
300
+
301
+ function confirmDeleteFeedback(item: any) {
302
+ feedbackToDelete.value = item;
303
+ showDeleteDialog.value = true;
304
+ }
305
+
306
+ const feedbackToDelete = ref<any>(null);
307
+
308
+ async function submitDelete() {
309
+ if (!feedbackToDelete.value) return;
310
+ submitting.value = true;
311
+ try {
312
+ const response = await deleteFeedback(feedbackToDelete.value._id);
313
+ showMessage(response.message, "success");
314
+ await getAllReqRefresh();
315
+ } catch (error) {
316
+ console.error("Failed to delete feedback:", error);
317
+ showMessage("Failed to delete feedback!", "error");
318
+ } finally {
319
+ submitting.value = false;
320
+ showDeleteDialog.value = false;
321
+ feedbackToDelete.value = null;
322
+ }
323
+ }
324
+
325
+ const resetFeedbackForm = () => {
326
+ _feedback.value = {
327
+ attachments: [] as string[],
328
+ category: "",
329
+ subject: "",
330
+ location: "",
331
+ description: "",
332
+ highPriority: false,
333
+ serviceProvider: "",
334
+ assignee: "",
335
+ organization: "",
336
+ site: "",
337
+ };
338
+ };
339
+
340
+ function handleCloseDialog() {
341
+ resetFeedbackForm();
342
+ isEditMode.value = false;
343
+ showCreateDialog.value = false;
344
+ }
345
+ const _feedbackId = ref<string | null>(null);
346
+
347
+ async function _createFeedback(organization: string, site: string) {
348
+ const createPayload: TFeedbackCreate = {
349
+ subject: _feedback.value.subject,
350
+ category: _feedback.value.category,
351
+ location: _feedback.value.location,
352
+ description: _feedback.value.description,
353
+ highPriority: _feedback.value.highPriority ?? false,
354
+ attachments: _feedback.value.attachments || [],
355
+ serviceProvider: _feedback.value.serviceProvider || "",
356
+ assignee: _feedback.value.assignee || "",
357
+ organization,
358
+ site,
359
+ };
360
+
361
+ const response = await createFeedback(createPayload);
362
+ showMessage(response?.message || "Feedback created successfully", "success");
363
+ }
364
+
365
+ async function _updateFeedback() {
366
+ if (!_feedbackId.value) return;
367
+
368
+ const updatePayload: TFeedbackUpdate = {
369
+ subject: _feedback.value.subject,
370
+ category: _feedback.value.category,
371
+ location: _feedback.value.location,
372
+ description: _feedback.value.description,
373
+ attachments: _feedback.value.attachments || [],
374
+ };
375
+
376
+ const response = await updateFeedback(_feedbackId.value, updatePayload);
377
+ showMessage(response?.message || "Feedback updated successfully", "success");
378
+ }
379
+
380
+ const submitFeedback = async () => {
381
+ submitting.value = true;
382
+
383
+ try {
384
+ const organization = route.params.org as string;
385
+ const site = route.params.site as string;
386
+
387
+ if (isEditMode.value) {
388
+ await _updateFeedback();
389
+ } else {
390
+ await _createFeedback(organization, site);
391
+ }
392
+
393
+ showCreateDialog.value = false;
394
+ await getAllReqRefresh();
395
+ resetFeedbackForm();
396
+ } catch (error) {
397
+ console.error(error);
398
+ showMessage("Something went wrong", "error");
399
+ } finally {
400
+ submitting.value = false;
401
+ }
402
+ };
403
+
404
+ function showMessage(msg: string, color: string) {
405
+ message.value = msg;
406
+ messageColor.value = color;
407
+ messageSnackbar.value = true;
408
+ }
409
+
410
+ const { addFile, deleteFile: _deleteFile } = useFile();
411
+
412
+ const API_DO_STORAGE_ENDPOINT =
413
+ useRuntimeConfig().public.API_DO_STORAGE_ENDPOINT;
414
+
415
+ async function handleFileAdded(file: File) {
416
+ try {
417
+ const res = await addFile(file);
418
+
419
+ const uploadedId = res?.id;
420
+ if (uploadedId) {
421
+ const url = `${API_DO_STORAGE_ENDPOINT}/${uploadedId}`;
422
+ _feedback.value.attachments = _feedback.value.attachments ?? [];
423
+ _feedback.value.attachments.push(url);
424
+ }
425
+ } catch (error) {
426
+ console.error("Error uploading file:", error);
427
+ showMessage("Failed to upload file", "error");
428
+ }
429
+ }
430
+
431
+ async function deleteFile(value: string) {
432
+ try {
433
+ await _deleteFile(value);
434
+ _feedback.value.attachments = (_feedback.value.attachments ?? []).filter(
435
+ (file) => file !== value
436
+ );
437
+ } catch (error) {
438
+ console.log(error);
439
+ showMessage("Failed to delete file", "error");
440
+ }
441
+ }
442
+
443
+ const { serviceProviderCategories, getServiceProviderCategories } =
444
+ useServiceProvider();
445
+
446
+ const categories = computed(() =>
447
+ serviceProviderCategories.value.map((name) => ({
448
+ title: name.charAt(0).toUpperCase() + name.slice(1),
449
+ value: name,
450
+ }))
451
+ );
452
+
453
+ getServiceProviderCategories();
454
+ </script>
@@ -0,0 +1,65 @@
1
+ <template>
2
+ <v-dialog v-model="model" max-width="600" persistent>
3
+ <v-card
4
+ class="px-3 d-flex flex-column"
5
+ :class="`${theme.name.value === 'light' ? 'bg-white' : ''} ${
6
+ mdAndUp ? 'rounded-xl' : ''
7
+ }`"
8
+ :style="{ border: 'none', boxShadow: 'none' }"
9
+ >
10
+ <v-toolbar
11
+ :color="theme.name.value === 'dark' ? 'grey-darken-4' : 'white'"
12
+ class="flex-shrink-0 mb-3"
13
+ style="position: sticky; top: 0; z-index: 10"
14
+ >
15
+ <slot name="title">
16
+ <span class="text-h6 font-weight-medium pt-1 text-capitalize">
17
+ Dialog
18
+ </span>
19
+ </slot>
20
+ <v-spacer />
21
+ <v-btn icon="mdi-close" @click="closeDialog" />
22
+ </v-toolbar>
23
+
24
+ <v-sheet class="flex-grow-1 overflow-y-auto px-3">
25
+ <slot />
26
+ </v-sheet>
27
+
28
+ <v-sheet class="flex-shrink-0 px-3 py-2">
29
+ <slot name="footer" />
30
+ </v-sheet>
31
+ </v-card>
32
+ </v-dialog>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import { useTheme, useDisplay } from "vuetify";
37
+
38
+ const theme = useTheme().global;
39
+ const { mdAndUp } = useDisplay();
40
+
41
+ const props = defineProps<{
42
+ modelValue: boolean;
43
+ }>();
44
+
45
+ const emit = defineEmits<{
46
+ (e: "update:modelValue", value: boolean): void;
47
+ (e: "close"): void;
48
+ }>();
49
+
50
+ const model = computed({
51
+ get: () => props.modelValue,
52
+ set: (value) => emit("update:modelValue", value),
53
+ });
54
+
55
+ function closeDialog() {
56
+ model.value = false;
57
+ emit("close");
58
+ }
59
+ </script>
60
+
61
+ <style scoped>
62
+ :deep(.v-dialog .v-overlay__content) {
63
+ margin: auto;
64
+ }
65
+ </style>
@@ -0,0 +1,203 @@
1
+ <template>
2
+ <div>
3
+ <v-row class="mb-4" align="center" no-gutters>
4
+ <v-col cols="10" class="pr-2">
5
+ <div
6
+ class="d-flex align-center justify-center pa-4 rounded-lg border-dashed border border-grey"
7
+ @dragover.prevent
8
+ @drop="handleDrop"
9
+ style="position: relative; z-index: 1"
10
+ >
11
+ <v-file-input
12
+ ref="dropInput"
13
+ v-model="files"
14
+ multiple
15
+ hide-details
16
+ :max-files="maxFiles"
17
+ style="
18
+ opacity: 0;
19
+ position: absolute;
20
+ inset: 0;
21
+ z-index: 2;
22
+ cursor: pointer;
23
+ "
24
+ @change="onFileChange"
25
+ />
26
+ <div
27
+ class="d-flex align-center"
28
+ style="z-index: 1; pointer-events: none"
29
+ >
30
+ <v-icon size="28" class="mr-2" color="primary"
31
+ >mdi-cloud-upload-outline</v-icon
32
+ >
33
+ <span class="text-body-1 font-weight-medium"
34
+ >Drag and drop files here</span
35
+ >
36
+ </div>
37
+ </div>
38
+ </v-col>
39
+
40
+ <v-col cols="2" class="d-flex justify-center">
41
+ <v-btn
42
+ color="primary-button"
43
+ min-width="55"
44
+ width="55"
45
+ height="55"
46
+ elevation="0"
47
+ @click="selectAttachment"
48
+ >
49
+ <v-icon size="20">mdi-camera-outline</v-icon>
50
+ </v-btn>
51
+ </v-col>
52
+ </v-row>
53
+
54
+ <v-sheet v-if="attachments.length > 0" elevation="0" class="py-3" rounded>
55
+ <v-row no-gutters>
56
+ <v-col
57
+ v-for="(file, index) in attachments"
58
+ :key="index"
59
+ cols="12"
60
+ class="d-flex align-center pa-2 mr-2 mb-2 rounded bg-white border-sm"
61
+ >
62
+ <div class="mr-3">
63
+ <v-img
64
+ v-if="!localErroredImages.includes(file)"
65
+ :src="file"
66
+ width="40"
67
+ height="40"
68
+ class="rounded"
69
+ cover
70
+ @error="onImageError(file)"
71
+ />
72
+ <v-img
73
+ v-else
74
+ :src="getThumbnail(file)"
75
+ width="40"
76
+ height="40"
77
+ class="rounded"
78
+ cover
79
+ />
80
+ </div>
81
+
82
+ <div class="flex-grow-1 text-truncate">
83
+ {{ getDisplayName(file) }}
84
+ </div>
85
+
86
+ <v-icon size="small" @click.stop="$emit('delete', file)">
87
+ mdi-trash-can-outline
88
+ </v-icon>
89
+ </v-col>
90
+ </v-row>
91
+ </v-sheet>
92
+ </div>
93
+ </template>
94
+
95
+ <script setup lang="ts">
96
+ const props = defineProps<{
97
+ attachments: string[];
98
+ erroredImages?: string[];
99
+ maxFiles?: number;
100
+ }>();
101
+
102
+ const emit = defineEmits<{
103
+ (e: "add", file: File): void;
104
+ (e: "delete", url: string): void;
105
+ (e: "errored", url: string): void;
106
+ }>();
107
+
108
+ const dropInput = ref<any>(null);
109
+ const files = ref<File[]>([]);
110
+ const localErroredImages = ref<string[]>(props.erroredImages || []);
111
+ // Store file names for display
112
+ const fileNamesMap = ref<Record<string, string>>({});
113
+
114
+ watch(
115
+ () => props.erroredImages,
116
+ (val) => {
117
+ if (val) {
118
+ localErroredImages.value = val;
119
+ }
120
+ }
121
+ );
122
+
123
+ function handleDrop(event: DragEvent) {
124
+ const droppedFiles = event.dataTransfer?.files;
125
+ if (droppedFiles?.length) {
126
+ files.value = Array.from(droppedFiles);
127
+ onFileChange();
128
+ }
129
+ }
130
+
131
+ function selectAttachment() {
132
+ const input = dropInput.value?.$el?.querySelector(
133
+ "input[type='file']"
134
+ ) as HTMLInputElement | null;
135
+ input?.click();
136
+ }
137
+
138
+ function onFileChange() {
139
+ if (files.value.length) {
140
+ console.log(
141
+ "onFileChange triggered with files:",
142
+ files.value.map((f) => f.name)
143
+ );
144
+
145
+ const maxFiles = props.maxFiles || 5;
146
+ if (files.value.length > maxFiles) {
147
+ console.warn(`Too many files selected. Maximum allowed: ${maxFiles}`);
148
+ files.value = [];
149
+ return;
150
+ }
151
+
152
+ files.value.forEach((file) => {
153
+ console.log(`Emitting 'add' event for file: ${file.name}`);
154
+ // For each file, we'll also need to store its original name
155
+ // This will be associated with the file URL in the parent component
156
+ emit("add", file);
157
+ });
158
+
159
+ files.value = [];
160
+ }
161
+ }
162
+
163
+ function onImageError(file: string) {
164
+ console.log(`Image error for file: ${file}`);
165
+ emit("errored", file);
166
+ }
167
+
168
+ function getThumbnail(fileUrl: string): string {
169
+ if (fileUrl.endsWith(".pdf")) return "mdi-file-pdf-outline";
170
+ if (fileUrl.match(/\.(doc|docx)$/i))
171
+ return "/images/file-thumbnails/word.png";
172
+ if (fileUrl.match(/\.(xls|xlsx)$/i))
173
+ return "/images/file-thumbnails/excel.png";
174
+ return "/images/file-thumbnails/file.png";
175
+ }
176
+
177
+ // Modified to try to display the friendly name
178
+ function getFileName(fileUrl: string): string {
179
+ try {
180
+ const url = new URL(fileUrl);
181
+ return decodeURIComponent(url.pathname.split("/").pop() || "");
182
+ } catch (e) {
183
+ return fileUrl.split("/").pop() || "";
184
+ }
185
+ }
186
+
187
+ // This will use our stored file names when available
188
+ function getDisplayName(fileUrl: string): string {
189
+ // If we have a friendly name stored, use it
190
+ if (fileNamesMap.value[fileUrl]) {
191
+ return fileNamesMap.value[fileUrl];
192
+ }
193
+ // Otherwise fall back to the ID/URL-based name
194
+ return getFileName(fileUrl);
195
+ }
196
+
197
+ // Method to update file names map - will be called from parent
198
+ defineExpose({
199
+ updateFileName: (url: string, name: string) => {
200
+ fileNamesMap.value[url] = name;
201
+ },
202
+ });
203
+ </script>