@escalated-dev/escalated 0.3.5 → 0.5.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 (32) hide show
  1. package/README.md +35 -1
  2. package/package.json +9 -2
  3. package/src/components/BulkActionBar.vue +97 -0
  4. package/src/components/EscalatedLayout.vue +244 -219
  5. package/src/components/FollowButton.vue +63 -0
  6. package/src/components/KeyboardShortcutHelp.vue +123 -0
  7. package/src/components/MacroDropdown.vue +90 -0
  8. package/src/components/PinnedNotes.vue +87 -0
  9. package/src/components/PluginSlot.vue +62 -0
  10. package/src/components/PresenceIndicator.vue +103 -0
  11. package/src/components/QuickFilters.vue +49 -0
  12. package/src/components/ReplyThread.vue +60 -38
  13. package/src/components/SatisfactionRating.vue +128 -0
  14. package/src/components/TicketFilters.vue +69 -58
  15. package/src/components/TicketList.vue +193 -94
  16. package/src/components/TicketSidebar.vue +99 -77
  17. package/src/composables/useKeyboardShortcuts.js +46 -0
  18. package/src/composables/usePluginExtensions.js +67 -0
  19. package/src/defineEscalatedPlugin.js +50 -0
  20. package/src/index.js +16 -0
  21. package/src/pages/Admin/ApiTokens/Index.vue +217 -0
  22. package/src/pages/Admin/Macros/Index.vue +287 -0
  23. package/src/pages/Admin/Plugins/Index.vue +197 -0
  24. package/src/pages/Admin/Plugins.vue +129 -0
  25. package/src/pages/Admin/Reports.vue +101 -66
  26. package/src/pages/Admin/Tickets/Index.vue +39 -22
  27. package/src/pages/Admin/Tickets/Show.vue +156 -109
  28. package/src/pages/Agent/Dashboard.vue +186 -63
  29. package/src/pages/Agent/TicketIndex.vue +38 -21
  30. package/src/pages/Agent/TicketShow.vue +155 -108
  31. package/src/pages/Customer/Show.vue +63 -55
  32. package/src/pages/Guest/Show.vue +93 -86
