@escalated-dev/escalated 0.6.0 → 0.7.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 (187) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +125 -2
  3. package/package.json +40 -5
  4. package/src/components/ActiveChatsPanel.vue +246 -0
  5. package/src/components/ActivityTimeline.stories.js +55 -0
  6. package/src/components/ActivityTimeline.vue +83 -66
  7. package/src/components/AdminDashboard.stories.js +1423 -0
  8. package/src/components/AgentLoadIndicator.stories.js +37 -0
  9. package/src/components/AgentLoadIndicator.vue +49 -0
  10. package/src/components/AgentPerformanceCard.vue +44 -0
  11. package/src/components/ArticleEditor.vue +82 -0
  12. package/src/components/AssigneeSelect.vue +37 -23
  13. package/src/components/AttachmentList.vue +65 -37
  14. package/src/components/AuditLogEntry.vue +95 -0
  15. package/src/components/BulkActionBar.vue +166 -97
  16. package/src/components/CategoryTree.vue +89 -0
  17. package/src/components/ChartWidget.vue +46 -0
  18. package/src/components/ChatActionBar.vue +226 -0
  19. package/src/components/ChatBubble.vue +162 -0
  20. package/src/components/ChatComposer.vue +308 -0
  21. package/src/components/ChatQueue.vue +222 -0
  22. package/src/components/ChatThread.vue +197 -0
  23. package/src/components/CollisionWarning.stories.js +30 -0
  24. package/src/components/CollisionWarning.vue +68 -0
  25. package/src/components/ConditionalFieldRules.vue +135 -0
  26. package/src/components/ContextPanel.vue +66 -0
  27. package/src/components/ContextPanelSection.vue +65 -0
  28. package/src/components/CustomFieldRenderer.vue +159 -0
  29. package/src/components/EscalatedLayout.vue +424 -70
  30. package/src/components/FileDropzone.vue +57 -41
  31. package/src/components/FollowButton.stories.js +30 -0
  32. package/src/components/FollowButton.vue +88 -63
  33. package/src/components/KeyboardShortcutHelp.vue +213 -123
  34. package/src/components/KnowledgePanel.vue +122 -0
  35. package/src/components/KpiCard.stories.js +32 -0
  36. package/src/components/KpiCard.vue +88 -0
  37. package/src/components/MacroDropdown.vue +150 -90
  38. package/src/components/MentionDropdown.vue +154 -0
  39. package/src/components/MetricComparison.vue +89 -0
  40. package/src/components/PercentileChart.vue +67 -0
  41. package/src/components/PeriodSelector.vue +98 -0
  42. package/src/components/PermissionMatrix.vue +107 -0
  43. package/src/components/PinnedNotes.vue +129 -90
  44. package/src/components/PluginSlot.vue +68 -62
  45. package/src/components/PresenceIndicator.vue +138 -28
  46. package/src/components/PriorityBadge.stories.js +33 -0
  47. package/src/components/PriorityBadge.vue +46 -39
  48. package/src/components/QuickFilters.vue +65 -49
  49. package/src/components/ReplyComposer.vue +207 -30
  50. package/src/components/ReplyThread.vue +111 -21
  51. package/src/components/ReportChart.vue +395 -0
  52. package/src/components/ReportExportButton.vue +108 -0
  53. package/src/components/SatisfactionRating.vue +180 -128
  54. package/src/components/SaveViewDialog.vue +172 -0
  55. package/src/components/SavedViewSidebar.vue +208 -0
  56. package/src/components/ScheduleEditor.vue +104 -0
  57. package/src/components/SideConversation.vue +165 -0
  58. package/src/components/SideConversationList.vue +147 -0
  59. package/src/components/SkillTagManager.vue +79 -0
  60. package/src/components/SlaComplianceChart.vue +46 -0
  61. package/src/components/SlaTimer.stories.js +35 -0
  62. package/src/components/SlaTimer.vue +62 -49
  63. package/src/components/SnoozeButton.vue +247 -0
  64. package/src/components/StatsCard.stories.js +24 -0
  65. package/src/components/StatsCard.vue +18 -6
  66. package/src/components/StatusBadge.stories.js +53 -0
  67. package/src/components/StatusBadge.vue +78 -46
  68. package/src/components/TagSelect.vue +84 -53
  69. package/src/components/TicketFilters.vue +247 -46
  70. package/src/components/TicketLinkPanel.vue +193 -0
  71. package/src/components/TicketList.vue +536 -74
  72. package/src/components/TicketMergeDialog.vue +157 -0
  73. package/src/components/TicketSidebar.vue +167 -99
  74. package/src/components/TicketSplitDialog.vue +98 -0
  75. package/src/components/TicketTypeSelector.vue +21 -0
  76. package/src/components/TotpInput.stories.js +9 -0
  77. package/src/components/TotpInput.vue +81 -0
  78. package/src/components/TwoFactorSetup.vue +113 -0
  79. package/src/components/VariableTokenInput.vue +111 -0
  80. package/src/components/WorkflowActionConfig.vue +287 -0
  81. package/src/components/WorkflowActionList.vue +350 -0
  82. package/src/components/WorkflowConditionBuilder.vue +265 -0
  83. package/src/components/WorkflowTriggerSelector.vue +125 -0
  84. package/src/composables/useChat.js +187 -0
  85. package/src/composables/useDebouncedSearch.js +38 -0
  86. package/src/composables/useI18n.js +80 -80
  87. package/src/composables/useKeyboardShortcuts.js +46 -46
  88. package/src/composables/useMentions.js +134 -0
  89. package/src/composables/usePluginExtensions.js +159 -67
  90. package/src/composables/usePluginHooks.js +65 -0
  91. package/src/composables/useRealtime.js +182 -0
  92. package/src/defineEscalatedPlugin.js +76 -50
  93. package/src/index.js +77 -28
  94. package/src/locales/ar.json +727 -0
  95. package/src/locales/de.json +678 -430
  96. package/src/locales/en.json +717 -430
  97. package/src/locales/es.json +679 -430
  98. package/src/locales/fr.json +680 -431
  99. package/src/locales/index.js +12 -2
  100. package/src/locales/it.json +727 -0
  101. package/src/locales/ja.json +727 -0
  102. package/src/locales/ko.json +727 -0
  103. package/src/locales/nl.json +727 -0
  104. package/src/locales/pl.json +727 -0
  105. package/src/locales/pt-BR.json +727 -0
  106. package/src/locales/ru.json +727 -0
  107. package/src/locales/tr.json +727 -0
  108. package/src/locales/zh-CN.json +727 -0
  109. package/src/pages/Admin/ApiTokens/Index.vue +381 -217
  110. package/src/pages/Admin/AuditLog/Index.vue +154 -0
  111. package/src/pages/Admin/BusinessHours/Form.vue +172 -0
  112. package/src/pages/Admin/BusinessHours/Index.vue +113 -0
  113. package/src/pages/Admin/CannedResponses/Index.vue +82 -25
  114. package/src/pages/Admin/Capacity/Index.vue +134 -0
  115. package/src/pages/Admin/CustomFields/Form.vue +195 -0
  116. package/src/pages/Admin/CustomFields/Index.vue +134 -0
  117. package/src/pages/Admin/CustomObjects/Form.vue +202 -0
  118. package/src/pages/Admin/CustomObjects/Index.vue +127 -0
  119. package/src/pages/Admin/CustomObjects/Records.vue +228 -0
  120. package/src/pages/Admin/Departments/Form.vue +35 -11
  121. package/src/pages/Admin/Departments/Index.vue +74 -21
  122. package/src/pages/Admin/EscalationRules/Form.vue +86 -25
  123. package/src/pages/Admin/EscalationRules/Index.vue +158 -37
  124. package/src/pages/Admin/Import/Connect.vue +95 -0
  125. package/src/pages/Admin/Import/Index.vue +102 -0
  126. package/src/pages/Admin/Import/Mapping.vue +110 -0
  127. package/src/pages/Admin/Import/Progress.vue +153 -0
  128. package/src/pages/Admin/Import/Review.vue +76 -0
  129. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
  130. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
  131. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
  132. package/src/pages/Admin/Macros/Index.vue +448 -287
  133. package/src/pages/Admin/Plugins/Index.vue +327 -197
  134. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  135. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
  136. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  137. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  138. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  139. package/src/pages/Admin/Reports/CsatReport.vue +127 -0
  140. package/src/pages/Admin/Reports/Dashboard.vue +226 -0
  141. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  142. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  143. package/src/pages/Admin/Reports/SlaReport.vue +162 -0
  144. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  145. package/src/pages/Admin/Reports.vue +206 -101
  146. package/src/pages/Admin/Roles/Form.vue +71 -0
  147. package/src/pages/Admin/Roles/Index.vue +122 -0
  148. package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
  149. package/src/pages/Admin/Settings/DataRetention.vue +182 -0
  150. package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
  151. package/src/pages/Admin/Settings/Sandbox.vue +135 -0
  152. package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
  153. package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
  154. package/src/pages/Admin/Settings.vue +847 -99
  155. package/src/pages/Admin/Skills/Form.vue +53 -0
  156. package/src/pages/Admin/Skills/Index.vue +94 -0
  157. package/src/pages/Admin/SlaPolicies/Form.vue +62 -19
  158. package/src/pages/Admin/SlaPolicies/Index.vue +85 -23
  159. package/src/pages/Admin/Statuses/Form.vue +114 -0
  160. package/src/pages/Admin/Statuses/Index.vue +153 -0
  161. package/src/pages/Admin/Tags/Index.vue +121 -35
  162. package/src/pages/Admin/Tickets/Index.vue +96 -9
  163. package/src/pages/Admin/Tickets/Show.vue +130 -37
  164. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
  165. package/src/pages/Admin/Webhooks/Form.vue +110 -0
  166. package/src/pages/Admin/Webhooks/Index.vue +136 -0
  167. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  168. package/src/pages/Admin/Workflows/Index.vue +235 -0
  169. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  170. package/src/pages/Agent/Dashboard.vue +311 -186
  171. package/src/pages/Agent/TicketIndex.vue +80 -6
  172. package/src/pages/Agent/TicketShow.vue +399 -60
  173. package/src/pages/Customer/Create.vue +37 -10
  174. package/src/pages/Customer/Index.vue +9 -2
  175. package/src/pages/Customer/KnowledgeBase/Article.vue +129 -0
  176. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
  177. package/src/pages/Customer/Show.vue +69 -63
  178. package/src/pages/Guest/Create.vue +40 -15
  179. package/src/pages/Guest/Show.vue +124 -93
  180. package/src/plugin.js +111 -1
  181. package/src/utils/constants.js +30 -0
  182. package/src/utils/formatting.js +35 -0
  183. package/src/utils/mentionHighlight.js +24 -0
  184. package/src/utils/sanitizeHtml.js +78 -64
  185. package/src/widget/EscalatedWidget.vue +804 -0
  186. package/src/widget/index.js +254 -0
  187. package/src/pages/Admin/Plugins.vue +0 -129
