@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,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>
@@ -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>