@escalated-dev/escalated 0.5.0 → 0.7.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 (136) hide show
  1. package/README.md +102 -0
  2. package/package.json +37 -6
  3. package/src/components/ActivityTimeline.stories.js +55 -0
  4. package/src/components/ActivityTimeline.vue +83 -66
  5. package/src/components/AdminDashboard.stories.js +1095 -0
  6. package/src/components/AgentLoadIndicator.stories.js +37 -0
  7. package/src/components/AgentLoadIndicator.vue +49 -0
  8. package/src/components/AgentPerformanceCard.vue +44 -0
  9. package/src/components/ArticleEditor.vue +82 -0
  10. package/src/components/AssigneeSelect.vue +37 -23
  11. package/src/components/AttachmentList.vue +53 -37
  12. package/src/components/AuditLogEntry.vue +95 -0
  13. package/src/components/BulkActionBar.vue +99 -30
  14. package/src/components/CategoryTree.vue +89 -0
  15. package/src/components/ChartWidget.vue +46 -0
  16. package/src/components/CollisionWarning.stories.js +30 -0
  17. package/src/components/CollisionWarning.vue +68 -0
  18. package/src/components/ConditionalFieldRules.vue +135 -0
  19. package/src/components/ContextPanel.vue +66 -0
  20. package/src/components/ContextPanelSection.vue +65 -0
  21. package/src/components/CustomFieldRenderer.vue +159 -0
  22. package/src/components/EscalatedLayout.vue +400 -69
  23. package/src/components/FileDropzone.vue +57 -41
  24. package/src/components/FollowButton.stories.js +30 -0
  25. package/src/components/FollowButton.vue +53 -28
  26. package/src/components/KeyboardShortcutHelp.vue +129 -39
  27. package/src/components/KnowledgePanel.vue +122 -0
  28. package/src/components/KpiCard.stories.js +32 -0
  29. package/src/components/KpiCard.vue +88 -0
  30. package/src/components/MacroDropdown.vue +95 -35
  31. package/src/components/PermissionMatrix.vue +107 -0
  32. package/src/components/PinnedNotes.vue +75 -33
  33. package/src/components/PresenceIndicator.vue +93 -27
  34. package/src/components/PriorityBadge.stories.js +33 -0
  35. package/src/components/PriorityBadge.vue +46 -39
  36. package/src/components/QuickFilters.vue +65 -49
  37. package/src/components/ReplyComposer.vue +273 -159
  38. package/src/components/ReplyThread.vue +78 -23
  39. package/src/components/SatisfactionRating.vue +98 -46
  40. package/src/components/ScheduleEditor.vue +104 -0
  41. package/src/components/SideConversation.vue +165 -0
  42. package/src/components/SideConversationList.vue +147 -0
  43. package/src/components/SkillTagManager.vue +79 -0
  44. package/src/components/SlaComplianceChart.vue +53 -0
  45. package/src/components/SlaTimer.stories.js +35 -0
  46. package/src/components/SlaTimer.vue +62 -49
  47. package/src/components/StatsCard.stories.js +24 -0
  48. package/src/components/StatsCard.vue +58 -46
  49. package/src/components/StatusBadge.stories.js +53 -0
  50. package/src/components/StatusBadge.vue +78 -46
  51. package/src/components/TagSelect.vue +84 -53
  52. package/src/components/TicketFilters.vue +225 -69
  53. package/src/components/TicketLinkPanel.vue +193 -0
  54. package/src/components/TicketList.vue +594 -193
  55. package/src/components/TicketMergeDialog.vue +157 -0
  56. package/src/components/TicketSidebar.vue +167 -99
  57. package/src/components/TicketTypeSelector.vue +21 -0
  58. package/src/components/TotpInput.stories.js +9 -0
  59. package/src/components/TotpInput.vue +81 -0
  60. package/src/components/TwoFactorSetup.vue +113 -0
  61. package/src/composables/useDebouncedSearch.js +38 -0
  62. package/src/composables/useI18n.js +80 -0
  63. package/src/composables/usePluginExtensions.js +95 -3
  64. package/src/composables/usePluginHooks.js +65 -0
  65. package/src/defineEscalatedPlugin.js +34 -8
  66. package/src/index.js +72 -35
  67. package/src/locales/de.json +442 -0
  68. package/src/locales/en.json +467 -0
  69. package/src/locales/es.json +442 -0
  70. package/src/locales/fr.json +442 -0
  71. package/src/locales/index.js +6 -0
  72. package/src/pages/Admin/ApiTokens/Index.vue +231 -67
  73. package/src/pages/Admin/AuditLog/Index.vue +154 -0
  74. package/src/pages/Admin/Automations/Form.vue +184 -0
  75. package/src/pages/Admin/Automations/Index.vue +129 -0
  76. package/src/pages/Admin/BusinessHours/Form.vue +172 -0
  77. package/src/pages/Admin/BusinessHours/Index.vue +113 -0
  78. package/src/pages/Admin/CannedResponses/Index.vue +127 -70
  79. package/src/pages/Admin/Capacity/Index.vue +134 -0
  80. package/src/pages/Admin/CustomFields/Form.vue +195 -0
  81. package/src/pages/Admin/CustomFields/Index.vue +134 -0
  82. package/src/pages/Admin/CustomObjects/Form.vue +202 -0
  83. package/src/pages/Admin/CustomObjects/Index.vue +127 -0
  84. package/src/pages/Admin/CustomObjects/Records.vue +228 -0
  85. package/src/pages/Admin/Departments/Form.vue +75 -51
  86. package/src/pages/Admin/Departments/Index.vue +109 -57
  87. package/src/pages/Admin/EscalationRules/Form.vue +157 -96
  88. package/src/pages/Admin/EscalationRules/Index.vue +178 -57
  89. package/src/pages/Admin/Import/Connect.vue +96 -0
  90. package/src/pages/Admin/Import/Index.vue +102 -0
  91. package/src/pages/Admin/Import/Mapping.vue +110 -0
  92. package/src/pages/Admin/Import/Progress.vue +153 -0
  93. package/src/pages/Admin/Import/Review.vue +76 -0
  94. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
  95. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
  96. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
  97. package/src/pages/Admin/Macros/Index.vue +250 -89
  98. package/src/pages/Admin/Plugins/Index.vue +205 -75
  99. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
  100. package/src/pages/Admin/Reports/CsatReport.vue +127 -0
  101. package/src/pages/Admin/Reports/Dashboard.vue +211 -0
  102. package/src/pages/Admin/Reports/SlaReport.vue +166 -0
  103. package/src/pages/Admin/Reports.vue +139 -34
  104. package/src/pages/Admin/Roles/Form.vue +71 -0
  105. package/src/pages/Admin/Roles/Index.vue +122 -0
  106. package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
  107. package/src/pages/Admin/Settings/DataRetention.vue +182 -0
  108. package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
  109. package/src/pages/Admin/Settings/Sandbox.vue +135 -0
  110. package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
  111. package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
  112. package/src/pages/Admin/Settings.vue +468 -260
  113. package/src/pages/Admin/Skills/Form.vue +53 -0
  114. package/src/pages/Admin/Skills/Index.vue +94 -0
  115. package/src/pages/Admin/SlaPolicies/Form.vue +118 -75
  116. package/src/pages/Admin/SlaPolicies/Index.vue +120 -59
  117. package/src/pages/Admin/Statuses/Form.vue +114 -0
  118. package/src/pages/Admin/Statuses/Index.vue +153 -0
  119. package/src/pages/Admin/Tags/Index.vue +191 -105
  120. package/src/pages/Admin/Tickets/Index.vue +91 -39
  121. package/src/pages/Admin/Tickets/Show.vue +101 -37
  122. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
  123. package/src/pages/Admin/Webhooks/Form.vue +110 -0
  124. package/src/pages/Admin/Webhooks/Index.vue +136 -0
  125. package/src/pages/Agent/Dashboard.vue +311 -186
  126. package/src/pages/Agent/TicketIndex.vue +88 -38
  127. package/src/pages/Agent/TicketShow.vue +258 -51
  128. package/src/pages/Customer/Create.vue +36 -9
  129. package/src/pages/Customer/Index.vue +31 -24
  130. package/src/pages/Customer/KnowledgeBase/Article.vue +127 -0
  131. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
  132. package/src/plugin.js +86 -1
  133. package/src/utils/constants.js +30 -0
  134. package/src/utils/formatting.js +35 -0
  135. package/src/utils/sanitizeHtml.js +64 -0
  136. package/src/pages/Admin/Plugins.vue +0 -129
