@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,195 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import EscalatedLayout from '../../../../components/EscalatedLayout.vue';
|
|
3
|
+
import { router } from '@inertiajs/vue3';
|
|
4
|
+
import { ref, watch } from 'vue';
|
|
5
|
+
import { Link } from '@inertiajs/vue3';
|
|
6
|
+
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
articles: Object,
|
|
9
|
+
categories: Array,
|
|
10
|
+
filters: Object,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const search = ref(props.filters?.search || '');
|
|
14
|
+
const status = ref(props.filters?.status || '');
|
|
15
|
+
const categoryId = ref(props.filters?.category_id || '');
|
|
16
|
+
|
|
17
|
+
let debounceTimer;
|
|
18
|
+
function applyFilters() {
|
|
19
|
+
clearTimeout(debounceTimer);
|
|
20
|
+
debounceTimer = setTimeout(() => {
|
|
21
|
+
router.get(
|
|
22
|
+
route('escalated.admin.kb-articles.index'),
|
|
23
|
+
{
|
|
24
|
+
search: search.value || undefined,
|
|
25
|
+
status: status.value || undefined,
|
|
26
|
+
category_id: categoryId.value || undefined,
|
|
27
|
+
},
|
|
28
|
+
{ preserveState: true, preserveScroll: true },
|
|
29
|
+
);
|
|
30
|
+
}, 300);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
watch([search, status, categoryId], applyFilters);
|
|
34
|
+
|
|
35
|
+
function deleteArticle(article) {
|
|
36
|
+
if (confirm('Delete this article?')) {
|
|
37
|
+
router.delete(route('escalated.admin.kb-articles.destroy', article.id));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const statusColors = {
|
|
42
|
+
published: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',
|
|
43
|
+
draft: 'bg-amber-500/10 text-amber-400 border-amber-500/20',
|
|
44
|
+
};
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<EscalatedLayout title="Knowledge Base — Articles">
|
|
49
|
+
<div class="mb-6 flex items-center justify-between">
|
|
50
|
+
<div class="flex items-center gap-3">
|
|
51
|
+
<input
|
|
52
|
+
v-model="search"
|
|
53
|
+
type="text"
|
|
54
|
+
placeholder="Search articles..."
|
|
55
|
+
class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40"
|
|
56
|
+
/>
|
|
57
|
+
<select
|
|
58
|
+
v-model="status"
|
|
59
|
+
class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40"
|
|
60
|
+
>
|
|
61
|
+
<option value="">All Statuses</option>
|
|
62
|
+
<option value="draft">Draft</option>
|
|
63
|
+
<option value="published">Published</option>
|
|
64
|
+
</select>
|
|
65
|
+
<select
|
|
66
|
+
v-model="categoryId"
|
|
67
|
+
class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40"
|
|
68
|
+
>
|
|
69
|
+
<option value="">All Categories</option>
|
|
70
|
+
<option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
|
|
71
|
+
</select>
|
|
72
|
+
</div>
|
|
73
|
+
<Link
|
|
74
|
+
:href="route('escalated.admin.kb-articles.create')"
|
|
75
|
+
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-opacity hover:opacity-90"
|
|
76
|
+
>
|
|
77
|
+
New Article
|
|
78
|
+
</Link>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="overflow-hidden rounded-xl border border-[var(--esc-panel-border)]">
|
|
82
|
+
<table class="w-full">
|
|
83
|
+
<thead>
|
|
84
|
+
<tr class="border-b border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)]">
|
|
85
|
+
<th
|
|
86
|
+
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
87
|
+
>
|
|
88
|
+
Title
|
|
89
|
+
</th>
|
|
90
|
+
<th
|
|
91
|
+
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
92
|
+
>
|
|
93
|
+
Category
|
|
94
|
+
</th>
|
|
95
|
+
<th
|
|
96
|
+
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
97
|
+
>
|
|
98
|
+
Status
|
|
99
|
+
</th>
|
|
100
|
+
<th
|
|
101
|
+
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
102
|
+
>
|
|
103
|
+
Views
|
|
104
|
+
</th>
|
|
105
|
+
<th
|
|
106
|
+
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
107
|
+
>
|
|
108
|
+
Helpful
|
|
109
|
+
</th>
|
|
110
|
+
<th
|
|
111
|
+
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
112
|
+
>
|
|
113
|
+
Date
|
|
114
|
+
</th>
|
|
115
|
+
<th
|
|
116
|
+
class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
|
|
117
|
+
>
|
|
118
|
+
Actions
|
|
119
|
+
</th>
|
|
120
|
+
</tr>
|
|
121
|
+
</thead>
|
|
122
|
+
<tbody class="divide-y divide-[var(--esc-panel-border)]">
|
|
123
|
+
<tr v-for="article in articles.data" :key="article.id" class="hover:bg-[var(--esc-panel-hover)]">
|
|
124
|
+
<td class="px-4 py-3">
|
|
125
|
+
<Link
|
|
126
|
+
:href="route('escalated.admin.kb-articles.edit', article.id)"
|
|
127
|
+
class="text-sm font-medium text-[var(--esc-panel-text-secondary)] hover:text-[var(--esc-panel-text)]"
|
|
128
|
+
>
|
|
129
|
+
{{ article.title }}
|
|
130
|
+
</Link>
|
|
131
|
+
</td>
|
|
132
|
+
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
133
|
+
{{ article.category?.name || '—' }}
|
|
134
|
+
</td>
|
|
135
|
+
<td class="px-4 py-3">
|
|
136
|
+
<span
|
|
137
|
+
:class="[
|
|
138
|
+
'inline-flex rounded-full border px-2 py-0.5 text-xs font-medium',
|
|
139
|
+
statusColors[article.status] || 'text-[var(--esc-panel-text-tertiary)]',
|
|
140
|
+
]"
|
|
141
|
+
>
|
|
142
|
+
{{ article.status }}
|
|
143
|
+
</span>
|
|
144
|
+
</td>
|
|
145
|
+
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
146
|
+
{{ article.view_count }}
|
|
147
|
+
</td>
|
|
148
|
+
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
149
|
+
{{ article.helpful_count }}
|
|
150
|
+
</td>
|
|
151
|
+
<td class="px-4 py-3 text-sm text-[var(--esc-panel-text-muted)]">
|
|
152
|
+
{{ article.created_at ? new Date(article.created_at).toLocaleDateString() : '—' }}
|
|
153
|
+
</td>
|
|
154
|
+
<td class="px-4 py-3 text-right">
|
|
155
|
+
<Link
|
|
156
|
+
:href="route('escalated.admin.kb-articles.edit', article.id)"
|
|
157
|
+
class="mr-2 text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
158
|
+
>
|
|
159
|
+
Edit
|
|
160
|
+
</Link>
|
|
161
|
+
<button class="text-sm text-red-500/60 hover:text-red-400" @click="deleteArticle(article)">
|
|
162
|
+
Delete
|
|
163
|
+
</button>
|
|
164
|
+
</td>
|
|
165
|
+
</tr>
|
|
166
|
+
<tr v-if="!articles.data?.length">
|
|
167
|
+
<td colspan="7" class="px-4 py-8 text-center text-sm text-[var(--esc-panel-text-muted)]">
|
|
168
|
+
No articles found.
|
|
169
|
+
</td>
|
|
170
|
+
</tr>
|
|
171
|
+
</tbody>
|
|
172
|
+
</table>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<!-- Pagination -->
|
|
176
|
+
<div v-if="articles.links && articles.last_page > 1" class="mt-4 flex items-center justify-center gap-1">
|
|
177
|
+
<Link
|
|
178
|
+
v-for="link in articles.links"
|
|
179
|
+
:key="link.label"
|
|
180
|
+
:href="link.url || '#'"
|
|
181
|
+
:class="[
|
|
182
|
+
'rounded-lg px-3 py-1.5 text-sm transition-colors',
|
|
183
|
+
link.active
|
|
184
|
+
? 'bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] text-white'
|
|
185
|
+
: link.url
|
|
186
|
+
? 'border border-[var(--esc-panel-border)] text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]'
|
|
187
|
+
: 'cursor-default text-[var(--esc-panel-text-muted)]',
|
|
188
|
+
]"
|
|
189
|
+
preserve-scroll
|
|
190
|
+
>
|
|
191
|
+
<!-- eslint-disable-next-line vue/no-v-html --><span v-html="link.label" />
|
|
192
|
+
</Link>
|
|
193
|
+
</div>
|
|
194
|
+
</EscalatedLayout>
|
|
195
|
+
</template>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import EscalatedLayout from '../../../../components/EscalatedLayout.vue';
|
|
3
|
+
import CategoryTree from '../../../../components/CategoryTree.vue';
|
|
4
|
+
import { useForm, router } from '@inertiajs/vue3';
|
|
5
|
+
import { ref, computed } from 'vue';
|
|
6
|
+
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
categories: Array,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const showForm = ref(false);
|
|
12
|
+
const editing = ref(null);
|
|
13
|
+
|
|
14
|
+
const form = useForm({
|
|
15
|
+
name: '',
|
|
16
|
+
slug: '',
|
|
17
|
+
parent_id: '',
|
|
18
|
+
position: 0,
|
|
19
|
+
description: '',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const isEditing = computed(() => !!editing.value);
|
|
23
|
+
|
|
24
|
+
function openCreate() {
|
|
25
|
+
editing.value = null;
|
|
26
|
+
form.reset();
|
|
27
|
+
showForm.value = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function openEdit(category) {
|
|
31
|
+
editing.value = category;
|
|
32
|
+
form.name = category.name;
|
|
33
|
+
form.slug = category.slug;
|
|
34
|
+
form.parent_id = category.parent_id || '';
|
|
35
|
+
form.position = category.position || 0;
|
|
36
|
+
form.description = category.description || '';
|
|
37
|
+
showForm.value = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function submit() {
|
|
41
|
+
if (isEditing.value) {
|
|
42
|
+
form.put(route('escalated.admin.kb-categories.update', editing.value.id), {
|
|
43
|
+
onSuccess: () => {
|
|
44
|
+
showForm.value = false;
|
|
45
|
+
form.reset();
|
|
46
|
+
editing.value = null;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
form.post(route('escalated.admin.kb-categories.store'), {
|
|
51
|
+
onSuccess: () => {
|
|
52
|
+
showForm.value = false;
|
|
53
|
+
form.reset();
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function deleteCategory(category) {
|
|
60
|
+
if (confirm(`Delete category "${category.name}"?`)) {
|
|
61
|
+
router.delete(route('escalated.admin.kb-categories.destroy', category.id));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const parentOptions = computed(() => {
|
|
66
|
+
if (!editing.value) return props.categories;
|
|
67
|
+
return props.categories.filter((c) => c.id !== editing.value.id);
|
|
68
|
+
});
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<template>
|
|
72
|
+
<EscalatedLayout title="Knowledge Base — Categories">
|
|
73
|
+
<div class="mb-6 flex items-center justify-between">
|
|
74
|
+
<p class="text-sm text-[var(--esc-panel-text-muted)]">
|
|
75
|
+
Organize your knowledge base articles into categories.
|
|
76
|
+
</p>
|
|
77
|
+
<button
|
|
78
|
+
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-opacity hover:opacity-90"
|
|
79
|
+
@click="openCreate"
|
|
80
|
+
>
|
|
81
|
+
New Category
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<!-- Inline form -->
|
|
86
|
+
<div
|
|
87
|
+
v-if="showForm"
|
|
88
|
+
class="mb-6 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5"
|
|
89
|
+
>
|
|
90
|
+
<h3 class="mb-4 text-sm font-semibold text-[var(--esc-panel-text-secondary)]">
|
|
91
|
+
{{ isEditing ? 'Edit Category' : 'New Category' }}
|
|
92
|
+
</h3>
|
|
93
|
+
<form class="space-y-4" @submit.prevent="submit">
|
|
94
|
+
<div class="grid grid-cols-2 gap-4">
|
|
95
|
+
<div>
|
|
96
|
+
<label class="mb-1 block text-xs font-medium text-[var(--esc-panel-text-tertiary)]">Name</label>
|
|
97
|
+
<input
|
|
98
|
+
v-model="form.name"
|
|
99
|
+
type="text"
|
|
100
|
+
class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40"
|
|
101
|
+
placeholder="Category name"
|
|
102
|
+
/>
|
|
103
|
+
<p v-if="form.errors.name" class="mt-1 text-xs text-red-400">{{ form.errors.name }}</p>
|
|
104
|
+
</div>
|
|
105
|
+
<div>
|
|
106
|
+
<label class="mb-1 block text-xs font-medium text-[var(--esc-panel-text-tertiary)]">Slug</label>
|
|
107
|
+
<input
|
|
108
|
+
v-model="form.slug"
|
|
109
|
+
type="text"
|
|
110
|
+
class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40"
|
|
111
|
+
placeholder="auto-generated-if-empty"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="grid grid-cols-2 gap-4">
|
|
116
|
+
<div>
|
|
117
|
+
<label class="mb-1 block text-xs font-medium text-[var(--esc-panel-text-tertiary)]"
|
|
118
|
+
>Parent Category</label
|
|
119
|
+
>
|
|
120
|
+
<select
|
|
121
|
+
v-model="form.parent_id"
|
|
122
|
+
class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40"
|
|
123
|
+
>
|
|
124
|
+
<option value="">None (Root)</option>
|
|
125
|
+
<option v-for="cat in parentOptions" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
|
|
126
|
+
</select>
|
|
127
|
+
</div>
|
|
128
|
+
<div>
|
|
129
|
+
<label class="mb-1 block text-xs font-medium text-[var(--esc-panel-text-tertiary)]"
|
|
130
|
+
>Position</label
|
|
131
|
+
>
|
|
132
|
+
<input
|
|
133
|
+
v-model.number="form.position"
|
|
134
|
+
type="number"
|
|
135
|
+
min="0"
|
|
136
|
+
class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40"
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div>
|
|
141
|
+
<label class="mb-1 block text-xs font-medium text-[var(--esc-panel-text-tertiary)]"
|
|
142
|
+
>Description</label
|
|
143
|
+
>
|
|
144
|
+
<textarea
|
|
145
|
+
v-model="form.description"
|
|
146
|
+
rows="2"
|
|
147
|
+
class="w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40"
|
|
148
|
+
placeholder="Optional description..."
|
|
149
|
+
></textarea>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="flex items-center gap-3">
|
|
152
|
+
<button
|
|
153
|
+
type="submit"
|
|
154
|
+
:disabled="form.processing"
|
|
155
|
+
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-opacity hover:opacity-90 disabled:opacity-50"
|
|
156
|
+
>
|
|
157
|
+
{{ isEditing ? 'Update' : 'Create' }}
|
|
158
|
+
</button>
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
162
|
+
@click="showForm = false"
|
|
163
|
+
>
|
|
164
|
+
Cancel
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
</form>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Category Tree -->
|
|
171
|
+
<CategoryTree :categories="categories" @edit="openEdit" @delete="deleteCategory" />
|
|
172
|
+
|
|
173
|
+
<div
|
|
174
|
+
v-if="!categories?.length && !showForm"
|
|
175
|
+
class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] py-12 text-center"
|
|
176
|
+
>
|
|
177
|
+
<svg
|
|
178
|
+
class="mx-auto mb-3 h-10 w-10 text-[var(--esc-panel-text-muted)]"
|
|
179
|
+
fill="none"
|
|
180
|
+
stroke="currentColor"
|
|
181
|
+
stroke-width="1.5"
|
|
182
|
+
viewBox="0 0 24 24"
|
|
183
|
+
>
|
|
184
|
+
<path
|
|
185
|
+
stroke-linecap="round"
|
|
186
|
+
stroke-linejoin="round"
|
|
187
|
+
d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
|
|
188
|
+
/>
|
|
189
|
+
</svg>
|
|
190
|
+
<p class="text-sm text-[var(--esc-panel-text-muted)]">No categories yet. Create one to get started.</p>
|
|
191
|
+
</div>
|
|
192
|
+
</EscalatedLayout>
|
|
193
|
+
</template>
|