@escalated-dev/escalated 0.7.0 → 0.8.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 (181) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +36 -1
  3. package/package.json +16 -7
  4. package/src/components/ActiveChatsPanel.vue +246 -0
  5. package/src/components/ActivityTimeline.stories.js +55 -55
  6. package/src/components/ActivityTimeline.vue +83 -83
  7. package/src/components/AdminDashboard.stories.js +1423 -1095
  8. package/src/components/AgentLoadIndicator.stories.js +37 -37
  9. package/src/components/AgentLoadIndicator.vue +49 -49
  10. package/src/components/AgentPerformanceCard.vue +44 -44
  11. package/src/components/ArticleEditor.vue +82 -82
  12. package/src/components/AssigneeSelect.vue +37 -37
  13. package/src/components/AttachmentList.vue +13 -1
  14. package/src/components/AuditLogEntry.vue +95 -95
  15. package/src/components/BulkActionBar.vue +166 -166
  16. package/src/components/CategoryTree.vue +89 -89
  17. package/src/components/ChartWidget.vue +46 -46
  18. package/src/components/ChatActionBar.vue +226 -0
  19. package/src/components/ChatBubble.vue +162 -0
  20. package/src/components/ChatComposer.vue +308 -0
  21. package/src/components/ChatQueue.vue +222 -0
  22. package/src/components/ChatThread.vue +197 -0
  23. package/src/components/CollisionWarning.stories.js +30 -30
  24. package/src/components/CollisionWarning.vue +68 -68
  25. package/src/components/ConditionalFieldRules.vue +135 -135
  26. package/src/components/ContextPanel.vue +66 -66
  27. package/src/components/ContextPanelSection.vue +65 -65
  28. package/src/components/CustomFieldRenderer.vue +159 -159
  29. package/src/components/EscalatedLayout.vue +35 -6
  30. package/src/components/FileDropzone.vue +57 -57
  31. package/src/components/FollowButton.stories.js +30 -30
  32. package/src/components/FollowButton.vue +88 -88
  33. package/src/components/KeyboardShortcutHelp.vue +213 -213
  34. package/src/components/KnowledgePanel.vue +122 -122
  35. package/src/components/KpiCard.stories.js +32 -32
  36. package/src/components/KpiCard.vue +88 -88
  37. package/src/components/MacroDropdown.vue +150 -150
  38. package/src/components/MentionDropdown.vue +154 -0
  39. package/src/components/MetricComparison.vue +89 -0
  40. package/src/components/PercentileChart.vue +67 -0
  41. package/src/components/PeriodSelector.vue +98 -0
  42. package/src/components/PermissionMatrix.vue +107 -107
  43. package/src/components/PinnedNotes.vue +129 -129
  44. package/src/components/PluginSlot.vue +68 -62
  45. package/src/components/PresenceIndicator.vue +46 -2
  46. package/src/components/PriorityBadge.stories.js +33 -33
  47. package/src/components/PriorityBadge.vue +46 -46
  48. package/src/components/ReplyComposer.vue +81 -18
  49. package/src/components/ReplyThread.vue +40 -2
  50. package/src/components/ReportChart.vue +395 -0
  51. package/src/components/ReportExportButton.vue +108 -0
  52. package/src/components/SatisfactionRating.vue +180 -180
  53. package/src/components/SaveViewDialog.vue +172 -0
  54. package/src/components/SavedViewSidebar.vue +208 -0
  55. package/src/components/ScheduleEditor.vue +104 -104
  56. package/src/components/SideConversation.vue +165 -165
  57. package/src/components/SideConversationList.vue +147 -147
  58. package/src/components/SkillTagManager.vue +83 -79
  59. package/src/components/SlaComplianceChart.vue +46 -53
  60. package/src/components/SlaTimer.stories.js +35 -35
  61. package/src/components/SlaTimer.vue +62 -62
  62. package/src/components/SnoozeButton.vue +247 -0
  63. package/src/components/StatsCard.stories.js +24 -24
  64. package/src/components/StatsCard.vue +58 -58
  65. package/src/components/StatusBadge.stories.js +53 -53
  66. package/src/components/StatusBadge.vue +78 -78
  67. package/src/components/TagSelect.vue +84 -84
  68. package/src/components/TicketFilters.vue +270 -225
  69. package/src/components/TicketLinkPanel.vue +193 -193
  70. package/src/components/TicketList.vue +655 -594
  71. package/src/components/TicketMergeDialog.vue +157 -157
  72. package/src/components/TicketSplitDialog.vue +98 -0
  73. package/src/components/TotpInput.stories.js +9 -9
  74. package/src/components/TotpInput.vue +81 -81
  75. package/src/components/TwoFactorSetup.vue +113 -113
  76. package/src/components/VariableTokenInput.vue +111 -0
  77. package/src/components/WorkflowActionConfig.vue +287 -0
  78. package/src/components/WorkflowActionList.vue +350 -0
  79. package/src/components/WorkflowConditionBuilder.vue +265 -0
  80. package/src/components/WorkflowTriggerSelector.vue +125 -0
  81. package/src/composables/useChat.js +187 -0
  82. package/src/composables/useI18n.js +30 -8
  83. package/src/composables/useKeyboardShortcuts.js +46 -46
  84. package/src/composables/useMentions.js +134 -0
  85. package/src/composables/usePluginExtensions.js +159 -159
  86. package/src/composables/usePluginHooks.js +65 -65
  87. package/src/composables/useRealtime.js +182 -0
  88. package/src/defineEscalatedPlugin.js +76 -76
  89. package/src/index.js +84 -72
  90. package/src/locales/README.md +13 -0
  91. package/src/locales/ar.json +727 -0
  92. package/src/locales/de.json +678 -430
  93. package/src/locales/en.json +717 -455
  94. package/src/locales/es.json +679 -430
  95. package/src/locales/fr.json +680 -431
  96. package/src/locales/index.js +12 -2
  97. package/src/locales/it.json +727 -0
  98. package/src/locales/ja.json +727 -0
  99. package/src/locales/ko.json +727 -0
  100. package/src/locales/nl.json +727 -0
  101. package/src/locales/pl.json +727 -0
  102. package/src/locales/pt-BR.json +727 -0
  103. package/src/locales/ru.json +727 -0
  104. package/src/locales/tr.json +727 -0
  105. package/src/locales/zh-CN.json +727 -0
  106. package/src/pages/Admin/ApiTokens/Index.vue +381 -381
  107. package/src/pages/Admin/AuditLog/Index.vue +154 -154
  108. package/src/pages/Admin/BusinessHours/Form.vue +172 -172
  109. package/src/pages/Admin/BusinessHours/Index.vue +113 -113
  110. package/src/pages/Admin/CannedResponses/Index.vue +127 -127
  111. package/src/pages/Admin/Capacity/Index.vue +134 -134
  112. package/src/pages/Admin/CustomFields/Form.vue +195 -195
  113. package/src/pages/Admin/CustomFields/Index.vue +134 -134
  114. package/src/pages/Admin/CustomObjects/Form.vue +202 -202
  115. package/src/pages/Admin/CustomObjects/Index.vue +127 -127
  116. package/src/pages/Admin/CustomObjects/Records.vue +228 -228
  117. package/src/pages/Admin/Departments/Form.vue +75 -75
  118. package/src/pages/Admin/Departments/Index.vue +110 -109
  119. package/src/pages/Admin/EscalationRules/Form.vue +157 -157
  120. package/src/pages/Admin/EscalationRules/Index.vue +178 -178
  121. package/src/pages/Admin/Import/Connect.vue +0 -1
  122. package/src/pages/Admin/Import/Index.vue +1 -1
  123. package/src/pages/Admin/Import/Progress.vue +1 -1
  124. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
  125. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
  126. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
  127. package/src/pages/Admin/Macros/Index.vue +448 -448
  128. package/src/pages/Admin/Plugins/Index.vue +327 -327
  129. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  130. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
  131. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  132. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  133. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  134. package/src/pages/Admin/Reports/CsatReport.vue +127 -127
  135. package/src/pages/Admin/Reports/Dashboard.vue +36 -21
  136. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  137. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  138. package/src/pages/Admin/Reports/SlaReport.vue +162 -166
  139. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  140. package/src/pages/Admin/Reports.vue +206 -206
  141. package/src/pages/Admin/Roles/Form.vue +71 -71
  142. package/src/pages/Admin/Roles/Index.vue +122 -122
  143. package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
  144. package/src/pages/Admin/Settings/DataRetention.vue +182 -182
  145. package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
  146. package/src/pages/Admin/Settings/Sandbox.vue +135 -135
  147. package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
  148. package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
  149. package/src/pages/Admin/Settings.vue +1008 -468
  150. package/src/pages/Admin/Skills/Form.vue +193 -53
  151. package/src/pages/Admin/Skills/Index.vue +105 -94
  152. package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
  153. package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
  154. package/src/pages/Admin/Statuses/Form.vue +114 -114
  155. package/src/pages/Admin/Statuses/Index.vue +153 -153
  156. package/src/pages/Admin/Tags/Index.vue +191 -191
  157. package/src/pages/Admin/Tickets/Index.vue +126 -91
  158. package/src/pages/Admin/Tickets/Show.vue +29 -0
  159. package/src/pages/Admin/Users/Index.vue +145 -0
  160. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
  161. package/src/pages/Admin/Webhooks/Form.vue +110 -110
  162. package/src/pages/Admin/Webhooks/Index.vue +136 -136
  163. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  164. package/src/pages/Admin/Workflows/Index.vue +235 -0
  165. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  166. package/src/pages/Agent/TicketIndex.vue +112 -88
  167. package/src/pages/Agent/TicketShow.vue +192 -60
  168. package/src/pages/Customer/Create.vue +1 -1
  169. package/src/pages/Customer/Index.vue +26 -1
  170. package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
  171. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
  172. package/src/pages/Customer/Show.vue +69 -63
  173. package/src/pages/Guest/Create.vue +40 -15
  174. package/src/pages/Guest/Show.vue +124 -93
  175. package/src/plugin.js +26 -1
  176. package/src/utils/mentionHighlight.js +24 -0
  177. package/src/utils/sanitizeHtml.js +78 -64
  178. package/src/widget/EscalatedWidget.vue +804 -0
  179. package/src/widget/index.js +254 -0
  180. package/src/pages/Admin/Automations/Form.vue +0 -184
  181. package/src/pages/Admin/Automations/Index.vue +0 -129
