@escalated-dev/escalated 0.7.1 → 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.
Files changed (117) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +380 -357
  3. package/package.json +85 -81
  4. package/src/components/ActiveChatsPanel.vue +246 -246
  5. package/src/components/AdminDashboard.stories.js +1423 -1423
  6. package/src/components/AttachmentList.vue +65 -65
  7. package/src/components/ChatActionBar.vue +226 -226
  8. package/src/components/ChatBubble.vue +162 -162
  9. package/src/components/ChatComposer.vue +308 -308
  10. package/src/components/ChatQueue.vue +222 -222
  11. package/src/components/ChatThread.vue +197 -197
  12. package/src/components/CustomTicketActions.stories.js +71 -0
  13. package/src/components/CustomTicketActions.vue +101 -0
  14. package/src/components/EscalatedLayout.vue +619 -598
  15. package/src/components/FollowButton.vue +20 -1
  16. package/src/components/MentionDropdown.vue +154 -154
  17. package/src/components/MetricComparison.vue +89 -89
  18. package/src/components/PercentileChart.vue +67 -67
  19. package/src/components/PeriodSelector.vue +98 -98
  20. package/src/components/PresenceIndicator.vue +228 -213
  21. package/src/components/ReplyComposer.vue +336 -336
  22. package/src/components/ReplyThread.vue +153 -153
  23. package/src/components/ReportChart.vue +395 -395
  24. package/src/components/ReportExportButton.vue +108 -108
  25. package/src/components/SaveViewDialog.vue +172 -172
  26. package/src/components/SavedViewSidebar.vue +208 -208
  27. package/src/components/SkillTagManager.vue +29 -22
  28. package/src/components/SnoozeButton.vue +247 -247
  29. package/src/components/TicketFilters.vue +270 -270
  30. package/src/components/TicketList.vue +655 -655
  31. package/src/components/TicketSidebar.vue +4 -0
  32. package/src/components/TicketSplitDialog.vue +98 -98
  33. package/src/components/TicketSubjects.stories.js +63 -0
  34. package/src/components/TicketSubjects.vue +107 -0
  35. package/src/components/TwoFactorSetup.vue +1 -37
  36. package/src/components/VariableTokenInput.vue +111 -111
  37. package/src/components/WorkflowActionConfig.vue +287 -287
  38. package/src/components/WorkflowActionList.vue +350 -350
  39. package/src/components/WorkflowConditionBuilder.vue +265 -265
  40. package/src/components/WorkflowTriggerSelector.vue +125 -125
  41. package/src/components/admin/newsletters/AnalyticsTiles.stories.js +14 -0
  42. package/src/components/admin/newsletters/AnalyticsTiles.vue +55 -0
  43. package/src/components/admin/newsletters/DeliveriesTable.stories.js +28 -0
  44. package/src/components/admin/newsletters/DeliveriesTable.vue +104 -0
  45. package/src/components/admin/newsletters/DynamicFilterBuilder.stories.js +11 -0
  46. package/src/components/admin/newsletters/DynamicFilterBuilder.vue +53 -0
  47. package/src/components/admin/newsletters/ListMemberTable.stories.js +12 -0
  48. package/src/components/admin/newsletters/ListMemberTable.vue +53 -0
  49. package/src/components/admin/newsletters/MarkdownEditor.stories.js +6 -0
  50. package/src/components/admin/newsletters/MarkdownEditor.vue +53 -0
  51. package/src/components/admin/newsletters/MergeFieldDropdown.stories.js +6 -0
  52. package/src/components/admin/newsletters/MergeFieldDropdown.vue +84 -0
  53. package/src/components/admin/newsletters/PreviewIframe.stories.js +7 -0
  54. package/src/components/admin/newsletters/PreviewIframe.vue +40 -0
  55. package/src/components/admin/newsletters/SummaryTiles.stories.js +8 -0
  56. package/src/components/admin/newsletters/SummaryTiles.vue +63 -0
  57. package/src/composables/useChat.js +187 -187
  58. package/src/composables/useI18n.js +30 -8
  59. package/src/composables/useMentions.js +134 -134
  60. package/src/composables/usePermissions.js +31 -0
  61. package/src/composables/useRealtime.js +182 -182
  62. package/src/index.js +85 -84
  63. package/src/locales/README.md +13 -0
  64. package/src/locales/ar.json +727 -727
  65. package/src/locales/de.json +690 -690
  66. package/src/locales/en.json +889 -729
  67. package/src/locales/es.json +691 -691
  68. package/src/locales/fr.json +691 -691
  69. package/src/locales/index.js +16 -16
  70. package/src/locales/it.json +727 -727
  71. package/src/locales/ja.json +727 -727
  72. package/src/locales/ko.json +727 -727
  73. package/src/locales/nl.json +727 -727
  74. package/src/locales/pl.json +727 -727
  75. package/src/locales/pt-BR.json +727 -727
  76. package/src/locales/ru.json +727 -727
  77. package/src/locales/tr.json +727 -727
  78. package/src/locales/zh-CN.json +727 -727
  79. package/src/pages/Admin/Newsletters/Compose.vue +192 -0
  80. package/src/pages/Admin/Newsletters/Edit.vue +24 -0
  81. package/src/pages/Admin/Newsletters/Index.vue +141 -0
  82. package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
  83. package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
  84. package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
  85. package/src/pages/Admin/Newsletters/Settings.vue +85 -0
  86. package/src/pages/Admin/Newsletters/Show.vue +140 -0
  87. package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
  88. package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
  89. package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
  90. package/src/pages/Admin/Newsletters/usePreview.js +44 -0
  91. package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
  92. package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
  93. package/src/pages/Admin/Reports/Cohorts.vue +180 -180
  94. package/src/pages/Admin/Reports/Comparison.vue +141 -141
  95. package/src/pages/Admin/Reports/Dashboard.vue +226 -226
  96. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
  97. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
  98. package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
  99. package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
  100. package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
  101. package/src/pages/Admin/Settings.vue +1008 -1008
  102. package/src/pages/Admin/Skills/Form.vue +164 -4
  103. package/src/pages/Admin/Skills/Index.vue +14 -3
  104. package/src/pages/Admin/Tickets/Index.vue +126 -126
  105. package/src/pages/Admin/Tickets/Show.vue +254 -249
  106. package/src/pages/Admin/Users/Index.vue +145 -0
  107. package/src/pages/Admin/Workflows/Builder.vue +207 -207
  108. package/src/pages/Admin/Workflows/Index.vue +235 -235
  109. package/src/pages/Admin/Workflows/Logs.vue +286 -286
  110. package/src/pages/Agent/TicketIndex.vue +112 -112
  111. package/src/pages/Agent/TicketShow.vue +547 -494
  112. package/src/pages/Customer/Index.vue +26 -1
  113. package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
  114. package/src/plugin.js +175 -175
  115. package/src/utils/mentionHighlight.js +24 -24
  116. package/src/widget/EscalatedWidget.vue +804 -804
  117. 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
- &larr; 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 (&lt;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
+ &larr; 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 (&lt;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)]">SSO URL</label>
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.sso_url"
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.sso_url" class="mt-1 text-sm text-rose-400">
84
- {{ form.errors.sso_url }}
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
- :qr-uri="setupData.qr_uri"
103
- :recovery-codes="setupData.recovery_codes"
104
- @confirm="confirmCode"
105
- @cancel="showSetup = false"
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>