@escalated-dev/escalated 0.7.0 → 0.7.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.
Files changed (179) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +31 -0
  3. package/package.json +12 -6
  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 +29 -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 +79 -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 +80 -80
  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/ar.json +727 -0
  91. package/src/locales/de.json +678 -430
  92. package/src/locales/en.json +717 -455
  93. package/src/locales/es.json +679 -430
  94. package/src/locales/fr.json +680 -431
  95. package/src/locales/index.js +12 -2
  96. package/src/locales/it.json +727 -0
  97. package/src/locales/ja.json +727 -0
  98. package/src/locales/ko.json +727 -0
  99. package/src/locales/nl.json +727 -0
  100. package/src/locales/pl.json +727 -0
  101. package/src/locales/pt-BR.json +727 -0
  102. package/src/locales/ru.json +727 -0
  103. package/src/locales/tr.json +727 -0
  104. package/src/locales/zh-CN.json +727 -0
  105. package/src/pages/Admin/ApiTokens/Index.vue +381 -381
  106. package/src/pages/Admin/AuditLog/Index.vue +154 -154
  107. package/src/pages/Admin/BusinessHours/Form.vue +172 -172
  108. package/src/pages/Admin/BusinessHours/Index.vue +113 -113
  109. package/src/pages/Admin/CannedResponses/Index.vue +127 -127
  110. package/src/pages/Admin/Capacity/Index.vue +134 -134
  111. package/src/pages/Admin/CustomFields/Form.vue +195 -195
  112. package/src/pages/Admin/CustomFields/Index.vue +134 -134
  113. package/src/pages/Admin/CustomObjects/Form.vue +202 -202
  114. package/src/pages/Admin/CustomObjects/Index.vue +127 -127
  115. package/src/pages/Admin/CustomObjects/Records.vue +228 -228
  116. package/src/pages/Admin/Departments/Form.vue +75 -75
  117. package/src/pages/Admin/Departments/Index.vue +110 -109
  118. package/src/pages/Admin/EscalationRules/Form.vue +157 -157
  119. package/src/pages/Admin/EscalationRules/Index.vue +178 -178
  120. package/src/pages/Admin/Import/Connect.vue +0 -1
  121. package/src/pages/Admin/Import/Index.vue +1 -1
  122. package/src/pages/Admin/Import/Progress.vue +1 -1
  123. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
  124. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
  125. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
  126. package/src/pages/Admin/Macros/Index.vue +448 -448
  127. package/src/pages/Admin/Plugins/Index.vue +327 -327
  128. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  129. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
  130. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  131. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  132. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  133. package/src/pages/Admin/Reports/CsatReport.vue +127 -127
  134. package/src/pages/Admin/Reports/Dashboard.vue +36 -21
  135. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  136. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  137. package/src/pages/Admin/Reports/SlaReport.vue +162 -166
  138. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  139. package/src/pages/Admin/Reports.vue +206 -206
  140. package/src/pages/Admin/Roles/Form.vue +71 -71
  141. package/src/pages/Admin/Roles/Index.vue +122 -122
  142. package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
  143. package/src/pages/Admin/Settings/DataRetention.vue +182 -182
  144. package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
  145. package/src/pages/Admin/Settings/Sandbox.vue +135 -135
  146. package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
  147. package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
  148. package/src/pages/Admin/Settings.vue +1008 -468
  149. package/src/pages/Admin/Skills/Form.vue +53 -53
  150. package/src/pages/Admin/Skills/Index.vue +94 -94
  151. package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
  152. package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
  153. package/src/pages/Admin/Statuses/Form.vue +114 -114
  154. package/src/pages/Admin/Statuses/Index.vue +153 -153
  155. package/src/pages/Admin/Tags/Index.vue +191 -191
  156. package/src/pages/Admin/Tickets/Index.vue +126 -91
  157. package/src/pages/Admin/Tickets/Show.vue +29 -0
  158. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
  159. package/src/pages/Admin/Webhooks/Form.vue +110 -110
  160. package/src/pages/Admin/Webhooks/Index.vue +136 -136
  161. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  162. package/src/pages/Admin/Workflows/Index.vue +235 -0
  163. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  164. package/src/pages/Agent/TicketIndex.vue +112 -88
  165. package/src/pages/Agent/TicketShow.vue +192 -60
  166. package/src/pages/Customer/Create.vue +1 -1
  167. package/src/pages/Customer/Index.vue +31 -31
  168. package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
  169. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
  170. package/src/pages/Customer/Show.vue +69 -63
  171. package/src/pages/Guest/Create.vue +40 -15
  172. package/src/pages/Guest/Show.vue +124 -93
  173. package/src/plugin.js +25 -0
  174. package/src/utils/mentionHighlight.js +24 -0
  175. package/src/utils/sanitizeHtml.js +78 -64
  176. package/src/widget/EscalatedWidget.vue +804 -0
  177. package/src/widget/index.js +254 -0
  178. package/src/pages/Admin/Automations/Form.vue +0 -184
  179. 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>