@escalated-dev/escalated 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +94 -2
  2. package/package.json +31 -2
  3. package/src/components/ActivityTimeline.stories.js +55 -0
  4. package/src/components/ActivityTimeline.vue +24 -7
  5. package/src/components/AdminDashboard.stories.js +1095 -0
  6. package/src/components/AgentLoadIndicator.stories.js +37 -0
  7. package/src/components/AgentLoadIndicator.vue +49 -0
  8. package/src/components/AgentPerformanceCard.vue +44 -0
  9. package/src/components/ArticleEditor.vue +82 -0
  10. package/src/components/AssigneeSelect.vue +19 -5
  11. package/src/components/AttachmentList.vue +53 -37
  12. package/src/components/AuditLogEntry.vue +95 -0
  13. package/src/components/BulkActionBar.vue +99 -30
  14. package/src/components/CategoryTree.vue +89 -0
  15. package/src/components/ChartWidget.vue +46 -0
  16. package/src/components/CollisionWarning.stories.js +30 -0
  17. package/src/components/CollisionWarning.vue +68 -0
  18. package/src/components/ConditionalFieldRules.vue +135 -0
  19. package/src/components/ContextPanel.vue +66 -0
  20. package/src/components/ContextPanelSection.vue +65 -0
  21. package/src/components/CustomFieldRenderer.vue +159 -0
  22. package/src/components/EscalatedLayout.vue +400 -69
  23. package/src/components/FileDropzone.vue +26 -10
  24. package/src/components/FollowButton.stories.js +30 -0
  25. package/src/components/FollowButton.vue +53 -28
  26. package/src/components/KeyboardShortcutHelp.vue +129 -39
  27. package/src/components/KnowledgePanel.vue +122 -0
  28. package/src/components/KpiCard.stories.js +32 -0
  29. package/src/components/KpiCard.vue +88 -0
  30. package/src/components/MacroDropdown.vue +95 -35
  31. package/src/components/PermissionMatrix.vue +107 -0
  32. package/src/components/PinnedNotes.vue +70 -31
  33. package/src/components/PresenceIndicator.vue +93 -27
  34. package/src/components/PriorityBadge.stories.js +33 -0
  35. package/src/components/PriorityBadge.vue +9 -2
  36. package/src/components/QuickFilters.vue +65 -49
  37. package/src/components/ReplyComposer.vue +144 -30
  38. package/src/components/ReplyThread.vue +72 -20
  39. package/src/components/SatisfactionRating.vue +98 -46
  40. package/src/components/ScheduleEditor.vue +104 -0
  41. package/src/components/SideConversation.vue +165 -0
  42. package/src/components/SideConversationList.vue +147 -0
  43. package/src/components/SkillTagManager.vue +79 -0
  44. package/src/components/SlaComplianceChart.vue +53 -0
  45. package/src/components/SlaTimer.stories.js +35 -0
  46. package/src/components/SlaTimer.vue +17 -4
  47. package/src/components/StatsCard.stories.js +24 -0
  48. package/src/components/StatsCard.vue +58 -46
  49. package/src/components/StatusBadge.stories.js +53 -0
  50. package/src/components/StatusBadge.vue +36 -4
  51. package/src/components/TagSelect.vue +43 -12
  52. package/src/components/TicketFilters.vue +225 -69
  53. package/src/components/TicketLinkPanel.vue +193 -0
  54. package/src/components/TicketList.vue +594 -193
  55. package/src/components/TicketMergeDialog.vue +157 -0
  56. package/src/components/TicketSidebar.vue +167 -99
  57. package/src/components/TicketTypeSelector.vue +21 -0
  58. package/src/components/TotpInput.stories.js +9 -0
  59. package/src/components/TotpInput.vue +81 -0
  60. package/src/components/TwoFactorSetup.vue +113 -0
  61. package/src/composables/useDebouncedSearch.js +38 -0
  62. package/src/composables/usePluginExtensions.js +95 -3
  63. package/src/composables/usePluginHooks.js +65 -0
  64. package/src/defineEscalatedPlugin.js +34 -8
  65. package/src/index.js +72 -35
  66. package/src/locales/en.json +25 -0
  67. package/src/pages/Admin/ApiTokens/Index.vue +231 -67
  68. package/src/pages/Admin/AuditLog/Index.vue +154 -0
  69. package/src/pages/Admin/Automations/Form.vue +184 -0
  70. package/src/pages/Admin/Automations/Index.vue +129 -0
  71. package/src/pages/Admin/BusinessHours/Form.vue +172 -0
  72. package/src/pages/Admin/BusinessHours/Index.vue +113 -0
  73. package/src/pages/Admin/CannedResponses/Index.vue +127 -70
  74. package/src/pages/Admin/Capacity/Index.vue +134 -0
  75. package/src/pages/Admin/CustomFields/Form.vue +195 -0
  76. package/src/pages/Admin/CustomFields/Index.vue +134 -0
  77. package/src/pages/Admin/CustomObjects/Form.vue +202 -0
  78. package/src/pages/Admin/CustomObjects/Index.vue +127 -0
  79. package/src/pages/Admin/CustomObjects/Records.vue +228 -0
  80. package/src/pages/Admin/Departments/Form.vue +75 -51
  81. package/src/pages/Admin/Departments/Index.vue +109 -57
  82. package/src/pages/Admin/EscalationRules/Form.vue +157 -96
  83. package/src/pages/Admin/EscalationRules/Index.vue +178 -57
  84. package/src/pages/Admin/Import/Connect.vue +96 -0
  85. package/src/pages/Admin/Import/Index.vue +102 -0
  86. package/src/pages/Admin/Import/Mapping.vue +110 -0
  87. package/src/pages/Admin/Import/Progress.vue +153 -0
  88. package/src/pages/Admin/Import/Review.vue +76 -0
  89. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
  90. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
  91. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
  92. package/src/pages/Admin/Macros/Index.vue +250 -89
  93. package/src/pages/Admin/Plugins/Index.vue +205 -75
  94. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
  95. package/src/pages/Admin/Reports/CsatReport.vue +127 -0
  96. package/src/pages/Admin/Reports/Dashboard.vue +211 -0
  97. package/src/pages/Admin/Reports/SlaReport.vue +166 -0
  98. package/src/pages/Admin/Reports.vue +139 -34
  99. package/src/pages/Admin/Roles/Form.vue +71 -0
  100. package/src/pages/Admin/Roles/Index.vue +122 -0
  101. package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
  102. package/src/pages/Admin/Settings/DataRetention.vue +182 -0
  103. package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
  104. package/src/pages/Admin/Settings/Sandbox.vue +135 -0
  105. package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
  106. package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
  107. package/src/pages/Admin/Settings.vue +468 -260
  108. package/src/pages/Admin/Skills/Form.vue +53 -0
  109. package/src/pages/Admin/Skills/Index.vue +94 -0
  110. package/src/pages/Admin/SlaPolicies/Form.vue +118 -75
  111. package/src/pages/Admin/SlaPolicies/Index.vue +120 -59
  112. package/src/pages/Admin/Statuses/Form.vue +114 -0
  113. package/src/pages/Admin/Statuses/Index.vue +153 -0
  114. package/src/pages/Admin/Tags/Index.vue +191 -105
  115. package/src/pages/Admin/Tickets/Index.vue +91 -39
  116. package/src/pages/Admin/Tickets/Show.vue +101 -37
  117. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
  118. package/src/pages/Admin/Webhooks/Form.vue +110 -0
  119. package/src/pages/Admin/Webhooks/Index.vue +136 -0
  120. package/src/pages/Agent/Dashboard.vue +311 -186
  121. package/src/pages/Agent/TicketIndex.vue +88 -38
  122. package/src/pages/Agent/TicketShow.vue +258 -51
  123. package/src/pages/Customer/Create.vue +36 -9
  124. package/src/pages/Customer/Index.vue +31 -24
  125. package/src/pages/Customer/KnowledgeBase/Article.vue +127 -0
  126. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
  127. package/src/plugin.js +86 -1
  128. package/src/utils/constants.js +30 -0
  129. package/src/utils/formatting.js +35 -0
  130. package/src/pages/Admin/Plugins.vue +0 -129
