@escalated-dev/escalated 0.6.0 → 0.7.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 (130) hide show
  1. package/README.md +94 -2
  2. package/package.json +31 -2
  3. package/src/components/ActivityTimeline.stories.js +55 -0
  4. package/src/components/ActivityTimeline.vue +24 -7
  5. package/src/components/AdminDashboard.stories.js +1095 -0
  6. package/src/components/AgentLoadIndicator.stories.js +37 -0
  7. package/src/components/AgentLoadIndicator.vue +49 -0
  8. package/src/components/AgentPerformanceCard.vue +44 -0
  9. package/src/components/ArticleEditor.vue +82 -0
  10. package/src/components/AssigneeSelect.vue +19 -5
  11. package/src/components/AttachmentList.vue +53 -37
  12. package/src/components/AuditLogEntry.vue +95 -0
  13. package/src/components/BulkActionBar.vue +99 -30
  14. package/src/components/CategoryTree.vue +89 -0
  15. package/src/components/ChartWidget.vue +46 -0
  16. package/src/components/CollisionWarning.stories.js +30 -0
  17. package/src/components/CollisionWarning.vue +68 -0
  18. package/src/components/ConditionalFieldRules.vue +135 -0
  19. package/src/components/ContextPanel.vue +66 -0
  20. package/src/components/ContextPanelSection.vue +65 -0
  21. package/src/components/CustomFieldRenderer.vue +159 -0
  22. package/src/components/EscalatedLayout.vue +400 -69
  23. package/src/components/FileDropzone.vue +26 -10
  24. package/src/components/FollowButton.stories.js +30 -0
  25. package/src/components/FollowButton.vue +53 -28
  26. package/src/components/KeyboardShortcutHelp.vue +129 -39
  27. package/src/components/KnowledgePanel.vue +122 -0
  28. package/src/components/KpiCard.stories.js +32 -0
  29. package/src/components/KpiCard.vue +88 -0
  30. package/src/components/MacroDropdown.vue +95 -35
  31. package/src/components/PermissionMatrix.vue +107 -0
  32. package/src/components/PinnedNotes.vue +70 -31
  33. package/src/components/PresenceIndicator.vue +93 -27
  34. package/src/components/PriorityBadge.stories.js +33 -0
  35. package/src/components/PriorityBadge.vue +9 -2
  36. package/src/components/QuickFilters.vue +65 -49
  37. package/src/components/ReplyComposer.vue +144 -30
  38. package/src/components/ReplyThread.vue +72 -20
  39. package/src/components/SatisfactionRating.vue +98 -46
  40. package/src/components/ScheduleEditor.vue +104 -0
  41. package/src/components/SideConversation.vue +165 -0
  42. package/src/components/SideConversationList.vue +147 -0
  43. package/src/components/SkillTagManager.vue +79 -0
  44. package/src/components/SlaComplianceChart.vue +53 -0
  45. package/src/components/SlaTimer.stories.js +35 -0
  46. package/src/components/SlaTimer.vue +17 -4
  47. package/src/components/StatsCard.stories.js +24 -0
  48. package/src/components/StatsCard.vue +58 -46
  49. package/src/components/StatusBadge.stories.js +53 -0
  50. package/src/components/StatusBadge.vue +36 -4
  51. package/src/components/TagSelect.vue +43 -12
  52. package/src/components/TicketFilters.vue +225 -69
  53. package/src/components/TicketLinkPanel.vue +193 -0
  54. package/src/components/TicketList.vue +594 -193
  55. package/src/components/TicketMergeDialog.vue +157 -0
  56. package/src/components/TicketSidebar.vue +167 -99
  57. package/src/components/TicketTypeSelector.vue +21 -0
  58. package/src/components/TotpInput.stories.js +9 -0
  59. package/src/components/TotpInput.vue +81 -0
  60. package/src/components/TwoFactorSetup.vue +113 -0
  61. package/src/composables/useDebouncedSearch.js +38 -0
  62. package/src/composables/usePluginExtensions.js +95 -3
  63. package/src/composables/usePluginHooks.js +65 -0
  64. package/src/defineEscalatedPlugin.js +34 -8
  65. package/src/index.js +72 -35
  66. package/src/locales/en.json +25 -0
  67. package/src/pages/Admin/ApiTokens/Index.vue +231 -67
  68. package/src/pages/Admin/AuditLog/Index.vue +154 -0
  69. package/src/pages/Admin/Automations/Form.vue +184 -0
  70. package/src/pages/Admin/Automations/Index.vue +129 -0
  71. package/src/pages/Admin/BusinessHours/Form.vue +172 -0
  72. package/src/pages/Admin/BusinessHours/Index.vue +113 -0
  73. package/src/pages/Admin/CannedResponses/Index.vue +127 -70
  74. package/src/pages/Admin/Capacity/Index.vue +134 -0
  75. package/src/pages/Admin/CustomFields/Form.vue +195 -0
  76. package/src/pages/Admin/CustomFields/Index.vue +134 -0
  77. package/src/pages/Admin/CustomObjects/Form.vue +202 -0
  78. package/src/pages/Admin/CustomObjects/Index.vue +127 -0
  79. package/src/pages/Admin/CustomObjects/Records.vue +228 -0
  80. package/src/pages/Admin/Departments/Form.vue +75 -51
  81. package/src/pages/Admin/Departments/Index.vue +109 -57
  82. package/src/pages/Admin/EscalationRules/Form.vue +157 -96
  83. package/src/pages/Admin/EscalationRules/Index.vue +178 -57
  84. package/src/pages/Admin/Import/Connect.vue +96 -0
  85. package/src/pages/Admin/Import/Index.vue +102 -0
  86. package/src/pages/Admin/Import/Mapping.vue +110 -0
  87. package/src/pages/Admin/Import/Progress.vue +153 -0
  88. package/src/pages/Admin/Import/Review.vue +76 -0
  89. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
  90. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
  91. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
  92. package/src/pages/Admin/Macros/Index.vue +250 -89
  93. package/src/pages/Admin/Plugins/Index.vue +205 -75
  94. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
  95. package/src/pages/Admin/Reports/CsatReport.vue +127 -0
  96. package/src/pages/Admin/Reports/Dashboard.vue +211 -0
  97. package/src/pages/Admin/Reports/SlaReport.vue +166 -0
  98. package/src/pages/Admin/Reports.vue +139 -34
  99. package/src/pages/Admin/Roles/Form.vue +71 -0
  100. package/src/pages/Admin/Roles/Index.vue +122 -0
  101. package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
  102. package/src/pages/Admin/Settings/DataRetention.vue +182 -0
  103. package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
  104. package/src/pages/Admin/Settings/Sandbox.vue +135 -0
  105. package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
  106. package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
  107. package/src/pages/Admin/Settings.vue +468 -260
  108. package/src/pages/Admin/Skills/Form.vue +53 -0
  109. package/src/pages/Admin/Skills/Index.vue +94 -0
  110. package/src/pages/Admin/SlaPolicies/Form.vue +118 -75
  111. package/src/pages/Admin/SlaPolicies/Index.vue +120 -59
  112. package/src/pages/Admin/Statuses/Form.vue +114 -0
  113. package/src/pages/Admin/Statuses/Index.vue +153 -0
  114. package/src/pages/Admin/Tags/Index.vue +191 -105
  115. package/src/pages/Admin/Tickets/Index.vue +91 -39
  116. package/src/pages/Admin/Tickets/Show.vue +101 -37
  117. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
  118. package/src/pages/Admin/Webhooks/Form.vue +110 -0
  119. package/src/pages/Admin/Webhooks/Index.vue +136 -0
  120. package/src/pages/Agent/Dashboard.vue +311 -186
  121. package/src/pages/Agent/TicketIndex.vue +88 -38
  122. package/src/pages/Agent/TicketShow.vue +258 -51
  123. package/src/pages/Customer/Create.vue +36 -9
  124. package/src/pages/Customer/Index.vue +31 -24
  125. package/src/pages/Customer/KnowledgeBase/Article.vue +127 -0
  126. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
  127. package/src/plugin.js +86 -1
  128. package/src/utils/constants.js +30 -0
  129. package/src/utils/formatting.js +35 -0
  130. package/src/pages/Admin/Plugins.vue +0 -129
