@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.
Files changed (187) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +125 -2
  3. package/package.json +40 -5
  4. package/src/components/ActiveChatsPanel.vue +246 -0
  5. package/src/components/ActivityTimeline.stories.js +55 -0
  6. package/src/components/ActivityTimeline.vue +83 -66
  7. package/src/components/AdminDashboard.stories.js +1423 -0
  8. package/src/components/AgentLoadIndicator.stories.js +37 -0
  9. package/src/components/AgentLoadIndicator.vue +49 -0
  10. package/src/components/AgentPerformanceCard.vue +44 -0
  11. package/src/components/ArticleEditor.vue +82 -0
  12. package/src/components/AssigneeSelect.vue +37 -23
  13. package/src/components/AttachmentList.vue +65 -37
  14. package/src/components/AuditLogEntry.vue +95 -0
  15. package/src/components/BulkActionBar.vue +166 -97
  16. package/src/components/CategoryTree.vue +89 -0
  17. package/src/components/ChartWidget.vue +46 -0
  18. package/src/components/ChatActionBar.vue +226 -0
  19. package/src/components/ChatBubble.vue +162 -0
  20. package/src/components/ChatComposer.vue +308 -0
  21. package/src/components/ChatQueue.vue +222 -0
  22. package/src/components/ChatThread.vue +197 -0
  23. package/src/components/CollisionWarning.stories.js +30 -0
  24. package/src/components/CollisionWarning.vue +68 -0
  25. package/src/components/ConditionalFieldRules.vue +135 -0
  26. package/src/components/ContextPanel.vue +66 -0
  27. package/src/components/ContextPanelSection.vue +65 -0
  28. package/src/components/CustomFieldRenderer.vue +159 -0
  29. package/src/components/EscalatedLayout.vue +424 -70
  30. package/src/components/FileDropzone.vue +57 -41
  31. package/src/components/FollowButton.stories.js +30 -0
  32. package/src/components/FollowButton.vue +88 -63
  33. package/src/components/KeyboardShortcutHelp.vue +213 -123
  34. package/src/components/KnowledgePanel.vue +122 -0
  35. package/src/components/KpiCard.stories.js +32 -0
  36. package/src/components/KpiCard.vue +88 -0
  37. package/src/components/MacroDropdown.vue +150 -90
  38. package/src/components/MentionDropdown.vue +154 -0
  39. package/src/components/MetricComparison.vue +89 -0
  40. package/src/components/PercentileChart.vue +67 -0
  41. package/src/components/PeriodSelector.vue +98 -0
  42. package/src/components/PermissionMatrix.vue +107 -0
  43. package/src/components/PinnedNotes.vue +129 -90
  44. package/src/components/PluginSlot.vue +68 -62
  45. package/src/components/PresenceIndicator.vue +138 -28
  46. package/src/components/PriorityBadge.stories.js +33 -0
  47. package/src/components/PriorityBadge.vue +46 -39
  48. package/src/components/QuickFilters.vue +65 -49
  49. package/src/components/ReplyComposer.vue +207 -30
  50. package/src/components/ReplyThread.vue +111 -21
  51. package/src/components/ReportChart.vue +395 -0
  52. package/src/components/ReportExportButton.vue +108 -0
  53. package/src/components/SatisfactionRating.vue +180 -128
  54. package/src/components/SaveViewDialog.vue +172 -0
  55. package/src/components/SavedViewSidebar.vue +208 -0
  56. package/src/components/ScheduleEditor.vue +104 -0
  57. package/src/components/SideConversation.vue +165 -0
  58. package/src/components/SideConversationList.vue +147 -0
  59. package/src/components/SkillTagManager.vue +79 -0
  60. package/src/components/SlaComplianceChart.vue +46 -0
  61. package/src/components/SlaTimer.stories.js +35 -0
  62. package/src/components/SlaTimer.vue +62 -49
  63. package/src/components/SnoozeButton.vue +247 -0
  64. package/src/components/StatsCard.stories.js +24 -0
  65. package/src/components/StatsCard.vue +18 -6
  66. package/src/components/StatusBadge.stories.js +53 -0
  67. package/src/components/StatusBadge.vue +78 -46
  68. package/src/components/TagSelect.vue +84 -53
  69. package/src/components/TicketFilters.vue +247 -46
  70. package/src/components/TicketLinkPanel.vue +193 -0
  71. package/src/components/TicketList.vue +536 -74
  72. package/src/components/TicketMergeDialog.vue +157 -0
  73. package/src/components/TicketSidebar.vue +167 -99
  74. package/src/components/TicketSplitDialog.vue +98 -0
  75. package/src/components/TicketTypeSelector.vue +21 -0
  76. package/src/components/TotpInput.stories.js +9 -0
  77. package/src/components/TotpInput.vue +81 -0
  78. package/src/components/TwoFactorSetup.vue +113 -0
  79. package/src/components/VariableTokenInput.vue +111 -0
  80. package/src/components/WorkflowActionConfig.vue +287 -0
  81. package/src/components/WorkflowActionList.vue +350 -0
  82. package/src/components/WorkflowConditionBuilder.vue +265 -0
  83. package/src/components/WorkflowTriggerSelector.vue +125 -0
  84. package/src/composables/useChat.js +187 -0
  85. package/src/composables/useDebouncedSearch.js +38 -0
  86. package/src/composables/useI18n.js +80 -80
  87. package/src/composables/useKeyboardShortcuts.js +46 -46
  88. package/src/composables/useMentions.js +134 -0
  89. package/src/composables/usePluginExtensions.js +159 -67
  90. package/src/composables/usePluginHooks.js +65 -0
  91. package/src/composables/useRealtime.js +182 -0
  92. package/src/defineEscalatedPlugin.js +76 -50
  93. package/src/index.js +77 -28
  94. package/src/locales/ar.json +727 -0
  95. package/src/locales/de.json +678 -430
  96. package/src/locales/en.json +717 -430
  97. package/src/locales/es.json +679 -430
  98. package/src/locales/fr.json +680 -431
  99. package/src/locales/index.js +12 -2
  100. package/src/locales/it.json +727 -0
  101. package/src/locales/ja.json +727 -0
  102. package/src/locales/ko.json +727 -0
  103. package/src/locales/nl.json +727 -0
  104. package/src/locales/pl.json +727 -0
  105. package/src/locales/pt-BR.json +727 -0
  106. package/src/locales/ru.json +727 -0
  107. package/src/locales/tr.json +727 -0
  108. package/src/locales/zh-CN.json +727 -0
  109. package/src/pages/Admin/ApiTokens/Index.vue +381 -217
  110. package/src/pages/Admin/AuditLog/Index.vue +154 -0
  111. package/src/pages/Admin/BusinessHours/Form.vue +172 -0
  112. package/src/pages/Admin/BusinessHours/Index.vue +113 -0
  113. package/src/pages/Admin/CannedResponses/Index.vue +82 -25
  114. package/src/pages/Admin/Capacity/Index.vue +134 -0
  115. package/src/pages/Admin/CustomFields/Form.vue +195 -0
  116. package/src/pages/Admin/CustomFields/Index.vue +134 -0
  117. package/src/pages/Admin/CustomObjects/Form.vue +202 -0
  118. package/src/pages/Admin/CustomObjects/Index.vue +127 -0
  119. package/src/pages/Admin/CustomObjects/Records.vue +228 -0
  120. package/src/pages/Admin/Departments/Form.vue +35 -11
  121. package/src/pages/Admin/Departments/Index.vue +74 -21
  122. package/src/pages/Admin/EscalationRules/Form.vue +86 -25
  123. package/src/pages/Admin/EscalationRules/Index.vue +158 -37
  124. package/src/pages/Admin/Import/Connect.vue +95 -0
  125. package/src/pages/Admin/Import/Index.vue +102 -0
  126. package/src/pages/Admin/Import/Mapping.vue +110 -0
  127. package/src/pages/Admin/Import/Progress.vue +153 -0
  128. package/src/pages/Admin/Import/Review.vue +76 -0
  129. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
  130. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
  131. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
  132. package/src/pages/Admin/Macros/Index.vue +448 -287
  133. package/src/pages/Admin/Plugins/Index.vue +327 -197
  134. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  135. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
  136. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  137. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  138. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  139. package/src/pages/Admin/Reports/CsatReport.vue +127 -0
  140. package/src/pages/Admin/Reports/Dashboard.vue +226 -0
  141. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  142. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  143. package/src/pages/Admin/Reports/SlaReport.vue +162 -0
  144. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  145. package/src/pages/Admin/Reports.vue +206 -101
  146. package/src/pages/Admin/Roles/Form.vue +71 -0
  147. package/src/pages/Admin/Roles/Index.vue +122 -0
  148. package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
  149. package/src/pages/Admin/Settings/DataRetention.vue +182 -0
  150. package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
  151. package/src/pages/Admin/Settings/Sandbox.vue +135 -0
  152. package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
  153. package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
  154. package/src/pages/Admin/Settings.vue +847 -99
  155. package/src/pages/Admin/Skills/Form.vue +53 -0
  156. package/src/pages/Admin/Skills/Index.vue +94 -0
  157. package/src/pages/Admin/SlaPolicies/Form.vue +62 -19
  158. package/src/pages/Admin/SlaPolicies/Index.vue +85 -23
  159. package/src/pages/Admin/Statuses/Form.vue +114 -0
  160. package/src/pages/Admin/Statuses/Index.vue +153 -0
  161. package/src/pages/Admin/Tags/Index.vue +121 -35
  162. package/src/pages/Admin/Tickets/Index.vue +96 -9
  163. package/src/pages/Admin/Tickets/Show.vue +130 -37
  164. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
  165. package/src/pages/Admin/Webhooks/Form.vue +110 -0
  166. package/src/pages/Admin/Webhooks/Index.vue +136 -0
  167. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  168. package/src/pages/Admin/Workflows/Index.vue +235 -0
  169. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  170. package/src/pages/Agent/Dashboard.vue +311 -186
  171. package/src/pages/Agent/TicketIndex.vue +80 -6
  172. package/src/pages/Agent/TicketShow.vue +399 -60
  173. package/src/pages/Customer/Create.vue +37 -10
  174. package/src/pages/Customer/Index.vue +9 -2
  175. package/src/pages/Customer/KnowledgeBase/Article.vue +129 -0
  176. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
  177. package/src/pages/Customer/Show.vue +69 -63
  178. package/src/pages/Guest/Create.vue +40 -15
  179. package/src/pages/Guest/Show.vue +124 -93
  180. package/src/plugin.js +111 -1
  181. package/src/utils/constants.js +30 -0
  182. package/src/utils/formatting.js +35 -0
  183. package/src/utils/mentionHighlight.js +24 -0
  184. package/src/utils/sanitizeHtml.js +78 -64
  185. package/src/widget/EscalatedWidget.vue +804 -0
  186. package/src/widget/index.js +254 -0
  187. package/src/pages/Admin/Plugins.vue +0 -129
