@escalated-dev/escalated 0.8.0 → 0.11.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 (124) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +388 -361
  3. package/package.json +85 -84
  4. package/src/components/ActiveChatsPanel.vue +246 -246
  5. package/src/components/AdminDashboard.stories.js +1423 -1423
  6. package/src/components/AttachmentList.vue +65 -65
  7. package/src/components/ChatActionBar.vue +226 -226
  8. package/src/components/ChatBubble.vue +162 -162
  9. package/src/components/ChatComposer.vue +308 -308
  10. package/src/components/ChatQueue.vue +222 -222
  11. package/src/components/ChatThread.vue +197 -197
  12. package/src/components/CustomTicketActions.stories.js +71 -0
  13. package/src/components/CustomTicketActions.vue +101 -0
  14. package/src/components/EscalatedLayout.vue +628 -604
  15. package/src/components/FollowButton.vue +20 -1
  16. package/src/components/MentionDropdown.vue +154 -154
  17. package/src/components/MetricComparison.vue +89 -89
  18. package/src/components/PercentileChart.vue +67 -67
  19. package/src/components/PeriodSelector.vue +98 -98
  20. package/src/components/PresenceIndicator.vue +228 -213
  21. package/src/components/ReplyComposer.vue +336 -336
  22. package/src/components/ReplyThread.vue +153 -153
  23. package/src/components/ReportChart.vue +395 -395
  24. package/src/components/ReportExportButton.vue +108 -108
  25. package/src/components/SaveViewDialog.vue +172 -172
  26. package/src/components/SavedViewSidebar.vue +208 -208
  27. package/src/components/SkillTagManager.vue +3 -0
  28. package/src/components/SnoozeButton.vue +247 -247
  29. package/src/components/TicketFilters.vue +270 -270
  30. package/src/components/TicketList.vue +655 -655
  31. package/src/components/TicketSidebar.vue +4 -0
  32. package/src/components/TicketSplitDialog.vue +98 -98
  33. package/src/components/TicketSubjects.stories.js +63 -0
  34. package/src/components/TicketSubjects.vue +107 -0
  35. package/src/components/TwoFactorSetup.vue +1 -37
  36. package/src/components/VariableTokenInput.vue +111 -111
  37. package/src/components/WorkflowActionConfig.vue +287 -287
  38. package/src/components/WorkflowActionList.vue +350 -350
  39. package/src/components/WorkflowConditionBuilder.vue +265 -265
  40. package/src/components/WorkflowTriggerSelector.vue +125 -125
  41. package/src/components/admin/newsletters/AnalyticsTiles.stories.js +14 -0
  42. package/src/components/admin/newsletters/AnalyticsTiles.vue +55 -0
  43. package/src/components/admin/newsletters/DeliveriesTable.stories.js +28 -0
  44. package/src/components/admin/newsletters/DeliveriesTable.vue +104 -0
  45. package/src/components/admin/newsletters/DynamicFilterBuilder.stories.js +11 -0
  46. package/src/components/admin/newsletters/DynamicFilterBuilder.vue +53 -0
  47. package/src/components/admin/newsletters/ListMemberTable.stories.js +12 -0
  48. package/src/components/admin/newsletters/ListMemberTable.vue +53 -0
  49. package/src/components/admin/newsletters/MarkdownEditor.stories.js +6 -0
  50. package/src/components/admin/newsletters/MarkdownEditor.vue +53 -0
  51. package/src/components/admin/newsletters/MergeFieldDropdown.stories.js +6 -0
  52. package/src/components/admin/newsletters/MergeFieldDropdown.vue +84 -0
  53. package/src/components/admin/newsletters/PreviewIframe.stories.js +7 -0
  54. package/src/components/admin/newsletters/PreviewIframe.vue +40 -0
  55. package/src/components/admin/newsletters/SummaryTiles.stories.js +8 -0
  56. package/src/components/admin/newsletters/SummaryTiles.vue +63 -0
  57. package/src/composables/useChat.js +187 -187
  58. package/src/composables/useI18n.js +102 -102
  59. package/src/composables/useMentions.js +134 -134
  60. package/src/composables/usePermissions.js +31 -0
  61. package/src/composables/useRealtime.js +182 -182
  62. package/src/index.js +85 -84
  63. package/src/locales/README.md +13 -13
  64. package/src/locales/ar.json +727 -727
  65. package/src/locales/de.json +690 -690
  66. package/src/locales/en.json +889 -729
  67. package/src/locales/es.json +691 -691
  68. package/src/locales/fr.json +691 -691
  69. package/src/locales/index.js +16 -16
  70. package/src/locales/it.json +727 -727
  71. package/src/locales/ja.json +727 -727
  72. package/src/locales/ko.json +727 -727
  73. package/src/locales/nl.json +727 -727
  74. package/src/locales/pl.json +727 -727
  75. package/src/locales/pt-BR.json +727 -727
  76. package/src/locales/ru.json +727 -727
  77. package/src/locales/tr.json +727 -727
  78. package/src/locales/zh-CN.json +727 -727
  79. package/src/pages/Admin/Automations/Form.vue +280 -0
  80. package/src/pages/Admin/Automations/Index.vue +230 -0
  81. package/src/pages/Admin/Newsletters/Compose.vue +192 -0
  82. package/src/pages/Admin/Newsletters/Edit.vue +24 -0
  83. package/src/pages/Admin/Newsletters/Index.vue +141 -0
  84. package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
  85. package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
  86. package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
  87. package/src/pages/Admin/Newsletters/Settings.vue +85 -0
  88. package/src/pages/Admin/Newsletters/Show.vue +140 -0
  89. package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
  90. package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
  91. package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
  92. package/src/pages/Admin/Newsletters/usePreview.js +44 -0
  93. package/src/pages/Admin/Plugins/Index.vue +10 -2
  94. package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
  95. package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
  96. package/src/pages/Admin/Reports/Cohorts.vue +180 -180
  97. package/src/pages/Admin/Reports/Comparison.vue +141 -141
  98. package/src/pages/Admin/Reports/Dashboard.vue +226 -226
  99. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
  100. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
  101. package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
  102. package/src/pages/Admin/Settings/DatabaseConnection.vue +228 -0
  103. package/src/pages/Admin/Settings/PublicTickets.vue +163 -0
  104. package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
  105. package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
  106. package/src/pages/Admin/Settings.vue +1008 -1008
  107. package/src/pages/Admin/Skills/Form.vue +30 -10
  108. package/src/pages/Admin/Tickets/Index.vue +126 -126
  109. package/src/pages/Admin/Tickets/Show.vue +254 -249
  110. package/src/pages/Admin/Users/Index.vue +145 -145
  111. package/src/pages/Admin/Webhooks/DeliveryLog.vue +1 -2
  112. package/src/pages/Admin/Workflows/Builder.vue +207 -207
  113. package/src/pages/Admin/Workflows/Form.vue +24 -0
  114. package/src/pages/Admin/Workflows/Index.vue +235 -235
  115. package/src/pages/Admin/Workflows/Logs.vue +286 -286
  116. package/src/pages/Agent/TicketIndex.vue +112 -112
  117. package/src/pages/Agent/TicketShow.vue +547 -494
  118. package/src/pages/Customer/Index.vue +56 -56
  119. package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
  120. package/src/pages/Plugin/Page.vue +61 -0
  121. package/src/plugin.js +175 -175
  122. package/src/utils/mentionHighlight.js +24 -24
  123. package/src/widget/EscalatedWidget.vue +804 -804
  124. package/src/widget/index.js +254 -254
