@fayz-ai/plugin-marketing 0.9.0-next.1 → 0.9.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/README.md +3 -3
- package/dist/{MarketingContext.d.ts → context.d.ts} +5 -3
- package/dist/context.d.ts.map +1 -0
- package/dist/data/mock.d.ts +1 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/{registries.d.ts → data/registries.d.ts} +1 -1
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -22
- package/dist/index.js.map +1 -1
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/{presets.d.ts → lib/presets.d.ts} +1 -1
- package/dist/lib/presets.d.ts.map +1 -0
- package/dist/{MarketingPage.d.ts → views/MarketingPage.d.ts} +5 -5
- package/dist/views/MarketingPage.d.ts.map +1 -0
- package/dist/views/blog/BlogAdminView.d.ts.map +1 -1
- package/dist/views/content/ContentPlannerContext.d.ts +1 -1
- package/dist/views/content/ContentPlannerContext.d.ts.map +1 -1
- package/dist/views/dashboardWidgets.d.ts +1 -1
- package/dist/views/dashboardWidgets.d.ts.map +1 -1
- package/package.json +6 -9
- package/dist/MarketingContext.d.ts.map +0 -1
- package/dist/MarketingPage.d.ts.map +0 -1
- package/dist/format.d.ts.map +0 -1
- package/dist/presets.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/MarketingContext.tsx +0 -42
- package/src/MarketingPage.tsx +0 -139
- package/src/components/MarketingBits.tsx +0 -131
- package/src/components/icons.tsx +0 -36
- package/src/data/blogEntities.tsx +0 -91
- package/src/data/capability.test.ts +0 -92
- package/src/data/contentMedia.ts +0 -25
- package/src/data/contentMock.ts +0 -253
- package/src/data/contentSupabase.ts +0 -263
- package/src/data/contentTypes.ts +0 -138
- package/src/data/mock.ts +0 -308
- package/src/data/supabase.ts +0 -613
- package/src/data/tables.ts +0 -33
- package/src/data/types.ts +0 -97
- package/src/format.ts +0 -40
- package/src/index.ts +0 -457
- package/src/integrations/ranklayer/RANKLAYER.md +0 -40
- package/src/integrations/ranklayer/connectorDef.tsx +0 -102
- package/src/integrations/ranklayer/data/supabase.ts +0 -106
- package/src/integrations/ranklayer/types.ts +0 -16
- package/src/locales/en.ts +0 -192
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -192
- package/src/migrations/000_plg_rename.sql +0 -15
- package/src/migrations/001_content_planner.sql +0 -94
- package/src/migrations/002_multi_platform.sql +0 -25
- package/src/migrations/003_recording_ops.sql +0 -25
- package/src/migrations/004_ranklayer.sql +0 -50
- package/src/migrations/005_analytics_base.sql +0 -60
- package/src/migrations/006_analytics_views.sql +0 -68
- package/src/migrations/007_agent_rpcs.sql +0 -119
- package/src/migrations/008_landing_pages.sql +0 -112
- package/src/migrations/index.ts +0 -601
- package/src/presets.ts +0 -119
- package/src/registries.ts +0 -62
- package/src/schema/index.ts +0 -106
- package/src/store.ts +0 -102
- package/src/types.ts +0 -153
- package/src/views/CampaignComposer.tsx +0 -89
- package/src/views/CampaignsView.tsx +0 -59
- package/src/views/ChannelDetailView.tsx +0 -59
- package/src/views/ChannelsView.tsx +0 -46
- package/src/views/FunnelView.tsx +0 -56
- package/src/views/LandingPageComposer.tsx +0 -201
- package/src/views/LandingPagesView.tsx +0 -172
- package/src/views/OverviewView.tsx +0 -24
- package/src/views/SettingsView.tsx +0 -72
- package/src/views/blog/BlogAdminView.tsx +0 -99
- package/src/views/blog/BlogEntityForm.tsx +0 -101
- package/src/views/blog/useEntityCrud.ts +0 -50
- package/src/views/content/ContentPlannerContext.tsx +0 -13
- package/src/views/content/ContentView.tsx +0 -908
- package/src/views/content/PlanBriefView.tsx +0 -268
- package/src/views/content/PostPage.tsx +0 -370
- package/src/views/content/PostSocialPreview.tsx +0 -212
- package/src/views/content/RecordingChecklist.tsx +0 -145
- package/src/views/content/contentBits.tsx +0 -114
- package/src/views/content/contentStore.ts +0 -160
- package/src/views/content/templates.ts +0 -172
- package/src/views/dashboardWidgets.tsx +0 -173
- /package/dist/{format.d.ts → lib/format.d.ts} +0 -0
package/src/data/types.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Campaign,
|
|
3
|
-
CampaignStatus,
|
|
4
|
-
ChannelPerformance,
|
|
5
|
-
DateRangeKey,
|
|
6
|
-
FunnelStage,
|
|
7
|
-
LandingPage,
|
|
8
|
-
LandingPagePerf,
|
|
9
|
-
LandingPageStatus,
|
|
10
|
-
MarketingOverview,
|
|
11
|
-
MarketingQuery,
|
|
12
|
-
MarketingSite,
|
|
13
|
-
} from '../types'
|
|
14
|
-
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Provider — everything the marketing surface reads/writes. The mock provider
|
|
17
|
-
// computes analytics from a vertical-flavored seed; a Supabase-backed provider
|
|
18
|
-
// lands later. Attribution + sites data can also come from sibling plugins via
|
|
19
|
-
// the bridge seams below (defined now, injected later).
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
export interface SaveCampaignInput {
|
|
23
|
-
id?: string
|
|
24
|
-
name: string
|
|
25
|
-
channelId: string
|
|
26
|
-
status: CampaignStatus
|
|
27
|
-
start: string
|
|
28
|
-
end?: string
|
|
29
|
-
spend: number
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface SaveLandingPageInput {
|
|
33
|
-
id?: string
|
|
34
|
-
siteId: string
|
|
35
|
-
name: string
|
|
36
|
-
route: string
|
|
37
|
-
leadTags?: string[]
|
|
38
|
-
status?: LandingPageStatus
|
|
39
|
-
prompt?: string
|
|
40
|
-
isActive?: boolean
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface MarketingDataProvider {
|
|
44
|
-
overview(query: MarketingQuery): Promise<MarketingOverview>
|
|
45
|
-
channelPerformance(query: MarketingQuery): Promise<ChannelPerformance[]>
|
|
46
|
-
listCampaigns(query: MarketingQuery): Promise<Campaign[]>
|
|
47
|
-
getCampaign(id: string): Promise<Campaign | null>
|
|
48
|
-
saveCampaign(input: SaveCampaignInput): Promise<Campaign>
|
|
49
|
-
deleteCampaign(id: string): Promise<void>
|
|
50
|
-
funnel(query: MarketingQuery): Promise<FunnelStage[]>
|
|
51
|
-
landingPages(query: MarketingQuery): Promise<LandingPagePerf[]>
|
|
52
|
-
saveLandingPage(input: SaveLandingPageInput): Promise<LandingPage>
|
|
53
|
-
deleteLandingPage(id: string): Promise<void>
|
|
54
|
-
/** Sites the tenant owns — the "site" half of a landing page. */
|
|
55
|
-
listSites(): Promise<MarketingSite[]>
|
|
56
|
-
/** Lead-tag catalog offered in the landing-page form (read-only). */
|
|
57
|
-
listLeadTags(): Promise<Array<{ slug: string; name: string }>>
|
|
58
|
-
/** Drop any provider-side read cache (external writes, e.g. agent RPCs). */
|
|
59
|
-
invalidateCache?(): void
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Bridge seams (optional DI). Apps inject these to swap mock numbers for real
|
|
64
|
-
// conversions read from plugin-crm / plugin-agenda / plugin-orders, and real
|
|
65
|
-
// landing-page performance read from plugin-sites. Mirrors the agenda↔financial
|
|
66
|
-
// bridge pattern (plugins compose via DI, never direct imports).
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
|
|
69
|
-
export interface AttributionBridge {
|
|
70
|
-
conversionsByChannel(
|
|
71
|
-
query: MarketingQuery,
|
|
72
|
-
): Promise<Array<{ channelId: string; conversions: number; value: number }>>
|
|
73
|
-
funnel(query: MarketingQuery): Promise<FunnelStage[]>
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Visit analytics for landing pages. The pages themselves are rows the tenant
|
|
78
|
-
* owns here; the VISITS live wherever the site is deployed, so this reads them
|
|
79
|
-
* back by (app id, route, period) — the contract the hosting platform's
|
|
80
|
-
* analytics endpoint is expected to take. Nothing implements it yet: without a
|
|
81
|
-
* bridge the surface shows the pages with visits pending ("—") rather than
|
|
82
|
-
* fake zeros.
|
|
83
|
-
*/
|
|
84
|
-
export interface SitesPerformanceBridge {
|
|
85
|
-
visitsByRoute(query: {
|
|
86
|
-
range: DateRangeKey
|
|
87
|
-
pages: Array<{
|
|
88
|
-
id: string
|
|
89
|
-
/** app manifest id of the deploy (fayz_projects.app_id) */
|
|
90
|
-
appId?: string
|
|
91
|
-
/** platform project id (fayz_projects.fayz_project_id) */
|
|
92
|
-
fayzProjectId?: string
|
|
93
|
-
siteUrl?: string
|
|
94
|
-
route: string
|
|
95
|
-
}>
|
|
96
|
-
}): Promise<Array<{ id: string; visits: number }>>
|
|
97
|
-
}
|
package/src/format.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,457 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import type { PluginManifest, PluginRegistryDef, PluginScope, VerticalId } from '@fayz-ai/core'
|
|
3
|
-
import { createSafeDataProvider, registerTranslations } from '@fayz-ai/core'
|
|
4
|
-
import { PluginSettingsPanel } from '@fayz-ai/saas'
|
|
5
|
-
import { MarketingPage } from './MarketingPage'
|
|
6
|
-
import { SettingsView } from './views/SettingsView'
|
|
7
|
-
import { buildMarketingRegistries } from './registries'
|
|
8
|
-
import { createMarketingDashboardWidgets } from './views/dashboardWidgets'
|
|
9
|
-
import {
|
|
10
|
-
MarketingContextProvider,
|
|
11
|
-
type ResolvedMarketingConfig,
|
|
12
|
-
type MarketingLabels,
|
|
13
|
-
} from './MarketingContext'
|
|
14
|
-
import type {
|
|
15
|
-
AttributionBridge,
|
|
16
|
-
MarketingDataProvider,
|
|
17
|
-
SitesPerformanceBridge,
|
|
18
|
-
} from './data/types'
|
|
19
|
-
import { createMockMarketingProvider } from './data/mock'
|
|
20
|
-
import { createSupabaseMarketingProvider } from './data/supabase'
|
|
21
|
-
import { T } from './data/tables'
|
|
22
|
-
import type { ContentPlannerProvider } from './data/contentTypes'
|
|
23
|
-
import { createMockContentPlannerProvider } from './data/contentMock'
|
|
24
|
-
import { createSupabaseContentPlannerProvider } from './data/contentSupabase'
|
|
25
|
-
import { createMarketingStore } from './store'
|
|
26
|
-
import { createContentPlannerStore } from './views/content/contentStore'
|
|
27
|
-
import { MIGRATION_000_PLG_RENAME, MIGRATION_001_CONTENT_PLANNER, MIGRATION_002_MULTI_PLATFORM, MIGRATION_003_RECORDING_OPS, MIGRATION_004_RANKLAYER, MIGRATION_005_ANALYTICS_BASE, MIGRATION_006_ANALYTICS_VIEWS, MIGRATION_007_AGENT_RPCS, MIGRATION_008_LANDING_PAGES } from './migrations'
|
|
28
|
-
import { ranklayerConnectorDef } from './integrations/ranklayer/connectorDef'
|
|
29
|
-
import { DEFAULT_CURRENCY, type MarketingCurrency } from './format'
|
|
30
|
-
import { MARKETING_PRESETS, type MarketingDomain, type MarketingDomainModules } from './presets'
|
|
31
|
-
import type { AcquisitionChannel, ConversionModel } from './types'
|
|
32
|
-
import { marketingLocales } from './locales'
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// @fayz-ai/plugin-marketing — acquisition & conversion analytics. ONE plugin,
|
|
36
|
-
// every vertical: a `domain` preset (or explicit conversion + channels) adapts
|
|
37
|
-
// what a conversion is and where it's attributed from. Channels, campaigns,
|
|
38
|
-
// funnel and landing-page CVR are computed generically.
|
|
39
|
-
//
|
|
40
|
-
// Ships a vertical-flavored mock today; real attribution comes via the
|
|
41
|
-
// AttributionBridge / SitesPerformanceBridge seams (read from crm/agenda/
|
|
42
|
-
// orders/sites). Outbound broadcasts + journeys are reserved behind flags.
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
export interface MarketingPluginOptions {
|
|
46
|
-
/** Vertical preset for conversion model + channels + module defaults. */
|
|
47
|
-
domain?: MarketingDomain
|
|
48
|
-
/** Override the conversion model (else taken from the domain preset). */
|
|
49
|
-
conversion?: ConversionModel
|
|
50
|
-
/** Override the acquisition channels (else from the domain preset). */
|
|
51
|
-
channels?: AcquisitionChannel[]
|
|
52
|
-
modules?: Partial<MarketingDomainModules>
|
|
53
|
-
currency?: Partial<MarketingCurrency>
|
|
54
|
-
labels?: Partial<MarketingLabels>
|
|
55
|
-
navPosition?: number
|
|
56
|
-
navSection?: 'main' | 'secondary' | 'settings'
|
|
57
|
-
navLabel?: string
|
|
58
|
-
scope?: PluginScope
|
|
59
|
-
verticalId?: VerticalId
|
|
60
|
-
dataProvider?: MarketingDataProvider
|
|
61
|
-
/** Content planner options (module toggled via `modules.contentPlanner`). */
|
|
62
|
-
contentPlanner?: { dataProvider?: ContentPlannerProvider }
|
|
63
|
-
/** App/vertical-owned settings registries rendered inside Marketing settings. */
|
|
64
|
-
settingsRegistries?: PluginRegistryDef[]
|
|
65
|
-
/** Optional DI to read real conversions (mounted later). */
|
|
66
|
-
attributionBridge?: AttributionBridge
|
|
67
|
-
/** Optional DI to read real landing-page visits (mounted later). */
|
|
68
|
-
sitesBridge?: SitesPerformanceBridge
|
|
69
|
-
/** Where the lead-tag catalog is read from for the landing-page form.
|
|
70
|
-
* Defaults to plugin-crm's plg_crm_tags (slug/name). */
|
|
71
|
-
leadTags?: { table?: string; valueField?: string; labelField?: string }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const DEFAULT_LABELS: MarketingLabels = {
|
|
75
|
-
pageTitle: 'Marketing',
|
|
76
|
-
pageSubtitle: 'Acquisition & conversion performance',
|
|
77
|
-
overview: 'Overview',
|
|
78
|
-
channels: 'Channels',
|
|
79
|
-
campaigns: 'Campaigns',
|
|
80
|
-
funnel: 'Funnel',
|
|
81
|
-
landingPages: 'Landing pages',
|
|
82
|
-
content: 'Content',
|
|
83
|
-
blog: 'Blog',
|
|
84
|
-
blogCategories: 'Categorias',
|
|
85
|
-
settings: 'Settings',
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function resolveConfig(options?: MarketingPluginOptions): { config: ResolvedMarketingConfig; domain: MarketingDomain } {
|
|
89
|
-
const domain = options?.domain ?? 'agency'
|
|
90
|
-
const preset = MARKETING_PRESETS[domain]
|
|
91
|
-
const config: ResolvedMarketingConfig = {
|
|
92
|
-
conversion: options?.conversion ?? preset.conversion,
|
|
93
|
-
channels: options?.channels ?? preset.channels,
|
|
94
|
-
modules: { ...preset.modules, ...options?.modules },
|
|
95
|
-
currency: { ...DEFAULT_CURRENCY, ...options?.currency },
|
|
96
|
-
labels: { ...DEFAULT_LABELS, ...options?.labels },
|
|
97
|
-
}
|
|
98
|
-
return { config, domain }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function createSafeProvider(
|
|
102
|
-
config: ResolvedMarketingConfig,
|
|
103
|
-
domain: MarketingDomain,
|
|
104
|
-
options?: MarketingPluginOptions,
|
|
105
|
-
): MarketingDataProvider {
|
|
106
|
-
return createSafeDataProvider(
|
|
107
|
-
() => createSupabaseMarketingProvider({
|
|
108
|
-
channels: config.channels,
|
|
109
|
-
conversion: config.conversion,
|
|
110
|
-
attributionBridge: options?.attributionBridge,
|
|
111
|
-
sitesBridge: options?.sitesBridge,
|
|
112
|
-
leadTags: options?.leadTags,
|
|
113
|
-
}),
|
|
114
|
-
() => createMockMarketingProvider({ channels: config.channels, conversion: config.conversion, domain }),
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function createMarketingPlugin(options?: MarketingPluginOptions): PluginManifest {
|
|
119
|
-
registerTranslations(marketingLocales)
|
|
120
|
-
const { config, domain } = resolveConfig(options)
|
|
121
|
-
const provider = options?.dataProvider ?? createSafeProvider(config, domain, options)
|
|
122
|
-
const store = createMarketingStore(provider)
|
|
123
|
-
const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store })
|
|
124
|
-
|
|
125
|
-
// Content planner has its own provider seam: real Supabase CRUD (with mock
|
|
126
|
-
// fallback) while the analytics provider stays mock-until-bridges.
|
|
127
|
-
const contentProvider = options?.contentPlanner?.dataProvider ?? createSafeDataProvider(
|
|
128
|
-
() => createSupabaseContentPlannerProvider(),
|
|
129
|
-
() => createMockContentPlannerProvider(),
|
|
130
|
-
)
|
|
131
|
-
const contentStore = createContentPlannerStore(contentProvider)
|
|
132
|
-
|
|
133
|
-
const PageComponent: React.ComponentType<unknown> = () =>
|
|
134
|
-
React.createElement(MarketingContextProvider, { config, provider, store },
|
|
135
|
-
React.createElement(MarketingPage, { config, provider, store, contentProvider, contentStore }))
|
|
136
|
-
PageComponent.displayName = 'MarketingPage'
|
|
137
|
-
|
|
138
|
-
// Settings lives in the SDK-core central Settings area (not a module tab).
|
|
139
|
-
// General preferences (channel tracking toggles) + a Channels registry tab,
|
|
140
|
-
// mirroring how plugin-menu surfaces Allergens.
|
|
141
|
-
const marketingRegistries = [
|
|
142
|
-
...buildMarketingRegistries(config.channels),
|
|
143
|
-
...(options?.settingsRegistries ?? []),
|
|
144
|
-
]
|
|
145
|
-
const SettingsComponent: React.ComponentType<unknown> = () =>
|
|
146
|
-
React.createElement(MarketingContextProvider, { config, provider, store },
|
|
147
|
-
React.createElement(PluginSettingsPanel, {
|
|
148
|
-
title: config.labels.settings ?? config.labels.pageTitle,
|
|
149
|
-
subtitle: config.labels.pageSubtitle,
|
|
150
|
-
generalSettings: React.createElement(SettingsView),
|
|
151
|
-
registries: marketingRegistries,
|
|
152
|
-
// Enables the "Integrações" tab (renders manifest connectors for this host).
|
|
153
|
-
hostPluginId: 'marketing',
|
|
154
|
-
routeBase: '/settings/marketing',
|
|
155
|
-
}))
|
|
156
|
-
SettingsComponent.displayName = 'MarketingSettings'
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
id: 'marketing',
|
|
160
|
-
name: options?.navLabel ?? config.labels.pageTitle,
|
|
161
|
-
icon: 'Megaphone',
|
|
162
|
-
version: '1.0.0',
|
|
163
|
-
scope: options?.scope ?? 'universal',
|
|
164
|
-
verticalId: options?.verticalId,
|
|
165
|
-
// Admin content-planning surface (campaigns, content calendar) — targets the
|
|
166
|
-
// saas/admin Panel world, not the ecommerce storefront/operator console.
|
|
167
|
-
scaffolds: ['saas'],
|
|
168
|
-
defaultEnabled: true,
|
|
169
|
-
dependencies: [],
|
|
170
|
-
declaredFeatures: [
|
|
171
|
-
{ id: 'marketing', label: config.labels.pageTitle, group: 'Engage' },
|
|
172
|
-
...(config.modules.landingPages ? [{ id: 'marketing.landing-pages', label: config.labels.landingPages, group: 'Engage' }] : []),
|
|
173
|
-
...(config.modules.contentPlanner ? [{ id: 'marketing.content', label: config.labels.content, group: 'Engage' }] : []),
|
|
174
|
-
...(config.modules.blog ? [{ id: 'marketing.blog', label: config.labels.blog, group: 'Engage' }] : []),
|
|
175
|
-
],
|
|
176
|
-
declaredLimits: [
|
|
177
|
-
...(config.modules.campaigns ? [{ key: 'campaigns_active', label: 'Active campaigns', table: T.campaigns }] : []),
|
|
178
|
-
...(config.modules.contentPlanner ? [{ key: 'content_posts_month', label: 'Content posts / month', table: 'plg_marketing_content_posts', period: 'month' as const }] : []),
|
|
179
|
-
...(config.modules.blog ? [{ key: 'blog_posts', label: 'Blog posts', table: 'plg_blog_posts' }] : []),
|
|
180
|
-
],
|
|
181
|
-
// Read-models for the agent's data primitives (searchRecords/queryData) —
|
|
182
|
-
// mirrors agenda's `agenda:appointments`. Keys are stable ASCII; fields are
|
|
183
|
-
// the provider's camelCase output shape.
|
|
184
|
-
queryEntities: [
|
|
185
|
-
...(config.modules.campaigns ? [{
|
|
186
|
-
key: 'marketing:campaigns',
|
|
187
|
-
writable: false,
|
|
188
|
-
entity: {
|
|
189
|
-
name: 'Campaign',
|
|
190
|
-
namePlural: 'Campaigns',
|
|
191
|
-
icon: 'Megaphone',
|
|
192
|
-
permission: { feature: 'marketing', action: 'read' as const },
|
|
193
|
-
fields: [
|
|
194
|
-
{ key: 'name', label: 'Name', type: 'text' as const, searchable: true },
|
|
195
|
-
{ key: 'channelKey', label: 'Channel', type: 'text' as const, searchable: true },
|
|
196
|
-
{ key: 'channelLabel', label: 'Channel label', type: 'text' as const },
|
|
197
|
-
{ key: 'status', label: 'Status', type: 'text' as const },
|
|
198
|
-
{ key: 'startsAt', label: 'Starts at', type: 'text' as const },
|
|
199
|
-
{ key: 'endsAt', label: 'Ends at', type: 'text' as const },
|
|
200
|
-
{ key: 'spend', label: 'Spend', type: 'number' as const },
|
|
201
|
-
{ key: 'createdAt', label: 'Created at', type: 'text' as const },
|
|
202
|
-
],
|
|
203
|
-
data: {
|
|
204
|
-
table: 'v_marketing_campaigns',
|
|
205
|
-
tenantScoped: true,
|
|
206
|
-
searchColumns: ['name', 'channel_key'],
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
}] : []),
|
|
210
|
-
...(config.modules.channels ? [{
|
|
211
|
-
key: 'marketing:channels',
|
|
212
|
-
writable: false,
|
|
213
|
-
entity: {
|
|
214
|
-
name: 'Marketing channel',
|
|
215
|
-
namePlural: 'Marketing channels',
|
|
216
|
-
icon: 'Radio',
|
|
217
|
-
permission: { feature: 'marketing', action: 'read' as const },
|
|
218
|
-
fields: [
|
|
219
|
-
{ key: 'channelKey', label: 'Key', type: 'text' as const, searchable: true },
|
|
220
|
-
{ key: 'label', label: 'Channel', type: 'text' as const, searchable: true },
|
|
221
|
-
{ key: 'kind', label: 'Type', type: 'text' as const },
|
|
222
|
-
{ key: 'isActive', label: 'Active', type: 'boolean' as const },
|
|
223
|
-
{ key: 'monthlySpend', label: 'Monthly spend', type: 'number' as const },
|
|
224
|
-
],
|
|
225
|
-
data: {
|
|
226
|
-
table: 'v_marketing_channels',
|
|
227
|
-
tenantScoped: true,
|
|
228
|
-
searchColumns: ['label', 'channel_key'],
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
}] : []),
|
|
232
|
-
...(config.modules.contentPlanner ? [{
|
|
233
|
-
key: 'marketing:contentPosts',
|
|
234
|
-
writable: false,
|
|
235
|
-
entity: {
|
|
236
|
-
name: 'Content post',
|
|
237
|
-
namePlural: 'Content posts',
|
|
238
|
-
icon: 'FileText',
|
|
239
|
-
permission: { feature: 'marketing.content', action: 'read' as const },
|
|
240
|
-
fields: [
|
|
241
|
-
{ key: 'title', label: 'Title', type: 'text' as const, searchable: true },
|
|
242
|
-
{ key: 'format', label: 'Format', type: 'text' as const },
|
|
243
|
-
{ key: 'status', label: 'Status', type: 'text' as const },
|
|
244
|
-
{ key: 'weekNumber', label: 'Week', type: 'number' as const },
|
|
245
|
-
{ key: 'scheduledDate', label: 'Scheduled date', type: 'text' as const },
|
|
246
|
-
{ key: 'hook', label: 'Hook', type: 'text' as const, searchable: true },
|
|
247
|
-
],
|
|
248
|
-
data: {
|
|
249
|
-
table: T.contentPosts,
|
|
250
|
-
tenantScoped: true,
|
|
251
|
-
searchColumns: ['title', 'hook'],
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
}] : []),
|
|
255
|
-
],
|
|
256
|
-
declaredRpcs: [
|
|
257
|
-
...(config.modules.campaigns ? [{
|
|
258
|
-
name: 'agent_marketing_create_campaign',
|
|
259
|
-
kind: 'write' as const,
|
|
260
|
-
description:
|
|
261
|
-
'Guarded campaign create: agent_guard (role→plan→campaigns_active cap), channel validated against the tenant channel set, audited.',
|
|
262
|
-
audits: true,
|
|
263
|
-
}] : []),
|
|
264
|
-
],
|
|
265
|
-
navigation: [
|
|
266
|
-
{
|
|
267
|
-
section: options?.navSection ?? 'main',
|
|
268
|
-
position: options?.navPosition ?? 5,
|
|
269
|
-
label: options?.navLabel ?? config.labels.pageTitle,
|
|
270
|
-
route: '/marketing',
|
|
271
|
-
icon: 'Megaphone',
|
|
272
|
-
permission: { feature: 'marketing', action: 'read' as const },
|
|
273
|
-
},
|
|
274
|
-
],
|
|
275
|
-
routes: [
|
|
276
|
-
{
|
|
277
|
-
path: '/marketing',
|
|
278
|
-
component: PageComponent,
|
|
279
|
-
permission: { feature: 'marketing', action: 'read' as const },
|
|
280
|
-
},
|
|
281
|
-
],
|
|
282
|
-
widgets: [],
|
|
283
|
-
dashboardWidgets,
|
|
284
|
-
events: [
|
|
285
|
-
{ name: 'marketing.conversion.tracked', description: 'A conversion was attributed to a channel/campaign' },
|
|
286
|
-
{ name: 'marketing.campaign.created', description: 'A campaign was created' },
|
|
287
|
-
{ name: 'marketing.campaign.updated', description: 'A campaign was updated' },
|
|
288
|
-
{ name: 'marketing.channel.synced', description: 'A channel pulled fresh attribution data' },
|
|
289
|
-
{ name: 'marketing.content.plan.updated', description: 'A content plan (brief/config) was updated' },
|
|
290
|
-
{ name: 'marketing.content.post.created', description: 'A content post was created' },
|
|
291
|
-
{ name: 'marketing.content.post.updated', description: 'A content post was updated' },
|
|
292
|
-
],
|
|
293
|
-
migrations: [
|
|
294
|
-
{
|
|
295
|
-
id: 'marketing-000-plg-rename',
|
|
296
|
-
version: '1.0.0',
|
|
297
|
-
sql: MIGRATION_000_PLG_RENAME,
|
|
298
|
-
description: 'Rename legacy mkt_* tables to plg_marketing_* in-place for pools provisioned before the industry-pool rename (guarded no-op on fresh pools)',
|
|
299
|
-
},
|
|
300
|
-
{
|
|
301
|
-
id: 'marketing-001-content-planner',
|
|
302
|
-
version: '1.0.0',
|
|
303
|
-
sql: MIGRATION_001_CONTENT_PLANNER,
|
|
304
|
-
description: 'Create plg_marketing_social_accounts, plg_marketing_content_plans and plg_marketing_content_posts',
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
id: 'marketing-002-multi-platform',
|
|
308
|
-
version: '1.1.0',
|
|
309
|
-
sql: MIGRATION_002_MULTI_PLATFORM,
|
|
310
|
-
description: 'Accounts publish to many platforms (platforms[]); posts gain optional platform targets',
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
id: 'marketing-003-recording-ops',
|
|
314
|
-
version: '1.2.0',
|
|
315
|
-
sql: MIGRATION_003_RECORDING_OPS,
|
|
316
|
-
description: 'Recording-day checklist + media asset URL on posts; content-media storage bucket',
|
|
317
|
-
},
|
|
318
|
-
{
|
|
319
|
-
id: 'marketing-004-ranklayer',
|
|
320
|
-
version: '1.3.0',
|
|
321
|
-
sql: MIGRATION_004_RANKLAYER,
|
|
322
|
-
description: 'RankLayer connector state: plg_marketing_ranklayer_integrations + _sync_log (tenant RLS)',
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
id: 'marketing-005-analytics-base',
|
|
326
|
-
version: '1.4.0',
|
|
327
|
-
sql: MIGRATION_005_ANALYTICS_BASE,
|
|
328
|
-
description: 'Analytics base: plg_marketing_channels + plg_marketing_campaigns with canonical tenant RLS',
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
id: 'marketing-006-analytics-views',
|
|
332
|
-
version: '1.4.0',
|
|
333
|
-
sql: MIGRATION_006_ANALYTICS_VIEWS,
|
|
334
|
-
description: 'Read views v_marketing_channels/_campaigns + generic spine-only v_marketing_attribution',
|
|
335
|
-
},
|
|
336
|
-
{
|
|
337
|
-
id: 'marketing-007-agent-rpcs',
|
|
338
|
-
version: '1.4.0',
|
|
339
|
-
sql: MIGRATION_007_AGENT_RPCS,
|
|
340
|
-
description: 'agent_marketing_create_campaign — guarded, audited server-plane campaign create',
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
id: 'marketing-008-landing-pages',
|
|
344
|
-
version: '1.5.0',
|
|
345
|
-
sql: MIGRATION_008_LANDING_PAGES,
|
|
346
|
-
description: 'Sites + landing pages (site + route + lead tag) with tenant RLS, v_marketing_landing_pages and v_marketing_lead_tag_events',
|
|
347
|
-
},
|
|
348
|
-
],
|
|
349
|
-
// RankLayer SEO connector — renders in the Integrações settings tab. Scaffold
|
|
350
|
-
// (control plane only); the real sync lands via an external PR (RANKLAYER.md).
|
|
351
|
-
connectors: [ranklayerConnectorDef],
|
|
352
|
-
aiTools: [
|
|
353
|
-
{
|
|
354
|
-
id: 'marketing.channel-performance',
|
|
355
|
-
name: 'channelPerformance',
|
|
356
|
-
description: 'Returns acquisition performance per channel (reach, conversions, CVR, spend, CPA).',
|
|
357
|
-
icon: 'Radio',
|
|
358
|
-
mode: 'read' as const,
|
|
359
|
-
category: 'Marketing',
|
|
360
|
-
parameters: {
|
|
361
|
-
type: 'object' as const,
|
|
362
|
-
properties: { range: { type: 'string' as const, enum: ['7d', '30d', '90d'] } },
|
|
363
|
-
},
|
|
364
|
-
suggestions: [{ label: 'Which channel converts best?' }, { label: 'What is my cost per acquisition?' }],
|
|
365
|
-
permission: { feature: 'marketing', action: 'read' as const },
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
id: 'marketing.top-channels',
|
|
369
|
-
name: 'topChannels',
|
|
370
|
-
description: 'Returns the top acquisition channels ranked by conversions.',
|
|
371
|
-
icon: 'Trophy',
|
|
372
|
-
mode: 'read' as const,
|
|
373
|
-
category: 'Marketing',
|
|
374
|
-
permission: { feature: 'marketing', action: 'read' as const },
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
id: 'marketing.campaign-cvr',
|
|
378
|
-
name: 'campaignCvr',
|
|
379
|
-
description: 'Lists campaigns with their conversion rate for a date range.',
|
|
380
|
-
icon: 'Percent',
|
|
381
|
-
mode: 'read' as const,
|
|
382
|
-
category: 'Marketing',
|
|
383
|
-
parameters: {
|
|
384
|
-
type: 'object' as const,
|
|
385
|
-
properties: { range: { type: 'string' as const, enum: ['7d', '30d', '90d'] } },
|
|
386
|
-
},
|
|
387
|
-
permission: { feature: 'marketing', action: 'read' as const },
|
|
388
|
-
},
|
|
389
|
-
{
|
|
390
|
-
id: 'marketing.create-campaign',
|
|
391
|
-
name: 'createCampaign',
|
|
392
|
-
description:
|
|
393
|
-
'Creates an acquisition campaign on a channel. Resolve the channel first via queryData/searchRecords on marketing channels (use its channel key). The user confirms before creating.',
|
|
394
|
-
icon: 'Plus',
|
|
395
|
-
mode: 'persist' as const,
|
|
396
|
-
limitKey: 'campaigns_active',
|
|
397
|
-
execution: { plane: 'server' as const, kind: 'rpc' as const, rpc: 'agent_marketing_create_campaign' },
|
|
398
|
-
category: 'Marketing',
|
|
399
|
-
parameters: {
|
|
400
|
-
type: 'object' as const,
|
|
401
|
-
properties: {
|
|
402
|
-
name: { type: 'string' as const, description: 'Campaign name' },
|
|
403
|
-
channel_key: { type: 'string' as const, description: "Acquisition channel key (e.g. 'instagram') or its label" },
|
|
404
|
-
status: { type: 'string' as const, enum: ['draft', 'active', 'paused'], description: 'Defaults to draft' },
|
|
405
|
-
starts_at: { type: 'string' as const, description: 'ISO start date-time (defaults to now)' },
|
|
406
|
-
ends_at: { type: 'string' as const, description: 'ISO end date-time (optional)' },
|
|
407
|
-
spend: { type: 'number' as const, description: 'Planned/committed spend (optional)' },
|
|
408
|
-
},
|
|
409
|
-
required: ['name', 'channel_key'],
|
|
410
|
-
},
|
|
411
|
-
suggestions: [{ label: 'Create an Instagram campaign for this month' }],
|
|
412
|
-
permission: { feature: 'marketing', action: 'create' as const },
|
|
413
|
-
},
|
|
414
|
-
],
|
|
415
|
-
registries: marketingRegistries,
|
|
416
|
-
settings: [
|
|
417
|
-
{
|
|
418
|
-
id: 'marketing',
|
|
419
|
-
label: config.labels.pageTitle,
|
|
420
|
-
icon: 'Megaphone',
|
|
421
|
-
component: SettingsComponent,
|
|
422
|
-
order: 20,
|
|
423
|
-
permission: { feature: 'marketing', action: 'read' as const },
|
|
424
|
-
},
|
|
425
|
-
],
|
|
426
|
-
locales: marketingLocales,
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
export type {
|
|
431
|
-
MarketingDataProvider,
|
|
432
|
-
AttributionBridge,
|
|
433
|
-
SitesPerformanceBridge,
|
|
434
|
-
} from './data/types'
|
|
435
|
-
export type {
|
|
436
|
-
AcquisitionChannel,
|
|
437
|
-
ConversionModel,
|
|
438
|
-
Campaign,
|
|
439
|
-
ChannelPerformance,
|
|
440
|
-
LandingPagePerf,
|
|
441
|
-
} from './types'
|
|
442
|
-
export { MARKETING_PRESETS, type MarketingDomain } from './presets'
|
|
443
|
-
export { createMockMarketingProvider } from './data/mock'
|
|
444
|
-
export { createSupabaseMarketingProvider } from './data/supabase'
|
|
445
|
-
export type {
|
|
446
|
-
ContentPlannerProvider,
|
|
447
|
-
ContentPlan,
|
|
448
|
-
ContentPost,
|
|
449
|
-
SocialAccount,
|
|
450
|
-
PostFormat,
|
|
451
|
-
PostStatus,
|
|
452
|
-
SaveContentPlanInput,
|
|
453
|
-
SaveContentPostInput,
|
|
454
|
-
SaveSocialAccountInput,
|
|
455
|
-
} from './data/contentTypes'
|
|
456
|
-
export { createMockContentPlannerProvider } from './data/contentMock'
|
|
457
|
-
export { createSupabaseContentPlannerProvider } from './data/contentSupabase'
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# RankLayer connector
|
|
2
|
-
|
|
3
|
-
Surfaces in **Marketing → settings → Integrações** (rendered by the shared
|
|
4
|
-
`ConnectorsHub`). Follows the connector contract in
|
|
5
|
-
`packages/core/src/integrations/index.ts` and mirrors the PlugBank precedent
|
|
6
|
-
(`beauty-saas/src/plugins/openbanking`).
|
|
7
|
-
|
|
8
|
-
## Status: SCAFFOLD
|
|
9
|
-
|
|
10
|
-
This ships the control plane only:
|
|
11
|
-
|
|
12
|
-
- `connectorDef.tsx` — the `ConnectorDefinition` (api-key form: API Key + site
|
|
13
|
-
domain, status badge, `ExtraPanel` with a "sync coming soon" note).
|
|
14
|
-
- `data/supabase.ts` — persists the connection row and reads the sync log.
|
|
15
|
-
- `../../migrations/004_ranklayer.sql` — `plg_marketing_ranklayer_integrations`
|
|
16
|
-
+ `plg_marketing_ranklayer_sync_log` (tenant RLS).
|
|
17
|
-
|
|
18
|
-
`testConnection` currently accepts any non-empty API key and **no real sync
|
|
19
|
-
runs** — RankLayer has no public API wired yet.
|
|
20
|
-
|
|
21
|
-
## What the RankLayer PR needs to fill in
|
|
22
|
-
|
|
23
|
-
1. **Edge function** `functions/ranklayer-sync/index.ts` holding the RankLayer
|
|
24
|
-
API key server-side (move it out of the `api_key` column into a secret if the
|
|
25
|
-
provider requires it), with actions:
|
|
26
|
-
- `test_connection` — validate the key against RankLayer.
|
|
27
|
-
- `sync` — the agreed direction. RankLayer generates/publishes SEO pages and
|
|
28
|
-
articles, so the likely first flow is **inbound**: pull RankLayer-published
|
|
29
|
-
articles and upsert them as `plg_blog_posts` (status `published`) so they
|
|
30
|
-
appear on the site's `/blog`. Confirm direction with the RankLayer team.
|
|
31
|
-
2. **Real `testConnection` / `sync`** in `data/supabase.ts` — replace the stubs
|
|
32
|
-
to invoke the edge function; write a `plg_marketing_ranklayer_sync_log` row
|
|
33
|
-
per run (the `SyncRun` audit shape).
|
|
34
|
-
3. **ExtraPanel** — swap the "coming soon" note for a "Sync now" button + the
|
|
35
|
-
real history table (the history rendering is already wired).
|
|
36
|
-
|
|
37
|
-
The connector is already registered on the marketing manifest
|
|
38
|
-
(`connectors: [ranklayerConnectorDef]`) and the Integrações tab is enabled
|
|
39
|
-
(`hostPluginId: 'marketing'` on the settings panel), so no wiring changes are
|
|
40
|
-
needed — only the data-plane implementation above.
|