@fayz-ai/plugin-marketing 0.1.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.
Files changed (74) hide show
  1. package/dist/MarketingContext.d.ts +35 -0
  2. package/dist/MarketingContext.d.ts.map +1 -0
  3. package/dist/MarketingPage.d.ts +11 -0
  4. package/dist/MarketingPage.d.ts.map +1 -0
  5. package/dist/components/MarketingBits.d.ts +29 -0
  6. package/dist/components/MarketingBits.d.ts.map +1 -0
  7. package/dist/components/icons.d.ts +6 -0
  8. package/dist/components/icons.d.ts.map +1 -0
  9. package/dist/data/mock.d.ts +10 -0
  10. package/dist/data/mock.d.ts.map +1 -0
  11. package/dist/data/types.d.ts +32 -0
  12. package/dist/data/types.d.ts.map +1 -0
  13. package/dist/format.d.ts +17 -0
  14. package/dist/format.d.ts.map +1 -0
  15. package/dist/index.cjs +1354 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.ts +33 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +1346 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/locales/en.d.ts +2 -0
  22. package/dist/locales/en.d.ts.map +1 -0
  23. package/dist/locales/index.d.ts +2 -0
  24. package/dist/locales/index.d.ts.map +1 -0
  25. package/dist/locales/pt-BR.d.ts +2 -0
  26. package/dist/locales/pt-BR.d.ts.map +1 -0
  27. package/dist/presets.d.ts +20 -0
  28. package/dist/presets.d.ts.map +1 -0
  29. package/dist/store.d.ts +18 -0
  30. package/dist/store.d.ts.map +1 -0
  31. package/dist/types.d.ts +89 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/views/CampaignComposer.d.ts +6 -0
  34. package/dist/views/CampaignComposer.d.ts.map +1 -0
  35. package/dist/views/CampaignsView.d.ts +3 -0
  36. package/dist/views/CampaignsView.d.ts.map +1 -0
  37. package/dist/views/ChannelDetailView.d.ts +6 -0
  38. package/dist/views/ChannelDetailView.d.ts.map +1 -0
  39. package/dist/views/ChannelsView.d.ts +5 -0
  40. package/dist/views/ChannelsView.d.ts.map +1 -0
  41. package/dist/views/FunnelView.d.ts +3 -0
  42. package/dist/views/FunnelView.d.ts.map +1 -0
  43. package/dist/views/LandingPagesView.d.ts +3 -0
  44. package/dist/views/LandingPagesView.d.ts.map +1 -0
  45. package/dist/views/OverviewView.d.ts +9 -0
  46. package/dist/views/OverviewView.d.ts.map +1 -0
  47. package/dist/views/SettingsView.d.ts +3 -0
  48. package/dist/views/SettingsView.d.ts.map +1 -0
  49. package/dist/views/dashboardWidgets.d.ts +11 -0
  50. package/dist/views/dashboardWidgets.d.ts.map +1 -0
  51. package/package.json +55 -0
  52. package/src/MarketingContext.tsx +39 -0
  53. package/src/MarketingPage.tsx +94 -0
  54. package/src/components/MarketingBits.tsx +131 -0
  55. package/src/components/icons.tsx +17 -0
  56. package/src/data/mock.ts +233 -0
  57. package/src/data/types.ts +55 -0
  58. package/src/format.ts +40 -0
  59. package/src/index.ts +225 -0
  60. package/src/locales/en.ts +74 -0
  61. package/src/locales/index.ts +7 -0
  62. package/src/locales/pt-BR.ts +74 -0
  63. package/src/presets.ts +115 -0
  64. package/src/store.ts +66 -0
  65. package/src/types.ts +114 -0
  66. package/src/views/CampaignComposer.tsx +89 -0
  67. package/src/views/CampaignsView.tsx +56 -0
  68. package/src/views/ChannelDetailView.tsx +59 -0
  69. package/src/views/ChannelsView.tsx +46 -0
  70. package/src/views/FunnelView.tsx +56 -0
  71. package/src/views/LandingPagesView.tsx +49 -0
  72. package/src/views/OverviewView.tsx +24 -0
  73. package/src/views/SettingsView.tsx +72 -0
  74. package/src/views/dashboardWidgets.tsx +173 -0
