@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,202 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { useForm } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ object: { type: Object, default: null },
7
+ });
8
+
9
+ const isEdit = !!props.object;
10
+
11
+ const form = useForm({
12
+ name: props.object?.name || '',
13
+ slug: props.object?.slug || '',
14
+ fields_schema: props.object?.fields_schema || [],
15
+ });
16
+
17
+ const fieldTypes = [
18
+ { value: 'text', label: 'Text' },
19
+ { value: 'number', label: 'Number' },
20
+ { value: 'select', label: 'Select' },
21
+ { value: 'date', label: 'Date' },
22
+ { value: 'lookup', label: 'Lookup' },
23
+ ];
24
+
25
+ function addField() {
26
+ form.fields_schema.push({
27
+ name: '',
28
+ type: 'text',
29
+ required: false,
30
+ options: [],
31
+ });
32
+ }
33
+
34
+ function removeField(index) {
35
+ form.fields_schema.splice(index, 1);
36
+ }
37
+
38
+ function autoSlug() {
39
+ if (!isEdit) {
40
+ form.slug = form.name
41
+ .toLowerCase()
42
+ .replace(/[^a-z0-9]+/g, '_')
43
+ .replace(/(^_|_$)/g, '');
44
+ }
45
+ }
46
+
47
+ function submit() {
48
+ if (isEdit) {
49
+ form.put(route('escalated.admin.custom-objects.update', props.object.id));
50
+ } else {
51
+ form.post(route('escalated.admin.custom-objects.store'));
52
+ }
53
+ }
54
+ </script>
55
+
56
+ <template>
57
+ <EscalatedLayout :title="isEdit ? 'Edit Custom Object' : 'New Custom Object'">
58
+ <form class="mx-auto max-w-2xl space-y-6" @submit.prevent="submit">
59
+ <!-- Basic Info -->
60
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
61
+ <h3 class="mb-5 text-sm font-semibold text-[var(--esc-panel-text)]">Object Details</h3>
62
+ <div class="space-y-4">
63
+ <div>
64
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Name</label>
65
+ <input
66
+ v-model="form.name"
67
+ type="text"
68
+ placeholder="e.g. Product, Contract"
69
+ 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)]"
70
+ @input="autoSlug"
71
+ />
72
+ <div v-if="form.errors.name" class="mt-1 text-sm text-rose-400">{{ form.errors.name }}</div>
73
+ </div>
74
+ <div>
75
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Slug</label>
76
+ <input
77
+ v-model="form.slug"
78
+ type="text"
79
+ :readonly="isEdit"
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 font-mono 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)]"
81
+ :class="{ 'bg-[var(--esc-panel-surface)] text-[var(--esc-panel-text-muted)]': isEdit }"
82
+ />
83
+ <div v-if="form.errors.slug" class="mt-1 text-sm text-rose-400">{{ form.errors.slug }}</div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <!-- Fields Schema -->
89
+ <div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6">
90
+ <div class="mb-5 flex items-center justify-between">
91
+ <h3 class="text-sm font-semibold text-[var(--esc-panel-text)]">Fields</h3>
92
+ <button
93
+ type="button"
94
+ class="rounded-lg border border-[var(--esc-panel-border-input)] px-3 py-1.5 text-sm text-[var(--esc-panel-text-tertiary)] hover:border-[var(--esc-panel-border-input)] hover:text-[var(--esc-panel-text-secondary)]"
95
+ @click="addField"
96
+ >
97
+ + Add Field
98
+ </button>
99
+ </div>
100
+
101
+ <div v-if="form.fields_schema.length" class="space-y-3">
102
+ <div
103
+ v-for="(field, idx) in form.fields_schema"
104
+ :key="idx"
105
+ class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)] p-4"
106
+ >
107
+ <div class="grid grid-cols-1 gap-3 sm:grid-cols-4">
108
+ <div class="sm:col-span-2">
109
+ <label class="block text-xs font-medium text-[var(--esc-panel-text-tertiary)]"
110
+ >Field Name</label
111
+ >
112
+ <input
113
+ v-model="field.name"
114
+ type="text"
115
+ placeholder="e.g. serial_number"
116
+ 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)]"
117
+ />
118
+ </div>
119
+ <div>
120
+ <label class="block text-xs font-medium text-[var(--esc-panel-text-tertiary)]"
121
+ >Type</label
122
+ >
123
+ <select
124
+ v-model="field.type"
125
+ 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)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
126
+ >
127
+ <option v-for="ft in fieldTypes" :key="ft.value" :value="ft.value">
128
+ {{ ft.label }}
129
+ </option>
130
+ </select>
131
+ </div>
132
+ <div class="flex items-end gap-2">
133
+ <label class="flex items-center gap-2 py-2">
134
+ <input
135
+ v-model="field.required"
136
+ type="checkbox"
137
+ class="rounded border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] text-cyan-500 focus:ring-[var(--esc-panel-border-input)]"
138
+ />
139
+ <span class="text-xs text-[var(--esc-panel-text-tertiary)]">Required</span>
140
+ </label>
141
+ <button
142
+ type="button"
143
+ class="ml-auto rounded p-1 text-[var(--esc-panel-text-muted)] hover:bg-red-500/10 hover:text-red-400"
144
+ @click="removeField(idx)"
145
+ >
146
+ <svg
147
+ class="h-4 w-4"
148
+ fill="none"
149
+ stroke="currentColor"
150
+ stroke-width="1.5"
151
+ viewBox="0 0 24 24"
152
+ >
153
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
154
+ </svg>
155
+ </button>
156
+ </div>
157
+ </div>
158
+ <!-- Options for select type -->
159
+ <div v-if="field.type === 'select'" class="mt-3">
160
+ <label class="block text-xs font-medium text-[var(--esc-panel-text-tertiary)]"
161
+ >Options (comma-separated)</label
162
+ >
163
+ <input
164
+ :value="(field.options || []).join(', ')"
165
+ type="text"
166
+ placeholder="Option1, Option2, Option3"
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
+ @input="
169
+ field.options = $event.target.value
170
+ .split(',')
171
+ .map((s) => s.trim())
172
+ .filter(Boolean)
173
+ "
174
+ />
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <div
180
+ v-else
181
+ class="rounded-lg border border-dashed border-[var(--esc-panel-border)] px-4 py-6 text-center"
182
+ >
183
+ <p class="text-sm text-[var(--esc-panel-text-muted)]">
184
+ No fields defined yet. Click "Add Field" to start.
185
+ </p>
186
+ </div>
187
+ </div>
188
+
189
+ <!-- Submit -->
190
+ <div class="flex items-center justify-end gap-3">
191
+ <span v-if="form.recentlySuccessful" class="text-sm text-emerald-400">Saved.</span>
192
+ <button
193
+ type="submit"
194
+ :disabled="form.processing"
195
+ 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"
196
+ >
197
+ {{ form.processing ? 'Saving...' : isEdit ? 'Update Object' : 'Create Object' }}
198
+ </button>
199
+ </div>
200
+ </form>
201
+ </EscalatedLayout>
202
+ </template>
@@ -0,0 +1,127 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { router } from '@inertiajs/vue3';
4
+ import { Link } from '@inertiajs/vue3';
5
+
6
+ defineProps({
7
+ objects: { type: Array, default: () => [] },
8
+ });
9
+
10
+ function deleteObject(obj) {
11
+ if (confirm(`Delete custom object "${obj.name}"? This will also delete all its records.`)) {
12
+ router.delete(route('escalated.admin.custom-objects.destroy', obj.id));
13
+ }
14
+ }
15
+ </script>
16
+
17
+ <template>
18
+ <EscalatedLayout title="Custom Objects">
19
+ <div class="mb-6 flex items-center justify-between">
20
+ <div>
21
+ <h2 class="text-lg font-semibold text-[var(--esc-panel-text-secondary)]">Custom Objects</h2>
22
+ <p class="mt-1 text-sm text-[var(--esc-panel-text-muted)]">
23
+ Define custom data structures with fields and relationships
24
+ </p>
25
+ </div>
26
+ <Link
27
+ :href="route('escalated.admin.custom-objects.create')"
28
+ 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)]"
29
+ >
30
+ New Object
31
+ </Link>
32
+ </div>
33
+
34
+ <div v-if="objects.length" class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)]">
35
+ <table class="w-full">
36
+ <thead>
37
+ <tr class="border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)]">
38
+ <th
39
+ class="px-4 py-3 text-left text-xs font-medium 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-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
45
+ >
46
+ Slug
47
+ </th>
48
+ <th
49
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
50
+ >
51
+ Fields
52
+ </th>
53
+ <th
54
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
55
+ >
56
+ Records
57
+ </th>
58
+ <th
59
+ class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
60
+ >
61
+ Actions
62
+ </th>
63
+ </tr>
64
+ </thead>
65
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
66
+ <tr v-for="obj in objects" :key="obj.id" class="hover:bg-[var(--esc-panel-hover)]">
67
+ <td class="px-4 py-3">
68
+ <Link
69
+ :href="route('escalated.admin.custom-objects.edit', obj.id)"
70
+ class="text-sm font-medium text-[var(--esc-panel-text-secondary)] hover:text-[var(--esc-panel-text)]"
71
+ >
72
+ {{ obj.name }}
73
+ </Link>
74
+ </td>
75
+ <td class="px-4 py-3 text-sm font-mono text-[var(--esc-panel-text-muted)]">{{ obj.slug }}</td>
76
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
77
+ {{ obj.fields_schema?.length || 0 }}
78
+ </td>
79
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
80
+ {{ obj.records_count ?? 0 }}
81
+ </td>
82
+ <td class="px-4 py-3 text-right">
83
+ <Link
84
+ :href="route('escalated.admin.custom-objects.records', obj.id)"
85
+ class="mr-3 text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
86
+ >
87
+ Records
88
+ </Link>
89
+ <Link
90
+ :href="route('escalated.admin.custom-objects.edit', obj.id)"
91
+ class="mr-3 text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
92
+ >
93
+ Edit
94
+ </Link>
95
+ <button class="text-sm text-red-500/60 hover:text-red-400" @click="deleteObject(obj)">
96
+ Delete
97
+ </button>
98
+ </td>
99
+ </tr>
100
+ </tbody>
101
+ </table>
102
+ </div>
103
+
104
+ <div v-else class="rounded-xl border border-dashed border-[var(--esc-panel-border)] px-6 py-12 text-center">
105
+ <svg
106
+ class="mx-auto mb-3 h-10 w-10 text-[var(--esc-panel-text-muted)]"
107
+ fill="none"
108
+ stroke="currentColor"
109
+ stroke-width="1.5"
110
+ viewBox="0 0 24 24"
111
+ >
112
+ <path
113
+ stroke-linecap="round"
114
+ stroke-linejoin="round"
115
+ d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"
116
+ />
117
+ </svg>
118
+ <p class="text-sm text-[var(--esc-panel-text-muted)]">No custom objects defined yet.</p>
119
+ <Link
120
+ :href="route('escalated.admin.custom-objects.create')"
121
+ class="mt-3 inline-block text-sm text-[var(--esc-panel-accent)] hover:text-[var(--esc-panel-accent)]"
122
+ >
123
+ Create your first custom object
124
+ </Link>
125
+ </div>
126
+ </EscalatedLayout>
127
+ </template>
@@ -0,0 +1,228 @@
1
+ <script setup>
2
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
+ import { useForm, router } from '@inertiajs/vue3';
4
+ import { ref, computed } from 'vue';
5
+
6
+ const props = defineProps({
7
+ object: Object,
8
+ records: { type: Array, default: () => [] },
9
+ });
10
+
11
+ const fields = computed(() => props.object.fields_schema || []);
12
+ const editingId = ref(null);
13
+ const showAddForm = ref(false);
14
+
15
+ const newRecordForm = useForm({
16
+ data: {},
17
+ });
18
+
19
+ const editForm = useForm({
20
+ data: {},
21
+ });
22
+
23
+ function startAdd() {
24
+ const data = {};
25
+ fields.value.forEach((f) => {
26
+ data[f.name] = '';
27
+ });
28
+ newRecordForm.data = data;
29
+ showAddForm.value = true;
30
+ }
31
+
32
+ function addRecord() {
33
+ newRecordForm.post(route('escalated.admin.custom-objects.records.store', props.object.id), {
34
+ preserveScroll: true,
35
+ onSuccess: () => {
36
+ showAddForm.value = false;
37
+ newRecordForm.reset();
38
+ },
39
+ });
40
+ }
41
+
42
+ function startEdit(record) {
43
+ editingId.value = record.id;
44
+ editForm.data = { ...(record.data || {}) };
45
+ }
46
+
47
+ function saveEdit(record) {
48
+ editForm.put(route('escalated.admin.custom-objects.records.update', [props.object.id, record.id]), {
49
+ preserveScroll: true,
50
+ onSuccess: () => {
51
+ editingId.value = null;
52
+ },
53
+ });
54
+ }
55
+
56
+ function cancelEdit() {
57
+ editingId.value = null;
58
+ }
59
+
60
+ function deleteRecord(record) {
61
+ if (confirm('Delete this record?')) {
62
+ router.delete(route('escalated.admin.custom-objects.records.destroy', [props.object.id, record.id]), {
63
+ preserveScroll: true,
64
+ });
65
+ }
66
+ }
67
+ </script>
68
+
69
+ <template>
70
+ <EscalatedLayout :title="`${object.name} Records`">
71
+ <div class="mb-6 flex items-center justify-between">
72
+ <div>
73
+ <h2 class="text-lg font-semibold text-[var(--esc-panel-text-secondary)]">{{ object.name }} Records</h2>
74
+ <p class="mt-1 text-sm text-[var(--esc-panel-text-muted)]">{{ records.length }} records</p>
75
+ </div>
76
+ <button
77
+ type="button"
78
+ 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)]"
79
+ @click="startAdd"
80
+ >
81
+ Add Record
82
+ </button>
83
+ </div>
84
+
85
+ <!-- Add Record Form -->
86
+ <div
87
+ v-if="showAddForm"
88
+ class="mb-6 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
89
+ >
90
+ <h3 class="mb-4 text-sm font-semibold text-[var(--esc-panel-text)]">New Record</h3>
91
+ <form class="space-y-3" @submit.prevent="addRecord">
92
+ <div v-for="field in fields" :key="field.name">
93
+ <label class="block text-xs font-medium text-[var(--esc-panel-text-tertiary)]">
94
+ {{ field.name }}
95
+ <span v-if="field.required" class="text-rose-400">*</span>
96
+ </label>
97
+ <select
98
+ v-if="field.type === 'select'"
99
+ v-model="newRecordForm.data[field.name]"
100
+ 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)]"
101
+ >
102
+ <option value="">Select...</option>
103
+ <option v-for="opt in field.options" :key="opt" :value="opt">{{ opt }}</option>
104
+ </select>
105
+ <input
106
+ v-else-if="field.type === 'date'"
107
+ v-model="newRecordForm.data[field.name]"
108
+ type="date"
109
+ 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)]"
110
+ />
111
+ <input
112
+ v-else-if="field.type === 'number'"
113
+ v-model="newRecordForm.data[field.name]"
114
+ type="number"
115
+ 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)]"
116
+ />
117
+ <input
118
+ v-else
119
+ v-model="newRecordForm.data[field.name]"
120
+ type="text"
121
+ 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)]"
122
+ />
123
+ </div>
124
+ <div class="flex gap-2">
125
+ <button
126
+ type="submit"
127
+ :disabled="newRecordForm.processing"
128
+ 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"
129
+ >
130
+ Save
131
+ </button>
132
+ <button
133
+ type="button"
134
+ class="rounded-lg border border-[var(--esc-panel-border-input)] px-4 py-1.5 text-sm text-[var(--esc-panel-text-tertiary)]"
135
+ @click="showAddForm = false"
136
+ >
137
+ Cancel
138
+ </button>
139
+ </div>
140
+ </form>
141
+ </div>
142
+
143
+ <!-- Records Table -->
144
+ <div v-if="records.length" class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)]">
145
+ <table class="w-full">
146
+ <thead>
147
+ <tr class="border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)]">
148
+ <th
149
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
150
+ >
151
+ ID
152
+ </th>
153
+ <th
154
+ v-for="field in fields"
155
+ :key="field.name"
156
+ class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
157
+ >
158
+ {{ field.name }}
159
+ </th>
160
+ <th
161
+ class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
162
+ >
163
+ Actions
164
+ </th>
165
+ </tr>
166
+ </thead>
167
+ <tbody class="divide-y divide-[var(--esc-panel-border)]">
168
+ <tr v-for="record in records" :key="record.id" class="hover:bg-[var(--esc-panel-hover)]">
169
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-muted)]">{{ record.id }}</td>
170
+ <td v-for="field in fields" :key="field.name" class="px-4 py-3">
171
+ <!-- Edit mode -->
172
+ <template v-if="editingId === record.id">
173
+ <input
174
+ v-model="editForm.data[field.name]"
175
+ :type="field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'"
176
+ class="w-full rounded border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] px-2 py-1 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none"
177
+ />
178
+ </template>
179
+ <!-- View mode -->
180
+ <template v-else>
181
+ <span class="text-sm text-[var(--esc-panel-text-secondary)]">
182
+ {{ record.data?.[field.name] || '---' }}
183
+ </span>
184
+ </template>
185
+ </td>
186
+ <td class="px-4 py-3 text-right">
187
+ <template v-if="editingId === record.id">
188
+ <button
189
+ class="mr-2 text-sm text-emerald-400 hover:text-emerald-300"
190
+ @click="saveEdit(record)"
191
+ >
192
+ Save
193
+ </button>
194
+ <button
195
+ class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
196
+ @click="cancelEdit"
197
+ >
198
+ Cancel
199
+ </button>
200
+ </template>
201
+ <template v-else>
202
+ <button
203
+ class="mr-2 text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
204
+ @click="startEdit(record)"
205
+ >
206
+ Edit
207
+ </button>
208
+ <button
209
+ class="text-sm text-red-500/60 hover:text-red-400"
210
+ @click="deleteRecord(record)"
211
+ >
212
+ Delete
213
+ </button>
214
+ </template>
215
+ </td>
216
+ </tr>
217
+ </tbody>
218
+ </table>
219
+ </div>
220
+
221
+ <div
222
+ v-else-if="!showAddForm"
223
+ class="rounded-xl border border-dashed border-[var(--esc-panel-border)] px-6 py-12 text-center"
224
+ >
225
+ <p class="text-sm text-[var(--esc-panel-text-muted)]">No records yet.</p>
226
+ </div>
227
+ </EscalatedLayout>
228
+ </template>
@@ -22,27 +22,51 @@ function submit() {
22
22
 
23
23
  <template>
24
24
  <EscalatedLayout :title="department ? 'Edit Department' : 'New Department'">
25
- <form @submit.prevent="submit" class="mx-auto max-w-lg space-y-5 rounded-xl border border-white/[0.06] bg-neutral-900/60 p-6">
25
+ <form
26
+ class="mx-auto max-w-lg space-y-5 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
27
+ @submit.prevent="submit"
28
+ >
26
29
  <div>
27
- <label class="block text-sm font-medium text-neutral-300">Name</label>
28
- <input v-model="form.name" type="text" required class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10" />
30
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Name</label>
31
+ <input
32
+ v-model="form.name"
33
+ type="text"
34
+ required
35
+ 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)]"
36
+ />
29
37
  <div v-if="form.errors.name" class="mt-1 text-sm text-rose-400">{{ form.errors.name }}</div>