@@ -1,46 +1,78 @@
1
- <script setup>
2
- import { inject, computed } from 'vue';
3
-
4
- const props = defineProps({
5
- status: { type: String, required: true },
6
- });
7
-
8
- const escDark = inject('esc-dark', computed(() => false));
9
-
10
- const darkConfig = {
11
- open: { label: 'Open', color: 'bg-cyan-500/10 text-white ring-1 ring-cyan-500/20' },
12
- in_progress: { label: 'In Progress', color: 'bg-violet-500/10 text-violet-400 ring-1 ring-violet-500/20' },
13
- waiting_on_customer: { label: 'Waiting on Customer', color: 'bg-amber-500/10 text-amber-400 ring-1 ring-amber-500/20' },
14
- waiting_on_agent: { label: 'Waiting on Agent', color: 'bg-orange-500/10 text-orange-400 ring-1 ring-orange-500/20' },
15
- escalated: { label: 'Escalated', color: 'bg-rose-500/10 text-rose-400 ring-1 ring-rose-500/20' },
16
- resolved: { label: 'Resolved', color: 'bg-emerald-500/10 text-emerald-400 ring-1 ring-emerald-500/20' },
17
- closed: { label: 'Closed', color: 'bg-gray-500/10 text-gray-400 ring-1 ring-gray-500/20' },
18
- reopened: { label: 'Reopened', color: 'bg-cyan-500/10 text-white ring-1 ring-cyan-500/20' },
19
- };
20
-
21
- const lightConfig = {
22
- open: { label: 'Open', color: 'bg-blue-100 text-blue-800' },
23
- in_progress: { label: 'In Progress', color: 'bg-purple-100 text-purple-800' },
24
- waiting_on_customer: { label: 'Waiting on Customer', color: 'bg-yellow-100 text-yellow-800' },
25
- waiting_on_agent: { label: 'Waiting on Agent', color: 'bg-orange-100 text-orange-800' },
26
- escalated: { label: 'Escalated', color: 'bg-red-100 text-red-800' },
27
- resolved: { label: 'Resolved', color: 'bg-green-100 text-green-800' },
28
- closed: { label: 'Closed', color: 'bg-gray-100 text-gray-800' },
29
- reopened: { label: 'Reopened', color: 'bg-blue-100 text-blue-800' },
30
- };
31
-
32
- const fallback = { label: props.status, color: 'bg-gray-100 text-gray-800' };
33
- const darkFallback = { label: props.status, color: 'bg-gray-500/10 text-gray-400 ring-1 ring-gray-500/20' };
34
-
35
- const config = computed(() => {
36
- const map = escDark.value ? darkConfig : lightConfig;
37
- const fb = escDark.value ? darkFallback : fallback;
38
- return map[props.status] || fb;
39
- });
40
- </script>
41
-
42
- <template>
43
- <span :class="['inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium', config.color]">
44
- {{ config.label }}
45
- </span>
46
- </template>
1
+ <script setup>
2
+ import { inject, computed } from 'vue';
3
+
4
+ const props = defineProps({
5
+ status: { type: String, required: true },
6
+ customStatus: { type: Object, default: null },
7
+ });
8
+
9
+ const escDark = inject(
10
+ 'esc-dark',
11
+ computed(() => false),
12
+ );
13
+
14
+ const darkConfig = {
15
+ open: { label: 'Open', color: 'bg-cyan-500/10 text-white ring-1 ring-cyan-500/20' },
16
+ in_progress: { label: 'In Progress', color: 'bg-violet-500/10 text-violet-400 ring-1 ring-violet-500/20' },
17
+ waiting_on_customer: {
18
+ label: 'Waiting on Customer',
19
+ color: 'bg-amber-500/10 text-amber-400 ring-1 ring-amber-500/20',
20
+ },
21
+ waiting_on_agent: {
22
+ label: 'Waiting on Agent',
23
+ color: 'bg-orange-500/10 text-orange-400 ring-1 ring-orange-500/20',
24
+ },
25
+ escalated: { label: 'Escalated', color: 'bg-rose-500/10 text-rose-400 ring-1 ring-rose-500/20' },
26
+ resolved: { label: 'Resolved', color: 'bg-emerald-500/10 text-emerald-400 ring-1 ring-emerald-500/20' },
27
+ closed: { label: 'Closed', color: 'bg-gray-500/10 text-gray-400 ring-1 ring-gray-500/20' },
28
+ reopened: { label: 'Reopened', color: 'bg-cyan-500/10 text-white ring-1 ring-cyan-500/20' },
29
+ };
30
+
31
+ const lightConfig = {
32
+ open: { label: 'Open', color: 'bg-blue-100 text-blue-800' },
33
+ in_progress: { label: 'In Progress', color: 'bg-purple-100 text-purple-800' },
34
+ waiting_on_customer: { label: 'Waiting on Customer', color: 'bg-yellow-100 text-yellow-800' },
35
+ waiting_on_agent: { label: 'Waiting on Agent', color: 'bg-orange-100 text-orange-800' },
36
+ escalated: { label: 'Escalated', color: 'bg-red-100 text-red-800' },
37
+ resolved: { label: 'Resolved', color: 'bg-green-100 text-green-800' },
38
+ closed: { label: 'Closed', color: 'bg-gray-100 text-gray-800' },
39
+ reopened: { label: 'Reopened', color: 'bg-blue-100 text-blue-800' },
40
+ };
41
+
42
+ const fallback = { label: props.status, color: 'bg-gray-100 text-gray-800' };
43
+ const darkFallback = { label: props.status, color: 'bg-gray-500/10 text-gray-400 ring-1 ring-gray-500/20' };
44
+
45
+ const config = computed(() => {
46
+ // When a customStatus object is provided, use its label and color directly
47
+ if (props.customStatus) {
48
+ const hex = props.customStatus.color || '#6b7280';
49
+ return {
50
+ label: props.customStatus.label || props.status,
51
+ color: '',
52
+ customColor: hex,
53
+ };
54
+ }
55
+ const map = escDark.value ? darkConfig : lightConfig;
56
+ const fb = escDark.value ? darkFallback : fallback;
57
+ return map[props.status] || fb;
58
+ });
59
+ </script>
60
+
61
+ <template>
62
+ <span
63
+ role="status"
64
+ :aria-label="'Status: ' + config.label"
65
+ :class="['inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium', config.color]"
66
+ :style="
67
+ config.customColor
68
+ ? {
69
+ backgroundColor: config.customColor + '1a',
70
+ color: config.customColor,
71
+ boxShadow: 'inset 0 0 0 1px ' + config.customColor + '33',
72
+ }
73
+ : {}
74
+ "
75
+ >
76
+ {{ config.label }}
77
+ </span>
78
+ </template>
@@ -1,53 +1,84 @@
1
- <script setup>
2
- import { ref, computed, inject } from 'vue';
3
-
4
- const props = defineProps({
5
- tags: { type: Array, required: true },
6
- modelValue: { type: Array, default: () => [] },
7
- });
8
-
9
- const emit = defineEmits(['update:modelValue']);
10
- const dark = inject('esc-dark', computed(() => false));
11
-
12
- const search = ref('');
13
-
14
- const filteredTags = computed(() => {
15
- if (!search.value) return props.tags;
16
- const q = search.value.toLowerCase();
17
- return props.tags.filter(t => t.name.toLowerCase().includes(q));
18
- });
19
-
20
- function toggle(tagId) {
21
- const current = [...props.modelValue];
22
- const index = current.indexOf(tagId);
23
- if (index >= 0) {
24
- current.splice(index, 1);
25
- } else {
26
- current.push(tagId);
27
- }
28
- emit('update:modelValue', current);
29
- }
30
-
31
- function isSelected(tagId) {
32
- return props.modelValue.includes(tagId);
33
- }
34
- </script>
35
-
36
- <template>
37
- <div>
38
- <label :class="['mb-1 block text-xs font-medium', dark ? 'text-neutral-500' : 'text-gray-600']">Tags</label>
39
- <input v-model="search" type="text" placeholder="Filter tags..."
40
- :class="['mb-2 w-full rounded-md border px-2 py-1 text-xs focus:outline-none',
41
- dark ? 'border-white/10 bg-neutral-950 text-neutral-200 placeholder-neutral-600 focus:border-white/20 focus:ring-1 focus:ring-white/10' : 'border-gray-300 focus:border-blue-500']" />
42
- <div class="flex flex-wrap gap-1">
43
- <button v-for="tag in filteredTags" :key="tag.id" @click="toggle(tag.id)"
44
- :class="['rounded-full px-2 py-0.5 text-xs font-medium transition-colors',
45
- dark
46
- ? (isSelected(tag.id) ? 'bg-white/[0.12] text-white ring-1 ring-white/20' : 'bg-white/[0.04] text-neutral-500 hover:bg-white/[0.08]')
47
- : (isSelected(tag.id) ? 'bg-blue-100 text-blue-700 ring-1 ring-blue-300' : 'bg-gray-100 text-gray-600 hover:bg-gray-200')]"
48
- :style="tag.color ? { backgroundColor: isSelected(tag.id) ? tag.color + '33' : undefined, color: isSelected(tag.id) ? tag.color : undefined } : {}">
49
- {{ tag.name }}
50
- </button>
51
- </div>
52
- </div>
53
- </template>
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+
4
+ const props = defineProps({
5
+ tags: { type: Array, required: true },
6
+ modelValue: { type: Array, default: () => [] },
7
+ });
8
+
9
+ const emit = defineEmits(['update:modelValue']);
10
+ const dark = inject(
11
+ 'esc-dark',
12
+ computed(() => false),
13
+ );
14
+
15
+ const search = ref('');
16
+
17
+ const filteredTags = computed(() => {
18
+ if (!search.value) return props.tags;
19
+ const q = search.value.toLowerCase();
20
+ return props.tags.filter((t) => t.name.toLowerCase().includes(q));
21
+ });
22
+
23
+ function toggle(tagId) {
24
+ const current = [...props.modelValue];
25
+ const index = current.indexOf(tagId);
26
+ if (index >= 0) {
27
+ current.splice(index, 1);
28
+ } else {
29
+ current.push(tagId);
30
+ }
31
+ emit('update:modelValue', current);
32
+ }
33
+
34
+ function isSelected(tagId) {
35
+ return props.modelValue.includes(tagId);
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <div>
41
+ <label
42
+ :class="['mb-1 block text-xs font-medium', dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-600']"
43
+ >Tags</label
44
+ >
45
+ <input
46
+ v-model="search"
47
+ type="text"
48
+ placeholder="Filter tags..."
49
+ :class="[
50
+ 'mb-2 w-full rounded-md border px-2 py-1 text-xs focus:outline-none',
51
+ dark
52
+ ? 'border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] text-[var(--esc-panel-text-secondary)] placeholder-[var(--esc-panel-text-muted)] focus:border-[var(--esc-panel-border-input)] focus:ring-1 focus:ring-[var(--esc-panel-border-input)]'
53
+ : 'border-gray-300 focus:border-blue-500',
54
+ ]"
55
+ />
56
+ <div class="flex flex-wrap gap-1">
57
+ <button
58
+ v-for="tag in filteredTags"
59
+ :key="tag.id"
60
+ :class="[
61
+ 'rounded-full px-2 py-0.5 text-xs font-medium transition-colors',
62
+ dark
63
+ ? isSelected(tag.id)
64
+ ? 'bg-[var(--esc-panel-active)] text-[var(--esc-panel-text)] ring-1 ring-[var(--esc-panel-border-input)]'
65
+ : 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-muted)] hover:bg-[var(--esc-panel-active)]'
66
+ : isSelected(tag.id)
67
+ ? 'bg-blue-100 text-blue-700 ring-1 ring-blue-300'
68
+ : 'bg-gray-100 text-gray-600 hover:bg-gray-200',
69
+ ]"
70
+ :style="
71
+ tag.color
72
+ ? {
73
+ backgroundColor: isSelected(tag.id) ? tag.color + '33' : undefined,
74
+ color: isSelected(tag.id) ? tag.color : undefined,
75
+ }
76
+ : {}
77
+ "
78
+ @click="toggle(tag.id)"
79
+ >
80
+ {{ tag.name }}
81
+ </button>
82
+ </div>
83
+ </div>
84
+ </template>
@@ -1,69 +1,270 @@
1
1
  <script setup>
