@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
@@ -1,186 +1,311 @@
1
- <script setup>
2
- import { computed, defineAsyncComponent } from 'vue';
3
- import { Link } from '@inertiajs/vue3';
4
- import EscalatedLayout from '../../components/EscalatedLayout.vue';
5
- import StatsCard from '../../components/StatsCard.vue';
6
- import StatusBadge from '../../components/StatusBadge.vue';
7
- import PriorityBadge from '../../components/PriorityBadge.vue';
8
- import PluginSlot from '../../components/PluginSlot.vue';
9
- import { usePluginExtensions } from '../../composables/usePluginExtensions';
10
-
11
- const props = defineProps({
12
- stats: Object,
13
- recentTickets: Array,
14
- needsAttention: { type: Object, default: () => ({}) },
15
- myPerformance: { type: Object, default: () => ({}) },
16
- });
17
-
18
- function timeAgo(date) {
19
- if (!date) return '';
20
- const diff = Date.now() - new Date(date).getTime();
21
- const mins = Math.floor(diff / 60000);
22
- if (mins < 60) return `${mins}m ago`;
23
- const hrs = Math.floor(mins / 60);
24
- if (hrs < 24) return `${hrs}h ago`;
25
- const days = Math.floor(hrs / 24);
26
- return `${days}d ago`;
27
- }
28
-
29
- function slaClass(ticket) {
30
- if (ticket.sla_first_response_breached || ticket.sla_resolution_breached) return 'bg-rose-500';
31
- if (ticket.first_response_due_at || ticket.resolution_due_at) {
32
- const due = ticket.resolution_due_at || ticket.first_response_due_at;
33
- const mins = (new Date(due) - Date.now()) / 60000;
34
- if (mins < 30) return 'bg-amber-500';
35
- return 'bg-emerald-500';
36
- }
37
- return '';
38
- }
39
-
40
- function truncate(str, len = 50) {
41
- if (!str) return '';
42
- return str.length > len ? str.slice(0, len) + '...' : str;
43
- }
44
-
45
- const hasSlaBreaching = computed(() => needsAttention.value?.sla_breaching?.length > 0);
46
- const hasUnassignedUrgent = computed(() => needsAttention.value?.unassigned_urgent?.length > 0);
47
- const needsAttention = computed(() => props.needsAttention);
48
-
49
- const { dashboardWidgets, getPageComponents } = usePluginExtensions();
50
-
51
- // Load a plugin widget component dynamically
52
- const loadWidgetComponent = (plugin, component) => {
53
- return defineAsyncComponent(() =>
54
- import(`../../../../../plugins/escalated/${plugin}/resources/js/Components/${component}.vue`)
55
- );
56
- };
57
- </script>
58
-
59
- <template>
60
- <EscalatedLayout title="Agent Dashboard">
61
- <!-- Plugin: dashboard header slot -->
62
- <PluginSlot slot="dashboard.header" :components="getPageComponents('dashboard', 'header')" />
63
-
64
- <!-- Stats row -->
65
- <div class="mb-8 grid grid-cols-2 gap-4 md:grid-cols-5">
66
- <StatsCard label="Open Tickets" :value="stats.open" color="cyan" />
67
- <StatsCard label="My Assigned" :value="stats.my_assigned" color="violet" />
68
- <StatsCard label="Unassigned" :value="stats.unassigned" color="amber" />
69
- <StatsCard label="SLA Breached" :value="stats.sla_breached" color="red" />
70
- <StatsCard label="Resolved Today" :value="stats.resolved_today" color="green" />
71
- </div>
72
-
73
- <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
74
- <div class="lg:col-span-2 space-y-6">
75
- <!-- Needs Attention -->
76
- <div v-if="hasSlaBreaching || hasUnassignedUrgent">
77
- <h2 class="mb-4 text-lg font-semibold text-neutral-200">Needs Attention</h2>
78
- <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
79
- <div v-if="hasSlaBreaching" class="rounded-xl border border-rose-500/20 bg-rose-500/5 p-4">
80
- <h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-rose-400">
81
- <span class="h-2 w-2 rounded-full bg-rose-500"></span>
82
- SLA Breaching
83
- </h3>
84
- <div v-for="ticket in needsAttention.sla_breaching" :key="ticket.id" class="mb-2 last:mb-0">
85
- <Link :href="route('escalated.agent.tickets.show', ticket.reference)"
86
- class="flex items-center justify-between rounded-lg px-2 py-1.5 text-sm transition-colors hover:bg-white/[0.04]">
87
- <span class="font-mono text-xs text-neutral-400">{{ ticket.reference }}</span>
88
- <span class="truncate text-neutral-300" style="max-width: 140px">{{ ticket.requester?.name }}</span>
89
- </Link>
90
- </div>
91
- </div>
92
- <div v-if="hasUnassignedUrgent" class="rounded-xl border border-amber-500/20 bg-amber-500/5 p-4">
93
- <h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-amber-400">
94
- <span class="h-2 w-2 rounded-full bg-amber-500"></span>
95
- Unassigned Urgent
96
- </h3>
97
- <div v-for="ticket in needsAttention.unassigned_urgent" :key="ticket.id" class="mb-2 last:mb-0">
98
- <Link :href="route('escalated.agent.tickets.show', ticket.reference)"
99
- class="flex items-center justify-between rounded-lg px-2 py-1.5 text-sm transition-colors hover:bg-white/[0.04]">
100
- <span class="font-mono text-xs text-neutral-400">{{ ticket.reference }}</span>
101
- <PriorityBadge :priority="ticket.priority" />
102
- </Link>
103
- </div>
104
- </div>
105
- </div>
106
- </div>
107
-
108
- <!-- Recent Tickets -->
109
- <div>
110
- <h2 class="mb-4 text-lg font-semibold text-neutral-200">Recent Tickets</h2>
111
- <div class="overflow-hidden rounded-xl border border-white/[0.06] bg-neutral-900/60">
112
- <table class="min-w-full divide-y divide-white/[0.06]">
113
- <thead>
114
- <tr class="bg-white/[0.02]">
115
- <th class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-neutral-500">Reference</th>
116
- <th class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-neutral-500">Subject</th>
117
- <th class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-neutral-500">Requester</th>
118
- <th class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-neutral-500">Status</th>
119
- <th class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-neutral-500">Priority</th>
120
- <th class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-neutral-500">Assignee</th>
121
- <th class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-neutral-500">Last Reply</th>
122
- </tr>
123
- </thead>
124
- <tbody class="divide-y divide-white/[0.04]">
125
- <tr v-for="ticket in recentTickets" :key="ticket.id" class="transition-colors hover:bg-white/[0.03]">
126
- <td class="whitespace-nowrap px-4 py-3 text-sm font-medium">
127
- <div class="flex items-center gap-2">
128
- <span v-if="slaClass(ticket)" :class="['h-2 w-2 shrink-0 rounded-full', slaClass(ticket)]"></span>
129
- <Link :href="route('escalated.agent.tickets.show', ticket.reference)" class="text-white hover:text-neutral-300">
130
- {{ ticket.reference }}
131
- </Link>
132
- </div>
133
- </td>
134
- <td class="px-4 py-3 text-sm text-neutral-300">{{ truncate(ticket.subject) }}</td>
135
- <td class="px-4 py-3 text-sm text-neutral-400">{{ ticket.requester?.name }}</td>
136
- <td class="px-4 py-3"><StatusBadge :status="ticket.status" /></td>
137
- <td class="px-4 py-3"><PriorityBadge :priority="ticket.priority" /></td>
138
- <td class="px-4 py-3 text-sm text-neutral-500">{{ ticket.assignee?.name || 'Unassigned' }}</td>
139
- <td class="whitespace-nowrap px-4 py-3 text-sm text-neutral-600">
140
- <template v-if="ticket.latest_reply">
141
- <div class="text-neutral-400">{{ timeAgo(ticket.latest_reply.created_at) }}</div>
142
- <div class="text-xs text-neutral-600">{{ ticket.latest_reply.author?.name || '' }}</div>
143
- </template>
144
- <span v-else class="text-neutral-700">—</span>
145
- </td>
146
- </tr>
147
- <tr v-if="!recentTickets?.length">
148
- <td colspan="7" class="px-4 py-8 text-center text-sm text-neutral-500">No recent tickets</td>
149
- </tr>
150
- </tbody>
151
- </table>
152
- </div>
153
- </div>
154
- </div>
155
-
156
- <!-- My Performance sidebar -->
157
- <div class="space-y-4">
158
- <div class="rounded-xl border border-white/[0.06] bg-neutral-900/60 p-5">
159
- <h3 class="mb-4 text-sm font-semibold text-neutral-200">My Performance</h3>
160
- <dl class="space-y-3">
161
- <div class="flex items-center justify-between">
162
- <dt class="text-sm text-neutral-500">Resolved This Week</dt>
163
- <dd class="text-lg font-bold text-white">{{ myPerformance.resolved_this_week || 0 }}</dd>
164
- </div>
165
- </dl>
166
- </div>
167
- </div>
168
- </div>
169
-
170
- <!-- Plugin dashboard widgets -->
171
- <div v-if="dashboardWidgets.length" class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
172
- <div v-for="widget in dashboardWidgets" :key="widget.id"
173
- :class="['rounded-xl border border-white/[0.06] bg-neutral-900/60 p-5',
174
- widget.width === 'full' ? 'md:col-span-2' : '']">
175
- <h3 v-if="widget.title" class="mb-3 text-sm font-semibold text-neutral-200">{{ widget.title }}</h3>
176
- <component
177
- :is="loadWidgetComponent(widget.plugin, widget.component)"
178
- v-bind="widget.data || {}"
179
- />
180
- </div>
181
- </div>
182
-
183
- <!-- Plugin: dashboard footer slot -->
184
- <PluginSlot slot="dashboard.footer" :components="getPageComponents('dashboard', 'footer')" />
185
- </EscalatedLayout>
186
- </template>
1
+ <script setup>
2
+ import { computed, defineAsyncComponent, inject } from 'vue';
3
+ import { Link } from '@inertiajs/vue3';
4
+ import EscalatedLayout from '../../components/EscalatedLayout.vue';
5
+ import StatsCard from '../../components/StatsCard.vue';
6
+ import StatusBadge from '../../components/StatusBadge.vue';
7
+ import PriorityBadge from '../../components/PriorityBadge.vue';
8
+ import PluginSlot from '../../components/PluginSlot.vue';
9
+ import { usePluginExtensions } from '../../composables/usePluginExtensions';
10
+ import { timeAgo, slaClass } from '../../utils/formatting';
11
+
12
+ const props = defineProps({
13
+ stats: Object,
14
+ recentTickets: Array,
15
+ needsAttention: { type: Object, default: () => ({}) },
16
+ myPerformance: { type: Object, default: () => ({}) },
17
+ });
18
+
19
+ function truncate(str, len = 50) {
20
+ if (!str) return '';
21
+ return str.length > len ? str.slice(0, len) + '...' : str;
22
+ }
23
+
24
+ const hasSlaBreaching = computed(() => needsAttention.value?.sla_breaching?.length > 0);
25
+ const hasUnassignedUrgent = computed(() => needsAttention.value?.unassigned_urgent?.length > 0);
26
+ const needsAttention = computed(() => props.needsAttention);
27
+
28
+ const escDark = inject(
29
+ 'esc-dark',
30
+ computed(() => false),
31
+ );
32
+
33
+ const { dashboardWidgets, getPageComponents } = usePluginExtensions();
34
+
35
+ // Load a plugin widget component dynamically
36
+ const loadWidgetComponent = (plugin, component) => {
37
+ return defineAsyncComponent(
38
+ () => import(`../../../../../plugins/escalated/${plugin}/resources/js/Components/${component}.vue`),
39
+ );
40
+ };
41
+ </script>
42
+
43
+ <template>
44
+ <EscalatedLayout title="Agent Dashboard">
45
+ <!-- Plugin: dashboard header slot -->
46
+ <PluginSlot slot="dashboard.header" :components="getPageComponents('dashboard', 'header')" />
47
+
48
+ <!-- Stats row -->
49
+ <div class="mb-8 grid grid-cols-2 gap-4 md:grid-cols-5">
50
+ <StatsCard label="Open Tickets" :value="stats.open" color="cyan" />
51
+ <StatsCard label="My Assigned" :value="stats.my_assigned" color="violet" />
52
+ <StatsCard label="Unassigned" :value="stats.unassigned" color="amber" />
53
+ <StatsCard label="SLA Breached" :value="stats.sla_breached" color="red" />
54
+ <StatsCard label="Resolved Today" :value="stats.resolved_today" color="green" />
55
+ </div>
56
+
57
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
58
+ <div class="lg:col-span-2 space-y-6">
59
+ <!-- Needs Attention -->
60
+ <div v-if="hasSlaBreaching || hasUnassignedUrgent">
61
+ <h2 :class="['mb-4 text-lg font-semibold', escDark ? 'text-neutral-200' : 'text-gray-900']">
62
+ Needs Attention
63
+ </h2>
64
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
65
+ <div
66
+ v-if="hasSlaBreaching"
67
+ :class="[
68
+ 'rounded-xl border p-4',
69
+ escDark ? 'border-rose-500/20 bg-rose-500/5' : 'border-rose-200 bg-rose-50',
70
+ ]"
71
+ >
72
+ <h3
73
+ :class="[
74
+ 'mb-3 flex items-center gap-2 text-sm font-semibold',
75
+ escDark ? 'text-rose-400' : 'text-rose-700',
76
+ ]"
77
+ >
78
+ <span class="h-2 w-2 rounded-full bg-rose-500"></span>
79
+ SLA Breaching
80
+ </h3>
81
+ <div v-for="ticket in needsAttention.sla_breaching" :key="ticket.id" class="mb-2 last:mb-0">
82
+ <Link
83
+ :href="route('escalated.agent.tickets.show', ticket.reference)"
84
+ :class="[
85
+ 'flex items-center justify-between rounded-lg px-2 py-1.5 text-sm transition-colors',
86
+ escDark ? 'hover:bg-white/[0.04]' : 'hover:bg-rose-100/60',
87
+ ]"
88
+ >
89
+ <span
90
+ :class="['font-mono text-xs', escDark ? 'text-neutral-400' : 'text-gray-500']"
91
+ >{{ ticket.reference }}</span
92
+ >
93
+ <span
94
+ :class="['truncate', escDark ? 'text-neutral-300' : 'text-gray-700']"
95
+ style="max-width: 140px"
96
+ >{{ ticket.requester?.name }}</span
97
+ >
98
+ </Link>
99
+ </div>
100
+ </div>
101
+ <div
102
+ v-if="hasUnassignedUrgent"
103
+ :class="[
104
+ 'rounded-xl border p-4',
105
+ escDark ? 'border-amber-500/20 bg-amber-500/5' : 'border-amber-200 bg-amber-50',
106
+ ]"
107
+ >
108
+ <h3
109
+ :class="[
110
+ 'mb-3 flex items-center gap-2 text-sm font-semibold',
111
+ escDark ? 'text-amber-400' : 'text-amber-700',
112
+ ]"
113
+ >
114
+ <span class="h-2 w-2 rounded-full bg-amber-500"></span>
115
+ Unassigned Urgent
116
+ </h3>
117
+ <div
118
+ v-for="ticket in needsAttention.unassigned_urgent"
119
+ :key="ticket.id"
120
+ class="mb-2 last:mb-0"
121
+ >
122
+ <Link
123
+ :href="route('escalated.agent.tickets.show', ticket.reference)"
124
+ :class="[
125
+ 'flex items-center justify-between rounded-lg px-2 py-1.5 text-sm transition-colors',
126
+ escDark ? 'hover:bg-white/[0.04]' : 'hover:bg-amber-100/60',
127
+ ]"
128
+ >
129
+ <span
130
+ :class="['font-mono text-xs', escDark ? 'text-neutral-400' : 'text-gray-500']"
131
+ >{{ ticket.reference }}</span
132
+ >
133
+ <PriorityBadge :priority="ticket.priority" />
134
+ </Link>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Recent Tickets -->
141
+ <div>
142
+ <h2 :class="['mb-4 text-lg font-semibold', escDark ? 'text-neutral-200' : 'text-gray-900']">
143
+ Recent Tickets
144
+ </h2>
145
+ <div
146
+ :class="[
147
+ 'overflow-hidden rounded-xl border',
148
+ escDark ? 'border-white/[0.06] bg-neutral-900/60' : 'border-gray-200 bg-white',
149
+ ]"
150
+ >
151
+ <table
152
+ :class="[
153
+ 'min-w-full',
154
+ escDark ? 'divide-y divide-white/[0.06]' : 'divide-y divide-gray-200',
155
+ ]"
156
+ >
157
+ <thead>
158
+ <tr :class="escDark ? 'bg-white/[0.02]' : 'bg-gray-50'">
159
+ <th
160
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-gray-500"
161
+ >
162
+ Reference
163
+ </th>
164
+ <th
165
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-gray-500"
166
+ >
167
+ Subject
168
+ </th>
169
+ <th
170
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-gray-500"
171
+ >
172
+ Requester
173
+ </th>
174
+ <th
175
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-gray-500"
176
+ >
177
+ Status
178
+ </th>
179
+ <th
180
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-gray-500"
181
+ >
182
+ Priority
183
+ </th>
184
+ <th
185
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-gray-500"
186
+ >
187
+ Assignee
188
+ </th>
189
+ <th
190
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-gray-500"
191
+ >
192
+ Last Reply
193
+ </th>
194
+ </tr>
195
+ </thead>
196
+ <tbody :class="escDark ? 'divide-y divide-white/[0.04]' : 'divide-y divide-gray-100'">
197
+ <tr
198
+ v-for="ticket in recentTickets"
199
+ :key="ticket.id"
200
+ :class="[
201
+ 'transition-colors',
202
+ escDark ? 'hover:bg-white/[0.03]' : 'hover:bg-gray-50',
203
+ ]"
204
+ >
205
+ <td class="whitespace-nowrap px-4 py-3 text-sm font-medium">
206
+ <div class="flex items-center gap-2">
207
+ <span
208
+ v-if="slaClass(ticket)"
209
+ :class="['h-2 w-2 shrink-0 rounded-full', slaClass(ticket)]"
210
+ ></span>
211
+ <Link
212
+ :href="route('escalated.agent.tickets.show', ticket.reference)"
213
+ :class="
214
+ escDark
215
+ ? 'text-white hover:text-neutral-300'
216
+ : 'text-gray-900 hover:text-gray-600'
217
+ "
218
+ >
219
+ {{ ticket.reference }}
220
+ </Link>
221
+ </div>
222
+ </td>
223
+ <td :class="['px-4 py-3 text-sm', escDark ? 'text-neutral-300' : 'text-gray-700']">
224
+ {{ truncate(ticket.subject) }}
225
+ </td>
226
+ <td :class="['px-4 py-3 text-sm', escDark ? 'text-neutral-400' : 'text-gray-500']">
227
+ {{ ticket.requester?.name }}
228
+ </td>
229
+ <td class="px-4 py-3"><StatusBadge :status="ticket.status" /></td>
230
+ <td class="px-4 py-3"><PriorityBadge :priority="ticket.priority" /></td>
231
+ <td :class="['px-4 py-3 text-sm', escDark ? 'text-neutral-500' : 'text-gray-500']">
232
+ {{ ticket.assignee?.name || 'Unassigned' }}
233
+ </td>
234
+ <td
235
+ :class="[
236
+ 'whitespace-nowrap px-4 py-3 text-sm',
237
+ escDark ? 'text-neutral-600' : 'text-gray-400',
238
+ ]"
239
+ >
240
+ <template v-if="ticket.latest_reply">
241
+ <div :class="escDark ? 'text-neutral-400' : 'text-gray-500'">
242
+ {{ timeAgo(ticket.latest_reply.created_at) }}
243
+ </div>
244
+ <div :class="['text-xs', escDark ? 'text-neutral-600' : 'text-gray-400']">
245
+ {{ ticket.latest_reply.author?.name || '' }}
246
+ </div>
247
+ </template>
248
+ <span v-else :class="escDark ? 'text-neutral-700' : 'text-gray-300'">—</span>
249
+ </td>
250
+ </tr>
251
+ <tr v-if="!recentTickets?.length">
252
+ <td colspan="7" class="px-4 py-8 text-center text-sm text-neutral-500">
253
+ No recent tickets
254
+ </td>
255
+ </tr>
256
+ </tbody>
257
+ </table>
258
+ </div>
259
+ </div>
260
+ </div>
261
+
262
+ <!-- My Performance sidebar -->
263
+ <div class="space-y-4">
264
+ <div
265
+ :class="[
266
+ 'rounded-xl border p-5',
267
+ escDark ? 'border-white/[0.06] bg-neutral-900/60' : 'border-gray-200 bg-white',
268
+ ]"
269
+ >
270
+ <h3 :class="['mb-4 text-sm font-semibold', escDark ? 'text-neutral-200' : 'text-gray-900']">
271
+ My Performance
272
+ </h3>
273
+ <dl class="space-y-3">
274
+ <div class="flex items-center justify-between">
275
+ <dt :class="['text-sm', escDark ? 'text-neutral-500' : 'text-gray-500']">
276
+ Resolved This Week
277
+ </dt>
278
+ <dd :class="['text-lg font-bold', escDark ? 'text-white' : 'text-gray-900']">
279
+ {{ myPerformance.resolved_this_week || 0 }}
280
+ </dd>
281
+ </div>
282
+ </dl>
283
+ </div>
284
+ </div>
285
+ </div>
286
+
287
+ <!-- Plugin dashboard widgets -->
288
+ <div v-if="dashboardWidgets.length" class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
289
+ <div
290
+ v-for="widget in dashboardWidgets"
291
+ :key="widget.id"
292
+ :class="[
293
+ 'rounded-xl border p-5',
294
+ escDark ? 'border-white/[0.06] bg-neutral-900/60' : 'border-gray-200 bg-white',
295
+ widget.width === 'full' ? 'md:col-span-2' : '',
296
+ ]"
297
+ >
298
+ <h3
299
+ v-if="widget.title"
300
+ :class="['mb-3 text-sm font-semibold', escDark ? 'text-neutral-200' : 'text-gray-900']"
301
+ >
302
+ {{ widget.title }}
303
+ </h3>
304
+ <component :is="loadWidgetComponent(widget.plugin, widget.component)" v-bind="widget.data || {}" />
305
+ </div>
306
+ </div>
307
+
308
+ <!-- Plugin: dashboard footer slot -->
309
+ <PluginSlot slot="dashboard.footer" :components="getPageComponents('dashboard', 'footer')" />
310
+ </EscalatedLayout>
311
+ </template>
@@ -1,11 +1,16 @@
1
1
  <script setup>
