@escalated-dev/escalated 0.8.0 → 0.9.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.
- package/LICENSE +21 -21
- package/README.md +380 -361
- package/package.json +85 -84
- package/src/components/ActiveChatsPanel.vue +246 -246
- package/src/components/AdminDashboard.stories.js +1423 -1423
- package/src/components/AttachmentList.vue +65 -65
- package/src/components/ChatActionBar.vue +226 -226
- package/src/components/ChatBubble.vue +162 -162
- package/src/components/ChatComposer.vue +308 -308
- package/src/components/ChatQueue.vue +222 -222
- package/src/components/ChatThread.vue +197 -197
- package/src/components/CustomTicketActions.stories.js +71 -0
- package/src/components/CustomTicketActions.vue +101 -0
- package/src/components/EscalatedLayout.vue +619 -604
- package/src/components/FollowButton.vue +20 -1
- package/src/components/MentionDropdown.vue +154 -154
- package/src/components/MetricComparison.vue +89 -89
- package/src/components/PercentileChart.vue +67 -67
- package/src/components/PeriodSelector.vue +98 -98
- package/src/components/PresenceIndicator.vue +228 -213
- package/src/components/ReplyComposer.vue +336 -336
- package/src/components/ReplyThread.vue +153 -153
- package/src/components/ReportChart.vue +395 -395
- package/src/components/ReportExportButton.vue +108 -108
- package/src/components/SaveViewDialog.vue +172 -172
- package/src/components/SavedViewSidebar.vue +208 -208
- package/src/components/SkillTagManager.vue +3 -0
- package/src/components/SnoozeButton.vue +247 -247
- package/src/components/TicketFilters.vue +270 -270
- package/src/components/TicketList.vue +655 -655
- package/src/components/TicketSidebar.vue +4 -0
- package/src/components/TicketSplitDialog.vue +98 -98
- package/src/components/TicketSubjects.stories.js +63 -0
- package/src/components/TicketSubjects.vue +107 -0
- package/src/components/TwoFactorSetup.vue +1 -37
- package/src/components/VariableTokenInput.vue +111 -111
- package/src/components/WorkflowActionConfig.vue +287 -287
- package/src/components/WorkflowActionList.vue +350 -350
- package/src/components/WorkflowConditionBuilder.vue +265 -265
- package/src/components/WorkflowTriggerSelector.vue +125 -125
- package/src/components/admin/newsletters/AnalyticsTiles.stories.js +14 -0
- package/src/components/admin/newsletters/AnalyticsTiles.vue +55 -0
- package/src/components/admin/newsletters/DeliveriesTable.stories.js +28 -0
- package/src/components/admin/newsletters/DeliveriesTable.vue +104 -0
- package/src/components/admin/newsletters/DynamicFilterBuilder.stories.js +11 -0
- package/src/components/admin/newsletters/DynamicFilterBuilder.vue +53 -0
- package/src/components/admin/newsletters/ListMemberTable.stories.js +12 -0
- package/src/components/admin/newsletters/ListMemberTable.vue +53 -0
- package/src/components/admin/newsletters/MarkdownEditor.stories.js +6 -0
- package/src/components/admin/newsletters/MarkdownEditor.vue +53 -0
- package/src/components/admin/newsletters/MergeFieldDropdown.stories.js +6 -0
- package/src/components/admin/newsletters/MergeFieldDropdown.vue +84 -0
- package/src/components/admin/newsletters/PreviewIframe.stories.js +7 -0
- package/src/components/admin/newsletters/PreviewIframe.vue +40 -0
- package/src/components/admin/newsletters/SummaryTiles.stories.js +8 -0
- package/src/components/admin/newsletters/SummaryTiles.vue +63 -0
- package/src/composables/useChat.js +187 -187
- package/src/composables/useI18n.js +102 -102
- package/src/composables/useMentions.js +134 -134
- package/src/composables/usePermissions.js +31 -0
- package/src/composables/useRealtime.js +182 -182
- package/src/index.js +85 -84
- package/src/locales/README.md +13 -13
- package/src/locales/ar.json +727 -727
- package/src/locales/de.json +690 -690
- package/src/locales/en.json +889 -729
- package/src/locales/es.json +691 -691
- package/src/locales/fr.json +691 -691
- package/src/locales/index.js +16 -16
- package/src/locales/it.json +727 -727
- package/src/locales/ja.json +727 -727
- package/src/locales/ko.json +727 -727
- package/src/locales/nl.json +727 -727
- package/src/locales/pl.json +727 -727
- package/src/locales/pt-BR.json +727 -727
- package/src/locales/ru.json +727 -727
- package/src/locales/tr.json +727 -727
- package/src/locales/zh-CN.json +727 -727
- package/src/pages/Admin/Newsletters/Compose.vue +192 -0
- package/src/pages/Admin/Newsletters/Edit.vue +24 -0
- package/src/pages/Admin/Newsletters/Index.vue +141 -0
- package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
- package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
- package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
- package/src/pages/Admin/Newsletters/Settings.vue +85 -0
- package/src/pages/Admin/Newsletters/Show.vue +140 -0
- package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
- package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
- package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
- package/src/pages/Admin/Newsletters/usePreview.js +44 -0
- package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
- package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
- package/src/pages/Admin/Reports/Cohorts.vue +180 -180
- package/src/pages/Admin/Reports/Comparison.vue +141 -141
- package/src/pages/Admin/Reports/Dashboard.vue +226 -226
- package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
- package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
- package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
- package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
- package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
- package/src/pages/Admin/Settings.vue +1008 -1008
- package/src/pages/Admin/Skills/Form.vue +30 -10
- package/src/pages/Admin/Tickets/Index.vue +126 -126
- package/src/pages/Admin/Tickets/Show.vue +254 -249
- package/src/pages/Admin/Users/Index.vue +145 -145
- package/src/pages/Admin/Workflows/Builder.vue +207 -207
- package/src/pages/Admin/Workflows/Index.vue +235 -235
- package/src/pages/Admin/Workflows/Logs.vue +286 -286
- package/src/pages/Agent/TicketIndex.vue +112 -112
- package/src/pages/Agent/TicketShow.vue +547 -494
- package/src/pages/Customer/Index.vue +56 -56
- package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
- package/src/plugin.js +175 -175
- package/src/utils/mentionHighlight.js +24 -24
- package/src/widget/EscalatedWidget.vue +804 -804
- package/src/widget/index.js +254 -254
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { computed } from 'vue';
|
|
3
|
-
|
|
4
|
-
const props = defineProps({
|
|
5
|
-
modelValue: { type: String, default: '' },
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
const emit = defineEmits(['update:modelValue']);
|
|
9
|
-
|
|
10
|
-
const triggers = [
|
|
11
|
-
{
|
|
12
|
-
value: 'ticket_created',
|
|
13
|
-
label: 'Ticket Created',
|
|
14
|
-
description: 'When a new ticket is submitted',
|
|
15
|
-
icon: 'M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
value: 'ticket_updated',
|
|
19
|
-
label: 'Ticket Updated',
|
|
20
|
-
description: 'When any ticket field is modified',
|
|
21
|
-
icon: 'M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182',
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
value: 'reply_added',
|
|
25
|
-
label: 'Reply Added',
|
|
26
|
-
description: 'When a reply is posted on a ticket',
|
|
27
|
-
icon: 'M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z',
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
value: 'status_changed',
|
|
31
|
-
label: 'Status Changed',
|
|
32
|
-
description: 'When the ticket status is updated',
|
|
33
|
-
icon: 'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
value: 'ticket_assigned',
|
|
37
|
-
label: 'Ticket Assigned',
|
|
38
|
-
description: 'When a ticket is assigned to an agent',
|
|
39
|
-
icon: 'M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z',
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
value: 'ticket_escalated',
|
|
43
|
-
label: 'Ticket Escalated',
|
|
44
|
-
description: 'When a ticket is escalated',
|
|
45
|
-
icon: 'M3 4.5h14.25M3 9h9.75M3 13.5h5.25m5.25-.75L17.25 9m0 0L21 12.75M17.25 9v12',
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
value: 'sla_breached',
|
|
49
|
-
label: 'SLA Breached',
|
|
50
|
-
description: 'When an SLA target is breached',
|
|
51
|
-
icon: 'M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z',
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
value: 'sla_warning',
|
|
55
|
-
label: 'SLA Warning',
|
|
56
|
-
description: 'When an SLA target is approaching',
|
|
57
|
-
icon: 'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
value: 'chat_started',
|
|
61
|
-
label: 'Chat Started',
|
|
62
|
-
description: 'When a live chat session begins',
|
|
63
|
-
icon: 'M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155',
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
value: 'chat_ended',
|
|
67
|
-
label: 'Chat Ended',
|
|
68
|
-
description: 'When a live chat session ends',
|
|
69
|
-
icon: 'M15 11.25l-3-3m0 0l-3 3m3-3v7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
70
|
-
},
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
const selected = computed(() => props.modelValue);
|
|
74
|
-
|
|
75
|
-
function select(value) {
|
|
76
|
-
emit('update:modelValue', value);
|
|
77
|
-
}
|
|
78
|
-
</script>
|
|
79
|
-
|
|
80
|
-
<template>
|
|
81
|
-
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
|
|
82
|
-
<button
|
|
83
|
-
v-for="trigger in triggers"
|
|
84
|
-
:key="trigger.value"
|
|
85
|
-
type="button"
|
|
86
|
-
class="group relative flex flex-col items-center rounded-xl border-2 p-4 text-center transition-all"
|
|
87
|
-
:class="
|
|
88
|
-
selected === trigger.value
|
|
89
|
-
? 'border-blue-500 bg-blue-500/10 shadow-lg shadow-blue-500/10'
|
|
90
|
-
: 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] hover:border-[var(--esc-panel-text-muted)] hover:bg-[var(--esc-panel-hover)]'
|
|
91
|
-
"
|
|
92
|
-
@click="select(trigger.value)"
|
|
93
|
-
>
|
|
94
|
-
<div
|
|
95
|
-
class="mb-2 flex h-10 w-10 items-center justify-center rounded-lg"
|
|
96
|
-
:class="
|
|
97
|
-
selected === trigger.value
|
|
98
|
-
? 'bg-blue-500/20 text-blue-400'
|
|
99
|
-
: 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-muted)] group-hover:text-[var(--esc-panel-text-secondary)]'
|
|
100
|
-
"
|
|
101
|
-
>
|
|
102
|
-
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
103
|
-
<path stroke-linecap="round" stroke-linejoin="round" :d="trigger.icon" />
|
|
104
|
-
</svg>
|
|
105
|
-
</div>
|
|
106
|
-
<span
|
|
107
|
-
class="text-xs font-semibold"
|
|
108
|
-
:class="selected === trigger.value ? 'text-blue-400' : 'text-[var(--esc-panel-text-secondary)]'"
|
|
109
|
-
>
|
|
110
|
-
{{ trigger.label }}
|
|
111
|
-
</span>
|
|
112
|
-
<span class="mt-1 text-[10px] leading-tight text-[var(--esc-panel-text-muted)]">
|
|
113
|
-
{{ trigger.description }}
|
|
114
|
-
</span>
|
|
115
|
-
<div
|
|
116
|
-
v-if="selected === trigger.value"
|
|
117
|
-
class="absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white"
|
|
118
|
-
>
|
|
119
|
-
<svg class="h-3 w-3" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
|
|
120
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
121
|
-
</svg>
|
|
122
|
-
</div>
|
|
123
|
-
</button>
|
|
124
|
-
</div>
|
|
125
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
modelValue: { type: String, default: '' },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const emit = defineEmits(['update:modelValue']);
|
|
9
|
+
|
|
10
|
+
const triggers = [
|
|
11
|
+
{
|
|
12
|
+
value: 'ticket_created',
|
|
13
|
+
label: 'Ticket Created',
|
|
14
|
+
description: 'When a new ticket is submitted',
|
|
15
|
+
icon: 'M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
value: 'ticket_updated',
|
|
19
|
+
label: 'Ticket Updated',
|
|
20
|
+
description: 'When any ticket field is modified',
|
|
21
|
+
icon: 'M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
value: 'reply_added',
|
|
25
|
+
label: 'Reply Added',
|
|
26
|
+
description: 'When a reply is posted on a ticket',
|
|
27
|
+
icon: 'M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
value: 'status_changed',
|
|
31
|
+
label: 'Status Changed',
|
|
32
|
+
description: 'When the ticket status is updated',
|
|
33
|
+
icon: 'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
value: 'ticket_assigned',
|
|
37
|
+
label: 'Ticket Assigned',
|
|
38
|
+
description: 'When a ticket is assigned to an agent',
|
|
39
|
+
icon: 'M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
value: 'ticket_escalated',
|
|
43
|
+
label: 'Ticket Escalated',
|
|
44
|
+
description: 'When a ticket is escalated',
|
|
45
|
+
icon: 'M3 4.5h14.25M3 9h9.75M3 13.5h5.25m5.25-.75L17.25 9m0 0L21 12.75M17.25 9v12',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
value: 'sla_breached',
|
|
49
|
+
label: 'SLA Breached',
|
|
50
|
+
description: 'When an SLA target is breached',
|
|
51
|
+
icon: 'M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
value: 'sla_warning',
|
|
55
|
+
label: 'SLA Warning',
|
|
56
|
+
description: 'When an SLA target is approaching',
|
|
57
|
+
icon: 'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
value: 'chat_started',
|
|
61
|
+
label: 'Chat Started',
|
|
62
|
+
description: 'When a live chat session begins',
|
|
63
|
+
icon: 'M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
value: 'chat_ended',
|
|
67
|
+
label: 'Chat Ended',
|
|
68
|
+
description: 'When a live chat session ends',
|
|
69
|
+
icon: 'M15 11.25l-3-3m0 0l-3 3m3-3v7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const selected = computed(() => props.modelValue);
|
|
74
|
+
|
|
75
|
+
function select(value) {
|
|
76
|
+
emit('update:modelValue', value);
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<template>
|
|
81
|
+
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
|
|
82
|
+
<button
|
|
83
|
+
v-for="trigger in triggers"
|
|
84
|
+
:key="trigger.value"
|
|
85
|
+
type="button"
|
|
86
|
+
class="group relative flex flex-col items-center rounded-xl border-2 p-4 text-center transition-all"
|
|
87
|
+
:class="
|
|
88
|
+
selected === trigger.value
|
|
89
|
+
? 'border-blue-500 bg-blue-500/10 shadow-lg shadow-blue-500/10'
|
|
90
|
+
: 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] hover:border-[var(--esc-panel-text-muted)] hover:bg-[var(--esc-panel-hover)]'
|
|
91
|
+
"
|
|
92
|
+
@click="select(trigger.value)"
|
|
93
|
+
>
|
|
94
|
+
<div
|
|
95
|
+
class="mb-2 flex h-10 w-10 items-center justify-center rounded-lg"
|
|
96
|
+
:class="
|
|
97
|
+
selected === trigger.value
|
|
98
|
+
? 'bg-blue-500/20 text-blue-400'
|
|
99
|
+
: 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-muted)] group-hover:text-[var(--esc-panel-text-secondary)]'
|
|
100
|
+
"
|
|
101
|
+
>
|
|
102
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
103
|
+
<path stroke-linecap="round" stroke-linejoin="round" :d="trigger.icon" />
|
|
104
|
+
</svg>
|
|
105
|
+
</div>
|
|
106
|
+
<span
|
|
107
|
+
class="text-xs font-semibold"
|
|
108
|
+
:class="selected === trigger.value ? 'text-blue-400' : 'text-[var(--esc-panel-text-secondary)]'"
|
|
109
|
+
>
|
|
110
|
+
{{ trigger.label }}
|
|
111
|
+
</span>
|
|
112
|
+
<span class="mt-1 text-[10px] leading-tight text-[var(--esc-panel-text-muted)]">
|
|
113
|
+
{{ trigger.description }}
|
|
114
|
+
</span>
|
|
115
|
+
<div
|
|
116
|
+
v-if="selected === trigger.value"
|
|
117
|
+
class="absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white"
|
|
118
|
+
>
|
|
119
|
+
<svg class="h-3 w-3" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
|
|
120
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
121
|
+
</svg>
|
|
122
|
+
</div>
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import AnalyticsTiles from './AnalyticsTiles.vue';
|
|
2
|
+
|
|
3
|
+
export default { title: 'Admin/Newsletters/AnalyticsTiles', component: AnalyticsTiles };
|
|
4
|
+
|
|
5
|
+
export const Default = {
|
|
6
|
+
args: {
|
|
7
|
+
summary: { total: 1000, sent: 990, opened: 400, clicked: 80, bounced: 10, complained: 1 },
|
|
8
|
+
topClicks: [
|
|
9
|
+
{ url: 'https://example.com/launch', clicks: 42 },
|
|
10
|
+
{ url: 'https://example.com/blog/post', clicks: 18 },
|
|
11
|
+
{ url: 'https://example.com/pricing', clicks: 7 },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="analytics-tiles">
|
|
3
|
+
<SummaryTiles :summary="summary" />
|
|
4
|
+
<section v-if="topClicks?.length" class="analytics-tiles__top-clicks">
|
|
5
|
+
<h3>Top clicked URLs</h3>
|
|
6
|
+
<ol>
|
|
7
|
+
<li v-for="row in topClicks" :key="row.url">
|
|
8
|
+
<span class="analytics-tiles__url">{{ row.url }}</span>
|
|
9
|
+
<span class="analytics-tiles__count">{{ row.clicks }}</span>
|
|
10
|
+
</li>
|
|
11
|
+
</ol>
|
|
12
|
+
</section>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
import SummaryTiles from './SummaryTiles.vue';
|
|
18
|
+
|
|
19
|
+
defineProps({
|
|
20
|
+
summary: { type: Object, required: true },
|
|
21
|
+
topClicks: { type: Array, default: () => [] },
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<style scoped>
|
|
26
|
+
.analytics-tiles {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
gap: 24px;
|
|
30
|
+
}
|
|
31
|
+
.analytics-tiles__top-clicks h3 {
|
|
32
|
+
font-size: 14px;
|
|
33
|
+
text-transform: uppercase;
|
|
34
|
+
letter-spacing: 0.04em;
|
|
35
|
+
color: var(--escalated-text-muted, #64748b);
|
|
36
|
+
margin: 0 0 8px;
|
|
37
|
+
}
|
|
38
|
+
.analytics-tiles__top-clicks ol {
|
|
39
|
+
margin: 0;
|
|
40
|
+
padding-left: 20px;
|
|
41
|
+
}
|
|
42
|
+
.analytics-tiles__top-clicks li {
|
|
43
|
+
display: flex;
|
|
44
|
+
justify-content: space-between;
|
|
45
|
+
gap: 12px;
|
|
46
|
+
padding: 4px 0;
|
|
47
|
+
}
|
|
48
|
+
.analytics-tiles__url {
|
|
49
|
+
word-break: break-all;
|
|
50
|
+
}
|
|
51
|
+
.analytics-tiles__count {
|
|
52
|
+
font-variant-numeric: tabular-nums;
|
|
53
|
+
color: var(--escalated-text-muted, #64748b);
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import DeliveriesTable from './DeliveriesTable.vue';
|
|
2
|
+
|
|
3
|
+
export default { title: 'Admin/Newsletters/DeliveriesTable', component: DeliveriesTable };
|
|
4
|
+
|
|
5
|
+
export const Default = {
|
|
6
|
+
args: {
|
|
7
|
+
rows: [
|
|
8
|
+
{
|
|
9
|
+
id: 1,
|
|
10
|
+
contact: { name: 'Maria', email: 'maria@example.com' },
|
|
11
|
+
status: 'sent',
|
|
12
|
+
sent_at: '2026-05-19T12:00:00Z',
|
|
13
|
+
opened_at: '2026-05-19T13:00:00Z',
|
|
14
|
+
last_clicked_at: '2026-05-19T13:05:00Z',
|
|
15
|
+
bounce_reason: null,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 2,
|
|
19
|
+
contact: { name: null, email: 'x@example.com' },
|
|
20
|
+
status: 'bounced',
|
|
21
|
+
sent_at: '2026-05-19T12:00:00Z',
|
|
22
|
+
opened_at: null,
|
|
23
|
+
last_clicked_at: null,
|
|
24
|
+
bounce_reason: '550 mailbox not found',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="deliveries-table">
|
|
3
|
+
<div class="deliveries-table__toolbar">
|
|
4
|
+
<select
|
|
5
|
+
class="deliveries-table__filter"
|
|
6
|
+
:value="statusFilter"
|
|
7
|
+
@change="$emit('filter', $event.target.value)"
|
|
8
|
+
>
|
|
9
|
+
<option value="">All statuses</option>
|
|
10
|
+
<option v-for="s in statuses" :key="s" :value="s">{{ s }}</option>
|
|
11
|
+
</select>
|
|
12
|
+
<button type="button" class="deliveries-table__export" @click="$emit('export')">Export CSV</button>
|
|
13
|
+
</div>
|
|
14
|
+
<table>
|
|
15
|
+
<thead>
|
|
16
|
+
<tr>
|
|
17
|
+
<th>Contact</th>
|
|
18
|
+
<th>Status</th>
|
|
19
|
+
<th>Sent</th>
|
|
20
|
+
<th>Opened</th>
|
|
21
|
+
<th>Last clicked</th>
|
|
22
|
+
<th>Bounce reason</th>
|
|
23
|
+
</tr>
|
|
24
|
+
</thead>
|
|
25
|
+
<tbody>
|
|
26
|
+
<tr v-for="row in rows" :key="row.id">
|
|
27
|
+
<td>
|
|
28
|
+
<div>{{ row.contact.name ?? '—' }}</div>
|
|
29
|
+
<div class="deliveries-table__email">{{ row.contact.email }}</div>
|
|
30
|
+
</td>
|
|
31
|
+
<td>
|
|
32
|
+
<span :class="`status status--${row.status}`">{{ row.status }}</span>
|
|
33
|
+
</td>
|
|
34
|
+
<td>{{ row.sent_at ? new Date(row.sent_at).toLocaleString() : '—' }}</td>
|
|
35
|
+
<td>{{ row.opened_at ? new Date(row.opened_at).toLocaleString() : '—' }}</td>
|
|
36
|
+
<td>{{ row.last_clicked_at ? new Date(row.last_clicked_at).toLocaleString() : '—' }}</td>
|
|
37
|
+
<td>{{ row.bounce_reason ?? '—' }}</td>
|
|
38
|
+
</tr>
|
|
39
|
+
</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<script setup>
|
|
45
|
+
defineProps({
|
|
46
|
+
rows: { type: Array, required: true },
|
|
47
|
+
statusFilter: { type: String, default: '' },
|
|
48
|
+
});
|
|
49
|
+
defineEmits(['filter', 'export']);
|
|
50
|
+
const statuses = ['pending', 'queued', 'sent', 'bounced', 'complained', 'suppressed', 'failed'];
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<style scoped>
|
|
54
|
+
.deliveries-table {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
gap: 12px;
|
|
58
|
+
}
|
|
59
|
+
.deliveries-table__toolbar {
|
|
60
|
+
display: flex;
|
|
61
|
+
gap: 8px;
|
|
62
|
+
align-items: center;
|
|
63
|
+
}
|
|
64
|
+
.deliveries-table table {
|
|
65
|
+
width: 100%;
|
|
66
|
+
border-collapse: collapse;
|
|
67
|
+
}
|
|
68
|
+
.deliveries-table th,
|
|
69
|
+
.deliveries-table td {
|
|
70
|
+
text-align: left;
|
|
71
|
+
padding: 8px 12px;
|
|
72
|
+
border-bottom: 1px solid var(--escalated-border, #e2e8f0);
|
|
73
|
+
font-size: 14px;
|
|
74
|
+
}
|
|
75
|
+
.deliveries-table__email {
|
|
76
|
+
color: var(--escalated-text-muted, #64748b);
|
|
77
|
+
font-size: 12px;
|
|
78
|
+
}
|
|
79
|
+
.status {
|
|
80
|
+
display: inline-block;
|
|
81
|
+
padding: 2px 8px;
|
|
82
|
+
border-radius: 4px;
|
|
83
|
+
font-size: 12px;
|
|
84
|
+
}
|
|
85
|
+
.status--sent {
|
|
86
|
+
background: #e0f2fe;
|
|
87
|
+
}
|
|
88
|
+
.status--bounced {
|
|
89
|
+
background: #fee2e2;
|
|
90
|
+
}
|
|
91
|
+
.status--complained {
|
|
92
|
+
background: #fef3c7;
|
|
93
|
+
}
|
|
94
|
+
.status--pending,
|
|
95
|
+
.status--queued {
|
|
96
|
+
background: #f1f5f9;
|
|
97
|
+
}
|
|
98
|
+
.status--failed {
|
|
99
|
+
background: #fecaca;
|
|
100
|
+
}
|
|
101
|
+
.status--suppressed {
|
|
102
|
+
background: #ede9fe;
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import DynamicFilterBuilder from './DynamicFilterBuilder.vue';
|
|
2
|
+
|
|
3
|
+
export default { title: 'Admin/Newsletters/DynamicFilterBuilder', component: DynamicFilterBuilder };
|
|
4
|
+
|
|
5
|
+
export const Empty = { args: { modelValue: { rules: [] }, matchCount: 0 } };
|
|
6
|
+
export const WithRules = {
|
|
7
|
+
args: {
|
|
8
|
+
modelValue: { rules: [{ field: 'tickets_count', op: '>=', value: 3 }] },
|
|
9
|
+
matchCount: 142,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dynamic-filter-builder">
|
|
3
|
+
<textarea
|
|
4
|
+
class="dynamic-filter-builder__fallback"
|
|
5
|
+
:value="JSON.stringify(modelValue, null, 2)"
|
|
6
|
+
rows="10"
|
|
7
|
+
spellcheck="false"
|
|
8
|
+
@input="onInput"
|
|
9
|
+
/>
|
|
10
|
+
<p class="dynamic-filter-builder__help">
|
|
11
|
+
Edit the saved filter JSON. A visual builder is planned for a follow-up.
|
|
12
|
+
</p>
|
|
13
|
+
<div class="dynamic-filter-builder__count">{{ matchCount }} contacts match</div>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup>
|
|
18
|
+
defineProps({
|
|
19
|
+
modelValue: { type: Object, required: true },
|
|
20
|
+
matchCount: { type: Number, default: 0 },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const emit = defineEmits(['update:modelValue']);
|
|
24
|
+
|
|
25
|
+
function onInput(event) {
|
|
26
|
+
try {
|
|
27
|
+
emit('update:modelValue', JSON.parse(event.target.value));
|
|
28
|
+
} catch {
|
|
29
|
+
// Invalid JSON during typing — ignore until it parses cleanly.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
.dynamic-filter-builder__fallback {
|
|
36
|
+
width: 100%;
|
|
37
|
+
font-family: ui-monospace, monospace;
|
|
38
|
+
font-size: 13px;
|
|
39
|
+
padding: 12px;
|
|
40
|
+
border: 1px solid var(--escalated-border, #e2e8f0);
|
|
41
|
+
border-radius: 6px;
|
|
42
|
+
}
|
|
43
|
+
.dynamic-filter-builder__help {
|
|
44
|
+
margin: 4px 0 0;
|
|
45
|
+
color: var(--escalated-text-muted, #64748b);
|
|
46
|
+
font-size: 12px;
|
|
47
|
+
}
|
|
48
|
+
.dynamic-filter-builder__count {
|
|
49
|
+
margin-top: 8px;
|
|
50
|
+
color: var(--escalated-text-muted, #64748b);
|
|
51
|
+
font-size: 13px;
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import ListMemberTable from './ListMemberTable.vue';
|
|
2
|
+
|
|
3
|
+
export default { title: 'Admin/Newsletters/ListMemberTable', component: ListMemberTable };
|
|
4
|
+
|
|
5
|
+
export const Default = {
|
|
6
|
+
args: {
|
|
7
|
+
members: [
|
|
8
|
+
{ id: 1, contact: { id: 10, name: 'Maria', email: 'maria@example.com' }, added_at: '2026-05-10T00:00:00Z' },
|
|
9
|
+
{ id: 2, contact: { id: 11, name: null, email: 'noname@example.com' }, added_at: '2026-05-12T00:00:00Z' },
|
|
10
|
+
],
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<table class="list-member-table">
|
|
3
|
+
<thead>
|
|
4
|
+
<tr>
|
|
5
|
+
<th>Contact</th>
|
|
6
|
+
<th>Added</th>
|
|
7
|
+
<th></th>
|
|
8
|
+
</tr>
|
|
9
|
+
</thead>
|
|
10
|
+
<tbody>
|
|
11
|
+
<tr v-for="m in members" :key="m.id">
|
|
12
|
+
<td>
|
|
13
|
+
<div>{{ m.contact.name ?? '—' }}</div>
|
|
14
|
+
<div class="list-member-table__email">{{ m.contact.email }}</div>
|
|
15
|
+
</td>
|
|
16
|
+
<td>{{ new Date(m.added_at).toLocaleDateString() }}</td>
|
|
17
|
+
<td>
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
data-action="remove"
|
|
21
|
+
:data-contact-id="m.contact.id"
|
|
22
|
+
@click="$emit('remove', m.contact.id)"
|
|
23
|
+
>
|
|
24
|
+
Remove
|
|
25
|
+
</button>
|
|
26
|
+
</td>
|
|
27
|
+
</tr>
|
|
28
|
+
</tbody>
|
|
29
|
+
</table>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup>
|
|
33
|
+
defineProps({ members: { type: Array, required: true } });
|
|
34
|
+
defineEmits(['remove']);
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<style scoped>
|
|
38
|
+
.list-member-table {
|
|
39
|
+
width: 100%;
|
|
40
|
+
border-collapse: collapse;
|
|
41
|
+
}
|
|
42
|
+
.list-member-table th,
|
|
43
|
+
.list-member-table td {
|
|
44
|
+
text-align: left;
|
|
45
|
+
padding: 8px 12px;
|
|
46
|
+
border-bottom: 1px solid var(--escalated-border, #e2e8f0);
|
|
47
|
+
font-size: 14px;
|
|
48
|
+
}
|
|
49
|
+
.list-member-table__email {
|
|
50
|
+
color: var(--escalated-text-muted, #64748b);
|
|
51
|
+
font-size: 12px;
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import MarkdownEditor from './MarkdownEditor.vue';
|
|
2
|
+
|
|
3
|
+
export default { title: 'Admin/Newsletters/MarkdownEditor', component: MarkdownEditor };
|
|
4
|
+
|
|
5
|
+
export const Empty = { args: { modelValue: '' } };
|
|
6
|
+
export const WithContent = { args: { modelValue: '# Welcome\n\nThanks for being a customer.' } };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="markdown-editor">
|
|
3
|
+
<textarea
|
|
4
|
+
ref="ta"
|
|
5
|
+
:value="modelValue"
|
|
6
|
+
:rows="rows"
|
|
7
|
+
class="markdown-editor__textarea"
|
|
8
|
+
:placeholder="placeholder"
|
|
9
|
+
@input="$emit('update:modelValue', $event.target.value)"
|
|
10
|
+
/>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup>
|
|
15
|
+
import { ref } from 'vue';
|
|
16
|
+
|
|
17
|
+
const props = defineProps({
|
|
18
|
+
modelValue: { type: String, default: '' },
|
|
19
|
+
rows: { type: Number, default: 20 },
|
|
20
|
+
placeholder: { type: String, default: '' },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const emit = defineEmits(['update:modelValue']);
|
|
24
|
+
const ta = ref(null);
|
|
25
|
+
|
|
26
|
+
function insertMergeField(path) {
|
|
27
|
+
const el = ta.value;
|
|
28
|
+
const token = `{{ ${path} }}`;
|
|
29
|
+
if (!el) {
|
|
30
|
+
emit('update:modelValue', `${props.modelValue}${token}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const start = el.selectionStart ?? props.modelValue.length;
|
|
34
|
+
const end = el.selectionEnd ?? props.modelValue.length;
|
|
35
|
+
const next = props.modelValue.slice(0, start) + token + props.modelValue.slice(end);
|
|
36
|
+
emit('update:modelValue', next);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
defineExpose({ insertMergeField });
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<style scoped>
|
|
43
|
+
.markdown-editor__textarea {
|
|
44
|
+
width: 100%;
|
|
45
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
46
|
+
font-size: 14px;
|
|
47
|
+
line-height: 1.5;
|
|
48
|
+
padding: 12px;
|
|
49
|
+
border: 1px solid var(--escalated-border, #e2e8f0);
|
|
50
|
+
border-radius: 6px;
|
|
51
|
+
resize: vertical;
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import MergeFieldDropdown from './MergeFieldDropdown.vue';
|
|
2
|
+
|
|
3
|
+
export default { title: 'Admin/Newsletters/MergeFieldDropdown', component: MergeFieldDropdown };
|
|
4
|
+
|
|
5
|
+
export const Default = {};
|
|
6
|
+
export const WithMetadataKeys = { args: { metadataKeys: ['tier', 'plan', 'lifecycle_stage'] } };
|