@escalated-dev/escalated 0.6.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 (187) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +125 -2
  3. package/package.json +40 -5
  4. package/src/components/ActiveChatsPanel.vue +246 -0
  5. package/src/components/ActivityTimeline.stories.js +55 -0
  6. package/src/components/ActivityTimeline.vue +83 -66
  7. package/src/components/AdminDashboard.stories.js +1423 -0
  8. package/src/components/AgentLoadIndicator.stories.js +37 -0
  9. package/src/components/AgentLoadIndicator.vue +49 -0
  10. package/src/components/AgentPerformanceCard.vue +44 -0
  11. package/src/components/ArticleEditor.vue +82 -0
  12. package/src/components/AssigneeSelect.vue +37 -23
  13. package/src/components/AttachmentList.vue +65 -37
  14. package/src/components/AuditLogEntry.vue +95 -0
  15. package/src/components/BulkActionBar.vue +166 -97
  16. package/src/components/CategoryTree.vue +89 -0
  17. package/src/components/ChartWidget.vue +46 -0
  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 -0
  24. package/src/components/CollisionWarning.vue +68 -0
  25. package/src/components/ConditionalFieldRules.vue +135 -0
  26. package/src/components/ContextPanel.vue +66 -0
  27. package/src/components/ContextPanelSection.vue +65 -0
  28. package/src/components/CustomFieldRenderer.vue +159 -0
  29. package/src/components/EscalatedLayout.vue +424 -70
  30. package/src/components/FileDropzone.vue +57 -41
  31. package/src/components/FollowButton.stories.js +30 -0
  32. package/src/components/FollowButton.vue +88 -63
  33. package/src/components/KeyboardShortcutHelp.vue +213 -123
  34. package/src/components/KnowledgePanel.vue +122 -0
  35. package/src/components/KpiCard.stories.js +32 -0
  36. package/src/components/KpiCard.vue +88 -0
  37. package/src/components/MacroDropdown.vue +150 -90
  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 -0
  43. package/src/components/PinnedNotes.vue +129 -90
  44. package/src/components/PluginSlot.vue +68 -62
  45. package/src/components/PresenceIndicator.vue +138 -28
  46. package/src/components/PriorityBadge.stories.js +33 -0
  47. package/src/components/PriorityBadge.vue +46 -39
  48. package/src/components/QuickFilters.vue +65 -49
  49. package/src/components/ReplyComposer.vue +207 -30
  50. package/src/components/ReplyThread.vue +111 -21
  51. package/src/components/ReportChart.vue +395 -0
  52. package/src/components/ReportExportButton.vue +108 -0
  53. package/src/components/SatisfactionRating.vue +180 -128
  54. package/src/components/SaveViewDialog.vue +172 -0
  55. package/src/components/SavedViewSidebar.vue +208 -0
  56. package/src/components/ScheduleEditor.vue +104 -0
  57. package/src/components/SideConversation.vue +165 -0
  58. package/src/components/SideConversationList.vue +147 -0
  59. package/src/components/SkillTagManager.vue +79 -0
  60. package/src/components/SlaComplianceChart.vue +46 -0
  61. package/src/components/SlaTimer.stories.js +35 -0
  62. package/src/components/SlaTimer.vue +62 -49
  63. package/src/components/SnoozeButton.vue +247 -0
  64. package/src/components/StatsCard.stories.js +24 -0
  65. package/src/components/StatsCard.vue +18 -6
  66. package/src/components/StatusBadge.stories.js +53 -0
  67. package/src/components/StatusBadge.vue +78 -46
  68. package/src/components/TagSelect.vue +84 -53
  69. package/src/components/TicketFilters.vue +247 -46
  70. package/src/components/TicketLinkPanel.vue +193 -0
  71. package/src/components/TicketList.vue +536 -74
  72. package/src/components/TicketMergeDialog.vue +157 -0
  73. package/src/components/TicketSidebar.vue +167 -99
  74. package/src/components/TicketSplitDialog.vue +98 -0
  75. package/src/components/TicketTypeSelector.vue +21 -0
  76. package/src/components/TotpInput.stories.js +9 -0
  77. package/src/components/TotpInput.vue +81 -0
  78. package/src/components/TwoFactorSetup.vue +113 -0
  79. package/src/components/VariableTokenInput.vue +111 -0
  80. package/src/components/WorkflowActionConfig.vue +287 -0
  81. package/src/components/WorkflowActionList.vue +350 -0
  82. package/src/components/WorkflowConditionBuilder.vue +265 -0
  83. package/src/components/WorkflowTriggerSelector.vue +125 -0
  84. package/src/composables/useChat.js +187 -0
  85. package/src/composables/useDebouncedSearch.js +38 -0
  86. package/src/composables/useI18n.js +80 -80
  87. package/src/composables/useKeyboardShortcuts.js +46 -46
  88. package/src/composables/useMentions.js +134 -0
  89. package/src/composables/usePluginExtensions.js +159 -67
  90. package/src/composables/usePluginHooks.js +65 -0
  91. package/src/composables/useRealtime.js +182 -0
  92. package/src/defineEscalatedPlugin.js +76 -50
  93. package/src/index.js +77 -28
  94. package/src/locales/ar.json +727 -0
  95. package/src/locales/de.json +678 -430
  96. package/src/locales/en.json +717 -430
  97. package/src/locales/es.json +679 -430
  98. package/src/locales/fr.json +680 -431
  99. package/src/locales/index.js +12 -2
  100. package/src/locales/it.json +727 -0
  101. package/src/locales/ja.json +727 -0
  102. package/src/locales/ko.json +727 -0
  103. package/src/locales/nl.json +727 -0
  104. package/src/locales/pl.json +727 -0
  105. package/src/locales/pt-BR.json +727 -0
  106. package/src/locales/ru.json +727 -0
  107. package/src/locales/tr.json +727 -0
  108. package/src/locales/zh-CN.json +727 -0
  109. package/src/pages/Admin/ApiTokens/Index.vue +381 -217
  110. package/src/pages/Admin/AuditLog/Index.vue +154 -0
  111. package/src/pages/Admin/BusinessHours/Form.vue +172 -0
  112. package/src/pages/Admin/BusinessHours/Index.vue +113 -0
  113. package/src/pages/Admin/CannedResponses/Index.vue +82 -25
  114. package/src/pages/Admin/Capacity/Index.vue +134 -0
  115. package/src/pages/Admin/CustomFields/Form.vue +195 -0
  116. package/src/pages/Admin/CustomFields/Index.vue +134 -0
  117. package/src/pages/Admin/CustomObjects/Form.vue +202 -0
  118. package/src/pages/Admin/CustomObjects/Index.vue +127 -0
  119. package/src/pages/Admin/CustomObjects/Records.vue +228 -0
  120. package/src/pages/Admin/Departments/Form.vue +35 -11
  121. package/src/pages/Admin/Departments/Index.vue +74 -21
  122. package/src/pages/Admin/EscalationRules/Form.vue +86 -25
  123. package/src/pages/Admin/EscalationRules/Index.vue +158 -37
  124. package/src/pages/Admin/Import/Connect.vue +95 -0
  125. package/src/pages/Admin/Import/Index.vue +102 -0
  126. package/src/pages/Admin/Import/Mapping.vue +110 -0
  127. package/src/pages/Admin/Import/Progress.vue +153 -0
  128. package/src/pages/Admin/Import/Review.vue +76 -0
  129. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
  130. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
  131. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
  132. package/src/pages/Admin/Macros/Index.vue +448 -287
  133. package/src/pages/Admin/Plugins/Index.vue +327 -197
  134. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  135. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
  136. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  137. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  138. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  139. package/src/pages/Admin/Reports/CsatReport.vue +127 -0
  140. package/src/pages/Admin/Reports/Dashboard.vue +226 -0
  141. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  142. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  143. package/src/pages/Admin/Reports/SlaReport.vue +162 -0
  144. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  145. package/src/pages/Admin/Reports.vue +206 -101
  146. package/src/pages/Admin/Roles/Form.vue +71 -0
  147. package/src/pages/Admin/Roles/Index.vue +122 -0
  148. package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
  149. package/src/pages/Admin/Settings/DataRetention.vue +182 -0
  150. package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
  151. package/src/pages/Admin/Settings/Sandbox.vue +135 -0
  152. package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
  153. package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
  154. package/src/pages/Admin/Settings.vue +847 -99
  155. package/src/pages/Admin/Skills/Form.vue +53 -0
  156. package/src/pages/Admin/Skills/Index.vue +94 -0
  157. package/src/pages/Admin/SlaPolicies/Form.vue +62 -19
  158. package/src/pages/Admin/SlaPolicies/Index.vue +85 -23
  159. package/src/pages/Admin/Statuses/Form.vue +114 -0
  160. package/src/pages/Admin/Statuses/Index.vue +153 -0
  161. package/src/pages/Admin/Tags/Index.vue +121 -35
  162. package/src/pages/Admin/Tickets/Index.vue +96 -9
  163. package/src/pages/Admin/Tickets/Show.vue +130 -37
  164. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
  165. package/src/pages/Admin/Webhooks/Form.vue +110 -0
  166. package/src/pages/Admin/Webhooks/Index.vue +136 -0
  167. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  168. package/src/pages/Admin/Workflows/Index.vue +235 -0
  169. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  170. package/src/pages/Agent/Dashboard.vue +311 -186
  171. package/src/pages/Agent/TicketIndex.vue +80 -6
  172. package/src/pages/Agent/TicketShow.vue +399 -60
  173. package/src/pages/Customer/Create.vue +37 -10
  174. package/src/pages/Customer/Index.vue +9 -2
  175. package/src/pages/Customer/KnowledgeBase/Article.vue +129 -0
  176. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
  177. package/src/pages/Customer/Show.vue +69 -63
  178. package/src/pages/Guest/Create.vue +40 -15
  179. package/src/pages/Guest/Show.vue +124 -93
  180. package/src/plugin.js +111 -1
  181. package/src/utils/constants.js +30 -0
  182. package/src/utils/formatting.js +35 -0
  183. package/src/utils/mentionHighlight.js +24 -0
  184. package/src/utils/sanitizeHtml.js +78 -64
  185. package/src/widget/EscalatedWidget.vue +804 -0
  186. package/src/widget/index.js +254 -0
  187. package/src/pages/Admin/Plugins.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>
