@escalated-dev/escalated 0.7.0 → 0.8.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 (181) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +36 -1
  3. package/package.json +16 -7
  4. package/src/components/ActiveChatsPanel.vue +246 -0
  5. package/src/components/ActivityTimeline.stories.js +55 -55
  6. package/src/components/ActivityTimeline.vue +83 -83
  7. package/src/components/AdminDashboard.stories.js +1423 -1095
  8. package/src/components/AgentLoadIndicator.stories.js +37 -37
  9. package/src/components/AgentLoadIndicator.vue +49 -49
  10. package/src/components/AgentPerformanceCard.vue +44 -44
  11. package/src/components/ArticleEditor.vue +82 -82
  12. package/src/components/AssigneeSelect.vue +37 -37
  13. package/src/components/AttachmentList.vue +13 -1
  14. package/src/components/AuditLogEntry.vue +95 -95
  15. package/src/components/BulkActionBar.vue +166 -166
  16. package/src/components/CategoryTree.vue +89 -89
  17. package/src/components/ChartWidget.vue +46 -46
  18. package/src/components/ChatActionBar.vue +226 -0
  19. package/src/components/ChatBubble.vue +162 -0
  20. package/src/components/ChatComposer.vue +308 -0
  21. package/src/components/ChatQueue.vue +222 -0
  22. package/src/components/ChatThread.vue +197 -0
  23. package/src/components/CollisionWarning.stories.js +30 -30
  24. package/src/components/CollisionWarning.vue +68 -68
  25. package/src/components/ConditionalFieldRules.vue +135 -135
  26. package/src/components/ContextPanel.vue +66 -66
  27. package/src/components/ContextPanelSection.vue +65 -65
  28. package/src/components/CustomFieldRenderer.vue +159 -159
  29. package/src/components/EscalatedLayout.vue +35 -6
  30. package/src/components/FileDropzone.vue +57 -57
  31. package/src/components/FollowButton.stories.js +30 -30
  32. package/src/components/FollowButton.vue +88 -88
  33. package/src/components/KeyboardShortcutHelp.vue +213 -213
  34. package/src/components/KnowledgePanel.vue +122 -122
  35. package/src/components/KpiCard.stories.js +32 -32
  36. package/src/components/KpiCard.vue +88 -88
  37. package/src/components/MacroDropdown.vue +150 -150
  38. package/src/components/MentionDropdown.vue +154 -0
  39. package/src/components/MetricComparison.vue +89 -0
  40. package/src/components/PercentileChart.vue +67 -0
  41. package/src/components/PeriodSelector.vue +98 -0
  42. package/src/components/PermissionMatrix.vue +107 -107
  43. package/src/components/PinnedNotes.vue +129 -129
  44. package/src/components/PluginSlot.vue +68 -62
  45. package/src/components/PresenceIndicator.vue +46 -2
  46. package/src/components/PriorityBadge.stories.js +33 -33
  47. package/src/components/PriorityBadge.vue +46 -46
  48. package/src/components/ReplyComposer.vue +81 -18
  49. package/src/components/ReplyThread.vue +40 -2
  50. package/src/components/ReportChart.vue +395 -0
  51. package/src/components/ReportExportButton.vue +108 -0
  52. package/src/components/SatisfactionRating.vue +180 -180
  53. package/src/components/SaveViewDialog.vue +172 -0
  54. package/src/components/SavedViewSidebar.vue +208 -0
  55. package/src/components/ScheduleEditor.vue +104 -104
  56. package/src/components/SideConversation.vue +165 -165
  57. package/src/components/SideConversationList.vue +147 -147
  58. package/src/components/SkillTagManager.vue +83 -79
  59. package/src/components/SlaComplianceChart.vue +46 -53
  60. package/src/components/SlaTimer.stories.js +35 -35
  61. package/src/components/SlaTimer.vue +62 -62
  62. package/src/components/SnoozeButton.vue +247 -0
  63. package/src/components/StatsCard.stories.js +24 -24
  64. package/src/components/StatsCard.vue +58 -58
  65. package/src/components/StatusBadge.stories.js +53 -53
  66. package/src/components/StatusBadge.vue +78 -78
  67. package/src/components/TagSelect.vue +84 -84
  68. package/src/components/TicketFilters.vue +270 -225
  69. package/src/components/TicketLinkPanel.vue +193 -193
  70. package/src/components/TicketList.vue +655 -594
  71. package/src/components/TicketMergeDialog.vue +157 -157
  72. package/src/components/TicketSplitDialog.vue +98 -0
  73. package/src/components/TotpInput.stories.js +9 -9
  74. package/src/components/TotpInput.vue +81 -81
  75. package/src/components/TwoFactorSetup.vue +113 -113
  76. package/src/components/VariableTokenInput.vue +111 -0
  77. package/src/components/WorkflowActionConfig.vue +287 -0
  78. package/src/components/WorkflowActionList.vue +350 -0
  79. package/src/components/WorkflowConditionBuilder.vue +265 -0
  80. package/src/components/WorkflowTriggerSelector.vue +125 -0
  81. package/src/composables/useChat.js +187 -0
  82. package/src/composables/useI18n.js +30 -8
  83. package/src/composables/useKeyboardShortcuts.js +46 -46
  84. package/src/composables/useMentions.js +134 -0
  85. package/src/composables/usePluginExtensions.js +159 -159
  86. package/src/composables/usePluginHooks.js +65 -65
  87. package/src/composables/useRealtime.js +182 -0
  88. package/src/defineEscalatedPlugin.js +76 -76
  89. package/src/index.js +84 -72
  90. package/src/locales/README.md +13 -0
  91. package/src/locales/ar.json +727 -0
  92. package/src/locales/de.json +678 -430
  93. package/src/locales/en.json +717 -455
  94. package/src/locales/es.json +679 -430
  95. package/src/locales/fr.json +680 -431
  96. package/src/locales/index.js +12 -2
  97. package/src/locales/it.json +727 -0
  98. package/src/locales/ja.json +727 -0
  99. package/src/locales/ko.json +727 -0
  100. package/src/locales/nl.json +727 -0
  101. package/src/locales/pl.json +727 -0
  102. package/src/locales/pt-BR.json +727 -0
  103. package/src/locales/ru.json +727 -0
  104. package/src/locales/tr.json +727 -0
  105. package/src/locales/zh-CN.json +727 -0
  106. package/src/pages/Admin/ApiTokens/Index.vue +381 -381
  107. package/src/pages/Admin/AuditLog/Index.vue +154 -154
  108. package/src/pages/Admin/BusinessHours/Form.vue +172 -172
  109. package/src/pages/Admin/BusinessHours/Index.vue +113 -113
  110. package/src/pages/Admin/CannedResponses/Index.vue +127 -127
  111. package/src/pages/Admin/Capacity/Index.vue +134 -134
  112. package/src/pages/Admin/CustomFields/Form.vue +195 -195
  113. package/src/pages/Admin/CustomFields/Index.vue +134 -134
  114. package/src/pages/Admin/CustomObjects/Form.vue +202 -202
  115. package/src/pages/Admin/CustomObjects/Index.vue +127 -127
  116. package/src/pages/Admin/CustomObjects/Records.vue +228 -228
  117. package/src/pages/Admin/Departments/Form.vue +75 -75
  118. package/src/pages/Admin/Departments/Index.vue +110 -109
  119. package/src/pages/Admin/EscalationRules/Form.vue +157 -157
  120. package/src/pages/Admin/EscalationRules/Index.vue +178 -178
  121. package/src/pages/Admin/Import/Connect.vue +0 -1
  122. package/src/pages/Admin/Import/Index.vue +1 -1
  123. package/src/pages/Admin/Import/Progress.vue +1 -1
  124. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
  125. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
  126. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
  127. package/src/pages/Admin/Macros/Index.vue +448 -448
  128. package/src/pages/Admin/Plugins/Index.vue +327 -327
  129. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  130. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
  131. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  132. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  133. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  134. package/src/pages/Admin/Reports/CsatReport.vue +127 -127
  135. package/src/pages/Admin/Reports/Dashboard.vue +36 -21
  136. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  137. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  138. package/src/pages/Admin/Reports/SlaReport.vue +162 -166
  139. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  140. package/src/pages/Admin/Reports.vue +206 -206
  141. package/src/pages/Admin/Roles/Form.vue +71 -71
  142. package/src/pages/Admin/Roles/Index.vue +122 -122
  143. package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
  144. package/src/pages/Admin/Settings/DataRetention.vue +182 -182
  145. package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
  146. package/src/pages/Admin/Settings/Sandbox.vue +135 -135
  147. package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
  148. package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
  149. package/src/pages/Admin/Settings.vue +1008 -468
  150. package/src/pages/Admin/Skills/Form.vue +193 -53
  151. package/src/pages/Admin/Skills/Index.vue +105 -94
  152. package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
  153. package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
  154. package/src/pages/Admin/Statuses/Form.vue +114 -114
  155. package/src/pages/Admin/Statuses/Index.vue +153 -153
  156. package/src/pages/Admin/Tags/Index.vue +191 -191
  157. package/src/pages/Admin/Tickets/Index.vue +126 -91
  158. package/src/pages/Admin/Tickets/Show.vue +29 -0
  159. package/src/pages/Admin/Users/Index.vue +145 -0
  160. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
  161. package/src/pages/Admin/Webhooks/Form.vue +110 -110
  162. package/src/pages/Admin/Webhooks/Index.vue +136 -136
  163. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  164. package/src/pages/Admin/Workflows/Index.vue +235 -0
  165. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  166. package/src/pages/Agent/TicketIndex.vue +112 -88
  167. package/src/pages/Agent/TicketShow.vue +192 -60
  168. package/src/pages/Customer/Create.vue +1 -1
  169. package/src/pages/Customer/Index.vue +26 -1
  170. package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
  171. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
  172. package/src/pages/Customer/Show.vue +69 -63
  173. package/src/pages/Guest/Create.vue +40 -15
  174. package/src/pages/Guest/Show.vue +124 -93
  175. package/src/plugin.js +26 -1
  176. package/src/utils/mentionHighlight.js +24 -0
  177. package/src/utils/sanitizeHtml.js +78 -64
  178. package/src/widget/EscalatedWidget.vue +804 -0
  179. package/src/widget/index.js +254 -0
  180. package/src/pages/Admin/Automations/Form.vue +0 -184
  181. package/src/pages/Admin/Automations/Index.vue +0 -129
