@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,270 +1,270 @@
1
- <script setup>
2
- import { inject, computed, reactive, ref, watch } from 'vue';
3
- import { router } from '@inertiajs/vue3';
4
- import { TICKET_STATUSES, TICKET_PRIORITIES } from '../utils/constants';
5
-
6
- const props = defineProps({
7
- statuses: {
8
- type: Array,
9
- default: () => TICKET_STATUSES,
10
- },
11
- priorities: { type: Array, default: () => TICKET_PRIORITIES },
12
- agents: { type: Array, default: () => [] },
13
- departments: { type: Array, default: () => [] },
14
- tags: { type: Array, default: () => [] },
15
- filters: { type: Object, default: () => ({}) },
16
- route: { type: String, required: true },
17
- modelValue: { type: Object, default: () => ({}) },
18
- showFollowing: { type: Boolean, default: false },
19
- showAssignee: { type: Boolean, default: false },
20
- });
21
-
22
- const emit = defineEmits(['update:modelValue', 'save-view']);
23
- const escDark = inject(
24
- 'esc-dark',
25
- computed(() => false),
26
- );
27
-
28
- const initial = { ...props.filters, ...props.modelValue };
29
-
30
- const filterData = reactive({
31
- status: initial.status || '',
32
- priority: initial.priority || '',
33
- assigned_to: initial.assigned_to || '',
34
- department_id: initial.department_id || '',
35
- search: initial.search || '',
36
- following: initial.following || '',
37
- created_after: initial.created_after || '',
38
- created_before: initial.created_before || '',
39
- tag: initial.tag || '',
40
- has_attachments: initial.has_attachments || '',
41
- requester: initial.requester || '',
42
- ticket_type: initial.ticket_type || '',
43
- snoozed: initial.snoozed || '',
44
- live_chat: initial.live_chat || '',
45
- });
46
-
47
- const showAdvanced = ref(
48
- !!(initial.created_after || initial.created_before || initial.tag || initial.has_attachments || initial.requester),
49
- );
50
-
51
- // Debounced watcher that navigates via Inertia router
52
- let debounceTimer = null;
53
-
54
- watch(
55
- filterData,
56
- (val) => {
57
- emit('update:modelValue', { ...val });
58
-
59
- if (debounceTimer) {
60
- clearTimeout(debounceTimer);
61
- }
62
- debounceTimer = setTimeout(() => {
63
- // Strip empty values to keep the URL clean
64
- const params = {};
65
- for (const [key, value] of Object.entries(val)) {
66
- if (value !== '' && value !== null && value !== undefined) {
67
- params[key] = value;
68
- }
69
- }
70
- router.get(props.route, params, { preserveState: true, preserveScroll: true });
71
- }, 300);
72
- },
73
- { deep: true },
74
- );
75
-
76
- const inputClass = computed(() =>
77
- escDark.value
78
- ? 'rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] px-3 py-1.5 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)]'
79
- : 'rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none',
80
- );
81
-
82
- const selectClass = computed(() =>
83
- escDark.value
84
- ? 'rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] px-2 py-1.5 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)]'
85
- : 'rounded-md border border-gray-300 px-2 py-1.5 text-sm',
86
- );
87
-
88
- const checkboxClass = computed(() =>
89
- escDark.value
90
- ? 'h-4 w-4 rounded border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] text-[var(--esc-panel-accent)] focus:ring-[var(--esc-panel-accent)]/20'
91
- : 'h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500',
92
- );
93
-
94
- const toggleBtnClass = computed(() =>
95
- escDark.value
96
- ? 'rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] px-3 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] hover:bg-[var(--esc-panel-surface-alt)] transition-colors'
97
- : 'rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50 transition-colors',
98
- );
99
-
100
- const labelClass = computed(() =>
101
- escDark.value ? 'text-xs font-medium text-[var(--esc-panel-text-tertiary)]' : 'text-xs font-medium text-gray-500',
102
- );
103
-
104
- const hasActiveFilters = computed(() => {
105
- return Object.values(filterData).some((v) => v !== '' && v !== null && v !== undefined);
106
- });
107
-
108
- defineExpose({ filterData });
109
- </script>
110
-
111
- <template>
112
- <div class="space-y-3">
113
- <div class="flex flex-wrap items-center gap-3">
114
- <input
115
- v-model="filterData.search"
116
- type="text"
117
- placeholder="Search tickets..."
118
- aria-label="Search tickets"
119
- :class="inputClass"
120
- />
121
- <button type="button" :class="toggleBtnClass" @click="showAdvanced = !showAdvanced">
122
- {{ showAdvanced ? 'Simple' : 'Advanced' }}
123
- </button>
124
- <button
125
- v-if="hasActiveFilters"
126
- type="button"
127
- :class="[
128
- 'rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
129
- escDark
130
- ? 'bg-cyan-600/20 text-cyan-400 hover:bg-cyan-600/30'
131
- : 'bg-blue-50 text-blue-700 hover:bg-blue-100',
132
- ]"
133
- @click="emit('save-view')"
134
- >
135
- Save as View
136
- </button>
137
- <select v-model="filterData.status" aria-label="Filter by status" :class="selectClass">
138
- <option value="">All Statuses</option>
139
- <option v-for="s in statuses" :key="s" :value="s">{{ s.replace(/_/g, ' ') }}</option>
140
- </select>
141
- <select v-model="filterData.priority" aria-label="Filter by priority" :class="selectClass">
142
- <option value="">All Priorities</option>
143
- <option v-for="p in priorities" :key="p" :value="p">{{ p }}</option>
144
- </select>
145
- <select v-model="filterData.ticket_type" aria-label="Filter by type" :class="selectClass">
146
- <option value="">All Types</option>
147
- <option value="question">Question</option>
148
- <option value="problem">Problem</option>
149
- <option value="incident">Incident</option>
150
- <option value="task">Task</option>
151
- </select>
152
- <select
153
- v-if="agents.length"
154
- v-model="filterData.assigned_to"
155
- aria-label="Filter by agent"
156
- :class="selectClass"
157
- >
158
- <option value="">All Agents</option>
159
- <option v-for="a in agents" :key="a.id" :value="a.id">{{ a.name }}</option>
160
- </select>
161
- <select
162
- v-if="departments.length"
163
- v-model="filterData.department_id"
164
- aria-label="Filter by department"
165
- :class="selectClass"
166
- >
167
- <option value="">All Departments</option>
168
- <option v-for="d in departments" :key="d.id" :value="d.id">{{ d.name }}</option>
169
- </select>
170
- <label v-if="showFollowing" class="flex items-center gap-2">
171
- <input
172
- v-model="filterData.following"
173
- type="checkbox"
174
- true-value="1"
175
- false-value=""
176
- :class="checkboxClass"
177
- />
178
- <span :class="['text-sm', escDark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600']"
179
- >Following</span
180
- >
181
- </label>
182
- </div>
183
-
184
- <!-- Advanced filters -->
185
- <div
186
- v-if="showAdvanced"
187
- class="flex flex-wrap items-end gap-4 rounded-lg border p-3"
188
- :class="
189
- escDark
190
- ? 'border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)]'
191
- : 'border-gray-200 bg-gray-50'
192
- "
193
- >
194
- <div class="flex flex-col gap-1">
195
- <label :class="labelClass">Created after</label>
196
- <input
197
- v-model="filterData.created_after"
198
- type="date"
199
- aria-label="Created after date"
200
- :class="inputClass"
201
- />
202
- </div>
203
- <div class="flex flex-col gap-1">
204
- <label :class="labelClass">Created before</label>
205
- <input
206
- v-model="filterData.created_before"
207
- type="date"
208
- aria-label="Created before date"
209
- :class="inputClass"
210
- />
211
- </div>
212
- <div class="flex flex-col gap-1">
213
- <label :class="labelClass">Tag</label>
214
- <input
215
- v-model="filterData.tag"
216
- type="text"
217
- placeholder="Tag name..."
218
- aria-label="Filter by tag name"
219
- :class="inputClass"
220
- />
221
- </div>
222
- <div class="flex flex-col gap-1">
223
- <label :class="labelClass">Requester</label>
224
- <input
225
- v-model="filterData.requester"
226
- type="text"
227
- placeholder="Name or email..."
228
- aria-label="Filter by requester"
229
- :class="inputClass"
230
- />
231
- </div>
232
- <label class="flex items-center gap-2 self-center pt-4">
233
- <input
234
- v-model="filterData.has_attachments"
235
- type="checkbox"
236
- true-value="1"
237
- false-value=""
238
- :class="checkboxClass"
239
- />
240
- <span :class="['text-sm', escDark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600']"
241
- >Has attachments</span
242
- >
243
- </label>
244
- <label class="flex items-center gap-2 self-center pt-4">
245
- <input
246
- v-model="filterData.snoozed"
247
- type="checkbox"
248
- true-value="1"
249
- false-value=""
250
- :class="checkboxClass"
251
- />
252
- <span :class="['text-sm', escDark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600']"
253
- >Snoozed</span
254
- >
255
- </label>
256
- <label class="flex items-center gap-2 self-center pt-4">
257
- <input
258
- v-model="filterData.live_chat"
259
- type="checkbox"
260
- true-value="1"
261
- false-value=""
262
- :class="checkboxClass"
263
- />
264
- <span :class="['text-sm', escDark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600']"
265
- >Live Chats</span
266
- >
267
- </label>
268
- </div>
269
- </div>
270
- </template>
1
+ <script setup>
2
+ import { inject, computed, reactive, ref, watch } from 'vue';
3
+ import { router } from '@inertiajs/vue3';
4
+ import { TICKET_STATUSES, TICKET_PRIORITIES } from '../utils/constants';
5
+
6
+ const props = defineProps({
7
+ statuses: {
8
+ type: Array,
9
+ default: () => TICKET_STATUSES,
10
+ },
11
+ priorities: { type: Array, default: () => TICKET_PRIORITIES },
12
+ agents: { type: Array, default: () => [] },
13
+ departments: { type: Array, default: () => [] },
14
+ tags: { type: Array, default: () => [] },
15
+ filters: { type: Object, default: () => ({}) },
16
+ route: { type: String, required: true },
17
+ modelValue: { type: Object, default: () => ({}) },
18
+ showFollowing: { type: Boolean, default: false },
19
+ showAssignee: { type: Boolean, default: false },
20
+ });
21
+
22
+ const emit = defineEmits(['update:modelValue', 'save-view']);
23
+ const escDark = inject(
24
+ 'esc-dark',
25
+ computed(() => false),
26
+ );
27
+
28
+ const initial = { ...props.filters, ...props.modelValue };
29
+
30
+ const filterData = reactive({
31
+ status: initial.status || '',
32
+ priority: initial.priority || '',
33
+ assigned_to: initial.assigned_to || '',
34
+ department_id: initial.department_id || '',
35
+ search: initial.search || '',
36
+ following: initial.following || '',
37
+ created_after: initial.created_after || '',
38
+ created_before: initial.created_before || '',
39
+ tag: initial.tag || '',
40
+ has_attachments: initial.has_attachments || '',
41
+ requester: initial.requester || '',
42
+ ticket_type: initial.ticket_type || '',
43
+ snoozed: initial.snoozed || '',
44
+ live_chat: initial.live_chat || '',
45
+ });
46
+
47
+ const showAdvanced = ref(
48
+ !!(initial.created_after || initial.created_before || initial.tag || initial.has_attachments || initial.requester),
49
+ );
50
+
51
+ // Debounced watcher that navigates via Inertia router
52
+ let debounceTimer = null;
53
+
54
+ watch(
55
+ filterData,
56
+ (val) => {
57
+ emit('update:modelValue', { ...val });
58
+
59
+ if (debounceTimer) {
60
+ clearTimeout(debounceTimer);
61
+ }
62
+ debounceTimer = setTimeout(() => {
63
+ // Strip empty values to keep the URL clean
64
+ const params = {};
65
+ for (const [key, value] of Object.entries(val)) {
66
+ if (value !== '' && value !== null && value !== undefined) {
67
+ params[key] = value;
68
+ }
69
+ }
70
+ router.get(props.route, params, { preserveState: true, preserveScroll: true });
71
+ }, 300);
72
+ },
73
+ { deep: true },
74
+ );
75
+
76
+ const inputClass = computed(() =>
77
+ escDark.value
78
+ ? 'rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] px-3 py-1.5 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)]'
79
+ : 'rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none',
80
+ );
81
+
82
+ const selectClass = computed(() =>
83
+ escDark.value
84
+ ? 'rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] px-2 py-1.5 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)]'
85
+ : 'rounded-md border border-gray-300 px-2 py-1.5 text-sm',
86
+ );
87
+
88
+ const checkboxClass = computed(() =>
89
+ escDark.value
90
+ ? 'h-4 w-4 rounded border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] text-[var(--esc-panel-accent)] focus:ring-[var(--esc-panel-accent)]/20'
91
+ : 'h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500',
92
+ );
93
+
94
+ const toggleBtnClass = computed(() =>
95
+ escDark.value
96
+ ? 'rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface)] px-3 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] hover:bg-[var(--esc-panel-surface-alt)] transition-colors'
97
+ : 'rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50 transition-colors',
98
+ );
99
+
100
+ const labelClass = computed(() =>
101
+ escDark.value ? 'text-xs font-medium text-[var(--esc-panel-text-tertiary)]' : 'text-xs font-medium text-gray-500',
102
+ );
103
+
104
+ const hasActiveFilters = computed(() => {
105
+ return Object.values(filterData).some((v) => v !== '' && v !== null && v !== undefined);
106
+ });
107
+
108
+ defineExpose({ filterData });
109
+ </script>
110
+
111
+ <template>
112
+ <div class="space-y-3">
113
+ <div class="flex flex-wrap items-center gap-3">
114
+ <input
115
+ v-model="filterData.search"
116
+ type="text"
117
+ placeholder="Search tickets..."
118
+ aria-label="Search tickets"
119
+ :class="inputClass"
120
+ />
121
+ <button type="button" :class="toggleBtnClass" @click="showAdvanced = !showAdvanced">
122
+ {{ showAdvanced ? 'Simple' : 'Advanced' }}
123
+ </button>
124
+ <button
125
+ v-if="hasActiveFilters"
126
+ type="button"
127
+ :class="[
128
+ 'rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
129
+ escDark
130
+ ? 'bg-cyan-600/20 text-cyan-400 hover:bg-cyan-600/30'
131
+ : 'bg-blue-50 text-blue-700 hover:bg-blue-100',
132
+ ]"
133
+ @click="emit('save-view')"
134
+ >
135
+ Save as View
136
+ </button>
137
+ <select v-model="filterData.status" aria-label="Filter by status" :class="selectClass">
138
+ <option value="">All Statuses</option>
139
+ <option v-for="s in statuses" :key="s" :value="s">{{ s.replace(/_/g, ' ') }}</option>
140
+ </select>
141
+ <select v-model="filterData.priority" aria-label="Filter by priority" :class="selectClass">
142
+ <option value="">All Priorities</option>
143
+ <option v-for="p in priorities" :key="p" :value="p">{{ p }}</option>
144
+ </select>
145
+ <select v-model="filterData.ticket_type" aria-label="Filter by type" :class="selectClass">
146
+ <option value="">All Types</option>
147
+ <option value="question">Question</option>
148
+ <option value="problem">Problem</option>
149
+ <option value="incident">Incident</option>
150
+ <option value="task">Task</option>
151
+ </select>
152
+ <select
153
+ v-if="agents.length"
154
+ v-model="filterData.assigned_to"
155
+ aria-label="Filter by agent"
156
+ :class="selectClass"
157
+ >
158
+ <option value="">All Agents</option>
159
+ <option v-for="a in agents" :key="a.id" :value="a.id">{{ a.name }}</option>
160
+ </select>
161
+ <select
162
+ v-if="departments.length"
163
+ v-model="filterData.department_id"
164
+ aria-label="Filter by department"
165
+ :class="selectClass"
166
+ >
167
+ <option value="">All Departments</option>
168
+ <option v-for="d in departments" :key="d.id" :value="d.id">{{ d.name }}</option>
169
+ </select>
170
+ <label v-if="showFollowing" class="flex items-center gap-2">
171
+ <input
172
+ v-model="filterData.following"
173
+ type="checkbox"
174
+ true-value="1"
175
+ false-value=""
176
+ :class="checkboxClass"
177
+ />
178
+ <span :class="['text-sm', escDark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600']"
179
+ >Following</span
180
+ >
181
+ </label>
182
+ </div>
183
+
184
+ <!-- Advanced filters -->
185
+ <div
186
+ v-if="showAdvanced"
187
+ class="flex flex-wrap items-end gap-4 rounded-lg border p-3"
188
+ :class="
189
+ escDark
190
+ ? 'border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)]'
191
+ : 'border-gray-200 bg-gray-50'
192
+ "
193
+ >
194
+ <div class="flex flex-col gap-1">
195
+ <label :class="labelClass">Created after</label>
196
+ <input
197
+ v-model="filterData.created_after"
198
+ type="date"
199
+ aria-label="Created after date"
200
+ :class="inputClass"
201
+ />
202
+ </div>
203
+ <div class="flex flex-col gap-1">
204
+ <label :class="labelClass">Created before</label>
205
+ <input
206
+ v-model="filterData.created_before"
207
+ type="date"
208
+ aria-label="Created before date"
209
+ :class="inputClass"
210
+ />
211
+ </div>
212
+ <div class="flex flex-col gap-1">
213
+ <label :class="labelClass">Tag</label>
214
+ <input
215
+ v-model="filterData.tag"
216
+ type="text"
217
+ placeholder="Tag name..."
218
+ aria-label="Filter by tag name"
219
+ :class="inputClass"
220
+ />
221
+ </div>
222
+ <div class="flex flex-col gap-1">
223
+ <label :class="labelClass">Requester</label>
224
+ <input
225
+ v-model="filterData.requester"
226
+ type="text"
227
+ placeholder="Name or email..."
228
+ aria-label="Filter by requester"
229
+ :class="inputClass"
230
+ />
231
+ </div>
232
+ <label class="flex items-center gap-2 self-center pt-4">
233
+ <input
234
+ v-model="filterData.has_attachments"
235
+ type="checkbox"
236
+ true-value="1"
237
+ false-value=""
238
+ :class="checkboxClass"
239
+ />
240
+ <span :class="['text-sm', escDark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600']"
241
+ >Has attachments</span
242
+ >
243
+ </label>
244
+ <label class="flex items-center gap-2 self-center pt-4">
245
+ <input
246
+ v-model="filterData.snoozed"
247
+ type="checkbox"
248
+ true-value="1"
249
+ false-value=""
250
+ :class="checkboxClass"
251
+ />
252
+ <span :class="['text-sm', escDark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600']"
253
+ >Snoozed</span
254
+ >
255
+ </label>
256
+ <label class="flex items-center gap-2 self-center pt-4">
257
+ <input
258
+ v-model="filterData.live_chat"
259
+ type="checkbox"
260
+ true-value="1"
261
+ false-value=""
262
+ :class="checkboxClass"
263
+ />
264
+ <span :class="['text-sm', escDark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600']"
265
+ >Live Chats</span
266
+ >
267
+ </label>
268
+ </div>
269
+ </div>
270
+ </template>