@@ -1,381 +1,381 @@
1
- <script setup>
2
- import { ref, computed, inject } from 'vue';
3
- import { useForm, usePage } from '@inertiajs/vue3';
4
- import EscalatedLayout from '../../../components/EscalatedLayout.vue';
5
-
6
- const props = defineProps({
7
- tokens: Array,
8
- users: Array,
9
- api_enabled: Boolean,
10
- });
11
-
12
- const dark = inject(
13
- 'esc-dark',
14
- computed(() => true),
15
- );
16
- const page = usePage();
17
- const prefix = computed(() => {
18
- const p = page.props.escalated?.prefix || 'support';
19
- return p.startsWith('/') ? p : `/${p}`;
20
- });
21
-
22
- const showCreate = ref(false);
23
- const copiedToken = ref(null);
24
-
25
- const form = useForm({
26
- name: '',
27
- user_id: '',
28
- abilities: ['agent'],
29
- expires_in_days: null,
30
- });
31
-
32
- function createToken() {
33
- form.post(`${prefix.value}/admin/api-tokens`, {
34
- preserveScroll: true,
35
- onSuccess: (page) => {
36
- const flash = page.props.flash || {};
37
- if (flash.plain_text_token) {
38
- copiedToken.value = flash.plain_text_token;
39
- }
40
- form.reset();
41
- showCreate.value = false;
42
- },
43
- });
44
- }
45
-
46
- function revokeToken(id) {
47
- if (!confirm('Are you sure you want to revoke this token? This cannot be undone.')) return;
48
- useForm({}).delete(`${prefix.value}/admin/api-tokens/${id}`, { preserveScroll: true });
49
- }
50
-
51
- function copyToken() {
52
- if (copiedToken.value) {
53
- navigator.clipboard.writeText(copiedToken.value);
54
- }
55
- }
56
-
57
- function dismissToken() {
58
- copiedToken.value = null;
59
- }
60
-
61
- function toggleAbility(ability) {
62
- const idx = form.abilities.indexOf(ability);
63
- if (idx >= 0) {
64
- form.abilities.splice(idx, 1);
65
- } else {
66
- form.abilities.push(ability);
67
- }
68
- }
69
-
70
- function formatDate(iso) {
71
- if (!iso) return '—';
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
- });
79
- }
80
- </script>
81
-
82
- <template>
83
- <EscalatedLayout title="API Tokens">
84
- <div class="space-y-6">
85
- <!-- Header -->
86
- <div class="flex items-center justify-between">
87
- <div>
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>
94
- </div>
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
- >
102
- Create Token
103
- </button>
104
- </div>
105
-
106
- <!-- Not enabled warning -->
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.
118
- </div>
119
-
120
- <!-- Copied token modal -->
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>
129
- <div class="mt-2 flex items-center gap-2">
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>
158
- </div>
159
- </div>
160
-
161
- <!-- Create form -->
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>
174
-
175
- <div class="grid grid-cols-2 gap-4">
176
- <div>
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
- />
193
- </div>
194
- <div>
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
- >
209
- <option value="">Select user...</option>
210
- <option v-for="u in users" :key="u.id" :value="u.id">{{ u.name }} ({{ u.email }})</option>
211
- </select>
212
- </div>
213
- </div>
214
-
215
- <div>
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
- >
221
- <div class="flex items-center gap-3">
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
- />
234
- {{ ability }}
235
- </label>
236
- </div>
237
- </div>
238
-
239
- <div>
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
- />
258
- </div>
259
-
260
- <div class="flex items-center gap-3 pt-2">
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
- >
271
- {{ form.processing ? 'Creating...' : 'Create Token' }}
272
- </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>
284
- </div>
285
- </div>
286
-
287
- <!-- Tokens table -->
288
- <div
289
- :class="dark ? 'border-[var(--esc-panel-border)]' : 'border-gray-200'"
290
- class="overflow-hidden rounded-lg border"
291
- >
292
- <table class="w-full text-sm">
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
- >
300
- <tr>
301
- <th class="px-4 py-3 text-left text-xs font-medium uppercase">Name</th>
302
- <th class="px-4 py-3 text-left text-xs font-medium uppercase">User</th>
303
- <th class="px-4 py-3 text-left text-xs font-medium uppercase">Abilities</th>
304
- <th class="px-4 py-3 text-left text-xs font-medium uppercase">Last Used</th>
305
- <th class="px-4 py-3 text-left text-xs font-medium uppercase">Expires</th>
306
- <th class="px-4 py-3 text-right text-xs font-medium uppercase">Actions</th>
307
- </tr>
308
- </thead>
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>
330
- <td class="px-4 py-3">
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
- >
342
- </td>
343
- <td
344
- :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'"
345
- class="px-4 py-3 text-xs"
346
- >
347
- <template v-if="token.last_used_at">{{ formatDate(token.last_used_at) }}</template>
348
- <template v-else>Never</template>
349
- </td>
350
- <td
351
- :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'"
352
- class="px-4 py-3 text-xs"
353
- >
354
- <span v-if="token.is_expired" class="text-rose-400">Expired</span>
355
- <template v-else-if="token.expires_at">{{ formatDate(token.expires_at) }}</template>
356
- <template v-else>Never</template>
357
- </td>
358
- <td class="px-4 py-3 text-right">
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>
365
- </td>
366
- </tr>
367
- <tr v-if="!tokens.length">
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
- >
373
- No API tokens created yet.
374
- </td>
375
- </tr>
376
- </tbody>
377
- </table>
378
- </div>
379
- </div>
380
- </EscalatedLayout>
381
- </template>
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+ import { useForm, usePage } from '@inertiajs/vue3';
4
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
5
+
6
+ defineProps({
7
+ tokens: Array,
8
+ users: Array,
9
+ api_enabled: Boolean,
10
+ });
11
+
12
+ const dark = inject(
13
+ 'esc-dark',
14
+ computed(() => true),
15
+ );
16
+ const page = usePage();
17
+ const prefix = computed(() => {
18
+ const p = page.props.escalated?.prefix || 'support';
19
+ return p.startsWith('/') ? p : `/${p}`;
20
+ });
21
+
22
+ const showCreate = ref(false);
23
+ const copiedToken = ref(null);
24
+
25
+ const form = useForm({
26
+ name: '',
27
+ user_id: '',
28
+ abilities: ['agent'],
29
+ expires_in_days: null,
30
+ });
31
+
32
+ function createToken() {
33
+ form.post(`${prefix.value}/admin/api-tokens`, {
34
+ preserveScroll: true,
35
+ onSuccess: (page) => {
36
+ const flash = page.props.flash || {};
37
+ if (flash.plain_text_token) {
38
+ copiedToken.value = flash.plain_text_token;
39
+ }
40
+ form.reset();
41
+ showCreate.value = false;
42
+ },
43
+ });
44
+ }
45
+
46
+ function revokeToken(id) {
47
+ if (!confirm('Are you sure you want to revoke this token? This cannot be undone.')) return;
48
+ useForm({}).delete(`${prefix.value}/admin/api-tokens/${id}`, { preserveScroll: true });
49
+ }
50
+
51
+ function copyToken() {
52
+ if (copiedToken.value) {
53
+ navigator.clipboard.writeText(copiedToken.value);
54
+ }
55
+ }
56
+
57
+ function dismissToken() {
58
+ copiedToken.value = null;
59
+ }
60
+
61
+ function toggleAbility(ability) {
62
+ const idx = form.abilities.indexOf(ability);
63
+ if (idx >= 0) {
64
+ form.abilities.splice(idx, 1);
65
+ } else {
66
+ form.abilities.push(ability);
67
+ }
68
+ }
69
+
70
+ function formatDate(iso) {
71
+ if (!iso) return '—';
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
+ });
79
+ }
80
+ </script>
81
+
82
+ <template>
83
+ <EscalatedLayout title="API Tokens">
84
+ <div class="space-y-6">
85
+ <!-- Header -->
86
+ <div class="flex items-center justify-between">
87
+ <div>
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>
94
+ </div>
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
+ >
102
+ Create Token
103
+ </button>
104
+ </div>
105
+
106
+ <!-- Not enabled warning -->
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.
118
+ </div>
119
+
120
+ <!-- Copied token modal -->
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>
129
+ <div class="mt-2 flex items-center gap-2">
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>
158
+ </div>
159
+ </div>
160
+
161
+ <!-- Create form -->
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>
174
+
175
+ <div class="grid grid-cols-2 gap-4">
176
+ <div>
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
+ />
193
+ </div>
194
+ <div>
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
+ >
209
+ <option value="">Select user...</option>
210
+ <option v-for="u in users" :key="u.id" :value="u.id">{{ u.name }} ({{ u.email }})</option>
211
+ </select>
212
+ </div>
213
+ </div>
214
+
215
+ <div>
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
+ >
221
+ <div class="flex items-center gap-3">
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
+ />
234
+ {{ ability }}
235
+ </label>
236
+ </div>
237
+ </div>
238
+
239
+ <div>
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
+ />
258
+ </div>
259
+
260
+ <div class="flex items-center gap-3 pt-2">
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
+ >
271
+ {{ form.processing ? 'Creating...' : 'Create Token' }}
272
+ </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>
284
+ </div>
285
+ </div>
286
+
287
+ <!-- Tokens table -->
288
+ <div
289
+ :class="dark ? 'border-[var(--esc-panel-border)]' : 'border-gray-200'"
290
+ class="overflow-hidden rounded-lg border"
291
+ >
292
+ <table class="w-full text-sm">
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
+ >
300
+ <tr>
301
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase">Name</th>
302
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase">User</th>
303
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase">Abilities</th>
304
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase">Last Used</th>
305
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase">Expires</th>
306
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase">Actions</th>
307
+ </tr>
308
+ </thead>
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>
330
+ <td class="px-4 py-3">
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
+ >
342
+ </td>
343
+ <td
344
+ :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'"
345
+ class="px-4 py-3 text-xs"
346
+ >
347
+ <template v-if="token.last_used_at">{{ formatDate(token.last_used_at) }}</template>
348
+ <template v-else>Never</template>
349
+ </td>
350
+ <td
351
+ :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'"
352
+ class="px-4 py-3 text-xs"
353
+ >
354
+ <span v-if="token.is_expired" class="text-rose-400">Expired</span>
355
+ <template v-else-if="token.expires_at">{{ formatDate(token.expires_at) }}</template>
356
+ <template v-else>Never</template>
357
+ </td>
358
+ <td class="px-4 py-3 text-right">
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>
365
+ </td>
366
+ </tr>
367
+ <tr v-if="!tokens.length">
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
+ >
373
+ No API tokens created yet.
374
+ </td>
375
+ </tr>
376
+ </tbody>
377
+ </table>
378
+ </div>
379
+ </div>
380
+ </EscalatedLayout>
381
+ </template>