@@ -0,0 +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
+ 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>
@@ -0,0 +1,180 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import ReportChart from '../../../components/ReportChart.vue';
4
+ import PeriodSelector from '../../../components/PeriodSelector.vue';
5
+ import ReportExportButton from '../../../components/ReportExportButton.vue';
6
+ import { router, Link } from '@inertiajs/vue3';
7
+ import { ref, computed } from 'vue';
8
+
9
+ const props = defineProps({
10
+ period_days: { type: Number, default: 30 },
11
+ by_tag: { type: Array, default: () => [] },
12
+ by_department: { type: Array, default: () => [] },
13
+ by_channel: { type: Array, default: () => [] },
14
+ by_type: { type: Array, default: () => [] },
15
+ by_priority: { type: Array, default: () => [] },
16
+ });
17
+
18
+ const periodDays = ref(props.period_days);
19
+ const activeTab = ref('department');
20
+ const sortColumn = ref('volume');
21
+ const sortDir = ref('desc');
22
+
23
+ const tabs = [
24
+ { key: 'department', label: 'By Department' },
25
+ { key: 'tag', label: 'By Tag' },
26
+ { key: 'channel', label: 'By Channel' },
27
+ { key: 'type', label: 'By Type' },
28
+ { key: 'priority', label: 'By Priority' },
29
+ ];
30
+
31
+ function changePeriod(days) {
32
+ periodDays.value = days;
33
+ router.get(route('escalated.admin.reports.cohorts'), { days }, { preserveState: true });
34
+ }
35
+
36
+ const activeCohortData = computed(() => {
37
+ const map = {
38
+ tag: props.by_tag,
39
+ department: props.by_department,
40
+ channel: props.by_channel,
41
+ type: props.by_type,
42
+ priority: props.by_priority,
43
+ };
44
+ return map[activeTab.value] || [];
45
+ });
46
+
47
+ const sortedCohorts = computed(() => {
48
+ const data = [...activeCohortData.value];
49
+ data.sort((a, b) => {
50
+ const aVal = a[sortColumn.value] ?? 0;
51
+ const bVal = b[sortColumn.value] ?? 0;
52
+ return sortDir.value === 'asc' ? aVal - bVal : bVal - aVal;
53
+ });
54
+ return data;
55
+ });
56
+
57
+ const chartData = computed(() => {
58
+ return activeCohortData.value.map((c) => ({
59
+ label: c.name || c.label,
60
+ value: c.volume || 0,
61
+ }));
62
+ });
63
+
64
+ function toggleSort(col) {
65
+ if (sortColumn.value === col) {
66
+ sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc';
67
+ } else {
68
+ sortColumn.value = col;
69
+ sortDir.value = 'desc';
70
+ }
71
+ }
72
+ </script>
73
+
74
+ <template>
75
+ <EscalatedLayout title="Cohort Analysis">
76
+ <div class="mb-6 flex items-center justify-between">
77
+ <PeriodSelector :model-value="periodDays" @update:model-value="changePeriod" />
78
+ <div class="flex items-center gap-3">
79
+ <ReportExportButton report-name="cohorts" :period-days="periodDays" />
80
+ <Link
81
+ :href="route('escalated.admin.reports')"
82
+ class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
83
+ >
84
+ &larr; Back
85
+ </Link>
86
+ </div>
87
+ </div>
88
+
89
+ <!-- Tab Selector -->
90
+ <div class="mb-6 flex gap-1 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] p-1">
91
+ <button
92
+ v-for="tab in tabs"
93
+ :key="tab.key"
94
+ :class="[
95
+ 'rounded-md px-4 py-2 text-sm font-medium transition-all',
96
+ activeTab === tab.key
97
+ ? 'bg-[var(--esc-panel-active)] text-[var(--esc-panel-text)]'
98
+ : 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]',
99
+ ]"
100
+ @click="activeTab = tab.key"
101
+ >
102
+ {{ tab.label }}
103
+ </button>
104
+ </div>
105
+
106
+ <!-- Chart -->
107
+ <div class="mb-6">
108
+ <ReportChart
109
+ type="bar"
110
+ :title="`Volume ${tabs.find((t) => t.key === activeTab)?.label || ''}`"
111
+ :data="chartData"
112
+ :colors="['#8b5cf6', '#06b6d4', '#f59e0b', '#10b981', '#ef4444']"
113
+ />
114
+ </div>
115
+
116
+ <!-- Cohort Table -->
117
+ <div class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)]">
118
+ <table class="w-full">
119
+ <thead>
120
+ <tr class="border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)]">
121
+ <th
122
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
123
+ >
124
+ Name
125
+ </th>
126
+ <th
127
+ v-for="col in [
128
+ { key: 'volume', label: 'Volume' },
129
+ { key: 'avg_resolution', label: 'Avg Resolution' },
130
+ { key: 'breach_rate', label: 'Breach Rate' },
131
+ { key: 'csat', label: 'CSAT' },
132
+ ]"
133
+ :key="col.key"
134
+ 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)]"
135
+ @click="toggleSort(col.key)"
136
+ >
137
+ {{ col.label }}
138
+ <span v-if="sortColumn === col.key">{{ sortDir === 'asc' ? ' &#9650;' : ' &#9660;' }}</span>
139
+ </th>
140
+ </tr>
141
+ </thead>
142
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
143
+ <tr
144
+ v-for="cohort in sortedCohorts"
145
+ :key="cohort.name || cohort.label"
146
+ class="hover:bg-[var(--esc-panel-hover)]"
147
+ >
148
+ <td class="px-4 py-3 text-sm font-medium text-[var(--esc-panel-text-secondary)]">
149
+ {{ cohort.name || cohort.label }}
150
+ </td>
151
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
152
+ {{ cohort.volume ?? 0 }}
153
+ </td>
154
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
155
+ {{ cohort.avg_resolution ?? '—' }}h
156
+ </td>
157
+ <td
158
+ class="px-4 py-3 text-sm"
159
+ :class="
160
+ (cohort.breach_rate ?? 0) > 20
161
+ ? 'text-red-400'
162
+ : 'text-[var(--esc-panel-text-tertiary)]'
163
+ "
164
+ >
165
+ {{ cohort.breach_rate ?? '—' }}%
166
+ </td>
167
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
168
+ {{ cohort.csat ?? '—' }}
169
+ </td>
170
+ </tr>
171
+ <tr v-if="!sortedCohorts.length">
172
+ <td colspan="5" class="px-4 py-8 text-center text-sm text-[var(--esc-panel-text-muted)]">
173
+ No data for this cohort type.
174
+ </td>
175
+ </tr>
176
+ </tbody>
177
+ </table>
178
+ </div>
179
+ </EscalatedLayout>
180
+ </template>