@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
@@ -12,6 +12,7 @@ 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 SnoozeButton from '../../../components/SnoozeButton.vue';
15
16
  import { useKeyboardShortcuts } from '../../../composables/useKeyboardShortcuts';
16
17
  import { usePluginExtensions } from '../../../composables/usePluginExtensions';
17
18
  import { router, useForm, usePage } from '@inertiajs/vue3';
@@ -26,6 +27,7 @@ const props = defineProps({
26
27
  macros: { type: Array, default: () => [] },
27
28
  is_following: { type: Boolean, default: false },
28
29
  followers_count: { type: Number, default: 0 },
30
+ is_snoozed: { type: Boolean, default: false },
29
31
  });
30
32
 
31
33
  const page = usePage();
@@ -62,12 +64,25 @@ const { getPageComponents } = usePluginExtensions();
62
64
 
63
65
  // Keyboard shortcuts
64
66
  useKeyboardShortcuts({
65
- 'r': () => { activeTab.value = 'reply'; replyComposerRef.value?.$el?.querySelector('textarea')?.focus(); },
66
- 'n': () => { activeTab.value = 'note'; },
67
- 's': () => { statusSelectRef.value?.focus(); },
68
- 'p': () => { prioritySelectRef.value?.focus(); },
69
- 'f': () => { toggleFollow(); },
70
- '?': () => { showShortcutHelp.value = true; },
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
+ },
71
86
  });
72
87
  </script>
73
88
 