2
- import { ref } from 'vue';
2
+ import { ref, onMounted } from 'vue';
3
3
  import { usePage, router } from '@inertiajs/vue3';
4
4
  import EscalatedLayout from '../../components/EscalatedLayout.vue';
5
5
  import TicketList from '../../components/TicketList.vue';
6
6
  import TicketFilters from '../../components/TicketFilters.vue';
7
7
  import QuickFilters from '../../components/QuickFilters.vue';
8
8
  import BulkActionBar from '../../components/BulkActionBar.vue';
9
+ import KeyboardShortcutHelp from '../../components/KeyboardShortcutHelp.vue';
10
+ import PluginSlot from '../../components/PluginSlot.vue';
11
+ import { useKeyboardShortcuts } from '../../composables/useKeyboardShortcuts';
12
+ import { usePluginExtensions } from '../../composables/usePluginExtensions';
13
+ import { useRealtime } from '../../composables/useRealtime';
9
14
 
10
15
  defineProps({
11
16
  tickets: Object,
@@ -16,10 +21,54 @@ defineProps({
16
21
 
17
22
  const page = usePage();
18
23
  const selectedIds = ref([]);
24
+ const ticketListRef = ref(null);
25
+ const showShortcutHelp = ref(false);
26
+
27
+ useKeyboardShortcuts({
28
+ j: () => {
29
+ ticketListRef.value?.moveDown();
30
+ },
31
+ k: () => {
32
+ ticketListRef.value?.moveUp();
33
+ },
34
+ x: () => {
35
+ ticketListRef.value?.toggleSelectFocused();
36
+ },
37
+ Enter: () => {
38
+ ticketListRef.value?.openFocused();
39
+ },
40
+ '?': () => {
41
+ showShortcutHelp.value = true;
42
+ },
43
+ });
44
+
45
+ const { getPageComponents } = usePluginExtensions();
19
46
 
20
47
  function applyQuickFilter(filter) {
21
48
  router.get(route('escalated.agent.tickets.index'), filter, { preserveState: true });
22
49
  }
50
+
51
+ // Real-time updates - refresh the ticket list when new tickets arrive or statuses change
52
+ const { echoAvailable, subscribeToTickets, subscribeToAgent } = useRealtime();
53
+
54
+ onMounted(() => {
55
+ if (!echoAvailable.value) return;
56
+
57
+ subscribeToTickets({
58
+ onTicketCreated() {
59
+ router.reload({ only: ['tickets'], preserveScroll: true });
60
+ },
61
+ });
62
+
63
+ const userId = page.props.auth?.user?.id;
64
+ if (userId) {
65
+ subscribeToAgent(userId, {
66
+ onTicketAssigned() {
67
+ router.reload({ only: ['tickets'], preserveScroll: true });
68
+ },
69
+ });
70
+ }
71
+ });
23
72
  </script>
24
73
 
25
74
  <template>
@@ -27,12 +76,37 @@ function applyQuickFilter(filter) {
27
76
  <div class="mb-4">
28
77
  <QuickFilters :current-user-id="page.props.auth?.user?.id" @filter="applyQuickFilter" />
29
78
  </div>
79
+ <PluginSlot
80
+ slot="ticket.list.before_filters"
81
+ :components="getPageComponents('ticket.list', 'before_filters')"
82
+ />
30
83
  <div class="mb-6">
31
- <TicketFilters :filters="filters" :route="route('escalated.agent.tickets.index')" :departments="departments" :tags="tags" show-assignee show-following />
84
+ <TicketFilters
85
+ :filters="filters"
86
+ :route="route('escalated.agent.tickets.index')"
87
+ :departments="departments"
88
+ :tags="tags"
89
+ show-assignee
90
+ show-following
91
+ />
32
92
  </div>
33
- <TicketList :tickets="tickets" route-prefix="escalated.agent.tickets" show-assignee
34
- selectable v-model:selected-ids="selectedIds" />
35
- <BulkActionBar :selected-ids="selectedIds" :bulk-route="route('escalated.agent.tickets.bulk')"
36
- :departments="departments" @clear="selectedIds = []" />
93
+ <TicketList
94
+ ref="ticketListRef"
95
+ v-model:selected-ids="selectedIds"
96
+ :tickets="tickets"
97
+ route-prefix="escalated.agent.tickets"
98
+ :columns="['reference', 'subject', 'requester', 'status', 'priority', 'assignee', 'updated']"
99
+ selectable
100
+ navigable
101
+ show-live-badge
102
+ />
103
+ <BulkActionBar
104
+ :selected-ids="selectedIds"
105
+ :bulk-route="route('escalated.agent.tickets.bulk')"
106
+ :departments="departments"
107
+ @clear="selectedIds = []"
108
+ />
109
+ <PluginSlot slot="ticket.list.footer" :components="getPageComponents('ticket.list', 'footer')" />
110
+ <KeyboardShortcutHelp v-model:show="showShortcutHelp" context="list" />
37
111
  </EscalatedLayout>
38
112
  </template>