diff --git a/package.json b/package.json index 298865b..4893a7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@escalated-dev/escalated", - "version": "0.8.0", + "version": "0.9.0", "description": "Vue 3 + Inertia.js UI components for Escalated \u00e2\u20ac\u201d the embeddable support ticket system", "author": "Escalated Dev ", "license": "MIT", diff --git a/src/components/EscalatedLayout.vue b/src/components/EscalatedLayout.vue index 8496a02..19ea367 100644 --- a/src/components/EscalatedLayout.vue +++ b/src/components/EscalatedLayout.vue @@ -26,6 +26,7 @@ const isPanel = computed(() => isAdminSection.value || isAgentSection.value); const isDark = computed(() => isPanel.value && panelConfig.mode !== 'light'); const showPoweredBy = computed(() => page.props.escalated?.show_powered_by !== false); const kbEnabled = computed(() => page.props.escalated?.knowledge_base_enabled !== false); +const newslettersEnabled = computed(() => page.props.escalated?.features?.newsletters === true); const kbPublic = computed(() => page.props.escalated?.knowledge_base_public !== false); const chatEnabled = computed(() => page.props.escalated?.chat_enabled === true); const activeChats = computed(() => page.props.escalated?.active_chats || []); @@ -116,6 +117,16 @@ const adminLinks = computed(() => { icon: 'M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z', position: 80, }, + ...(newslettersEnabled.value + ? [ + { + href: `${p}/admin/newsletters`, + label: 'Newsletters', + icon: 'M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75', + position: 81, + }, + ] + : []), { href: `${p}/admin/skills`, label: 'Skills', diff --git a/src/components/admin/newsletters/AnalyticsTiles.stories.js b/src/components/admin/newsletters/AnalyticsTiles.stories.js new file mode 100644 index 0000000..98e57e7 --- /dev/null +++ b/src/components/admin/newsletters/AnalyticsTiles.stories.js @@ -0,0 +1,14 @@ +import AnalyticsTiles from './AnalyticsTiles.vue'; + +export default { title: 'Admin/Newsletters/AnalyticsTiles', component: AnalyticsTiles }; + +export const Default = { + args: { + summary: { total: 1000, sent: 990, opened: 400, clicked: 80, bounced: 10, complained: 1 }, + topClicks: [ + { url: 'https://example.com/launch', clicks: 42 }, + { url: 'https://example.com/blog/post', clicks: 18 }, + { url: 'https://example.com/pricing', clicks: 7 }, + ], + }, +}; diff --git a/src/components/admin/newsletters/AnalyticsTiles.vue b/src/components/admin/newsletters/AnalyticsTiles.vue new file mode 100644 index 0000000..66246b9 --- /dev/null +++ b/src/components/admin/newsletters/AnalyticsTiles.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/src/components/admin/newsletters/DeliveriesTable.stories.js b/src/components/admin/newsletters/DeliveriesTable.stories.js new file mode 100644 index 0000000..30505bd --- /dev/null +++ b/src/components/admin/newsletters/DeliveriesTable.stories.js @@ -0,0 +1,28 @@ +import DeliveriesTable from './DeliveriesTable.vue'; + +export default { title: 'Admin/Newsletters/DeliveriesTable', component: DeliveriesTable }; + +export const Default = { + args: { + rows: [ + { + id: 1, + contact: { name: 'Maria', email: 'maria@example.com' }, + status: 'sent', + sent_at: '2026-05-19T12:00:00Z', + opened_at: '2026-05-19T13:00:00Z', + last_clicked_at: '2026-05-19T13:05:00Z', + bounce_reason: null, + }, + { + id: 2, + contact: { name: null, email: 'x@example.com' }, + status: 'bounced', + sent_at: '2026-05-19T12:00:00Z', + opened_at: null, + last_clicked_at: null, + bounce_reason: '550 mailbox not found', + }, + ], + }, +}; diff --git a/src/components/admin/newsletters/DeliveriesTable.vue b/src/components/admin/newsletters/DeliveriesTable.vue new file mode 100644 index 0000000..d8d5730 --- /dev/null +++ b/src/components/admin/newsletters/DeliveriesTable.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/src/components/admin/newsletters/DynamicFilterBuilder.stories.js b/src/components/admin/newsletters/DynamicFilterBuilder.stories.js new file mode 100644 index 0000000..30569cd --- /dev/null +++ b/src/components/admin/newsletters/DynamicFilterBuilder.stories.js @@ -0,0 +1,11 @@ +import DynamicFilterBuilder from './DynamicFilterBuilder.vue'; + +export default { title: 'Admin/Newsletters/DynamicFilterBuilder', component: DynamicFilterBuilder }; + +export const Empty = { args: { modelValue: { rules: [] }, matchCount: 0 } }; +export const WithRules = { + args: { + modelValue: { rules: [{ field: 'tickets_count', op: '>=', value: 3 }] }, + matchCount: 142, + }, +}; diff --git a/src/components/admin/newsletters/DynamicFilterBuilder.vue b/src/components/admin/newsletters/DynamicFilterBuilder.vue new file mode 100644 index 0000000..30a8dc9 --- /dev/null +++ b/src/components/admin/newsletters/DynamicFilterBuilder.vue @@ -0,0 +1,53 @@ +