@@ -1,219 +1,244 @@
1
- <script setup>
2
- import { computed, inject, provide } from 'vue';
3
- import { usePage, Link } from '@inertiajs/vue3';
4
-
5
- const props = defineProps({
6
- title: { type: String, default: 'Support' },
7
- });
8
-
9
- const page = usePage();
10
- const hostLayout = inject('escalated-layout', null);
11
-
12
- const isAgent = computed(() => page.props.escalated?.is_agent);
13
- const isAdmin = computed(() => page.props.escalated?.is_admin);
14
- const prefix = computed(() => {
15
- const p = page.props.escalated?.prefix || 'support';
16
- return p.startsWith('/') ? p : `/${p}`;
17
- });
18
-
19
- const currentUrl = computed(() => page.url);
20
- const isAdminSection = computed(() => currentUrl.value?.includes('/admin'));
21
- const isAgentSection = computed(() => currentUrl.value?.includes('/agent'));
22
- const isDark = computed(() => isAdminSection.value || isAgentSection.value);
23
-
24
- provide('esc-dark', isDark);
25
-
26
- const adminLinks = computed(() => [
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/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' },
35
- ]);
36
-
37
- const agentLinks = computed(() => [
38
- { 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' },
39
- { 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' },
40
- ]);
41
-
42
- const userName = computed(() => page.props.auth?.user?.name || 'User');
43
- const userInitial = computed(() => userName.value.charAt(0).toUpperCase());
44
-
45
- function isActive(href) {
46
- if (!currentUrl.value) return false;
47
- return currentUrl.value === href || currentUrl.value.startsWith(href + '/');
48
- }
49
- </script>
50
-
51
- <template>
52
- <!-- MODE 1: Admin — dark sidebar layout -->
53
- <div v-if="isAdminSection" class="flex min-h-screen bg-black" style="color-scheme: dark">
54
- <!-- Sidebar -->
55
- <aside class="fixed inset-y-0 left-0 z-30 flex w-64 flex-col border-r border-white/[0.06] bg-neutral-950">
56
- <!-- Logo -->
57
- <div class="flex h-16 items-center gap-3 px-5">
58
- <div class="flex h-9 w-9 items-center justify-center rounded-lg bg-white/10">
59
- <svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
60
- <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>
61
- <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>
62
- </svg>
63
- </div>
64
- <div>
65
- <span class="text-sm font-bold text-white tracking-wide">Escalated</span>
66
- <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>
67
- </div>
68
- </div>
69
-
70
- <!-- Nav -->
71
- <nav class="mt-1 flex-1 space-y-0.5 overflow-y-auto px-3">
72
- <Link v-for="link in adminLinks" :key="link.href" :href="link.href"
73
- :class="['group flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] font-medium transition-all',
74
- isActive(link.href)
75
- ? 'bg-white/[0.08] text-white'
76
- : 'text-neutral-500 hover:bg-white/[0.04] hover:text-neutral-300']">
77
- <svg :class="['h-[18px] w-[18px] shrink-0', isActive(link.href) ? 'text-white' : 'text-neutral-600 group-hover:text-neutral-400']"
78
- fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
79
- <path stroke-linecap="round" stroke-linejoin="round" :d="link.icon" />
80
- </svg>
81
- {{ link.label }}
82
- </Link>
83
- </nav>
84
-
85
- <!-- Bottom section -->
86
- <div class="border-t border-white/[0.06] p-3">
87
- <Link v-if="isAgent" :href="`${prefix}/agent`"
88
- 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">
89
- <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>
90
- Agent Panel
91
- </Link>
92
- <Link :href="prefix"
93
- 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">
94
- <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>
95
- Back to App
96
- </Link>
97
-
98
- <!-- User -->
99
- <div class="mt-2 flex items-center gap-3 rounded-lg bg-white/[0.03] px-3 py-2.5">
100
- <div class="flex h-7 w-7 items-center justify-center rounded-md bg-white/[0.08] text-xs font-semibold text-neutral-400">
101
- {{ userInitial }}
102
- </div>
103
- <span class="text-sm text-neutral-400">{{ userName }}</span>
104
- </div>
105
- </div>
106
- </aside>
107
-
108
- <!-- Main content -->
109
- <div class="flex flex-1 flex-col pl-64">
110
- <!-- Top bar -->
111
- <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">
112
- <h1 class="text-sm font-semibold text-white">{{ title }}</h1>
113
- </header>
114
-
115
- <!-- Page content -->
116
- <main class="flex-1 p-6">
117
- <slot />
118
- </main>
119
- </div>
120
- </div>
121
-
122
- <!-- MODE 2: Agent — dark top-nav layout -->
123
- <div v-else-if="isAgentSection" class="min-h-screen bg-black" style="color-scheme: dark">
124
- <!-- Top nav -->
125
- <nav class="sticky top-0 z-30 border-b border-white/[0.06] bg-neutral-950/95 backdrop-blur-xl">
126
- <div class="mx-auto flex h-14 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
127
- <!-- Left: branding -->
128
- <div class="flex items-center gap-3">
129
- <div class="flex h-8 w-8 items-center justify-center rounded-lg bg-white/10">
130
- <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
131
- <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>
132
- <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>
133
- </svg>
134
- </div>
135
- <span class="text-sm font-bold text-white tracking-wide">Escalated</span>
136
- </div>
137
-
138
- <!-- Center: nav links -->
139
- <div class="flex items-center gap-1">
140
- <Link v-for="link in agentLinks" :key="link.href" :href="link.href"
141
- :class="['flex items-center gap-2 rounded-lg px-3.5 py-1.5 text-[13px] font-medium transition-all',
142
- isActive(link.href)
143
- ? 'bg-white/[0.08] text-white'
144
- : 'text-neutral-500 hover:bg-white/[0.04] hover:text-neutral-300']">
145
- <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
146
- <path stroke-linecap="round" stroke-linejoin="round" :d="link.icon" />
147
- </svg>
148
- {{ link.label }}
149
- </Link>
150
- </div>
151
-
152
- <!-- Right: user + links -->
153
- <div class="flex items-center gap-3">
154
- <Link v-if="isAdmin" :href="`${prefix}/admin/reports`"
155
- class="text-[13px] text-neutral-500 transition-colors hover:text-white">
156
- Admin
157
- </Link>
158
- <Link :href="prefix"
159
- class="text-[13px] text-neutral-500 transition-colors hover:text-white">
160
- Back to App
161
- </Link>
162
- <div class="ml-1 h-5 w-px bg-white/[0.08]"></div>
163
- <div class="flex items-center gap-2">
164
- <div class="flex h-7 w-7 items-center justify-center rounded-md bg-white/[0.08] text-[10px] font-semibold text-neutral-400">
165
- {{ userInitial }}
166
- </div>
167
- <span class="text-[13px] text-neutral-400">{{ userName }}</span>
168
- </div>
169
- </div>
170
- </div>
171
- </nav>
172
-
173
- <!-- Page content -->
174
- <main class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
175
- <slot />
176
- </main>
177
- </div>
178
-
179
- <!-- MODE 3: Customer pages — use host app layout if provided -->
180
- <component :is="hostLayout" v-else-if="hostLayout">
181
- <template #header>
182
- <div class="flex items-center justify-between">
183
- <h2 class="text-xl font-semibold leading-tight text-gray-800">{{ title }}</h2>
184
- <nav class="flex items-center gap-4 text-sm">
185
- <Link :href="prefix" class="text-gray-600 hover:text-gray-900">My Tickets</Link>
186
- <Link v-if="isAgent" :href="`${prefix}/agent`" class="text-gray-600 hover:text-gray-900">Agent Panel</Link>
187
- <Link v-if="isAdmin" :href="`${prefix}/admin/reports`" class="text-gray-600 hover:text-gray-900">Admin</Link>
188
- </nav>
189
- </div>
190
- </template>
191
-
192
- <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
193
- <slot />
194
- </div>
195
- </component>
196
-
197
- <!-- MODE 4: Customer standalone fallback (no host layout) -->
198
- <div v-else class="min-h-screen bg-gray-50">
199
- <nav class="border-b border-gray-200 bg-white">
200
- <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
201
- <div class="flex h-14 items-center justify-between">
202
- <div class="flex items-center gap-6">
203
- <span class="text-lg font-bold text-gray-900">{{ title }}</span>
204
- <div class="flex items-center gap-4 text-sm">
205
- <Link :href="prefix" class="text-gray-600 hover:text-gray-900">My Tickets</Link>
206
- <Link v-if="isAgent" :href="`${prefix}/agent`" class="text-gray-600 hover:text-gray-900">Agent Panel</Link>
207
- <Link v-if="isAdmin" :href="`${prefix}/admin/reports`" class="text-gray-600 hover:text-gray-900">Admin</Link>
208
- </div>
209
- </div>
210
- </div>
211
- </div>
212
- </nav>
213
- <main class="flex-1">
214
- <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
215
- <slot />
216
- </div>
217
- </main>
218
- </div>
219
- </template>
1
+ <script setup>
2
+ import { computed, inject, provide } from 'vue';
3
+ import { usePage, Link } from '@inertiajs/vue3';
4
+ import { usePluginExtensions } from '../composables/usePluginExtensions';
5
+
6
+ const props = defineProps({
7
+ title: { type: String, default: 'Support' },
8
+ });
9
+
10
+ const page = usePage();
11
+ const hostLayout = inject('escalated-layout', null);
12
+
13
+ const isAgent = computed(() => page.props.escalated?.is_agent);
14
+ const isAdmin = computed(() => page.props.escalated?.is_admin);
15
+ const prefix = computed(() => {
16
+ const p = page.props.escalated?.prefix || 'support';
17
+ return p.startsWith('/') ? p : `/${p}`;
18
+ });
19
+
20
+ const currentUrl = computed(() => page.url);
21
+ const isAdminSection = computed(() => currentUrl.value?.includes('/admin'));
22
+ const isAgentSection = computed(() => currentUrl.value?.includes('/agent'));
23
+ const isDark = computed(() => isAdminSection.value || isAgentSection.value);
24
+
25
+ provide('esc-dark', isDark);
26
+
27
+ const { menuItems: pluginMenuItems } = usePluginExtensions();
28
+
29
+ const adminLinks = computed(() => {
30
+ const p = prefix.value;
31
+ const coreLinks = [
32
+ { href: `${p}/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', position: 10 },
33
+ { href: `${p}/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', position: 20 },
34
+ { href: `${p}/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', position: 30 },
35
+ { href: `${p}/admin/sla-policies`, label: 'SLA Policies', icon: 'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z', position: 40 },
36
+ { href: `${p}/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', position: 50 },
37
+ { href: `${p}/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', position: 60 },
38
+ { href: `${p}/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', position: 70 },
39
+ { href: `${p}/admin/macros`, label: 'Macros', icon: 'M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z', position: 80 },
40
+ { href: `${p}/admin/api-tokens`, label: 'API Tokens', icon: 'M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z', position: 90 },
41
+ { href: `${p}/admin/plugins`, label: 'Plugins', icon: 'M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 01-.657.643 48.39 48.39 0 01-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 01-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 00-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 01-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 00.657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 005.427-.63 48.05 48.05 0 00.582-4.717.532.532 0 00-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.96.401v0a.656.656 0 00.658-.663 48.422 48.422 0 00-.37-5.36c-1.886.342-3.81.574-5.766.689a.578.578 0 01-.61-.58v0z', position: 95 },
42
+ { href: `${p}/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', position: 100 },
43
+ ];
44
+
45
+ // Merge core admin links with plugin-provided menu items
46
+ const pluginLinks = pluginMenuItems.value.map(item => ({
47
+ href: item.route,
48
+ label: item.label,
49
+ icon: item.icon,
50
+ position: item.position || 100,
51
+ badge: item.badge || null,
52
+ submenu: item.submenu || [],
53
+ }));
54
+
55
+ return [...coreLinks, ...pluginLinks].sort((a, b) => (a.position || 100) - (b.position || 100));
56
+ });
57
+
58
+ const agentLinks = computed(() => [
59
+ { 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' },
60
+ { 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' },
61
+ ]);
62
+
63
+ const userName = computed(() => page.props.auth?.user?.name || 'User');
64
+ const userInitial = computed(() => userName.value.charAt(0).toUpperCase());
65
+
66
+ function isActive(href) {
67
+ if (!currentUrl.value) return false;
68
+ return currentUrl.value === href || currentUrl.value.startsWith(href + '/');
69
+ }
70
+ </script>
71
+
72
+ <template>
73
+ <!-- MODE 1: Admin — dark sidebar layout -->
74
+ <div v-if="isAdminSection" class="flex min-h-screen bg-black" style="color-scheme: dark">
75
+ <!-- Sidebar -->
76
+ <aside class="fixed inset-y-0 left-0 z-30 flex w-64 flex-col border-r border-white/[0.06] bg-neutral-950">
77
+ <!-- Logo -->
78
+ <div class="flex h-16 items-center gap-3 px-5">
79
+ <div class="flex h-9 w-9 items-center justify-center rounded-lg bg-white/10">
80
+ <svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
81
+ <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>
82
+ <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>
83
+ </svg>
84
+ </div>
85
+ <div>
86
+ <span class="text-sm font-bold text-white tracking-wide">Escalated</span>
87
+ <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>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Nav -->
92
+ <nav class="mt-1 flex-1 space-y-0.5 overflow-y-auto px-3">
93
+ <Link v-for="link in adminLinks" :key="link.href" :href="link.href"
94
+ :class="['group flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] font-medium transition-all',
95
+ isActive(link.href)
96
+ ? 'bg-white/[0.08] text-white'
97
+ : 'text-neutral-500 hover:bg-white/[0.04] hover:text-neutral-300']">
98
+ <svg :class="['h-[18px] w-[18px] shrink-0', isActive(link.href) ? 'text-white' : 'text-neutral-600 group-hover:text-neutral-400']"
99
+ fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
100
+ <path stroke-linecap="round" stroke-linejoin="round" :d="link.icon" />
101
+ </svg>
102
+ {{ link.label }}
103
+ <span v-if="link.badge"
104
+ class="ml-auto inline-flex h-5 min-w-[20px] items-center justify-center rounded-full bg-cyan-500/20 px-1.5 text-[10px] font-semibold text-cyan-400">
105
+ {{ link.badge }}
106
+ </span>
107
+ </Link>
108
+ </nav>
109
+
110
+ <!-- Bottom section -->
111
+ <div class="border-t border-white/[0.06] p-3">
112
+ <Link v-if="isAgent" :href="`${prefix}/agent`"
113
+ 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">
114
+ <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>
115
+ Agent Panel
116
+ </Link>
117
+ <Link :href="prefix"
118
+ 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">
119
+ <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>
120
+ Back to App
121
+ </Link>
122
+
123
+ <!-- User -->
124
+ <div class="mt-2 flex items-center gap-3 rounded-lg bg-white/[0.03] px-3 py-2.5">
125
+ <div class="flex h-7 w-7 items-center justify-center rounded-md bg-white/[0.08] text-xs font-semibold text-neutral-400">
126
+ {{ userInitial }}
127
+ </div>
128
+ <span class="text-sm text-neutral-400">{{ userName }}</span>
129
+ </div>
130
+ </div>
131
+ </aside>
132
+
133
+ <!-- Main content -->
134
+ <div class="flex flex-1 flex-col pl-64">
135
+ <!-- Top bar -->
136
+ <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">
137
+ <h1 class="text-sm font-semibold text-white">{{ title }}</h1>
138
+ </header>
139
+
140
+ <!-- Page content -->
141
+ <main class="flex-1 p-6">
142
+ <slot />
143
+ </main>
144
+ </div>
145
+ </div>
146
+
147
+ <!-- MODE 2: Agent — dark top-nav layout -->
148
+ <div v-else-if="isAgentSection" class="min-h-screen bg-black" style="color-scheme: dark">
149
+ <!-- Top nav -->
150
+ <nav class="sticky top-0 z-30 border-b border-white/[0.06] bg-neutral-950/95 backdrop-blur-xl">
151
+ <div class="mx-auto flex h-14 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
152
+ <!-- Left: branding -->
153
+ <div class="flex items-center gap-3">
154
+ <div class="flex h-8 w-8 items-center justify-center rounded-lg bg-white/10">
155
+ <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
156
+ <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>
157
+ <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>
158
+ </svg>
159
+ </div>
160
+ <span class="text-sm font-bold text-white tracking-wide">Escalated</span>
161
+ </div>
162
+
163
+ <!-- Center: nav links -->
164
+ <div class="flex items-center gap-1">
165
+ <Link v-for="link in agentLinks" :key="link.href" :href="link.href"
166
+ :class="['flex items-center gap-2 rounded-lg px-3.5 py-1.5 text-[13px] font-medium transition-all',
167
+ isActive(link.href)
168
+ ? 'bg-white/[0.08] text-white'
169
+ : 'text-neutral-500 hover:bg-white/[0.04] hover:text-neutral-300']">
170
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
171
+ <path stroke-linecap="round" stroke-linejoin="round" :d="link.icon" />
172
+ </svg>
173
+ {{ link.label }}
174
+ </Link>
175
+ </div>
176
+
177
+ <!-- Right: user + links -->
178
+ <div class="flex items-center gap-3">
179
+ <Link v-if="isAdmin" :href="`${prefix}/admin/reports`"
180
+ class="text-[13px] text-neutral-500 transition-colors hover:text-white">
181
+ Admin
182
+ </Link>
183
+ <Link :href="prefix"
184
+ class="text-[13px] text-neutral-500 transition-colors hover:text-white">
185
+ Back to App
186
+ </Link>
187
+ <div class="ml-1 h-5 w-px bg-white/[0.08]"></div>
188
+ <div class="flex items-center gap-2">
189
+ <div class="flex h-7 w-7 items-center justify-center rounded-md bg-white/[0.08] text-[10px] font-semibold text-neutral-400">
190
+ {{ userInitial }}
191
+ </div>
192
+ <span class="text-[13px] text-neutral-400">{{ userName }}</span>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </nav>
197
+
198
+ <!-- Page content -->
199
+ <main class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
200
+ <slot />
201
+ </main>
202
+ </div>
203
+
204
+ <!-- MODE 3: Customer pages — use host app layout if provided -->
205
+ <component :is="hostLayout" v-else-if="hostLayout">
206
+ <template #header>
207
+ <div class="flex items-center justify-between">
208
+ <h2 class="text-xl font-semibold leading-tight text-gray-800">{{ title }}</h2>
209
+ <nav class="flex items-center gap-4 text-sm">
210
+ <Link :href="prefix" class="text-gray-600 hover:text-gray-900">My Tickets</Link>
211
+ <Link v-if="isAgent" :href="`${prefix}/agent`" class="text-gray-600 hover:text-gray-900">Agent Panel</Link>
212
+ <Link v-if="isAdmin" :href="`${prefix}/admin/reports`" class="text-gray-600 hover:text-gray-900">Admin</Link>
213
+ </nav>
214
+ </div>
215
+ </template>
216
+
217
+ <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
218
+ <slot />
219
+ </div>
220
+ </component>
221
+
222
+ <!-- MODE 4: Customer standalone fallback (no host layout) -->
223
+ <div v-else class="min-h-screen bg-gray-50">
224
+ <nav class="border-b border-gray-200 bg-white">
225
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
226
+ <div class="flex h-14 items-center justify-between">
227
+ <div class="flex items-center gap-6">
228
+ <span class="text-lg font-bold text-gray-900">{{ title }}</span>
229
+ <div class="flex items-center gap-4 text-sm">
230
+ <Link :href="prefix" class="text-gray-600 hover:text-gray-900">My Tickets</Link>
231
+ <Link v-if="isAgent" :href="`${prefix}/agent`" class="text-gray-600 hover:text-gray-900">Agent Panel</Link>
232
+ <Link v-if="isAdmin" :href="`${prefix}/admin/reports`" class="text-gray-600 hover:text-gray-900">Admin</Link>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </nav>
238
+ <main class="flex-1">
239
+ <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
240
+ <slot />
241
+ </div>
242
+ </main>
243
+ </div>
244
+ </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>