2
- import { inject, computed, reactive, watch } from 'vue';
2
+ import { inject, computed, reactive, ref, watch } from 'vue';
3
+ import { router } from '@inertiajs/vue3';
4
+ import { TICKET_STATUSES, TICKET_PRIORITIES } from '../utils/constants';
3
5
 
4
6
  const props = defineProps({
5
- statuses: { type: Array, default: () => ['open', 'in_progress', 'waiting_on_customer', 'waiting_on_agent', 'escalated', 'resolved', 'closed'] },
6
- priorities: { type: Array, default: () => ['low', 'medium', 'high', 'urgent', 'critical'] },
7
+ statuses: {
8
+ type: Array,
9
+ default: () => TICKET_STATUSES,
10
+ },
11
+ priorities: { type: Array, default: () => TICKET_PRIORITIES },
7
12
  agents: { type: Array, default: () => [] },
8
13
  departments: { type: Array, default: () => [] },
14
+ tags: { type: Array, default: () => [] },
15
+ filters: { type: Object, default: () => ({}) },
16
+ route: { type: String, required: true },
9
17
  modelValue: { type: Object, default: () => ({}) },
10
18
  showFollowing: { type: Boolean, default: false },
19
+ showAssignee: { type: Boolean, default: false },
11
20
  });
