@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,15 +1,27 @@
1
1
  <script setup>
2
+ import { computed } from 'vue';
2
3
  import EscalatedLayout from '../../../components/EscalatedLayout.vue';
3
- import { useForm } from '@inertiajs/vue3';
4
+ import SkillTagManager from '../../../components/SkillTagManager.vue';
5
+ import { Link, useForm } from '@inertiajs/vue3';
4
6
 
5
7
  const props = defineProps({
6
8
  skill: { type: Object, default: null },
9
+ availableAgents: { type: Array, default: () => [] },
10
+ availableTags: { type: Array, default: () => [] },
11
+ availableDepartments: { type: Array, default: () => [] },
7
12
  });
8
13
 
9
14
  const form = useForm({
10
15
  name: props.skill?.name || '',
16
+ routing_tag_ids: props.skill?.routing_tag_ids || [],
17
+ routing_department_ids: props.skill?.routing_department_ids || [],
18
+ agents: props.skill?.agents?.length ? props.skill.agents.map((agent) => ({ ...agent })) : [],
11
19
  });
12
20
 
21
+ const unassignedAgents = computed(() =>
22
+ props.availableAgents.filter((agent) => !form.agents.some((entry) => entry.user_id === agent.id)),
23
+ );
24
+
13
25
  function submit() {
14
26
  if (props.skill) {
15
27
  form.put(route('escalated.admin.skills.update', props.skill.id));
@@ -17,11 +29,27 @@ function submit() {
17
29
  form.post(route('escalated.admin.skills.store'));
18
30
  }
19
31
  }
32
+
33
+ function addAgent() {
34
+ const nextAgent = unassignedAgents.value[0];
35
+ if (!nextAgent) {
36
+ return;
37
+ }
38
+
39
+ form.agents.push({ user_id: nextAgent.id, proficiency: 3 });
40
+ }
41
+
42
+ function removeAgent(index) {
43
+ form.agents.splice(index, 1);
44
+ }
20
45
  </script>
21
46
 
22
47
  <template>
23
48
  <EscalatedLayout :title="skill ? 'Edit Skill' : 'Create Skill'">
24
- <form class="max-w-lg space-y-4" @submit.prevent="submit">
49
+ <form
50
+ class="mx-auto max-w-4xl space-y-5 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-6"
51
+ @submit.prevent="submit"
52
+ >
25
53
  <div>
26
54
  <label class="block text-sm font-medium text-[var(--esc-panel-text-secondary)]">Name</label>
27
55
  <input
@@ -33,6 +61,138 @@ function submit() {
33
61
  <p v-if="form.errors.name" class="mt-1 text-xs text-rose-400">{{ form.errors.name }}</p>
34
62
  </div>
35
63
 
64
+ <div class="grid gap-5 lg:grid-cols-2">
65
+ <div
66
+ class="space-y-3 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)] p-4"
67
+ >
68
+ <div>
69
+ <h2 class="text-sm font-semibold text-[var(--esc-panel-text-secondary)]">Routing Rules</h2>
70
+ <p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
71
+ Match tickets by tag or department to require this skill during auto-assignment.
72
+ </p>
73
+ </div>
74
+ <div>
75
+ <label
76
+ class="mb-2 block text-xs font-medium uppercase tracking-wide text-[var(--esc-panel-text-muted)]"
77
+ >
78
+ Required Tags
79
+ </label>
80
+ <SkillTagManager
81
+ v-model="form.routing_tag_ids"
82
+ :items="availableTags"
83
+ add-label="Add Tag"
84
+ empty-label="No ticket tags mapped"
85
+ />
86
+ <p v-if="form.errors.routing_tag_ids" class="mt-1 text-xs text-rose-400">
87
+ {{ form.errors.routing_tag_ids }}
88
+ </p>
89
+ </div>
90
+ <div>
91
+ <label
92
+ class="mb-2 block text-xs font-medium uppercase tracking-wide text-[var(--esc-panel-text-muted)]"
93
+ >
94
+ Departments
95
+ </label>
96
+ <SkillTagManager
97
+ v-model="form.routing_department_ids"
98
+ :items="availableDepartments"
99
+ add-label="Add Department"
100
+ empty-label="No departments mapped"
101
+ />
102
+ <p v-if="form.errors.routing_department_ids" class="mt-1 text-xs text-rose-400">
103
+ {{ form.errors.routing_department_ids }}
104
+ </p>
105
+ </div>
106
+ </div>
107
+
108
+ <div
109
+ class="space-y-3 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)] p-4"
110
+ >
111
+ <div class="flex items-start justify-between gap-3">
112
+ <div>
113
+ <h2 class="text-sm font-semibold text-[var(--esc-panel-text-secondary)]">
114
+ Agent Proficiency
115
+ </h2>
116
+ <p class="mt-1 text-xs text-[var(--esc-panel-text-muted)]">
117
+ Assign eligible agents and tune their proficiency from 1 to 5.
118
+ </p>
119
+ </div>
120
+ <button
121
+ v-if="unassignedAgents.length"
122
+ type="button"
123
+ class="rounded-lg border border-dashed border-[var(--esc-panel-border-input)] px-3 py-1.5 text-xs font-medium text-[var(--esc-panel-text-secondary)] transition-colors hover:bg-[var(--esc-panel-hover)]"
124
+ @click="addAgent"
125
+ >
126
+ Add Agent
127
+ </button>
128
+ </div>
129
+
130
+ <div v-if="form.agents.length" class="space-y-3">
131
+ <div
132
+ v-for="(agent, index) in form.agents"
133
+ :key="`${agent.user_id}-${index}`"
134
+ class="grid gap-3 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-3 md:grid-cols-[minmax(0,1fr)_110px_auto]"
135
+ >
136
+ <div>
137
+ <label
138
+ class="mb-1 block text-xs font-medium uppercase tracking-wide text-[var(--esc-panel-text-muted)]"
139
+ >
140
+ Agent
141
+ </label>
142
+ <select
143
+ v-model="agent.user_id"
144
+ class="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)]"
145
+ >
146
+ <option
147
+ v-for="option in [
148
+ ...unassignedAgents,
149
+ availableAgents.find((entry) => entry.id === agent.user_id),
150
+ ].filter(Boolean)"
151
+ :key="option.id"
152
+ :value="option.id"
153
+ >
154
+ {{ option.name }} / {{ option.email }}
155
+ </option>
156
+ </select>
157
+ </div>
158
+ <div>
159
+ <label
160
+ class="mb-1 block text-xs font-medium uppercase tracking-wide text-[var(--esc-panel-text-muted)]"
161
+ >
162
+ Proficiency
163
+ </label>
164
+ <select
165
+ v-model="agent.proficiency"
166
+ class="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)]"
167
+ >
168
+ <option :value="1">1</option>
169
+ <option :value="2">2</option>
170
+ <option :value="3">3</option>
171
+ <option :value="4">4</option>
172
+ <option :value="5">5</option>
173
+ </select>
174
+ </div>
175
+ <div class="flex items-end">
176
+ <button
177
+ type="button"
178
+ class="rounded-lg px-3 py-2 text-sm font-medium text-rose-300 transition-colors hover:bg-rose-500/10"
179
+ @click="removeAgent(index)"
180
+ >
181
+ Remove
182
+ </button>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <div
188
+ v-else
189
+ class="rounded-lg border border-dashed border-[var(--esc-panel-border)] px-4 py-6 text-center text-sm text-[var(--esc-panel-text-muted)]"
190
+ >
191
+ No agents assigned yet.
192
+ </div>
193
+ </div>
194
+ </div>
195
+
36
196
  <div class="flex gap-3">
37
197
  <button
38
198
  type="submit"
@@ -41,12 +201,12 @@ function submit() {
41
201
  >
42
202
  {{ skill ? 'Update' : 'Create' }}
43
203
  </button>
44
- <a
204
+ <Link
45
205
  :href="route('escalated.admin.skills.index')"
46
206
  class="rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] px-4 py-2 text-sm font-medium text-[var(--esc-panel-text-secondary)] transition-colors hover:bg-[var(--esc-panel-hover)]"
47
207
  >
48
208
  Cancel
49
- </a>
209
+ </Link>
50
210
  </div>
51
211
  </form>
52
212
  </EscalatedLayout>
@@ -33,7 +33,12 @@ function destroy(id) {
33
33
  <th
34
34
  class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
35
35
  >
36
- Agents
36
+ Routing
37
+ </th>
38
+ <th
39
+ class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
40
+ >
41
+ Updated
37
42
  </th>
38
43
  <th
39
44
  class="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-[var(--esc-panel-text-muted)]"
@@ -44,7 +49,7 @@ function destroy(id) {
44
49
  </thead>
45
50
  <tbody class="divide-y divide-[var(--esc-panel-border)]">
46
51
  <tr v-if="!skills?.length">
47
- <td colspan="3" class="px-4 py-12 text-center">
52
+ <td colspan="4" class="px-4 py-12 text-center">
48
53
  <svg
49
54
  class="mx-auto mb-3 h-8 w-8 text-[var(--esc-panel-text-muted)]"
50
55
  fill="none"
@@ -73,7 +78,13 @@ function destroy(id) {
73
78
  {{ skill.name }}
74
79
  </td>
75
80
  <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
76
- {{ skill.agents_count }}
81
+ <div>{{ skill.agents_count }} agents</div>
82
+ <div class="text-xs text-[var(--esc-panel-text-muted)]">
83
+ {{ skill.routing_tags_count }} tags / {{ skill.routing_departments_count }} departments
84
+ </div>
85
+ </td>
86
+ <td class="px-4 py-3 text-sm text-[var(--esc-panel-text-tertiary)]">
87
+ {{ skill.updated_at ? new Date(skill.updated_at).toLocaleDateString() : '-' }}
77
88
  </td>
78
89
  <td class="px-4 py-3 text-right text-sm">
79
90
  <Link
@@ -1,126 +1,126 @@
1
- <script setup>
2
- import { ref } from 'vue';
3
- import { usePage, router } from '@inertiajs/vue3';
4
- import EscalatedLayout from '../../../components/EscalatedLayout.vue';
5
- import TicketList from '../../../components/TicketList.vue';
6
- import TicketFilters from '../../../components/TicketFilters.vue';
7
- import QuickFilters from '../../../components/QuickFilters.vue';
8
- import BulkActionBar from '../../../components/BulkActionBar.vue';
9
- import KeyboardShortcutHelp from '../../../components/KeyboardShortcutHelp.vue';
10
- import PluginSlot from '../../../components/PluginSlot.vue';
11
- import SavedViewSidebar from '../../../components/SavedViewSidebar.vue';
12
- import SaveViewDialog from '../../../components/SaveViewDialog.vue';
13
- import { useKeyboardShortcuts } from '../../../composables/useKeyboardShortcuts';
14
- import { usePluginExtensions } from '../../../composables/usePluginExtensions';
15
-
16
- defineProps({
17
- tickets: Object,
18
- filters: Object,
19
- departments: Array,
20
- tags: Array,
21
- agents: Array,
22
- savedViews: { type: Array, default: () => [] },
23
- });
24
-
25
- const page = usePage();
26
- const selectedIds = ref([]);
27
- const ticketListRef = ref(null);
28
- const showShortcutHelp = ref(false);
29
- const showSaveDialog = ref(false);
30
- const ticketFiltersRef = ref(null);
31
-
32
- useKeyboardShortcuts({
33
- j: () => {
34
- ticketListRef.value?.moveDown();
35
- },
36
- k: () => {
37
- ticketListRef.value?.moveUp();
38
- },
39
- x: () => {
40
- ticketListRef.value?.toggleSelectFocused();
41
- },
42
- Enter: () => {
43
- ticketListRef.value?.openFocused();
44
- },
45
- '?': () => {
46
- showShortcutHelp.value = true;
47
- },
48
- });
49
-
50
- const { getPageComponents } = usePluginExtensions();
51
-
52
- function applyQuickFilter(filter) {
53
- router.get(route('escalated.admin.tickets.index'), filter, { preserveState: true });
54
- }
55
-
56
- function onViewSaved() {
57
- router.reload({ only: ['savedViews'] });
58
- }
59
- </script>
60
-
61
- <template>
62
- <EscalatedLayout title="All Tickets">
63
- <div class="flex gap-6">
64
- <!-- Saved Views Sidebar -->
65
- <aside v-if="savedViews && savedViews.length" class="hidden w-52 shrink-0 lg:block">
66
- <div class="sticky top-4">
67
- <h3 class="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400">Views</h3>
68
- <SavedViewSidebar
69
- :saved-views="savedViews"
70
- :current-filters="filters"
71
- :route="route('escalated.admin.tickets.index')"
72
- />
73
- </div>
74
- </aside>
75
-
76
- <!-- Main content -->
77
- <div class="min-w-0 flex-1">
78
- <div class="mb-4">
79
- <QuickFilters :current-user-id="page.props.auth?.user?.id" @filter="applyQuickFilter" />
80
- </div>
81
- <PluginSlot
82
- slot="ticket.list.before_filters"
83
- :components="getPageComponents('ticket.list', 'before_filters')"
84
- />
85
- <div class="mb-6">
86
- <TicketFilters
87
- ref="ticketFiltersRef"
88
- :filters="filters"
89
- :route="route('escalated.admin.tickets.index')"
90
- :departments="departments"
91
- :tags="tags"
92
- :agents="agents"
93
- show-assignee
94
- show-following
95
- @save-view="showSaveDialog = true"
96
- />
97
- </div>
98
- <TicketList
99
- ref="ticketListRef"
100
- v-model:selected-ids="selectedIds"
101
- :tickets="tickets"
102
- route-prefix="escalated.admin.tickets"
103
- :columns="['reference', 'subject', 'requester', 'status', 'priority', 'assignee', 'updated']"
104
- selectable
105
- navigable
106
- />
107
- <BulkActionBar
108
- :selected-ids="selectedIds"
109
- :bulk-route="route('escalated.admin.tickets.bulk')"
110
- :agents="agents"
111
- :departments="departments"
112
- @clear="selectedIds = []"
113
- />
114
- <PluginSlot slot="ticket.list.footer" :components="getPageComponents('ticket.list', 'footer')" />
115
- </div>
116
- </div>
117
-
118
- <SaveViewDialog
119
- :show="showSaveDialog"
120
- :filters="ticketFiltersRef?.filterData || filters"
121
- @close="showSaveDialog = false"
122
- @saved="onViewSaved"
123
- />
124
- <KeyboardShortcutHelp v-model:show="showShortcutHelp" context="list" />
125
- </EscalatedLayout>
126
- </template>
1
+ <script setup>
2
+ import { ref } from 'vue';
3
+ import { usePage, router } from '@inertiajs/vue3';
4
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
5
+ import TicketList from '../../../components/TicketList.vue';
6
+ import TicketFilters from '../../../components/TicketFilters.vue';
7
+ import QuickFilters from '../../../components/QuickFilters.vue';
8
+ import BulkActionBar from '../../../components/BulkActionBar.vue';
9
+ import KeyboardShortcutHelp from '../../../components/KeyboardShortcutHelp.vue';
10
+ import PluginSlot from '../../../components/PluginSlot.vue';
11
+ import SavedViewSidebar from '../../../components/SavedViewSidebar.vue';
12
+ import SaveViewDialog from '../../../components/SaveViewDialog.vue';
13
+ import { useKeyboardShortcuts } from '../../../composables/useKeyboardShortcuts';
14
+ import { usePluginExtensions } from '../../../composables/usePluginExtensions';
15
+
16
+ defineProps({
17
+ tickets: Object,
18
+ filters: Object,
19
+ departments: Array,
20
+ tags: Array,
21
+ agents: Array,
22
+ savedViews: { type: Array, default: () => [] },
23
+ });
24
+
25
+ const page = usePage();
26
+ const selectedIds = ref([]);
27
+ const ticketListRef = ref(null);
28
+ const showShortcutHelp = ref(false);
29
+ const showSaveDialog = ref(false);
30
+ const ticketFiltersRef = ref(null);
31
+
32
+ useKeyboardShortcuts({
33
+ j: () => {
34
+ ticketListRef.value?.moveDown();
35
+ },
36
+ k: () => {
37
+ ticketListRef.value?.moveUp();
38
+ },
39
+ x: () => {
40
+ ticketListRef.value?.toggleSelectFocused();
41
+ },
42
+ Enter: () => {
43
+ ticketListRef.value?.openFocused();
44
+ },
45
+ '?': () => {
46
+ showShortcutHelp.value = true;
47
+ },
48
+ });
49
+
50
+ const { getPageComponents } = usePluginExtensions();
51
+
52
+ function applyQuickFilter(filter) {
53
+ router.get(route('escalated.admin.tickets.index'), filter, { preserveState: true });
54
+ }
55
+
56
+ function onViewSaved() {
57
+ router.reload({ only: ['savedViews'] });
58
+ }
59
+ </script>
60
+
61
+ <template>
62
+ <EscalatedLayout title="All Tickets">
63
+ <div class="flex gap-6">
64
+ <!-- Saved Views Sidebar -->
65
+ <aside v-if="savedViews && savedViews.length" class="hidden w-52 shrink-0 lg:block">
66
+ <div class="sticky top-4">
67
+ <h3 class="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400">Views</h3>
68
+ <SavedViewSidebar
69
+ :saved-views="savedViews"
70
+ :current-filters="filters"
71
+ :route="route('escalated.admin.tickets.index')"
72
+ />
73
+ </div>
74
+ </aside>
75
+
76
+ <!-- Main content -->
77
+ <div class="min-w-0 flex-1">
78
+ <div class="mb-4">
79
+ <QuickFilters :current-user-id="page.props.auth?.user?.id" @filter="applyQuickFilter" />
80
+ </div>
81
+ <PluginSlot
82
+ slot="ticket.list.before_filters"
83
+ :components="getPageComponents('ticket.list', 'before_filters')"
84
+ />
85
+ <div class="mb-6">
86
+ <TicketFilters
87
+ ref="ticketFiltersRef"
88
+ :filters="filters"
89
+ :route="route('escalated.admin.tickets.index')"
90
+ :departments="departments"
91
+ :tags="tags"
92
+ :agents="agents"
93
+ show-assignee
94
+ show-following
95
+ @save-view="showSaveDialog = true"
96
+ />
97
+ </div>
98
+ <TicketList
99
+ ref="ticketListRef"
100
+ v-model:selected-ids="selectedIds"
101
+ :tickets="tickets"
102
+ route-prefix="escalated.admin.tickets"
103
+ :columns="['reference', 'subject', 'requester', 'status', 'priority', 'assignee', 'updated']"
104
+ selectable
105
+ navigable
106
+ />
107
+ <BulkActionBar
108
+ :selected-ids="selectedIds"
109
+ :bulk-route="route('escalated.admin.tickets.bulk')"
110
+ :agents="agents"
111
+ :departments="departments"
112
+ @clear="selectedIds = []"
113
+ />
114
+ <PluginSlot slot="ticket.list.footer" :components="getPageComponents('ticket.list', 'footer')" />
115
+ </div>
116
+ </div>
117
+
118
+ <SaveViewDialog
119
+ :show="showSaveDialog"
120
+ :filters="ticketFiltersRef?.filterData || filters"
121
+ @close="showSaveDialog = false"
122
+ @saved="onViewSaved"
123
+ />
124
+ <KeyboardShortcutHelp v-model:show="showShortcutHelp" context="list" />
125
+ </EscalatedLayout>
126
+ </template>