@escalated-dev/escalated 0.8.0 → 0.9.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.
Files changed (116) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +380 -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 +619 -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/Newsletters/Compose.vue +192 -0
  80. package/src/pages/Admin/Newsletters/Edit.vue +24 -0
  81. package/src/pages/Admin/Newsletters/Index.vue +141 -0
  82. package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
  83. package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
  84. package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
  85. package/src/pages/Admin/Newsletters/Settings.vue +85 -0
  86. package/src/pages/Admin/Newsletters/Show.vue +140 -0
  87. package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
  88. package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
  89. package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
  90. package/src/pages/Admin/Newsletters/usePreview.js +44 -0
  91. package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
  92. package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
  93. package/src/pages/Admin/Reports/Cohorts.vue +180 -180
  94. package/src/pages/Admin/Reports/Comparison.vue +141 -141
  95. package/src/pages/Admin/Reports/Dashboard.vue +226 -226
  96. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
  97. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
  98. package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
  99. package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
  100. package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
  101. package/src/pages/Admin/Settings.vue +1008 -1008
  102. package/src/pages/Admin/Skills/Form.vue +30 -10
  103. package/src/pages/Admin/Tickets/Index.vue +126 -126
  104. package/src/pages/Admin/Tickets/Show.vue +254 -249
  105. package/src/pages/Admin/Users/Index.vue +145 -145
  106. package/src/pages/Admin/Workflows/Builder.vue +207 -207
  107. package/src/pages/Admin/Workflows/Index.vue +235 -235
  108. package/src/pages/Admin/Workflows/Logs.vue +286 -286
  109. package/src/pages/Agent/TicketIndex.vue +112 -112
  110. package/src/pages/Agent/TicketShow.vue +547 -494
  111. package/src/pages/Customer/Index.vue +56 -56
  112. package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
  113. package/src/plugin.js +175 -175
  114. package/src/utils/mentionHighlight.js +24 -24
  115. package/src/widget/EscalatedWidget.vue +804 -804
  116. 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
- &larr; 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' ? ' &#9650;' : ' &#9660;' }}</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
+ &larr; 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' ? ' &#9650;' : ' &#9660;' }}</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>