@@ -0,0 +1,88 @@
1
+ <script setup>
2
+ import { inject, computed } from 'vue';
3
+
4
+ defineProps({
5
+ label: { type: String, required: true },
6
+ value: { type: [Number, String], required: true },
7
+ trend: { type: Number, default: null },
8
+ icon: { type: String, default: null },
9
+ });
10
+
11
+ const escDark = inject(
12
+ 'esc-dark',
13
+ computed(() => false),
14
+ );
15
+ </script>
16
+
17
+ <template>
18
+ <div
19
+ :class="[
20
+ 'rounded-xl border p-5',
21
+ escDark ? 'border-white/[0.06] bg-neutral-900/60' : 'border-gray-200 bg-white',
22
+ ]"
23
+ >
24
+ <div class="flex items-start justify-between">
25
+ <div>
26
+ <div :class="['text-[13px] font-medium', escDark ? 'text-neutral-500' : 'text-gray-500']">
27
+ {{ label }}
28
+ </div>
29
+ <div :class="['mt-2 text-2xl font-bold tracking-tight', escDark ? 'text-white' : 'text-gray-900']">
30
+ {{ value }}
31
+ </div>
32
+ </div>
33
+ <div
34
+ v-if="icon"
35
+ :class="[
36
+ 'flex h-10 w-10 items-center justify-center rounded-lg',
37
+ escDark ? 'bg-white/[0.04]' : 'bg-gray-100',
38
+ ]"
39
+ >
40
+ <svg
41
+ :class="['h-5 w-5', escDark ? 'text-neutral-500' : 'text-gray-400']"
42
+ fill="none"
43
+ stroke="currentColor"
44
+ stroke-width="1.5"
45
+ viewBox="0 0 24 24"
46
+ >
47
+ <path stroke-linecap="round" stroke-linejoin="round" :d="icon" />
48
+ </svg>
49
+ </div>
50
+ </div>
51
+ <div v-if="trend !== null && trend !== 0" class="mt-3 flex items-center gap-1">
52
+ <svg
53
+ v-if="trend > 0"
54
+ :class="['h-4 w-4', escDark ? 'text-emerald-400' : 'text-emerald-500']"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ stroke-width="2"
58
+ viewBox="0 0 24 24"
59
+ >
60
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25" />
61
+ </svg>
62
+ <svg
63
+ v-else
64
+ :class="['h-4 w-4', escDark ? 'text-red-400' : 'text-red-500']"
65
+ fill="none"
66
+ stroke="currentColor"
67
+ stroke-width="2"
68
+ viewBox="0 0 24 24"
69
+ >
70
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 4.5l15 15m0 0V8.25m0 11.25H8.25" />
71
+ </svg>
72
+ <span
73
+ :class="[
74
+ 'text-xs font-medium',
75
+ trend > 0
76
+ ? escDark
77
+ ? 'text-emerald-400'
78
+ : 'text-emerald-600'
79
+ : escDark
80
+ ? 'text-red-400'
81
+ : 'text-red-600',
82
+ ]"
83
+ >
84
+ {{ Math.abs(trend) }}%
85
+ </span>
86
+ </div>
87
+ </div>
88
+ </template>
@@ -7,20 +7,27 @@ const props = defineProps({
7
7
  action: { type: String, required: true },
8
8
  });
