@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,164 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import KpiCard from '../../../components/KpiCard.vue';
4
+ import ReportChart from '../../../components/ReportChart.vue';
5
+ import PeriodSelector from '../../../components/PeriodSelector.vue';
6
+ import ReportExportButton from '../../../components/ReportExportButton.vue';
7
+ import { router, Link } from '@inertiajs/vue3';
8
+ import { ref, computed } from 'vue';
9
+
10
+ const props = defineProps({
11
+ period_days: { type: Number, default: 30 },
12
+ avg_frt: { type: Number, default: 0 },
13
+ median_frt: { type: Number, default: 0 },
14
+ p90_frt: { type: Number, default: 0 },
15
+ pct_under_target: { type: Number, default: 0 },
16
+ distribution: { type: Array, default: () => [] },
17
+ trend: { type: Array, default: () => [] },
18
+ by_agent: { type: Array, default: () => [] },
19
+ by_department: { type: Array, default: () => [] },
20
+ by_priority: { type: Array, default: () => [] },
21
+ target_hours: { type: Number, default: 4 },
22
+ });
23
+
24
+ const periodDays = ref(props.period_days);
25
+ const sortColumn = ref('avg');
26
+ const sortDir = ref('asc');
27
+
28
+ function changePeriod(days) {
29
+ periodDays.value = days;
30
+ router.get(route('escalated.admin.reports.response-times'), { days }, { preserveState: true });
31
+ }
32
+
33
+ function frtColor(value) {
34
+ if (value <= props.target_hours * 0.5) return 'text-emerald-400';
35
+ if (value <= props.target_hours) return 'text-amber-400';
36
+ return 'text-red-400';
37
+ }
38
+
39
+ const sortedAgents = computed(() => {
40
+ const agents = [...(props.by_agent || [])];
41
+ agents.sort((a, b) => {
42
+ const aVal = a[sortColumn.value] ?? 0;
43
+ const bVal = b[sortColumn.value] ?? 0;
44
+ return sortDir.value === 'asc' ? aVal - bVal : bVal - aVal;
45
+ });
46
+ return agents;
47
+ });
48
+
49
+ function toggleSort(col) {
50
+ if (sortColumn.value === col) {
51
+ sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc';
52
+ } else {
53
+ sortColumn.value = col;
54
+ sortDir.value = 'asc';
55
+ }
56
+ }
57
+ </script>
58
+
59
+ <template>
60
+ <EscalatedLayout title="First Response Time Analysis">
61
+ <div class="mb-6 flex items-center justify-between">
62
+ <PeriodSelector :model-value="periodDays" @update:model-value="changePeriod" />
63
+ <div class="flex items-center gap-3">
64
+ <ReportExportButton report-name="response-times" :period-days="periodDays" />
65
+ <Link
66
+ :href="route('escalated.admin.reports')"
67
+ class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
68
+ >
69
+ &larr; Back
70
+ </Link>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- KPI Cards -->
75
+ <div class="mb-6 grid grid-cols-2 gap-4 md:grid-cols-4">
76
+ <KpiCard
77
+ label="Average FRT"
78
+ :value="`${avg_frt}h`"
79
+ :icon="'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z'"
80
+ />
81
+ <KpiCard
82
+ label="Median FRT"
83
+ :value="`${median_frt}h`"
84
+ :icon="'M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5'"
85
+ />
86
+ <KpiCard
87
+ label="P90 FRT"
88
+ :value="`${p90_frt}h`"
89
+ :icon="'M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75z'"
90
+ />
91
+ <KpiCard
92
+ label="Under Target"
93
+ :value="`${pct_under_target}%`"
94
+ :icon="'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z'"
95
+ />
96
+ </div>
97
+
98
+ <!-- Charts -->
99
+ <div class="mb-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
100
+ <ReportChart
101
+ type="histogram"
102
+ title="FRT Distribution"
103
+ :data="distribution"
104
+ :colors="['#06b6d4']"
105
+ x-label="Response Time Buckets"
106
+ />
107
+ <ReportChart type="line" title="Average FRT Trend" :data="trend" :colors="['#8b5cf6']" />
108
+ <ReportChart type="bar" title="FRT by Department" :data="by_department" :colors="['#f59e0b']" />
109
+ <ReportChart type="bar" title="FRT by Priority" :data="by_priority" :colors="['#10b981']" />
110
+ </div>
111
+
112
+ <!-- Agent Table -->
113
+ <div class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)]">
114
+ <div class="border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-4 py-3">
115
+ <h3 class="text-sm font-semibold text-[var(--esc-panel-text-secondary)]">FRT by Agent</h3>
116
+ </div>
117
+ <table class="w-full">
118
+ <thead>
119
+ <tr class="border-b border-[var(--esc-panel-border)]">
120
+ <th
121
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
122
+ >
123
+ Agent
124
+ </th>
125
+ <th
126
+ v-for="col in [
127
+ { key: 'avg', label: 'Avg' },
128
+ { key: 'median', label: 'Median' },
129
+ { key: 'p90', label: 'P90' },
130
+ ]"
131
+ :key="col.key"
132
+ 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)]"
133
+ @click="toggleSort(col.key)"
134
+ >
135
+ {{ col.label }}
136
+ <span v-if="sortColumn === col.key">{{ sortDir === 'asc' ? ' &#9650;' : ' &#9660;' }}</span>
137
+ </th>
138
+ </tr>
139
+ </thead>
140
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
141
+ <tr v-for="agent in sortedAgents" :key="agent.agent_id" class="hover:bg-[var(--esc-panel-hover)]">
142
+ <td class="px-4 py-3 text-sm font-medium text-[var(--esc-panel-text-secondary)]">
143
+ {{ agent.agent_name }}
144
+ </td>
145
+ <td class="px-4 py-3 text-sm font-medium" :class="frtColor(agent.avg)">
146
+ {{ agent.avg ?? '—' }}h
147
+ </td>
148
+ <td class="px-4 py-3 text-sm font-medium" :class="frtColor(agent.median)">
149
+ {{ agent.median ?? '—' }}h
150
+ </td>
151
+ <td class="px-4 py-3 text-sm font-medium" :class="frtColor(agent.p90)">
152
+ {{ agent.p90 ?? '—' }}h
153
+ </td>
154
+ </tr>
155
+ <tr v-if="!sortedAgents.length">
156
+ <td colspan="4" class="px-4 py-8 text-center text-sm text-[var(--esc-panel-text-muted)]">
157
+ No agent data for this period.
158
+ </td>
159
+ </tr>
160
+ </tbody>
161
+ </table>
162
+ </div>
163
+ </EscalatedLayout>
164
+ </template>
@@ -0,0 +1,162 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import KpiCard from '../../../components/KpiCard.vue';
4
+ import SlaComplianceChart from '../../../components/SlaComplianceChart.vue';
5
+ import { router, Link } from '@inertiajs/vue3';
6
+ import { computed } from 'vue';
7
+
8
+ const props = defineProps({
9
+ period_days: Number,
10
+ compliance_rate: Number,
11
+ compliance_by_policy: Array,
12
+ breaches: Array,
13
+ });
14
+
15
+ function changePeriod(days) {
16
+ router.get(route('escalated.admin.reports.sla'), { days }, { preserveState: true });
17
+ }
18
+
19
+ const totalTickets = computed(() => {
20
+ return props.compliance_by_policy?.reduce((sum, p) => sum + p.total, 0) || 0;
21
+ });
22
+
23
+ const firstResponseBreaches = computed(() => {
24
+ return props.breaches?.filter((b) => b.sla_first_response_breached).length || 0;
25
+ });
26
+
27
+ const resolutionBreaches = computed(() => {
28
+ return props.breaches?.filter((b) => b.sla_resolution_breached).length || 0;
29
+ });
30
+ </script>
31
+
32
+ <template>
33
+ <EscalatedLayout title="SLA Achievement Report">
34
+ <div class="mb-6 flex items-center justify-between">
35
+ <div class="flex gap-2">
36
+ <button
37
+ v-for="d in [7, 30, 90]"
38
+ :key="d"
39
+ :class="[
40
+ 'rounded-lg px-3.5 py-1.5 text-sm font-medium transition-all',
41
+ period_days === d
42
+ ? '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'
43
+ : '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)]',
44
+ ]"
45
+ @click="changePeriod(d)"
46
+ >
47
+ Last {{ d }} days
48
+ </button>
49
+ </div>
50
+ <Link
51
+ :href="route('escalated.admin.reports')"
52
+ class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
53
+ >
54
+ &larr; Back to Reports
55
+ </Link>
56
+ </div>
57
+
58
+ <!-- Summary KPIs -->
59
+ <div class="mb-6 grid grid-cols-2 gap-4 md:grid-cols-4">
60
+ <KpiCard
61
+ label="Total SLA Tickets"
62
+ :value="totalTickets"
63
+ :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'"
64
+ />
65
+ <KpiCard
66
+ label="Compliance Rate"
67
+ :value="`${compliance_rate}%`"
68
+ :icon="'M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z'"
69
+ />
70
+ <KpiCard
71
+ label="First Response Breaches"
72
+ :value="firstResponseBreaches"
73
+ :icon="'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z'"
74
+ />
75
+ <KpiCard
76
+ label="Resolution Breaches"
77
+ :value="resolutionBreaches"
78
+ :icon="'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'"
79
+ />
80
+ </div>
81
+
82
+ <!-- Compliance by Policy -->
83
+ <div class="mb-8">
84
+ <SlaComplianceChart :data="compliance_by_policy" />
85
+ </div>
86
+
87
+ <!-- Breach Details Table -->
88
+ <div class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)]">
89
+ <div class="border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-4 py-3">
90
+ <h3 class="text-sm font-semibold text-[var(--esc-panel-text-secondary)]">Breach Details</h3>
91
+ </div>
92
+ <table class="w-full">
93
+ <thead>
94
+ <tr class="border-b border-[var(--esc-panel-border)]">
95
+ <th
96
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
97
+ >
98
+ Ticket
99
+ </th>
100
+ <th
101
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
102
+ >
103
+ Subject
104
+ </th>
105
+ <th
106
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
107
+ >
108
+ SLA Policy
109
+ </th>
110
+ <th
111
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
112
+ >
113
+ Breach Type
114
+ </th>
115
+ <th
116
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
117
+ >
118
+ Created
119
+ </th>
120
+ </tr>
121
+ </thead>
122
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
123
+ <tr v-for="ticket in breaches" :key="ticket.id" class="hover:bg-[var(--esc-panel-hover)]">
124
+ <td class="px-4 py-3 text-sm font-mono text-[var(--esc-panel-accent)]">
125
+ {{ ticket.reference }}
126
+ </td>
127
+ <td class="max-w-xs truncate px-4 py-3 text-sm text-[var(--esc-panel-text-secondary)]">
128
+ {{ ticket.subject }}
129
+ </td>
130
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
131
+ {{ ticket.sla_policy?.name || '—' }}
132
+ </td>
133
+ <td class="px-4 py-3">
134
+ <div class="flex gap-1.5">
135
+ <span
136
+ v-if="ticket.sla_first_response_breached"
137
+ class="inline-flex rounded-full border border-red-500/20 bg-red-500/10 px-2 py-0.5 text-xs font-medium text-red-400"
138
+ >
139
+ Response
140
+ </span>
141
+ <span
142
+ v-if="ticket.sla_resolution_breached"
143
+ class="inline-flex rounded-full border border-red-500/20 bg-red-500/10 px-2 py-0.5 text-xs font-medium text-red-400"
144
+ >
145
+ Resolution
146
+ </span>
147
+ </div>
148
+ </td>
149
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-muted)]">
150
+ {{ ticket.created_at ? new Date(ticket.created_at).toLocaleDateString() : '—' }}
151
+ </td>
152
+ </tr>
153
+ <tr v-if="!breaches?.length">
154
+ <td colspan="5" class="px-4 py-8 text-center text-sm text-[var(--esc-panel-text-muted)]">
155
+ No SLA breaches in this period.
156
+ </td>
157
+ </tr>
158
+ </tbody>
159
+ </table>
160
+ </div>
161
+ </EscalatedLayout>
162
+ </template>
@@ -0,0 +1,183 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import KpiCard from '../../../components/KpiCard.vue';
4
+ import ReportChart from '../../../components/ReportChart.vue';
5
+ import PeriodSelector from '../../../components/PeriodSelector.vue';
6
+ import ReportExportButton from '../../../components/ReportExportButton.vue';
7
+ import { router, Link } from '@inertiajs/vue3';
8
+ import { ref, computed } from 'vue';
9
+
10
+ const props = defineProps({
11
+ period_days: { type: Number, default: 30 },
12
+ breach_trend: { type: Array, default: () => [] },
13
+ breach_by_type_trend: { type: Array, default: () => [] },
14
+ breach_by_department: { type: Array, default: () => [] },
15
+ breach_by_priority: { type: Array, default: () => [] },
16
+ at_risk_tickets: { type: Array, default: () => [] },
17
+ total_breaches: { type: Number, default: 0 },
18
+ breach_rate: { type: Number, default: 0 },
19
+ first_response_breaches: { type: Number, default: 0 },
20
+ resolution_breaches: { type: Number, default: 0 },
21
+ });
22
+
23
+ const periodDays = ref(props.period_days);
24
+
25
+ function changePeriod(days) {
26
+ periodDays.value = days;
27
+ router.get(route('escalated.admin.reports.sla-trends'), { days }, { preserveState: true });
28
+ }
29
+
30
+ const riskCategories = computed(() => {
31
+ const tickets = props.at_risk_tickets || [];
32
+ return {
33
+ critical: tickets.filter((t) => t.hours_remaining <= 1),
34
+ warning: tickets.filter((t) => t.hours_remaining > 1 && t.hours_remaining <= 4),
35
+ watch: tickets.filter((t) => t.hours_remaining > 4 && t.hours_remaining <= 8),
36
+ };
37
+ });
38
+ </script>
39
+
40
+ <template>
41
+ <EscalatedLayout title="SLA Breach Trends">
42
+ <div class="mb-6 flex items-center justify-between">
43
+ <PeriodSelector :model-value="periodDays" @update:model-value="changePeriod" />
44
+ <div class="flex items-center gap-3">
45
+ <ReportExportButton report-name="sla-trends" :period-days="periodDays" />
46
+ <Link
47
+ :href="route('escalated.admin.reports')"
48
+ class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
49
+ >
50
+ &larr; Back
51
+ </Link>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- KPI Cards -->
56
+ <div class="mb-6 grid grid-cols-2 gap-4 md:grid-cols-4">
57
+ <KpiCard
58
+ label="Total Breaches"
59
+ :value="total_breaches"
60
+ :icon="'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'"
61
+ />
62
+ <KpiCard
63
+ label="Breach Rate"
64
+ :value="`${breach_rate}%`"
65
+ :icon="'M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75z'"
66
+ />
67
+ <KpiCard
68
+ label="FRT Breaches"
69
+ :value="first_response_breaches"
70
+ :icon="'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z'"
71
+ />
72
+ <KpiCard
73
+ label="Resolution Breaches"
74
+ :value="resolution_breaches"
75
+ :icon="'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z'"
76
+ />
77
+ </div>
78
+
79
+ <!-- Charts -->
80
+ <div class="mb-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
81
+ <ReportChart type="line" title="Breach Count Over Time" :data="breach_trend" :colors="['#ef4444']" />
82
+ <ReportChart
83
+ type="stacked-bar"
84
+ title="First Response vs Resolution Breaches"
85
+ :data="breach_by_type_trend"
86
+ :colors="['#f59e0b', '#ef4444']"
87
+ />
88
+ <ReportChart
89
+ type="bar"
90
+ title="Breach Rate by Department"
91
+ :data="breach_by_department"
92
+ :colors="['#8b5cf6']"
93
+ />
94
+ <ReportChart type="bar" title="Breach Rate by Priority" :data="breach_by_priority" :colors="['#06b6d4']" />
95
+ </div>
96
+
97
+ <!-- At Risk Panel -->
98
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5">
99
+ <h3 class="mb-4 text-sm font-semibold text-[var(--esc-panel-text-secondary)]">
100
+ At Risk - Approaching SLA Breach
101
+ </h3>
102
+ <div v-if="at_risk_tickets?.length" class="space-y-4">
103
+ <!-- Critical: < 1h -->
104
+ <div v-if="riskCategories.critical.length">
105
+ <div class="mb-2 flex items-center gap-2">
106
+ <span class="inline-block h-2 w-2 rounded-full bg-red-500"></span>
107
+ <span class="text-xs font-semibold uppercase tracking-wider text-red-400"
108
+ >Critical (&lt;1h)</span
109
+ >
110
+ </div>
111
+ <div class="space-y-1">
112
+ <div
113
+ v-for="ticket in riskCategories.critical"
114
+ :key="ticket.id"
115
+ class="flex items-center justify-between rounded-lg bg-red-500/5 px-3 py-2 border border-red-500/10"
116
+ >
117
+ <div class="flex items-center gap-3">
118
+ <span class="font-mono text-xs text-red-400">{{ ticket.reference }}</span>
119
+ <span class="truncate text-sm text-[var(--esc-panel-text-secondary)]">{{
120
+ ticket.subject
121
+ }}</span>
122
+ </div>
123
+ <span class="text-xs font-bold text-red-400">{{ ticket.hours_remaining }}h remaining</span>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <!-- Warning: 1-4h -->
129
+ <div v-if="riskCategories.warning.length">
130
+ <div class="mb-2 flex items-center gap-2">
131
+ <span class="inline-block h-2 w-2 rounded-full bg-amber-500"></span>
132
+ <span class="text-xs font-semibold uppercase tracking-wider text-amber-400"
133
+ >Warning (1-4h)</span
134
+ >
135
+ </div>
136
+ <div class="space-y-1">
137
+ <div
138
+ v-for="ticket in riskCategories.warning"
139
+ :key="ticket.id"
140
+ class="flex items-center justify-between rounded-lg bg-amber-500/5 px-3 py-2 border border-amber-500/10"
141
+ >
142
+ <div class="flex items-center gap-3">
143
+ <span class="font-mono text-xs text-amber-400">{{ ticket.reference }}</span>
144
+ <span class="truncate text-sm text-[var(--esc-panel-text-secondary)]">{{
145
+ ticket.subject
146
+ }}</span>
147
+ </div>
148
+ <span class="text-xs font-bold text-amber-400"
149
+ >{{ ticket.hours_remaining }}h remaining</span
150
+ >
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <!-- Watch: 4-8h -->
156
+ <div v-if="riskCategories.watch.length">
157
+ <div class="mb-2 flex items-center gap-2">
158
+ <span class="inline-block h-2 w-2 rounded-full bg-blue-500"></span>
159
+ <span class="text-xs font-semibold uppercase tracking-wider text-blue-400">Watch (4-8h)</span>
160
+ </div>
161
+ <div class="space-y-1">
162
+ <div
163
+ v-for="ticket in riskCategories.watch"
164
+ :key="ticket.id"
165
+ class="flex items-center justify-between rounded-lg bg-blue-500/5 px-3 py-2 border border-blue-500/10"
166
+ >
167
+ <div class="flex items-center gap-3">
168
+ <span class="font-mono text-xs text-blue-400">{{ ticket.reference }}</span>
169
+ <span class="truncate text-sm text-[var(--esc-panel-text-secondary)]">{{
170
+ ticket.subject
171
+ }}</span>
172
+ </div>
173
+ <span class="text-xs font-bold text-blue-400">{{ ticket.hours_remaining }}h remaining</span>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ <div v-else class="py-6 text-center text-sm text-[var(--esc-panel-text-muted)]">
179
+ No tickets currently at risk of breaching SLA.
180
+ </div>
181
+ </div>
182
+ </EscalatedLayout>
183
+ </template>