@escalated-dev/escalated 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +380 -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 +619 -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/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/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/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/Workflows/Builder.vue +207 -207
- 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/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
|
@@ -1,183 +1,183 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
3
|
-
import KpiCard from '../../../components/KpiCard.vue';
|
|
4
|
-
import ReportChart from '../../../components/ReportChart.vue';
|
|
5
|
-
import PeriodSelector from '../../../components/PeriodSelector.vue';
|
|
6
|
-
import ReportExportButton from '../../../components/ReportExportButton.vue';
|
|
7
|
-
import { router, Link } from '@inertiajs/vue3';
|
|
8
|
-
import { ref, computed } from 'vue';
|
|
9
|
-
|
|
10
|
-
const props = defineProps({
|
|
11
|
-
period_days: { type: Number, default: 30 },
|
|
12
|
-
breach_trend: { type: Array, default: () => [] },
|
|
13
|
-
breach_by_type_trend: { type: Array, default: () => [] },
|
|
14
|
-
breach_by_department: { type: Array, default: () => [] },
|
|
15
|
-
breach_by_priority: { type: Array, default: () => [] },
|
|
16
|
-
at_risk_tickets: { type: Array, default: () => [] },
|
|
17
|
-
total_breaches: { type: Number, default: 0 },
|
|
18
|
-
breach_rate: { type: Number, default: 0 },
|
|
19
|
-
first_response_breaches: { type: Number, default: 0 },
|
|
20
|
-
resolution_breaches: { type: Number, default: 0 },
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const periodDays = ref(props.period_days);
|
|
24
|
-
|
|
25
|
-
function changePeriod(days) {
|
|
26
|
-
periodDays.value = days;
|
|
27
|
-
router.get(route('escalated.admin.reports.sla-trends'), { days }, { preserveState: true });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const riskCategories = computed(() => {
|
|
31
|
-
const tickets = props.at_risk_tickets || [];
|
|
32
|
-
return {
|
|
33
|
-
critical: tickets.filter((t) => t.hours_remaining <= 1),
|
|
34
|
-
warning: tickets.filter((t) => t.hours_remaining > 1 && t.hours_remaining <= 4),
|
|
35
|
-
watch: tickets.filter((t) => t.hours_remaining > 4 && t.hours_remaining <= 8),
|
|
36
|
-
};
|
|
37
|
-
});
|
|
38
|
-
</script>
|
|
39
|
-
|
|
40
|
-
<template>
|
|
41
|
-
<EscalatedLayout title="SLA Breach Trends">
|
|
42
|
-
<div class="mb-6 flex items-center justify-between">
|
|
43
|
-
<PeriodSelector :model-value="periodDays" @update:model-value="changePeriod" />
|
|
44
|
-
<div class="flex items-center gap-3">
|
|
45
|
-
<ReportExportButton report-name="sla-trends" :period-days="periodDays" />
|
|
46
|
-
<Link
|
|
47
|
-
:href="route('escalated.admin.reports')"
|
|
48
|
-
class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
49
|
-
>
|
|
50
|
-
← Back
|
|
51
|
-
</Link>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<!-- KPI Cards -->
|
|
56
|
-
<div class="mb-6 grid grid-cols-2 gap-4 md:grid-cols-4">
|
|
57
|
-
<KpiCard
|
|
58
|
-
label="Total Breaches"
|
|
59
|
-
:value="total_breaches"
|
|
60
|
-
:icon="'M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z'"
|
|
61
|
-
/>
|
|
62
|
-
<KpiCard
|
|
63
|
-
label="Breach Rate"
|
|
64
|
-
:value="`${breach_rate}%`"
|
|
65
|
-
: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.75z'"
|
|
66
|
-
/>
|
|
67
|
-
<KpiCard
|
|
68
|
-
label="FRT Breaches"
|
|
69
|
-
:value="first_response_breaches"
|
|
70
|
-
:icon="'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z'"
|
|
71
|
-
/>
|
|
72
|
-
<KpiCard
|
|
73
|
-
label="Resolution Breaches"
|
|
74
|
-
:value="resolution_breaches"
|
|
75
|
-
:icon="'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z'"
|
|
76
|
-
/>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
<!-- Charts -->
|
|
80
|
-
<div class="mb-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
81
|
-
<ReportChart type="line" title="Breach Count Over Time" :data="breach_trend" :colors="['#ef4444']" />
|
|
82
|
-
<ReportChart
|
|
83
|
-
type="stacked-bar"
|
|
84
|
-
title="First Response vs Resolution Breaches"
|
|
85
|
-
:data="breach_by_type_trend"
|
|
86
|
-
:colors="['#f59e0b', '#ef4444']"
|
|
87
|
-
/>
|
|
88
|
-
<ReportChart
|
|
89
|
-
type="bar"
|
|
90
|
-
title="Breach Rate by Department"
|
|
91
|
-
:data="breach_by_department"
|
|
92
|
-
:colors="['#8b5cf6']"
|
|
93
|
-
/>
|
|
94
|
-
<ReportChart type="bar" title="Breach Rate by Priority" :data="breach_by_priority" :colors="['#06b6d4']" />
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
<!-- At Risk Panel -->
|
|
98
|
-
<div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5">
|
|
99
|
-
<h3 class="mb-4 text-sm font-semibold text-[var(--esc-panel-text-secondary)]">
|
|
100
|
-
At Risk - Approaching SLA Breach
|
|
101
|
-
</h3>
|
|
102
|
-
<div v-if="at_risk_tickets?.length" class="space-y-4">
|
|
103
|
-
<!-- Critical: < 1h -->
|
|
104
|
-
<div v-if="riskCategories.critical.length">
|
|
105
|
-
<div class="mb-2 flex items-center gap-2">
|
|
106
|
-
<span class="inline-block h-2 w-2 rounded-full bg-red-500"></span>
|
|
107
|
-
<span class="text-xs font-semibold uppercase tracking-wider text-red-400"
|
|
108
|
-
>Critical (<1h)</span
|
|
109
|
-
>
|
|
110
|
-
</div>
|
|
111
|
-
<div class="space-y-1">
|
|
112
|
-
<div
|
|
113
|
-
v-for="ticket in riskCategories.critical"
|
|
114
|
-
:key="ticket.id"
|
|
115
|
-
class="flex items-center justify-between rounded-lg bg-red-500/5 px-3 py-2 border border-red-500/10"
|
|
116
|
-
>
|
|
117
|
-
<div class="flex items-center gap-3">
|
|
118
|
-
<span class="font-mono text-xs text-red-400">{{ ticket.reference }}</span>
|
|
119
|
-
<span class="truncate text-sm text-[var(--esc-panel-text-secondary)]">{{
|
|
120
|
-
ticket.subject
|
|
121
|
-
}}</span>
|
|
122
|
-
</div>
|
|
123
|
-
<span class="text-xs font-bold text-red-400">{{ ticket.hours_remaining }}h remaining</span>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
<!-- Warning: 1-4h -->
|
|
129
|
-
<div v-if="riskCategories.warning.length">
|
|
130
|
-
<div class="mb-2 flex items-center gap-2">
|
|
131
|
-
<span class="inline-block h-2 w-2 rounded-full bg-amber-500"></span>
|
|
132
|
-
<span class="text-xs font-semibold uppercase tracking-wider text-amber-400"
|
|
133
|
-
>Warning (1-4h)</span
|
|
134
|
-
>
|
|
135
|
-
</div>
|
|
136
|
-
<div class="space-y-1">
|
|
137
|
-
<div
|
|
138
|
-
v-for="ticket in riskCategories.warning"
|
|
139
|
-
:key="ticket.id"
|
|
140
|
-
class="flex items-center justify-between rounded-lg bg-amber-500/5 px-3 py-2 border border-amber-500/10"
|
|
141
|
-
>
|
|
142
|
-
<div class="flex items-center gap-3">
|
|
143
|
-
<span class="font-mono text-xs text-amber-400">{{ ticket.reference }}</span>
|
|
144
|
-
<span class="truncate text-sm text-[var(--esc-panel-text-secondary)]">{{
|
|
145
|
-
ticket.subject
|
|
146
|
-
}}</span>
|
|
147
|
-
</div>
|
|
148
|
-
<span class="text-xs font-bold text-amber-400"
|
|
149
|
-
>{{ ticket.hours_remaining }}h remaining</span
|
|
150
|
-
>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
|
|
155
|
-
<!-- Watch: 4-8h -->
|
|
156
|
-
<div v-if="riskCategories.watch.length">
|
|
157
|
-
<div class="mb-2 flex items-center gap-2">
|
|
158
|
-
<span class="inline-block h-2 w-2 rounded-full bg-blue-500"></span>
|
|
159
|
-
<span class="text-xs font-semibold uppercase tracking-wider text-blue-400">Watch (4-8h)</span>
|
|
160
|
-
</div>
|
|
161
|
-
<div class="space-y-1">
|
|
162
|
-
<div
|
|
163
|
-
v-for="ticket in riskCategories.watch"
|
|
164
|
-
:key="ticket.id"
|
|
165
|
-
class="flex items-center justify-between rounded-lg bg-blue-500/5 px-3 py-2 border border-blue-500/10"
|
|
166
|
-
>
|
|
167
|
-
<div class="flex items-center gap-3">
|
|
168
|
-
<span class="font-mono text-xs text-blue-400">{{ ticket.reference }}</span>
|
|
169
|
-
<span class="truncate text-sm text-[var(--esc-panel-text-secondary)]">{{
|
|
170
|
-
ticket.subject
|
|
171
|
-
}}</span>
|
|
172
|
-
</div>
|
|
173
|
-
<span class="text-xs font-bold text-blue-400">{{ ticket.hours_remaining }}h remaining</span>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
</div>
|
|
178
|
-
<div v-else class="py-6 text-center text-sm text-[var(--esc-panel-text-muted)]">
|
|
179
|
-
No tickets currently at risk of breaching SLA.
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
182
|
-
</EscalatedLayout>
|
|
183
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
3
|
+
import KpiCard from '../../../components/KpiCard.vue';
|
|
4
|
+
import ReportChart from '../../../components/ReportChart.vue';
|
|
5
|
+
import PeriodSelector from '../../../components/PeriodSelector.vue';
|
|
6
|
+
import ReportExportButton from '../../../components/ReportExportButton.vue';
|
|
7
|
+
import { router, Link } from '@inertiajs/vue3';
|
|
8
|
+
import { ref, computed } from 'vue';
|
|
9
|
+
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
period_days: { type: Number, default: 30 },
|
|
12
|
+
breach_trend: { type: Array, default: () => [] },
|
|
13
|
+
breach_by_type_trend: { type: Array, default: () => [] },
|
|
14
|
+
breach_by_department: { type: Array, default: () => [] },
|
|
15
|
+
breach_by_priority: { type: Array, default: () => [] },
|
|
16
|
+
at_risk_tickets: { type: Array, default: () => [] },
|
|
17
|
+
total_breaches: { type: Number, default: 0 },
|
|
18
|
+
breach_rate: { type: Number, default: 0 },
|
|
19
|
+
first_response_breaches: { type: Number, default: 0 },
|
|
20
|
+
resolution_breaches: { type: Number, default: 0 },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const periodDays = ref(props.period_days);
|
|
24
|
+
|
|
25
|
+
function changePeriod(days) {
|
|
26
|
+
periodDays.value = days;
|
|
27
|
+
router.get(route('escalated.admin.reports.sla-trends'), { days }, { preserveState: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const riskCategories = computed(() => {
|
|
31
|
+
const tickets = props.at_risk_tickets || [];
|
|
32
|
+
return {
|
|
33
|
+
critical: tickets.filter((t) => t.hours_remaining <= 1),
|
|
34
|
+
warning: tickets.filter((t) => t.hours_remaining > 1 && t.hours_remaining <= 4),
|
|
35
|
+
watch: tickets.filter((t) => t.hours_remaining > 4 && t.hours_remaining <= 8),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<EscalatedLayout title="SLA Breach Trends">
|
|
42
|
+
<div class="mb-6 flex items-center justify-between">
|
|
43
|
+
<PeriodSelector :model-value="periodDays" @update:model-value="changePeriod" />
|
|
44
|
+
<div class="flex items-center gap-3">
|
|
45
|
+
<ReportExportButton report-name="sla-trends" :period-days="periodDays" />
|
|
46
|
+
<Link
|
|
47
|
+
:href="route('escalated.admin.reports')"
|
|
48
|
+
class="text-sm text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
49
|
+
>
|
|
50
|
+
← Back
|
|
51
|
+
</Link>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<!-- KPI Cards -->
|
|
56
|
+
<div class="mb-6 grid grid-cols-2 gap-4 md:grid-cols-4">
|
|
57
|
+
<KpiCard
|
|
58
|
+
label="Total Breaches"
|
|
59
|
+
:value="total_breaches"
|
|
60
|
+
:icon="'M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z'"
|
|
61
|
+
/>
|
|
62
|
+
<KpiCard
|
|
63
|
+
label="Breach Rate"
|
|
64
|
+
:value="`${breach_rate}%`"
|
|
65
|
+
: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.75z'"
|
|
66
|
+
/>
|
|
67
|
+
<KpiCard
|
|
68
|
+
label="FRT Breaches"
|
|
69
|
+
:value="first_response_breaches"
|
|
70
|
+
:icon="'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z'"
|
|
71
|
+
/>
|
|
72
|
+
<KpiCard
|
|
73
|
+
label="Resolution Breaches"
|
|
74
|
+
:value="resolution_breaches"
|
|
75
|
+
:icon="'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z'"
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Charts -->
|
|
80
|
+
<div class="mb-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
81
|
+
<ReportChart type="line" title="Breach Count Over Time" :data="breach_trend" :colors="['#ef4444']" />
|
|
82
|
+
<ReportChart
|
|
83
|
+
type="stacked-bar"
|
|
84
|
+
title="First Response vs Resolution Breaches"
|
|
85
|
+
:data="breach_by_type_trend"
|
|
86
|
+
:colors="['#f59e0b', '#ef4444']"
|
|
87
|
+
/>
|
|
88
|
+
<ReportChart
|
|
89
|
+
type="bar"
|
|
90
|
+
title="Breach Rate by Department"
|
|
91
|
+
:data="breach_by_department"
|
|
92
|
+
:colors="['#8b5cf6']"
|
|
93
|
+
/>
|
|
94
|
+
<ReportChart type="bar" title="Breach Rate by Priority" :data="breach_by_priority" :colors="['#06b6d4']" />
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- At Risk Panel -->
|
|
98
|
+
<div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-5">
|
|
99
|
+
<h3 class="mb-4 text-sm font-semibold text-[var(--esc-panel-text-secondary)]">
|
|
100
|
+
At Risk - Approaching SLA Breach
|
|
101
|
+
</h3>
|
|
102
|
+
<div v-if="at_risk_tickets?.length" class="space-y-4">
|
|
103
|
+
<!-- Critical: < 1h -->
|
|
104
|
+
<div v-if="riskCategories.critical.length">
|
|
105
|
+
<div class="mb-2 flex items-center gap-2">
|
|
106
|
+
<span class="inline-block h-2 w-2 rounded-full bg-red-500"></span>
|
|
107
|
+
<span class="text-xs font-semibold uppercase tracking-wider text-red-400"
|
|
108
|
+
>Critical (<1h)</span
|
|
109
|
+
>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="space-y-1">
|
|
112
|
+
<div
|
|
113
|
+
v-for="ticket in riskCategories.critical"
|
|
114
|
+
:key="ticket.id"
|
|
115
|
+
class="flex items-center justify-between rounded-lg bg-red-500/5 px-3 py-2 border border-red-500/10"
|
|
116
|
+
>
|
|
117
|
+
<div class="flex items-center gap-3">
|
|
118
|
+
<span class="font-mono text-xs text-red-400">{{ ticket.reference }}</span>
|
|
119
|
+
<span class="truncate text-sm text-[var(--esc-panel-text-secondary)]">{{
|
|
120
|
+
ticket.subject
|
|
121
|
+
}}</span>
|
|
122
|
+
</div>
|
|
123
|
+
<span class="text-xs font-bold text-red-400">{{ ticket.hours_remaining }}h remaining</span>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Warning: 1-4h -->
|
|
129
|
+
<div v-if="riskCategories.warning.length">
|
|
130
|
+
<div class="mb-2 flex items-center gap-2">
|
|
131
|
+
<span class="inline-block h-2 w-2 rounded-full bg-amber-500"></span>
|
|
132
|
+
<span class="text-xs font-semibold uppercase tracking-wider text-amber-400"
|
|
133
|
+
>Warning (1-4h)</span
|
|
134
|
+
>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="space-y-1">
|
|
137
|
+
<div
|
|
138
|
+
v-for="ticket in riskCategories.warning"
|
|
139
|
+
:key="ticket.id"
|
|
140
|
+
class="flex items-center justify-between rounded-lg bg-amber-500/5 px-3 py-2 border border-amber-500/10"
|
|
141
|
+
>
|
|
142
|
+
<div class="flex items-center gap-3">
|
|
143
|
+
<span class="font-mono text-xs text-amber-400">{{ ticket.reference }}</span>
|
|
144
|
+
<span class="truncate text-sm text-[var(--esc-panel-text-secondary)]">{{
|
|
145
|
+
ticket.subject
|
|
146
|
+
}}</span>
|
|
147
|
+
</div>
|
|
148
|
+
<span class="text-xs font-bold text-amber-400"
|
|
149
|
+
>{{ ticket.hours_remaining }}h remaining</span
|
|
150
|
+
>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<!-- Watch: 4-8h -->
|
|
156
|
+
<div v-if="riskCategories.watch.length">
|
|
157
|
+
<div class="mb-2 flex items-center gap-2">
|
|
158
|
+
<span class="inline-block h-2 w-2 rounded-full bg-blue-500"></span>
|
|
159
|
+
<span class="text-xs font-semibold uppercase tracking-wider text-blue-400">Watch (4-8h)</span>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="space-y-1">
|
|
162
|
+
<div
|
|
163
|
+
v-for="ticket in riskCategories.watch"
|
|
164
|
+
:key="ticket.id"
|
|
165
|
+
class="flex items-center justify-between rounded-lg bg-blue-500/5 px-3 py-2 border border-blue-500/10"
|
|
166
|
+
>
|
|
167
|
+
<div class="flex items-center gap-3">
|
|
168
|
+
<span class="font-mono text-xs text-blue-400">{{ ticket.reference }}</span>
|
|
169
|
+
<span class="truncate text-sm text-[var(--esc-panel-text-secondary)]">{{
|
|
170
|
+
ticket.subject
|
|
171
|
+
}}</span>
|
|
172
|
+
</div>
|
|
173
|
+
<span class="text-xs font-bold text-blue-400">{{ ticket.hours_remaining }}h remaining</span>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div v-else class="py-6 text-center text-sm text-[var(--esc-panel-text-muted)]">
|
|
179
|
+
No tickets currently at risk of breaching SLA.
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</EscalatedLayout>
|
|
183
|
+
</template>
|
|
@@ -8,14 +8,34 @@ const form = useForm({
|
|
|
8
8
|
sso_provider: props.settings.sso_provider ?? 'none',
|
|
9
9
|
sso_entity_id: props.settings.sso_entity_id ?? '',
|
|
10
10
|
sso_url: props.settings.sso_url ?? '',
|
|
11
|
+
sso_login_url: props.settings.sso_login_url ?? props.settings.sso_url ?? '',
|
|
12
|
+
sso_logout_url: props.settings.sso_logout_url ?? '',
|
|
13
|
+
sso_metadata_url: props.settings.sso_metadata_url ?? '',
|
|
11
14
|
sso_certificate: props.settings.sso_certificate ?? '',
|
|
12
15
|
sso_attr_email: props.settings.sso_attr_email ?? 'email',
|
|
13
16
|
sso_attr_name: props.settings.sso_attr_name ?? 'name',
|
|
14
17
|
sso_attr_role: props.settings.sso_attr_role ?? 'role',
|
|
15
18
|
sso_jwt_secret: props.settings.sso_jwt_secret ?? '',
|
|
16
19
|
sso_jwt_algorithm: props.settings.sso_jwt_algorithm ?? 'HS256',
|
|
20
|
+
sso_oauth_authorize_url: props.settings.sso_oauth_authorize_url ?? '',
|
|
21
|
+
sso_oauth_token_url: props.settings.sso_oauth_token_url ?? '',
|
|
22
|
+
sso_oauth_userinfo_url: props.settings.sso_oauth_userinfo_url ?? '',
|
|
23
|
+
sso_oauth_client_id: props.settings.sso_oauth_client_id ?? '',
|
|
24
|
+
sso_oauth_client_secret: props.settings.sso_oauth_client_secret ?? '',
|
|
25
|
+
sso_oauth_scopes: props.settings.sso_oauth_scopes ?? 'openid profile email',
|
|
17
26
|
});
|
|
18
27
|
|
|
28
|
+
function readCertificate(event) {
|
|
29
|
+
const [file] = event.target.files ?? [];
|
|
30
|
+
if (!file) return;
|
|
31
|
+
|
|
32
|
+
const reader = new window.FileReader();
|
|
33
|
+
reader.onload = () => {
|
|
34
|
+
form.sso_certificate = typeof reader.result === 'string' ? reader.result : '';
|
|
35
|
+
};
|
|
36
|
+
reader.readAsText(file);
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
function submit() {
|
|
20
40
|
form.post(route('escalated.admin.settings.sso.update'));
|
|
21
41
|
}
|
|
@@ -42,6 +62,7 @@ function submit() {
|
|
|
42
62
|
<option value="none">None (disabled)</option>
|
|
43
63
|
<option value="saml">SAML 2.0</option>
|
|
44
64
|
<option value="jwt">JWT</option>
|
|
65
|
+
<option value="oauth">OAuth 2.0 / OIDC</option>
|
|
45
66
|
</select>
|
|
46
67
|
</div>
|
|
47
68
|
</div>
|
|
@@ -72,16 +93,48 @@ function submit() {
|
|
|
72
93
|
</div>
|
|
73
94
|
</div>
|
|
74
95
|
<div>
|
|
75
|
-
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
|
|
96
|
+
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
|
|
97
|
+
>Login URL</label
|
|
98
|
+
>
|
|
76
99
|
<p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">The Single Sign-On URL endpoint</p>
|
|
77
100
|
<input
|
|
78
|
-
v-model="form.
|
|
101
|
+
v-model="form.sso_login_url"
|
|
79
102
|
type="url"
|
|
80
103
|
placeholder="https://idp.example.com/sso"
|
|
81
104
|
class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
82
105
|
/>
|
|
83
|
-
<div v-if="form.errors.
|
|
84
|
-
{{ form.errors.
|
|
106
|
+
<div v-if="form.errors.sso_login_url" class="mt-1 text-sm text-rose-400">
|
|
107
|
+
{{ form.errors.sso_login_url }}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="grid gap-5 md:grid-cols-2">
|
|
111
|
+
<div>
|
|
112
|
+
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
|
|
113
|
+
>Logout URL</label
|
|
114
|
+
>
|
|
115
|
+
<input
|
|
116
|
+
v-model="form.sso_logout_url"
|
|
117
|
+
type="url"
|
|
118
|
+
placeholder="https://idp.example.com/logout"
|
|
119
|
+
class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
120
|
+
/>
|
|
121
|
+
<div v-if="form.errors.sso_logout_url" class="mt-1 text-sm text-rose-400">
|
|
122
|
+
{{ form.errors.sso_logout_url }}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<div>
|
|
126
|
+
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
|
|
127
|
+
>Metadata URL</label
|
|
128
|
+
>
|
|
129
|
+
<input
|
|
130
|
+
v-model="form.sso_metadata_url"
|
|
131
|
+
type="url"
|
|
132
|
+
placeholder="https://idp.example.com/metadata"
|
|
133
|
+
class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
134
|
+
/>
|
|
135
|
+
<div v-if="form.errors.sso_metadata_url" class="mt-1 text-sm text-rose-400">
|
|
136
|
+
{{ form.errors.sso_metadata_url }}
|
|
137
|
+
</div>
|
|
85
138
|
</div>
|
|
86
139
|
</div>
|
|
87
140
|
<div>
|
|
@@ -91,6 +144,12 @@ function submit() {
|
|
|
91
144
|
<p class="mt-0.5 text-xs text-[var(--esc-panel-text-muted)]">
|
|
92
145
|
X.509 certificate from your Identity Provider (PEM format)
|
|
93
146
|
</p>
|
|
147
|
+
<input
|
|
148
|
+
type="file"
|
|
149
|
+
accept=".pem,.crt,.cer,.txt"
|
|
150
|
+
class="mt-2 block text-xs text-[var(--esc-panel-text-muted)]"
|
|
151
|
+
@change="readCertificate"
|
|
152
|
+
/>
|
|
94
153
|
<textarea
|
|
95
154
|
v-model="form.sso_certificate"
|
|
96
155
|
rows="6"
|
|
@@ -139,6 +198,8 @@ function submit() {
|
|
|
139
198
|
<option value="HS384">HS384</option>
|
|
140
199
|
<option value="HS512">HS512</option>
|
|
141
200
|
<option value="RS256">RS256</option>
|
|
201
|
+
<option value="RS384">RS384</option>
|
|
202
|
+
<option value="RS512">RS512</option>
|
|
142
203
|
</select>
|
|
143
204
|
</div>
|
|
144
205
|
<div>
|
|
@@ -156,6 +217,98 @@ function submit() {
|
|
|
156
217
|
</div>
|
|
157
218
|
</div>
|
|
158
219
|
|
|
220
|
+
<div
|
|
221
|
+
v-if="form.sso_provider === 'oauth'"
|
|
222
|
+
class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
|
|
223
|
+
>
|
|
224
|
+
<h3 class="mb-5 text-sm font-semibold text-[var(--esc-panel-text)]">OAuth / OIDC Configuration</h3>
|
|
225
|
+
<div class="space-y-5">
|
|
226
|
+
<div class="grid gap-5 md:grid-cols-2">
|
|
227
|
+
<div>
|
|
228
|
+
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
|
|
229
|
+
>Authorize URL</label
|
|
230
|
+
>
|
|
231
|
+
<input
|
|
232
|
+
v-model="form.sso_oauth_authorize_url"
|
|
233
|
+
type="url"
|
|
234
|
+
placeholder="https://idp.example.com/oauth/authorize"
|
|
235
|
+
class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
236
|
+
/>
|
|
237
|
+
<div v-if="form.errors.sso_oauth_authorize_url" class="mt-1 text-sm text-rose-400">
|
|
238
|
+
{{ form.errors.sso_oauth_authorize_url }}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
<div>
|
|
242
|
+
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
|
|
243
|
+
>Token URL</label
|
|
244
|
+
>
|
|
245
|
+
<input
|
|
246
|
+
v-model="form.sso_oauth_token_url"
|
|
247
|
+
type="url"
|
|
248
|
+
placeholder="https://idp.example.com/oauth/token"
|
|
249
|
+
class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
250
|
+
/>
|
|
251
|
+
<div v-if="form.errors.sso_oauth_token_url" class="mt-1 text-sm text-rose-400">
|
|
252
|
+
{{ form.errors.sso_oauth_token_url }}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
<div class="grid gap-5 md:grid-cols-2">
|
|
257
|
+
<div>
|
|
258
|
+
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
|
|
259
|
+
>Userinfo URL</label
|
|
260
|
+
>
|
|
261
|
+
<input
|
|
262
|
+
v-model="form.sso_oauth_userinfo_url"
|
|
263
|
+
type="url"
|
|
264
|
+
placeholder="https://idp.example.com/oauth/userinfo"
|
|
265
|
+
class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
266
|
+
/>
|
|
267
|
+
<div v-if="form.errors.sso_oauth_userinfo_url" class="mt-1 text-sm text-rose-400">
|
|
268
|
+
{{ form.errors.sso_oauth_userinfo_url }}
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
<div>
|
|
272
|
+
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
|
|
273
|
+
>Scopes</label
|
|
274
|
+
>
|
|
275
|
+
<input
|
|
276
|
+
v-model="form.sso_oauth_scopes"
|
|
277
|
+
type="text"
|
|
278
|
+
placeholder="openid profile email"
|
|
279
|
+
class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="grid gap-5 md:grid-cols-2">
|
|
284
|
+
<div>
|
|
285
|
+
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
|
|
286
|
+
>Client ID</label
|
|
287
|
+
>
|
|
288
|
+
<input
|
|
289
|
+
v-model="form.sso_oauth_client_id"
|
|
290
|
+
type="text"
|
|
291
|
+
placeholder="escalated-admin"
|
|
292
|
+
class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
293
|
+
/>
|
|
294
|
+
<div v-if="form.errors.sso_oauth_client_id" class="mt-1 text-sm text-rose-400">
|
|
295
|
+
{{ form.errors.sso_oauth_client_id }}
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
<div>
|
|
299
|
+
<label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]"
|
|
300
|
+
>Client Secret</label
|
|
301
|
+
>
|
|
302
|
+
<input
|
|
303
|
+
v-model="form.sso_oauth_client_secret"
|
|
304
|
+
type="password"
|
|
305
|
+
class="mt-2 w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
306
|
+
/>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
159
312
|
<!-- Attribute Mapping -->
|
|
160
313
|
<div
|
|
161
314
|
v-if="form.sso_provider !== 'none'"
|
|
@@ -12,6 +12,7 @@ defineProps({
|
|
|
12
12
|
const page = usePage();
|
|
13
13
|
const showSetup = ref(false);
|
|
14
14
|
const setupData = computed(() => page.props.flash?.two_factor_setup);
|
|
15
|
+
const confirmedData = computed(() => page.props.flash?.two_factor_confirmed);
|
|
15
16
|
|
|
16
17
|
function startSetup() {
|
|
17
18
|
router.post(
|
|
@@ -98,12 +99,37 @@ function disable() {
|
|
|
98
99
|
v-if="setupData && !enabled"
|
|
99
100
|
class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
|
|
100
101
|
>
|
|
101
|
-
<TwoFactorSetup
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
<TwoFactorSetup :qr-uri="setupData.qr_uri" @confirm="confirmCode" @cancel="showSetup = false" />
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div
|
|
106
|
+
v-if="confirmedData?.recovery_codes?.length"
|
|
107
|
+
class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
|
|
108
|
+
>
|
|
109
|
+
<div class="flex items-start justify-between gap-4">
|
|
110
|
+
<div>
|
|
111
|
+
<h3 class="text-sm font-semibold text-[var(--esc-panel-text)]">Recovery Codes</h3>
|
|
112
|
+
<p class="mt-2 text-sm text-[var(--esc-panel-text-tertiary)]">
|
|
113
|
+
Save these codes now. Each one can be used once if you lose access to your authenticator.
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
class="rounded-lg border border-[var(--esc-panel-border)] px-4 py-2 text-sm text-[var(--esc-panel-text-secondary)] hover:bg-[var(--esc-panel-surface-alt)]"
|
|
119
|
+
@click="navigator.clipboard?.writeText(confirmedData.recovery_codes.join('\n'))"
|
|
120
|
+
>
|
|
121
|
+
Copy Codes
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="mt-4 grid gap-2 md:grid-cols-2">
|
|
125
|
+
<code
|
|
126
|
+
v-for="code in confirmedData.recovery_codes"
|
|
127
|
+
:key="code"
|
|
128
|
+
class="rounded-lg bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-center text-sm font-mono text-[var(--esc-panel-text-secondary)]"
|
|
129
|
+
>
|
|
130
|
+
{{ code }}
|
|
131
|
+
</code>
|
|
132
|
+
</div>
|
|
107
133
|
</div>
|
|
108
134
|
</div>
|
|
109
135
|
</EscalatedLayout>
|