@iservice365/layer-common 0.1.0 → 0.2.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.
- package/.playground/app.vue +7 -2
- package/.playground/pages/feedback.vue +30 -0
- package/CHANGELOG.md +6 -0
- package/components/Chat/Bubbles.vue +53 -0
- package/components/Chat/Information.vue +187 -0
- package/components/Chat/ListCard.vue +62 -0
- package/components/Chat/Message.vue +149 -0
- package/components/Chat/Navigation.vue +150 -0
- package/components/ConfirmDialog.vue +66 -0
- package/components/Container/Standard.vue +33 -0
- package/components/Feedback/Form.vue +136 -0
- package/components/FeedbackDetail.vue +465 -0
- package/components/FeedbackMain.vue +454 -0
- package/components/FormDialog.vue +65 -0
- package/components/Input/File.vue +203 -0
- package/components/Input/ListGroupSelection.vue +96 -0
- package/components/Input/NewDate.vue +123 -0
- package/components/Input/Number.vue +124 -0
- package/components/InvitationMain.vue +284 -0
- package/components/Layout/Header.vue +14 -4
- package/components/ListView.vue +87 -0
- package/components/MemberMain.vue +459 -0
- package/components/RolePermissionFormCreate.vue +161 -0
- package/components/RolePermissionFormPreviewUpdate.vue +183 -0
- package/components/RolePermissionMain.vue +361 -0
- package/components/ServiceProviderFormCreate.vue +154 -0
- package/components/ServiceProviderMain.vue +195 -0
- package/components/SignaturePad.vue +73 -0
- package/components/SpecificAttr.vue +53 -0
- package/components/SwitchContext.vue +26 -5
- package/components/TableList.vue +150 -0
- package/components/TableListSecondary.vue +164 -0
- package/components/WorkOrder/Create.vue +197 -0
- package/components/WorkOrder/ListView.vue +96 -0
- package/components/WorkOrder/Main.vue +308 -0
- package/components/Workorder.vue +1 -0
- package/composables/useAddress.ts +107 -0
- package/composables/useCommonPermission.ts +130 -0
- package/composables/useCustomer.ts +113 -0
- package/composables/useFeedback.ts +117 -0
- package/composables/useFile.ts +40 -0
- package/composables/useInvoice.ts +18 -0
- package/composables/useLocal.ts +24 -4
- package/composables/useLocalAuth.ts +58 -14
- package/composables/useLocalSetup.ts +52 -0
- package/composables/useMember.ts +104 -0
- package/composables/useOrg.ts +76 -92
- package/composables/usePaymentMethod.ts +101 -0
- package/composables/usePrice.ts +15 -0
- package/composables/usePromoCode.ts +36 -0
- package/composables/useRole.ts +38 -7
- package/composables/useServiceProvider.ts +218 -0
- package/composables/useSite.ts +108 -0
- package/composables/useSubscription.ts +149 -0
- package/composables/useUser.ts +38 -14
- package/composables/useUtils.ts +218 -6
- package/composables/useVerification.ts +33 -0
- package/composables/useWorkOrder.ts +68 -0
- package/middleware/01.auth.ts +11 -0
- package/middleware/02.org.ts +18 -0
- package/middleware/03.customer.ts +13 -0
- package/nuxt.config.ts +2 -1
- package/package.json +7 -3
- package/pages/index.vue +3 -0
- package/pages/payment-method-linked.vue +31 -0
- package/pages/require-customer.vue +56 -0
- package/pages/require-organization-membership.vue +47 -0
- package/pages/unauthorized.vue +29 -0
- package/plugins/API.ts +1 -3
- package/plugins/iconify.client.ts +5 -0
- package/plugins/vuetify.ts +2 -0
- package/public/bg-camera.jpg +0 -0
- package/public/bg-city.jpg +0 -0
- package/public/bg-condo.jpg +0 -0
- package/public/images/icons/delete-icon.png +0 -0
- package/public/sprite.svg +1 -0
- package/types/address.d.ts +13 -0
- package/types/customer.d.ts +15 -0
- package/types/feedback.d.ts +63 -0
- package/types/local.d.ts +46 -38
- package/types/member.d.ts +21 -0
- package/types/org.d.ts +13 -0
- package/types/permission.d.ts +1 -0
- package/types/price.d.ts +17 -0
- package/types/promo-code.d.ts +19 -0
- package/types/service-provider.d.ts +15 -0
- package/types/site.d.ts +13 -0
- package/types/subscription.d.ts +23 -0
- package/types/user.d.ts +19 -0
- package/types/verification.d.ts +20 -0
- 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>
|