@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,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>