@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,125 @@
1
+ <script setup>
2
+ import { computed } from 'vue';
3
+
4
+ const props = defineProps({
5
+ modelValue: { type: String, default: '' },
6
+ });
7
+
8
+ const emit = defineEmits(['update:modelValue']);
9
+
10
+ const triggers = [
11
+ {
12
+ value: 'ticket_created',
13
+ label: 'Ticket Created',
14
+ description: 'When a new ticket is submitted',
15
+ icon: 'M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z',
16
+ },
17
+ {
18
+ value: 'ticket_updated',
19
+ label: 'Ticket Updated',
20
+ description: 'When any ticket field is modified',
21
+ icon: 'M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182',
22
+ },
23
+ {
24
+ value: 'reply_added',
25
+ label: 'Reply Added',
26
+ description: 'When a reply is posted on a ticket',
27
+ icon: 'M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z',
28
+ },
29
+ {
30
+ value: 'status_changed',
31
+ label: 'Status Changed',
32
+ description: 'When the ticket status is updated',
33
+ icon: 'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
34
+ },
35
+ {
36
+ value: 'ticket_assigned',
37
+ label: 'Ticket Assigned',
38
+ description: 'When a ticket is assigned to an agent',
39
+ icon: 'M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z',
40
+ },
41
+ {
42
+ value: 'ticket_escalated',
43
+ label: 'Ticket Escalated',
44
+ description: 'When a ticket is escalated',
45
+ icon: 'M3 4.5h14.25M3 9h9.75M3 13.5h5.25m5.25-.75L17.25 9m0 0L21 12.75M17.25 9v12',
46
+ },
47
+ {
48
+ value: 'sla_breached',
49
+ label: 'SLA Breached',
50
+ description: 'When an SLA target is breached',
51
+ 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',
52
+ },
53
+ {
54
+ value: 'sla_warning',
55
+ label: 'SLA Warning',
56
+ description: 'When an SLA target is approaching',
57
+ icon: 'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z',
58
+ },
59
+ {
60
+ value: 'chat_started',
61
+ label: 'Chat Started',
62
+ description: 'When a live chat session begins',
63
+ icon: 'M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155',
64
+ },
65
+ {
66
+ value: 'chat_ended',
67
+ label: 'Chat Ended',
68
+ description: 'When a live chat session ends',
69
+ icon: 'M15 11.25l-3-3m0 0l-3 3m3-3v7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
70
+ },
71
+ ];
72
+
73
+ const selected = computed(() => props.modelValue);
74
+
75
+ function select(value) {
76
+ emit('update:modelValue', value);
77
+ }
78
+ </script>
79
+
80
+ <template>
81
+ <div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
82
+ <button
83
+ v-for="trigger in triggers"
84
+ :key="trigger.value"
85
+ type="button"
86
+ class="group relative flex flex-col items-center rounded-xl border-2 p-4 text-center transition-all"
87
+ :class="
88
+ selected === trigger.value
89
+ ? 'border-blue-500 bg-blue-500/10 shadow-lg shadow-blue-500/10'
90
+ : 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] hover:border-[var(--esc-panel-text-muted)] hover:bg-[var(--esc-panel-hover)]'
91
+ "
92
+ @click="select(trigger.value)"
93
+ >
94
+ <div
95
+ class="mb-2 flex h-10 w-10 items-center justify-center rounded-lg"
96
+ :class="
97
+ selected === trigger.value
98
+ ? 'bg-blue-500/20 text-blue-400'
99
+ : 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-muted)] group-hover:text-[var(--esc-panel-text-secondary)]'
100
+ "
101
+ >
102
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
103
+ <path stroke-linecap="round" stroke-linejoin="round" :d="trigger.icon" />
104
+ </svg>
105
+ </div>
106
+ <span
107
+ class="text-xs font-semibold"
108
+ :class="selected === trigger.value ? 'text-blue-400' : 'text-[var(--esc-panel-text-secondary)]'"
109
+ >
110
+ {{ trigger.label }}
111
+ </span>
112
+ <span class="mt-1 text-[10px] leading-tight text-[var(--esc-panel-text-muted)]">
113
+ {{ trigger.description }}
114
+ </span>
115
+ <div
116
+ v-if="selected === trigger.value"
117
+ class="absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white"
118
+ >
119
+ <svg class="h-3 w-3" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
120
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
121
+ </svg>
122
+ </div>
123
+ </button>
124
+ </div>
125
+ </template>
@@ -0,0 +1,187 @@
1
+ import { ref, readonly } from 'vue';
2
+ import { useRealtime } from './useRealtime';
3
+
4
+ /**
5
+ * Composable for live chat functionality.
6
+ *
7
+ * Provides methods to start, send messages, end, and rate chat sessions,
8
+ * plus real-time subscription via useRealtime.
9
+ *
10
+ * @param {Object} [options]
11
+ * @param {string} [options.widgetPath] - Path prefix under which the host
12
+ * framework mounts widget routes. Defaults to `/support/widget` which
13
+ * matches every host-adapter plugin (Laravel / Rails / Django / Adonis /
14
+ * WordPress / Filament / Symfony / .NET / Go / Spring / Phoenix). On a
15
+ * NestJS backend, pass `/escalated/widget` (or read from whatever
16
+ * configuration channel the host app exposes to the client).
17
+ */
18
+ export function useChat(options = {}) {
19
+ const widgetPath = options.widgetPath ?? '/support/widget';
20
+ const connectionState = ref('disconnected'); // disconnected | connecting | connected
21
+ const messageBuffer = ref([]);
22
+
23
+ const { echoAvailable, listen } = useRealtime();
24
+
25
+ function csrfToken() {
26
+ return typeof document !== 'undefined' ? document.querySelector('meta[name="csrf-token"]')?.content || '' : '';
27
+ }
28
+
29
+ async function apiRequest(method, path, body = null) {
30
+ const opts = {
31
+ method,
32
+ headers: {
33
+ Accept: 'application/json',
34
+ 'Content-Type': 'application/json',
35
+ 'X-Requested-With': 'XMLHttpRequest',
36
+ 'X-CSRF-TOKEN': csrfToken(),
37
+ },
38
+ };
39
+ if (body) opts.body = JSON.stringify(body);
40
+ const res = await fetch(path, opts);
41
+ const data = await res.json();
42
+ if (!res.ok) throw { status: res.status, data };
43
+ return data;
44
+ }
45
+
46
+ /**
47
+ * Start a new chat session.
48
+ * @param {Object} data - { name, email, department_id, message }
49
+ * @returns {Promise<Object>} The created chat session
50
+ */
51
+ async function startChat(data) {
52
+ return apiRequest('POST', `${widgetPath}/chat/start`, data);
53
+ }
54
+
55
+ /**
56
+ * Send a message in a chat session.
57
+ * @param {string|number} sessionId
58
+ * @param {Object} body - { body, is_internal_note?, attachments? }
59
+ * @returns {Promise<Object>}
60
+ */
61
+ async function sendMessage(sessionId, body) {
62
+ const message = await apiRequest('POST', `${widgetPath}/chat/${sessionId}/messages`, body);
63
+ return message;
64
+ }
65
+
66
+ /**
67
+ * Send typing indicator (debounced by caller).
68
+ * @param {string|number} sessionId
69
+ * @returns {Promise<void>}
70
+ */
71
+ let lastTypingSent = 0;
72
+ async function sendTyping(sessionId) {
73
+ const now = Date.now();
74
+ if (now - lastTypingSent < 3000) return;
75
+ lastTypingSent = now;
76
+ try {
77
+ await apiRequest('POST', `${widgetPath}/chat/${sessionId}/typing`);
78
+ } catch {
79
+ // silently ignore typing failures
80
+ }
81
+ }
82
+
83
+ /**
84
+ * End a chat session.
85
+ * @param {string|number} sessionId
86
+ * @returns {Promise<Object>}
87
+ */
88
+ async function endChat(sessionId) {
89
+ return apiRequest('POST', `${widgetPath}/chat/${sessionId}/end`);
90
+ }
91
+
92
+ /**
93
+ * Rate a completed chat session.
94
+ * @param {string|number} sessionId
95
+ * @param {number} rating - 1-5 star rating
96
+ * @param {string} [comment]
97
+ * @returns {Promise<Object>}
98
+ */
99
+ async function rateChat(sessionId, rating, comment = '') {
100
+ return apiRequest('POST', `${widgetPath}/chat/${sessionId}/rate`, { rating, comment });
101
+ }
102
+
103
+ /**
104
+ * Check chat availability for a department.
105
+ * @param {string|number} [departmentId]
106
+ * @returns {Promise<Object>} { available, message, estimated_wait }
107
+ */
108
+ async function checkAvailability(departmentId = null) {
109
+ const query = departmentId ? `?department_id=${departmentId}` : '';
110
+ return apiRequest('GET', `${widgetPath}/chat/availability${query}`);
111
+ }
112
+
113
+ /**
114
+ * Subscribe to real-time chat events for a session.
115
+ * @param {string|number} sessionId
116
+ * @param {Object} callbacks
117
+ * @param {Function} [callbacks.onMessage]
118
+ * @param {Function} [callbacks.onTyping]
119
+ * @param {Function} [callbacks.onEnded]
120
+ * @param {Function} [callbacks.onAssigned]
121
+ * @returns {Function} Unsubscribe function
122
+ */
123
+ function subscribeToChat(sessionId, callbacks = {}) {
124
+ if (!echoAvailable.value) return () => {};
125
+
126
+ const channelName = `escalated.chat.${sessionId}`;
127
+ const unsubs = [];
128
+
129
+ if (callbacks.onMessage) {
130
+ unsubs.push(listen(channelName, '.chat.message', callbacks.onMessage));
131
+ }
132
+ if (callbacks.onTyping) {
133
+ unsubs.push(listen(channelName, '.chat.typing', callbacks.onTyping));
134
+ }
135
+ if (callbacks.onEnded) {
136
+ unsubs.push(listen(channelName, '.chat.ended', callbacks.onEnded));
137
+ }
138
+ if (callbacks.onAssigned) {
139
+ unsubs.push(listen(channelName, '.chat.assigned', callbacks.onAssigned));
140
+ }
141
+
142
+ connectionState.value = 'connected';
143
+
144
+ return () => {
145
+ unsubs.forEach((fn) => fn());
146
+ connectionState.value = 'disconnected';
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Subscribe to the chat queue channel for new incoming chats.
152
+ * @param {Object} callbacks
153
+ * @param {Function} [callbacks.onNewChat]
154
+ * @param {Function} [callbacks.onChatAccepted]
155
+ * @returns {Function} Unsubscribe function
156
+ */
157
+ function subscribeToChatQueue(callbacks = {}) {
158
+ if (!echoAvailable.value) return () => {};
159
+
160
+ const channelName = 'escalated.chat.queue';
161
+ const unsubs = [];
162
+
163
+ if (callbacks.onNewChat) {
164
+ unsubs.push(listen(channelName, '.chat.new', callbacks.onNewChat));
165
+ }
166
+ if (callbacks.onChatAccepted) {
167
+ unsubs.push(listen(channelName, '.chat.accepted', callbacks.onChatAccepted));
168
+ }
169
+
170
+ return () => {
171
+ unsubs.forEach((fn) => fn());
172
+ };
173
+ }
174
+
175
+ return {
176
+ connectionState: readonly(connectionState),
177
+ messageBuffer: readonly(messageBuffer),
178
+ startChat,
179
+ sendMessage,
180
+ sendTyping,
181
+ endChat,
182
+ rateChat,
183
+ checkAvailability,
184
+ subscribeToChat,
185
+ subscribeToChatQueue,
186
+ };
187
+ }
@@ -0,0 +1,38 @@
1
+ import { ref, watch, onUnmounted } from 'vue';
2
+
3
+ /**
4
+ * Composable that debounces a reactive search term and invokes a callback.
5
+ *
6
+ * @param {Function} callback - Called with the search term value after the debounce delay.
7
+ * @param {number} [delay=300] - Debounce delay in milliseconds.
8
+ * @returns {{ searchTerm: import('vue').Ref<string>, clearSearch: () => void }}
9
+ */
10
+ export function useDebouncedSearch(callback, delay = 300) {
11
+ const searchTerm = ref('');
12
+ let timer = null;
13
+
14
+ function clearTimer() {
15
+ if (timer !== null) {
16
+ window.clearTimeout(timer);
17
+ timer = null;
18
+ }
19
+ }
20
+
21
+ watch(searchTerm, (val) => {
22
+ clearTimer();
23
+ if (!val || val.length < 2) {
24
+ callback(null);
25
+ return;
26
+ }
27
+ timer = window.setTimeout(() => callback(val), delay);
28
+ });
29
+
30
+ function clearSearch() {
31
+ clearTimer();
32
+ searchTerm.value = '';
33
+ }
34
+
35
+ onUnmounted(clearTimer);
36
+
37
+ return { searchTerm, clearSearch };
38
+ }
@@ -1,80 +1,80 @@
1
- import { ref, computed } from 'vue';
2
- import locales from '../locales/index.js';
3
-
4
- const currentLocale = ref('en');
5
- const customMessages = ref({});
6
-
7
- function resolve(obj, path) {
8
- return path.split('.').reduce((acc, key) => acc?.[key], obj);
9
- }
10
-
11
- function applyReplacements(str, replacements) {
12
- if (!replacements) return str;
13
- return Object.entries(replacements).reduce(
14
- (result, [key, value]) => result.replace(new RegExp(`:${key}`, 'g'), value),
15
- str,
16
- );
17
- }
18
-
19
- export function setLocale(locale) {
20
- currentLocale.value = locale;
21
- }
22
-
23
- export function getLocale() {
24
- return currentLocale.value;
25
- }
26
-
27
- export function mergeMessages(locale, messages) {
28
- customMessages.value = {
29
- ...customMessages.value,
30
- [locale]: deepMerge(customMessages.value[locale] || {}, messages),
31
- };
32
- }
33
-
34
- function deepMerge(target, source) {
35
- const result = { ...target };
36
- for (const key of Object.keys(source)) {
37
- if (
38
- source[key] &&
39
- typeof source[key] === 'object' &&
40
- !Array.isArray(source[key]) &&
41
- target[key] &&
42
- typeof target[key] === 'object'
43
- ) {
44
- result[key] = deepMerge(target[key], source[key]);
45
- } else {
46
- result[key] = source[key];
47
- }
48
- }
49
- return result;
50
- }
51
-
52
- export function useI18n() {
53
- const locale = computed(() => currentLocale.value);
54
-
55
- function t(key, replacements) {
56
- const lang = currentLocale.value;
57
-
58
- // Try custom messages for current locale
59
- const custom = resolve(customMessages.value[lang], key);
60
- if (typeof custom === 'string') return applyReplacements(custom, replacements);
61
-
62
- // Try built-in messages for current locale
63
- const msg = resolve(locales[lang], key);
64
- if (typeof msg === 'string') return applyReplacements(msg, replacements);
65
-
66
- // Fallback to English
67
- if (lang !== 'en') {
68
- const customEn = resolve(customMessages.value.en, key);
69
- if (typeof customEn === 'string') return applyReplacements(customEn, replacements);
70
-
71
- const fallback = resolve(locales.en, key);
72
- if (typeof fallback === 'string') return applyReplacements(fallback, replacements);
73
- }
74
-
75
- // Return raw key
76
- return key;
77
- }
78
-
79
- return { t, locale };
80
- }
1
+ import { ref, computed } from 'vue';
2
+ import locales from '../locales/index.js';
3
+
4
+ const currentLocale = ref('en');
5
+ const customMessages = ref({});
6
+
7
+ function resolve(obj, path) {
8
+ return path.split('.').reduce((acc, key) => acc?.[key], obj);
9
+ }
10
+
11
+ function applyReplacements(str, replacements) {
12
+ if (!replacements) return str;
13
+ return Object.entries(replacements).reduce(
14
+ (result, [key, value]) => result.replace(new RegExp(`:${key}`, 'g'), value),
15
+ str,
16
+ );
17
+ }
18
+
19
+ export function setLocale(locale) {
20
+ currentLocale.value = locale;
21
+ }
22
+
23
+ export function getLocale() {
24
+ return currentLocale.value;
25
+ }
26
+
27
+ export function mergeMessages(locale, messages) {
28
+ customMessages.value = {
29
+ ...customMessages.value,
30
+ [locale]: deepMerge(customMessages.value[locale] || {}, messages),
31
+ };
32
+ }
33
+
34
+ function deepMerge(target, source) {
35
+ const result = { ...target };
36
+ for (const key of Object.keys(source)) {
37
+ if (
38
+ source[key] &&
39
+ typeof source[key] === 'object' &&
40
+ !Array.isArray(source[key]) &&
41
+ target[key] &&
42
+ typeof target[key] === 'object'
43
+ ) {
44
+ result[key] = deepMerge(target[key], source[key]);
45
+ } else {
46
+ result[key] = source[key];
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+
52
+ export function useI18n() {
53
+ const locale = computed(() => currentLocale.value);
54
+
55
+ function t(key, replacements) {
56
+ const lang = currentLocale.value;
57
+
58
+ // Try custom messages for current locale
59
+ const custom = resolve(customMessages.value[lang], key);
60
+ if (typeof custom === 'string') return applyReplacements(custom, replacements);
61
+
62
+ // Try built-in messages for current locale
63
+ const msg = resolve(locales[lang], key);
64
+ if (typeof msg === 'string') return applyReplacements(msg, replacements);
65
+
66
+ // Fallback to English
67
+ if (lang !== 'en') {
68
+ const customEn = resolve(customMessages.value.en, key);
69
+ if (typeof customEn === 'string') return applyReplacements(customEn, replacements);
70
+
71
+ const fallback = resolve(locales.en, key);
72
+ if (typeof fallback === 'string') return applyReplacements(fallback, replacements);
73
+ }
74
+
75
+ // Return raw key
76
+ return key;
77
+ }
78
+
79
+ return { t, locale };
80
+ }
@@ -1,46 +1,46 @@
1
- import { onMounted, onUnmounted, unref } from 'vue';
2
-
3
- /**
4
- * Composable for registering keyboard shortcuts.
5
- *
6
- * @param {Object} shortcuts - Map of key to handler function, e.g. { 'r': () => {}, 'n': () => {} }
7
- * @param {Object} options - Optional configuration
8
- * @param {import('vue').Ref<boolean>} options.enabled - Ref controlling whether shortcuts are active (default: true)
9
- */
10
- export function useKeyboardShortcuts(shortcuts, options = {}) {
11
- const ignoredTags = new Set(['INPUT', 'TEXTAREA', 'SELECT']);
12
-
13
- function handleKeydown(event) {
14
- // Check if shortcuts are enabled
15
- if (options.enabled !== undefined && !unref(options.enabled)) {
16
- return;
17
- }
18
-
19
- // Skip when focus is inside form elements or contenteditable
20
- const target = event.target;
21
- if (target && (ignoredTags.has(target.tagName) || target.isContentEditable)) {
22
- return;
23
- }
24
-
25
- // Skip if modifier keys are held (allow shift for ? and similar)
26
- if (event.ctrlKey || event.metaKey || event.altKey) {
27
- return;
28
- }
29
-
30
- const key = event.key;
31
- const handler = shortcuts[key];
32
-
33
- if (handler && typeof handler === 'function') {
34
- event.preventDefault();
35
- handler(event);
36
- }
37
- }
38
-
39
- onMounted(() => {
40
- document.addEventListener('keydown', handleKeydown);
41
- });
42
-
43
- onUnmounted(() => {
44
- document.removeEventListener('keydown', handleKeydown);
45
- });
46
- }
1
+ import { onMounted, onUnmounted, unref } from 'vue';
2
+
3
+ /**
4
+ * Composable for registering keyboard shortcuts.
5
+ *
6
+ * @param {Object} shortcuts - Map of key to handler function, e.g. { 'r': () => {}, 'n': () => {} }
7
+ * @param {Object} options - Optional configuration
8
+ * @param {import('vue').Ref<boolean>} options.enabled - Ref controlling whether shortcuts are active (default: true)
9
+ */
10
+ export function useKeyboardShortcuts(shortcuts, options = {}) {
11
+ const ignoredTags = new Set(['INPUT', 'TEXTAREA', 'SELECT']);
12
+
13
+ function handleKeydown(event) {
14
+ // Check if shortcuts are enabled
15
+ if (options.enabled !== undefined && !unref(options.enabled)) {
16
+ return;
17
+ }
18
+
19
+ // Skip when focus is inside form elements or contenteditable
20
+ const target = event.target;
21
+ if (target && (ignoredTags.has(target.tagName) || target.isContentEditable)) {
22
+ return;
23
+ }
24
+
25
+ // Skip if modifier keys are held (allow shift for ? and similar)
26
+ if (event.ctrlKey || event.metaKey || event.altKey) {
27
+ return;
28
+ }
29
+
30
+ const key = event.key;
31
+ const handler = shortcuts[key];
32
+
33
+ if (handler && typeof handler === 'function') {
34
+ event.preventDefault();
35
+ handler(event);
36
+ }
37
+ }
38
+
39
+ onMounted(() => {
40
+ document.addEventListener('keydown', handleKeydown);
41
+ });
42
+
43
+ onUnmounted(() => {
44
+ document.removeEventListener('keydown', handleKeydown);
45
+ });
46
+ }