@@ -76,31 +91,79 @@ useKeyboardShortcuts({
76
91
  <!-- Plugin: ticket show header slot -->
77
92
  <PluginSlot slot="ticket.show.header" :components="getPageComponents('ticket.show', 'header')" />
78
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
+
79
120
  <div class="mb-5 flex flex-wrap items-center gap-3">
80
- <span class="text-sm font-mono font-medium text-white">{{ ticket.reference }}</span>
121
+ <span class="text-sm font-mono font-medium text-[var(--esc-panel-text)]">{{ ticket.reference }}</span>
81
122
  <StatusBadge :status="ticket.status" />
82
123
  <PriorityBadge :priority="ticket.priority" />
83
- <span class="text-sm text-neutral-500">by {{ ticket.requester?.name }}</span>
124
+ <span class="text-sm text-[var(--esc-panel-text-muted)]">by {{ ticket.requester?.name }}</span>
84
125
  <PresenceIndicator :ticket-reference="ticket.reference" route-prefix="escalated.admin" />
85
126
  <div class="ml-auto flex items-center gap-2">
86
- <FollowButton :is-following="is_following" :followers-count="followers_count"
87
- :action="route('escalated.admin.tickets.follow', ticket.reference)" />
88
- <MacroDropdown v-if="macros.length" :macros="macros"
89
- :action="route('escalated.admin.tickets.macro', ticket.reference)" />
90
- <button v-if="!ticket.assigned_to" @click="assignToMe"
91
- 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">
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
+ >
92
142
  Assign to Me
93
143
  </button>
94
- <select ref="statusSelectRef" @change="changeStatus($event.target.value); $event.target.value = ''"
95
- 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">
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
+ >
96
153
  <option value="">Change Status...</option>
97
154
  <option value="in_progress">In Progress</option>
98
155
  <option value="waiting_on_customer">Waiting on Customer</option>
99
156
  <option value="resolved">Resolved</option>
100
157
  <option value="closed">Closed</option>
101
158
  </select>
102
- <select ref="prioritySelectRef" @change="changePriority($event.target.value); $event.target.value = ''"
103
- 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">
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
+ >
104
167
  <option value="">Change Priority...</option>
105
168
  <option value="low">Low</option>
106
169
  <option value="medium">Medium</option>
@@ -114,35 +177,65 @@ useKeyboardShortcuts({
114
177
  </div>
115
178
  <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
116
179
  <div class="lg:col-span-2 space-y-6">
117
- <PinnedNotes v-if="ticket.pinned_notes?.length" :notes="ticket.pinned_notes"
118
- :ticket-reference="ticket.reference" route-prefix="escalated.admin" />
119
- <div class="rounded-xl border border-white/[0.06] bg-neutral-900/60 p-5">
120
- <p class="whitespace-pre-wrap text-sm text-neutral-300">{{ ticket.description }}</p>
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>
121
190
  <AttachmentList v-if="ticket.attachments?.length" :attachments="ticket.attachments" class="mt-3" />
122
191
  </div>
123
192
  <div>
124
- <div class="mb-4 flex gap-4 border-b border-white/[0.06]">
125
- <button @click="activeTab = 'reply'"
126
- :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']">
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
+ >
127
203
  Reply
128
204
  </button>
129
- <button @click="activeTab = 'note'"
130
- :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']">
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
+ >
131
214
  Internal Note
132
215
  </button>
133
216
  </div>
134
- <ReplyComposer v-if="activeTab === 'reply'" ref="replyComposerRef"
135
- :action="route('escalated.admin.tickets.reply', ticket.reference)"
136
- :canned-responses="cannedResponses" />
137
- <ReplyComposer v-else
138
- :action="route('escalated.admin.tickets.note', ticket.reference)"
139
- placeholder="Write an internal note..."
140
- submit-label="Add Note" />
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
+ />
141
229
  </div>
142
230
  <div>
143
- <h2 class="mb-4 text-lg font-semibold text-neutral-200">Conversation</h2>
144
- <ReplyThread :replies="ticket.replies || []" :current-user-id="page.props.auth?.user?.id"
145
- :ticket-reference="ticket.reference" route-prefix="escalated.admin" pinnable />
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
+ />
146
239
  </div>
147
240
  </div>
148
241
  <div class="space-y-6">
@@ -0,0 +1,120 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { router } from '@inertiajs/vue3';
4
+ import { ref } from 'vue';
5
+
6
+ defineProps({
7
+ webhook: Object,
8
+ deliveries: Object,
9
+ });
10
+
11
+ const expandedId = ref(null);
12
+
13
+ function retry(deliveryId) {
14
+ router.post(route('escalated.admin.webhooks.retry', deliveryId), {}, { preserveScroll: true });
15
+ }
16
+
17
+ function togglePayload(id) {
18
+ expandedId.value = expandedId.value === id ? null : id;
19
+ }
20
+ </script>
21
+
22
+ <template>
23
+ <EscalatedLayout :title="`Webhook Delivery Log - ${webhook.url}`">
24
+ <div class="mb-4">
25
+ <a
26
+ :href="route('escalated.admin.webhooks.index')"
27
+ class="text-sm text-[var(--esc-panel-text-tertiary)] hover:text-[var(--esc-panel-text-secondary)]"
28
+ >
29
+ &larr; Back to Webhooks
30
+ </a>
31
+ </div>
32
+
33
+ <div class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)]">
34
+ <table class="min-w-full divide-y divide-[var(--esc-panel-border)]">
35
+ <thead>
36
+ <tr class="bg-[var(--esc-panel-hover)]">
37
+ <th
38
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
39
+ >
40
+ Event
41
+ </th>
42
+ <th
43
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
44
+ >
45
+ Status
46
+ </th>
47
+ <th
48
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
49
+ >
50
+ Attempts
51
+ </th>
52
+ <th
53
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
54
+ >
55
+ Timestamp
56
+ </th>
57
+ <th
58
+ class="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
59
+ >
60
+ Actions
61
+ </th>
62
+ </tr>
63
+ </thead>
64
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
65
+ <tr v-if="!deliveries?.data?.length">
66
+ <td colspan="5" class="px-4 py-12 text-center text-sm text-[var(--esc-panel-text-muted)]">
67
+ No deliveries yet.
68
+ </td>
69
+ </tr>
70
+ <template v-for="delivery in deliveries?.data" :key="delivery.id">
71
+ <tr class="transition-colors hover:bg-[var(--esc-panel-hover)]">
72
+ <td class="px-4 py-3 text-sm font-mono text-[var(--esc-panel-text-secondary)]">
73
+ {{ delivery.event }}
74
+ </td>
75
+ <td class="px-4 py-3 text-sm">
76
+ <span
77
+ :class="
78
+ delivery.response_code >= 200 && delivery.response_code < 300
79
+ ? 'text-emerald-400'
80
+ : 'text-rose-400'
81
+ "
82
+ >
83
+ {{ delivery.response_code || 'Failed' }}
84
+ </span>
85
+ </td>
86
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
87
+ {{ delivery.attempts }}
88
+ </td>
89
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
90
+ {{ delivery.created_at }}
91
+ </td>
92
+ <td class="px-4 py-3 text-right text-sm">
93
+ <button
94
+ class="text-[var(--esc-panel-text-tertiary)] hover:text-[var(--esc-panel-text-secondary)]"
95
+ @click="togglePayload(delivery.id)"
96
+ >
97
+ {{ expandedId === delivery.id ? 'Hide' : 'Payload' }}
98
+ </button>
99
+ <button
100
+ class="ml-3 text-[var(--esc-panel-accent)] hover:text-cyan-300"
101
+ @click="retry(delivery.id)"
102
+ >
103
+ Retry
104
+ </button>
105
+ </td>
106
+ </tr>
107
+ <tr v-if="expandedId === delivery.id">
108
+ <td colspan="5" class="bg-[var(--esc-panel-hover)] px-4 py-3">
109
+ <pre
110
+ class="max-h-40 overflow-auto rounded-lg bg-[var(--esc-panel-surface-alt)] p-3 text-xs text-[var(--esc-panel-text-tertiary)]"
111
+ >{{ JSON.stringify(delivery.payload, null, 2) }}</pre
112
+ >
113
+ </td>
114
+ </tr>
115
+ </template>
116
+ </tbody>
117
+ </table>
118
+ </div>
119
+ </EscalatedLayout>
120
+ </template>
@@ -0,0 +1,110 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { useForm } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ webhook: { type: Object, default: null },
7
+ availableEvents: { type: Array, default: () => [] },
8
+ });
9
+
10
+ const form = useForm({
11
+ url: props.webhook?.url || '',
12
+ events: props.webhook?.events || [],
13
+ secret: props.webhook?.secret || '',
14
+ active: props.webhook?.active ?? true,
15
+ });
16
+
17
+ function toggleEvent(event) {
18
+ const idx = form.events.indexOf(event);
19
+ if (idx >= 0) {
20
+ form.events.splice(idx, 1);
21
+ } else {
22
+ form.events.push(event);
23
+ }
24
+ }
25
+
26
+ function submit() {
27
+ if (props.webhook) {
28
+ form.put(route('escalated.admin.webhooks.update', props.webhook.id));
29
+ } else {
30
+ form.post(route('escalated.admin.webhooks.store'));
31
+ }
32
+ }
33
+ </script>
34
+
35
+ <template>
36
+ <EscalatedLayout :title="webhook ? 'Edit Webhook' : 'Create Webhook'">
37
+ <form class="max-w-lg space-y-4" @submit.prevent="submit">
38
+ <div>
39
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">URL</label>
40
+ <input
41
+ v-model="form.url"
42
+ type="url"
43
+ required
44
+ placeholder="https://example.com/webhook"
45
+ class="mt-1 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
46
+ />
47
+ <p v-if="form.errors.url" class="mt-1 text-xs text-rose-400">{{ form.errors.url }}</p>
48
+ </div>
49
+
50
+ <div>
51
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
52
+ >Secret (for HMAC signing)</label
53
+ >
54
+ <input
55
+ v-model="form.secret"
56
+ type="text"
57
+ placeholder="Optional secret key"
58
+ class="mt-1 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
59
+ />
60
+ </div>
61
+
62
+ <div>
63
+ <label class="mb-2 block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Events</label>
64
+ <div class="grid grid-cols-2 gap-2">
65
+ <label
66
+ v-for="event in availableEvents"
67
+ :key="event"
68
+ class="flex items-center gap-2 text-sm text-[var(--esc-panel-text-secondary)]"
69
+ >
70
+ <input
71
+ type="checkbox"
72
+ :checked="form.events.includes(event)"
73
+ class="rounded border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] text-cyan-500 focus:ring-cyan-500/20"
74
+ @change="toggleEvent(event)"
75
+ />
76
+ {{ event }}
77
+ </label>
78
+ </div>
79
+ <p v-if="form.errors.events" class="mt-1 text-xs text-rose-400">{{ form.errors.events }}</p>
80
+ </div>
81
+
82
+ <div>
83
+ <label class="flex items-center gap-2 text-sm text-[var(--esc-panel-text-secondary)]">
84
+ <input
85
+ v-model="form.active"
86
+ type="checkbox"
87
+ class="rounded border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] text-cyan-500 focus:ring-cyan-500/20"
88
+ />
89
+ Active
90
+ </label>
91
+ </div>
92
+
93
+ <div class="flex gap-3">
94
+ <button
95
+ type="submit"
96
+ :disabled="form.processing"
97
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-4 py-2 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)] disabled:opacity-50"
98
+ >
99
+ {{ webhook ? 'Update' : 'Create' }}
100
+ </button>
101
+ <a
102
+ :href="route('escalated.admin.webhooks.index')"
103
+ class="rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] px-4 py-2 text-sm font-medium text-[var(--esc-panel-text-secondary)] transition-colors hover:bg-[var(--esc-panel-hover)]"
104
+ >
105
+ Cancel
106
+ </a>
107
+ </div>
108
+ </form>
109
+ </EscalatedLayout>
110
+ </template>
@@ -0,0 +1,136 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { Link, router } from '@inertiajs/vue3';
4
+
5
+ defineProps({ webhooks: Array });
6
+
7
+ function destroy(id) {
8
+ if (confirm('Delete this webhook?')) {
9
+ router.delete(route('escalated.admin.webhooks.destroy', id));
10
+ }
11
+ }
12
+
13
+ function lastDeliveryStatus(webhook) {
14
+ const last = webhook.deliveries?.[0];
15
+ if (!last) return null;
16
+ return last.response_code;
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <EscalatedLayout title="Webhooks">
22
+ <div class="mb-4 flex justify-end">
23
+ <Link
24
+ :href="route('escalated.admin.webhooks.create')"
25
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-4 py-2 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)]"
26
+ >
27
+ Add Webhook
28
+ </Link>
29
+ </div>
30
+ <div class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)]">
31
+ <table class="min-w-full divide-y divide-[var(--esc-panel-border)]">
32
+ <thead>
33
+ <tr class="bg-[var(--esc-panel-hover)]">
34
+ <th
35
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
36
+ >
37
+ URL
38
+ </th>
39
+ <th
40
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
41
+ >
42
+ Events
43
+ </th>
44
+ <th
45
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
46
+ >
47
+ Active
48
+ </th>
49
+ <th
50
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
51
+ >
52
+ Last Status
53
+ </th>
54
+ <th
55
+ class="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
56
+ >
57
+ Actions
58
+ </th>
59
+ </tr>
60
+ </thead>
61
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
62
+ <tr v-if="!webhooks?.length">
63
+ <td colspan="5" class="px-4 py-12 text-center">
64
+ <svg
65
+ class="mx-auto mb-3 h-8 w-8 text-[var(--esc-panel-text-muted)]"
66
+ fill="none"
67
+ stroke="currentColor"
68
+ stroke-width="1.5"
69
+ viewBox="0 0 24 24"
70
+ >
71
+ <path
72
+ stroke-linecap="round"
73
+ stroke-linejoin="round"
74
+ d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-9.86a4.5 4.5 0 00-6.364 0l-4.5 4.5a4.5 4.5 0 001.242 7.244"
75
+ />
76
+ </svg>
77
+ <p class="text-sm text-[var(--esc-panel-text-muted)]">No webhooks configured</p>
78
+ <p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
79
+ Set up webhooks to send event notifications to external services
80
+ </p>
81
+ </td>
82
+ </tr>
83
+ <tr
84
+ v-for="webhook in webhooks"
85
+ :key="webhook.id"
86
+ class="transition-colors hover:bg-[var(--esc-panel-hover)]"
87
+ >
88
+ <td
89
+ class="px-4 py-3 text-sm font-mono text-[var(--esc-panel-text-secondary)] max-w-xs truncate"
90
+ >
91
+ {{ webhook.url }}
92
+ </td>
93
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
94
+ {{ (webhook.events || []).length }} events
95
+ </td>
96
+ <td class="px-4 py-3 text-sm">
97
+ <span :class="webhook.active ? 'text-emerald-400' : 'text-[var(--esc-panel-text-muted)]'">{{
98
+ webhook.active ? 'Yes' : 'No'
99
+ }}</span>
100
+ </td>
101
+ <td class="px-4 py-3 text-sm">
102
+ <span
103
+ v-if="lastDeliveryStatus(webhook)"
104
+ :class="
105
+ lastDeliveryStatus(webhook) >= 200 && lastDeliveryStatus(webhook) < 300
106
+ ? 'text-emerald-400'
107
+ : 'text-rose-400'
108
+ "
109
+ >
110
+ {{ lastDeliveryStatus(webhook) }}
111
+ </span>
112
+ <span v-else class="text-[var(--esc-panel-text-muted)]">&mdash;</span>
113
+ </td>
114
+ <td class="px-4 py-3 text-right text-sm">
115
+ <Link
116
+ :href="route('escalated.admin.webhooks.deliveries', webhook.id)"
117
+ class="text-[var(--esc-panel-text-tertiary)] hover:text-[var(--esc-panel-text-secondary)]"
118
+ >
119
+ Log
120
+ </Link>
121
+ <Link
122
+ :href="route('escalated.admin.webhooks.edit', webhook.id)"
123
+ class="ml-3 text-[var(--esc-panel-text-secondary)] hover:text-[var(--esc-panel-text)]"
124
+ >
125
+ Edit
126
+ </Link>
127
+ <button class="ml-3 text-rose-400 hover:text-rose-300" @click="destroy(webhook.id)">
128
+ Delete
129
+ </button>
130
+ </td>
131
+ </tr>
132
+ </tbody>
133
+ </table>
134
+ </div>
135
+ </EscalatedLayout>
136
+ </template>