@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
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { ref, inject, computed } from 'vue';
|
|
3
|
-
import { router } from '@inertiajs/vue3';
|
|
4
|
-
|
|
5
|
-
const escDark = inject(
|
|
6
|
-
'esc-dark',
|
|
7
|
-
computed(() => false),
|
|
8
|
-
);
|
|
9
|
-
|
|
10
|
-
const props = defineProps({
|
|
11
|
-
conversation: { type: Object, required: true },
|
|
12
|
-
ticketReference: { type: String, required: true },
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const emit = defineEmits(['updated']);
|
|
16
|
-
const replyBody = ref('');
|
|
17
|
-
const submitting = ref(false);
|
|
18
|
-
const expanded = ref(false);
|
|
19
|
-
|
|
20
|
-
function submitReply() {
|
|
21
|
-
if (!replyBody.value.trim() || submitting.value) return;
|
|
22
|
-
submitting.value = true;
|
|
23
|
-
|
|
24
|
-
router.post(
|
|
25
|
-
route('escalated.admin.tickets.side-conversations.reply', [props.ticketReference, props.conversation.id]),
|
|
26
|
-
{ body: replyBody.value },
|
|
27
|
-
{
|
|
28
|
-
preserveScroll: true,
|
|
29
|
-
onSuccess: () => {
|
|
30
|
-
replyBody.value = '';
|
|
31
|
-
emit('updated');
|
|
32
|
-
},
|
|
33
|
-
onFinish: () => {
|
|
34
|
-
submitting.value = false;
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function closeConversation() {
|
|
41
|
-
router.post(
|
|
42
|
-
route('escalated.admin.tickets.side-conversations.close', [props.ticketReference, props.conversation.id]),
|
|
43
|
-
{},
|
|
44
|
-
{
|
|
45
|
-
preserveScroll: true,
|
|
46
|
-
onSuccess: () => emit('updated'),
|
|
47
|
-
},
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
</script>
|
|
51
|
-
|
|
52
|
-
<template>
|
|
53
|
-
<div
|
|
54
|
-
:class="[
|
|
55
|
-
'rounded-lg border p-3',
|
|
56
|
-
escDark ? 'border-white/[0.06] bg-white/[0.02]' : 'border-gray-200 bg-gray-50',
|
|
57
|
-
]"
|
|
58
|
-
>
|
|
59
|
-
<button
|
|
60
|
-
class="flex w-full items-center justify-between"
|
|
61
|
-
:aria-expanded="expanded"
|
|
62
|
-
@click="expanded = !expanded"
|
|
63
|
-
>
|
|
64
|
-
<div class="flex items-center gap-2">
|
|
65
|
-
<span
|
|
66
|
-
:class="[
|
|
67
|
-
'inline-flex h-5 items-center rounded px-1.5 text-[10px] font-semibold uppercase',
|
|
68
|
-
conversation.status === 'open'
|
|
69
|
-
? escDark
|
|
70
|
-
? 'bg-emerald-500/15 text-emerald-400'
|
|
71
|
-
: 'bg-emerald-100 text-emerald-700'
|
|
72
|
-
: escDark
|
|
73
|
-
? 'bg-neutral-500/15 text-neutral-400'
|
|
74
|
-
: 'bg-gray-100 text-gray-500',
|
|
75
|
-
]"
|
|
76
|
-
>
|
|
77
|
-
{{ conversation.status }}
|
|
78
|
-
</span>
|
|
79
|
-
<span :class="['text-sm font-medium', escDark ? 'text-neutral-200' : 'text-gray-900']">
|
|
80
|
-
{{ conversation.subject }}
|
|
81
|
-
</span>
|
|
82
|
-
<span :class="['text-xs', escDark ? 'text-neutral-500' : 'text-gray-400']">
|
|
83
|
-
{{ conversation.channel }}
|
|
84
|
-
</span>
|
|
85
|
-
</div>
|
|
86
|
-
<svg
|
|
87
|
-
:class="[
|
|
88
|
-
'h-4 w-4 transition-transform',
|
|
89
|
-
escDark ? 'text-neutral-500' : 'text-gray-400',
|
|
90
|
-
expanded && 'rotate-180',
|
|
91
|
-
]"
|
|
92
|
-
fill="none"
|
|
93
|
-
stroke="currentColor"
|
|
94
|
-
stroke-width="1.5"
|
|
95
|
-
viewBox="0 0 24 24"
|
|
96
|
-
>
|
|
97
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
98
|
-
</svg>
|
|
99
|
-
</button>
|
|
100
|
-
|
|
101
|
-
<div v-if="expanded" class="mt-3 space-y-3">
|
|
102
|
-
<!-- Replies -->
|
|
103
|
-
<div
|
|
104
|
-
v-for="reply in conversation.replies"
|
|
105
|
-
:key="reply.id"
|
|
106
|
-
:class="['border-l-2 pl-3', escDark ? 'border-white/[0.06]' : 'border-gray-200']"
|
|
107
|
-
>
|
|
108
|
-
<div class="flex items-center gap-2">
|
|
109
|
-
<span :class="['text-xs font-medium', escDark ? 'text-neutral-300' : 'text-gray-700']">
|
|
110
|
-
{{ reply.author?.name || 'System' }}
|
|
111
|
-
</span>
|
|
112
|
-
<span :class="['text-xs', escDark ? 'text-neutral-500' : 'text-gray-400']">
|
|
113
|
-
{{ reply.created_at }}
|
|
114
|
-
</span>
|
|
115
|
-
</div>
|
|
116
|
-
<p :class="['mt-1 whitespace-pre-wrap text-sm', escDark ? 'text-neutral-400' : 'text-gray-600']">
|
|
117
|
-
{{ reply.body }}
|
|
118
|
-
</p>
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
<div
|
|
122
|
-
v-if="!conversation.replies?.length"
|
|
123
|
-
:class="['text-xs', escDark ? 'text-neutral-500' : 'text-gray-400']"
|
|
124
|
-
>
|
|
125
|
-
No replies yet.
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
<!-- Reply form -->
|
|
129
|
-
<div v-if="conversation.status === 'open'" class="pt-2">
|
|
130
|
-
<textarea
|
|
131
|
-
v-model="replyBody"
|
|
132
|
-
rows="2"
|
|
133
|
-
placeholder="Write a reply..."
|
|
134
|
-
:class="[
|
|
135
|
-
'w-full rounded-lg border px-3 py-2 text-sm focus:outline-none focus:ring-1',
|
|
136
|
-
escDark
|
|
137
|
-
? 'border-white/10 bg-neutral-950 text-neutral-200 placeholder-neutral-500 focus:border-white/20 focus:ring-white/10'
|
|
138
|
-
: 'border-gray-300 bg-white text-gray-900 placeholder-gray-400 focus:border-blue-400 focus:ring-blue-200',
|
|
139
|
-
]"
|
|
140
|
-
></textarea>
|
|
141
|
-
<div class="mt-2 flex items-center gap-2">
|
|
142
|
-
<button
|
|
143
|
-
:disabled="!replyBody.trim() || submitting"
|
|
144
|
-
:class="[
|
|
145
|
-
'rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-3 py-1.5 text-xs font-medium text-white transition-all hover:from-cyan-400 hover:to-violet-400',
|
|
146
|
-
(!replyBody.trim() || submitting) && 'cursor-not-allowed opacity-40',
|
|
147
|
-
]"
|
|
148
|
-
@click="submitReply"
|
|
149
|
-
>
|
|
150
|
-
Reply
|
|
151
|
-
</button>
|
|
152
|
-
<button
|
|
153
|
-
:class="[
|
|
154
|
-
'rounded-lg px-3 py-1.5 text-xs',
|
|
155
|
-
escDark ? 'text-neutral-400 hover:text-neutral-300' : 'text-gray-500 hover:text-gray-700',
|
|
156
|
-
]"
|
|
157
|
-
@click="closeConversation"
|
|
158
|
-
>
|
|
159
|
-
Close
|
|
160
|
-
</button>
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, inject, computed } from 'vue';
|
|
3
|
+
import { router } from '@inertiajs/vue3';
|
|
4
|
+
|
|
5
|
+
const escDark = inject(
|
|
6
|
+
'esc-dark',
|
|
7
|
+
computed(() => false),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
conversation: { type: Object, required: true },
|
|
12
|
+
ticketReference: { type: String, required: true },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits(['updated']);
|
|
16
|
+
const replyBody = ref('');
|
|
17
|
+
const submitting = ref(false);
|
|
18
|
+
const expanded = ref(false);
|
|
19
|
+
|
|
20
|
+
function submitReply() {
|
|
21
|
+
if (!replyBody.value.trim() || submitting.value) return;
|
|
22
|
+
submitting.value = true;
|
|
23
|
+
|
|
24
|
+
router.post(
|
|
25
|
+
route('escalated.admin.tickets.side-conversations.reply', [props.ticketReference, props.conversation.id]),
|
|
26
|
+
{ body: replyBody.value },
|
|
27
|
+
{
|
|
28
|
+
preserveScroll: true,
|
|
29
|
+
onSuccess: () => {
|
|
30
|
+
replyBody.value = '';
|
|
31
|
+
emit('updated');
|
|
32
|
+
},
|
|
33
|
+
onFinish: () => {
|
|
34
|
+
submitting.value = false;
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function closeConversation() {
|
|
41
|
+
router.post(
|
|
42
|
+
route('escalated.admin.tickets.side-conversations.close', [props.ticketReference, props.conversation.id]),
|
|
43
|
+
{},
|
|
44
|
+
{
|
|
45
|
+
preserveScroll: true,
|
|
46
|
+
onSuccess: () => emit('updated'),
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div
|
|
54
|
+
:class="[
|
|
55
|
+
'rounded-lg border p-3',
|
|
56
|
+
escDark ? 'border-white/[0.06] bg-white/[0.02]' : 'border-gray-200 bg-gray-50',
|
|
57
|
+
]"
|
|
58
|
+
>
|
|
59
|
+
<button
|
|
60
|
+
class="flex w-full items-center justify-between"
|
|
61
|
+
:aria-expanded="expanded"
|
|
62
|
+
@click="expanded = !expanded"
|
|
63
|
+
>
|
|
64
|
+
<div class="flex items-center gap-2">
|
|
65
|
+
<span
|
|
66
|
+
:class="[
|
|
67
|
+
'inline-flex h-5 items-center rounded px-1.5 text-[10px] font-semibold uppercase',
|
|
68
|
+
conversation.status === 'open'
|
|
69
|
+
? escDark
|
|
70
|
+
? 'bg-emerald-500/15 text-emerald-400'
|
|
71
|
+
: 'bg-emerald-100 text-emerald-700'
|
|
72
|
+
: escDark
|
|
73
|
+
? 'bg-neutral-500/15 text-neutral-400'
|
|
74
|
+
: 'bg-gray-100 text-gray-500',
|
|
75
|
+
]"
|
|
76
|
+
>
|
|
77
|
+
{{ conversation.status }}
|
|
78
|
+
</span>
|
|
79
|
+
<span :class="['text-sm font-medium', escDark ? 'text-neutral-200' : 'text-gray-900']">
|
|
80
|
+
{{ conversation.subject }}
|
|
81
|
+
</span>
|
|
82
|
+
<span :class="['text-xs', escDark ? 'text-neutral-500' : 'text-gray-400']">
|
|
83
|
+
{{ conversation.channel }}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
<svg
|
|
87
|
+
:class="[
|
|
88
|
+
'h-4 w-4 transition-transform',
|
|
89
|
+
escDark ? 'text-neutral-500' : 'text-gray-400',
|
|
90
|
+
expanded && 'rotate-180',
|
|
91
|
+
]"
|
|
92
|
+
fill="none"
|
|
93
|
+
stroke="currentColor"
|
|
94
|
+
stroke-width="1.5"
|
|
95
|
+
viewBox="0 0 24 24"
|
|
96
|
+
>
|
|
97
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
98
|
+
</svg>
|
|
99
|
+
</button>
|
|
100
|
+
|
|
101
|
+
<div v-if="expanded" class="mt-3 space-y-3">
|
|
102
|
+
<!-- Replies -->
|
|
103
|
+
<div
|
|
104
|
+
v-for="reply in conversation.replies"
|
|
105
|
+
:key="reply.id"
|
|
106
|
+
:class="['border-l-2 pl-3', escDark ? 'border-white/[0.06]' : 'border-gray-200']"
|
|
107
|
+
>
|
|
108
|
+
<div class="flex items-center gap-2">
|
|
109
|
+
<span :class="['text-xs font-medium', escDark ? 'text-neutral-300' : 'text-gray-700']">
|
|
110
|
+
{{ reply.author?.name || 'System' }}
|
|
111
|
+
</span>
|
|
112
|
+
<span :class="['text-xs', escDark ? 'text-neutral-500' : 'text-gray-400']">
|
|
113
|
+
{{ reply.created_at }}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
<p :class="['mt-1 whitespace-pre-wrap text-sm', escDark ? 'text-neutral-400' : 'text-gray-600']">
|
|
117
|
+
{{ reply.body }}
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div
|
|
122
|
+
v-if="!conversation.replies?.length"
|
|
123
|
+
:class="['text-xs', escDark ? 'text-neutral-500' : 'text-gray-400']"
|
|
124
|
+
>
|
|
125
|
+
No replies yet.
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Reply form -->
|
|
129
|
+
<div v-if="conversation.status === 'open'" class="pt-2">
|
|
130
|
+
<textarea
|
|
131
|
+
v-model="replyBody"
|
|
132
|
+
rows="2"
|
|
133
|
+
placeholder="Write a reply..."
|
|
134
|
+
:class="[
|
|
135
|
+
'w-full rounded-lg border px-3 py-2 text-sm focus:outline-none focus:ring-1',
|
|
136
|
+
escDark
|
|
137
|
+
? 'border-white/10 bg-neutral-950 text-neutral-200 placeholder-neutral-500 focus:border-white/20 focus:ring-white/10'
|
|
138
|
+
: 'border-gray-300 bg-white text-gray-900 placeholder-gray-400 focus:border-blue-400 focus:ring-blue-200',
|
|
139
|
+
]"
|
|
140
|
+
></textarea>
|
|
141
|
+
<div class="mt-2 flex items-center gap-2">
|
|
142
|
+
<button
|
|
143
|
+
:disabled="!replyBody.trim() || submitting"
|
|
144
|
+
:class="[
|
|
145
|
+
'rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-3 py-1.5 text-xs font-medium text-white transition-all hover:from-cyan-400 hover:to-violet-400',
|
|
146
|
+
(!replyBody.trim() || submitting) && 'cursor-not-allowed opacity-40',
|
|
147
|
+
]"
|
|
148
|
+
@click="submitReply"
|
|
149
|
+
>
|
|
150
|
+
Reply
|
|
151
|
+
</button>
|
|
152
|
+
<button
|
|
153
|
+
:class="[
|
|
154
|
+
'rounded-lg px-3 py-1.5 text-xs',
|
|
155
|
+
escDark ? 'text-neutral-400 hover:text-neutral-300' : 'text-gray-500 hover:text-gray-700',
|
|
156
|
+
]"
|
|
157
|
+
@click="closeConversation"
|
|
158
|
+
>
|
|
159
|
+
Close
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</template>
|
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { ref, onMounted } from 'vue';
|
|
3
|
-
import { router } from '@inertiajs/vue3';
|
|
4
|
-
import SideConversation from './SideConversation.vue';
|
|
5
|
-
|
|
6
|
-
const props = defineProps({
|
|
7
|
-
ticketReference: { type: String, required: true },
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
const conversations = ref([]);
|
|
11
|
-
const expanded = ref(true);
|
|
12
|
-
const showNew = ref(false);
|
|
13
|
-
const newSubject = ref('');
|
|
14
|
-
const newChannel = ref('internal');
|
|
15
|
-
const newBody = ref('');
|
|
16
|
-
const creating = ref(false);
|
|
17
|
-
|
|
18
|
-
onMounted(() => {
|
|
19
|
-
fetchConversations();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
async function fetchConversations() {
|
|
23
|
-
try {
|
|
24
|
-
const response = await fetch(route('escalated.admin.tickets.side-conversations.index', props.ticketReference), {
|
|
25
|
-
headers: {
|
|
26
|
-
Accept: 'application/json',
|
|
27
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
28
|
-
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
if (response.ok) {
|
|
32
|
-
const data = await response.json();
|
|
33
|
-
conversations.value = data.conversations || [];
|
|
34
|
-
}
|
|
35
|
-
} catch {
|
|
36
|
-
// silently ignore
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function createConversation() {
|
|
41
|
-
if (!newSubject.value.trim() || !newBody.value.trim() || creating.value) return;
|
|
42
|
-
creating.value = true;
|
|
43
|
-
|
|
44
|
-
router.post(
|
|
45
|
-
route('escalated.admin.tickets.side-conversations.store', props.ticketReference),
|
|
46
|
-
{
|
|
47
|
-
subject: newSubject.value,
|
|
48
|
-
channel: newChannel.value,
|
|
49
|
-
body: newBody.value,
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
preserveScroll: true,
|
|
53
|
-
onSuccess: () => {
|
|
54
|
-
newSubject.value = '';
|
|
55
|
-
newBody.value = '';
|
|
56
|
-
newChannel.value = 'internal';
|
|
57
|
-
showNew.value = false;
|
|
58
|
-
fetchConversations();
|
|
59
|
-
},
|
|
60
|
-
onFinish: () => {
|
|
61
|
-
creating.value = false;
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
</script>
|
|
67
|
-
|
|
68
|
-
<template>
|
|
69
|
-
<div class="rounded-xl border border-white/[0.06] bg-neutral-900/60 p-4">
|
|
70
|
-
<button
|
|
71
|
-
class="flex w-full items-center justify-between text-sm font-semibold text-neutral-200"
|
|
72
|
-
@click="expanded = !expanded"
|
|
73
|
-
>
|
|
74
|
-
<span>Side Conversations ({{ conversations.length }})</span>
|
|
75
|
-
<svg
|
|
76
|
-
:class="['h-4 w-4 text-neutral-500 transition-transform', expanded && 'rotate-180']"
|
|
77
|
-
fill="none"
|
|
78
|
-
stroke="currentColor"
|
|
79
|
-
stroke-width="1.5"
|
|
80
|
-
viewBox="0 0 24 24"
|
|
81
|
-
>
|
|
82
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
83
|
-
</svg>
|
|
84
|
-
</button>
|
|
85
|
-
|
|
86
|
-
<div v-if="expanded" class="mt-3 space-y-3">
|
|
87
|
-
<SideConversation
|
|
88
|
-
v-for="conv in conversations"
|
|
89
|
-
:key="conv.id"
|
|
90
|
-
:conversation="conv"
|
|
91
|
-
:ticket-reference="ticketReference"
|
|
92
|
-
@updated="fetchConversations"
|
|
93
|
-
/>
|
|
94
|
-
|
|
95
|
-
<div v-if="!conversations.length && !showNew" class="text-xs text-neutral-500">No side conversations.</div>
|
|
96
|
-
|
|
97
|
-
<!-- New conversation form -->
|
|
98
|
-
<div v-if="showNew" class="rounded-lg border border-white/[0.06] bg-white/[0.02] p-3 space-y-2">
|
|
99
|
-
<input
|
|
100
|
-
v-model="newSubject"
|
|
101
|
-
type="text"
|
|
102
|
-
placeholder="Subject..."
|
|
103
|
-
class="w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-500 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
104
|
-
/>
|
|
105
|
-
<select
|
|
106
|
-
v-model="newChannel"
|
|
107
|
-
class="rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 focus:border-white/20 focus:outline-none"
|
|
108
|
-
>
|
|
109
|
-
<option value="internal">Internal</option>
|
|
110
|
-
<option value="email">Email</option>
|
|
111
|
-
</select>
|
|
112
|
-
<textarea
|
|
113
|
-
v-model="newBody"
|
|
114
|
-
rows="3"
|
|
115
|
-
placeholder="Write the first message..."
|
|
116
|
-
class="w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-500 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
117
|
-
></textarea>
|
|
118
|
-
<div class="flex gap-2">
|
|
119
|
-
<button
|
|
120
|
-
:disabled="!newSubject.trim() || !newBody.trim() || creating"
|
|
121
|
-
:class="[
|
|
122
|
-
'rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-3 py-1.5 text-xs font-medium text-white transition-all hover:from-cyan-400 hover:to-violet-400',
|
|
123
|
-
(!newSubject.trim() || !newBody.trim() || creating) && 'cursor-not-allowed opacity-40',
|
|
124
|
-
]"
|
|
125
|
-
@click="createConversation"
|
|
126
|
-
>
|
|
127
|
-
Create
|
|
128
|
-
</button>
|
|
129
|
-
<button
|
|
130
|
-
class="rounded-lg px-3 py-1.5 text-xs text-neutral-400 hover:text-neutral-300"
|
|
131
|
-
@click="showNew = false"
|
|
132
|
-
>
|
|
133
|
-
Cancel
|
|
134
|
-
</button>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
<button
|
|
139
|
-
v-if="!showNew"
|
|
140
|
-
class="w-full rounded-lg border border-dashed border-white/[0.08] py-2 text-xs text-neutral-400 transition-colors hover:border-white/[0.15] hover:text-neutral-300"
|
|
141
|
-
@click="showNew = true"
|
|
142
|
-
>
|
|
143
|
-
+ New Side Conversation
|
|
144
|
-
</button>
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, onMounted } from 'vue';
|
|
3
|
+
import { router } from '@inertiajs/vue3';
|
|
4
|
+
import SideConversation from './SideConversation.vue';
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
ticketReference: { type: String, required: true },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const conversations = ref([]);
|
|
11
|
+
const expanded = ref(true);
|
|
12
|
+
const showNew = ref(false);
|
|
13
|
+
const newSubject = ref('');
|
|
14
|
+
const newChannel = ref('internal');
|
|
15
|
+
const newBody = ref('');
|
|
16
|
+
const creating = ref(false);
|
|
17
|
+
|
|
18
|
+
onMounted(() => {
|
|
19
|
+
fetchConversations();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
async function fetchConversations() {
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(route('escalated.admin.tickets.side-conversations.index', props.ticketReference), {
|
|
25
|
+
headers: {
|
|
26
|
+
Accept: 'application/json',
|
|
27
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
28
|
+
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
if (response.ok) {
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
conversations.value = data.conversations || [];
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// silently ignore
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createConversation() {
|
|
41
|
+
if (!newSubject.value.trim() || !newBody.value.trim() || creating.value) return;
|
|
42
|
+
creating.value = true;
|
|
43
|
+
|
|
44
|
+
router.post(
|
|
45
|
+
route('escalated.admin.tickets.side-conversations.store', props.ticketReference),
|
|
46
|
+
{
|
|
47
|
+
subject: newSubject.value,
|
|
48
|
+
channel: newChannel.value,
|
|
49
|
+
body: newBody.value,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
preserveScroll: true,
|
|
53
|
+
onSuccess: () => {
|
|
54
|
+
newSubject.value = '';
|
|
55
|
+
newBody.value = '';
|
|
56
|
+
newChannel.value = 'internal';
|
|
57
|
+
showNew.value = false;
|
|
58
|
+
fetchConversations();
|
|
59
|
+
},
|
|
60
|
+
onFinish: () => {
|
|
61
|
+
creating.value = false;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<div class="rounded-xl border border-white/[0.06] bg-neutral-900/60 p-4">
|
|
70
|
+
<button
|
|
71
|
+
class="flex w-full items-center justify-between text-sm font-semibold text-neutral-200"
|
|
72
|
+
@click="expanded = !expanded"
|
|
73
|
+
>
|
|
74
|
+
<span>Side Conversations ({{ conversations.length }})</span>
|
|
75
|
+
<svg
|
|
76
|
+
:class="['h-4 w-4 text-neutral-500 transition-transform', expanded && 'rotate-180']"
|
|
77
|
+
fill="none"
|
|
78
|
+
stroke="currentColor"
|
|
79
|
+
stroke-width="1.5"
|
|
80
|
+
viewBox="0 0 24 24"
|
|
81
|
+
>
|
|
82
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
83
|
+
</svg>
|
|
84
|
+
</button>
|
|
85
|
+
|
|
86
|
+
<div v-if="expanded" class="mt-3 space-y-3">
|
|
87
|
+
<SideConversation
|
|
88
|
+
v-for="conv in conversations"
|
|
89
|
+
:key="conv.id"
|
|
90
|
+
:conversation="conv"
|
|
91
|
+
:ticket-reference="ticketReference"
|
|
92
|
+
@updated="fetchConversations"
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
<div v-if="!conversations.length && !showNew" class="text-xs text-neutral-500">No side conversations.</div>
|
|
96
|
+
|
|
97
|
+
<!-- New conversation form -->
|
|
98
|
+
<div v-if="showNew" class="rounded-lg border border-white/[0.06] bg-white/[0.02] p-3 space-y-2">
|
|
99
|
+
<input
|
|
100
|
+
v-model="newSubject"
|
|
101
|
+
type="text"
|
|
102
|
+
placeholder="Subject..."
|
|
103
|
+
class="w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-500 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
104
|
+
/>
|
|
105
|
+
<select
|
|
106
|
+
v-model="newChannel"
|
|
107
|
+
class="rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 focus:border-white/20 focus:outline-none"
|
|
108
|
+
>
|
|
109
|
+
<option value="internal">Internal</option>
|
|
110
|
+
<option value="email">Email</option>
|
|
111
|
+
</select>
|
|
112
|
+
<textarea
|
|
113
|
+
v-model="newBody"
|
|
114
|
+
rows="3"
|
|
115
|
+
placeholder="Write the first message..."
|
|
116
|
+
class="w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-500 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
117
|
+
></textarea>
|
|
118
|
+
<div class="flex gap-2">
|
|
119
|
+
<button
|
|
120
|
+
:disabled="!newSubject.trim() || !newBody.trim() || creating"
|
|
121
|
+
:class="[
|
|
122
|
+
'rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-3 py-1.5 text-xs font-medium text-white transition-all hover:from-cyan-400 hover:to-violet-400',
|
|
123
|
+
(!newSubject.trim() || !newBody.trim() || creating) && 'cursor-not-allowed opacity-40',
|
|
124
|
+
]"
|
|
125
|
+
@click="createConversation"
|
|
126
|
+
>
|
|
127
|
+
Create
|
|
128
|
+
</button>
|
|
129
|
+
<button
|
|
130
|
+
class="rounded-lg px-3 py-1.5 text-xs text-neutral-400 hover:text-neutral-300"
|
|
131
|
+
@click="showNew = false"
|
|
132
|
+
>
|
|
133
|
+
Cancel
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<button
|
|
139
|
+
v-if="!showNew"
|
|
140
|
+
class="w-full rounded-lg border border-dashed border-white/[0.08] py-2 text-xs text-neutral-400 transition-colors hover:border-white/[0.15] hover:text-neutral-300"
|
|
141
|
+
@click="showNew = true"
|
|
142
|
+
>
|
|
143
|
+
+ New Side Conversation
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|