@escalated-dev/escalated 0.6.0 → 0.7.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.
Files changed (130) hide show
  1. package/README.md +94 -2
  2. package/package.json +31 -2
  3. package/src/components/ActivityTimeline.stories.js +55 -0
  4. package/src/components/ActivityTimeline.vue +24 -7
  5. package/src/components/AdminDashboard.stories.js +1095 -0
  6. package/src/components/AgentLoadIndicator.stories.js +37 -0
  7. package/src/components/AgentLoadIndicator.vue +49 -0
  8. package/src/components/AgentPerformanceCard.vue +44 -0
  9. package/src/components/ArticleEditor.vue +82 -0
  10. package/src/components/AssigneeSelect.vue +19 -5
  11. package/src/components/AttachmentList.vue +53 -37
  12. package/src/components/AuditLogEntry.vue +95 -0
  13. package/src/components/BulkActionBar.vue +99 -30
  14. package/src/components/CategoryTree.vue +89 -0
  15. package/src/components/ChartWidget.vue +46 -0
  16. package/src/components/CollisionWarning.stories.js +30 -0
  17. package/src/components/CollisionWarning.vue +68 -0
  18. package/src/components/ConditionalFieldRules.vue +135 -0
  19. package/src/components/ContextPanel.vue +66 -0
  20. package/src/components/ContextPanelSection.vue +65 -0
  21. package/src/components/CustomFieldRenderer.vue +159 -0
  22. package/src/components/EscalatedLayout.vue +400 -69
  23. package/src/components/FileDropzone.vue +26 -10
  24. package/src/components/FollowButton.stories.js +30 -0
  25. package/src/components/FollowButton.vue +53 -28
  26. package/src/components/KeyboardShortcutHelp.vue +129 -39
  27. package/src/components/KnowledgePanel.vue +122 -0
  28. package/src/components/KpiCard.stories.js +32 -0
  29. package/src/components/KpiCard.vue +88 -0
  30. package/src/components/MacroDropdown.vue +95 -35
  31. package/src/components/PermissionMatrix.vue +107 -0
  32. package/src/components/PinnedNotes.vue +70 -31
  33. package/src/components/PresenceIndicator.vue +93 -27
  34. package/src/components/PriorityBadge.stories.js +33 -0
  35. package/src/components/PriorityBadge.vue +9 -2
  36. package/src/components/QuickFilters.vue +65 -49
  37. package/src/components/ReplyComposer.vue +144 -30
  38. package/src/components/ReplyThread.vue +72 -20
  39. package/src/components/SatisfactionRating.vue +98 -46
  40. package/src/components/ScheduleEditor.vue +104 -0
  41. package/src/components/SideConversation.vue +165 -0
  42. package/src/components/SideConversationList.vue +147 -0
  43. package/src/components/SkillTagManager.vue +79 -0
  44. package/src/components/SlaComplianceChart.vue +53 -0
  45. package/src/components/SlaTimer.stories.js +35 -0
  46. package/src/components/SlaTimer.vue +17 -4
  47. package/src/components/StatsCard.stories.js +24 -0
  48. package/src/components/StatsCard.vue +58 -46
  49. package/src/components/StatusBadge.stories.js +53 -0
  50. package/src/components/StatusBadge.vue +36 -4
  51. package/src/components/TagSelect.vue +43 -12
  52. package/src/components/TicketFilters.vue +225 -69
  53. package/src/components/TicketLinkPanel.vue +193 -0
  54. package/src/components/TicketList.vue +594 -193
  55. package/src/components/TicketMergeDialog.vue +157 -0
  56. package/src/components/TicketSidebar.vue +167 -99
  57. package/src/components/TicketTypeSelector.vue +21 -0
  58. package/src/components/TotpInput.stories.js +9 -0
  59. package/src/components/TotpInput.vue +81 -0
  60. package/src/components/TwoFactorSetup.vue +113 -0
  61. package/src/composables/useDebouncedSearch.js +38 -0
  62. package/src/composables/usePluginExtensions.js +95 -3
  63. package/src/composables/usePluginHooks.js +65 -0
  64. package/src/defineEscalatedPlugin.js +34 -8
  65. package/src/index.js +72 -35
  66. package/src/locales/en.json +25 -0
  67. package/src/pages/Admin/ApiTokens/Index.vue +231 -67
  68. package/src/pages/Admin/AuditLog/Index.vue +154 -0
  69. package/src/pages/Admin/Automations/Form.vue +184 -0
  70. package/src/pages/Admin/Automations/Index.vue +129 -0
  71. package/src/pages/Admin/BusinessHours/Form.vue +172 -0
  72. package/src/pages/Admin/BusinessHours/Index.vue +113 -0
  73. package/src/pages/Admin/CannedResponses/Index.vue +127 -70
  74. package/src/pages/Admin/Capacity/Index.vue +134 -0
  75. package/src/pages/Admin/CustomFields/Form.vue +195 -0
  76. package/src/pages/Admin/CustomFields/Index.vue +134 -0
  77. package/src/pages/Admin/CustomObjects/Form.vue +202 -0
  78. package/src/pages/Admin/CustomObjects/Index.vue +127 -0
  79. package/src/pages/Admin/CustomObjects/Records.vue +228 -0
  80. package/src/pages/Admin/Departments/Form.vue +75 -51
  81. package/src/pages/Admin/Departments/Index.vue +109 -57
  82. package/src/pages/Admin/EscalationRules/Form.vue +157 -96
  83. package/src/pages/Admin/EscalationRules/Index.vue +178 -57
  84. package/src/pages/Admin/Import/Connect.vue +96 -0
  85. package/src/pages/Admin/Import/Index.vue +102 -0
  86. package/src/pages/Admin/Import/Mapping.vue +110 -0
  87. package/src/pages/Admin/Import/Progress.vue +153 -0
  88. package/src/pages/Admin/Import/Review.vue +76 -0
  89. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
  90. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
  91. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
  92. package/src/pages/Admin/Macros/Index.vue +250 -89
  93. package/src/pages/Admin/Plugins/Index.vue +205 -75
  94. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
  95. package/src/pages/Admin/Reports/CsatReport.vue +127 -0
  96. package/src/pages/Admin/Reports/Dashboard.vue +211 -0
  97. package/src/pages/Admin/Reports/SlaReport.vue +166 -0
  98. package/src/pages/Admin/Reports.vue +139 -34
  99. package/src/pages/Admin/Roles/Form.vue +71 -0
  100. package/src/pages/Admin/Roles/Index.vue +122 -0
  101. package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
  102. package/src/pages/Admin/Settings/DataRetention.vue +182 -0
  103. package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
  104. package/src/pages/Admin/Settings/Sandbox.vue +135 -0
  105. package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
  106. package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
  107. package/src/pages/Admin/Settings.vue +468 -260
  108. package/src/pages/Admin/Skills/Form.vue +53 -0
  109. package/src/pages/Admin/Skills/Index.vue +94 -0
  110. package/src/pages/Admin/SlaPolicies/Form.vue +118 -75
  111. package/src/pages/Admin/SlaPolicies/Index.vue +120 -59
  112. package/src/pages/Admin/Statuses/Form.vue +114 -0
  113. package/src/pages/Admin/Statuses/Index.vue +153 -0
  114. package/src/pages/Admin/Tags/Index.vue +191 -105
  115. package/src/pages/Admin/Tickets/Index.vue +91 -39
  116. package/src/pages/Admin/Tickets/Show.vue +101 -37
  117. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
  118. package/src/pages/Admin/Webhooks/Form.vue +110 -0
  119. package/src/pages/Admin/Webhooks/Index.vue +136 -0
  120. package/src/pages/Agent/Dashboard.vue +311 -186
  121. package/src/pages/Agent/TicketIndex.vue +88 -38
  122. package/src/pages/Agent/TicketShow.vue +258 -51
  123. package/src/pages/Customer/Create.vue +36 -9
  124. package/src/pages/Customer/Index.vue +31 -24
  125. package/src/pages/Customer/KnowledgeBase/Article.vue +127 -0
  126. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
  127. package/src/plugin.js +86 -1
  128. package/src/utils/constants.js +30 -0
  129. package/src/utils/formatting.js +35 -0
  130. package/src/pages/Admin/Plugins.vue +0 -129
