@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,128 +1,180 @@
1
- <script setup>
2
- import { ref, computed, inject } from 'vue';
3
- import { router } from '@inertiajs/vue3';
4
-
5
- const props = defineProps({
6
- action: { type: String, required: true },
7
- existingRating: { type: Object, default: null },
8
- });
9
-
10
- const escDark = inject('esc-dark', computed(() => false));
11
- const hoveredStar = ref(0);
12
- const selectedRating = ref(props.existingRating?.rating || 0);
13
- const comment = ref(props.existingRating?.comment || '');
14
- const processing = ref(false);
15
- const submitted = ref(false);
16
-
17
- const isReadOnly = computed(() => !!props.existingRating);
18
-
19
- const displayRating = computed(() => {
20
- if (isReadOnly.value) return props.existingRating.rating;
21
- if (hoveredStar.value > 0) return hoveredStar.value;
22
- return selectedRating.value;
23
- });
24
-
25
- const ratingLabels = ['', 'Terrible', 'Poor', 'Okay', 'Good', 'Excellent'];
26
-
27
- function selectStar(star) {
28
- if (isReadOnly.value) return;
29
- selectedRating.value = star;
30
- }
31
-
32
- function hoverStar(star) {
33
- if (isReadOnly.value) return;
34
- hoveredStar.value = star;
35
- }
36
-
37
- function leaveStars() {
38
- hoveredStar.value = 0;
39
- }
40
-
41
- function submit() {
42
- if (!selectedRating.value || processing.value || isReadOnly.value) return;
43
- processing.value = true;
44
- router.post(props.action, {
45
- rating: selectedRating.value,
46
- comment: comment.value || null,
47
- }, {
48
- preserveScroll: true,
49
- onSuccess: () => {
50
- submitted.value = true;
51
- },
52
- onFinish: () => {
53
- processing.value = false;
54
- },
55
- });
56
- }
57
- </script>
58
-
59
- <template>
60
- <div :class="['rounded-xl border p-5',
61
- escDark ? 'border-white/[0.06] bg-neutral-900/60' : 'border-gray-200 bg-white']">
62
- <h3 :class="['text-sm font-semibold',
63
- escDark ? 'text-neutral-200' : 'text-gray-900']">
64
- {{ isReadOnly ? 'Customer Rating' : 'How was your experience?' }}
65
- </h3>
66
-
67
- <!-- Stars -->
68
- <div class="mt-3 flex items-center gap-1" @mouseleave="leaveStars">
69
- <button v-for="star in 5" :key="star"
70
- @click="selectStar(star)"
71
- @mouseenter="hoverStar(star)"
72
- :disabled="isReadOnly"
73
- :class="['transition-transform', !isReadOnly && 'hover:scale-110', isReadOnly && 'cursor-default']">
74
- <!-- Filled star -->
75
- <svg v-if="star <= displayRating" class="h-7 w-7" viewBox="0 0 24 24" fill="currentColor"
76
- :class="escDark ? 'text-amber-400' : 'text-yellow-400'">
77
- <path d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z" />
78
- </svg>
79
- <!-- Empty star -->
80
- <svg v-else class="h-7 w-7" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"
81
- :class="escDark ? 'text-neutral-600' : 'text-gray-300'">
82
- <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
83
- </svg>
84
- </button>
85
- <span v-if="displayRating > 0"
86
- :class="['ml-2 text-sm font-medium',
87
- escDark ? 'text-neutral-400' : 'text-gray-500']">
88
- {{ ratingLabels[displayRating] }}
89
- </span>
90
- </div>
91
-
92
- <!-- Comment -->
93
- <div v-if="!isReadOnly" class="mt-4">
94
- <textarea v-model="comment" rows="3"
95
- placeholder="Any additional feedback? (optional)"
96
- :class="['w-full rounded-lg border px-3 py-2 text-sm focus:outline-none',
97
- escDark
98
- ? 'border-white/10 bg-neutral-950 text-neutral-200 placeholder-neutral-600 focus:border-white/20 focus:ring-1 focus:ring-white/10'
99
- : 'border-gray-300 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500']">
100
- </textarea>
101
- </div>
102
-
103
- <!-- Read-only comment display -->
104
- <div v-if="isReadOnly && existingRating.comment" class="mt-3">
105
- <p :class="['text-sm italic', escDark ? 'text-neutral-400' : 'text-gray-600']">
106
- "{{ existingRating.comment }}"
107
- </p>
108
- </div>
109
-
110
- <!-- Submit -->
111
- <div v-if="!isReadOnly" class="mt-3">
112
- <div v-if="submitted"
113
- :class="['rounded-lg px-3 py-2 text-sm font-medium',
114
- escDark ? 'bg-emerald-500/10 text-emerald-400 ring-1 ring-emerald-500/20' : 'bg-green-50 text-green-700']">
115
- Thank you for your feedback!
116
- </div>
117
- <button v-else @click="submit"
118
- :disabled="!selectedRating || processing"
119
- :class="['rounded-lg px-4 py-2 text-sm font-medium text-white transition-all',
120
- escDark
121
- ? 'bg-gradient-to-r from-cyan-500 to-violet-500 hover:from-cyan-400 hover:to-violet-400'
122
- : 'bg-blue-600 hover:bg-blue-700',
123
- (!selectedRating || processing) && 'cursor-not-allowed opacity-40']">
124
- Submit Rating
125
- </button>
126
- </div>
127
- </div>
128
- </template>
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+ import { router } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ action: { type: String, required: true },
7
+ existingRating: { type: Object, default: null },
8
+ });
9
+
10
+ const escDark = inject(
11
+ 'esc-dark',
12
+ computed(() => false),
13
+ );
14
+ const hoveredStar = ref(0);
15
+ const selectedRating = ref(props.existingRating?.rating || 0);
16
+ const comment = ref(props.existingRating?.comment || '');
17
+ const processing = ref(false);
18
+ const submitted = ref(false);
19
+
20
+ const isReadOnly = computed(() => !!props.existingRating);
21
+
22
+ const displayRating = computed(() => {
23
+ if (isReadOnly.value) return props.existingRating.rating;
24
+ if (hoveredStar.value > 0) return hoveredStar.value;
25
+ return selectedRating.value;
26
+ });
27
+
28
+ const ratingLabels = ['', 'Terrible', 'Poor', 'Okay', 'Good', 'Excellent'];
29
+
30
+ function selectStar(star) {
31
+ if (isReadOnly.value) return;
32
+ selectedRating.value = star;
33
+ }
34
+
35
+ function hoverStar(star) {
36
+ if (isReadOnly.value) return;
37
+ hoveredStar.value = star;
38
+ }
39
+
40
+ function leaveStars() {
41
+ hoveredStar.value = 0;
42
+ }
43
+
44
+ function submit() {
45
+ if (!selectedRating.value || processing.value || isReadOnly.value) return;
46
+ processing.value = true;
47
+ router.post(
48
+ props.action,
49
+ {
50
+ rating: selectedRating.value,
51
+ comment: comment.value || null,
52
+ },
53
+ {
54
+ preserveScroll: true,
55
+ onSuccess: () => {
56
+ submitted.value = true;
57
+ },
58
+ onFinish: () => {
59
+ processing.value = false;
60
+ },
61
+ },
62
+ );
63
+ }
64
+ </script>
65
+
66
+ <template>
67
+ <div
68
+ :class="[
69
+ 'rounded-xl border p-5',
70
+ escDark ? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)]' : 'border-gray-200 bg-white',
71
+ ]"
72
+ >
73
+ <h3 :class="['text-sm font-semibold', escDark ? 'text-[var(--esc-panel-text-secondary)]' : 'text-gray-900']">
74
+ {{ isReadOnly ? 'Customer Rating' : 'How was your experience?' }}
75
+ </h3>
76
+
77
+ <!-- Stars -->
78
+ <div class="mt-3 flex items-center gap-1" @mouseleave="leaveStars">
79
+ <button
80
+ v-for="star in 5"
81
+ :key="star"
82
+ :disabled="isReadOnly"
83
+ :aria-label="'Rate ' + star + ' star' + (star !== 1 ? 's' : '')"
84
+ :class="['transition-transform', !isReadOnly && 'hover:scale-110', isReadOnly && 'cursor-default']"
85
+ @click="selectStar(star)"
86
+ @mouseenter="hoverStar(star)"
87
+ >
88
+ <!-- Filled star -->
89
+ <svg
90
+ v-if="star <= displayRating"
91
+ class="h-7 w-7"
92
+ viewBox="0 0 24 24"
93
+ fill="currentColor"
94
+ :class="escDark ? 'text-amber-400' : 'text-yellow-400'"
95
+ >
96
+ <path
97
+ d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z"
98
+ />
99
+ </svg>
100
+ <!-- Empty star -->
101
+ <svg
102
+ v-else
103
+ class="h-7 w-7"
104
+ fill="none"
105
+ stroke="currentColor"
106
+ stroke-width="1.5"
107
+ viewBox="0 0 24 24"
108
+ :class="escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-300'"
109
+ >
110
+ <path
111
+ stroke-linecap="round"
112
+ stroke-linejoin="round"
113
+ d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"
114
+ />
115
+ </svg>
116
+ </button>
117
+ <span
118
+ v-if="displayRating > 0"
119
+ :class="[
120
+ 'ml-2 text-sm font-medium',
121
+ escDark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-500',
122
+ ]"
123
+ >
124
+ {{ ratingLabels[displayRating] }}
125
+ </span>
126
+ </div>
127
+
128
+ <!-- Comment -->
129
+ <div v-if="!isReadOnly" class="mt-4">
130
+ <textarea
131
+ v-model="comment"
132
+ rows="3"
133
+ placeholder="Any additional feedback? (optional)"
134
+ :class="[
135
+ 'w-full rounded-lg border px-3 py-2 text-sm focus:outline-none',
136
+ escDark
137
+ ? '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)]'
138
+ : 'border-gray-300 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500',
139
+ ]"
140
+ >
141
+ </textarea>
142
+ </div>
143
+
144
+ <!-- Read-only comment display -->
145
+ <div v-if="isReadOnly && existingRating.comment" class="mt-3">
146
+ <p :class="['text-sm italic', escDark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600']">
147
+ "{{ existingRating.comment }}"
148
+ </p>
149
+ </div>
150
+
151
+ <!-- Submit -->
152
+ <div v-if="!isReadOnly" class="mt-3">
153
+ <div
154
+ v-if="submitted"
155
+ :class="[
156
+ 'rounded-lg px-3 py-2 text-sm font-medium',
157
+ escDark
158
+ ? 'bg-emerald-500/10 text-emerald-400 ring-1 ring-emerald-500/20'
159
+ : 'bg-green-50 text-green-700',
160
+ ]"
161
+ >
162
+ Thank you for your feedback!
163
+ </div>
164
+ <button
165
+ v-else
166
+ :disabled="!selectedRating || processing"
167
+ :class="[
168
+ 'rounded-lg px-4 py-2 text-sm font-medium text-white transition-all',
169
+ escDark
170
+ ? 'bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] hover:from-[var(--esc-panel-accent-hover)] hover:to-[var(--esc-panel-accent-secondary-hover)]'
171
+ : 'bg-blue-600 hover:bg-blue-700',
172
+ (!selectedRating || processing) && 'cursor-not-allowed opacity-40',
173
+ ]"
174
+ @click="submit"
175
+ >
176
+ Submit Rating
177
+ </button>
178
+ </div>
179
+ </div>
180
+ </template>
@@ -0,0 +1,172 @@
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+
4
+ const props = defineProps({
5
+ show: { type: Boolean, default: false },
6
+ filters: { type: Object, default: () => ({}) },
7
+ });
8
+
9
+ const emit = defineEmits(['close', 'saved']);
10
+ const escDark = inject(
11
+ 'esc-dark',
12
+ computed(() => false),
13
+ );
14
+
15
+ const name = ref('');
16
+ const isShared = ref(false);
17
+ const saving = ref(false);
18
+ const error = ref('');
19
+
20
+ function reset() {
21
+ name.value = '';
22
+ isShared.value = false;
23
+ error.value = '';
24
+ }
25
+
26
+ async function save() {
27
+ if (!name.value.trim()) {
28
+ error.value = 'Name is required.';
29
+ return;
30
+ }
31
+
32
+ const nonEmptyFilters = {};
33
+ for (const [key, value] of Object.entries(props.filters)) {
34
+ if (value !== '' && value !== null && value !== undefined) {
35
+ nonEmptyFilters[key] = value;
36
+ }
37
+ }
38
+
39
+ if (Object.keys(nonEmptyFilters).length === 0) {
40
+ error.value = 'Apply some filters first before saving a view.';
41
+ return;
42
+ }
43
+
44
+ saving.value = true;
45
+ error.value = '';
46
+
47
+ try {
48
+ const response = await fetch(route('escalated.admin.saved-views.store'), {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
53
+ Accept: 'application/json',
54
+ 'X-Requested-With': 'XMLHttpRequest',
55
+ },
56
+ body: JSON.stringify({
57
+ name: name.value.trim(),
58
+ filters: nonEmptyFilters,
59
+ is_shared: isShared.value,
60
+ }),
61
+ });
62
+
63
+ if (!response.ok) {
64
+ const data = await response.json();
65
+ error.value = data.message || 'Failed to save view.';
66
+ return;
67
+ }
68
+
69
+ emit('saved');
70
+ reset();
71
+ emit('close');
72
+ } catch {
73
+ error.value = 'Failed to save view.';
74
+ } finally {
75
+ saving.value = false;
76
+ }
77
+ }
78
+
79
+ function cancel() {
80
+ reset();
81
+ emit('close');
82
+ }
83
+
84
+ const overlayClass = computed(() => (escDark.value ? 'bg-black/60' : 'bg-black/30'));
85
+
86
+ const panelClass = computed(() =>
87
+ escDark.value
88
+ ? 'border border-white/10 bg-[var(--esc-panel-surface)] text-neutral-200 shadow-xl'
89
+ : 'bg-white text-gray-900 shadow-xl',
90
+ );
91
+
92
+ const inputClass = computed(() =>
93
+ escDark.value
94
+ ? 'rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-500 focus:border-cyan-500/40 focus:outline-none focus:ring-1 focus:ring-cyan-500/40'
95
+ : 'rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500',
96
+ );
97
+ </script>
98
+
99
+ <template>
100
+ <Teleport to="body">
101
+ <div v-if="show" class="fixed inset-0 z-50 flex items-center justify-center" @click.self="cancel">
102
+ <div :class="['absolute inset-0', overlayClass]" @click="cancel"></div>
103
+ <div :class="['relative w-full max-w-md rounded-lg p-6', panelClass]">
104
+ <h3 :class="['mb-4 text-lg font-semibold', escDark ? 'text-neutral-100' : 'text-gray-900']">
105
+ Save Current View
106
+ </h3>
107
+
108
+ <div class="space-y-4">
109
+ <div>
110
+ <label
111
+ :class="['mb-1 block text-sm font-medium', escDark ? 'text-neutral-400' : 'text-gray-700']"
112
+ >
113
+ Name
114
+ </label>
115
+ <input
116
+ v-model="name"
117
+ type="text"
118
+ placeholder="e.g. My Open Tickets"
119
+ :class="['w-full', inputClass]"
120
+ @keydown.enter="save"
121
+ />
122
+ </div>
123
+
124
+ <label class="flex items-center gap-2">
125
+ <input
126
+ v-model="isShared"
127
+ type="checkbox"
128
+ :class="
129
+ escDark
130
+ ? 'h-4 w-4 rounded border-white/10 bg-white/5 text-cyan-500 focus:ring-cyan-500/20'
131
+ : 'h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500'
132
+ "
133
+ />
134
+ <span :class="['text-sm', escDark ? 'text-neutral-400' : 'text-gray-600']">
135
+ Share with all agents
136
+ </span>
137
+ </label>
138
+
139
+ <p v-if="error" :class="['text-sm', escDark ? 'text-red-400' : 'text-red-600']">
140
+ {{ error }}
141
+ </p>
142
+ </div>
143
+
144
+ <div class="mt-6 flex justify-end gap-3">
145
+ <button
146
+ type="button"
147
+ :class="[
148
+ 'rounded-md px-4 py-2 text-sm font-medium',
149
+ escDark ? 'text-neutral-400 hover:text-neutral-200' : 'text-gray-600 hover:text-gray-800',
150
+ ]"
151
+ @click="cancel"
152
+ >
153
+ Cancel
154
+ </button>
155
+ <button
156
+ type="button"
157
+ :disabled="saving"
158
+ :class="[
159
+ 'rounded-md px-4 py-2 text-sm font-medium disabled:opacity-50',
160
+ escDark
161
+ ? 'bg-cyan-600 text-white hover:bg-cyan-500'
162
+ : 'bg-blue-600 text-white hover:bg-blue-700',
163
+ ]"
164
+ @click="save"
165
+ >
166
+ {{ saving ? 'Saving...' : 'Save View' }}
167
+ </button>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </Teleport>
172
+ </template>