@@ -0,0 +1,280 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { Link, useForm } from '@inertiajs/vue3';
4
+
5
+ /**
6
+ * Create and edit a time-based automation.
7
+ *
8
+ * The vocabulary here is the runner's: every field, operator and action type
9
+ * below is one the backend actually evaluates. Offering anything else would
10
+ * produce a rule that saves cleanly and then silently never matches.
11
+ */
12
+ const props = defineProps({
13
+ automation: { type: Object, default: null },
14
+ });
15
+
16
+ /** Fields that count hours take an operator and a number; the rest take a value. */
17
+ const TIME_FIELDS = ['hours_since_created', 'hours_since_updated', 'hours_since_assigned'];
18
+
19
+ const FIELDS = [
20
+ { value: 'hours_since_created', label: 'Hours since created' },
21
+ { value: 'hours_since_updated', label: 'Hours since last update' },
22
+ { value: 'hours_since_assigned', label: 'Hours since assigned' },
23
+ { value: 'status', label: 'Status' },
24
+ { value: 'priority', label: 'Priority' },
25
+ { value: 'assigned', label: 'Assignment' },
26
+ { value: 'ticket_type', label: 'Ticket type' },
27
+ { value: 'subject_contains', label: 'Subject contains' },
28
+ ];
29
+
30
+ const OPERATORS = [
31
+ { value: '>', label: 'more than' },
32
+ { value: '>=', label: 'at least' },
33
+ { value: '<', label: 'fewer than' },
34
+ { value: '<=', label: 'at most' },
35
+ { value: '=', label: 'exactly' },
36
+ ];
37
+
38
+ const ACTIONS = [
39
+ { value: 'change_status', label: 'Change status' },
40
+ { value: 'change_priority', label: 'Change priority' },
41
+ { value: 'assign', label: 'Assign to agent' },
42
+ { value: 'add_tag', label: 'Add tag' },
43
+ { value: 'set_ticket_type', label: 'Set ticket type' },
44
+ { value: 'add_note', label: 'Add internal note' },
45
+ ];
46
+
47
+ const ASSIGNMENT_VALUES = [
48
+ { value: 'assigned', label: 'Assigned' },
49
+ { value: 'unassigned', label: 'Unassigned' },
50
+ ];
51
+
52
+ const form = useForm({
53
+ name: props.automation?.name ?? '',
54
+ conditions: (props.automation?.conditions ?? []).map((c) => ({
55
+ field: c.field ?? 'hours_since_updated',
56
+ operator: c.operator ?? '>',
57
+ value: c.value ?? '',
58
+ })),
59
+ actions: (props.automation?.actions ?? []).map((a) => ({
60
+ type: a.type ?? 'change_status',
61
+ value: a.value ?? '',
62
+ })),
63
+ active: props.automation?.active ?? true,
64
+ });
65
+
66
+ // A new automation starts with one of each, because an empty one cannot be
67
+ // saved: the backend requires at least one condition and one action.
68
+ if (!form.conditions.length) {
69
+ form.conditions.push({ field: 'hours_since_updated', operator: '>', value: 24 });
70
+ }
71
+
72
+ if (!form.actions.length) {
73
+ form.actions.push({ type: 'change_status', value: '' });
74
+ }
75
+
76
+ function isTimeField(field) {
77
+ return TIME_FIELDS.includes(field);
78
+ }
79
+
80
+ function addCondition() {
81
+ form.conditions.push({ field: 'hours_since_updated', operator: '>', value: 24 });
82
+ }
83
+
84
+ function removeCondition(index) {
85
+ form.conditions.splice(index, 1);
86
+ }
87
+
88
+ function addAction() {
89
+ form.actions.push({ type: 'change_status', value: '' });
90
+ }
91
+
92
+ function removeAction(index) {
93
+ form.actions.splice(index, 1);
94
+ }
95
+
96
+ function submit() {
97
+ if (props.automation?.id) {
98
+ form.put(route('escalated.admin.automations.update', props.automation.id));
99
+ } else {
100
+ form.post(route('escalated.admin.automations.store'));
101
+ }
102
+ }
103
+ </script>
104
+
105
+ <template>
106
+ <EscalatedLayout :title="automation ? 'Edit Automation' : 'Create Automation'">
107
+ <div class="mb-4">
108
+ <Link
109
+ :href="route('escalated.admin.automations.index')"
110
+ class="text-xs text-[var(--esc-panel-text-muted)] transition-colors hover:text-[var(--esc-panel-accent)]"
111
+ >
112
+ &larr; Back to automations
113
+ </Link>
114
+ <h2 class="mt-2 text-lg font-semibold text-[var(--esc-panel-text-secondary)]">
115
+ {{ automation ? 'Edit Automation' : 'Create Automation' }}
116
+ </h2>
117
+ <p class="text-xs text-[var(--esc-panel-text-muted)]">
118
+ Runs on a schedule against open tickets. Every condition must match for the actions to apply.
119
+ </p>
120
+ </div>
121
+
122
+ <form class="space-y-6" @submit.prevent="submit">
123
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-4">
124
+ <label class="block text-xs font-medium text-[var(--esc-panel-text-muted)]" for="automation-name">
125
+ Name
126
+ </label>
127
+ <input
128
+ id="automation-name"
129
+ v-model="form.name"
130
+ type="text"
131
+ required
132
+ maxlength="255"
133
+ placeholder="Escalate tickets untouched for a day"
134
+ class="mt-1 w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-accent)] focus:outline-none"
135
+ />
136
+ <p v-if="form.errors.name" class="mt-1 text-xs text-red-400">{{ form.errors.name }}</p>
137
+
138
+ <label class="mt-4 flex items-center gap-2 text-sm text-[var(--esc-panel-text-secondary)]">
139
+ <input v-model="form.active" type="checkbox" class="rounded border-[var(--esc-panel-border)]" />
140
+ Active
141
+ </label>
142
+ </div>
143
+
144
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-4">
145
+ <div class="mb-3 flex items-center justify-between">
146
+ <div>
147
+ <h3 class="text-sm font-semibold text-[var(--esc-panel-text-secondary)]">When</h3>
148
+ <p class="text-xs text-[var(--esc-panel-text-muted)]">All conditions must match</p>
149
+ </div>
150
+ <button
151
+ type="button"
152
+ class="rounded-lg border border-[var(--esc-panel-border)] px-3 py-1.5 text-xs font-medium text-[var(--esc-panel-text-secondary)] transition-colors hover:bg-[var(--esc-panel-hover)]"
153
+ @click="addCondition"
154
+ >
155
+ Add condition
156
+ </button>
157
+ </div>
158
+
159
+ <div v-for="(condition, i) in form.conditions" :key="`condition-${i}`" class="mb-2 flex gap-2">
160
+ <select
161
+ v-model="condition.field"
162
+ :aria-label="`Condition ${i + 1} field`"
163
+ class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
164
+ >
165
+ <option v-for="field in FIELDS" :key="field.value" :value="field.value">
166
+ {{ field.label }}
167
+ </option>
168
+ </select>
169
+
170
+ <select
171
+ v-if="isTimeField(condition.field)"
172
+ v-model="condition.operator"
173
+ :aria-label="`Condition ${i + 1} operator`"
174
+ class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
175
+ >
176
+ <option v-for="operator in OPERATORS" :key="operator.value" :value="operator.value">
177
+ {{ operator.label }}
178
+ </option>
179
+ </select>
180
+
181
+ <select
182
+ v-if="condition.field === 'assigned'"
183
+ v-model="condition.value"
184
+ :aria-label="`Condition ${i + 1} value`"
185
+ class="flex-1 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
186
+ >
187
+ <option v-for="option in ASSIGNMENT_VALUES" :key="option.value" :value="option.value">
188
+ {{ option.label }}
189
+ </option>
190
+ </select>
191
+ <input
192
+ v-else
193
+ v-model="condition.value"
194
+ :type="isTimeField(condition.field) ? 'number' : 'text'"
195
+ :min="isTimeField(condition.field) ? 0 : undefined"
196
+ :aria-label="`Condition ${i + 1} value`"
197
+ :placeholder="isTimeField(condition.field) ? 'hours' : 'value'"
198
+ class="flex-1 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
199
+ />
200
+
201
+ <button
202
+ type="button"
203
+ :aria-label="`Remove condition ${i + 1}`"
204
+ class="rounded-lg px-2 text-xs text-[var(--esc-panel-text-muted)] transition-colors hover:text-red-400"
205
+ :disabled="form.conditions.length === 1"
206
+ @click="removeCondition(i)"
207
+ >
208
+ Remove
209
+ </button>
210
+ </div>
211
+
212
+ <p v-if="form.errors.conditions" class="mt-1 text-xs text-red-400">{{ form.errors.conditions }}</p>
213
+ </div>
214
+
215
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-4">
216
+ <div class="mb-3 flex items-center justify-between">
217
+ <div>
218
+ <h3 class="text-sm font-semibold text-[var(--esc-panel-text-secondary)]">Then</h3>
219
+ <p class="text-xs text-[var(--esc-panel-text-muted)]">Applied to every matching ticket</p>
220
+ </div>
221
+ <button
222
+ type="button"
223
+ class="rounded-lg border border-[var(--esc-panel-border)] px-3 py-1.5 text-xs font-medium text-[var(--esc-panel-text-secondary)] transition-colors hover:bg-[var(--esc-panel-hover)]"
224
+ @click="addAction"
225
+ >
226
+ Add action
227
+ </button>
228
+ </div>
229
+
230
+ <div v-for="(action, i) in form.actions" :key="`action-${i}`" class="mb-2 flex gap-2">
231
+ <select
232
+ v-model="action.type"
233
+ :aria-label="`Action ${i + 1} type`"
234
+ class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
235
+ >
236
+ <option v-for="option in ACTIONS" :key="option.value" :value="option.value">
237
+ {{ option.label }}
238
+ </option>
239
+ </select>
240
+
241
+ <input
242
+ v-model="action.value"
243
+ type="text"
244
+ :aria-label="`Action ${i + 1} value`"
245
+ placeholder="value"
246
+ class="flex-1 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
247
+ />
248
+
249
+ <button
250
+ type="button"
251
+ :aria-label="`Remove action ${i + 1}`"
252
+ class="rounded-lg px-2 text-xs text-[var(--esc-panel-text-muted)] transition-colors hover:text-red-400"
253
+ :disabled="form.actions.length === 1"
254
+ @click="removeAction(i)"
255
+ >
256
+ Remove
257
+ </button>
258
+ </div>
259
+
260
+ <p v-if="form.errors.actions" class="mt-1 text-xs text-red-400">{{ form.errors.actions }}</p>
261
+ </div>
262
+
263
+ <div class="flex items-center gap-3">
264
+ <button
265
+ type="submit"
266
+ :disabled="form.processing"
267
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-4 py-2 text-sm font-medium text-white transition-all hover:from-[var(--esc-panel-accent-hover)] hover:to-[var(--esc-panel-accent-secondary-hover)] disabled:opacity-50"
268
+ >
269
+ {{ automation ? 'Save changes' : 'Create automation' }}
270
+ </button>
271
+ <Link
272
+ :href="route('escalated.admin.automations.index')"
273
+ class="text-sm text-[var(--esc-panel-text-muted)] transition-colors hover:text-[var(--esc-panel-text-secondary)]"
274
+ >
275
+ Cancel
276
+ </Link>
277
+ </div>
278
+ </form>
279
+ </EscalatedLayout>
280
+ </template>
@@ -0,0 +1,230 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { Link, router } from '@inertiajs/vue3';
4
+
5
+ /**
6
+ * Automations are the time-based half of the admin automation surface: a cron
7
+ * scan matches open tickets on how long they have been sitting and applies
8
+ * actions. Workflows are the event-driven half, and Macros are the agent's
9
+ * manual one. The three co-exist; none subsumes the others.
10
+ */
11
+ defineProps({
12
+ automations: { type: Array, default: () => [] },
13
+ });
14
+
15
+ const FIELD_LABELS = {
16
+ hours_since_created: 'Hours since created',
17
+ hours_since_updated: 'Hours since updated',
18
+ hours_since_assigned: 'Hours since assigned',
19
+ status: 'Status',
20
+ priority: 'Priority',
21
+ assigned: 'Assignment',
22
+ ticket_type: 'Type',
23
+ subject_contains: 'Subject contains',
24
+ };
25
+
26
+ const ACTION_LABELS = {
27
+ change_status: 'Change status',
28
+ assign: 'Assign to',
29
+ add_tag: 'Add tag',
30
+ change_priority: 'Change priority',
31
+ add_note: 'Add internal note',
32
+ set_ticket_type: 'Set type',
33
+ };
34
+
35
+ function destroy(id) {
36
+ if (confirm('Delete this automation? This cannot be undone.')) {
37
+ router.delete(route('escalated.admin.automations.destroy', id));
38
+ }
39
+ }
40
+
41
+ function toggleActive(automation) {
42
+ router.put(route('escalated.admin.automations.update', automation.id), {
43
+ name: automation.name,
44
+ conditions: automation.conditions ?? [],
45
+ actions: automation.actions ?? [],
46
+ active: !automation.active,
47
+ });
48
+ }
49
+
50
+ /** A condition in the words an admin would use, not the words it is stored in. */
51
+ function conditionSummary(condition) {
52
+ const label = FIELD_LABELS[condition?.field] ?? condition?.field ?? '';
53
+
54
+ if (!label) {
55
+ return '';
56
+ }
57
+
58
+ if (String(condition.field ?? '').startsWith('hours_since_')) {
59
+ return `${label} ${condition.operator ?? '>'} ${condition.value}`;
60
+ }
61
+
62
+ return `${label} is ${condition.value}`;
63
+ }
64
+
65
+ function actionSummary(action) {
66
+ const label = ACTION_LABELS[action?.type] ?? action?.type ?? '';
67
+
68
+ if (!label) {
69
+ return '';
70
+ }
71
+
72
+ return action.value === undefined || action.value === null || action.value === ''
73
+ ? label
74
+ : `${label}: ${action.value}`;
75
+ }
76
+ </script>
77
+
78
+ <template>
79
+ <EscalatedLayout title="Automations">
80
+ <div class="mb-4 flex items-center justify-between">
81
+ <div>
82
+ <h2 class="text-lg font-semibold text-[var(--esc-panel-text-secondary)]">Automations</h2>
83
+ <p class="text-xs text-[var(--esc-panel-text-muted)]">
84
+ Time-based rules. A scheduled scan matches open tickets on how long they have been waiting and
85
+ applies actions — for events as they happen, use Workflows.
86
+ </p>
87
+ </div>
88
+ <Link
89
+ :href="route('escalated.admin.automations.create')"
90
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-4 py-2 text-sm font-medium text-white shadow-lg shadow-[var(--esc-panel-bg)]/20 transition-all hover:from-[var(--esc-panel-accent-hover)] hover:to-[var(--esc-panel-accent-secondary-hover)]"
91
+ >
92
+ Create Automation
93
+ </Link>
94
+ </div>
95
+
96
+ <div class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)]">
97
+ <table class="min-w-full divide-y divide-[var(--esc-panel-border)]">
98
+ <thead>
99
+ <tr class="bg-[var(--esc-panel-hover)]">
100
+ <th
101
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
102
+ >
103
+ Name
104
+ </th>
105
+ <th
106
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
107
+ >
108
+ When
109
+ </th>
110
+ <th
111
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
112
+ >
113
+ Then
114
+ </th>
115
+ <th
116
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
117
+ >
118
+ Status
119
+ </th>
120
+ <th
121
+ class="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
122
+ >
123
+ Actions
124
+ </th>
125
+ </tr>
126
+ </thead>
127
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
128
+ <tr v-if="!automations?.length">
129
+ <td colspan="5" class="px-4 py-12 text-center">
130
+ <svg
131
+ class="mx-auto mb-3 h-8 w-8 text-[var(--esc-panel-text-muted)]"
132
+ fill="none"
133
+ stroke="currentColor"
134
+ stroke-width="1.5"
135
+ viewBox="0 0 24 24"
136
+ >
137
+ <path
138
+ stroke-linecap="round"
139
+ stroke-linejoin="round"
140
+ d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
141
+ />
142
+ </svg>
143
+ <p class="text-sm text-[var(--esc-panel-text-muted)]">No automations yet</p>
144
+ <p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
145
+ Chase tickets that have gone quiet — escalate after 24 hours, close after a week
146
+ </p>
147
+ </td>
148
+ </tr>
149
+ <tr
150
+ v-for="automation in automations"
151
+ :key="automation.id"
152
+ class="transition-colors hover:bg-[var(--esc-panel-hover)]"
153
+ >
154
+ <td class="px-4 py-3">
155
+ <Link
156
+ :href="route('escalated.admin.automations.edit', automation.id)"
157
+ class="text-sm font-medium text-[var(--esc-panel-text-secondary)] hover:text-[var(--esc-panel-accent)]"
158
+ >
159
+ {{ automation.name }}
160
+ </Link>
161
+ </td>
162
+ <td class="px-4 py-3">
163
+ <div class="flex flex-wrap gap-1">
164
+ <span
165
+ v-for="(condition, i) in automation.conditions ?? []"
166
+ :key="i"
167
+ class="rounded bg-[var(--esc-panel-hover)] px-2 py-0.5 text-[11px] text-[var(--esc-panel-text-muted)]"
168
+ >
169
+ {{ conditionSummary(condition) }}
170
+ </span>
171
+ <span
172
+ v-if="!(automation.conditions ?? []).length"
173
+ class="text-[11px] text-[var(--esc-panel-text-muted)]"
174
+ >
175
+ —
176
+ </span>
177
+ </div>
178
+ </td>
179
+ <td class="px-4 py-3">
180
+ <div class="flex flex-wrap gap-1">
181
+ <span
182
+ v-for="(action, i) in automation.actions ?? []"
183
+ :key="i"
184
+ class="rounded bg-[var(--esc-panel-accent)]/10 px-2 py-0.5 text-[11px] text-[var(--esc-panel-accent)]"
185
+ >
186
+ {{ actionSummary(action) }}
187
+ </span>
188
+ <span
189
+ v-if="!(automation.actions ?? []).length"
190
+ class="text-[11px] text-[var(--esc-panel-text-muted)]"
191
+ >
192
+ —
193
+ </span>
194
+ </div>
195
+ </td>
196
+ <td class="px-4 py-3">
197
+ <button
198
+ type="button"
199
+ class="rounded px-2 py-0.5 text-[11px] font-medium transition-colors"
200
+ :class="
201
+ automation.active
202
+ ? 'bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20'
203
+ : 'bg-gray-500/10 text-gray-400 hover:bg-gray-500/20'
204
+ "
205
+ @click="toggleActive(automation)"
206
+ >
207
+ {{ automation.active ? 'Active' : 'Paused' }}
208
+ </button>
209
+ </td>
210
+ <td class="px-4 py-3 text-right">
211
+ <Link
212
+ :href="route('escalated.admin.automations.edit', automation.id)"
213
+ class="text-xs font-medium text-[var(--esc-panel-text-muted)] transition-colors hover:text-[var(--esc-panel-accent)]"
214
+ >
215
+ Edit
216
+ </Link>
217
+ <button
218
+ type="button"
219
+ class="ml-3 text-xs font-medium text-[var(--esc-panel-text-muted)] transition-colors hover:text-red-400"
220
+ @click="destroy(automation.id)"
221
+ >
222
+ Delete
223
+ </button>
224
+ </td>
225
+ </tr>
226
+ </tbody>
227
+ </table>
228
+ </div>
229
+ </EscalatedLayout>
230
+ </template>