@@ -1,38 +1,88 @@
1
- <script setup>
2
- import { ref } from 'vue';
3
- import { usePage, router } from '@inertiajs/vue3';
4
- import EscalatedLayout from '../../components/EscalatedLayout.vue';
5
- import TicketList from '../../components/TicketList.vue';
6
- import TicketFilters from '../../components/TicketFilters.vue';
7
- import QuickFilters from '../../components/QuickFilters.vue';
8
- import BulkActionBar from '../../components/BulkActionBar.vue';
9
-
10
- defineProps({
11
- tickets: Object,
12
- filters: Object,
13
- departments: Array,
14
- tags: Array,
15
- });
16
-
17
- const page = usePage();
18
- const selectedIds = ref([]);
19
-
20
- function applyQuickFilter(filter) {
21
- router.get(route('escalated.agent.tickets.index'), filter, { preserveState: true });
22
- }
23
- </script>
24
-
25
- <template>
26
- <EscalatedLayout title="Ticket Queue">
27
- <div class="mb-4">
28
- <QuickFilters :current-user-id="page.props.auth?.user?.id" @filter="applyQuickFilter" />
29
- </div>
30
- <div class="mb-6">
31
- <TicketFilters :filters="filters" :route="route('escalated.agent.tickets.index')" :departments="departments" :tags="tags" show-assignee show-following />
32
- </div>
33
- <TicketList :tickets="tickets" route-prefix="escalated.agent.tickets" show-assignee
34
- selectable v-model:selected-ids="selectedIds" />
35
- <BulkActionBar :selected-ids="selectedIds" :bulk-route="route('escalated.agent.tickets.bulk')"
36
- :departments="departments" @clear="selectedIds = []" />
37
- </EscalatedLayout>
38
- </template>
1
+ <script setup>
2
+ import { ref } from 'vue';
3
+ import { usePage, router } from '@inertiajs/vue3';
4
+ import EscalatedLayout from '../../components/EscalatedLayout.vue';
5
+ import TicketList from '../../components/TicketList.vue';
6
+ import TicketFilters from '../../components/TicketFilters.vue';
7
+ import QuickFilters from '../../components/QuickFilters.vue';
8
+ import BulkActionBar from '../../components/BulkActionBar.vue';
9
+ import KeyboardShortcutHelp from '../../components/KeyboardShortcutHelp.vue';
10
+ import PluginSlot from '../../components/PluginSlot.vue';
11
+ import { useKeyboardShortcuts } from '../../composables/useKeyboardShortcuts';
12
+ import { usePluginExtensions } from '../../composables/usePluginExtensions';
13
+
14
+ defineProps({
15
+ tickets: Object,
16
+ filters: Object,
17
+ departments: Array,
18
+ tags: Array,
19
+ });
20
+
21
+ const page = usePage();
22
+ const selectedIds = ref([]);
23
+ const ticketListRef = ref(null);
24
+ const showShortcutHelp = ref(false);
25
+
26
+ useKeyboardShortcuts({
27
+ j: () => {
28
+ ticketListRef.value?.moveDown();
29
+ },
30
+ k: () => {
31
+ ticketListRef.value?.moveUp();
32
+ },
33
+ x: () => {
34
+ ticketListRef.value?.toggleSelectFocused();
35
+ },
36
+ Enter: () => {
37
+ ticketListRef.value?.openFocused();
38
+ },
39
+ '?': () => {
40
+ showShortcutHelp.value = true;
41
+ },
42
+ });
43
+
44
+ const { getPageComponents } = usePluginExtensions();
45
+
46
+ function applyQuickFilter(filter) {
47
+ router.get(route('escalated.agent.tickets.index'), filter, { preserveState: true });
48
+ }
49
+ </script>
50
+
51
+ <template>
52
+ <EscalatedLayout title="Ticket Queue">
53
+ <div class="mb-4">
54
+ <QuickFilters :current-user-id="page.props.auth?.user?.id" @filter="applyQuickFilter" />
55
+ </div>
56
+ <PluginSlot
57
+ slot="ticket.list.before_filters"
58
+ :components="getPageComponents('ticket.list', 'before_filters')"
59
+ />
60
+ <div class="mb-6">
61
+ <TicketFilters
62
+ :filters="filters"
63
+ :route="route('escalated.agent.tickets.index')"
64
+ :departments="departments"
65
+ :tags="tags"
66
+ show-assignee
67
+ show-following
68
+ />
69
+ </div>
70
+ <TicketList
71
+ ref="ticketListRef"
72
+ v-model:selected-ids="selectedIds"
73
+ :tickets="tickets"
74
+ route-prefix="escalated.agent.tickets"
75
+ :columns="['reference', 'subject', 'requester', 'status', 'priority', 'assignee', 'updated']"
76
+ selectable
77
+ navigable
78
+ />
79
+ <BulkActionBar
80
+ :selected-ids="selectedIds"
81
+ :bulk-route="route('escalated.agent.tickets.bulk')"
82
+ :departments="departments"
83
+ @clear="selectedIds = []"
84
+ />
85
+ <PluginSlot slot="ticket.list.footer" :components="getPageComponents('ticket.list', 'footer')" />
86
+ <KeyboardShortcutHelp v-model:show="showShortcutHelp" context="list" />
87
+ </EscalatedLayout>
88
+ </template>
@@ -12,10 +12,13 @@ import PresenceIndicator from '../../components/PresenceIndicator.vue';
12
12
  import PinnedNotes from '../../components/PinnedNotes.vue';
