@escalated-dev/escalated 0.5.0 → 0.7.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 (136) hide show
  1. package/README.md +102 -0
  2. package/package.json +37 -6
  3. package/src/components/ActivityTimeline.stories.js +55 -0
  4. package/src/components/ActivityTimeline.vue +83 -66
  5. package/src/components/AdminDashboard.stories.js +1095 -0
  6. package/src/components/AgentLoadIndicator.stories.js +37 -0
  7. package/src/components/AgentLoadIndicator.vue +49 -0
  8. package/src/components/AgentPerformanceCard.vue +44 -0
  9. package/src/components/ArticleEditor.vue +82 -0
  10. package/src/components/AssigneeSelect.vue +37 -23
  11. package/src/components/AttachmentList.vue +53 -37
  12. package/src/components/AuditLogEntry.vue +95 -0
  13. package/src/components/BulkActionBar.vue +99 -30
  14. package/src/components/CategoryTree.vue +89 -0
  15. package/src/components/ChartWidget.vue +46 -0
  16. package/src/components/CollisionWarning.stories.js +30 -0
  17. package/src/components/CollisionWarning.vue +68 -0
  18. package/src/components/ConditionalFieldRules.vue +135 -0
  19. package/src/components/ContextPanel.vue +66 -0
  20. package/src/components/ContextPanelSection.vue +65 -0
  21. package/src/components/CustomFieldRenderer.vue +159 -0
  22. package/src/components/EscalatedLayout.vue +400 -69
  23. package/src/components/FileDropzone.vue +57 -41
  24. package/src/components/FollowButton.stories.js +30 -0
  25. package/src/components/FollowButton.vue +53 -28
  26. package/src/components/KeyboardShortcutHelp.vue +129 -39
  27. package/src/components/KnowledgePanel.vue +122 -0
  28. package/src/components/KpiCard.stories.js +32 -0
  29. package/src/components/KpiCard.vue +88 -0
  30. package/src/components/MacroDropdown.vue +95 -35
  31. package/src/components/PermissionMatrix.vue +107 -0
  32. package/src/components/PinnedNotes.vue +75 -33
  33. package/src/components/PresenceIndicator.vue +93 -27
  34. package/src/components/PriorityBadge.stories.js +33 -0
  35. package/src/components/PriorityBadge.vue +46 -39
  36. package/src/components/QuickFilters.vue +65 -49
  37. package/src/components/ReplyComposer.vue +273 -159
  38. package/src/components/ReplyThread.vue +78 -23
  39. package/src/components/SatisfactionRating.vue +98 -46
  40. package/src/components/ScheduleEditor.vue +104 -0
  41. package/src/components/SideConversation.vue +165 -0
  42. package/src/components/SideConversationList.vue +147 -0
  43. package/src/components/SkillTagManager.vue +79 -0
  44. package/src/components/SlaComplianceChart.vue +53 -0
  45. package/src/components/SlaTimer.stories.js +35 -0
  46. package/src/components/SlaTimer.vue +62 -49
  47. package/src/components/StatsCard.stories.js +24 -0
  48. package/src/components/StatsCard.vue +58 -46
  49. package/src/components/StatusBadge.stories.js +53 -0
  50. package/src/components/StatusBadge.vue +78 -46
  51. package/src/components/TagSelect.vue +84 -53
  52. package/src/components/TicketFilters.vue +225 -69
  53. package/src/components/TicketLinkPanel.vue +193 -0
  54. package/src/components/TicketList.vue +594 -193
  55. package/src/components/TicketMergeDialog.vue +157 -0
  56. package/src/components/TicketSidebar.vue +167 -99
  57. package/src/components/TicketTypeSelector.vue +21 -0
  58. package/src/components/TotpInput.stories.js +9 -0
  59. package/src/components/TotpInput.vue +81 -0
  60. package/src/components/TwoFactorSetup.vue +113 -0
  61. package/src/composables/useDebouncedSearch.js +38 -0
  62. package/src/composables/useI18n.js +80 -0
  63. package/src/composables/usePluginExtensions.js +95 -3
  64. package/src/composables/usePluginHooks.js +65 -0
  65. package/src/defineEscalatedPlugin.js +34 -8
  66. package/src/index.js +72 -35
  67. package/src/locales/de.json +442 -0
  68. package/src/locales/en.json +467 -0
  69. package/src/locales/es.json +442 -0
  70. package/src/locales/fr.json +442 -0
  71. package/src/locales/index.js +6 -0
  72. package/src/pages/Admin/ApiTokens/Index.vue +231 -67
  73. package/src/pages/Admin/AuditLog/Index.vue +154 -0
  74. package/src/pages/Admin/Automations/Form.vue +184 -0
  75. package/src/pages/Admin/Automations/Index.vue +129 -0
  76. package/src/pages/Admin/BusinessHours/Form.vue +172 -0
  77. package/src/pages/Admin/BusinessHours/Index.vue +113 -0
  78. package/src/pages/Admin/CannedResponses/Index.vue +127 -70
  79. package/src/pages/Admin/Capacity/Index.vue +134 -0
  80. package/src/pages/Admin/CustomFields/Form.vue +195 -0
  81. package/src/pages/Admin/CustomFields/Index.vue +134 -0
  82. package/src/pages/Admin/CustomObjects/Form.vue +202 -0
  83. package/src/pages/Admin/CustomObjects/Index.vue +127 -0
  84. package/src/pages/Admin/CustomObjects/Records.vue +228 -0
  85. package/src/pages/Admin/Departments/Form.vue +75 -51
  86. package/src/pages/Admin/Departments/Index.vue +109 -57
  87. package/src/pages/Admin/EscalationRules/Form.vue +157 -96
  88. package/src/pages/Admin/EscalationRules/Index.vue +178 -57
  89. package/src/pages/Admin/Import/Connect.vue +96 -0
  90. package/src/pages/Admin/Import/Index.vue +102 -0
  91. package/src/pages/Admin/Import/Mapping.vue +110 -0
  92. package/src/pages/Admin/Import/Progress.vue +153 -0
  93. package/src/pages/Admin/Import/Review.vue +76 -0
  94. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
  95. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
  96. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
  97. package/src/pages/Admin/Macros/Index.vue +250 -89
  98. package/src/pages/Admin/Plugins/Index.vue +205 -75
  99. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
  100. package/src/pages/Admin/Reports/CsatReport.vue +127 -0
  101. package/src/pages/Admin/Reports/Dashboard.vue +211 -0
  102. package/src/pages/Admin/Reports/SlaReport.vue +166 -0
  103. package/src/pages/Admin/Reports.vue +139 -34
  104. package/src/pages/Admin/Roles/Form.vue +71 -0
  105. package/src/pages/Admin/Roles/Index.vue +122 -0
  106. package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
  107. package/src/pages/Admin/Settings/DataRetention.vue +182 -0
  108. package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
  109. package/src/pages/Admin/Settings/Sandbox.vue +135 -0
  110. package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
  111. package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
  112. package/src/pages/Admin/Settings.vue +468 -260
  113. package/src/pages/Admin/Skills/Form.vue +53 -0
  114. package/src/pages/Admin/Skills/Index.vue +94 -0
  115. package/src/pages/Admin/SlaPolicies/Form.vue +118 -75
  116. package/src/pages/Admin/SlaPolicies/Index.vue +120 -59
  117. package/src/pages/Admin/Statuses/Form.vue +114 -0
  118. package/src/pages/Admin/Statuses/Index.vue +153 -0
  119. package/src/pages/Admin/Tags/Index.vue +191 -105
  120. package/src/pages/Admin/Tickets/Index.vue +91 -39
  121. package/src/pages/Admin/Tickets/Show.vue +101 -37
  122. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
  123. package/src/pages/Admin/Webhooks/Form.vue +110 -0
  124. package/src/pages/Admin/Webhooks/Index.vue +136 -0
  125. package/src/pages/Agent/Dashboard.vue +311 -186
  126. package/src/pages/Agent/TicketIndex.vue +88 -38
  127. package/src/pages/Agent/TicketShow.vue +258 -51
  128. package/src/pages/Customer/Create.vue +36 -9
  129. package/src/pages/Customer/Index.vue +31 -24
  130. package/src/pages/Customer/KnowledgeBase/Article.vue +127 -0
  131. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
  132. package/src/plugin.js +86 -1
  133. package/src/utils/constants.js +30 -0
  134. package/src/utils/formatting.js +35 -0
  135. package/src/utils/sanitizeHtml.js +64 -0
  136. package/src/pages/Admin/Plugins.vue +0 -129