@@ -1,39 +1,91 @@
1
- <script setup>
2
- import { ref } from 'vue';
3
- import { usePage, router } from '@inertiajs/vue3';
4
- import EscalatedLayout from '../../../components/EscalatedLayout.vue';
5
- import TicketList from '../../../components/TicketList.vue';
6
- import TicketFilters from '../../../components/TicketFilters.vue';
7
- import QuickFilters from '../../../components/QuickFilters.vue';
8
- import BulkActionBar from '../../../components/BulkActionBar.vue';
9
-
10
- defineProps({
11
- tickets: Object,
12
- filters: Object,
13
- departments: Array,
14
- tags: Array,
15
- agents: Array,
16
- });
17
-
18
- const page = usePage();
19
- const selectedIds = ref([]);
20
-
21
- function applyQuickFilter(filter) {
22
- router.get(route('escalated.admin.tickets.index'), filter, { preserveState: true });
23
- }
24
- </script>
25
-
26
- <template>
27
- <EscalatedLayout title="All Tickets">
28
- <div class="mb-4">
29
- <QuickFilters :current-user-id="page.props.auth?.user?.id" @filter="applyQuickFilter" />
30
- </div>
31
- <div class="mb-6">
32
- <TicketFilters :filters="filters" :route="route('escalated.admin.tickets.index')" :departments="departments" :tags="tags" :agents="agents" show-assignee show-following />
33
- </div>
34
- <TicketList :tickets="tickets" route-prefix="escalated.admin.tickets" show-assignee
35
- selectable v-model:selected-ids="selectedIds" />
36
- <BulkActionBar :selected-ids="selectedIds" :bulk-route="route('escalated.admin.tickets.bulk')"
37
- :agents="agents" :departments="departments" @clear="selectedIds = []" />
38
- </EscalatedLayout>
39
- </template>
1
+ <script setup>
2
+ import { ref } from 'vue';
3
+ import { usePage, router } from '@inertiajs/vue3';
4
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
5
+ import TicketList from '../../../components/TicketList.vue';
6
+ import TicketFilters from '../../../components/TicketFilters.vue';
7
+ import QuickFilters from '../../../components/QuickFilters.vue';
8
+ import BulkActionBar from '../../../components/BulkActionBar.vue';
9
+ import KeyboardShortcutHelp from '../../../components/KeyboardShortcutHelp.vue';
10
+ import PluginSlot from '../../../components/PluginSlot.vue';
11
+ import { useKeyboardShortcuts } from '../../../composables/useKeyboardShortcuts';
12
+ import { usePluginExtensions } from '../../../composables/usePluginExtensions';
13
+
14
+ defineProps({
15
+ tickets: Object,
16
+ filters: Object,
17
+ departments: Array,
18
+ tags: Array,
19
+ agents: Array,
20
+ });
21
+
22
+ const page = usePage();
23
+ const selectedIds = ref([]);
24
+ const ticketListRef = ref(null);
25
+ const showShortcutHelp = ref(false);
26
+
27
+ useKeyboardShortcuts({
28
+ j: () => {
29
+ ticketListRef.value?.moveDown();
30
+ },
31
+ k: () => {
32
+ ticketListRef.value?.moveUp();
33
+ },
34
+ x: () => {
35
+ ticketListRef.value?.toggleSelectFocused();
36
+ },
37
+ Enter: () => {
38
+ ticketListRef.value?.openFocused();
39
+ },
40
+ '?': () => {
41
+ showShortcutHelp.value = true;
42
+ },
43
+ });
44
+
45
+ const { getPageComponents } = usePluginExtensions();
46
+
47
+ function applyQuickFilter(filter) {
48
+ router.get(route('escalated.admin.tickets.index'), filter, { preserveState: true });
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <EscalatedLayout title="All Tickets">
54
+ <div class="mb-4">
55
+ <QuickFilters :current-user-id="page.props.auth?.user?.id" @filter="applyQuickFilter" />
56
+ </div>
57
+ <PluginSlot
58
+ slot="ticket.list.before_filters"
59
+ :components="getPageComponents('ticket.list', 'before_filters')"
60
+ />
61
+ <div class="mb-6">
62
+ <TicketFilters
63
+ :filters="filters"
64
+ :route="route('escalated.admin.tickets.index')"
65
+ :departments="departments"
66
+ :tags="tags"
67
+ :agents="agents"
68
+ show-assignee
69
+ show-following
70
+ />
71
+ </div>
72
+ <TicketList
73
+ ref="ticketListRef"
74
+ v-model:selected-ids="selectedIds"
75
+ :tickets="tickets"
76
+ route-prefix="escalated.admin.tickets"
77
+ :columns="['reference', 'subject', 'requester', 'status', 'priority', 'assignee', 'updated']"
78
+ selectable
79
+ navigable
80
+ />
81
+ <BulkActionBar
82
+ :selected-ids="selectedIds"
83
+ :bulk-route="route('escalated.admin.tickets.bulk')"
84
+ :agents="agents"
85
+ :departments="departments"
86
+ @clear="selectedIds = []"
87
+ />
88
+ <PluginSlot slot="ticket.list.footer" :components="getPageComponents('ticket.list', 'footer')" />
89
+ <KeyboardShortcutHelp v-model:show="showShortcutHelp" context="list" />
90
+ </EscalatedLayout>
91
+ </template>
@@ -62,12 +62,25 @@ const { getPageComponents } = usePluginExtensions();
62
62
 
63
63
  // Keyboard shortcuts
64
64
  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; },
