@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
@@ -11,17 +11,31 @@ const confirmingDelete = ref(null);
11
11
  const uploading = ref(false);
12
12
  const fileInput = ref(null);
13
13
 
14
- const activePlugins = computed(() => props.plugins.filter(p => p.active));
15
- const inactivePlugins = computed(() => props.plugins.filter(p => !p.active));
14
+ const activePlugins = computed(() => props.plugins.filter((p) => p.is_active));
15
+ const inactivePlugins = computed(() => props.plugins.filter((p) => !p.is_active));
16
16
 
17
- function togglePlugin(plugin) {
18
- router.post(route('escalated.admin.plugins.toggle', plugin.id), {}, {
19
- preserveScroll: true,
20
- });
17
+ function activate(slug) {
18
+ router.post(
19
+ route('escalated.admin.plugins.activate', slug),
20
+ {},
21
+ {
22
+ preserveScroll: true,
23
+ },
24
+ );
25
+ }
26
+
27
+ function deactivate(slug) {
28
+ router.post(
29
+ route('escalated.admin.plugins.deactivate', slug),
30
+ {},
31
+ {
32
+ preserveScroll: true,
33
+ },
34
+ );
21
35
  }
22
36
 
23
37
  function confirmDelete(plugin) {
24
- confirmingDelete.value = plugin.id;
38
+ confirmingDelete.value = plugin.slug;
25
39
  }
26
40
 
27
41
  function cancelDelete() {
@@ -29,9 +43,11 @@ function cancelDelete() {
29
43
  }
30
44
 
31
45
  function deletePlugin(plugin) {
32
- router.delete(route('escalated.admin.plugins.destroy', plugin.id), {
46
+ router.delete(route('escalated.admin.plugins.destroy', plugin.slug), {
33
47
  preserveScroll: true,
34
- onSuccess: () => { confirmingDelete.value = null; },
48
+ onSuccess: () => {
49
+ confirmingDelete.value = null;
50
+ },
35
51
  });
36
52
  }
37
53
 
@@ -44,23 +60,27 @@ function handleUpload(event) {
44
60
  if (!file) return;
45
61
 
46
62
  uploading.value = true;
47
- router.post(route('escalated.admin.plugins.upload'), { file }, {
48
- forceFormData: true,
49
- preserveScroll: true,
50
- onFinish: () => {
51
- uploading.value = false;
52
- if (fileInput.value) fileInput.value.value = '';
63
+ router.post(
64
+ route('escalated.admin.plugins.upload'),
65
+ { plugin: file },
66
+ {
67
+ forceFormData: true,
68
+ preserveScroll: true,
69
+ onFinish: () => {
70
+ uploading.value = false;
71
+ if (fileInput.value) fileInput.value.value = '';
72
+ },
53
73
  },
54
- });
74
+ );
55
75
  }
56
76
 
57
77
  function statusColor(plugin) {
58
- if (plugin.active) return 'bg-emerald-500';
78
+ if (plugin.is_active) return 'bg-emerald-500';
59
79
  return 'bg-neutral-600';
60
80
  }
61
81
 
62
82
  function statusLabel(plugin) {
63
- return plugin.active ? 'Active' : 'Inactive';
83
+ return plugin.is_active ? 'Active' : 'Inactive';
64
84
  }
65
85
  </script>
66
86
 
@@ -69,24 +89,35 @@ function statusLabel(plugin) {
69
89
  <!-- Header -->
70
90
  <div class="mb-6 flex items-center justify-between">
71
91
  <div>
72
- <p class="text-sm text-neutral-400">
92
+ <p class="text-sm text-[var(--esc-panel-text-tertiary)]">
73
93
  {{ activePlugins.length }} active, {{ inactivePlugins.length }} inactive
74
94
  </p>
75
95
  </div>
76
96
  <div>
77
97
  <input ref="fileInput" type="file" accept=".zip" class="hidden" @change="handleUpload" />
78
- <button @click="triggerUpload" :disabled="uploading"
79
- class="rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-4 py-2 text-sm font-medium text-white shadow-lg shadow-black/20 transition-all hover:from-cyan-400 hover:to-violet-400 disabled:opacity-50">
98
+ <button
99
+ :disabled="uploading"
100
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-4 py-2 text-sm font-medium text-white shadow-lg shadow-[var(--esc-panel-bg)]/20 transition-all hover:from-[var(--esc-panel-accent-hover)] hover:to-[var(--esc-panel-accent-secondary-hover)] disabled:opacity-50"
101
+ @click="triggerUpload"
102
+ >
80
103
  <span v-if="uploading" class="flex items-center gap-2">
81
104
  <svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
82
105
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
83
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
106
+ <path
107
+ class="opacity-75"
108
+ fill="currentColor"
109
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
110
+ />
84
111
  </svg>
85
112
  Uploading...
86
113
  </span>
87
114
  <span v-else class="flex items-center gap-2">
88
115
  <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
89
- <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
116
+ <path
117
+ stroke-linecap="round"
118
+ stroke-linejoin="round"
119
+ d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
120
+ />
90
121
  </svg>
91
122
  Upload Plugin
92
123
  </span>
@@ -95,49 +126,111 @@ function statusLabel(plugin) {
95
126
  </div>
96
127
 
97
128
  <!-- Empty state -->
98
- <div v-if="!plugins?.length" class="rounded-xl border border-white/[0.06] bg-neutral-900/60 px-6 py-16 text-center">
99
- <svg class="mx-auto mb-4 h-12 w-12 text-neutral-700" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
100
- <path stroke-linecap="round" stroke-linejoin="round" d="M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 01-.657.643 48.39 48.39 0 01-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 01-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 00-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 01-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 00.657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 005.427-.63 48.05 48.05 0 00.582-4.717.532.532 0 00-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.96.401v0a.656.656 0 00.658-.663 48.422 48.422 0 00-.37-5.36c-1.886.342-3.81.574-5.766.689a.578.578 0 01-.61-.58v0z" />
129
+ <div
130
+ v-if="!plugins?.length"
131
+ class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] px-6 py-16 text-center"
132
+ >
133
+ <svg
134
+ class="mx-auto mb-4 h-12 w-12 text-[var(--esc-panel-text-muted)]"
135
+ fill="none"
136
+ stroke="currentColor"
137
+ stroke-width="1.5"
138
+ viewBox="0 0 24 24"
139
+ >
140
+ <path
141
+ stroke-linecap="round"
142
+ stroke-linejoin="round"
143
+ d="M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 01-.657.643 48.39 48.39 0 01-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 01-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 00-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 01-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 00.657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 005.427-.63 48.05 48.05 0 00.582-4.717.532.532 0 00-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.96.401v0a.656.656 0 00.658-.663 48.422 48.422 0 00-.37-5.36c-1.886.342-3.81.574-5.766.689a.578.578 0 01-.61-.58v0z"
144
+ />
101
145
  </svg>
102
- <h3 class="text-sm font-medium text-neutral-300">No plugins installed</h3>
103
- <p class="mt-1 text-xs text-neutral-500">Upload a plugin ZIP to extend Escalated with custom functionality.</p>
146
+ <h3 class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">No plugins installed</h3>
147
+ <p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
148
+ Upload a plugin ZIP to extend Escalated with custom functionality.
149
+ </p>
104
150
  </div>
105
151
 
106
152
  <!-- Plugin cards -->
107
153
  <div v-else class="space-y-3">
108
- <div v-for="plugin in plugins" :key="plugin.id"
109
- class="rounded-xl border border-white/[0.06] bg-neutral-900/60 p-5 transition-colors hover:bg-neutral-900/80">
154
+ <div
155
+ v-for="plugin in plugins"
156
+ :key="plugin.slug"
157
+ class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5 transition-colors hover:bg-[var(--esc-panel-surface)]"
158
+ >
110
159
  <div class="flex items-start justify-between gap-4">
111
160
  <!-- Plugin info -->
112
161
  <div class="min-w-0 flex-1">
113
162
  <div class="flex items-center gap-3">
114
- <h3 class="text-sm font-semibold text-white">{{ plugin.name }}</h3>
115
- <span :class="['inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[10px] font-semibold',
116
- plugin.active
117
- ? 'bg-emerald-500/10 text-emerald-400'
118
- : 'bg-neutral-500/10 text-neutral-500']">
163
+ <h3 class="text-sm font-semibold text-[var(--esc-panel-text)]">{{ plugin.name }}</h3>
164
+ <span
165
+ :class="[
166
+ 'inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[10px] font-semibold',
167
+ plugin.is_active
168
+ ? 'bg-emerald-500/10 text-emerald-400'
169
+ : 'bg-neutral-500/10 text-[var(--esc-panel-text-muted)]',
170
+ ]"
171
+ >
119
172
  <span :class="['h-1.5 w-1.5 rounded-full', statusColor(plugin)]"></span>
120
173
  {{ statusLabel(plugin) }}
121
174
  </span>
122
- <span v-if="plugin.version" class="text-[11px] text-neutral-600">v{{ plugin.version }}</span>
175
+ <span v-if="plugin.version" class="text-[11px] text-[var(--esc-panel-text-muted)]"
176
+ >v{{ plugin.version }}</span
177
+ >
123
178
  </div>
124
- <p v-if="plugin.description" class="mt-1 text-sm text-neutral-400">{{ plugin.description }}</p>
125
- <div class="mt-2 flex flex-wrap items-center gap-4 text-xs text-neutral-600">
179
+ <p v-if="plugin.description" class="mt-1 text-sm text-[var(--esc-panel-text-tertiary)]">
180
+ {{ plugin.description }}
181
+ </p>
182
+ <div class="mt-2 flex flex-wrap items-center gap-4 text-xs text-[var(--esc-panel-text-muted)]">
126
183
  <span v-if="plugin.author" class="flex items-center gap-1">
127
- <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
128
- <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
184
+ <svg
185
+ class="h-3.5 w-3.5"
186
+ fill="none"
187
+ stroke="currentColor"
188
+ stroke-width="1.5"
189
+ viewBox="0 0 24 24"
190
+ >
191
+ <path
192
+ stroke-linecap="round"
193
+ stroke-linejoin="round"
194
+ d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
195
+ />
129
196
  </svg>
130
197
  {{ plugin.author }}
131
198
  </span>
132
199
  <span v-if="plugin.homepage" class="flex items-center gap-1">
133
- <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
134
- <path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
200
+ <svg
201
+ class="h-3.5 w-3.5"
202
+ fill="none"
203
+ stroke="currentColor"
204
+ stroke-width="1.5"
205
+ viewBox="0 0 24 24"
206
+ >
207
+ <path
208
+ stroke-linecap="round"
209
+ stroke-linejoin="round"
210
+ d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"
211
+ />
135
212
  </svg>
136
- <a :href="plugin.homepage" target="_blank" rel="noopener" class="hover:text-neutral-400">{{ plugin.homepage }}</a>
213
+ <a
214
+ :href="plugin.homepage"
215
+ target="_blank"
216
+ rel="noopener"
217
+ class="hover:text-[var(--esc-panel-text-tertiary)]"
218
+ >{{ plugin.homepage }}</a
219
+ >
137
220
  </span>
138
221
  <span v-if="plugin.installed_at" class="flex items-center gap-1">
139
- <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
140
- <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
222
+ <svg
223
+ class="h-3.5 w-3.5"
224
+ fill="none"
225
+ stroke="currentColor"
226
+ stroke-width="1.5"
227
+ viewBox="0 0 24 24"
228
+ >
229
+ <path
230
+ stroke-linecap="round"
231
+ stroke-linejoin="round"
232
+ d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
233
+ />
141
234
  </svg>
142
235
  Installed {{ plugin.installed_at }}
143
236
  </span>
@@ -145,17 +238,27 @@ function statusLabel(plugin) {
145
238
 
146
239
  <!-- Provided extensions summary -->
147
240
  <div v-if="plugin.provides" class="mt-3 flex flex-wrap gap-2">
148
- <span v-if="plugin.provides.menu_items"
149
- class="rounded bg-white/[0.04] px-2 py-0.5 text-[10px] text-neutral-500">
150
- {{ plugin.provides.menu_items }} menu item{{ plugin.provides.menu_items !== 1 ? 's' : '' }}
241
+ <span
242
+ v-if="plugin.provides.menu_items"
243
+ class="rounded bg-[var(--esc-panel-hover)] px-2 py-0.5 text-[10px] text-[var(--esc-panel-text-muted)]"
244
+ >
245
+ {{ plugin.provides.menu_items }} menu item{{
246
+ plugin.provides.menu_items !== 1 ? 's' : ''
247
+ }}
151
248
  </span>
152
- <span v-if="plugin.provides.widgets"
153
- class="rounded bg-white/[0.04] px-2 py-0.5 text-[10px] text-neutral-500">
249
+ <span
250
+ v-if="plugin.provides.widgets"
251
+ class="rounded bg-[var(--esc-panel-hover)] px-2 py-0.5 text-[10px] text-[var(--esc-panel-text-muted)]"
252
+ >
154
253
  {{ plugin.provides.widgets }} widget{{ plugin.provides.widgets !== 1 ? 's' : '' }}
155
254
  </span>
156
- <span v-if="plugin.provides.page_components"
157
- class="rounded bg-white/[0.04] px-2 py-0.5 text-[10px] text-neutral-500">
158
- {{ plugin.provides.page_components }} page component{{ plugin.provides.page_components !== 1 ? 's' : '' }}
255
+ <span
256
+ v-if="plugin.provides.page_components"
257
+ class="rounded bg-[var(--esc-panel-hover)] px-2 py-0.5 text-[10px] text-[var(--esc-panel-text-muted)]"
258
+ >
259
+ {{ plugin.provides.page_components }} page component{{
260
+ plugin.provides.page_components !== 1 ? 's' : ''
261
+ }}
159
262
  </span>
160
263
  </div>
161
264
  </div>
@@ -163,32 +266,59 @@ function statusLabel(plugin) {
163
266
  <!-- Actions -->
164
267
  <div class="flex shrink-0 items-center gap-2">
165
268
  <!-- Toggle active/inactive -->
166
- <button @click="togglePlugin(plugin)"
167
- :class="['relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors',
168
- plugin.active ? 'bg-emerald-500' : 'bg-neutral-700']"
169
- :title="plugin.active ? 'Deactivate' : 'Activate'">
170
- <span :class="['pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition-transform',
171
- plugin.active ? 'translate-x-5' : 'translate-x-0']" />
269
+ <button
270
+ :class="[
271
+ 'relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors',
272
+ plugin.is_active ? 'bg-emerald-500' : 'bg-neutral-700',
273
+ ]"
274
+ :title="plugin.is_active ? 'Deactivate' : 'Activate'"
275
+ @click="plugin.is_active ? deactivate(plugin.slug) : activate(plugin.slug)"
276
+ >
277
+ <span
278
+ :class="[
279
+ 'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition-transform',
280
+ plugin.is_active ? 'translate-x-5' : 'translate-x-0',
281
+ ]"
282
+ />
172
283
  </button>
173
284
 
174
- <!-- Delete -->
175
- <template v-if="confirmingDelete === plugin.id">
176
- <button @click="deletePlugin(plugin)"
177
- class="rounded-lg bg-rose-500/20 px-3 py-1.5 text-xs font-medium text-rose-400 transition-colors hover:bg-rose-500/30">
178
- Confirm
179
- </button>
180
- <button @click="cancelDelete"
181
- class="rounded-lg border border-white/10 px-3 py-1.5 text-xs font-medium text-neutral-400 transition-colors hover:bg-white/[0.04]">
182
- Cancel
285
+ <!-- Delete (local plugins only) -->
286
+ <template v-if="plugin.source !== 'composer'">
287
+ <template v-if="confirmingDelete === plugin.slug">
288
+ <button
289
+ class="rounded-lg bg-rose-500/20 px-3 py-1.5 text-xs font-medium text-rose-400 transition-colors hover:bg-rose-500/30"
290
+ @click="deletePlugin(plugin)"
291
+ >
292
+ Confirm
293
+ </button>
294
+ <button
295
+ class="rounded-lg border border-[var(--esc-panel-border-input)] px-3 py-1.5 text-xs font-medium text-[var(--esc-panel-text-tertiary)] transition-colors hover:bg-[var(--esc-panel-hover)]"
296
+ @click="cancelDelete"
297
+ >
298
+ Cancel
299
+ </button>
300
+ </template>
301
+ <button
302
+ v-else
303
+ class="rounded-lg border border-[var(--esc-panel-border)] p-1.5 text-[var(--esc-panel-text-muted)] transition-colors hover:border-rose-500/20 hover:bg-rose-500/5 hover:text-rose-400"
304
+ title="Delete plugin"
305
+ @click="confirmDelete(plugin)"
306
+ >
307
+ <svg
308
+ class="h-4 w-4"
309
+ fill="none"
310
+ stroke="currentColor"
311
+ stroke-width="1.5"
312
+ viewBox="0 0 24 24"
313
+ >
314
+ <path
315
+ stroke-linecap="round"
316
+ stroke-linejoin="round"
317
+ d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
318
+ />
319
+ </svg>
183
320
  </button>
184
321
  </template>
185
- <button v-else @click="confirmDelete(plugin)"
186
- class="rounded-lg border border-white/[0.06] p-1.5 text-neutral-600 transition-colors hover:border-rose-500/20 hover:bg-rose-500/5 hover:text-rose-400"
187
- title="Delete plugin">
188
- <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
189
- <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
190
- </svg>
191
- </button>
192
322
  </div>
193
323
  </div>
194
324
  </div>
@@ -0,0 +1,115 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import AgentPerformanceCard from '../../../components/AgentPerformanceCard.vue';
4
+ import { router, Link } from '@inertiajs/vue3';
5
+
6
+ const props = defineProps({
7
+ period_days: Number,
8
+ agents: Array,
9
+ });
10
+
11
+ function changePeriod(days) {
12
+ router.get(route('escalated.admin.reports.agents'), { days }, { preserveState: true });
13
+ }
14
+ </script>
15
+
16
+ <template>
17
+ <EscalatedLayout title="Agent Performance Metrics">
18
+ <div class="mb-6 flex items-center justify-between">
19
+ <div class="flex gap-2">
20
+ <button
21
+ v-for="d in [7, 30, 90]"
22
+ :key="d"
23
+ :class="[
24
+ 'rounded-lg px-3.5 py-1.5 text-sm font-medium transition-all',
25
+ period_days === d
26
+ ? 'bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] text-white shadow-lg shadow-[var(--esc-panel-bg)]/20'
27
+ : 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]',
28
+ ]"
29
+ @click="changePeriod(d)"
30
+ >
31
+ Last {{ d }} days
32
+ </button>
33
+ </div>
34
+ <Link
35
+ :href="route('escalated.admin.reports')"
36
+ class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
37
+ >
38
+ &larr; Back to Reports
39
+ </Link>
40
+ </div>
41
+
42
+ <!-- Agent Table -->
43
+ <div class="mb-8 overflow-hidden rounded-xl border border-[var(--esc-panel-border)]">
44
+ <table class="w-full">
45
+ <thead>
46
+ <tr class="border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)]">
47
+ <th
48
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
49
+ >
50
+ Agent
51
+ </th>
52
+ <th
53
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
54
+ >
55
+ Total Tickets
56
+ </th>
57
+ <th
58
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
59
+ >
60
+ Resolved
61
+ </th>
62
+ <th
63
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
64
+ >
65
+ Avg Response
66
+ </th>
67
+ <th
68
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
69
+ >
70
+ Avg Resolution
71
+ </th>
72
+ </tr>
73
+ </thead>
74
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
75
+ <tr v-for="agent in agents" :key="agent.agent_id" class="hover:bg-[var(--esc-panel-hover)]">
76
+ <td class="px-4 py-3">
77
+ <div class="flex items-center gap-3">
78
+ <div
79
+ class="flex h-8 w-8 items-center justify-center rounded-md bg-[var(--esc-panel-hover)] text-xs font-semibold text-[var(--esc-panel-text-tertiary)]"
80
+ >
81
+ {{ agent.agent_name?.charAt(0)?.toUpperCase() || '?' }}
82
+ </div>
83
+ <span class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">{{
84
+ agent.agent_name
85
+ }}</span>
86
+ </div>
87
+ </td>
88
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
89
+ {{ agent.total_tickets }}
90
+ </td>
91
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
92
+ {{ agent.resolved_tickets }}
93
+ </td>
94
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
95
+ {{ agent.avg_response_hours ?? '—' }}h
96
+ </td>
97
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
98
+ {{ agent.avg_resolution_hours ?? '—' }}h
99
+ </td>
100
+ </tr>
101
+ <tr v-if="!agents?.length">
102
+ <td colspan="5" class="px-4 py-8 text-center text-sm text-[var(--esc-panel-text-muted)]">
103
+ No agent data for this period.
104
+ </td>
105
+ </tr>
106
+ </tbody>
107
+ </table>
108
+ </div>
109
+
110
+ <!-- Agent Cards (visual) -->
111
+ <div v-if="agents?.length" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
112
+ <AgentPerformanceCard v-for="agent in agents" :key="agent.agent_id" :agent="agent" />
113
+ </div>
114
+ </EscalatedLayout>
115
+ </template>
@@ -0,0 +1,127 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import KpiCard from '../../../components/KpiCard.vue';
4
+ import ChartWidget from '../../../components/ChartWidget.vue';
5
+ import { router, Link } from '@inertiajs/vue3';
6
+
7
+ const props = defineProps({
8
+ period_days: Number,
9
+ csat_average: Number,
10
+ response_rate: Number,
11
+ total_ratings: Number,
12
+ by_agent: Array,
13
+ over_time: Array,
14
+ });
15
+
16
+ function changePeriod(days) {
17
+ router.get(route('escalated.admin.reports.csat'), { days }, { preserveState: true });
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <EscalatedLayout title="Customer Satisfaction Report">
23
+ <div class="mb-6 flex items-center justify-between">
24
+ <div class="flex gap-2">
25
+ <button
26
+ v-for="d in [7, 30, 90]"
27
+ :key="d"
28
+ :class="[
29
+ 'rounded-lg px-3.5 py-1.5 text-sm font-medium transition-all',
30
+ period_days === d
31
+ ? 'bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] text-white shadow-lg shadow-[var(--esc-panel-bg)]/20'
32
+ : 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]',
33
+ ]"
34
+ @click="changePeriod(d)"
35
+ >
36
+ Last {{ d }} days
37
+ </button>
38
+ </div>
39
+ <div class="flex items-center gap-3">
40
+ <Link
41
+ :href="route('escalated.admin.settings.csat')"
42
+ class="text-sm text-[var(--esc-panel-accent)] hover:text-[var(--esc-panel-accent)]"
43
+ >
44
+ CSAT Settings
45
+ </Link>
46
+ <Link
47
+ :href="route('escalated.admin.reports')"
48
+ class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
49
+ >
50
+ &larr; Back to Reports
51
+ </Link>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- KPIs -->
56
+ <div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-3">
57
+ <KpiCard
58
+ label="Average Rating"
59
+ :value="csat_average || '—'"
60
+ :icon="'M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z'"
61
+ />
62
+ <KpiCard
63
+ label="Response Rate"
64
+ :value="`${response_rate}%`"
65
+ :icon="'M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z'"
66
+ />
67
+ <KpiCard
68
+ label="Total Ratings"
69
+ :value="total_ratings"
70
+ :icon="'M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z'"
71
+ />
72
+ </div>
73
+
74
+ <!-- Charts -->
75
+ <div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2">
76
+ <ChartWidget title="CSAT Over Time" :data="over_time" color="amber" />
77
+
78
+ <!-- By Agent -->
79
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5">
80
+ <h3 class="mb-4 text-sm font-semibold text-[var(--esc-panel-text-secondary)]">Ratings by Agent</h3>
81
+ <div v-if="by_agent?.length" class="space-y-3">
82
+ <div v-for="agent in by_agent" :key="agent.agent_id" class="flex items-center justify-between">
83
+ <div class="flex items-center gap-3">
84
+ <div
85
+ class="flex h-8 w-8 items-center justify-center rounded-md bg-[var(--esc-panel-hover)] text-xs font-semibold text-[var(--esc-panel-text-tertiary)]"
86
+ >
87
+ {{ agent.agent_name?.charAt(0)?.toUpperCase() || '?' }}
88
+ </div>
89
+ <div>
90
+ <div class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">
91
+ {{ agent.agent_name }}
92
+ </div>
93
+ <div class="text-xs text-[var(--esc-panel-text-muted)]">
94
+ {{ agent.total_ratings }} ratings
95
+ </div>
96
+ </div>
97
+ </div>
98
+ <div class="flex items-center gap-2">
99
+ <div class="flex items-center gap-0.5">
100
+ <svg
101
+ v-for="n in 5"
102
+ :key="n"
103
+ class="h-3.5 w-3.5"
104
+ :class="
105
+ n <= Math.round(agent.avg_rating)
106
+ ? 'text-amber-400'
107
+ : 'text-[var(--esc-panel-text-muted)]'
108
+ "
109
+ fill="currentColor"
110
+ viewBox="0 0 20 20"
111
+ >
112
+ <path
113
+ d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"
114
+ />
115
+ </svg>
116
+ </div>
117
+ <span class="text-sm font-bold text-[var(--esc-panel-text)]">{{ agent.avg_rating }}</span>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ <div v-else class="py-6 text-center text-sm text-[var(--esc-panel-text-muted)]">
122
+ No agent ratings for this period.
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </EscalatedLayout>
127
+ </template>