@escalated-dev/escalated 0.8.0 → 0.11.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 (124) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +388 -361
  3. package/package.json +85 -84
  4. package/src/components/ActiveChatsPanel.vue +246 -246
  5. package/src/components/AdminDashboard.stories.js +1423 -1423
  6. package/src/components/AttachmentList.vue +65 -65
  7. package/src/components/ChatActionBar.vue +226 -226
  8. package/src/components/ChatBubble.vue +162 -162
  9. package/src/components/ChatComposer.vue +308 -308
  10. package/src/components/ChatQueue.vue +222 -222
  11. package/src/components/ChatThread.vue +197 -197
  12. package/src/components/CustomTicketActions.stories.js +71 -0
  13. package/src/components/CustomTicketActions.vue +101 -0
  14. package/src/components/EscalatedLayout.vue +628 -604
  15. package/src/components/FollowButton.vue +20 -1
  16. package/src/components/MentionDropdown.vue +154 -154
  17. package/src/components/MetricComparison.vue +89 -89
  18. package/src/components/PercentileChart.vue +67 -67
  19. package/src/components/PeriodSelector.vue +98 -98
  20. package/src/components/PresenceIndicator.vue +228 -213
  21. package/src/components/ReplyComposer.vue +336 -336
  22. package/src/components/ReplyThread.vue +153 -153
  23. package/src/components/ReportChart.vue +395 -395
  24. package/src/components/ReportExportButton.vue +108 -108
  25. package/src/components/SaveViewDialog.vue +172 -172
  26. package/src/components/SavedViewSidebar.vue +208 -208
  27. package/src/components/SkillTagManager.vue +3 -0
  28. package/src/components/SnoozeButton.vue +247 -247
  29. package/src/components/TicketFilters.vue +270 -270
  30. package/src/components/TicketList.vue +655 -655
  31. package/src/components/TicketSidebar.vue +4 -0
  32. package/src/components/TicketSplitDialog.vue +98 -98
  33. package/src/components/TicketSubjects.stories.js +63 -0
  34. package/src/components/TicketSubjects.vue +107 -0
  35. package/src/components/TwoFactorSetup.vue +1 -37
  36. package/src/components/VariableTokenInput.vue +111 -111
  37. package/src/components/WorkflowActionConfig.vue +287 -287
  38. package/src/components/WorkflowActionList.vue +350 -350
  39. package/src/components/WorkflowConditionBuilder.vue +265 -265
  40. package/src/components/WorkflowTriggerSelector.vue +125 -125
  41. package/src/components/admin/newsletters/AnalyticsTiles.stories.js +14 -0
  42. package/src/components/admin/newsletters/AnalyticsTiles.vue +55 -0
  43. package/src/components/admin/newsletters/DeliveriesTable.stories.js +28 -0
  44. package/src/components/admin/newsletters/DeliveriesTable.vue +104 -0
  45. package/src/components/admin/newsletters/DynamicFilterBuilder.stories.js +11 -0
  46. package/src/components/admin/newsletters/DynamicFilterBuilder.vue +53 -0
  47. package/src/components/admin/newsletters/ListMemberTable.stories.js +12 -0
  48. package/src/components/admin/newsletters/ListMemberTable.vue +53 -0
  49. package/src/components/admin/newsletters/MarkdownEditor.stories.js +6 -0
  50. package/src/components/admin/newsletters/MarkdownEditor.vue +53 -0
  51. package/src/components/admin/newsletters/MergeFieldDropdown.stories.js +6 -0
  52. package/src/components/admin/newsletters/MergeFieldDropdown.vue +84 -0
  53. package/src/components/admin/newsletters/PreviewIframe.stories.js +7 -0
  54. package/src/components/admin/newsletters/PreviewIframe.vue +40 -0
  55. package/src/components/admin/newsletters/SummaryTiles.stories.js +8 -0
  56. package/src/components/admin/newsletters/SummaryTiles.vue +63 -0
  57. package/src/composables/useChat.js +187 -187
  58. package/src/composables/useI18n.js +102 -102
  59. package/src/composables/useMentions.js +134 -134
  60. package/src/composables/usePermissions.js +31 -0
  61. package/src/composables/useRealtime.js +182 -182
  62. package/src/index.js +85 -84
  63. package/src/locales/README.md +13 -13
  64. package/src/locales/ar.json +727 -727
  65. package/src/locales/de.json +690 -690
  66. package/src/locales/en.json +889 -729
  67. package/src/locales/es.json +691 -691
  68. package/src/locales/fr.json +691 -691
  69. package/src/locales/index.js +16 -16
  70. package/src/locales/it.json +727 -727
  71. package/src/locales/ja.json +727 -727
  72. package/src/locales/ko.json +727 -727
  73. package/src/locales/nl.json +727 -727
  74. package/src/locales/pl.json +727 -727
  75. package/src/locales/pt-BR.json +727 -727
  76. package/src/locales/ru.json +727 -727
  77. package/src/locales/tr.json +727 -727
  78. package/src/locales/zh-CN.json +727 -727
  79. package/src/pages/Admin/Automations/Form.vue +280 -0
  80. package/src/pages/Admin/Automations/Index.vue +230 -0
  81. package/src/pages/Admin/Newsletters/Compose.vue +192 -0
  82. package/src/pages/Admin/Newsletters/Edit.vue +24 -0
  83. package/src/pages/Admin/Newsletters/Index.vue +141 -0
  84. package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
  85. package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
  86. package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
  87. package/src/pages/Admin/Newsletters/Settings.vue +85 -0
  88. package/src/pages/Admin/Newsletters/Show.vue +140 -0
  89. package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
  90. package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
  91. package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
  92. package/src/pages/Admin/Newsletters/usePreview.js +44 -0
  93. package/src/pages/Admin/Plugins/Index.vue +10 -2
  94. package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
  95. package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
  96. package/src/pages/Admin/Reports/Cohorts.vue +180 -180
  97. package/src/pages/Admin/Reports/Comparison.vue +141 -141
  98. package/src/pages/Admin/Reports/Dashboard.vue +226 -226
  99. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
  100. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
  101. package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
  102. package/src/pages/Admin/Settings/DatabaseConnection.vue +228 -0
  103. package/src/pages/Admin/Settings/PublicTickets.vue +163 -0
  104. package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
  105. package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
  106. package/src/pages/Admin/Settings.vue +1008 -1008
  107. package/src/pages/Admin/Skills/Form.vue +30 -10
  108. package/src/pages/Admin/Tickets/Index.vue +126 -126
  109. package/src/pages/Admin/Tickets/Show.vue +254 -249
  110. package/src/pages/Admin/Users/Index.vue +145 -145
  111. package/src/pages/Admin/Webhooks/DeliveryLog.vue +1 -2
  112. package/src/pages/Admin/Workflows/Builder.vue +207 -207
  113. package/src/pages/Admin/Workflows/Form.vue +24 -0
  114. package/src/pages/Admin/Workflows/Index.vue +235 -235
  115. package/src/pages/Admin/Workflows/Logs.vue +286 -286
  116. package/src/pages/Agent/TicketIndex.vue +112 -112
  117. package/src/pages/Agent/TicketShow.vue +547 -494
  118. package/src/pages/Customer/Index.vue +56 -56
  119. package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
  120. package/src/pages/Plugin/Page.vue +61 -0
  121. package/src/plugin.js +175 -175
  122. package/src/utils/mentionHighlight.js +24 -24
  123. package/src/widget/EscalatedWidget.vue +804 -804
  124. package/src/widget/index.js +254 -254