@@ -4,6 +4,9 @@ import StatusBadge from '../../components/StatusBadge.vue';
4
4
  import PriorityBadge from '../../components/PriorityBadge.vue';
5
5
  import ReplyThread from '../../components/ReplyThread.vue';
6
6
  import ReplyComposer from '../../components/ReplyComposer.vue';
7
+ import ChatThread from '../../components/ChatThread.vue';
8
+ import ChatComposer from '../../components/ChatComposer.vue';
9
+ import ChatActionBar from '../../components/ChatActionBar.vue';
7
10
  import TicketSidebar from '../../components/TicketSidebar.vue';
8
11
  import AttachmentList from '../../components/AttachmentList.vue';
9
12
  import MacroDropdown from '../../components/MacroDropdown.vue';
@@ -12,10 +15,15 @@ import PresenceIndicator from '../../components/PresenceIndicator.vue';
12
15
  import PinnedNotes from '../../components/PinnedNotes.vue';
13
16
  import KeyboardShortcutHelp from '../../components/KeyboardShortcutHelp.vue';
14
17
  import PluginSlot from '../../components/PluginSlot.vue';
18
+ import KnowledgePanel from '../../components/KnowledgePanel.vue';
19
+ import ContextPanel from '../../components/ContextPanel.vue';
20
+ import ContextPanelSection from '../../components/ContextPanelSection.vue';
15
21
  import { useKeyboardShortcuts } from '../../composables/useKeyboardShortcuts';