@@ -4,9 +4,16 @@ import { ref, computed, inject, onMounted, onUnmounted } from 'vue';
4
4
  const props = defineProps({
5
5
  ticketReference: { type: String, required: true },
6
6
  routePrefix: { type: String, required: true },
7
+ pollInterval: { type: Number, default: 30000 },
8
+ showLabel: { type: Boolean, default: false },
7
9
  });
8
10
 
9
- const escDark = inject('esc-dark', computed(() => false));
11
+ const effectiveInterval = computed(() => Math.max(props.pollInterval, 5000));
12
+
13
+ const escDark = inject(
14
+ 'esc-dark',
15
+ computed(() => false),
16
+ );
10
17
  const viewers = ref([]);
11
18
  const hovering = ref(false);
12
19
  let intervalId = null;
@@ -16,7 +23,7 @@ async function fetchPresence() {
16
23
  const response = await fetch(route(`${props.routePrefix}.tickets.presence`, props.ticketReference), {
17
24
  method: 'POST',
18
25
  headers: {
19
- 'Accept': 'application/json',
26
+ Accept: 'application/json',
20
27
  'Content-Type': 'application/json',
21
28
  'X-Requested-With': 'XMLHttpRequest',
22
29
  'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
@@ -33,7 +40,7 @@ async function fetchPresence() {
33
40
 
34
41
  const tooltipText = computed(() => {
35
42
  if (!viewers.value.length) return '';
36
- const names = viewers.value.map(v => v.name);
43
+ const names = viewers.value.map((v) => v.name);
37
44
  return 'Also viewing: ' + names.join(', ');
38
45
  });
39
46
 
@@ -46,8 +53,14 @@ function getInitial(viewer) {
46
53
  }
47
54
 
48
55
  const colors = [
49
- 'bg-cyan-500', 'bg-violet-500', 'bg-amber-500', 'bg-emerald-500',
50
- 'bg-rose-500', 'bg-blue-500', 'bg-pink-500', 'bg-teal-500',
56
+ 'bg-cyan-500',
57
+ 'bg-violet-500',
58
+ 'bg-amber-500',
59
+ 'bg-emerald-500',
60
+ 'bg-rose-500',
61
+ 'bg-blue-500',
62
+ 'bg-pink-500',
63
+ 'bg-teal-500',
51
64
  ];
52
65
 
53
66
  function getColor(index) {
@@ -56,7 +69,7 @@ function getColor(index) {
56
69
 
57
70
  onMounted(() => {
58
71
  fetchPresence();
59
- intervalId = setInterval(fetchPresence, 30000);
72
+ intervalId = setInterval(fetchPresence, effectiveInterval.value);
60
73
  });
61
74
 
62
75
  onUnmounted(() => {
@@ -68,35 +81,88 @@ onUnmounted(() => {
68
81
  </script>
69
82
 
70
83
  <template>
71
- <div v-if="viewers.length > 0" class="relative" @mouseenter="hovering = true" @mouseleave="hovering = false">
84
+ <div
85
+ v-if="viewers.length > 0"
86
+ class="relative flex items-center"
87
+ @mouseenter="hovering = true"
88
+ @mouseleave="hovering = false"
89
+ >
72
90
  <!-- Avatar stack -->
73
- <div class="flex -space-x-2">
74
- <div v-for="(viewer, index) in visibleViewers" :key="viewer.id || index"
75
- :class="['flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold text-white ring-2',
76
- getColor(index),
77
- escDark ? 'ring-neutral-900' : 'ring-white']">
91
+ <TransitionGroup
92
+ tag="div"
93
+ class="flex -space-x-2"
94
+ enter-active-class="transition-all duration-300 ease-out"
95
+ enter-from-class="opacity-0 scale-75 -ml-7"
96
+ enter-to-class="opacity-100 scale-100 ml-0"
97
+ leave-active-class="transition-all duration-200 ease-in"
98
+ leave-from-class="opacity-100 scale-100"
99
+ leave-to-class="opacity-0 scale-75 -ml-7"
100
+ >
101
+ <div
102
+ v-for="(viewer, index) in visibleViewers"
103
+ :key="viewer.id || viewer.name"
104
+ :aria-label="viewer.name"
105
+ :class="[
106
+ 'relative flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold text-white ring-2',
107
+ getColor(index),
108
+ escDark ? 'ring-neutral-900' : 'ring-white',
109
+ ]"
110
+ >
78
111
  {{ getInitial(viewer) }}
112
+ <!-- Pulsing live indicator on first avatar -->
113
+ <span v-if="index === 0" class="absolute -right-0.5 -top-0.5 flex h-2.5 w-2.5">
114
+ <span
115
+ class="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75"
116
+ ></span>
117
+ <span class="relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500"></span>
118
+ </span>
79
119
  </div>
80
- <div v-if="overflowCount > 0"
81
- :class="['flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold ring-2',
82
- escDark
83
- ? 'bg-white/[0.08] text-neutral-300 ring-neutral-900'
84
- : 'bg-gray-200 text-gray-600 ring-white']">
120
+ <div
121
+ v-if="overflowCount > 0"
122
+ :key="'overflow'"
123
+ :class="[
124
+ 'flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold ring-2',
125
+ escDark
126
+ ? 'bg-white/[0.08] text-neutral-300 ring-neutral-900'
127
+ : 'bg-gray-200 text-gray-600 ring-white',
128
+ ]"
129
+ >
85
130
  +{{ overflowCount }}
86
131
  </div>
87
- </div>
132
+ </TransitionGroup>
133
+
134
+ <!-- Viewer count label -->
135
+ <span
136
+ v-if="showLabel"
137
+ aria-live="polite"
138
+ :class="['ml-2 text-xs font-medium', escDark ? 'text-neutral-400' : 'text-gray-500']"
139
+ >
140
+ {{ viewers.length }} viewing
141
+ </span>
88
142
 
89
143
  <!-- Tooltip -->
90
- <Transition enter-active-class="transition duration-100 ease-out" enter-from-class="opacity-0 translate-y-1" enter-to-class="opacity-100 translate-y-0"
91
- leave-active-class="transition duration-75 ease-in" leave-from-class="opacity-100 translate-y-0" leave-to-class="opacity-0 translate-y-1">
92
- <div v-if="hovering"
93
- :class="['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',
94
- escDark
95
- ? 'border border-white/[0.06] bg-neutral-800 text-neutral-200'
96
- : 'bg-gray-900 text-white']">
144
+ <Transition
145
+ enter-active-class="transition duration-100 ease-out"
146
+ enter-from-class="opacity-0 translate-y-1"
147
+ enter-to-class="opacity-100 translate-y-0"
148
+ leave-active-class="transition duration-75 ease-in"
149
+ leave-from-class="opacity-100 translate-y-0"
150
+ leave-to-class="opacity-0 translate-y-1"
151
+ >
152
+ <div
153
+ v-if="hovering"
154
+ :class="[
155
+ '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',
156
+ escDark ? 'border border-white/[0.06] bg-neutral-800 text-neutral-200' : 'bg-gray-900 text-white',
157
+ ]"
158
+ >
97
159
  {{ tooltipText }}
98
- <div :class="['absolute -top-1 left-1/2 h-2 w-2 -translate-x-1/2 rotate-45',
99
- escDark ? 'bg-neutral-800' : 'bg-gray-900']"></div>
160
+ <div
161
+ :class="[
162
+ 'absolute -top-1 left-1/2 h-2 w-2 -translate-x-1/2 rotate-45',
163
+ escDark ? 'bg-neutral-800' : 'bg-gray-900',
164
+ ]"
165
+ ></div>
100
166
  </div>
101
167
  </Transition>
102
168
  </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('esc-dark', computed(() => false));
9
-
10
- const darkConfig = {
11
- low: { label: 'Low', color: 'bg-gray-500/10 text-gray-400 ring-1 ring-gray-500/20' },
12
- medium: { label: 'Medium', color: 'bg-cyan-500/10 text-white ring-1 ring-cyan-500/20' },
13
- high: { label: 'High', color: 'bg-amber-500/10 text-amber-400 ring-1 ring-amber-500/20' },
14
- urgent: { label: 'Urgent', color: 'bg-orange-500/10 text-orange-400 ring-1 ring-orange-500/20' },
15
- critical: { label: 'Critical', color: 'bg-rose-500/10 text-rose-300 ring-1 ring-rose-500/20' },
16
- };
17
-
18
- const lightConfig = {
19
- low: { label: 'Low', color: 'bg-gray-100 text-gray-800' },
20
- medium: { label: 'Medium', color: 'bg-blue-100 text-blue-800' },
21
- high: { label: 'High', color: 'bg-yellow-100 text-yellow-800' },
22
- urgent: { label: 'Urgent', color: 'bg-orange-100 text-orange-800' },
23
- critical: { label: 'Critical', color: 'bg-red-100 text-red-800' },
24
- };
25
-
26
- const config = computed(() => {
27
- const map = escDark.value ? darkConfig : lightConfig;
28
- const fallback = escDark.value
29
- ? { label: props.priority, color: 'bg-gray-500/10 text-gray-400 ring-1 ring-gray-500/20' }
30
- : { label: props.priority, color: 'bg-gray-100 text-gray-800' };
31
- return map[props.priority] || fallback;
32
- });
33
- </script>
34
-
35
- <template>
36
- <span :class="['inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium', config.color]">
37
- {{ config.label }}
38
- </span>
39
- </template>
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('esc-dark', computed(() => false));
10
- const activeChip = ref(null);
11
-
12
- const chips = computed(() => {
13
- const list = [
14
- { key: 'my_tickets', label: 'My Tickets', filter: { assigned_to: props.currentUserId } },
15
- { key: 'unassigned', label: 'Unassigned', filter: { unassigned: true } },
16
- { key: 'urgent', label: 'Urgent+', filter: { priority: 'urgent' } },
17
- { key: 'sla_breaching', label: 'SLA Breaching', filter: { sla_breached: true } },
18
- { key: 'following', label: 'Following', filter: { following: true } },
19
- ];
20
- if (props.currentUserId) return list;
21
- return list.filter(c => c.key !== 'my_tickets');
22
- });
23
-
24
- function toggle(chip) {
25
- if (activeChip.value === chip.key) {
26
- activeChip.value = null;
27
- emit('filter', {});
28
- } else {
29
- activeChip.value = chip.key;
30
- emit('filter', chip.filter);
31
- }
32
- }
33
- </script>
34
-
35
- <template>
36
- <div class="flex flex-wrap items-center gap-2">
37
- <button v-for="chip in chips" :key="chip.key" @click="toggle(chip)"
38
- :class="['rounded-full px-3 py-1.5 text-xs font-medium transition-all',
39
- activeChip === chip.key
40
- ? (escDark
41
- ? 'bg-gradient-to-r from-cyan-500/20 to-violet-500/20 text-white ring-1 ring-cyan-500/30'
42
- : 'bg-gradient-to-r from-blue-50 to-indigo-50 text-blue-700 ring-1 ring-blue-200')
43
- : (escDark
44
- ? 'bg-white/[0.04] text-neutral-400 ring-1 ring-white/[0.06] hover:bg-white/[0.06] hover:text-neutral-300'
45
- : 'bg-gray-50 text-gray-600 ring-1 ring-gray-200 hover:bg-gray-100')]">
46
- {{ chip.label }}
47
- </button>
48
- </div>
49
- </template>
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>