@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.
- package/LICENSE +21 -21
- package/README.md +388 -361
- package/package.json +85 -84
- package/src/components/ActiveChatsPanel.vue +246 -246
- package/src/components/AdminDashboard.stories.js +1423 -1423
- package/src/components/AttachmentList.vue +65 -65
- package/src/components/ChatActionBar.vue +226 -226
- package/src/components/ChatBubble.vue +162 -162
- package/src/components/ChatComposer.vue +308 -308
- package/src/components/ChatQueue.vue +222 -222
- package/src/components/ChatThread.vue +197 -197
- package/src/components/CustomTicketActions.stories.js +71 -0
- package/src/components/CustomTicketActions.vue +101 -0
- package/src/components/EscalatedLayout.vue +628 -604
- package/src/components/FollowButton.vue +20 -1
- package/src/components/MentionDropdown.vue +154 -154
- package/src/components/MetricComparison.vue +89 -89
- package/src/components/PercentileChart.vue +67 -67
- package/src/components/PeriodSelector.vue +98 -98
- package/src/components/PresenceIndicator.vue +228 -213
- package/src/components/ReplyComposer.vue +336 -336
- package/src/components/ReplyThread.vue +153 -153
- package/src/components/ReportChart.vue +395 -395
- package/src/components/ReportExportButton.vue +108 -108
- package/src/components/SaveViewDialog.vue +172 -172
- package/src/components/SavedViewSidebar.vue +208 -208
- package/src/components/SkillTagManager.vue +3 -0
- package/src/components/SnoozeButton.vue +247 -247
- package/src/components/TicketFilters.vue +270 -270
- package/src/components/TicketList.vue +655 -655
- package/src/components/TicketSidebar.vue +4 -0
- package/src/components/TicketSplitDialog.vue +98 -98
- package/src/components/TicketSubjects.stories.js +63 -0
- package/src/components/TicketSubjects.vue +107 -0
- package/src/components/TwoFactorSetup.vue +1 -37
- package/src/components/VariableTokenInput.vue +111 -111
- package/src/components/WorkflowActionConfig.vue +287 -287
- package/src/components/WorkflowActionList.vue +350 -350
- package/src/components/WorkflowConditionBuilder.vue +265 -265
- package/src/components/WorkflowTriggerSelector.vue +125 -125
- package/src/components/admin/newsletters/AnalyticsTiles.stories.js +14 -0
- package/src/components/admin/newsletters/AnalyticsTiles.vue +55 -0
- package/src/components/admin/newsletters/DeliveriesTable.stories.js +28 -0
- package/src/components/admin/newsletters/DeliveriesTable.vue +104 -0
- package/src/components/admin/newsletters/DynamicFilterBuilder.stories.js +11 -0
- package/src/components/admin/newsletters/DynamicFilterBuilder.vue +53 -0
- package/src/components/admin/newsletters/ListMemberTable.stories.js +12 -0
- package/src/components/admin/newsletters/ListMemberTable.vue +53 -0
- package/src/components/admin/newsletters/MarkdownEditor.stories.js +6 -0
- package/src/components/admin/newsletters/MarkdownEditor.vue +53 -0
- package/src/components/admin/newsletters/MergeFieldDropdown.stories.js +6 -0
- package/src/components/admin/newsletters/MergeFieldDropdown.vue +84 -0
- package/src/components/admin/newsletters/PreviewIframe.stories.js +7 -0
- package/src/components/admin/newsletters/PreviewIframe.vue +40 -0
- package/src/components/admin/newsletters/SummaryTiles.stories.js +8 -0
- package/src/components/admin/newsletters/SummaryTiles.vue +63 -0
- package/src/composables/useChat.js +187 -187
- package/src/composables/useI18n.js +102 -102
- package/src/composables/useMentions.js +134 -134
- package/src/composables/usePermissions.js +31 -0
- package/src/composables/useRealtime.js +182 -182
- package/src/index.js +85 -84
- package/src/locales/README.md +13 -13
- package/src/locales/ar.json +727 -727
- package/src/locales/de.json +690 -690
- package/src/locales/en.json +889 -729
- package/src/locales/es.json +691 -691
- package/src/locales/fr.json +691 -691
- package/src/locales/index.js +16 -16
- package/src/locales/it.json +727 -727
- package/src/locales/ja.json +727 -727
- package/src/locales/ko.json +727 -727
- package/src/locales/nl.json +727 -727
- package/src/locales/pl.json +727 -727
- package/src/locales/pt-BR.json +727 -727
- package/src/locales/ru.json +727 -727
- package/src/locales/tr.json +727 -727
- package/src/locales/zh-CN.json +727 -727
- package/src/pages/Admin/Automations/Form.vue +280 -0
- package/src/pages/Admin/Automations/Index.vue +230 -0
- package/src/pages/Admin/Newsletters/Compose.vue +192 -0
- package/src/pages/Admin/Newsletters/Edit.vue +24 -0
- package/src/pages/Admin/Newsletters/Index.vue +141 -0
- package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
- package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
- package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
- package/src/pages/Admin/Newsletters/Settings.vue +85 -0
- package/src/pages/Admin/Newsletters/Show.vue +140 -0
- package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
- package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
- package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
- package/src/pages/Admin/Newsletters/usePreview.js +44 -0
- package/src/pages/Admin/Plugins/Index.vue +10 -2
- package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
- package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
- package/src/pages/Admin/Reports/Cohorts.vue +180 -180
- package/src/pages/Admin/Reports/Comparison.vue +141 -141
- package/src/pages/Admin/Reports/Dashboard.vue +226 -226
- package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
- package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
- package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
- package/src/pages/Admin/Settings/DatabaseConnection.vue +228 -0
- package/src/pages/Admin/Settings/PublicTickets.vue +163 -0
- package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
- package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
- package/src/pages/Admin/Settings.vue +1008 -1008
- package/src/pages/Admin/Skills/Form.vue +30 -10
- package/src/pages/Admin/Tickets/Index.vue +126 -126
- package/src/pages/Admin/Tickets/Show.vue +254 -249
- package/src/pages/Admin/Users/Index.vue +145 -145
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +1 -2
- package/src/pages/Admin/Workflows/Builder.vue +207 -207
- package/src/pages/Admin/Workflows/Form.vue +24 -0
- package/src/pages/Admin/Workflows/Index.vue +235 -235
- package/src/pages/Admin/Workflows/Logs.vue +286 -286
- package/src/pages/Agent/TicketIndex.vue +112 -112
- package/src/pages/Agent/TicketShow.vue +547 -494
- package/src/pages/Customer/Index.vue +56 -56
- package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
- package/src/pages/Plugin/Page.vue +61 -0
- package/src/plugin.js +175 -175
- package/src/utils/mentionHighlight.js +24 -24
- package/src/widget/EscalatedWidget.vue +804 -804
- 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
|
+
← 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>
|