9
9
 
10
- const escDark = inject('esc-dark', computed(() => false));
10
+ const escDark = inject(
11
+ 'esc-dark',
12
+ computed(() => false),
13
+ );
11
14
  const open = ref(false);
12
15
  const processing = ref(false);
13
16
 
14
17
  function applyMacro(macroId) {
15
18
  if (processing.value) return;
16
19
  processing.value = true;
17
- router.post(props.action, { macro_id: macroId }, {
18
- preserveScroll: true,
19
- onFinish: () => {
20
- processing.value = false;
21
- open.value = false;
20
+ router.post(
21
+ props.action,
22
+ { macro_id: macroId },
23
+ {
24
+ preserveScroll: true,
25
+ onFinish: () => {
26
+ processing.value = false;
27
+ open.value = false;
28
+ },
22
29
  },
23
- });
30
+ );
24
31
  }
25
32
 
26
33
  function toggle() {
@@ -35,17 +42,33 @@ function close() {
35
42
  <template>
36
43
  <div class="relative">
37
44
  <!-- Trigger button -->
38
- <button @click="toggle"
39
- :class="['inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
40
- escDark
41
- ? 'border border-white/10 bg-white/[0.03] text-neutral-300 hover:bg-white/[0.06]'
42
- : 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50']"
43
- :disabled="processing">
45
+ <button
46
+ :class="[
47
+ 'inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
48
+ escDark
49
+ ? 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-secondary)] hover:bg-[var(--esc-panel-hover)]'
50
+ : 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50',
51
+ ]"
52
+ :disabled="processing"
53
+ aria-label="Apply macro"
54
+ :aria-expanded="open"
55
+ @click="toggle"
56
+ >
44
57
  <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
