@escalated-dev/escalated 0.8.0 → 0.11.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 -21
- package/README.md +388 -361
- package/package.json +85 -84
- package/src/components/ActiveChatsPanel.vue +246 -246
- package/src/components/AdminDashboard.stories.js +1423 -1423
- package/src/components/AttachmentList.vue +65 -65
- package/src/components/ChatActionBar.vue +226 -226
- package/src/components/ChatBubble.vue +162 -162
- package/src/components/ChatComposer.vue +308 -308
- package/src/components/ChatQueue.vue +222 -222
- package/src/components/ChatThread.vue +197 -197
- package/src/components/CustomTicketActions.stories.js +71 -0
- package/src/components/CustomTicketActions.vue +101 -0
- package/src/components/EscalatedLayout.vue +628 -604
- package/src/components/FollowButton.vue +20 -1
- package/src/components/MentionDropdown.vue +154 -154
- package/src/components/MetricComparison.vue +89 -89
- package/src/components/PercentileChart.vue +67 -67
- package/src/components/PeriodSelector.vue +98 -98
- package/src/components/PresenceIndicator.vue +228 -213
- package/src/components/ReplyComposer.vue +336 -336
- package/src/components/ReplyThread.vue +153 -153
- package/src/components/ReportChart.vue +395 -395
- package/src/components/ReportExportButton.vue +108 -108
- package/src/components/SaveViewDialog.vue +172 -172
- package/src/components/SavedViewSidebar.vue +208 -208
- package/src/components/SkillTagManager.vue +3 -0
- package/src/components/SnoozeButton.vue +247 -247
- package/src/components/TicketFilters.vue +270 -270
- package/src/components/TicketList.vue +655 -655
- package/src/components/TicketSidebar.vue +4 -0
- package/src/components/TicketSplitDialog.vue +98 -98
- package/src/components/TicketSubjects.stories.js +63 -0
- package/src/components/TicketSubjects.vue +107 -0
- package/src/components/TwoFactorSetup.vue +1 -37
- package/src/components/VariableTokenInput.vue +111 -111
- package/src/components/WorkflowActionConfig.vue +287 -287
- package/src/components/WorkflowActionList.vue +350 -350
- package/src/components/WorkflowConditionBuilder.vue +265 -265
- package/src/components/WorkflowTriggerSelector.vue +125 -125
- package/src/components/admin/newsletters/AnalyticsTiles.stories.js +14 -0
- package/src/components/admin/newsletters/AnalyticsTiles.vue +55 -0
- package/src/components/admin/newsletters/DeliveriesTable.stories.js +28 -0
- package/src/components/admin/newsletters/DeliveriesTable.vue +104 -0
- package/src/components/admin/newsletters/DynamicFilterBuilder.stories.js +11 -0
- package/src/components/admin/newsletters/DynamicFilterBuilder.vue +53 -0
- package/src/components/admin/newsletters/ListMemberTable.stories.js +12 -0
- package/src/components/admin/newsletters/ListMemberTable.vue +53 -0
- package/src/components/admin/newsletters/MarkdownEditor.stories.js +6 -0
- package/src/components/admin/newsletters/MarkdownEditor.vue +53 -0
- package/src/components/admin/newsletters/MergeFieldDropdown.stories.js +6 -0
- package/src/components/admin/newsletters/MergeFieldDropdown.vue +84 -0
- package/src/components/admin/newsletters/PreviewIframe.stories.js +7 -0
- package/src/components/admin/newsletters/PreviewIframe.vue +40 -0
- package/src/components/admin/newsletters/SummaryTiles.stories.js +8 -0
- package/src/components/admin/newsletters/SummaryTiles.vue +63 -0
- package/src/composables/useChat.js +187 -187
- package/src/composables/useI18n.js +102 -102
- package/src/composables/useMentions.js +134 -134
- package/src/composables/usePermissions.js +31 -0
- package/src/composables/useRealtime.js +182 -182
- package/src/index.js +85 -84
- package/src/locales/README.md +13 -13
- package/src/locales/ar.json +727 -727
- package/src/locales/de.json +690 -690
- package/src/locales/en.json +889 -729
- package/src/locales/es.json +691 -691
- package/src/locales/fr.json +691 -691
- package/src/locales/index.js +16 -16
- package/src/locales/it.json +727 -727
- package/src/locales/ja.json +727 -727
- package/src/locales/ko.json +727 -727
- package/src/locales/nl.json +727 -727
- package/src/locales/pl.json +727 -727
- package/src/locales/pt-BR.json +727 -727
- package/src/locales/ru.json +727 -727
- package/src/locales/tr.json +727 -727
- package/src/locales/zh-CN.json +727 -727
- package/src/pages/Admin/Automations/Form.vue +280 -0
- package/src/pages/Admin/Automations/Index.vue +230 -0
- package/src/pages/Admin/Newsletters/Compose.vue +192 -0
- package/src/pages/Admin/Newsletters/Edit.vue +24 -0
- package/src/pages/Admin/Newsletters/Index.vue +141 -0
- package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
- package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
- package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
- package/src/pages/Admin/Newsletters/Settings.vue +85 -0
- package/src/pages/Admin/Newsletters/Show.vue +140 -0
- package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
- package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
- package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
- package/src/pages/Admin/Newsletters/usePreview.js +44 -0
- package/src/pages/Admin/Plugins/Index.vue +10 -2
- package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
- package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
- package/src/pages/Admin/Reports/Cohorts.vue +180 -180
- package/src/pages/Admin/Reports/Comparison.vue +141 -141
- package/src/pages/Admin/Reports/Dashboard.vue +226 -226
- package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
- package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
- package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
- package/src/pages/Admin/Settings/DatabaseConnection.vue +228 -0
- package/src/pages/Admin/Settings/PublicTickets.vue +163 -0
- package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
- package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
- package/src/pages/Admin/Settings.vue +1008 -1008
- package/src/pages/Admin/Skills/Form.vue +30 -10
- package/src/pages/Admin/Tickets/Index.vue +126 -126
- package/src/pages/Admin/Tickets/Show.vue +254 -249
- package/src/pages/Admin/Users/Index.vue +145 -145
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +1 -2
- package/src/pages/Admin/Workflows/Builder.vue +207 -207
- package/src/pages/Admin/Workflows/Form.vue +24 -0
- package/src/pages/Admin/Workflows/Index.vue +235 -235
- package/src/pages/Admin/Workflows/Logs.vue +286 -286
- package/src/pages/Agent/TicketIndex.vue +112 -112
- package/src/pages/Agent/TicketShow.vue +547 -494
- package/src/pages/Customer/Index.vue +56 -56
- package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
- package/src/pages/Plugin/Page.vue +61 -0
- package/src/plugin.js +175 -175
- package/src/utils/mentionHighlight.js +24 -24
- package/src/widget/EscalatedWidget.vue +804 -804
- package/src/widget/index.js +254 -254
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<EscalatedLayout title="Newsletter">
|
|
3
|
+
<div class="show">
|
|
4
|
+
<header>
|
|
5
|
+
<div>
|
|
6
|
+
<h1>{{ newsletter.subject }}</h1>
|
|
7
|
+
<div class="show__meta">
|
|
8
|
+
{{ $t('newsletters.detail.from') }}
|
|
9
|
+
{{
|
|
10
|
+
newsletter.from_name
|
|
11
|
+
? `${newsletter.from_name} <${newsletter.from_email}>`
|
|
12
|
+
: newsletter.from_email
|
|
13
|
+
}}
|
|
14
|
+
→ {{ newsletter.target_list.name }}
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="show__status">
|
|
18
|
+
<span :class="`status status--${newsletter.status}`">{{ newsletter.status }}</span>
|
|
19
|
+
</div>
|
|
20
|
+
</header>
|
|
21
|
+
|
|
22
|
+
<nav class="show__tabs">
|
|
23
|
+
<Link
|
|
24
|
+
:href="`/admin/newsletters/${newsletter.id}?tab=overview`"
|
|
25
|
+
:class="{ active: tab === 'overview' }"
|
|
26
|
+
>
|
|
27
|
+
{{ $t('newsletters.detail.tabs.overview') }}
|
|
28
|
+
</Link>
|
|
29
|
+
<Link
|
|
30
|
+
:href="`/admin/newsletters/${newsletter.id}?tab=deliveries`"
|
|
31
|
+
:class="{ active: tab === 'deliveries' }"
|
|
32
|
+
>
|
|
33
|
+
{{ $t('newsletters.detail.tabs.deliveries') }}
|
|
34
|
+
</Link>
|
|
35
|
+
<Link
|
|
36
|
+
:href="`/admin/newsletters/${newsletter.id}?tab=analytics`"
|
|
37
|
+
:class="{ active: tab === 'analytics' }"
|
|
38
|
+
>
|
|
39
|
+
{{ $t('newsletters.detail.tabs.analytics') }}
|
|
40
|
+
</Link>
|
|
41
|
+
</nav>
|
|
42
|
+
|
|
43
|
+
<section v-if="tab === 'overview'">
|
|
44
|
+
<SummaryTiles :summary="summary" />
|
|
45
|
+
</section>
|
|
46
|
+
|
|
47
|
+
<section v-else-if="tab === 'deliveries'">
|
|
48
|
+
<DeliveriesTable :rows="deliveries.data" @filter="onFilter" @export="onExport" />
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<section v-else-if="tab === 'analytics'">
|
|
52
|
+
<AnalyticsTiles :summary="summary" :top-clicks="topClicks" />
|
|
53
|
+
</section>
|
|
54
|
+
</div>
|
|
55
|
+
</EscalatedLayout>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup>
|
|
59
|
+
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
60
|
+
import SummaryTiles from '../../../components/admin/newsletters/SummaryTiles.vue';
|
|
61
|
+
import DeliveriesTable from '../../../components/admin/newsletters/DeliveriesTable.vue';
|
|
62
|
+
import AnalyticsTiles from '../../../components/admin/newsletters/AnalyticsTiles.vue';
|
|
63
|
+
import { Link, router } from '@inertiajs/vue3';
|
|
64
|
+
import { computed } from 'vue';
|
|
65
|
+
|
|
66
|
+
const props = defineProps({
|
|
67
|
+
newsletter: { type: Object, required: true },
|
|
68
|
+
deliveries: { type: Object, required: true },
|
|
69
|
+
topClicks: { type: Array, default: () => [] },
|
|
70
|
+
tab: { type: String, default: 'overview' },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const summary = computed(() => ({
|
|
74
|
+
total: props.newsletter.summary_total ?? 0,
|
|
75
|
+
sent: props.newsletter.summary_sent ?? 0,
|
|
76
|
+
opened: props.newsletter.summary_opened ?? 0,
|
|
77
|
+
clicked: props.newsletter.summary_clicked ?? 0,
|
|
78
|
+
bounced: props.newsletter.summary_bounced ?? 0,
|
|
79
|
+
complained: props.newsletter.summary_complained ?? 0,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
function onFilter(status) {
|
|
83
|
+
router.get(`/admin/newsletters/${props.newsletter.id}?tab=deliveries&status=${status}`);
|
|
84
|
+
}
|
|
85
|
+
function onExport() {
|
|
86
|
+
window.location.href = `/admin/newsletters/${props.newsletter.id}/deliveries.csv`;
|
|
87
|
+
}
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<style scoped>
|
|
91
|
+
.show header {
|
|
92
|
+
display: flex;
|
|
93
|
+
justify-content: space-between;
|
|
94
|
+
align-items: flex-start;
|
|
95
|
+
margin-bottom: 16px;
|
|
96
|
+
}
|
|
97
|
+
.show__meta {
|
|
98
|
+
color: var(--escalated-text-muted, #64748b);
|
|
99
|
+
font-size: 14px;
|
|
100
|
+
margin-top: 4px;
|
|
101
|
+
}
|
|
102
|
+
.show__tabs {
|
|
103
|
+
display: flex;
|
|
104
|
+
gap: 16px;
|
|
105
|
+
border-bottom: 1px solid var(--escalated-border, #e2e8f0);
|
|
106
|
+
margin-bottom: 16px;
|
|
107
|
+
}
|
|
108
|
+
.show__tabs a {
|
|
109
|
+
padding: 8px 4px;
|
|
110
|
+
color: var(--escalated-text-muted, #64748b);
|
|
111
|
+
border-bottom: 2px solid transparent;
|
|
112
|
+
text-decoration: none;
|
|
113
|
+
}
|
|
114
|
+
.show__tabs a.active {
|
|
115
|
+
color: var(--escalated-text, #0f172a);
|
|
116
|
+
border-bottom-color: var(--escalated-accent, #2563eb);
|
|
117
|
+
}
|
|
118
|
+
.status {
|
|
119
|
+
display: inline-block;
|
|
120
|
+
padding: 2px 8px;
|
|
121
|
+
border-radius: 4px;
|
|
122
|
+
font-size: 12px;
|
|
123
|
+
}
|
|
124
|
+
.status--draft {
|
|
125
|
+
background: #f1f5f9;
|
|
126
|
+
}
|
|
127
|
+
.status--scheduled,
|
|
128
|
+
.status--sending {
|
|
129
|
+
background: #e0f2fe;
|
|
130
|
+
}
|
|
131
|
+
.status--sent {
|
|
132
|
+
background: #dcfce7;
|
|
133
|
+
}
|
|
134
|
+
.status--paused {
|
|
135
|
+
background: #fef3c7;
|
|
136
|
+
}
|
|
137
|
+
.status--failed {
|
|
138
|
+
background: #fee2e2;
|
|
139
|
+
}
|
|
140
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TemplateShow :template="emptyTemplate" :themes="themes" :is-new="true" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
import TemplateShow from './Show.vue';
|
|
7
|
+
|
|
8
|
+
defineProps({ themes: { type: Array, required: true } });
|
|
9
|
+
|
|
10
|
+
const emptyTemplate = { id: null, name: '', theme: 'default', subject_template: '', body_markdown: '' };
|
|
11
|
+
</script>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<EscalatedLayout title="Templates">
|
|
3
|
+
<div class="templates-index">
|
|
4
|
+
<header>
|
|
5
|
+
<h1>{{ $t('newsletters.templates.title') }}</h1>
|
|
6
|
+
<Link href="/admin/newsletters/templates/new" class="button button--primary">
|
|
7
|
+
{{ $t('newsletters.templates.new') }}
|
|
8
|
+
</Link>
|
|
9
|
+
</header>
|
|
10
|
+
<div class="templates-index__grid">
|
|
11
|
+
<Link v-for="t in templates" :key="t.id" :href="`/admin/newsletters/templates/${t.id}`" class="card">
|
|
12
|
+
<h3>{{ t.name }}</h3>
|
|
13
|
+
<div class="card__meta">Theme: {{ t.theme }}</div>
|
|
14
|
+
<div class="card__meta">Updated: {{ new Date(t.updated_at).toLocaleDateString() }}</div>
|
|
15
|
+
</Link>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</EscalatedLayout>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup>
|
|
22
|
+
import EscalatedLayout from '../../../../components/EscalatedLayout.vue';
|
|
23
|
+
import { Link } from '@inertiajs/vue3';
|
|
24
|
+
|
|
25
|
+
defineProps({ templates: { type: Array, required: true } });
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<style scoped>
|
|
29
|
+
header {
|
|
30
|
+
display: flex;
|
|
31
|
+
justify-content: space-between;
|
|
32
|
+
align-items: center;
|
|
33
|
+
margin-bottom: 16px;
|
|
34
|
+
}
|
|
35
|
+
.templates-index__grid {
|
|
36
|
+
display: grid;
|
|
37
|
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
38
|
+
gap: 12px;
|
|
39
|
+
}
|
|
40
|
+
.card {
|
|
41
|
+
display: block;
|
|
42
|
+
padding: 16px;
|
|
43
|
+
border: 1px solid var(--escalated-border, #e2e8f0);
|
|
44
|
+
border-radius: 8px;
|
|
45
|
+
background: white;
|
|
46
|
+
text-decoration: none;
|
|
47
|
+
color: inherit;
|
|
48
|
+
}
|
|
49
|
+
.card__meta {
|
|
50
|
+
color: var(--escalated-text-muted, #64748b);
|
|
51
|
+
font-size: 12px;
|
|
52
|
+
margin-top: 4px;
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<EscalatedLayout :title="isNew ? 'New template' : template.name">
|
|
3
|
+
<form class="templates-show" @submit.prevent="save">
|
|
4
|
+
<header>
|
|
5
|
+
<h1>{{ isNew ? $t('newsletters.templates.new') : template.name }}</h1>
|
|
6
|
+
</header>
|
|
7
|
+
<label>
|
|
8
|
+
{{ $t('newsletters.templates.columns.name') }}
|
|
9
|
+
<input v-model="form.name" required />
|
|
10
|
+
</label>
|
|
11
|
+
<label>
|
|
12
|
+
{{ $t('newsletters.templates.theme') }}
|
|
13
|
+
<select v-model="form.theme">
|
|
14
|
+
<option v-for="t in themes" :key="t" :value="t">{{ t }}</option>
|
|
15
|
+
</select>
|
|
16
|
+
</label>
|
|
17
|
+
<label>
|
|
18
|
+
{{ $t('newsletters.templates.subject_template') }}
|
|
19
|
+
<input v-model="form.subject_template" />
|
|
20
|
+
</label>
|
|
21
|
+
<div class="editor-section">
|
|
22
|
+
<div class="editor-toolbar"><MergeFieldDropdown @insert="onInsert" /></div>
|
|
23
|
+
<MarkdownEditor ref="editorRef" v-model="form.body_markdown" />
|
|
24
|
+
</div>
|
|
25
|
+
<button type="submit">{{ $t('form.save') }}</button>
|
|
26
|
+
</form>
|
|
27
|
+
</EscalatedLayout>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup>
|
|
31
|
+
import EscalatedLayout from '../../../../components/EscalatedLayout.vue';
|
|
32
|
+
import MarkdownEditor from '../../../../components/admin/newsletters/MarkdownEditor.vue';
|
|
33
|
+
import MergeFieldDropdown from '../../../../components/admin/newsletters/MergeFieldDropdown.vue';
|
|
34
|
+
import { reactive, ref } from 'vue';
|
|
35
|
+
import { router } from '@inertiajs/vue3';
|
|
36
|
+
|
|
37
|
+
const props = defineProps({
|
|
38
|
+
template: { type: Object, required: true },
|
|
39
|
+
themes: { type: Array, required: true },
|
|
40
|
+
isNew: { type: Boolean, default: false },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const form = reactive({ ...props.template });
|
|
44
|
+
const editorRef = ref(null);
|
|
45
|
+
|
|
46
|
+
function save() {
|
|
47
|
+
if (props.isNew) router.post('/admin/newsletters/templates', form);
|
|
48
|
+
else router.put(`/admin/newsletters/templates/${props.template.id}`, form);
|
|
49
|
+
}
|
|
50
|
+
function onInsert(path) {
|
|
51
|
+
editorRef.value?.insertMergeField(path);
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<style scoped>
|
|
56
|
+
.templates-show {
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
gap: 12px;
|
|
60
|
+
max-width: 800px;
|
|
61
|
+
}
|
|
62
|
+
.editor-toolbar {
|
|
63
|
+
margin-bottom: 8px;
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ref, watch } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Debounced HTTP preview composable for the newsletter compose form.
|
|
5
|
+
*
|
|
6
|
+
* @param {import('vue').Ref<object>} formRef — reactive form snapshot to POST as the preview payload
|
|
7
|
+
* @param {string} endpoint — POST URL that returns `{ html: string }`
|
|
8
|
+
*/
|
|
9
|
+
export function usePreview(formRef, endpoint) {
|
|
10
|
+
const html = ref('<p>(empty)</p>');
|
|
11
|
+
const loading = ref(false);
|
|
12
|
+
let timer = null;
|
|
13
|
+
|
|
14
|
+
watch(
|
|
15
|
+
formRef,
|
|
16
|
+
() => {
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
loading.value = true;
|
|
19
|
+
timer = setTimeout(async () => {
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(endpoint, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify(formRef.value),
|
|
28
|
+
});
|
|
29
|
+
if (res.ok) {
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
html.value = data.html;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Swallow — preview is best-effort
|
|
35
|
+
} finally {
|
|
36
|
+
loading.value = false;
|
|
37
|
+
}
|
|
38
|
+
}, 500);
|
|
39
|
+
},
|
|
40
|
+
{ deep: true, immediate: true },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return { html, loading };
|
|
44
|
+
}
|
|
@@ -5,6 +5,10 @@ import { ref, computed } from 'vue';
|
|
|
5
5
|
|
|
6
6
|
const props = defineProps({
|
|
7
7
|
plugins: { type: Array, default: () => [] },
|
|
8
|
+
// Backends that load plugins from uploaded ZIPs (e.g. Laravel) support the
|
|
9
|
+
// upload flow; backends where plugins are compiled host modules registered
|
|
10
|
+
// in config (e.g. Phoenix) pass can_upload: false to hide it.
|
|
11
|
+
can_upload: { type: Boolean, default: true },
|
|
8
12
|
});
|
|
9
13
|
|
|
10
14
|
const confirmingDelete = ref(null);
|
|
@@ -93,7 +97,7 @@ function statusLabel(plugin) {
|
|
|
93
97
|
{{ activePlugins.length }} active, {{ inactivePlugins.length }} inactive
|
|
94
98
|
</p>
|
|
95
99
|
</div>
|
|
96
|
-
<div>
|
|
100
|
+
<div v-if="can_upload">
|
|
97
101
|
<input ref="fileInput" type="file" accept=".zip" class="hidden" @change="handleUpload" />
|
|
98
102
|
<button
|
|
99
103
|
:disabled="uploading"
|
|
@@ -144,9 +148,12 @@ function statusLabel(plugin) {
|
|
|
144
148
|
/>
|
|
145
149
|
</svg>
|
|
146
150
|
<h3 class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">No plugins installed</h3>
|
|
147
|
-
<p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
|
|
151
|
+
<p v-if="can_upload" class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
|
|
148
152
|
Upload a plugin ZIP to extend Escalated with custom functionality.
|
|
149
153
|
</p>
|
|
154
|
+
<p v-else class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
|
|
155
|
+
Register a plugin in the host application to extend Escalated with custom functionality.
|
|
156
|
+
</p>
|
|
150
157
|
</div>
|
|
151
158
|
|
|
152
159
|
<!-- Plugin cards -->
|
|
@@ -234,6 +241,7 @@ function statusLabel(plugin) {
|
|
|
234
241
|
</svg>
|
|
235
242
|
Installed {{ plugin.installed_at }}
|
|
236
243
|
</span>
|
|
244
|
+
<span v-if="plugin.module" class="font-mono">{{ plugin.module }}</span>
|
|
237
245
|
</div>
|
|
238
246
|
|
|
239
247
|
<!-- Provided extensions summary -->
|
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
3
|
-
import MetricComparison from '../../../components/MetricComparison.vue';
|
|
4
|
-
import ReportChart from '../../../components/ReportChart.vue';
|
|
5
|
-
import PercentileChart from '../../../components/PercentileChart.vue';
|
|
6
|
-
import PeriodSelector from '../../../components/PeriodSelector.vue';
|
|
7
|
-
import ReportExportButton from '../../../components/ReportExportButton.vue';
|
|
8
|
-
import { router, Link } from '@inertiajs/vue3';
|
|
9
|
-
import { ref } from 'vue';
|
|
10
|
-
|
|
11
|
-
const props = defineProps({
|
|
12
|
-
period_days: { type: Number, default: 30 },
|
|
13
|
-
agent: { type: Object, default: () => ({}) },
|
|
14
|
-
total_tickets: { type: Number, default: 0 },
|
|
15
|
-
resolved_tickets: { type: Number, default: 0 },
|
|
16
|
-
avg_frt: { type: Number, default: 0 },
|
|
17
|
-
avg_resolution: { type: Number, default: 0 },
|
|
18
|
-
csat_score: { type: [Number, String], default: '—' },
|
|
19
|
-
response_percentiles: { type: Object, default: () => ({ p50: 0, p75: 0, p90: 0, p95: 0, p99: 0 }) },
|
|
20
|
-
productivity: { type: Array, default: () => [] },
|
|
21
|
-
workload: { type: Array, default: () => [] },
|
|
22
|
-
csat_breakdown: { type: Array, default: () => [] },
|
|
23
|
-
prev_total_tickets: { type: Number, default: null },
|
|
24
|
-
prev_resolved_tickets: { type: Number, default: null },
|
|
25
|
-
prev_avg_frt: { type: Number, default: null },
|
|
26
|
-
prev_avg_resolution: { type: Number, default: null },
|
|
27
|
-
prev_csat_score: { type: [Number, String], default: null },
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const periodDays = ref(props.period_days);
|
|
31
|
-
|
|
32
|
-
function changePeriod(days) {
|
|
33
|
-
periodDays.value = days;
|
|
34
|
-
router.get(
|
|
35
|
-
route('escalated.admin.reports.agent-detail', { id: props.agent?.id }),
|
|
36
|
-
{ days },
|
|
37
|
-
{ preserveState: true },
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
</script>
|
|
41
|
-
|
|
42
|
-
<template>
|
|
43
|
-
<EscalatedLayout :title="`Agent: ${agent?.name || 'Unknown'}`">
|
|
44
|
-
<div class="mb-6 flex items-center justify-between">
|
|
45
|
-
<PeriodSelector :model-value="periodDays" @update:model-value="changePeriod" />
|
|
46
|
-
<div class="flex items-center gap-3">
|
|
47
|
-
<ReportExportButton :report-name="`agent-${agent?.id}`" :period-days="periodDays" />
|
|
48
|
-
<Link
|
|
49
|
-
:href="route('escalated.admin.reports.agent-ranking', { days: periodDays })"
|
|
50
|
-
class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
51
|
-
>
|
|
52
|
-
← Back to Rankings
|
|
53
|
-
</Link>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
|
|
57
|
-
<!-- Agent Header -->
|
|
58
|
-
<div
|
|
59
|
-
class="mb-6 flex items-center gap-4 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5"
|
|
60
|
-
>
|
|
61
|
-
<div
|
|
62
|
-
class="flex h-14 w-14 items-center justify-center rounded-xl bg-gradient-to-br from-cyan-500/20 to-violet-500/20 text-xl font-bold text-white"
|
|
63
|
-
>
|
|
64
|
-
{{ agent?.name?.charAt(0)?.toUpperCase() || '?' }}
|
|
65
|
-
</div>
|
|
66
|
-
<div>
|
|
67
|
-
<h2 class="text-lg font-bold text-[var(--esc-panel-text)]">{{ agent?.name || 'Unknown Agent' }}</h2>
|
|
68
|
-
<p class="text-sm text-[var(--esc-panel-text-muted)]">{{ agent?.email || '' }}</p>
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
<!-- KPI Cards with comparison -->
|
|
73
|
-
<div class="mb-6 grid grid-cols-2 gap-4 md:grid-cols-5">
|
|
74
|
-
<MetricComparison
|
|
75
|
-
label="Total Tickets"
|
|
76
|
-
:value="total_tickets"
|
|
77
|
-
:previous-value="prev_total_tickets"
|
|
78
|
-
: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'"
|
|
79
|
-
/>
|
|
80
|
-
<MetricComparison
|
|
81
|
-
label="Resolved"
|
|
82
|
-
:value="resolved_tickets"
|
|
83
|
-
:previous-value="prev_resolved_tickets"
|
|
84
|
-
:icon="'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z'"
|
|
85
|
-
/>
|
|
86
|
-
<MetricComparison
|
|
87
|
-
label="Avg FRT"
|
|
88
|
-
:value="avg_frt"
|
|
89
|
-
:previous-value="prev_avg_frt"
|
|
90
|
-
format="h"
|
|
91
|
-
:invert-color="true"
|
|
92
|
-
:icon="'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z'"
|
|
93
|
-
/>
|
|
94
|
-
<MetricComparison
|
|
95
|
-
label="Avg Resolution"
|
|
96
|
-
:value="avg_resolution"
|
|
97
|
-
:previous-value="prev_avg_resolution"
|
|
98
|
-
format="h"
|
|
99
|
-
:invert-color="true"
|
|
100
|
-
:icon="'M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5'"
|
|
101
|
-
/>
|
|
102
|
-
<MetricComparison
|
|
103
|
-
label="CSAT Score"
|
|
104
|
-
:value="csat_score"
|
|
105
|
-
:previous-value="prev_csat_score"
|
|
106
|
-
:icon="'M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z'"
|
|
107
|
-
/>
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
<!-- Percentile & Productivity Charts -->
|
|
111
|
-
<div class="mb-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
112
|
-
<PercentileChart title="Response Time Percentiles" :percentiles="response_percentiles" unit="h" />
|
|
113
|
-
<ReportChart
|
|
114
|
-
type="bar"
|
|
115
|
-
title="Productivity (Replies & Resolutions per Day)"
|
|
116
|
-
:data="productivity"
|
|
117
|
-
:colors="['#06b6d4', '#8b5cf6']"
|
|
118
|
-
/>
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
<!-- Workload & CSAT -->
|
|
122
|
-
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
123
|
-
<ReportChart type="area" title="Workload Over Time" :data="workload" :colors="['#f59e0b']" />
|
|
124
|
-
<ReportChart
|
|
125
|
-
type="pie"
|
|
126
|
-
title="CSAT Breakdown"
|
|
127
|
-
:data="csat_breakdown"
|
|
128
|
-
:colors="['#ef4444', '#f97316', '#f59e0b', '#10b981', '#06b6d4']"
|
|
129
|
-
/>
|
|
130
|
-
</div>
|
|
131
|
-
</EscalatedLayout>
|
|
132
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
3
|
+
import MetricComparison from '../../../components/MetricComparison.vue';
|
|
4
|
+
import ReportChart from '../../../components/ReportChart.vue';
|
|
5
|
+
import PercentileChart from '../../../components/PercentileChart.vue';
|
|
6
|
+
import PeriodSelector from '../../../components/PeriodSelector.vue';
|
|
7
|
+
import ReportExportButton from '../../../components/ReportExportButton.vue';
|
|
8
|
+
import { router, Link } from '@inertiajs/vue3';
|
|
9
|
+
import { ref } from 'vue';
|
|
10
|
+
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
period_days: { type: Number, default: 30 },
|
|
13
|
+
agent: { type: Object, default: () => ({}) },
|
|
14
|
+
total_tickets: { type: Number, default: 0 },
|
|
15
|
+
resolved_tickets: { type: Number, default: 0 },
|
|
16
|
+
avg_frt: { type: Number, default: 0 },
|
|
17
|
+
avg_resolution: { type: Number, default: 0 },
|
|
18
|
+
csat_score: { type: [Number, String], default: '—' },
|
|
19
|
+
response_percentiles: { type: Object, default: () => ({ p50: 0, p75: 0, p90: 0, p95: 0, p99: 0 }) },
|
|
20
|
+
productivity: { type: Array, default: () => [] },
|
|
21
|
+
workload: { type: Array, default: () => [] },
|
|
22
|
+
csat_breakdown: { type: Array, default: () => [] },
|
|
23
|
+
prev_total_tickets: { type: Number, default: null },
|
|
24
|
+
prev_resolved_tickets: { type: Number, default: null },
|
|
25
|
+
prev_avg_frt: { type: Number, default: null },
|
|
26
|
+
prev_avg_resolution: { type: Number, default: null },
|
|
27
|
+
prev_csat_score: { type: [Number, String], default: null },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const periodDays = ref(props.period_days);
|
|
31
|
+
|
|
32
|
+
function changePeriod(days) {
|
|
33
|
+
periodDays.value = days;
|
|
34
|
+
router.get(
|
|
35
|
+
route('escalated.admin.reports.agent-detail', { id: props.agent?.id }),
|
|
36
|
+
{ days },
|
|
37
|
+
{ preserveState: true },
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<EscalatedLayout :title="`Agent: ${agent?.name || 'Unknown'}`">
|
|
44
|
+
<div class="mb-6 flex items-center justify-between">
|
|
45
|
+
<PeriodSelector :model-value="periodDays" @update:model-value="changePeriod" />
|
|
46
|
+
<div class="flex items-center gap-3">
|
|
47
|
+
<ReportExportButton :report-name="`agent-${agent?.id}`" :period-days="periodDays" />
|
|
48
|
+
<Link
|
|
49
|
+
:href="route('escalated.admin.reports.agent-ranking', { days: periodDays })"
|
|
50
|
+
class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
51
|
+
>
|
|
52
|
+
← Back to Rankings
|
|
53
|
+
</Link>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Agent Header -->
|
|
58
|
+
<div
|
|
59
|
+
class="mb-6 flex items-center gap-4 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5"
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
class="flex h-14 w-14 items-center justify-center rounded-xl bg-gradient-to-br from-cyan-500/20 to-violet-500/20 text-xl font-bold text-white"
|
|
63
|
+
>
|
|
64
|
+
{{ agent?.name?.charAt(0)?.toUpperCase() || '?' }}
|
|
65
|
+
</div>
|
|
66
|
+
<div>
|
|
67
|
+
<h2 class="text-lg font-bold text-[var(--esc-panel-text)]">{{ agent?.name || 'Unknown Agent' }}</h2>
|
|
68
|
+
<p class="text-sm text-[var(--esc-panel-text-muted)]">{{ agent?.email || '' }}</p>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<!-- KPI Cards with comparison -->
|
|
73
|
+
<div class="mb-6 grid grid-cols-2 gap-4 md:grid-cols-5">
|
|
74
|
+
<MetricComparison
|
|
75
|
+
label="Total Tickets"
|
|
76
|
+
:value="total_tickets"
|
|
77
|
+
:previous-value="prev_total_tickets"
|
|
78
|
+
: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'"
|
|
79
|
+
/>
|
|
80
|
+
<MetricComparison
|
|
81
|
+
label="Resolved"
|
|
82
|
+
:value="resolved_tickets"
|
|
83
|
+
:previous-value="prev_resolved_tickets"
|
|
84
|
+
:icon="'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z'"
|
|
85
|
+
/>
|
|
86
|
+
<MetricComparison
|
|
87
|
+
label="Avg FRT"
|
|
88
|
+
:value="avg_frt"
|
|
89
|
+
:previous-value="prev_avg_frt"
|
|
90
|
+
format="h"
|
|
91
|
+
:invert-color="true"
|
|
92
|
+
:icon="'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z'"
|
|
93
|
+
/>
|
|
94
|
+
<MetricComparison
|
|
95
|
+
label="Avg Resolution"
|
|
96
|
+
:value="avg_resolution"
|
|
97
|
+
:previous-value="prev_avg_resolution"
|
|
98
|
+
format="h"
|
|
99
|
+
:invert-color="true"
|
|
100
|
+
:icon="'M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5'"
|
|
101
|
+
/>
|
|
102
|
+
<MetricComparison
|
|
103
|
+
label="CSAT Score"
|
|
104
|
+
:value="csat_score"
|
|
105
|
+
:previous-value="prev_csat_score"
|
|
106
|
+
:icon="'M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z'"
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Percentile & Productivity Charts -->
|
|
111
|
+
<div class="mb-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
112
|
+
<PercentileChart title="Response Time Percentiles" :percentiles="response_percentiles" unit="h" />
|
|
113
|
+
<ReportChart
|
|
114
|
+
type="bar"
|
|
115
|
+
title="Productivity (Replies & Resolutions per Day)"
|
|
116
|
+
:data="productivity"
|
|
117
|
+
:colors="['#06b6d4', '#8b5cf6']"
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<!-- Workload & CSAT -->
|
|
122
|
+
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
123
|
+
<ReportChart type="area" title="Workload Over Time" :data="workload" :colors="['#f59e0b']" />
|
|
124
|
+
<ReportChart
|
|
125
|
+
type="pie"
|
|
126
|
+
title="CSAT Breakdown"
|
|
127
|
+
:data="csat_breakdown"
|
|
128
|
+
:colors="['#ef4444', '#f97316', '#f59e0b', '#10b981', '#06b6d4']"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</EscalatedLayout>
|
|
132
|
+
</template>
|