@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
|
@@ -1,246 +1,246 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
3
|
-
import PeriodSelector from '../../../components/PeriodSelector.vue';
|
|
4
|
-
import ReportExportButton from '../../../components/ReportExportButton.vue';
|
|
5
|
-
import { router, Link } from '@inertiajs/vue3';
|
|
6
|
-
import { ref, computed } from 'vue';
|
|
7
|
-
|
|
8
|
-
const props = defineProps({
|
|
9
|
-
period_days: { type: Number, default: 30 },
|
|
10
|
-
agents: { type: Array, default: () => [] },
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
const periodDays = ref(props.period_days);
|
|
14
|
-
const sortColumn = ref('composite_score');
|
|
15
|
-
const sortDir = ref('desc');
|
|
16
|
-
|
|
17
|
-
function changePeriod(days) {
|
|
18
|
-
periodDays.value = days;
|
|
19
|
-
router.get(route('escalated.admin.reports.agent-ranking'), { days }, { preserveState: true });
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const sortedAgents = computed(() => {
|
|
23
|
-
const list = [...(props.agents || [])];
|
|
24
|
-
list.sort((a, b) => {
|
|
25
|
-
const aVal = a[sortColumn.value] ?? 0;
|
|
26
|
-
const bVal = b[sortColumn.value] ?? 0;
|
|
27
|
-
return sortDir.value === 'asc' ? aVal - bVal : bVal - aVal;
|
|
28
|
-
});
|
|
29
|
-
return list;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const topPerformers = computed(() => sortedAgents.value.slice(0, 3));
|
|
33
|
-
const needsAttention = computed(() => {
|
|
34
|
-
return [...(props.agents || [])].sort((a, b) => (a.composite_score ?? 0) - (b.composite_score ?? 0)).slice(0, 3);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
function toggleSort(col) {
|
|
38
|
-
if (sortColumn.value === col) {
|
|
39
|
-
sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc';
|
|
40
|
-
} else {
|
|
41
|
-
sortColumn.value = col;
|
|
42
|
-
sortDir.value = 'desc';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function scoreColor(score) {
|
|
47
|
-
if (score >= 80) return 'text-emerald-400';
|
|
48
|
-
if (score >= 60) return 'text-amber-400';
|
|
49
|
-
return 'text-red-400';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function renderSparkline(points) {
|
|
53
|
-
if (!points?.length) return '';
|
|
54
|
-
const max = Math.max(...points, 1);
|
|
55
|
-
const w = 80;
|
|
56
|
-
const h = 24;
|
|
57
|
-
const step = w / Math.max(points.length - 1, 1);
|
|
58
|
-
return points
|
|
59
|
-
.map((v, i) => {
|
|
60
|
-
const x = i * step;
|
|
61
|
-
const y = h - (v / max) * h;
|
|
62
|
-
return `${i === 0 ? 'M' : 'L'}${x},${y}`;
|
|
63
|
-
})
|
|
64
|
-
.join(' ');
|
|
65
|
-
}
|
|
66
|
-
</script>
|
|
67
|
-
|
|
68
|
-
<template>
|
|
69
|
-
<EscalatedLayout title="Agent Performance Rankings">
|
|
70
|
-
<div class="mb-6 flex items-center justify-between">
|
|
71
|
-
<PeriodSelector :model-value="periodDays" @update:model-value="changePeriod" />
|
|
72
|
-
<div class="flex items-center gap-3">
|
|
73
|
-
<ReportExportButton report-name="agent-ranking" :period-days="periodDays" />
|
|
74
|
-
<Link
|
|
75
|
-
:href="route('escalated.admin.reports')"
|
|
76
|
-
class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
77
|
-
>
|
|
78
|
-
← Back
|
|
79
|
-
</Link>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
|
-
<!-- Top / Needs Attention -->
|
|
84
|
-
<div v-if="agents?.length" class="mb-6 grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
85
|
-
<div class="rounded-xl border border-emerald-500/20 bg-emerald-500/5 p-5">
|
|
86
|
-
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-emerald-400">
|
|
87
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
88
|
-
<path
|
|
89
|
-
stroke-linecap="round"
|
|
90
|
-
stroke-linejoin="round"
|
|
91
|
-
d="M16.5 18.75h-9m9 0a3 3 0 013 3h-15a3 3 0 013-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 01-.982-3.172M9.497 14.25a7.454 7.454 0 00.981-3.172M5.25 4.236c-.996.178-1.768.654-2.127 1.38a8.69 8.69 0 00-.808 3.06c0 2.625 1.418 4.9 3.524 6.13M18.75 4.236c.996.178 1.768.654 2.127 1.38a8.69 8.69 0 01.808 3.06c0 2.625-1.418 4.9-3.524 6.13"
|
|
92
|
-
/>
|
|
93
|
-
</svg>
|
|
94
|
-
Top Performers
|
|
95
|
-
</h3>
|
|
96
|
-
<div class="space-y-2">
|
|
97
|
-
<div
|
|
98
|
-
v-for="(agent, idx) in topPerformers"
|
|
99
|
-
:key="agent.agent_id"
|
|
100
|
-
class="flex items-center justify-between rounded-lg bg-emerald-500/5 px-3 py-2"
|
|
101
|
-
>
|
|
102
|
-
<div class="flex items-center gap-3">
|
|
103
|
-
<span
|
|
104
|
-
class="flex h-6 w-6 items-center justify-center rounded-full bg-emerald-500/20 text-xs font-bold text-emerald-400"
|
|
105
|
-
>
|
|
106
|
-
{{ idx + 1 }}
|
|
107
|
-
</span>
|
|
108
|
-
<span class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">{{
|
|
109
|
-
agent.agent_name
|
|
110
|
-
}}</span>
|
|
111
|
-
</div>
|
|
112
|
-
<span class="text-sm font-bold text-emerald-400">{{ agent.composite_score }}</span>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
|
|
117
|
-
<div class="rounded-xl border border-red-500/20 bg-red-500/5 p-5">
|
|
118
|
-
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-red-400">
|
|
119
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
120
|
-
<path
|
|
121
|
-
stroke-linecap="round"
|
|
122
|
-
stroke-linejoin="round"
|
|
123
|
-
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
|
|
124
|
-
/>
|
|
125
|
-
</svg>
|
|
126
|
-
Needs Attention
|
|
127
|
-
</h3>
|
|
128
|
-
<div class="space-y-2">
|
|
129
|
-
<div
|
|
130
|
-
v-for="agent in needsAttention"
|
|
131
|
-
:key="agent.agent_id"
|
|
132
|
-
class="flex items-center justify-between rounded-lg bg-red-500/5 px-3 py-2"
|
|
133
|
-
>
|
|
134
|
-
<span class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">{{
|
|
135
|
-
agent.agent_name
|
|
136
|
-
}}</span>
|
|
137
|
-
<span class="text-sm font-bold text-red-400">{{ agent.composite_score }}</span>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
<!-- Ranked Table -->
|
|
144
|
-
<div class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)]">
|
|
145
|
-
<table class="w-full">
|
|
146
|
-
<thead>
|
|
147
|
-
<tr class="border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)]">
|
|
148
|
-
<th
|
|
149
|
-
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
150
|
-
>
|
|
151
|
-
#
|
|
152
|
-
</th>
|
|
153
|
-
<th
|
|
154
|
-
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
155
|
-
>
|
|
156
|
-
Agent
|
|
157
|
-
</th>
|
|
158
|
-
<th
|
|
159
|
-
v-for="col in [
|
|
160
|
-
{ key: 'composite_score', label: 'Score' },
|
|
161
|
-
{ key: 'resolution_rate', label: 'Resolution %' },
|
|
162
|
-
{ key: 'avg_frt', label: 'Avg FRT' },
|
|
163
|
-
{ key: 'avg_resolution', label: 'Avg Res' },
|
|
164
|
-
{ key: 'csat', label: 'CSAT' },
|
|
165
|
-
{ key: 'volume', label: 'Volume' },
|
|
166
|
-
]"
|
|
167
|
-
:key="col.key"
|
|
168
|
-
class="cursor-pointer px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
169
|
-
@click="toggleSort(col.key)"
|
|
170
|
-
>
|
|
171
|
-
{{ col.label }}
|
|
172
|
-
<span v-if="sortColumn === col.key">{{ sortDir === 'asc' ? ' ▲' : ' ▼' }}</span>
|
|
173
|
-
</th>
|
|
174
|
-
<th
|
|
175
|
-
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
176
|
-
>
|
|
177
|
-
Trend
|
|
178
|
-
</th>
|
|
179
|
-
</tr>
|
|
180
|
-
</thead>
|
|
181
|
-
<tbody class="divide-y divide-[var(--esc-panel-border)]">
|
|
182
|
-
<tr
|
|
183
|
-
v-for="(agent, idx) in sortedAgents"
|
|
184
|
-
:key="agent.agent_id"
|
|
185
|
-
class="cursor-pointer hover:bg-[var(--esc-panel-hover)]"
|
|
186
|
-
@click="
|
|
187
|
-
router.visit(
|
|
188
|
-
route('escalated.admin.reports.agent-detail', { id: agent.agent_id, days: periodDays }),
|
|
189
|
-
)
|
|
190
|
-
"
|
|
191
|
-
>
|
|
192
|
-
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-muted)]">{{ idx + 1 }}</td>
|
|
193
|
-
<td class="px-4 py-3">
|
|
194
|
-
<div class="flex items-center gap-3">
|
|
195
|
-
<div
|
|
196
|
-
class="flex h-8 w-8 items-center justify-center rounded-md bg-[var(--esc-panel-hover)] text-xs font-semibold text-[var(--esc-panel-text-tertiary)]"
|
|
197
|
-
>
|
|
198
|
-
{{ agent.agent_name?.charAt(0)?.toUpperCase() || '?' }}
|
|
199
|
-
</div>
|
|
200
|
-
<span class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">{{
|
|
201
|
-
agent.agent_name
|
|
202
|
-
}}</span>
|
|
203
|
-
</div>
|
|
204
|
-
</td>
|
|
205
|
-
<td class="px-4 py-3 text-sm font-bold" :class="scoreColor(agent.composite_score)">
|
|
206
|
-
{{ agent.composite_score ?? '—' }}
|
|
207
|
-
</td>
|
|
208
|
-
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
209
|
-
{{ agent.resolution_rate ?? '—' }}%
|
|
210
|
-
</td>
|
|
211
|
-
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
212
|
-
{{ agent.avg_frt ?? '—' }}h
|
|
213
|
-
</td>
|
|
214
|
-
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
215
|
-
{{ agent.avg_resolution ?? '—' }}h
|
|
216
|
-
</td>
|
|
217
|
-
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
218
|
-
{{ agent.csat ?? '—' }}
|
|
219
|
-
</td>
|
|
220
|
-
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
221
|
-
{{ agent.volume ?? 0 }}
|
|
222
|
-
</td>
|
|
223
|
-
<td class="px-4 py-3">
|
|
224
|
-
<svg v-if="agent.sparkline?.length" width="80" height="24" class="overflow-visible">
|
|
225
|
-
<path
|
|
226
|
-
:d="renderSparkline(agent.sparkline)"
|
|
227
|
-
fill="none"
|
|
228
|
-
stroke="#06b6d4"
|
|
229
|
-
stroke-width="1.5"
|
|
230
|
-
stroke-linecap="round"
|
|
231
|
-
stroke-linejoin="round"
|
|
232
|
-
/>
|
|
233
|
-
</svg>
|
|
234
|
-
<span v-else class="text-xs text-[var(--esc-panel-text-muted)]">--</span>
|
|
235
|
-
</td>
|
|
236
|
-
</tr>
|
|
237
|
-
<tr v-if="!sortedAgents.length">
|
|
238
|
-
<td colspan="9" class="px-4 py-8 text-center text-sm text-[var(--esc-panel-text-muted)]">
|
|
239
|
-
No agent data for this period.
|
|
240
|
-
</td>
|
|
241
|
-
</tr>
|
|
242
|
-
</tbody>
|
|
243
|
-
</table>
|
|
244
|
-
</div>
|
|
245
|
-
</EscalatedLayout>
|
|
246
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
3
|
+
import PeriodSelector from '../../../components/PeriodSelector.vue';
|
|
4
|
+
import ReportExportButton from '../../../components/ReportExportButton.vue';
|
|
5
|
+
import { router, Link } from '@inertiajs/vue3';
|
|
6
|
+
import { ref, computed } from 'vue';
|
|
7
|
+
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
period_days: { type: Number, default: 30 },
|
|
10
|
+
agents: { type: Array, default: () => [] },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const periodDays = ref(props.period_days);
|
|
14
|
+
const sortColumn = ref('composite_score');
|
|
15
|
+
const sortDir = ref('desc');
|
|
16
|
+
|
|
17
|
+
function changePeriod(days) {
|
|
18
|
+
periodDays.value = days;
|
|
19
|
+
router.get(route('escalated.admin.reports.agent-ranking'), { days }, { preserveState: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sortedAgents = computed(() => {
|
|
23
|
+
const list = [...(props.agents || [])];
|
|
24
|
+
list.sort((a, b) => {
|
|
25
|
+
const aVal = a[sortColumn.value] ?? 0;
|
|
26
|
+
const bVal = b[sortColumn.value] ?? 0;
|
|
27
|
+
return sortDir.value === 'asc' ? aVal - bVal : bVal - aVal;
|
|
28
|
+
});
|
|
29
|
+
return list;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const topPerformers = computed(() => sortedAgents.value.slice(0, 3));
|
|
33
|
+
const needsAttention = computed(() => {
|
|
34
|
+
return [...(props.agents || [])].sort((a, b) => (a.composite_score ?? 0) - (b.composite_score ?? 0)).slice(0, 3);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function toggleSort(col) {
|
|
38
|
+
if (sortColumn.value === col) {
|
|
39
|
+
sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc';
|
|
40
|
+
} else {
|
|
41
|
+
sortColumn.value = col;
|
|
42
|
+
sortDir.value = 'desc';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function scoreColor(score) {
|
|
47
|
+
if (score >= 80) return 'text-emerald-400';
|
|
48
|
+
if (score >= 60) return 'text-amber-400';
|
|
49
|
+
return 'text-red-400';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderSparkline(points) {
|
|
53
|
+
if (!points?.length) return '';
|
|
54
|
+
const max = Math.max(...points, 1);
|
|
55
|
+
const w = 80;
|
|
56
|
+
const h = 24;
|
|
57
|
+
const step = w / Math.max(points.length - 1, 1);
|
|
58
|
+
return points
|
|
59
|
+
.map((v, i) => {
|
|
60
|
+
const x = i * step;
|
|
61
|
+
const y = h - (v / max) * h;
|
|
62
|
+
return `${i === 0 ? 'M' : 'L'}${x},${y}`;
|
|
63
|
+
})
|
|
64
|
+
.join(' ');
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<EscalatedLayout title="Agent Performance Rankings">
|
|
70
|
+
<div class="mb-6 flex items-center justify-between">
|
|
71
|
+
<PeriodSelector :model-value="periodDays" @update:model-value="changePeriod" />
|
|
72
|
+
<div class="flex items-center gap-3">
|
|
73
|
+
<ReportExportButton report-name="agent-ranking" :period-days="periodDays" />
|
|
74
|
+
<Link
|
|
75
|
+
:href="route('escalated.admin.reports')"
|
|
76
|
+
class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
77
|
+
>
|
|
78
|
+
← Back
|
|
79
|
+
</Link>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Top / Needs Attention -->
|
|
84
|
+
<div v-if="agents?.length" class="mb-6 grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
85
|
+
<div class="rounded-xl border border-emerald-500/20 bg-emerald-500/5 p-5">
|
|
86
|
+
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-emerald-400">
|
|
87
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
88
|
+
<path
|
|
89
|
+
stroke-linecap="round"
|
|
90
|
+
stroke-linejoin="round"
|
|
91
|
+
d="M16.5 18.75h-9m9 0a3 3 0 013 3h-15a3 3 0 013-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 01-.982-3.172M9.497 14.25a7.454 7.454 0 00.981-3.172M5.25 4.236c-.996.178-1.768.654-2.127 1.38a8.69 8.69 0 00-.808 3.06c0 2.625 1.418 4.9 3.524 6.13M18.75 4.236c.996.178 1.768.654 2.127 1.38a8.69 8.69 0 01.808 3.06c0 2.625-1.418 4.9-3.524 6.13"
|
|
92
|
+
/>
|
|
93
|
+
</svg>
|
|
94
|
+
Top Performers
|
|
95
|
+
</h3>
|
|
96
|
+
<div class="space-y-2">
|
|
97
|
+
<div
|
|
98
|
+
v-for="(agent, idx) in topPerformers"
|
|
99
|
+
:key="agent.agent_id"
|
|
100
|
+
class="flex items-center justify-between rounded-lg bg-emerald-500/5 px-3 py-2"
|
|
101
|
+
>
|
|
102
|
+
<div class="flex items-center gap-3">
|
|
103
|
+
<span
|
|
104
|
+
class="flex h-6 w-6 items-center justify-center rounded-full bg-emerald-500/20 text-xs font-bold text-emerald-400"
|
|
105
|
+
>
|
|
106
|
+
{{ idx + 1 }}
|
|
107
|
+
</span>
|
|
108
|
+
<span class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">{{
|
|
109
|
+
agent.agent_name
|
|
110
|
+
}}</span>
|
|
111
|
+
</div>
|
|
112
|
+
<span class="text-sm font-bold text-emerald-400">{{ agent.composite_score }}</span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="rounded-xl border border-red-500/20 bg-red-500/5 p-5">
|
|
118
|
+
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-red-400">
|
|
119
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
120
|
+
<path
|
|
121
|
+
stroke-linecap="round"
|
|
122
|
+
stroke-linejoin="round"
|
|
123
|
+
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
|
|
124
|
+
/>
|
|
125
|
+
</svg>
|
|
126
|
+
Needs Attention
|
|
127
|
+
</h3>
|
|
128
|
+
<div class="space-y-2">
|
|
129
|
+
<div
|
|
130
|
+
v-for="agent in needsAttention"
|
|
131
|
+
:key="agent.agent_id"
|
|
132
|
+
class="flex items-center justify-between rounded-lg bg-red-500/5 px-3 py-2"
|
|
133
|
+
>
|
|
134
|
+
<span class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">{{
|
|
135
|
+
agent.agent_name
|
|
136
|
+
}}</span>
|
|
137
|
+
<span class="text-sm font-bold text-red-400">{{ agent.composite_score }}</span>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- Ranked Table -->
|
|
144
|
+
<div class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)]">
|
|
145
|
+
<table class="w-full">
|
|
146
|
+
<thead>
|
|
147
|
+
<tr class="border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)]">
|
|
148
|
+
<th
|
|
149
|
+
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
150
|
+
>
|
|
151
|
+
#
|
|
152
|
+
</th>
|
|
153
|
+
<th
|
|
154
|
+
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
155
|
+
>
|
|
156
|
+
Agent
|
|
157
|
+
</th>
|
|
158
|
+
<th
|
|
159
|
+
v-for="col in [
|
|
160
|
+
{ key: 'composite_score', label: 'Score' },
|
|
161
|
+
{ key: 'resolution_rate', label: 'Resolution %' },
|
|
162
|
+
{ key: 'avg_frt', label: 'Avg FRT' },
|
|
163
|
+
{ key: 'avg_resolution', label: 'Avg Res' },
|
|
164
|
+
{ key: 'csat', label: 'CSAT' },
|
|
165
|
+
{ key: 'volume', label: 'Volume' },
|
|
166
|
+
]"
|
|
167
|
+
:key="col.key"
|
|
168
|
+
class="cursor-pointer px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
169
|
+
@click="toggleSort(col.key)"
|
|
170
|
+
>
|
|
171
|
+
{{ col.label }}
|
|
172
|
+
<span v-if="sortColumn === col.key">{{ sortDir === 'asc' ? ' ▲' : ' ▼' }}</span>
|
|
173
|
+
</th>
|
|
174
|
+
<th
|
|
175
|
+
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
176
|
+
>
|
|
177
|
+
Trend
|
|
178
|
+
</th>
|
|
179
|
+
</tr>
|
|
180
|
+
</thead>
|
|
181
|
+
<tbody class="divide-y divide-[var(--esc-panel-border)]">
|
|
182
|
+
<tr
|
|
183
|
+
v-for="(agent, idx) in sortedAgents"
|
|
184
|
+
:key="agent.agent_id"
|
|
185
|
+
class="cursor-pointer hover:bg-[var(--esc-panel-hover)]"
|
|
186
|
+
@click="
|
|
187
|
+
router.visit(
|
|
188
|
+
route('escalated.admin.reports.agent-detail', { id: agent.agent_id, days: periodDays }),
|
|
189
|
+
)
|
|
190
|
+
"
|
|
191
|
+
>
|
|
192
|
+
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-muted)]">{{ idx + 1 }}</td>
|
|
193
|
+
<td class="px-4 py-3">
|
|
194
|
+
<div class="flex items-center gap-3">
|
|
195
|
+
<div
|
|
196
|
+
class="flex h-8 w-8 items-center justify-center rounded-md bg-[var(--esc-panel-hover)] text-xs font-semibold text-[var(--esc-panel-text-tertiary)]"
|
|
197
|
+
>
|
|
198
|
+
{{ agent.agent_name?.charAt(0)?.toUpperCase() || '?' }}
|
|
199
|
+
</div>
|
|
200
|
+
<span class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">{{
|
|
201
|
+
agent.agent_name
|
|
202
|
+
}}</span>
|
|
203
|
+
</div>
|
|
204
|
+
</td>
|
|
205
|
+
<td class="px-4 py-3 text-sm font-bold" :class="scoreColor(agent.composite_score)">
|
|
206
|
+
{{ agent.composite_score ?? '—' }}
|
|
207
|
+
</td>
|
|
208
|
+
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
209
|
+
{{ agent.resolution_rate ?? '—' }}%
|
|
210
|
+
</td>
|
|
211
|
+
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
212
|
+
{{ agent.avg_frt ?? '—' }}h
|
|
213
|
+
</td>
|
|
214
|
+
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
215
|
+
{{ agent.avg_resolution ?? '—' }}h
|
|
216
|
+
</td>
|
|
217
|
+
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
218
|
+
{{ agent.csat ?? '—' }}
|
|
219
|
+
</td>
|
|
220
|
+
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
221
|
+
{{ agent.volume ?? 0 }}
|
|
222
|
+
</td>
|
|
223
|
+
<td class="px-4 py-3">
|
|
224
|
+
<svg v-if="agent.sparkline?.length" width="80" height="24" class="overflow-visible">
|
|
225
|
+
<path
|
|
226
|
+
:d="renderSparkline(agent.sparkline)"
|
|
227
|
+
fill="none"
|
|
228
|
+
stroke="#06b6d4"
|
|
229
|
+
stroke-width="1.5"
|
|
230
|
+
stroke-linecap="round"
|
|
231
|
+
stroke-linejoin="round"
|
|
232
|
+
/>
|
|
233
|
+
</svg>
|
|
234
|
+
<span v-else class="text-xs text-[var(--esc-panel-text-muted)]">--</span>
|
|
235
|
+
</td>
|
|
236
|
+
</tr>
|
|
237
|
+
<tr v-if="!sortedAgents.length">
|
|
238
|
+
<td colspan="9" class="px-4 py-8 text-center text-sm text-[var(--esc-panel-text-muted)]">
|
|
239
|
+
No agent data for this period.
|
|
240
|
+
</td>
|
|
241
|
+
</tr>
|
|
242
|
+
</tbody>
|
|
243
|
+
</table>
|
|
244
|
+
</div>
|
|
245
|
+
</EscalatedLayout>
|
|
246
|
+
</template>
|