13
13
  import KeyboardShortcutHelp from '../../components/KeyboardShortcutHelp.vue';
14
14
  import PluginSlot from '../../components/PluginSlot.vue';
15
+ import KnowledgePanel from '../../components/KnowledgePanel.vue';
16
+ import ContextPanel from '../../components/ContextPanel.vue';
17
+ import ContextPanelSection from '../../components/ContextPanelSection.vue';
15
18
  import { useKeyboardShortcuts } from '../../composables/useKeyboardShortcuts';
16
19
  import { usePluginExtensions } from '../../composables/usePluginExtensions';
17
20
  import { router, useForm, usePage } from '@inertiajs/vue3';
18
- import { ref } from 'vue';
21
+ import { ref, computed, inject } from 'vue';
19
22
 
20
23
  const props = defineProps({
21
24
  ticket: Object,
@@ -28,12 +31,15 @@ const props = defineProps({
28
31
  });
29
32
 
30
33
  const page = usePage();
31
- const activeTab = ref('reply');
34
+ const isLightAgent = computed(() => page.props.escalated?.agent_type === 'light');
35
+ const activeTab = ref(isLightAgent.value ? 'note' : 'reply');
32
36
  const showShortcutHelp = ref(false);
33
37
  const replyComposerRef = ref(null);
34
38
  const statusSelectRef = ref(null);
35
39
  const prioritySelectRef = ref(null);
36
40
 
41
+ const showContextPanel = ref(false);
42
+
37
43
  const statusForm = useForm({ status: '' });
38
44
  const priorityForm = useForm({ priority: '' });
39
45
  const assignForm = useForm({ agent_id: '' });
@@ -57,16 +63,34 @@ function toggleFollow() {
57
63
  router.post(route('escalated.agent.tickets.follow', props.ticket.reference), {}, { preserveScroll: true });
58
64
  }
59
65
 
66
+ const escDark = inject(
67
+ 'esc-dark',
68
+ computed(() => false),
69
+ );
70
+
60
71
  const { getPageComponents } = usePluginExtensions();
61
72
 
62
73
  // Keyboard shortcuts
63
74
  useKeyboardShortcuts({
64
- 'r': () => { activeTab.value = 'reply'; replyComposerRef.value?.$el?.querySelector('textarea')?.focus(); },
65
- 'n': () => { activeTab.value = 'note'; },
66
- 's': () => { statusSelectRef.value?.focus(); },
67
- 'p': () => { prioritySelectRef.value?.focus(); },
68
- 'f': () => { toggleFollow(); },
69
- '?': () => { showShortcutHelp.value = true; },
75
+ r: () => {
76
+ activeTab.value = 'reply';
77
+ replyComposerRef.value?.$el?.querySelector('textarea')?.focus();
78
+ },
79
+ n: () => {
80
+ activeTab.value = 'note';
81
+ },
82
+ s: () => {
83
+ statusSelectRef.value?.focus();
84
+ },
85
+ p: () => {
86
+ prioritySelectRef.value?.focus();
87
+ },
88
+ f: () => {
89
+ toggleFollow();
90
+ },
91
+ '?': () => {
92
+ showShortcutHelp.value = true;
93
+ },
70
94
  });
71
95
  </script>
72
96
 
@@ -76,80 +100,263 @@ useKeyboardShortcuts({
76
100
  <PluginSlot slot="ticket.show.header" :components="getPageComponents('ticket.show', 'header')" />
77
101
 
78
102
  <div class="mb-5 flex flex-wrap items-center gap-3">
79
- <span class="text-sm font-mono font-medium text-white">{{ ticket.reference }}</span>
103
+ <span :class="['text-sm font-mono font-medium', escDark ? 'text-white' : 'text-gray-900']">{{
104
+ ticket.reference
105
+ }}</span>
80
106
  <StatusBadge :status="ticket.status" />
81
107
  <PriorityBadge :priority="ticket.priority" />
82
108
  <span class="text-sm text-neutral-500">by {{ ticket.requester?.name }}</span>
83
109
  <PresenceIndicator :ticket-reference="ticket.reference" route-prefix="escalated.agent" />
84
110
  <div class="ml-auto flex items-center gap-2">
85
- <FollowButton :is-following="is_following" :followers-count="followers_count"
86
- :action="route('escalated.agent.tickets.follow', ticket.reference)" />
87
- <MacroDropdown v-if="macros.length" :macros="macros"
88
- :action="route('escalated.agent.tickets.macro', ticket.reference)" />
89
- <button v-if="!ticket.assigned_to" @click="assignToMe"
90
- class="rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-3 py-1.5 text-sm font-medium text-white shadow-lg shadow-black/20 transition-all hover:from-cyan-400 hover:to-violet-400">
111
+ <FollowButton
112
+ :is-following="is_following"
113
+ :followers-count="followers_count"
114
+ :action="route('escalated.agent.tickets.follow', ticket.reference)"
115
+ />
116
+ <MacroDropdown
117
+ v-if="macros.length && !isLightAgent"
118
+ :macros="macros"
119
+ :action="route('escalated.agent.tickets.macro', ticket.reference)"
120
+ />
121
+ <button
122
+ v-if="!ticket.assigned_to && !isLightAgent"
123
+ class="rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-3 py-1.5 text-sm font-medium text-white shadow-lg shadow-black/20 transition-all hover:from-cyan-400 hover:to-violet-400"
124
+ @click="assignToMe"
125
+ >
91
126
  Assign to Me
92
127
  </button>
93
- <select ref="statusSelectRef" @change="changeStatus($event.target.value); $event.target.value = ''"
94
- class="rounded-lg border border-white/10 bg-neutral-950 px-3 py-1.5 text-sm text-neutral-200 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10">
95
- <option value="">Change Status...</option>
96
- <option value="in_progress">In Progress</option>
97
- <option value="waiting_on_customer">Waiting on Customer</option>
98
- <option value="resolved">Resolved</option>
99
- <option value="closed">Closed</option>
100
- </select>
101
- <select ref="prioritySelectRef" @change="changePriority($event.target.value); $event.target.value = ''"
102
- class="rounded-lg border border-white/10 bg-neutral-950 px-3 py-1.5 text-sm text-neutral-200 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10">
103
- <option value="">Change Priority...</option>
104
- <option value="low">Low</option>
105
- <option value="medium">Medium</option>
106
- <option value="high">High</option>
107
- <option value="urgent">Urgent</option>
108
- <option value="critical">Critical</option>
109
- </select>
128
+ <template v-if="!isLightAgent">
129
+ <select
130
+ ref="statusSelectRef"
131
+ :class="[
132
+ 'rounded-lg border px-3 py-1.5 text-sm focus:outline-none focus:ring-1',
133
+ escDark
134
+ ? 'border-white/10 bg-neutral-950 text-neutral-200 focus:border-white/20 focus:ring-white/10'
135
+ : 'border-gray-300 bg-white text-gray-700 focus:border-gray-400 focus:ring-gray-200',
136
+ ]"
137
+ @change="
138
+ changeStatus($event.target.value);
139
+ $event.target.value = '';
140
+ "
141
+ >
142
+ <option value="">Change Status...</option>
143
+ <option value="in_progress">In Progress</option>
144
+ <option value="waiting_on_customer">Waiting on Customer</option>
145
+ <option value="resolved">Resolved</option>
146
+ <option value="closed">Closed</option>
147
+ </select>
148
+ <select
149
+ ref="prioritySelectRef"
150
+ :class="[
151
+ 'rounded-lg border px-3 py-1.5 text-sm focus:outline-none focus:ring-1',
152
+ escDark
153
+ ? 'border-white/10 bg-neutral-950 text-neutral-200 focus:border-white/20 focus:ring-white/10'
154
+ : 'border-gray-300 bg-white text-gray-700 focus:border-gray-400 focus:ring-gray-200',
155
+ ]"
156
+ @change="
157
+ changePriority($event.target.value);
158
+ $event.target.value = '';
159
+ "
160
+ >
161
+ <option value="">Change Priority...</option>
162
+ <option value="low">Low</option>
163
+ <option value="medium">Medium</option>
164
+ <option value="high">High</option>
165
+ <option value="urgent">Urgent</option>
166
+ <option value="critical">Critical</option>
167
+ </select>
168
+ </template>
169
+ <!-- Context Panel Toggle -->
170
+ <button
171
+ type="button"
172
+ :class="[
173
+ 'rounded-lg border px-3 py-1.5 text-sm transition-colors',
174
+ showContextPanel
175
+ ? escDark
176
+ ? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-400'
177
+ : 'border-blue-300 bg-blue-50 text-blue-600'
178
+ : escDark
179
+ ? 'border-white/10 text-neutral-400 hover:border-white/20 hover:text-neutral-200'
180
+ : 'border-gray-300 text-gray-400 hover:border-gray-400 hover:text-gray-600',
181
+ ]"
182
+ @click="showContextPanel = !showContextPanel"
183
+ >
184
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
185
+ <path
186
+ stroke-linecap="round"
187
+ stroke-linejoin="round"
188
+ d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
189
+ />
190
+ </svg>
191
+ </button>
110
192
  <!-- Plugin: ticket show actions slot -->
111
193
  <PluginSlot slot="ticket.show.actions" :components="getPageComponents('ticket.show', 'actions')" />
112
194
  </div>
113
195
  </div>
114
196
  <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
115
197
  <div class="lg:col-span-2 space-y-6">
116
- <PinnedNotes v-if="ticket.pinned_notes?.length" :notes="ticket.pinned_notes"
117
- :ticket-reference="ticket.reference" route-prefix="escalated.agent" />
118
- <div class="rounded-xl border border-white/[0.06] bg-neutral-900/60 p-5">
119
- <p class="whitespace-pre-wrap text-sm text-neutral-300">{{ ticket.description }}</p>
198
+ <PinnedNotes
199
+ v-if="ticket.pinned_notes?.length"
200
+ :notes="ticket.pinned_notes"
201
+ :ticket-reference="ticket.reference"
202
+ route-prefix="escalated.agent"
203
+ />
204
+ <div
205
+ :class="[
206
+ 'rounded-xl border p-5',
207
+ escDark ? 'border-white/[0.06] bg-neutral-900/60' : 'border-gray-200 bg-white',
208
+ ]"
209
+ >
210
+ <p :class="['whitespace-pre-wrap text-sm', escDark ? 'text-neutral-300' : 'text-gray-700']">
211
+ {{ ticket.description }}
212
+ </p>
120
213
  <AttachmentList v-if="ticket.attachments?.length" :attachments="ticket.attachments" class="mt-3" />
