@escalated-dev/escalated 0.6.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.
- package/README.md +94 -2
- package/package.json +31 -2
- package/src/components/ActivityTimeline.stories.js +55 -0
- package/src/components/ActivityTimeline.vue +24 -7
- package/src/components/AdminDashboard.stories.js +1095 -0
- package/src/components/AgentLoadIndicator.stories.js +37 -0
- package/src/components/AgentLoadIndicator.vue +49 -0
- package/src/components/AgentPerformanceCard.vue +44 -0
- package/src/components/ArticleEditor.vue +82 -0
- package/src/components/AssigneeSelect.vue +19 -5
- package/src/components/AttachmentList.vue +53 -37
- package/src/components/AuditLogEntry.vue +95 -0
- package/src/components/BulkActionBar.vue +99 -30
- package/src/components/CategoryTree.vue +89 -0
- package/src/components/ChartWidget.vue +46 -0
- package/src/components/CollisionWarning.stories.js +30 -0
- package/src/components/CollisionWarning.vue +68 -0
- package/src/components/ConditionalFieldRules.vue +135 -0
- package/src/components/ContextPanel.vue +66 -0
- package/src/components/ContextPanelSection.vue +65 -0
- package/src/components/CustomFieldRenderer.vue +159 -0
- package/src/components/EscalatedLayout.vue +400 -69
- package/src/components/FileDropzone.vue +26 -10
- package/src/components/FollowButton.stories.js +30 -0
- package/src/components/FollowButton.vue +53 -28
- package/src/components/KeyboardShortcutHelp.vue +129 -39
- package/src/components/KnowledgePanel.vue +122 -0
- package/src/components/KpiCard.stories.js +32 -0
- package/src/components/KpiCard.vue +88 -0
- package/src/components/MacroDropdown.vue +95 -35
- package/src/components/PermissionMatrix.vue +107 -0
- package/src/components/PinnedNotes.vue +70 -31
- package/src/components/PresenceIndicator.vue +93 -27
- package/src/components/PriorityBadge.stories.js +33 -0
- package/src/components/PriorityBadge.vue +9 -2
- package/src/components/QuickFilters.vue +65 -49
- package/src/components/ReplyComposer.vue +144 -30
- package/src/components/ReplyThread.vue +72 -20
- package/src/components/SatisfactionRating.vue +98 -46
- package/src/components/ScheduleEditor.vue +104 -0
- package/src/components/SideConversation.vue +165 -0
- package/src/components/SideConversationList.vue +147 -0
- package/src/components/SkillTagManager.vue +79 -0
- package/src/components/SlaComplianceChart.vue +53 -0
- package/src/components/SlaTimer.stories.js +35 -0
- package/src/components/SlaTimer.vue +17 -4
- package/src/components/StatsCard.stories.js +24 -0
- package/src/components/StatsCard.vue +58 -46
- package/src/components/StatusBadge.stories.js +53 -0
- package/src/components/StatusBadge.vue +36 -4
- package/src/components/TagSelect.vue +43 -12
- package/src/components/TicketFilters.vue +225 -69
- package/src/components/TicketLinkPanel.vue +193 -0
- package/src/components/TicketList.vue +594 -193
- package/src/components/TicketMergeDialog.vue +157 -0
- package/src/components/TicketSidebar.vue +167 -99
- package/src/components/TicketTypeSelector.vue +21 -0
- package/src/components/TotpInput.stories.js +9 -0
- package/src/components/TotpInput.vue +81 -0
- package/src/components/TwoFactorSetup.vue +113 -0
- package/src/composables/useDebouncedSearch.js +38 -0
- package/src/composables/usePluginExtensions.js +95 -3
- package/src/composables/usePluginHooks.js +65 -0
- package/src/defineEscalatedPlugin.js +34 -8
- package/src/index.js +72 -35
- package/src/locales/en.json +25 -0
- package/src/pages/Admin/ApiTokens/Index.vue +231 -67
- package/src/pages/Admin/AuditLog/Index.vue +154 -0
- package/src/pages/Admin/Automations/Form.vue +184 -0
- package/src/pages/Admin/Automations/Index.vue +129 -0
- package/src/pages/Admin/BusinessHours/Form.vue +172 -0
- package/src/pages/Admin/BusinessHours/Index.vue +113 -0
- package/src/pages/Admin/CannedResponses/Index.vue +127 -70
- package/src/pages/Admin/Capacity/Index.vue +134 -0
- package/src/pages/Admin/CustomFields/Form.vue +195 -0
- package/src/pages/Admin/CustomFields/Index.vue +134 -0
- package/src/pages/Admin/CustomObjects/Form.vue +202 -0
- package/src/pages/Admin/CustomObjects/Index.vue +127 -0
- package/src/pages/Admin/CustomObjects/Records.vue +228 -0
- package/src/pages/Admin/Departments/Form.vue +75 -51
- package/src/pages/Admin/Departments/Index.vue +109 -57
- package/src/pages/Admin/EscalationRules/Form.vue +157 -96
- package/src/pages/Admin/EscalationRules/Index.vue +178 -57
- package/src/pages/Admin/Import/Connect.vue +96 -0
- package/src/pages/Admin/Import/Index.vue +102 -0
- package/src/pages/Admin/Import/Mapping.vue +110 -0
- package/src/pages/Admin/Import/Progress.vue +153 -0
- package/src/pages/Admin/Import/Review.vue +76 -0
- package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
- package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
- package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
- package/src/pages/Admin/Macros/Index.vue +250 -89
- package/src/pages/Admin/Plugins/Index.vue +205 -75
- package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
- package/src/pages/Admin/Reports/CsatReport.vue +127 -0
- package/src/pages/Admin/Reports/Dashboard.vue +211 -0
- package/src/pages/Admin/Reports/SlaReport.vue +166 -0
- package/src/pages/Admin/Reports.vue +139 -34
- package/src/pages/Admin/Roles/Form.vue +71 -0
- package/src/pages/Admin/Roles/Index.vue +122 -0
- package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
- package/src/pages/Admin/Settings/DataRetention.vue +182 -0
- package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
- package/src/pages/Admin/Settings/Sandbox.vue +135 -0
- package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
- package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
- package/src/pages/Admin/Settings.vue +468 -260
- package/src/pages/Admin/Skills/Form.vue +53 -0
- package/src/pages/Admin/Skills/Index.vue +94 -0
- package/src/pages/Admin/SlaPolicies/Form.vue +118 -75
- package/src/pages/Admin/SlaPolicies/Index.vue +120 -59
- package/src/pages/Admin/Statuses/Form.vue +114 -0
- package/src/pages/Admin/Statuses/Index.vue +153 -0
- package/src/pages/Admin/Tags/Index.vue +191 -105
- package/src/pages/Admin/Tickets/Index.vue +91 -39
- package/src/pages/Admin/Tickets/Show.vue +101 -37
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
- package/src/pages/Admin/Webhooks/Form.vue +110 -0
- package/src/pages/Admin/Webhooks/Index.vue +136 -0
- package/src/pages/Agent/Dashboard.vue +311 -186
- package/src/pages/Agent/TicketIndex.vue +88 -38
- package/src/pages/Agent/TicketShow.vue +258 -51
- package/src/pages/Customer/Create.vue +36 -9
- package/src/pages/Customer/Index.vue +31 -24
- package/src/pages/Customer/KnowledgeBase/Article.vue +127 -0
- package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
- package/src/plugin.js +86 -1
- package/src/utils/constants.js +30 -0
- package/src/utils/formatting.js +35 -0
- 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>
|