16
22
  import { usePluginExtensions } from '../../composables/usePluginExtensions';
23
+ import { useRealtime } from '../../composables/useRealtime';
24
+ import { useChat } from '../../composables/useChat';
17
25
  import { router, useForm, usePage } from '@inertiajs/vue3';
18
- import { ref } from 'vue';
26
+ import { ref, computed, inject, onMounted } from 'vue';
19
27
 
20
28
  const props = defineProps({
21
29
  ticket: Object,
@@ -28,12 +36,19 @@ const props = defineProps({
28
36
  });
29
37
 
30
38
  const page = usePage();
31
- const activeTab = ref('reply');
39
+ const isLightAgent = computed(() => page.props.escalated?.agent_type === 'light');
40
+ // Route prefix the host framework mounts Escalated under. Defaults to
41
+ // `support`, matching every host-adapter plugin. NestJS uses `escalated`.
42
+ const routePrefix = computed(() => page.props.escalated?.prefix || 'support');
43
+ const widgetPath = computed(() => `/${routePrefix.value}/widget`);
44
+ const activeTab = ref(isLightAgent.value ? 'note' : 'reply');
32
45
  const showShortcutHelp = ref(false);
33
46
  const replyComposerRef = ref(null);
34
47
  const statusSelectRef = ref(null);
35
48
  const prioritySelectRef = ref(null);
36
49
 
50
+ const showContextPanel = ref(false);
51
+
37
52
  const statusForm = useForm({ status: '' });
38
53
  const priorityForm = useForm({ priority: '' });
39
54
  const assignForm = useForm({ agent_id: '' });
@@ -57,16 +72,108 @@ function toggleFollow() {
57
72
  router.post(route('escalated.agent.tickets.follow', props.ticket.reference), {}, { preserveScroll: true });
58
73
  }
59
74
 
75
+ const escDark = inject(
76
+ 'esc-dark',
77
+ computed(() => false),
78
+ );
79
+
80
+ // Live chat mode detection
81
+ const isChatMode = computed(() => props.ticket.channel === 'chat' && props.ticket.status === 'live');
82
+ const chatMessages = ref(props.ticket.chat_messages || []);
83
+ const chatTypingUser = ref(null);
84
+
85
+ // Subscribe to chat channel for real-time messages. Pass widgetPath so
86
+ // useChat's API calls resolve against whatever prefix the host framework
87
+ // serves Escalated under (config on NestJS, default /support elsewhere).
88
+ const { subscribeToChat } = useChat({ widgetPath: widgetPath.value });
89
+ let chatTypingTimer = null;
90
+
91
+ onMounted(() => {
92
+ if (!isChatMode.value || !props.ticket.chat_session_id) return;
93
+ subscribeToChat(props.ticket.chat_session_id, {
94
+ onMessage(data) {
95
+ chatMessages.value.push(data.message);
96
+ },
97
+ onTyping(data) {
98
+ chatTypingUser.value = data.name;
99
+ clearTimeout(chatTypingTimer);
100
+ chatTypingTimer = setTimeout(() => {
101
+ chatTypingUser.value = null;
102
+ }, 4000);
103
+ },
104
+ onEnded() {
105
+ router.reload({ preserveScroll: true });
106
+ },
107
+ onAssigned() {
108
+ router.reload({ preserveScroll: true });
109
+ },
110
+ });
111
+ });
112
+
113
+ function onChatSend(payload) {
114
+ // Optimistically add message to thread
115
+ chatMessages.value.push({
116
+ id: Date.now(),
117
+ body: payload.body,
118
+ is_internal_note: payload.is_internal_note,
119
+ is_agent: true,
120
+ author: { id: page.props.auth?.user?.id, name: page.props.auth?.user?.name },
121
+ created_at: new Date().toISOString(),
122
+ });
123
+ }
124
+
125
+ function onEndChat() {
126
+ router.reload({ preserveScroll: true });
127
+ }
128
+
60
129
  const { getPageComponents } = usePluginExtensions();
61
130
 
62
131
  // Keyboard shortcuts
63
132
  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; },
133
+ r: () => {
134
+ activeTab.value = 'reply';
135
+ replyComposerRef.value?.$el?.querySelector('textarea')?.focus();
136
+ },
137
+ n: () => {
138
+ activeTab.value = 'note';
139
+ },
140
+ s: () => {
141
+ statusSelectRef.value?.focus();
142
+ },
143
+ p: () => {
144
+ prioritySelectRef.value?.focus();
145
+ },
146
+ f: () => {
147
+ toggleFollow();
148
+ },
149
+ '?': () => {
150
+ showShortcutHelp.value = true;
151
+ },
152
+ });
153
+
154
+ // Real-time updates via WebSocket (gracefully degrades to no-op when Echo is absent)
155
+ const { echoAvailable, subscribeToTicket } = useRealtime();
156
+
157
+ onMounted(() => {
158
+ if (!echoAvailable.value || !props.ticket?.id) return;
159
+
160
+ subscribeToTicket(props.ticket.id, {
161
+ onReplyCreated() {
162
+ router.reload({ only: ['ticket'], preserveScroll: true });
163
+ },
164
+ onStatusChanged() {
165
+ router.reload({ only: ['ticket'], preserveScroll: true });
166
+ },
167
+ onTicketUpdated() {
168
+ router.reload({ only: ['ticket'], preserveScroll: true });
169
+ },
170
+ onTicketAssigned() {
171
+ router.reload({ only: ['ticket'], preserveScroll: true });
172
+ },
173
+ onTicketEscalated() {
174
+ router.reload({ only: ['ticket'], preserveScroll: true });
175
+ },
176
+ });
70
177
  });
