@escalated-dev/escalated 0.2.1
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/LICENSE +21 -0
- package/README.md +190 -0
- package/package.json +37 -0
- package/src/components/ActivityTimeline.vue +62 -0
- package/src/components/AssigneeSelect.vue +19 -0
- package/src/components/AttachmentList.vue +31 -0
- package/src/components/EscalatedLayout.vue +213 -0
- package/src/components/FileDropzone.vue +36 -0
- package/src/components/PriorityBadge.vue +21 -0
- package/src/components/ReplyComposer.vue +110 -0
- package/src/components/ReplyThread.vue +30 -0
- package/src/components/SlaTimer.vue +43 -0
- package/src/components/StatsCard.vue +26 -0
- package/src/components/StatusBadge.vue +24 -0
- package/src/components/TagSelect.vue +49 -0
- package/src/components/TicketFilters.vue +52 -0
- package/src/components/TicketList.vue +51 -0
- package/src/components/TicketSidebar.vue +70 -0
- package/src/index.js +19 -0
- package/src/pages/Admin/CannedResponses/Index.vue +58 -0
- package/src/pages/Admin/Departments/Form.vue +50 -0
- package/src/pages/Admin/Departments/Index.vue +49 -0
- package/src/pages/Admin/EscalationRules/Form.vue +95 -0
- package/src/pages/Admin/EscalationRules/Index.vue +49 -0
- package/src/pages/Admin/Reports.vue +52 -0
- package/src/pages/Admin/SlaPolicies/Form.vue +74 -0
- package/src/pages/Admin/SlaPolicies/Index.vue +51 -0
- package/src/pages/Admin/Tags/Index.vue +80 -0
- package/src/pages/Agent/Dashboard.vue +51 -0
- package/src/pages/Agent/TicketIndex.vue +21 -0
- package/src/pages/Agent/TicketShow.vue +108 -0
- package/src/pages/Customer/Create.vue +66 -0
- package/src/pages/Customer/Index.vue +24 -0
- package/src/pages/Customer/Show.vue +55 -0
- package/src/plugin.js +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Escalated Dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<h1>
|
|
2
|
+
<img src="https://escalated.dev/apple-touch-icon.png" width="28" style="vertical-align:middle;" />
|
|
3
|
+
Escalated
|
|
4
|
+
</h1>
|
|
5
|
+
|
|
6
|
+
Escalated is an embeddable support ticket system with SLA tracking, escalation rules, agent workflows, and a customer portal. This repo contains all the shared frontend assets (Vue 3 + Inertia.js) used across every supported backend framework.
|
|
7
|
+
|
|
8
|
+
👉 **Learn more, view demos, and compare Cloud vs Self-Hosted options at** **[https://escalated.dev](https://escalated.dev)**
|
|
9
|
+
|
|
10
|
+
**You don't install this package directly.** Start with the backend package for your framework — it handles everything including pulling in these frontend assets.
|
|
11
|
+
|
|
12
|
+
## Get Started
|
|
13
|
+
|
|
14
|
+
Pick your framework:
|
|
15
|
+
|
|
16
|
+
| Framework | Repo | Install |
|
|
17
|
+
|-----------|------|---------|
|
|
18
|
+
| **Laravel** | [escalated-dev/escalated-laravel](https://github.com/escalated-dev/escalated-laravel) | `composer require escalated-dev/escalated-laravel` |
|
|
19
|
+
| **Rails** | [escalated-dev/escalated-rails](https://github.com/escalated-dev/escalated-rails) | `gem "escalated"` |
|
|
20
|
+
| **Django** | [escalated-dev/escalated-django](https://github.com/escalated-dev/escalated-django) | `pip install escalated-django` |
|
|
21
|
+
|
|
22
|
+
Each backend repo has full setup instructions — install command, migrations, config, and frontend integration.
|
|
23
|
+
|
|
24
|
+
## Tailwind CSS
|
|
25
|
+
|
|
26
|
+
Escalated components use Tailwind CSS classes. You **must** add this package to your Tailwind `content` config so its classes aren't purged:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
// tailwind.config.js
|
|
30
|
+
export default {
|
|
31
|
+
content: [
|
|
32
|
+
// ... your existing paths
|
|
33
|
+
'./node_modules/@escalated-dev/escalated/src/**/*.vue',
|
|
34
|
+
],
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Without this, Escalated UI will render but styles like button backgrounds and badge colors will be missing.
|
|
39
|
+
|
|
40
|
+
## Theming
|
|
41
|
+
|
|
42
|
+
Escalated renders inside a standalone layout by default. To integrate it into your app's design system, use the `EscalatedPlugin`:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { createApp } from 'vue'
|
|
46
|
+
import { EscalatedPlugin } from '@escalated-dev/escalated'
|
|
47
|
+
import AppLayout from '@/Layouts/AppLayout.vue'
|
|
48
|
+
|
|
49
|
+
const app = createApp(...)
|
|
50
|
+
|
|
51
|
+
app.use(EscalatedPlugin, {
|
|
52
|
+
layout: AppLayout,
|
|
53
|
+
theme: {
|
|
54
|
+
primary: '#3b82f6',
|
|
55
|
+
radius: '0.75rem',
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Layout Integration
|
|
61
|
+
|
|
62
|
+
Pass your app's layout component and all Escalated pages render inside it automatically. The layout component must accept a `#header` slot and a default slot:
|
|
63
|
+
|
|
64
|
+
```vue
|
|
65
|
+
<!-- Your layout must support these slots -->
|
|
66
|
+
<template>
|
|
67
|
+
<div>
|
|
68
|
+
<nav>...</nav>
|
|
69
|
+
<header><slot name="header" /></header>
|
|
70
|
+
<main><slot /></main>
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
When no layout is provided, Escalated uses its own built-in navigation bar.
|
|
76
|
+
|
|
77
|
+
### CSS Custom Properties
|
|
78
|
+
|
|
79
|
+
The `theme` option sets CSS custom properties you can reference in your own styles:
|
|
80
|
+
|
|
81
|
+
| Property | Default | Description |
|
|
82
|
+
|----------|---------|-------------|
|
|
83
|
+
| `--esc-primary` | `#4f46e5` | Primary action color |
|
|
84
|
+
| `--esc-primary-hover` | auto-darkened | Primary hover color |
|
|
85
|
+
| `--esc-radius` | `0.5rem` | Border radius for inputs and buttons |
|
|
86
|
+
| `--esc-radius-lg` | auto-scaled | Border radius for cards and panels |
|
|
87
|
+
| `--esc-font-family` | inherit | Font family override |
|
|
88
|
+
|
|
89
|
+
### Framework Examples
|
|
90
|
+
|
|
91
|
+
**Laravel** (Inertia + Vue 3):
|
|
92
|
+
```js
|
|
93
|
+
import { EscalatedPlugin } from '@escalated-dev/escalated'
|
|
94
|
+
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
|
|
95
|
+
|
|
96
|
+
app.use(EscalatedPlugin, { layout: AuthenticatedLayout })
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Rails** (Inertia + Vue 3):
|
|
100
|
+
```js
|
|
101
|
+
import { EscalatedPlugin } from '@escalated-dev/escalated'
|
|
102
|
+
import AppLayout from '@/layouts/AppLayout.vue'
|
|
103
|
+
|
|
104
|
+
app.use(EscalatedPlugin, { layout: AppLayout })
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Django** (Inertia + Vue 3):
|
|
108
|
+
```js
|
|
109
|
+
import { EscalatedPlugin } from '@escalated-dev/escalated'
|
|
110
|
+
import BaseLayout from '@/layouts/BaseLayout.vue'
|
|
111
|
+
|
|
112
|
+
app.use(EscalatedPlugin, { layout: BaseLayout })
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## What's in This Repo
|
|
116
|
+
|
|
117
|
+
All the Vue 3 + Inertia.js components that power the Escalated UI. These are identical across Laravel, Rails, and Django — the backend framework renders them via Inertia.
|
|
118
|
+
|
|
119
|
+
### Pages
|
|
120
|
+
|
|
121
|
+
**Customer Portal** — Self-service ticket management
|
|
122
|
+
- `pages/Customer/Index.vue` — Ticket list with status filters and search
|
|
123
|
+
- `pages/Customer/Create.vue` — New ticket form with file attachments
|
|
124
|
+
- `pages/Customer/Show.vue` — Ticket detail with reply thread
|
|
125
|
+
|
|
126
|
+
**Agent Dashboard** — Ticket queue and workflows
|
|
127
|
+
- `pages/Agent/Dashboard.vue` — Stats overview and recent tickets
|
|
128
|
+
- `pages/Agent/TicketIndex.vue` — Filterable ticket queue
|
|
129
|
+
- `pages/Agent/TicketShow.vue` — Full ticket view with sidebar, internal notes, canned responses
|
|
130
|
+
|
|
131
|
+
**Admin Panel** — System configuration
|
|
132
|
+
- `pages/Admin/Reports.vue` — Analytics dashboard
|
|
133
|
+
- `pages/Admin/Departments/` — Department CRUD
|
|
134
|
+
- `pages/Admin/SlaPolicies/` — SLA policy management
|
|
135
|
+
- `pages/Admin/EscalationRules/` — Escalation rule builder
|
|
136
|
+
- `pages/Admin/Tags/` — Tag management
|
|
137
|
+
- `pages/Admin/CannedResponses/` — Canned response templates
|
|
138
|
+
|
|
139
|
+
### Shared Components
|
|
140
|
+
|
|
141
|
+
Reusable building blocks used across the pages above.
|
|
142
|
+
|
|
143
|
+
| Component | Description |
|
|
144
|
+
|-----------|-------------|
|
|
145
|
+
| `StatusBadge` | Colored badge for ticket status |
|
|
146
|
+
| `PriorityBadge` | Colored badge for ticket priority |
|
|
147
|
+
| `TicketList` | Paginated ticket table |
|
|
148
|
+
| `ReplyThread` | Chronological reply display |
|
|
149
|
+
| `ReplyComposer` | Reply/note editor with file upload and canned response insertion |
|
|
150
|
+
| `ActivityTimeline` | Audit log of ticket events |
|
|
151
|
+
| `SlaTimer` | SLA countdown with breach/warning states |
|
|
152
|
+
| `TicketFilters` | Status, priority, agent, department filter bar |
|
|
153
|
+
| `TicketSidebar` | Ticket detail sidebar (status, SLA, tags, activity) |
|
|
154
|
+
| `AssigneeSelect` | Agent assignment dropdown |
|
|
155
|
+
| `TagSelect` | Multi-select tag picker |
|
|
156
|
+
| `FileDropzone` | Drag-and-drop file upload |
|
|
157
|
+
| `AttachmentList` | File attachment display with download links |
|
|
158
|
+
| `StatsCard` | Metric card with label, value, and trend |
|
|
159
|
+
| `EscalatedLayout` | Top-level layout with navigation (supports host layout injection) |
|
|
160
|
+
|
|
161
|
+
### Plugin
|
|
162
|
+
|
|
163
|
+
| Export | Description |
|
|
164
|
+
|--------|-------------|
|
|
165
|
+
| `EscalatedPlugin` | Vue plugin for layout injection and CSS theming |
|
|
166
|
+
|
|
167
|
+
## For Package Maintainers
|
|
168
|
+
|
|
169
|
+
If you're building a new backend integration, this package is available on npm:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npm install @escalated-dev/escalated
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
// Import the plugin
|
|
177
|
+
import { EscalatedPlugin } from '@escalated-dev/escalated'
|
|
178
|
+
|
|
179
|
+
// Import individual components
|
|
180
|
+
import { StatusBadge, SlaTimer } from '@escalated-dev/escalated'
|
|
181
|
+
|
|
182
|
+
// Or reference pages directly for Inertia resolution
|
|
183
|
+
import CustomerIndex from '@escalated-dev/escalated/pages/Customer/Index.vue'
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Peer dependencies: `vue` ^3.3.0, `@inertiajs/vue3` ^1.0.0 || ^2.0.0
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@escalated-dev/escalated",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Vue 3 + Inertia.js UI components for Escalated — the embeddable support ticket system",
|
|
5
|
+
"author": "Escalated Dev <hello@escalated.dev>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/escalated-dev/escalated",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/escalated-dev/escalated.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"vue",
|
|
14
|
+
"inertia",
|
|
15
|
+
"support",
|
|
16
|
+
"tickets",
|
|
17
|
+
"helpdesk",
|
|
18
|
+
"escalated"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
"./components/*": "./src/components/*",
|
|
23
|
+
"./pages/*": "./src/pages/*",
|
|
24
|
+
".": "./src/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"vue": "^3.3.0",
|
|
31
|
+
"@inertiajs/vue3": "^1.0.0 || ^2.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"vue": "^3.5.0",
|
|
35
|
+
"@inertiajs/vue3": "^2.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({
|
|
3
|
+
activities: { type: Array, required: true },
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
const typeLabels = {
|
|
7
|
+
status_changed: 'changed status',
|
|
8
|
+
assigned: 'assigned ticket',
|
|
9
|
+
unassigned: 'unassigned ticket',
|
|
10
|
+
priority_changed: 'changed priority',
|
|
11
|
+
tag_added: 'added tag',
|
|
12
|
+
tag_removed: 'removed tag',
|
|
13
|
+
escalated: 'escalated ticket',
|
|
14
|
+
sla_breached: 'SLA breached',
|
|
15
|
+
replied: 'replied',
|
|
16
|
+
note_added: 'added note',
|
|
17
|
+
department_changed: 'changed department',
|
|
18
|
+
reopened: 'reopened ticket',
|
|
19
|
+
resolved: 'resolved ticket',
|
|
20
|
+
closed: 'closed ticket',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function formatDate(date) {
|
|
24
|
+
return new Date(date).toLocaleString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function describeActivity(activity) {
|
|
28
|
+
const label = typeLabels[activity.type] || activity.type;
|
|
29
|
+
const who = activity.causer?.name || 'System';
|
|
30
|
+
const props = activity.properties || {};
|
|
31
|
+
|
|
32
|
+
if (activity.type === 'status_changed' && props.to) {
|
|
33
|
+
return `${who} ${label} to ${props.to}`;
|
|
34
|
+
}
|
|
35
|
+
if (activity.type === 'assigned' && props.agent_name) {
|
|
36
|
+
return `${who} assigned to ${props.agent_name}`;
|
|
37
|
+
}
|
|
38
|
+
if (activity.type === 'priority_changed' && props.to) {
|
|
39
|
+
return `${who} ${label} to ${props.to}`;
|
|
40
|
+
}
|
|
41
|
+
if ((activity.type === 'tag_added' || activity.type === 'tag_removed') && props.tag) {
|
|
42
|
+
return `${who} ${label} "${props.tag}"`;
|
|
43
|
+
}
|
|
44
|
+
if (activity.type === 'department_changed' && props.department) {
|
|
45
|
+
return `${who} ${label} to ${props.department}`;
|
|
46
|
+
}
|
|
47
|
+
return `${who} ${label}`;
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<div class="space-y-3">
|
|
53
|
+
<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>
|
|
55
|
+
<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>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div v-if="!activities?.length" class="py-4 text-center text-sm text-gray-500">No activity yet.</div>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({
|
|
3
|
+
agents: { type: Array, required: true },
|
|
4
|
+
modelValue: { type: [Number, String], default: null },
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
const emit = defineEmits(['update:modelValue']);
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div>
|
|
12
|
+
<label class="mb-1 block text-xs font-medium text-gray-600">Assigned To</label>
|
|
13
|
+
<select :value="modelValue" @change="emit('update:modelValue', $event.target.value || null)"
|
|
14
|
+
class="w-full rounded-md border border-gray-300 px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none">
|
|
15
|
+
<option value="">Unassigned</option>
|
|
16
|
+
<option v-for="agent in agents" :key="agent.id" :value="agent.id">{{ agent.name }}</option>
|
|
17
|
+
</select>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({
|
|
3
|
+
attachments: { type: Array, required: true },
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
function formatSize(bytes) {
|
|
7
|
+
if (bytes < 1024) return bytes + ' B';
|
|
8
|
+
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
9
|
+
return (bytes / 1048576).toFixed(1) + ' MB';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function iconForMime(mime) {
|
|
13
|
+
if (mime?.startsWith('image/')) return '🖼️';
|
|
14
|
+
if (mime?.startsWith('video/')) return '🎬';
|
|
15
|
+
if (mime === 'application/pdf') return '📄';
|
|
16
|
+
return '📎';
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div class="space-y-1">
|
|
22
|
+
<div v-for="attachment in attachments" :key="attachment.id"
|
|
23
|
+
class="flex items-center gap-2 rounded border border-gray-200 bg-gray-50 px-3 py-2 text-sm">
|
|
24
|
+
<span>{{ iconForMime(attachment.mime_type) }}</span>
|
|
25
|
+
<a :href="attachment.url" target="_blank" class="flex-1 truncate font-medium text-blue-600 hover:underline">
|
|
26
|
+
{{ attachment.original_filename }}
|
|
27
|
+
</a>
|
|
28
|
+
<span class="text-xs text-gray-400">{{ formatSize(attachment.size) }}</span>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, inject } 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 staticLayout = inject('escalated-layout', null);
|
|
11
|
+
const layoutResolver = inject('escalated-layout-resolver', null);
|
|
12
|
+
|
|
13
|
+
const hostLayout = computed(() => {
|
|
14
|
+
if (layoutResolver) return layoutResolver();
|
|
15
|
+
return staticLayout;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const isAgent = computed(() => page.props.escalated?.is_agent);
|
|
19
|
+
const isAdmin = computed(() => page.props.escalated?.is_admin);
|
|
20
|
+
const prefix = computed(() => {
|
|
21
|
+
const p = page.props.escalated?.prefix || 'support';
|
|
22
|
+
return p.startsWith('/') ? p : `/${p}`;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const currentUrl = computed(() => page.url);
|
|
26
|
+
const isAdminSection = computed(() => currentUrl.value?.includes('/admin'));
|
|
27
|
+
const isAgentSection = computed(() => currentUrl.value?.includes('/agent'));
|
|
28
|
+
|
|
29
|
+
const adminLinks = computed(() => [
|
|
30
|
+
{ href: `${prefix.value}/admin/reports`, label: 'Reports', icon: 'chart' },
|
|
31
|
+
{ href: `${prefix.value}/admin/departments`, label: 'Departments', icon: 'grid' },
|
|
32
|
+
{ href: `${prefix.value}/admin/sla-policies`, label: 'SLA Policies', icon: 'clock' },
|
|
33
|
+
{ href: `${prefix.value}/admin/escalation-rules`, label: 'Escalation Rules', icon: 'arrow' },
|
|
34
|
+
{ href: `${prefix.value}/admin/tags`, label: 'Tags', icon: 'tag' },
|
|
35
|
+
{ href: `${prefix.value}/admin/canned-responses`, label: 'Canned Responses', icon: 'message' },
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const agentLinks = computed(() => [
|
|
39
|
+
{ href: `${prefix.value}/agent`, label: 'Dashboard' },
|
|
40
|
+
{ href: `${prefix.value}/agent/tickets`, label: 'Tickets' },
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const userName = computed(() => page.props.auth?.user?.name || 'User');
|
|
44
|
+
const userInitial = computed(() => userName.value.charAt(0).toUpperCase());
|
|
45
|
+
|
|
46
|
+
function isActive(href) {
|
|
47
|
+
if (!currentUrl.value) return false;
|
|
48
|
+
return currentUrl.value === href || currentUrl.value.startsWith(href + '/');
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<!-- MODE 1: Host layout provided (customer pages within host app) -->
|
|
54
|
+
<component :is="hostLayout" v-if="hostLayout">
|
|
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">
|
|
73
|
+
<!-- Sidebar -->
|
|
74
|
+
<aside class="fixed inset-y-0 left-0 z-30 flex w-64 flex-col border-r border-white/10 bg-gray-900">
|
|
75
|
+
<!-- Logo -->
|
|
76
|
+
<div class="flex h-16 items-center gap-3 px-6">
|
|
77
|
+
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-orange-500 via-cyan-500 to-violet-500">
|
|
78
|
+
<svg class="h-4 w-4 text-white" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
|
79
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
|
|
80
|
+
</svg>
|
|
81
|
+
</div>
|
|
82
|
+
<span class="text-lg font-bold text-white">Escalated</span>
|
|
83
|
+
<span class="rounded bg-white/10 px-1.5 py-0.5 text-[10px] font-medium text-gray-400">Admin</span>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Nav -->
|
|
87
|
+
<nav class="mt-2 flex-1 space-y-1 px-3">
|
|
88
|
+
<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.5 text-sm font-medium transition-all',
|
|
90
|
+
isActive(link.href)
|
|
91
|
+
? 'bg-gradient-to-r from-orange-500/20 via-cyan-500/20 to-violet-500/20 text-white ring-1 ring-white/10'
|
|
92
|
+
: 'text-gray-400 hover:bg-white/5 hover:text-white']">
|
|
93
|
+
{{ link.label }}
|
|
94
|
+
</Link>
|
|
95
|
+
</nav>
|
|
96
|
+
|
|
97
|
+
<!-- Bottom -->
|
|
98
|
+
<div class="border-t border-white/10 p-4">
|
|
99
|
+
<Link v-if="isAgent" :href="`${prefix}/agent`"
|
|
100
|
+
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-400 transition-colors hover:bg-white/5 hover:text-white">
|
|
101
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" 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
|
+
Agent Panel
|
|
103
|
+
</Link>
|
|
104
|
+
<Link :href="prefix"
|
|
105
|
+
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-400 transition-colors hover:bg-white/5 hover:text-white">
|
|
106
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" 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
|
+
Back to App
|
|
108
|
+
</Link>
|
|
109
|
+
</div>
|
|
110
|
+
</aside>
|
|
111
|
+
|
|
112
|
+
<!-- Main content -->
|
|
113
|
+
<div class="flex flex-1 flex-col pl-64">
|
|
114
|
+
<!-- Top bar -->
|
|
115
|
+
<header class="sticky top-0 z-20 flex h-16 items-center justify-between border-b border-white/10 bg-gray-900/80 px-6 backdrop-blur-xl">
|
|
116
|
+
<h1 class="text-lg font-semibold text-white">{{ title }}</h1>
|
|
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>
|
|
123
|
+
</header>
|
|
124
|
+
|
|
125
|
+
<!-- Page content -->
|
|
126
|
+
<main class="flex-1 p-4">
|
|
127
|
+
<div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5">
|
|
128
|
+
<slot />
|
|
129
|
+
</div>
|
|
130
|
+
</main>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<!-- MODE 3: Agent standalone — dark top-nav layout -->
|
|
135
|
+
<div v-else-if="isAgentSection" class="esc-root min-h-screen bg-gray-950">
|
|
136
|
+
<!-- Top nav -->
|
|
137
|
+
<nav class="sticky top-0 z-30 border-b border-white/10 bg-gray-900">
|
|
138
|
+
<div class="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
|
|
139
|
+
<!-- Left: branding -->
|
|
140
|
+
<div class="flex items-center gap-3">
|
|
141
|
+
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-orange-500 via-cyan-500 to-violet-500">
|
|
142
|
+
<svg class="h-4 w-4 text-white" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
|
143
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
|
|
144
|
+
</svg>
|
|
145
|
+
</div>
|
|
146
|
+
<span class="text-lg font-bold text-white">Escalated</span>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<!-- Center: nav links -->
|
|
150
|
+
<div class="flex items-center gap-1">
|
|
151
|
+
<Link v-for="link in agentLinks" :key="link.href" :href="link.href"
|
|
152
|
+
:class="['rounded-lg px-4 py-2 text-sm font-medium transition-all',
|
|
153
|
+
isActive(link.href)
|
|
154
|
+
? 'bg-gradient-to-r from-orange-500/20 via-cyan-500/20 to-violet-500/20 text-white ring-1 ring-white/10'
|
|
155
|
+
: 'text-gray-400 hover:bg-white/5 hover:text-white']">
|
|
156
|
+
{{ link.label }}
|
|
157
|
+
</Link>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<!-- Right: user + back -->
|
|
161
|
+
<div class="flex items-center gap-4">
|
|
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>
|
|
169
|
+
<Link v-if="isAdmin" :href="`${prefix}/admin/reports`"
|
|
170
|
+
class="text-sm text-gray-400 transition-colors hover:text-white">
|
|
171
|
+
Admin
|
|
172
|
+
</Link>
|
|
173
|
+
<Link :href="prefix"
|
|
174
|
+
class="text-sm text-gray-400 transition-colors hover:text-white">
|
|
175
|
+
Back to App
|
|
176
|
+
</Link>
|
|
177
|
+
</div>
|
|
178
|
+
</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
|
+
</nav>
|
|
182
|
+
|
|
183
|
+
<!-- Page content -->
|
|
184
|
+
<main class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
|
185
|
+
<div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5">
|
|
186
|
+
<slot />
|
|
187
|
+
</div>
|
|
188
|
+
</main>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<!-- MODE 4: Customer standalone fallback (no host layout, not admin/agent) -->
|
|
192
|
+
<div v-else class="esc-root min-h-screen bg-gray-50">
|
|
193
|
+
<nav class="border-b border-gray-200 bg-white">
|
|
194
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
195
|
+
<div class="flex h-14 items-center justify-between">
|
|
196
|
+
<div class="flex items-center gap-6">
|
|
197
|
+
<span class="text-lg font-bold text-gray-900">{{ title }}</span>
|
|
198
|
+
<div class="flex items-center gap-4 text-sm">
|
|
199
|
+
<Link :href="prefix" class="text-gray-600 hover:text-gray-900">My Tickets</Link>
|
|
200
|
+
<Link v-if="isAgent" :href="`${prefix}/agent`" class="text-gray-600 hover:text-gray-900">Agent Panel</Link>
|
|
201
|
+
<Link v-if="isAdmin" :href="`${prefix}/admin/reports`" class="text-gray-600 hover:text-gray-900">Admin</Link>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</nav>
|
|
207
|
+
<main class="flex-1">
|
|
208
|
+
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
|
209
|
+
<slot />
|
|
210
|
+
</div>
|
|
211
|
+
</main>
|
|
212
|
+
</div>
|
|
213
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
const emit = defineEmits(['files']);
|
|
5
|
+
|
|
6
|
+
const dragging = ref(false);
|
|
7
|
+
const fileInput = ref(null);
|
|
8
|
+
|
|
9
|
+
function onDrop(e) {
|
|
10
|
+
dragging.value = false;
|
|
11
|
+
if (e.dataTransfer?.files?.length) {
|
|
12
|
+
emit('files', Array.from(e.dataTransfer.files));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function onFileSelect(e) {
|
|
17
|
+
if (e.target.files?.length) {
|
|
18
|
+
emit('files', Array.from(e.target.files));
|
|
19
|
+
e.target.value = '';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function browse() {
|
|
24
|
+
fileInput.value?.click();
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div @dragover.prevent="dragging = true" @dragleave="dragging = false" @drop.prevent="onDrop"
|
|
30
|
+
:class="['cursor-pointer rounded-md border-2 border-dashed px-4 py-3 text-center text-xs transition-colors',
|
|
31
|
+
dragging ? 'border-blue-400 bg-blue-50' : 'border-gray-300 hover:border-gray-400']"
|
|
32
|
+
@click="browse">
|
|
33
|
+
<p class="text-gray-500">Drop files here or <span class="font-medium text-blue-600">browse</span></p>
|
|
34
|
+
<input ref="fileInput" type="file" multiple class="hidden" @change="onFileSelect" />
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
priority: { type: String, required: true },
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
const priorityConfig = {
|
|
7
|
+
low: { label: 'Low', color: 'bg-gray-100 text-gray-800' },
|
|
8
|
+
medium: { label: 'Medium', color: 'bg-blue-100 text-blue-800' },
|
|
9
|
+
high: { label: 'High', color: 'bg-yellow-100 text-yellow-800' },
|
|
10
|
+
urgent: { label: 'Urgent', color: 'bg-orange-100 text-orange-800' },
|
|
11
|
+
critical: { label: 'Critical', color: 'bg-red-100 text-red-800' },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const config = priorityConfig[props.priority] || { label: props.priority, color: 'bg-gray-100 text-gray-800' };
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<span :class="['inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium', config.color]">
|
|
19
|
+
{{ config.label }}
|
|
20
|
+
</span>
|
|
21
|
+
</template>
|