package/src/format.ts ADDED
@@ -0,0 +1,40 @@
1
+ export interface MarketingCurrency {
2
+ code: string
3
+ locale: string
4
+ symbol: string
5
+ }
6
+
7
+ export const DEFAULT_CURRENCY: MarketingCurrency = { code: 'USD', locale: 'en-US', symbol: '$' }
8
+
9
+ export function formatCurrency(value: number, currency: MarketingCurrency): string {
10
+ try {
11
+ return new Intl.NumberFormat(currency.locale, {
12
+ style: 'currency',
13
+ currency: currency.code,
14
+ maximumFractionDigits: value >= 1000 ? 0 : 2,
15
+ }).format(value)
16
+ } catch {
17
+ return `${currency.symbol}${Math.round(value).toLocaleString()}`
18
+ }
19
+ }
20
+
21
+ export function formatNumber(value: number): string {
22
+ return Math.round(value).toLocaleString()
23
+ }
24
+
25
+ export function formatPercent(value: number, digits = 1): string {
26
+ return `${value.toFixed(digits)}%`
27
+ }
28
+
29
+ /** Compact form for big reach numbers (12.4k). */
30
+ export function formatCompact(value: number): string {
31
+ if (value >= 1000) return `${(value / 1000).toFixed(value >= 10000 ? 0 : 1)}k`
32
+ return String(Math.round(value))
33
+ }
34
+
35
+ /** Percentage-point delta vs a previous value, signed. */
36
+ export function trendDelta(current: number, previous: number): { pct: number; dir: 'up' | 'down' | 'neutral' } {
37
+ if (previous === 0) return { pct: current > 0 ? 100 : 0, dir: current > 0 ? 'up' : 'neutral' }
38
+ const pct = ((current - previous) / previous) * 100
39
+ return { pct: Math.abs(pct), dir: pct > 0.5 ? 'up' : pct < -0.5 ? 'down' : 'neutral' }
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,225 @@
1
+ import React from 'react'
2
+ import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
3
+ import { getSupabaseClientOptional, registerTranslations } from '@fayz-ai/core'
4
+ import { MarketingPage } from './MarketingPage'
5
+ import { SettingsView } from './views/SettingsView'
6
+ import { createMarketingDashboardWidgets } from './views/dashboardWidgets'
7
+ import {
8
+ MarketingContextProvider,
9
+ type ResolvedMarketingConfig,
10
+ type MarketingLabels,
11
+ } from './MarketingContext'
12
+ import type {
13
+ AttributionBridge,
14
+ MarketingDataProvider,
15
+ SitesPerformanceBridge,
16
+ } from './data/types'
17
+ import { createMockMarketingProvider } from './data/mock'
18
+ import { createMarketingStore } from './store'
19
+ import { DEFAULT_CURRENCY, type MarketingCurrency } from './format'
20
+ import { MARKETING_PRESETS, type MarketingDomain, type MarketingDomainModules } from './presets'
21
+ import type { AcquisitionChannel, ConversionModel } from './types'
22
+ import { marketingLocales } from './locales'
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // @fayz-ai/plugin-marketing — acquisition & conversion analytics. ONE plugin,
26
+ // every vertical: a `domain` preset (or explicit conversion + channels) adapts
27
+ // what a conversion is and where it's attributed from. Channels, campaigns,
28
+ // funnel and landing-page CVR are computed generically.
29
+ //
30
+ // Ships a vertical-flavored mock today; real attribution comes via the
31
+ // AttributionBridge / SitesPerformanceBridge seams (read from crm/agenda/
32
+ // orders/sites). Outbound broadcasts + journeys are reserved behind flags.
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export interface MarketingPluginOptions {
36
+ /** Vertical preset for conversion model + channels + module defaults. */
37
+ domain?: MarketingDomain
38
+ /** Override the conversion model (else taken from the domain preset). */
39
+ conversion?: ConversionModel
40
+ /** Override the acquisition channels (else from the domain preset). */
41
+ channels?: AcquisitionChannel[]
42
+ modules?: Partial<MarketingDomainModules>
43
+ currency?: Partial<MarketingCurrency>
44
+ labels?: Partial<MarketingLabels>
45
+ navPosition?: number
46
+ navSection?: 'main' | 'secondary' | 'settings'
47
+ navLabel?: string
48
+ scope?: PluginScope
49
+ verticalId?: VerticalId
50
+ dataProvider?: MarketingDataProvider
51
+ /** Optional DI to read real conversions (mounted later). */
52
+ attributionBridge?: AttributionBridge
53
+ /** Optional DI to read real landing-page performance (mounted later). */
54
+ sitesBridge?: SitesPerformanceBridge
55
+ }
56
+
57
+ const DEFAULT_LABELS: MarketingLabels = {
58
+ pageTitle: 'Marketing',
59
+ pageSubtitle: 'Acquisition & conversion performance',
60
+ overview: 'Overview',
61
+ channels: 'Channels',
62
+ campaigns: 'Campaigns',
63
+ funnel: 'Funnel',
64
+ landingPages: 'Landing pages',
65
+ settings: 'Settings',
66
+ }
67
+
68
+ function resolveConfig(options?: MarketingPluginOptions): { config: ResolvedMarketingConfig; domain: MarketingDomain } {
69
+ const domain = options?.domain ?? 'agency'
70
+ const preset = MARKETING_PRESETS[domain]
71
+ const config: ResolvedMarketingConfig = {
72
+ conversion: options?.conversion ?? preset.conversion,
73
+ channels: options?.channels ?? preset.channels,
74
+ modules: { ...preset.modules, ...options?.modules },
75
+ currency: { ...DEFAULT_CURRENCY, ...options?.currency },
76
+ labels: { ...DEFAULT_LABELS, ...options?.labels },
77
+ }
78
+ return { config, domain }
79
+ }
80
+
81
+ function createSafeProvider(config: ResolvedMarketingConfig, domain: MarketingDomain): MarketingDataProvider {
82
+ // Supabase-backed + bridge-fed providers land later; mock powers it for now.
83
+ void getSupabaseClientOptional()
84
+ return createMockMarketingProvider({ channels: config.channels, conversion: config.conversion, domain })
85
+ }
86
+
87
+ export function createMarketingPlugin(options?: MarketingPluginOptions): PluginManifest {
88
+ registerTranslations(marketingLocales)
89
+ const { config, domain } = resolveConfig(options)
90
+ const provider = options?.dataProvider ?? createSafeProvider(config, domain)
91
+ const store = createMarketingStore(provider)
92
+ const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store })
93
+
94
+ const PageComponent: React.ComponentType<unknown> = () =>
95
+ React.createElement(MarketingContextProvider, { config, provider, store },
96
+ React.createElement(MarketingPage, { config, provider, store }))
97
+ PageComponent.displayName = 'MarketingPage'
98
+
99
+ // Settings lives in the SDK-core central Settings area (not a module tab).
100
+ const SettingsComponent: React.ComponentType<unknown> = () =>
101
+ React.createElement(MarketingContextProvider, { config, provider, store },
102
+ React.createElement(SettingsView))
103
+ SettingsComponent.displayName = 'MarketingSettings'
104
+
105
+ return {
106
+ id: 'marketing',
107
+ name: options?.navLabel ?? config.labels.pageTitle,
108
+ icon: 'Megaphone',
109
+ version: '1.0.0',
110
+ scope: options?.scope ?? 'universal',
111
+ verticalId: options?.verticalId,
112
+ defaultEnabled: true,
113
+ dependencies: [],
114
+ declaredFeatures: [
115
+ { id: 'marketing', label: config.labels.pageTitle, group: 'Engage' },
116
+ ...(config.modules.landingPages ? [{ id: 'marketing.landing-pages', label: config.labels.landingPages, group: 'Engage' }] : []),
117
+ ],
118
+ navigation: [
119
+ {
120
+ section: options?.navSection ?? 'main',
121
+ position: options?.navPosition ?? 5,
122
+ label: options?.navLabel ?? config.labels.pageTitle,
123
+ route: '/marketing',
124
+ icon: 'Megaphone',
125
+ permission: { feature: 'marketing', action: 'read' as const },
126
+ },
127
+ ],
128
+ routes: [
129
+ {
130
+ path: '/marketing',
131
+ component: PageComponent,
132
+ permission: { feature: 'marketing', action: 'read' as const },
133
+ },
134
+ ],
135
+ widgets: [],
136
+ dashboardWidgets,
137
+ events: [
138
+ { name: 'marketing.conversion.tracked', description: 'A conversion was attributed to a channel/campaign' },
139
+ { name: 'marketing.campaign.created', description: 'A campaign was created' },
140
+ { name: 'marketing.campaign.updated', description: 'A campaign was updated' },
141
+ { name: 'marketing.channel.synced', description: 'A channel pulled fresh attribution data' },
142
+ ],
143
+ aiTools: [
144
+ {
145
+ id: 'marketing.channel-performance',
146
+ name: 'channelPerformance',
147
+ description: 'Returns acquisition performance per channel (reach, conversions, CVR, spend, CPA).',
148
+ icon: 'Radio',
149
+ mode: 'read' as const,
150
+ category: 'Marketing',
151
+ parameters: {
152
+ type: 'object' as const,
153
+ properties: { range: { type: 'string' as const, enum: ['7d', '30d', '90d'] } },
154
+ },
155
+ suggestions: [{ label: 'Which channel converts best?' }, { label: 'What is my cost per acquisition?' }],
156
+ permission: { feature: 'marketing', action: 'read' as const },
157
+ },
158
+ {
159
+ id: 'marketing.top-channels',
160
+ name: 'topChannels',
161
+ description: 'Returns the top acquisition channels ranked by conversions.',
162
+ icon: 'Trophy',
163
+ mode: 'read' as const,
164
+ category: 'Marketing',
165
+ permission: { feature: 'marketing', action: 'read' as const },
166
+ },
167
+ {
168
+ id: 'marketing.campaign-cvr',
169
+ name: 'campaignCvr',
170
+ description: 'Lists campaigns with their conversion rate for a date range.',
171
+ icon: 'Percent',
172
+ mode: 'read' as const,
173
+ category: 'Marketing',
174
+ parameters: {
175
+ type: 'object' as const,
176
+ properties: { range: { type: 'string' as const, enum: ['7d', '30d', '90d'] } },
177
+ },
178
+ permission: { feature: 'marketing', action: 'read' as const },
179
+ },
180
+ {
181
+ id: 'marketing.create-campaign',
182
+ name: 'createCampaign',
183
+ description: 'Creates an acquisition campaign on a channel.',
184
+ icon: 'Plus',
185
+ mode: 'persist' as const,
186
+ category: 'Marketing',
187
+ parameters: {
188
+ type: 'object' as const,
189
+ properties: {
190
+ name: { type: 'string' as const, description: 'Campaign name' },
191
+ channelId: { type: 'string' as const, description: 'Acquisition channel id' },
192
+ },
193
+ required: ['name', 'channelId'],
194
+ },
195
+ permission: { feature: 'marketing', action: 'create' as const },
196
+ },
197
+ ],
198
+ settings: [
199
+ {
200
+ id: 'marketing',
201
+ label: config.labels.pageTitle,
202
+ icon: 'Megaphone',
203
+ component: SettingsComponent,
204
+ order: 20,
205
+ permission: { feature: 'marketing', action: 'read' as const },
206
+ },
207
+ ],
208
+ locales: marketingLocales,
209
+ }
210
+ }
211
+
212
+ export type {
213
+ MarketingDataProvider,
214
+ AttributionBridge,
215
+ SitesPerformanceBridge,
216
+ } from './data/types'
217
+ export type {
218
+ AcquisitionChannel,
219
+ ConversionModel,
220
+ Campaign,
221
+ ChannelPerformance,
222
+ LandingPagePerf,
223
+ } from './types'
224
+ export { MARKETING_PRESETS, type MarketingDomain } from './presets'
225
+ export { createMockMarketingProvider } from './data/mock'
@@ -0,0 +1,74 @@
1
+ export const en: Record<string, string> = {
2
+ // metrics
3
+ 'marketing.metric.conversionRate': 'Conversion rate',
4
+ 'marketing.metric.cpa': 'Cost per acquisition',
5
+ 'marketing.metric.topChannel': 'Top channel',
6
+ 'marketing.metric.spend': 'spend',
7
+ 'marketing.metric.reach': 'Reach',
8
+ 'marketing.metric.cvr': 'CVR',
9
+ 'marketing.metric.totalVisits': 'Total visits',
10
+ 'marketing.metric.pages': 'Pages',
11
+ 'marketing.metric.avgConversion': 'Avg. conversion',
12
+ // columns
13
+ 'marketing.col.campaign': 'Campaign',
14
+ 'marketing.col.channel': 'Channel',
15
+ 'marketing.col.reach': 'Reach',
16
+ 'marketing.col.leads': 'Leads',
17
+ 'marketing.col.cvr': 'CVR',
18
+ 'marketing.col.spend': 'Spend',
19
+ 'marketing.col.cpa': 'CPA',
20
+ 'marketing.col.status': 'Status',
21
+ 'marketing.col.visits': 'Visits',
22
+ 'marketing.col.conversions': 'Conversions',
23
+ 'marketing.col.type': 'Type',
24
+ 'marketing.col.page': 'Page',
25
+ // overview
26
+ 'marketing.overview.byChannelSuffix': 'by channel',
27
+ 'marketing.overview.recent': 'Recent campaigns',
28
+ // channels
29
+ 'marketing.channels.subtitle': 'Acquisition performance by channel',
30
+ 'marketing.channels.campaignsOn': 'Campaigns on this channel',
31
+ 'marketing.channels.noCampaigns': 'No campaigns yet on this channel.',
32
+ // campaigns
33
+ 'marketing.campaigns.subtitle': 'All acquisition campaigns and their conversion performance',
34
+ 'marketing.campaigns.new': 'New campaign',
35
+ 'marketing.campaigns.empty': 'No campaigns yet — create your first one.',
36
+ 'marketing.campaigns.delete': 'Delete campaign',
37
+ // composer
38
+ 'marketing.composer.title': 'New campaign',
39
+ 'marketing.composer.name': 'Campaign name',
40
+ 'marketing.composer.namePlaceholder': 'e.g. Spring Promo — Paid Social',
41
+ 'marketing.composer.channel': 'Channel',
42
+ 'marketing.composer.channelPlaceholder': 'Select a channel',
43
+ 'marketing.composer.status': 'Status',
44
+ 'marketing.composer.budget': 'Budget / spend',
45
+ 'marketing.composer.cancel': 'Cancel',
46
+ 'marketing.composer.create': 'Create campaign',
47
+ 'marketing.status.draft': 'draft',
48
+ 'marketing.status.active': 'active',
49
+ 'marketing.status.paused': 'paused',
50
+ 'marketing.status.ended': 'ended',
51
+ // funnel
52
+ 'marketing.funnel.subtitlePrefix': 'Attribution funnel — reach to',
53
+ 'marketing.funnel.overall': 'Overall conversion rate',
54
+ 'marketing.funnel.ofPrev': 'of prev',
55
+ // landing pages
56
+ 'marketing.landing.subtitle': 'Per-page visits → conversions performance',
57
+ // settings
58
+ 'marketing.settings.conversionModel': 'Conversion model',
59
+ 'marketing.settings.conversionModelDesc': "What counts as a conversion in this workspace and where it's attributed from.",
60
+ 'marketing.settings.conversion': 'Conversion',
61
+ 'marketing.settings.valueMetric': 'Value metric',
62
+ 'marketing.settings.attributedFrom': 'Attributed from',
63
+ 'marketing.settings.channels': 'Acquisition channels',
64
+ 'marketing.settings.channelsDesc': 'Sources tracked for this workspace.',
65
+ 'marketing.settings.trackValue': 'Track monetary value',
66
+ 'marketing.settings.trackValueDesc': 'Attribute revenue to channels and campaigns.',
67
+ 'marketing.settings.attribution': 'Attribution',
68
+ 'marketing.settings.attributionDesc': 'How conversions are matched to channels.',
69
+ 'marketing.settings.autoSync': 'Auto-sync conversions',
70
+ 'marketing.settings.autoSyncDesc': 'Pull conversions automatically from the source.',
71
+ 'marketing.settings.assisted': 'Count assisted conversions',
72
+ 'marketing.settings.assistedDesc': 'Credit channels that contributed to a conversion.',
73
+ 'marketing.settings.channelTrackDesc': 'Track this acquisition source.',
74
+ }
@@ -0,0 +1,7 @@
1
+ import { en } from './en'
2
+ import { ptBR } from './pt-BR'
3
+
4
+ export const marketingLocales: Record<string, Record<string, string>> = {
5
+ en,
6
+ 'pt-BR': ptBR,
7
+ }
@@ -0,0 +1,74 @@
1
+ export const ptBR: Record<string, string> = {
2
+ // metrics
3
+ 'marketing.metric.conversionRate': 'Taxa de conversão',
4
+ 'marketing.metric.cpa': 'Custo por aquisição',
5
+ 'marketing.metric.topChannel': 'Principal canal',
6
+ 'marketing.metric.spend': 'investido',
7
+ 'marketing.metric.reach': 'Alcance',
8
+ 'marketing.metric.cvr': 'CVR',
9
+ 'marketing.metric.totalVisits': 'Total de visitas',
10
+ 'marketing.metric.pages': 'Páginas',
11
+ 'marketing.metric.avgConversion': 'Conversão média',
12
+ // columns
13
+ 'marketing.col.campaign': 'Campanha',
14
+ 'marketing.col.channel': 'Canal',
15
+ 'marketing.col.reach': 'Alcance',
16
+ 'marketing.col.leads': 'Leads',
17
+ 'marketing.col.cvr': 'CVR',
18
+ 'marketing.col.spend': 'Investimento',
19
+ 'marketing.col.cpa': 'CPA',
20
+ 'marketing.col.status': 'Status',
21
+ 'marketing.col.visits': 'Visitas',
22
+ 'marketing.col.conversions': 'Conversões',
23
+ 'marketing.col.type': 'Tipo',
24
+ 'marketing.col.page': 'Página',
25
+ // overview
26
+ 'marketing.overview.byChannelSuffix': 'por canal',
27
+ 'marketing.overview.recent': 'Campanhas recentes',
28
+ // channels
29
+ 'marketing.channels.subtitle': 'Desempenho de aquisição por canal',
30
+ 'marketing.channels.campaignsOn': 'Campanhas neste canal',
31
+ 'marketing.channels.noCampaigns': 'Nenhuma campanha ainda neste canal.',
32
+ // campaigns
33
+ 'marketing.campaigns.subtitle': 'Todas as campanhas de aquisição e seu desempenho de conversão',
34
+ 'marketing.campaigns.new': 'Nova campanha',
35
+ 'marketing.campaigns.empty': 'Nenhuma campanha ainda — crie a primeira.',
36
+ 'marketing.campaigns.delete': 'Excluir campanha',
37
+ // composer
38
+ 'marketing.composer.title': 'Nova campanha',
39
+ 'marketing.composer.name': 'Nome da campanha',
40
+ 'marketing.composer.namePlaceholder': 'ex.: Promo de Verão — Mídia Paga',
41
+ 'marketing.composer.channel': 'Canal',
42
+ 'marketing.composer.channelPlaceholder': 'Selecione um canal',
43
+ 'marketing.composer.status': 'Status',
44
+ 'marketing.composer.budget': 'Orçamento / investimento',
45
+ 'marketing.composer.cancel': 'Cancelar',
46
+ 'marketing.composer.create': 'Criar campanha',
47
+ 'marketing.status.draft': 'rascunho',
48
+ 'marketing.status.active': 'ativa',
49
+ 'marketing.status.paused': 'pausada',
50
+ 'marketing.status.ended': 'encerrada',
51
+ // funnel
52
+ 'marketing.funnel.subtitlePrefix': 'Funil de atribuição — alcance até',
53
+ 'marketing.funnel.overall': 'Taxa de conversão geral',
54
+ 'marketing.funnel.ofPrev': 'do anterior',
55
+ // landing pages
56
+ 'marketing.landing.subtitle': 'Desempenho de visitas → conversões por página',
57
+ // settings
58
+ 'marketing.settings.conversionModel': 'Modelo de conversão',
59
+ 'marketing.settings.conversionModelDesc': 'O que conta como conversão neste workspace e de onde é atribuída.',
60
+ 'marketing.settings.conversion': 'Conversão',
61
+ 'marketing.settings.valueMetric': 'Métrica de valor',
62
+ 'marketing.settings.attributedFrom': 'Atribuída de',
63
+ 'marketing.settings.channels': 'Canais de aquisição',
64
+ 'marketing.settings.channelsDesc': 'Fontes monitoradas neste workspace.',
65
+ 'marketing.settings.trackValue': 'Acompanhar valor monetário',
66
+ 'marketing.settings.trackValueDesc': 'Atribuir receita a canais e campanhas.',
67
+ 'marketing.settings.attribution': 'Atribuição',
68
+ 'marketing.settings.attributionDesc': 'Como as conversões são associadas aos canais.',
69
+ 'marketing.settings.autoSync': 'Sincronizar conversões automaticamente',
70
+ 'marketing.settings.autoSyncDesc': 'Importar conversões automaticamente da fonte.',
71
+ 'marketing.settings.assisted': 'Contar conversões assistidas',
72
+ 'marketing.settings.assistedDesc': 'Creditar canais que contribuíram para uma conversão.',
73
+ 'marketing.settings.channelTrackDesc': 'Monitorar esta fonte de aquisição.',
74
+ }
package/src/presets.ts ADDED
@@ -0,0 +1,115 @@
1
+ import type { AcquisitionChannel, ConversionModel } from './types'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Domain presets — the abstraction that lets ONE plugin feed every vertical.
5
+ // Each preset defines what a "conversion" is (and where it's attributed from)
6
+ // plus the acquisition channels that vertical actually uses. Channel ids align
7
+ // with plugin-crm's `lead-sources` registry where they overlap.
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export type MarketingDomain = 'agency' | 'beauty' | 'resto'
11
+
12
+ export interface MarketingDomainModules {
13
+ channels: boolean
14
+ campaigns: boolean
15
+ funnel: boolean
16
+ landingPages: boolean
17
+ /** Outbound — reserved, off by default, implemented in a later milestone. */
18
+ broadcasts: boolean
19
+ journeys: boolean
20
+ }
21
+
22
+ export interface MarketingDomainPreset {
23
+ conversion: ConversionModel
24
+ channels: AcquisitionChannel[]
25
+ modules: MarketingDomainModules
26
+ }
27
+
28
+ const AGENCY: MarketingDomainPreset = {
29
+ conversion: {
30
+ id: 'won-deal',
31
+ label: 'Won deal',
32
+ labelPlural: 'Won deals',
33
+ valueLabel: 'Pipeline value',
34
+ source: 'crm',
35
+ avgValue: 3200,
36
+ },
37
+ channels: [
38
+ { id: 'paid-search', label: 'Paid Search', icon: 'Search', kind: 'paid' },
39
+ { id: 'paid-social', label: 'Paid Social', icon: 'Megaphone', kind: 'paid' },
40
+ { id: 'organic', label: 'Organic / SEO', icon: 'Globe', kind: 'organic' },
41
+ { id: 'email', label: 'Email', icon: 'Mail', kind: 'outbound' },
42
+ { id: 'referral', label: 'Referral', icon: 'Users', kind: 'referral' },
43
+ { id: 'direct', label: 'Direct', icon: 'MousePointerClick', kind: 'direct' },
44
+ ],
45
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: true, broadcasts: false, journeys: false },
46
+ }
47
+
48
+ const BEAUTY: MarketingDomainPreset = {
49
+ conversion: {
50
+ id: 'booking',
51
+ label: 'Booking',
52
+ labelPlural: 'Bookings',
53
+ valueLabel: 'Booking revenue',
54
+ source: 'agenda',
55
+ avgValue: 85,
56
+ },
57
+ channels: [
58
+ { id: 'instagram', label: 'Instagram', icon: 'Instagram', kind: 'social' },
59
+ { id: 'google', label: 'Google', icon: 'Globe', kind: 'organic' },
60
+ { id: 'referral', label: 'Referral', icon: 'Users', kind: 'referral' },
61
+ { id: 'whatsapp', label: 'WhatsApp', icon: 'MessageCircle', kind: 'social' },
62
+ { id: 'walkin', label: 'Walk-in', icon: 'DoorOpen', kind: 'direct' },
63
+ ],
64
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: false, broadcasts: false, journeys: false },
65
+ }
66
+
67
+ const RESTO: MarketingDomainPreset = {
68
+ conversion: {
69
+ id: 'order',
70
+ label: 'Order',
71
+ labelPlural: 'Orders',
72
+ valueLabel: 'Revenue',
73
+ source: 'orders',
74
+ avgValue: 42,
75
+ },
76
+ channels: [
77
+ { id: 'ifood', label: 'iFood', icon: 'Utensils', kind: 'paid' },
78
+ { id: 'delivery', label: 'Delivery apps', icon: 'Bike', kind: 'paid' },
79
+ { id: 'google', label: 'Google', icon: 'Globe', kind: 'organic' },
80
+ { id: 'instagram', label: 'Instagram', icon: 'Instagram', kind: 'social' },
81
+ { id: 'walkin', label: 'Walk-in', icon: 'DoorOpen', kind: 'direct' },
82
+ ],
83
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: false, broadcasts: false, journeys: false },
84
+ }
85
+
86
+ export const MARKETING_PRESETS: Record<MarketingDomain, MarketingDomainPreset> = {
87
+ agency: AGENCY,
88
+ beauty: BEAUTY,
89
+ resto: RESTO,
90
+ }
91
+
92
+ /** Vertical-flavored campaign name templates for the mock seed. */
93
+ export const CAMPAIGN_NAME_SEEDS: Record<MarketingDomain, string[]> = {
94
+ agency: [
95
+ 'Q2 Lead-gen — Search',
96
+ 'LinkedIn Retargeting',
97
+ 'Webinar Funnel',
98
+ 'Newsletter Nurture',
99
+ 'Partner Referral Push',
100
+ ],
101
+ beauty: [
102
+ 'Summer Glow Promo',
103
+ 'New Client — Instagram',
104
+ 'Birthday Offer Blast',
105
+ 'Win-back 90 days',
106
+ 'Google Reviews Boost',
107
+ ],
108
+ resto: [
109
+ 'iFood Weekend Combo',
110
+ 'Happy Hour Promo',
111
+ 'New Menu Launch',
112
+ 'Loyalty Double Points',
113
+ 'Instagram Stories Drop',
114
+ ],
115
+ }
package/src/store.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { createStore, type StoreApi } from 'zustand/vanilla'
2
+ import type { MarketingDataProvider, SaveCampaignInput } from './data/types'
3
+ import type {
4
+ Campaign,
5
+ ChannelPerformance,
6
+ DateRangeKey,
7
+ FunnelStage,
8
+ LandingPagePerf,
9
+ MarketingOverview,
10
+ } from './types'
11
+
12
+ export interface MarketingUIState {
13
+ range: DateRangeKey
14
+ overview: MarketingOverview | null
15
+ channels: ChannelPerformance[]
16
+ campaigns: Campaign[]
17
+ funnel: FunnelStage[]
18
+ landingPages: LandingPagePerf[]
19
+ loading: boolean
20
+
21
+ load(): Promise<void>
22
+ setRange(range: DateRangeKey): Promise<void>
23
+ saveCampaign(input: SaveCampaignInput): Promise<Campaign>
24
+ deleteCampaign(id: string): Promise<void>
25
+ }
26
+
27
+ export function createMarketingStore(provider: MarketingDataProvider): StoreApi<MarketingUIState> {
28
+ return createStore<MarketingUIState>((set, get) => ({
29
+ range: '30d',
30
+ overview: null,
31
+ channels: [],
32
+ campaigns: [],
33
+ funnel: [],
34
+ landingPages: [],
35
+ loading: false,
36
+
37
+ async load() {
38
+ const range = get().range
39
+ set({ loading: true })
40
+ const [overview, channels, campaigns, funnel, landingPages] = await Promise.all([
41
+ provider.overview({ range }),
42
+ provider.channelPerformance({ range }),
43
+ provider.listCampaigns({ range }),
44
+ provider.funnel({ range }),
45
+ provider.landingPages({ range }),
46
+ ])
47
+ set({ overview, channels, campaigns, funnel, landingPages, loading: false })
48
+ },
49
+
50
+ async setRange(range) {
51
+ set({ range })
52
+ await get().load()
53
+ },
54
+
55
+ async saveCampaign(input) {
56
+ const created = await provider.saveCampaign(input)
57
+ await get().load()
58
+ return created
59
+ },
60
+
61
+ async deleteCampaign(id) {
62
+ await provider.deleteCampaign(id)
63
+ await get().load()
64
+ },
65
+ }))
66
+ }