@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,165 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { useForm, Link } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ settings: Object,
7
+ });
8
+
9
+ const form = useForm({
10
+ csat_question_text: props.settings?.csat_question_text || 'How would you rate your support experience?',
11
+ csat_scale: props.settings?.csat_scale || '1-5',
12
+ csat_delivery_trigger: props.settings?.csat_delivery_trigger || 'on_resolve',
13
+ csat_delay_hours: props.settings?.csat_delay_hours || 0,
14
+ });
15
+
16
+ function submit() {
17
+ form.post(route('escalated.admin.settings.csat.update'));
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <EscalatedLayout title="CSAT Survey Settings">
23
+ <div class="mx-auto max-w-2xl">
24
+ <div class="mb-6">
25
+ <Link
26
+ :href="route('escalated.admin.settings')"
27
+ class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
28
+ >
29
+ &larr; Back to Settings
30
+ </Link>
31
+ </div>
32
+
33
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
34
+ <h2 class="mb-6 text-lg font-semibold text-[var(--esc-panel-text-secondary)]">
35
+ Customer Satisfaction Survey
36
+ </h2>
37
+
38
+ <form class="space-y-6" @submit.prevent="submit">
39
+ <!-- Question Text -->
40
+ <div>
41
+ <label class="mb-1.5 block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
42
+ >Survey Question</label
43
+ >
44
+ <textarea
45
+ v-model="form.csat_question_text"
46
+ rows="3"
47
+ class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2.5 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-accent)]/40 focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-accent)]/40"
48
+ placeholder="How would you rate your support experience?"
49
+ ></textarea>
50
+ <p v-if="form.errors.csat_question_text" class="mt-1 text-xs text-red-400">
51
+ {{ form.errors.csat_question_text }}
52
+ </p>
53
+ <p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
54
+ This question will be shown to customers after their ticket is resolved.
55
+ </p>
56
+ </div>
57
+
58
+ <!-- Rating Scale -->
59
+ <div>
60
+ <label class="mb-1.5 block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
61
+ >Rating Scale</label
62
+ >
63
+ <div class="flex gap-3">
64
+ <label
65
+ :class="[
66
+ 'flex cursor-pointer items-center gap-2 rounded-lg border px-4 py-3 transition-all',
67
+ form.csat_scale === '1-3'
68
+ ? 'border-[var(--esc-panel-accent)]/40 bg-[var(--esc-panel-accent)]/10 text-[var(--esc-panel-accent)]'
69
+ : 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)] hover:border-[var(--esc-panel-border-input)]',
70
+ ]"
71
+ >
72
+ <input v-model="form.csat_scale" type="radio" value="1-3" class="sr-only" />
73
+ <span class="text-sm font-medium">1-3 Scale</span>
74
+ <span class="text-xs text-[var(--esc-panel-text-muted)]">(Simple)</span>
75
+ </label>
76
+ <label
77
+ :class="[
78
+ 'flex cursor-pointer items-center gap-2 rounded-lg border px-4 py-3 transition-all',
79
+ form.csat_scale === '1-5'
80
+ ? 'border-[var(--esc-panel-accent)]/40 bg-[var(--esc-panel-accent)]/10 text-[var(--esc-panel-accent)]'
81
+ : 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)] hover:border-[var(--esc-panel-border-input)]',
82
+ ]"
83
+ >
84
+ <input v-model="form.csat_scale" type="radio" value="1-5" class="sr-only" />
85
+ <span class="text-sm font-medium">1-5 Scale</span>
86
+ <span class="text-xs text-[var(--esc-panel-text-muted)]">(Detailed)</span>
87
+ </label>
88
+ </div>
89
+ <p v-if="form.errors.csat_scale" class="mt-1 text-xs text-red-400">
90
+ {{ form.errors.csat_scale }}
91
+ </p>
92
+ </div>
93
+
94
+ <!-- Delivery Trigger -->
95
+ <div>
96
+ <label class="mb-1.5 block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
97
+ >Delivery Trigger</label
98
+ >
99
+ <select
100
+ v-model="form.csat_delivery_trigger"
101
+ class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-accent)]/40 focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-accent)]/40"
102
+ >
103
+ <option value="on_resolve">On ticket resolution</option>
104
+ <option value="delayed">Delayed after resolution</option>
105
+ <option value="manual">Manual only</option>
106
+ </select>
107
+ <p v-if="form.errors.csat_delivery_trigger" class="mt-1 text-xs text-red-400">
108
+ {{ form.errors.csat_delivery_trigger }}
109
+ </p>
110
+ </div>
111
+
112
+ <!-- Delay Hours (shown only for delayed trigger) -->
113
+ <div v-if="form.csat_delivery_trigger === 'delayed'">
114
+ <label class="mb-1.5 block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
115
+ >Delay (hours)</label
116
+ >
117
+ <input
118
+ v-model.number="form.csat_delay_hours"
119
+ type="number"
120
+ min="0"
121
+ max="168"
122
+ class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-accent)]/40 focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-accent)]/40"
123
+ />
124
+ <p v-if="form.errors.csat_delay_hours" class="mt-1 text-xs text-red-400">
125
+ {{ form.errors.csat_delay_hours }}
126
+ </p>
127
+ <p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
128
+ How many hours after resolution to send the survey (max 168 = 7 days).
129
+ </p>
130
+ </div>
131
+
132
+ <!-- Submit -->
133
+ <div class="pt-2">
134
+ <button
135
+ type="submit"
136
+ :disabled="form.processing"
137
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-5 py-2.5 text-sm font-medium text-white shadow-lg shadow-[var(--esc-panel-bg)]/20 transition-opacity hover:opacity-90 disabled:opacity-50"
138
+ >
139
+ Save Settings
140
+ </button>
141
+ </div>
142
+ </form>
143
+ </div>
144
+
145
+ <!-- Preview -->
146
+ <div class="mt-6 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
147
+ <h3 class="mb-4 text-sm font-semibold text-[var(--esc-panel-text-secondary)]">Preview</h3>
148
+ <div
149
+ class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] p-6 text-center"
150
+ >
151
+ <p class="mb-4 text-sm text-[var(--esc-panel-text-secondary)]">{{ form.csat_question_text }}</p>
152
+ <div class="flex items-center justify-center gap-2">
153
+ <button
154
+ v-for="n in form.csat_scale === '1-3' ? 3 : 5"
155
+ :key="n"
156
+ class="flex h-10 w-10 items-center justify-center rounded-lg border border-[var(--esc-panel-border)] text-sm font-medium text-[var(--esc-panel-text-tertiary)] transition-colors hover:border-amber-500/40 hover:bg-amber-500/10 hover:text-amber-400"
157
+ >
158
+ {{ n }}
159
+ </button>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </EscalatedLayout>
165
+ </template>
@@ -0,0 +1,182 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { useForm } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ settings: Object,
7
+ purgePreview: { type: Object, default: () => ({}) },
8
+ });
9
+
10
+ const form = useForm({
11
+ retention_closed_tickets: props.settings.retention_closed_tickets ?? 'never',
12
+ retention_attachments: props.settings.retention_attachments ?? 'never',
13
+ retention_audit_logs: props.settings.retention_audit_logs ?? 'never',
14
+ retention_user_data_gdpr: props.settings.retention_user_data_gdpr ?? false,
15
+ });
16
+
17
+ function submit() {
18
+ form.post(route('escalated.admin.settings.data-retention.update'));
19
+ }
20
+
21
+ const ticketOptions = [
22
+ { value: 'never', label: 'Never delete' },
23
+ { value: '1_year', label: '1 year' },
24
+ { value: '2_years', label: '2 years' },
25
+ { value: '3_years', label: '3 years' },
26
+ { value: '5_years', label: '5 years' },
27
+ ];
28
+
29
+ const attachmentOptions = [
30
+ { value: 'never', label: 'Never delete' },
31
+ { value: '1_year', label: '1 year' },
32
+ { value: '2_years', label: '2 years' },
33
+ { value: '3_years', label: '3 years' },
34
+ { value: '5_years', label: '5 years' },
35
+ ];
36
+
37
+ const auditLogOptions = [
38
+ { value: 'never', label: 'Never delete' },
39
+ { value: '90_days', label: '90 days' },
40
+ { value: '180_days', label: '180 days' },
41
+ { value: '365_days', label: '365 days' },
42
+ ];
43
+ </script>
44
+
45
+ <template>
46
+ <EscalatedLayout title="Data Retention">
47
+ <form class="mx-auto max-w-2xl space-y-6" @submit.prevent="submit">
48
+ <!-- Closed Tickets -->
49
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
50
+ <h3 class="mb-5 text-sm font-semibold text-[var(--esc-panel-text)]">Retention Policies</h3>
51
+ <p class="mb-5 text-xs text-[var(--esc-panel-text-muted)]">
52
+ Configure how long data is retained before automatic purging. Records are soft-deleted first with a
53
+ 30-day grace period before permanent removal.
54
+ </p>
55
+ <div class="space-y-5">
56
+ <div>
57
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
58
+ >Closed Tickets</label
59
+ >
60
+ <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">
61
+ How long to keep closed tickets before purging
62
+ </p>
63
+ <select
64
+ v-model="form.retention_closed_tickets"
65
+ class="mt-2 w-48 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
66
+ >
67
+ <option v-for="opt in ticketOptions" :key="opt.value" :value="opt.value">
68
+ {{ opt.label }}
69
+ </option>
70
+ </select>
71
+ </div>
72
+
73
+ <div>
74
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
75
+ >Attachments</label
76
+ >
77
+ <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">
78
+ How long to keep attachments on closed tickets
79
+ </p>
80
+ <select
81
+ v-model="form.retention_attachments"
82
+ class="mt-2 w-48 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
83
+ >
84
+ <option v-for="opt in attachmentOptions" :key="opt.value" :value="opt.value">
85
+ {{ opt.label }}
86
+ </option>
87
+ </select>
88
+ </div>
89
+
90
+ <div>
91
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
92
+ >Audit Logs</label
93
+ >
94
+ <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">
95
+ How long to keep audit log entries
96
+ </p>
97
+ <select
98
+ v-model="form.retention_audit_logs"
99
+ class="mt-2 w-48 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
100
+ >
101
+ <option v-for="opt in auditLogOptions" :key="opt.value" :value="opt.value">
102
+ {{ opt.label }}
103
+ </option>
104
+ </select>
105
+ </div>
106
+
107
+ <label class="flex items-center justify-between">
108
+ <div>
109
+ <span class="text-sm font-medium text-[var(--esc-panel-text-secondary)]"
110
+ >GDPR User Data Handling</span
111
+ >
112
+ <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">
113
+ Anonymize user data upon account deletion to comply with GDPR
114
+ </p>
115
+ </div>
116
+ <button
117
+ type="button"
118
+ :class="[
119
+ 'relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors',
120
+ form.retention_user_data_gdpr ? 'bg-emerald-500' : 'bg-neutral-700',
121
+ ]"
122
+ @click="form.retention_user_data_gdpr = !form.retention_user_data_gdpr"
123
+ >
124
+ <span
125
+ :class="[
126
+ 'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition-transform',
127
+ form.retention_user_data_gdpr ? 'translate-x-5' : 'translate-x-0',
128
+ ]"
129
+ />
130
+ </button>
131
+ </label>
132
+ </div>
133
+ </div>
134
+
135
+ <!-- Purge Preview -->
136
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
137
+ <h3 class="mb-4 text-sm font-semibold text-[var(--esc-panel-text)]">Purge Preview</h3>
138
+ <p class="mb-4 text-xs text-[var(--esc-panel-text-muted)]">
139
+ Estimated records that would be purged based on current settings.
140
+ </p>
141
+ <div class="grid grid-cols-3 gap-4">
142
+ <div
143
+ class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)] p-4 text-center"
144
+ >
145
+ <div class="text-2xl font-bold text-[var(--esc-panel-text-secondary)]">
146
+ {{ purgePreview.tickets ?? 0 }}
147
+ </div>
148
+ <div class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">Closed Tickets</div>
149
+ </div>
150
+ <div
151
+ class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)] p-4 text-center"
152
+ >
153
+ <div class="text-2xl font-bold text-[var(--esc-panel-text-secondary)]">
154
+ {{ purgePreview.attachments ?? 0 }}
155
+ </div>
156
+ <div class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">Attachments</div>
157
+ </div>
158
+ <div
159
+ class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)] p-4 text-center"
160
+ >
161
+ <div class="text-2xl font-bold text-[var(--esc-panel-text-secondary)]">
162
+ {{ purgePreview.audit_logs ?? 0 }}
163
+ </div>
164
+ <div class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">Audit Logs</div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <!-- Submit -->
170
+ <div class="flex items-center justify-end gap-3">
171
+ <span v-if="form.recentlySuccessful" class="text-sm text-emerald-400">Saved.</span>
172
+ <button
173
+ type="submit"
174
+ :disabled="form.processing"
175
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-5 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"
176
+ >
177
+ {{ form.processing ? 'Saving...' : 'Save Retention Settings' }}
178
+ </button>
179
+ </div>
180
+ </form>
181
+ </EscalatedLayout>
182
+ </template>
@@ -0,0 +1,243 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { useForm } from '@inertiajs/vue3';
4
+ import { ref } from 'vue';
5
+
6
+ const props = defineProps({
7
+ addresses: { type: Array, default: () => [] },
8
+ defaultReplyAddress: { type: String, default: '' },
9
+ });
10
+
11
+ const form = useForm({
12
+ addresses: props.addresses.map((a) => ({ ...a })),
13
+ default_reply_address: props.defaultReplyAddress,
14
+ });
15
+
16
+ const showAddForm = ref(false);
17
+ const newAddress = ref({
18
+ email: '',
19
+ display_name: '',
20
+ department_id: null,
21
+ });
22
+
23
+ function addAddress() {
24
+ form.addresses.push({
25
+ email: newAddress.value.email,
26
+ display_name: newAddress.value.display_name,
27
+ department_id: newAddress.value.department_id,
28
+ dkim_status: 'unknown',
29
+ });
30
+ newAddress.value = { email: '', display_name: '', department_id: null };
31
+ showAddForm.value = false;
32
+ }
33
+
34
+ function removeAddress(index) {
35
+ form.addresses.splice(index, 1);
36
+ }
37
+
38
+ function submit() {
39
+ form.post(route('escalated.admin.settings.email.update'));
40
+ }
41
+
42
+ const dkimStatusClasses = {
43
+ verified: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',
44
+ pending: 'bg-amber-500/10 text-amber-400 border-amber-500/20',
45
+ failed: 'bg-rose-500/10 text-rose-400 border-rose-500/20',
46
+ unknown: 'bg-neutral-500/10 text-neutral-400 border-neutral-500/20',
47
+ };
48
+ </script>
49
+
50
+ <template>
51
+ <EscalatedLayout title="Email Channel Settings">
52
+ <form class="mx-auto max-w-3xl space-y-6" @submit.prevent="submit">
53
+ <!-- Email Addresses -->
54
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
55
+ <div class="mb-5 flex items-center justify-between">
56
+ <div>
57
+ <h3 class="text-sm font-semibold text-[var(--esc-panel-text)]">Support Email Addresses</h3>
58
+ <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">
59
+ Configure multiple email addresses for receiving and responding to tickets
60
+ </p>
61
+ </div>
62
+ <button
63
+ type="button"
64
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-4 py-1.5 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)]"
65
+ @click="showAddForm = true"
66
+ >
67
+ Add Address
68
+ </button>
69
+ </div>
70
+
71
+ <!-- Address Table -->
72
+ <div
73
+ v-if="form.addresses.length"
74
+ class="overflow-hidden rounded-lg border border-[var(--esc-panel-border)]"
75
+ >
76
+ <table class="w-full">
77
+ <thead>
78
+ <tr class="border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)]">
79
+ <th
80
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
81
+ >
82
+ Email
83
+ </th>
84
+ <th
85
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
86
+ >
87
+ Display Name
88
+ </th>
89
+ <th
90
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
91
+ >
92
+ Department
93
+ </th>
94
+ <th
95
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
96
+ >
97
+ DKIM
98
+ </th>
99
+ <th
100
+ class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
101
+ >
102
+ Actions
103
+ </th>
104
+ </tr>
105
+ </thead>
106
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
107
+ <tr
108
+ v-for="(addr, idx) in form.addresses"
109
+ :key="idx"
110
+ class="hover:bg-[var(--esc-panel-hover)]"
111
+ >
112
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-secondary)]">
113
+ {{ addr.email }}
114
+ </td>
115
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
116
+ {{ addr.display_name || '---' }}
117
+ </td>
118
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
119
+ {{ addr.department_id ? `Dept #${addr.department_id}` : 'All' }}
120
+ </td>
121
+ <td class="px-4 py-3">
122
+ <span
123
+ :class="[
124
+ 'inline-flex rounded-full border px-2 py-0.5 text-xs font-medium',
125
+ dkimStatusClasses[addr.dkim_status] || dkimStatusClasses.unknown,
126
+ ]"
127
+ >
128
+ {{ addr.dkim_status || 'Unknown' }}
129
+ </span>
130
+ </td>
131
+ <td class="px-4 py-3 text-right">
132
+ <button
133
+ type="button"
134
+ class="text-sm text-red-500/60 hover:text-red-400"
135
+ @click="removeAddress(idx)"
136
+ >
137
+ Remove
138
+ </button>
139
+ </td>
140
+ </tr>
141
+ </tbody>
142
+ </table>
143
+ </div>
144
+
145
+ <div
146
+ v-else
147
+ class="rounded-lg border border-dashed border-[var(--esc-panel-active)] px-6 py-8 text-center"
148
+ >
149
+ <p class="text-sm text-[var(--esc-panel-text-muted)]">No email addresses configured yet.</p>
150
+ </div>
151
+
152
+ <!-- Add Address Form -->
153
+ <div
154
+ v-if="showAddForm"
155
+ class="mt-4 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)] p-4"
156
+ >
157
+ <h4 class="mb-3 text-sm font-medium text-[var(--esc-panel-text-secondary)]">Add Email Address</h4>
158
+ <div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
159
+ <div>
160
+ <label class="block text-xs font-medium text-[var(--esc-panel-text-tertiary)]"
161
+ >Email Address</label
162
+ >
163
+ <input
164
+ v-model="newAddress.email"
165
+ type="email"
166
+ placeholder="support@example.com"
167
+ class="mt-1 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
168
+ />
169
+ </div>
170
+ <div>
171
+ <label class="block text-xs font-medium text-[var(--esc-panel-text-tertiary)]"
172
+ >Display Name</label
173
+ >
174
+ <input
175
+ v-model="newAddress.display_name"
176
+ type="text"
177
+ placeholder="Support Team"
178
+ class="mt-1 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
179
+ />
180
+ </div>
181
+ <div>
182
+ <label class="block text-xs font-medium text-[var(--esc-panel-text-tertiary)]"
183
+ >Department Routing</label
184
+ >
185
+ <input
186
+ v-model.number="newAddress.department_id"
187
+ type="number"
188
+ placeholder="All departments"
189
+ class="mt-1 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
190
+ />
191
+ </div>
192
+ </div>
193
+ <div class="mt-3 flex gap-2">
194
+ <button
195
+ type="button"
196
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-4 py-1.5 text-sm font-medium text-white"
197
+ :disabled="!newAddress.email"
198
+ @click="addAddress"
199
+ >
200
+ Add
201
+ </button>
202
+ <button
203
+ type="button"
204
+ class="rounded-lg border border-[var(--esc-panel-border-input)] px-4 py-1.5 text-sm text-[var(--esc-panel-text-tertiary)] hover:text-[var(--esc-panel-text-secondary)]"
205
+ @click="showAddForm = false"
206
+ >
207
+ Cancel
208
+ </button>
209
+ </div>
210
+ </div>
211
+ </div>
212
+
213
+ <!-- Default Reply Address -->
214
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
215
+ <h3 class="mb-5 text-sm font-semibold text-[var(--esc-panel-text)]">Default Reply Address</h3>
216
+ <p class="mb-3 text-xs text-[var(--esc-panel-text-muted)]">
217
+ Select the default email address for outbound replies
218
+ </p>
219
+ <select
220
+ v-model="form.default_reply_address"
221
+ class="w-full max-w-sm rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
222
+ >
223
+ <option value="">Select an address...</option>
224
+ <option v-for="(addr, idx) in form.addresses" :key="idx" :value="addr.email">
225
+ {{ addr.display_name ? `${addr.display_name} <${addr.email}>` : addr.email }}
226
+ </option>
227
+ </select>
228
+ </div>
229
+
230
+ <!-- Submit -->
231
+ <div class="flex items-center justify-end gap-3">
232
+ <span v-if="form.recentlySuccessful" class="text-sm text-emerald-400">Saved.</span>
233
+ <button
234
+ type="submit"
235
+ :disabled="form.processing"
236
+ class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-5 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"
237
+ >
238
+ {{ form.processing ? 'Saving...' : 'Save Email Settings' }}
239
+ </button>
240
+ </div>
241
+ </form>
242
+ </EscalatedLayout>
243
+ </template>