45
- <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
58
+ <path
59
+ stroke-linecap="round"
60
+ stroke-linejoin="round"
61
+ d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
62
+ />
46
63
  </svg>
47
64
  Apply Macro
48
- <svg :class="['h-4 w-4 transition-transform', open && 'rotate-180']" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
65
+ <svg
66
+ :class="['h-4 w-4 transition-transform', open && 'rotate-180']"
67
+ fill="none"
68
+ stroke="currentColor"
69
+ stroke-width="1.5"
70
+ viewBox="0 0 24 24"
71
+ >
49
72
  <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
50
73
  </svg>
51
74
  </button>
@@ -54,32 +77,69 @@ function close() {
54
77
  <div v-if="open" class="fixed inset-0 z-30" @click="close"></div>
55
78
 
56
79
  <!-- Dropdown -->
57
- <Transition enter-active-class="transition duration-100 ease-out" enter-from-class="scale-95 opacity-0" enter-to-class="scale-100 opacity-100"
58
- leave-active-class="transition duration-75 ease-in" leave-from-class="scale-100 opacity-100" leave-to-class="scale-95 opacity-0">
59
- <div v-if="open"
60
- :class="['absolute right-0 z-40 mt-1 w-64 overflow-hidden rounded-xl border shadow-xl',
61
- escDark ? 'border-white/[0.06] bg-neutral-900' : 'border-gray-200 bg-white']">
62
- <div :class="['px-3 py-2 text-xs font-semibold uppercase tracking-wider',
63
- escDark ? 'text-neutral-500' : 'text-gray-500']">
80
+ <Transition
81
+ enter-active-class="transition duration-100 ease-out"
82
+ enter-from-class="scale-95 opacity-0"
83
+ enter-to-class="scale-100 opacity-100"
84
+ leave-active-class="transition duration-75 ease-in"
85
+ leave-from-class="scale-100 opacity-100"
86
+ leave-to-class="scale-95 opacity-0"
87
+ >
88
+ <div
89
+ v-if="open"
90
+ :class="[
91
+ 'absolute right-0 z-40 mt-1 w-64 overflow-hidden rounded-xl border shadow-xl',
92
+ escDark
93
+ ? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)]'
94
+ : 'border-gray-200 bg-white',
95
+ ]"
96
+ >
97
+ <div
98
+ :class="[
99
+ 'px-3 py-2 text-xs font-semibold uppercase tracking-wider',
100
+ escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500',
101
+ ]"
102
+ >
64
103
  Macros
65
104
  </div>
66
- <div class="max-h-64 overflow-y-auto">
67
- <div v-if="!macros.length"
68
- :class="['px-3 py-4 text-center text-sm', escDark ? 'text-neutral-500' : 'text-gray-400']">
105
+ <div class="max-h-64 overflow-y-auto" role="menu">
106
+ <div
107
+ v-if="!macros.length"
108
+ :class="[
109
+ 'px-3 py-4 text-center text-sm',
110
+ escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-400',
111
+ ]"
112
+ >
69
113
  No macros available
70
114
  </div>
71
- <button v-for="macro in macros" :key="macro.id"
72
- @click="applyMacro(macro.id)"
73
- :disabled="processing"
74
- :class="['flex w-full flex-col px-3 py-2.5 text-left transition-colors',
75
- escDark
76
- ? 'hover:bg-white/[0.04] disabled:opacity-50'
77
- : 'hover:bg-gray-50 disabled:opacity-50']">
78
- <span :class="['text-sm font-medium', escDark ? 'text-neutral-200' : 'text-gray-900']">
115
+ <button
116
+ v-for="macro in macros"
117
+ :key="macro.id"
118
+ role="menuitem"
119
+ :disabled="processing"
120
+ :class="[
121
+ 'flex w-full flex-col px-3 py-2.5 text-left transition-colors',
122
+ escDark
123
+ ? 'hover:bg-[var(--esc-panel-hover)] disabled:opacity-50'
124
+ : 'hover:bg-gray-50 disabled:opacity-50',
125
+ ]"
126
+ @click="applyMacro(macro.id)"
127
+ >
128
+ <span
129
+ :class="[
130
+ 'text-sm font-medium',
131
+ escDark ? 'text-[var(--esc-panel-text-secondary)]' : 'text-gray-900',
132
+ ]"
133
+ >
79
134
  {{ macro.name }}