package/README.md CHANGED
@@ -3,6 +3,11 @@
3
3
  Escalated
4
4
  </h1>
5
5
 
6
+ [![Tests](https://github.com/escalated-dev/escalated/actions/workflows/run-tests.yml/badge.svg)](https://github.com/escalated-dev/escalated/actions/workflows/run-tests.yml)
7
+ [![npm](https://img.shields.io/npm/v/@escalated-dev/escalated)](https://www.npmjs.com/package/@escalated-dev/escalated)
8
+ [![Vue 3](https://img.shields.io/badge/vue-3.x-4FC08D?logo=vue.js&logoColor=white)](https://vuejs.org/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+
6
11
  Escalated is an embeddable support ticket system with SLA tracking, escalation rules, agent workflows, and a customer portal. This repo contains all the shared frontend assets (Vue 3 + Inertia.js) used across every supported backend framework.
7
12
 
8
13
  👉 **Learn more, view demos, and compare Cloud vs Self-Hosted options at** **[https://escalated.dev](https://escalated.dev)**
@@ -19,6 +24,10 @@ Pick your framework:
19
24
  | **Rails** | [escalated-dev/escalated-rails](https://github.com/escalated-dev/escalated-rails) | `gem "escalated"` |
20
25
  | **Django** | [escalated-dev/escalated-django](https://github.com/escalated-dev/escalated-django) | `pip install escalated-django` |
21
26
  | **AdonisJS** | [escalated-dev/escalated-adonis](https://github.com/escalated-dev/escalated-adonis) | `npm install @escalated-dev/escalated-adonis` |
27
+ | **WordPress** | [escalated-dev/escalated-wordpress](https://github.com/escalated-dev/escalated-wordpress) | [Download escalated.zip](https://github.com/escalated-dev/escalated-wordpress/releases/latest) |
28
+ | **Filament** | [escalated-dev/escalated-filament](https://github.com/escalated-dev/escalated-filament) | `composer require escalated-dev/escalated-filament` |
29
+ | **React Native** | [escalated-dev/escalated-react-native](https://github.com/escalated-dev/escalated-react-native) | `npm install @escalated-dev/escalated-react-native` |
30
+ | **Flutter** | [escalated-dev/escalated-flutter](https://github.com/escalated-dev/escalated-flutter) | See [pubspec.yaml setup](https://github.com/escalated-dev/escalated-flutter#quick-start) |
22
31
 
23
32
  Each backend repo has full setup instructions — install command, migrations, config, and frontend integration.
24
33
 
@@ -125,6 +134,35 @@ app.use(EscalatedPlugin, { layout: AppLayout })
125
134
 
126
135
  All the Vue 3 + Inertia.js components that power the Escalated UI. These are identical across Laravel, Rails, Django, and AdonisJS — the backend framework renders them via Inertia.
127
136
 
137
+ ## 📸 Screenshots
138
+
139
+ > Screenshots are auto-generated from Storybook via the [component-screenshots](.github/workflows/screenshots.yml) workflow.
140
+
141
+ <p align="center">
142
+ <strong>Admin Panel (Dark)</strong><br/>
143
+ <img src="docs/assets/escalated_admin_1.png" alt="Escalated Admin Panel — dark mode with sidebar navigation, KPI cards, stats, and ticket list" width="800" />
144
+ </p>
145
+
146
+ <p align="center">
147
+ <strong>Admin Panel (Light)</strong><br/>
148
+ <img src="docs/assets/escalated_admin_3.png" alt="Escalated Admin Panel — light mode with sidebar navigation, KPI cards, stats, and ticket list" width="800" />
149
+ </p>
150
+
151
+ <p align="center">
152
+ <strong>Ticket Queue</strong><br/>
153
+ <img src="docs/assets/escalated_ticket_list.png" alt="Escalated Ticket Queue — agent ticket list with filters, search, bulk actions, and SLA indicators" width="800" />
154
+ </p>
155
+
156
+ <p align="center">
157
+ <strong>Agent Panel</strong><br/>
158
+ <img src="docs/assets/escalated_admin_2.png" alt="Escalated Agent Panel — top navigation, stats, and assigned ticket queue" width="800" />
159
+ </p>
160
+
161
+ <p align="center">
162
+ <strong>Ticket Detail View</strong><br/>
163
+ <img src="docs/assets/escalated_ticket_view.png" alt="Escalated Ticket Detail View — conversation thread, reply composer, and ticket sidebar with SLA timer" width="800" />
164
+ </p>
165
+
128
166
  ### Pages
129
167
 
130
168
  **Customer Portal** — Self-service ticket management
@@ -187,6 +225,70 @@ Reusable building blocks used across the pages above.
187
225
  |--------|-------------|
188
226
  | `EscalatedPlugin` | Vue plugin for layout injection and CSS theming |
189
227
 
228
+ ## Plugin Development
229
+
230
+ Escalated supports framework-agnostic plugins built with the [Plugin SDK](https://github.com/escalated-dev/escalated-plugin-sdk). Plugins are written once in TypeScript and work across all Escalated backends.
231
+
232
+ ### How the Frontend Plugin System Works
233
+
234
+ The frontend uses `defineEscalatedPlugin()` to register Vue components — custom admin pages, ticket sidebar widgets, or dashboard panels — that are mounted automatically when the plugin is active.
235
+
236
+ ```typescript
237
+ import { defineEscalatedPlugin } from '@escalated-dev/escalated'
238
+ import MySettingsPage from './MySettingsPage.vue'
239
+
240
+ export default defineEscalatedPlugin({
241
+ name: 'my-plugin',
242
+ pages: {
243
+ 'admin/my-plugin/settings': MySettingsPage,
244
+ },
245
+ })
246
+ ```
247
+
248
+ ### How It Connects to the Backend
249
+
250
+ The backend uses `definePlugin()` from the [Plugin SDK](https://github.com/escalated-dev/escalated-plugin-sdk) to handle TypeScript business logic — subscribing to ticket lifecycle hooks, exposing API endpoints, and persisting data. The frontend and backend entries work together as a single npm package.
251
+
252
+ ```typescript
253
+ // backend entry (index.ts)
254
+ import { definePlugin } from '@escalated-dev/plugin-sdk'
255
+
256
+ export default definePlugin({
257
+ name: 'my-plugin',
258
+ version: '1.0.0',
259
+ actions: {
260
+ 'ticket.created': async (event, ctx) => {
261
+ ctx.log.info('New ticket!', event)
262
+ },
263
+ },
264
+ })
265
+ ```
266
+
267
+ ### Quick Example: Both Entry Points
268
+
269
+ A published plugin package typically exports both:
270
+
271
+ ```
272
+ my-plugin/
273
+ index.ts ← backend: definePlugin() for TypeScript logic
274
+ frontend.ts ← frontend: defineEscalatedPlugin() for Vue components
275
+ ```
276
+
277
+ The backend framework (Laravel, Rails, Django, AdonisJS) loads `index.ts` via the [Plugin Runtime](https://github.com/escalated-dev/escalated-plugin-runtime). The Vue app imports `frontend.ts` and registers it with `app.use()`.
278
+
279
+ ### Installing Plugins
280
+
281
+ ```bash
282
+ npm install @escalated-dev/plugin-slack
283
+ npm install @escalated-dev/plugin-jira
284
+ ```
285
+
286
+ ### Resources
287
+
288
+ - [Plugin SDK](https://github.com/escalated-dev/escalated-plugin-sdk) — TypeScript SDK for building plugins
289
+ - [Plugin Runtime](https://github.com/escalated-dev/escalated-plugin-runtime) — Runtime host for plugins
290
+ - [Plugin Development Guide](https://github.com/escalated-dev/escalated-docs) — Full documentation
291
+
190
292
  ## For Package Maintainers
191
293
 
192
294
  If you're building a new backend integration, this package is available on npm:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escalated-dev/escalated",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Vue 3 + Inertia.js UI components for Escalated — the embeddable support ticket system",
5
5
  "author": "Escalated Dev <hello@escalated.dev>",
6
6
  "license": "MIT",
@@ -21,24 +21,55 @@
21
21
  "exports": {
22
22
  "./components/*": "./src/components/*",
23
23
  "./pages/*": "./src/pages/*",
24
+ "./locales": "./src/locales/index.js",
25
+ "./locales/*": "./src/locales/*",
24
26
  ".": "./src/index.js"
25
27
  },
26
28
  "files": [
27
29
  "src"
28
30
  ],
29
31
  "peerDependencies": {
30
- "vue": "^3.3.0",
31
- "@inertiajs/vue3": "^1.0.0 || ^2.0.0"
32
+ "@inertiajs/vue3": "^1.0.0 || ^2.0.0",
33
+ "vue": "^3.3.0"
32
34
  },
33
35
  "scripts": {
34
- "test": "vitest run"
36
+ "test": "vitest run",
37
+ "lint": "eslint src/",
38
+ "lint:fix": "eslint src/ --fix",
39
+ "format": "prettier --write src/",
40
+ "format:check": "prettier --check src/",
41
+ "prepare": "husky",
42
+ "storybook": "storybook dev -p 6006",
43
+ "build-storybook": "storybook build",
44
+ "screenshot": "npx playwright test --config=playwright-screenshots.config.js"
45
+ },
46
+ "lint-staged": {
47
+ "*.{js,vue}": [
48
+ "eslint --fix",
49
+ "prettier --write"
50
+ ]
35
51
  },
36
52
  "devDependencies": {
37
- "vue": "^3.5.0",
53
+ "@eslint/js": "^10.0.1",
38
54
  "@inertiajs/vue3": "^2.0.0",
55
+ "@playwright/test": "^1.50.0",
56
+ "@storybook/addon-a11y": "^10.2.12",
57
+ "@storybook/addon-docs": "^10.2.12",
58
+ "@storybook/vue3-vite": "^10.2.12",
39
59
  "@vitejs/plugin-vue": "^5.0.0",
40
60
  "@vue/test-utils": "^2.4.0",
61
+ "autoprefixer": "^10.4.20",
62
+ "eslint": "^10.0.1",
63
+ "eslint-plugin-storybook": "^10.2.12",
64
+ "eslint-plugin-vue": "^10.8.0",
41
65
  "happy-dom": "^15.0.0",
42
- "vitest": "^2.0.0"
66
+ "husky": "^9.1.7",
67
+ "lint-staged": "^16.2.7",
68
+ "postcss": "^8.5.0",
69
+ "prettier": "^3.8.1",
70
+ "storybook": "^10.2.12",
71
+ "tailwindcss": "^3.4.0",
72
+ "vitest": "^2.0.0",
73
+ "vue": "^3.5.0"
43
74
  }
44
75
  }
@@ -0,0 +1,55 @@
1
+ import ActivityTimeline from './ActivityTimeline.vue';
2
+
3
+ export default {
4
+ title: 'Components/ActivityTimeline',
5
+ component: ActivityTimeline,
6
+ };
7
+
8
+ const sampleActivities = [
9
+ {
10
+ id: 1,
11
+ type: 'status_changed',
12
+ causer: { name: 'Alice Chen' },
13
+ properties: { from: 'open', to: 'in_progress' },
14
+ created_at: new Date(Date.now() - 300000).toISOString(),
15
+ },
16
+ {
17
+ id: 2,
18
+ type: 'assigned',
19
+ causer: { name: 'Bob Smith' },
20
+ properties: { agent_name: 'Alice Chen' },
21
+ created_at: new Date(Date.now() - 600000).toISOString(),
22
+ },
23
+ {
24
+ id: 3,
25
+ type: 'priority_changed',
26
+ causer: { name: 'Alice Chen' },
27
+ properties: { from: 'medium', to: 'high' },
28
+ created_at: new Date(Date.now() - 1200000).toISOString(),
29
+ },
30
+ {
31
+ id: 4,
32
+ type: 'tag_added',
33
+ causer: { name: 'Alice Chen' },
34
+ properties: { tag: 'billing' },
35
+ created_at: new Date(Date.now() - 1800000).toISOString(),
36
+ },
37
+ {
38
+ id: 5,
39
+ type: 'replied',
40
+ causer: { name: 'Alice Chen' },
41
+ properties: {},
42
+ created_at: new Date(Date.now() - 2400000).toISOString(),
43
+ },
44
+ {
45
+ id: 6,
46
+ type: 'sla_breached',
47
+ causer: null,
48
+ properties: {},
49
+ created_at: new Date(Date.now() - 3600000).toISOString(),
50
+ },
51
+ ];
52
+
53
+ export const Default = { args: { activities: sampleActivities } };
54
+ export const Empty = { args: { activities: [] } };
55
+ export const SingleItem = { args: { activities: [sampleActivities[0]] } };
@@ -1,66 +1,83 @@
1
- <script setup>
2
- import { computed, inject } from 'vue';
3
-
4
- defineProps({
5
- activities: { type: Array, required: true },
6
- });
7
-
8
- const dark = inject('esc-dark', computed(() => false));
9
-
10
- const typeLabels = {
11
- status_changed: 'changed status',
12
- assigned: 'assigned ticket',
13
- unassigned: 'unassigned ticket',
14
- priority_changed: 'changed priority',
15
- tag_added: 'added tag',
16
- tag_removed: 'removed tag',
17
- escalated: 'escalated ticket',
18
- sla_breached: 'SLA breached',
19
- replied: 'replied',
20
- note_added: 'added note',
21
- department_changed: 'changed department',
22
- reopened: 'reopened ticket',
23
- resolved: 'resolved ticket',
24
- closed: 'closed ticket',
25
- };
26
-
27
- function formatDate(date) {
28
- return new Date(date).toLocaleString();
29
- }
30
-
31
- function describeActivity(activity) {
32
- const label = typeLabels[activity.type] || activity.type;
33
- const who = activity.causer?.name || 'System';
34
- const props = activity.properties || {};
35
-
36
- if (activity.type === 'status_changed' && props.to) {
37
- return `${who} ${label} to ${props.to}`;
38
- }
39
- if (activity.type === 'assigned' && props.agent_name) {
40
- return `${who} assigned to ${props.agent_name}`;
41
- }
42
- if (activity.type === 'priority_changed' && props.to) {
43
- return `${who} ${label} to ${props.to}`;
44
- }
45
- if ((activity.type === 'tag_added' || activity.type === 'tag_removed') && props.tag) {
46
- return `${who} ${label} "${props.tag}"`;
47
- }
48
- if (activity.type === 'department_changed' && props.department) {
49
- return `${who} ${label} to ${props.department}`;
50
- }
51
- return `${who} ${label}`;
52
- }
53
- </script>
54
-
55
- <template>
56
- <div class="space-y-3">
57
- <div v-for="activity in activities" :key="activity.id" class="flex gap-3 text-sm">
58
- <div :class="['mt-1 h-2 w-2 flex-shrink-0 rounded-full', dark ? 'bg-neutral-600' : 'bg-gray-400']"></div>
59
- <div class="flex-1">
60
- <p :class="dark ? 'text-neutral-300' : 'text-gray-700'">{{ describeActivity(activity) }}</p>
61
- <p :class="['text-xs', dark ? 'text-neutral-600' : 'text-gray-400']">{{ formatDate(activity.created_at) }}</p>
62
- </div>
63
- </div>
64
- <div v-if="!activities?.length" :class="['py-4 text-center text-sm', dark ? 'text-neutral-600' : 'text-gray-500']">No activity yet.</div>
65
- </div>
66
- </template>
1
+ <script setup>
2
+ import { computed, inject } from 'vue';
3
+
4
+ defineProps({
5
+ activities: { type: Array, required: true },
6
+ });
7
+
8
+ const dark = inject(
9
+ 'esc-dark',
10
+ computed(() => false),
11
+ );
12
+
13
+ const typeLabels = {
14
+ status_changed: 'changed status',
15
+ assigned: 'assigned ticket',
16
+ unassigned: 'unassigned ticket',
17
+ priority_changed: 'changed priority',
18
+ tag_added: 'added tag',
19
+ tag_removed: 'removed tag',
20
+ escalated: 'escalated ticket',
21
+ sla_breached: 'SLA breached',
22
+ replied: 'replied',
23
+ note_added: 'added note',
24
+ department_changed: 'changed department',
25
+ reopened: 'reopened ticket',
26
+ resolved: 'resolved ticket',
27
+ closed: 'closed ticket',
28
+ };
29
+
30
+ function formatDate(date) {
31
+ return new Date(date).toLocaleString();
32
+ }
33
+
34
+ function describeActivity(activity) {
35
+ const label = typeLabels[activity.type] || activity.type;
36
+ const who = activity.causer?.name || 'System';
37
+ const props = activity.properties || {};
38
+
39
+ if (activity.type === 'status_changed' && props.to) {
40
+ return `${who} ${label} to ${props.to}`;
41
+ }
42
+ if (activity.type === 'assigned' && props.agent_name) {
43
+ return `${who} assigned to ${props.agent_name}`;
44
+ }
45
+ if (activity.type === 'priority_changed' && props.to) {
46
+ return `${who} ${label} to ${props.to}`;
47
+ }
48
+ if ((activity.type === 'tag_added' || activity.type === 'tag_removed') && props.tag) {
49
+ return `${who} ${label} "${props.tag}"`;
50
+ }
51
+ if (activity.type === 'department_changed' && props.department) {
52
+ return `${who} ${label} to ${props.department}`;
53
+ }
54
+ return `${who} ${label}`;
55
+ }
56
+ </script>
57
+
58
+ <template>
59
+ <div class="space-y-3" role="feed">
60
+ <div v-for="activity in activities" :key="activity.id" role="article" class="flex gap-3 text-sm">
61
+ <div
62
+ :class="[
63
+ 'mt-1 h-2 w-2 flex-shrink-0 rounded-full',
64
+ dark ? 'bg-[var(--esc-panel-text-muted)]' : 'bg-gray-400',
65
+ ]"
66
+ ></div>
67
+ <div class="flex-1">
68
+ <p :class="dark ? 'text-[var(--esc-panel-text-secondary)]' : 'text-gray-700'">
69
+ {{ describeActivity(activity) }}
70
+ </p>
71
+ <p :class="['text-xs', dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-400']">
72
+ {{ formatDate(activity.created_at) }}
73
+ </p>
74
+ </div>
75
+ </div>
76
+ <div
77
+ v-if="!activities?.length"
78
+ :class="['py-4 text-center text-sm', dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500']"
79
+ >
80
+ No activity yet.
81
+ </div>
82
+ </div>
83
+ </template>