@escalated-dev/escalated 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -0
  3. package/package.json +37 -0
  4. package/src/components/ActivityTimeline.vue +62 -0
  5. package/src/components/AssigneeSelect.vue +19 -0
  6. package/src/components/AttachmentList.vue +31 -0
  7. package/src/components/EscalatedLayout.vue +213 -0
  8. package/src/components/FileDropzone.vue +36 -0
  9. package/src/components/PriorityBadge.vue +21 -0
  10. package/src/components/ReplyComposer.vue +110 -0
  11. package/src/components/ReplyThread.vue +30 -0
  12. package/src/components/SlaTimer.vue +43 -0
  13. package/src/components/StatsCard.vue +26 -0
  14. package/src/components/StatusBadge.vue +24 -0
  15. package/src/components/TagSelect.vue +49 -0
  16. package/src/components/TicketFilters.vue +52 -0
  17. package/src/components/TicketList.vue +51 -0
  18. package/src/components/TicketSidebar.vue +70 -0
  19. package/src/index.js +19 -0
  20. package/src/pages/Admin/CannedResponses/Index.vue +58 -0
  21. package/src/pages/Admin/Departments/Form.vue +50 -0
  22. package/src/pages/Admin/Departments/Index.vue +49 -0
  23. package/src/pages/Admin/EscalationRules/Form.vue +95 -0
  24. package/src/pages/Admin/EscalationRules/Index.vue +49 -0
  25. package/src/pages/Admin/Reports.vue +52 -0
  26. package/src/pages/Admin/SlaPolicies/Form.vue +74 -0
  27. package/src/pages/Admin/SlaPolicies/Index.vue +51 -0
  28. package/src/pages/Admin/Tags/Index.vue +80 -0
  29. package/src/pages/Agent/Dashboard.vue +51 -0
  30. package/src/pages/Agent/TicketIndex.vue +21 -0
  31. package/src/pages/Agent/TicketShow.vue +108 -0
  32. package/src/pages/Customer/Create.vue +66 -0
  33. package/src/pages/Customer/Index.vue +24 -0
  34. package/src/pages/Customer/Show.vue +55 -0
  35. package/src/plugin.js +65 -0