80
135
  </span>
81
- <span v-if="macro.description"
82
- :class="['mt-0.5 text-xs', escDark ? 'text-neutral-500' : 'text-gray-500']">
136
+ <span
137
+ v-if="macro.description"
138
+ :class="[
139
+ 'mt-0.5 text-xs',
140
+ escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500',
141
+ ]"
142
+ >
83
143
  {{ macro.description }}
84
144
  </span>
85
145
  </button>
@@ -0,0 +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>
@@ -10,19 +10,26 @@ const props = defineProps({
10
10
  routePrefix: { type: String, required: true },
11
11
  });
12
12
 
13
- const escDark = inject('esc-dark', computed(() => false));
13
+ const escDark = inject(
14
+ 'esc-dark',
15
+ computed(() => false),
16
+ );
14
17
  const { t } = useI18n();
15
18
  const processingId = ref(null);
16
19
 
17
20
  function unpinNote(note) {
18
21
  if (processingId.value) return;
19
22
  processingId.value = note.id;
20
- router.post(route(`${props.routePrefix}.tickets.pin`, [props.ticketReference, note.id]), {}, {
21
- preserveScroll: true,
22
- onFinish: () => {
23
- processingId.value = null;
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
+ },
24
31
  },
25
- });
32
+ );
26
33
  }
27
34
 
28
35
  function formatDate(dateStr) {
@@ -39,48 +46,80 @@ function formatDate(dateStr) {
39
46
  </script>
40
47
 
41
48
  <template>
42
- <div v-if="notes.length > 0"
43
- :class="['rounded-xl border p-4',
44
- escDark
45
- ? 'border-amber-500/20 bg-amber-500/[0.06]'
46
- : 'border-amber-300 bg-amber-50']">
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
+ >
47
56
  <!-- Header -->
48
57
  <div class="mb-3 flex items-center gap-2">
49
- <svg :class="['h-4 w-4', escDark ? 'text-amber-400' : 'text-amber-600']"
50
- fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
51
- <path stroke-linecap="round" stroke-linejoin="round" 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" />
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
+ />
52
70
  </svg>
53
- <span :class="['text-xs font-semibold uppercase tracking-wider',
54
- escDark ? 'text-amber-400' : 'text-amber-700']">
71
+ <span
72
+ :class="[
73
+ 'text-xs font-semibold uppercase tracking-wider',
74
+ escDark ? 'text-amber-400' : 'text-amber-700',
75
+ ]"
76
+ >
55
77
  {{ t('pinned_notes.title') }}
56
78
  </span>
57
79
  </div>
58
80
 
59
81
  <!-- Notes list -->
60
82
  <div class="space-y-3">
61
- <div v-for="note in notes" :key="note.id"
62
- :class="['rounded-lg border p-3',
63
- escDark
64
- ? 'border-amber-500/10 bg-amber-500/[0.04]'
65
- : 'border-amber-200 bg-white']">
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
+ >
66
91
  <!-- Note body -->
67
- <div :class="['prose prose-sm max-w-none', escDark ? 'prose-invert text-neutral-200' : 'text-gray-800']" v-html="sanitizeHtml(note.body)"></div>
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>
68
99
 
69
100
  <!-- Meta row -->
70
101
  <div class="mt-2 flex items-center justify-between">
71
- <div :class="['flex items-center gap-2 text-xs',
72
- escDark ? 'text-neutral-500' : 'text-gray-500']">
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
+ >
73
108
  <span class="font-medium">{{ note.author?.name || 'Unknown' }}</span>
74
109
  <span>&middot;</span>
75
110
  <span>{{ formatDate(note.created_at) }}</span>
76
111
  </div>
77
- <button @click="unpinNote(note)"
78
- :disabled="processingId === note.id"
79
- :class="['rounded-md px-2 py-1 text-xs font-medium transition-colors',
80
- escDark
81
- ? 'text-amber-400 hover:bg-amber-500/10 hover:text-amber-300'
82
- : 'text-amber-600 hover:bg-amber-100 hover:text-amber-700',
83
- processingId === note.id && 'opacity-50 cursor-not-allowed']">
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
+ >
84
123
  {{ t('pinned_notes.unpin') }}
85
124
  </button>
86
125
  </div>