@escalated-dev/escalated 0.3.5 → 0.4.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/package.json +1 -1
- package/src/components/BulkActionBar.vue +97 -0
- package/src/components/EscalatedLayout.vue +220 -219
- package/src/components/FollowButton.vue +63 -0
- package/src/components/KeyboardShortcutHelp.vue +123 -0
- package/src/components/MacroDropdown.vue +90 -0
- package/src/components/PinnedNotes.vue +87 -0
- package/src/components/PresenceIndicator.vue +103 -0
- package/src/components/QuickFilters.vue +49 -0
- package/src/components/ReplyThread.vue +60 -38
- package/src/components/SatisfactionRating.vue +128 -0
- package/src/components/TicketFilters.vue +69 -58
- package/src/components/TicketList.vue +193 -94
- package/src/components/TicketSidebar.vue +99 -77
- package/src/composables/useKeyboardShortcuts.js +46 -0
- package/src/index.js +11 -0
- package/src/pages/Admin/Macros/Index.vue +287 -0
- package/src/pages/Admin/Reports.vue +101 -66
- package/src/pages/Admin/Tickets/Index.vue +39 -22
- package/src/pages/Admin/Tickets/Show.vue +145 -109
- package/src/pages/Agent/Dashboard.vue +156 -63
- package/src/pages/Agent/TicketIndex.vue +38 -21
- package/src/pages/Agent/TicketShow.vue +144 -108
- package/src/pages/Customer/Show.vue +63 -55
- package/src/pages/Guest/Show.vue +93 -86
|
@@ -1,55 +1,63 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import EscalatedLayout from '../../components/EscalatedLayout.vue';
|
|
3
|
-
import StatusBadge from '../../components/StatusBadge.vue';
|
|
4
|
-
import PriorityBadge from '../../components/PriorityBadge.vue';
|
|
5
|
-
import ReplyThread from '../../components/ReplyThread.vue';
|
|
6
|
-
import ReplyComposer from '../../components/ReplyComposer.vue';
|
|
7
|
-
import AttachmentList from '../../components/AttachmentList.vue';
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<span
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<div
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
</
|
|
1
|
+
<script setup>
|
|
2
|
+
import EscalatedLayout from '../../components/EscalatedLayout.vue';
|
|
3
|
+
import StatusBadge from '../../components/StatusBadge.vue';
|
|
4
|
+
import PriorityBadge from '../../components/PriorityBadge.vue';
|
|
5
|
+
import ReplyThread from '../../components/ReplyThread.vue';
|
|
6
|
+
import ReplyComposer from '../../components/ReplyComposer.vue';
|
|
7
|
+
import AttachmentList from '../../components/AttachmentList.vue';
|
|
8
|
+
import SatisfactionRating from '../../components/SatisfactionRating.vue';
|
|
9
|
+
import { router, usePage } from '@inertiajs/vue3';
|
|
10
|
+
|
|
11
|
+
const props = defineProps({ ticket: Object });
|
|
12
|
+
const page = usePage();
|
|
13
|
+
|
|
14
|
+
const isResolved = ['resolved', 'closed'].includes(props.ticket.status);
|
|
15
|
+
|
|
16
|
+
function closeTicket() {
|
|
17
|
+
router.post(route('escalated.customer.tickets.close', props.ticket.reference));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function reopenTicket() {
|
|
21
|
+
router.post(route('escalated.customer.tickets.reopen', props.ticket.reference));
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<EscalatedLayout :title="ticket.subject">
|
|
27
|
+
<div class="mb-4 flex flex-wrap items-center gap-3">
|
|
28
|
+
<span class="text-sm font-medium text-neutral-500">{{ ticket.reference }}</span>
|
|
29
|
+
<StatusBadge :status="ticket.status" />
|
|
30
|
+
<PriorityBadge :priority="ticket.priority" />
|
|
31
|
+
<span v-if="ticket.department" class="text-sm text-neutral-500">{{ ticket.department.name }}</span>
|
|
32
|
+
<div class="ml-auto flex gap-2">
|
|
33
|
+
<button v-if="ticket.status === 'resolved' || ticket.status === 'closed'"
|
|
34
|
+
@click="reopenTicket"
|
|
35
|
+
class="rounded-lg border border-gray-300 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50">
|
|
36
|
+
Reopen
|
|
37
|
+
</button>
|
|
38
|
+
<button v-if="ticket.status !== 'closed' && ticket.status !== 'resolved'"
|
|
39
|
+
@click="closeTicket"
|
|
40
|
+
class="rounded-lg border border-red-300 px-3 py-1.5 text-sm text-red-600 hover:bg-red-50">
|
|
41
|
+
Close Ticket
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- CSAT Rating -->
|
|
47
|
+
<SatisfactionRating v-if="isResolved && !ticket.satisfaction_rating"
|
|
48
|
+
:action="route('escalated.customer.tickets.rate', ticket.reference)" />
|
|
49
|
+
|
|
50
|
+
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-4">
|
|
51
|
+
<p class="whitespace-pre-wrap text-sm text-gray-700">{{ ticket.description }}</p>
|
|
52
|
+
<AttachmentList v-if="ticket.attachments?.length" :attachments="ticket.attachments" class="mt-3" />
|
|
53
|
+
</div>
|
|
54
|
+
<div class="mb-6">
|
|
55
|
+
<h2 class="mb-4 text-lg font-semibold text-gray-900">Replies</h2>
|
|
56
|
+
<ReplyThread :replies="ticket.replies || []" :current-user-id="page.props.auth?.user?.id" />
|
|
57
|
+
</div>
|
|
58
|
+
<div v-if="ticket.status !== 'closed'" class="rounded-lg border border-gray-200 bg-white p-4">
|
|
59
|
+
<h2 class="mb-4 text-lg font-semibold text-gray-900">Reply</h2>
|
|
60
|
+
<ReplyComposer :action="route('escalated.customer.tickets.reply', ticket.reference)" />
|
|
61
|
+
</div>
|
|
62
|
+
</EscalatedLayout>
|
|
63
|
+
</template>
|
package/src/pages/Guest/Show.vue
CHANGED
|
@@ -1,86 +1,93 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import EscalatedLayout from '../../components/EscalatedLayout.vue';
|
|
3
|
-
import StatusBadge from '../../components/StatusBadge.vue';
|
|
4
|
-
import PriorityBadge from '../../components/PriorityBadge.vue';
|
|
5
|
-
import ReplyThread from '../../components/ReplyThread.vue';
|
|
6
|
-
import AttachmentList from '../../components/AttachmentList.vue';
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<span
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
</
|
|
1
|
+
<script setup>
|
|
2
|
+
import EscalatedLayout from '../../components/EscalatedLayout.vue';
|
|
3
|
+
import StatusBadge from '../../components/StatusBadge.vue';
|
|
4
|
+
import PriorityBadge from '../../components/PriorityBadge.vue';
|
|
5
|
+
import ReplyThread from '../../components/ReplyThread.vue';
|
|
6
|
+
import AttachmentList from '../../components/AttachmentList.vue';
|
|
7
|
+
import SatisfactionRating from '../../components/SatisfactionRating.vue';
|
|
8
|
+
import { useForm } from '@inertiajs/vue3';
|
|
9
|
+
import { ref } from 'vue';
|
|
10
|
+
|
|
11
|
+
const props = defineProps({ ticket: Object, token: String });
|
|
12
|
+
|
|
13
|
+
const isResolved = ['resolved', 'closed'].includes(props.ticket.status);
|
|
14
|
+
|
|
15
|
+
const replyForm = useForm({ body: '', attachments: [] });
|
|
16
|
+
const showCopied = ref(false);
|
|
17
|
+
|
|
18
|
+
function submitReply() {
|
|
19
|
+
replyForm.post(route('escalated.guest.tickets.reply', props.token), {
|
|
20
|
+
onSuccess: () => replyForm.reset(),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function copyLink() {
|
|
25
|
+
navigator.clipboard.writeText(window.location.href);
|
|
26
|
+
showCopied.value = true;
|
|
27
|
+
setTimeout(() => showCopied.value = false, 2000);
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<EscalatedLayout :title="ticket.subject">
|
|
33
|
+
<div class="mx-auto max-w-3xl">
|
|
34
|
+
<!-- Bookmark notice -->
|
|
35
|
+
<div class="mb-4 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3">
|
|
36
|
+
<div class="flex items-start gap-3">
|
|
37
|
+
<svg class="mt-0.5 h-5 w-5 shrink-0 text-amber-500" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" /></svg>
|
|
38
|
+
<div>
|
|
39
|
+
<p class="text-sm font-medium text-amber-800">Bookmark this page</p>
|
|
40
|
+
<p class="mt-0.5 text-xs text-amber-600">This is your private link to view and reply to this ticket.</p>
|
|
41
|
+
</div>
|
|
42
|
+
<button @click="copyLink"
|
|
43
|
+
class="ml-auto shrink-0 rounded-md border border-amber-300 bg-white px-2.5 py-1 text-xs font-medium text-amber-700 hover:bg-amber-50">
|
|
44
|
+
{{ showCopied ? 'Copied!' : 'Copy Link' }}
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Ticket header -->
|
|
50
|
+
<div class="mb-4 flex flex-wrap items-center gap-3">
|
|
51
|
+
<span class="font-mono text-sm text-gray-500">{{ ticket.reference }}</span>
|
|
52
|
+
<StatusBadge :status="ticket.status" />
|
|
53
|
+
<PriorityBadge :priority="ticket.priority" />
|
|
54
|
+
<span v-if="ticket.department" class="text-sm text-gray-500">{{ ticket.department.name }}</span>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- CSAT Rating -->
|
|
58
|
+
<SatisfactionRating v-if="isResolved && !ticket.satisfaction_rating"
|
|
59
|
+
:action="route('escalated.guest.tickets.rate', token)" />
|
|
60
|
+
|
|
61
|
+
<!-- Description -->
|
|
62
|
+
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-4">
|
|
63
|
+
<p class="whitespace-pre-wrap text-sm text-gray-700">{{ ticket.description }}</p>
|
|
64
|
+
<AttachmentList v-if="ticket.attachments?.length" :attachments="ticket.attachments" class="mt-3" />
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Replies -->
|
|
68
|
+
<div class="mb-6">
|
|
69
|
+
<h2 class="mb-4 text-lg font-semibold text-gray-900">Replies</h2>
|
|
70
|
+
<ReplyThread :replies="ticket.replies || []" />
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<!-- Reply form -->
|
|
74
|
+
<div v-if="ticket.status !== 'closed'" class="rounded-lg border border-gray-200 bg-white p-4">
|
|
75
|
+
<h2 class="mb-4 text-lg font-semibold text-gray-900">Reply</h2>
|
|
76
|
+
<form @submit.prevent="submitReply">
|
|
77
|
+
<textarea v-model="replyForm.body" rows="4" required placeholder="Write your reply..."
|
|
78
|
+
class="w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"></textarea>
|
|
79
|
+
<div v-if="replyForm.errors.body" class="mt-1 text-sm text-red-600">{{ replyForm.errors.body }}</div>
|
|
80
|
+
<div class="mt-3 flex justify-end">
|
|
81
|
+
<button type="submit" :disabled="replyForm.processing"
|
|
82
|
+
class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50">
|
|
83
|
+
{{ replyForm.processing ? 'Sending...' : 'Send Reply' }}
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</form>
|
|
87
|
+
</div>
|
|
88
|
+
<div v-else class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-6 text-center text-sm text-gray-500">
|
|
89
|
+
This ticket is closed. No further replies can be added.
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</EscalatedLayout>
|
|
93
|
+
</template>
|