@escalated-dev/escalated 0.6.0 → 0.7.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 +125 -2
- package/package.json +40 -5
- package/src/components/ActiveChatsPanel.vue +246 -0
- package/src/components/ActivityTimeline.stories.js +55 -0
- package/src/components/ActivityTimeline.vue +83 -66
- package/src/components/AdminDashboard.stories.js +1423 -0
- package/src/components/AgentLoadIndicator.stories.js +37 -0
- package/src/components/AgentLoadIndicator.vue +49 -0
- package/src/components/AgentPerformanceCard.vue +44 -0
- package/src/components/ArticleEditor.vue +82 -0
- package/src/components/AssigneeSelect.vue +37 -23
- package/src/components/AttachmentList.vue +65 -37
- package/src/components/AuditLogEntry.vue +95 -0
- package/src/components/BulkActionBar.vue +166 -97
- package/src/components/CategoryTree.vue +89 -0
- package/src/components/ChartWidget.vue +46 -0
- package/src/components/ChatActionBar.vue +226 -0
- package/src/components/ChatBubble.vue +162 -0
- package/src/components/ChatComposer.vue +308 -0
- package/src/components/ChatQueue.vue +222 -0
- package/src/components/ChatThread.vue +197 -0
- package/src/components/CollisionWarning.stories.js +30 -0
- package/src/components/CollisionWarning.vue +68 -0
- package/src/components/ConditionalFieldRules.vue +135 -0
- package/src/components/ContextPanel.vue +66 -0
- package/src/components/ContextPanelSection.vue +65 -0
- package/src/components/CustomFieldRenderer.vue +159 -0
- package/src/components/EscalatedLayout.vue +424 -70
- package/src/components/FileDropzone.vue +57 -41
- package/src/components/FollowButton.stories.js +30 -0
- package/src/components/FollowButton.vue +88 -63
- package/src/components/KeyboardShortcutHelp.vue +213 -123
- package/src/components/KnowledgePanel.vue +122 -0
- package/src/components/KpiCard.stories.js +32 -0
- package/src/components/KpiCard.vue +88 -0
- package/src/components/MacroDropdown.vue +150 -90
- package/src/components/MentionDropdown.vue +154 -0
- package/src/components/MetricComparison.vue +89 -0
- package/src/components/PercentileChart.vue +67 -0
- package/src/components/PeriodSelector.vue +98 -0
- package/src/components/PermissionMatrix.vue +107 -0
- package/src/components/PinnedNotes.vue +129 -90
- package/src/components/PluginSlot.vue +68 -62
- package/src/components/PresenceIndicator.vue +138 -28
- package/src/components/PriorityBadge.stories.js +33 -0
- package/src/components/PriorityBadge.vue +46 -39
- package/src/components/QuickFilters.vue +65 -49
- package/src/components/ReplyComposer.vue +207 -30
- package/src/components/ReplyThread.vue +111 -21
- package/src/components/ReportChart.vue +395 -0
- package/src/components/ReportExportButton.vue +108 -0
- package/src/components/SatisfactionRating.vue +180 -128
- package/src/components/SaveViewDialog.vue +172 -0
- package/src/components/SavedViewSidebar.vue +208 -0
- package/src/components/ScheduleEditor.vue +104 -0
- package/src/components/SideConversation.vue +165 -0
- package/src/components/SideConversationList.vue +147 -0
- package/src/components/SkillTagManager.vue +79 -0
- package/src/components/SlaComplianceChart.vue +46 -0
- package/src/components/SlaTimer.stories.js +35 -0
- package/src/components/SlaTimer.vue +62 -49
- package/src/components/SnoozeButton.vue +247 -0
- package/src/components/StatsCard.stories.js +24 -0
- package/src/components/StatsCard.vue +18 -6
- package/src/components/StatusBadge.stories.js +53 -0
- package/src/components/StatusBadge.vue +78 -46
- package/src/components/TagSelect.vue +84 -53
- package/src/components/TicketFilters.vue +247 -46
- package/src/components/TicketLinkPanel.vue +193 -0
- package/src/components/TicketList.vue +536 -74
- package/src/components/TicketMergeDialog.vue +157 -0
- package/src/components/TicketSidebar.vue +167 -99
- package/src/components/TicketSplitDialog.vue +98 -0
- package/src/components/TicketTypeSelector.vue +21 -0
- package/src/components/TotpInput.stories.js +9 -0
- package/src/components/TotpInput.vue +81 -0
- package/src/components/TwoFactorSetup.vue +113 -0
- package/src/components/VariableTokenInput.vue +111 -0
- package/src/components/WorkflowActionConfig.vue +287 -0
- package/src/components/WorkflowActionList.vue +350 -0
- package/src/components/WorkflowConditionBuilder.vue +265 -0
- package/src/components/WorkflowTriggerSelector.vue +125 -0
- package/src/composables/useChat.js +187 -0
- package/src/composables/useDebouncedSearch.js +38 -0
- package/src/composables/useI18n.js +80 -80
- package/src/composables/useKeyboardShortcuts.js +46 -46
- package/src/composables/useMentions.js +134 -0
- package/src/composables/usePluginExtensions.js +159 -67
- package/src/composables/usePluginHooks.js +65 -0
- package/src/composables/useRealtime.js +182 -0
- package/src/defineEscalatedPlugin.js +76 -50
- package/src/index.js +77 -28
- package/src/locales/ar.json +727 -0
- package/src/locales/de.json +678 -430
- package/src/locales/en.json +717 -430
- package/src/locales/es.json +679 -430
- package/src/locales/fr.json +680 -431
- package/src/locales/index.js +12 -2
- package/src/locales/it.json +727 -0
- package/src/locales/ja.json +727 -0
- package/src/locales/ko.json +727 -0
- package/src/locales/nl.json +727 -0
- package/src/locales/pl.json +727 -0
- package/src/locales/pt-BR.json +727 -0
- package/src/locales/ru.json +727 -0
- package/src/locales/tr.json +727 -0
- package/src/locales/zh-CN.json +727 -0
- package/src/pages/Admin/ApiTokens/Index.vue +381 -217
- package/src/pages/Admin/AuditLog/Index.vue +154 -0
- package/src/pages/Admin/BusinessHours/Form.vue +172 -0
- package/src/pages/Admin/BusinessHours/Index.vue +113 -0
- package/src/pages/Admin/CannedResponses/Index.vue +82 -25
- package/src/pages/Admin/Capacity/Index.vue +134 -0
- package/src/pages/Admin/CustomFields/Form.vue +195 -0
- package/src/pages/Admin/CustomFields/Index.vue +134 -0
- package/src/pages/Admin/CustomObjects/Form.vue +202 -0
- package/src/pages/Admin/CustomObjects/Index.vue +127 -0
- package/src/pages/Admin/CustomObjects/Records.vue +228 -0
- package/src/pages/Admin/Departments/Form.vue +35 -11
- package/src/pages/Admin/Departments/Index.vue +74 -21
- package/src/pages/Admin/EscalationRules/Form.vue +86 -25
- package/src/pages/Admin/EscalationRules/Index.vue +158 -37
- package/src/pages/Admin/Import/Connect.vue +95 -0
- package/src/pages/Admin/Import/Index.vue +102 -0
- package/src/pages/Admin/Import/Mapping.vue +110 -0
- package/src/pages/Admin/Import/Progress.vue +153 -0
- package/src/pages/Admin/Import/Review.vue +76 -0
- package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
- package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
- package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
- package/src/pages/Admin/Macros/Index.vue +448 -287
- package/src/pages/Admin/Plugins/Index.vue +327 -197
- package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
- package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
- package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
- package/src/pages/Admin/Reports/Cohorts.vue +180 -0
- package/src/pages/Admin/Reports/Comparison.vue +141 -0
- package/src/pages/Admin/Reports/CsatReport.vue +127 -0
- package/src/pages/Admin/Reports/Dashboard.vue +226 -0
- package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
- package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
- package/src/pages/Admin/Reports/SlaReport.vue +162 -0
- package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
- package/src/pages/Admin/Reports.vue +206 -101
- package/src/pages/Admin/Roles/Form.vue +71 -0
- package/src/pages/Admin/Roles/Index.vue +122 -0
- package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
- package/src/pages/Admin/Settings/DataRetention.vue +182 -0
- package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
- package/src/pages/Admin/Settings/Sandbox.vue +135 -0
- package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
- package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
- package/src/pages/Admin/Settings.vue +847 -99
- package/src/pages/Admin/Skills/Form.vue +53 -0
- package/src/pages/Admin/Skills/Index.vue +94 -0
- package/src/pages/Admin/SlaPolicies/Form.vue +62 -19
- package/src/pages/Admin/SlaPolicies/Index.vue +85 -23
- package/src/pages/Admin/Statuses/Form.vue +114 -0
- package/src/pages/Admin/Statuses/Index.vue +153 -0
- package/src/pages/Admin/Tags/Index.vue +121 -35
- package/src/pages/Admin/Tickets/Index.vue +96 -9
- package/src/pages/Admin/Tickets/Show.vue +130 -37
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
- package/src/pages/Admin/Webhooks/Form.vue +110 -0
- package/src/pages/Admin/Webhooks/Index.vue +136 -0
- package/src/pages/Admin/Workflows/Builder.vue +207 -0
- package/src/pages/Admin/Workflows/Index.vue +235 -0
- package/src/pages/Admin/Workflows/Logs.vue +286 -0
- package/src/pages/Agent/Dashboard.vue +311 -186
- package/src/pages/Agent/TicketIndex.vue +80 -6
- package/src/pages/Agent/TicketShow.vue +399 -60
- package/src/pages/Customer/Create.vue +37 -10
- package/src/pages/Customer/Index.vue +9 -2
- package/src/pages/Customer/KnowledgeBase/Article.vue +129 -0
- package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
- package/src/pages/Customer/Show.vue +69 -63
- package/src/pages/Guest/Create.vue +40 -15
- package/src/pages/Guest/Show.vue +124 -93
- package/src/plugin.js +111 -1
- package/src/utils/constants.js +30 -0
- package/src/utils/formatting.js +35 -0
- package/src/utils/mentionHighlight.js +24 -0
- package/src/utils/sanitizeHtml.js +78 -64
- package/src/widget/EscalatedWidget.vue +804 -0
- package/src/widget/index.js +254 -0
- package/src/pages/Admin/Plugins.vue +0 -129
|
@@ -1,62 +1,68 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { defineAsyncComponent, h } from 'vue';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
slot: {
|
|
6
|
-
type: String,
|
|
7
|
-
required: true,
|
|
8
|
-
},
|
|
9
|
-
components: {
|
|
10
|
-
type: Array,
|
|
11
|
-
default: () => [],
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
// Error fallback component rendered when a plugin component fails to load
|
|
16
|
-
const ErrorFallback = {
|
|
17
|
-
props: ['error', 'plugin', 'component'],
|
|
18
|
-
render() {
|
|
19
|
-
return h(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
1
|
+
<script setup>
|
|
2
|
+
import { defineAsyncComponent, h } from 'vue';
|
|
3
|
+
|
|
4
|
+
defineProps({
|
|
5
|
+
slot: {
|
|
6
|
+
type: String,
|
|
7
|
+
required: true,
|
|
8
|
+
},
|
|
9
|
+
components: {
|
|
10
|
+
type: Array,
|
|
11
|
+
default: () => [],
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Error fallback component rendered when a plugin component fails to load
|
|
16
|
+
const ErrorFallback = {
|
|
17
|
+
props: ['error', 'plugin', 'component'],
|
|
18
|
+
render() {
|
|
19
|
+
return h(
|
|
20
|
+
'div',
|
|
21
|
+
{
|
|
22
|
+
class: 'rounded-lg border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-xs text-rose-400',
|
|
23
|
+
},
|
|
24
|
+
`Plugin "${this.plugin}" failed to load component "${this.component}"`,
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Loading placeholder component
|
|
30
|
+
const LoadingFallback = {
|
|
31
|
+
render() {
|
|
32
|
+
return h(
|
|
33
|
+
'div',
|
|
34
|
+
{
|
|
35
|
+
class: 'animate-pulse rounded-lg bg-white/[0.03] px-3 py-2',
|
|
36
|
+
},
|
|
37
|
+
[h('div', { class: 'h-4 w-24 rounded bg-white/[0.06]' })],
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Load plugin components dynamically from host app's plugins directory
|
|
43
|
+
const loadPluginComponent = (plugin, component) => {
|
|
44
|
+
return defineAsyncComponent({
|
|
45
|
+
loader: () => import(`../../../../plugins/escalated/${plugin}/resources/js/Components/${component}.vue`),
|
|
46
|
+
loadingComponent: LoadingFallback,
|
|
47
|
+
errorComponent: {
|
|
48
|
+
props: ['error'],
|
|
49
|
+
render() {
|
|
50
|
+
return h(ErrorFallback, { error: this.error, plugin, component });
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
delay: 100,
|
|
54
|
+
timeout: 10000,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<template>
|
|
60
|
+
<template v-if="components?.length > 0">
|
|
61
|
+
<component
|
|
62
|
+
:is="loadPluginComponent(pluginComp.plugin, pluginComp.component)"
|
|
63
|
+
v-for="(pluginComp, index) in components"
|
|
64
|
+
:key="`${slot}-${pluginComp.plugin}-${pluginComp.component}-${index}`"
|
|
65
|
+
v-bind="pluginComp.data || {}"
|
|
66
|
+
/>
|
|
67
|
+
</template>
|
|
68
|
+
</template>
|
|
@@ -3,20 +3,30 @@ import { ref, computed, inject, onMounted, onUnmounted } from 'vue';
|
|
|
3
3
|
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
ticketReference: { type: String, required: true },
|
|
6
|
+
ticketId: { type: [Number, String], default: null },
|
|
6
7
|
routePrefix: { type: String, required: true },
|
|
8
|
+
pollInterval: { type: Number, default: 30000 },
|
|
9
|
+
showLabel: { type: Boolean, default: false },
|
|
7
10
|
});
|
|
8
11
|
|
|
9
|
-
const
|
|
12
|
+
const effectiveInterval = computed(() => Math.max(props.pollInterval, 5000));
|
|
13
|
+
const echoAvailable = typeof window !== 'undefined' && !!window.Echo;
|
|
14
|
+
|
|
15
|
+
const escDark = inject(
|
|
16
|
+
'esc-dark',
|
|
17
|
+
computed(() => false),
|
|
18
|
+
);
|
|
10
19
|
const viewers = ref([]);
|
|
11
20
|
const hovering = ref(false);
|
|
12
21
|
let intervalId = null;
|
|
22
|
+
let presenceChannel = null;
|
|
13
23
|
|
|
14
24
|
async function fetchPresence() {
|
|
15
25
|
try {
|
|
16
26
|
const response = await fetch(route(`${props.routePrefix}.tickets.presence`, props.ticketReference), {
|
|
17
27
|
method: 'POST',
|
|
18
28
|
headers: {
|
|
19
|
-
|
|
29
|
+
Accept: 'application/json',
|
|
20
30
|
'Content-Type': 'application/json',
|
|
21
31
|
'X-Requested-With': 'XMLHttpRequest',
|
|
22
32
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
|
@@ -31,9 +41,38 @@ async function fetchPresence() {
|
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Use Echo presence channel for real-time viewer tracking when available.
|
|
46
|
+
* Falls back to polling when Echo is not configured.
|
|
47
|
+
*/
|
|
48
|
+
function initPresenceChannel() {
|
|
49
|
+
if (!echoAvailable || !props.ticketId) return false;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
presenceChannel = window.Echo.join(`escalated.tickets.${props.ticketId}`);
|
|
53
|
+
|
|
54
|
+
presenceChannel
|
|
55
|
+
.here((users) => {
|
|
56
|
+
viewers.value = users;
|
|
57
|
+
})
|
|
58
|
+
.joining((user) => {
|
|
59
|
+
if (!viewers.value.find((v) => v.id === user.id)) {
|
|
60
|
+
viewers.value = [...viewers.value, user];
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
.leaving((user) => {
|
|
64
|
+
viewers.value = viewers.value.filter((v) => v.id !== user.id);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
34
73
|
const tooltipText = computed(() => {
|
|
35
74
|
if (!viewers.value.length) return '';
|
|
36
|
-
const names = viewers.value.map(v => v.name);
|
|
75
|
+
const names = viewers.value.map((v) => v.name);
|
|
37
76
|
return 'Also viewing: ' + names.join(', ');
|
|
38
77
|
});
|
|
39
78
|
|
|
@@ -46,8 +85,14 @@ function getInitial(viewer) {
|
|
|
46
85
|
}
|
|
47
86
|
|
|
48
87
|
const colors = [
|
|
49
|
-
'bg-cyan-500',
|
|
50
|
-
'bg-
|
|
88
|
+
'bg-cyan-500',
|
|
89
|
+
'bg-violet-500',
|
|
90
|
+
'bg-amber-500',
|
|
91
|
+
'bg-emerald-500',
|
|
92
|
+
'bg-rose-500',
|
|
93
|
+
'bg-blue-500',
|
|
94
|
+
'bg-pink-500',
|
|
95
|
+
'bg-teal-500',
|
|
51
96
|
];
|
|
52
97
|
|
|
53
98
|
function getColor(index) {
|
|
@@ -55,8 +100,12 @@ function getColor(index) {
|
|
|
55
100
|
}
|
|
56
101
|
|
|
57
102
|
onMounted(() => {
|
|
58
|
-
|
|
59
|
-
|
|
103
|
+
// Prefer presence channels via Echo; fall back to polling
|
|
104
|
+
const usingEcho = initPresenceChannel();
|
|
105
|
+
if (!usingEcho) {
|
|
106
|
+
fetchPresence();
|
|
107
|
+
intervalId = setInterval(fetchPresence, effectiveInterval.value);
|
|
108
|
+
}
|
|
60
109
|
});
|
|
61
110
|
|
|
62
111
|
onUnmounted(() => {
|
|
@@ -64,39 +113,100 @@ onUnmounted(() => {
|
|
|
64
113
|
clearInterval(intervalId);
|
|
65
114
|
intervalId = null;
|
|
66
115
|
}
|
|
116
|
+
if (presenceChannel && props.ticketId) {
|
|
117
|
+
try {
|
|
118
|
+
window.Echo.leave(`escalated.tickets.${props.ticketId}`);
|
|
119
|
+
} catch {
|
|
120
|
+
// ignore
|
|
121
|
+
}
|
|
122
|
+
presenceChannel = null;
|
|
123
|
+
}
|
|
67
124
|
});
|
|
68
125
|
</script>
|
|
69
126
|
|
|
70
127
|
<template>
|
|
71
|
-
<div
|
|
128
|
+
<div
|
|
129
|
+
v-if="viewers.length > 0"
|
|
130
|
+
class="relative flex items-center"
|
|
131
|
+
@mouseenter="hovering = true"
|
|
132
|
+
@mouseleave="hovering = false"
|
|
133
|
+
>
|
|
72
134
|
<!-- Avatar stack -->
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
135
|
+
<TransitionGroup
|
|
136
|
+
tag="div"
|
|
137
|
+
class="flex -space-x-2"
|
|
138
|
+
enter-active-class="transition-all duration-300 ease-out"
|
|
139
|
+
enter-from-class="opacity-0 scale-75 -ml-7"
|
|
140
|
+
enter-to-class="opacity-100 scale-100 ml-0"
|
|
141
|
+
leave-active-class="transition-all duration-200 ease-in"
|
|
142
|
+
leave-from-class="opacity-100 scale-100"
|
|
143
|
+
leave-to-class="opacity-0 scale-75 -ml-7"
|
|
144
|
+
>
|
|
145
|
+
<div
|
|
146
|
+
v-for="(viewer, index) in visibleViewers"
|
|
147
|
+
:key="viewer.id || viewer.name"
|
|
148
|
+
:aria-label="viewer.name"
|
|
149
|
+
:class="[
|
|
150
|
+
'relative flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold text-white ring-2',
|
|
151
|
+
getColor(index),
|
|
152
|
+
escDark ? 'ring-neutral-900' : 'ring-white',
|
|
153
|
+
]"
|
|
154
|
+
>
|
|
78
155
|
{{ getInitial(viewer) }}
|
|
156
|
+
<!-- Pulsing live indicator on first avatar -->
|
|
157
|
+
<span v-if="index === 0" class="absolute -right-0.5 -top-0.5 flex h-2.5 w-2.5">
|
|
158
|
+
<span
|
|
159
|
+
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75"
|
|
160
|
+
></span>
|
|
161
|
+
<span class="relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500"></span>
|
|
162
|
+
</span>
|
|
79
163
|
</div>
|
|
80
|
-
<div
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
164
|
+
<div
|
|
165
|
+
v-if="overflowCount > 0"
|
|
166
|
+
:key="'overflow'"
|
|
167
|
+
:class="[
|
|
168
|
+
'flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold ring-2',
|
|
169
|
+
escDark
|
|
170
|
+
? 'bg-white/[0.08] text-neutral-300 ring-neutral-900'
|
|
171
|
+
: 'bg-gray-200 text-gray-600 ring-white',
|
|
172
|
+
]"
|
|
173
|
+
>
|
|
85
174
|
+{{ overflowCount }}
|
|
86
175
|
</div>
|
|
87
|
-
</
|
|
176
|
+
</TransitionGroup>
|
|
177
|
+
|
|
178
|
+
<!-- Viewer count label -->
|
|
179
|
+
<span
|
|
180
|
+
v-if="showLabel"
|
|
181
|
+
aria-live="polite"
|
|
182
|
+
:class="['ml-2 text-xs font-medium', escDark ? 'text-neutral-400' : 'text-gray-500']"
|
|
183
|
+
>
|
|
184
|
+
{{ viewers.length }} viewing
|
|
185
|
+
</span>
|
|
88
186
|
|
|
89
187
|
<!-- Tooltip -->
|
|
90
|
-
<Transition
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
188
|
+
<Transition
|
|
189
|
+
enter-active-class="transition duration-100 ease-out"
|
|
190
|
+
enter-from-class="opacity-0 translate-y-1"
|
|
191
|
+
enter-to-class="opacity-100 translate-y-0"
|
|
192
|
+
leave-active-class="transition duration-75 ease-in"
|
|
193
|
+
leave-from-class="opacity-100 translate-y-0"
|
|
194
|
+
leave-to-class="opacity-0 translate-y-1"
|
|
195
|
+
>
|
|
196
|
+
<div
|
|
197
|
+
v-if="hovering"
|
|
198
|
+
:class="[
|
|
199
|
+
'absolute left-1/2 top-full z-50 mt-2 -translate-x-1/2 whitespace-nowrap rounded-lg px-3 py-1.5 text-xs font-medium shadow-lg',
|
|
200
|
+
escDark ? 'border border-white/[0.06] bg-neutral-800 text-neutral-200' : 'bg-gray-900 text-white',
|
|
201
|
+
]"
|
|
202
|
+
>
|
|
97
203
|
{{ tooltipText }}
|
|
98
|
-
<div
|
|
99
|
-
|
|
204
|
+
<div
|
|
205
|
+
:class="[
|
|
206
|
+
'absolute -top-1 left-1/2 h-2 w-2 -translate-x-1/2 rotate-45',
|
|
207
|
+
escDark ? 'bg-neutral-800' : 'bg-gray-900',
|
|
208
|
+
]"
|
|
209
|
+
></div>
|
|
100
210
|
</div>
|
|
101
211
|
</Transition>
|
|
102
212
|
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import PriorityBadge from './PriorityBadge.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Components/PriorityBadge',
|
|
5
|
+
component: PriorityBadge,
|
|
6
|
+
argTypes: {
|
|
7
|
+
priority: {
|
|
8
|
+
control: 'select',
|
|
9
|
+
options: ['low', 'medium', 'high', 'urgent', 'critical'],
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const Low = { args: { priority: 'low' } };
|
|
15
|
+
export const Medium = { args: { priority: 'medium' } };
|
|
16
|
+
export const High = { args: { priority: 'high' } };
|
|
17
|
+
export const Urgent = { args: { priority: 'urgent' } };
|
|
18
|
+
export const Critical = { args: { priority: 'critical' } };
|
|
19
|
+
|
|
20
|
+
export const AllPriorities = {
|
|
21
|
+
render: () => ({
|
|
22
|
+
components: { PriorityBadge },
|
|
23
|
+
template: `
|
|
24
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
25
|
+
<PriorityBadge priority="low" />
|
|
26
|
+
<PriorityBadge priority="medium" />
|
|
27
|
+
<PriorityBadge priority="high" />
|
|
28
|
+
<PriorityBadge priority="urgent" />
|
|
29
|
+
<PriorityBadge priority="critical" />
|
|
30
|
+
</div>
|
|
31
|
+
`,
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
@@ -1,39 +1,46 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { inject, computed } from 'vue';
|
|
3
|
-
|
|
4
|
-
const props = defineProps({
|
|
5
|
-
priority: { type: String, required: true },
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
const escDark = inject(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
1
|
+
<script setup>
|
|
2
|
+
import { inject, computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
priority: { type: String, required: true },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const escDark = inject(
|
|
9
|
+
'esc-dark',
|
|
10
|
+
computed(() => false),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const darkConfig = {
|
|
14
|
+
low: { label: 'Low', color: 'bg-gray-500/10 text-gray-400 ring-1 ring-gray-500/20' },
|
|
15
|
+
medium: { label: 'Medium', color: 'bg-cyan-500/10 text-white ring-1 ring-cyan-500/20' },
|
|
16
|
+
high: { label: 'High', color: 'bg-amber-500/10 text-amber-400 ring-1 ring-amber-500/20' },
|
|
17
|
+
urgent: { label: 'Urgent', color: 'bg-orange-500/10 text-orange-400 ring-1 ring-orange-500/20' },
|
|
18
|
+
critical: { label: 'Critical', color: 'bg-rose-500/10 text-rose-300 ring-1 ring-rose-500/20' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const lightConfig = {
|
|
22
|
+
low: { label: 'Low', color: 'bg-gray-100 text-gray-800' },
|
|
23
|
+
medium: { label: 'Medium', color: 'bg-blue-100 text-blue-800' },
|
|
24
|
+
high: { label: 'High', color: 'bg-yellow-100 text-yellow-800' },
|
|
25
|
+
urgent: { label: 'Urgent', color: 'bg-orange-100 text-orange-800' },
|
|
26
|
+
critical: { label: 'Critical', color: 'bg-red-100 text-red-800' },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const config = computed(() => {
|
|
30
|
+
const map = escDark.value ? darkConfig : lightConfig;
|
|
31
|
+
const fallback = escDark.value
|
|
32
|
+
? { label: props.priority, color: 'bg-gray-500/10 text-gray-400 ring-1 ring-gray-500/20' }
|
|
33
|
+
: { label: props.priority, color: 'bg-gray-100 text-gray-800' };
|
|
34
|
+
return map[props.priority] || fallback;
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<span
|
|
40
|
+
role="status"
|
|
41
|
+
:aria-label="'Priority: ' + config.label"
|
|
42
|
+
:class="['inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium', config.color]"
|
|
43
|
+
>
|
|
44
|
+
{{ config.label }}
|
|
45
|
+
</span>
|
|
46
|
+
</template>
|
|
@@ -1,49 +1,65 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { ref, computed, inject } from 'vue';
|
|
3
|
-
|
|
4
|
-
const props = defineProps({
|
|
5
|
-
currentUserId: { type: [Number, String], default: null },
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
const emit = defineEmits(['filter']);
|
|
9
|
-
const escDark = inject(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{ key:
|
|
18
|
-
{ key: '
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, inject } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
currentUserId: { type: [Number, String], default: null },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const emit = defineEmits(['filter']);
|
|
9
|
+
const escDark = inject(
|
|
10
|
+
'esc-dark',
|
|
11
|
+
computed(() => false),
|
|
12
|
+
);
|
|
13
|
+
const activeTab = ref(null);
|
|
14
|
+
|
|
15
|
+
const tabs = computed(() => {
|
|
16
|
+
const list = [
|
|
17
|
+
{ key: null, label: 'All Tickets', filter: {} },
|
|
18
|
+
{ key: 'assigned_to_me', label: 'Assigned to Me', filter: { assigned_to: props.currentUserId } },
|
|
19
|
+
{ key: 'unassigned', label: 'Unassigned', filter: { unassigned: true } },
|
|
20
|
+
{ key: 'urgent', label: 'Urgent', filter: { priority: 'urgent' } },
|
|
21
|
+
{ key: 'sla_breaching', label: 'SLA Breaching', filter: { sla_breached: true } },
|
|
22
|
+
{ key: 'following', label: 'Following', filter: { following: true } },
|
|
23
|
+
];
|
|
24
|
+
if (props.currentUserId) return list;
|
|
25
|
+
return list.filter((t) => t.key !== 'assigned_to_me');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function selectTab(tab) {
|
|
29
|
+
activeTab.value = tab.key;
|
|
30
|
+
emit('filter', tab.filter);
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<div
|
|
36
|
+
:class="['flex items-center gap-0 border-b', escDark ? 'border-white/[0.08]' : 'border-gray-200']"
|
|
37
|
+
role="tablist"
|
|
38
|
+
aria-label="Ticket view tabs"
|
|
39
|
+
>
|
|
40
|
+
<button
|
|
41
|
+
v-for="tab in tabs"
|
|
42
|
+
:key="tab.key"
|
|
43
|
+
role="tab"
|
|
44
|
+
:aria-selected="activeTab === tab.key"
|
|
45
|
+
:class="[
|
|
46
|
+
'relative px-4 py-2.5 text-sm font-medium transition-colors whitespace-nowrap',
|
|
47
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-0',
|
|
48
|
+
activeTab === tab.key
|
|
49
|
+
? escDark
|
|
50
|
+
? 'text-cyan-400 focus-visible:ring-cyan-500/40'
|
|
51
|
+
: 'text-blue-600 focus-visible:ring-blue-500/40'
|
|
52
|
+
: escDark
|
|
53
|
+
? 'text-neutral-500 hover:text-neutral-300 focus-visible:ring-cyan-500/40'
|
|
54
|
+
: 'text-gray-500 hover:text-gray-700 focus-visible:ring-blue-500/40',
|
|
55
|
+
]"
|
|
56
|
+
@click="selectTab(tab)"
|
|
57
|
+
>
|
|
58
|
+
{{ tab.label }}
|
|
59
|
+
<span
|
|
60
|
+
v-if="activeTab === tab.key"
|
|
61
|
+
:class="['absolute bottom-0 left-0 right-0 h-0.5 rounded-t', escDark ? 'bg-cyan-400' : 'bg-blue-600']"
|
|
62
|
+
></span>
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|