121
214
  </div>
122
215
  <div>
123
- <div class="mb-4 flex gap-4 border-b border-white/[0.06]">
124
- <button @click="activeTab = 'reply'"
125
- :class="['pb-2 text-sm font-medium transition-colors', activeTab === 'reply' ? 'border-b-2 border-cyan-500 text-white' : 'text-neutral-500 hover:text-neutral-300']">
216
+ <div :class="['mb-4 flex gap-4 border-b', escDark ? 'border-white/[0.06]' : 'border-gray-200']">
217
+ <button
218
+ v-if="!isLightAgent"
219
+ :class="[
220
+ 'pb-2 text-sm font-medium transition-colors',
221
+ activeTab === 'reply'
222
+ ? escDark
223
+ ? 'border-b-2 border-cyan-500 text-white'
224
+ : 'border-b-2 border-blue-500 text-gray-900'
225
+ : escDark
226
+ ? 'text-neutral-500 hover:text-neutral-300'
227
+ : 'text-gray-400 hover:text-gray-600',
228
+ ]"
229
+ @click="activeTab = 'reply'"
230
+ >
126
231
  Reply
127
232
  </button>
128
- <button @click="activeTab = 'note'"
129
- :class="['pb-2 text-sm font-medium transition-colors', activeTab === 'note' ? 'border-b-2 border-amber-500 text-amber-400' : 'text-neutral-500 hover:text-neutral-300']">
233
+ <button
234
+ :class="[
235
+ 'pb-2 text-sm font-medium transition-colors',
236
+ activeTab === 'note'
237
+ ? escDark
238
+ ? 'border-b-2 border-amber-500 text-amber-400'
239
+ : 'border-b-2 border-amber-500 text-amber-700'
240
+ : escDark
241
+ ? 'text-neutral-500 hover:text-neutral-300'
242
+ : 'text-gray-400 hover:text-gray-600',
243
+ ]"
244
+ @click="activeTab = 'note'"
245
+ >
130
246
  Internal Note
