@antelopejs/dms-saas 0.3.1 → 0.3.2
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/README.md +0 -86
- package/dist/config/runtime.d.ts +0 -1
- package/dist/config/runtime.js +0 -7
- package/dist/config/runtime.js.map +1 -1
- package/dist/data-api/platformOwner/index.d.ts +0 -1
- package/dist/data-api/platformOwner/index.js +0 -1
- package/dist/data-api/platformOwner/index.js.map +1 -1
- package/dist/data-api/tenant/index.d.ts +0 -1
- package/dist/data-api/tenant/index.js +0 -1
- package/dist/data-api/tenant/index.js.map +1 -1
- package/dist/notifications/subjects.d.ts +0 -2
- package/dist/notifications/subjects.js +1 -13
- package/dist/notifications/subjects.js.map +1 -1
- package/dist/pages/platform/categories.d.ts +0 -1
- package/dist/pages/platform/categories.js +1 -8
- package/dist/pages/platform/categories.js.map +1 -1
- package/dist/pages/platform/index.d.ts +0 -1
- package/dist/pages/platform/index.js +0 -1
- package/dist/pages/platform/index.js.map +1 -1
- package/dist/pages/tenant/index.d.ts +0 -1
- package/dist/pages/tenant/index.js +0 -1
- package/dist/pages/tenant/index.js.map +1 -1
- package/dist/routes/platformOwner/index.d.ts +0 -1
- package/dist/routes/platformOwner/index.js +0 -1
- package/dist/routes/platformOwner/index.js.map +1 -1
- package/dist/routes/tenant/index.d.ts +0 -1
- package/dist/routes/tenant/index.js +0 -1
- package/dist/routes/tenant/index.js.map +1 -1
- package/dist/types/config.d.ts +0 -1
- package/frontend-vue/i18n/locales/saas-en-GB.json +1 -142
- package/frontend-vue/i18n/locales/saas-fr-FR.json +1 -142
- package/package.json +2 -4
- package/dist/data-api/platformOwner/support.d.ts +0 -58
- package/dist/data-api/platformOwner/support.js +0 -111
- package/dist/data-api/platformOwner/support.js.map +0 -1
- package/dist/data-api/support-fields.d.ts +0 -49
- package/dist/data-api/support-fields.js +0 -166
- package/dist/data-api/support-fields.js.map +0 -1
- package/dist/data-api/support-routes.d.ts +0 -10
- package/dist/data-api/support-routes.js +0 -71
- package/dist/data-api/support-routes.js.map +0 -1
- package/dist/data-api/tenant/support.d.ts +0 -39
- package/dist/data-api/tenant/support.js +0 -53
- package/dist/data-api/tenant/support.js.map +0 -1
- package/dist/pages/platform/support.d.ts +0 -9
- package/dist/pages/platform/support.js +0 -29
- package/dist/pages/platform/support.js.map +0 -1
- package/dist/pages/tenant/support.d.ts +0 -9
- package/dist/pages/tenant/support.js +0 -27
- package/dist/pages/tenant/support.js.map +0 -1
- package/dist/routes/platformOwner/support.d.ts +0 -21
- package/dist/routes/platformOwner/support.js +0 -180
- package/dist/routes/platformOwner/support.js.map +0 -1
- package/dist/routes/tenant/support.d.ts +0 -27
- package/dist/routes/tenant/support.js +0 -185
- package/dist/routes/tenant/support.js.map +0 -1
- package/dist/support/admissions.d.ts +0 -36
- package/dist/support/admissions.js +0 -136
- package/dist/support/admissions.js.map +0 -1
- package/dist/support/attachments.d.ts +0 -12
- package/dist/support/attachments.js +0 -56
- package/dist/support/attachments.js.map +0 -1
- package/dist/support/config.d.ts +0 -19
- package/dist/support/config.js +0 -75
- package/dist/support/config.js.map +0 -1
- package/dist/support/index.d.ts +0 -6
- package/dist/support/index.js +0 -23
- package/dist/support/index.js.map +0 -1
- package/dist/support/operations.d.ts +0 -51
- package/dist/support/operations.js +0 -109
- package/dist/support/operations.js.map +0 -1
- package/dist/support/plans.d.ts +0 -6
- package/dist/support/plans.js +0 -130
- package/dist/support/plans.js.map +0 -1
- package/dist/support/publications.d.ts +0 -8
- package/dist/support/publications.js +0 -106
- package/dist/support/publications.js.map +0 -1
- package/dist/support/recovery.d.ts +0 -7
- package/dist/support/recovery.js +0 -43
- package/dist/support/recovery.js.map +0 -1
- package/dist/support/service.d.ts +0 -51
- package/dist/support/service.js +0 -143
- package/dist/support/service.js.map +0 -1
- package/dist/support/transitions.d.ts +0 -4
- package/dist/support/transitions.js +0 -29
- package/dist/support/transitions.js.map +0 -1
- package/dist/support/uploads.d.ts +0 -39
- package/dist/support/uploads.js +0 -115
- package/dist/support/uploads.js.map +0 -1
- package/dist/support/validation.d.ts +0 -15
- package/dist/support/validation.js +0 -54
- package/dist/support/validation.js.map +0 -1
- package/frontend-vue/app/components/PlatformSupport.vue +0 -461
- package/frontend-vue/app/components/SupportAttachmentUpload.vue +0 -140
- package/frontend-vue/app/components/SupportThreadHistory.vue +0 -139
- package/frontend-vue/app/components/TenantSupport.vue +0 -415
- package/frontend-vue/app/composables/useSupportRequest.ts +0 -74
- package/frontend-vue/app/types/support.ts +0 -123
|
@@ -1,461 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, ref } from "vue";
|
|
3
|
-
import { sendSupportRequest } from "../composables/useSupportRequest";
|
|
4
|
-
import SupportThreadHistory from "./SupportThreadHistory.vue";
|
|
5
|
-
|
|
6
|
-
const { $authFetch } = useAuthFetch();
|
|
7
|
-
const { t } = useI18n();
|
|
8
|
-
const toast = useToast();
|
|
9
|
-
const { resolveApiError } = useApiErrorMessage();
|
|
10
|
-
|
|
11
|
-
const FILTER_ALL = "all";
|
|
12
|
-
const ASSIGNEE_UNASSIGNED = "unassigned";
|
|
13
|
-
|
|
14
|
-
const inbox = ref<PlatformSupportTicketView[]>([]);
|
|
15
|
-
const owners = ref<PlatformOwnerOptionView[]>([]);
|
|
16
|
-
const selected = ref<PlatformSupportThreadView | null>(null);
|
|
17
|
-
const statusFilter = ref<string>(FILTER_ALL);
|
|
18
|
-
const priorityFilter = ref<string>(FILTER_ALL);
|
|
19
|
-
const isLoading = ref(true);
|
|
20
|
-
const isSubmitting = ref(false);
|
|
21
|
-
const replyBody = ref("");
|
|
22
|
-
const PAGE_SIZE = 20;
|
|
23
|
-
const OWNER_PAGE_SIZE = 100;
|
|
24
|
-
const listPage = ref(1);
|
|
25
|
-
const messagesPage = ref(1);
|
|
26
|
-
const eventsPage = ref(1);
|
|
27
|
-
const inboxTotal = ref(0);
|
|
28
|
-
let selectionRequest = 0;
|
|
29
|
-
let messagesRequest = 0;
|
|
30
|
-
let eventsRequest = 0;
|
|
31
|
-
let inboxRequest = 0;
|
|
32
|
-
|
|
33
|
-
const statusItems = [
|
|
34
|
-
{ value: FILTER_ALL, label: t("saas.support.filters.all_statuses") },
|
|
35
|
-
...["open", "in_progress", "waiting_customer", "resolved", "closed"].map(
|
|
36
|
-
(value) => ({ value, label: t(`saas.support.status.${value}`) }),
|
|
37
|
-
),
|
|
38
|
-
];
|
|
39
|
-
const priorityItems = [
|
|
40
|
-
{ value: FILTER_ALL, label: t("saas.support.filters.all_priorities") },
|
|
41
|
-
...["low", "normal", "high", "urgent"].map((value) => ({
|
|
42
|
-
value,
|
|
43
|
-
label: t(`saas.support.priority.${value}`),
|
|
44
|
-
})),
|
|
45
|
-
];
|
|
46
|
-
const assignmentItems = computed(() => [
|
|
47
|
-
{
|
|
48
|
-
value: ASSIGNEE_UNASSIGNED,
|
|
49
|
-
label: t("saas.support.platform.unassigned"),
|
|
50
|
-
},
|
|
51
|
-
...owners.value.map((owner) => ({
|
|
52
|
-
value: owner._id,
|
|
53
|
-
label: owner.name || owner.email || owner._id,
|
|
54
|
-
})),
|
|
55
|
-
]);
|
|
56
|
-
const assigneeLabels = computed<Record<string, string>>(() =>
|
|
57
|
-
Object.fromEntries(
|
|
58
|
-
owners.value.map((owner) => [
|
|
59
|
-
owner._id,
|
|
60
|
-
owner.name || owner.email || owner._id,
|
|
61
|
-
]),
|
|
62
|
-
),
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
function formatDate(value: string): string {
|
|
66
|
-
return new Intl.DateTimeFormat(undefined, {
|
|
67
|
-
dateStyle: "medium",
|
|
68
|
-
timeStyle: "short",
|
|
69
|
-
}).format(new Date(value));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function inboxQuery(): string {
|
|
73
|
-
const query = new URLSearchParams({
|
|
74
|
-
offset: String((listPage.value - 1) * PAGE_SIZE),
|
|
75
|
-
limit: String(PAGE_SIZE),
|
|
76
|
-
sortKey: "lastMessageAt",
|
|
77
|
-
sortDirection: "desc",
|
|
78
|
-
});
|
|
79
|
-
if (statusFilter.value !== FILTER_ALL) {
|
|
80
|
-
query.set("filter_status", `is:${statusFilter.value}`);
|
|
81
|
-
}
|
|
82
|
-
if (priorityFilter.value !== FILTER_ALL) {
|
|
83
|
-
query.set("filter_priority", `is:${priorityFilter.value}`);
|
|
84
|
-
}
|
|
85
|
-
return query.toString();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function threadQuery(page: number): string {
|
|
89
|
-
return new URLSearchParams({
|
|
90
|
-
offset: String((page - 1) * PAGE_SIZE),
|
|
91
|
-
limit: String(PAGE_SIZE),
|
|
92
|
-
sortKey: "createdAt",
|
|
93
|
-
sortDirection: "desc",
|
|
94
|
-
}).toString();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function loadInbox(): Promise<void> {
|
|
98
|
-
const request = ++inboxRequest;
|
|
99
|
-
isLoading.value = true;
|
|
100
|
-
try {
|
|
101
|
-
const suffix = inboxQuery();
|
|
102
|
-
const page = await $authFetch<SupportPageView<PlatformSupportTicketView>>(
|
|
103
|
-
`/api/saas/tables/support-tickets/list?${suffix}`,
|
|
104
|
-
);
|
|
105
|
-
if (request !== inboxRequest) return;
|
|
106
|
-
inbox.value = page.results;
|
|
107
|
-
inboxTotal.value = page.total;
|
|
108
|
-
} catch (error) {
|
|
109
|
-
showError(error, "saas.support.error.load");
|
|
110
|
-
} finally {
|
|
111
|
-
if (request === inboxRequest) isLoading.value = false;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function loadOwners(): Promise<void> {
|
|
116
|
-
try {
|
|
117
|
-
const loadedOwners: PlatformOwnerOptionView[] = [];
|
|
118
|
-
let offset = 0;
|
|
119
|
-
let total = 0;
|
|
120
|
-
do {
|
|
121
|
-
const query = new URLSearchParams({
|
|
122
|
-
offset: String(offset),
|
|
123
|
-
limit: String(OWNER_PAGE_SIZE),
|
|
124
|
-
});
|
|
125
|
-
const page = await $authFetch<SupportPageView<PlatformOwnerOptionView>>(
|
|
126
|
-
`/api/saas/tables/support-owners/list?${query}`,
|
|
127
|
-
);
|
|
128
|
-
loadedOwners.push(...page.results);
|
|
129
|
-
if (!page.results.length) break;
|
|
130
|
-
offset += page.results.length;
|
|
131
|
-
total = page.total;
|
|
132
|
-
} while (offset < total);
|
|
133
|
-
owners.value = loadedOwners;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
showError(error, "saas.support.error.load");
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function selectTicket(
|
|
140
|
-
ticket: PlatformSupportTicketView,
|
|
141
|
-
): Promise<void> {
|
|
142
|
-
messagesPage.value = 1;
|
|
143
|
-
eventsPage.value = 1;
|
|
144
|
-
const request = ++selectionRequest;
|
|
145
|
-
const messageRequest = ++messagesRequest;
|
|
146
|
-
const eventRequest = ++eventsRequest;
|
|
147
|
-
const baseUrl = `/api/saas/support/platform/${ticket._instance}/${ticket._id}`;
|
|
148
|
-
try {
|
|
149
|
-
const [detail, messages, events] = await Promise.all([
|
|
150
|
-
$authFetch<PlatformSupportTicketDetailView>(baseUrl),
|
|
151
|
-
$authFetch<SupportPageView<SupportMessageView>>(
|
|
152
|
-
`${baseUrl}/messages/list?${threadQuery(messagesPage.value)}`,
|
|
153
|
-
),
|
|
154
|
-
$authFetch<SupportPageView<SupportTicketEventView>>(
|
|
155
|
-
`${baseUrl}/events/list?${threadQuery(eventsPage.value)}`,
|
|
156
|
-
),
|
|
157
|
-
]);
|
|
158
|
-
if (
|
|
159
|
-
request === selectionRequest &&
|
|
160
|
-
messageRequest === messagesRequest &&
|
|
161
|
-
eventRequest === eventsRequest
|
|
162
|
-
) {
|
|
163
|
-
selected.value = { ...detail, messages, events };
|
|
164
|
-
}
|
|
165
|
-
} catch (error) {
|
|
166
|
-
showError(error, "saas.support.error.load");
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function updateTicket(patch: SupportTicketPatch): Promise<void> {
|
|
171
|
-
if (!selected.value) return;
|
|
172
|
-
isSubmitting.value = true;
|
|
173
|
-
const { tenantId, ticket } = selected.value;
|
|
174
|
-
try {
|
|
175
|
-
await sendSupportRequest(
|
|
176
|
-
`platform:${tenantId}:${ticket._id}:update`,
|
|
177
|
-
patch,
|
|
178
|
-
(body) =>
|
|
179
|
-
$authFetch(`/api/saas/support/platform/${tenantId}/${ticket._id}`, {
|
|
180
|
-
method: "PUT",
|
|
181
|
-
body,
|
|
182
|
-
}),
|
|
183
|
-
);
|
|
184
|
-
eventsPage.value = 1;
|
|
185
|
-
await Promise.all([
|
|
186
|
-
loadInbox(),
|
|
187
|
-
refreshDetail(),
|
|
188
|
-
loadThreadPage("events"),
|
|
189
|
-
]);
|
|
190
|
-
} catch (error) {
|
|
191
|
-
showError(error, "saas.support.error.update");
|
|
192
|
-
} finally {
|
|
193
|
-
isSubmitting.value = false;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function changeStatus(status: string): void {
|
|
198
|
-
void updateTicket({ status: status as SupportStatus });
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function changeAssignment(assignedTo: string): void {
|
|
202
|
-
void updateTicket({
|
|
203
|
-
assignedTo: assignedTo === ASSIGNEE_UNASSIGNED ? null : assignedTo,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function sendReply(): Promise<void> {
|
|
208
|
-
if (!selected.value || !replyBody.value.trim()) return;
|
|
209
|
-
const { tenantId, ticket } = selected.value;
|
|
210
|
-
isSubmitting.value = true;
|
|
211
|
-
try {
|
|
212
|
-
await sendSupportRequest(
|
|
213
|
-
`platform:${tenantId}:${ticket._id}:reply`,
|
|
214
|
-
{ body: replyBody.value, attachments: [] },
|
|
215
|
-
(body) =>
|
|
216
|
-
$authFetch(
|
|
217
|
-
`/api/saas/support/platform/${tenantId}/${ticket._id}/messages`,
|
|
218
|
-
{ method: "POST", body },
|
|
219
|
-
),
|
|
220
|
-
);
|
|
221
|
-
replyBody.value = "";
|
|
222
|
-
messagesPage.value = 1;
|
|
223
|
-
eventsPage.value = 1;
|
|
224
|
-
await Promise.all([
|
|
225
|
-
loadInbox(),
|
|
226
|
-
refreshDetail(),
|
|
227
|
-
loadThreadPage("messages"),
|
|
228
|
-
loadThreadPage("events"),
|
|
229
|
-
]);
|
|
230
|
-
} catch (error) {
|
|
231
|
-
showError(error, "saas.support.error.reply");
|
|
232
|
-
} finally {
|
|
233
|
-
isSubmitting.value = false;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function openAttachment(resourceKey: string): Promise<void> {
|
|
238
|
-
if (!selected.value) return;
|
|
239
|
-
const { tenantId, ticket } = selected.value;
|
|
240
|
-
const query = new URLSearchParams({ resourceKey });
|
|
241
|
-
try {
|
|
242
|
-
const file = await $authFetch<SupportAttachmentView>(
|
|
243
|
-
`/api/saas/support/platform/${tenantId}/${ticket._id}/attachment?${query}`,
|
|
244
|
-
);
|
|
245
|
-
window.open(file.url, "_blank", "noopener,noreferrer");
|
|
246
|
-
} catch (error) {
|
|
247
|
-
showError(error, "saas.support.error.attachment");
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function showError(error: unknown, fallback: string): void {
|
|
252
|
-
toast.add({
|
|
253
|
-
title: resolveApiError(error, fallback),
|
|
254
|
-
color: "error",
|
|
255
|
-
icon: "i-ph-warning-circle",
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
onMounted(() => Promise.all([loadInbox(), loadOwners()]));
|
|
260
|
-
|
|
261
|
-
function applyFilters(): void {
|
|
262
|
-
listPage.value = 1;
|
|
263
|
-
void loadInbox();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function changeListPage(page: number): void {
|
|
267
|
-
listPage.value = page;
|
|
268
|
-
void loadInbox();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async function refreshDetail(): Promise<void> {
|
|
272
|
-
if (!selected.value) return Promise.resolve();
|
|
273
|
-
const { tenantId, ticket } = selected.value;
|
|
274
|
-
const request = ++selectionRequest;
|
|
275
|
-
const detail = await $authFetch<PlatformSupportTicketDetailView>(
|
|
276
|
-
`/api/saas/support/platform/${tenantId}/${ticket._id}`,
|
|
277
|
-
);
|
|
278
|
-
if (
|
|
279
|
-
request === selectionRequest &&
|
|
280
|
-
selected.value?.ticket._id === ticket._id &&
|
|
281
|
-
selected.value?.tenantId === tenantId
|
|
282
|
-
) {
|
|
283
|
-
Object.assign(selected.value, detail);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function changeThreadPage(kind: "messages" | "events", page: number): void {
|
|
288
|
-
const pages = { messages: messagesPage, events: eventsPage };
|
|
289
|
-
pages[kind].value = page;
|
|
290
|
-
void loadThreadPage(kind);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
async function loadThreadPage(kind: "messages" | "events"): Promise<void> {
|
|
294
|
-
if (!selected.value) return;
|
|
295
|
-
const { tenantId, ticket } = selected.value;
|
|
296
|
-
const request = kind === "messages" ? ++messagesRequest : ++eventsRequest;
|
|
297
|
-
const pageNumber =
|
|
298
|
-
kind === "messages" ? messagesPage.value : eventsPage.value;
|
|
299
|
-
try {
|
|
300
|
-
const page = await $authFetch<
|
|
301
|
-
SupportPageView<SupportMessageView | SupportTicketEventView>
|
|
302
|
-
>(
|
|
303
|
-
`/api/saas/support/platform/${tenantId}/${ticket._id}/${kind}/list?${threadQuery(pageNumber)}`,
|
|
304
|
-
);
|
|
305
|
-
const currentRequest =
|
|
306
|
-
kind === "messages" ? messagesRequest : eventsRequest;
|
|
307
|
-
if (
|
|
308
|
-
request !== currentRequest ||
|
|
309
|
-
selected.value?.ticket._id !== ticket._id ||
|
|
310
|
-
selected.value?.tenantId !== tenantId
|
|
311
|
-
)
|
|
312
|
-
return;
|
|
313
|
-
if (kind === "messages") {
|
|
314
|
-
selected.value.messages = page as SupportPageView<SupportMessageView>;
|
|
315
|
-
} else {
|
|
316
|
-
selected.value.events = page as SupportPageView<SupportTicketEventView>;
|
|
317
|
-
}
|
|
318
|
-
} catch (error) {
|
|
319
|
-
showError(error, "saas.support.error.load");
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
</script>
|
|
323
|
-
|
|
324
|
-
<template>
|
|
325
|
-
<div class="flex flex-col gap-5">
|
|
326
|
-
<UCard>
|
|
327
|
-
<div class="flex flex-wrap items-end gap-4">
|
|
328
|
-
<UFormField
|
|
329
|
-
:label="$t('saas.support.filters.status')"
|
|
330
|
-
class="min-w-48"
|
|
331
|
-
>
|
|
332
|
-
<USelect
|
|
333
|
-
v-model="statusFilter"
|
|
334
|
-
:items="statusItems"
|
|
335
|
-
class="w-full"
|
|
336
|
-
/>
|
|
337
|
-
</UFormField>
|
|
338
|
-
<UFormField
|
|
339
|
-
:label="$t('saas.support.filters.priority')"
|
|
340
|
-
class="min-w-48"
|
|
341
|
-
>
|
|
342
|
-
<USelect
|
|
343
|
-
v-model="priorityFilter"
|
|
344
|
-
:items="priorityItems"
|
|
345
|
-
class="w-full"
|
|
346
|
-
/>
|
|
347
|
-
</UFormField>
|
|
348
|
-
<UButton color="neutral" variant="outline" @click="applyFilters">
|
|
349
|
-
{{ $t("saas.support.filters.apply") }}
|
|
350
|
-
</UButton>
|
|
351
|
-
</div>
|
|
352
|
-
</UCard>
|
|
353
|
-
|
|
354
|
-
<div class="grid gap-6 xl:grid-cols-[24rem_1fr]">
|
|
355
|
-
<div class="flex flex-col gap-3">
|
|
356
|
-
<USkeleton v-if="isLoading" class="h-40" />
|
|
357
|
-
<UCard v-else-if="!inbox.length">
|
|
358
|
-
<p class="text-muted text-center text-sm">
|
|
359
|
-
{{ $t("saas.support.platform.empty") }}
|
|
360
|
-
</p>
|
|
361
|
-
</UCard>
|
|
362
|
-
<template v-else>
|
|
363
|
-
<button
|
|
364
|
-
v-for="ticket in inbox"
|
|
365
|
-
:key="`${ticket._instance}:${ticket._id}`"
|
|
366
|
-
class="border-default hover:bg-elevated rounded-lg border p-4 text-left"
|
|
367
|
-
:class="{
|
|
368
|
-
'ring-primary ring-2':
|
|
369
|
-
selected?.ticket._id === ticket._id &&
|
|
370
|
-
selected?.tenantId === ticket._instance,
|
|
371
|
-
}"
|
|
372
|
-
@click="selectTicket(ticket)"
|
|
373
|
-
>
|
|
374
|
-
<div class="flex justify-between gap-2">
|
|
375
|
-
<p class="font-medium">{{ ticket.subject }}</p>
|
|
376
|
-
<UBadge size="sm" variant="subtle">
|
|
377
|
-
{{ $t(`saas.support.priority.${ticket.priority}`) }}
|
|
378
|
-
</UBadge>
|
|
379
|
-
</div>
|
|
380
|
-
<p class="text-muted mt-1 text-sm">{{ ticket.tenantName }}</p>
|
|
381
|
-
<div class="text-muted mt-3 flex justify-between text-xs">
|
|
382
|
-
<span>{{ $t(`saas.support.status.${ticket.status}`) }}</span>
|
|
383
|
-
<span>{{ formatDate(ticket.lastMessageAt) }}</span>
|
|
384
|
-
</div>
|
|
385
|
-
</button>
|
|
386
|
-
<UPagination
|
|
387
|
-
v-if="inboxTotal > PAGE_SIZE"
|
|
388
|
-
:page="listPage"
|
|
389
|
-
:total="inboxTotal"
|
|
390
|
-
:items-per-page="PAGE_SIZE"
|
|
391
|
-
size="sm"
|
|
392
|
-
class="self-center"
|
|
393
|
-
@update:page="changeListPage"
|
|
394
|
-
/>
|
|
395
|
-
</template>
|
|
396
|
-
</div>
|
|
397
|
-
|
|
398
|
-
<UCard v-if="selected">
|
|
399
|
-
<template #header>
|
|
400
|
-
<div>
|
|
401
|
-
<p class="text-muted text-sm">{{ selected.tenantName }}</p>
|
|
402
|
-
<h2 class="text-lg font-semibold">
|
|
403
|
-
{{ selected.ticket.subject }}
|
|
404
|
-
</h2>
|
|
405
|
-
</div>
|
|
406
|
-
</template>
|
|
407
|
-
<div class="flex flex-col gap-5">
|
|
408
|
-
<div class="grid gap-4 sm:grid-cols-2">
|
|
409
|
-
<UFormField :label="$t('saas.support.filters.status')">
|
|
410
|
-
<USelect
|
|
411
|
-
:model-value="selected.ticket.status"
|
|
412
|
-
:items="statusItems.slice(1)"
|
|
413
|
-
class="w-full"
|
|
414
|
-
:disabled="isSubmitting"
|
|
415
|
-
@update:model-value="changeStatus"
|
|
416
|
-
/>
|
|
417
|
-
</UFormField>
|
|
418
|
-
<UFormField :label="$t('saas.support.platform.assignee')">
|
|
419
|
-
<USelect
|
|
420
|
-
:model-value="
|
|
421
|
-
selected.ticket.assignedTo ?? ASSIGNEE_UNASSIGNED
|
|
422
|
-
"
|
|
423
|
-
:items="assignmentItems"
|
|
424
|
-
class="w-full"
|
|
425
|
-
:disabled="isSubmitting"
|
|
426
|
-
@update:model-value="changeAssignment"
|
|
427
|
-
/>
|
|
428
|
-
</UFormField>
|
|
429
|
-
</div>
|
|
430
|
-
<USeparator />
|
|
431
|
-
<SupportThreadHistory
|
|
432
|
-
:messages="selected.messages"
|
|
433
|
-
:events="selected.events"
|
|
434
|
-
:assignee-labels="assigneeLabels"
|
|
435
|
-
@attachment="openAttachment"
|
|
436
|
-
@messages-page="changeThreadPage('messages', $event)"
|
|
437
|
-
@events-page="changeThreadPage('events', $event)"
|
|
438
|
-
/>
|
|
439
|
-
<UTextarea
|
|
440
|
-
v-model="replyBody"
|
|
441
|
-
:rows="5"
|
|
442
|
-
:placeholder="$t('saas.support.reply_placeholder')"
|
|
443
|
-
class="w-full"
|
|
444
|
-
/>
|
|
445
|
-
<div class="flex justify-end">
|
|
446
|
-
<UButton :loading="isSubmitting" @click="sendReply">
|
|
447
|
-
{{ $t("saas.support.send") }}
|
|
448
|
-
</UButton>
|
|
449
|
-
</div>
|
|
450
|
-
</div>
|
|
451
|
-
</UCard>
|
|
452
|
-
<UCard v-else class="min-h-64">
|
|
453
|
-
<div
|
|
454
|
-
class="text-muted flex h-full items-center justify-center text-sm"
|
|
455
|
-
>
|
|
456
|
-
{{ $t("saas.support.platform.select") }}
|
|
457
|
-
</div>
|
|
458
|
-
</UCard>
|
|
459
|
-
</div>
|
|
460
|
-
</div>
|
|
461
|
-
</template>
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, ref, watch } from "vue";
|
|
3
|
-
|
|
4
|
-
interface AttachmentConstraints {
|
|
5
|
-
maxSize?: number;
|
|
6
|
-
allowedMimetypes?: string[];
|
|
7
|
-
}
|
|
8
|
-
interface UploadProps {
|
|
9
|
-
modelValue: string[];
|
|
10
|
-
multiple?: boolean;
|
|
11
|
-
constraints?: AttachmentConstraints;
|
|
12
|
-
disabled?: boolean;
|
|
13
|
-
}
|
|
14
|
-
interface SupportPresign {
|
|
15
|
-
resourceKey: string;
|
|
16
|
-
uploadUrl: string;
|
|
17
|
-
headers: Record<string, string>;
|
|
18
|
-
}
|
|
19
|
-
const props = defineProps<UploadProps>();
|
|
20
|
-
const emit = defineEmits<{
|
|
21
|
-
"update:modelValue": [string[]];
|
|
22
|
-
busy: [boolean];
|
|
23
|
-
}>();
|
|
24
|
-
const { $authFetch } = useAuthFetch();
|
|
25
|
-
const { uploadWithProgress } = useUploadWithProgress();
|
|
26
|
-
const { t } = useI18n();
|
|
27
|
-
const selected = ref<File | File[] | null>(null);
|
|
28
|
-
const names = ref(new Map<string, string>());
|
|
29
|
-
const isUploading = ref(false);
|
|
30
|
-
const progress = ref(0);
|
|
31
|
-
const currentName = ref("");
|
|
32
|
-
const error = ref("");
|
|
33
|
-
const accept = computed(() => props.constraints?.allowedMimetypes?.join(","));
|
|
34
|
-
|
|
35
|
-
function validate(file: File): boolean {
|
|
36
|
-
const { maxSize, allowedMimetypes } = props.constraints ?? {};
|
|
37
|
-
if (maxSize !== undefined && file.size > maxSize) {
|
|
38
|
-
error.value = t("dms.form.file.rejected_size", {
|
|
39
|
-
name: file.name,
|
|
40
|
-
size: `${maxSize} B`,
|
|
41
|
-
});
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
if (
|
|
45
|
-
allowedMimetypes?.length &&
|
|
46
|
-
!allowedMimetypes.some((mime) =>
|
|
47
|
-
mime.endsWith("/*")
|
|
48
|
-
? file.type.startsWith(mime.slice(0, -1))
|
|
49
|
-
: mime === file.type,
|
|
50
|
-
)
|
|
51
|
-
) {
|
|
52
|
-
error.value = t("dms.form.file.rejected_type", { name: file.name });
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function upload(file: File): Promise<void> {
|
|
59
|
-
if (!validate(file)) return;
|
|
60
|
-
currentName.value = file.name;
|
|
61
|
-
progress.value = 0;
|
|
62
|
-
const presign = await $authFetch<SupportPresign>(
|
|
63
|
-
"/api/saas/support/uploads",
|
|
64
|
-
{
|
|
65
|
-
method: "POST",
|
|
66
|
-
body: { filename: file.name, size: file.size, mimetype: file.type },
|
|
67
|
-
},
|
|
68
|
-
);
|
|
69
|
-
await uploadWithProgress(presign, file, (value: number) => {
|
|
70
|
-
progress.value = value;
|
|
71
|
-
});
|
|
72
|
-
names.value.set(presign.resourceKey, file.name);
|
|
73
|
-
emit(
|
|
74
|
-
"update:modelValue",
|
|
75
|
-
props.multiple
|
|
76
|
-
? [...props.modelValue, presign.resourceKey]
|
|
77
|
-
: [presign.resourceKey],
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
watch(selected, async (value) => {
|
|
82
|
-
if (!value || props.disabled || isUploading.value) return;
|
|
83
|
-
isUploading.value = true;
|
|
84
|
-
emit("busy", true);
|
|
85
|
-
error.value = "";
|
|
86
|
-
try {
|
|
87
|
-
const files = Array.isArray(value) ? value : [value];
|
|
88
|
-
for (const file of props.multiple ? files : files.slice(0, 1))
|
|
89
|
-
await upload(file);
|
|
90
|
-
} catch {
|
|
91
|
-
error.value = t("saas.support.error.upload");
|
|
92
|
-
} finally {
|
|
93
|
-
selected.value = null;
|
|
94
|
-
isUploading.value = false;
|
|
95
|
-
emit("busy", false);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
function remove(key: string): void {
|
|
100
|
-
if (props.disabled || isUploading.value) return;
|
|
101
|
-
emit(
|
|
102
|
-
"update:modelValue",
|
|
103
|
-
props.modelValue.filter((value) => value !== key),
|
|
104
|
-
);
|
|
105
|
-
names.value.delete(key);
|
|
106
|
-
}
|
|
107
|
-
</script>
|
|
108
|
-
|
|
109
|
-
<template>
|
|
110
|
-
<div class="flex flex-col gap-2">
|
|
111
|
-
<UFileUpload
|
|
112
|
-
v-model="selected"
|
|
113
|
-
:multiple="multiple"
|
|
114
|
-
:accept="accept"
|
|
115
|
-
:disabled="disabled || isUploading"
|
|
116
|
-
/>
|
|
117
|
-
<div v-if="isUploading" role="status" class="bg-elevated rounded-md p-2">
|
|
118
|
-
<span class="text-sm">{{ currentName }}</span>
|
|
119
|
-
<UProgress :model-value="progress" />
|
|
120
|
-
</div>
|
|
121
|
-
<p v-if="error" role="alert" class="text-error text-sm">{{ error }}</p>
|
|
122
|
-
<div
|
|
123
|
-
v-for="key in modelValue"
|
|
124
|
-
:key="key"
|
|
125
|
-
class="bg-elevated flex items-center justify-between gap-2 rounded-md p-2"
|
|
126
|
-
>
|
|
127
|
-
<span class="min-w-0 truncate text-sm">
|
|
128
|
-
{{ names.get(key) ?? $t("saas.support.attachments") }}
|
|
129
|
-
</span>
|
|
130
|
-
<UButton
|
|
131
|
-
icon="i-lucide-x"
|
|
132
|
-
color="neutral"
|
|
133
|
-
variant="ghost"
|
|
134
|
-
:aria-label="$t('saas.support.remove_attachment')"
|
|
135
|
-
:disabled="disabled || isUploading"
|
|
136
|
-
@click="remove(key)"
|
|
137
|
-
/>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
</template>
|