71
178
  </script>
72
179
 
@@ -76,80 +183,312 @@ useKeyboardShortcuts({
76
183
  <PluginSlot slot="ticket.show.header" :components="getPageComponents('ticket.show', 'header')" />
77
184
 
78
185
  <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>
186
+ <span :class="['text-sm font-mono font-medium', escDark ? 'text-white' : 'text-gray-900']">{{
187
+ ticket.reference
188
+ }}</span>
80
189
  <StatusBadge :status="ticket.status" />
81
190
  <PriorityBadge :priority="ticket.priority" />
82
191
  <span class="text-sm text-neutral-500">by {{ ticket.requester?.name }}</span>
83
- <PresenceIndicator :ticket-reference="ticket.reference" route-prefix="escalated.agent" />
192
+ <PresenceIndicator
193
+ :ticket-reference="ticket.reference"
194
+ :ticket-id="ticket.id"
195
+ route-prefix="escalated.agent"
196
+ />
84
197
  <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">
198
+ <FollowButton
199
+ :is-following="is_following"
200
+ :followers-count="followers_count"
201
+ :action="route('escalated.agent.tickets.follow', ticket.reference)"
202
+ />
203
+ <MacroDropdown
204
+ v-if="macros.length && !isLightAgent"
205
+ :macros="macros"
206
+ :action="route('escalated.agent.tickets.macro', ticket.reference)"
207
+ />
208
+ <button
209
+ v-if="!ticket.assigned_to && !isLightAgent"
210
+ 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"
211
+ @click="assignToMe"
212
+ >
91
213
  Assign to Me
92
214
  </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>
215
+ <template v-if="!isLightAgent">
216
+ <select
217
+ ref="statusSelectRef"
218
+ :class="[
219
+ 'rounded-lg border px-3 py-1.5 text-sm focus:outline-none focus:ring-1',
220
+ escDark
221
+ ? 'border-white/10 bg-neutral-950 text-neutral-200 focus:border-white/20 focus:ring-white/10'
222
+ : 'border-gray-300 bg-white text-gray-700 focus:border-gray-400 focus:ring-gray-200',
223
+ ]"
224
+ @change="
225
+ changeStatus($event.target.value);
226
+ $event.target.value = '';
227
+ "
228
+ >
229
+ <option value="">Change Status...</option>
230
+ <option value="in_progress">In Progress</option>
231
+ <option value="waiting_on_customer">Waiting on Customer</option>
232
+ <option value="resolved">Resolved</option>
233
+ <option value="closed">Closed</option>
234
+ </select>
235
+ <select
236
+ ref="prioritySelectRef"
237
+ :class="[
238
+ 'rounded-lg border px-3 py-1.5 text-sm focus:outline-none focus:ring-1',
239
+ escDark
240
+ ? 'border-white/10 bg-neutral-950 text-neutral-200 focus:border-white/20 focus:ring-white/10'
241
+ : 'border-gray-300 bg-white text-gray-700 focus:border-gray-400 focus:ring-gray-200',
242
+ ]"
243
+ @change="
244
+ changePriority($event.target.value);
245
+ $event.target.value = '';
246
+ "
247
+ >
248
+ <option value="">Change Priority...</option>
249
+ <option value="low">Low</option>
250
+ <option value="medium">Medium</option>
251
+ <option value="high">High</option>
252
+ <option value="urgent">Urgent</option>
253
+ <option value="critical">Critical</option>
254
+ </select>
255
+ </template>
256
+ <!-- Context Panel Toggle -->
257
+ <button
258
+ type="button"
259
+ :class="[
260
+ 'rounded-lg border px-3 py-1.5 text-sm transition-colors',
261
+ showContextPanel
262
+ ? escDark
263
+ ? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-400'
264
+ : 'border-blue-300 bg-blue-50 text-blue-600'
265
+ : escDark
266
+ ? 'border-white/10 text-neutral-400 hover:border-white/20 hover:text-neutral-200'
267
+ : 'border-gray-300 text-gray-400 hover:border-gray-400 hover:text-gray-600',
268
+ ]"
269
+ @click="showContextPanel = !showContextPanel"
270
+ >
271
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
272
+ <path
273
+ stroke-linecap="round"
274
+ stroke-linejoin="round"
275
+ 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"
276
+ />
277
+ </svg>
278
+ </button>
110
279
  <!-- Plugin: ticket show actions slot -->