131
247
  </button>
132
248
  </div>
133
- <ReplyComposer v-if="activeTab === 'reply'" ref="replyComposerRef"
134
- :action="route('escalated.agent.tickets.reply', ticket.reference)"
135
- :canned-responses="cannedResponses" />
136
- <ReplyComposer v-else
137
- :action="route('escalated.agent.tickets.note', ticket.reference)"
138
- placeholder="Write an internal note..."
139
- submit-label="Add Note" />
249
+ <ReplyComposer
250
+ v-if="activeTab === 'reply' && !isLightAgent"
251
+ ref="replyComposerRef"
252
+ :action="route('escalated.agent.tickets.reply', ticket.reference)"
253
+ :canned-responses="cannedResponses"
254
+ />
255
+ <ReplyComposer
256
+ v-if="activeTab === 'note'"
257
+ :action="route('escalated.agent.tickets.note', ticket.reference)"
258
+ placeholder="Write an internal note..."
259
+ submit-label="Add Note"
260
+ />
140
261
  </div>
141
262
  <div>
142
- <h2 class="mb-4 text-lg font-semibold text-neutral-200">Conversation</h2>
143
- <ReplyThread :replies="ticket.replies || []" :current-user-id="page.props.auth?.user?.id"
144
- :ticket-reference="ticket.reference" route-prefix="escalated.agent" pinnable />
263
+ <h2 :class="['mb-4 text-lg font-semibold', escDark ? 'text-neutral-200' : 'text-gray-900']">
264
+ Conversation
265
+ </h2>
266
+ <ReplyThread
267
+ :replies="ticket.replies || []"
268
+ :current-user-id="page.props.auth?.user?.id"
269
+ :ticket-reference="ticket.reference"
270
+ route-prefix="escalated.agent"
271
+ pinnable
272
+ />
145
273
  </div>