65
+ r: () => {
66
+ activeTab.value = 'reply';
67
+ replyComposerRef.value?.$el?.querySelector('textarea')?.focus();
68
+ },
69
+ n: () => {
70
+ activeTab.value = 'note';
71
+ },
72
+ s: () => {
73
+ statusSelectRef.value?.focus();
74
+ },
75
+ p: () => {
76
+ prioritySelectRef.value?.focus();
77
+ },
78
+ f: () => {
79
+ toggleFollow();
80
+ },
81
+ '?': () => {
82
+ showShortcutHelp.value = true;
83
+ },
71
84
  });
72
85
  </script>
73
86
 
@@ -77,30 +90,51 @@ useKeyboardShortcuts({
77
90
  <PluginSlot slot="ticket.show.header" :components="getPageComponents('ticket.show', 'header')" />
78
91
 
79
92
  <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>
93
+ <span class="text-sm font-mono font-medium text-[var(--esc-panel-text)]">{{ ticket.reference }}</span>
81
94
  <StatusBadge :status="ticket.status" />
82
95
  <PriorityBadge :priority="ticket.priority" />
83
- <span class="text-sm text-neutral-500">by {{ ticket.requester?.name }}</span>
96
+ <span class="text-sm text-[var(--esc-panel-text-muted)]">by {{ ticket.requester?.name }}</span>
84
97
  <PresenceIndicator :ticket-reference="ticket.reference" route-prefix="escalated.admin" />
85
98
  <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">
99
+ <FollowButton
100
+ :is-following="is_following"
101
+ :followers-count="followers_count"
102
+ :action="route('escalated.admin.tickets.follow', ticket.reference)"
103
+ />
104
+ <MacroDropdown
105
+ v-if="macros.length"
106
+ :macros="macros"
107
+ :action="route('escalated.admin.tickets.macro', ticket.reference)"
108
+ />
109
+ <button
110
+ v-if="!ticket.assigned_to"
111
+ 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)]"
112
+ @click="assignToMe"
113
+ >
92
114
  Assign to Me
