@escalated-dev/escalated 0.7.0 → 0.8.0

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 (181) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +36 -1
  3. package/package.json +16 -7
  4. package/src/components/ActiveChatsPanel.vue +246 -0
  5. package/src/components/ActivityTimeline.stories.js +55 -55
  6. package/src/components/ActivityTimeline.vue +83 -83
  7. package/src/components/AdminDashboard.stories.js +1423 -1095
  8. package/src/components/AgentLoadIndicator.stories.js +37 -37
  9. package/src/components/AgentLoadIndicator.vue +49 -49
  10. package/src/components/AgentPerformanceCard.vue +44 -44
  11. package/src/components/ArticleEditor.vue +82 -82
  12. package/src/components/AssigneeSelect.vue +37 -37
  13. package/src/components/AttachmentList.vue +13 -1
  14. package/src/components/AuditLogEntry.vue +95 -95
  15. package/src/components/BulkActionBar.vue +166 -166
  16. package/src/components/CategoryTree.vue +89 -89
  17. package/src/components/ChartWidget.vue +46 -46
  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 -30
  24. package/src/components/CollisionWarning.vue +68 -68
  25. package/src/components/ConditionalFieldRules.vue +135 -135
  26. package/src/components/ContextPanel.vue +66 -66
  27. package/src/components/ContextPanelSection.vue +65 -65
  28. package/src/components/CustomFieldRenderer.vue +159 -159
  29. package/src/components/EscalatedLayout.vue +35 -6
  30. package/src/components/FileDropzone.vue +57 -57
  31. package/src/components/FollowButton.stories.js +30 -30
  32. package/src/components/FollowButton.vue +88 -88
  33. package/src/components/KeyboardShortcutHelp.vue +213 -213
  34. package/src/components/KnowledgePanel.vue +122 -122
  35. package/src/components/KpiCard.stories.js +32 -32
  36. package/src/components/KpiCard.vue +88 -88
  37. package/src/components/MacroDropdown.vue +150 -150
  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 -107
  43. package/src/components/PinnedNotes.vue +129 -129
  44. package/src/components/PluginSlot.vue +68 -62
  45. package/src/components/PresenceIndicator.vue +46 -2
  46. package/src/components/PriorityBadge.stories.js +33 -33
  47. package/src/components/PriorityBadge.vue +46 -46
  48. package/src/components/ReplyComposer.vue +81 -18
  49. package/src/components/ReplyThread.vue +40 -2
  50. package/src/components/ReportChart.vue +395 -0
  51. package/src/components/ReportExportButton.vue +108 -0
  52. package/src/components/SatisfactionRating.vue +180 -180
  53. package/src/components/SaveViewDialog.vue +172 -0
  54. package/src/components/SavedViewSidebar.vue +208 -0
  55. package/src/components/ScheduleEditor.vue +104 -104
  56. package/src/components/SideConversation.vue +165 -165
  57. package/src/components/SideConversationList.vue +147 -147
  58. package/src/components/SkillTagManager.vue +83 -79
  59. package/src/components/SlaComplianceChart.vue +46 -53
  60. package/src/components/SlaTimer.stories.js +35 -35
  61. package/src/components/SlaTimer.vue +62 -62
  62. package/src/components/SnoozeButton.vue +247 -0
  63. package/src/components/StatsCard.stories.js +24 -24
  64. package/src/components/StatsCard.vue +58 -58
  65. package/src/components/StatusBadge.stories.js +53 -53
  66. package/src/components/StatusBadge.vue +78 -78
  67. package/src/components/TagSelect.vue +84 -84
  68. package/src/components/TicketFilters.vue +270 -225
  69. package/src/components/TicketLinkPanel.vue +193 -193
  70. package/src/components/TicketList.vue +655 -594
  71. package/src/components/TicketMergeDialog.vue +157 -157
  72. package/src/components/TicketSplitDialog.vue +98 -0
  73. package/src/components/TotpInput.stories.js +9 -9
  74. package/src/components/TotpInput.vue +81 -81
  75. package/src/components/TwoFactorSetup.vue +113 -113
  76. package/src/components/VariableTokenInput.vue +111 -0
  77. package/src/components/WorkflowActionConfig.vue +287 -0
  78. package/src/components/WorkflowActionList.vue +350 -0
  79. package/src/components/WorkflowConditionBuilder.vue +265 -0
  80. package/src/components/WorkflowTriggerSelector.vue +125 -0
  81. package/src/composables/useChat.js +187 -0
  82. package/src/composables/useI18n.js +30 -8
  83. package/src/composables/useKeyboardShortcuts.js +46 -46
  84. package/src/composables/useMentions.js +134 -0
  85. package/src/composables/usePluginExtensions.js +159 -159
  86. package/src/composables/usePluginHooks.js +65 -65
  87. package/src/composables/useRealtime.js +182 -0
  88. package/src/defineEscalatedPlugin.js +76 -76
  89. package/src/index.js +84 -72
  90. package/src/locales/README.md +13 -0
  91. package/src/locales/ar.json +727 -0
  92. package/src/locales/de.json +678 -430
  93. package/src/locales/en.json +717 -455
  94. package/src/locales/es.json +679 -430
  95. package/src/locales/fr.json +680 -431
  96. package/src/locales/index.js +12 -2
  97. package/src/locales/it.json +727 -0
  98. package/src/locales/ja.json +727 -0
  99. package/src/locales/ko.json +727 -0
  100. package/src/locales/nl.json +727 -0
  101. package/src/locales/pl.json +727 -0
  102. package/src/locales/pt-BR.json +727 -0
  103. package/src/locales/ru.json +727 -0
  104. package/src/locales/tr.json +727 -0
  105. package/src/locales/zh-CN.json +727 -0
  106. package/src/pages/Admin/ApiTokens/Index.vue +381 -381
  107. package/src/pages/Admin/AuditLog/Index.vue +154 -154
  108. package/src/pages/Admin/BusinessHours/Form.vue +172 -172
  109. package/src/pages/Admin/BusinessHours/Index.vue +113 -113
  110. package/src/pages/Admin/CannedResponses/Index.vue +127 -127
  111. package/src/pages/Admin/Capacity/Index.vue +134 -134
  112. package/src/pages/Admin/CustomFields/Form.vue +195 -195
  113. package/src/pages/Admin/CustomFields/Index.vue +134 -134
  114. package/src/pages/Admin/CustomObjects/Form.vue +202 -202
  115. package/src/pages/Admin/CustomObjects/Index.vue +127 -127
  116. package/src/pages/Admin/CustomObjects/Records.vue +228 -228
  117. package/src/pages/Admin/Departments/Form.vue +75 -75
  118. package/src/pages/Admin/Departments/Index.vue +110 -109
  119. package/src/pages/Admin/EscalationRules/Form.vue +157 -157
  120. package/src/pages/Admin/EscalationRules/Index.vue +178 -178
  121. package/src/pages/Admin/Import/Connect.vue +0 -1
  122. package/src/pages/Admin/Import/Index.vue +1 -1
  123. package/src/pages/Admin/Import/Progress.vue +1 -1
  124. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
  125. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
  126. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
  127. package/src/pages/Admin/Macros/Index.vue +448 -448
  128. package/src/pages/Admin/Plugins/Index.vue +327 -327
  129. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  130. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
  131. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  132. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  133. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  134. package/src/pages/Admin/Reports/CsatReport.vue +127 -127
  135. package/src/pages/Admin/Reports/Dashboard.vue +36 -21
  136. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  137. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  138. package/src/pages/Admin/Reports/SlaReport.vue +162 -166
  139. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  140. package/src/pages/Admin/Reports.vue +206 -206
  141. package/src/pages/Admin/Roles/Form.vue +71 -71
  142. package/src/pages/Admin/Roles/Index.vue +122 -122
  143. package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
  144. package/src/pages/Admin/Settings/DataRetention.vue +182 -182
  145. package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
  146. package/src/pages/Admin/Settings/Sandbox.vue +135 -135
  147. package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
  148. package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
  149. package/src/pages/Admin/Settings.vue +1008 -468
  150. package/src/pages/Admin/Skills/Form.vue +193 -53
  151. package/src/pages/Admin/Skills/Index.vue +105 -94
  152. package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
  153. package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
  154. package/src/pages/Admin/Statuses/Form.vue +114 -114
  155. package/src/pages/Admin/Statuses/Index.vue +153 -153
  156. package/src/pages/Admin/Tags/Index.vue +191 -191
  157. package/src/pages/Admin/Tickets/Index.vue +126 -91
  158. package/src/pages/Admin/Tickets/Show.vue +29 -0
  159. package/src/pages/Admin/Users/Index.vue +145 -0
  160. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
  161. package/src/pages/Admin/Webhooks/Form.vue +110 -110
  162. package/src/pages/Admin/Webhooks/Index.vue +136 -136
  163. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  164. package/src/pages/Admin/Workflows/Index.vue +235 -0
  165. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  166. package/src/pages/Agent/TicketIndex.vue +112 -88
  167. package/src/pages/Agent/TicketShow.vue +192 -60
  168. package/src/pages/Customer/Create.vue +1 -1
  169. package/src/pages/Customer/Index.vue +26 -1
  170. package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
  171. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
  172. package/src/pages/Customer/Show.vue +69 -63
  173. package/src/pages/Guest/Create.vue +40 -15
  174. package/src/pages/Guest/Show.vue +124 -93
  175. package/src/plugin.js +26 -1
  176. package/src/utils/mentionHighlight.js +24 -0
  177. package/src/utils/sanitizeHtml.js +78 -64
  178. package/src/widget/EscalatedWidget.vue +804 -0
  179. package/src/widget/index.js +254 -0
  180. package/src/pages/Admin/Automations/Form.vue +0 -184
  181. package/src/pages/Admin/Automations/Index.vue +0 -129
