@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,134 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import AgentLoadIndicator from '../../../components/AgentLoadIndicator.vue';
4
+ import { useForm } from '@inertiajs/vue3';
5
+ import { ref } from 'vue';
6
+
7
+ defineProps({ capacities: Array });
8
+
9
+ const editingId = ref(null);
10
+ const editForm = useForm({ max_concurrent: 10 });
11
+
12
+ function startEdit(cap) {
13
+ editingId.value = cap.id;
14
+ editForm.max_concurrent = cap.max_concurrent;
15
+ }
16
+
17
+ function saveEdit(id) {
18
+ editForm.put(route('escalated.admin.capacity.update', id), {
19
+ preserveScroll: true,
20
+ onSuccess: () => {
21
+ editingId.value = null;
22
+ },
23
+ });
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <EscalatedLayout title="Agent Capacity">
29
+ <div class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)]">
30
+ <table class="min-w-full divide-y divide-[var(--esc-panel-border)]">
31
+ <thead>
32
+ <tr class="bg-[var(--esc-panel-hover)]">
33
+ <th
34
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
35
+ >
36
+ Agent
37
+ </th>
38
+ <th
39
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
40
+ >
41
+ Channel
42
+ </th>
43
+ <th
44
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
45
+ >
46
+ Max Concurrent
47
+ </th>
48
+ <th
49
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
50
+ >
51
+ Current Load
52
+ </th>
53
+ <th
54
+ class="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
55
+ >
56
+ Actions
57
+ </th>
58
+ </tr>
59
+ </thead>
60
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
61
+ <tr v-if="!capacities?.length">
62
+ <td colspan="5" class="px-4 py-12 text-center">
63
+ <svg
64
+ class="mx-auto mb-3 h-8 w-8 text-[var(--esc-panel-text-muted)]"
65
+ fill="none"
66
+ stroke="currentColor"
67
+ stroke-width="1.5"
68
+ viewBox="0 0 24 24"
69
+ >
70
+ <path
71
+ stroke-linecap="round"
72
+ stroke-linejoin="round"
73
+ d="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"
74
+ />
75
+ </svg>
76
+ <p class="text-sm text-[var(--esc-panel-text-muted)]">No capacity records yet</p>
77
+ <p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
78
+ Capacity records are created when agents are assigned tickets
79
+ </p>
80
+ </td>
81
+ </tr>
82
+ <tr
83
+ v-for="cap in capacities"
84
+ :key="cap.id"
85
+ class="transition-colors hover:bg-[var(--esc-panel-hover)]"
86
+ >
87
+ <td class="px-4 py-3 text-sm font-medium text-[var(--esc-panel-text-secondary)]">
88
+ {{ cap.agent_name }}
89
+ </td>
90
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">{{ cap.channel }}</td>
91
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-secondary)]">
92
+ <template v-if="editingId === cap.id">
93
+ <form class="flex items-center gap-2" @submit.prevent="saveEdit(cap.id)">
94
+ <input
95
+ v-model.number="editForm.max_concurrent"
96
+ type="number"
97
+ min="1"
98
+ max="999"
99
+ class="w-20 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-2 py-1 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
+ <button
102
+ type="submit"
103
+ class="text-xs text-[var(--esc-panel-text-secondary)] hover:text-[var(--esc-panel-text)]"
104
+ >
105
+ Save
106
+ </button>
107
+ <button
108
+ type="button"
109
+ class="text-xs text-[var(--esc-panel-text-tertiary)] hover:text-[var(--esc-panel-text-secondary)]"
110
+ @click="editingId = null"
111
+ >
112
+ Cancel
113
+ </button>
114
+ </form>
115
+ </template>
116
+ <template v-else>{{ cap.max_concurrent }}</template>
117
+ </td>
118
+ <td class="px-4 py-3">
119
+ <AgentLoadIndicator :current="cap.current_count" :max="cap.max_concurrent" />
120
+ </td>
121
+ <td class="px-4 py-3 text-right text-sm">
122
+ <button
123
+ class="text-[var(--esc-panel-text-secondary)] hover:text-[var(--esc-panel-text)]"
124
+ @click="startEdit(cap)"
125
+ >
126
+ Edit
127
+ </button>
128
+ </td>
129
+ </tr>
130
+ </tbody>
131
+ </table>
132
+ </div>
133
+ </EscalatedLayout>
134
+ </template>
@@ -0,0 +1,195 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { useForm } from '@inertiajs/vue3';
4
+ import { ref, computed } from 'vue';
5
+
6
+ const props = defineProps({
7
+ field: { type: Object, default: null },
8
+ contexts: { type: Array, default: () => ['ticket', 'user', 'organization'] },
9
+ });
10
+
11
+ const form = useForm({
12
+ name: props.field?.name || '',
13
+ type: props.field?.type || 'text',
14
+ context: props.field?.context || 'ticket',
15
+ options: props.field?.options || [],
16
+ required: props.field?.required ?? false,
17
+ placeholder: props.field?.placeholder || '',
18
+ description: props.field?.description || '',
19
+ position: props.field?.position ?? 0,
20
+ active: props.field?.active ?? true,
21
+ });
22
+
23
+ const newOption = ref('');
24
+
25
+ const showOptionsEditor = computed(() => ['select', 'multi_select'].includes(form.type));
26
+
27
+ function addOption() {
28
+ const val = newOption.value.trim();
29
+ if (val && !form.options.includes(val)) {
30
+ form.options.push(val);
31
+ newOption.value = '';
32
+ }
33
+ }
34
+
35
+ function removeOption(index) {
36
+ form.options.splice(index, 1);
37
+ }
38
+
39
+ function submit() {
40
+ if (props.field) {
41
+ form.put(route('escalated.admin.custom-fields.update', props.field.id));
42
+ } else {
43
+ form.post(route('escalated.admin.custom-fields.store'));
44
+ }
45
+ }
46
+
47
+ const fieldTypes = [
48
+ { value: 'text', label: 'Text' },
49
+ { value: 'textarea', label: 'Textarea' },
50
+ { value: 'select', label: 'Select' },
51
+ { value: 'multi_select', label: 'Multi-Select' },
52
+ { value: 'checkbox', label: 'Checkbox' },
53
+ { value: 'date', label: 'Date' },
54
+ { value: 'number', label: 'Number' },
55
+ ];
56
+ </script>
57
+
58
+ <template>
59
+ <EscalatedLayout :title="field ? 'Edit Custom Field' : 'New Custom Field'">
60
+ <form
61
+ class="mx-auto max-w-lg space-y-5 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
62
+ @submit.prevent="submit"
63
+ >
64
+ <div>
65
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Name</label>
66
+ <input
67
+ v-model="form.name"
68
+ type="text"
69
+ required
70
+ class="mt-1 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)]"
71
+ />
72
+ <div v-if="form.errors.name" class="mt-1 text-sm text-rose-400">{{ form.errors.name }}</div>
73
+ </div>
74
+
75
+ <div>
76
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Type</label>
77
+ <select
78
+ v-model="form.type"
79
+ required
80
+ class="mt-1 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)]"
81
+ >
82
+ <option v-for="ft in fieldTypes" :key="ft.value" :value="ft.value">{{ ft.label }}</option>
83
+ </select>
84
+ <div v-if="form.errors.type" class="mt-1 text-sm text-rose-400">{{ form.errors.type }}</div>
85
+ </div>
86
+
87
+ <div>
88
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Context</label>
89
+ <select
90
+ v-model="form.context"
91
+ required
92
+ class="mt-1 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)]"
93
+ >
94
+ <option v-for="ctx in contexts" :key="ctx" :value="ctx">
95
+ {{ ctx.charAt(0).toUpperCase() + ctx.slice(1) }}
96
+ </option>
97
+ </select>
98
+ <div v-if="form.errors.context" class="mt-1 text-sm text-rose-400">{{ form.errors.context }}</div>
99
+ </div>
100
+
101
+ <!-- Options editor for select/multi_select -->
102
+ <div v-if="showOptionsEditor">
103
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Options</label>
104
+ <div class="mt-1 space-y-2">
105
+ <div v-for="(opt, idx) in form.options" :key="idx" class="flex items-center gap-2">
106
+ <span
107
+ class="flex-1 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-1.5 text-sm text-[var(--esc-panel-text-secondary)]"
108
+ >{{ opt }}</span
109
+ >
110
+ <button
111
+ type="button"
112
+ class="text-rose-400 hover:text-rose-300 text-sm"
113
+ @click="removeOption(idx)"
114
+ >
115
+ Remove
116
+ </button>
117
+ </div>
118
+ <div class="flex items-center gap-2">
119
+ <input
120
+ v-model="newOption"
121
+ type="text"
122
+ placeholder="Add option..."
123
+ class="flex-1 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-1.5 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)]"
124
+ @keydown.enter.prevent="addOption"
125
+ />
126
+ <button
127
+ type="button"
128
+ class="rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] px-3 py-1.5 text-sm font-medium text-[var(--esc-panel-text-secondary)] transition-colors hover:bg-[var(--esc-panel-hover)]"
129
+ @click="addOption"
130
+ >
131
+ Add
132
+ </button>
133
+ </div>
134
+ </div>
135
+ <div v-if="form.errors.options" class="mt-1 text-sm text-rose-400">{{ form.errors.options }}</div>
136
+ </div>
137
+
138
+ <div>
139
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Placeholder</label>
140
+ <input
141
+ v-model="form.placeholder"
142
+ type="text"
143
+ class="mt-1 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)]"
144
+ />
145
+ </div>
146
+
147
+ <div>
148
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Description</label>
149
+ <textarea
150
+ v-model="form.description"
151
+ rows="2"
152
+ class="mt-1 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)]"
153
+ ></textarea>
154
+ </div>
155
+
156
+ <div>
157
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Position</label>
158
+ <input
159
+ v-model.number="form.position"
160
+ type="number"
161
+ min="0"
162
+ class="mt-1 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)]"
163
+ />
164
+ </div>
165
+
166
+ <label class="flex items-center gap-2">
167
+ <input
168
+ v-model="form.required"
169
+ type="checkbox"
170
+ class="rounded border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] text-cyan-500 focus:ring-[var(--esc-panel-border-input)]"
171
+ />
172
+ <span class="text-sm text-[var(--esc-panel-text-secondary)]">Required</span>
173
+ </label>
174
+
175
+ <label class="flex items-center gap-2">
176
+ <input
177
+ v-model="form.active"
178
+ type="checkbox"
179
+ class="rounded border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] text-cyan-500 focus:ring-[var(--esc-panel-border-input)]"
180
+ />
181
+ <span class="text-sm text-[var(--esc-panel-text-secondary)]">Active</span>
182
+ </label>
183
+
184
+ <div class="flex justify-end">
185
+ <button
186
+ type="submit"
187
+ :disabled="form.processing"
188
+ 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"
189
+ >
190
+ {{ field ? 'Update' : 'Create' }}
191
+ </button>
192
+ </div>
193
+ </form>
194
+ </EscalatedLayout>
195
+ </template>
@@ -0,0 +1,134 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { Link, router } from '@inertiajs/vue3';
4
+
5
+ defineProps({ fields: Array });
6
+
7
+ const typeLabels = {
8
+ text: 'Text',
9
+ textarea: 'Textarea',
10
+ select: 'Select',
11
+ multi_select: 'Multi-Select',
12
+ checkbox: 'Checkbox',
13
+ date: 'Date',
14
+ number: 'Number',
15
+ };
16
+
17
+ function destroy(id) {
18
+ if (confirm('Delete this custom field?')) {
19
+ router.delete(route('escalated.admin.custom-fields.destroy', id));
20
+ }
21
+ }
22
+ </script>
23
+
24
+ <template>
25
+ <EscalatedLayout title="Custom Fields">
26
+ <div class="mb-4 flex justify-end">
27
+ <Link
28
+ :href="route('escalated.admin.custom-fields.create')"
29
+ 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)]"
30
+ >
31
+ Add Field
32
+ </Link>
33
+ </div>
34
+ <div class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)]">
35
+ <table class="min-w-full divide-y divide-[var(--esc-panel-border)]">
36
+ <thead>
37
+ <tr class="bg-[var(--esc-panel-hover)]">
38
+ <th
39
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
40
+ >
41
+ Name
42
+ </th>
43
+ <th
44
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
45
+ >
46
+ Type
47
+ </th>
48
+ <th
49
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
50
+ >
51
+ Context
52
+ </th>
53
+ <th
54
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
55
+ >
56
+ Required
57
+ </th>
58
+ <th
59
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
60
+ >
61
+ Position
62
+ </th>
63
+ <th
64
+ class="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
65
+ >
66
+ Actions
67
+ </th>
68
+ </tr>
69
+ </thead>
70
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
71
+ <tr v-if="!fields?.length">
72
+ <td colspan="6" class="px-4 py-12 text-center">
73
+ <svg
74
+ class="mx-auto mb-3 h-8 w-8 text-[var(--esc-panel-text-muted)]"
75
+ fill="none"
76
+ stroke="currentColor"
77
+ stroke-width="1.5"
78
+ viewBox="0 0 24 24"
79
+ >
80
+ <path
81
+ stroke-linecap="round"
82
+ stroke-linejoin="round"
83
+ d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75"
84
+ />
85
+ </svg>
86
+ <p class="text-sm text-[var(--esc-panel-text-muted)]">No custom fields yet</p>
87
+ <p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
88
+ Create custom fields to capture additional data on tickets, users, and organizations
89
+ </p>
90
+ </td>
91
+ </tr>
92
+ <tr
93
+ v-for="field in fields"
94
+ :key="field.id"
95
+ class="transition-colors hover:bg-[var(--esc-panel-hover)]"
96
+ >
97
+ <td class="px-4 py-3 text-sm font-medium text-[var(--esc-panel-text-secondary)]">
98
+ {{ field.name }}
99
+ </td>
100
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
101
+ {{ typeLabels[field.type] || field.type }}
102
+ </td>
103
+ <td class="px-4 py-3 text-sm">
104
+ <span
105
+ class="inline-flex items-center rounded-full bg-[var(--esc-panel-hover)] px-2 py-0.5 text-xs font-medium text-[var(--esc-panel-text-secondary)]"
106
+ >{{ field.context }}</span
107
+ >
108
+ </td>
109
+ <td class="px-4 py-3 text-sm">
110
+ <span
111
+ v-if="field.required"
112
+ class="inline-flex items-center rounded-full bg-cyan-500/10 px-2 py-0.5 text-xs font-medium text-[var(--esc-panel-accent)] ring-1 ring-cyan-500/20"
113
+ >Required</span
114
+ >
115
+ <span v-else class="text-[var(--esc-panel-text-muted)]">-</span>
116
+ </td>
117
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">{{ field.position }}</td>
118
+ <td class="px-4 py-3 text-right text-sm">
119
+ <Link
120
+ :href="route('escalated.admin.custom-fields.edit', field.id)"
121
+ class="text-[var(--esc-panel-text-secondary)] hover:text-[var(--esc-panel-text)]"
122
+ >
123
+ Edit
124
+ </Link>
125
+ <button class="ml-3 text-rose-400 hover:text-rose-300" @click="destroy(field.id)">
126
+ Delete
127
+ </button>
128
+ </td>
129
+ </tr>
130
+ </tbody>
131
+ </table>
132
+ </div>
133
+ </EscalatedLayout>
134
+ </template>