@escalated-dev/escalated 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -2
- package/package.json +31 -2
- package/src/components/ActivityTimeline.stories.js +55 -0
- package/src/components/ActivityTimeline.vue +24 -7
- package/src/components/AdminDashboard.stories.js +1095 -0
- package/src/components/AgentLoadIndicator.stories.js +37 -0
- package/src/components/AgentLoadIndicator.vue +49 -0
- package/src/components/AgentPerformanceCard.vue +44 -0
- package/src/components/ArticleEditor.vue +82 -0
- package/src/components/AssigneeSelect.vue +19 -5
- package/src/components/AttachmentList.vue +53 -37
- package/src/components/AuditLogEntry.vue +95 -0
- package/src/components/BulkActionBar.vue +99 -30
- package/src/components/CategoryTree.vue +89 -0
- package/src/components/ChartWidget.vue +46 -0
- package/src/components/CollisionWarning.stories.js +30 -0
- package/src/components/CollisionWarning.vue +68 -0
- package/src/components/ConditionalFieldRules.vue +135 -0
- package/src/components/ContextPanel.vue +66 -0
- package/src/components/ContextPanelSection.vue +65 -0
- package/src/components/CustomFieldRenderer.vue +159 -0
- package/src/components/EscalatedLayout.vue +400 -69
- package/src/components/FileDropzone.vue +26 -10
- package/src/components/FollowButton.stories.js +30 -0
- package/src/components/FollowButton.vue +53 -28
- package/src/components/KeyboardShortcutHelp.vue +129 -39
- package/src/components/KnowledgePanel.vue +122 -0
- package/src/components/KpiCard.stories.js +32 -0
- package/src/components/KpiCard.vue +88 -0
- package/src/components/MacroDropdown.vue +95 -35
- package/src/components/PermissionMatrix.vue +107 -0
- package/src/components/PinnedNotes.vue +70 -31
- package/src/components/PresenceIndicator.vue +93 -27
- package/src/components/PriorityBadge.stories.js +33 -0
- package/src/components/PriorityBadge.vue +9 -2
- package/src/components/QuickFilters.vue +65 -49
- package/src/components/ReplyComposer.vue +144 -30
- package/src/components/ReplyThread.vue +72 -20
- package/src/components/SatisfactionRating.vue +98 -46
- package/src/components/ScheduleEditor.vue +104 -0
- package/src/components/SideConversation.vue +165 -0
- package/src/components/SideConversationList.vue +147 -0
- package/src/components/SkillTagManager.vue +79 -0
- package/src/components/SlaComplianceChart.vue +53 -0
- package/src/components/SlaTimer.stories.js +35 -0
- package/src/components/SlaTimer.vue +17 -4
- package/src/components/StatsCard.stories.js +24 -0
- package/src/components/StatsCard.vue +58 -46
- package/src/components/StatusBadge.stories.js +53 -0
- package/src/components/StatusBadge.vue +36 -4
- package/src/components/TagSelect.vue +43 -12
- package/src/components/TicketFilters.vue +225 -69
- package/src/components/TicketLinkPanel.vue +193 -0
- package/src/components/TicketList.vue +594 -193
- package/src/components/TicketMergeDialog.vue +157 -0
- package/src/components/TicketSidebar.vue +167 -99
- package/src/components/TicketTypeSelector.vue +21 -0
- package/src/components/TotpInput.stories.js +9 -0
- package/src/components/TotpInput.vue +81 -0
- package/src/components/TwoFactorSetup.vue +113 -0
- package/src/composables/useDebouncedSearch.js +38 -0
- package/src/composables/usePluginExtensions.js +95 -3
- package/src/composables/usePluginHooks.js +65 -0
- package/src/defineEscalatedPlugin.js +34 -8
- package/src/index.js +72 -35
- package/src/locales/en.json +25 -0
- package/src/pages/Admin/ApiTokens/Index.vue +231 -67
- package/src/pages/Admin/AuditLog/Index.vue +154 -0
- package/src/pages/Admin/Automations/Form.vue +184 -0
- package/src/pages/Admin/Automations/Index.vue +129 -0
- package/src/pages/Admin/BusinessHours/Form.vue +172 -0
- package/src/pages/Admin/BusinessHours/Index.vue +113 -0
- package/src/pages/Admin/CannedResponses/Index.vue +127 -70
- package/src/pages/Admin/Capacity/Index.vue +134 -0
- package/src/pages/Admin/CustomFields/Form.vue +195 -0
- package/src/pages/Admin/CustomFields/Index.vue +134 -0
- package/src/pages/Admin/CustomObjects/Form.vue +202 -0
- package/src/pages/Admin/CustomObjects/Index.vue +127 -0
- package/src/pages/Admin/CustomObjects/Records.vue +228 -0
- package/src/pages/Admin/Departments/Form.vue +75 -51
- package/src/pages/Admin/Departments/Index.vue +109 -57
- package/src/pages/Admin/EscalationRules/Form.vue +157 -96
- package/src/pages/Admin/EscalationRules/Index.vue +178 -57
- package/src/pages/Admin/Import/Connect.vue +96 -0
- package/src/pages/Admin/Import/Index.vue +102 -0
- package/src/pages/Admin/Import/Mapping.vue +110 -0
- package/src/pages/Admin/Import/Progress.vue +153 -0
- package/src/pages/Admin/Import/Review.vue +76 -0
- package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
- package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
- package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
- package/src/pages/Admin/Macros/Index.vue +250 -89
- package/src/pages/Admin/Plugins/Index.vue +205 -75
- package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
- package/src/pages/Admin/Reports/CsatReport.vue +127 -0
- package/src/pages/Admin/Reports/Dashboard.vue +211 -0
- package/src/pages/Admin/Reports/SlaReport.vue +166 -0
- package/src/pages/Admin/Reports.vue +139 -34
- package/src/pages/Admin/Roles/Form.vue +71 -0
- package/src/pages/Admin/Roles/Index.vue +122 -0
- package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
- package/src/pages/Admin/Settings/DataRetention.vue +182 -0
- package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
- package/src/pages/Admin/Settings/Sandbox.vue +135 -0
- package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
- package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
- package/src/pages/Admin/Settings.vue +468 -260
- package/src/pages/Admin/Skills/Form.vue +53 -0
- package/src/pages/Admin/Skills/Index.vue +94 -0
- package/src/pages/Admin/SlaPolicies/Form.vue +118 -75
- package/src/pages/Admin/SlaPolicies/Index.vue +120 -59
- package/src/pages/Admin/Statuses/Form.vue +114 -0
- package/src/pages/Admin/Statuses/Index.vue +153 -0
- package/src/pages/Admin/Tags/Index.vue +191 -105
- package/src/pages/Admin/Tickets/Index.vue +91 -39
- package/src/pages/Admin/Tickets/Show.vue +101 -37
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
- package/src/pages/Admin/Webhooks/Form.vue +110 -0
- package/src/pages/Admin/Webhooks/Index.vue +136 -0
- package/src/pages/Agent/Dashboard.vue +311 -186
- package/src/pages/Agent/TicketIndex.vue +88 -38
- package/src/pages/Agent/TicketShow.vue +258 -51
- package/src/pages/Customer/Create.vue +36 -9
- package/src/pages/Customer/Index.vue +31 -24
- package/src/pages/Customer/KnowledgeBase/Article.vue +127 -0
- package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
- package/src/plugin.js +86 -1
- package/src/utils/constants.js +30 -0
- package/src/utils/formatting.js +35 -0
- package/src/pages/Admin/Plugins.vue +0 -129
|
@@ -1,96 +1,157 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
3
|
-
import { useForm } from '@inertiajs/vue3';
|
|
4
|
-
|
|
5
|
-
const props = defineProps({ rule: { type: Object, default: null } });
|
|
6
|
-
|
|
7
|
-
const form = useForm({
|
|
8
|
-
name: props.rule?.name || '',
|
|
9
|
-
description: props.rule?.description || '',
|
|
10
|
-
trigger_type: props.rule?.trigger_type || 'time_based',
|
|
11
|
-
conditions: props.rule?.conditions || [{ field: '', value: '' }],
|
|
12
|
-
actions: props.rule?.actions || [{ type: '', value: '' }],
|
|
13
|
-
order: props.rule?.order ?? 0,
|
|
14
|
-
is_active: props.rule?.is_active ?? true,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
function addCondition() {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
class="rounded-lg
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
</
|
|
1
|
+
<script setup>
|
|
2
|
+
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
3
|
+
import { useForm } from '@inertiajs/vue3';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({ rule: { type: Object, default: null } });
|
|
6
|
+
|
|
7
|
+
const form = useForm({
|
|
8
|
+
name: props.rule?.name || '',
|
|
9
|
+
description: props.rule?.description || '',
|
|
10
|
+
trigger_type: props.rule?.trigger_type || 'time_based',
|
|
11
|
+
conditions: props.rule?.conditions || [{ field: '', value: '' }],
|
|
12
|
+
actions: props.rule?.actions || [{ type: '', value: '' }],
|
|
13
|
+
order: props.rule?.order ?? 0,
|
|
14
|
+
is_active: props.rule?.is_active ?? true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function addCondition() {
|
|
18
|
+
form.conditions.push({ field: '', value: '' });
|
|
19
|
+
}
|
|
20
|
+
function removeCondition(idx) {
|
|
21
|
+
form.conditions.splice(idx, 1);
|
|
22
|
+
}
|
|
23
|
+
function addAction() {
|
|
24
|
+
form.actions.push({ type: '', value: '' });
|
|
25
|
+
}
|
|
26
|
+
function removeAction(idx) {
|
|
27
|
+
form.actions.splice(idx, 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function submit() {
|
|
31
|
+
if (props.rule) {
|
|
32
|
+
form.put(route('escalated.admin.escalation-rules.update', props.rule.id));
|
|
33
|
+
} else {
|
|
34
|
+
form.post(route('escalated.admin.escalation-rules.store'));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<EscalatedLayout :title="rule ? 'Edit Escalation Rule' : 'New Escalation Rule'">
|
|
41
|
+
<form
|
|
42
|
+
class="mx-auto max-w-lg space-y-5 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
|
|
43
|
+
@submit.prevent="submit"
|
|
44
|
+
>
|
|
45
|
+
<div>
|
|
46
|
+
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Name</label>
|
|
47
|
+
<input
|
|
48
|
+
v-model="form.name"
|
|
49
|
+
type="text"
|
|
50
|
+
required
|
|
51
|
+
class="mt-1 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
<div>
|
|
55
|
+
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Trigger Type</label>
|
|
56
|
+
<select
|
|
57
|
+
v-model="form.trigger_type"
|
|
58
|
+
class="mt-1 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
59
|
+
>
|
|
60
|
+
<option value="time_based">Time Based</option>
|
|
61
|
+
<option value="sla_breach">SLA Breach</option>
|
|
62
|
+
<option value="priority_based">Priority Based</option>
|
|
63
|
+
</select>
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<div class="mb-2 flex items-center justify-between">
|
|
67
|
+
<label class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">Conditions</label>
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
class="text-sm text-[var(--esc-panel-text-secondary)] hover:text-[var(--esc-panel-text)]"
|
|
71
|
+
@click="addCondition"
|
|
72
|
+
>
|
|
73
|
+
+ Add
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
<div v-for="(cond, idx) in form.conditions" :key="idx" class="mb-2 flex gap-2">
|
|
77
|
+
<select
|
|
78
|
+
v-model="cond.field"
|
|
79
|
+
class="w-1/2 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-2 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
80
|
+
>
|
|
81
|
+
<option value="status">Status</option>
|
|
82
|
+
<option value="priority">Priority</option>
|
|
83
|
+
<option value="assigned">Assignment</option>
|
|
84
|
+
<option value="age_hours">Age (hours)</option>
|
|
85
|
+
<option value="no_response_hours">No Response (hours)</option>
|
|
86
|
+
<option value="sla_breached">SLA Breached</option>
|
|
87
|
+
</select>
|
|
88
|
+
<input
|
|
89
|
+
v-model="cond.value"
|
|
90
|
+
class="w-1/2 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-2 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] placeholder-gray-500 focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
91
|
+
placeholder="Value"
|
|
92
|
+
/>
|
|
93
|
+
<button type="button" class="text-rose-400 hover:text-rose-300" @click="removeCondition(idx)">
|
|
94
|
+
×
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<div>
|
|
99
|
+
<div class="mb-2 flex items-center justify-between">
|
|
100
|
+
<label class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">Actions</label>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
class="text-sm text-[var(--esc-panel-text-secondary)] hover:text-[var(--esc-panel-text)]"
|
|
104
|
+
@click="addAction"
|
|
105
|
+
>
|
|
106
|
+
+ Add
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
<div v-for="(action, idx) in form.actions" :key="idx" class="mb-2 flex gap-2">
|
|
110
|
+
<select
|
|
111
|
+
v-model="action.type"
|
|
112
|
+
class="w-1/2 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-2 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
113
|
+
>
|
|
114
|
+
<option value="escalate">Escalate</option>
|
|
115
|
+
<option value="change_priority">Change Priority</option>
|
|
116
|
+
<option value="assign_to">Assign To</option>
|
|
117
|
+
<option value="change_department">Change Department</option>
|
|
118
|
+
</select>
|
|
119
|
+
<input
|
|
120
|
+
v-model="action.value"
|
|
121
|
+
class="w-1/2 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-2 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] placeholder-gray-500 focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
122
|
+
placeholder="Value"
|
|
123
|
+
/>
|
|
124
|
+
<button type="button" class="text-rose-400 hover:text-rose-300" @click="removeAction(idx)">
|
|
125
|
+
×
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div>
|
|
130
|
+
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Order</label>
|
|
131
|
+
<input
|
|
132
|
+
v-model.number="form.order"
|
|
133
|
+
type="number"
|
|
134
|
+
min="0"
|
|
135
|
+
class="mt-1 w-24 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
<label class="flex items-center gap-2">
|
|
139
|
+
<input
|
|
140
|
+
v-model="form.is_active"
|
|
141
|
+
type="checkbox"
|
|
142
|
+
class="rounded border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] text-[var(--esc-panel-text)] focus:ring-[var(--esc-panel-border-input)]"
|
|
143
|
+
/>
|
|
144
|
+
<span class="text-sm text-[var(--esc-panel-text-secondary)]">Active</span>
|
|
145
|
+
</label>
|
|
146
|
+
<div class="flex justify-end">
|
|
147
|
+
<button
|
|
148
|
+
type="submit"
|
|
149
|
+
:disabled="form.processing"
|
|
150
|
+
class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-5 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)] disabled:opacity-50"
|
|
151
|
+
>
|
|
152
|
+
{{ rule ? 'Update' : 'Create' }}
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
</form>
|
|
156
|
+
</EscalatedLayout>
|
|
157
|
+
</template>
|
|
@@ -1,57 +1,178 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
3
|
-
import { Link, router } from '@inertiajs/vue3';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
</
|
|
53
|
-
</
|
|
54
|
-
</
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
1
|
+
<script setup>
|
|
2
|
+
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
3
|
+
import { Link, router } from '@inertiajs/vue3';
|
|
4
|
+
import { ref, computed } from 'vue';
|
|
5
|
+
|
|
6
|
+
const props = defineProps({ rules: Array });
|
|
7
|
+
|
|
8
|
+
const selectedCategory = ref('');
|
|
9
|
+
const collapsedCategories = ref({});
|
|
10
|
+
|
|
11
|
+
function destroy(id) {
|
|
12
|
+
if (confirm('Delete this escalation rule?')) {
|
|
13
|
+
router.delete(route('escalated.admin.escalation-rules.destroy', id));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const categories = computed(() => {
|
|
18
|
+
const cats = new Set();
|
|
19
|
+
(props.rules || []).forEach((r) => cats.add(r.category || 'Uncategorized'));
|
|
20
|
+
return Array.from(cats).sort();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const filteredRules = computed(() => {
|
|
24
|
+
if (!selectedCategory.value) return props.rules || [];
|
|
25
|
+
return (props.rules || []).filter((r) => (r.category || 'Uncategorized') === selectedCategory.value);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const groupedRules = computed(() => {
|
|
29
|
+
const groups = {};
|
|
30
|
+
filteredRules.value.forEach((rule) => {
|
|
31
|
+
const cat = rule.category || 'Uncategorized';
|
|
32
|
+
if (!groups[cat]) groups[cat] = [];
|
|
33
|
+
groups[cat].push(rule);
|
|
34
|
+
});
|
|
35
|
+
return groups;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function toggleCategory(cat) {
|
|
39
|
+
collapsedCategories.value[cat] = !collapsedCategories.value[cat];
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<EscalatedLayout title="Escalation Rules">
|
|
45
|
+
<div class="mb-4 flex items-center justify-between">
|
|
46
|
+
<div class="flex items-center gap-3">
|
|
47
|
+
<select
|
|
48
|
+
v-model="selectedCategory"
|
|
49
|
+
class="rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
50
|
+
>
|
|
51
|
+
<option value="">All Categories</option>
|
|
52
|
+
<option v-for="cat in categories" :key="cat" :value="cat">{{ cat }}</option>
|
|
53
|
+
</select>
|
|
54
|
+
</div>
|
|
55
|
+
<Link
|
|
56
|
+
:href="route('escalated.admin.escalation-rules.create')"
|
|
57
|
+
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)]"
|
|
58
|
+
>
|
|
59
|
+
Add Rule
|
|
60
|
+
</Link>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div
|
|
64
|
+
v-if="!rules?.length"
|
|
65
|
+
class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] px-4 py-12 text-center"
|
|
66
|
+
>
|
|
67
|
+
<svg
|
|
68
|
+
class="mx-auto mb-3 h-8 w-8 text-[var(--esc-panel-text-muted)]"
|
|
69
|
+
fill="none"
|
|
70
|
+
stroke="currentColor"
|
|
71
|
+
stroke-width="1.5"
|
|
72
|
+
viewBox="0 0 24 24"
|
|
73
|
+
>
|
|
74
|
+
<path
|
|
75
|
+
stroke-linecap="round"
|
|
76
|
+
stroke-linejoin="round"
|
|
77
|
+
d="M3 4.5h14.25M3 9h9.75M3 13.5h5.25m5.25-.75L17.25 9m0 0L21 12.75M17.25 9v12"
|
|
78
|
+
/>
|
|
79
|
+
</svg>
|
|
80
|
+
<p class="text-sm text-[var(--esc-panel-text-muted)]">No escalation rules yet</p>
|
|
81
|
+
<p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
|
|
82
|
+
Set up automatic escalation for overdue tickets
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Grouped by category -->
|
|
87
|
+
<div v-for="(catRules, category) in groupedRules" :key="category" class="mb-4">
|
|
88
|
+
<button
|
|
89
|
+
class="mb-2 flex w-full items-center gap-2 text-sm font-semibold text-[var(--esc-panel-text-secondary)] hover:text-[var(--esc-panel-text)]"
|
|
90
|
+
@click="toggleCategory(category)"
|
|
91
|
+
>
|
|
92
|
+
<svg
|
|
93
|
+
:class="[
|
|
94
|
+
'h-4 w-4 text-[var(--esc-panel-text-muted)] transition-transform',
|
|
95
|
+
!collapsedCategories[category] && 'rotate-90',
|
|
96
|
+
]"
|
|
97
|
+
fill="none"
|
|
98
|
+
stroke="currentColor"
|
|
99
|
+
stroke-width="1.5"
|
|
100
|
+
viewBox="0 0 24 24"
|
|
101
|
+
>
|
|
102
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
|
103
|
+
</svg>
|
|
104
|
+
{{ category }}
|
|
105
|
+
<span class="text-xs text-[var(--esc-panel-text-muted)]">({{ catRules.length }})</span>
|
|
106
|
+
</button>
|
|
107
|
+
|
|
108
|
+
<div
|
|
109
|
+
v-if="!collapsedCategories[category]"
|
|
110
|
+
class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)]"
|
|
111
|
+
>
|
|
112
|
+
<table class="min-w-full divide-y divide-[var(--esc-panel-border)]">
|
|
113
|
+
<thead>
|
|
114
|
+
<tr class="bg-[var(--esc-panel-hover)]">
|
|
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
|
+
Order
|
|
119
|
+
</th>
|
|
120
|
+
<th
|
|
121
|
+
class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
122
|
+
>
|
|
123
|
+
Name
|
|
124
|
+
</th>
|
|
125
|
+
<th
|
|
126
|
+
class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
127
|
+
>
|
|
128
|
+
Trigger
|
|
129
|
+
</th>
|
|
130
|
+
<th
|
|
131
|
+
class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
132
|
+
>
|
|
133
|
+
Active
|
|
134
|
+
</th>
|
|
135
|
+
<th
|
|
136
|
+
class="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
137
|
+
>
|
|
138
|
+
Actions
|
|
139
|
+
</th>
|
|
140
|
+
</tr>
|
|
141
|
+
</thead>
|
|
142
|
+
<tbody class="divide-y divide-[var(--esc-panel-border)]">
|
|
143
|
+
<tr
|
|
144
|
+
v-for="rule in catRules"
|
|
145
|
+
:key="rule.id"
|
|
146
|
+
class="transition-colors hover:bg-[var(--esc-panel-hover)]"
|
|
147
|
+
>
|
|
148
|
+
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">{{ rule.order }}</td>
|
|
149
|
+
<td class="px-4 py-3 text-sm font-medium text-[var(--esc-panel-text-secondary)]">
|
|
150
|
+
{{ rule.name }}
|
|
151
|
+
</td>
|
|
152
|
+
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
153
|
+
{{ rule.trigger_type }}
|
|
154
|
+
</td>
|
|
155
|
+
<td class="px-4 py-3 text-sm">
|
|
156
|
+
<span
|
|
157
|
+
:class="rule.is_active ? 'text-emerald-400' : 'text-[var(--esc-panel-text-muted)]'"
|
|
158
|
+
>{{ rule.is_active ? 'Yes' : 'No' }}</span
|
|
159
|
+
>
|
|
160
|
+
</td>
|
|
161
|
+
<td class="px-4 py-3 text-right text-sm">
|
|
162
|
+
<Link
|
|
163
|
+
:href="route('escalated.admin.escalation-rules.edit', rule.id)"
|
|
164
|
+
class="text-[var(--esc-panel-text-secondary)] hover:text-[var(--esc-panel-text)]"
|
|
165
|
+
>
|
|
166
|
+
Edit
|
|
167
|
+
</Link>
|
|
168
|
+
<button class="ml-3 text-rose-400 hover:text-rose-300" @click="destroy(rule.id)">
|
|
169
|
+
Delete
|
|
170
|
+
</button>
|
|
171
|
+
</td>
|
|
172
|
+
</tr>
|
|
173
|
+
</tbody>
|
|
174
|
+
</table>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</EscalatedLayout>
|
|
178
|
+
</template>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useForm } from '@inertiajs/vue3';
|
|
3
|
+
import { router } from '@inertiajs/vue3';
|
|
4
|
+
import { ref } from 'vue';
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
platform: String,
|
|
8
|
+
display_name: String,
|
|
9
|
+
credential_fields: { type: Array, default: () => [] },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const credentials = ref({});
|
|
13
|
+
const testing = ref(false);
|
|
14
|
+
const testResult = ref(null);
|
|
15
|
+
|
|
16
|
+
props.credential_fields.forEach((f) => {
|
|
17
|
+
credentials.value[f.name] = '';
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
async function testConnection() {
|
|
21
|
+
testing.value = true;
|
|
22
|
+
testResult.value = null;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(route('escalated.admin.import.test-connection', props.platform), {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content,
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ credentials: credentials.value }),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
testResult.value = data.success ? 'success' : data.error || 'Connection failed';
|
|
36
|
+
} catch (e) {
|
|
37
|
+
testResult.value = 'Connection failed: ' + e.message;
|
|
38
|
+
} finally {
|
|
39
|
+
testing.value = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function proceed() {
|
|
44
|
+
// Credentials already stored server-side after successful test connection
|
|
45
|
+
router.get(route('escalated.admin.import.mapping', props.platform));
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<div class="mx-auto max-w-lg space-y-6">
|
|
51
|
+
<div>
|
|
52
|
+
<h2 class="text-lg font-semibold text-[var(--esc-panel-text)]">Connect to {{ display_name }}</h2>
|
|
53
|
+
<p class="mt-1 text-sm text-[var(--esc-panel-text-secondary)]">
|
|
54
|
+
Enter your {{ display_name }} credentials to connect.
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="space-y-4 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
|
|
59
|
+
<div v-for="field in credential_fields" :key="field.name">
|
|
60
|
+
<label class="mb-1 block text-sm font-medium text-[var(--esc-panel-text)]">{{ field.label }}</label>
|
|
61
|
+
<input
|
|
62
|
+
v-model="credentials[field.name]"
|
|
63
|
+
:type="field.type || 'text'"
|
|
64
|
+
:placeholder="field.help || ''"
|
|
65
|
+
class="w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
66
|
+
/>
|
|
67
|
+
<p v-if="field.help" class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">{{ field.help }}</p>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Test connection -->
|
|
72
|
+
<div class="flex items-center gap-4">
|
|
73
|
+
<button
|
|
74
|
+
:disabled="testing"
|
|
75
|
+
class="rounded-lg border border-[var(--esc-panel-border)] px-4 py-2 text-sm font-medium text-[var(--esc-panel-text)] hover:bg-[var(--esc-panel-hover)]"
|
|
76
|
+
@click="testConnection"
|
|
77
|
+
>
|
|
78
|
+
{{ testing ? 'Testing...' : 'Test Connection' }}
|
|
79
|
+
</button>
|
|
80
|
+
|
|
81
|
+
<span v-if="testResult === 'success'" class="text-sm text-green-400">Connected successfully</span>
|
|
82
|
+
<span v-else-if="testResult" class="text-sm text-red-400">{{ testResult }}</span>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<!-- Proceed -->
|
|
86
|
+
<div class="flex justify-end">
|
|
87
|
+
<button
|
|
88
|
+
:disabled="testResult !== 'success'"
|
|
89
|
+
class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-6 py-2 text-sm font-medium text-white disabled:opacity-50"
|
|
90
|
+
@click="proceed"
|
|
91
|
+
>
|
|
92
|
+
Next: Configure Mappings
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|