111
280
  <PluginSlot slot="ticket.show.actions" :components="getPageComponents('ticket.show', 'actions')" />
112
281
  </div>
113
282
  </div>
114
283
  <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
115
284
  <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>
285
+ <!-- Chat Action Bar (live chat mode) -->
286
+ <ChatActionBar
287
+ v-if="isChatMode"
288
+ :session-id="ticket.chat_session_id"
289
+ :started-at="ticket.chat_started_at || ticket.created_at"
290
+ :customer="{
291
+ name: ticket.requester?.name,
292
+ email: ticket.requester?.email,
293
+ page_url: ticket.chat_metadata?.page_url,
294
+ }"
295
+ :agents="[]"
296
+ :departments="departments"
297
+ @end-chat="onEndChat"
298
+ />
299
+
300
+ <PinnedNotes
301
+ v-if="ticket.pinned_notes?.length"
302
+ :notes="ticket.pinned_notes"
303
+ :ticket-reference="ticket.reference"
304
+ route-prefix="escalated.agent"
305
+ />
306
+ <div
307
+ :class="[
308
+ 'rounded-xl border p-5',
309
+ escDark ? 'border-white/[0.06] bg-neutral-900/60' : 'border-gray-200 bg-white',
310
+ ]"
311
+ >
312
+ <p :class="['whitespace-pre-wrap text-sm', escDark ? 'text-neutral-300' : 'text-gray-700']">
313
+ {{ ticket.description }}
314
+ </p>
120
315
  <AttachmentList v-if="ticket.attachments?.length" :attachments="ticket.attachments" class="mt-3" />