12
21
 
13
- const emit = defineEmits(['update:modelValue']);
14
- const escDark = inject('esc-dark', computed(() => false));
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 };
15
29
 
16
- const filters = reactive({
17
- status: props.modelValue.status || '',
18
- priority: props.modelValue.priority || '',
19
- assigned_to: props.modelValue.assigned_to || '',
20
- department_id: props.modelValue.department_id || '',
21
- search: props.modelValue.search || '',
22
- following: props.modelValue.following || '',
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 || '',
23
45
  });
24
46
 
25
- watch(filters, (val) => {
26
- emit('update:modelValue', { ...val });
27
- }, { deep: true });
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;
28
53
 
29
- const inputClass = computed(() => escDark.value
30
- ? 'rounded-lg border border-white/10 bg-gray-900 px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10'
31
- : 'rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none'
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',
32
80
  );
33
81
 
34
- const selectClass = computed(() => escDark.value
35
- ? 'rounded-lg border border-white/10 bg-gray-900 px-2 py-1.5 text-sm text-gray-200 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10'
36
- : 'rounded-md border border-gray-300 px-2 py-1.5 text-sm'
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',
37
86
  );
38
87
 
39
- const checkboxClass = computed(() => escDark.value
40
- ? 'h-4 w-4 rounded border-white/20 bg-neutral-950 text-cyan-500 focus:ring-cyan-500/20'
41
- : 'h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500'
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',
42
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 });
43
109
  </script>
