@escalated-dev/escalated 0.2.1 → 0.4.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/package.json +1 -1
- package/src/components/ActivityTimeline.vue +8 -4
- package/src/components/AssigneeSelect.vue +6 -2
- package/src/components/AttachmentList.vue +9 -3
- package/src/components/BulkActionBar.vue +97 -0
- package/src/components/EscalatedLayout.vue +101 -94
- package/src/components/FileDropzone.vue +9 -4
- package/src/components/FollowButton.vue +63 -0
- package/src/components/KeyboardShortcutHelp.vue +123 -0
- package/src/components/MacroDropdown.vue +90 -0
- package/src/components/PinnedNotes.vue +87 -0
- package/src/components/PresenceIndicator.vue +103 -0
- package/src/components/PriorityBadge.vue +20 -2
- package/src/components/QuickFilters.vue +49 -0
- package/src/components/ReplyComposer.vue +52 -3
- package/src/components/ReplyThread.vue +60 -30
- package/src/components/SatisfactionRating.vue +128 -0
- package/src/components/SlaTimer.vue +12 -6
- package/src/components/StatsCard.vue +26 -6
- package/src/components/StatusBadge.vue +24 -2
- package/src/components/TagSelect.vue +8 -4
- package/src/components/TicketFilters.vue +69 -52
- package/src/components/TicketList.vue +193 -51
- package/src/components/TicketSidebar.vue +99 -70
- package/src/composables/useKeyboardShortcuts.js +46 -0
- package/src/index.js +11 -0
- package/src/pages/Admin/CannedResponses/Index.vue +29 -17
- package/src/pages/Admin/Departments/Form.vue +12 -11
- package/src/pages/Admin/Departments/Index.vue +26 -18
- package/src/pages/Admin/EscalationRules/Form.vue +21 -20
- package/src/pages/Admin/EscalationRules/Index.vue +26 -18
- package/src/pages/Admin/Macros/Index.vue +287 -0
- package/src/pages/Admin/Reports.vue +101 -52
- package/src/pages/Admin/Settings.vue +260 -0
- package/src/pages/Admin/SlaPolicies/Form.vue +21 -20
- package/src/pages/Admin/SlaPolicies/Index.vue +28 -20
- package/src/pages/Admin/Tags/Index.vue +48 -23
- package/src/pages/Admin/Tickets/Index.vue +39 -0
- package/src/pages/Admin/Tickets/Show.vue +145 -0
- package/src/pages/Agent/Dashboard.vue +156 -51
- package/src/pages/Agent/TicketIndex.vue +38 -21
- package/src/pages/Agent/TicketShow.vue +144 -108
- package/src/pages/Customer/Show.vue +63 -55
- package/src/pages/Guest/Create.vue +97 -0
- package/src/pages/Guest/Show.vue +93 -0
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed, inject } from 'vue';
|
|
3
|
+
|
|
2
4
|
defineProps({
|
|
3
5
|
activities: { type: Array, required: true },
|
|
4
6
|
});
|
|
5
7
|
|
|
8
|
+
const dark = inject('esc-dark', computed(() => false));
|
|
9
|
+
|
|
6
10
|
const typeLabels = {
|
|
7
11
|
status_changed: 'changed status',
|
|
8
12
|
assigned: 'assigned ticket',
|
|
@@ -51,12 +55,12 @@ function describeActivity(activity) {
|
|
|
51
55
|
<template>
|
|
52
56
|
<div class="space-y-3">
|
|
53
57
|
<div v-for="activity in activities" :key="activity.id" class="flex gap-3 text-sm">
|
|
54
|
-
<div class="mt-1 h-2 w-2 flex-shrink-0 rounded-full bg-gray-400"></div>
|
|
58
|
+
<div :class="['mt-1 h-2 w-2 flex-shrink-0 rounded-full', dark ? 'bg-neutral-600' : 'bg-gray-400']"></div>
|
|
55
59
|
<div class="flex-1">
|
|
56
|
-
<p class="text-gray-700">{{ describeActivity(activity) }}</p>
|
|
57
|
-
<p class="text-xs text-gray-400">{{ formatDate(activity.created_at) }}</p>
|
|
60
|
+
<p :class="dark ? 'text-neutral-300' : 'text-gray-700'">{{ describeActivity(activity) }}</p>
|
|
61
|
+
<p :class="['text-xs', dark ? 'text-neutral-600' : 'text-gray-400']">{{ formatDate(activity.created_at) }}</p>
|
|
58
62
|
</div>
|
|
59
63
|
</div>
|
|
60
|
-
<div v-if="!activities?.length" class="py-4 text-center text-sm text-gray-500">No activity yet.</div>
|
|
64
|
+
<div v-if="!activities?.length" :class="['py-4 text-center text-sm', dark ? 'text-neutral-600' : 'text-gray-500']">No activity yet.</div>
|
|
61
65
|
</div>
|
|
62
66
|
</template>
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed, inject } from 'vue';
|
|
3
|
+
|
|
2
4
|
defineProps({
|
|
3
5
|
agents: { type: Array, required: true },
|
|
4
6
|
modelValue: { type: [Number, String], default: null },
|
|
5
7
|
});
|
|
6
8
|
|
|
7
9
|
const emit = defineEmits(['update:modelValue']);
|
|
10
|
+
const dark = inject('esc-dark', computed(() => false));
|
|
8
11
|
</script>
|
|
9
12
|
|
|
10
13
|
<template>
|
|
11
14
|
<div>
|
|
12
|
-
<label class="mb-1 block text-xs font-medium text-gray-600">Assigned To</label>
|
|
15
|
+
<label :class="['mb-1 block text-xs font-medium', dark ? 'text-neutral-500' : 'text-gray-600']">Assigned To</label>
|
|
13
16
|
<select :value="modelValue" @change="emit('update:modelValue', $event.target.value || null)"
|
|
14
|
-
class="w-full rounded-md border
|
|
17
|
+
:class="['w-full rounded-md border px-2 py-1.5 text-sm focus:outline-none',
|
|
18
|
+
dark ? 'border-white/10 bg-neutral-950 text-neutral-200 focus:border-white/20 focus:ring-1 focus:ring-white/10' : 'border-gray-300 focus:border-blue-500']">
|
|
15
19
|
<option value="">Unassigned</option>
|
|
16
20
|
<option v-for="agent in agents" :key="agent.id" :value="agent.id">{{ agent.name }}</option>
|
|
17
21
|
</select>
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed, inject } from 'vue';
|
|
3
|
+
|
|
2
4
|
defineProps({
|
|
3
5
|
attachments: { type: Array, required: true },
|
|
4
6
|
});
|
|
5
7
|
|
|
8
|
+
const dark = inject('esc-dark', computed(() => false));
|
|
9
|
+
|
|
6
10
|
function formatSize(bytes) {
|
|
7
11
|
if (bytes < 1024) return bytes + ' B';
|
|
8
12
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
@@ -20,12 +24,14 @@ function iconForMime(mime) {
|
|
|
20
24
|
<template>
|
|
21
25
|
<div class="space-y-1">
|
|
22
26
|
<div v-for="attachment in attachments" :key="attachment.id"
|
|
23
|
-
class="flex items-center gap-2 rounded border
|
|
27
|
+
:class="['flex items-center gap-2 rounded-lg border px-3 py-2 text-sm',
|
|
28
|
+
dark ? 'border-white/[0.06] bg-white/[0.03]' : 'border-gray-200 bg-gray-50']">
|
|
24
29
|
<span>{{ iconForMime(attachment.mime_type) }}</span>
|
|
25
|
-
<a :href="attachment.url" target="_blank"
|
|
30
|
+
<a :href="attachment.url" target="_blank"
|
|
31
|
+
:class="['flex-1 truncate font-medium hover:underline', dark ? 'text-white' : 'text-blue-600']">
|
|
26
32
|
{{ attachment.original_filename }}
|
|
27
33
|
</a>
|
|
28
|
-
<span class="text-xs text-gray-400">{{ formatSize(attachment.size) }}</span>
|
|
34
|
+
<span :class="['text-xs', dark ? 'text-neutral-500' : 'text-gray-400']">{{ formatSize(attachment.size) }}</span>
|
|
29
35
|
</div>
|
|
30
36
|
</div>
|
|
31
37
|
</template>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, inject } from 'vue';
|
|
3
|
+
import { router } from '@inertiajs/vue3';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
selectedIds: { type: Array, required: true },
|
|
7
|
+
bulkRoute: { type: String, required: true },
|
|
8
|
+
agents: { type: Array, default: () => [] },
|
|
9
|
+
departments: { type: Array, default: () => [] },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits(['clear']);
|
|
13
|
+
const escDark = inject('esc-dark', computed(() => false));
|
|
14
|
+
const processing = ref(false);
|
|
15
|
+
|
|
16
|
+
function performAction(action, value) {
|
|
17
|
+
if (!props.selectedIds.length || processing.value) return;
|
|
18
|
+
processing.value = true;
|
|
19
|
+
router.post(props.bulkRoute, {
|
|
20
|
+
ticket_ids: props.selectedIds,
|
|
21
|
+
action,
|
|
22
|
+
value,
|
|
23
|
+
}, {
|
|
24
|
+
preserveScroll: true,
|
|
25
|
+
onSuccess: () => emit('clear'),
|
|
26
|
+
onFinish: () => { processing.value = false; },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const statusOptions = [
|
|
31
|
+
{ value: 'in_progress', label: 'In Progress' },
|
|
32
|
+
{ value: 'waiting_on_customer', label: 'Waiting on Customer' },
|
|
33
|
+
{ value: 'resolved', label: 'Resolved' },
|
|
34
|
+
{ value: 'closed', label: 'Closed' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const priorityOptions = [
|
|
38
|
+
{ value: 'low', label: 'Low' },
|
|
39
|
+
{ value: 'medium', label: 'Medium' },
|
|
40
|
+
{ value: 'high', label: 'High' },
|
|
41
|
+
{ value: 'urgent', label: 'Urgent' },
|
|
42
|
+
{ value: 'critical', label: 'Critical' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const selectClass = computed(() => escDark.value
|
|
46
|
+
? 'rounded-lg border border-white/10 bg-neutral-950 px-2 py-1.5 text-sm text-neutral-200 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10'
|
|
47
|
+
: 'rounded-md border border-gray-300 bg-white px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none'
|
|
48
|
+
);
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<Transition enter-active-class="transition-all duration-200 ease-out" enter-from-class="translate-y-full opacity-0" enter-to-class="translate-y-0 opacity-100"
|
|
53
|
+
leave-active-class="transition-all duration-150 ease-in" leave-from-class="translate-y-0 opacity-100" leave-to-class="translate-y-full opacity-0">
|
|
54
|
+
<div v-if="selectedIds.length > 0"
|
|
55
|
+
:class="['fixed bottom-0 left-0 right-0 z-50 border-t px-6 py-3',
|
|
56
|
+
escDark ? 'border-white/[0.06] bg-neutral-950/95 backdrop-blur-xl' : 'border-gray-200 bg-white/95 backdrop-blur-xl shadow-lg']">
|
|
57
|
+
<div class="mx-auto flex max-w-7xl items-center gap-4">
|
|
58
|
+
<span :class="['text-sm font-semibold', escDark ? 'text-white' : 'text-gray-900']">
|
|
59
|
+
{{ selectedIds.length }} ticket{{ selectedIds.length !== 1 ? 's' : '' }} selected
|
|
60
|
+
</span>
|
|
61
|
+
|
|
62
|
+
<select @change="performAction('status', $event.target.value); $event.target.value = ''" :class="selectClass" :disabled="processing">
|
|
63
|
+
<option value="">Status...</option>
|
|
64
|
+
<option v-for="o in statusOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
|
|
65
|
+
</select>
|
|
66
|
+
|
|
67
|
+
<select @change="performAction('priority', $event.target.value); $event.target.value = ''" :class="selectClass" :disabled="processing">
|
|
68
|
+
<option value="">Priority...</option>
|
|
69
|
+
<option v-for="o in priorityOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
|
|
70
|
+
</select>
|
|
71
|
+
|
|
72
|
+
<select v-if="agents.length" @change="performAction('assign', $event.target.value); $event.target.value = ''" :class="selectClass" :disabled="processing">
|
|
73
|
+
<option value="">Assign to...</option>
|
|
74
|
+
<option v-for="a in agents" :key="a.id" :value="a.id">{{ a.name }}</option>
|
|
75
|
+
</select>
|
|
76
|
+
|
|
77
|
+
<select v-if="departments.length" @change="performAction('department', $event.target.value); $event.target.value = ''" :class="selectClass" :disabled="processing">
|
|
78
|
+
<option value="">Department...</option>
|
|
79
|
+
<option v-for="d in departments" :key="d.id" :value="d.id">{{ d.name }}</option>
|
|
80
|
+
</select>
|
|
81
|
+
|
|
82
|
+
<button @click="performAction('delete', null)"
|
|
83
|
+
:class="['rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
|
|
84
|
+
escDark ? 'bg-rose-500/15 text-rose-400 ring-1 ring-rose-500/20 hover:bg-rose-500/25' : 'bg-red-50 text-red-600 ring-1 ring-red-200 hover:bg-red-100']"
|
|
85
|
+
:disabled="processing">
|
|
86
|
+
Delete
|
|
87
|
+
</button>
|
|
88
|
+
|
|
89
|
+
<button @click="emit('clear')"
|
|
90
|
+
:class="['ml-auto rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
|
|
91
|
+
escDark ? 'text-neutral-400 hover:bg-white/[0.04] hover:text-neutral-200' : 'text-gray-500 hover:bg-gray-100 hover:text-gray-700']">
|
|
92
|
+
Deselect All
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</Transition>
|
|
97
|
+
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, inject } from 'vue';
|
|
2
|
+
import { computed, inject, provide } from 'vue';
|
|
3
3
|
import { usePage, Link } from '@inertiajs/vue3';
|
|
4
4
|
|
|
5
5
|
const props = defineProps({
|
|
@@ -7,13 +7,7 @@ const props = defineProps({
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
const page = usePage();
|
|
10
|
-
const
|
|
11
|
-
const layoutResolver = inject('escalated-layout-resolver', null);
|
|
12
|
-
|
|
13
|
-
const hostLayout = computed(() => {
|
|
14
|
-
if (layoutResolver) return layoutResolver();
|
|
15
|
-
return staticLayout;
|
|
16
|
-
});
|
|
10
|
+
const hostLayout = inject('escalated-layout', null);
|
|
17
11
|
|
|
18
12
|
const isAgent = computed(() => page.props.escalated?.is_agent);
|
|
19
13
|
const isAdmin = computed(() => page.props.escalated?.is_admin);
|
|
@@ -25,19 +19,25 @@ const prefix = computed(() => {
|
|
|
25
19
|
const currentUrl = computed(() => page.url);
|
|
26
20
|
const isAdminSection = computed(() => currentUrl.value?.includes('/admin'));
|
|
27
21
|
const isAgentSection = computed(() => currentUrl.value?.includes('/agent'));
|
|
22
|
+
const isDark = computed(() => isAdminSection.value || isAgentSection.value);
|
|
23
|
+
|
|
24
|
+
provide('esc-dark', isDark);
|
|
28
25
|
|
|
29
26
|
const adminLinks = computed(() => [
|
|
30
|
-
{ href: `${prefix.value}/admin/reports`, label: 'Reports', icon: '
|
|
31
|
-
{ href: `${prefix.value}/admin/
|
|
32
|
-
{ href: `${prefix.value}/admin/
|
|
33
|
-
{ href: `${prefix.value}/admin/
|
|
34
|
-
{ href: `${prefix.value}/admin/
|
|
35
|
-
{ href: `${prefix.value}/admin/
|
|
27
|
+
{ href: `${prefix.value}/admin/reports`, label: 'Reports', icon: 'M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z' },
|
|
28
|
+
{ href: `${prefix.value}/admin/tickets`, label: 'Tickets', icon: 'M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z' },
|
|
29
|
+
{ href: `${prefix.value}/admin/departments`, label: 'Departments', icon: 'M2.25 7.125C2.25 6.504 2.754 6 3.375 6h6c.621 0 1.125.504 1.125 1.125v3.75c0 .621-.504 1.125-1.125 1.125h-6a1.125 1.125 0 01-1.125-1.125v-3.75zM14.25 8.625c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 01-1.125-1.125v-8.25zM3.75 16.125c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 01-1.125-1.125v-2.25z' },
|
|
30
|
+
{ href: `${prefix.value}/admin/sla-policies`, label: 'SLA Policies', icon: 'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z' },
|
|
31
|
+
{ href: `${prefix.value}/admin/escalation-rules`, label: 'Escalation Rules', icon: 'M3 4.5h14.25M3 9h9.75M3 13.5h5.25m5.25-.75L17.25 9m0 0L21 12.75M17.25 9v12' },
|
|
32
|
+
{ href: `${prefix.value}/admin/tags`, label: 'Tags', icon: 'M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z M6 6h.008v.008H6V6z' },
|
|
33
|
+
{ href: `${prefix.value}/admin/canned-responses`, label: 'Canned Responses', icon: 'M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z' },
|
|
34
|
+
{ href: `${prefix.value}/admin/macros`, label: 'Macros', icon: 'M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z' },
|
|
35
|
+
{ href: `${prefix.value}/admin/settings`, label: 'Settings', icon: 'M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.204-.107-.397.165-.71.505-.781.93l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894zM15 12a3 3 0 11-6 0 3 3 0 016 0z' },
|
|
36
36
|
]);
|
|
37
37
|
|
|
38
38
|
const agentLinks = computed(() => [
|
|
39
|
-
{ href: `${prefix.value}/agent`, label: 'Dashboard' },
|
|
40
|
-
{ href: `${prefix.value}/agent/tickets`, label: 'Tickets' },
|
|
39
|
+
{ href: `${prefix.value}/agent`, label: 'Dashboard', icon: 'M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25' },
|
|
40
|
+
{ href: `${prefix.value}/agent/tickets`, label: 'Tickets', icon: 'M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z' },
|
|
41
41
|
]);
|
|
42
42
|
|
|
43
43
|
const userName = computed(() => page.props.auth?.user?.name || 'User');
|
|
@@ -50,146 +50,153 @@ function isActive(href) {
|
|
|
50
50
|
</script>
|
|
51
51
|
|
|
52
52
|
<template>
|
|
53
|
-
<!-- MODE 1:
|
|
54
|
-
<
|
|
55
|
-
<template #header>
|
|
56
|
-
<div class="flex items-center justify-between">
|
|
57
|
-
<h2 class="esc-heading text-xl font-semibold leading-tight text-gray-800">{{ title }}</h2>
|
|
58
|
-
<nav class="flex items-center gap-4 text-sm">
|
|
59
|
-
<Link :href="prefix" class="text-gray-600 hover:text-gray-900">My Tickets</Link>
|
|
60
|
-
<Link v-if="isAgent" :href="`${prefix}/agent`" class="text-gray-600 hover:text-gray-900">Agent Panel</Link>
|
|
61
|
-
<Link v-if="isAdmin" :href="`${prefix}/admin/reports`" class="text-gray-600 hover:text-gray-900">Admin</Link>
|
|
62
|
-
</nav>
|
|
63
|
-
</div>
|
|
64
|
-
</template>
|
|
65
|
-
|
|
66
|
-
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
|
67
|
-
<slot />
|
|
68
|
-
</div>
|
|
69
|
-
</component>
|
|
70
|
-
|
|
71
|
-
<!-- MODE 2: Admin standalone — dark sidebar layout -->
|
|
72
|
-
<div v-else-if="isAdminSection" class="esc-root flex min-h-screen bg-gray-950">
|
|
53
|
+
<!-- MODE 1: Admin — dark sidebar layout -->
|
|
54
|
+
<div v-if="isAdminSection" class="flex min-h-screen bg-black" style="color-scheme: dark">
|
|
73
55
|
<!-- Sidebar -->
|
|
74
|
-
<aside class="fixed inset-y-0 left-0 z-30 flex w-64 flex-col border-r border-white/
|
|
56
|
+
<aside class="fixed inset-y-0 left-0 z-30 flex w-64 flex-col border-r border-white/[0.06] bg-neutral-950">
|
|
75
57
|
<!-- Logo -->
|
|
76
|
-
<div class="flex h-16 items-center gap-3 px-
|
|
77
|
-
<div class="flex h-
|
|
78
|
-
<svg class="h-
|
|
79
|
-
<
|
|
58
|
+
<div class="flex h-16 items-center gap-3 px-5">
|
|
59
|
+
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-white/10">
|
|
60
|
+
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
61
|
+
<defs><linearGradient id="esc-rainbow-admin" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f97316"/><stop offset="30%" stop-color="#eab308"/><stop offset="50%" stop-color="#22c55e"/><stop offset="70%" stop-color="#3b82f6"/><stop offset="100%" stop-color="#8b5cf6"/></linearGradient></defs>
|
|
62
|
+
<g transform="translate(12,12) scale(1.35) translate(-12,-12)"><polyline points="17 11 12 6 7 11" stroke="url(#esc-rainbow-admin)"/><polyline points="17 18 12 13 7 18" stroke="url(#esc-rainbow-admin)"/></g>
|
|
80
63
|
</svg>
|
|
81
64
|
</div>
|
|
82
|
-
<
|
|
83
|
-
|
|
65
|
+
<div>
|
|
66
|
+
<span class="text-sm font-bold text-white tracking-wide">Escalated</span>
|
|
67
|
+
<span class="ml-1.5 rounded bg-white/[0.08] px-1.5 py-0.5 text-[10px] font-semibold text-neutral-500">ADMIN</span>
|
|
68
|
+
</div>
|
|
84
69
|
</div>
|
|
85
70
|
|
|
86
71
|
<!-- Nav -->
|
|
87
|
-
<nav class="mt-
|
|
72
|
+
<nav class="mt-1 flex-1 space-y-0.5 overflow-y-auto px-3">
|
|
88
73
|
<Link v-for="link in adminLinks" :key="link.href" :href="link.href"
|
|
89
|
-
:class="['group flex items-center gap-3 rounded-lg px-3 py-2
|
|
74
|
+
:class="['group flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] font-medium transition-all',
|
|
90
75
|
isActive(link.href)
|
|
91
|
-
? 'bg-
|
|
92
|
-
: 'text-
|
|
76
|
+
? 'bg-white/[0.08] text-white'
|
|
77
|
+
: 'text-neutral-500 hover:bg-white/[0.04] hover:text-neutral-300']">
|
|
78
|
+
<svg :class="['h-[18px] w-[18px] shrink-0', isActive(link.href) ? 'text-white' : 'text-neutral-600 group-hover:text-neutral-400']"
|
|
79
|
+
fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
80
|
+
<path stroke-linecap="round" stroke-linejoin="round" :d="link.icon" />
|
|
81
|
+
</svg>
|
|
93
82
|
{{ link.label }}
|
|
94
83
|
</Link>
|
|
95
84
|
</nav>
|
|
96
85
|
|
|
97
|
-
<!-- Bottom -->
|
|
98
|
-
<div class="border-t border-white/
|
|
86
|
+
<!-- Bottom section -->
|
|
87
|
+
<div class="border-t border-white/[0.06] p-3">
|
|
99
88
|
<Link v-if="isAgent" :href="`${prefix}/agent`"
|
|
100
|
-
class="flex items-center gap-2 rounded-lg px-3 py-2 text-
|
|
101
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="
|
|
89
|
+
class="flex items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] text-neutral-500 transition-colors hover:bg-white/[0.04] hover:text-neutral-300">
|
|
90
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" /></svg>
|
|
102
91
|
Agent Panel
|
|
103
92
|
</Link>
|
|
104
93
|
<Link :href="prefix"
|
|
105
|
-
class="flex items-center gap-2 rounded-lg px-3 py-2 text-
|
|
106
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="
|
|
94
|
+
class="flex items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] text-neutral-500 transition-colors hover:bg-white/[0.04] hover:text-neutral-300">
|
|
95
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" /></svg>
|
|
107
96
|
Back to App
|
|
108
97
|
</Link>
|
|
98
|
+
|
|
99
|
+
<!-- User -->
|
|
100
|
+
<div class="mt-2 flex items-center gap-3 rounded-lg bg-white/[0.03] px-3 py-2.5">
|
|
101
|
+
<div class="flex h-7 w-7 items-center justify-center rounded-md bg-white/[0.08] text-xs font-semibold text-neutral-400">
|
|
102
|
+
{{ userInitial }}
|
|
103
|
+
</div>
|
|
104
|
+
<span class="text-sm text-neutral-400">{{ userName }}</span>
|
|
105
|
+
</div>
|
|
109
106
|
</div>
|
|
110
107
|
</aside>
|
|
111
108
|
|
|
112
109
|
<!-- Main content -->
|
|
113
110
|
<div class="flex flex-1 flex-col pl-64">
|
|
114
111
|
<!-- Top bar -->
|
|
115
|
-
<header class="sticky top-0 z-20 flex h-
|
|
116
|
-
<h1 class="text-
|
|
117
|
-
<div class="flex items-center gap-3">
|
|
118
|
-
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-cyan-500 to-violet-500 text-xs font-bold text-white">
|
|
119
|
-
{{ userInitial }}
|
|
120
|
-
</div>
|
|
121
|
-
<span class="text-sm text-gray-300">{{ userName }}</span>
|
|
122
|
-
</div>
|
|
112
|
+
<header class="sticky top-0 z-20 flex h-14 items-center border-b border-white/[0.06] bg-black/80 px-6 backdrop-blur-xl">
|
|
113
|
+
<h1 class="text-sm font-semibold text-white">{{ title }}</h1>
|
|
123
114
|
</header>
|
|
124
115
|
|
|
125
116
|
<!-- Page content -->
|
|
126
|
-
<main class="flex-1 p-
|
|
127
|
-
<
|
|
128
|
-
<slot />
|
|
129
|
-
</div>
|
|
117
|
+
<main class="flex-1 p-6">
|
|
118
|
+
<slot />
|
|
130
119
|
</main>
|
|
131
120
|
</div>
|
|
132
121
|
</div>
|
|
133
122
|
|
|
134
|
-
<!-- MODE
|
|
135
|
-
<div v-else-if="isAgentSection" class="
|
|
123
|
+
<!-- MODE 2: Agent — dark top-nav layout -->
|
|
124
|
+
<div v-else-if="isAgentSection" class="min-h-screen bg-black" style="color-scheme: dark">
|
|
136
125
|
<!-- Top nav -->
|
|
137
|
-
<nav class="sticky top-0 z-30 border-b border-white/
|
|
138
|
-
<div class="mx-auto flex h-
|
|
126
|
+
<nav class="sticky top-0 z-30 border-b border-white/[0.06] bg-neutral-950/95 backdrop-blur-xl">
|
|
127
|
+
<div class="mx-auto flex h-14 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
|
|
139
128
|
<!-- Left: branding -->
|
|
140
129
|
<div class="flex items-center gap-3">
|
|
141
|
-
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-
|
|
142
|
-
<svg class="h-4 w-4
|
|
143
|
-
<
|
|
130
|
+
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-white/10">
|
|
131
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
132
|
+
<defs><linearGradient id="esc-rainbow-agent" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f97316"/><stop offset="30%" stop-color="#eab308"/><stop offset="50%" stop-color="#22c55e"/><stop offset="70%" stop-color="#3b82f6"/><stop offset="100%" stop-color="#8b5cf6"/></linearGradient></defs>
|
|
133
|
+
<g transform="translate(12,12) scale(1.35) translate(-12,-12)"><polyline points="17 11 12 6 7 11" stroke="url(#esc-rainbow-agent)"/><polyline points="17 18 12 13 7 18" stroke="url(#esc-rainbow-agent)"/></g>
|
|
144
134
|
</svg>
|
|
145
135
|
</div>
|
|
146
|
-
<span class="text-
|
|
136
|
+
<span class="text-sm font-bold text-white tracking-wide">Escalated</span>
|
|
147
137
|
</div>
|
|
148
138
|
|
|
149
139
|
<!-- Center: nav links -->
|
|
150
140
|
<div class="flex items-center gap-1">
|
|
151
141
|
<Link v-for="link in agentLinks" :key="link.href" :href="link.href"
|
|
152
|
-
:class="['rounded-lg px-
|
|
142
|
+
:class="['flex items-center gap-2 rounded-lg px-3.5 py-1.5 text-[13px] font-medium transition-all',
|
|
153
143
|
isActive(link.href)
|
|
154
|
-
? 'bg-
|
|
155
|
-
: 'text-
|
|
144
|
+
? 'bg-white/[0.08] text-white'
|
|
145
|
+
: 'text-neutral-500 hover:bg-white/[0.04] hover:text-neutral-300']">
|
|
146
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
147
|
+
<path stroke-linecap="round" stroke-linejoin="round" :d="link.icon" />
|
|
148
|
+
</svg>
|
|
156
149
|
{{ link.label }}
|
|
157
150
|
</Link>
|
|
158
151
|
</div>
|
|
159
152
|
|
|
160
|
-
<!-- Right: user +
|
|
161
|
-
<div class="flex items-center gap-
|
|
162
|
-
<div class="flex items-center gap-2">
|
|
163
|
-
<div class="flex h-7 w-7 items-center justify-center rounded-full bg-gradient-to-br from-cyan-500 to-violet-500 text-xs font-bold text-white">
|
|
164
|
-
{{ userInitial }}
|
|
165
|
-
</div>
|
|
166
|
-
<span class="text-sm text-gray-300">{{ userName }}</span>
|
|
167
|
-
</div>
|
|
168
|
-
<div class="h-5 w-px bg-white/10"></div>
|
|
153
|
+
<!-- Right: user + links -->
|
|
154
|
+
<div class="flex items-center gap-3">
|
|
169
155
|
<Link v-if="isAdmin" :href="`${prefix}/admin/reports`"
|
|
170
|
-
class="text-
|
|
156
|
+
class="text-[13px] text-neutral-500 transition-colors hover:text-white">
|
|
171
157
|
Admin
|
|
172
158
|
</Link>
|
|
173
159
|
<Link :href="prefix"
|
|
174
|
-
class="text-
|
|
160
|
+
class="text-[13px] text-neutral-500 transition-colors hover:text-white">
|
|
175
161
|
Back to App
|
|
176
162
|
</Link>
|
|
163
|
+
<div class="ml-1 h-5 w-px bg-white/[0.08]"></div>
|
|
164
|
+
<div class="flex items-center gap-2">
|
|
165
|
+
<div class="flex h-7 w-7 items-center justify-center rounded-md bg-white/[0.08] text-[10px] font-semibold text-neutral-400">
|
|
166
|
+
{{ userInitial }}
|
|
167
|
+
</div>
|
|
168
|
+
<span class="text-[13px] text-neutral-400">{{ userName }}</span>
|
|
169
|
+
</div>
|
|
177
170
|
</div>
|
|
178
171
|
</div>
|
|
179
|
-
<!-- Rainbow accent line -->
|
|
180
|
-
<div class="h-px bg-gradient-to-r from-orange-500 via-cyan-500 to-violet-500"></div>
|
|
181
172
|
</nav>
|
|
182
173
|
|
|
183
174
|
<!-- Page content -->
|
|
184
175
|
<main class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
|
185
|
-
<
|
|
186
|
-
<slot />
|
|
187
|
-
</div>
|
|
176
|
+
<slot />
|
|
188
177
|
</main>
|
|
189
178
|
</div>
|
|
190
179
|
|
|
191
|
-
<!-- MODE
|
|
192
|
-
<
|
|
180
|
+
<!-- MODE 3: Customer pages — use host app layout if provided -->
|
|
181
|
+
<component :is="hostLayout" v-else-if="hostLayout">
|
|
182
|
+
<template #header>
|
|
183
|
+
<div class="flex items-center justify-between">
|
|
184
|
+
<h2 class="text-xl font-semibold leading-tight text-gray-800">{{ title }}</h2>
|
|
185
|
+
<nav class="flex items-center gap-4 text-sm">
|
|
186
|
+
<Link :href="prefix" class="text-gray-600 hover:text-gray-900">My Tickets</Link>
|
|
187
|
+
<Link v-if="isAgent" :href="`${prefix}/agent`" class="text-gray-600 hover:text-gray-900">Agent Panel</Link>
|
|
188
|
+
<Link v-if="isAdmin" :href="`${prefix}/admin/reports`" class="text-gray-600 hover:text-gray-900">Admin</Link>
|
|
189
|
+
</nav>
|
|
190
|
+
</div>
|
|
191
|
+
</template>
|
|
192
|
+
|
|
193
|
+
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
|
194
|
+
<slot />
|
|
195
|
+
</div>
|
|
196
|
+
</component>
|
|
197
|
+
|
|
198
|
+
<!-- MODE 4: Customer standalone fallback (no host layout) -->
|
|
199
|
+
<div v-else class="min-h-screen bg-gray-50">
|
|
193
200
|
<nav class="border-b border-gray-200 bg-white">
|
|
194
201
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
195
202
|
<div class="flex h-14 items-center justify-between">
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref } from 'vue';
|
|
2
|
+
import { ref, computed, inject } from 'vue';
|
|
3
3
|
|
|
4
4
|
const emit = defineEmits(['files']);
|
|
5
|
+
const dark = inject('esc-dark', computed(() => false));
|
|
5
6
|
|
|
6
7
|
const dragging = ref(false);
|
|
7
8
|
const fileInput = ref(null);
|
|
@@ -27,10 +28,14 @@ function browse() {
|
|
|
27
28
|
|
|
28
29
|
<template>
|
|
29
30
|
<div @dragover.prevent="dragging = true" @dragleave="dragging = false" @drop.prevent="onDrop"
|
|
30
|
-
:class="['cursor-pointer rounded-
|
|
31
|
-
|
|
31
|
+
:class="['cursor-pointer rounded-lg border-2 border-dashed px-4 py-3 text-center text-xs transition-colors',
|
|
32
|
+
dark
|
|
33
|
+
? (dragging ? 'border-white/20 bg-white/[0.04]' : 'border-white/[0.08] hover:border-white/[0.12]')
|
|
34
|
+
: (dragging ? 'border-blue-400 bg-blue-50' : 'border-gray-300 hover:border-gray-400')]"
|
|
32
35
|
@click="browse">
|
|
33
|
-
<p class="text-
|
|
36
|
+
<p :class="dark ? 'text-neutral-500' : 'text-gray-500'">
|
|
37
|
+
Drop files here or <span :class="['font-medium', dark ? 'text-white' : 'text-blue-600']">browse</span>
|
|
38
|
+
</p>
|
|
34
39
|
<input ref="fileInput" type="file" multiple class="hidden" @change="onFileSelect" />
|
|
35
40
|
</div>
|
|
36
41
|
</template>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, inject } from 'vue';
|
|
3
|
+
import { router } from '@inertiajs/vue3';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
action: { type: String, required: true },
|
|
7
|
+
isFollowing: { type: Boolean, default: false },
|
|
8
|
+
followersCount: { type: Number, default: 0 },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const escDark = inject('esc-dark', computed(() => false));
|
|
12
|
+
const following = ref(props.isFollowing);
|
|
13
|
+
const count = ref(props.followersCount);
|
|
14
|
+
const processing = ref(false);
|
|
15
|
+
|
|
16
|
+
function toggle() {
|
|
17
|
+
if (processing.value) return;
|
|
18
|
+
processing.value = true;
|
|
19
|
+
router.post(props.action, {}, {
|
|
20
|
+
preserveScroll: true,
|
|
21
|
+
onSuccess: () => {
|
|
22
|
+
following.value = !following.value;
|
|
23
|
+
count.value += following.value ? 1 : -1;
|
|
24
|
+
},
|
|
25
|
+
onFinish: () => {
|
|
26
|
+
processing.value = false;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<button @click="toggle"
|
|
34
|
+
:disabled="processing"
|
|
35
|
+
:class="['inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
36
|
+
escDark
|
|
37
|
+
? (following
|
|
38
|
+
? 'bg-cyan-500/15 text-white ring-1 ring-cyan-500/20 hover:bg-cyan-500/25'
|
|
39
|
+
: 'border border-white/10 bg-white/[0.03] text-neutral-400 hover:bg-white/[0.06] hover:text-neutral-200')
|
|
40
|
+
: (following
|
|
41
|
+
? 'bg-blue-100 text-blue-700 ring-1 ring-blue-300 hover:bg-blue-200'
|
|
42
|
+
: 'border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-800'),
|
|
43
|
+
processing && 'opacity-50 cursor-not-allowed']">
|
|
44
|
+
<!-- Eye icon -->
|
|
45
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
46
|
+
<template v-if="following">
|
|
47
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
|
48
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
49
|
+
</template>
|
|
50
|
+
<template v-else>
|
|
51
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
|
52
|
+
</template>
|
|
53
|
+
</svg>
|
|
54
|
+
{{ following ? 'Following' : 'Follow' }}
|
|
55
|
+
<span v-if="count > 0"
|
|
56
|
+
:class="['rounded-full px-1.5 py-0.5 text-xs',
|
|
57
|
+
escDark
|
|
58
|
+
? 'bg-white/[0.06] text-neutral-400'
|
|
59
|
+
: 'bg-gray-100 text-gray-500']">
|
|
60
|
+
{{ count }}
|
|
61
|
+
</span>
|
|
62
|
+
</button>
|
|
63
|
+
</template>
|