@escalated-dev/escalated 0.7.0 → 0.7.1

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 (179) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +31 -0
  3. package/package.json +12 -6
  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 +29 -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 +79 -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 +80 -80
  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/ar.json +727 -0
  91. package/src/locales/de.json +678 -430
  92. package/src/locales/en.json +717 -455
  93. package/src/locales/es.json +679 -430
  94. package/src/locales/fr.json +680 -431
  95. package/src/locales/index.js +12 -2
  96. package/src/locales/it.json +727 -0
  97. package/src/locales/ja.json +727 -0
  98. package/src/locales/ko.json +727 -0
  99. package/src/locales/nl.json +727 -0
  100. package/src/locales/pl.json +727 -0
  101. package/src/locales/pt-BR.json +727 -0
  102. package/src/locales/ru.json +727 -0
  103. package/src/locales/tr.json +727 -0
  104. package/src/locales/zh-CN.json +727 -0
  105. package/src/pages/Admin/ApiTokens/Index.vue +381 -381
  106. package/src/pages/Admin/AuditLog/Index.vue +154 -154
  107. package/src/pages/Admin/BusinessHours/Form.vue +172 -172
  108. package/src/pages/Admin/BusinessHours/Index.vue +113 -113
  109. package/src/pages/Admin/CannedResponses/Index.vue +127 -127
  110. package/src/pages/Admin/Capacity/Index.vue +134 -134
  111. package/src/pages/Admin/CustomFields/Form.vue +195 -195
  112. package/src/pages/Admin/CustomFields/Index.vue +134 -134
  113. package/src/pages/Admin/CustomObjects/Form.vue +202 -202
  114. package/src/pages/Admin/CustomObjects/Index.vue +127 -127
  115. package/src/pages/Admin/CustomObjects/Records.vue +228 -228
  116. package/src/pages/Admin/Departments/Form.vue +75 -75
  117. package/src/pages/Admin/Departments/Index.vue +110 -109
  118. package/src/pages/Admin/EscalationRules/Form.vue +157 -157
  119. package/src/pages/Admin/EscalationRules/Index.vue +178 -178
  120. package/src/pages/Admin/Import/Connect.vue +0 -1
  121. package/src/pages/Admin/Import/Index.vue +1 -1
  122. package/src/pages/Admin/Import/Progress.vue +1 -1
  123. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
  124. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
  125. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
  126. package/src/pages/Admin/Macros/Index.vue +448 -448
  127. package/src/pages/Admin/Plugins/Index.vue +327 -327
  128. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  129. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
  130. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  131. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  132. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  133. package/src/pages/Admin/Reports/CsatReport.vue +127 -127
  134. package/src/pages/Admin/Reports/Dashboard.vue +36 -21
  135. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  136. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  137. package/src/pages/Admin/Reports/SlaReport.vue +162 -166
  138. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  139. package/src/pages/Admin/Reports.vue +206 -206
  140. package/src/pages/Admin/Roles/Form.vue +71 -71
  141. package/src/pages/Admin/Roles/Index.vue +122 -122
  142. package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
  143. package/src/pages/Admin/Settings/DataRetention.vue +182 -182
  144. package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
  145. package/src/pages/Admin/Settings/Sandbox.vue +135 -135
  146. package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
  147. package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
  148. package/src/pages/Admin/Settings.vue +1008 -468
  149. package/src/pages/Admin/Skills/Form.vue +53 -53
  150. package/src/pages/Admin/Skills/Index.vue +94 -94
  151. package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
  152. package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
  153. package/src/pages/Admin/Statuses/Form.vue +114 -114
  154. package/src/pages/Admin/Statuses/Index.vue +153 -153
  155. package/src/pages/Admin/Tags/Index.vue +191 -191
  156. package/src/pages/Admin/Tickets/Index.vue +126 -91
  157. package/src/pages/Admin/Tickets/Show.vue +29 -0
  158. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
  159. package/src/pages/Admin/Webhooks/Form.vue +110 -110
  160. package/src/pages/Admin/Webhooks/Index.vue +136 -136
  161. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  162. package/src/pages/Admin/Workflows/Index.vue +235 -0
  163. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  164. package/src/pages/Agent/TicketIndex.vue +112 -88
  165. package/src/pages/Agent/TicketShow.vue +192 -60
  166. package/src/pages/Customer/Create.vue +1 -1
  167. package/src/pages/Customer/Index.vue +31 -31
  168. package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
  169. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
  170. package/src/pages/Customer/Show.vue +69 -63
  171. package/src/pages/Guest/Create.vue +40 -15
  172. package/src/pages/Guest/Show.vue +124 -93
  173. package/src/plugin.js +25 -0
  174. package/src/utils/mentionHighlight.js +24 -0
  175. package/src/utils/sanitizeHtml.js +78 -64
  176. package/src/widget/EscalatedWidget.vue +804 -0
  177. package/src/widget/index.js +254 -0
  178. package/src/pages/Admin/Automations/Form.vue +0 -184
  179. 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>