@escalated-dev/escalated 0.7.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 (179) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +31 -0
  3. package/package.json +12 -6
  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 +29 -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 +79 -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 +80 -80
  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/ar.json +727 -0
  91. package/src/locales/de.json +678 -430
  92. package/src/locales/en.json +717 -455
  93. package/src/locales/es.json +679 -430
  94. package/src/locales/fr.json +680 -431
  95. package/src/locales/index.js +12 -2
  96. package/src/locales/it.json +727 -0
  97. package/src/locales/ja.json +727 -0
  98. package/src/locales/ko.json +727 -0
  99. package/src/locales/nl.json +727 -0
  100. package/src/locales/pl.json +727 -0
  101. package/src/locales/pt-BR.json +727 -0
  102. package/src/locales/ru.json +727 -0
  103. package/src/locales/tr.json +727 -0
  104. package/src/locales/zh-CN.json +727 -0
  105. package/src/pages/Admin/ApiTokens/Index.vue +381 -381
  106. package/src/pages/Admin/AuditLog/Index.vue +154 -154
  107. package/src/pages/Admin/BusinessHours/Form.vue +172 -172
  108. package/src/pages/Admin/BusinessHours/Index.vue +113 -113
  109. package/src/pages/Admin/CannedResponses/Index.vue +127 -127
  110. package/src/pages/Admin/Capacity/Index.vue +134 -134
  111. package/src/pages/Admin/CustomFields/Form.vue +195 -195
  112. package/src/pages/Admin/CustomFields/Index.vue +134 -134
  113. package/src/pages/Admin/CustomObjects/Form.vue +202 -202
  114. package/src/pages/Admin/CustomObjects/Index.vue +127 -127
  115. package/src/pages/Admin/CustomObjects/Records.vue +228 -228
  116. package/src/pages/Admin/Departments/Form.vue +75 -75
  117. package/src/pages/Admin/Departments/Index.vue +110 -109
  118. package/src/pages/Admin/EscalationRules/Form.vue +157 -157
  119. package/src/pages/Admin/EscalationRules/Index.vue +178 -178
  120. package/src/pages/Admin/Import/Connect.vue +0 -1
  121. package/src/pages/Admin/Import/Index.vue +1 -1
  122. package/src/pages/Admin/Import/Progress.vue +1 -1
  123. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
  124. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
  125. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
  126. package/src/pages/Admin/Macros/Index.vue +448 -448
  127. package/src/pages/Admin/Plugins/Index.vue +327 -327
  128. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  129. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
  130. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  131. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  132. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  133. package/src/pages/Admin/Reports/CsatReport.vue +127 -127
  134. package/src/pages/Admin/Reports/Dashboard.vue +36 -21
  135. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  136. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  137. package/src/pages/Admin/Reports/SlaReport.vue +162 -166
  138. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  139. package/src/pages/Admin/Reports.vue +206 -206
  140. package/src/pages/Admin/Roles/Form.vue +71 -71
  141. package/src/pages/Admin/Roles/Index.vue +122 -122
  142. package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
  143. package/src/pages/Admin/Settings/DataRetention.vue +182 -182
  144. package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
  145. package/src/pages/Admin/Settings/Sandbox.vue +135 -135
  146. package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
  147. package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
  148. package/src/pages/Admin/Settings.vue +1008 -468
  149. package/src/pages/Admin/Skills/Form.vue +53 -53
  150. package/src/pages/Admin/Skills/Index.vue +94 -94
  151. package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
  152. package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
  153. package/src/pages/Admin/Statuses/Form.vue +114 -114
  154. package/src/pages/Admin/Statuses/Index.vue +153 -153
  155. package/src/pages/Admin/Tags/Index.vue +191 -191
  156. package/src/pages/Admin/Tickets/Index.vue +126 -91
  157. package/src/pages/Admin/Tickets/Show.vue +29 -0
  158. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
  159. package/src/pages/Admin/Webhooks/Form.vue +110 -110
  160. package/src/pages/Admin/Webhooks/Index.vue +136 -136
  161. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  162. package/src/pages/Admin/Workflows/Index.vue +235 -0
  163. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  164. package/src/pages/Agent/TicketIndex.vue +112 -88
  165. package/src/pages/Agent/TicketShow.vue +192 -60
  166. package/src/pages/Customer/Create.vue +1 -1
  167. package/src/pages/Customer/Index.vue +31 -31
  168. package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
  169. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
  170. package/src/pages/Customer/Show.vue +69 -63
  171. package/src/pages/Guest/Create.vue +40 -15
  172. package/src/pages/Guest/Show.vue +124 -93
  173. package/src/plugin.js +25 -0
  174. package/src/utils/mentionHighlight.js +24 -0
  175. package/src/utils/sanitizeHtml.js +78 -64
  176. package/src/widget/EscalatedWidget.vue +804 -0
  177. package/src/widget/index.js +254 -0
  178. package/src/pages/Admin/Automations/Form.vue +0 -184
  179. 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>