@@ -1,249 +1,254 @@
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 MacroDropdown from '../../../components/MacroDropdown.vue';
10
- import FollowButton from '../../../components/FollowButton.vue';
11
- import PresenceIndicator from '../../../components/PresenceIndicator.vue';
12
- import PinnedNotes from '../../../components/PinnedNotes.vue';
13
- import KeyboardShortcutHelp from '../../../components/KeyboardShortcutHelp.vue';
14
- import PluginSlot from '../../../components/PluginSlot.vue';
15
- import SnoozeButton from '../../../components/SnoozeButton.vue';
16
- import { useKeyboardShortcuts } from '../../../composables/useKeyboardShortcuts';
17
- import { usePluginExtensions } from '../../../composables/usePluginExtensions';
18
- import { router, useForm, usePage } from '@inertiajs/vue3';
19
- import { ref } from 'vue';
20
-
21
- const props = defineProps({
22
- ticket: Object,
23
- departments: Array,
24
- tags: Array,
25
- cannedResponses: Array,
26
- agents: Array,
27
- macros: { type: Array, default: () => [] },
28
- is_following: { type: Boolean, default: false },
29
- followers_count: { type: Number, default: 0 },
30
- is_snoozed: { type: Boolean, default: false },
31
- });
32
-
33
- const page = usePage();
34
- const activeTab = ref('reply');
35
- const showShortcutHelp = ref(false);
36
- const replyComposerRef = ref(null);
37
- const statusSelectRef = ref(null);
38
- const prioritySelectRef = ref(null);
39
-
40
- const statusForm = useForm({ status: '' });
41
- const priorityForm = useForm({ priority: '' });
42
- const assignForm = useForm({ agent_id: '' });
43
-
44
- function changeStatus(status) {
45
- statusForm.status = status;
46
- statusForm.post(route('escalated.admin.tickets.status', props.ticket.reference), { preserveScroll: true });
47
- }
48
-
49
- function changePriority(priority) {
50
- priorityForm.priority = priority;
51
- priorityForm.post(route('escalated.admin.tickets.priority', props.ticket.reference), { preserveScroll: true });
52
- }
53
-
54
- function assignToMe() {
55
- assignForm.agent_id = page.props.auth.user.id;
56
- assignForm.post(route('escalated.admin.tickets.assign', props.ticket.reference), { preserveScroll: true });
57
- }
58
-
59
- function toggleFollow() {
60
- router.post(route('escalated.admin.tickets.follow', props.ticket.reference), {}, { preserveScroll: true });
61
- }
62
-
63
- const { getPageComponents } = usePluginExtensions();
64
-
65
- // Keyboard shortcuts
66
- useKeyboardShortcuts({
67
- r: () => {
68
- activeTab.value = 'reply';
69
- replyComposerRef.value?.$el?.querySelector('textarea')?.focus();
70
- },
71
- n: () => {
72
- activeTab.value = 'note';
73
- },
74
- s: () => {
75
- statusSelectRef.value?.focus();
76
- },
77
- p: () => {
78
- prioritySelectRef.value?.focus();
79
- },
80
- f: () => {
81
- toggleFollow();
82
- },
83
- '?': () => {
84
- showShortcutHelp.value = true;
85
- },
86
- });
87
- </script>
88
-
89
- <template>
90
- <EscalatedLayout :title="ticket.subject">
91
- <!-- Plugin: ticket show header slot -->
92
- <PluginSlot slot="ticket.show.header" :components="getPageComponents('ticket.show', 'header')" />
93
-
94
- <!-- Snoozed banner (visible only when ticket is snoozed) -->
95
- <div
96
- v-if="ticket.is_snoozed || (ticket.snoozed_until && new Date(ticket.snoozed_until) > new Date())"
97
- class="mb-4 flex items-center gap-3 rounded-lg px-4 py-2.5 text-sm font-medium border border-amber-500/20 bg-amber-500/10 text-amber-300"
98
- >
99
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" viewBox="0 0 20 20" fill="currentColor">
100
- <path
101
- fill-rule="evenodd"
102
- d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.828a1 1 0 101.415-1.414L11 9.586V6z"
103
- clip-rule="evenodd"
104
- />
105
- </svg>
106
- <span
107
- >Snoozed until
108
- {{
109
- new Date(ticket.snoozed_until).toLocaleString(undefined, {
110
- month: 'short',
111
- day: 'numeric',
112
- year: 'numeric',
113
- hour: 'numeric',
114
- minute: '2-digit',
115
- })
116
- }}</span
117
- >
118
- </div>
119
-
120
- <div class="mb-5 flex flex-wrap items-center gap-3">
121
- <span class="text-sm font-mono font-medium text-[var(--esc-panel-text)]">{{ ticket.reference }}</span>
122
- <StatusBadge :status="ticket.status" />
123
- <PriorityBadge :priority="ticket.priority" />
124
- <span class="text-sm text-[var(--esc-panel-text-muted)]">by {{ ticket.requester?.name }}</span>
125
- <PresenceIndicator :ticket-reference="ticket.reference" route-prefix="escalated.admin" />
126
- <div class="ml-auto flex items-center gap-2">
127
- <FollowButton
128
- :is-following="is_following"
129
- :followers-count="followers_count"
130
- :action="route('escalated.admin.tickets.follow', ticket.reference)"
131
- />
132
- <MacroDropdown
133
- v-if="macros.length"
134
- :macros="macros"
135
- :action="route('escalated.admin.tickets.macro', ticket.reference)"
136
- />
137
- <button
138
- v-if="!ticket.assigned_to"
139
- class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-3 py-1.5 text-sm font-medium text-white shadow-lg shadow-[var(--esc-panel-bg)]/20 transition-all hover:from-[var(--esc-panel-accent-hover)] hover:to-[var(--esc-panel-accent-secondary-hover)]"
140
- @click="assignToMe"
141
- >
142
- Assign to Me
143
- </button>
144
- <SnoozeButton :ticket="ticket" route-prefix="escalated.admin" />
145
- <select
146
- ref="statusSelectRef"
147
- class="rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
148
- @change="
149
- changeStatus($event.target.value);
150
- $event.target.value = '';
151
- "
152
- >
153
- <option value="">Change Status...</option>
154
- <option value="in_progress">In Progress</option>
155
- <option value="waiting_on_customer">Waiting on Customer</option>
156
- <option value="resolved">Resolved</option>
157
- <option value="closed">Closed</option>
158
- </select>
159
- <select
160
- ref="prioritySelectRef"
161
- class="rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
162
- @change="
163
- changePriority($event.target.value);
164
- $event.target.value = '';
165
- "
166
- >
167
- <option value="">Change Priority...</option>
168
- <option value="low">Low</option>
169
- <option value="medium">Medium</option>
170
- <option value="high">High</option>
171
- <option value="urgent">Urgent</option>
172
- <option value="critical">Critical</option>
173
- </select>
174
- <!-- Plugin: ticket show actions slot -->
175
- <PluginSlot slot="ticket.show.actions" :components="getPageComponents('ticket.show', 'actions')" />
176
- </div>
177
- </div>
178
- <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
179
- <div class="lg:col-span-2 space-y-6">
180
- <PinnedNotes
181
- v-if="ticket.pinned_notes?.length"
182
- :notes="ticket.pinned_notes"
183
- :ticket-reference="ticket.reference"
184
- route-prefix="escalated.admin"
185
- />
186
- <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5">
187
- <p class="whitespace-pre-wrap text-sm text-[var(--esc-panel-text-secondary)]">
188
- {{ ticket.description }}
189
- </p>
190
- <AttachmentList v-if="ticket.attachments?.length" :attachments="ticket.attachments" class="mt-3" />
191
- </div>
192
- <div>
193
- <div class="mb-4 flex gap-4 border-b border-[var(--esc-panel-border)]">
194
- <button
195
- :class="[
196
- 'pb-2 text-sm font-medium transition-colors',
197
- activeTab === 'reply'
198
- ? 'border-b-2 border-[var(--esc-panel-accent)] text-[var(--esc-panel-text)]'
199
- : 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]',
200
- ]"
201
- @click="activeTab = 'reply'"
202
- >
203
- Reply
204
- </button>
205
- <button
206
- :class="[
207
- 'pb-2 text-sm font-medium transition-colors',
208
- activeTab === 'note'
209
- ? 'border-b-2 border-amber-500 text-amber-400'
210
- : 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]',
211
- ]"
212
- @click="activeTab = 'note'"
213
- >
214
- Internal Note
215
- </button>
216
- </div>
217
- <ReplyComposer
218
- v-if="activeTab === 'reply'"
219
- ref="replyComposerRef"
220
- :action="route('escalated.admin.tickets.reply', ticket.reference)"
221
- :canned-responses="cannedResponses"
222
- />
223
- <ReplyComposer
224
- v-else
225
- :action="route('escalated.admin.tickets.note', ticket.reference)"
226
- placeholder="Write an internal note..."
227
- submit-label="Add Note"
228
- />
229
- </div>
230
- <div>
231
- <h2 class="mb-4 text-lg font-semibold text-[var(--esc-panel-text-secondary)]">Conversation</h2>
232
- <ReplyThread
233
- :replies="ticket.replies || []"
234
- :current-user-id="page.props.auth?.user?.id"
235
- :ticket-reference="ticket.reference"
236
- route-prefix="escalated.admin"
237
- pinnable
238
- />
239
- </div>
240
- </div>
241
- <div class="space-y-6">
242
- <TicketSidebar :ticket="ticket" :tags="tags" :departments="departments" />
243
- <!-- Plugin: ticket show sidebar slot -->
244
- <PluginSlot slot="ticket.show.sidebar" :components="getPageComponents('ticket.show', 'sidebar')" />
245
- </div>
246
- </div>
247
- <KeyboardShortcutHelp v-model:show="showShortcutHelp" context="detail" />
248
- </EscalatedLayout>
249
- </template>
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 MacroDropdown from '../../../components/MacroDropdown.vue';
10
+ import FollowButton from '../../../components/FollowButton.vue';
11
+ import PresenceIndicator from '../../../components/PresenceIndicator.vue';
12
+ import PinnedNotes from '../../../components/PinnedNotes.vue';
13
+ import KeyboardShortcutHelp from '../../../components/KeyboardShortcutHelp.vue';
14
+ import PluginSlot from '../../../components/PluginSlot.vue';
15
+ import SnoozeButton from '../../../components/SnoozeButton.vue';
16
+ import { useKeyboardShortcuts } from '../../../composables/useKeyboardShortcuts';
17
+ import { usePluginExtensions } from '../../../composables/usePluginExtensions';
18
+ import { router, useForm, usePage } from '@inertiajs/vue3';
19
+ import { ref } from 'vue';
20
+
21
+ const props = defineProps({
22
+ ticket: Object,
23
+ departments: Array,
24
+ tags: Array,
25
+ cannedResponses: Array,
26
+ agents: Array,
27
+ macros: { type: Array, default: () => [] },
28
+ is_following: { type: Boolean, default: false },
29
+ followers_count: { type: Number, default: 0 },
30
+ is_snoozed: { type: Boolean, default: false },
31
+ });
32
+
33
+ const page = usePage();
34
+ const activeTab = ref('reply');
35
+ const showShortcutHelp = ref(false);
36
+ const replyComposerRef = ref(null);
37
+ const statusSelectRef = ref(null);
38
+ const prioritySelectRef = ref(null);
39
+
40
+ const statusForm = useForm({ status: '' });
41
+ const priorityForm = useForm({ priority: '' });
42
+ const assignForm = useForm({ agent_id: '' });
43
+
44
+ function changeStatus(status) {
45
+ statusForm.status = status;
46
+ statusForm.post(route('escalated.admin.tickets.status', props.ticket.reference), { preserveScroll: true });
47
+ }
48
+
49
+ function changePriority(priority) {
50
+ priorityForm.priority = priority;
51
+ priorityForm.post(route('escalated.admin.tickets.priority', props.ticket.reference), { preserveScroll: true });
52
+ }
53
+
54
+ function assignToMe() {
55
+ // auth.user may be absent if the host app hasn't shared it (other call
56
+ // sites in this page already guard with auth?.user?.id). Bail rather than
57
+ // crash on the click.
58
+ const userId = page.props.auth?.user?.id;
59
+ if (userId == null) return;
60
+ assignForm.agent_id = userId;
61
+ assignForm.post(route('escalated.admin.tickets.assign', props.ticket.reference), { preserveScroll: true });
62
+ }
63
+
64
+ function toggleFollow() {
65
+ router.post(route('escalated.admin.tickets.follow', props.ticket.reference), {}, { preserveScroll: true });
66
+ }
67
+
68
+ const { getPageComponents } = usePluginExtensions();
69
+
70
+ // Keyboard shortcuts
71
+ useKeyboardShortcuts({
72
+ r: () => {
73
+ activeTab.value = 'reply';
74
+ replyComposerRef.value?.$el?.querySelector('textarea')?.focus();
75
+ },
76
+ n: () => {
77
+ activeTab.value = 'note';
78
+ },
79
+ s: () => {
80
+ statusSelectRef.value?.focus();
81
+ },
82
+ p: () => {
83
+ prioritySelectRef.value?.focus();
84
+ },
85
+ f: () => {
86
+ toggleFollow();
87
+ },
88
+ '?': () => {
89
+ showShortcutHelp.value = true;
90
+ },
91
+ });
92
+ </script>
93
+
94
+ <template>
95
+ <EscalatedLayout :title="ticket.subject">
96
+ <!-- Plugin: ticket show header slot -->
97
+ <PluginSlot slot="ticket.show.header" :components="getPageComponents('ticket.show', 'header')" />
98
+
99
+ <!-- Snoozed banner (visible only when ticket is snoozed) -->
100
+ <div
101
+ v-if="ticket.is_snoozed || (ticket.snoozed_until && new Date(ticket.snoozed_until) > new Date())"
102
+ class="mb-4 flex items-center gap-3 rounded-lg px-4 py-2.5 text-sm font-medium border border-amber-500/20 bg-amber-500/10 text-amber-300"
103
+ >
104
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" viewBox="0 0 20 20" fill="currentColor">
105
+ <path
106
+ fill-rule="evenodd"
107
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.828a1 1 0 101.415-1.414L11 9.586V6z"
108
+ clip-rule="evenodd"
109
+ />
110
+ </svg>
111
+ <span
112
+ >Snoozed until
113
+ {{
114
+ new Date(ticket.snoozed_until).toLocaleString(undefined, {
115
+ month: 'short',
116
+ day: 'numeric',
117
+ year: 'numeric',
118
+ hour: 'numeric',
119
+ minute: '2-digit',
120
+ })
121
+ }}</span
122
+ >
123
+ </div>
124
+
125
+ <div class="mb-5 flex flex-wrap items-center gap-3">
126
+ <span class="text-sm font-mono font-medium text-[var(--esc-panel-text)]">{{ ticket.reference }}</span>
127
+ <StatusBadge :status="ticket.status" />
128
+ <PriorityBadge :priority="ticket.priority" />
129
+ <span class="text-sm text-[var(--esc-panel-text-muted)]">by {{ ticket.requester?.name }}</span>
130
+ <PresenceIndicator :ticket-reference="ticket.reference" route-prefix="escalated.admin" />
131
+ <div class="ml-auto flex items-center gap-2">
132
+ <FollowButton
133
+ :is-following="is_following"
134
+ :followers-count="followers_count"
135
+ :action="route('escalated.admin.tickets.follow', ticket.reference)"
136
+ />
137
+ <MacroDropdown
138
+ v-if="macros.length"
139
+ :macros="macros"
140
+ :action="route('escalated.admin.tickets.macro', ticket.reference)"
141
+ />
142
+ <button
143
+ v-if="!ticket.assigned_to"
144
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-3 py-1.5 text-sm font-medium text-white shadow-lg shadow-[var(--esc-panel-bg)]/20 transition-all hover:from-[var(--esc-panel-accent-hover)] hover:to-[var(--esc-panel-accent-secondary-hover)]"
145
+ @click="assignToMe"
146
+ >
147
+ Assign to Me
148
+ </button>
149
+ <SnoozeButton :ticket="ticket" route-prefix="escalated.admin" />
150
+ <select
151
+ ref="statusSelectRef"
152
+ class="rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
153
+ @change="
154
+ changeStatus($event.target.value);
155
+ $event.target.value = '';
156
+ "
157
+ >
158
+ <option value="">Change Status...</option>
159
+ <option value="in_progress">In Progress</option>
160
+ <option value="waiting_on_customer">Waiting on Customer</option>
161
+ <option value="resolved">Resolved</option>
162
+ <option value="closed">Closed</option>
163
+ </select>
164
+ <select
165
+ ref="prioritySelectRef"
166
+ class="rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
167
+ @change="
168
+ changePriority($event.target.value);
169
+ $event.target.value = '';
170
+ "
171
+ >
172
+ <option value="">Change Priority...</option>
173
+ <option value="low">Low</option>
174
+ <option value="medium">Medium</option>
175
+ <option value="high">High</option>
176
+ <option value="urgent">Urgent</option>
177
+ <option value="critical">Critical</option>
178
+ </select>
179
+ <!-- Plugin: ticket show actions slot -->
180
+ <PluginSlot slot="ticket.show.actions" :components="getPageComponents('ticket.show', 'actions')" />
181
+ </div>
182
+ </div>
183
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
184
+ <div class="lg:col-span-2 space-y-6">
185
+ <PinnedNotes
186
+ v-if="ticket.pinned_notes?.length"
187
+ :notes="ticket.pinned_notes"
188
+ :ticket-reference="ticket.reference"
189
+ route-prefix="escalated.admin"
190
+ />
191
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5">
192
+ <p class="whitespace-pre-wrap text-sm text-[var(--esc-panel-text-secondary)]">
193
+ {{ ticket.description }}
194
+ </p>
195
+ <AttachmentList v-if="ticket.attachments?.length" :attachments="ticket.attachments" class="mt-3" />
196
+ </div>
197
+ <div>
198
+ <div class="mb-4 flex gap-4 border-b border-[var(--esc-panel-border)]">
199
+ <button
200
+ :class="[
201
+ 'pb-2 text-sm font-medium transition-colors',
202
+ activeTab === 'reply'
203
+ ? 'border-b-2 border-[var(--esc-panel-accent)] text-[var(--esc-panel-text)]'
204
+ : 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]',
205
+ ]"
206
+ @click="activeTab = 'reply'"
207
+ >
208
+ Reply
209
+ </button>
210
+ <button
211
+ :class="[
212
+ 'pb-2 text-sm font-medium transition-colors',
213
+ activeTab === 'note'
214
+ ? 'border-b-2 border-amber-500 text-amber-400'
215
+ : 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]',
216
+ ]"
217
+ @click="activeTab = 'note'"
218
+ >
219
+ Internal Note
220
+ </button>
221
+ </div>
222
+ <ReplyComposer
223
+ v-if="activeTab === 'reply'"
224
+ ref="replyComposerRef"
225
+ :action="route('escalated.admin.tickets.reply', ticket.reference)"
226
+ :canned-responses="cannedResponses"
227
+ />
228
+ <ReplyComposer
229
+ v-else
230
+ :action="route('escalated.admin.tickets.note', ticket.reference)"
231
+ placeholder="Write an internal note..."
232
+ submit-label="Add Note"
233
+ />
234
+ </div>
235
+ <div>
236
+ <h2 class="mb-4 text-lg font-semibold text-[var(--esc-panel-text-secondary)]">Conversation</h2>
237
+ <ReplyThread
238
+ :replies="ticket.replies || []"
239
+ :current-user-id="page.props.auth?.user?.id"
240
+ :ticket-reference="ticket.reference"
241
+ route-prefix="escalated.admin"
242
+ pinnable
243
+ />
244
+ </div>
245
+ </div>
246
+ <div class="space-y-6">
247
+ <TicketSidebar :ticket="ticket" :tags="tags" :departments="departments" />
248
+ <!-- Plugin: ticket show sidebar slot -->
249
+ <PluginSlot slot="ticket.show.sidebar" :components="getPageComponents('ticket.show', 'sidebar')" />
250
+ </div>
251
+ </div>
252
+ <KeyboardShortcutHelp v-model:show="showShortcutHelp" context="detail" />
253
+ </EscalatedLayout>
254
+ </template>