121
316
  </div>
122
- <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']">
126
- Reply
127
- </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']">
130
- Internal Note
131
- </button>
317
+ <!-- Live Chat Mode -->
318
+ <template v-if="isChatMode">
319
+ <div
320
+ :class="[
321
+ 'overflow-hidden rounded-xl border',
322
+ escDark ? 'border-white/[0.06] bg-neutral-900/60' : 'border-gray-200 bg-white',
323
+ ]"
324
+ >
325
+ <ChatThread
326
+ :messages="chatMessages"
327
+ :typing-user="chatTypingUser"
328
+ :current-user-id="page.props.auth?.user?.id"
329
+ />
330
+ <ChatComposer
331
+ :session-id="ticket.chat_session_id"
332
+ :send-endpoint="
333
+ ticket.chat_session_id ? `${widgetPath}/chat/${ticket.chat_session_id}/messages` : ''
334
+ "
335
+ :typing-endpoint="
336
+ ticket.chat_session_id ? `${widgetPath}/chat/${ticket.chat_session_id}/typing` : ''
337
+ "
338
+ allow-notes
339
+ @send="onChatSend"
340
+ />
132
341
  </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" />
140
- </div>
141
- <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 />
145
- </div>
342
+ </template>
343
+
344
+ <!-- Standard Reply Mode -->
345
+ <template v-else>
346
+ <div>
347
+ <div :class="['mb-4 flex gap-4 border-b', escDark ? 'border-white/[0.06]' : 'border-gray-200']">
348
+ <button
349
+ v-if="!isLightAgent"
350
+ :class="[
351
+ 'pb-2 text-sm font-medium transition-colors',
352
+ activeTab === 'reply'
353
+ ? escDark
354
+ ? 'border-b-2 border-cyan-500 text-white'
355
+ : 'border-b-2 border-blue-500 text-gray-900'
356
+ : escDark
357
+ ? 'text-neutral-500 hover:text-neutral-300'
358
+ : 'text-gray-400 hover:text-gray-600',
359
+ ]"
360
+ @click="activeTab = 'reply'"
361
+ >
362
+ Reply
363
+ </button>
364
+ <button
365
+ :class="[
366
+ 'pb-2 text-sm font-medium transition-colors',
367
+ activeTab === 'note'
368
+ ? escDark
369
+ ? 'border-b-2 border-amber-500 text-amber-400'
370
+ : 'border-b-2 border-amber-500 text-amber-700'
371
+ : escDark
372
+ ? 'text-neutral-500 hover:text-neutral-300'
373
+ : 'text-gray-400 hover:text-gray-600',
374
+ ]"
375
+ @click="activeTab = 'note'"
376
+ >
377
+ Internal Note
378
+ </button>
379
+ </div>
380
+ <ReplyComposer
381
+ v-if="activeTab === 'reply' && !isLightAgent"
382
+ ref="replyComposerRef"
383
+ :action="route('escalated.agent.tickets.reply', ticket.reference)"
384
+ :canned-responses="cannedResponses"
385
+ />
386
+ <ReplyComposer
387
+ v-if="activeTab === 'note'"
388
+ :action="route('escalated.agent.tickets.note', ticket.reference)"
389
+ placeholder="Write an internal note..."
390
+ submit-label="Add Note"
391
+ />
392
+ </div>
393
+ <div>
394
+ <h2 :class="['mb-4 text-lg font-semibold', escDark ? 'text-neutral-200' : 'text-gray-900']">
395
+ Conversation
396
+ </h2>
397
+ <ReplyThread
398
+ :replies="ticket.replies || []"
399
+ :current-user-id="page.props.auth?.user?.id"
400
+ :ticket-reference="ticket.reference"
401
+ route-prefix="escalated.agent"
402
+ pinnable
403
+ />
404
+ </div>
405
+ </template>
146
406
  </div>
