@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
@@ -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>
@@ -0,0 +1,135 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ </script>
4
+
5
+ <template>
6
+ <EscalatedLayout title="Sandbox Environment">
7
+ <div class="mx-auto max-w-2xl space-y-6">
8
+ <!-- Info -->
9
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
10
+ <h3 class="mb-4 text-sm font-semibold text-[var(--esc-panel-text)]">Sandbox Environment</h3>
11
+ <p class="text-sm text-[var(--esc-panel-text-tertiary)]">
12
+ A sandbox environment allows you to test configuration changes, automations, and workflows without
13
+ affecting your production data. Changes made in a sandbox can be promoted to production once
14
+ validated.
15
+ </p>
16
+ <div
17
+ class="mt-4 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)] p-4"
18
+ >
19
+ <h4 class="mb-2 text-sm font-medium text-[var(--esc-panel-text-secondary)]">Key Features</h4>
20
+ <ul class="space-y-2 text-sm text-[var(--esc-panel-text-muted)]">
21
+ <li class="flex items-start gap-2">
22
+ <svg
23
+ class="mt-0.5 h-4 w-4 shrink-0 text-[var(--esc-panel-text-muted)]"
24
+ fill="none"
25
+ stroke="currentColor"
26
+ stroke-width="1.5"
27
+ viewBox="0 0 24 24"
28
+ >
29
+ <path
30
+ stroke-linecap="round"
31
+ stroke-linejoin="round"
32
+ d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
33
+ />
34
+ </svg>
35
+ Isolated copy of your helpdesk configuration
36
+ </li>
37
+ <li class="flex items-start gap-2">
38
+ <svg
39
+ class="mt-0.5 h-4 w-4 shrink-0 text-[var(--esc-panel-text-muted)]"
40
+ fill="none"
41
+ stroke="currentColor"
42
+ stroke-width="1.5"
43
+ viewBox="0 0 24 24"
44
+ >
45
+ <path
46
+ stroke-linecap="round"
47
+ stroke-linejoin="round"
48
+ d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
49
+ />
50
+ </svg>
51
+ Test automations, triggers, and SLA policies safely
52
+ </li>
53
+ <li class="flex items-start gap-2">
54
+ <svg
55
+ class="mt-0.5 h-4 w-4 shrink-0 text-[var(--esc-panel-text-muted)]"
56
+ fill="none"
57
+ stroke="currentColor"
58
+ stroke-width="1.5"
59
+ viewBox="0 0 24 24"
60
+ >
61
+ <path
62
+ stroke-linecap="round"
63
+ stroke-linejoin="round"
64
+ d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
65
+ />
66
+ </svg>
67
+ Promote validated changes to production
68
+ </li>
69
+ <li class="flex items-start gap-2">
70
+ <svg
71
+ class="mt-0.5 h-4 w-4 shrink-0 text-[var(--esc-panel-text-muted)]"
72
+ fill="none"
73
+ stroke="currentColor"
74
+ stroke-width="1.5"
75
+ viewBox="0 0 24 24"
76
+ >
77
+ <path
78
+ stroke-linecap="round"
79
+ stroke-linejoin="round"
80
+ d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
81
+ />
82
+ </svg>
83
+ Sandboxes auto-expire after 30 days
84
+ </li>
85
+ </ul>
86
+ </div>
87
+ </div>
88
+
89
+ <!-- Status -->
90
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
91
+ <h3 class="mb-4 text-sm font-semibold text-[var(--esc-panel-text)]">Sandbox Status</h3>
92
+ <div class="rounded-lg border border-dashed border-[var(--esc-panel-active)] px-6 py-8 text-center">
93
+ <svg
94
+ class="mx-auto mb-3 h-10 w-10 text-[var(--esc-panel-text-muted)]"
95
+ fill="none"
96
+ stroke="currentColor"
97
+ stroke-width="1.5"
98
+ viewBox="0 0 24 24"
99
+ >
100
+ <path
101
+ stroke-linecap="round"
102
+ stroke-linejoin="round"
103
+ d="M21 7.5l-2.25-1.313M21 7.5v2.25m0-2.25l-2.25 1.313M3 7.5l2.25-1.313M3 7.5l2.25 1.313M3 7.5v2.25m9 3l2.25-1.313M12 12.75l-2.25-1.313M12 12.75V15m0 6.75l2.25-1.313M12 21.75V19.5m0 2.25l-2.25-1.313m0-16.875L12 2.25l2.25 1.313M21 14.25v2.25l-2.25 1.313m-13.5 0L3 16.5v-2.25"
104
+ />
105
+ </svg>
106
+ <p class="text-sm text-[var(--esc-panel-text-muted)]">No sandbox active.</p>
107
+ <p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
108
+ Create a sandbox to start testing changes safely.
109
+ </p>
110
+ </div>
111
+ </div>
112
+
113
+ <!-- Create Button -->
114
+ <div class="flex justify-end">
115
+ <div class="group relative">
116
+ <button
117
+ type="button"
118
+ disabled
119
+ class="cursor-not-allowed rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)]/50 to-[var(--esc-panel-accent-secondary)]/50 px-5 py-2 text-sm font-medium text-white/50 shadow-lg shadow-[var(--esc-panel-bg)]/20"
120
+ >
121
+ Create Sandbox
122
+ </button>
123
+ <div
124
+ class="pointer-events-none absolute bottom-full left-1/2 mb-2 -translate-x-1/2 rounded-lg bg-neutral-800 px-3 py-2 text-xs text-[var(--esc-panel-text-secondary)] opacity-0 shadow-lg transition-opacity group-hover:opacity-100"
125
+ >
126
+ Coming soon &mdash; requires infrastructure setup
127
+ <div
128
+ class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-neutral-800"
129
+ ></div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </EscalatedLayout>
135
+ </template>
@@ -0,0 +1,218 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { useForm } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({ settings: Object });
6
+
7
+ const form = useForm({
8
+ sso_provider: props.settings.sso_provider ?? 'none',
9
+ sso_entity_id: props.settings.sso_entity_id ?? '',
10
+ sso_url: props.settings.sso_url ?? '',
11
+ sso_certificate: props.settings.sso_certificate ?? '',
12
+ sso_attr_email: props.settings.sso_attr_email ?? 'email',
13
+ sso_attr_name: props.settings.sso_attr_name ?? 'name',
14
+ sso_attr_role: props.settings.sso_attr_role ?? 'role',
15
+ sso_jwt_secret: props.settings.sso_jwt_secret ?? '',
16
+ sso_jwt_algorithm: props.settings.sso_jwt_algorithm ?? 'HS256',
17
+ });
18
+
19
+ function submit() {
20
+ form.post(route('escalated.admin.settings.sso.update'));
21
+ }
22
+ </script>
23
+
24
+ <template>
25
+ <EscalatedLayout title="SSO Settings">
26
+ <form class="mx-auto max-w-2xl space-y-6" @submit.prevent="submit">
27
+ <!-- Provider Selection -->
28
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
29
+ <h3 class="mb-5 text-sm font-semibold text-[var(--esc-panel-text)]">Single Sign-On</h3>
30
+ <div class="space-y-5">
31
+ <div>
32
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
33
+ >SSO Provider</label
34
+ >
35
+ <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">
36
+ Choose how users authenticate with your identity provider
37
+ </p>
38
+ <select
39
+ v-model="form.sso_provider"
40
+ 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)]"
41
+ >
42
+ <option value="none">None (disabled)</option>
43
+ <option value="saml">SAML 2.0</option>
44
+ <option value="jwt">JWT</option>
45
+ </select>
46
+ </div>
47
+ </div>
48
+ </div>
49
+
50
+ <!-- SAML Settings -->
51
+ <div
52
+ v-if="form.sso_provider === 'saml'"
53
+ class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
54
+ >
55
+ <h3 class="mb-5 text-sm font-semibold text-[var(--esc-panel-text)]">SAML Configuration</h3>
56
+ <div class="space-y-5">
57
+ <div>
58
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
59
+ >Entity ID</label
60
+ >
61
+ <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">
62
+ The Entity ID (Issuer) from your Identity Provider
63
+ </p>
64
+ <input
65
+ v-model="form.sso_entity_id"
66
+ type="text"
67
+ placeholder="https://idp.example.com/metadata"
68
+ class="mt-2 w-full 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)] 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)]"
69
+ />
70
+ <div v-if="form.errors.sso_entity_id" class="mt-1 text-sm text-rose-400">
71
+ {{ form.errors.sso_entity_id }}
72
+ </div>
73
+ </div>
74
+ <div>
75
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">SSO URL</label>
76
+ <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">The Single Sign-On URL endpoint</p>
77
+ <input
78
+ v-model="form.sso_url"
79
+ type="url"
80
+ placeholder="https://idp.example.com/sso"
81
+ class="mt-2 w-full 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)] 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)]"
82
+ />
83
+ <div v-if="form.errors.sso_url" class="mt-1 text-sm text-rose-400">
84
+ {{ form.errors.sso_url }}
85
+ </div>
86
+ </div>
87
+ <div>
88
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
89
+ >Certificate</label
90
+ >
91
+ <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">
92
+ X.509 certificate from your Identity Provider (PEM format)
93
+ </p>
94
+ <textarea
95
+ v-model="form.sso_certificate"
96
+ rows="6"
97
+ placeholder="-----BEGIN CERTIFICATE-----&#10;...&#10;-----END CERTIFICATE-----"
98
+ class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 font-mono text-xs 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)]"
99
+ ></textarea>
100
+ <div v-if="form.errors.sso_certificate" class="mt-1 text-sm text-rose-400">
101
+ {{ form.errors.sso_certificate }}
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <!-- JWT Settings -->
108
+ <div
109
+ v-if="form.sso_provider === 'jwt'"
110
+ class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
111
+ >
112
+ <h3 class="mb-5 text-sm font-semibold text-[var(--esc-panel-text)]">JWT Configuration</h3>
113
+ <div class="space-y-5">
114
+ <div>
115
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
116
+ >Shared Secret</label
117
+ >
118
+ <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">
119
+ Secret key used to sign and verify JWT tokens
120
+ </p>
121
+ <input
122
+ v-model="form.sso_jwt_secret"
123
+ type="password"
124
+ class="mt-2 w-full 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)]"
125
+ />
126
+ <div v-if="form.errors.sso_jwt_secret" class="mt-1 text-sm text-rose-400">
127
+ {{ form.errors.sso_jwt_secret }}
128
+ </div>
129
+ </div>
130
+ <div>
131
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
132
+ >Algorithm</label
133
+ >
134
+ <select
135
+ v-model="form.sso_jwt_algorithm"
136
+ 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)]"
137
+ >
138
+ <option value="HS256">HS256</option>
139
+ <option value="HS384">HS384</option>
140
+ <option value="HS512">HS512</option>
141
+ <option value="RS256">RS256</option>
142
+ </select>
143
+ </div>
144
+ <div>
145
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">SSO URL</label>
146
+ <p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">
147
+ Redirect URL where users authenticate and receive a JWT
148
+ </p>
149
+ <input
150
+ v-model="form.sso_url"
151
+ type="url"
152
+ placeholder="https://auth.example.com/jwt-sso"
153
+ class="mt-2 w-full 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)] 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)]"
154
+ />
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Attribute Mapping -->
160
+ <div
161
+ v-if="form.sso_provider !== 'none'"
162
+ class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
163
+ >
164
+ <h3 class="mb-5 text-sm font-semibold text-[var(--esc-panel-text)]">Attribute Mapping</h3>
165
+ <p class="mb-4 text-xs text-[var(--esc-panel-text-muted)]">
166
+ Map the attribute names from your identity provider to Escalated user fields
167
+ </p>
168
+ <div class="space-y-5">
169
+ <div>
170
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
171
+ >Email Attribute</label
172
+ >
173
+ <input
174
+ v-model="form.sso_attr_email"
175
+ type="text"
176
+ placeholder="email"
177
+ class="mt-2 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)] 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)]"
178
+ />
179
+ </div>
180
+ <div>
181
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
182
+ >Name Attribute</label
183
+ >
184
+ <input
185
+ v-model="form.sso_attr_name"
186
+ type="text"
187
+ placeholder="name"
188
+ class="mt-2 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)] 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)]"
189
+ />
190
+ </div>
191
+ <div>
192
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
193
+ >Role Attribute</label
194
+ >
195
+ <input
196
+ v-model="form.sso_attr_role"
197
+ type="text"
198
+ placeholder="role"
199
+ class="mt-2 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)] 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)]"
200
+ />
201
+ </div>
202
+ </div>
203
+ </div>
204
+
205
+ <!-- Submit -->
206
+ <div class="flex items-center justify-end gap-3">
207
+ <span v-if="form.recentlySuccessful" class="text-sm text-emerald-400">Saved.</span>
208
+ <button
209
+ type="submit"
210
+ :disabled="form.processing"
211
+ 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"
212
+ >
213
+ {{ form.processing ? 'Saving...' : 'Save SSO Settings' }}
214
+ </button>
215
+ </div>
216
+ </form>
217
+ </EscalatedLayout>
218
+ </template>