93
115
  </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">
116
+ <select
117
+ ref="statusSelectRef"
118
+ 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)]"
119
+ @change="
120
+ changeStatus($event.target.value);
121
+ $event.target.value = '';
122
+ "
123
+ >
96
124
  <option value="">Change Status...</option>
97
125
  <option value="in_progress">In Progress</option>
98
126
  <option value="waiting_on_customer">Waiting on Customer</option>
99
127
  <option value="resolved">Resolved</option>
100
128
  <option value="closed">Closed</option>
101
129
  </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">
130
+ <select
131
+ ref="prioritySelectRef"
132
+ 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)]"
133
+ @change="
134
+ changePriority($event.target.value);
135
+ $event.target.value = '';
136
+ "
137
+ >
104
138
  <option value="">Change Priority...</option>
105
139
  <option value="low">Low</option>
106
140
  <option value="medium">Medium</option>
@@ -114,35 +148,65 @@ useKeyboardShortcuts({
114
148
  </div>
115
149
  <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
116
150
  <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>
151
+ <PinnedNotes
152
+ v-if="ticket.pinned_notes?.length"
153
+ :notes="ticket.pinned_notes"
154
+ :ticket-reference="ticket.reference"
155
+ route-prefix="escalated.admin"
156
+ />
157
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5">
158
+ <p class="whitespace-pre-wrap text-sm text-[var(--esc-panel-text-secondary)]">
159
+ {{ ticket.description }}
160
+ </p>
121
161
  <AttachmentList v-if="ticket.attachments?.length" :attachments="ticket.attachments" class="mt-3" />
122
162
  </div>
123
163
  <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']">
164
+ <div class="mb-4 flex gap-4 border-b border-[var(--esc-panel-border)]">
165
+ <button
166
+ :class="[
167
+ 'pb-2 text-sm font-medium transition-colors',
168
+ activeTab === 'reply'
169
+ ? 'border-b-2 border-[var(--esc-panel-accent)] text-[var(--esc-panel-text)]'
170
+ : 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]',
171
+ ]"
172
+ @click="activeTab = 'reply'"
173
+ >
127
174
  Reply
