@escalated-dev/escalated 0.7.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.
- package/LICENSE +21 -21
- package/README.md +31 -0
- package/package.json +12 -6
- package/src/components/ActiveChatsPanel.vue +246 -0
- package/src/components/ActivityTimeline.stories.js +55 -55
- package/src/components/ActivityTimeline.vue +83 -83
- package/src/components/AdminDashboard.stories.js +1423 -1095
- package/src/components/AgentLoadIndicator.stories.js +37 -37
- package/src/components/AgentLoadIndicator.vue +49 -49
- package/src/components/AgentPerformanceCard.vue +44 -44
- package/src/components/ArticleEditor.vue +82 -82
- package/src/components/AssigneeSelect.vue +37 -37
- package/src/components/AttachmentList.vue +13 -1
- package/src/components/AuditLogEntry.vue +95 -95
- package/src/components/BulkActionBar.vue +166 -166
- package/src/components/CategoryTree.vue +89 -89
- package/src/components/ChartWidget.vue +46 -46
- package/src/components/ChatActionBar.vue +226 -0
- package/src/components/ChatBubble.vue +162 -0
- package/src/components/ChatComposer.vue +308 -0
- package/src/components/ChatQueue.vue +222 -0
- package/src/components/ChatThread.vue +197 -0
- package/src/components/CollisionWarning.stories.js +30 -30
- package/src/components/CollisionWarning.vue +68 -68
- package/src/components/ConditionalFieldRules.vue +135 -135
- package/src/components/ContextPanel.vue +66 -66
- package/src/components/ContextPanelSection.vue +65 -65
- package/src/components/CustomFieldRenderer.vue +159 -159
- package/src/components/EscalatedLayout.vue +29 -6
- package/src/components/FileDropzone.vue +57 -57
- package/src/components/FollowButton.stories.js +30 -30
- package/src/components/FollowButton.vue +88 -88
- package/src/components/KeyboardShortcutHelp.vue +213 -213
- package/src/components/KnowledgePanel.vue +122 -122
- package/src/components/KpiCard.stories.js +32 -32
- package/src/components/KpiCard.vue +88 -88
- package/src/components/MacroDropdown.vue +150 -150
- package/src/components/MentionDropdown.vue +154 -0
- package/src/components/MetricComparison.vue +89 -0
- package/src/components/PercentileChart.vue +67 -0
- package/src/components/PeriodSelector.vue +98 -0
- package/src/components/PermissionMatrix.vue +107 -107
- package/src/components/PinnedNotes.vue +129 -129
- package/src/components/PluginSlot.vue +68 -62
- package/src/components/PresenceIndicator.vue +46 -2
- package/src/components/PriorityBadge.stories.js +33 -33
- package/src/components/PriorityBadge.vue +46 -46
- package/src/components/ReplyComposer.vue +81 -18
- package/src/components/ReplyThread.vue +40 -2
- package/src/components/ReportChart.vue +395 -0
- package/src/components/ReportExportButton.vue +108 -0
- package/src/components/SatisfactionRating.vue +180 -180
- package/src/components/SaveViewDialog.vue +172 -0
- package/src/components/SavedViewSidebar.vue +208 -0
- package/src/components/ScheduleEditor.vue +104 -104
- package/src/components/SideConversation.vue +165 -165
- package/src/components/SideConversationList.vue +147 -147
- package/src/components/SkillTagManager.vue +79 -79
- package/src/components/SlaComplianceChart.vue +46 -53
- package/src/components/SlaTimer.stories.js +35 -35
- package/src/components/SlaTimer.vue +62 -62
- package/src/components/SnoozeButton.vue +247 -0
- package/src/components/StatsCard.stories.js +24 -24
- package/src/components/StatsCard.vue +58 -58
- package/src/components/StatusBadge.stories.js +53 -53
- package/src/components/StatusBadge.vue +78 -78
- package/src/components/TagSelect.vue +84 -84
- package/src/components/TicketFilters.vue +270 -225
- package/src/components/TicketLinkPanel.vue +193 -193
- package/src/components/TicketList.vue +655 -594
- package/src/components/TicketMergeDialog.vue +157 -157
- package/src/components/TicketSplitDialog.vue +98 -0
- package/src/components/TotpInput.stories.js +9 -9
- package/src/components/TotpInput.vue +81 -81
- package/src/components/TwoFactorSetup.vue +113 -113
- package/src/components/VariableTokenInput.vue +111 -0
- package/src/components/WorkflowActionConfig.vue +287 -0
- package/src/components/WorkflowActionList.vue +350 -0
- package/src/components/WorkflowConditionBuilder.vue +265 -0
- package/src/components/WorkflowTriggerSelector.vue +125 -0
- package/src/composables/useChat.js +187 -0
- package/src/composables/useI18n.js +80 -80
- package/src/composables/useKeyboardShortcuts.js +46 -46
- package/src/composables/useMentions.js +134 -0
- package/src/composables/usePluginExtensions.js +159 -159
- package/src/composables/usePluginHooks.js +65 -65
- package/src/composables/useRealtime.js +182 -0
- package/src/defineEscalatedPlugin.js +76 -76
- package/src/index.js +84 -72
- package/src/locales/ar.json +727 -0
- package/src/locales/de.json +678 -430
- package/src/locales/en.json +717 -455
- package/src/locales/es.json +679 -430
- package/src/locales/fr.json +680 -431
- package/src/locales/index.js +12 -2
- package/src/locales/it.json +727 -0
- package/src/locales/ja.json +727 -0
- package/src/locales/ko.json +727 -0
- package/src/locales/nl.json +727 -0
- package/src/locales/pl.json +727 -0
- package/src/locales/pt-BR.json +727 -0
- package/src/locales/ru.json +727 -0
- package/src/locales/tr.json +727 -0
- package/src/locales/zh-CN.json +727 -0
- package/src/pages/Admin/ApiTokens/Index.vue +381 -381
- package/src/pages/Admin/AuditLog/Index.vue +154 -154
- package/src/pages/Admin/BusinessHours/Form.vue +172 -172
- package/src/pages/Admin/BusinessHours/Index.vue +113 -113
- package/src/pages/Admin/CannedResponses/Index.vue +127 -127
- package/src/pages/Admin/Capacity/Index.vue +134 -134
- package/src/pages/Admin/CustomFields/Form.vue +195 -195
- package/src/pages/Admin/CustomFields/Index.vue +134 -134
- package/src/pages/Admin/CustomObjects/Form.vue +202 -202
- package/src/pages/Admin/CustomObjects/Index.vue +127 -127
- package/src/pages/Admin/CustomObjects/Records.vue +228 -228
- package/src/pages/Admin/Departments/Form.vue +75 -75
- package/src/pages/Admin/Departments/Index.vue +110 -109
- package/src/pages/Admin/EscalationRules/Form.vue +157 -157
- package/src/pages/Admin/EscalationRules/Index.vue +178 -178
- package/src/pages/Admin/Import/Connect.vue +0 -1
- package/src/pages/Admin/Import/Index.vue +1 -1
- package/src/pages/Admin/Import/Progress.vue +1 -1
- package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
- package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
- package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
- package/src/pages/Admin/Macros/Index.vue +448 -448
- package/src/pages/Admin/Plugins/Index.vue +327 -327
- package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
- package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
- package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
- package/src/pages/Admin/Reports/Cohorts.vue +180 -0
- package/src/pages/Admin/Reports/Comparison.vue +141 -0
- package/src/pages/Admin/Reports/CsatReport.vue +127 -127
- package/src/pages/Admin/Reports/Dashboard.vue +36 -21
- package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
- package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
- package/src/pages/Admin/Reports/SlaReport.vue +162 -166
- package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
- package/src/pages/Admin/Reports.vue +206 -206
- package/src/pages/Admin/Roles/Form.vue +71 -71
- package/src/pages/Admin/Roles/Index.vue +122 -122
- package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
- package/src/pages/Admin/Settings/DataRetention.vue +182 -182
- package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
- package/src/pages/Admin/Settings/Sandbox.vue +135 -135
- package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
- package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
- package/src/pages/Admin/Settings.vue +1008 -468
- package/src/pages/Admin/Skills/Form.vue +53 -53
- package/src/pages/Admin/Skills/Index.vue +94 -94
- package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
- package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
- package/src/pages/Admin/Statuses/Form.vue +114 -114
- package/src/pages/Admin/Statuses/Index.vue +153 -153
- package/src/pages/Admin/Tags/Index.vue +191 -191
- package/src/pages/Admin/Tickets/Index.vue +126 -91
- package/src/pages/Admin/Tickets/Show.vue +29 -0
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
- package/src/pages/Admin/Webhooks/Form.vue +110 -110
- package/src/pages/Admin/Webhooks/Index.vue +136 -136
- package/src/pages/Admin/Workflows/Builder.vue +207 -0
- package/src/pages/Admin/Workflows/Index.vue +235 -0
- package/src/pages/Admin/Workflows/Logs.vue +286 -0
- package/src/pages/Agent/TicketIndex.vue +112 -88
- package/src/pages/Agent/TicketShow.vue +192 -60
- package/src/pages/Customer/Create.vue +1 -1
- package/src/pages/Customer/Index.vue +31 -31
- package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
- package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
- package/src/pages/Customer/Show.vue +69 -63
- package/src/pages/Guest/Create.vue +40 -15
- package/src/pages/Guest/Show.vue +124 -93
- package/src/plugin.js +25 -0
- package/src/utils/mentionHighlight.js +24 -0
- package/src/utils/sanitizeHtml.js +78 -64
- package/src/widget/EscalatedWidget.vue +804 -0
- package/src/widget/index.js +254 -0
- package/src/pages/Admin/Automations/Form.vue +0 -184
- 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>·</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>·</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>
|