@escalated-dev/escalated 0.7.0 → 0.8.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 (181) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +36 -1
  3. package/package.json +16 -7
  4. package/src/components/ActiveChatsPanel.vue +246 -0
  5. package/src/components/ActivityTimeline.stories.js +55 -55
  6. package/src/components/ActivityTimeline.vue +83 -83
  7. package/src/components/AdminDashboard.stories.js +1423 -1095
  8. package/src/components/AgentLoadIndicator.stories.js +37 -37
  9. package/src/components/AgentLoadIndicator.vue +49 -49
  10. package/src/components/AgentPerformanceCard.vue +44 -44
  11. package/src/components/ArticleEditor.vue +82 -82
  12. package/src/components/AssigneeSelect.vue +37 -37
  13. package/src/components/AttachmentList.vue +13 -1
  14. package/src/components/AuditLogEntry.vue +95 -95
  15. package/src/components/BulkActionBar.vue +166 -166
  16. package/src/components/CategoryTree.vue +89 -89
  17. package/src/components/ChartWidget.vue +46 -46
  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 -30
  24. package/src/components/CollisionWarning.vue +68 -68
  25. package/src/components/ConditionalFieldRules.vue +135 -135
  26. package/src/components/ContextPanel.vue +66 -66
  27. package/src/components/ContextPanelSection.vue +65 -65
  28. package/src/components/CustomFieldRenderer.vue +159 -159
  29. package/src/components/EscalatedLayout.vue +35 -6
  30. package/src/components/FileDropzone.vue +57 -57
  31. package/src/components/FollowButton.stories.js +30 -30
  32. package/src/components/FollowButton.vue +88 -88
  33. package/src/components/KeyboardShortcutHelp.vue +213 -213
  34. package/src/components/KnowledgePanel.vue +122 -122
  35. package/src/components/KpiCard.stories.js +32 -32
  36. package/src/components/KpiCard.vue +88 -88
  37. package/src/components/MacroDropdown.vue +150 -150
  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 -107
  43. package/src/components/PinnedNotes.vue +129 -129
  44. package/src/components/PluginSlot.vue +68 -62
  45. package/src/components/PresenceIndicator.vue +46 -2
  46. package/src/components/PriorityBadge.stories.js +33 -33
  47. package/src/components/PriorityBadge.vue +46 -46
  48. package/src/components/ReplyComposer.vue +81 -18
  49. package/src/components/ReplyThread.vue +40 -2
  50. package/src/components/ReportChart.vue +395 -0
  51. package/src/components/ReportExportButton.vue +108 -0
  52. package/src/components/SatisfactionRating.vue +180 -180
  53. package/src/components/SaveViewDialog.vue +172 -0
  54. package/src/components/SavedViewSidebar.vue +208 -0
  55. package/src/components/ScheduleEditor.vue +104 -104
  56. package/src/components/SideConversation.vue +165 -165
  57. package/src/components/SideConversationList.vue +147 -147
  58. package/src/components/SkillTagManager.vue +83 -79
  59. package/src/components/SlaComplianceChart.vue +46 -53
  60. package/src/components/SlaTimer.stories.js +35 -35
  61. package/src/components/SlaTimer.vue +62 -62
  62. package/src/components/SnoozeButton.vue +247 -0
  63. package/src/components/StatsCard.stories.js +24 -24
  64. package/src/components/StatsCard.vue +58 -58
  65. package/src/components/StatusBadge.stories.js +53 -53
  66. package/src/components/StatusBadge.vue +78 -78
  67. package/src/components/TagSelect.vue +84 -84
  68. package/src/components/TicketFilters.vue +270 -225
  69. package/src/components/TicketLinkPanel.vue +193 -193
  70. package/src/components/TicketList.vue +655 -594
  71. package/src/components/TicketMergeDialog.vue +157 -157
  72. package/src/components/TicketSplitDialog.vue +98 -0
  73. package/src/components/TotpInput.stories.js +9 -9
  74. package/src/components/TotpInput.vue +81 -81
  75. package/src/components/TwoFactorSetup.vue +113 -113
  76. package/src/components/VariableTokenInput.vue +111 -0
  77. package/src/components/WorkflowActionConfig.vue +287 -0
  78. package/src/components/WorkflowActionList.vue +350 -0
  79. package/src/components/WorkflowConditionBuilder.vue +265 -0
  80. package/src/components/WorkflowTriggerSelector.vue +125 -0
  81. package/src/composables/useChat.js +187 -0
  82. package/src/composables/useI18n.js +30 -8
  83. package/src/composables/useKeyboardShortcuts.js +46 -46
  84. package/src/composables/useMentions.js +134 -0
  85. package/src/composables/usePluginExtensions.js +159 -159
  86. package/src/composables/usePluginHooks.js +65 -65
  87. package/src/composables/useRealtime.js +182 -0
  88. package/src/defineEscalatedPlugin.js +76 -76
  89. package/src/index.js +84 -72
  90. package/src/locales/README.md +13 -0
  91. package/src/locales/ar.json +727 -0
  92. package/src/locales/de.json +678 -430
  93. package/src/locales/en.json +717 -455
  94. package/src/locales/es.json +679 -430
  95. package/src/locales/fr.json +680 -431
  96. package/src/locales/index.js +12 -2
  97. package/src/locales/it.json +727 -0
  98. package/src/locales/ja.json +727 -0
  99. package/src/locales/ko.json +727 -0
  100. package/src/locales/nl.json +727 -0
  101. package/src/locales/pl.json +727 -0
  102. package/src/locales/pt-BR.json +727 -0
  103. package/src/locales/ru.json +727 -0
  104. package/src/locales/tr.json +727 -0
  105. package/src/locales/zh-CN.json +727 -0
  106. package/src/pages/Admin/ApiTokens/Index.vue +381 -381
  107. package/src/pages/Admin/AuditLog/Index.vue +154 -154
  108. package/src/pages/Admin/BusinessHours/Form.vue +172 -172
  109. package/src/pages/Admin/BusinessHours/Index.vue +113 -113
  110. package/src/pages/Admin/CannedResponses/Index.vue +127 -127
  111. package/src/pages/Admin/Capacity/Index.vue +134 -134
  112. package/src/pages/Admin/CustomFields/Form.vue +195 -195
  113. package/src/pages/Admin/CustomFields/Index.vue +134 -134
  114. package/src/pages/Admin/CustomObjects/Form.vue +202 -202
  115. package/src/pages/Admin/CustomObjects/Index.vue +127 -127
  116. package/src/pages/Admin/CustomObjects/Records.vue +228 -228
  117. package/src/pages/Admin/Departments/Form.vue +75 -75
  118. package/src/pages/Admin/Departments/Index.vue +110 -109
  119. package/src/pages/Admin/EscalationRules/Form.vue +157 -157
  120. package/src/pages/Admin/EscalationRules/Index.vue +178 -178
  121. package/src/pages/Admin/Import/Connect.vue +0 -1
  122. package/src/pages/Admin/Import/Index.vue +1 -1
  123. package/src/pages/Admin/Import/Progress.vue +1 -1
  124. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
  125. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
  126. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
  127. package/src/pages/Admin/Macros/Index.vue +448 -448
  128. package/src/pages/Admin/Plugins/Index.vue +327 -327
  129. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  130. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
  131. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  132. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  133. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  134. package/src/pages/Admin/Reports/CsatReport.vue +127 -127
  135. package/src/pages/Admin/Reports/Dashboard.vue +36 -21
  136. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  137. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  138. package/src/pages/Admin/Reports/SlaReport.vue +162 -166
  139. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  140. package/src/pages/Admin/Reports.vue +206 -206
  141. package/src/pages/Admin/Roles/Form.vue +71 -71
  142. package/src/pages/Admin/Roles/Index.vue +122 -122
  143. package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
  144. package/src/pages/Admin/Settings/DataRetention.vue +182 -182
  145. package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
  146. package/src/pages/Admin/Settings/Sandbox.vue +135 -135
  147. package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
  148. package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
  149. package/src/pages/Admin/Settings.vue +1008 -468
  150. package/src/pages/Admin/Skills/Form.vue +193 -53
  151. package/src/pages/Admin/Skills/Index.vue +105 -94
  152. package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
  153. package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
  154. package/src/pages/Admin/Statuses/Form.vue +114 -114
  155. package/src/pages/Admin/Statuses/Index.vue +153 -153
  156. package/src/pages/Admin/Tags/Index.vue +191 -191
  157. package/src/pages/Admin/Tickets/Index.vue +126 -91
  158. package/src/pages/Admin/Tickets/Show.vue +29 -0
  159. package/src/pages/Admin/Users/Index.vue +145 -0
  160. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
  161. package/src/pages/Admin/Webhooks/Form.vue +110 -110
  162. package/src/pages/Admin/Webhooks/Index.vue +136 -136
  163. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  164. package/src/pages/Admin/Workflows/Index.vue +235 -0
  165. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  166. package/src/pages/Agent/TicketIndex.vue +112 -88
  167. package/src/pages/Agent/TicketShow.vue +192 -60
  168. package/src/pages/Customer/Create.vue +1 -1
  169. package/src/pages/Customer/Index.vue +26 -1
  170. package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
  171. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
  172. package/src/pages/Customer/Show.vue +69 -63
  173. package/src/pages/Guest/Create.vue +40 -15
  174. package/src/pages/Guest/Show.vue +124 -93
  175. package/src/plugin.js +26 -1
  176. package/src/utils/mentionHighlight.js +24 -0
  177. package/src/utils/sanitizeHtml.js +78 -64
  178. package/src/widget/EscalatedWidget.vue +804 -0
  179. package/src/widget/index.js +254 -0
  180. package/src/pages/Admin/Automations/Form.vue +0 -184
  181. package/src/pages/Admin/Automations/Index.vue +0 -129