128
175
  </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']">
176
+ <button
177
+ :class="[
178
+ 'pb-2 text-sm font-medium transition-colors',
179
+ activeTab === 'note'
180
+ ? 'border-b-2 border-amber-500 text-amber-400'
181
+ : 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]',
182
+ ]"
183
+ @click="activeTab = 'note'"
184
+ >
131
185
  Internal Note
132
186
  </button>
133
187
  </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" />
188
+ <ReplyComposer
189
+ v-if="activeTab === 'reply'"
190
+ ref="replyComposerRef"
191
+ :action="route('escalated.admin.tickets.reply', ticket.reference)"
192
+ :canned-responses="cannedResponses"
193
+ />
194
+ <ReplyComposer
195
+ v-else
196
+ :action="route('escalated.admin.tickets.note', ticket.reference)"
197
+ placeholder="Write an internal note..."
198
+ submit-label="Add Note"
199
+ />
141
200
  </div>
142
201
  <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 />
202
+ <h2 class="mb-4 text-lg font-semibold text-[var(--esc-panel-text-secondary)]">Conversation</h2>
203
+ <ReplyThread
204
+ :replies="ticket.replies || []"
205
+ :current-user-id="page.props.auth?.user?.id"
206
+ :ticket-reference="ticket.reference"
207
+ route-prefix="escalated.admin"
208
+ pinnable
209
+ />
146
210
  </div>
147
211
  </div>
148
212
  <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>