@@ -0,0 +1,132 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import MetricComparison from '../../../components/MetricComparison.vue';
4
+ import ReportChart from '../../../components/ReportChart.vue';
5
+ import PercentileChart from '../../../components/PercentileChart.vue';
6
+ import PeriodSelector from '../../../components/PeriodSelector.vue';
7
+ import ReportExportButton from '../../../components/ReportExportButton.vue';
8
+ import { router, Link } from '@inertiajs/vue3';
9
+ import { ref } from 'vue';
10
+
11
+ const props = defineProps({
12
+ period_days: { type: Number, default: 30 },
13
+ agent: { type: Object, default: () => ({}) },
14
+ total_tickets: { type: Number, default: 0 },
15
+ resolved_tickets: { type: Number, default: 0 },
16
+ avg_frt: { type: Number, default: 0 },
17
+ avg_resolution: { type: Number, default: 0 },
18
+ csat_score: { type: [Number, String], default: '—' },
19
+ response_percentiles: { type: Object, default: () => ({ p50: 0, p75: 0, p90: 0, p95: 0, p99: 0 }) },
20
+ productivity: { type: Array, default: () => [] },
21
+ workload: { type: Array, default: () => [] },
22
+ csat_breakdown: { type: Array, default: () => [] },
23
+ prev_total_tickets: { type: Number, default: null },
24
+ prev_resolved_tickets: { type: Number, default: null },
25
+ prev_avg_frt: { type: Number, default: null },
26
+ prev_avg_resolution: { type: Number, default: null },
27
+ prev_csat_score: { type: [Number, String], default: null },
28
+ });
29
+
30
+ const periodDays = ref(props.period_days);
31
+
32
+ function changePeriod(days) {
33
+ periodDays.value = days;
34
+ router.get(
35
+ route('escalated.admin.reports.agent-detail', { id: props.agent?.id }),
36
+ { days },
37
+ { preserveState: true },
38
+ );
39
+ }
40
+ </script>
41
+
42
+ <template>
43
+ <EscalatedLayout :title="`Agent: ${agent?.name || 'Unknown'}`">
44
+ <div class="mb-6 flex items-center justify-between">
45
+ <PeriodSelector :model-value="periodDays" @update:model-value="changePeriod" />
46
+ <div class="flex items-center gap-3">
47
+ <ReportExportButton :report-name="`agent-${agent?.id}`" :period-days="periodDays" />
48
+ <Link
49
+ :href="route('escalated.admin.reports.agent-ranking', { days: periodDays })"
50
+ class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
51
+ >
52
+ &larr; Back to Rankings
53
+ </Link>
54
+ </div>
55
+ </div>
56
+
57
+ <!-- Agent Header -->
58
+ <div
59
+ class="mb-6 flex items-center gap-4 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5"
60
+ >
61
+ <div
62
+ class="flex h-14 w-14 items-center justify-center rounded-xl bg-gradient-to-br from-cyan-500/20 to-violet-500/20 text-xl font-bold text-white"
63
+ >
64
+ {{ agent?.name?.charAt(0)?.toUpperCase() || '?' }}
65
+ </div>
66
+ <div>
67
+ <h2 class="text-lg font-bold text-[var(--esc-panel-text)]">{{ agent?.name || 'Unknown Agent' }}</h2>
68
+ <p class="text-sm text-[var(--esc-panel-text-muted)]">{{ agent?.email || '' }}</p>
69
+ </div>
70
+ </div>
71
+
72
+ <!-- KPI Cards with comparison -->
73
+ <div class="mb-6 grid grid-cols-2 gap-4 md:grid-cols-5">
74
+ <MetricComparison
75
+ label="Total Tickets"
76
+ :value="total_tickets"
77
+ :previous-value="prev_total_tickets"
78
+ :icon="'M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z'"
79
+ />
80
+ <MetricComparison
81
+ label="Resolved"
82
+ :value="resolved_tickets"
83
+ :previous-value="prev_resolved_tickets"
84
+ :icon="'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z'"
85
+ />
86
+ <MetricComparison
87
+ label="Avg FRT"
88
+ :value="avg_frt"
89
+ :previous-value="prev_avg_frt"
90
+ format="h"
91
+ :invert-color="true"
92
+ :icon="'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z'"
93
+ />
94
+ <MetricComparison
95
+ label="Avg Resolution"
96
+ :value="avg_resolution"
97
+ :previous-value="prev_avg_resolution"
98
+ format="h"
99
+ :invert-color="true"
100
+ :icon="'M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5'"
101
+ />
102
+ <MetricComparison
103
+ label="CSAT Score"
104
+ :value="csat_score"
105
+ :previous-value="prev_csat_score"
106
+ :icon="'M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z'"
107
+ />
108
+ </div>
109
+
110
+ <!-- Percentile & Productivity Charts -->
111
+ <div class="mb-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
112
+ <PercentileChart title="Response Time Percentiles" :percentiles="response_percentiles" unit="h" />
113
+ <ReportChart
114
+ type="bar"
115
+ title="Productivity (Replies & Resolutions per Day)"
116
+ :data="productivity"
117
+ :colors="['#06b6d4', '#8b5cf6']"
118
+ />
119
+ </div>
120
+
121
+ <!-- Workload & CSAT -->
122
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
123
+ <ReportChart type="area" title="Workload Over Time" :data="workload" :colors="['#f59e0b']" />
124
+ <ReportChart
125
+ type="pie"
126
+ title="CSAT Breakdown"
127
+ :data="csat_breakdown"
128
+ :colors="['#ef4444', '#f97316', '#f59e0b', '#10b981', '#06b6d4']"
129
+ />
130
+ </div>
131
+ </EscalatedLayout>
132
+ </template>
@@ -1,115 +1,115 @@
1
- <script setup>
2
- import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
- import AgentPerformanceCard from '../../../components/AgentPerformanceCard.vue';
4
- import { router, Link } from '@inertiajs/vue3';
5
-
6
- const props = defineProps({
7
- period_days: Number,
8
- agents: Array,
9
- });
10
-
11
- function changePeriod(days) {
12
- router.get(route('escalated.admin.reports.agents'), { days }, { preserveState: true });
13
- }
14
- </script>
15
-
16
- <template>
17
- <EscalatedLayout title="Agent Performance Metrics">
18
- <div class="mb-6 flex items-center justify-between">
19
- <div class="flex gap-2">
20
- <button
21
- v-for="d in [7, 30, 90]"
22
- :key="d"
23
- :class="[
24
- 'rounded-lg px-3.5 py-1.5 text-sm font-medium transition-all',
25
- period_days === d
26
- ? 'bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] text-white shadow-lg shadow-[var(--esc-panel-bg)]/20'
27
- : 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]',
28
- ]"
29
- @click="changePeriod(d)"
30
- >
31
- Last {{ d }} days
32
- </button>
33
- </div>
34
- <Link
35
- :href="route('escalated.admin.reports')"
36
- class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
37
- >
38
- &larr; Back to Reports
39
- </Link>
40
- </div>
41
-
42
- <!-- Agent Table -->
43
- <div class="mb-8 overflow-hidden rounded-xl border border-[var(--esc-panel-border)]">
44
- <table class="w-full">
45
- <thead>
46
- <tr class="border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)]">
47
- <th
48
- class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
49
- >
50
- Agent
51
- </th>
52
- <th
53
- class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
54
- >
55
- Total Tickets
56
- </th>
57
- <th
58
- class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
59
- >
60
- Resolved
61
- </th>
62
- <th
63
- class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
64
- >
65
- Avg Response
66
- </th>
67
- <th
68
- class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
69
- >
70
- Avg Resolution
71
- </th>
72
- </tr>
73
- </thead>
74
- <tbody class="divide-y divide-[var(--esc-panel-border)]">
75
- <tr v-for="agent in agents" :key="agent.agent_id" class="hover:bg-[var(--esc-panel-hover)]">
76
- <td class="px-4 py-3">
77
- <div class="flex items-center gap-3">
78
- <div
79
- 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)]"
80
- >
81
- {{ agent.agent_name?.charAt(0)?.toUpperCase() || '?' }}
82
- </div>
83
- <span class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">{{
84
- agent.agent_name
85
- }}</span>
86
- </div>
87
- </td>
88
- <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
89
- {{ agent.total_tickets }}
90
- </td>
91
- <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
92
- {{ agent.resolved_tickets }}
93
- </td>
94
- <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
95
- {{ agent.avg_response_hours ?? '—' }}h
96
- </td>
97
- <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
98
- {{ agent.avg_resolution_hours ?? '—' }}h
99
- </td>
100
- </tr>
101
- <tr v-if="!agents?.length">
102
- <td colspan="5" class="px-4 py-8 text-center text-sm text-[var(--esc-panel-text-muted)]">
103
- No agent data for this period.
104
- </td>
105
- </tr>
106
- </tbody>
107
- </table>
108
- </div>
109
-
110
- <!-- Agent Cards (visual) -->
111
- <div v-if="agents?.length" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
112
- <AgentPerformanceCard v-for="agent in agents" :key="agent.agent_id" :agent="agent" />
113
- </div>
114
- </EscalatedLayout>
115
- </template>
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import AgentPerformanceCard from '../../../components/AgentPerformanceCard.vue';
4
+ import { router, Link } from '@inertiajs/vue3';
5
+
6
+ defineProps({
7
+ period_days: Number,
8
+ agents: Array,
9
+ });
10
+
11
+ function changePeriod(days) {
12
+ router.get(route('escalated.admin.reports.agents'), { days }, { preserveState: true });
13
+ }
14
+ </script>
15
+
16
+ <template>
17
+ <EscalatedLayout title="Agent Performance Metrics">
18
+ <div class="mb-6 flex items-center justify-between">
19
+ <div class="flex gap-2">
20
+ <button
21
+ v-for="d in [7, 30, 90]"
22
+ :key="d"
23
+ :class="[
24
+ 'rounded-lg px-3.5 py-1.5 text-sm font-medium transition-all',
25
+ period_days === d
26
+ ? 'bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] text-white shadow-lg shadow-[var(--esc-panel-bg)]/20'
27
+ : 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]',
28
+ ]"
29
+ @click="changePeriod(d)"
30
+ >
31
+ Last {{ d }} days
32
+ </button>
33
+ </div>
34
+ <Link
35
+ :href="route('escalated.admin.reports')"
36
+ class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
37
+ >
38
+ &larr; Back to Reports
39
+ </Link>
40
+ </div>
41
+
42
+ <!-- Agent Table -->
43
+ <div class="mb-8 overflow-hidden rounded-xl border border-[var(--esc-panel-border)]">
44
+ <table class="w-full">
45
+ <thead>
46
+ <tr class="border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)]">
47
+ <th
48
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
49
+ >
50
+ Agent
51
+ </th>
52
+ <th
53
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
54
+ >
55
+ Total Tickets
56
+ </th>
57
+ <th
58
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
59
+ >
60
+ Resolved
61
+ </th>
62
+ <th
63
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
64
+ >
65
+ Avg Response
66
+ </th>
67
+ <th
68
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
69
+ >
70
+ Avg Resolution
71
+ </th>
72
+ </tr>
73
+ </thead>
74
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
75
+ <tr v-for="agent in agents" :key="agent.agent_id" class="hover:bg-[var(--esc-panel-hover)]">
76
+ <td class="px-4 py-3">
77
+ <div class="flex items-center gap-3">
78
+ <div
79
+ 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)]"
80
+ >
81
+ {{ agent.agent_name?.charAt(0)?.toUpperCase() || '?' }}
82
+ </div>
83
+ <span class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">{{
84
+ agent.agent_name
85
+ }}</span>
86
+ </div>
87
+ </td>
88
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
89
+ {{ agent.total_tickets }}
90
+ </td>
91
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
92
+ {{ agent.resolved_tickets }}
93
+ </td>
94
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
95
+ {{ agent.avg_response_hours ?? '—' }}h
96
+ </td>
97
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
98
+ {{ agent.avg_resolution_hours ?? '—' }}h
99
+ </td>
100
+ </tr>
101
+ <tr v-if="!agents?.length">
102
+ <td colspan="5" class="px-4 py-8 text-center text-sm text-[var(--esc-panel-text-muted)]">
103
+ No agent data for this period.
104
+ </td>
105
+ </tr>
106
+ </tbody>
107
+ </table>
108
+ </div>
109
+
110
+ <!-- Agent Cards (visual) -->
111
+ <div v-if="agents?.length" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
112
+ <AgentPerformanceCard v-for="agent in agents" :key="agent.agent_id" :agent="agent" />
113
+ </div>
114
+ </EscalatedLayout>
115
+ </template>
@@ -0,0 +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>