147
407
  <div class="space-y-6">
148
408
  <TicketSidebar :ticket="ticket" :tags="tags" :departments="departments" />
409
+ <KnowledgePanel :ticket-reference="ticket.reference" />
149
410
  <!-- Plugin: ticket show sidebar slot -->
150
411
  <PluginSlot slot="ticket.show.sidebar" :components="getPageComponents('ticket.show', 'sidebar')" />
151
412
  </div>
152
413
  </div>
414
+ <!-- Context Panel -->
415
+ <ContextPanel v-model:visible="showContextPanel">
416
+ <ContextPanelSection
417
+ title="Customer Info"
418
+ 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"
419
+ >
420
+ <div class="space-y-2 text-sm">
421
+ <div class="flex justify-between">
422
+ <span :class="escDark ? 'text-neutral-500' : 'text-gray-500'">Name</span>
423
+ <span :class="escDark ? 'text-neutral-200' : 'text-gray-900'">{{
424
+ ticket.requester?.name || 'Unknown'
425
+ }}</span>
426
+ </div>
427
+ <div class="flex justify-between">
428
+ <span :class="escDark ? 'text-neutral-500' : 'text-gray-500'">Email</span>
429
+ <span :class="['truncate', escDark ? 'text-neutral-200' : 'text-gray-900']">{{
430
+ ticket.requester?.email || '---'
431
+ }}</span>
432
+ </div>
433
+ <div class="flex justify-between">
434
+ <span :class="escDark ? 'text-neutral-500' : 'text-gray-500'">Tickets</span>
435
+ <span :class="escDark ? 'text-neutral-200' : 'text-gray-900'">{{
436
+ ticket.requester_ticket_count ?? '---'
437
+ }}</span>
438
+ </div>
439
+ </div>
440
+ </ContextPanelSection>
441
+
442
+ <ContextPanelSection
443
+ title="Related Tickets"
444
+ 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"
445
+ :default-open="false"
446
+ >
447
+ <div v-if="ticket.related_tickets?.length" class="space-y-2">
448
+ <div
449
+ v-for="rt in ticket.related_tickets"
450
+ :key="rt.reference"
451
+ :class="[
452
+ 'rounded-lg border px-3 py-2',
453
+ escDark ? 'border-white/[0.06] bg-neutral-950' : 'border-gray-200 bg-gray-50',
454
+ ]"
455
+ >
456
+ <span :class="['font-mono text-xs', escDark ? 'text-cyan-400' : 'text-blue-600']">{{
457
+ rt.reference
458
+ }}</span>
459
+ <p :class="['mt-0.5 truncate text-xs', escDark ? 'text-neutral-400' : 'text-gray-500']">
460
+ {{ rt.subject }}
461
+ </p>
462
+ </div>
463
+ </div>
464
+ <p v-else :class="['text-xs', escDark ? 'text-neutral-600' : 'text-gray-400']">
465
+ No related tickets found.
466
+ </p>
467
+ </ContextPanelSection>
468
+
469
+ <ContextPanelSection
470
+ title="Recent Activity"
471
+ icon="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
472
+ :default-open="false"
473
+ >
474
+ <div v-if="ticket.activities?.length" class="space-y-2">
475
+ <div v-for="activity in ticket.activities.slice(0, 5)" :key="activity.id" class="text-xs">
476
+ <span :class="escDark ? 'text-neutral-500' : 'text-gray-600'">{{ activity.description }}</span>
477
+ <span :class="['ml-1', escDark ? 'text-neutral-600' : 'text-gray-400']">{{
478
+ activity.created_at_human
479
+ }}</span>
480
+ </div>
481
+ </div>
482
+ <p v-else :class="['text-xs', escDark ? 'text-neutral-600' : 'text-gray-400']">No recent activity.</p>
483
+ </ContextPanelSection>
484
+
485
+ <!-- Plugin: context panel slot -->
486
+ <PluginSlot
487
+ slot="ticket.show.context-panel"
488
+ :components="getPageComponents('ticket.show', 'context-panel')"
489
+ />
490
+ </ContextPanel>
491
+
153
492
  <KeyboardShortcutHelp v-model:show="showShortcutHelp" context="detail" />
154
493
  </EscalatedLayout>
155
494
  </template>
@@ -1,9 +1,12 @@
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
- const props = defineProps({
9
+ defineProps({
7
10
  departments: Array,
8
11
  priorities: Array,
9
12
  });
@@ -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>