@@ -0,0 +1,98 @@
1
+ <script setup>
2
+ import { ref, computed } from 'vue';
3
+
4
+ const props = defineProps({
5
+ modelValue: { type: Number, default: 30 },
6
+ options: { type: Array, default: () => [7, 30, 90, 365] },
7
+ });
8
+
9
+ const emit = defineEmits(['update:modelValue']);
10
+
11
+ const showCustom = ref(false);
12
+ const customStart = ref('');
13
+ const customEnd = ref('');
14
+
15
+ const isCustom = computed(() => !props.options.includes(props.modelValue));
16
+
17
+ function selectPeriod(days) {
18
+ showCustom.value = false;
19
+ emit('update:modelValue', days);
20
+ }
21
+
22
+ function applyCustom() {
23
+ if (customStart.value && customEnd.value) {
24
+ const start = new Date(customStart.value);
25
+ const end = new Date(customEnd.value);
26
+ const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
27
+ if (days > 0) {
28
+ emit('update:modelValue', days);
29
+ showCustom.value = false;
30
+ }
31
+ }
32
+ }
33
+
34
+ function formatLabel(days) {
35
+ if (days === 365) return '1y';
36
+ return `${days}d`;
37
+ }
38
+ </script>
39
+
40
+ <template>
41
+ <div class="flex items-center gap-2">
42
+ <button
43
+ v-for="d in options"
44
+ :key="d"
45
+ :class="[
46
+ 'rounded-lg px-3.5 py-1.5 text-sm font-medium transition-all',
47
+ modelValue === d
48
+ ? 'bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] text-white shadow-lg shadow-[var(--esc-panel-bg)]/20'
49
+ : 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]',
50
+ ]"
51
+ @click="selectPeriod(d)"
52
+ >
53
+ {{ formatLabel(d) }}
54
+ </button>
55
+ <div class="relative">
56
+ <button
57
+ :class="[
58
+ 'rounded-lg px-3.5 py-1.5 text-sm font-medium transition-all',
59
+ isCustom
60
+ ? 'bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] text-white shadow-lg shadow-[var(--esc-panel-bg)]/20'
61
+ : 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]',
62
+ ]"
63
+ @click="showCustom = !showCustom"
64
+ >
65
+ Custom
66
+ </button>
67
+ <div
68
+ v-if="showCustom"
69
+ class="absolute right-0 top-full z-10 mt-2 rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-4 shadow-xl"
70
+ >
71
+ <div class="flex flex-col gap-3">
72
+ <label class="text-xs font-medium text-[var(--esc-panel-text-muted)]">
73
+ Start
74
+ <input
75
+ v-model="customStart"
76
+ type="date"
77
+ class="mt-1 block w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] px-3 py-1.5 text-sm text-[var(--esc-panel-text)]"
78
+ />
79
+ </label>
80
+ <label class="text-xs font-medium text-[var(--esc-panel-text-muted)]">
81
+ End
82
+ <input
83
+ v-model="customEnd"
84
+ type="date"
85
+ class="mt-1 block w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] px-3 py-1.5 text-sm text-[var(--esc-panel-text)]"
86
+ />
87
+ </label>
88
+ <button
89
+ class="rounded-lg bg-[var(--esc-panel-accent)] px-4 py-1.5 text-sm font-medium text-white transition-all hover:opacity-90"
90
+ @click="applyCustom"
91
+ >
92
+ Apply
93
+ </button>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </template>
@@ -1,107 +1,107 @@
1
- <script setup>
2
- import { computed } from 'vue';
3
-
4
- const props = defineProps({
5
- permissions: {
6
- type: Object,
7
- required: true,
8
- },
9
- modelValue: {
10
- type: Array,
11
- default: () => [],
12
- },
13
- });
14
-
15
- const emit = defineEmits(['update:modelValue']);
16
-
17
- const groups = computed(() => Object.keys(props.permissions || {}));
18
-
19
- const actions = ['view', 'create', 'edit', 'delete'];
20
-
21
- function getPermission(group, action) {
22
- const perms = props.permissions[group] || [];
23
- return perms.find((p) => p.slug === `${group.toLowerCase()}.${action}` || p.slug.endsWith(`.${action}`));
24
- }
25
-
26
- function isChecked(permissionId) {
27
- return props.modelValue.includes(permissionId);
28
- }
29
-
30
- function toggle(permissionId) {
31
- if (!permissionId) return;
32
- const updated = isChecked(permissionId)
33
- ? props.modelValue.filter((id) => id !== permissionId)
34
- : [...props.modelValue, permissionId];
35
- emit('update:modelValue', updated);
36
- }
37
-
38
- function toggleGroup(group) {
39
- const groupPerms = (props.permissions[group] || []).map((p) => p.id);
40
- const allChecked = groupPerms.every((id) => props.modelValue.includes(id));
41
-
42
- let updated;
43
- if (allChecked) {
44
- updated = props.modelValue.filter((id) => !groupPerms.includes(id));
45
- } else {
46
- const newIds = groupPerms.filter((id) => !props.modelValue.includes(id));
47
- updated = [...props.modelValue, ...newIds];
48
- }
49
- emit('update:modelValue', updated);
50
- }
51
-
52
- function isGroupAllChecked(group) {
53
- const groupPerms = (props.permissions[group] || []).map((p) => p.id);
54
- return groupPerms.length > 0 && groupPerms.every((id) => props.modelValue.includes(id));
55
- }
56
- </script>
57
-
58
- <template>
59
- <div class="overflow-hidden rounded-xl border border-white/[0.06] bg-neutral-900/60">
60
- <table class="min-w-full divide-y divide-white/[0.06]">
61
- <thead>
62
- <tr class="bg-white/[0.02]">
63
- <th class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-neutral-500">
64
- Group
65
- </th>
66
- <th
67
- v-for="action in actions"
68
- :key="action"
69
- class="px-4 py-3 text-center text-[11px] font-semibold uppercase tracking-wider text-neutral-500"
70
- >
71
- {{ action }}
72
- </th>
73
- <th
74
- class="px-4 py-3 text-center text-[11px] font-semibold uppercase tracking-wider text-neutral-500"
75
- >
76
- All
77
- </th>
78
- </tr>
79
- </thead>
80
- <tbody class="divide-y divide-white/[0.04]">
81
- <tr v-for="group in groups" :key="group" class="transition-colors hover:bg-white/[0.03]">
82
- <td class="px-4 py-3 text-sm font-medium text-neutral-200">{{ group }}</td>
83
- <td v-for="action in actions" :key="action" class="px-4 py-3 text-center">
84
- <template v-if="getPermission(group, action)">
85
- <input
86
- type="checkbox"
87
- :checked="isChecked(getPermission(group, action).id)"
88
- :aria-label="group + ' ' + action + ' permission'"
89
- class="rounded border-white/20 bg-neutral-900 text-cyan-500 focus:ring-white/10"
90
- @change="toggle(getPermission(group, action).id)"
91
- />
92
- </template>
93
- <span v-else class="text-neutral-600">-</span>
94
- </td>
95
- <td class="px-4 py-3 text-center">
96
- <input
97
- type="checkbox"
98
- :checked="isGroupAllChecked(group)"
99
- class="rounded border-white/20 bg-neutral-900 text-cyan-500 focus:ring-white/10"
100
- @change="toggleGroup(group)"
101
- />
102
- </td>
103
- </tr>
104
- </tbody>
105
- </table>
106
- </div>
107
- </template>
1
+ <script setup>
2
+ import { computed } from 'vue';
3
+
4
+ const props = defineProps({
5
+ permissions: {
6
+ type: Object,
7
+ required: true,
8
+ },
9
+ modelValue: {
10
+ type: Array,
11
+ default: () => [],
12
+ },
13
+ });
14
+
15
+ const emit = defineEmits(['update:modelValue']);
16
+
17
+ const groups = computed(() => Object.keys(props.permissions || {}));
18
+
19
+ const actions = ['view', 'create', 'edit', 'delete'];
20
+
21
+ function getPermission(group, action) {
22
+ const perms = props.permissions[group] || [];
23
+ return perms.find((p) => p.slug === `${group.toLowerCase()}.${action}` || p.slug.endsWith(`.${action}`));
24
+ }
25
+
26
+ function isChecked(permissionId) {
27
+ return props.modelValue.includes(permissionId);
28
+ }
29
+
30
+ function toggle(permissionId) {
31
+ if (!permissionId) return;
32
+ const updated = isChecked(permissionId)
33
+ ? props.modelValue.filter((id) => id !== permissionId)
34
+ : [...props.modelValue, permissionId];
35
+ emit('update:modelValue', updated);
36
+ }
37
+
38
+ function toggleGroup(group) {
39
+ const groupPerms = (props.permissions[group] || []).map((p) => p.id);
40
+ const allChecked = groupPerms.every((id) => props.modelValue.includes(id));
41
+
42
+ let updated;
43
+ if (allChecked) {
44
+ updated = props.modelValue.filter((id) => !groupPerms.includes(id));
45
+ } else {
46
+ const newIds = groupPerms.filter((id) => !props.modelValue.includes(id));
47
+ updated = [...props.modelValue, ...newIds];
48
+ }
49
+ emit('update:modelValue', updated);
50
+ }
51
+
52
+ function isGroupAllChecked(group) {
53
+ const groupPerms = (props.permissions[group] || []).map((p) => p.id);
54
+ return groupPerms.length > 0 && groupPerms.every((id) => props.modelValue.includes(id));
55
+ }
56
+ </script>
57
+
58
+ <template>
59
+ <div class="overflow-hidden rounded-xl border border-white/[0.06] bg-neutral-900/60">
60
+ <table class="min-w-full divide-y divide-white/[0.06]">
61
+ <thead>
62
+ <tr class="bg-white/[0.02]">
63
+ <th class="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-neutral-500">
64
+ Group
65
+ </th>
66
+ <th
67
+ v-for="action in actions"
68
+ :key="action"
69
+ class="px-4 py-3 text-center text-[11px] font-semibold uppercase tracking-wider text-neutral-500"
70
+ >
71
+ {{ action }}
72
+ </th>
73
+ <th
74
+ class="px-4 py-3 text-center text-[11px] font-semibold uppercase tracking-wider text-neutral-500"
75
+ >
76
+ All
77
+ </th>
78
+ </tr>
79
+ </thead>
80
+ <tbody class="divide-y divide-white/[0.04]">
81
+ <tr v-for="group in groups" :key="group" class="transition-colors hover:bg-white/[0.03]">
82
+ <td class="px-4 py-3 text-sm font-medium text-neutral-200">{{ group }}</td>
83
+ <td v-for="action in actions" :key="action" class="px-4 py-3 text-center">
84
+ <template v-if="getPermission(group, action)">
85
+ <input
86
+ type="checkbox"
87
+ :checked="isChecked(getPermission(group, action).id)"
88
+ :aria-label="group + ' ' + action + ' permission'"
89
+ class="rounded border-white/20 bg-neutral-900 text-cyan-500 focus:ring-white/10"
90
+ @change="toggle(getPermission(group, action).id)"
91
+ />
92
+ </template>
93
+ <span v-else class="text-neutral-600">-</span>
94
+ </td>
95
+ <td class="px-4 py-3 text-center">
96
+ <input
97
+ type="checkbox"
98
+ :checked="isGroupAllChecked(group)"
99
+ class="rounded border-white/20 bg-neutral-900 text-cyan-500 focus:ring-white/10"
100
+ @change="toggleGroup(group)"
101
+ />
102
+ </td>
103
+ </tr>
104
+ </tbody>
105
+ </table>
106
+ </div>
107
+ </template>
@@ -1,129 +1,129 @@
1
- <script setup>
2
- import { ref, computed, inject } from 'vue';
3
- import { router } from '@inertiajs/vue3';
4
- import { sanitizeHtml } from '../utils/sanitizeHtml';
5
- import { useI18n } from '../composables/useI18n';
6
-
7
- const props = defineProps({
8
- notes: { type: Array, default: () => [] },
9
- ticketReference: { type: String, required: true },
10
- routePrefix: { type: String, required: true },
11
- });
12
-
13
- const escDark = inject(
14
- 'esc-dark',
15
- computed(() => false),
16
- );
17
- const { t } = useI18n();
18
- const processingId = ref(null);
19
-
20
- function unpinNote(note) {
21
- if (processingId.value) return;
22
- processingId.value = note.id;
23
- router.post(
24
- route(`${props.routePrefix}.tickets.pin`, [props.ticketReference, note.id]),
25
- {},
26
- {
27
- preserveScroll: true,
28
- onFinish: () => {
29
- processingId.value = null;
30
- },
31
- },
32
- );
33
- }
34
-
35
- function formatDate(dateStr) {
36
- if (!dateStr) return '';
37
- const date = new Date(dateStr);
38
- return date.toLocaleDateString(undefined, {
39
- month: 'short',
40
- day: 'numeric',
41
- year: 'numeric',
42
- hour: 'numeric',
43
- minute: '2-digit',
44
- });
45
- }
46
- </script>
47
-
48
- <template>
49
- <div
50
- v-if="notes.length > 0"
51
- :class="[
52
- 'rounded-xl border p-4',
53
- escDark ? 'border-amber-500/20 bg-amber-500/[0.06]' : 'border-amber-300 bg-amber-50',
54
- ]"
55
- >
56
- <!-- Header -->
57
- <div class="mb-3 flex items-center gap-2">
58
- <svg
59
- :class="['h-4 w-4', escDark ? 'text-amber-400' : 'text-amber-600']"
60
- fill="none"
61
- stroke="currentColor"
62
- stroke-width="1.5"
63
- viewBox="0 0 24 24"
64
- >
65
- <path
66
- stroke-linecap="round"
67
- stroke-linejoin="round"
68
- d="M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9"
69
- />
70
- </svg>
71
- <span
72
- :class="[
73
- 'text-xs font-semibold uppercase tracking-wider',
74
- escDark ? 'text-amber-400' : 'text-amber-700',
75
- ]"
76
- >
77
- {{ t('pinned_notes.title') }}
78
- </span>
79
- </div>
80
-
81
- <!-- Notes list -->
82
- <div class="space-y-3">
83
- <div
84
- v-for="note in notes"
85
- :key="note.id"
86
- :class="[
87
- 'rounded-lg border p-3',
88
- escDark ? 'border-amber-500/10 bg-amber-500/[0.04]' : 'border-amber-200 bg-white',
89
- ]"
90
- >
91
- <!-- Note body -->
92
- <div
93
- :class="[
94
- 'prose prose-sm max-w-none',
95
- escDark ? 'prose-invert text-[var(--esc-panel-text-secondary)]' : 'text-gray-800',
96
- ]"
97
- v-html="sanitizeHtml(note.body)"
98
- ></div>
99
-
100
- <!-- Meta row -->
101
- <div class="mt-2 flex items-center justify-between">
102
- <div
103
- :class="[
104
- 'flex items-center gap-2 text-xs',
105
- escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500',
106
- ]"
107
- >
108
- <span class="font-medium">{{ note.author?.name || 'Unknown' }}</span>
109
- <span>&middot;</span>
110
- <span>{{ formatDate(note.created_at) }}</span>
111
- </div>
112
- <button
113
- :disabled="processingId === note.id"
114
- :class="[
115
- 'rounded-md px-2 py-1 text-xs font-medium transition-colors',
116
- escDark
117
- ? 'text-amber-400 hover:bg-amber-500/10 hover:text-amber-300'
118
- : 'text-amber-600 hover:bg-amber-100 hover:text-amber-700',
119
- processingId === note.id && 'opacity-50 cursor-not-allowed',
120
- ]"
121
- @click="unpinNote(note)"
122
- >
123
- {{ t('pinned_notes.unpin') }}
124
- </button>
125
- </div>
126
- </div>
127
- </div>
128
- </div>
129
- </template>
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+ import { router } from '@inertiajs/vue3';
4
+ import { sanitizeHtml } from '../utils/sanitizeHtml';
5
+ import { useI18n } from '../composables/useI18n';
6
+
7
+ const props = defineProps({
8
+ notes: { type: Array, default: () => [] },
9
+ ticketReference: { type: String, required: true },
10
+ routePrefix: { type: String, required: true },
11
+ });
12
+
13
+ const escDark = inject(
14
+ 'esc-dark',
15
+ computed(() => false),
16
+ );
17
+ const { t } = useI18n();
18
+ const processingId = ref(null);
19
+
20
+ function unpinNote(note) {
21
+ if (processingId.value) return;
22
+ processingId.value = note.id;
23
+ router.post(
24
+ route(`${props.routePrefix}.tickets.pin`, [props.ticketReference, note.id]),
25
+ {},
26
+ {
27
+ preserveScroll: true,
28
+ onFinish: () => {
29
+ processingId.value = null;
30
+ },
31
+ },
32
+ );
33
+ }
34
+
35
+ function formatDate(dateStr) {
36
+ if (!dateStr) return '';
37
+ const date = new Date(dateStr);
38
+ return date.toLocaleDateString(undefined, {
39
+ month: 'short',
40
+ day: 'numeric',
41
+ year: 'numeric',
42
+ hour: 'numeric',
43
+ minute: '2-digit',
44
+ });
45
+ }
46
+ </script>
47
+
48
+ <template>
49
+ <div
50
+ v-if="notes.length > 0"
51
+ :class="[
52
+ 'rounded-xl border p-4',
53
+ escDark ? 'border-amber-500/20 bg-amber-500/[0.06]' : 'border-amber-300 bg-amber-50',
54
+ ]"
55
+ >
56
+ <!-- Header -->
57
+ <div class="mb-3 flex items-center gap-2">
58
+ <svg
59
+ :class="['h-4 w-4', escDark ? 'text-amber-400' : 'text-amber-600']"
60
+ fill="none"
61
+ stroke="currentColor"
62
+ stroke-width="1.5"
63
+ viewBox="0 0 24 24"
64
+ >
65
+ <path
66
+ stroke-linecap="round"
67
+ stroke-linejoin="round"
68
+ d="M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9"
69
+ />
70
+ </svg>
71
+ <span
72
+ :class="[
73
+ 'text-xs font-semibold uppercase tracking-wider',
74
+ escDark ? 'text-amber-400' : 'text-amber-700',
75
+ ]"
76
+ >
77
+ {{ t('pinned_notes.title') }}
78
+ </span>
79
+ </div>
80
+
81
+ <!-- Notes list -->
82
+ <div class="space-y-3">
83
+ <div
84
+ v-for="note in notes"
85
+ :key="note.id"
86
+ :class="[
87
+ 'rounded-lg border p-3',
88
+ escDark ? 'border-amber-500/10 bg-amber-500/[0.04]' : 'border-amber-200 bg-white',
89
+ ]"
90
+ >
91
+ <!-- Note body -->
92
+ <div
93
+ :class="[
94
+ 'prose prose-sm max-w-none',
95
+ escDark ? 'prose-invert text-[var(--esc-panel-text-secondary)]' : 'text-gray-800',
96
+ ]"
97
+ v-html="sanitizeHtml(note.body)"
98
+ ></div>
99
+
100
+ <!-- Meta row -->
101
+ <div class="mt-2 flex items-center justify-between">
102
+ <div
103
+ :class="[
104
+ 'flex items-center gap-2 text-xs',
105
+ escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500',
106
+ ]"
107
+ >
108
+ <span class="font-medium">{{ note.author?.name || 'Unknown' }}</span>
109
+ <span>&middot;</span>
110
+ <span>{{ formatDate(note.created_at) }}</span>
111
+ </div>
112
+ <button
113
+ :disabled="processingId === note.id"
114
+ :class="[
115
+ 'rounded-md px-2 py-1 text-xs font-medium transition-colors',
116
+ escDark
117
+ ? 'text-amber-400 hover:bg-amber-500/10 hover:text-amber-300'
118
+ : 'text-amber-600 hover:bg-amber-100 hover:text-amber-700',
119
+ processingId === note.id && 'opacity-50 cursor-not-allowed',
120
+ ]"
121
+ @click="unpinNote(note)"
122
+ >
123
+ {{ t('pinned_notes.unpin') }}
124
+ </button>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </template>