44
110
 
45
111
  <template>
46
- <div class="flex flex-wrap items-center gap-3">
47
- <input v-model="filters.search" type="text" placeholder="Search tickets..." :class="inputClass" />
48
- <select v-model="filters.status" :class="selectClass">
49
- <option value="">All Statuses</option>
50
- <option v-for="s in statuses" :key="s" :value="s">{{ s.replace(/_/g, ' ') }}</option>
51
- </select>
52
- <select v-model="filters.priority" :class="selectClass">
53
- <option value="">All Priorities</option>
54
- <option v-for="p in priorities" :key="p" :value="p">{{ p }}</option>
55
- </select>
56
- <select v-if="agents.length" v-model="filters.assigned_to" :class="selectClass">
57
- <option value="">All Agents</option>
58
- <option v-for="a in agents" :key="a.id" :value="a.id">{{ a.name }}</option>
59
- </select>
60
- <select v-if="departments.length" v-model="filters.department_id" :class="selectClass">
61
- <option value="">All Departments</option>
62
- <option v-for="d in departments" :key="d.id" :value="d.id">{{ d.name }}</option>
63
- </select>
64
- <label v-if="showFollowing" class="flex items-center gap-2">
65
- <input v-model="filters.following" type="checkbox" true-value="1" false-value="" :class="checkboxClass" />
66
- <span :class="['text-sm', escDark ? 'text-neutral-400' : 'text-gray-600']">Following</span>
67
- </label>
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>
68
269
  </div>
69
270
  </template>