@fayz-ai/plugin-marketing 0.12.4 → 0.12.6
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/dist/context.d.ts +16 -9
- package/dist/context.d.ts.map +1 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +19 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +502 -63
- package/dist/index.js.map +1 -1
- package/dist/lib/presets.d.ts +10 -0
- package/dist/lib/presets.d.ts.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CadencesView.d.ts +5 -0
- package/dist/views/CadencesView.d.ts.map +1 -0
- package/dist/views/CampaignComposer.d.ts +4 -1
- package/dist/views/CampaignComposer.d.ts.map +1 -1
- package/dist/views/MarketingPage.d.ts +3 -1
- package/dist/views/MarketingPage.d.ts.map +1 -1
- package/dist/views/blog/BlogAdminView.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React9, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
2
2
|
import { connectionSettings, connectionStatus, settingsStatus, createConnectionStore, getActiveTenantId, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, countByTenant, useTranslation, useDataChanged, resolveDataProvider } from '@fayz-ai/core';
|
|
3
3
|
import { createPluginContext, fayzConnectorOAuth, PluginSettingsPanel, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, SelectRow, ToggleRow, askAI, PageActions, useLimitGuard, useOrganizationStore, CrudFormPage, RightRailPage, invalidateLimit, CrudListView } from '@fayz-ai/admin';
|
|
4
|
-
import { defineKpiWidget, defineCustomWidget, defineTableWidget, ModulePage, PageToolbar, DataTable, SubpageHeader, RowActions, PageHeaderActions, Button, Skeleton, ConfirmDialog, ContentSplit, ContentSplitTrigger, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MarkdownEditor, DashboardCanvas, RangeControl, cn, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, SheetBody,
|
|
4
|
+
import { defineKpiWidget, defineCustomWidget, defineTableWidget, ModulePage, PageToolbar, DataTable, SubpageHeader, RowActions, PageHeaderActions, Button, Skeleton, ConfirmDialog, ContentSplit, ContentSplitTrigger, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MarkdownEditor, DashboardCanvas, RangeControl, cn, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, SegmentedControl, ModalFooter, SheetBody, Badge, SheetFooter, SelectSeparator, Checkbox, toast, KpiCard as KpiCard$1, Card, CardHeader, CardTitle, CardContent, TableWidget } from '@fayz-ai/ui';
|
|
5
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
-
import { History, CheckCircle2, AlertCircle, ChevronRight, Users, Target, Percent, DollarSign, Trash2, WandSparkles, SlidersHorizontal, ExternalLink, Sparkles, Pencil, Plus, Layers, MousePointerClick, NotebookPen, ArrowUpRight, ArrowDownRight, Settings2, Columns3, CalendarDays, List, ChevronLeft, Loader2, ImagePlus, X, Bike, Utensils, DoorOpen, MessageCircle, Instagram, Mail, Globe,
|
|
6
|
+
import { History, CheckCircle2, AlertCircle, ChevronRight, Users, Target, Percent, DollarSign, Trash2, WandSparkles, SlidersHorizontal, Workflow, ExternalLink, Sparkles, Pencil, Plus, Layers, MousePointerClick, NotebookPen, ArrowUpRight, ArrowDownRight, Send, Megaphone, Settings2, Columns3, CalendarDays, List, ChevronLeft, Loader2, ImagePlus, X, Bike, Utensils, DoorOpen, MessageCircle, Instagram, Mail, Globe, Search, Link2, AtSign, Pin, Twitter, Linkedin, Facebook, Youtube, Music2, Repeat2, Heart, BarChart2, Share2, ThumbsUp, MoreHorizontal, Bookmark, GripVertical, Image, Trophy } from 'lucide-react';
|
|
7
7
|
import { createStore } from 'zustand/vanilla';
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
@@ -249,41 +249,81 @@ function ChannelDetailView({ channelId, onBack }) {
|
|
|
249
249
|
] });
|
|
250
250
|
}
|
|
251
251
|
var STATUSES = ["draft", "active", "paused"];
|
|
252
|
-
|
|
252
|
+
var PURPOSES = ["promotion", "newsletter", "survey", "transactional"];
|
|
253
|
+
function CampaignComposer({ open, onClose, initialSegmentId }) {
|
|
253
254
|
const t = useTranslation();
|
|
254
255
|
const { channels } = useMarketingConfig();
|
|
256
|
+
const provider2 = useMarketingProvider();
|
|
255
257
|
const saveCampaign = useMarketingStore((s) => s.saveCampaign);
|
|
258
|
+
const [kind, setKind] = React9.useState("ad_spend");
|
|
256
259
|
const [name, setName] = React9.useState("");
|
|
257
260
|
const [channelId, setChannelId] = React9.useState(channels[0]?.id ?? "");
|
|
258
|
-
const [status, setStatus] = React9.useState("
|
|
261
|
+
const [status, setStatus] = React9.useState("draft");
|
|
259
262
|
const [spend, setSpend] = React9.useState("");
|
|
263
|
+
const [purpose, setPurpose] = React9.useState("promotion");
|
|
264
|
+
const [segmentId, setSegmentId] = React9.useState("");
|
|
265
|
+
const [templateKey, setTemplateKey] = React9.useState("");
|
|
266
|
+
const [segments, setSegments] = React9.useState([]);
|
|
260
267
|
const [saving, setSaving] = React9.useState(false);
|
|
261
268
|
React9.useEffect(() => {
|
|
262
|
-
if (open)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
+
if (!open) return;
|
|
270
|
+
setName("");
|
|
271
|
+
setChannelId(channels[0]?.id ?? "");
|
|
272
|
+
setSpend("");
|
|
273
|
+
setPurpose("promotion");
|
|
274
|
+
setTemplateKey("");
|
|
275
|
+
setStatus("draft");
|
|
276
|
+
setSegmentId(initialSegmentId ?? "");
|
|
277
|
+
setKind(initialSegmentId ? "outbound" : "ad_spend");
|
|
278
|
+
void provider2.listAudienceSegments().then(setSegments).catch(() => setSegments([]));
|
|
279
|
+
}, [open, channels, initialSegmentId, provider2]);
|
|
280
|
+
const isOutbound = kind === "outbound";
|
|
281
|
+
const canSave = name.trim().length > 0 && (isOutbound ? Boolean(segmentId && templateKey.trim()) : Boolean(channelId));
|
|
269
282
|
async function handleSave() {
|
|
270
|
-
if (!
|
|
283
|
+
if (!canSave) return;
|
|
271
284
|
setSaving(true);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
try {
|
|
286
|
+
await saveCampaign({
|
|
287
|
+
name: name.trim(),
|
|
288
|
+
// Um disparo também tem canal: é por ele que o relatório soma o que
|
|
289
|
+
// saiu por WhatsApp e o que saiu por e-mail.
|
|
290
|
+
channelId: channelId || channels[0]?.id || "email",
|
|
291
|
+
status,
|
|
292
|
+
kind,
|
|
293
|
+
purpose: isOutbound ? purpose : "promotion",
|
|
294
|
+
segmentId: isOutbound ? segmentId : void 0,
|
|
295
|
+
templateKey: isOutbound ? templateKey.trim() : void 0,
|
|
296
|
+
start: (/* @__PURE__ */ new Date()).toISOString(),
|
|
297
|
+
spend: isOutbound ? 0 : Number(spend) || 0
|
|
298
|
+
});
|
|
299
|
+
onClose();
|
|
300
|
+
} finally {
|
|
301
|
+
setSaving(false);
|
|
302
|
+
}
|
|
281
303
|
}
|
|
304
|
+
const selectedSegment = segments.find((s) => s.id === segmentId);
|
|
282
305
|
return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (o) => {
|
|
283
306
|
if (!o) onClose();
|
|
284
307
|
}, children: /* @__PURE__ */ jsxs(ModalContent, { size: "md", children: [
|
|
285
308
|
/* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: t("marketing.composer.title") }) }),
|
|
286
309
|
/* @__PURE__ */ jsxs(ModalBody, { className: "space-y-4", children: [
|
|
310
|
+
/* @__PURE__ */ jsx(
|
|
311
|
+
SegmentedControl,
|
|
312
|
+
{
|
|
313
|
+
className: "w-full",
|
|
314
|
+
value: kind,
|
|
315
|
+
onChange: setKind,
|
|
316
|
+
"aria-label": t("marketing.composer.kind"),
|
|
317
|
+
options: [
|
|
318
|
+
{ value: "ad_spend", label: t("marketing.composer.kindSpend") },
|
|
319
|
+
{ value: "outbound", label: t("marketing.composer.kindOutbound") }
|
|
320
|
+
]
|
|
321
|
+
}
|
|
322
|
+
),
|
|
323
|
+
/* @__PURE__ */ jsxs("p", { className: "-mt-2 flex items-start gap-1.5 text-[11px] text-muted-foreground", children: [
|
|
324
|
+
isOutbound ? /* @__PURE__ */ jsx(Send, { className: "mt-0.5 h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(Megaphone, { className: "mt-0.5 h-3 w-3 shrink-0" }),
|
|
325
|
+
isOutbound ? t("marketing.composer.kindOutboundHint") : t("marketing.composer.kindSpendHint")
|
|
326
|
+
] }),
|
|
287
327
|
/* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
|
|
288
328
|
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.name") }),
|
|
289
329
|
/* @__PURE__ */ jsx(Input, { value: name, onChange: (e) => setName(e.target.value), placeholder: t("marketing.composer.namePlaceholder") })
|
|
@@ -295,23 +335,46 @@ function CampaignComposer({ open, onClose }) {
|
|
|
295
335
|
/* @__PURE__ */ jsx(SelectContent, { children: channels.map((c) => /* @__PURE__ */ jsx(SelectItem, { value: c.id, children: c.label }, c.id)) })
|
|
296
336
|
] })
|
|
297
337
|
] }),
|
|
298
|
-
/* @__PURE__ */ jsxs(
|
|
338
|
+
isOutbound ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
299
339
|
/* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
|
|
300
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.
|
|
301
|
-
/* @__PURE__ */ jsxs(Select, { value:
|
|
302
|
-
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
303
|
-
/* @__PURE__ */ jsx(SelectContent, { children:
|
|
340
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.audience") }),
|
|
341
|
+
segments.length > 0 ? /* @__PURE__ */ jsxs(Select, { value: segmentId, onValueChange: setSegmentId, children: [
|
|
342
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("marketing.composer.audiencePlaceholder") }) }),
|
|
343
|
+
/* @__PURE__ */ jsx(SelectContent, { children: segments.map((s) => /* @__PURE__ */ jsx(SelectItem, { value: s.id, children: s.name }, s.id)) })
|
|
344
|
+
] }) : /* @__PURE__ */ jsx("p", { className: "rounded-lg border border-dashed px-3 py-2 text-xs text-muted-foreground", children: t("marketing.composer.noSegments") }),
|
|
345
|
+
selectedSegment?.description && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [
|
|
346
|
+
/* @__PURE__ */ jsx(Users, { className: "h-3 w-3" }),
|
|
347
|
+
selectedSegment.description
|
|
304
348
|
] })
|
|
305
349
|
] }),
|
|
306
350
|
/* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
|
|
307
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.
|
|
308
|
-
/* @__PURE__ */
|
|
351
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.purpose") }),
|
|
352
|
+
/* @__PURE__ */ jsxs(Select, { value: purpose, onValueChange: (v) => setPurpose(v), children: [
|
|
353
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
354
|
+
/* @__PURE__ */ jsx(SelectContent, { children: PURPOSES.map((p) => /* @__PURE__ */ jsx(SelectItem, { value: p, children: t(`marketing.purpose.${p}`) }, p)) })
|
|
355
|
+
] }),
|
|
356
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: t(`marketing.purpose.${purpose}.hint`) })
|
|
357
|
+
] }),
|
|
358
|
+
/* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
|
|
359
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.template") }),
|
|
360
|
+
/* @__PURE__ */ jsx(Input, { value: templateKey, onChange: (e) => setTemplateKey(e.target.value), placeholder: "ex.: campanha_reativacao" }),
|
|
361
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: t("marketing.composer.templateHint") })
|
|
362
|
+
] })
|
|
363
|
+
] }) : /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
|
|
364
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.budget") }),
|
|
365
|
+
/* @__PURE__ */ jsx(Input, { type: "number", min: 0, value: spend, onChange: (e) => setSpend(e.target.value), placeholder: "0" })
|
|
366
|
+
] }),
|
|
367
|
+
/* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
|
|
368
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.status") }),
|
|
369
|
+
/* @__PURE__ */ jsxs(Select, { value: status, onValueChange: (v) => setStatus(v), children: [
|
|
370
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
371
|
+
/* @__PURE__ */ jsx(SelectContent, { children: STATUSES.map((s) => /* @__PURE__ */ jsx(SelectItem, { value: s, className: "capitalize", children: t(`marketing.status.${s}`) }, s)) })
|
|
309
372
|
] })
|
|
310
373
|
] })
|
|
311
374
|
] }),
|
|
312
375
|
/* @__PURE__ */ jsxs(ModalFooter, { children: [
|
|
313
376
|
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: t("marketing.composer.cancel") }),
|
|
314
|
-
/* @__PURE__ */ jsx(Button, { onClick: handleSave, disabled: saving || !
|
|
377
|
+
/* @__PURE__ */ jsx(Button, { onClick: handleSave, disabled: saving || !canSave, children: t("marketing.composer.create") })
|
|
315
378
|
] })
|
|
316
379
|
] }) });
|
|
317
380
|
}
|
|
@@ -431,6 +494,17 @@ function FunnelView() {
|
|
|
431
494
|
] })
|
|
432
495
|
] });
|
|
433
496
|
}
|
|
497
|
+
function CadencesView({ surface: Surface }) {
|
|
498
|
+
const t = useTranslation();
|
|
499
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
500
|
+
/* @__PURE__ */ jsx(SubpageHeader, { title: t("marketing.cadences.title"), subtitle: t("marketing.cadences.subtitle") }),
|
|
501
|
+
Surface ? /* @__PURE__ */ jsx(Surface, {}) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 rounded-lg border border-dashed py-12 text-center", children: [
|
|
502
|
+
/* @__PURE__ */ jsx(Workflow, { className: "mb-2 h-9 w-9 text-muted-foreground/40", strokeWidth: 1.5 }),
|
|
503
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("marketing.cadences.missingTitle") }),
|
|
504
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-md text-xs text-muted-foreground", children: t("marketing.cadences.missingHint") })
|
|
505
|
+
] })
|
|
506
|
+
] });
|
|
507
|
+
}
|
|
434
508
|
|
|
435
509
|
// src/types.ts
|
|
436
510
|
var CAPTURE_FIELDS = ["name", "phone", "email", "document", "notes"];
|
|
@@ -2713,11 +2787,12 @@ function CategoriesList({ navigate, navTabs }) {
|
|
|
2713
2787
|
}
|
|
2714
2788
|
function BlogAdminView({ navigate }) {
|
|
2715
2789
|
const { labels } = useMarketingConfig();
|
|
2790
|
+
const t = useTranslation();
|
|
2716
2791
|
const [lens, setLens] = useState("posts");
|
|
2717
2792
|
const navTabs = useMemo(() => [
|
|
2718
|
-
{ id: "posts", label: labels.blog, onSelect: () => setLens("posts") },
|
|
2719
|
-
{ id: "categories", label: labels.blogCategories, onSelect: () => setLens("categories") }
|
|
2720
|
-
], [labels.blog, labels.blogCategories]);
|
|
2793
|
+
{ id: "posts", label: labels.blog ?? t("marketing.nav.blog"), onSelect: () => setLens("posts") },
|
|
2794
|
+
{ id: "categories", label: labels.blogCategories ?? t("marketing.nav.blogCategories"), onSelect: () => setLens("categories") }
|
|
2795
|
+
], [labels.blog, labels.blogCategories, t]);
|
|
2721
2796
|
return lens === "posts" ? /* @__PURE__ */ jsx(PostsList, { navigate, navTabs }) : /* @__PURE__ */ jsx(CategoriesList, { navigate, navTabs });
|
|
2722
2797
|
}
|
|
2723
2798
|
function BlogEntityForm({
|
|
@@ -2793,32 +2868,35 @@ function BlogEntityForm({
|
|
|
2793
2868
|
)
|
|
2794
2869
|
] });
|
|
2795
2870
|
}
|
|
2796
|
-
function buildNav(config, view, navigate) {
|
|
2871
|
+
function buildNav(config, view, navigate, hasCadenceSurface, t) {
|
|
2797
2872
|
const items = [
|
|
2798
|
-
{ id: "overview", label: config.labels.overview, icon: "BarChart3", active: view === "overview", onClick: () => navigate("overview") }
|
|
2873
|
+
{ id: "overview", label: config.labels.overview ?? t("marketing.nav.overview"), icon: "BarChart3", active: view === "overview", onClick: () => navigate("overview") }
|
|
2799
2874
|
];
|
|
2800
2875
|
if (config.modules.channels) {
|
|
2801
2876
|
items.push({
|
|
2802
2877
|
id: "channels",
|
|
2803
|
-
label: config.labels.channels,
|
|
2878
|
+
label: config.labels.channels ?? t("marketing.nav.channels"),
|
|
2804
2879
|
icon: "Radio",
|
|
2805
2880
|
active: view === "channels" || view.startsWith("channel-detail:"),
|
|
2806
2881
|
onClick: () => navigate("channels")
|
|
2807
2882
|
});
|
|
2808
2883
|
}
|
|
2809
2884
|
if (config.modules.campaigns) {
|
|
2810
|
-
items.push({ id: "campaigns", label: config.labels.campaigns, icon: "Megaphone", active: view === "campaigns", onClick: () => navigate("campaigns") });
|
|
2885
|
+
items.push({ id: "campaigns", label: config.labels.campaigns ?? t("marketing.nav.campaigns"), icon: "Megaphone", active: view === "campaigns", onClick: () => navigate("campaigns") });
|
|
2811
2886
|
}
|
|
2812
2887
|
if (config.modules.funnel) {
|
|
2813
|
-
items.push({ id: "funnel", label: config.labels.funnel, icon: "Filter", active: view === "funnel", onClick: () => navigate("funnel") });
|
|
2888
|
+
items.push({ id: "funnel", label: config.labels.funnel ?? t("marketing.nav.funnel"), icon: "Filter", active: view === "funnel", onClick: () => navigate("funnel") });
|
|
2889
|
+
}
|
|
2890
|
+
if (config.modules.cadences && hasCadenceSurface) {
|
|
2891
|
+
items.push({ id: "cadences", label: config.labels.cadences ?? t("marketing.cadences.title"), icon: "Workflow", active: view === "cadences", onClick: () => navigate("cadences") });
|
|
2814
2892
|
}
|
|
2815
2893
|
if (config.modules.landingPages) {
|
|
2816
|
-
items.push({ id: "landing-pages", label: config.labels.landingPages, icon: "LayoutTemplate", active: view === "landing-pages", onClick: () => navigate("landing-pages") });
|
|
2894
|
+
items.push({ id: "landing-pages", label: config.labels.landingPages ?? t("marketing.nav.landingPages"), icon: "LayoutTemplate", active: view === "landing-pages", onClick: () => navigate("landing-pages") });
|
|
2817
2895
|
}
|
|
2818
2896
|
if (config.modules.contentPlanner) {
|
|
2819
2897
|
items.push({
|
|
2820
2898
|
id: "content",
|
|
2821
|
-
label: config.labels.content,
|
|
2899
|
+
label: config.labels.content ?? t("marketing.nav.content"),
|
|
2822
2900
|
icon: "Clapperboard",
|
|
2823
2901
|
active: view === "content" || view === "content-brief" || view.startsWith("content-post:"),
|
|
2824
2902
|
onClick: () => navigate("content")
|
|
@@ -2827,7 +2905,7 @@ function buildNav(config, view, navigate) {
|
|
|
2827
2905
|
if (config.modules.blog) {
|
|
2828
2906
|
items.push({
|
|
2829
2907
|
id: "blog",
|
|
2830
|
-
label: config.labels.blog,
|
|
2908
|
+
label: config.labels.blog ?? t("marketing.nav.blog"),
|
|
2831
2909
|
icon: "Newspaper",
|
|
2832
2910
|
active: view === "blog" || view === "blog-new" || view.startsWith("blog-post:") || view === "blog-cat-new" || view.startsWith("blog-cat:"),
|
|
2833
2911
|
onClick: () => navigate("blog")
|
|
@@ -2835,13 +2913,14 @@ function buildNav(config, view, navigate) {
|
|
|
2835
2913
|
}
|
|
2836
2914
|
return items;
|
|
2837
2915
|
}
|
|
2838
|
-
function MarketingPage({ config, provider: provider2, store: store3, contentProvider, contentStore }) {
|
|
2916
|
+
function MarketingPage({ config, provider: provider2, store: store3, contentProvider, contentStore, cadenceSurface }) {
|
|
2839
2917
|
const t = useTranslation();
|
|
2840
2918
|
const { view, direction, navigate } = useModuleNavigation("/marketing", {
|
|
2841
2919
|
overview: 0,
|
|
2842
2920
|
channels: 0,
|
|
2843
2921
|
campaigns: 0,
|
|
2844
2922
|
funnel: 0,
|
|
2923
|
+
cadences: 0,
|
|
2845
2924
|
"landing-pages": 0,
|
|
2846
2925
|
content: 0,
|
|
2847
2926
|
blog: 0,
|
|
@@ -2860,7 +2939,7 @@ function MarketingPage({ config, provider: provider2, store: store3, contentProv
|
|
|
2860
2939
|
void store3.getState().refresh();
|
|
2861
2940
|
}, [store3]);
|
|
2862
2941
|
const isOverview = view === "overview";
|
|
2863
|
-
const nav = buildNav(config, view, navigate);
|
|
2942
|
+
const nav = buildNav(config, view, navigate, Boolean(cadenceSurface), t);
|
|
2864
2943
|
const blogPostEntity = React9.useMemo(() => buildBlogPostEntity(), []);
|
|
2865
2944
|
const blogCategoryEntity = React9.useMemo(() => buildBlogCategoryEntity(), []);
|
|
2866
2945
|
const quickActions = React9.useMemo(() => [
|
|
@@ -2871,15 +2950,16 @@ function MarketingPage({ config, provider: provider2, store: store3, contentProv
|
|
|
2871
2950
|
{ id: "channel-detail", render: ({ id }) => /* @__PURE__ */ jsx(ChannelDetailView, { channelId: id, onBack: () => navigate("channels") }) },
|
|
2872
2951
|
{ id: "campaigns", render: () => /* @__PURE__ */ jsx(CampaignsView, {}) },
|
|
2873
2952
|
{ id: "funnel", render: () => /* @__PURE__ */ jsx(FunnelView, {}) },
|
|
2953
|
+
{ id: "cadences", render: () => /* @__PURE__ */ jsx(CadencesView, { surface: cadenceSurface }) },
|
|
2874
2954
|
{ id: "landing-pages", render: () => /* @__PURE__ */ jsx(LandingPagesView, {}) },
|
|
2875
2955
|
{ id: "content", render: () => /* @__PURE__ */ jsx(ContentView, { onOpenPost: (id) => navigate(`content-post:${id}`) }) },
|
|
2876
2956
|
{ id: "content-post", render: ({ id }) => /* @__PURE__ */ jsx(PostPage, { postId: id, onBack: () => navigate("content") }) },
|
|
2877
2957
|
{ id: "content-brief", render: () => /* @__PURE__ */ jsx(PlanBriefView, { onBack: () => navigate("content") }) },
|
|
2878
2958
|
{ id: "blog", render: () => /* @__PURE__ */ jsx(BlogAdminView, { navigate }) },
|
|
2879
|
-
{ id: "blog-new", render: () => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogPostEntity, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
|
|
2880
|
-
{ id: "blog-post", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogPostEntity, editId: id, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
|
|
2881
|
-
{ id: "blog-cat-new", render: () => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
|
|
2882
|
-
{ id: "blog-cat", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, editId: id, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
|
|
2959
|
+
{ id: "blog-new", render: () => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogPostEntity, parentLabel: config.labels.blog ?? t("marketing.nav.blog"), onDone: () => navigate("blog") }) },
|
|
2960
|
+
{ id: "blog-post", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogPostEntity, editId: id, parentLabel: config.labels.blog ?? t("marketing.nav.blog"), onDone: () => navigate("blog") }) },
|
|
2961
|
+
{ id: "blog-cat-new", render: () => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, parentLabel: config.labels.blogCategories ?? t("marketing.nav.blogCategories"), onDone: () => navigate("blog") }) },
|
|
2962
|
+
{ id: "blog-cat", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, editId: id, parentLabel: config.labels.blogCategories ?? t("marketing.nav.blogCategories"), onDone: () => navigate("blog") }) },
|
|
2883
2963
|
{ id: "overview", render: () => /* @__PURE__ */ jsx(OverviewView, {}) }
|
|
2884
2964
|
], "overview");
|
|
2885
2965
|
return /* @__PURE__ */ jsx(MarketingContextProvider, { config, provider: provider2, store: store3, children: /* @__PURE__ */ jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsx(
|
|
@@ -3259,7 +3339,7 @@ var AGENCY = {
|
|
|
3259
3339
|
{ id: "referral", label: "Referral", icon: "Users", kind: "referral" },
|
|
3260
3340
|
{ id: "direct", label: "Direct", icon: "MousePointerClick", kind: "direct" }
|
|
3261
3341
|
],
|
|
3262
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
|
|
3342
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, blog: false, cadences: true, broadcasts: false, journeys: false }
|
|
3263
3343
|
};
|
|
3264
3344
|
var BEAUTY = {
|
|
3265
3345
|
conversion: {
|
|
@@ -3277,7 +3357,7 @@ var BEAUTY = {
|
|
|
3277
3357
|
{ id: "whatsapp", label: "WhatsApp", icon: "MessageCircle", kind: "social" },
|
|
3278
3358
|
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
3279
3359
|
],
|
|
3280
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, blog: false, broadcasts: false, journeys: false }
|
|
3360
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, blog: false, cadences: true, broadcasts: false, journeys: false }
|
|
3281
3361
|
};
|
|
3282
3362
|
var RESTO = {
|
|
3283
3363
|
conversion: {
|
|
@@ -3295,7 +3375,7 @@ var RESTO = {
|
|
|
3295
3375
|
{ id: "instagram", label: "Instagram", icon: "Instagram", kind: "social" },
|
|
3296
3376
|
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
3297
3377
|
],
|
|
3298
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
|
|
3378
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, blog: false, cadences: true, broadcasts: false, journeys: false }
|
|
3299
3379
|
};
|
|
3300
3380
|
var MARKETING_PRESETS = {
|
|
3301
3381
|
agency: AGENCY,
|
|
@@ -3401,6 +3481,8 @@ function seedCampaigns(cfg) {
|
|
|
3401
3481
|
name,
|
|
3402
3482
|
channelId: ch.id,
|
|
3403
3483
|
status,
|
|
3484
|
+
kind: "ad_spend",
|
|
3485
|
+
purpose: "promotion",
|
|
3404
3486
|
start,
|
|
3405
3487
|
end: status === "ended" ? new Date(baseDate - 2 * 864e5).toISOString() : void 0,
|
|
3406
3488
|
spend: draft ? 0 : Math.round(b.spend * share),
|
|
@@ -3536,7 +3618,11 @@ function createMockMarketingProvider(cfg) {
|
|
|
3536
3618
|
status: input.status,
|
|
3537
3619
|
start: input.start,
|
|
3538
3620
|
end: input.end,
|
|
3539
|
-
spend: input.spend
|
|
3621
|
+
spend: input.spend,
|
|
3622
|
+
kind: input.kind ?? existing.kind,
|
|
3623
|
+
purpose: input.purpose ?? existing.purpose,
|
|
3624
|
+
segmentId: input.segmentId,
|
|
3625
|
+
templateKey: input.templateKey
|
|
3540
3626
|
});
|
|
3541
3627
|
return existing;
|
|
3542
3628
|
}
|
|
@@ -3548,6 +3634,10 @@ function createMockMarketingProvider(cfg) {
|
|
|
3548
3634
|
name: input.name,
|
|
3549
3635
|
channelId: input.channelId,
|
|
3550
3636
|
status: input.status,
|
|
3637
|
+
kind: input.kind ?? "ad_spend",
|
|
3638
|
+
purpose: input.purpose ?? "promotion",
|
|
3639
|
+
segmentId: input.segmentId,
|
|
3640
|
+
templateKey: input.templateKey,
|
|
3551
3641
|
start: input.start,
|
|
3552
3642
|
end: input.end,
|
|
3553
3643
|
spend: input.spend,
|
|
@@ -3560,6 +3650,11 @@ function createMockMarketingProvider(cfg) {
|
|
|
3560
3650
|
campaigns.unshift(created);
|
|
3561
3651
|
return created;
|
|
3562
3652
|
},
|
|
3653
|
+
// Sem cluster não há recorte para apontar: a tela cai no público por regra
|
|
3654
|
+
// própria em vez de mostrar um seletor vazio.
|
|
3655
|
+
async listAudienceSegments() {
|
|
3656
|
+
return [];
|
|
3657
|
+
},
|
|
3563
3658
|
async deleteCampaign(id) {
|
|
3564
3659
|
const idx = campaigns.findIndex((c) => c.id === id);
|
|
3565
3660
|
if (idx >= 0) campaigns.splice(idx, 1);
|
|
@@ -3841,6 +3936,10 @@ function createSupabaseMarketingProvider(options) {
|
|
|
3841
3936
|
name: row.name,
|
|
3842
3937
|
channelId: row.channel_key,
|
|
3843
3938
|
status: row.status,
|
|
3939
|
+
kind: row.kind ?? "ad_spend",
|
|
3940
|
+
purpose: row.purpose ?? "promotion",
|
|
3941
|
+
segmentId: (row.audience_query ?? {}).segment_id ?? void 0,
|
|
3942
|
+
templateKey: row.template_key ?? void 0,
|
|
3844
3943
|
start: row.starts_at ?? row.created_at,
|
|
3845
3944
|
end: row.ends_at ?? void 0,
|
|
3846
3945
|
spend: Number(row.spend ?? 0),
|
|
@@ -3935,14 +4034,23 @@ function createSupabaseMarketingProvider(options) {
|
|
|
3935
4034
|
},
|
|
3936
4035
|
async saveCampaign(input) {
|
|
3937
4036
|
const tenantId = requireTenantId();
|
|
4037
|
+
const kind = input.kind ?? "ad_spend";
|
|
3938
4038
|
const payload = {
|
|
3939
4039
|
name: input.name,
|
|
3940
4040
|
channel_key: input.channelId,
|
|
3941
4041
|
status: input.status,
|
|
3942
4042
|
starts_at: input.start || null,
|
|
3943
4043
|
ends_at: input.end || null,
|
|
3944
|
-
spend: input.spend ?? 0
|
|
4044
|
+
spend: input.spend ?? 0,
|
|
4045
|
+
kind,
|
|
4046
|
+
purpose: input.purpose ?? "promotion"
|
|
3945
4047
|
};
|
|
4048
|
+
if (kind === "outbound") {
|
|
4049
|
+
payload.audience_query = input.segmentId ? { segment_id: input.segmentId } : {};
|
|
4050
|
+
payload.template_key = input.templateKey ?? null;
|
|
4051
|
+
payload.key = input.id ? void 0 : `camp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
|
4052
|
+
if (payload.key === void 0) delete payload.key;
|
|
4053
|
+
}
|
|
3946
4054
|
if (input.id) {
|
|
3947
4055
|
const { data: data2, error: error2 } = await getClient().from(T.campaigns).update(payload).eq("id", input.id).select("*").single();
|
|
3948
4056
|
if (error2) throw error2;
|
|
@@ -3954,6 +4062,18 @@ function createSupabaseMarketingProvider(options) {
|
|
|
3954
4062
|
cache.clear();
|
|
3955
4063
|
return campaignFromRow(data);
|
|
3956
4064
|
},
|
|
4065
|
+
/**
|
|
4066
|
+
* Os recortes do CRM. Leitura direta de `plg_crm_segments` — a mesma
|
|
4067
|
+
* licença que este plugin já usa para o catálogo de tags: quem é DONO da
|
|
4068
|
+
* tabela é o CRM, o Marketing só lê. Sem o CRM montado a tabela não
|
|
4069
|
+
* existe, e a resposta é uma lista vazia em vez de um erro na tela.
|
|
4070
|
+
*/
|
|
4071
|
+
async listAudienceSegments() {
|
|
4072
|
+
const tenantId = requireTenantId();
|
|
4073
|
+
const { data, error } = await getClient().from("plg_crm_segments").select("id, name, description").eq("tenant_id", tenantId).eq("is_active", true).order("sort_order");
|
|
4074
|
+
if (error) return [];
|
|
4075
|
+
return (data ?? []).map((r) => ({ id: r.id, name: r.name, description: r.description ?? void 0 }));
|
|
4076
|
+
},
|
|
3957
4077
|
async deleteCampaign(id) {
|
|
3958
4078
|
const { error } = await getClient().from(T.campaigns).delete().eq("id", id);
|
|
3959
4079
|
if (error) throw error;
|
|
@@ -6001,10 +6121,269 @@ DROP FUNCTION IF EXISTS public.crm_capture_page_load(text);
|
|
|
6001
6121
|
DROP FUNCTION IF EXISTS public.crm_capture_window_check(text, timestamp with time zone);
|
|
6002
6122
|
DROP FUNCTION IF EXISTS public.trg_crm_capture_page_key();
|
|
6003
6123
|
`;
|
|
6124
|
+
var MIGRATION_004_A_CAMPANHA_APONTA_PARA_UM_RECORTE = `-- 004_a_campanha_aponta_para_um_recorte.sql
|
|
6125
|
+
--
|
|
6126
|
+
-- A CAMPANHA N\xC3O CONSEGUIA APONTAR PARA UM SEGMENTO, QUE \xC9 PARA O QUE O
|
|
6127
|
+
-- SEGMENTO FOI FEITO.
|
|
6128
|
+
--
|
|
6129
|
+
-- O coment\xE1rio da 004 do CRM diz textualmente por que \`plg_crm_segments\`
|
|
6130
|
+
-- existe: "a campanha aponta para ele em vez de carregar a pr\xF3pria c\xF3pia do
|
|
6131
|
+
-- filtro". S\xF3 que \`marketing_resolve_audience\` nunca aprendeu a ler um
|
|
6132
|
+
-- segmento \u2014 ela resolve \`audience_query = {source_view, where[]}\`, a c\xF3pia do
|
|
6133
|
+
-- filtro. Ent\xE3o havia duas verdades sobre quem \xE9 "cliente ativo": a da tela de
|
|
6134
|
+
-- segmentos e a da campanha, e elas divergiam no dia em que algu\xE9m corrigisse
|
|
6135
|
+
-- uma.
|
|
6136
|
+
--
|
|
6137
|
+
-- E O VOCABUL\xC1RIO DELA \xC9 MENOR, que \xE9 o problema mais grave dos dois. A
|
|
6138
|
+
-- campanha aceita seis operadores de compara\xE7\xE3o e mais nada: sem \`within_days\`,
|
|
6139
|
+
-- sem \`has_tag\`, sem \`is_null\`. Copiar a regra do segmento para dentro da
|
|
6140
|
+
-- campanha, al\xE9m de duplicar, QUEBRA \u2014 "comprou nos \xFAltimos 30 dias" n\xE3o tem
|
|
6141
|
+
-- como ser expresso, e "anivers\xE1rio este m\xEAs" tamb\xE9m n\xE3o.
|
|
6142
|
+
--
|
|
6143
|
+
-- \u2500\u2500 as duas decis\xF5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6144
|
+
--
|
|
6145
|
+
-- 1. A CAMPANHA GUARDA UM PONTEIRO, N\xC3O UMA C\xD3PIA. \`{"segment_id": "..."}\` no
|
|
6146
|
+
-- \`audience_query\`. Editar o recorte muda para quem a campanha vai \u2014 que \xE9
|
|
6147
|
+
-- a expectativa de quem acabou de editar o recorte. Quem quiser a foto
|
|
6148
|
+
-- congelada usa um segmento do tipo \`list\`, que \xE9 exatamente o que ele \xE9.
|
|
6149
|
+
--
|
|
6150
|
+
-- 2. O CONJUNTO DE OPERADORES PASSA A SER O MESMO DO CRM, ponteiro ou n\xE3o.
|
|
6151
|
+
-- Uma campanha que aponta direto (sem segmento) ganha \`within_days\`,
|
|
6152
|
+
-- \`has_tag\` e companhia pela mesma tabela de CASE. Duas linguagens de
|
|
6153
|
+
-- filtro no mesmo produto \xE9 como se chega a duas respostas diferentes para
|
|
6154
|
+
-- a mesma pergunta.
|
|
6155
|
+
--
|
|
6156
|
+
-- O QUE N\xC3O MUDA, E \xC9 O MAIS IMPORTANTE: o filtro de consentimento continua na
|
|
6157
|
+
-- sa\xEDda. \`marketing_has_consent(tenant, pessoa, purpose)\` \xE9 a \xFAltima cl\xE1usula,
|
|
6158
|
+
-- sempre \u2014 um p\xFAblico resolvido por segmento n\xE3o atravessa o opt-out s\xF3 porque
|
|
6159
|
+
-- veio do CRM. A \`purpose\` da campanha \xE9 o que d\xE1 sentido a ele, e \xE9 por isso
|
|
6160
|
+
-- que "criar campanha com uma finalidade espec\xEDfica" \xE9 uma funcionalidade e
|
|
6161
|
+
-- n\xE3o um campo de texto: a finalidade DECIDE quem pode receber.
|
|
6162
|
+
--
|
|
6163
|
+
-- Sem depend\xEAncia dura de plugin: se \`plg_crm_segments\` n\xE3o existe neste app,
|
|
6164
|
+
-- um \`segment_id\` \xE9 recusado com mensagem clara em vez de estourar 42P01.
|
|
6165
|
+
|
|
6166
|
+
SET check_function_bodies = false;
|
|
6167
|
+
|
|
6168
|
+
CREATE OR REPLACE FUNCTION public.marketing_resolve_audience(p_campaign_id uuid)
|
|
6169
|
+
RETURNS TABLE(person_id uuid)
|
|
6170
|
+
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
|
6171
|
+
SET search_path TO ''
|
|
6172
|
+
AS $_$
|
|
6173
|
+
DECLARE
|
|
6174
|
+
c public.plg_marketing_campaigns%ROWTYPE;
|
|
6175
|
+
v_source text;
|
|
6176
|
+
v_rules jsonb;
|
|
6177
|
+
v_where text := '';
|
|
6178
|
+
v_clause jsonb;
|
|
6179
|
+
v_col text;
|
|
6180
|
+
v_op text;
|
|
6181
|
+
v_kind char;
|
|
6182
|
+
v_seg_id uuid;
|
|
6183
|
+
v_seg record;
|
|
6184
|
+
BEGIN
|
|
6185
|
+
SELECT * INTO c FROM public.plg_marketing_campaigns WHERE id = p_campaign_id;
|
|
6186
|
+
IF c.id IS NULL THEN
|
|
6187
|
+
RAISE EXCEPTION 'marketing_resolve_audience: campaign % not found', p_campaign_id USING ERRCODE = 'P0002';
|
|
6188
|
+
END IF;
|
|
6189
|
+
|
|
6190
|
+
-- \u2500\u2500 o ponteiro \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6191
|
+
IF c.audience_query ? 'segment_id' THEN
|
|
6192
|
+
IF to_regclass('public.plg_crm_segments') IS NULL THEN
|
|
6193
|
+
RAISE EXCEPTION 'marketing_resolve_audience: campanha aponta para um segmento, mas o CRM n\xE3o est\xE1 instalado neste app'
|
|
6194
|
+
USING ERRCODE = '42P01';
|
|
6195
|
+
END IF;
|
|
6196
|
+
BEGIN
|
|
6197
|
+
v_seg_id := (c.audience_query ->> 'segment_id')::uuid;
|
|
6198
|
+
EXCEPTION WHEN OTHERS THEN
|
|
6199
|
+
RAISE EXCEPTION 'marketing_resolve_audience: segment_id inv\xE1lido' USING ERRCODE = '22023';
|
|
6200
|
+
END;
|
|
6201
|
+
|
|
6202
|
+
-- Mesmo tenant, sempre. Um p\xFAblico que atravessasse inquilino seria a pior
|
|
6203
|
+
-- falha poss\xEDvel deste arquivo.
|
|
6204
|
+
SELECT s.source_view, s.rules, s.kind INTO v_seg
|
|
6205
|
+
FROM public.plg_crm_segments s
|
|
6206
|
+
WHERE s.id = v_seg_id AND s.tenant_id = c.tenant_id;
|
|
6207
|
+
IF NOT FOUND THEN
|
|
6208
|
+
RAISE EXCEPTION 'marketing_resolve_audience: segmento % n\xE3o \xE9 deste tenant', v_seg_id USING ERRCODE = 'P0002';
|
|
6209
|
+
END IF;
|
|
6210
|
+
|
|
6211
|
+
-- A lista escolhida a dedo \xC9 a resposta \u2014 e mesmo ela passa pelo consentimento.
|
|
6212
|
+
IF v_seg.kind = 'list' THEN
|
|
6213
|
+
RETURN QUERY
|
|
6214
|
+
SELECT m.person_id FROM public.plg_crm_segment_members m
|
|
6215
|
+
WHERE m.segment_id = v_seg_id AND m.tenant_id = c.tenant_id
|
|
6216
|
+
AND public.marketing_has_consent(c.tenant_id, m.person_id, c.purpose);
|
|
6217
|
+
RETURN;
|
|
6218
|
+
END IF;
|
|
6219
|
+
|
|
6220
|
+
v_source := coalesce(v_seg.source_view, 'people');
|
|
6221
|
+
v_rules := coalesce(v_seg.rules -> 'where', '[]'::jsonb);
|
|
6222
|
+
ELSE
|
|
6223
|
+
v_source := coalesce(c.audience_query ->> 'source_view', 'people');
|
|
6224
|
+
v_rules := coalesce(c.audience_query -> 'where', '[]'::jsonb);
|
|
6225
|
+
END IF;
|
|
6226
|
+
|
|
6227
|
+
IF v_source !~ '^[a-z_][a-z0-9_]*$' THEN
|
|
6228
|
+
RAISE EXCEPTION 'marketing_resolve_audience: % is not a bare relation name', v_source USING ERRCODE = '22023';
|
|
6229
|
+
END IF;
|
|
6230
|
+
SELECT cl.relkind INTO v_kind FROM pg_catalog.pg_class cl
|
|
6231
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = cl.relnamespace
|
|
6232
|
+
WHERE n.nspname = 'public' AND cl.relname = v_source;
|
|
6233
|
+
IF v_kind IS NULL THEN
|
|
6234
|
+
RAISE EXCEPTION 'marketing_resolve_audience: public.% does not exist', v_source USING ERRCODE = '42P01';
|
|
6235
|
+
END IF;
|
|
6236
|
+
|
|
6237
|
+
v_where := format('tenant_id = %L', c.tenant_id);
|
|
6238
|
+
|
|
6239
|
+
-- Unidade da campanha estreita o p\xFAblico, quando a leitura tem a coluna.
|
|
6240
|
+
IF c.unit_id IS NOT NULL AND EXISTS (
|
|
6241
|
+
SELECT 1 FROM information_schema.columns
|
|
6242
|
+
WHERE table_schema = 'public' AND table_name = v_source AND column_name = 'unit_id'
|
|
6243
|
+
) THEN
|
|
6244
|
+
v_where := v_where || format(' AND unit_id = %L', c.unit_id);
|
|
6245
|
+
END IF;
|
|
6246
|
+
|
|
6247
|
+
-- \u2500\u2500 o mesmo CASE do CRM, operador por operador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6248
|
+
FOR v_clause IN SELECT * FROM jsonb_array_elements(v_rules) LOOP
|
|
6249
|
+
v_col := v_clause ->> 'column';
|
|
6250
|
+
v_op := coalesce(v_clause ->> 'op', '=');
|
|
6251
|
+
|
|
6252
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
6253
|
+
WHERE table_schema = 'public' AND table_name = v_source AND column_name = v_col) THEN
|
|
6254
|
+
RAISE EXCEPTION 'marketing_resolve_audience: public.% has no column %', v_source, v_col USING ERRCODE = '42703';
|
|
6255
|
+
END IF;
|
|
6256
|
+
|
|
6257
|
+
v_where := v_where || CASE
|
|
6258
|
+
WHEN v_op IN ('=', '<>', '>', '<', '>=', '<=') THEN
|
|
6259
|
+
format(' AND %I %s %L', v_col,
|
|
6260
|
+
CASE v_op WHEN '<>' THEN '<>' WHEN '>' THEN '>' WHEN '<' THEN '<'
|
|
6261
|
+
WHEN '>=' THEN '>=' WHEN '<=' THEN '<=' ELSE '=' END,
|
|
6262
|
+
v_clause ->> 'value')
|
|
6263
|
+
|
|
6264
|
+
WHEN v_op = 'ilike' THEN
|
|
6265
|
+
format(' AND %I ILIKE %L', v_col, '%' || coalesce(v_clause ->> 'value', '') || '%')
|
|
6266
|
+
|
|
6267
|
+
WHEN v_op IN ('in', 'not_in') THEN (
|
|
6268
|
+
SELECT format(' AND %I %s (%s)', v_col,
|
|
6269
|
+
CASE WHEN v_op = 'not_in' THEN 'NOT IN' ELSE 'IN' END,
|
|
6270
|
+
coalesce(string_agg(quote_literal(e), ','), 'NULL'))
|
|
6271
|
+
FROM jsonb_array_elements_text(coalesce(v_clause -> 'value', '[]'::jsonb)) AS e)
|
|
6272
|
+
|
|
6273
|
+
WHEN v_op = 'is_null' THEN format(' AND %I IS NULL', v_col)
|
|
6274
|
+
WHEN v_op = 'not_null' THEN format(' AND %I IS NOT NULL', v_col)
|
|
6275
|
+
|
|
6276
|
+
WHEN v_op = 'has_tag' THEN
|
|
6277
|
+
format(' AND %L = ANY(%I)', v_clause ->> 'value', v_col)
|
|
6278
|
+
|
|
6279
|
+
WHEN v_op IN ('within_days', 'older_than_days') THEN (
|
|
6280
|
+
SELECT format(' AND %I %s now() - make_interval(days => %s)', v_col,
|
|
6281
|
+
CASE WHEN v_op = 'within_days' THEN '>=' ELSE '<' END,
|
|
6282
|
+
(coalesce(v_clause ->> 'value', '0'))::integer))
|
|
6283
|
+
|
|
6284
|
+
WHEN v_op = 'birthday_within_days' THEN (
|
|
6285
|
+
SELECT format(
|
|
6286
|
+
' AND %I IS NOT NULL AND ((EXTRACT(doy FROM %I::date)::int - EXTRACT(doy FROM now())::int + 366) %% 366) <= %s',
|
|
6287
|
+
v_col, v_col, (coalesce(v_clause ->> 'value', '0'))::integer))
|
|
6288
|
+
|
|
6289
|
+
ELSE
|
|
6290
|
+
-- Operador desconhecido ALARGA o p\xFAblico se for ignorado \u2014 e um p\xFAblico
|
|
6291
|
+
-- alargado em sil\xEAncio \xE9 a mensagem indo para quem n\xE3o devia.
|
|
6292
|
+
NULL
|
|
6293
|
+
END;
|
|
6294
|
+
|
|
6295
|
+
IF v_where IS NULL THEN
|
|
6296
|
+
RAISE EXCEPTION 'marketing_resolve_audience: operador % n\xE3o \xE9 reconhecido', v_op USING ERRCODE = '22023';
|
|
6297
|
+
END IF;
|
|
6298
|
+
END LOOP;
|
|
6299
|
+
|
|
6300
|
+
-- O consentimento \xE9 a \xDALTIMA cl\xE1usula, e continua sendo, venha o p\xFAblico de
|
|
6301
|
+
-- onde vier.
|
|
6302
|
+
RETURN QUERY EXECUTE format(
|
|
6303
|
+
'SELECT s.id FROM public.%I s WHERE %s AND public.marketing_has_consent(%L, s.id, %L)',
|
|
6304
|
+
v_source, v_where, c.tenant_id, c.purpose);
|
|
6305
|
+
END $_$;
|
|
6306
|
+
|
|
6307
|
+
COMMENT ON FUNCTION public.marketing_resolve_audience(uuid) IS
|
|
6308
|
+
'Quem a campanha alcan\xE7a. Aponta para um segmento do CRM (segment_id) ou carrega a pr\xF3pria regra; mesmo conjunto de operadores do CRM; consentimento sempre na sa\xEDda (004).';
|
|
6309
|
+
`;
|
|
6310
|
+
var MIGRATION_005_A_ATRIBUICAO_DO_LEAD_ATRAVESSA_A_ORIGEM_ATE_O_CANAL = `-- ---------------------------------------------------------------------------
|
|
6311
|
+
-- 005_a_atribuicao_do_lead_atravessa_a_origem_ate_o_canal.sql \u2014 os tr\xEAs n\xEDveis
|
|
6312
|
+
-- de atribui\xE7\xE3o num lugar s\xF3, e o conserto de uma tela que contava zero.
|
|
6313
|
+
--
|
|
6314
|
+
-- \u2500\u2500 por que este arquivo est\xE1 no marketing e n\xE3o no CRM \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6315
|
+
--
|
|
6316
|
+
-- A view junta tr\xEAs tabelas de duas casas: \`plg_crm_lead_sources\` (CRM) com
|
|
6317
|
+
-- \`plg_marketing_channels\` e \`plg_marketing_campaigns\` (aqui). Na ordem da
|
|
6318
|
+
-- chain o CRM roda ANTES do marketing, ent\xE3o do lado de l\xE1 as tabelas deste
|
|
6319
|
+
-- plugin ainda n\xE3o existem. Do lado de c\xE1 as do CRM j\xE1 existem.
|
|
6320
|
+
--
|
|
6321
|
+
-- A regra que isso ensina: quem cria uma view que atravessa plugins \xE9 o \xDALTIMO
|
|
6322
|
+
-- dos seus dependentes. \`crm/038\` tentou o contr\xE1rio e o \`db-from-zero\` pegou
|
|
6323
|
+
-- na primeira passada.
|
|
6324
|
+
--
|
|
6325
|
+
-- \u2500\u2500 as tr\xEAs perguntas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6326
|
+
--
|
|
6327
|
+
-- CANAL (medium) COMO a gente se encontrou. "Evento presencial"
|
|
6328
|
+
-- ORIGEM (source) ONDE, especificamente. "Sal\xE3o Abrasel"
|
|
6329
|
+
-- CAMPANHA QUAL esfor\xE7o, com custo. "ABRASEL 2026"
|
|
6330
|
+
--
|
|
6331
|
+
-- Salesforce separa \`LeadSource\` de \`Campaign\`; HubSpot separa "Original
|
|
6332
|
+
-- source" do drill-down; Odoo e Mautic usam o trio UTM. Nenhum junta os tr\xEAs
|
|
6333
|
+
-- num campo s\xF3: "quanto custou este lead" s\xF3 existe na campanha, e "que tipo
|
|
6334
|
+
-- de esfor\xE7o traz gente melhor" s\xF3 existe no canal.
|
|
6335
|
+
--
|
|
6336
|
+
-- \u2500\u2500 o defeito que isto conserta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6337
|
+
--
|
|
6338
|
+
-- \`v_marketing_attribution\` lia \`metadata.sourceId\` e chamava aquilo de
|
|
6339
|
+
-- \`channel_raw\` \u2014 a ORIGEM ocupando o lugar do CANAL. Com isso a tela de
|
|
6340
|
+
-- Canais contava zero para todos, porque nenhum \`channel_key\` jamais bate com
|
|
6341
|
+
-- um uuid de origem. Esta view atravessa a origem at\xE9 o canal, que \xE9 o salto
|
|
6342
|
+
-- que faltava. A antiga fica: outras telas dependem da forma dela.
|
|
6343
|
+
-- ---------------------------------------------------------------------------
|
|
6344
|
+
|
|
6345
|
+
DROP VIEW IF EXISTS public.v_marketing_lead_attribution;
|
|
6346
|
+
CREATE VIEW public.v_marketing_lead_attribution
|
|
6347
|
+
WITH (security_invoker = true) AS
|
|
6348
|
+
SELECT p.id AS lead_id,
|
|
6349
|
+
p.tenant_id,
|
|
6350
|
+
p.created_at,
|
|
6351
|
+
s.id AS source_id,
|
|
6352
|
+
s.name AS source_name,
|
|
6353
|
+
-- O canal vem da ORIGEM; quando ela n\xE3o declara, o lead n\xE3o \xE9
|
|
6354
|
+
-- atribu\xEDdo a canal nenhum. Nulo \xE9 honesto: for\xE7ar 'direct' faria a
|
|
6355
|
+
-- tela afirmar uma proced\xEAncia que ningu\xE9m mediu.
|
|
6356
|
+
s.channel_key,
|
|
6357
|
+
ch.label AS channel_label,
|
|
6358
|
+
ch.kind AS channel_kind,
|
|
6359
|
+
(p.metadata ->> 'campaignId') AS campaign_id,
|
|
6360
|
+
mc.name AS campaign_name,
|
|
6361
|
+
mc.spend AS campaign_spend
|
|
6362
|
+
FROM public.people p
|
|
6363
|
+
LEFT JOIN public.plg_crm_lead_sources s
|
|
6364
|
+
ON s.tenant_id = p.tenant_id
|
|
6365
|
+
AND s.id::text = (p.metadata ->> 'sourceId')
|
|
6366
|
+
LEFT JOIN public.plg_marketing_channels ch
|
|
6367
|
+
ON ch.tenant_id = p.tenant_id
|
|
6368
|
+
AND ch.channel_key = s.channel_key
|
|
6369
|
+
LEFT JOIN public.plg_marketing_campaigns mc
|
|
6370
|
+
ON mc.tenant_id = p.tenant_id
|
|
6371
|
+
AND mc.id::text = (p.metadata ->> 'campaignId')
|
|
6372
|
+
WHERE p.kind = 'lead'
|
|
6373
|
+
AND p.merged_into_id IS NULL
|
|
6374
|
+
AND p.tenant_id IN (SELECT public.user_tenant_ids());
|
|
6375
|
+
|
|
6376
|
+
GRANT SELECT ON public.v_marketing_lead_attribution TO authenticated;
|
|
6377
|
+
|
|
6378
|
+
COMMENT ON VIEW public.v_marketing_lead_attribution IS
|
|
6379
|
+
'Cada lead com seus tr\xEAs n\xEDveis: canal (COMO), origem (ONDE) e campanha (QUAL esfor\xE7o, com custo). A origem resolve o canal \u2014 o lead n\xE3o o carrega (marketing/005).';
|
|
6380
|
+
`;
|
|
6004
6381
|
var MIGRATIONS = [
|
|
6005
6382
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
6006
6383
|
{ id: "001_the_landing_page_names_the_site", sql: MIGRATION_001_THE_LANDING_PAGE_NAMES_THE_SITE },
|
|
6007
|
-
{ id: "003_a_porta_publica_e_a_pagina", sql: MIGRATION_003_A_PORTA_PUBLICA_E_A_PAGINA }
|
|
6384
|
+
{ id: "003_a_porta_publica_e_a_pagina", sql: MIGRATION_003_A_PORTA_PUBLICA_E_A_PAGINA },
|
|
6385
|
+
{ id: "004_a_campanha_aponta_para_um_recorte", sql: MIGRATION_004_A_CAMPANHA_APONTA_PARA_UM_RECORTE },
|
|
6386
|
+
{ id: "005_a_atribuicao_do_lead_atravessa_a_origem_ate_o_canal", sql: MIGRATION_005_A_ATRIBUICAO_DO_LEAD_ATRAVESSA_A_ORIGEM_ATE_O_CANAL }
|
|
6008
6387
|
];
|
|
6009
6388
|
|
|
6010
6389
|
// src/integrations/ranklayer/settings.ts
|
|
@@ -6224,6 +6603,37 @@ var ranklayerConnectorDef = {
|
|
|
6224
6603
|
|
|
6225
6604
|
// src/locales/en.ts
|
|
6226
6605
|
var en = {
|
|
6606
|
+
"marketing.composer.kind": "Campaign type",
|
|
6607
|
+
"marketing.composer.kindSpend": "Ad spend",
|
|
6608
|
+
"marketing.composer.kindOutbound": "Outbound",
|
|
6609
|
+
"marketing.composer.kindSpendHint": "Records what was spent on a channel. Sends nothing \u2014 it is what makes cost per acquisition exist.",
|
|
6610
|
+
"marketing.composer.kindOutboundHint": "Sends a message to a segment of people. Runs on its own and respects each recipient's opt-in.",
|
|
6611
|
+
"marketing.composer.audience": "Who it reaches",
|
|
6612
|
+
"marketing.composer.audiencePlaceholder": "Pick a segment",
|
|
6613
|
+
"marketing.composer.noSegments": "No active segment. Create one in Settings \u2192 CRM \u2192 Segments and it shows up here.",
|
|
6614
|
+
"marketing.composer.purpose": "Purpose",
|
|
6615
|
+
"marketing.composer.template": "Message template",
|
|
6616
|
+
"marketing.composer.templateHint": "The template key in Communication. It defines the text that goes out.",
|
|
6617
|
+
"marketing.purpose.promotion": "Promotion",
|
|
6618
|
+
"marketing.purpose.newsletter": "Newsletter",
|
|
6619
|
+
"marketing.purpose.survey": "Survey",
|
|
6620
|
+
"marketing.purpose.transactional": "Notice about something they already have",
|
|
6621
|
+
"marketing.purpose.promotion.hint": "Only reaches people who opted in to promotions.",
|
|
6622
|
+
"marketing.purpose.newsletter.hint": "Only reaches people who opted in to the newsletter.",
|
|
6623
|
+
"marketing.purpose.survey.hint": "Only reaches people who opted in to surveys.",
|
|
6624
|
+
"marketing.purpose.transactional.hint": "Reaches everyone: talking about something they bought is not marketing. Still respects the channel block.",
|
|
6625
|
+
"marketing.nav.blogCategories": "Categories",
|
|
6626
|
+
"marketing.nav.overview": "Overview",
|
|
6627
|
+
"marketing.nav.channels": "Channels",
|
|
6628
|
+
"marketing.nav.campaigns": "Campaigns",
|
|
6629
|
+
"marketing.nav.funnel": "Funnel",
|
|
6630
|
+
"marketing.nav.landingPages": "Landing pages",
|
|
6631
|
+
"marketing.nav.content": "Content",
|
|
6632
|
+
"marketing.nav.blog": "Blog",
|
|
6633
|
+
"marketing.cadences.title": "Cadences",
|
|
6634
|
+
"marketing.cadences.subtitle": "The sequence: what goes out on day 0, day 2 and day 5 \u2014 and what takes somebody out of it",
|
|
6635
|
+
"marketing.cadences.missingTitle": "Cadences are not mounted in this app",
|
|
6636
|
+
"marketing.cadences.missingHint": "The screen comes from the Automations plugin. Install it and pass `cadenceSurface` to the Marketing plugin to plan the sequence here.",
|
|
6227
6637
|
// metrics
|
|
6228
6638
|
"marketing.metric.conversionRate": "Conversion rate",
|
|
6229
6639
|
"marketing.metric.cpa": "Cost per acquisition",
|
|
@@ -6469,6 +6879,37 @@ var en = {
|
|
|
6469
6879
|
|
|
6470
6880
|
// src/locales/pt-BR.ts
|
|
6471
6881
|
var ptBR = {
|
|
6882
|
+
"marketing.composer.kind": "Tipo de campanha",
|
|
6883
|
+
"marketing.composer.kindSpend": "Investimento",
|
|
6884
|
+
"marketing.composer.kindOutbound": "Disparo",
|
|
6885
|
+
"marketing.composer.kindSpendHint": "Registra quanto foi gasto num canal. N\xE3o envia nada \u2014 \xE9 o que faz o custo por aquisi\xE7\xE3o existir.",
|
|
6886
|
+
"marketing.composer.kindOutboundHint": "Manda uma mensagem para um recorte de gente. Roda sozinha e respeita o opt-in de quem for receber.",
|
|
6887
|
+
"marketing.composer.audience": "Para quem",
|
|
6888
|
+
"marketing.composer.audiencePlaceholder": "Escolha um segmento",
|
|
6889
|
+
"marketing.composer.noSegments": "Nenhum segmento ativo. Crie um em Configura\xE7\xF5es \u2192 CRM \u2192 Segmentos e ele aparece aqui.",
|
|
6890
|
+
"marketing.composer.purpose": "Finalidade",
|
|
6891
|
+
"marketing.composer.template": "Modelo de mensagem",
|
|
6892
|
+
"marketing.composer.templateHint": "A chave do modelo em Comunica\xE7\xE3o. \xC9 ele que define o texto que sai.",
|
|
6893
|
+
"marketing.purpose.promotion": "Promo\xE7\xE3o",
|
|
6894
|
+
"marketing.purpose.newsletter": "Novidades",
|
|
6895
|
+
"marketing.purpose.survey": "Pesquisa",
|
|
6896
|
+
"marketing.purpose.transactional": "Aviso sobre algo que a pessoa j\xE1 tem",
|
|
6897
|
+
"marketing.purpose.promotion.hint": "S\xF3 alcan\xE7a quem aceitou receber promo\xE7\xE3o.",
|
|
6898
|
+
"marketing.purpose.newsletter.hint": "S\xF3 alcan\xE7a quem aceitou receber novidades.",
|
|
6899
|
+
"marketing.purpose.survey.hint": "S\xF3 alcan\xE7a quem aceitou responder pesquisas.",
|
|
6900
|
+
"marketing.purpose.transactional.hint": "Alcan\xE7a todo mundo: falar de algo que a pessoa j\xE1 comprou n\xE3o \xE9 marketing. Ainda respeita o bloqueio do canal.",
|
|
6901
|
+
"marketing.nav.blogCategories": "Categorias",
|
|
6902
|
+
"marketing.nav.overview": "Vis\xE3o geral",
|
|
6903
|
+
"marketing.nav.channels": "Canais",
|
|
6904
|
+
"marketing.nav.campaigns": "Campanhas",
|
|
6905
|
+
"marketing.nav.funnel": "Funil",
|
|
6906
|
+
"marketing.nav.landingPages": "P\xE1ginas de captura",
|
|
6907
|
+
"marketing.nav.content": "Conte\xFAdo",
|
|
6908
|
+
"marketing.nav.blog": "Blog",
|
|
6909
|
+
"marketing.cadences.title": "Cad\xEAncias",
|
|
6910
|
+
"marketing.cadences.subtitle": "A r\xE9gua: o que sai no dia 0, no dia 2 e no dia 5 \u2014 e o que faz algu\xE9m sair dela",
|
|
6911
|
+
"marketing.cadences.missingTitle": "Cad\xEAncias n\xE3o est\xE3o montadas neste app",
|
|
6912
|
+
"marketing.cadences.missingHint": "A tela vem do plugin de Automa\xE7\xF5es. Instale-o e passe `cadenceSurface` ao plugin de Marketing para planejar a r\xE9gua por aqui.",
|
|
6472
6913
|
// metrics
|
|
6473
6914
|
"marketing.metric.conversionRate": "Taxa de convers\xE3o",
|
|
6474
6915
|
"marketing.metric.cpa": "Custo por aquisi\xE7\xE3o",
|
|
@@ -6721,16 +7162,13 @@ var marketingLocales = {
|
|
|
6721
7162
|
// src/index.ts
|
|
6722
7163
|
var DEFAULT_LABELS = {
|
|
6723
7164
|
pageTitle: "Marketing",
|
|
6724
|
-
pageSubtitle: "Acquisition & conversion performance"
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
campaigns: "Campaigns",
|
|
6728
|
-
funnel: "Funnel",
|
|
7165
|
+
pageSubtitle: "Acquisition & conversion performance"
|
|
7166
|
+
};
|
|
7167
|
+
var FEATURE_FALLBACK = {
|
|
6729
7168
|
landingPages: "Landing pages",
|
|
6730
7169
|
content: "Content",
|
|
6731
7170
|
blog: "Blog",
|
|
6732
|
-
|
|
6733
|
-
settings: "Settings"
|
|
7171
|
+
cadences: "Cadences"
|
|
6734
7172
|
};
|
|
6735
7173
|
function resolveConfig(options) {
|
|
6736
7174
|
const domain = options?.domain ?? "agency";
|
|
@@ -6771,7 +7209,7 @@ function createMarketingPlugin(options) {
|
|
|
6771
7209
|
const PageComponent = () => React9.createElement(
|
|
6772
7210
|
MarketingContextProvider,
|
|
6773
7211
|
{ config, provider: provider2, store: store3 },
|
|
6774
|
-
React9.createElement(MarketingPage, { config, provider: provider2, store: store3, contentProvider, contentStore })
|
|
7212
|
+
React9.createElement(MarketingPage, { config, provider: provider2, store: store3, contentProvider, contentStore, cadenceSurface: options?.cadenceSurface })
|
|
6775
7213
|
);
|
|
6776
7214
|
PageComponent.displayName = "MarketingPage";
|
|
6777
7215
|
const marketingRegistries = [
|
|
@@ -6806,9 +7244,10 @@ function createMarketingPlugin(options) {
|
|
|
6806
7244
|
dependencies: [],
|
|
6807
7245
|
declaredFeatures: [
|
|
6808
7246
|
{ id: "marketing", label: config.labels.pageTitle, group: "Engage" },
|
|
6809
|
-
...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : [],
|
|
6810
|
-
...config.modules.
|
|
6811
|
-
...config.modules.
|
|
7247
|
+
...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages ?? FEATURE_FALLBACK.landingPages, group: "Engage" }] : [],
|
|
7248
|
+
...config.modules.cadences && options?.cadenceSurface ? [{ id: "marketing.cadences", label: config.labels.cadences ?? FEATURE_FALLBACK.cadences, group: "Engage" }] : [],
|
|
7249
|
+
...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content ?? FEATURE_FALLBACK.content, group: "Engage" }] : [],
|
|
7250
|
+
...config.modules.blog ? [{ id: "marketing.blog", label: config.labels.blog ?? FEATURE_FALLBACK.blog, group: "Engage" }] : []
|
|
6812
7251
|
],
|
|
6813
7252
|
declaredLimits: [
|
|
6814
7253
|
...config.modules.campaigns ? [{ key: "campaigns_active", label: "Active campaigns", table: T.campaigns }] : [],
|