@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
|
@@ -9,7 +9,10 @@ const props = defineProps({
|
|
|
9
9
|
api_enabled: Boolean,
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
const dark = inject(
|
|
12
|
+
const dark = inject(
|
|
13
|
+
'esc-dark',
|
|
14
|
+
computed(() => true),
|
|
15
|
+
);
|
|
13
16
|
const page = usePage();
|
|
14
17
|
const prefix = computed(() => {
|
|
15
18
|
const p = page.props.escalated?.prefix || 'support';
|
|
@@ -66,7 +69,13 @@ function toggleAbility(ability) {
|
|
|
66
69
|
|
|
67
70
|
function formatDate(iso) {
|
|
68
71
|
if (!iso) return '—';
|
|
69
|
-
return new Date(iso).toLocaleDateString(undefined, {
|
|
72
|
+
return new Date(iso).toLocaleDateString(undefined, {
|
|
73
|
+
year: 'numeric',
|
|
74
|
+
month: 'short',
|
|
75
|
+
day: 'numeric',
|
|
76
|
+
hour: '2-digit',
|
|
77
|
+
minute: '2-digit',
|
|
78
|
+
});
|
|
70
79
|
}
|
|
71
80
|
</script>
|
|
72
81
|
|
|
@@ -76,59 +85,127 @@ function formatDate(iso) {
|
|
|
76
85
|
<!-- Header -->
|
|
77
86
|
<div class="flex items-center justify-between">
|
|
78
87
|
<div>
|
|
79
|
-
<h2 :class="dark ? 'text-
|
|
80
|
-
|
|
88
|
+
<h2 :class="dark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900'" class="text-lg font-semibold">
|
|
89
|
+
API Tokens
|
|
90
|
+
</h2>
|
|
91
|
+
<p :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'" class="mt-1 text-sm">
|
|
92
|
+
Manage API tokens for desktop apps and external integrations.
|
|
93
|
+
</p>
|
|
81
94
|
</div>
|
|
82
|
-
<button
|
|
83
|
-
:class="
|
|
84
|
-
|
|
95
|
+
<button
|
|
96
|
+
:class="
|
|
97
|
+
dark ? 'bg-white text-black hover:bg-neutral-200' : 'bg-gray-900 text-white hover:bg-gray-800'
|
|
98
|
+
"
|
|
99
|
+
class="rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
|
100
|
+
@click="showCreate = true"
|
|
101
|
+
>
|
|
85
102
|
Create Token
|
|
86
103
|
</button>
|
|
87
104
|
</div>
|
|
88
105
|
|
|
89
106
|
<!-- Not enabled warning -->
|
|
90
|
-
<div
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
<div
|
|
108
|
+
v-if="!api_enabled"
|
|
109
|
+
:class="
|
|
110
|
+
dark
|
|
111
|
+
? 'border-amber-500/30 bg-amber-500/10 text-amber-300'
|
|
112
|
+
: 'border-amber-300 bg-amber-50 text-amber-800'
|
|
113
|
+
"
|
|
114
|
+
class="rounded-lg border p-4 text-sm"
|
|
115
|
+
>
|
|
116
|
+
The REST API is currently disabled. Set <code class="font-mono">ESCALATED_API_ENABLED=true</code> in
|
|
117
|
+
your .env to enable it.
|
|
94
118
|
</div>
|
|
95
119
|
|
|
96
120
|
<!-- Copied token modal -->
|
|
97
|
-
<div
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
121
|
+
<div
|
|
122
|
+
v-if="copiedToken"
|
|
123
|
+
:class="dark ? 'border-emerald-500/30 bg-emerald-500/10' : 'border-emerald-300 bg-emerald-50'"
|
|
124
|
+
class="rounded-lg border p-4"
|
|
125
|
+
>
|
|
126
|
+
<p :class="dark ? 'text-emerald-300' : 'text-emerald-800'" class="text-sm font-medium">
|
|
127
|
+
Token created! Copy it now — it won't be shown again.
|
|
128
|
+
</p>
|
|
101
129
|
<div class="mt-2 flex items-center gap-2">
|
|
102
|
-
<code
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<button
|
|
109
|
-
:class="
|
|
110
|
-
|
|
130
|
+
<code
|
|
131
|
+
:class="dark ? 'bg-black text-emerald-400' : 'bg-white text-emerald-700'"
|
|
132
|
+
class="flex-1 rounded border px-3 py-2 text-xs font-mono break-all"
|
|
133
|
+
:style="dark ? 'border-color: rgba(255,255,255,0.1)' : ''"
|
|
134
|
+
>{{ copiedToken }}</code
|
|
135
|
+
>
|
|
136
|
+
<button
|
|
137
|
+
:class="
|
|
138
|
+
dark
|
|
139
|
+
? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)] hover:bg-[var(--esc-panel-active)]'
|
|
140
|
+
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
141
|
+
"
|
|
142
|
+
class="rounded-lg px-3 py-2 text-sm font-medium transition-colors"
|
|
143
|
+
@click="copyToken"
|
|
144
|
+
>
|
|
145
|
+
Copy
|
|
146
|
+
</button>
|
|
147
|
+
<button
|
|
148
|
+
:class="
|
|
149
|
+
dark
|
|
150
|
+
? 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text)]'
|
|
151
|
+
: 'text-gray-400 hover:text-gray-600'
|
|
152
|
+
"
|
|
153
|
+
class="px-2 py-2 text-sm transition-colors"
|
|
154
|
+
@click="dismissToken"
|
|
155
|
+
>
|
|
156
|
+
Dismiss
|
|
157
|
+
</button>
|
|
111
158
|
</div>
|
|
112
159
|
</div>
|
|
113
160
|
|
|
114
161
|
<!-- Create form -->
|
|
115
|
-
<div
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
162
|
+
<div
|
|
163
|
+
v-if="showCreate"
|
|
164
|
+
:class="
|
|
165
|
+
dark
|
|
166
|
+
? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)]'
|
|
167
|
+
: 'border-gray-200 bg-white'
|
|
168
|
+
"
|
|
169
|
+
class="rounded-lg border p-5 space-y-4"
|
|
170
|
+
>
|
|
171
|
+
<h3 :class="dark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900'" class="text-sm font-semibold">
|
|
172
|
+
New API Token
|
|
173
|
+
</h3>
|
|
119
174
|
|
|
120
175
|
<div class="grid grid-cols-2 gap-4">
|
|
121
176
|
<div>
|
|
122
|
-
<label
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
177
|
+
<label
|
|
178
|
+
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
179
|
+
class="mb-1 block text-xs font-medium"
|
|
180
|
+
>Token Name</label
|
|
181
|
+
>
|
|
182
|
+
<input
|
|
183
|
+
v-model="form.name"
|
|
184
|
+
type="text"
|
|
185
|
+
placeholder="e.g. Desktop App - Work Laptop"
|
|
186
|
+
:class="
|
|
187
|
+
dark
|
|
188
|
+
? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)]'
|
|
189
|
+
: 'border-gray-300 bg-white text-gray-900'
|
|
190
|
+
"
|
|
191
|
+
class="w-full rounded-lg border px-3 py-2 text-sm"
|
|
192
|
+
/>
|
|
126
193
|
</div>
|
|
127
194
|
<div>
|
|
128
|
-
<label
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
195
|
+
<label
|
|
196
|
+
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
197
|
+
class="mb-1 block text-xs font-medium"
|
|
198
|
+
>User</label
|
|
199
|
+
>
|
|
200
|
+
<select
|
|
201
|
+
v-model="form.user_id"
|
|
202
|
+
:class="
|
|
203
|
+
dark
|
|
204
|
+
? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)]'
|
|
205
|
+
: 'border-gray-300 bg-white text-gray-900'
|
|
206
|
+
"
|
|
207
|
+
class="w-full rounded-lg border px-3 py-2 text-sm"
|
|
208
|
+
>
|
|
132
209
|
<option value="">Select user...</option>
|
|
133
210
|
<option v-for="u in users" :key="u.id" :value="u.id">{{ u.name }} ({{ u.email }})</option>
|
|
134
211
|
</select>
|
|
@@ -136,41 +213,90 @@ function formatDate(iso) {
|
|
|
136
213
|
</div>
|
|
137
214
|
|
|
138
215
|
<div>
|
|
139
|
-
<label
|
|
216
|
+
<label
|
|
217
|
+
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
218
|
+
class="mb-1 block text-xs font-medium"
|
|
219
|
+
>Abilities</label
|
|
220
|
+
>
|
|
140
221
|
<div class="flex items-center gap-3">
|
|
141
|
-
<label
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
222
|
+
<label
|
|
223
|
+
v-for="ability in ['agent', 'admin']"
|
|
224
|
+
:key="ability"
|
|
225
|
+
:class="dark ? 'text-[var(--esc-panel-text-secondary)]' : 'text-gray-700'"
|
|
226
|
+
class="flex items-center gap-1.5 text-sm cursor-pointer"
|
|
227
|
+
>
|
|
228
|
+
<input
|
|
229
|
+
type="checkbox"
|
|
230
|
+
:checked="form.abilities.includes(ability)"
|
|
231
|
+
class="rounded border-neutral-600"
|
|
232
|
+
@change="toggleAbility(ability)"
|
|
233
|
+
/>
|
|
146
234
|
{{ ability }}
|
|
147
235
|
</label>
|
|
148
236
|
</div>
|
|
149
237
|
</div>
|
|
150
238
|
|
|
151
239
|
<div>
|
|
152
|
-
<label
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
240
|
+
<label
|
|
241
|
+
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
242
|
+
class="mb-1 block text-xs font-medium"
|
|
243
|
+
>Expires In (days)</label
|
|
244
|
+
>
|
|
245
|
+
<input
|
|
246
|
+
v-model.number="form.expires_in_days"
|
|
247
|
+
type="number"
|
|
248
|
+
min="1"
|
|
249
|
+
max="365"
|
|
250
|
+
placeholder="Leave empty for no expiration"
|
|
251
|
+
:class="
|
|
252
|
+
dark
|
|
253
|
+
? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)]'
|
|
254
|
+
: 'border-gray-300 bg-white text-gray-900'
|
|
255
|
+
"
|
|
256
|
+
class="w-48 rounded-lg border px-3 py-2 text-sm"
|
|
257
|
+
/>
|
|
156
258
|
</div>
|
|
157
259
|
|
|
158
260
|
<div class="flex items-center gap-3 pt-2">
|
|
159
|
-
<button
|
|
160
|
-
:
|
|
161
|
-
class="
|
|
261
|
+
<button
|
|
262
|
+
:disabled="form.processing || !form.name || !form.user_id"
|
|
263
|
+
:class="
|
|
264
|
+
dark
|
|
265
|
+
? 'bg-white text-black hover:bg-neutral-200 disabled:bg-[var(--esc-panel-text-muted)] disabled:text-[var(--esc-panel-text-muted)]'
|
|
266
|
+
: 'bg-gray-900 text-white hover:bg-gray-800 disabled:bg-gray-300 disabled:text-gray-500'
|
|
267
|
+
"
|
|
268
|
+
class="rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
|
269
|
+
@click="createToken"
|
|
270
|
+
>
|
|
162
271
|
{{ form.processing ? 'Creating...' : 'Create Token' }}
|
|
163
272
|
</button>
|
|
164
|
-
<button
|
|
165
|
-
:class="
|
|
166
|
-
|
|
273
|
+
<button
|
|
274
|
+
:class="
|
|
275
|
+
dark
|
|
276
|
+
? 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text)]'
|
|
277
|
+
: 'text-gray-500 hover:text-gray-900'
|
|
278
|
+
"
|
|
279
|
+
class="text-sm transition-colors"
|
|
280
|
+
@click="showCreate = false"
|
|
281
|
+
>
|
|
282
|
+
Cancel
|
|
283
|
+
</button>
|
|
167
284
|
</div>
|
|
168
285
|
</div>
|
|
169
286
|
|
|
170
287
|
<!-- Tokens table -->
|
|
171
|
-
<div
|
|
288
|
+
<div
|
|
289
|
+
:class="dark ? 'border-[var(--esc-panel-border)]' : 'border-gray-200'"
|
|
290
|
+
class="overflow-hidden rounded-lg border"
|
|
291
|
+
>
|
|
172
292
|
<table class="w-full text-sm">
|
|
173
|
-
<thead
|
|
293
|
+
<thead
|
|
294
|
+
:class="
|
|
295
|
+
dark
|
|
296
|
+
? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-muted)]'
|
|
297
|
+
: 'bg-gray-50 text-gray-500'
|
|
298
|
+
"
|
|
299
|
+
>
|
|
174
300
|
<tr>
|
|
175
301
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase">Name</th>
|
|
176
302
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase">User</th>
|
|
@@ -180,32 +306,70 @@ function formatDate(iso) {
|
|
|
180
306
|
<th class="px-4 py-3 text-right text-xs font-medium uppercase">Actions</th>
|
|
181
307
|
</tr>
|
|
182
308
|
</thead>
|
|
183
|
-
<tbody :class="dark ? 'divide-
|
|
184
|
-
<tr
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
309
|
+
<tbody :class="dark ? 'divide-[var(--esc-panel-border)]' : 'divide-gray-100'" class="divide-y">
|
|
310
|
+
<tr
|
|
311
|
+
v-for="token in tokens"
|
|
312
|
+
:key="token.id"
|
|
313
|
+
:class="[
|
|
314
|
+
dark ? 'hover:bg-[var(--esc-panel-hover)]' : 'hover:bg-gray-50',
|
|
315
|
+
token.is_expired ? 'opacity-50' : '',
|
|
316
|
+
]"
|
|
317
|
+
>
|
|
318
|
+
<td
|
|
319
|
+
:class="dark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900'"
|
|
320
|
+
class="px-4 py-3 font-medium"
|
|
321
|
+
>
|
|
322
|
+
{{ token.name }}
|
|
323
|
+
</td>
|
|
324
|
+
<td
|
|
325
|
+
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
326
|
+
class="px-4 py-3"
|
|
327
|
+
>
|
|
328
|
+
{{ token.user_name }}
|
|
329
|
+
</td>
|
|
188
330
|
<td class="px-4 py-3">
|
|
189
|
-
<span
|
|
190
|
-
|
|
191
|
-
|
|
331
|
+
<span
|
|
332
|
+
v-for="a in token.abilities || []"
|
|
333
|
+
:key="a"
|
|
334
|
+
:class="
|
|
335
|
+
dark
|
|
336
|
+
? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-secondary)]'
|
|
337
|
+
: 'bg-gray-100 text-gray-700'
|
|
338
|
+
"
|
|
339
|
+
class="mr-1 rounded px-1.5 py-0.5 text-xs"
|
|
340
|
+
>{{ a }}</span
|
|
341
|
+
>
|
|
192
342
|
</td>
|
|
193
|
-
<td
|
|
343
|
+
<td
|
|
344
|
+
:class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'"
|
|
345
|
+
class="px-4 py-3 text-xs"
|
|
346
|
+
>
|
|
194
347
|
<template v-if="token.last_used_at">{{ formatDate(token.last_used_at) }}</template>
|
|
195
348
|
<template v-else>Never</template>
|
|
196
349
|
</td>
|
|
197
|
-
<td
|
|
350
|
+
<td
|
|
351
|
+
:class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'"
|
|
352
|
+
class="px-4 py-3 text-xs"
|
|
353
|
+
>
|
|
198
354
|
<span v-if="token.is_expired" class="text-rose-400">Expired</span>
|
|
199
355
|
<template v-else-if="token.expires_at">{{ formatDate(token.expires_at) }}</template>
|
|
200
356
|
<template v-else>Never</template>
|
|
201
357
|
</td>
|
|
202
358
|
<td class="px-4 py-3 text-right">
|
|
203
|
-
<button
|
|
204
|
-
class="text-xs text-rose-400 hover:text-rose-300 transition-colors"
|
|
359
|
+
<button
|
|
360
|
+
class="text-xs text-rose-400 hover:text-rose-300 transition-colors"
|
|
361
|
+
@click="revokeToken(token.id)"
|
|
362
|
+
>
|
|
363
|
+
Revoke
|
|
364
|
+
</button>
|
|
205
365
|
</td>
|
|
206
366
|
</tr>
|
|
207
367
|
<tr v-if="!tokens.length">
|
|
208
|
-
<td
|
|
368
|
+
<td
|
|
369
|
+
colspan="6"
|
|
370
|
+
:class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-400'"
|
|
371
|
+
class="px-4 py-8 text-center text-sm"
|
|
372
|
+
>
|
|
209
373
|
No API tokens created yet.
|
|
210
374
|
</td>
|
|
211
375
|
</tr>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
3
|
+
import AuditLogEntry from '../../../components/AuditLogEntry.vue';
|
|
4
|
+
import { router } from '@inertiajs/vue3';
|
|
5
|
+
import { ref, watch } from 'vue';
|
|
6
|
+
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
logs: Object,
|
|
9
|
+
filters: Object,
|
|
10
|
+
users: Array,
|
|
11
|
+
actions: Array,
|
|
12
|
+
resourceTypes: Array,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const filterForm = ref({
|
|
16
|
+
user_id: props.filters?.user_id || '',
|
|
17
|
+
action: props.filters?.action || '',
|
|
18
|
+
auditable_type: props.filters?.auditable_type || '',
|
|
19
|
+
date_from: props.filters?.date_from || '',
|
|
20
|
+
date_to: props.filters?.date_to || '',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function applyFilters() {
|
|
24
|
+
const params = {};
|
|
25
|
+
for (const [key, value] of Object.entries(filterForm.value)) {
|
|
26
|
+
if (value) params[key] = value;
|
|
27
|
+
}
|
|
28
|
+
router.get(route('escalated.admin.audit-log'), params, { preserveState: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clearFilters() {
|
|
32
|
+
filterForm.value = { user_id: '', action: '', auditable_type: '', date_from: '', date_to: '' };
|
|
33
|
+
router.get(route('escalated.admin.audit-log'), {}, { preserveState: true });
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<EscalatedLayout title="Audit Log">
|
|
39
|
+
<!-- Filters -->
|
|
40
|
+
<div
|
|
41
|
+
class="mb-6 flex flex-wrap items-end gap-3 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-4"
|
|
42
|
+
>
|
|
43
|
+
<div>
|
|
44
|
+
<label class="block text-xs font-medium text-[var(--esc-panel-text-tertiary)]">User</label>
|
|
45
|
+
<select
|
|
46
|
+
v-model="filterForm.user_id"
|
|
47
|
+
class="mt-1 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)]"
|
|
48
|
+
>
|
|
49
|
+
<option value="">All Users</option>
|
|
50
|
+
<option v-for="user in users" :key="user.id" :value="user.id">{{ user.name }}</option>
|
|
51
|
+
</select>
|
|
52
|
+
</div>
|
|
53
|
+
<div>
|
|
54
|
+
<label class="block text-xs font-medium text-[var(--esc-panel-text-tertiary)]">Action</label>
|
|
55
|
+
<select
|
|
56
|
+
v-model="filterForm.action"
|
|
57
|
+
class="mt-1 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)]"
|
|
58
|
+
>
|
|
59
|
+
<option value="">All Actions</option>
|
|
60
|
+
<option v-for="a in actions" :key="a" :value="a">{{ a }}</option>
|
|
61
|
+
</select>
|
|
62
|
+
</div>
|
|
63
|
+
<div>
|
|
64
|
+
<label class="block text-xs font-medium text-[var(--esc-panel-text-tertiary)]">Resource Type</label>
|
|
65
|
+
<select
|
|
66
|
+
v-model="filterForm.auditable_type"
|
|
67
|
+
class="mt-1 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)]"
|
|
68
|
+
>
|
|
69
|
+
<option value="">All Types</option>
|
|
70
|
+
<option v-for="t in resourceTypes" :key="t" :value="t">{{ t.split('\\').pop() }}</option>
|
|
71
|
+
</select>
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<label class="block text-xs font-medium text-[var(--esc-panel-text-tertiary)]">From</label>
|
|
75
|
+
<input
|
|
76
|
+
v-model="filterForm.date_from"
|
|
77
|
+
type="date"
|
|
78
|
+
class="mt-1 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)]"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<label class="block text-xs font-medium text-[var(--esc-panel-text-tertiary)]">To</label>
|
|
83
|
+
<input
|
|
84
|
+
v-model="filterForm.date_to"
|
|
85
|
+
type="date"
|
|
86
|
+
class="mt-1 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)]"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
<button
|
|
90
|
+
class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-4 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)]"
|
|
91
|
+
@click="applyFilters"
|
|
92
|
+
>
|
|
93
|
+
Filter
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
class="rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] px-4 py-1.5 text-sm font-medium text-[var(--esc-panel-text-secondary)] transition-colors hover:bg-[var(--esc-panel-hover)]"
|
|
97
|
+
@click="clearFilters"
|
|
98
|
+
>
|
|
99
|
+
Clear
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Log entries -->
|
|
104
|
+
<div class="space-y-2">
|
|
105
|
+
<template v-if="logs?.data?.length">
|
|
106
|
+
<AuditLogEntry v-for="log in logs.data" :key="log.id" :log="log" />
|
|
107
|
+
</template>
|
|
108
|
+
<div
|
|
109
|
+
v-else
|
|
110
|
+
class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] px-4 py-12 text-center"
|
|
111
|
+
>
|
|
112
|
+
<svg
|
|
113
|
+
class="mx-auto mb-3 h-8 w-8 text-[var(--esc-panel-text-muted)]"
|
|
114
|
+
fill="none"
|
|
115
|
+
stroke="currentColor"
|
|
116
|
+
stroke-width="1.5"
|
|
117
|
+
viewBox="0 0 24 24"
|
|
118
|
+
>
|
|
119
|
+
<path
|
|
120
|
+
stroke-linecap="round"
|
|
121
|
+
stroke-linejoin="round"
|
|
122
|
+
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
|
123
|
+
/>
|
|
124
|
+
</svg>
|
|
125
|
+
<p class="text-sm text-[var(--esc-panel-text-muted)]">No audit log entries found</p>
|
|
126
|
+
<p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
|
|
127
|
+
Activity will appear here as changes are made
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<!-- Pagination -->
|
|
133
|
+
<div v-if="logs?.links?.length > 3" class="mt-6 flex justify-center gap-1">
|
|
134
|
+
<template v-for="link in logs.links" :key="link.label">
|
|
135
|
+
<button
|
|
136
|
+
v-if="link.url"
|
|
137
|
+
:class="[
|
|
138
|
+
'rounded-lg px-3 py-1.5 text-sm transition-colors',
|
|
139
|
+
link.active
|
|
140
|
+
? 'bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] text-white'
|
|
141
|
+
: 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-secondary)] hover:bg-[var(--esc-panel-hover)]',
|
|
142
|
+
]"
|
|
143
|
+
@click="router.get(link.url, {}, { preserveState: true })"
|
|
144
|
+
v-html="link.label"
|
|
145
|
+
></button>
|
|
146
|
+
<span
|
|
147
|
+
v-else
|
|
148
|
+
class="rounded-lg px-3 py-1.5 text-sm text-[var(--esc-panel-text-muted)]"
|
|
149
|
+
v-html="link.label"
|
|
150
|
+
></span>
|
|
151
|
+
</template>
|
|
152
|
+
</div>
|
|
153
|
+
</EscalatedLayout>
|
|
154
|
+
</template>
|