@@ -0,0 +1,95 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { useForm } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({ rule: { type: Object, default: null } });
6
+
7
+ const form = useForm({
8
+ name: props.rule?.name || '',
9
+ description: props.rule?.description || '',
10
+ trigger_type: props.rule?.trigger_type || 'time_based',
11
+ conditions: props.rule?.conditions || [{ field: '', value: '' }],
12
+ actions: props.rule?.actions || [{ type: '', value: '' }],
13
+ order: props.rule?.order ?? 0,
14
+ is_active: props.rule?.is_active ?? true,
15
+ });
16
+
17
+ function addCondition() { form.conditions.push({ field: '', value: '' }); }
18
+ function removeCondition(idx) { form.conditions.splice(idx, 1); }
19
+ function addAction() { form.actions.push({ type: '', value: '' }); }
20
+ function removeAction(idx) { form.actions.splice(idx, 1); }
21
+
22
+ function submit() {
23
+ if (props.rule) {
24
+ form.put(route('escalated.admin.escalation-rules.update', props.rule.id));
25
+ } else {
26
+ form.post(route('escalated.admin.escalation-rules.store'));
27
+ }
28
+ }
29
+ </script>
30
+
31
+ <template>
32
+ <EscalatedLayout :title="rule ? 'Edit Escalation Rule' : 'New Escalation Rule'">
33
+ <form @submit.prevent="submit" class="mx-auto max-w-lg space-y-4 rounded-lg border border-gray-200 bg-white p-6">
34
+ <div>
35
+ <label class="block text-sm font-medium text-gray-700">Name</label>
36
+ <input v-model="form.name" type="text" required class="mt-1 w-full rounded-lg border-gray-300 shadow-sm" />
37
+ </div>
38
+ <div>
39
+ <label class="block text-sm font-medium text-gray-700">Trigger Type</label>
40
+ <select v-model="form.trigger_type" class="mt-1 w-full rounded-lg border-gray-300 shadow-sm">
41
+ <option value="time_based">Time Based</option>
42
+ <option value="sla_breach">SLA Breach</option>
43
+ <option value="priority_based">Priority Based</option>
44
+ </select>
45
+ </div>
46
+ <div>
47
+ <div class="mb-2 flex items-center justify-between">
48
+ <label class="text-sm font-medium text-gray-700">Conditions</label>
49
+ <button type="button" @click="addCondition" class="text-sm text-indigo-600">+ Add</button>
50
+ </div>
51
+ <div v-for="(cond, idx) in form.conditions" :key="idx" class="mb-2 flex gap-2">
52
+ <select v-model="cond.field" class="w-1/2 rounded border-gray-300 text-sm">
53
+ <option value="status">Status</option>
54
+ <option value="priority">Priority</option>
55
+ <option value="assigned">Assignment</option>
56
+ <option value="age_hours">Age (hours)</option>
57
+ <option value="no_response_hours">No Response (hours)</option>
58
+ <option value="sla_breached">SLA Breached</option>
59
+ </select>
60
+ <input v-model="cond.value" class="w-1/2 rounded border-gray-300 text-sm" placeholder="Value" />
61
+ <button type="button" @click="removeCondition(idx)" class="text-red-500">&times;</button>
62
+ </div>
63
+ </div>
64
+ <div>
65
+ <div class="mb-2 flex items-center justify-between">
66
+ <label class="text-sm font-medium text-gray-700">Actions</label>
67
+ <button type="button" @click="addAction" class="text-sm text-indigo-600">+ Add</button>
68
+ </div>
69
+ <div v-for="(action, idx) in form.actions" :key="idx" class="mb-2 flex gap-2">
70
+ <select v-model="action.type" class="w-1/2 rounded border-gray-300 text-sm">
71
+ <option value="escalate">Escalate</option>
72
+ <option value="change_priority">Change Priority</option>
73
+ <option value="assign_to">Assign To</option>
74
+ <option value="change_department">Change Department</option>
75
+ </select>
76
+ <input v-model="action.value" class="w-1/2 rounded border-gray-300 text-sm" placeholder="Value" />
77
+ <button type="button" @click="removeAction(idx)" class="text-red-500">&times;</button>
78
+ </div>
79
+ </div>
80
+ <div>
81
+ <label class="block text-sm font-medium text-gray-700">Order</label>
82
+ <input v-model.number="form.order" type="number" min="0" class="mt-1 w-24 rounded-lg border-gray-300 shadow-sm" />
83
+ </div>
84
+ <label class="flex items-center gap-2">
85
+ <input v-model="form.is_active" type="checkbox" class="rounded border-gray-300" />
86
+ <span class="text-sm text-gray-700">Active</span>
87
+ </label>
88
+ <div class="flex justify-end">
89
+ <button type="submit" :disabled="form.processing" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50">
90
+ {{ rule ? 'Update' : 'Create' }}
91
+ </button>
92
+ </div>
93
+ </form>
94
+ </EscalatedLayout>
95
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { Link, router } from '@inertiajs/vue3';
4
+
5
+ defineProps({ rules: Array });
6
+
7
+ function destroy(id) {
8
+ if (confirm('Delete this escalation rule?')) {
9
+ router.delete(route('escalated.admin.escalation-rules.destroy', id));
10
+ }
11
+ }
12
+ </script>
13
+
14
+ <template>
15
+ <EscalatedLayout title="Escalation Rules">
16
+ <div class="mb-4 flex justify-end">
17
+ <Link :href="route('escalated.admin.escalation-rules.create')" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700">
18
+ Add Rule
19
+ </Link>
20
+ </div>
21
+ <div class="overflow-hidden rounded-lg border border-gray-200 bg-white">
22
+ <table class="min-w-full divide-y divide-gray-200">
23
+ <thead class="bg-gray-50">
24
+ <tr>
25
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Order</th>
26
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Name</th>
27
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Trigger</th>
28
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Active</th>
29
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Actions</th>
30
+ </tr>
31
+ </thead>
32
+ <tbody class="divide-y divide-gray-200">
33
+ <tr v-for="rule in rules" :key="rule.id">
34
+ <td class="px-4 py-3 text-sm text-gray-500">{{ rule.order }}</td>
35
+ <td class="px-4 py-3 text-sm font-medium text-gray-900">{{ rule.name }}</td>
36
+ <td class="px-4 py-3 text-sm text-gray-500">{{ rule.trigger_type }}</td>
37
+ <td class="px-4 py-3 text-sm">
38
+ <span :class="rule.is_active ? 'text-green-600' : 'text-gray-400'">{{ rule.is_active ? 'Yes' : 'No' }}</span>
39
+ </td>
40
+ <td class="px-4 py-3 text-right text-sm">
41
+ <Link :href="route('escalated.admin.escalation-rules.edit', rule.id)" class="text-indigo-600 hover:text-indigo-900">Edit</Link>
42
+ <button @click="destroy(rule.id)" class="ml-3 text-red-600 hover:text-red-900">Delete</button>
43
+ </td>
44
+ </tr>
45
+ </tbody>
46
+ </table>
47
+ </div>
48
+ </EscalatedLayout>
49
+ </template>
@@ -0,0 +1,52 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../components/EscalatedLayout.vue';
3
+ import StatsCard from '../../components/StatsCard.vue';
4
+ import { router } from '@inertiajs/vue3';
5
+
6
+ const props = defineProps({
7
+ period_days: Number,
8
+ total_tickets: Number,
9
+ resolved_tickets: Number,
10
+ avg_first_response_hours: Number,
11
+ sla_breach_count: Number,
12
+ by_status: Object,
13
+ by_priority: Object,
14
+ });
15
+
16
+ function changePeriod(days) {
17
+ router.get(route('escalated.admin.reports'), { days }, { preserveState: true });
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <EscalatedLayout title="Reports">
23
+ <div class="mb-6 flex gap-2">
24
+ <button v-for="d in [7, 30, 90]" :key="d" @click="changePeriod(d)"
25
+ :class="['rounded-lg px-3 py-1.5 text-sm', period_days === d ? 'bg-indigo-600 text-white' : 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50']">
26
+ Last {{ d }} days
27
+ </button>
28
+ </div>
29
+ <div class="mb-8 grid grid-cols-2 gap-4 md:grid-cols-4">
30
+ <StatsCard title="Total Tickets" :value="total_tickets" color="indigo" />
31
+ <StatsCard title="Resolved" :value="resolved_tickets" color="green" />
32
+ <StatsCard title="Avg First Response" :value="`${avg_first_response_hours}h`" color="yellow" />
33
+ <StatsCard title="SLA Breaches" :value="sla_breach_count" color="red" />
34
+ </div>
35
+ <div class="grid grid-cols-1 gap-6 md:grid-cols-2">
36
+ <div class="rounded-lg border border-gray-200 bg-white p-4">
37
+ <h3 class="mb-3 text-sm font-medium text-gray-700">By Status</h3>
38
+ <div v-for="(count, status) in by_status" :key="status" class="mb-2 flex items-center justify-between">
39
+ <span class="text-sm capitalize text-gray-600">{{ status.replace('_', ' ') }}</span>
40
+ <span class="text-sm font-medium text-gray-900">{{ count }}</span>
41
+ </div>
42
+ </div>
43
+ <div class="rounded-lg border border-gray-200 bg-white p-4">
44
+ <h3 class="mb-3 text-sm font-medium text-gray-700">By Priority</h3>
45
+ <div v-for="(count, priority) in by_priority" :key="priority" class="mb-2 flex items-center justify-between">
46
+ <span class="text-sm capitalize text-gray-600">{{ priority }}</span>
47
+ <span class="text-sm font-medium text-gray-900">{{ count }}</span>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </EscalatedLayout>
52
+ </template>
@@ -0,0 +1,74 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { useForm } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({ policy: { type: Object, default: null }, priorities: Array });
6
+
7
+ const form = useForm({
8
+ name: props.policy?.name || '',
9
+ description: props.policy?.description || '',
10
+ is_default: props.policy?.is_default ?? false,
11
+ first_response_hours: props.policy?.first_response_hours || {},
12
+ resolution_hours: props.policy?.resolution_hours || {},
13
+ business_hours_only: props.policy?.business_hours_only ?? false,
14
+ is_active: props.policy?.is_active ?? true,
15
+ });
16
+
17
+ function submit() {
18
+ if (props.policy) {
19
+ form.put(route('escalated.admin.sla-policies.update', props.policy.id));
20
+ } else {
21
+ form.post(route('escalated.admin.sla-policies.store'));
22
+ }
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <EscalatedLayout :title="policy ? 'Edit SLA Policy' : 'New SLA Policy'">
28
+ <form @submit.prevent="submit" class="mx-auto max-w-lg space-y-4 rounded-lg border border-gray-200 bg-white p-6">
29
+ <div>
30
+ <label class="block text-sm font-medium text-gray-700">Name</label>
31
+ <input v-model="form.name" type="text" required class="mt-1 w-full rounded-lg border-gray-300 shadow-sm" />
32
+ </div>
33
+ <div>
34
+ <label class="block text-sm font-medium text-gray-700">Description</label>
35
+ <textarea v-model="form.description" rows="2" class="mt-1 w-full rounded-lg border-gray-300 shadow-sm"></textarea>
36
+ </div>
37
+ <div>
38
+ <h3 class="mb-2 text-sm font-medium text-gray-700">First Response Hours (by priority)</h3>
39
+ <div class="grid grid-cols-2 gap-2">
40
+ <div v-for="p in priorities" :key="p">
41
+ <label class="text-xs capitalize text-gray-500">{{ p }}</label>
42
+ <input v-model.number="form.first_response_hours[p]" type="number" step="0.5" min="0" class="w-full rounded border-gray-300 text-sm" />
43
+ </div>
44
+ </div>
45
+ </div>
46
+ <div>
47
+ <h3 class="mb-2 text-sm font-medium text-gray-700">Resolution Hours (by priority)</h3>
48
+ <div class="grid grid-cols-2 gap-2">
49
+ <div v-for="p in priorities" :key="p">
50
+ <label class="text-xs capitalize text-gray-500">{{ p }}</label>
51
+ <input v-model.number="form.resolution_hours[p]" type="number" step="0.5" min="0" class="w-full rounded border-gray-300 text-sm" />
52
+ </div>
53
+ </div>
54
+ </div>
55
+ <label class="flex items-center gap-2">
56
+ <input v-model="form.business_hours_only" type="checkbox" class="rounded border-gray-300" />
57
+ <span class="text-sm text-gray-700">Business hours only</span>
58
+ </label>
59
+ <label class="flex items-center gap-2">
60
+ <input v-model="form.is_default" type="checkbox" class="rounded border-gray-300" />
61
+ <span class="text-sm text-gray-700">Default policy</span>
62
+ </label>
63
+ <label class="flex items-center gap-2">
64
+ <input v-model="form.is_active" type="checkbox" class="rounded border-gray-300" />
65
+ <span class="text-sm text-gray-700">Active</span>
66
+ </label>
67
+ <div class="flex justify-end">
68
+ <button type="submit" :disabled="form.processing" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50">
69
+ {{ policy ? 'Update' : 'Create' }}
70
+ </button>
71
+ </div>
72
+ </form>
73
+ </EscalatedLayout>
74
+ </template>
@@ -0,0 +1,51 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { Link, router } from '@inertiajs/vue3';
4
+
5
+ defineProps({ policies: Array });
6
+
7
+ function destroy(id) {
8
+ if (confirm('Delete this SLA policy?')) {
9
+ router.delete(route('escalated.admin.sla-policies.destroy', id));
10
+ }
11
+ }
12
+ </script>
13
+
14
+ <template>
15
+ <EscalatedLayout title="SLA Policies">
16
+ <div class="mb-4 flex justify-end">
17
+ <Link :href="route('escalated.admin.sla-policies.create')" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700">
18
+ Add Policy
19
+ </Link>
20
+ </div>
21
+ <div class="overflow-hidden rounded-lg border border-gray-200 bg-white">
22
+ <table class="min-w-full divide-y divide-gray-200">
23
+ <thead class="bg-gray-50">
24
+ <tr>
25
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Name</th>
26
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Default</th>
27
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Business Hours</th>
28
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Tickets</th>
29
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Active</th>
30
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Actions</th>
31
+ </tr>
32
+ </thead>
33
+ <tbody class="divide-y divide-gray-200">
34
+ <tr v-for="policy in policies" :key="policy.id">
35
+ <td class="px-4 py-3 text-sm font-medium text-gray-900">{{ policy.name }}</td>
36
+ <td class="px-4 py-3 text-sm">{{ policy.is_default ? 'Yes' : 'No' }}</td>
37
+ <td class="px-4 py-3 text-sm">{{ policy.business_hours_only ? 'Yes' : 'No' }}</td>
38
+ <td class="px-4 py-3 text-sm text-gray-500">{{ policy.tickets_count }}</td>
39
+ <td class="px-4 py-3 text-sm">
40
+ <span :class="policy.is_active ? 'text-green-600' : 'text-gray-400'">{{ policy.is_active ? 'Yes' : 'No' }}</span>
41
+ </td>
42
+ <td class="px-4 py-3 text-right text-sm">
43
+ <Link :href="route('escalated.admin.sla-policies.edit', policy.id)" class="text-indigo-600 hover:text-indigo-900">Edit</Link>
44
+ <button @click="destroy(policy.id)" class="ml-3 text-red-600 hover:text-red-900">Delete</button>
45
+ </td>
46
+ </tr>
47
+ </tbody>
48
+ </table>
49
+ </div>
50
+ </EscalatedLayout>
51
+ </template>
@@ -0,0 +1,80 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { useForm, router } from '@inertiajs/vue3';
4
+ import { ref } from 'vue';
5
+
6
+ defineProps({ tags: Array });
7
+
8
+ const showForm = ref(false);
9
+ const editingTag = ref(null);
10
+
11
+ const form = useForm({ name: '', color: '#6B7280' });
12
+
13
+ function createTag() {
14
+ form.post(route('escalated.admin.tags.store'), {
15
+ onSuccess: () => { form.reset(); showForm.value = false; },
16
+ });
17
+ }
18
+
19
+ function startEdit(tag) {
20
+ editingTag.value = tag.id;
21
+ form.name = tag.name;
22
+ form.color = tag.color;
23
+ }
24
+
25
+ function updateTag(id) {
26
+ form.put(route('escalated.admin.tags.update', id), {
27
+ onSuccess: () => { editingTag.value = null; form.reset(); },
28
+ });
29
+ }
30
+
31
+ function destroy(id) {
32
+ if (confirm('Delete this tag?')) {
33
+ router.delete(route('escalated.admin.tags.destroy', id));
34
+ }
35
+ }
36
+ </script>
37
+
38
+ <template>
39
+ <EscalatedLayout title="Tags">
40
+ <div class="mb-4 flex justify-end">
41
+ <button @click="showForm = !showForm" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700">
42
+ {{ showForm ? 'Cancel' : 'Add Tag' }}
43
+ </button>
44
+ </div>
45
+ <form v-if="showForm" @submit.prevent="createTag" class="mb-6 flex items-end gap-3 rounded-lg border border-gray-200 bg-white p-4">
46
+ <div>
47
+ <label class="block text-sm font-medium text-gray-700">Name</label>
48
+ <input v-model="form.name" type="text" required class="mt-1 rounded-lg border-gray-300 shadow-sm" />
49
+ </div>
50
+ <div>
51
+ <label class="block text-sm font-medium text-gray-700">Color</label>
52
+ <input v-model="form.color" type="color" class="mt-1 h-10 w-16 rounded border-gray-300" />
53
+ </div>
54
+ <button type="submit" :disabled="form.processing" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700">Create</button>
55
+ </form>
56
+ <div class="overflow-hidden rounded-lg border border-gray-200 bg-white">
57
+ <table class="min-w-full divide-y divide-gray-200">
58
+ <thead class="bg-gray-50">
59
+ <tr>
60
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Color</th>
61
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Name</th>
62
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Tickets</th>
63
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Actions</th>
64
+ </tr>
65
+ </thead>
66
+ <tbody class="divide-y divide-gray-200">
67
+ <tr v-for="tag in tags" :key="tag.id">
68
+ <td class="px-4 py-3"><span class="inline-block h-4 w-4 rounded-full" :style="{ backgroundColor: tag.color }"></span></td>
69
+ <td class="px-4 py-3 text-sm font-medium text-gray-900">{{ tag.name }}</td>
70
+ <td class="px-4 py-3 text-sm text-gray-500">{{ tag.tickets_count }}</td>
71
+ <td class="px-4 py-3 text-right text-sm">
72
+ <button @click="startEdit(tag)" class="text-indigo-600 hover:text-indigo-900">Edit</button>
73
+ <button @click="destroy(tag.id)" class="ml-3 text-red-600 hover:text-red-900">Delete</button>
74
+ </td>
75
+ </tr>
76
+ </tbody>
77
+ </table>
78
+ </div>
79
+ </EscalatedLayout>
80
+ </template>
@@ -0,0 +1,51 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../components/EscalatedLayout.vue';
3
+ import StatsCard from '../../components/StatsCard.vue';
4
+ import TicketList from '../../components/TicketList.vue';
5
+
6
+ defineProps({
7
+ stats: Object,
8
+ recentTickets: Array,
9
+ });
10
+ </script>
11
+
12
+ <template>
13
+ <EscalatedLayout title="Agent Dashboard">
14
+ <div class="mb-8 grid grid-cols-2 gap-4 md:grid-cols-5">
15
+ <StatsCard title="Open Tickets" :value="stats.open" color="indigo" />
16
+ <StatsCard title="My Assigned" :value="stats.my_assigned" color="indigo" />
17
+ <StatsCard title="Unassigned" :value="stats.unassigned" color="yellow" />
18
+ <StatsCard title="SLA Breached" :value="stats.sla_breached" color="red" />
19
+ <StatsCard title="Resolved Today" :value="stats.resolved_today" color="green" />
20
+ </div>
21
+ <h2 class="mb-4 text-lg font-semibold text-gray-900">Recent Tickets</h2>
22
+ <div class="overflow-hidden rounded-lg border border-gray-200 bg-white">
23
+ <table class="min-w-full divide-y divide-gray-200">
24
+ <thead class="bg-gray-50">
25
+ <tr>
26
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Reference</th>
27
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Subject</th>
28
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Requester</th>
29
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Status</th>
30
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Priority</th>
31
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Assignee</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody class="divide-y divide-gray-200">
35
+ <tr v-for="ticket in recentTickets" :key="ticket.id" class="hover:bg-gray-50">
36
+ <td class="px-4 py-3 text-sm">
37
+ <a :href="route('escalated.agent.tickets.show', ticket.reference)" class="font-medium text-indigo-600 hover:text-indigo-900">{{ ticket.reference }}</a>
38
+ </td>
39
+ <td class="px-4 py-3 text-sm text-gray-900">{{ ticket.subject }}</td>
40
+ <td class="px-4 py-3 text-sm text-gray-500">{{ ticket.requester?.name }}</td>
41
+ <td class="px-4 py-3 text-sm">
42
+ <span :class="['inline-flex rounded-full px-2 py-0.5 text-xs font-medium', { 'bg-blue-100 text-blue-800': ticket.status === 'open', 'bg-green-100 text-green-800': ticket.status === 'resolved', 'bg-red-100 text-red-800': ticket.status === 'escalated' }]">{{ ticket.status }}</span>
43
+ </td>
44
+ <td class="px-4 py-3 text-sm text-gray-500">{{ ticket.priority }}</td>
45
+ <td class="px-4 py-3 text-sm text-gray-500">{{ ticket.assignee?.name || 'Unassigned' }}</td>
46
+ </tr>
47
+ </tbody>
48
+ </table>
49
+ </div>
50
+ </EscalatedLayout>
51
+ </template>
@@ -0,0 +1,21 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../components/EscalatedLayout.vue';
3
+ import TicketList from '../../components/TicketList.vue';
4
+ import TicketFilters from '../../components/TicketFilters.vue';
5
+
6
+ defineProps({
7
+ tickets: Object,
8
+ filters: Object,
9
+ departments: Array,
10
+ tags: Array,
11
+ });
12
+ </script>
13
+
14
+ <template>
15
+ <EscalatedLayout title="Ticket Queue">
16
+ <div class="mb-6">
17
+ <TicketFilters :filters="filters" :route="route('escalated.agent.tickets.index')" :departments="departments" :tags="tags" show-assignee />
18
+ </div>
19
+ <TicketList :tickets="tickets" route-prefix="escalated.agent.tickets" show-assignee />
20
+ </EscalatedLayout>
21
+ </template>
@@ -0,0 +1,108 @@
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 TicketSidebar from '../../components/TicketSidebar.vue';
8
+ import AttachmentList from '../../components/AttachmentList.vue';
9
+ import { router, useForm, usePage } from '@inertiajs/vue3';
10
+ import { ref } from 'vue';
11
+
12
+ const props = defineProps({
13
+ ticket: Object,
14
+ departments: Array,
15
+ tags: Array,
16
+ cannedResponses: Array,
17
+ });
18
+
19
+ const page = usePage();
20
+ const activeTab = ref('reply');
21
+
22
+ const statusForm = useForm({ status: '' });
23
+ const priorityForm = useForm({ priority: '' });
24
+ const assignForm = useForm({ agent_id: '' });
25
+
26
+ function changeStatus(status) {
27
+ statusForm.status = status;
28
+ statusForm.post(route('escalated.agent.tickets.status', props.ticket.reference), { preserveScroll: true });
29
+ }
30
+
31
+ function changePriority(priority) {
32
+ priorityForm.priority = priority;
33
+ priorityForm.post(route('escalated.agent.tickets.priority', props.ticket.reference), { preserveScroll: true });
34
+ }
35
+
36
+ function assignToMe() {
37
+ assignForm.agent_id = page.props.auth.user.id;
38
+ assignForm.post(route('escalated.agent.tickets.assign', props.ticket.reference), { preserveScroll: true });
39
+ }
40
+ </script>
41
+
42
+ <template>
43
+ <EscalatedLayout :title="ticket.subject">
44
+ <div class="mb-4 flex flex-wrap items-center gap-3">
45
+ <span class="text-sm font-medium text-gray-500">{{ ticket.reference }}</span>
46
+ <StatusBadge :status="ticket.status" />
47
+ <PriorityBadge :priority="ticket.priority" />
48
+ <span class="text-sm text-gray-500">by {{ ticket.requester?.name }}</span>
49
+ <div class="ml-auto flex gap-2">
50
+ <button v-if="!ticket.assigned_to" @click="assignToMe"
51
+ class="rounded-lg bg-indigo-600 px-3 py-1.5 text-sm text-white hover:bg-indigo-700">
52
+ Assign to Me
53
+ </button>
54
+ <select @change="changeStatus($event.target.value); $event.target.value = ''"
55
+ class="rounded-lg border-gray-300 text-sm">
56
+ <option value="">Change Status...</option>
57
+ <option value="in_progress">In Progress</option>
58
+ <option value="waiting_on_customer">Waiting on Customer</option>
59
+ <option value="resolved">Resolved</option>
60
+ <option value="closed">Closed</option>
61
+ </select>
62
+ <select @change="changePriority($event.target.value); $event.target.value = ''"
63
+ class="rounded-lg border-gray-300 text-sm">
64
+ <option value="">Change Priority...</option>
65
+ <option value="low">Low</option>
66
+ <option value="medium">Medium</option>
67
+ <option value="high">High</option>
68
+ <option value="urgent">Urgent</option>
69
+ <option value="critical">Critical</option>
70
+ </select>
71
+ </div>
72
+ </div>
73
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
74
+ <div class="lg:col-span-2 space-y-6">
75
+ <div class="rounded-lg border border-gray-200 bg-white p-4">
76
+ <p class="whitespace-pre-wrap text-sm text-gray-700">{{ ticket.description }}</p>
77
+ <AttachmentList v-if="ticket.attachments?.length" :attachments="ticket.attachments" class="mt-3" />
78
+ </div>
79
+ <div>
80
+ <div class="mb-4 flex gap-4 border-b border-gray-200">
81
+ <button @click="activeTab = 'reply'"
82
+ :class="['pb-2 text-sm font-medium', activeTab === 'reply' ? 'border-b-2 border-indigo-500 text-indigo-600' : 'text-gray-500']">
83
+ Reply
84
+ </button>
85
+ <button @click="activeTab = 'note'"
86
+ :class="['pb-2 text-sm font-medium', activeTab === 'note' ? 'border-b-2 border-yellow-500 text-yellow-600' : 'text-gray-500']">
87
+ Internal Note
88
+ </button>
89
+ </div>
90
+ <ReplyComposer v-if="activeTab === 'reply'"
91
+ :action="route('escalated.agent.tickets.reply', ticket.reference)"
92
+ :canned-responses="cannedResponses" />
93
+ <ReplyComposer v-else
94
+ :action="route('escalated.agent.tickets.note', ticket.reference)"
95
+ placeholder="Write an internal note..."
96
+ submit-label="Add Note" />
97
+ </div>
98
+ <div>
99
+ <h2 class="mb-4 text-lg font-semibold text-gray-900">Conversation</h2>
100
+ <ReplyThread :replies="ticket.replies || []" :current-user-id="page.props.auth?.user?.id" />
101
+ </div>
102
+ </div>
103
+ <div>
104
+ <TicketSidebar :ticket="ticket" :tags="tags" :departments="departments" />
105
+ </div>
106
+ </div>
107
+ </EscalatedLayout>
108
+ </template>