146
274
  </div>
147
275
  <div class="space-y-6">
148
276
  <TicketSidebar :ticket="ticket" :tags="tags" :departments="departments" />
277
+ <KnowledgePanel :ticket-reference="ticket.reference" />
149
278
  <!-- Plugin: ticket show sidebar slot -->
150
279
  <PluginSlot slot="ticket.show.sidebar" :components="getPageComponents('ticket.show', 'sidebar')" />
151
280
  </div>
152
281
  </div>
282
+ <!-- Context Panel -->
283
+ <ContextPanel v-model:visible="showContextPanel">
284
+ <ContextPanelSection
285
+ title="Customer Info"
286
+ icon="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
287
+ >
288
+ <div class="space-y-2 text-sm">
289
+ <div class="flex justify-between">
290
+ <span :class="escDark ? 'text-neutral-500' : 'text-gray-500'">Name</span>
291
+ <span :class="escDark ? 'text-neutral-200' : 'text-gray-900'">{{
292
+ ticket.requester?.name || 'Unknown'
293
+ }}</span>
294
+ </div>
295
+ <div class="flex justify-between">
296
+ <span :class="escDark ? 'text-neutral-500' : 'text-gray-500'">Email</span>
297
+ <span :class="['truncate', escDark ? 'text-neutral-200' : 'text-gray-900']">{{
298
+ ticket.requester?.email || '---'
299
+ }}</span>
300
+ </div>
301
+ <div class="flex justify-between">
302
+ <span :class="escDark ? 'text-neutral-500' : 'text-gray-500'">Tickets</span>
303
+ <span :class="escDark ? 'text-neutral-200' : 'text-gray-900'">{{
304
+ ticket.requester_ticket_count ?? '---'
305
+ }}</span>
306
+ </div>
307
+ </div>
308
+ </ContextPanelSection>
309
+
310
+ <ContextPanelSection
311
+ title="Related Tickets"
312
+ icon="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
313
+ :default-open="false"
314
+ >
315
+ <div v-if="ticket.related_tickets?.length" class="space-y-2">
316
+ <div
317
+ v-for="rt in ticket.related_tickets"
318
+ :key="rt.reference"
319
+ :class="[
320
+ 'rounded-lg border px-3 py-2',
321
+ escDark ? 'border-white/[0.06] bg-neutral-950' : 'border-gray-200 bg-gray-50',
322
+ ]"
323
+ >
324
+ <span :class="['font-mono text-xs', escDark ? 'text-cyan-400' : 'text-blue-600']">{{
325
+ rt.reference
326
+ }}</span>
327
+ <p :class="['mt-0.5 truncate text-xs', escDark ? 'text-neutral-400' : 'text-gray-500']">
328
+ {{ rt.subject }}
329
+ </p>
330
+ </div>
331
+ </div>
332
+ <p v-else :class="['text-xs', escDark ? 'text-neutral-600' : 'text-gray-400']">
333
+ No related tickets found.
334
+ </p>
335
+ </ContextPanelSection>
336
+
337
+ <ContextPanelSection
338
+ title="Recent Activity"
339
+ icon="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
340
+ :default-open="false"
341
+ >
342
+ <div v-if="ticket.activities?.length" class="space-y-2">
343
+ <div v-for="activity in ticket.activities.slice(0, 5)" :key="activity.id" class="text-xs">
344
+ <span :class="escDark ? 'text-neutral-500' : 'text-gray-600'">{{ activity.description }}</span>
345
+ <span :class="['ml-1', escDark ? 'text-neutral-600' : 'text-gray-400']">{{
346
+ activity.created_at_human
347
+ }}</span>
348
+ </div>
349
+ </div>
350
+ <p v-else :class="['text-xs', escDark ? 'text-neutral-600' : 'text-gray-400']">No recent activity.</p>
351
+ </ContextPanelSection>
352
+
353
+ <!-- Plugin: context panel slot -->
354
+ <PluginSlot
355
+ slot="ticket.show.context-panel"
356
+ :components="getPageComponents('ticket.show', 'context-panel')"
357
+ />
358
+ </ContextPanel>
359
+
153
360
  <KeyboardShortcutHelp v-model:show="showShortcutHelp" context="detail" />
