@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.
- package/README.md +94 -2
- package/package.json +31 -2
- package/src/components/ActivityTimeline.stories.js +55 -0
- package/src/components/ActivityTimeline.vue +24 -7
- package/src/components/AdminDashboard.stories.js +1095 -0
- package/src/components/AgentLoadIndicator.stories.js +37 -0
- package/src/components/AgentLoadIndicator.vue +49 -0
- package/src/components/AgentPerformanceCard.vue +44 -0
- package/src/components/ArticleEditor.vue +82 -0
- package/src/components/AssigneeSelect.vue +19 -5
- package/src/components/AttachmentList.vue +53 -37
- package/src/components/AuditLogEntry.vue +95 -0
- package/src/components/BulkActionBar.vue +99 -30
- package/src/components/CategoryTree.vue +89 -0
- package/src/components/ChartWidget.vue +46 -0
- package/src/components/CollisionWarning.stories.js +30 -0
- package/src/components/CollisionWarning.vue +68 -0
- package/src/components/ConditionalFieldRules.vue +135 -0
- package/src/components/ContextPanel.vue +66 -0
- package/src/components/ContextPanelSection.vue +65 -0
- package/src/components/CustomFieldRenderer.vue +159 -0
- package/src/components/EscalatedLayout.vue +400 -69
- package/src/components/FileDropzone.vue +26 -10
- package/src/components/FollowButton.stories.js +30 -0
- package/src/components/FollowButton.vue +53 -28
- package/src/components/KeyboardShortcutHelp.vue +129 -39
- package/src/components/KnowledgePanel.vue +122 -0
- package/src/components/KpiCard.stories.js +32 -0
- package/src/components/KpiCard.vue +88 -0
- package/src/components/MacroDropdown.vue +95 -35
- package/src/components/PermissionMatrix.vue +107 -0
- package/src/components/PinnedNotes.vue +70 -31
- package/src/components/PresenceIndicator.vue +93 -27
- package/src/components/PriorityBadge.stories.js +33 -0
- package/src/components/PriorityBadge.vue +9 -2
- package/src/components/QuickFilters.vue +65 -49
- package/src/components/ReplyComposer.vue +144 -30
- package/src/components/ReplyThread.vue +72 -20
- package/src/components/SatisfactionRating.vue +98 -46
- package/src/components/ScheduleEditor.vue +104 -0
- package/src/components/SideConversation.vue +165 -0
- package/src/components/SideConversationList.vue +147 -0
- package/src/components/SkillTagManager.vue +79 -0
- package/src/components/SlaComplianceChart.vue +53 -0
- package/src/components/SlaTimer.stories.js +35 -0
- package/src/components/SlaTimer.vue +17 -4
- package/src/components/StatsCard.stories.js +24 -0
- package/src/components/StatsCard.vue +58 -46
- package/src/components/StatusBadge.stories.js +53 -0
- package/src/components/StatusBadge.vue +36 -4
- package/src/components/TagSelect.vue +43 -12
- package/src/components/TicketFilters.vue +225 -69
- package/src/components/TicketLinkPanel.vue +193 -0
- package/src/components/TicketList.vue +594 -193
- package/src/components/TicketMergeDialog.vue +157 -0
- package/src/components/TicketSidebar.vue +167 -99
- package/src/components/TicketTypeSelector.vue +21 -0
- package/src/components/TotpInput.stories.js +9 -0
- package/src/components/TotpInput.vue +81 -0
- package/src/components/TwoFactorSetup.vue +113 -0
- package/src/composables/useDebouncedSearch.js +38 -0
- package/src/composables/usePluginExtensions.js +95 -3
- package/src/composables/usePluginHooks.js +65 -0
- package/src/defineEscalatedPlugin.js +34 -8
- package/src/index.js +72 -35
- package/src/locales/en.json +25 -0
- package/src/pages/Admin/ApiTokens/Index.vue +231 -67
- package/src/pages/Admin/AuditLog/Index.vue +154 -0
- package/src/pages/Admin/Automations/Form.vue +184 -0
- package/src/pages/Admin/Automations/Index.vue +129 -0
- package/src/pages/Admin/BusinessHours/Form.vue +172 -0
- package/src/pages/Admin/BusinessHours/Index.vue +113 -0
- package/src/pages/Admin/CannedResponses/Index.vue +127 -70
- package/src/pages/Admin/Capacity/Index.vue +134 -0
- package/src/pages/Admin/CustomFields/Form.vue +195 -0
- package/src/pages/Admin/CustomFields/Index.vue +134 -0
- package/src/pages/Admin/CustomObjects/Form.vue +202 -0
- package/src/pages/Admin/CustomObjects/Index.vue +127 -0
- package/src/pages/Admin/CustomObjects/Records.vue +228 -0
- package/src/pages/Admin/Departments/Form.vue +75 -51
- package/src/pages/Admin/Departments/Index.vue +109 -57
- package/src/pages/Admin/EscalationRules/Form.vue +157 -96
- package/src/pages/Admin/EscalationRules/Index.vue +178 -57
- package/src/pages/Admin/Import/Connect.vue +96 -0
- package/src/pages/Admin/Import/Index.vue +102 -0
- package/src/pages/Admin/Import/Mapping.vue +110 -0
- package/src/pages/Admin/Import/Progress.vue +153 -0
- package/src/pages/Admin/Import/Review.vue +76 -0
- package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
- package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
- package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
- package/src/pages/Admin/Macros/Index.vue +250 -89
- package/src/pages/Admin/Plugins/Index.vue +205 -75
- package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
- package/src/pages/Admin/Reports/CsatReport.vue +127 -0
- package/src/pages/Admin/Reports/Dashboard.vue +211 -0
- package/src/pages/Admin/Reports/SlaReport.vue +166 -0
- package/src/pages/Admin/Reports.vue +139 -34
- package/src/pages/Admin/Roles/Form.vue +71 -0
- package/src/pages/Admin/Roles/Index.vue +122 -0
- package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
- package/src/pages/Admin/Settings/DataRetention.vue +182 -0
- package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
- package/src/pages/Admin/Settings/Sandbox.vue +135 -0
- package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
- package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
- package/src/pages/Admin/Settings.vue +468 -260
- package/src/pages/Admin/Skills/Form.vue +53 -0
- package/src/pages/Admin/Skills/Index.vue +94 -0
- package/src/pages/Admin/SlaPolicies/Form.vue +118 -75
- package/src/pages/Admin/SlaPolicies/Index.vue +120 -59
- package/src/pages/Admin/Statuses/Form.vue +114 -0
- package/src/pages/Admin/Statuses/Index.vue +153 -0
- package/src/pages/Admin/Tags/Index.vue +191 -105
- package/src/pages/Admin/Tickets/Index.vue +91 -39
- package/src/pages/Admin/Tickets/Show.vue +101 -37
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
- package/src/pages/Admin/Webhooks/Form.vue +110 -0
- package/src/pages/Admin/Webhooks/Index.vue +136 -0
- package/src/pages/Agent/Dashboard.vue +311 -186
- package/src/pages/Agent/TicketIndex.vue +88 -38
- package/src/pages/Agent/TicketShow.vue +258 -51
- package/src/pages/Customer/Create.vue +36 -9
- package/src/pages/Customer/Index.vue +31 -24
- package/src/pages/Customer/KnowledgeBase/Article.vue +127 -0
- package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
- package/src/plugin.js +86 -1
- package/src/utils/constants.js +30 -0
- package/src/utils/formatting.js +35 -0
- 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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-
|
|
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-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
95
|
-
|
|
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
|
|
103
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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-
|
|
125
|
-
<button
|
|
126
|
-
|
|
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
|
|
130
|
-
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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-
|
|
144
|
-
<ReplyThread
|
|
145
|
-
|
|
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
|
+
← 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>
|