30
38
  </div>
31
39
  <div>
32
- <label class="block text-sm font-medium text-neutral-300">Slug</label>
33
- <input v-model="form.slug" type="text" placeholder="Auto-generated if empty" class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-gray-500 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10" />
40
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Slug</label>
41
+ <input
42
+ v-model="form.slug"
43
+ type="text"
44
+ placeholder="Auto-generated if empty"
45
+ 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-gray-500 focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
46
+ />
34
47
  </div>
35
48
  <div>
36
- <label class="block text-sm font-medium text-neutral-300">Description</label>
37
- <textarea v-model="form.description" rows="3" class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"></textarea>
49
+ <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Description</label>
50
+ <textarea
51
+ v-model="form.description"
52
+ rows="3"
53
+ 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)]"
54
+ ></textarea>
38
55
  </div>
39
56
  <label class="flex items-center gap-2">
40
- <input v-model="form.is_active" type="checkbox" class="rounded border-white/20 bg-neutral-900 text-white focus:ring-white/10" />
41
- <span class="text-sm text-neutral-300">Active</span>
57
+ <input
58
+ v-model="form.is_active"
59
+ type="checkbox"
60
+ class="rounded border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] text-[var(--esc-panel-text)] focus:ring-[var(--esc-panel-border-input)]"
61
+ />
62
+ <span class="text-sm text-[var(--esc-panel-text-secondary)]">Active</span>
42
63
  </label>
43
64
  <div class="flex justify-end">
44
- <button type="submit" :disabled="form.processing"
45
- class="rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-5 py-2 text-sm font-medium text-white shadow-lg shadow-black/20 transition-all hover:from-cyan-400 hover:to-violet-400 disabled:opacity-50">
65
+ <button
66
+ type="submit"
67
+ :disabled="form.processing"
68
+ 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"
69
+ >
46
70
  {{ department ? 'Update' : 'Create' }}
47
71
  </button>
48
72
  </div>