@antelopejs/dms-saas 0.3.1 → 0.3.3
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/public/register.js +1 -3
- package/dist/routes/public/register.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/routes/tenant/tenant-billing.d.ts +2 -1
- package/dist/routes/tenant/tenant-billing.js +14 -11
- package/dist/routes/tenant/tenant-billing.js.map +1 -1
- package/dist/stripe/webhook-handlers.js +19 -20
- package/dist/stripe/webhook-handlers.js.map +1 -1
- package/dist/stripe/webhook-shared.js +9 -11
- package/dist/stripe/webhook-shared.js.map +1 -1
- package/dist/types/config.d.ts +0 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/keyed-insert.d.ts +7 -0
- package/dist/utils/keyed-insert.js +14 -0
- package/dist/utils/keyed-insert.js.map +1 -0
- package/dist/workspaces/provisioning.js +22 -27
- package/dist/workspaces/provisioning.js.map +1 -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 +7 -9
- 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,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>
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed } from "vue";
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
messages: SupportPageView<SupportMessageView>;
|
|
6
|
-
events: SupportPageView<SupportTicketEventView>;
|
|
7
|
-
assigneeLabels?: Record<string, string>;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface HistoryEmits {
|
|
11
|
-
attachment: [resourceKey: string];
|
|
12
|
-
messagesPage: [page: number];
|
|
13
|
-
eventsPage: [page: number];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
17
|
-
assigneeLabels: () => ({}),
|
|
18
|
-
});
|
|
19
|
-
const emit = defineEmits<HistoryEmits>();
|
|
20
|
-
const { t } = useI18n();
|
|
21
|
-
|
|
22
|
-
const chronologicalMessages = computed(() =>
|
|
23
|
-
[...props.messages.results].reverse(),
|
|
24
|
-
);
|
|
25
|
-
const messagesPage = computed(
|
|
26
|
-
() => Math.floor(props.messages.offset / props.messages.limit) + 1,
|
|
27
|
-
);
|
|
28
|
-
const eventsPage = computed(
|
|
29
|
-
() => Math.floor(props.events.offset / props.events.limit) + 1,
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
function formatDate(value: string): string {
|
|
33
|
-
return new Intl.DateTimeFormat(undefined, {
|
|
34
|
-
dateStyle: "medium",
|
|
35
|
-
timeStyle: "short",
|
|
36
|
-
}).format(new Date(value));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function eventValue(
|
|
40
|
-
event: SupportTicketEventView,
|
|
41
|
-
value: string | null,
|
|
42
|
-
): string {
|
|
43
|
-
if (!value) return t("saas.support.platform.unassigned");
|
|
44
|
-
if (event.type === "status_changed")
|
|
45
|
-
return t(`saas.support.status.${value}`);
|
|
46
|
-
return props.assigneeLabels[value] ?? value;
|
|
47
|
-
}
|
|
48
|
-
</script>
|
|
49
|
-
|
|
50
|
-
<template>
|
|
51
|
-
<section class="flex flex-col gap-4">
|
|
52
|
-
<div class="flex items-center justify-between gap-3">
|
|
53
|
-
<h3 class="font-medium">{{ $t("saas.support.conversation") }}</h3>
|
|
54
|
-
<span v-if="messagesPage === 1" class="text-muted text-xs">
|
|
55
|
-
{{ $t("saas.support.pagination.latest_page") }}
|
|
56
|
-
</span>
|
|
57
|
-
</div>
|
|
58
|
-
<article
|
|
59
|
-
v-for="message in chronologicalMessages"
|
|
60
|
-
:key="message._id"
|
|
61
|
-
class="rounded-lg p-4"
|
|
62
|
-
:class="
|
|
63
|
-
message.authorType === 'platform' ? 'bg-primary/10' : 'bg-elevated'
|
|
64
|
-
"
|
|
65
|
-
>
|
|
66
|
-
<div class="text-muted mb-2 flex justify-between text-xs">
|
|
67
|
-
<span>{{ $t(`saas.support.author.${message.authorType}`) }}</span>
|
|
68
|
-
<time>{{ formatDate(message.createdAt) }}</time>
|
|
69
|
-
</div>
|
|
70
|
-
<p class="whitespace-pre-wrap text-sm">{{ message.body }}</p>
|
|
71
|
-
<div
|
|
72
|
-
v-if="message.attachments.length"
|
|
73
|
-
class="mt-3 flex flex-wrap gap-2"
|
|
74
|
-
>
|
|
75
|
-
<UButton
|
|
76
|
-
v-for="(attachment, index) in message.attachments"
|
|
77
|
-
:key="attachment"
|
|
78
|
-
size="xs"
|
|
79
|
-
color="neutral"
|
|
80
|
-
variant="outline"
|
|
81
|
-
icon="i-ph-paperclip"
|
|
82
|
-
@click="emit('attachment', attachment)"
|
|
83
|
-
>
|
|
84
|
-
{{ $t("saas.support.attachment", { number: index + 1 }) }}
|
|
85
|
-
</UButton>
|
|
86
|
-
</div>
|
|
87
|
-
</article>
|
|
88
|
-
<div
|
|
89
|
-
v-if="messages.total > messages.limit"
|
|
90
|
-
class="flex flex-col items-center gap-1"
|
|
91
|
-
>
|
|
92
|
-
<span class="text-muted text-xs">
|
|
93
|
-
{{ $t("saas.support.pagination.older_messages") }}
|
|
94
|
-
</span>
|
|
95
|
-
<UPagination
|
|
96
|
-
:page="messagesPage"
|
|
97
|
-
:total="messages.total"
|
|
98
|
-
:items-per-page="messages.limit"
|
|
99
|
-
size="sm"
|
|
100
|
-
@update:page="emit('messagesPage', $event)"
|
|
101
|
-
/>
|
|
102
|
-
</div>
|
|
103
|
-
</section>
|
|
104
|
-
|
|
105
|
-
<USeparator />
|
|
106
|
-
<section class="flex flex-col gap-3">
|
|
107
|
-
<h3 class="font-medium">{{ $t("saas.support.history.title") }}</h3>
|
|
108
|
-
<p v-if="!events.results.length" class="text-muted text-sm">
|
|
109
|
-
{{ $t("saas.support.history.empty") }}
|
|
110
|
-
</p>
|
|
111
|
-
<div
|
|
112
|
-
v-for="event in events.results"
|
|
113
|
-
:key="event._id"
|
|
114
|
-
class="bg-elevated rounded-lg p-3 text-sm"
|
|
115
|
-
>
|
|
116
|
-
<p>
|
|
117
|
-
{{
|
|
118
|
-
$t(`saas.support.history.${event.type}`, {
|
|
119
|
-
actor: event.actorName,
|
|
120
|
-
previous: eventValue(event, event.previousValue),
|
|
121
|
-
next: eventValue(event, event.newValue),
|
|
122
|
-
})
|
|
123
|
-
}}
|
|
124
|
-
</p>
|
|
125
|
-
<time class="text-muted text-xs">
|
|
126
|
-
{{ formatDate(event.createdAt) }}
|
|
127
|
-
</time>
|
|
128
|
-
</div>
|
|
129
|
-
<UPagination
|
|
130
|
-
v-if="events.total > events.limit"
|
|
131
|
-
:page="eventsPage"
|
|
132
|
-
:total="events.total"
|
|
133
|
-
:items-per-page="events.limit"
|
|
134
|
-
size="sm"
|
|
135
|
-
class="self-center"
|
|
136
|
-
@update:page="emit('eventsPage', $event)"
|
|
137
|
-
/>
|
|
138
|
-
</section>
|
|
139
|
-
</template>
|
|
@@ -1,415 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, reactive, ref } from "vue";
|
|
3
|
-
import { sendSupportRequest } from "../composables/useSupportRequest";
|
|
4
|
-
import SupportAttachmentUpload from "./SupportAttachmentUpload.vue";
|
|
5
|
-
import SupportThreadHistory from "./SupportThreadHistory.vue";
|
|
6
|
-
|
|
7
|
-
const { $authFetch } = useAuthFetch();
|
|
8
|
-
const { t } = useI18n();
|
|
9
|
-
const toast = useToast();
|
|
10
|
-
const { resolveApiError } = useApiErrorMessage();
|
|
11
|
-
|
|
12
|
-
const tickets = ref<SupportTicketView[]>([]);
|
|
13
|
-
const selected = ref<SupportThreadView | null>(null);
|
|
14
|
-
const config = ref<SupportConfigView | null>(null);
|
|
15
|
-
const isLoading = ref(true);
|
|
16
|
-
const isSubmitting = ref(false);
|
|
17
|
-
const isReplyUploading = ref(false);
|
|
18
|
-
const isDraftUploading = ref(false);
|
|
19
|
-
const isCreateOpen = ref(false);
|
|
20
|
-
const replyBody = ref("");
|
|
21
|
-
const replyAttachments = ref<string[]>([]);
|
|
22
|
-
const PAGE_SIZE = 20;
|
|
23
|
-
const listPage = ref(1);
|
|
24
|
-
const messagesPage = ref(1);
|
|
25
|
-
const eventsPage = ref(1);
|
|
26
|
-
const ticketTotal = ref(0);
|
|
27
|
-
let selectionRequest = 0;
|
|
28
|
-
let messagesRequest = 0;
|
|
29
|
-
let eventsRequest = 0;
|
|
30
|
-
let listRequest = 0;
|
|
31
|
-
|
|
32
|
-
const draft = reactive<SupportTicketDraft>({
|
|
33
|
-
subject: "",
|
|
34
|
-
category: "question",
|
|
35
|
-
priority: "normal",
|
|
36
|
-
body: "",
|
|
37
|
-
attachments: [],
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const categoryItems = [
|
|
41
|
-
"question",
|
|
42
|
-
"incident",
|
|
43
|
-
"billing",
|
|
44
|
-
"feature_request",
|
|
45
|
-
].map((value) => ({ value, label: t(`saas.support.category.${value}`) }));
|
|
46
|
-
const priorityItems = computed(() =>
|
|
47
|
-
(config.value?.policy.priorities ?? []).map((value) => ({
|
|
48
|
-
value,
|
|
49
|
-
label: t(`saas.support.priority.${value}`),
|
|
50
|
-
})),
|
|
51
|
-
);
|
|
52
|
-
const attachmentConstraints = computed(() => ({
|
|
53
|
-
maxSize: config.value?.maxAttachmentSize,
|
|
54
|
-
allowedMimetypes: config.value?.allowedMimetypes,
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
function formatDate(value: string): string {
|
|
58
|
-
return new Intl.DateTimeFormat(undefined, {
|
|
59
|
-
dateStyle: "medium",
|
|
60
|
-
timeStyle: "short",
|
|
61
|
-
}).format(new Date(value));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function resetDraft(): void {
|
|
65
|
-
draft.subject = "";
|
|
66
|
-
draft.category = "question";
|
|
67
|
-
draft.priority = config.value?.policy.priorities[0] ?? "normal";
|
|
68
|
-
draft.body = "";
|
|
69
|
-
draft.attachments = [];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function pageQuery(page: number, sortKey: string): string {
|
|
73
|
-
return new URLSearchParams({
|
|
74
|
-
offset: String((page - 1) * PAGE_SIZE),
|
|
75
|
-
limit: String(PAGE_SIZE),
|
|
76
|
-
sortKey,
|
|
77
|
-
sortDirection: "desc",
|
|
78
|
-
}).toString();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function load(): Promise<void> {
|
|
82
|
-
const request = ++listRequest;
|
|
83
|
-
isLoading.value = true;
|
|
84
|
-
try {
|
|
85
|
-
const [loadedConfig, loadedTickets] = await Promise.all([
|
|
86
|
-
$authFetch<SupportConfigView>("/api/saas/support/config"),
|
|
87
|
-
$authFetch<SupportPageView<SupportTicketView>>(
|
|
88
|
-
`/api/saas/tenant/tables/support-tickets/list?${pageQuery(listPage.value, "lastMessageAt")}`,
|
|
89
|
-
),
|
|
90
|
-
]);
|
|
91
|
-
if (request !== listRequest) return;
|
|
92
|
-
config.value = loadedConfig;
|
|
93
|
-
tickets.value = loadedTickets.results;
|
|
94
|
-
ticketTotal.value = loadedTickets.total;
|
|
95
|
-
if (!loadedConfig.policy.priorities.includes(draft.priority)) resetDraft();
|
|
96
|
-
} catch (error) {
|
|
97
|
-
showError(error, "saas.support.error.load");
|
|
98
|
-
} finally {
|
|
99
|
-
if (request === listRequest) isLoading.value = false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function selectTicket(ticketId: string): Promise<void> {
|
|
104
|
-
messagesPage.value = 1;
|
|
105
|
-
eventsPage.value = 1;
|
|
106
|
-
const request = ++selectionRequest;
|
|
107
|
-
const messageRequest = ++messagesRequest;
|
|
108
|
-
const eventRequest = ++eventsRequest;
|
|
109
|
-
const baseUrl = `/api/saas/support/${ticketId}`;
|
|
110
|
-
try {
|
|
111
|
-
const [detail, messages, events] = await Promise.all([
|
|
112
|
-
$authFetch<SupportTicketDetailView>(baseUrl),
|
|
113
|
-
$authFetch<SupportPageView<SupportMessageView>>(
|
|
114
|
-
`${baseUrl}/messages/list?${pageQuery(messagesPage.value, "createdAt")}`,
|
|
115
|
-
),
|
|
116
|
-
$authFetch<SupportPageView<SupportTicketEventView>>(
|
|
117
|
-
`${baseUrl}/events/list?${pageQuery(eventsPage.value, "createdAt")}`,
|
|
118
|
-
),
|
|
119
|
-
]);
|
|
120
|
-
if (
|
|
121
|
-
request === selectionRequest &&
|
|
122
|
-
messageRequest === messagesRequest &&
|
|
123
|
-
eventRequest === eventsRequest
|
|
124
|
-
) {
|
|
125
|
-
selected.value = { ticket: detail.ticket, messages, events };
|
|
126
|
-
}
|
|
127
|
-
} catch (error) {
|
|
128
|
-
showError(error, "saas.support.error.load");
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function createTicket(): Promise<void> {
|
|
133
|
-
if (!draft.subject.trim() || !draft.body.trim()) return;
|
|
134
|
-
isSubmitting.value = true;
|
|
135
|
-
try {
|
|
136
|
-
const ticket = await sendSupportRequest(
|
|
137
|
-
`${config.value?.uploadPath}:create`,
|
|
138
|
-
draft,
|
|
139
|
-
(body) =>
|
|
140
|
-
$authFetch<SupportTicketView>("/api/saas/support", {
|
|
141
|
-
method: "POST",
|
|
142
|
-
body,
|
|
143
|
-
}),
|
|
144
|
-
);
|
|
145
|
-
isCreateOpen.value = false;
|
|
146
|
-
resetDraft();
|
|
147
|
-
listPage.value = 1;
|
|
148
|
-
await load();
|
|
149
|
-
await selectTicket(ticket._id);
|
|
150
|
-
toast.add({
|
|
151
|
-
title: t("saas.support.tenant.created"),
|
|
152
|
-
color: "success",
|
|
153
|
-
});
|
|
154
|
-
} catch (error) {
|
|
155
|
-
showError(error, "saas.support.error.create");
|
|
156
|
-
} finally {
|
|
157
|
-
isSubmitting.value = false;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function sendReply(): Promise<void> {
|
|
162
|
-
if (!selected.value || !replyBody.value.trim()) return;
|
|
163
|
-
const ticketId = selected.value.ticket._id;
|
|
164
|
-
isSubmitting.value = true;
|
|
165
|
-
try {
|
|
166
|
-
await sendSupportRequest(
|
|
167
|
-
`${config.value?.uploadPath}:${ticketId}:reply`,
|
|
168
|
-
{ body: replyBody.value, attachments: replyAttachments.value },
|
|
169
|
-
(body) =>
|
|
170
|
-
$authFetch(`/api/saas/support/${ticketId}/messages`, {
|
|
171
|
-
method: "POST",
|
|
172
|
-
body,
|
|
173
|
-
}),
|
|
174
|
-
);
|
|
175
|
-
replyBody.value = "";
|
|
176
|
-
replyAttachments.value = [];
|
|
177
|
-
if (selected.value?.ticket._id === ticketId) await selectTicket(ticketId);
|
|
178
|
-
await load();
|
|
179
|
-
} catch (error) {
|
|
180
|
-
showError(error, "saas.support.error.reply");
|
|
181
|
-
} finally {
|
|
182
|
-
isSubmitting.value = false;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function openAttachment(
|
|
187
|
-
ticketId: string,
|
|
188
|
-
resourceKey: string,
|
|
189
|
-
): Promise<void> {
|
|
190
|
-
try {
|
|
191
|
-
const query = new URLSearchParams({ resourceKey });
|
|
192
|
-
const file = await $authFetch<SupportAttachmentView>(
|
|
193
|
-
`/api/saas/support/${ticketId}/attachment?${query}`,
|
|
194
|
-
);
|
|
195
|
-
window.open(file.url, "_blank", "noopener,noreferrer");
|
|
196
|
-
} catch (error) {
|
|
197
|
-
showError(error, "saas.support.error.attachment");
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function showError(error: unknown, fallback: string): void {
|
|
202
|
-
toast.add({
|
|
203
|
-
title: resolveApiError(error, fallback),
|
|
204
|
-
color: "error",
|
|
205
|
-
icon: "i-ph-warning-circle",
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
onMounted(load);
|
|
210
|
-
|
|
211
|
-
function changeListPage(page: number): void {
|
|
212
|
-
listPage.value = page;
|
|
213
|
-
void load();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function changeThreadPage(kind: "messages" | "events", page: number): void {
|
|
217
|
-
if (!selected.value) return;
|
|
218
|
-
const pages = { messages: messagesPage, events: eventsPage };
|
|
219
|
-
pages[kind].value = page;
|
|
220
|
-
void loadThreadPage(kind, selected.value.ticket._id);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async function loadThreadPage(
|
|
224
|
-
kind: "messages" | "events",
|
|
225
|
-
ticketId: string,
|
|
226
|
-
): Promise<void> {
|
|
227
|
-
const requests = { messages: messagesRequest, events: eventsRequest };
|
|
228
|
-
const request = ++requests[kind];
|
|
229
|
-
if (kind === "messages") messagesRequest = request;
|
|
230
|
-
else eventsRequest = request;
|
|
231
|
-
try {
|
|
232
|
-
const page = await $authFetch<
|
|
233
|
-
SupportPageView<SupportMessageView | SupportTicketEventView>
|
|
234
|
-
>(
|
|
235
|
-
`/api/saas/support/${ticketId}/${kind}/list?${pageQuery(kind === "messages" ? messagesPage.value : eventsPage.value, "createdAt")}`,
|
|
236
|
-
);
|
|
237
|
-
const currentRequest =
|
|
238
|
-
kind === "messages" ? messagesRequest : eventsRequest;
|
|
239
|
-
if (request !== currentRequest || selected.value?.ticket._id !== ticketId)
|
|
240
|
-
return;
|
|
241
|
-
if (kind === "messages") {
|
|
242
|
-
selected.value.messages = page as SupportPageView<SupportMessageView>;
|
|
243
|
-
} else {
|
|
244
|
-
selected.value.events = page as SupportPageView<SupportTicketEventView>;
|
|
245
|
-
}
|
|
246
|
-
} catch (error) {
|
|
247
|
-
showError(error, "saas.support.error.load");
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
</script>
|
|
251
|
-
|
|
252
|
-
<template>
|
|
253
|
-
<div class="grid gap-6 xl:grid-cols-[22rem_1fr]">
|
|
254
|
-
<div class="flex flex-col gap-4">
|
|
255
|
-
<UAlert
|
|
256
|
-
v-if="config"
|
|
257
|
-
color="primary"
|
|
258
|
-
variant="subtle"
|
|
259
|
-
icon="i-ph-clock-countdown"
|
|
260
|
-
:title="$t(`saas.support.sla.${config.policy.level}.title`)"
|
|
261
|
-
:description="$t(`saas.support.sla.${config.policy.level}.description`)"
|
|
262
|
-
/>
|
|
263
|
-
<UButton icon="i-ph-plus" block @click="isCreateOpen = true">
|
|
264
|
-
{{ $t("saas.support.tenant.new") }}
|
|
265
|
-
</UButton>
|
|
266
|
-
<USkeleton v-if="isLoading" class="h-32" />
|
|
267
|
-
<UCard v-else-if="!tickets.length">
|
|
268
|
-
<p class="text-muted text-center text-sm">
|
|
269
|
-
{{ $t("saas.support.tenant.empty") }}
|
|
270
|
-
</p>
|
|
271
|
-
</UCard>
|
|
272
|
-
<template v-else>
|
|
273
|
-
<button
|
|
274
|
-
v-for="ticket in tickets"
|
|
275
|
-
:key="ticket._id"
|
|
276
|
-
class="border-default hover:bg-elevated rounded-lg border p-4 text-left"
|
|
277
|
-
:class="{
|
|
278
|
-
'ring-primary ring-2': selected?.ticket._id === ticket._id,
|
|
279
|
-
}"
|
|
280
|
-
@click="selectTicket(ticket._id)"
|
|
281
|
-
>
|
|
282
|
-
<div class="flex items-start justify-between gap-2">
|
|
283
|
-
<p class="font-medium">{{ ticket.subject }}</p>
|
|
284
|
-
<UBadge size="sm" variant="subtle">
|
|
285
|
-
{{ $t(`saas.support.status.${ticket.status}`) }}
|
|
286
|
-
</UBadge>
|
|
287
|
-
</div>
|
|
288
|
-
<p class="text-muted mt-2 text-xs">
|
|
289
|
-
{{ formatDate(ticket.lastMessageAt) }}
|
|
290
|
-
</p>
|
|
291
|
-
</button>
|
|
292
|
-
<UPagination
|
|
293
|
-
v-if="ticketTotal > PAGE_SIZE"
|
|
294
|
-
:page="listPage"
|
|
295
|
-
:total="ticketTotal"
|
|
296
|
-
:items-per-page="PAGE_SIZE"
|
|
297
|
-
size="sm"
|
|
298
|
-
class="self-center"
|
|
299
|
-
@update:page="changeListPage"
|
|
300
|
-
/>
|
|
301
|
-
</template>
|
|
302
|
-
</div>
|
|
303
|
-
|
|
304
|
-
<UCard v-if="selected">
|
|
305
|
-
<template #header>
|
|
306
|
-
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
307
|
-
<div>
|
|
308
|
-
<h2 class="text-lg font-semibold">
|
|
309
|
-
{{ selected.ticket.subject }}
|
|
310
|
-
</h2>
|
|
311
|
-
<p class="text-muted text-sm">
|
|
312
|
-
{{ $t(`saas.support.category.${selected.ticket.category}`) }}
|
|
313
|
-
·
|
|
314
|
-
{{ $t(`saas.support.priority.${selected.ticket.priority}`) }}
|
|
315
|
-
</p>
|
|
316
|
-
</div>
|
|
317
|
-
<UBadge>
|
|
318
|
-
{{ $t(`saas.support.status.${selected.ticket.status}`) }}
|
|
319
|
-
</UBadge>
|
|
320
|
-
</div>
|
|
321
|
-
</template>
|
|
322
|
-
<div class="flex flex-col gap-4">
|
|
323
|
-
<SupportThreadHistory
|
|
324
|
-
:messages="selected.messages"
|
|
325
|
-
:events="selected.events"
|
|
326
|
-
@attachment="openAttachment(selected.ticket._id, $event)"
|
|
327
|
-
@messages-page="changeThreadPage('messages', $event)"
|
|
328
|
-
@events-page="changeThreadPage('events', $event)"
|
|
329
|
-
/>
|
|
330
|
-
<USeparator />
|
|
331
|
-
<UTextarea
|
|
332
|
-
v-model="replyBody"
|
|
333
|
-
:placeholder="$t('saas.support.reply_placeholder')"
|
|
334
|
-
:rows="4"
|
|
335
|
-
class="w-full"
|
|
336
|
-
/>
|
|
337
|
-
<SupportAttachmentUpload
|
|
338
|
-
v-if="config"
|
|
339
|
-
v-model="replyAttachments"
|
|
340
|
-
multiple
|
|
341
|
-
:constraints="attachmentConstraints"
|
|
342
|
-
:disabled="isSubmitting"
|
|
343
|
-
@busy="isReplyUploading = $event"
|
|
344
|
-
/>
|
|
345
|
-
<div class="flex justify-end">
|
|
346
|
-
<UButton
|
|
347
|
-
:loading="isSubmitting"
|
|
348
|
-
:disabled="isReplyUploading"
|
|
349
|
-
@click="sendReply"
|
|
350
|
-
>
|
|
351
|
-
{{ $t("saas.support.send") }}
|
|
352
|
-
</UButton>
|
|
353
|
-
</div>
|
|
354
|
-
</div>
|
|
355
|
-
</UCard>
|
|
356
|
-
<UCard v-else class="min-h-64">
|
|
357
|
-
<div class="text-muted flex h-full items-center justify-center text-sm">
|
|
358
|
-
{{ $t("saas.support.tenant.select") }}
|
|
359
|
-
</div>
|
|
360
|
-
</UCard>
|
|
361
|
-
</div>
|
|
362
|
-
|
|
363
|
-
<UModal v-model:open="isCreateOpen" :title="$t('saas.support.tenant.new')">
|
|
364
|
-
<template #body>
|
|
365
|
-
<div class="flex flex-col gap-4">
|
|
366
|
-
<UFormField :label="$t('saas.support.subject')" required>
|
|
367
|
-
<UInput v-model="draft.subject" class="w-full" />
|
|
368
|
-
</UFormField>
|
|
369
|
-
<div class="grid gap-4 sm:grid-cols-2">
|
|
370
|
-
<UFormField :label="$t('saas.support.category.label')" required>
|
|
371
|
-
<USelect
|
|
372
|
-
v-model="draft.category"
|
|
373
|
-
:items="categoryItems"
|
|
374
|
-
class="w-full"
|
|
375
|
-
/>
|
|
376
|
-
</UFormField>
|
|
377
|
-
<UFormField :label="$t('saas.support.priority.label')" required>
|
|
378
|
-
<USelect
|
|
379
|
-
v-model="draft.priority"
|
|
380
|
-
:items="priorityItems"
|
|
381
|
-
class="w-full"
|
|
382
|
-
/>
|
|
383
|
-
</UFormField>
|
|
384
|
-
</div>
|
|
385
|
-
<UFormField :label="$t('saas.support.message')" required>
|
|
386
|
-
<UTextarea v-model="draft.body" :rows="5" class="w-full" />
|
|
387
|
-
</UFormField>
|
|
388
|
-
<UFormField :label="$t('saas.support.attachments')">
|
|
389
|
-
<SupportAttachmentUpload
|
|
390
|
-
v-if="config"
|
|
391
|
-
v-model="draft.attachments"
|
|
392
|
-
multiple
|
|
393
|
-
:constraints="attachmentConstraints"
|
|
394
|
-
:disabled="isSubmitting"
|
|
395
|
-
@busy="isDraftUploading = $event"
|
|
396
|
-
/>
|
|
397
|
-
</UFormField>
|
|
398
|
-
</div>
|
|
399
|
-
</template>
|
|
400
|
-
<template #footer>
|
|
401
|
-
<div class="flex w-full justify-end gap-2">
|
|
402
|
-
<UButton color="neutral" variant="ghost" @click="isCreateOpen = false">
|
|
403
|
-
{{ $t("common.cancel") }}
|
|
404
|
-
</UButton>
|
|
405
|
-
<UButton
|
|
406
|
-
:loading="isSubmitting"
|
|
407
|
-
:disabled="isDraftUploading"
|
|
408
|
-
@click="createTicket"
|
|
409
|
-
>
|
|
410
|
-
{{ $t("saas.support.tenant.create") }}
|
|
411
|
-
</UButton>
|
|
412
|
-
</div>
|
|
413
|
-
</template>
|
|
414
|
-
</UModal>
|
|
415
|
-
</template>
|