@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,97 +1,166 @@
1
- <script setup>
2
- import { ref, computed, inject } from 'vue';
3
- import { router } from '@inertiajs/vue3';
4
-
5
- const props = defineProps({
6
- selectedIds: { type: Array, required: true },
7
- bulkRoute: { type: String, required: true },
8
- agents: { type: Array, default: () => [] },
9
- departments: { type: Array, default: () => [] },
10
- });
11
-
12
- const emit = defineEmits(['clear']);
13
- const escDark = inject('esc-dark', computed(() => false));
14
- const processing = ref(false);
15
-
16
- function performAction(action, value) {
17
- if (!props.selectedIds.length || processing.value) return;
18
- processing.value = true;
19
- router.post(props.bulkRoute, {
20
- ticket_ids: props.selectedIds,
21
- action,
22
- value,
23
- }, {
24
- preserveScroll: true,
25
- onSuccess: () => emit('clear'),
26
- onFinish: () => { processing.value = false; },
27
- });
28
- }
29
-
30
- const statusOptions = [
31
- { value: 'in_progress', label: 'In Progress' },
32
- { value: 'waiting_on_customer', label: 'Waiting on Customer' },
33
- { value: 'resolved', label: 'Resolved' },
34
- { value: 'closed', label: 'Closed' },
35
- ];
36
-
37
- const priorityOptions = [
38
- { value: 'low', label: 'Low' },
39
- { value: 'medium', label: 'Medium' },
40
- { value: 'high', label: 'High' },
41
- { value: 'urgent', label: 'Urgent' },
42
- { value: 'critical', label: 'Critical' },
43
- ];
44
-
45
- const selectClass = computed(() => escDark.value
46
- ? 'rounded-lg border border-white/10 bg-neutral-950 px-2 py-1.5 text-sm text-neutral-200 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10'
47
- : 'rounded-md border border-gray-300 bg-white px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none'
48
- );
49
- </script>
50
-
51
- <template>
52
- <Transition enter-active-class="transition-all duration-200 ease-out" enter-from-class="translate-y-full opacity-0" enter-to-class="translate-y-0 opacity-100"
53
- leave-active-class="transition-all duration-150 ease-in" leave-from-class="translate-y-0 opacity-100" leave-to-class="translate-y-full opacity-0">
54
- <div v-if="selectedIds.length > 0"
55
- :class="['fixed bottom-0 left-0 right-0 z-50 border-t px-6 py-3',
56
- escDark ? 'border-white/[0.06] bg-neutral-950/95 backdrop-blur-xl' : 'border-gray-200 bg-white/95 backdrop-blur-xl shadow-lg']">
57
- <div class="mx-auto flex max-w-7xl items-center gap-4">
58
- <span :class="['text-sm font-semibold', escDark ? 'text-white' : 'text-gray-900']">
59
- {{ selectedIds.length }} ticket{{ selectedIds.length !== 1 ? 's' : '' }} selected
60
- </span>
61
-
62
- <select @change="performAction('status', $event.target.value); $event.target.value = ''" :class="selectClass" :disabled="processing">
63
- <option value="">Status...</option>
64
- <option v-for="o in statusOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
65
- </select>
66
-
67
- <select @change="performAction('priority', $event.target.value); $event.target.value = ''" :class="selectClass" :disabled="processing">
68
- <option value="">Priority...</option>
69
- <option v-for="o in priorityOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
70
- </select>
71
-
72
- <select v-if="agents.length" @change="performAction('assign', $event.target.value); $event.target.value = ''" :class="selectClass" :disabled="processing">
73
- <option value="">Assign to...</option>
74
- <option v-for="a in agents" :key="a.id" :value="a.id">{{ a.name }}</option>
75
- </select>
76
-
77
- <select v-if="departments.length" @change="performAction('department', $event.target.value); $event.target.value = ''" :class="selectClass" :disabled="processing">
78
- <option value="">Department...</option>
79
- <option v-for="d in departments" :key="d.id" :value="d.id">{{ d.name }}</option>
80
- </select>
81
-
82
- <button @click="performAction('delete', null)"
83
- :class="['rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
84
- escDark ? 'bg-rose-500/15 text-rose-400 ring-1 ring-rose-500/20 hover:bg-rose-500/25' : 'bg-red-50 text-red-600 ring-1 ring-red-200 hover:bg-red-100']"
85
- :disabled="processing">
86
- Delete
87
- </button>
88
-
89
- <button @click="emit('clear')"
90
- :class="['ml-auto rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
91
- escDark ? 'text-neutral-400 hover:bg-white/[0.04] hover:text-neutral-200' : 'text-gray-500 hover:bg-gray-100 hover:text-gray-700']">
92
- Deselect All
93
- </button>
94
- </div>
95
- </div>
96
- </Transition>
97
- </template>
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+ import { router } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ selectedIds: { type: Array, required: true },
7
+ bulkRoute: { type: String, required: true },
8
+ agents: { type: Array, default: () => [] },
9
+ departments: { type: Array, default: () => [] },
10
+ });
11
+
12
+ const emit = defineEmits(['clear']);
13
+ const escDark = inject(
14
+ 'esc-dark',
15
+ computed(() => false),
16
+ );
17
+ const processing = ref(false);
18
+
19
+ function performAction(action, value) {
20
+ if (!props.selectedIds.length || processing.value) return;
21
+ processing.value = true;
22
+ router.post(
23
+ props.bulkRoute,
24
+ {
25
+ ticket_ids: props.selectedIds,
26
+ action,
27
+ value,
28
+ },
29
+ {
30
+ preserveScroll: true,
31
+ onSuccess: () => emit('clear'),
32
+ onFinish: () => {
33
+ processing.value = false;
34
+ },
35
+ },
36
+ );
37
+ }
38
+
39
+ const statusOptions = [
40
+ { value: 'in_progress', label: 'In Progress' },
41
+ { value: 'waiting_on_customer', label: 'Waiting on Customer' },
42
+ { value: 'resolved', label: 'Resolved' },
43
+ { value: 'closed', label: 'Closed' },
44
+ ];
45
+
46
+ const priorityOptions = [
47
+ { value: 'low', label: 'Low' },
48
+ { value: 'medium', label: 'Medium' },
49
+ { value: 'high', label: 'High' },
50
+ { value: 'urgent', label: 'Urgent' },
51
+ { value: 'critical', label: 'Critical' },
52
+ ];
53
+
54
+ const selectClass = computed(() =>
55
+ escDark.value
56
+ ? 'rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-2 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]'
57
+ : 'rounded-md border border-gray-300 bg-white px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none',
58
+ );
59
+ </script>
60
+
61
+ <template>
62
+ <Transition
63
+ enter-active-class="transition-all duration-200 ease-out"
64
+ enter-from-class="translate-y-full opacity-0"
65
+ enter-to-class="translate-y-0 opacity-100"
66
+ leave-active-class="transition-all duration-150 ease-in"
67
+ leave-from-class="translate-y-0 opacity-100"
68
+ leave-to-class="translate-y-full opacity-0"
69
+ >
70
+ <div
71
+ v-if="selectedIds.length > 0"
72
+ :class="[
73
+ 'fixed bottom-0 left-0 right-0 z-50 border-t px-6 py-3',
74
+ escDark
75
+ ? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)]/95 backdrop-blur-xl'
76
+ : 'border-gray-200 bg-white/95 backdrop-blur-xl shadow-lg',
77
+ ]"
78
+ >
79
+ <div class="mx-auto flex max-w-7xl items-center gap-4">
80
+ <span
81
+ :class="['text-sm font-semibold', escDark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900']"
82
+ aria-live="polite"
83
+ >
84
+ {{ selectedIds.length }} ticket{{ selectedIds.length !== 1 ? 's' : '' }} selected
85
+ </span>
86
+
87
+ <select
88
+ :class="selectClass"
89
+ :disabled="processing"
90
+ aria-label="Change status"
91
+ @change="
92
+ performAction('status', $event.target.value);
93
+ $event.target.value = '';
94
+ "
95
+ >
96
+ <option value="">Status...</option>
97
+ <option v-for="o in statusOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
98
+ </select>
99
+
100
+ <select
101
+ :class="selectClass"
102
+ :disabled="processing"
103
+ aria-label="Change priority"
104
+ @change="
105
+ performAction('priority', $event.target.value);
106
+ $event.target.value = '';
107
+ "
108
+ >
109
+ <option value="">Priority...</option>
110
+ <option v-for="o in priorityOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
111
+ </select>
112
+
113
+ <select
114
+ v-if="agents.length"
115
+ :class="selectClass"
116
+ :disabled="processing"
117
+ @change="
118
+ performAction('assign', $event.target.value);
119
+ $event.target.value = '';
120
+ "
121
+ >
122
+ <option value="">Assign to...</option>
123
+ <option v-for="a in agents" :key="a.id" :value="a.id">{{ a.name }}</option>
124
+ </select>
125
+
126
+ <select
127
+ v-if="departments.length"
128
+ :class="selectClass"
129
+ :disabled="processing"
130
+ @change="
131
+ performAction('department', $event.target.value);
132
+ $event.target.value = '';
133
+ "
134
+ >
135
+ <option value="">Department...</option>
136
+ <option v-for="d in departments" :key="d.id" :value="d.id">{{ d.name }}</option>
137
+ </select>
138
+
139
+ <button
140
+ :class="[
141
+ 'rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
142
+ escDark
143
+ ? 'bg-rose-500/15 text-rose-400 ring-1 ring-rose-500/20 hover:bg-rose-500/25'
144
+ : 'bg-red-50 text-red-600 ring-1 ring-red-200 hover:bg-red-100',
145
+ ]"
146
+ :disabled="processing"
147
+ @click="performAction('delete', null)"
148
+ >
149
+ Delete
150
+ </button>
151
+
152
+ <button
153
+ :class="[
154
+ 'ml-auto rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
155
+ escDark
156
+ ? 'text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]'
157
+ : 'text-gray-500 hover:bg-gray-100 hover:text-gray-700',
158
+ ]"
159
+ @click="emit('clear')"
160
+ >
161
+ Deselect All
162
+ </button>
163
+ </div>
164
+ </div>
165
+ </Transition>
166
+ </template>
@@ -0,0 +1,89 @@
1
+ <script setup>
2
+ import { computed } from 'vue';
3
+
4
+ const props = defineProps({
5
+ categories: { type: Array, default: () => [] },
6
+ level: { type: Number, default: 0 },
7
+ parentId: { type: [Number, null], default: null },
8
+ });
9
+
10
+ defineEmits(['edit', 'delete']);
11
+
12
+ const children = computed(() => {
13
+ return props.categories.filter((c) => {
14
+ if (props.parentId === null) return !c.parent_id;
15
+ return c.parent_id === props.parentId;
16
+ });
17
+ });
18
+ </script>
19
+
20
+ <template>
21
+ <div>
22
+ <div v-for="category in children" :key="category.id">
23
+ <div
24
+ :class="[
25
+ 'flex items-center justify-between rounded-lg border border-white/[0.06] bg-neutral-900/60 px-4 py-3 mb-1.5',
26
+ ]"
27
+ :style="{ marginLeft: level * 24 + 'px' }"
28
+ >
29
+ <div class="flex items-center gap-3">
30
+ <svg
31
+ class="h-4 w-4 text-neutral-600"
32
+ fill="none"
33
+ stroke="currentColor"
34
+ stroke-width="1.5"
35
+ viewBox="0 0 24 24"
36
+ >
37
+ <path
38
+ stroke-linecap="round"
39
+ stroke-linejoin="round"
40
+ d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
41
+ />
42
+ </svg>
43
+ <div>
44
+ <span class="text-sm font-medium text-neutral-200">{{ category.name }}</span>
45
+ <span v-if="category.articles_count !== undefined" class="ml-2 text-xs text-neutral-500">
46
+ {{ category.articles_count }} articles
47
+ </span>
48
+ </div>
49
+ </div>
50
+ <div class="flex items-center gap-2">
51
+ <span class="text-xs text-neutral-600">pos: {{ category.position }}</span>
52
+ <button
53
+ type="button"
54
+ class="rounded p-1 text-neutral-500 hover:bg-white/[0.06] hover:text-neutral-300"
55
+ @click="$emit('edit', category)"
56
+ >
57
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
58
+ <path
59
+ stroke-linecap="round"
60
+ stroke-linejoin="round"
61
+ d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
62
+ />
63
+ </svg>
64
+ </button>
65
+ <button
66
+ type="button"
67
+ class="rounded p-1 text-neutral-500 hover:bg-red-500/10 hover:text-red-400"
68
+ @click="$emit('delete', category)"
69
+ >
70
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
71
+ <path
72
+ stroke-linecap="round"
73
+ stroke-linejoin="round"
74
+ d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
75
+ />
76
+ </svg>
77
+ </button>
78
+ </div>
79
+ </div>
80
+ <CategoryTree
81
+ :categories="categories"
82
+ :level="level + 1"
83
+ :parent-id="category.id"
84
+ @edit="$emit('edit', $event)"
85
+ @delete="$emit('delete', $event)"
86
+ />
87
+ </div>
88
+ </div>
89
+ </template>
@@ -0,0 +1,46 @@
1
+ <script setup>
2
+ import { computed } from 'vue';
3
+
4
+ const props = defineProps({
5
+ title: { type: String, default: '' },
6
+ data: { type: Array, default: () => [] },
7
+ color: { type: String, default: 'cyan' },
8
+ });
9
+
10
+ const colorMap = {
11
+ cyan: 'bg-cyan-500',
12
+ violet: 'bg-violet-500',
13
+ emerald: 'bg-emerald-500',
14
+ amber: 'bg-amber-500',
15
+ red: 'bg-red-500',
16
+ blue: 'bg-blue-500',
17
+ };
18
+
19
+ const barColor = computed(() => colorMap[props.color] || colorMap.cyan);
20
+
21
+ const maxValue = computed(() => {
22
+ if (!props.data.length) return 1;
23
+ return Math.max(...props.data.map((d) => d.value), 1);
24
+ });
25
+ </script>
26
+
27
+ <template>
28
+ <div class="rounded-xl border border-white/[0.06] bg-neutral-900/60 p-5">
29
+ <h3 v-if="title" class="mb-4 text-sm font-semibold text-neutral-200">{{ title }}</h3>
30
+ <div v-if="data.length" class="space-y-2.5">
31
+ <div v-for="item in data" :key="item.label" class="flex items-center gap-3">
32
+ <span class="w-20 shrink-0 truncate text-xs text-neutral-500">{{ item.label }}</span>
33
+ <div class="flex-1">
34
+ <div class="h-5 overflow-hidden rounded-full bg-white/[0.04]">
35
+ <div
36
+ :class="['h-full rounded-full transition-all', barColor]"
37
+ :style="{ width: `${(item.value / maxValue) * 100}%`, opacity: 0.8 }"
38
+ ></div>
39
+ </div>
40
+ </div>
41
+ <span class="w-10 text-right text-xs font-medium text-neutral-300">{{ item.value }}</span>
42
+ </div>
43
+ </div>
44
+ <div v-else class="py-6 text-center text-sm text-neutral-500">No data available</div>
45
+ </div>
46
+ </template>
@@ -0,0 +1,226 @@
1
+ <script setup>
2
+ import { ref, computed, inject, onMounted, onUnmounted } from 'vue';
3
+ import { useI18n } from '../composables/useI18n';
4
+
5
+ const props = defineProps({
6
+ sessionId: { type: [String, Number], default: null },
7
+ startedAt: { type: String, default: null },
8
+ customer: { type: Object, default: () => ({}) },
9
+ agents: { type: Array, default: () => [] },
10
+ departments: { type: Array, default: () => [] },
11
+ endEndpoint: { type: String, default: '' },
12
+ transferEndpoint: { type: String, default: '' },
13
+ });
14
+
15
+ const emit = defineEmits(['end-chat', 'transfer']);
16
+
17
+ const escDark = inject(
18
+ 'esc-dark',
19
+ computed(() => false),
20
+ );
21
+ const { t } = useI18n();
22
+
23
+ const showEndConfirm = ref(false);
24
+ const showTransfer = ref(false);
25
+ const elapsed = ref('0:00');
26
+ let timerInterval = null;
27
+
28
+ function updateTimer() {
29
+ if (!props.startedAt) return;
30
+ const diffMs = Date.now() - new Date(props.startedAt).getTime();
31
+ const totalSecs = Math.max(0, Math.floor(diffMs / 1000));
32
+ const mins = Math.floor(totalSecs / 60);
33
+ const secs = totalSecs % 60;
34
+ if (mins >= 60) {
35
+ const hrs = Math.floor(mins / 60);
36
+ const remMins = mins % 60;
37
+ elapsed.value = `${hrs}:${String(remMins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
38
+ } else {
39
+ elapsed.value = `${mins}:${String(secs).padStart(2, '0')}`;
40
+ }
41
+ }
42
+
43
+ onMounted(() => {
44
+ updateTimer();
45
+ timerInterval = setInterval(updateTimer, 1000);
46
+ });
47
+
48
+ onUnmounted(() => {
49
+ clearInterval(timerInterval);
50
+ });
51
+
52
+ async function confirmEndChat() {
53
+ emit('end-chat', props.sessionId);
54
+ if (props.endEndpoint) {
55
+ try {
56
+ await fetch(props.endEndpoint, {
57
+ method: 'POST',
58
+ headers: {
59
+ Accept: 'application/json',
60
+ 'Content-Type': 'application/json',
61
+ 'X-Requested-With': 'XMLHttpRequest',
62
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
63
+ },
64
+ });
65
+ } catch {
66
+ // handled by caller
67
+ }
68
+ }
69
+ showEndConfirm.value = false;
70
+ }
71
+
72
+ function transferTo(type, id) {
73
+ emit('transfer', { type, id });
74
+ if (props.transferEndpoint) {
75
+ fetch(props.transferEndpoint, {
76
+ method: 'POST',
77
+ headers: {
78
+ Accept: 'application/json',
79
+ 'Content-Type': 'application/json',
80
+ 'X-Requested-With': 'XMLHttpRequest',
81
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
82
+ },
83
+ body: JSON.stringify({ type, id }),
84
+ }).catch(() => {});
85
+ }
86
+ showTransfer.value = false;
87
+ }
88
+ </script>
89
+
90
+ <template>
91
+ <div
92
+ :class="[
93
+ 'flex flex-wrap items-center gap-3 rounded-xl border px-4 py-3',
94
+ escDark ? 'border-white/[0.06] bg-neutral-900/60' : 'border-gray-200 bg-white',
95
+ ]"
96
+ >
97
+ <!-- Chat duration -->
98
+ <div class="flex items-center gap-2">
99
+ <span :class="['h-2 w-2 animate-pulse rounded-full bg-emerald-500']"></span>
100
+ <span :class="['text-sm font-mono font-medium', escDark ? 'text-neutral-300' : 'text-gray-700']">
101
+ {{ elapsed }}
102
+ </span>
103
+ <span :class="['text-xs', escDark ? 'text-neutral-600' : 'text-gray-400']">
104
+ {{ t('chat.live') }}
105
+ </span>
106
+ </div>
107
+
108
+ <!-- Customer info -->
109
+ <div
110
+ v-if="customer.name || customer.email"
111
+ :class="['flex items-center gap-2 text-xs', escDark ? 'text-neutral-500' : 'text-gray-500']"
112
+ >
113
+ <span v-if="customer.name">{{ customer.name }}</span>
114
+ <span v-if="customer.email" class="hidden sm:inline">&middot; {{ customer.email }}</span>
115
+ <span v-if="customer.page_url" class="hidden md:inline truncate max-w-[200px]"
116
+ >&middot; {{ customer.page_url }}</span
117
+ >
118
+ </div>
119
+
120
+ <div class="ml-auto flex items-center gap-2">
121
+ <!-- Transfer -->
122
+ <div class="relative">
123
+ <button
124
+ :class="[
125
+ 'rounded-lg border px-3 py-1.5 text-sm transition-colors',
126
+ escDark
127
+ ? 'border-white/10 text-neutral-400 hover:border-white/20 hover:text-neutral-200'
128
+ : 'border-gray-300 text-gray-600 hover:border-gray-400 hover:text-gray-800',
129
+ ]"
130
+ @click="showTransfer = !showTransfer"
131
+ >
132
+ {{ t('chat.transfer') }}
133
+ </button>
134
+ <div
135
+ v-if="showTransfer"
136
+ :class="[
137
+ 'absolute right-0 top-full z-50 mt-1 w-56 rounded-lg border py-1 shadow-xl',
138
+ escDark ? 'border-white/[0.08] bg-neutral-800' : 'border-gray-200 bg-white',
139
+ ]"
140
+ >
141
+ <div
142
+ v-if="agents.length"
143
+ :class="[
144
+ 'px-3 py-1 text-[10px] font-semibold uppercase',
145
+ escDark ? 'text-neutral-600' : 'text-gray-400',
146
+ ]"
147
+ >
148
+ {{ t('chat.agents') }}
149
+ </div>
150
+ <button
151
+ v-for="agent in agents"
152
+ :key="'a-' + agent.id"
153
+ :class="[
154
+ 'block w-full px-3 py-1.5 text-left text-sm',
155
+ escDark ? 'text-neutral-300 hover:bg-white/[0.06]' : 'text-gray-700 hover:bg-gray-50',
156
+ ]"
157
+ @click="transferTo('agent', agent.id)"
158
+ >
159
+ {{ agent.name }}
160
+ </button>
161
+ <div
162
+ v-if="departments.length"
163
+ :class="[
164
+ 'mt-1 border-t px-3 py-1 text-[10px] font-semibold uppercase',
165
+ escDark ? 'border-white/[0.06] text-neutral-600' : 'border-gray-100 text-gray-400',
166
+ ]"
167
+ >
168
+ {{ t('chat.departments') }}
169
+ </div>
170
+ <button
171
+ v-for="dept in departments"
172
+ :key="'d-' + dept.id"
173
+ :class="[
174
+ 'block w-full px-3 py-1.5 text-left text-sm',
175
+ escDark ? 'text-neutral-300 hover:bg-white/[0.06]' : 'text-gray-700 hover:bg-gray-50',
176
+ ]"
177
+ @click="transferTo('department', dept.id)"
178
+ >
179
+ {{ dept.name }}
180
+ </button>
181
+ </div>
182
+ </div>
183
+
184
+ <!-- End Chat -->
185
+ <div class="relative">
186
+ <button
187
+ v-if="!showEndConfirm"
188
+ :class="[
189
+ 'rounded-lg border px-3 py-1.5 text-sm transition-colors',
190
+ escDark
191
+ ? 'border-rose-500/20 text-rose-400 hover:bg-rose-500/10'
192
+ : 'border-red-200 text-red-600 hover:bg-red-50',
193
+ ]"
194
+ @click="showEndConfirm = true"
195
+ >
196
+ {{ t('chat.end_chat') }}
197
+ </button>
198
+ <div v-else class="flex items-center gap-2">
199
+ <span :class="['text-xs', escDark ? 'text-neutral-400' : 'text-gray-500']">
200
+ {{ t('chat.end_confirm') }}
201
+ </span>
202
+ <button
203
+ :class="[
204
+ 'rounded-lg px-3 py-1.5 text-sm font-medium text-white',
205
+ escDark ? 'bg-rose-600 hover:bg-rose-500' : 'bg-red-600 hover:bg-red-500',
206
+ ]"
207
+ @click="confirmEndChat"
208
+ >
209
+ {{ t('form.confirm') }}
210
+ </button>
211
+ <button
212
+ :class="[
213
+ 'rounded-lg border px-3 py-1.5 text-sm',
214
+ escDark
215
+ ? 'border-white/10 text-neutral-400 hover:text-neutral-200'
216
+ : 'border-gray-300 text-gray-500 hover:text-gray-700',
217
+ ]"
218
+ @click="showEndConfirm = false"
219
+ >
220
+ {{ t('form.cancel') }}
221
+ </button>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ </template>