154
361
  </EscalatedLayout>
155
362
  </template>
@@ -1,7 +1,10 @@
1
1
  <script setup>
2
2
  import EscalatedLayout from '../../components/EscalatedLayout.vue';
3
3
  import FileDropzone from '../../components/FileDropzone.vue';
4
+ import TicketTypeSelector from '../../components/TicketTypeSelector.vue';
5
+ import PluginSlot from '../../components/PluginSlot.vue';
4
6
  import { useForm } from '@inertiajs/vue3';
7
+ import { usePluginExtensions } from '../../composables/usePluginExtensions';
5
8
 
6
9
  const props = defineProps({
7
10
  departments: Array,
@@ -13,9 +16,12 @@ const form = useForm({
13
16
  description: '',
14
17
  priority: 'medium',
15
18
  department_id: '',
19
+ ticket_type: 'question',
16
20
  attachments: [],
17
21
  });
18
22
 
23
+ const { getPageComponents } = usePluginExtensions();
24
+
19
25
  function submit() {
20
26
  form.post(route('escalated.customer.tickets.store'));
21
27
  }
@@ -23,20 +29,33 @@ function submit() {
23
29
 
24
30
  <template>
25
31
  <EscalatedLayout title="Create Ticket">
26
- <form @submit.prevent="submit" class="mx-auto max-w-2xl space-y-6 rounded-lg border border-gray-200 bg-white p-6">
32
+ <form
33
+ class="mx-auto max-w-2xl space-y-6 rounded-lg border border-gray-200 bg-white p-6"
34
+ @submit.prevent="submit"
35
+ >
27
36
  <div>
28
37
  <label class="block text-sm font-medium text-gray-700">Subject</label>
29
- <input v-model="form.subject" type="text" required
30
- class="mt-1 w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
38
+ <input
39
+ v-model="form.subject"
40
+ type="text"
41
+ required
42
+ class="mt-1 w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
43
+ />
31
44
  <div v-if="form.errors.subject" class="mt-1 text-sm text-red-600">{{ form.errors.subject }}</div>
32
45
  </div>
33
46
  <div>
34
47
  <label class="block text-sm font-medium text-gray-700">Description</label>
35
- <textarea v-model="form.description" rows="6" required
36
- class="mt-1 w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"></textarea>
37
- <div v-if="form.errors.description" class="mt-1 text-sm text-red-600">{{ form.errors.description }}</div>
48
+ <textarea
49
+ v-model="form.description"
50
+ rows="6"
51
+ required
52
+ class="mt-1 w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
53
+ ></textarea>
54
+ <div v-if="form.errors.description" class="mt-1 text-sm text-red-600">
55
+ {{ form.errors.description }}
56
+ </div>
38
57
  </div>
39
- <div class="grid grid-cols-2 gap-4">
58
+ <div class="grid grid-cols-3 gap-4">
40
59
  <div>
41
60
  <label class="block text-sm font-medium text-gray-700">Priority</label>
42
61
  <select v-model="form.priority" class="mt-1 w-full rounded-lg border-gray-300 shadow-sm">
@@ -50,17 +69,25 @@ function submit() {
50
69
  <option v-for="d in departments" :key="d.id" :value="d.id">{{ d.name }}</option>
51
70
  </select>
52
71
  </div>
72
+ <div>
73
+ <label class="block text-sm font-medium text-gray-700">Type</label>
74
+ <TicketTypeSelector v-model="form.ticket_type" class="mt-1 w-full" />
75
+ </div>
53
76
  </div>
54
77
  <div>
55
78
  <label class="block text-sm font-medium text-gray-700 mb-1">Attachments</label>
56
79
  <FileDropzone v-model="form.attachments" />
57
80
  </div>
58
81
  <div class="flex justify-end">
59
- <button type="submit" :disabled="form.processing"
60
- class="rounded-lg bg-indigo-600 px-6 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50">
82
+ <button
83
+ type="submit"
84
+ :disabled="form.processing"
85
+ class="rounded-lg bg-indigo-600 px-6 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50"
86
+ >
61
87
  {{ form.processing ? 'Creating...' : 'Create Ticket' }}
62
88
  </button>
63
89
  </div>
64
90
  </form>
91
+ <PluginSlot slot="customer.create.footer" :components="getPageComponents('customer.create', 'footer')" />
65
92
  </EscalatedLayout>
66
93
  </template>