@@ -0,0 +1,395 @@
1
+ <script setup>
2
+ import { computed, ref, onMounted } from 'vue';
3
+
4
+ const props = defineProps({
5
+ type: {
6
+ type: String,
7
+ default: 'bar',
8
+ validator: (v) => ['line', 'bar', 'area', 'histogram', 'stacked-bar', 'pie'].includes(v),
9
+ },
10
+ data: { type: Array, default: () => [] },
11
+ title: { type: String, default: '' },
12
+ height: { type: Number, default: 240 },
13
+ colors: { type: Array, default: () => ['#06b6d4', '#8b5cf6', '#f59e0b', '#10b981', '#ef4444', '#3b82f6'] },
14
+ xLabel: { type: String, default: '' },
15
+ yLabel: { type: String, default: '' },
16
+ stacked: { type: Boolean, default: false },
17
+ });
18
+
19
+ const svgRef = ref(null);
20
+ const tooltip = ref({ show: false, x: 0, y: 0, label: '', value: '' });
21
+ const animated = ref(false);
22
+
23
+ onMounted(() => {
24
+ setTimeout(() => {
25
+ animated.value = true;
26
+ }, 50);
27
+ });
28
+
29
+ const padding = { top: 20, right: 20, bottom: 40, left: 50 };
30
+ const chartWidth = 600;
31
+ const chartHeight = computed(() => props.height);
32
+ const innerWidth = chartWidth - padding.left - padding.right;
33
+ const innerHeight = computed(() => chartHeight.value - padding.top - padding.bottom);
34
+
35
+ const maxValue = computed(() => {
36
+ if (!props.data.length) return 1;
37
+ if (props.type === 'stacked-bar') {
38
+ return Math.max(
39
+ ...props.data.map((d) => {
40
+ const vals = Array.isArray(d.values) ? d.values : [d.value || 0];
41
+ return vals.reduce((a, b) => a + b, 0);
42
+ }),
43
+ 1,
44
+ );
45
+ }
46
+ return Math.max(...props.data.map((d) => d.value || 0), 1);
47
+ });
48
+
49
+ const barWidth = computed(() => {
50
+ if (!props.data.length) return 0;
51
+ return Math.min(innerWidth / props.data.length - 4, 40);
52
+ });
53
+
54
+ const linePath = computed(() => {
55
+ if (!props.data.length) return '';
56
+ const stepX = innerWidth / Math.max(props.data.length - 1, 1);
57
+ return props.data
58
+ .map((d, i) => {
59
+ const x = padding.left + i * stepX;
60
+ const y = padding.top + innerHeight.value - (d.value / maxValue.value) * innerHeight.value;
61
+ return `${i === 0 ? 'M' : 'L'}${x},${y}`;
62
+ })
63
+ .join(' ');
64
+ });
65
+
66
+ const areaPath = computed(() => {
67
+ if (!props.data.length) return '';
68
+ const stepX = innerWidth / Math.max(props.data.length - 1, 1);
69
+ const baseline = padding.top + innerHeight.value;
70
+ const top = props.data
71
+ .map((d, i) => {
72
+ const x = padding.left + i * stepX;
73
+ const y = padding.top + innerHeight.value - (d.value / maxValue.value) * innerHeight.value;
74
+ return `${i === 0 ? 'M' : 'L'}${x},${y}`;
75
+ })
76
+ .join(' ');
77
+ const lastX = padding.left + (props.data.length - 1) * stepX;
78
+ return `${top} L${lastX},${baseline} L${padding.left},${baseline} Z`;
79
+ });
80
+
81
+ const yTicks = computed(() => {
82
+ const ticks = [];
83
+ const steps = 5;
84
+ for (let i = 0; i <= steps; i++) {
85
+ const val = Math.round((maxValue.value / steps) * i);
86
+ const y = padding.top + innerHeight.value - (val / maxValue.value) * innerHeight.value;
87
+ ticks.push({ value: val, y });
88
+ }
89
+ return ticks;
90
+ });
91
+
92
+ const pieSlices = computed(() => {
93
+ if (!props.data.length) return [];
94
+ const total = props.data.reduce((s, d) => s + (d.value || 0), 0);
95
+ if (total === 0) return [];
96
+ const cx = chartWidth / 2;
97
+ const cy = chartHeight.value / 2;
98
+ const r = Math.min(cx, cy) - 30;
99
+ let startAngle = -Math.PI / 2;
100
+ return props.data.map((d, i) => {
101
+ const angle = (d.value / total) * Math.PI * 2;
102
+ const endAngle = startAngle + angle;
103
+ const x1 = cx + r * Math.cos(startAngle);
104
+ const y1 = cy + r * Math.sin(startAngle);
105
+ const x2 = cx + r * Math.cos(endAngle);
106
+ const y2 = cy + r * Math.sin(endAngle);
107
+ const largeArc = angle > Math.PI ? 1 : 0;
108
+ const path = `M${cx},${cy} L${x1},${y1} A${r},${r} 0 ${largeArc} 1 ${x2},${y2} Z`;
109
+ const midAngle = startAngle + angle / 2;
110
+ const labelX = cx + r * 0.65 * Math.cos(midAngle);
111
+ const labelY = cy + r * 0.65 * Math.sin(midAngle);
112
+ startAngle = endAngle;
113
+ return {
114
+ path,
115
+ color: props.colors[i % props.colors.length],
116
+ label: d.label,
117
+ value: d.value,
118
+ percent: Math.round((d.value / total) * 100),
119
+ labelX,
120
+ labelY,
121
+ };
122
+ });
123
+ });
124
+
125
+ function showTooltip(event, label, value) {
126
+ const rect = svgRef.value?.getBoundingClientRect();
127
+ if (!rect) return;
128
+ tooltip.value = {
129
+ show: true,
130
+ x: event.clientX - rect.left,
131
+ y: event.clientY - rect.top - 10,
132
+ label,
133
+ value: typeof value === 'number' ? value.toLocaleString() : value,
134
+ };
135
+ }
136
+
137
+ function hideTooltip() {
138
+ tooltip.value.show = false;
139
+ }
140
+
141
+ function getBarX(index) {
142
+ const step = innerWidth / props.data.length;
143
+ return padding.left + index * step + (step - barWidth.value) / 2;
144
+ }
145
+
146
+ function getBarHeight(value) {
147
+ return (value / maxValue.value) * innerHeight.value;
148
+ }
149
+
150
+ function getBarY(value) {
151
+ return padding.top + innerHeight.value - getBarHeight(value);
152
+ }
153
+ </script>
154
+
155
+ <template>
156
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5">
157
+ <h3 v-if="title" class="mb-4 text-sm font-semibold text-[var(--esc-panel-text-secondary)]">{{ title }}</h3>
158
+ <div
159
+ v-if="!data.length"
160
+ class="flex items-center justify-center py-12 text-sm text-[var(--esc-panel-text-muted)]"
161
+ >
162
+ No data available
163
+ </div>
164
+ <div v-else class="relative">
165
+ <svg
166
+ ref="svgRef"
167
+ :viewBox="`0 0 ${chartWidth} ${chartHeight}`"
168
+ class="w-full"
169
+ preserveAspectRatio="xMidYMid meet"
170
+ >
171
+ <!-- Grid lines -->
172
+ <template v-if="type !== 'pie'">
173
+ <line
174
+ v-for="tick in yTicks"
175
+ :key="tick.value"
176
+ :x1="padding.left"
177
+ :y1="tick.y"
178
+ :x2="chartWidth - padding.right"
179
+ :y2="tick.y"
180
+ stroke="var(--esc-panel-border, #333)"
181
+ stroke-width="0.5"
182
+ stroke-dasharray="4 4"
183
+ />
184
+ <text
185
+ v-for="tick in yTicks"
186
+ :key="'label-' + tick.value"
187
+ :x="padding.left - 8"
188
+ :y="tick.y + 4"
189
+ text-anchor="end"
190
+ class="fill-[var(--esc-panel-text-muted)] text-[10px]"
191
+ >
192
+ {{ tick.value }}
193
+ </text>
194
+ </template>
195
+
196
+ <!-- Bar chart -->
197
+ <template v-if="type === 'bar' || type === 'histogram'">
198
+ <rect
199
+ v-for="(d, i) in data"
200
+ :key="i"
201
+ :x="getBarX(i)"
202
+ :y="animated ? getBarY(d.value) : padding.top + innerHeight"
203
+ :width="barWidth"
204
+ :height="animated ? getBarHeight(d.value) : 0"
205
+ :fill="d.color || colors[i % colors.length]"
206
+ rx="3"
207
+ class="transition-all duration-700 ease-out"
208
+ style="cursor: pointer"
209
+ @mouseenter="showTooltip($event, d.label, d.value)"
210
+ @mouseleave="hideTooltip"
211
+ />
212
+ <text
213
+ v-for="(d, i) in data"
214
+ :key="'x-' + i"
215
+ :x="getBarX(i) + barWidth / 2"
216
+ :y="chartHeight - 8"
217
+ text-anchor="middle"
218
+ class="fill-[var(--esc-panel-text-muted)] text-[10px]"
219
+ >
220
+ {{ d.label?.length > 8 ? d.label.slice(0, 8) + '..' : d.label }}
221
+ </text>
222
+ </template>
223
+
224
+ <!-- Stacked bar -->
225
+ <template v-if="type === 'stacked-bar'">
226
+ <template v-for="(d, i) in data" :key="i">
227
+ <rect
228
+ v-for="(val, si) in d.values || [d.value]"
229
+ :key="si"
230
+ :x="getBarX(i)"
231
+ :y="
232
+ animated
233
+ ? getBarY((d.values || [d.value]).slice(0, si + 1).reduce((a, b) => a + b, 0))
234
+ : padding.top + innerHeight
235
+ "
236
+ :width="barWidth"
237
+ :height="animated ? getBarHeight(val) : 0"
238
+ :fill="colors[si % colors.length]"
239
+ class="transition-all duration-700 ease-out"
240
+ style="cursor: pointer"
241
+ @mouseenter="showTooltip($event, d.label + ' (' + (d.seriesLabels?.[si] || si) + ')', val)"
242
+ @mouseleave="hideTooltip"
243
+ />
244
+ </template>
245
+ <text
246
+ v-for="(d, i) in data"
247
+ :key="'sx-' + i"
248
+ :x="getBarX(i) + barWidth / 2"
249
+ :y="chartHeight - 8"
250
+ text-anchor="middle"
251
+ class="fill-[var(--esc-panel-text-muted)] text-[10px]"
252
+ >
253
+ {{ d.label?.length > 8 ? d.label.slice(0, 8) + '..' : d.label }}
254
+ </text>
255
+ </template>
256
+
257
+ <!-- Line chart -->
258
+ <template v-if="type === 'line'">
259
+ <path
260
+ :d="linePath"
261
+ fill="none"
262
+ :stroke="colors[0]"
263
+ stroke-width="2.5"
264
+ stroke-linecap="round"
265
+ stroke-linejoin="round"
266
+ class="transition-all duration-700"
267
+ />
268
+ <circle
269
+ v-for="(d, i) in data"
270
+ :key="i"
271
+ :cx="padding.left + (innerWidth / Math.max(data.length - 1, 1)) * i"
272
+ :cy="
273
+ animated
274
+ ? padding.top + innerHeight - (d.value / maxValue) * innerHeight
275
+ : padding.top + innerHeight
276
+ "
277
+ r="4"
278
+ :fill="colors[0]"
279
+ class="transition-all duration-700 ease-out"
280
+ style="cursor: pointer"
281
+ @mouseenter="showTooltip($event, d.label, d.value)"
282
+ @mouseleave="hideTooltip"
283
+ />
284
+ <text
285
+ v-for="(d, i) in data"
286
+ :key="'lx-' + i"
287
+ :x="padding.left + (innerWidth / Math.max(data.length - 1, 1)) * i"
288
+ :y="chartHeight - 8"
289
+ text-anchor="middle"
290
+ class="fill-[var(--esc-panel-text-muted)] text-[10px]"
291
+ >
292
+ {{ d.label?.length > 6 ? d.label.slice(0, 6) : d.label }}
293
+ </text>
294
+ </template>
295
+
296
+ <!-- Area chart -->
297
+ <template v-if="type === 'area'">
298
+ <defs>
299
+ <linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
300
+ <stop offset="0%" :stop-color="colors[0]" stop-opacity="0.3" />
301
+ <stop offset="100%" :stop-color="colors[0]" stop-opacity="0.02" />
302
+ </linearGradient>
303
+ </defs>
304
+ <path :d="areaPath" fill="url(#areaGrad)" class="transition-all duration-700" />
305
+ <path
306
+ :d="linePath"
307
+ fill="none"
308
+ :stroke="colors[0]"
309
+ stroke-width="2"
310
+ stroke-linecap="round"
311
+ stroke-linejoin="round"
312
+ class="transition-all duration-700"
313
+ />
314
+ <circle
315
+ v-for="(d, i) in data"
316
+ :key="i"
317
+ :cx="padding.left + (innerWidth / Math.max(data.length - 1, 1)) * i"
318
+ :cy="
319
+ animated
320
+ ? padding.top + innerHeight - (d.value / maxValue) * innerHeight
321
+ : padding.top + innerHeight
322
+ "
323
+ r="3.5"
324
+ :fill="colors[0]"
325
+ class="transition-all duration-700 ease-out"
326
+ style="cursor: pointer"
327
+ @mouseenter="showTooltip($event, d.label, d.value)"
328
+ @mouseleave="hideTooltip"
329
+ />
330
+ </template>
331
+
332
+ <!-- Pie chart -->
333
+ <template v-if="type === 'pie'">
334
+ <path
335
+ v-for="(slice, i) in pieSlices"
336
+ :key="i"
337
+ :d="slice.path"
338
+ :fill="slice.color"
339
+ stroke="var(--esc-panel-surface, #111)"
340
+ stroke-width="2"
341
+ style="cursor: pointer"
342
+ @mouseenter="showTooltip($event, slice.label, `${slice.value} (${slice.percent}%)`)"
343
+ @mouseleave="hideTooltip"
344
+ />
345
+ <text
346
+ v-for="(slice, i) in pieSlices"
347
+ :key="'pl-' + i"
348
+ :x="slice.labelX"
349
+ :y="slice.labelY"
350
+ text-anchor="middle"
351
+ class="fill-white text-[10px] font-medium"
352
+ >
353
+ {{ slice.percent }}%
354
+ </text>
355
+ </template>
356
+
357
+ <!-- Axis labels -->
358
+ <text
359
+ v-if="xLabel"
360
+ :x="chartWidth / 2"
361
+ :y="chartHeight - 2"
362
+ text-anchor="middle"
363
+ class="fill-[var(--esc-panel-text-muted)] text-[11px]"
364
+ >
365
+ {{ xLabel }}
366
+ </text>
367
+ </svg>
368
+
369
+ <!-- Tooltip -->
370
+ <div
371
+ v-if="tooltip.show"
372
+ class="pointer-events-none absolute z-20 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-active)] px-3 py-2 text-xs shadow-lg"
373
+ :style="{ left: tooltip.x + 'px', top: tooltip.y + 'px', transform: 'translate(-50%, -100%)' }"
374
+ >
375
+ <div class="font-medium text-[var(--esc-panel-text)]">{{ tooltip.value }}</div>
376
+ <div class="text-[var(--esc-panel-text-muted)]">{{ tooltip.label }}</div>
377
+ </div>
378
+ </div>
379
+
380
+ <!-- Legend for stacked/pie -->
381
+ <div v-if="(type === 'pie' || type === 'stacked-bar') && data.length" class="mt-4 flex flex-wrap gap-3">
382
+ <div
383
+ v-for="(d, i) in type === 'pie' ? data : data[0]?.seriesLabels || []"
384
+ :key="i"
385
+ class="flex items-center gap-1.5"
386
+ >
387
+ <span
388
+ class="inline-block h-2.5 w-2.5 rounded-full"
389
+ :style="{ backgroundColor: colors[i % colors.length] }"
390
+ ></span>
391
+ <span class="text-xs text-[var(--esc-panel-text-muted)]">{{ type === 'pie' ? d.label : d }}</span>
392
+ </div>
393
+ </div>
394
+ </div>
395
+ </template>
@@ -0,0 +1,108 @@
1
+ <script setup>
2
+ import { ref } from 'vue';
3
+
4
+ const props = defineProps({
5
+ exportUrl: { type: String, default: '' },
6
+ reportName: { type: String, default: 'report' },
7
+ periodDays: { type: Number, default: 30 },
8
+ });
9
+
10
+ const open = ref(false);
11
+ const loading = ref(false);
12
+
13
+ async function exportAs(format) {
14
+ loading.value = true;
15
+ try {
16
+ const separator = props.exportUrl.includes('?') ? '&' : '?';
17
+ const url = props.exportUrl ? `${props.exportUrl}${separator}format=${format}&days=${props.periodDays}` : '#';
18
+
19
+ if (!props.exportUrl) {
20
+ const data = { report: props.reportName, format, period_days: props.periodDays };
21
+ const blob = new Blob([format === 'json' ? JSON.stringify(data, null, 2) : 'No export URL configured'], {
22
+ type: format === 'json' ? 'application/json' : 'text/csv',
23
+ });
24
+ const a = document.createElement('a');
25
+ a.href = URL.createObjectURL(blob);
26
+ a.download = `${props.reportName}-${props.periodDays}d.${format}`;
27
+ a.click();
28
+ URL.revokeObjectURL(a.href);
29
+ } else {
30
+ const response = await fetch(url);
31
+ const blob = await response.blob();
32
+ const a = document.createElement('a');
33
+ a.href = URL.createObjectURL(blob);
34
+ a.download = `${props.reportName}-${props.periodDays}d.${format}`;
35
+ a.click();
36
+ URL.revokeObjectURL(a.href);
37
+ }
38
+ } finally {
39
+ loading.value = false;
40
+ open.value = false;
41
+ }
42
+ }
43
+ </script>
44
+
45
+ <template>
46
+ <div class="relative">
47
+ <button
48
+ class="flex items-center gap-2 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] px-3.5 py-1.5 text-sm font-medium text-[var(--esc-panel-text-tertiary)] transition-all hover:text-[var(--esc-panel-text-secondary)]"
49
+ @click="open = !open"
50
+ >
51
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
52
+ <path
53
+ stroke-linecap="round"
54
+ stroke-linejoin="round"
55
+ d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
56
+ />
57
+ </svg>
58
+ <span>Export</span>
59
+ <svg v-if="loading" class="h-3.5 w-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
60
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
61
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
62
+ </svg>
63
+ </button>
64
+ <div
65
+ v-if="open"
66
+ class="absolute right-0 top-full z-10 mt-1 w-44 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] py-1 shadow-xl"
67
+ >
68
+ <button
69
+ class="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-[var(--esc-panel-text-secondary)] hover:bg-[var(--esc-panel-hover)]"
70
+ @click="exportAs('csv')"
71
+ >
72
+ <svg
73
+ class="h-4 w-4 text-[var(--esc-panel-text-muted)]"
74
+ fill="none"
75
+ stroke="currentColor"
76
+ stroke-width="1.5"
77
+ viewBox="0 0 24 24"
78
+ >
79
+ <path
80
+ stroke-linecap="round"
81
+ stroke-linejoin="round"
82
+ d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
83
+ />
84
+ </svg>
85
+ Export as CSV
86
+ </button>
87
+ <button
88
+ class="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-[var(--esc-panel-text-secondary)] hover:bg-[var(--esc-panel-hover)]"
89
+ @click="exportAs('json')"
90
+ >
91
+ <svg
92
+ class="h-4 w-4 text-[var(--esc-panel-text-muted)]"
93
+ fill="none"
94
+ stroke="currentColor"
95
+ stroke-width="1.5"
96
+ viewBox="0 0 24 24"
97
+ >
98
+ <path
99
+ stroke-linecap="round"
100
+ stroke-linejoin="round"
101
+ d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"
102
+ />
103
+ </svg>
104
+ Export as JSON
105
+ </button>
106
+ </div>
107
+ </div>
108
+ </template>