@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
@@ -9,7 +9,10 @@ const props = defineProps({
9
9
  api_enabled: Boolean,
10
10
  });
11
11
 
12
- const dark = inject('esc-dark', computed(() => true));
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, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
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-white' : 'text-gray-900'" class="text-lg font-semibold">API Tokens</h2>
80
- <p :class="dark ? 'text-neutral-500' : 'text-gray-500'" class="mt-1 text-sm">Manage API tokens for desktop apps and external integrations.</p>
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 @click="showCreate = true"
83
- :class="dark ? 'bg-white text-black hover:bg-neutral-200' : 'bg-gray-900 text-white hover:bg-gray-800'"
84
- class="rounded-lg px-4 py-2 text-sm font-medium transition-colors">
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 v-if="!api_enabled"
91
- :class="dark ? 'border-amber-500/30 bg-amber-500/10 text-amber-300' : 'border-amber-300 bg-amber-50 text-amber-800'"
92
- class="rounded-lg border p-4 text-sm">
93
- The REST API is currently disabled. Set <code class="font-mono">ESCALATED_API_ENABLED=true</code> in your .env to enable it.
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 v-if="copiedToken"
98
- :class="dark ? 'border-emerald-500/30 bg-emerald-500/10' : 'border-emerald-300 bg-emerald-50'"
99
- class="rounded-lg border p-4">
100
- <p :class="dark ? 'text-emerald-300' : 'text-emerald-800'" class="text-sm font-medium">Token created! Copy it now — it won't be shown again.</p>
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 :class="dark ? 'bg-black text-emerald-400' : 'bg-white text-emerald-700'"
103
- class="flex-1 rounded border px-3 py-2 text-xs font-mono break-all"
104
- :style="dark ? 'border-color: rgba(255,255,255,0.1)' : ''">{{ copiedToken }}</code>
105
- <button @click="copyToken"
106
- :class="dark ? 'bg-white/10 text-white hover:bg-white/20' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
107
- class="rounded-lg px-3 py-2 text-sm font-medium transition-colors">Copy</button>
108
- <button @click="dismissToken"
109
- :class="dark ? 'text-neutral-500 hover:text-white' : 'text-gray-400 hover:text-gray-600'"
110
- class="px-2 py-2 text-sm transition-colors">Dismiss</button>
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 v-if="showCreate"
116
- :class="dark ? 'border-white/[0.06] bg-neutral-950' : 'border-gray-200 bg-white'"
117
- class="rounded-lg border p-5 space-y-4">
118
- <h3 :class="dark ? 'text-white' : 'text-gray-900'" class="text-sm font-semibold">New API Token</h3>
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 :class="dark ? 'text-neutral-400' : 'text-gray-600'" class="mb-1 block text-xs font-medium">Token Name</label>
123
- <input v-model="form.name" type="text" placeholder="e.g. Desktop App - Work Laptop"
124
- :class="dark ? 'border-white/[0.08] bg-white/[0.04] text-white' : 'border-gray-300 bg-white text-gray-900'"
125
- class="w-full rounded-lg border px-3 py-2 text-sm" />
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 :class="dark ? 'text-neutral-400' : 'text-gray-600'" class="mb-1 block text-xs font-medium">User</label>
129
- <select v-model="form.user_id"
130
- :class="dark ? 'border-white/[0.08] bg-white/[0.04] text-white' : 'border-gray-300 bg-white text-gray-900'"
131
- class="w-full rounded-lg border px-3 py-2 text-sm">
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 :class="dark ? 'text-neutral-400' : 'text-gray-600'" class="mb-1 block text-xs font-medium">Abilities</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 v-for="ability in ['agent', 'admin']" :key="ability"
142
- :class="dark ? 'text-neutral-300' : 'text-gray-700'"
143
- class="flex items-center gap-1.5 text-sm cursor-pointer">
144
- <input type="checkbox" :checked="form.abilities.includes(ability)" @change="toggleAbility(ability)"
145
- class="rounded border-neutral-600" />
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 :class="dark ? 'text-neutral-400' : 'text-gray-600'" class="mb-1 block text-xs font-medium">Expires In (days)</label>
153
- <input v-model.number="form.expires_in_days" type="number" min="1" max="365" placeholder="Leave empty for no expiration"
154
- :class="dark ? 'border-white/[0.08] bg-white/[0.04] text-white' : 'border-gray-300 bg-white text-gray-900'"
155
- class="w-48 rounded-lg border px-3 py-2 text-sm" />
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 @click="createToken" :disabled="form.processing || !form.name || !form.user_id"
160
- :class="dark ? 'bg-white text-black hover:bg-neutral-200 disabled:bg-neutral-700 disabled:text-neutral-500' : 'bg-gray-900 text-white hover:bg-gray-800 disabled:bg-gray-300 disabled:text-gray-500'"
161
- class="rounded-lg px-4 py-2 text-sm font-medium transition-colors">
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 @click="showCreate = false"
165
- :class="dark ? 'text-neutral-500 hover:text-white' : 'text-gray-500 hover:text-gray-900'"
166
- class="text-sm transition-colors">Cancel</button>
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 :class="dark ? 'border-white/[0.06]' : 'border-gray-200'" class="overflow-hidden rounded-lg border">
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 :class="dark ? 'bg-white/[0.03] text-neutral-500' : 'bg-gray-50 text-gray-500'">
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-white/[0.04]' : 'divide-gray-100'" class="divide-y">
184
- <tr v-for="token in tokens" :key="token.id"
185
- :class="[dark ? 'hover:bg-white/[0.02]' : 'hover:bg-gray-50', token.is_expired ? 'opacity-50' : '']">
186
- <td :class="dark ? 'text-white' : 'text-gray-900'" class="px-4 py-3 font-medium">{{ token.name }}</td>
187
- <td :class="dark ? 'text-neutral-400' : 'text-gray-600'" class="px-4 py-3">{{ token.user_name }}</td>
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 v-for="a in (token.abilities || [])" :key="a"
190
- :class="dark ? 'bg-white/[0.06] text-neutral-300' : 'bg-gray-100 text-gray-700'"
191
- class="mr-1 rounded px-1.5 py-0.5 text-xs">{{ a }}</span>
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 :class="dark ? 'text-neutral-500' : 'text-gray-500'" class="px-4 py-3 text-xs">
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 :class="dark ? 'text-neutral-500' : 'text-gray-500'" class="px-4 py-3 text-xs">
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 @click="revokeToken(token.id)"
204
- class="text-xs text-rose-400 hover:text-rose-300 transition-colors">Revoke</button>
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 colspan="6" :class="dark ? 'text-neutral-500' : 'text-gray-400'" class="px-4 py-8 text-center text-sm">
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>