@fayz-ai/plugin-marketing 0.2.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +2 -0
  2. package/dist/MarketingContext.d.ts +1 -0
  3. package/dist/MarketingContext.d.ts.map +1 -1
  4. package/dist/MarketingPage.d.ts +5 -1
  5. package/dist/MarketingPage.d.ts.map +1 -1
  6. package/dist/components/icons.d.ts +4 -0
  7. package/dist/components/icons.d.ts.map +1 -1
  8. package/dist/data/contentMedia.d.ts +3 -0
  9. package/dist/data/contentMedia.d.ts.map +1 -0
  10. package/dist/data/contentMock.d.ts +3 -0
  11. package/dist/data/contentMock.d.ts.map +1 -0
  12. package/dist/data/contentSupabase.d.ts +3 -0
  13. package/dist/data/contentSupabase.d.ts.map +1 -0
  14. package/dist/data/contentTypes.d.ts +111 -0
  15. package/dist/data/contentTypes.d.ts.map +1 -0
  16. package/dist/data/tables.d.ts +6 -0
  17. package/dist/data/tables.d.ts.map +1 -0
  18. package/dist/index.cjs +2813 -129
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2815 -133
  23. package/dist/index.js.map +1 -1
  24. package/dist/locales/en.d.ts.map +1 -1
  25. package/dist/locales/pt-BR.d.ts.map +1 -1
  26. package/dist/migrations/index.d.ts +9 -0
  27. package/dist/migrations/index.d.ts.map +1 -0
  28. package/dist/presets.d.ts +2 -0
  29. package/dist/presets.d.ts.map +1 -1
  30. package/dist/schema/index.d.ts +717 -0
  31. package/dist/schema/index.d.ts.map +1 -0
  32. package/dist/schema.cjs +65 -0
  33. package/dist/schema.cjs.map +1 -0
  34. package/dist/schema.js +61 -0
  35. package/dist/schema.js.map +1 -0
  36. package/dist/views/content/ContentPlannerContext.d.ts +13 -0
  37. package/dist/views/content/ContentPlannerContext.d.ts.map +1 -0
  38. package/dist/views/content/ContentView.d.ts +5 -0
  39. package/dist/views/content/ContentView.d.ts.map +1 -0
  40. package/dist/views/content/PlanBriefView.d.ts +12 -0
  41. package/dist/views/content/PlanBriefView.d.ts.map +1 -0
  42. package/dist/views/content/PostPage.d.ts +6 -0
  43. package/dist/views/content/PostPage.d.ts.map +1 -0
  44. package/dist/views/content/PostSocialPreview.d.ts +11 -0
  45. package/dist/views/content/PostSocialPreview.d.ts.map +1 -0
  46. package/dist/views/content/RecordingChecklist.d.ts +6 -0
  47. package/dist/views/content/RecordingChecklist.d.ts.map +1 -0
  48. package/dist/views/content/contentBits.d.ts +28 -0
  49. package/dist/views/content/contentBits.d.ts.map +1 -0
  50. package/dist/views/content/contentStore.d.ts +28 -0
  51. package/dist/views/content/contentStore.d.ts.map +1 -0
  52. package/dist/views/content/templates.d.ts +3 -0
  53. package/dist/views/content/templates.d.ts.map +1 -0
  54. package/package.json +17 -4
  55. package/src/MarketingContext.tsx +1 -0
  56. package/src/MarketingPage.tsx +23 -3
  57. package/src/components/icons.tsx +19 -0
  58. package/src/data/contentMedia.ts +25 -0
  59. package/src/data/contentMock.ts +253 -0
  60. package/src/data/contentSupabase.ts +263 -0
  61. package/src/data/contentTypes.ts +138 -0
  62. package/src/data/tables.ts +7 -0
  63. package/src/index.ts +64 -2
  64. package/src/locales/en.ts +95 -0
  65. package/src/locales/pt-BR.ts +95 -0
  66. package/src/migrations/000_plg_rename.sql +15 -0
  67. package/src/migrations/001_content_planner.sql +94 -0
  68. package/src/migrations/002_multi_platform.sql +25 -0
  69. package/src/migrations/003_recording_ops.sql +25 -0
  70. package/src/migrations/index.ts +177 -0
  71. package/src/presets.ts +5 -3
  72. package/src/schema/index.ts +69 -0
  73. package/src/views/content/ContentPlannerContext.tsx +13 -0
  74. package/src/views/content/ContentView.tsx +901 -0
  75. package/src/views/content/PlanBriefView.tsx +268 -0
  76. package/src/views/content/PostPage.tsx +370 -0
  77. package/src/views/content/PostSocialPreview.tsx +212 -0
  78. package/src/views/content/RecordingChecklist.tsx +145 -0
  79. package/src/views/content/contentBits.tsx +114 -0
  80. package/src/views/content/contentStore.ts +160 -0
  81. package/src/views/content/templates.ts +172 -0
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var React4 = require('react');
3
+ var React8 = require('react');
4
4
  var core = require('@fayz-ai/core');
5
5
  var saas = require('@fayz-ai/saas');
6
6
  var ui = require('@fayz-ai/ui');
@@ -10,7 +10,7 @@ var vanilla = require('zustand/vanilla');
10
10
 
11
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
12
 
13
- var React4__default = /*#__PURE__*/_interopDefault(React4);
13
+ var React8__default = /*#__PURE__*/_interopDefault(React8);
14
14
 
15
15
  // src/index.ts
16
16
  var ctx = saas.createPluginContext("MarketingPage");
@@ -20,7 +20,7 @@ var useMarketingProvider = ctx.useProvider;
20
20
  var useMarketingStore = ctx.useStore;
21
21
  function useChannelLookup() {
22
22
  const { channels } = useMarketingConfig();
23
- return React4__default.default.useMemo(() => new Map(channels.map((c) => [c.id, c])), [channels]);
23
+ return React8__default.default.useMemo(() => new Map(channels.map((c) => [c.id, c])), [channels]);
24
24
  }
25
25
  function OverviewView() {
26
26
  const setRange = useMarketingStore((s) => s.setRange);
@@ -82,6 +82,20 @@ function ChannelIcon({ name, className }) {
82
82
  const Icon = ICONS[name] ?? lucideReact.Link2;
83
83
  return /* @__PURE__ */ jsxRuntime.jsx(Icon, { className });
84
84
  }
85
+ var PLATFORM_ICONS = {
86
+ instagram: lucideReact.Instagram,
87
+ tiktok: lucideReact.Music2,
88
+ youtube: lucideReact.Youtube,
89
+ facebook: lucideReact.Facebook,
90
+ linkedin: lucideReact.Linkedin,
91
+ x: lucideReact.Twitter,
92
+ pinterest: lucideReact.Pin,
93
+ threads: lucideReact.AtSign
94
+ };
95
+ function PlatformIcon({ platform, className }) {
96
+ const Icon = PLATFORM_ICONS[platform] ?? lucideReact.Link2;
97
+ return /* @__PURE__ */ jsxRuntime.jsx(Icon, { className });
98
+ }
85
99
  function KpiCard({
86
100
  icon: Icon,
87
101
  label,
@@ -166,7 +180,7 @@ function ChannelsView({ onOpen }) {
166
180
  const { conversion, currency } = useMarketingConfig();
167
181
  const channels = useMarketingStore((s) => s.channels);
168
182
  const sorted = [...channels].sort((a, b) => b.conversions - a.conversions);
169
- const columns = React4__default.default.useMemo(() => [
183
+ const columns = React8__default.default.useMemo(() => [
170
184
  { accessorKey: "channelId", header: t("marketing.col.channel"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx(ChannelCell, { channelId: getValue() }) },
171
185
  { accessorKey: "reach", header: t("marketing.col.reach"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
172
186
  { accessorKey: "leads", header: t("marketing.col.leads"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
@@ -206,13 +220,13 @@ function ChannelDetailView({ channelId, onBack }) {
206
220
  const channels = useMarketingStore((s) => s.channels);
207
221
  const lookup = useChannelLookup();
208
222
  const provider = useMarketingProvider();
209
- const [campaigns, setCampaigns] = React4__default.default.useState([]);
210
- React4__default.default.useEffect(() => {
223
+ const [campaigns, setCampaigns] = React8__default.default.useState([]);
224
+ React8__default.default.useEffect(() => {
211
225
  void provider.listCampaigns({ range, channelId }).then(setCampaigns);
212
226
  }, [provider, range, channelId]);
213
227
  const perf = channels.find((c) => c.channelId === channelId);
214
228
  const channel = lookup.get(channelId);
215
- const columns = React4__default.default.useMemo(() => [
229
+ const columns = React8__default.default.useMemo(() => [
216
230
  { accessorKey: "name", header: t("marketing.col.campaign"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
217
231
  { accessorKey: "conversions", header: conversion.labelPlural, cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
218
232
  { id: "cvr", accessorFn: (c) => c.reach > 0 ? c.conversions / c.reach * 100 : 0, header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) },
@@ -249,12 +263,12 @@ function CampaignComposer({ open, onClose }) {
249
263
  const t = core.useTranslation();
250
264
  const { channels } = useMarketingConfig();
251
265
  const saveCampaign = useMarketingStore((s) => s.saveCampaign);
252
- const [name, setName] = React4__default.default.useState("");
253
- const [channelId, setChannelId] = React4__default.default.useState(channels[0]?.id ?? "");
254
- const [status, setStatus] = React4__default.default.useState("active");
255
- const [spend, setSpend] = React4__default.default.useState("");
256
- const [saving, setSaving] = React4__default.default.useState(false);
257
- React4__default.default.useEffect(() => {
266
+ const [name, setName] = React8__default.default.useState("");
267
+ const [channelId, setChannelId] = React8__default.default.useState(channels[0]?.id ?? "");
268
+ const [status, setStatus] = React8__default.default.useState("active");
269
+ const [spend, setSpend] = React8__default.default.useState("");
270
+ const [saving, setSaving] = React8__default.default.useState(false);
271
+ React8__default.default.useEffect(() => {
258
272
  if (open) {
259
273
  setName("");
260
274
  setChannelId(channels[0]?.id ?? "");
@@ -316,8 +330,8 @@ function CampaignsView() {
316
330
  const { conversion, currency } = useMarketingConfig();
317
331
  const campaigns = useMarketingStore((s) => s.campaigns);
318
332
  const deleteCampaign = useMarketingStore((s) => s.deleteCampaign);
319
- const [composerOpen, setComposerOpen] = React4__default.default.useState(false);
320
- const columns = React4__default.default.useMemo(() => [
333
+ const [composerOpen, setComposerOpen] = React8__default.default.useState(false);
334
+ const columns = React8__default.default.useMemo(() => [
321
335
  { accessorKey: "name", header: t("marketing.col.campaign"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
322
336
  { accessorKey: "channelId", header: t("marketing.col.channel"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx(ChannelCell, { channelId: getValue() }) },
323
337
  { accessorKey: "reach", header: t("marketing.col.reach"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
@@ -345,102 +359,1824 @@ function CampaignsView() {
345
359
  }
346
360
  )
347
361
  }
348
- ], [t, conversion.labelPlural, currency, deleteCampaign]);
349
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
350
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
351
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.campaigns.subtitle") }),
352
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
353
- /* @__PURE__ */ jsxRuntime.jsx(RangeTabs, {}),
354
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: () => setComposerOpen(true), children: [
355
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "mr-1.5 h-4 w-4" }),
356
- " ",
357
- t("marketing.campaigns.new")
362
+ ], [t, conversion.labelPlural, currency, deleteCampaign]);
363
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
364
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
365
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.campaigns.subtitle") }),
366
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
367
+ /* @__PURE__ */ jsxRuntime.jsx(RangeTabs, {}),
368
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: () => setComposerOpen(true), children: [
369
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "mr-1.5 h-4 w-4" }),
370
+ " ",
371
+ t("marketing.campaigns.new")
372
+ ] })
373
+ ] })
374
+ ] }),
375
+ /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: campaigns, emptyMessage: t("marketing.campaigns.empty") }),
376
+ /* @__PURE__ */ jsxRuntime.jsx(CampaignComposer, { open: composerOpen, onClose: () => setComposerOpen(false) })
377
+ ] });
378
+ }
379
+ function FunnelView() {
380
+ const t = core.useTranslation();
381
+ const { conversion } = useMarketingConfig();
382
+ const funnel = useMarketingStore((s) => s.funnel);
383
+ const max = Math.max(...funnel.map((s) => s.count), 1);
384
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-4xl space-y-4", children: [
385
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
386
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground", children: [
387
+ t("marketing.funnel.subtitlePrefix"),
388
+ " ",
389
+ conversion.label.toLowerCase()
390
+ ] }),
391
+ /* @__PURE__ */ jsxRuntime.jsx(RangeTabs, {})
392
+ ] }),
393
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-card border border-border bg-card p-5", children: [
394
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-3", children: funnel.map((stage, i) => {
395
+ const pct = max > 0 ? Math.max(stage.count / max * 100, 4) : 0;
396
+ const prev = funnel[i - 1];
397
+ const stepRate = prev && prev.count > 0 ? stage.count / prev.count * 100 : null;
398
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
399
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-1 flex items-center justify-between text-sm", children: [
400
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: stage.label }),
401
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
402
+ formatNumber(stage.count),
403
+ stepRate != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-2 text-xs", children: [
404
+ "(",
405
+ formatPercent(stepRate, 0),
406
+ " ",
407
+ t("marketing.funnel.ofPrev"),
408
+ ")"
409
+ ] })
410
+ ] })
411
+ ] }),
412
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-9 overflow-hidden rounded-md bg-muted/40", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full rounded-md", style: { width: `${pct}%`, backgroundColor: stage.color + "33", borderLeft: `3px solid ${stage.color}` } }) })
413
+ ] }, stage.id);
414
+ }) }),
415
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-5 border-t border-border pt-4 text-sm", children: [
416
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
417
+ t("marketing.funnel.overall"),
418
+ ": "
419
+ ] }),
420
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-foreground", children: funnel.length > 1 && funnel[0].count > 0 ? formatPercent(funnel[funnel.length - 1].count / funnel[0].count * 100) : "\u2014" })
421
+ ] })
422
+ ] })
423
+ ] });
424
+ }
425
+ var TYPE_STYLE = {
426
+ Funnel: "bg-indigo-100 text-indigo-700",
427
+ "Landing page": "bg-pink-100 text-pink-700",
428
+ Website: "bg-sky-100 text-sky-700"
429
+ };
430
+ function LandingPagesView() {
431
+ const t = core.useTranslation();
432
+ const pages = useMarketingStore((s) => s.landingPages);
433
+ const totalVisits = pages.reduce((s, p) => s + p.visits, 0);
434
+ const totalConv = pages.reduce((s, p) => s + p.conversions, 0);
435
+ const avgCvr = totalVisits > 0 ? totalConv / totalVisits * 100 : 0;
436
+ const columns = React8__default.default.useMemo(() => [
437
+ { accessorKey: "name", header: t("marketing.col.page"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
438
+ { accessorKey: "type", header: t("marketing.col.type"), cell: ({ getValue }) => {
439
+ const v = getValue();
440
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `rounded-full px-2 py-0.5 text-xs font-medium ${TYPE_STYLE[v] ?? "bg-muted text-muted-foreground"}`, children: v });
441
+ } },
442
+ { accessorKey: "visits", header: t("marketing.col.visits"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
443
+ { accessorKey: "conversions", header: t("marketing.col.conversions"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: formatNumber(getValue()) }) },
444
+ { accessorKey: "cvr", header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) }
445
+ ], [t]);
446
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
447
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
448
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.landing.subtitle") }),
449
+ /* @__PURE__ */ jsxRuntime.jsx(RangeTabs, {})
450
+ ] }),
451
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-3 gap-4", children: [
452
+ /* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.MousePointerClick, label: t("marketing.metric.totalVisits"), value: formatNumber(totalVisits) }),
453
+ /* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.Layers, label: t("marketing.metric.pages"), value: String(pages.length) }),
454
+ /* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.Percent, label: t("marketing.metric.avgConversion"), value: formatPercent(avgCvr) })
455
+ ] }),
456
+ /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: pages, emptyMessage: t("marketing.landing.subtitle") })
457
+ ] });
458
+ }
459
+ var ctx2 = saas.createPluginContext("ContentPlanner");
460
+ var ContentPlannerContextProvider = ctx2.ContextProvider;
461
+ ctx2.useConfig;
462
+ var useContentPlannerProvider = ctx2.useProvider;
463
+ var useContentPlannerStore = ctx2.useStore;
464
+
465
+ // src/data/contentTypes.ts
466
+ var PLATFORMS = [
467
+ "instagram",
468
+ "tiktok",
469
+ "youtube",
470
+ "facebook",
471
+ "linkedin",
472
+ "x",
473
+ "pinterest",
474
+ "threads"
475
+ ];
476
+ var POST_STATUS_ORDER = ["idea", "script", "recording", "editing", "scheduled", "published"];
477
+ var POST_FORMATS = ["reel", "static", "carousel", "story", "video", "live", "article"];
478
+ var FORMAT_STYLE = {
479
+ reel: "bg-violet-100 text-violet-700",
480
+ static: "bg-sky-100 text-sky-700",
481
+ carousel: "bg-amber-100 text-amber-700",
482
+ story: "bg-pink-100 text-pink-700",
483
+ video: "bg-red-100 text-red-700",
484
+ live: "bg-rose-100 text-rose-700",
485
+ article: "bg-teal-100 text-teal-700"
486
+ };
487
+ var POST_STATUS_COLOR = {
488
+ idea: "#6b7280",
489
+ script: "#6366f1",
490
+ recording: "#f59e0b",
491
+ editing: "#f97316",
492
+ scheduled: "#3b82f6",
493
+ published: "#10b981"
494
+ };
495
+ function StatusDot({ status, className }) {
496
+ return /* @__PURE__ */ jsxRuntime.jsx(
497
+ "span",
498
+ {
499
+ className: ui.cn("h-2 w-2 shrink-0 rounded-full", className),
500
+ style: { backgroundColor: POST_STATUS_COLOR[status] }
501
+ }
502
+ );
503
+ }
504
+ var STATUS_STYLE2 = {
505
+ idea: "bg-slate-100 text-slate-600",
506
+ script: "bg-indigo-100 text-indigo-700",
507
+ recording: "bg-amber-100 text-amber-700",
508
+ editing: "bg-orange-100 text-orange-700",
509
+ scheduled: "bg-sky-100 text-sky-700",
510
+ published: "bg-emerald-100 text-emerald-700"
511
+ };
512
+ function FormatBadge({ format }) {
513
+ const t = core.useTranslation();
514
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn("rounded-full px-2 py-0.5 text-xs font-medium", FORMAT_STYLE[format]), children: t(`marketing.content.format.${format}`) });
515
+ }
516
+ function PostStatusBadge({ status }) {
517
+ const t = core.useTranslation();
518
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn("rounded-full px-2 py-0.5 text-xs font-medium", STATUS_STYLE2[status]), children: t(`marketing.content.status.${status}`) });
519
+ }
520
+ function PlatformBadges({ platforms, className }) {
521
+ if (platforms.length === 0) return null;
522
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn("inline-flex items-center gap-1 text-muted-foreground", className), children: platforms.map((p) => /* @__PURE__ */ jsxRuntime.jsx(PlatformIcon, { platform: p, className: "h-3.5 w-3.5" }, p)) });
523
+ }
524
+ function PlatformPicker({ value, onChange }) {
525
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: PLATFORMS.map((platform) => {
526
+ const selected = value.includes(platform);
527
+ return /* @__PURE__ */ jsxRuntime.jsxs(
528
+ "button",
529
+ {
530
+ type: "button",
531
+ "aria-pressed": selected,
532
+ onClick: () => onChange(selected ? value.filter((p) => p !== platform) : [...value, platform]),
533
+ className: ui.cn(
534
+ "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium capitalize transition-colors",
535
+ selected ? "border-primary bg-primary/10 text-foreground" : "border-border text-muted-foreground hover:bg-muted/40"
536
+ ),
537
+ children: [
538
+ /* @__PURE__ */ jsxRuntime.jsx(PlatformIcon, { platform, className: "h-3.5 w-3.5" }),
539
+ platform
540
+ ]
541
+ },
542
+ platform
543
+ );
544
+ }) });
545
+ }
546
+ function toDraft(plan) {
547
+ return {
548
+ name: plan.name,
549
+ objective: plan.objective ?? "",
550
+ tone: plan.tone ?? "",
551
+ pillars: plan.pillars,
552
+ formats: plan.formats,
553
+ weeklyFrequency: plan.weeklyFrequency,
554
+ weeksCount: plan.weeksCount,
555
+ briefMd: plan.briefMd
556
+ };
557
+ }
558
+ function Field({ label, children }) {
559
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
560
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: label }),
561
+ children
562
+ ] });
563
+ }
564
+ function useBriefDraft() {
565
+ const plan = useContentPlannerStore((s) => s.activePlan);
566
+ const savePlan = useContentPlannerStore((s) => s.savePlan);
567
+ const [draft, setDraft] = React8__default.default.useState(plan ? toDraft(plan) : null);
568
+ const [saving, setSaving] = React8__default.default.useState(false);
569
+ React8__default.default.useEffect(() => {
570
+ if (plan) setDraft(toDraft(plan));
571
+ }, [plan?.id]);
572
+ const patch = (partial) => setDraft((d) => d ? { ...d, ...partial } : d);
573
+ const dirty = !!plan && !!draft && JSON.stringify(draft) !== JSON.stringify(toDraft(plan));
574
+ const save = async () => {
575
+ if (!plan || !draft) return;
576
+ setSaving(true);
577
+ try {
578
+ await savePlan({
579
+ id: plan.id,
580
+ name: draft.name,
581
+ objective: draft.objective,
582
+ tone: draft.tone,
583
+ pillars: draft.pillars,
584
+ formats: draft.formats,
585
+ weeklyFrequency: draft.weeklyFrequency,
586
+ weeksCount: draft.weeksCount,
587
+ briefMd: draft.briefMd
588
+ });
589
+ } finally {
590
+ setSaving(false);
591
+ }
592
+ };
593
+ return { plan, draft, patch, dirty, saving, save };
594
+ }
595
+ function BriefFields({ draft, patch }) {
596
+ const t = core.useTranslation();
597
+ const [pillarInput, setPillarInput] = React8__default.default.useState("");
598
+ const addPillar = () => {
599
+ const value = pillarInput.trim();
600
+ if (!value || draft.pillars.includes(value)) return;
601
+ patch({ pillars: [...draft.pillars, value] });
602
+ setPillarInput("");
603
+ };
604
+ const toggleFormat = (format) => {
605
+ patch({
606
+ formats: draft.formats.includes(format) ? draft.formats.filter((f) => f !== format) : [...draft.formats, format]
607
+ });
608
+ };
609
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
610
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [
611
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.planName"), children: /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: draft.name, onChange: (e) => patch({ name: e.target.value }) }) }),
612
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.objective"), children: /* @__PURE__ */ jsxRuntime.jsx(
613
+ ui.Input,
614
+ {
615
+ value: draft.objective,
616
+ onChange: (e) => patch({ objective: e.target.value }),
617
+ placeholder: t("marketing.content.objectivePlaceholder")
618
+ }
619
+ ) }),
620
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.tone"), children: /* @__PURE__ */ jsxRuntime.jsx(
621
+ ui.Input,
622
+ {
623
+ value: draft.tone,
624
+ onChange: (e) => patch({ tone: e.target.value }),
625
+ placeholder: t("marketing.content.tonePlaceholder")
626
+ }
627
+ ) }),
628
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
629
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.weeklyFrequency"), children: /* @__PURE__ */ jsxRuntime.jsx(
630
+ ui.Input,
631
+ {
632
+ type: "number",
633
+ min: 1,
634
+ max: 14,
635
+ value: draft.weeklyFrequency,
636
+ onChange: (e) => patch({ weeklyFrequency: Math.max(1, Number(e.target.value) || 1) })
637
+ }
638
+ ) }),
639
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.weeksCount"), children: /* @__PURE__ */ jsxRuntime.jsx(
640
+ ui.Input,
641
+ {
642
+ type: "number",
643
+ min: 1,
644
+ max: 52,
645
+ value: draft.weeksCount,
646
+ onChange: (e) => patch({ weeksCount: Math.max(1, Number(e.target.value) || 1) })
647
+ }
648
+ ) })
649
+ ] })
650
+ ] }),
651
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.pillars"), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
652
+ draft.pillars.map((pillar) => /* @__PURE__ */ jsxRuntime.jsxs(
653
+ "span",
654
+ {
655
+ className: "inline-flex items-center gap-1 rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium text-foreground",
656
+ children: [
657
+ pillar,
658
+ /* @__PURE__ */ jsxRuntime.jsx(
659
+ "button",
660
+ {
661
+ type: "button",
662
+ onClick: () => patch({ pillars: draft.pillars.filter((p) => p !== pillar) }),
663
+ className: "text-muted-foreground hover:text-foreground",
664
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-3 w-3" })
665
+ }
666
+ )
667
+ ]
668
+ },
669
+ pillar
670
+ )),
671
+ /* @__PURE__ */ jsxRuntime.jsx(
672
+ ui.Input,
673
+ {
674
+ value: pillarInput,
675
+ onChange: (e) => setPillarInput(e.target.value),
676
+ onKeyDown: (e) => {
677
+ if (e.key === "Enter") {
678
+ e.preventDefault();
679
+ addPillar();
680
+ }
681
+ },
682
+ onBlur: addPillar,
683
+ placeholder: t("marketing.content.pillarPlaceholder"),
684
+ className: "h-7 w-44 text-xs"
685
+ }
686
+ )
687
+ ] }) }),
688
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.formatsLabel"), children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-4", children: POST_FORMATS.map((format) => /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 text-sm text-foreground", children: [
689
+ /* @__PURE__ */ jsxRuntime.jsx(
690
+ ui.Checkbox,
691
+ {
692
+ checked: draft.formats.includes(format),
693
+ onChange: () => toggleFormat(format)
694
+ }
695
+ ),
696
+ t(`marketing.content.format.${format}`)
697
+ ] }, format)) }) }),
698
+ /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("marketing.content.briefBody"), children: /* @__PURE__ */ jsxRuntime.jsx(
699
+ ui.MarkdownEditor,
700
+ {
701
+ value: draft.briefMd,
702
+ onChange: (briefMd) => patch({ briefMd }),
703
+ placeholder: t("marketing.content.briefPlaceholder"),
704
+ editLabel: t("marketing.content.edit"),
705
+ previewLabel: t("marketing.content.preview"),
706
+ minRows: 10
707
+ }
708
+ ) })
709
+ ] });
710
+ }
711
+ function PlanBriefView({ onBack }) {
712
+ const t = core.useTranslation();
713
+ const { plan, draft, patch, dirty, saving, save } = useBriefDraft();
714
+ if (!plan || !draft) {
715
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto max-w-3xl text-sm text-muted-foreground", children: "\u2026" });
716
+ }
717
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
718
+ /* @__PURE__ */ jsxRuntime.jsx(
719
+ ui.SubpageHeader,
720
+ {
721
+ title: t("marketing.content.brief"),
722
+ subtitle: plan.name,
723
+ parentLabel: t("marketing.content.title"),
724
+ onBack,
725
+ actions: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "sm", onClick: () => void save(), disabled: !dirty || saving, children: t("marketing.content.save") })
726
+ }
727
+ ),
728
+ /* @__PURE__ */ jsxRuntime.jsx(BriefFields, { draft, patch })
729
+ ] });
730
+ }
731
+ function PlanBriefSheet({ open, onClose }) {
732
+ const t = core.useTranslation();
733
+ const { plan, draft, patch, dirty, saving, save } = useBriefDraft();
734
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Sheet, { open, onOpenChange: (v) => {
735
+ if (!v) onClose();
736
+ }, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.SheetContent, { width: "max-w-2xl", children: [
737
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.SheetHeader, { children: [
738
+ /* @__PURE__ */ jsxRuntime.jsx(ui.SheetTitle, { children: t("marketing.content.brief") }),
739
+ plan && /* @__PURE__ */ jsxRuntime.jsx(ui.SheetDescription, { children: plan.name })
740
+ ] }),
741
+ /* @__PURE__ */ jsxRuntime.jsx(ui.SheetBody, { children: draft ? /* @__PURE__ */ jsxRuntime.jsx(BriefFields, { draft, patch }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" }) }),
742
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.SheetFooter, { children: [
743
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", onClick: onClose, disabled: saving, children: t("marketing.content.cancel") }),
744
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: () => void save(), disabled: !dirty || saving, children: t("marketing.content.save") })
745
+ ] })
746
+ ] }) });
747
+ }
748
+ var DEFAULT_WEEKS = 8;
749
+ var ADD_ACCOUNT_VALUE = "__add-account__";
750
+ var POST_DRAG_MIME = "application/x-mkt-post";
751
+ function PostCard({ post, onOpen, onDelete }) {
752
+ const t = core.useTranslation();
753
+ const [dragging, setDragging] = React8__default.default.useState(false);
754
+ return /* @__PURE__ */ jsxRuntime.jsxs(
755
+ "div",
756
+ {
757
+ role: "button",
758
+ tabIndex: 0,
759
+ onClick: onOpen,
760
+ onKeyDown: (e) => {
761
+ if (e.key === "Enter") onOpen();
762
+ },
763
+ draggable: true,
764
+ onDragStart: (e) => {
765
+ e.dataTransfer.setData(POST_DRAG_MIME, post.id);
766
+ e.dataTransfer.effectAllowed = "move";
767
+ setDragging(true);
768
+ },
769
+ onDragEnd: () => setDragging(false),
770
+ className: ui.cn(
771
+ "group/post relative w-full cursor-pointer rounded-card border border-border bg-card p-3 pl-7 text-left transition-colors hover:bg-muted/40",
772
+ dragging && "opacity-50"
773
+ ),
774
+ children: [
775
+ /* @__PURE__ */ jsxRuntime.jsx(
776
+ lucideReact.GripVertical,
777
+ {
778
+ className: "absolute left-1.5 top-3 h-4 w-4 cursor-grab text-muted-foreground/50 opacity-0 transition-opacity group-hover/post:opacity-100",
779
+ "aria-hidden": true
780
+ }
781
+ ),
782
+ onDelete && /* @__PURE__ */ jsxRuntime.jsx(
783
+ "button",
784
+ {
785
+ type: "button",
786
+ "aria-label": t("marketing.content.deletePost"),
787
+ onClick: (e) => {
788
+ e.stopPropagation();
789
+ onDelete();
790
+ },
791
+ className: "absolute right-1.5 top-2 rounded p-1 text-muted-foreground/60 opacity-0 transition-opacity hover:bg-destructive/10 hover:text-destructive group-hover/post:opacity-100",
792
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" })
793
+ }
794
+ ),
795
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "pr-5 text-sm font-medium text-foreground", children: post.title || t("marketing.content.untitled") }),
796
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-1.5", children: [
797
+ /* @__PURE__ */ jsxRuntime.jsx(FormatBadge, { format: post.format }),
798
+ /* @__PURE__ */ jsxRuntime.jsx(PostStatusBadge, { status: post.status }),
799
+ post.scheduledDate && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: post.scheduledDate }),
800
+ /* @__PURE__ */ jsxRuntime.jsx(PlatformBadges, { platforms: post.platforms })
801
+ ] })
802
+ ]
803
+ }
804
+ );
805
+ }
806
+ function WeekColumn({ week, rangeLabel, posts, onOpenPost, onNewPost, onDeletePost, onDropPost, disabled }) {
807
+ const t = core.useTranslation();
808
+ const [isDropTarget, setIsDropTarget] = React8__default.default.useState(false);
809
+ return /* @__PURE__ */ jsxRuntime.jsxs(
810
+ "div",
811
+ {
812
+ className: ui.cn(
813
+ "flex flex-col gap-2 rounded-card border border-border bg-muted/20 p-2.5 transition-colors",
814
+ isDropTarget && "border-primary bg-primary/5 ring-1 ring-primary/40"
815
+ ),
816
+ onDragOver: (e) => {
817
+ if (!onDropPost || !e.dataTransfer.types.includes(POST_DRAG_MIME)) return;
818
+ e.preventDefault();
819
+ e.dataTransfer.dropEffect = "move";
820
+ setIsDropTarget(true);
821
+ },
822
+ onDragLeave: () => setIsDropTarget(false),
823
+ onDrop: (e) => {
824
+ if (!onDropPost) return;
825
+ e.preventDefault();
826
+ setIsDropTarget(false);
827
+ const postId = e.dataTransfer.getData(POST_DRAG_MIME);
828
+ if (postId) onDropPost(postId, week);
829
+ },
830
+ children: [
831
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline justify-between px-1", children: [
832
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: [
833
+ t("marketing.content.week"),
834
+ " ",
835
+ week
836
+ ] }),
837
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground/80", children: rangeLabel })
838
+ ] }),
839
+ posts.map((post) => /* @__PURE__ */ jsxRuntime.jsx(
840
+ PostCard,
841
+ {
842
+ post,
843
+ onOpen: () => onOpenPost(post.id),
844
+ onDelete: onDeletePost ? () => onDeletePost(post.id) : void 0
845
+ },
846
+ post.id
847
+ )),
848
+ /* @__PURE__ */ jsxRuntime.jsxs(
849
+ "button",
850
+ {
851
+ type: "button",
852
+ onClick: () => onNewPost(week),
853
+ disabled,
854
+ className: "flex items-center justify-center gap-1 rounded-card border border-dashed border-border p-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground disabled:pointer-events-none disabled:opacity-50",
855
+ children: [
856
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
857
+ t("marketing.content.newPost")
858
+ ]
859
+ }
860
+ )
861
+ ]
862
+ }
863
+ );
864
+ }
865
+ function AccountModal({ open, account, onClose }) {
866
+ const t = core.useTranslation();
867
+ const saveAccount = useContentPlannerStore((s) => s.saveAccount);
868
+ const deleteAccount = useContentPlannerStore((s) => s.deleteAccount);
869
+ const isEdit = !!account;
870
+ const [name, setName] = React8__default.default.useState("");
871
+ const [handle, setHandle] = React8__default.default.useState("");
872
+ const [platforms, setPlatforms] = React8__default.default.useState(["instagram"]);
873
+ const [saving, setSaving] = React8__default.default.useState(false);
874
+ const [confirmDelete, setConfirmDelete] = React8__default.default.useState(false);
875
+ React8__default.default.useEffect(() => {
876
+ if (open) {
877
+ setName(account?.name ?? "");
878
+ setHandle(account?.handle ?? "");
879
+ setPlatforms(account?.platforms?.length ? account.platforms : ["instagram"]);
880
+ setSaving(false);
881
+ setConfirmDelete(false);
882
+ }
883
+ }, [open, account]);
884
+ const create = async () => {
885
+ if (!name.trim() || platforms.length === 0 || saving) return;
886
+ setSaving(true);
887
+ try {
888
+ await saveAccount({
889
+ ...isEdit ? { id: account.id } : {},
890
+ name: name.trim(),
891
+ handle: handle.trim() || void 0,
892
+ platforms
893
+ });
894
+ onClose();
895
+ } finally {
896
+ setSaving(false);
897
+ }
898
+ };
899
+ const remove = async () => {
900
+ if (!account || saving) return;
901
+ setSaving(true);
902
+ try {
903
+ await deleteAccount(account.id);
904
+ onClose();
905
+ } finally {
906
+ setSaving(false);
907
+ }
908
+ };
909
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Modal, { open, onOpenChange: (o) => {
910
+ if (!o) onClose();
911
+ }, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.ModalContent, { size: "sm", children: [
912
+ /* @__PURE__ */ jsxRuntime.jsx(ui.ModalHeader, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.ModalTitle, { children: isEdit ? t("marketing.content.editAccountTitle") : t("marketing.content.addAccountTitle") }) }),
913
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.ModalBody, { className: "space-y-4", children: [
914
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
915
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.nameLabel") }),
916
+ /* @__PURE__ */ jsxRuntime.jsx(
917
+ ui.Input,
918
+ {
919
+ autoFocus: true,
920
+ value: name,
921
+ onChange: (e) => setName(e.target.value),
922
+ placeholder: t("marketing.content.accountNamePlaceholder"),
923
+ onKeyDown: (e) => {
924
+ if (e.key === "Enter") void create();
925
+ }
926
+ }
927
+ )
928
+ ] }),
929
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
930
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.handleLabel") }),
931
+ /* @__PURE__ */ jsxRuntime.jsx(
932
+ ui.Input,
933
+ {
934
+ value: handle,
935
+ onChange: (e) => setHandle(e.target.value),
936
+ placeholder: t("marketing.content.handlePlaceholder")
937
+ }
938
+ )
939
+ ] }),
940
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
941
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.platformsLabel") }),
942
+ /* @__PURE__ */ jsxRuntime.jsx(PlatformPicker, { value: platforms, onChange: setPlatforms })
943
+ ] })
944
+ ] }),
945
+ /* @__PURE__ */ jsxRuntime.jsx(ui.ModalFooter, { children: confirmDelete && isEdit ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full items-center gap-3 rounded-card border border-destructive/30 bg-destructive/5 p-3", children: [
946
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "flex-1 text-xs text-destructive", children: t("marketing.content.deleteAccountConfirm") }),
947
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", size: "sm", onClick: () => setConfirmDelete(false), disabled: saving, children: t("marketing.content.cancel") }),
948
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "destructive", size: "sm", onClick: () => void remove(), disabled: saving, children: t("marketing.content.deleteAccount") })
949
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
950
+ isEdit && /* @__PURE__ */ jsxRuntime.jsxs(
951
+ ui.Button,
952
+ {
953
+ variant: "outline",
954
+ onClick: () => setConfirmDelete(true),
955
+ disabled: saving,
956
+ className: "mr-auto text-destructive hover:text-destructive",
957
+ children: [
958
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "mr-1.5 h-4 w-4" }),
959
+ t("marketing.content.deleteAccount")
960
+ ]
961
+ }
962
+ ),
963
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", onClick: onClose, disabled: saving, children: t("marketing.content.cancel") }),
964
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: () => void create(), disabled: saving || !name.trim() || platforms.length === 0, children: isEdit ? t("marketing.content.save") : t("marketing.content.createAccount") })
965
+ ] }) })
966
+ ] }) });
967
+ }
968
+ function AccountSelector() {
969
+ const t = core.useTranslation();
970
+ const loaded = useContentPlannerStore((s) => s.loaded);
971
+ const accounts = useContentPlannerStore((s) => s.accounts);
972
+ const activeAccountId = useContentPlannerStore((s) => s.activeAccountId);
973
+ const setAccount = useContentPlannerStore((s) => s.setAccount);
974
+ const [open, setOpen] = React8__default.default.useState(false);
975
+ const [modal, setModal] = React8__default.default.useState(null);
976
+ const activeAccount = accounts.find((a) => a.id === activeAccountId) ?? null;
977
+ if (!loaded) {
978
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
979
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-9 w-64" }),
980
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-9 w-9" })
981
+ ] });
982
+ }
983
+ const openSettings = (account) => {
984
+ setOpen(false);
985
+ setModal({ mode: "edit", account });
986
+ };
987
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
988
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
989
+ /* @__PURE__ */ jsxRuntime.jsxs(
990
+ ui.Select,
991
+ {
992
+ open,
993
+ onOpenChange: setOpen,
994
+ value: activeAccountId ?? void 0,
995
+ onValueChange: (id) => {
996
+ if (id === ADD_ACCOUNT_VALUE) {
997
+ setModal({ mode: "create" });
998
+ return;
999
+ }
1000
+ void setAccount(id);
1001
+ },
1002
+ children: [
1003
+ /* @__PURE__ */ jsxRuntime.jsx(ui.SelectTrigger, { className: "w-64", children: /* @__PURE__ */ jsxRuntime.jsx(ui.SelectValue, { placeholder: t("marketing.content.account") }) }),
1004
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.SelectContent, { children: [
1005
+ accounts.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: "__none__", disabled: true, children: t("marketing.content.noAccountsAvailable") }) : accounts.map((acc) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
1006
+ /* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: acc.id, className: "pr-9", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [
1007
+ acc.name,
1008
+ /* @__PURE__ */ jsxRuntime.jsx(PlatformBadges, { platforms: acc.platforms })
1009
+ ] }) }),
1010
+ /* @__PURE__ */ jsxRuntime.jsx(
1011
+ "button",
1012
+ {
1013
+ type: "button",
1014
+ "aria-label": t("marketing.content.accountSettings"),
1015
+ onPointerDown: (e) => {
1016
+ e.stopPropagation();
1017
+ e.preventDefault();
1018
+ },
1019
+ onPointerUp: (e) => {
1020
+ e.stopPropagation();
1021
+ e.preventDefault();
1022
+ },
1023
+ onClick: (e) => {
1024
+ e.stopPropagation();
1025
+ openSettings(acc);
1026
+ },
1027
+ className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-muted-foreground/70 transition-colors hover:bg-muted/60 hover:text-foreground",
1028
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings2, { className: "h-3.5 w-3.5" })
1029
+ }
1030
+ )
1031
+ ] }, acc.id)),
1032
+ /* @__PURE__ */ jsxRuntime.jsx(ui.SelectSeparator, {}),
1033
+ /* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: ADD_ACCOUNT_VALUE, children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
1034
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
1035
+ t("marketing.content.addAccount")
1036
+ ] }) })
1037
+ ] })
1038
+ ]
1039
+ }
1040
+ ),
1041
+ activeAccount && /* @__PURE__ */ jsxRuntime.jsx(
1042
+ ui.Button,
1043
+ {
1044
+ variant: "ghost",
1045
+ size: "sm",
1046
+ className: "h-9 w-9 p-0 text-muted-foreground",
1047
+ "aria-label": t("marketing.content.accountSettings"),
1048
+ onClick: () => setModal({ mode: "edit", account: activeAccount }),
1049
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings2, { className: "h-4 w-4" })
1050
+ }
1051
+ )
1052
+ ] }),
1053
+ /* @__PURE__ */ jsxRuntime.jsx(
1054
+ AccountModal,
1055
+ {
1056
+ open: !!modal,
1057
+ account: modal?.mode === "edit" ? modal.account : null,
1058
+ onClose: () => setModal(null)
1059
+ }
1060
+ )
1061
+ ] });
1062
+ }
1063
+ function ViewSwitcher({ mode, onChange }) {
1064
+ const t = core.useTranslation();
1065
+ return /* @__PURE__ */ jsxRuntime.jsx(
1066
+ ui.SegmentedControl,
1067
+ {
1068
+ size: "md",
1069
+ "aria-label": t("marketing.content.viewBoard"),
1070
+ value: mode,
1071
+ onChange,
1072
+ options: [
1073
+ { value: "board", label: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
1074
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Columns3, { className: "h-3.5 w-3.5" }),
1075
+ t("marketing.content.viewBoard")
1076
+ ] }) },
1077
+ { value: "calendar", label: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
1078
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CalendarDays, { className: "h-3.5 w-3.5" }),
1079
+ t("marketing.content.viewCalendar")
1080
+ ] }) },
1081
+ { value: "list", label: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
1082
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.List, { className: "h-3.5 w-3.5" }),
1083
+ t("marketing.content.viewList")
1084
+ ] }) }
1085
+ ]
1086
+ }
1087
+ );
1088
+ }
1089
+ function shortDay(d) {
1090
+ return d.toLocaleDateString(void 0, { day: "numeric", month: "short" });
1091
+ }
1092
+ function buildBoardWeeks(startDate, weeksCount) {
1093
+ return Array.from({ length: weeksCount }, (_, i) => {
1094
+ const start = new Date(startDate);
1095
+ start.setDate(start.getDate() + i * 7);
1096
+ const end = new Date(start);
1097
+ end.setDate(start.getDate() + 6);
1098
+ return {
1099
+ week: i + 1,
1100
+ rangeLabel: `${shortDay(start)} \u2013 ${shortDay(end)}`,
1101
+ monthLabel: start.toLocaleDateString(void 0, { month: "long", year: "numeric" })
1102
+ };
1103
+ });
1104
+ }
1105
+ function BoardView({ startDate, weeksCount, postsByWeek, onOpenPost, onNewPost, onDeletePost, onDropPost, disabled }) {
1106
+ const months = [];
1107
+ for (const bw of buildBoardWeeks(startDate, weeksCount)) {
1108
+ const current = months[months.length - 1];
1109
+ if (current && current.label === bw.monthLabel) current.weeks.push(bw);
1110
+ else months.push({ label: bw.monthLabel, weeks: [bw] });
1111
+ }
1112
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-8", children: months.map(({ label, weeks }) => /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "space-y-3", children: [
1113
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: label }),
1114
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4", children: weeks.map(({ week, rangeLabel }) => /* @__PURE__ */ jsxRuntime.jsx(
1115
+ WeekColumn,
1116
+ {
1117
+ week,
1118
+ rangeLabel,
1119
+ posts: postsByWeek.get(week) ?? [],
1120
+ onOpenPost,
1121
+ onNewPost,
1122
+ onDeletePost,
1123
+ onDropPost,
1124
+ disabled
1125
+ },
1126
+ week
1127
+ )) })
1128
+ ] }, label)) });
1129
+ }
1130
+ function toISODate(d) {
1131
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1132
+ }
1133
+ function mondayOfCurrentWeek() {
1134
+ const now = /* @__PURE__ */ new Date();
1135
+ const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1136
+ monday.setDate(monday.getDate() - (monday.getDay() + 6) % 7);
1137
+ return monday;
1138
+ }
1139
+ function CalendarView({ posts, onOpenPost, onNewPostAt, disabled }) {
1140
+ const t = core.useTranslation();
1141
+ const [cursor, setCursor] = React8__default.default.useState(() => {
1142
+ const first = posts.find((p) => p.scheduledDate)?.scheduledDate;
1143
+ const base = first ? /* @__PURE__ */ new Date(`${first}T00:00:00`) : /* @__PURE__ */ new Date();
1144
+ return new Date(base.getFullYear(), base.getMonth(), 1);
1145
+ });
1146
+ const [expandedDay, setExpandedDay] = React8__default.default.useState(null);
1147
+ const MAX_VISIBLE = 3;
1148
+ const postsByDate = React8__default.default.useMemo(() => {
1149
+ const map = /* @__PURE__ */ new Map();
1150
+ for (const post of posts) {
1151
+ if (!post.scheduledDate) continue;
1152
+ const list = map.get(post.scheduledDate) ?? [];
1153
+ list.push(post);
1154
+ map.set(post.scheduledDate, list);
1155
+ }
1156
+ return map;
1157
+ }, [posts]);
1158
+ const unscheduled = posts.filter((p) => !p.scheduledDate);
1159
+ const days = React8__default.default.useMemo(() => {
1160
+ const firstOfMonth = new Date(cursor.getFullYear(), cursor.getMonth(), 1);
1161
+ const start = new Date(firstOfMonth);
1162
+ start.setDate(start.getDate() - (firstOfMonth.getDay() + 6) % 7);
1163
+ const cells = [];
1164
+ for (let i = 0; i < 42; i++) {
1165
+ const d = new Date(start);
1166
+ d.setDate(start.getDate() + i);
1167
+ cells.push(d);
1168
+ }
1169
+ while (cells.length > 7 && cells[cells.length - 7].getMonth() !== cursor.getMonth()) {
1170
+ cells.splice(cells.length - 7, 7);
1171
+ }
1172
+ return cells;
1173
+ }, [cursor]);
1174
+ const monthLabel = cursor.toLocaleDateString(void 0, { month: "long", year: "numeric" });
1175
+ const weekdayLabels = days.slice(0, 7).map(
1176
+ (d) => d.toLocaleDateString(void 0, { weekday: "short" })
1177
+ );
1178
+ const todayISO = toISODate(/* @__PURE__ */ new Date());
1179
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
1180
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
1181
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold capitalize text-foreground", children: monthLabel }),
1182
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
1183
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "sm", onClick: () => setCursor(new Date(cursor.getFullYear(), cursor.getMonth() - 1, 1)), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-4 w-4" }) }),
1184
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "sm", onClick: () => setCursor(new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1)), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4" }) })
1185
+ ] })
1186
+ ] }),
1187
+ unscheduled.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 rounded-card border border-dashed border-border bg-muted/20 p-2.5", children: [
1188
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t("marketing.content.unscheduled") }),
1189
+ unscheduled.map((post) => /* @__PURE__ */ jsxRuntime.jsxs(
1190
+ "button",
1191
+ {
1192
+ type: "button",
1193
+ onClick: () => onOpenPost(post.id),
1194
+ className: "inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-2.5 py-1 text-xs font-medium text-foreground transition-colors hover:bg-muted/40",
1195
+ children: [
1196
+ post.title || t("marketing.content.untitled"),
1197
+ /* @__PURE__ */ jsxRuntime.jsx(FormatBadge, { format: post.format })
1198
+ ]
1199
+ },
1200
+ post.id
1201
+ ))
1202
+ ] }),
1203
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "overflow-hidden rounded-card border border-border", children: [
1204
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-7 border-b border-border bg-muted/30", children: weekdayLabels.map((label, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-2 py-1.5 text-center text-xs font-medium capitalize text-muted-foreground", children: label }, i)) }),
1205
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-7", children: days.map((day, i) => {
1206
+ const iso = toISODate(day);
1207
+ const inMonth = day.getMonth() === cursor.getMonth();
1208
+ const dayPosts = postsByDate.get(iso) ?? [];
1209
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1210
+ "div",
1211
+ {
1212
+ className: ui.cn(
1213
+ "group min-h-24 border-b border-r border-border p-1.5 [&:nth-child(7n)]:border-r-0",
1214
+ !inMonth && "bg-muted/20"
1215
+ ),
1216
+ children: [
1217
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
1218
+ /* @__PURE__ */ jsxRuntime.jsx(
1219
+ "span",
1220
+ {
1221
+ className: ui.cn(
1222
+ "text-xs",
1223
+ inMonth ? "text-foreground" : "text-muted-foreground/60",
1224
+ iso === todayISO && "flex h-5 w-5 items-center justify-center rounded-full bg-primary font-semibold text-primary-foreground"
1225
+ ),
1226
+ children: day.getDate()
1227
+ }
1228
+ ),
1229
+ !disabled && /* @__PURE__ */ jsxRuntime.jsx(
1230
+ "button",
1231
+ {
1232
+ type: "button",
1233
+ onClick: () => onNewPostAt(iso),
1234
+ className: "rounded p-0.5 text-muted-foreground opacity-0 transition-opacity hover:bg-muted/60 hover:text-foreground group-hover:opacity-100",
1235
+ "aria-label": t("marketing.content.newPost"),
1236
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" })
1237
+ }
1238
+ )
1239
+ ] }),
1240
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 space-y-1", children: [
1241
+ (expandedDay === iso ? dayPosts : dayPosts.slice(0, MAX_VISIBLE)).map((post) => /* @__PURE__ */ jsxRuntime.jsx(
1242
+ "button",
1243
+ {
1244
+ type: "button",
1245
+ onClick: () => onOpenPost(post.id),
1246
+ className: "block w-full truncate rounded border border-border bg-card px-1.5 py-1 text-left text-xs text-foreground transition-colors hover:bg-muted/40",
1247
+ children: post.title || t("marketing.content.untitled")
1248
+ },
1249
+ post.id
1250
+ )),
1251
+ dayPosts.length > MAX_VISIBLE && /* @__PURE__ */ jsxRuntime.jsx(
1252
+ "button",
1253
+ {
1254
+ type: "button",
1255
+ onClick: () => setExpandedDay(expandedDay === iso ? null : iso),
1256
+ className: "block w-full px-1.5 py-0.5 text-left text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
1257
+ children: expandedDay === iso ? t("marketing.content.showLess") : `+${dayPosts.length - MAX_VISIBLE}`
1258
+ }
1259
+ )
1260
+ ] })
1261
+ ]
1262
+ },
1263
+ i
1264
+ );
1265
+ }) })
1266
+ ] })
1267
+ ] });
1268
+ }
1269
+ function ListView({ posts, onOpenPost }) {
1270
+ const t = core.useTranslation();
1271
+ if (posts.length === 0) {
1272
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.content.noPosts") });
1273
+ }
1274
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-card border border-border", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full text-sm", children: [
1275
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "border-b border-border bg-muted/30 text-left text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [
1276
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2", children: t("marketing.content.listWeek") }),
1277
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2", children: t("marketing.content.listTitle") }),
1278
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2", children: t("marketing.content.listFormat") }),
1279
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2", children: t("marketing.content.listStatus") }),
1280
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2", children: t("marketing.content.listDate") })
1281
+ ] }) }),
1282
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: posts.map((post) => /* @__PURE__ */ jsxRuntime.jsxs(
1283
+ "tr",
1284
+ {
1285
+ onClick: () => onOpenPost(post.id),
1286
+ className: "cursor-pointer border-b border-border last:border-b-0 transition-colors hover:bg-muted/30",
1287
+ children: [
1288
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-muted-foreground", children: post.weekNumber }),
1289
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 font-medium text-foreground", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [
1290
+ post.title || t("marketing.content.untitled"),
1291
+ /* @__PURE__ */ jsxRuntime.jsx(PlatformBadges, { platforms: post.platforms })
1292
+ ] }) }),
1293
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsxRuntime.jsx(FormatBadge, { format: post.format }) }),
1294
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsxRuntime.jsx(PostStatusBadge, { status: post.status }) }),
1295
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-muted-foreground", children: post.scheduledDate ?? "\u2014" })
1296
+ ]
1297
+ },
1298
+ post.id
1299
+ )) })
1300
+ ] }) });
1301
+ }
1302
+ function ContentView({ onOpenPost }) {
1303
+ const t = core.useTranslation();
1304
+ const loaded = useContentPlannerStore((s) => s.loaded);
1305
+ const loading = useContentPlannerStore((s) => s.loading);
1306
+ const accounts = useContentPlannerStore((s) => s.accounts);
1307
+ const activeAccountId = useContentPlannerStore((s) => s.activeAccountId);
1308
+ const plan = useContentPlannerStore((s) => s.activePlan);
1309
+ const posts = useContentPlannerStore((s) => s.posts);
1310
+ const load = useContentPlannerStore((s) => s.load);
1311
+ const savePlan = useContentPlannerStore((s) => s.savePlan);
1312
+ const savePost = useContentPlannerStore((s) => s.savePost);
1313
+ const deletePost = useContentPlannerStore((s) => s.deletePost);
1314
+ const viewMode = useContentPlannerStore((s) => s.viewMode);
1315
+ const setViewMode = useContentPlannerStore((s) => s.setViewMode);
1316
+ const loadedTenantId = useContentPlannerStore((s) => s.loadedTenantId);
1317
+ const [briefOpen, setBriefOpen] = React8__default.default.useState(false);
1318
+ const [pendingDeleteId, setPendingDeleteId] = React8__default.default.useState(null);
1319
+ const tenantId = saas.useOrganizationStore((state) => state.currentOrg?.id);
1320
+ React8__default.default.useEffect(() => {
1321
+ if (!loading && (!loaded || tenantId && tenantId !== loadedTenantId)) void load();
1322
+ }, [tenantId, loadedTenantId, loaded, loading, load]);
1323
+ const postsByWeek = React8__default.default.useMemo(() => {
1324
+ const map = /* @__PURE__ */ new Map();
1325
+ for (const post of posts) {
1326
+ const list = map.get(post.weekNumber) ?? [];
1327
+ list.push(post);
1328
+ map.set(post.weekNumber, list);
1329
+ }
1330
+ return map;
1331
+ }, [posts]);
1332
+ const ensurePlan = async () => {
1333
+ if (plan) return plan;
1334
+ if (!activeAccountId) return null;
1335
+ return savePlan({
1336
+ accountId: activeAccountId,
1337
+ name: t("marketing.content.defaultPlanName"),
1338
+ status: "active",
1339
+ weeksCount: DEFAULT_WEEKS,
1340
+ startDate: toISODate(mondayOfCurrentWeek())
1341
+ });
1342
+ };
1343
+ const createPost = async (week) => {
1344
+ const targetPlan = await ensurePlan();
1345
+ if (!targetPlan) return;
1346
+ const start = targetPlan.startDate ? /* @__PURE__ */ new Date(`${targetPlan.startDate}T00:00:00`) : mondayOfCurrentWeek();
1347
+ start.setDate(start.getDate() + (week - 1) * 7);
1348
+ const post = await savePost({
1349
+ planId: targetPlan.id,
1350
+ weekNumber: week,
1351
+ title: "",
1352
+ format: "reel",
1353
+ status: "idea",
1354
+ scheduledDate: toISODate(start)
1355
+ });
1356
+ onOpenPost(post.id);
1357
+ };
1358
+ const movePost = async (postId, week) => {
1359
+ const post = posts.find((p) => p.id === postId);
1360
+ if (!post || post.weekNumber === week) return;
1361
+ await savePost({ id: postId, weekNumber: week });
1362
+ };
1363
+ const createPostAt = async (isoDate) => {
1364
+ const targetPlan = await ensurePlan();
1365
+ if (!targetPlan) return;
1366
+ let week = 1;
1367
+ if (targetPlan.startDate) {
1368
+ const start = /* @__PURE__ */ new Date(`${targetPlan.startDate}T00:00:00`);
1369
+ const day = /* @__PURE__ */ new Date(`${isoDate}T00:00:00`);
1370
+ const diff = Math.floor((day.getTime() - start.getTime()) / (7 * 24 * 3600 * 1e3)) + 1;
1371
+ week = Math.min(Math.max(diff, 1), targetPlan.weeksCount);
1372
+ }
1373
+ const post = await savePost({
1374
+ planId: targetPlan.id,
1375
+ weekNumber: week,
1376
+ title: "",
1377
+ format: "reel",
1378
+ status: "idea",
1379
+ scheduledDate: isoDate
1380
+ });
1381
+ onOpenPost(post.id);
1382
+ };
1383
+ const busy = !loaded || loading;
1384
+ const hasAccount = accounts.length > 0;
1385
+ const weeksCount = plan?.weeksCount ?? DEFAULT_WEEKS;
1386
+ const boardStart = plan?.startDate ? /* @__PURE__ */ new Date(`${plan.startDate}T00:00:00`) : mondayOfCurrentWeek();
1387
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl space-y-6", children: [
1388
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
1389
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
1390
+ /* @__PURE__ */ jsxRuntime.jsx(AccountSelector, {}),
1391
+ plan && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-muted-foreground", children: plan.name })
1392
+ ] }),
1393
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1394
+ /* @__PURE__ */ jsxRuntime.jsx(ViewSwitcher, { mode: viewMode, onChange: setViewMode }),
1395
+ plan && /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: () => setBriefOpen(true), children: [
1396
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.NotebookPen, { className: "mr-1.5 h-4 w-4" }),
1397
+ t("marketing.content.brief")
1398
+ ] })
1399
+ ] })
1400
+ ] }),
1401
+ !busy && !hasAccount && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.content.noAccountsHint") }),
1402
+ busy && viewMode === "board" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4", children: Array.from({ length: 8 }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-44" }, i)) }),
1403
+ busy && viewMode === "calendar" && /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-96 w-full" }),
1404
+ busy && viewMode === "list" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: Array.from({ length: 6 }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-10 w-full" }, i)) }),
1405
+ !busy && viewMode === "board" && /* @__PURE__ */ jsxRuntime.jsx(
1406
+ BoardView,
1407
+ {
1408
+ startDate: boardStart,
1409
+ weeksCount,
1410
+ postsByWeek,
1411
+ onOpenPost,
1412
+ onNewPost: (w) => void createPost(w),
1413
+ onDeletePost: setPendingDeleteId,
1414
+ onDropPost: (id, w) => void movePost(id, w),
1415
+ disabled: !hasAccount
1416
+ }
1417
+ ),
1418
+ !busy && viewMode === "calendar" && /* @__PURE__ */ jsxRuntime.jsx(
1419
+ CalendarView,
1420
+ {
1421
+ posts,
1422
+ onOpenPost,
1423
+ onNewPostAt: (iso) => void createPostAt(iso),
1424
+ disabled: !hasAccount
1425
+ }
1426
+ ),
1427
+ !busy && viewMode === "list" && /* @__PURE__ */ jsxRuntime.jsx(ListView, { posts, onOpenPost }),
1428
+ /* @__PURE__ */ jsxRuntime.jsx(PlanBriefSheet, { open: briefOpen, onClose: () => setBriefOpen(false) }),
1429
+ /* @__PURE__ */ jsxRuntime.jsx(
1430
+ ui.ConfirmDialog,
1431
+ {
1432
+ open: !!pendingDeleteId,
1433
+ variant: "destructive",
1434
+ title: t("marketing.content.deletePost"),
1435
+ description: t("marketing.content.deletePostConfirm"),
1436
+ confirmLabel: t("marketing.content.deletePost"),
1437
+ cancelLabel: t("marketing.content.cancel"),
1438
+ onCancel: () => setPendingDeleteId(null),
1439
+ onConfirm: () => {
1440
+ const id = pendingDeleteId;
1441
+ setPendingDeleteId(null);
1442
+ if (id) void deletePost(id);
1443
+ }
1444
+ }
1445
+ )
1446
+ ] });
1447
+ }
1448
+ function canUploadMedia() {
1449
+ return !!core.getSupabaseClientOptional();
1450
+ }
1451
+ async function uploadPostMedia(postId, file) {
1452
+ const supabase = core.getSupabaseClientOptional();
1453
+ if (!supabase) throw new Error("Supabase not initialized");
1454
+ const tenantId = core.getActiveTenantId();
1455
+ if (!tenantId) throw new Error("No active tenant");
1456
+ const ext = file.name.split(".").pop()?.toLowerCase() ?? "bin";
1457
+ const path = `content/${tenantId}/${postId}-${Date.now()}.${ext}`;
1458
+ const { error } = await supabase.storage.from("mkt-media").upload(path, file, { upsert: true });
1459
+ if (error) throw error;
1460
+ const { data } = supabase.storage.from("mkt-media").getPublicUrl(path);
1461
+ return data.publicUrl;
1462
+ }
1463
+ function newId() {
1464
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
1465
+ }
1466
+ function RecordingChecklist({ post }) {
1467
+ const t = core.useTranslation();
1468
+ const savePost = useContentPlannerStore((s) => s.savePost);
1469
+ const [items, setItems] = React8__default.default.useState(post.checklist);
1470
+ const [text, setText] = React8__default.default.useState("");
1471
+ React8__default.default.useEffect(() => {
1472
+ setItems(post.checklist);
1473
+ }, [post.id]);
1474
+ const persist = (next) => {
1475
+ setItems(next);
1476
+ void savePost({ id: post.id, checklist: next });
1477
+ };
1478
+ const toggle = (id) => persist(items.map((i) => i.id === id ? { ...i, done: !i.done } : i));
1479
+ const remove = (id) => persist(items.filter((i) => i.id !== id));
1480
+ const add = () => {
1481
+ const value = text.trim();
1482
+ if (!value) return;
1483
+ persist([...items, { id: newId(), text: value, done: false }]);
1484
+ setText("");
1485
+ };
1486
+ const generateFromScript = () => {
1487
+ const takes = [...post.contentMd.matchAll(/^##\s+(TAKE\s+.+)$/gim)].map(
1488
+ (m) => m[1].trim()
1489
+ );
1490
+ const existing = new Set(items.map((i) => i.text));
1491
+ const fresh = takes.filter((take) => !existing.has(take)).map((take) => ({ id: newId(), text: take, done: false }));
1492
+ if (fresh.length > 0) persist([...items, ...fresh]);
1493
+ };
1494
+ const hasTakes = /^##\s+TAKE\s+/im.test(post.contentMd);
1495
+ const done = items.filter((i) => i.done).length;
1496
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1497
+ items.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5", children: [
1498
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [
1499
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1500
+ done,
1501
+ "/",
1502
+ items.length
1503
+ ] }),
1504
+ hasTakes && /* @__PURE__ */ jsxRuntime.jsxs(
1505
+ "button",
1506
+ {
1507
+ type: "button",
1508
+ onClick: generateFromScript,
1509
+ className: "flex items-center gap-1 font-medium text-muted-foreground transition-colors hover:text-foreground",
1510
+ children: [
1511
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "h-3 w-3" }),
1512
+ t("marketing.content.checklistGenerate")
1513
+ ]
1514
+ }
1515
+ )
1516
+ ] }),
1517
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-1 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsxRuntime.jsx(
1518
+ "div",
1519
+ {
1520
+ className: "h-full rounded-full bg-primary transition-[width]",
1521
+ style: { width: items.length ? `${done / items.length * 100}%` : 0 }
1522
+ }
1523
+ ) })
1524
+ ] }),
1525
+ items.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 rounded-card border border-dashed border-border p-3 text-center", children: [
1526
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.checklistEmpty") }),
1527
+ hasTakes && /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: generateFromScript, children: [
1528
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "mr-1.5 h-3.5 w-3.5" }),
1529
+ t("marketing.content.checklistGenerate")
1530
+ ] })
1531
+ ] }),
1532
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-1", children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "group/item flex items-start gap-2.5 rounded-button px-1 py-2 transition-colors hover:bg-muted/40", children: [
1533
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex min-w-0 flex-1 cursor-pointer items-start gap-2.5", children: [
1534
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Checkbox, { checked: item.done, onChange: () => toggle(item.id), className: "mt-0.5" }),
1535
+ /* @__PURE__ */ jsxRuntime.jsx(
1536
+ "span",
1537
+ {
1538
+ className: ui.cn(
1539
+ "min-w-0 flex-1 text-sm leading-snug",
1540
+ item.done ? "text-muted-foreground line-through" : "text-foreground"
1541
+ ),
1542
+ children: item.text
1543
+ }
1544
+ )
1545
+ ] }),
1546
+ /* @__PURE__ */ jsxRuntime.jsx(
1547
+ "button",
1548
+ {
1549
+ type: "button",
1550
+ "aria-label": t("marketing.content.cancel"),
1551
+ onClick: () => remove(item.id),
1552
+ className: "rounded p-0.5 text-muted-foreground/50 opacity-0 transition-opacity hover:text-destructive group-hover/item:opacity-100 focus:opacity-100",
1553
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-3.5 w-3.5" })
1554
+ }
1555
+ )
1556
+ ] }, item.id)) }),
1557
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1558
+ /* @__PURE__ */ jsxRuntime.jsx(
1559
+ ui.Input,
1560
+ {
1561
+ value: text,
1562
+ onChange: (e) => setText(e.target.value),
1563
+ placeholder: t("marketing.content.checklistAddPlaceholder"),
1564
+ onKeyDown: (e) => {
1565
+ if (e.key === "Enter") add();
1566
+ },
1567
+ className: "h-8 text-sm"
1568
+ }
1569
+ ),
1570
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", size: "sm", onClick: add, disabled: !text.trim(), "aria-label": t("marketing.content.checklistAdd"), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" }) })
1571
+ ] })
1572
+ ] });
1573
+ }
1574
+ function toCaption(md) {
1575
+ return md.replace(/^#+\s*/gm, "").replace(/\*\*/g, "").replace(/^[-*]\s+/gm, "").trim();
1576
+ }
1577
+ function isVideo(url) {
1578
+ return !!url && /\.(mp4|webm|mov)($|\?)/i.test(url);
1579
+ }
1580
+ function Media({ mediaUrl, className }) {
1581
+ if (!mediaUrl) {
1582
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: ui.cn("flex aspect-square w-full items-center justify-center bg-muted/40", className), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Image, { className: "h-8 w-8 text-muted-foreground/40" }) });
1583
+ }
1584
+ return isVideo(mediaUrl) ? /* @__PURE__ */ jsxRuntime.jsx("video", { src: mediaUrl, controls: true, className: ui.cn("w-full bg-black object-contain", className) }) : /* @__PURE__ */ jsxRuntime.jsx("img", { src: mediaUrl, alt: "", className: ui.cn("w-full object-cover", className) });
1585
+ }
1586
+ function Avatar({ name, ring }) {
1587
+ return /* @__PURE__ */ jsxRuntime.jsx(
1588
+ "span",
1589
+ {
1590
+ className: ui.cn(
1591
+ "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold text-foreground",
1592
+ ring && "ring-2 ring-offset-2 ring-pink-500/60"
1593
+ ),
1594
+ children: (name.replace(/^@/, "")[0] ?? "?").toUpperCase()
1595
+ }
1596
+ );
1597
+ }
1598
+ function InstagramFrame({ caption, mediaUrl, accountName, accountHandle }) {
1599
+ const t = core.useTranslation();
1600
+ const handle = (accountHandle || accountName).replace(/^@/, "");
1601
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-sm overflow-hidden rounded-card border border-border bg-card", children: [
1602
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [
1603
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: handle, ring: true }),
1604
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate text-sm font-semibold text-foreground", children: handle }),
1605
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreHorizontal, { className: "h-4 w-4 text-muted-foreground" })
1606
+ ] }),
1607
+ /* @__PURE__ */ jsxRuntime.jsx(Media, { mediaUrl }),
1608
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 px-3 py-2.5", children: [
1609
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 text-foreground", children: [
1610
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Heart, { className: "h-6 w-6" }),
1611
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageCircle, { className: "h-6 w-6" }),
1612
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, { className: "h-6 w-6" }),
1613
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Bookmark, { className: "ml-auto h-6 w-6" })
1614
+ ] }),
1615
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "whitespace-pre-wrap text-sm leading-snug text-foreground", children: [
1616
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-1.5 font-semibold", children: handle }),
1617
+ toCaption(caption)
1618
+ ] }),
1619
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: t("marketing.content.previewNow") })
1620
+ ] })
1621
+ ] });
1622
+ }
1623
+ function FacebookFrame({ caption, mediaUrl, accountName }) {
1624
+ const t = core.useTranslation();
1625
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-sm overflow-hidden rounded-card border border-border bg-card", children: [
1626
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [
1627
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: accountName }),
1628
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
1629
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: accountName.replace(/^@/, "") }),
1630
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
1631
+ t("marketing.content.previewNow"),
1632
+ " \xB7 ",
1633
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Globe, { className: "h-3 w-3" })
1634
+ ] })
1635
+ ] }),
1636
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreHorizontal, { className: "h-4 w-4 text-muted-foreground" })
1637
+ ] }),
1638
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "whitespace-pre-wrap px-3 pb-2.5 text-sm leading-snug text-foreground", children: toCaption(caption) }),
1639
+ /* @__PURE__ */ jsxRuntime.jsx(Media, { mediaUrl, className: "aspect-auto" }),
1640
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-around border-t border-border px-3 py-2 text-muted-foreground", children: [
1641
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
1642
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ThumbsUp, { className: "h-4 w-4" }),
1643
+ " ",
1644
+ t("marketing.content.previewLike")
1645
+ ] }),
1646
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
1647
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageCircle, { className: "h-4 w-4" }),
1648
+ " ",
1649
+ t("marketing.content.previewComment")
1650
+ ] }),
1651
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
1652
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Share2, { className: "h-4 w-4" }),
1653
+ " ",
1654
+ t("marketing.content.previewShare")
1655
+ ] })
1656
+ ] })
1657
+ ] });
1658
+ }
1659
+ function LinkedinFrame({ caption, mediaUrl, accountName }) {
1660
+ const t = core.useTranslation();
1661
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-sm overflow-hidden rounded-card border border-border bg-card", children: [
1662
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [
1663
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: accountName }),
1664
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
1665
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: accountName.replace(/^@/, "") }),
1666
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.previewNow") })
1667
+ ] })
1668
+ ] }),
1669
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "whitespace-pre-wrap px-3 pb-2.5 text-sm leading-snug text-foreground", children: toCaption(caption) }),
1670
+ /* @__PURE__ */ jsxRuntime.jsx(Media, { mediaUrl, className: "aspect-auto" }),
1671
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-around border-t border-border px-3 py-2 text-muted-foreground", children: [
1672
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ThumbsUp, { className: "h-4 w-4" }),
1673
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageCircle, { className: "h-4 w-4" }),
1674
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Repeat2, { className: "h-4 w-4" }),
1675
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, { className: "h-4 w-4" })
1676
+ ] })
1677
+ ] });
1678
+ }
1679
+ function XFrame({ caption, mediaUrl, accountName, accountHandle }) {
1680
+ const t = core.useTranslation();
1681
+ const handle = (accountHandle || accountName).replace(/^@/, "");
1682
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto w-full max-w-sm rounded-card border border-border bg-card p-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2.5", children: [
1683
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: handle }),
1684
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
1685
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "truncate text-sm text-foreground", children: [
1686
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold", children: accountName.replace(/^@/, "") }),
1687
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
1688
+ " @",
1689
+ handle,
1690
+ " \xB7 ",
1691
+ t("marketing.content.previewNow")
1692
+ ] })
1693
+ ] }),
1694
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 whitespace-pre-wrap text-sm leading-snug text-foreground", children: toCaption(caption) }),
1695
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 overflow-hidden rounded-xl border border-border", children: /* @__PURE__ */ jsxRuntime.jsx(Media, { mediaUrl, className: "aspect-auto" }) }),
1696
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex items-center justify-between pr-6 text-muted-foreground", children: [
1697
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageCircle, { className: "h-4 w-4" }),
1698
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Repeat2, { className: "h-4 w-4" }),
1699
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Heart, { className: "h-4 w-4" }),
1700
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.BarChart2, { className: "h-4 w-4" }),
1701
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Share2, { className: "h-4 w-4" })
1702
+ ] })
1703
+ ] })
1704
+ ] }) });
1705
+ }
1706
+ var FRAMES = {
1707
+ instagram: InstagramFrame,
1708
+ facebook: FacebookFrame,
1709
+ linkedin: LinkedinFrame,
1710
+ x: XFrame
1711
+ };
1712
+ function PostSocialPreview(props) {
1713
+ const platforms = props.platforms.length > 0 ? props.platforms : ["instagram"];
1714
+ const [selected, setSelected] = React8__default.default.useState(platforms[0]);
1715
+ const active = platforms.includes(selected) ? selected : platforms[0];
1716
+ const Frame = FRAMES[active] ?? InstagramFrame;
1717
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 rounded-md border border-border bg-muted/20 p-4", children: [
1718
+ platforms.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center gap-1", children: platforms.map((platform) => /* @__PURE__ */ jsxRuntime.jsxs(
1719
+ "button",
1720
+ {
1721
+ type: "button",
1722
+ onClick: () => setSelected(platform),
1723
+ "aria-pressed": active === platform,
1724
+ className: ui.cn(
1725
+ "flex items-center gap-1.5 rounded-button px-2.5 py-1.5 text-xs font-medium capitalize transition-colors",
1726
+ active === platform ? "bg-card text-foreground shadow-button" : "text-muted-foreground hover:text-foreground"
1727
+ ),
1728
+ children: [
1729
+ /* @__PURE__ */ jsxRuntime.jsx(PlatformIcon, { platform, className: "h-3.5 w-3.5" }),
1730
+ platform
1731
+ ]
1732
+ },
1733
+ platform
1734
+ )) }),
1735
+ /* @__PURE__ */ jsxRuntime.jsx(Frame, { ...props })
1736
+ ] });
1737
+ }
1738
+
1739
+ // src/views/content/templates.ts
1740
+ var reel = (title) => `# ${title}
1741
+
1742
+ **TEMA:** ${title.toUpperCase()}
1743
+ **Formato:** Reel \xB7 **Dura\xE7\xE3o:** 45 a 60 segundos
1744
+
1745
+ ---
1746
+
1747
+ ## TAKE 1 \u2013 GANCHO (0s a 5s)
1748
+
1749
+ *Dire\xE7\xE3o de cena: olhando para a c\xE2mera, energia alta.*
1750
+
1751
+ **Texto na tela:** \u2026
1752
+
1753
+ **Fala:** "\u2026"
1754
+
1755
+ ---
1756
+
1757
+ ## TAKE 2 \u2013 DESENVOLVIMENTO (5s a 40s)
1758
+
1759
+ *Dire\xE7\xE3o de cena: \u2026*
1760
+
1761
+ **Texto na tela:** \u2026
1762
+
1763
+ **Fala:** "\u2026"
1764
+
1765
+ ---
1766
+
1767
+ ## TAKE 3 \u2013 CTA (40s a 60s)
1768
+
1769
+ **Fala:** "\u2026"
1770
+
1771
+ **CTA:** \u2026
1772
+ `;
1773
+ var staticPost = (title) => `# ${title}
1774
+
1775
+ **TEMA:** ${title.toUpperCase()}
1776
+ **Formato:** Est\xE1tico
1777
+
1778
+ ## Texto da arte
1779
+
1780
+ \u2026
1781
+
1782
+ ## Legenda
1783
+
1784
+ \u2026
1785
+
1786
+ **CTA:** \u2026
1787
+ `;
1788
+ var carousel = (title) => `# ${title}
1789
+
1790
+ **TEMA:** ${title.toUpperCase()}
1791
+ **Formato:** Carrossel
1792
+
1793
+ ## Slide 1 \u2014 Capa (gancho)
1794
+
1795
+ \u2026
1796
+
1797
+ ## Slide 2
1798
+
1799
+ \u2026
1800
+
1801
+ ## Slide 3
1802
+
1803
+ \u2026
1804
+
1805
+ ## \xDAltimo slide \u2014 CTA
1806
+
1807
+ \u2026
1808
+ `;
1809
+ var story = (title) => `# ${title}
1810
+
1811
+ **TEMA:** ${title.toUpperCase()}
1812
+ **Formato:** Story
1813
+
1814
+ ## Frame 1
1815
+
1816
+ \u2026
1817
+
1818
+ ## Frame 2
1819
+
1820
+ \u2026
1821
+
1822
+ ## Frame final \u2014 CTA
1823
+
1824
+ *Sticker de link / caixa de pergunta / enquete.*
1825
+
1826
+ \u2026
1827
+ `;
1828
+ var video = (title) => `# ${title}
1829
+
1830
+ **TEMA:** ${title.toUpperCase()}
1831
+ **Formato:** V\xEDdeo longo (YouTube) \xB7 **Dura\xE7\xE3o:** 8 a 12 minutos
1832
+
1833
+ ## Gancho (0s a 30s)
1834
+
1835
+ \u2026
1836
+
1837
+ ## Roteiro por blocos
1838
+
1839
+ ### Bloco 1 \u2014 Contexto
1840
+
1841
+ \u2026
1842
+
1843
+ ### Bloco 2 \u2014 Desenvolvimento
1844
+
1845
+ \u2026
1846
+
1847
+ ### Bloco 3 \u2014 Fechamento
1848
+
1849
+ \u2026
1850
+
1851
+ **CTA:** inscreva-se / comente / link na descri\xE7\xE3o
1852
+ `;
1853
+ var live = (title) => `# ${title}
1854
+
1855
+ **TEMA:** ${title.toUpperCase()}
1856
+ **Formato:** Live
1857
+
1858
+ ## Pauta
1859
+
1860
+ 1. Abertura e aquecimento (5 min)
1861
+ 2. \u2026
1862
+ 3. Perguntas e respostas (15 min)
1863
+
1864
+ ## Avisos / CTAs durante a live
1865
+
1866
+ \u2026
1867
+ `;
1868
+ var article = (title) => `# ${title}
1869
+
1870
+ **TEMA:** ${title.toUpperCase()}
1871
+ **Formato:** Artigo / Newsletter
1872
+
1873
+ ## T\xEDtulo de trabalho
1874
+
1875
+ \u2026
1876
+
1877
+ ## Estrutura
1878
+
1879
+ - Abertura (dor / promessa)
1880
+ - Desenvolvimento em 3 pontos
1881
+ - Conclus\xE3o + CTA
1882
+
1883
+ ## Rascunho
1884
+
1885
+ \u2026
1886
+ `;
1887
+ var POST_TEMPLATES = {
1888
+ reel,
1889
+ static: staticPost,
1890
+ carousel,
1891
+ story,
1892
+ video,
1893
+ live,
1894
+ article
1895
+ };
1896
+ function MediaAsset({ post, onChanged }) {
1897
+ const t = core.useTranslation();
1898
+ const inputRef = React8__default.default.useRef(null);
1899
+ const [busy, setBusy] = React8__default.default.useState(false);
1900
+ const isVideo2 = post.mediaUrl ? /\.(mp4|webm|mov)($|\?)/i.test(post.mediaUrl) : false;
1901
+ const pick = () => inputRef.current?.click();
1902
+ const upload = async (file) => {
1903
+ setBusy(true);
1904
+ try {
1905
+ const url = await uploadPostMedia(post.id, file);
1906
+ onChanged(url);
1907
+ } finally {
1908
+ setBusy(false);
1909
+ }
1910
+ };
1911
+ if (!canUploadMedia()) {
1912
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.mediaUnavailable") });
1913
+ }
1914
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
1915
+ /* @__PURE__ */ jsxRuntime.jsx(
1916
+ "input",
1917
+ {
1918
+ ref: inputRef,
1919
+ type: "file",
1920
+ accept: "image/*,video/*",
1921
+ className: "hidden",
1922
+ onChange: (e) => {
1923
+ const file = e.target.files?.[0];
1924
+ if (file) void upload(file);
1925
+ e.target.value = "";
1926
+ }
1927
+ }
1928
+ ),
1929
+ post.mediaUrl ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative overflow-hidden rounded-card border border-border bg-muted/20", children: [
1930
+ isVideo2 ? /* @__PURE__ */ jsxRuntime.jsx("video", { src: post.mediaUrl, controls: true, className: "max-h-72 w-full object-contain" }) : /* @__PURE__ */ jsxRuntime.jsx("img", { src: post.mediaUrl, alt: "", className: "max-h-72 w-full object-contain" }),
1931
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-border p-2", children: [
1932
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: pick, disabled: busy, children: [
1933
+ busy ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "mr-1.5 h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ImagePlus, { className: "mr-1.5 h-3.5 w-3.5" }),
1934
+ t("marketing.content.mediaReplace")
1935
+ ] }),
1936
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "ghost", size: "sm", onClick: () => onChanged(null), disabled: busy, children: [
1937
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "mr-1 h-3.5 w-3.5" }),
1938
+ t("marketing.content.mediaRemove")
358
1939
  ] })
359
1940
  ] })
360
- ] }),
361
- /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: campaigns, emptyMessage: t("marketing.campaigns.empty") }),
362
- /* @__PURE__ */ jsxRuntime.jsx(CampaignComposer, { open: composerOpen, onClose: () => setComposerOpen(false) })
1941
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
1942
+ "button",
1943
+ {
1944
+ type: "button",
1945
+ onClick: pick,
1946
+ disabled: busy,
1947
+ className: "flex w-full flex-col items-center justify-center gap-2 rounded-card border border-dashed border-border p-6 text-muted-foreground transition-colors hover:bg-muted/30 hover:text-foreground",
1948
+ children: [
1949
+ busy ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-5 w-5 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ImagePlus, { className: "h-5 w-5" }),
1950
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: t("marketing.content.mediaUpload") }),
1951
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs", children: t("marketing.content.mediaHint") })
1952
+ ]
1953
+ }
1954
+ )
363
1955
  ] });
364
1956
  }
365
- function FunnelView() {
1957
+ function toDraft2(post) {
1958
+ return {
1959
+ title: post.title,
1960
+ format: post.format,
1961
+ status: post.status,
1962
+ scheduledDate: post.scheduledDate ?? "",
1963
+ platforms: post.platforms,
1964
+ hook: post.hook ?? "",
1965
+ cta: post.cta ?? "",
1966
+ contentMd: post.contentMd
1967
+ };
1968
+ }
1969
+ function PostPage({ postId, onBack }) {
366
1970
  const t = core.useTranslation();
367
- const { conversion } = useMarketingConfig();
368
- const funnel = useMarketingStore((s) => s.funnel);
369
- const max = Math.max(...funnel.map((s) => s.count), 1);
370
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-4xl space-y-4", children: [
371
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
372
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground", children: [
373
- t("marketing.funnel.subtitlePrefix"),
374
- " ",
375
- conversion.label.toLowerCase()
1971
+ const provider = useContentPlannerProvider();
1972
+ const storePost = useContentPlannerStore((s) => s.posts.find((p) => p.id === postId));
1973
+ const savePost = useContentPlannerStore((s) => s.savePost);
1974
+ const deletePost = useContentPlannerStore((s) => s.deletePost);
1975
+ const accounts = useContentPlannerStore((s) => s.accounts);
1976
+ const activeAccountId = useContentPlannerStore((s) => s.activeAccountId);
1977
+ const [post, setPost] = React8__default.default.useState(storePost ?? null);
1978
+ const [draft, setDraft] = React8__default.default.useState(storePost ? toDraft2(storePost) : null);
1979
+ const [saving, setSaving] = React8__default.default.useState(false);
1980
+ const [confirmDelete, setConfirmDelete] = React8__default.default.useState(false);
1981
+ React8__default.default.useEffect(() => {
1982
+ if (post?.id === postId) return;
1983
+ if (storePost) {
1984
+ setPost(storePost);
1985
+ setDraft(toDraft2(storePost));
1986
+ return;
1987
+ }
1988
+ void provider.getPost(postId).then((fetched) => {
1989
+ if (fetched) {
1990
+ setPost(fetched);
1991
+ setDraft(toDraft2(fetched));
1992
+ }
1993
+ });
1994
+ }, [postId, storePost, post?.id, provider]);
1995
+ if (!post || !draft) {
1996
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
1997
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
1998
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-56" }),
1999
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-40" })
376
2000
  ] }),
377
- /* @__PURE__ */ jsxRuntime.jsx(RangeTabs, {})
378
- ] }),
379
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-card border border-border bg-card p-5", children: [
380
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-3", children: funnel.map((stage, i) => {
381
- const pct = max > 0 ? Math.max(stage.count / max * 100, 4) : 0;
382
- const prev = funnel[i - 1];
383
- const stepRate = prev && prev.count > 0 ? stage.count / prev.count * 100 : null;
384
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
385
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-1 flex items-center justify-between text-sm", children: [
386
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: stage.label }),
387
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
388
- formatNumber(stage.count),
389
- stepRate != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-2 text-xs", children: [
390
- "(",
391
- formatPercent(stepRate, 0),
392
- " ",
393
- t("marketing.funnel.ofPrev"),
394
- ")"
395
- ] })
2001
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-10 w-full" }),
2002
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: Array.from({ length: 3 }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-14" }, i)) }),
2003
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-64 w-full" })
2004
+ ] });
2005
+ }
2006
+ const dirty = draft.title !== post.title || draft.format !== post.format || draft.status !== post.status || draft.scheduledDate !== (post.scheduledDate ?? "") || draft.platforms.join(",") !== post.platforms.join(",") || draft.hook !== (post.hook ?? "") || draft.cta !== (post.cta ?? "") || draft.contentMd !== post.contentMd;
2007
+ const patch = (partial) => setDraft((d) => d ? { ...d, ...partial } : d);
2008
+ const save = async () => {
2009
+ setSaving(true);
2010
+ try {
2011
+ const updated = await savePost({
2012
+ id: post.id,
2013
+ title: draft.title,
2014
+ format: draft.format,
2015
+ status: draft.status,
2016
+ scheduledDate: draft.scheduledDate || null,
2017
+ platforms: draft.platforms,
2018
+ hook: draft.hook,
2019
+ cta: draft.cta,
2020
+ contentMd: draft.contentMd
2021
+ });
2022
+ setPost(updated);
2023
+ setDraft(toDraft2(updated));
2024
+ } finally {
2025
+ setSaving(false);
2026
+ }
2027
+ };
2028
+ const insertTemplate = () => {
2029
+ patch({ contentMd: POST_TEMPLATES[draft.format](draft.title || t("marketing.content.untitled")) });
2030
+ };
2031
+ const isStatic = draft.format === "static";
2032
+ const account = accounts.find((a) => a.id === activeAccountId) ?? null;
2033
+ const now = /* @__PURE__ */ new Date();
2034
+ const todayISO = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
2035
+ const isReleaseDay = !!post.scheduledDate && post.scheduledDate === todayISO;
2036
+ const handleMediaChanged = (mediaUrl) => {
2037
+ void savePost({ id: post.id, mediaUrl }).then((updated) => {
2038
+ setPost(updated);
2039
+ setDraft((d) => d ? { ...d } : d);
2040
+ });
2041
+ };
2042
+ return /* @__PURE__ */ jsxRuntime.jsx(
2043
+ ui.ContentSplit,
2044
+ {
2045
+ className: "mx-auto max-w-5xl",
2046
+ aside: /* @__PURE__ */ jsxRuntime.jsx(RecordingChecklist, { post }),
2047
+ asideTitle: t("marketing.content.checklistTitle"),
2048
+ asideDescription: t("marketing.content.checklistDescription"),
2049
+ collapsible: true,
2050
+ defaultCollapsed: !isReleaseDay,
2051
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
2052
+ /* @__PURE__ */ jsxRuntime.jsx(
2053
+ ui.SubpageHeader,
2054
+ {
2055
+ title: draft.title || t("marketing.content.untitled"),
2056
+ parentLabel: t("marketing.content.title"),
2057
+ onBack,
2058
+ actions: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2059
+ /* @__PURE__ */ jsxRuntime.jsx(ui.ContentSplitTrigger, { label: t("marketing.content.checklistOpen") }),
2060
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", size: "sm", onClick: () => setConfirmDelete(true), children: t("marketing.content.deletePost") }),
2061
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "sm", onClick: () => void save(), disabled: !dirty || saving, children: t("marketing.content.save") })
2062
+ ] })
2063
+ }
2064
+ ),
2065
+ /* @__PURE__ */ jsxRuntime.jsx(
2066
+ ui.Input,
2067
+ {
2068
+ value: draft.title,
2069
+ onChange: (e) => patch({ title: e.target.value }),
2070
+ placeholder: t("marketing.content.titlePlaceholder"),
2071
+ className: "text-base font-medium"
2072
+ }
2073
+ ),
2074
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: [
2075
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
2076
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.formatLabel") }),
2077
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: draft.format, onValueChange: (v) => patch({ format: v }), children: [
2078
+ /* @__PURE__ */ jsxRuntime.jsx(ui.SelectTrigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.SelectValue, {}) }),
2079
+ /* @__PURE__ */ jsxRuntime.jsx(ui.SelectContent, { children: POST_FORMATS.map((f) => /* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: f, children: t(`marketing.content.format.${f}`) }, f)) })
396
2080
  ] })
397
2081
  ] }),
398
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-9 overflow-hidden rounded-md bg-muted/40", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full rounded-md", style: { width: `${pct}%`, backgroundColor: stage.color + "33", borderLeft: `3px solid ${stage.color}` } }) })
399
- ] }, stage.id);
400
- }) }),
401
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-5 border-t border-border pt-4 text-sm", children: [
402
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
403
- t("marketing.funnel.overall"),
404
- ": "
2082
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
2083
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.statusLabel") }),
2084
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: draft.status, onValueChange: (v) => patch({ status: v }), children: [
2085
+ /* @__PURE__ */ jsxRuntime.jsx(ui.SelectTrigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.SelectValue, {}) }),
2086
+ /* @__PURE__ */ jsxRuntime.jsx(ui.SelectContent, { children: POST_STATUS_ORDER.map((s) => /* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: s, children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [
2087
+ /* @__PURE__ */ jsxRuntime.jsx(StatusDot, { status: s }),
2088
+ t(`marketing.content.status.${s}`)
2089
+ ] }) }, s)) })
2090
+ ] })
2091
+ ] }),
2092
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
2093
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.scheduledDate") }),
2094
+ /* @__PURE__ */ jsxRuntime.jsx(
2095
+ ui.Input,
2096
+ {
2097
+ type: "date",
2098
+ value: draft.scheduledDate,
2099
+ onChange: (e) => patch({ scheduledDate: e.target.value })
2100
+ }
2101
+ )
2102
+ ] }),
2103
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1 sm:col-span-2", children: [
2104
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.hook") }),
2105
+ /* @__PURE__ */ jsxRuntime.jsx(
2106
+ ui.Input,
2107
+ {
2108
+ value: draft.hook,
2109
+ onChange: (e) => patch({ hook: e.target.value }),
2110
+ placeholder: t("marketing.content.hookPlaceholder")
2111
+ }
2112
+ )
2113
+ ] }),
2114
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
2115
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.cta") }),
2116
+ /* @__PURE__ */ jsxRuntime.jsx(
2117
+ ui.Input,
2118
+ {
2119
+ value: draft.cta,
2120
+ onChange: (e) => patch({ cta: e.target.value }),
2121
+ placeholder: t("marketing.content.ctaPlaceholder")
2122
+ }
2123
+ )
2124
+ ] }),
2125
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1 sm:col-span-3", children: [
2126
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.platformsLabel") }),
2127
+ /* @__PURE__ */ jsxRuntime.jsx(PlatformPicker, { value: draft.platforms, onChange: (platforms) => patch({ platforms }) })
2128
+ ] })
405
2129
  ] }),
406
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-foreground", children: funnel.length > 1 && funnel[0].count > 0 ? formatPercent(funnel[funnel.length - 1].count / funnel[0].count * 100) : "\u2014" })
2130
+ isStatic && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
2131
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.mediaLabel") }),
2132
+ /* @__PURE__ */ jsxRuntime.jsx(MediaAsset, { post, onChanged: handleMediaChanged })
2133
+ ] }),
2134
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
2135
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
2136
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: isStatic ? t("marketing.content.captionLabel") : t("marketing.content.scriptLabel") }),
2137
+ draft.contentMd.trim() === "" && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", size: "sm", onClick: insertTemplate, children: t("marketing.content.insertTemplate") })
2138
+ ] }),
2139
+ /* @__PURE__ */ jsxRuntime.jsx(
2140
+ ui.MarkdownEditor,
2141
+ {
2142
+ value: draft.contentMd,
2143
+ onChange: (contentMd) => patch({ contentMd }),
2144
+ placeholder: isStatic ? t("marketing.content.captionPlaceholder") : t("marketing.content.bodyPlaceholder"),
2145
+ editLabel: t("marketing.content.edit"),
2146
+ previewLabel: t("marketing.content.preview"),
2147
+ renderPreview: isStatic ? (value) => /* @__PURE__ */ jsxRuntime.jsx(
2148
+ PostSocialPreview,
2149
+ {
2150
+ caption: value,
2151
+ mediaUrl: post.mediaUrl,
2152
+ accountName: account?.name ?? "@conta",
2153
+ accountHandle: account?.handle,
2154
+ platforms: draft.platforms.length > 0 ? draft.platforms : account?.platforms ?? []
2155
+ }
2156
+ ) : void 0
2157
+ }
2158
+ )
2159
+ ] }),
2160
+ /* @__PURE__ */ jsxRuntime.jsx(
2161
+ ui.ConfirmDialog,
2162
+ {
2163
+ open: confirmDelete,
2164
+ variant: "destructive",
2165
+ title: t("marketing.content.deletePost"),
2166
+ description: t("marketing.content.deletePostConfirm"),
2167
+ confirmLabel: t("marketing.content.deletePost"),
2168
+ cancelLabel: t("marketing.content.cancel"),
2169
+ onCancel: () => setConfirmDelete(false),
2170
+ onConfirm: () => {
2171
+ setConfirmDelete(false);
2172
+ void deletePost(post.id).then(onBack);
2173
+ }
2174
+ }
2175
+ )
407
2176
  ] })
408
- ] })
409
- ] });
410
- }
411
- var TYPE_STYLE = {
412
- Funnel: "bg-indigo-100 text-indigo-700",
413
- "Landing page": "bg-pink-100 text-pink-700",
414
- Website: "bg-sky-100 text-sky-700"
415
- };
416
- function LandingPagesView() {
417
- const t = core.useTranslation();
418
- const pages = useMarketingStore((s) => s.landingPages);
419
- const totalVisits = pages.reduce((s, p) => s + p.visits, 0);
420
- const totalConv = pages.reduce((s, p) => s + p.conversions, 0);
421
- const avgCvr = totalVisits > 0 ? totalConv / totalVisits * 100 : 0;
422
- const columns = React4__default.default.useMemo(() => [
423
- { accessorKey: "name", header: t("marketing.col.page"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
424
- { accessorKey: "type", header: t("marketing.col.type"), cell: ({ getValue }) => {
425
- const v = getValue();
426
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `rounded-full px-2 py-0.5 text-xs font-medium ${TYPE_STYLE[v] ?? "bg-muted text-muted-foreground"}`, children: v });
427
- } },
428
- { accessorKey: "visits", header: t("marketing.col.visits"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
429
- { accessorKey: "conversions", header: t("marketing.col.conversions"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: formatNumber(getValue()) }) },
430
- { accessorKey: "cvr", header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) }
431
- ], [t]);
432
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
433
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
434
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.landing.subtitle") }),
435
- /* @__PURE__ */ jsxRuntime.jsx(RangeTabs, {})
436
- ] }),
437
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-3 gap-4", children: [
438
- /* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.MousePointerClick, label: t("marketing.metric.totalVisits"), value: formatNumber(totalVisits) }),
439
- /* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.Layers, label: t("marketing.metric.pages"), value: String(pages.length) }),
440
- /* @__PURE__ */ jsxRuntime.jsx(KpiCard, { icon: lucideReact.Percent, label: t("marketing.metric.avgConversion"), value: formatPercent(avgCvr) })
441
- ] }),
442
- /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: pages, emptyMessage: t("marketing.landing.subtitle") })
443
- ] });
2177
+ },
2178
+ post.id
2179
+ );
444
2180
  }
445
2181
  function buildNav(config, view, navigate) {
446
2182
  const items = [
@@ -464,9 +2200,18 @@ function buildNav(config, view, navigate) {
464
2200
  if (config.modules.landingPages) {
465
2201
  items.push({ id: "landing-pages", label: config.labels.landingPages, icon: "LayoutTemplate", active: view === "landing-pages", onClick: () => navigate("landing-pages") });
466
2202
  }
2203
+ if (config.modules.contentPlanner) {
2204
+ items.push({
2205
+ id: "content",
2206
+ label: config.labels.content,
2207
+ icon: "Clapperboard",
2208
+ active: view === "content" || view === "content-brief" || view.startsWith("content-post:"),
2209
+ onClick: () => navigate("content")
2210
+ });
2211
+ }
467
2212
  return items;
468
2213
  }
469
- function MarketingPage({ config, provider, store }) {
2214
+ function MarketingPage({ config, provider, store, contentProvider, contentStore }) {
470
2215
  const t = core.useTranslation();
471
2216
  const { view, direction, navigate } = saas.useModuleNavigation("/marketing", {
472
2217
  overview: 0,
@@ -474,14 +2219,17 @@ function MarketingPage({ config, provider, store }) {
474
2219
  campaigns: 0,
475
2220
  funnel: 0,
476
2221
  "landing-pages": 0,
477
- "channel-detail": 1
2222
+ content: 0,
2223
+ "channel-detail": 1,
2224
+ "content-post": 1,
2225
+ "content-brief": 1
478
2226
  }, "overview");
479
- React4__default.default.useEffect(() => {
2227
+ React8__default.default.useEffect(() => {
480
2228
  void store.getState().load();
481
2229
  }, [store]);
482
2230
  const isOverview = view === "overview";
483
2231
  const nav = buildNav(config, view, navigate);
484
- const quickActions = React4__default.default.useMemo(() => [
2232
+ const quickActions = React8__default.default.useMemo(() => [
485
2233
  { id: "new-campaign", label: t("marketing.campaigns.new"), icon: "Megaphone", description: config.labels.campaigns, action: () => navigate("campaigns") }
486
2234
  ], [t, config.labels.campaigns]);
487
2235
  const renderView = saas.createViewRouter([
@@ -490,9 +2238,12 @@ function MarketingPage({ config, provider, store }) {
490
2238
  { id: "campaigns", render: () => /* @__PURE__ */ jsxRuntime.jsx(CampaignsView, {}) },
491
2239
  { id: "funnel", render: () => /* @__PURE__ */ jsxRuntime.jsx(FunnelView, {}) },
492
2240
  { id: "landing-pages", render: () => /* @__PURE__ */ jsxRuntime.jsx(LandingPagesView, {}) },
2241
+ { id: "content", render: () => /* @__PURE__ */ jsxRuntime.jsx(ContentView, { onOpenPost: (id) => navigate(`content-post:${id}`) }) },
2242
+ { id: "content-post", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(PostPage, { postId: id, onBack: () => navigate("content") }) },
2243
+ { id: "content-brief", render: () => /* @__PURE__ */ jsxRuntime.jsx(PlanBriefView, { onBack: () => navigate("content") }) },
493
2244
  { id: "overview", render: () => /* @__PURE__ */ jsxRuntime.jsx(OverviewView, {}) }
494
2245
  ], "overview");
495
- return /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(
2246
+ return /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsxRuntime.jsx(
496
2247
  ui.ModulePage,
497
2248
  {
498
2249
  title: config.labels.pageTitle,
@@ -511,7 +2262,7 @@ function MarketingPage({ config, provider, store }) {
511
2262
  ),
512
2263
  children: renderView(view)
513
2264
  }
514
- ) });
2265
+ ) }) });
515
2266
  }
516
2267
  var SOURCE_OPTIONS = [
517
2268
  { value: "crm", label: "CRM" },
@@ -522,11 +2273,11 @@ var SOURCE_OPTIONS = [
522
2273
  function SettingsView() {
523
2274
  const t = core.useTranslation();
524
2275
  const { conversion, channels } = useMarketingConfig();
525
- const [source, setSource] = React4__default.default.useState(conversion.source);
526
- const [trackValue, setTrackValue] = React4__default.default.useState(true);
527
- const [autoSync, setAutoSync] = React4__default.default.useState(true);
528
- const [assisted, setAssisted] = React4__default.default.useState(false);
529
- const [tracked, setTracked] = React4__default.default.useState(
2276
+ const [source, setSource] = React8__default.default.useState(conversion.source);
2277
+ const [trackValue, setTrackValue] = React8__default.default.useState(true);
2278
+ const [autoSync, setAutoSync] = React8__default.default.useState(true);
2279
+ const [assisted, setAssisted] = React8__default.default.useState(false);
2280
+ const [tracked, setTracked] = React8__default.default.useState(
530
2281
  () => Object.fromEntries(channels.map((c) => [c.id, true]))
531
2282
  );
532
2283
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
@@ -623,7 +2374,7 @@ function useEnsureMarketingData() {
623
2374
  const overview = useMarketingStore((s) => s.overview);
624
2375
  const loading = useMarketingStore((s) => s.loading);
625
2376
  const load = useMarketingStore((s) => s.load);
626
- React4.useEffect(() => {
2377
+ React8.useEffect(() => {
627
2378
  if (!overview && !loading) void load();
628
2379
  }, []);
629
2380
  }
@@ -729,9 +2480,9 @@ function CampaignsTable() {
729
2480
  const recent = campaigns.filter((c) => c.status !== "draft").slice(0, 5);
730
2481
  return /* @__PURE__ */ jsxRuntime.jsx(ui.TableWidget, { title: t("marketing.overview.recent"), icon: "Megaphone", columns, data: recent, emptyMessage: t("marketing.campaigns.empty") });
731
2482
  }
732
- function createMarketingDashboardWidgets(ctx2) {
2483
+ function createMarketingDashboardWidgets(ctx3) {
733
2484
  const withCtx = (Inner) => {
734
- const Wrapped = () => /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config: ctx2.config, provider: ctx2.provider, store: ctx2.store, children: /* @__PURE__ */ jsxRuntime.jsx(Inner, {}) });
2485
+ const Wrapped = () => /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config: ctx3.config, provider: ctx3.provider, store: ctx3.store, children: /* @__PURE__ */ jsxRuntime.jsx(Inner, {}) });
735
2486
  Wrapped.displayName = `MarketingWidget(${Inner.displayName ?? Inner.name})`;
736
2487
  return Wrapped;
737
2488
  };
@@ -767,7 +2518,7 @@ var AGENCY = {
767
2518
  { id: "referral", label: "Referral", icon: "Users", kind: "referral" },
768
2519
  { id: "direct", label: "Direct", icon: "MousePointerClick", kind: "direct" }
769
2520
  ],
770
- modules: { channels: true, campaigns: true, funnel: true, landingPages: true, broadcasts: false, journeys: false }
2521
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, broadcasts: false, journeys: false }
771
2522
  };
772
2523
  var BEAUTY = {
773
2524
  conversion: {
@@ -785,7 +2536,7 @@ var BEAUTY = {
785
2536
  { id: "whatsapp", label: "WhatsApp", icon: "MessageCircle", kind: "social" },
786
2537
  { id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
787
2538
  ],
788
- modules: { channels: true, campaigns: true, funnel: true, landingPages: false, broadcasts: false, journeys: false }
2539
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, broadcasts: false, journeys: false }
789
2540
  };
790
2541
  var RESTO = {
791
2542
  conversion: {
@@ -803,7 +2554,7 @@ var RESTO = {
803
2554
  { id: "instagram", label: "Instagram", icon: "Instagram", kind: "social" },
804
2555
  { id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
805
2556
  ],
806
- modules: { channels: true, campaigns: true, funnel: true, landingPages: false, broadcasts: false, journeys: false }
2557
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, broadcasts: false, journeys: false }
807
2558
  };
808
2559
  var MARKETING_PRESETS = {
809
2560
  agency: AGENCY,
@@ -942,7 +2693,7 @@ function createMockMarketingProvider(cfg) {
942
2693
  const base = channelBase30(cfg.channels, cfg.conversion);
943
2694
  const campaigns = seedCampaigns(cfg);
944
2695
  const landing = seedLandingPages();
945
- let counter = 100;
2696
+ let counter2 = 100;
946
2697
  async function channelPerformance(query) {
947
2698
  const list = base.map((b) => toPerformance(b, query.range));
948
2699
  return query.channelId ? list.filter((c) => c.channelId === query.channelId) : list;
@@ -1007,7 +2758,7 @@ function createMockMarketingProvider(cfg) {
1007
2758
  const b = base.find((x) => x.channelId === input.channelId) ?? base[0];
1008
2759
  const active = input.status === "active";
1009
2760
  const created = {
1010
- id: `cmp-${++counter}`,
2761
+ id: `cmp-${++counter2}`,
1011
2762
  name: input.name,
1012
2763
  channelId: input.channelId,
1013
2764
  status: input.status,
@@ -1036,6 +2787,434 @@ function createMockMarketingProvider(cfg) {
1036
2787
  }
1037
2788
  };
1038
2789
  }
2790
+
2791
+ // src/data/contentMock.ts
2792
+ var counter = 0;
2793
+ var nextId = (prefix) => `${prefix}-${++counter}`;
2794
+ var SAMPLE_SCRIPT = `# 5 sinais de que sua queda capilar n\xE3o \xE9 normal
2795
+
2796
+ **TEMA:** 5 SINAIS DE QUE SUA QUEDA CAPILAR N\xC3O \xC9 NORMAL
2797
+ **Formato:** Reel \xB7 **Dura\xE7\xE3o:** 45 a 60 segundos
2798
+
2799
+ ---
2800
+
2801
+ ## TAKE 1 \u2013 GANCHO (0s a 5s)
2802
+
2803
+ *Dra. andando at\xE9 a c\xE2mera.*
2804
+
2805
+ **Texto na tela:** \u26A0\uFE0F 5 sinais de alerta
2806
+
2807
+ **Fala:** "Seu cabelo est\xE1 caindo e voc\xEA n\xE3o sabe se isso \xE9 normal? Existem sinais que merecem aten\xE7\xE3o."
2808
+
2809
+ ---
2810
+
2811
+ ## TAKE 2 \u2013 SINAL 1 (5s a 12s)
2812
+
2813
+ *Mostrar a Dra. segurando alguns fios de cabelo ou imagem ilustrativa.*
2814
+
2815
+ **Texto na tela:** Excesso de fios caindo.
2816
+
2817
+ **Fala:** "Primeiro sinal: voc\xEA percebe muitos fios no travesseiro, no banho ou ao pentear os cabelos."
2818
+
2819
+ ---
2820
+
2821
+ ## TAKE 3 \u2013 CTA (40s a 60s)
2822
+
2823
+ **Fala:** "Se voc\xEA se identificou com algum desses sinais, agende uma avalia\xE7\xE3o capilar."
2824
+
2825
+ **CTA:** Agende sua avalia\xE7\xE3o
2826
+ `;
2827
+ var WEEK_TITLES = [
2828
+ ["5 sinais de que sua queda capilar n\xE3o \xE9 normal", "Antes e depois de tratamento capilar"],
2829
+ ["O que \xE9 DHT?", "Tour pela cl\xEDnica"],
2830
+ ["3 erros que aceleram a calv\xEDcie", "Depoimento de paciente"],
2831
+ ["Como funciona uma avalia\xE7\xE3o capilar", "Mitos sobre queda de cabelo"],
2832
+ ["Por que seu tratamento anterior n\xE3o funcionou?", "Ozonioterapia para gordura localizada"],
2833
+ ["Diferen\xE7a entre dermatologista e tricologista", "Caso real de evolu\xE7\xE3o"],
2834
+ ["Alopecia feminina explicada", "Bastidores da cl\xEDnica"],
2835
+ ["Quando procurar ajuda para queda capilar", "Convite para avalia\xE7\xE3o gratuita"]
2836
+ ];
2837
+ function buildSeed() {
2838
+ const account = {
2839
+ id: nextId("acc"),
2840
+ name: "Conta demo",
2841
+ handle: "@clinica.demo",
2842
+ platforms: ["instagram"],
2843
+ isActive: true
2844
+ };
2845
+ const plan = {
2846
+ id: nextId("plan"),
2847
+ accountId: account.id,
2848
+ name: "Plano de conte\xFAdo \u2014 8 semanas",
2849
+ status: "active",
2850
+ weeksCount: 8,
2851
+ objective: "Atrair pacientes qualificados para avalia\xE7\xE3o capilar",
2852
+ tone: "Autoridade acess\xEDvel \u2014 t\xE9cnica, mas em linguagem simples",
2853
+ pillars: ["Educa\xE7\xE3o", "Autoridade", "Prova social", "Bastidores", "Convers\xE3o"],
2854
+ formats: ["reel", "static"],
2855
+ weeklyFrequency: 3,
2856
+ briefMd: "## Objetivo\n\nPosicionar a especialista como refer\xEAncia em sa\xFAde capilar e gerar agendamentos de avalia\xE7\xE3o.\n\n## Estrat\xE9gia\n\n- 2 reels educativos/prova social por semana\n- 1 post est\xE1tico por semana\n- CTA recorrente: agendar avalia\xE7\xE3o"
2857
+ };
2858
+ const posts = [];
2859
+ WEEK_TITLES.forEach(([reel1, reel2], i) => {
2860
+ const week = i + 1;
2861
+ posts.push(
2862
+ {
2863
+ id: nextId("post"),
2864
+ planId: plan.id,
2865
+ weekNumber: week,
2866
+ position: 0,
2867
+ title: reel1,
2868
+ format: "reel",
2869
+ status: week === 1 ? "script" : "idea",
2870
+ platforms: [],
2871
+ checklist: [],
2872
+ contentMd: week === 1 ? SAMPLE_SCRIPT : ""
2873
+ },
2874
+ {
2875
+ id: nextId("post"),
2876
+ planId: plan.id,
2877
+ weekNumber: week,
2878
+ position: 1,
2879
+ title: reel2,
2880
+ format: "reel",
2881
+ status: "idea",
2882
+ platforms: [],
2883
+ checklist: [],
2884
+ contentMd: ""
2885
+ },
2886
+ {
2887
+ id: nextId("post"),
2888
+ planId: plan.id,
2889
+ weekNumber: week,
2890
+ position: 2,
2891
+ title: "Est\xE1tico",
2892
+ format: "static",
2893
+ status: "idea",
2894
+ platforms: [],
2895
+ checklist: [],
2896
+ contentMd: ""
2897
+ }
2898
+ );
2899
+ });
2900
+ return { accounts: [account], plans: [plan], posts };
2901
+ }
2902
+ function createMockContentPlannerProvider() {
2903
+ const db = buildSeed();
2904
+ return {
2905
+ async listAccounts() {
2906
+ return [...db.accounts];
2907
+ },
2908
+ async saveAccount(input) {
2909
+ if (input.id) {
2910
+ const acc2 = db.accounts.find((a) => a.id === input.id);
2911
+ if (!acc2) throw new Error("Account not found");
2912
+ Object.assign(acc2, {
2913
+ name: input.name ?? acc2.name,
2914
+ handle: input.handle ?? acc2.handle,
2915
+ platforms: input.platforms ?? acc2.platforms,
2916
+ isActive: input.isActive ?? acc2.isActive
2917
+ });
2918
+ return { ...acc2 };
2919
+ }
2920
+ const acc = {
2921
+ id: nextId("acc"),
2922
+ name: input.name,
2923
+ handle: input.handle,
2924
+ platforms: input.platforms ?? ["instagram"],
2925
+ isActive: input.isActive ?? true
2926
+ };
2927
+ db.accounts.push(acc);
2928
+ return { ...acc };
2929
+ },
2930
+ async deleteAccount(id) {
2931
+ db.accounts = db.accounts.filter((a) => a.id !== id);
2932
+ const planIds = new Set(db.plans.filter((p) => p.accountId === id).map((p) => p.id));
2933
+ db.plans = db.plans.filter((p) => p.accountId !== id);
2934
+ db.posts = db.posts.filter((p) => !planIds.has(p.planId));
2935
+ },
2936
+ async listPlans(accountId) {
2937
+ return db.plans.filter((p) => p.accountId === accountId).map((p) => ({ ...p }));
2938
+ },
2939
+ async getPlan(id) {
2940
+ const plan = db.plans.find((p) => p.id === id);
2941
+ return plan ? { ...plan } : null;
2942
+ },
2943
+ async savePlan(input) {
2944
+ if (input.id) {
2945
+ const plan2 = db.plans.find((p) => p.id === input.id);
2946
+ if (!plan2) throw new Error("Plan not found");
2947
+ Object.assign(plan2, {
2948
+ ...input.name !== void 0 && { name: input.name },
2949
+ ...input.status !== void 0 && { status: input.status },
2950
+ ...input.weeksCount !== void 0 && { weeksCount: input.weeksCount },
2951
+ ...input.startDate !== void 0 && { startDate: input.startDate ?? void 0 },
2952
+ ...input.objective !== void 0 && { objective: input.objective },
2953
+ ...input.tone !== void 0 && { tone: input.tone },
2954
+ ...input.pillars !== void 0 && { pillars: input.pillars },
2955
+ ...input.formats !== void 0 && { formats: input.formats },
2956
+ ...input.weeklyFrequency !== void 0 && { weeklyFrequency: input.weeklyFrequency },
2957
+ ...input.briefMd !== void 0 && { briefMd: input.briefMd }
2958
+ });
2959
+ return { ...plan2 };
2960
+ }
2961
+ if (!input.accountId) throw new Error("accountId is required");
2962
+ const plan = {
2963
+ id: nextId("plan"),
2964
+ accountId: input.accountId,
2965
+ name: input.name ?? "Novo plano",
2966
+ status: input.status ?? "draft",
2967
+ weeksCount: input.weeksCount ?? 4,
2968
+ startDate: input.startDate ?? void 0,
2969
+ objective: input.objective,
2970
+ tone: input.tone,
2971
+ pillars: input.pillars ?? [],
2972
+ formats: input.formats ?? ["reel", "static"],
2973
+ weeklyFrequency: input.weeklyFrequency ?? 3,
2974
+ briefMd: input.briefMd ?? ""
2975
+ };
2976
+ db.plans.push(plan);
2977
+ return { ...plan };
2978
+ },
2979
+ async deletePlan(id) {
2980
+ db.plans = db.plans.filter((p) => p.id !== id);
2981
+ db.posts = db.posts.filter((p) => p.planId !== id);
2982
+ },
2983
+ async listPosts(planId) {
2984
+ return db.posts.filter((p) => p.planId === planId).sort((a, b) => a.weekNumber - b.weekNumber || a.position - b.position).map((p) => ({ ...p }));
2985
+ },
2986
+ async getPost(id) {
2987
+ const post = db.posts.find((p) => p.id === id);
2988
+ return post ? { ...post } : null;
2989
+ },
2990
+ async savePost(input) {
2991
+ if (input.id) {
2992
+ const post2 = db.posts.find((p) => p.id === input.id);
2993
+ if (!post2) throw new Error("Post not found");
2994
+ Object.assign(post2, {
2995
+ ...input.weekNumber !== void 0 && { weekNumber: input.weekNumber },
2996
+ ...input.position !== void 0 && { position: input.position },
2997
+ ...input.title !== void 0 && { title: input.title },
2998
+ ...input.format !== void 0 && { format: input.format },
2999
+ ...input.status !== void 0 && { status: input.status },
3000
+ ...input.scheduledDate !== void 0 && { scheduledDate: input.scheduledDate ?? void 0 },
3001
+ ...input.platforms !== void 0 && { platforms: input.platforms },
3002
+ ...input.checklist !== void 0 && { checklist: input.checklist },
3003
+ ...input.mediaUrl !== void 0 && { mediaUrl: input.mediaUrl ?? void 0 },
3004
+ ...input.hook !== void 0 && { hook: input.hook },
3005
+ ...input.cta !== void 0 && { cta: input.cta },
3006
+ ...input.contentMd !== void 0 && { contentMd: input.contentMd }
3007
+ });
3008
+ return { ...post2 };
3009
+ }
3010
+ if (!input.planId) throw new Error("planId is required");
3011
+ const week = input.weekNumber ?? 1;
3012
+ const siblings = db.posts.filter((p) => p.planId === input.planId && p.weekNumber === week);
3013
+ const post = {
3014
+ id: nextId("post"),
3015
+ planId: input.planId,
3016
+ weekNumber: week,
3017
+ position: input.position ?? siblings.length,
3018
+ title: input.title ?? "",
3019
+ format: input.format ?? "reel",
3020
+ status: input.status ?? "idea",
3021
+ scheduledDate: input.scheduledDate ?? void 0,
3022
+ platforms: input.platforms ?? [],
3023
+ checklist: input.checklist ?? [],
3024
+ mediaUrl: input.mediaUrl ?? void 0,
3025
+ hook: input.hook,
3026
+ cta: input.cta,
3027
+ contentMd: input.contentMd ?? ""
3028
+ };
3029
+ db.posts.push(post);
3030
+ return { ...post };
3031
+ },
3032
+ async deletePost(id) {
3033
+ db.posts = db.posts.filter((p) => p.id !== id);
3034
+ }
3035
+ };
3036
+ }
3037
+
3038
+ // src/data/tables.ts
3039
+ var T = {
3040
+ socialAccounts: "plg_marketing_social_accounts",
3041
+ contentPlans: "plg_marketing_content_plans",
3042
+ contentPosts: "plg_marketing_content_posts"
3043
+ };
3044
+
3045
+ // src/data/contentSupabase.ts
3046
+ function getClient() {
3047
+ const supabase = core.getSupabaseClientOptional();
3048
+ if (!supabase) throw new Error("Supabase not initialized");
3049
+ return supabase;
3050
+ }
3051
+ function requireTenantId() {
3052
+ const tenantId = core.getActiveTenantId();
3053
+ if (!tenantId) throw new Error("No active tenant");
3054
+ return tenantId;
3055
+ }
3056
+ function snakeToCamel(obj) {
3057
+ const result = {};
3058
+ for (const [key, value] of Object.entries(obj)) {
3059
+ result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value ?? void 0;
3060
+ }
3061
+ return result;
3062
+ }
3063
+ function toAccount(row) {
3064
+ const account = snakeToCamel(row);
3065
+ if (!Array.isArray(account.platforms)) account.platforms = [];
3066
+ return account;
3067
+ }
3068
+ function toPlan(row) {
3069
+ const plan = snakeToCamel(row);
3070
+ if (!Array.isArray(plan.pillars)) plan.pillars = [];
3071
+ if (!Array.isArray(plan.formats)) plan.formats = [];
3072
+ plan.briefMd = plan.briefMd ?? "";
3073
+ return plan;
3074
+ }
3075
+ function toPost(row) {
3076
+ const post = snakeToCamel(row);
3077
+ post.contentMd = post.contentMd ?? "";
3078
+ if (!Array.isArray(post.platforms)) post.platforms = [];
3079
+ if (!Array.isArray(post.checklist)) post.checklist = [];
3080
+ return post;
3081
+ }
3082
+ function createSupabaseContentPlannerProvider() {
3083
+ return {
3084
+ async listAccounts() {
3085
+ const tenantId = core.getActiveTenantId();
3086
+ if (!tenantId) return [];
3087
+ const { data } = await getClient().from(T.socialAccounts).select("*").eq("tenant_id", tenantId).eq("is_active", true).order("created_at", { ascending: true });
3088
+ return (data ?? []).map(toAccount);
3089
+ },
3090
+ async saveAccount(input) {
3091
+ if (input.id) {
3092
+ const payload = {};
3093
+ if (input.name !== void 0) payload.name = input.name;
3094
+ if (input.handle !== void 0) payload.handle = input.handle;
3095
+ if (input.platforms !== void 0) payload.platforms = input.platforms;
3096
+ if (input.isActive !== void 0) payload.is_active = input.isActive;
3097
+ const { data: data2, error: error2 } = await getClient().from(T.socialAccounts).update(payload).eq("id", input.id).select("*").single();
3098
+ if (error2) throw error2;
3099
+ return toAccount(data2);
3100
+ }
3101
+ const { data, error } = await getClient().from(T.socialAccounts).insert({
3102
+ tenant_id: requireTenantId(),
3103
+ name: input.name,
3104
+ handle: input.handle ?? null,
3105
+ platforms: input.platforms ?? ["instagram"],
3106
+ is_active: input.isActive ?? true
3107
+ }).select("*").single();
3108
+ if (error) throw error;
3109
+ return toAccount(data);
3110
+ },
3111
+ async deleteAccount(id) {
3112
+ const { error } = await getClient().from(T.socialAccounts).delete().eq("id", id);
3113
+ if (error) throw error;
3114
+ },
3115
+ async listPlans(accountId) {
3116
+ const tenantId = core.getActiveTenantId();
3117
+ if (!tenantId) return [];
3118
+ const { data } = await getClient().from(T.contentPlans).select("*").eq("tenant_id", tenantId).eq("account_id", accountId).order("created_at", { ascending: false });
3119
+ return (data ?? []).map(toPlan);
3120
+ },
3121
+ async getPlan(id) {
3122
+ const { data } = await getClient().from(T.contentPlans).select("*").eq("id", id).maybeSingle();
3123
+ return data ? toPlan(data) : null;
3124
+ },
3125
+ async savePlan(input) {
3126
+ if (input.id) {
3127
+ const payload = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
3128
+ if (input.name !== void 0) payload.name = input.name;
3129
+ if (input.status !== void 0) payload.status = input.status;
3130
+ if (input.weeksCount !== void 0) payload.weeks_count = input.weeksCount;
3131
+ if (input.startDate !== void 0) payload.start_date = input.startDate;
3132
+ if (input.objective !== void 0) payload.objective = input.objective;
3133
+ if (input.tone !== void 0) payload.tone = input.tone;
3134
+ if (input.pillars !== void 0) payload.pillars = input.pillars;
3135
+ if (input.formats !== void 0) payload.formats = input.formats;
3136
+ if (input.weeklyFrequency !== void 0) payload.weekly_frequency = input.weeklyFrequency;
3137
+ if (input.briefMd !== void 0) payload.brief_md = input.briefMd;
3138
+ const { data: data2, error: error2 } = await getClient().from(T.contentPlans).update(payload).eq("id", input.id).select("*").single();
3139
+ if (error2) throw error2;
3140
+ return toPlan(data2);
3141
+ }
3142
+ if (!input.accountId) throw new Error("accountId is required");
3143
+ const { data, error } = await getClient().from(T.contentPlans).insert({
3144
+ tenant_id: requireTenantId(),
3145
+ account_id: input.accountId,
3146
+ name: input.name ?? "Novo plano",
3147
+ status: input.status ?? "draft",
3148
+ weeks_count: input.weeksCount ?? 4,
3149
+ start_date: input.startDate ?? null,
3150
+ objective: input.objective ?? null,
3151
+ tone: input.tone ?? null,
3152
+ pillars: input.pillars ?? [],
3153
+ formats: input.formats ?? ["reel", "static"],
3154
+ weekly_frequency: input.weeklyFrequency ?? 3,
3155
+ brief_md: input.briefMd ?? ""
3156
+ }).select("*").single();
3157
+ if (error) throw error;
3158
+ return toPlan(data);
3159
+ },
3160
+ async deletePlan(id) {
3161
+ await getClient().from(T.contentPlans).delete().eq("id", id);
3162
+ },
3163
+ async listPosts(planId) {
3164
+ const { data } = await getClient().from(T.contentPosts).select("*").eq("plan_id", planId).order("week_number", { ascending: true }).order("position", { ascending: true });
3165
+ return (data ?? []).map(toPost);
3166
+ },
3167
+ async getPost(id) {
3168
+ const { data } = await getClient().from(T.contentPosts).select("*").eq("id", id).maybeSingle();
3169
+ return data ? toPost(data) : null;
3170
+ },
3171
+ async savePost(input) {
3172
+ if (input.id) {
3173
+ const payload = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
3174
+ if (input.weekNumber !== void 0) payload.week_number = input.weekNumber;
3175
+ if (input.position !== void 0) payload.position = input.position;
3176
+ if (input.title !== void 0) payload.title = input.title;
3177
+ if (input.format !== void 0) payload.format = input.format;
3178
+ if (input.status !== void 0) payload.status = input.status;
3179
+ if (input.scheduledDate !== void 0) payload.scheduled_date = input.scheduledDate;
3180
+ if (input.platforms !== void 0) payload.platforms = input.platforms;
3181
+ if (input.checklist !== void 0) payload.checklist = input.checklist;
3182
+ if (input.mediaUrl !== void 0) payload.media_url = input.mediaUrl;
3183
+ if (input.hook !== void 0) payload.hook = input.hook;
3184
+ if (input.cta !== void 0) payload.cta = input.cta;
3185
+ if (input.contentMd !== void 0) payload.content_md = input.contentMd;
3186
+ const { data: data2, error: error2 } = await getClient().from(T.contentPosts).update(payload).eq("id", input.id).select("*").single();
3187
+ if (error2) throw error2;
3188
+ return toPost(data2);
3189
+ }
3190
+ if (!input.planId) throw new Error("planId is required");
3191
+ const week = input.weekNumber ?? 1;
3192
+ const { data: maxData } = await getClient().from(T.contentPosts).select("position").eq("plan_id", input.planId).eq("week_number", week).order("position", { ascending: false }).limit(1);
3193
+ const maxPos = maxData?.[0]?.position ?? -1;
3194
+ const { data, error } = await getClient().from(T.contentPosts).insert({
3195
+ tenant_id: requireTenantId(),
3196
+ plan_id: input.planId,
3197
+ week_number: week,
3198
+ position: input.position ?? maxPos + 1,
3199
+ title: input.title ?? "",
3200
+ format: input.format ?? "reel",
3201
+ status: input.status ?? "idea",
3202
+ scheduled_date: input.scheduledDate ?? null,
3203
+ platforms: input.platforms ?? [],
3204
+ checklist: input.checklist ?? [],
3205
+ media_url: input.mediaUrl ?? null,
3206
+ hook: input.hook ?? null,
3207
+ cta: input.cta ?? null,
3208
+ content_md: input.contentMd ?? ""
3209
+ }).select("*").single();
3210
+ if (error) throw error;
3211
+ return toPost(data);
3212
+ },
3213
+ async deletePost(id) {
3214
+ await getClient().from(T.contentPosts).delete().eq("id", id);
3215
+ }
3216
+ };
3217
+ }
1039
3218
  function createMarketingStore(provider) {
1040
3219
  return vanilla.createStore((set, get) => ({
1041
3220
  range: "30d",
@@ -1072,6 +3251,280 @@ function createMarketingStore(provider) {
1072
3251
  }
1073
3252
  }));
1074
3253
  }
3254
+ function createContentPlannerStore(provider) {
3255
+ async function loadPlansFor(accountId) {
3256
+ const plans = await provider.listPlans(accountId);
3257
+ const activePlan = plans.find((p) => p.status === "active") ?? plans[0] ?? null;
3258
+ const posts = activePlan ? await provider.listPosts(activePlan.id) : [];
3259
+ return { plans, activePlan, posts };
3260
+ }
3261
+ return vanilla.createStore((set, get) => ({
3262
+ loaded: false,
3263
+ loading: false,
3264
+ loadedTenantId: null,
3265
+ viewMode: "board",
3266
+ setViewMode(mode) {
3267
+ set({ viewMode: mode });
3268
+ },
3269
+ accounts: [],
3270
+ activeAccountId: null,
3271
+ plans: [],
3272
+ activePlan: null,
3273
+ posts: [],
3274
+ async load() {
3275
+ if (get().loading) return;
3276
+ set({ loading: true });
3277
+ try {
3278
+ const accounts = await provider.listAccounts();
3279
+ const current = get().activeAccountId;
3280
+ const activeAccountId = (current && accounts.some((a) => a.id === current) ? current : accounts[0]?.id) ?? null;
3281
+ const { plans, activePlan, posts } = activeAccountId ? await loadPlansFor(activeAccountId) : { plans: [], activePlan: null, posts: [] };
3282
+ set({
3283
+ accounts,
3284
+ activeAccountId,
3285
+ plans,
3286
+ activePlan,
3287
+ posts,
3288
+ loaded: true,
3289
+ loading: false,
3290
+ loadedTenantId: core.getActiveTenantId() ?? null
3291
+ });
3292
+ } catch (err) {
3293
+ set({ loading: false, loaded: true });
3294
+ throw err;
3295
+ }
3296
+ },
3297
+ async setAccount(accountId) {
3298
+ set({ activeAccountId: accountId, loading: true });
3299
+ try {
3300
+ const { plans, activePlan, posts } = await loadPlansFor(accountId);
3301
+ set({ plans, activePlan, posts, loading: false });
3302
+ } catch (err) {
3303
+ set({ loading: false });
3304
+ throw err;
3305
+ }
3306
+ },
3307
+ async selectPlan(planId) {
3308
+ const plan = get().plans.find((p) => p.id === planId) ?? null;
3309
+ const posts = plan ? await provider.listPosts(plan.id) : [];
3310
+ set({ activePlan: plan, posts });
3311
+ },
3312
+ async saveAccount(input) {
3313
+ const account = await provider.saveAccount(input);
3314
+ const accounts = await provider.listAccounts();
3315
+ set({ accounts });
3316
+ if (!get().activeAccountId) await get().setAccount(account.id);
3317
+ return account;
3318
+ },
3319
+ async deleteAccount(id) {
3320
+ await provider.deleteAccount(id);
3321
+ const accounts = await provider.listAccounts();
3322
+ const wasActive = get().activeAccountId === id;
3323
+ set({ accounts });
3324
+ if (wasActive) {
3325
+ const next = accounts[0]?.id ?? null;
3326
+ if (next) {
3327
+ await get().setAccount(next);
3328
+ } else {
3329
+ set({ activeAccountId: null, plans: [], activePlan: null, posts: [] });
3330
+ }
3331
+ }
3332
+ },
3333
+ async savePlan(input) {
3334
+ const plan = await provider.savePlan(input);
3335
+ const accountId = get().activeAccountId;
3336
+ if (accountId) {
3337
+ const plans = await provider.listPlans(accountId);
3338
+ const activePlan = plans.find((p) => p.id === plan.id) ?? get().activePlan;
3339
+ const posts = activePlan ? await provider.listPosts(activePlan.id) : [];
3340
+ set({ plans, activePlan, posts });
3341
+ }
3342
+ return plan;
3343
+ },
3344
+ async savePost(input) {
3345
+ const post = await provider.savePost(input);
3346
+ const planId = get().activePlan?.id;
3347
+ if (planId && post.planId === planId) {
3348
+ const posts = await provider.listPosts(planId);
3349
+ set({ posts });
3350
+ }
3351
+ return post;
3352
+ },
3353
+ async deletePost(id) {
3354
+ await provider.deletePost(id);
3355
+ const planId = get().activePlan?.id;
3356
+ if (planId) {
3357
+ const posts = await provider.listPosts(planId);
3358
+ set({ posts });
3359
+ }
3360
+ }
3361
+ }));
3362
+ }
3363
+
3364
+ // src/migrations/index.ts
3365
+ var MIGRATION_000_PLG_RENAME = `-- 000_plg_rename.sql \u2014 rename legacy marketing tables to plg_marketing_* for pools
3366
+ -- provisioned before the industry-pool rename. Guarded: fires only when legacy
3367
+ -- name exists and target does not, so fresh pools skip every branch.
3368
+ DO $$
3369
+ BEGIN
3370
+ IF to_regclass('public.mkt_social_accounts') IS NOT NULL AND to_regclass('public.plg_marketing_social_accounts') IS NULL THEN
3371
+ ALTER TABLE public.mkt_social_accounts RENAME TO plg_marketing_social_accounts;
3372
+ END IF;
3373
+ IF to_regclass('public.mkt_content_plans') IS NOT NULL AND to_regclass('public.plg_marketing_content_plans') IS NULL THEN
3374
+ ALTER TABLE public.mkt_content_plans RENAME TO plg_marketing_content_plans;
3375
+ END IF;
3376
+ IF to_regclass('public.mkt_content_posts') IS NOT NULL AND to_regclass('public.plg_marketing_content_posts') IS NULL THEN
3377
+ ALTER TABLE public.mkt_content_posts RENAME TO plg_marketing_content_posts;
3378
+ END IF;
3379
+ END $$;
3380
+ `;
3381
+ var MIGRATION_001_CONTENT_PLANNER = `-- Marketing Plugin: Content Planner
3382
+ -- Prefix: plg_marketing_
3383
+ -- Social accounts + content plans + posts (Notion-style markdown pages).
3384
+
3385
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_social_accounts (
3386
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3387
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3388
+ name text NOT NULL,
3389
+ handle text,
3390
+ platform text NOT NULL DEFAULT 'instagram'
3391
+ CHECK (platform IN ('instagram', 'tiktok', 'youtube', 'facebook', 'linkedin')),
3392
+ is_active boolean NOT NULL DEFAULT true,
3393
+ created_at timestamptz NOT NULL DEFAULT now()
3394
+ );
3395
+ ALTER TABLE public.plg_marketing_social_accounts ENABLE ROW LEVEL SECURITY;
3396
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_social_accounts_tenant ON public.plg_marketing_social_accounts(tenant_id);
3397
+
3398
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_content_plans (
3399
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3400
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3401
+ account_id uuid NOT NULL REFERENCES public.plg_marketing_social_accounts(id) ON DELETE CASCADE,
3402
+ name text NOT NULL,
3403
+ status text NOT NULL DEFAULT 'active'
3404
+ CHECK (status IN ('draft', 'active', 'archived')),
3405
+ weeks_count integer NOT NULL DEFAULT 8,
3406
+ start_date date,
3407
+ objective text,
3408
+ tone text,
3409
+ pillars jsonb NOT NULL DEFAULT '[]'::jsonb,
3410
+ formats jsonb NOT NULL DEFAULT '[]'::jsonb,
3411
+ weekly_frequency integer NOT NULL DEFAULT 3,
3412
+ brief_md text NOT NULL DEFAULT '',
3413
+ created_at timestamptz NOT NULL DEFAULT now(),
3414
+ updated_at timestamptz NOT NULL DEFAULT now()
3415
+ );
3416
+ ALTER TABLE public.plg_marketing_content_plans ENABLE ROW LEVEL SECURITY;
3417
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_tenant ON public.plg_marketing_content_plans(tenant_id);
3418
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_account ON public.plg_marketing_content_plans(account_id);
3419
+
3420
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_content_posts (
3421
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3422
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3423
+ plan_id uuid NOT NULL REFERENCES public.plg_marketing_content_plans(id) ON DELETE CASCADE,
3424
+ week_number integer NOT NULL DEFAULT 1,
3425
+ position integer NOT NULL DEFAULT 0,
3426
+ title text NOT NULL,
3427
+ format text NOT NULL DEFAULT 'reel'
3428
+ CHECK (format IN ('reel', 'static', 'carousel', 'story')),
3429
+ status text NOT NULL DEFAULT 'idea'
3430
+ CHECK (status IN ('idea', 'script', 'recording', 'editing', 'scheduled', 'published')),
3431
+ scheduled_date date,
3432
+ hook text,
3433
+ cta text,
3434
+ content_md text NOT NULL DEFAULT '',
3435
+ created_at timestamptz NOT NULL DEFAULT now(),
3436
+ updated_at timestamptz NOT NULL DEFAULT now()
3437
+ );
3438
+ ALTER TABLE public.plg_marketing_content_posts ENABLE ROW LEVEL SECURITY;
3439
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_tenant ON public.plg_marketing_content_posts(tenant_id);
3440
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_plan ON public.plg_marketing_content_posts(plan_id, week_number, position);
3441
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_status ON public.plg_marketing_content_posts(tenant_id, status);
3442
+
3443
+ -- RLS policies for plg_marketing_social_accounts
3444
+ DROP POLICY IF EXISTS plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts;
3445
+ DROP POLICY IF EXISTS plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts;
3446
+ DROP POLICY IF EXISTS plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts;
3447
+ DROP POLICY IF EXISTS plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts;
3448
+ CREATE POLICY plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3449
+ CREATE POLICY plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3450
+ CREATE POLICY plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3451
+ CREATE POLICY plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3452
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_social_accounts TO authenticated;
3453
+
3454
+ -- RLS policies for plg_marketing_content_plans
3455
+ DROP POLICY IF EXISTS plg_marketing_content_plans_select ON public.plg_marketing_content_plans;
3456
+ DROP POLICY IF EXISTS plg_marketing_content_plans_insert ON public.plg_marketing_content_plans;
3457
+ DROP POLICY IF EXISTS plg_marketing_content_plans_update ON public.plg_marketing_content_plans;
3458
+ DROP POLICY IF EXISTS plg_marketing_content_plans_delete ON public.plg_marketing_content_plans;
3459
+ CREATE POLICY plg_marketing_content_plans_select ON public.plg_marketing_content_plans FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3460
+ CREATE POLICY plg_marketing_content_plans_insert ON public.plg_marketing_content_plans FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3461
+ CREATE POLICY plg_marketing_content_plans_update ON public.plg_marketing_content_plans FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3462
+ CREATE POLICY plg_marketing_content_plans_delete ON public.plg_marketing_content_plans FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3463
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_plans TO authenticated;
3464
+
3465
+ -- RLS policies for plg_marketing_content_posts
3466
+ DROP POLICY IF EXISTS plg_marketing_content_posts_select ON public.plg_marketing_content_posts;
3467
+ DROP POLICY IF EXISTS plg_marketing_content_posts_insert ON public.plg_marketing_content_posts;
3468
+ DROP POLICY IF EXISTS plg_marketing_content_posts_update ON public.plg_marketing_content_posts;
3469
+ DROP POLICY IF EXISTS plg_marketing_content_posts_delete ON public.plg_marketing_content_posts;
3470
+ CREATE POLICY plg_marketing_content_posts_select ON public.plg_marketing_content_posts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3471
+ CREATE POLICY plg_marketing_content_posts_insert ON public.plg_marketing_content_posts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3472
+ CREATE POLICY plg_marketing_content_posts_update ON public.plg_marketing_content_posts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3473
+ CREATE POLICY plg_marketing_content_posts_delete ON public.plg_marketing_content_posts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3474
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_posts TO authenticated;
3475
+ `;
3476
+ var MIGRATION_002_MULTI_PLATFORM = `-- Marketing Plugin: multi-platform accounts + per-post targets.
3477
+ -- An account is a brand/profile that publishes to MANY platforms (Instagram,
3478
+ -- YouTube, TikTok, ...). Posts optionally narrow the target platforms; an
3479
+ -- empty array means "inherit the account's platforms".
3480
+
3481
+ ALTER TABLE public.plg_marketing_social_accounts
3482
+ ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{instagram}';
3483
+
3484
+ -- Backfill from the legacy single-platform column, then drop it (also drops
3485
+ -- its CHECK constraint \u2014 the platform list is UI-driven from here on).
3486
+ DO $$
3487
+ BEGIN
3488
+ IF EXISTS (
3489
+ SELECT 1 FROM information_schema.columns
3490
+ WHERE table_schema = 'public' AND table_name = 'plg_marketing_social_accounts' AND column_name = 'platform'
3491
+ ) THEN
3492
+ UPDATE public.plg_marketing_social_accounts
3493
+ SET platforms = ARRAY[platform]
3494
+ WHERE platform IS NOT NULL AND platforms = '{instagram}';
3495
+ ALTER TABLE public.plg_marketing_social_accounts DROP COLUMN platform;
3496
+ END IF;
3497
+ END $$;
3498
+
3499
+ ALTER TABLE public.plg_marketing_content_posts
3500
+ ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';
3501
+ `;
3502
+ var MIGRATION_003_RECORDING_OPS = `-- Marketing Plugin: recording-day ops on posts.
3503
+ -- checklist: shooting checklist items [{ id, text, done }] \u2014 the on-set
3504
+ -- companion for recording day. media_url: uploaded asset (static posts get a
3505
+ -- caption + final art instead of a script).
3506
+
3507
+ ALTER TABLE public.plg_marketing_content_posts
3508
+ ADD COLUMN IF NOT EXISTS checklist jsonb NOT NULL DEFAULT '[]';
3509
+
3510
+ ALTER TABLE public.plg_marketing_content_posts
3511
+ ADD COLUMN IF NOT EXISTS media_url text;
3512
+
3513
+ -- Public media bucket for content assets (mirrors the avatars-bucket pattern:
3514
+ -- public read, authenticated write). Path convention: content/{tenantId}/{postId}-{ts}.{ext}
3515
+ INSERT INTO storage.buckets (id, name, public)
3516
+ VALUES ('mkt-media', 'mkt-media', true)
3517
+ ON CONFLICT (id) DO NOTHING;
3518
+
3519
+ DROP POLICY IF EXISTS plg_marketing_media_read ON storage.objects;
3520
+ DROP POLICY IF EXISTS plg_marketing_media_insert ON storage.objects;
3521
+ DROP POLICY IF EXISTS plg_marketing_media_update ON storage.objects;
3522
+ DROP POLICY IF EXISTS plg_marketing_media_delete ON storage.objects;
3523
+ CREATE POLICY plg_marketing_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
3524
+ CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
3525
+ CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
3526
+ CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
3527
+ `;
1075
3528
 
1076
3529
  // src/locales/en.ts
1077
3530
  var en = {
@@ -1146,7 +3599,102 @@ var en = {
1146
3599
  "marketing.settings.autoSyncDesc": "Pull conversions automatically from the source.",
1147
3600
  "marketing.settings.assisted": "Count assisted conversions",
1148
3601
  "marketing.settings.assistedDesc": "Credit channels that contributed to a conversion.",
1149
- "marketing.settings.channelTrackDesc": "Track this acquisition source."
3602
+ "marketing.settings.channelTrackDesc": "Track this acquisition source.",
3603
+ // content planner
3604
+ "marketing.content.title": "Content",
3605
+ "marketing.content.week": "Week",
3606
+ "marketing.content.month": "Month",
3607
+ "marketing.content.newPost": "New post",
3608
+ "marketing.content.untitled": "Untitled",
3609
+ "marketing.content.account": "Account",
3610
+ "marketing.content.addAccount": "Add account",
3611
+ "marketing.content.previewNow": "Just now",
3612
+ "marketing.content.previewLike": "Like",
3613
+ "marketing.content.previewComment": "Comment",
3614
+ "marketing.content.previewShare": "Share",
3615
+ "marketing.content.showLess": "show less",
3616
+ "marketing.content.checklistTitle": "Recording checklist",
3617
+ "marketing.content.checklistDescription": "Shooting-day operations",
3618
+ "marketing.content.checklistOpen": "Checklist",
3619
+ "marketing.content.checklistEmpty": "No items yet \u2014 generate them from the script or add your own below.",
3620
+ "marketing.content.checklistAddPlaceholder": "New item\u2026",
3621
+ "marketing.content.checklistAdd": "Add item",
3622
+ "marketing.content.checklistGenerate": "Generate from script",
3623
+ "marketing.content.scriptLabel": "Script",
3624
+ "marketing.content.captionLabel": "Caption",
3625
+ "marketing.content.captionPlaceholder": "Write the post caption\u2026",
3626
+ "marketing.content.mediaLabel": "Media asset",
3627
+ "marketing.content.mediaUpload": "Upload media",
3628
+ "marketing.content.mediaHint": "Image or video for this post",
3629
+ "marketing.content.mediaReplace": "Replace",
3630
+ "marketing.content.mediaRemove": "Remove",
3631
+ "marketing.content.mediaUnavailable": "Media upload requires a connected Supabase project.",
3632
+ "marketing.content.editAccountTitle": "Account settings",
3633
+ "marketing.content.accountSettings": "Account settings",
3634
+ "marketing.content.deleteAccount": "Delete account",
3635
+ "marketing.content.deleteAccountConfirm": "The account and all of its content plans and posts will be permanently deleted.",
3636
+ "marketing.content.noAccountsAvailable": "No accounts available",
3637
+ "marketing.content.addAccountTitle": "New account",
3638
+ "marketing.content.nameLabel": "Name",
3639
+ "marketing.content.handleLabel": "Handle",
3640
+ "marketing.content.handlePlaceholder": "@handle (optional)",
3641
+ "marketing.content.viewBoard": "Weekly",
3642
+ "marketing.content.viewCalendar": "Monthly",
3643
+ "marketing.content.viewList": "List",
3644
+ "marketing.content.unscheduled": "Unscheduled",
3645
+ "marketing.content.noPosts": "No posts yet \u2014 add your first one from any week.",
3646
+ "marketing.content.listTitle": "Title",
3647
+ "marketing.content.listWeek": "Week",
3648
+ "marketing.content.listDate": "Date",
3649
+ "marketing.content.listFormat": "Format",
3650
+ "marketing.content.listStatus": "Status",
3651
+ "marketing.content.noAccountsHint": "Create the account (e.g. your Instagram profile) to start planning content.",
3652
+ "marketing.content.accountNamePlaceholder": "e.g. @myclinic",
3653
+ "marketing.content.createAccount": "Create account",
3654
+ "marketing.content.defaultPlanName": "Content plan \u2014 8 weeks",
3655
+ "marketing.content.brief": "Brief",
3656
+ "marketing.content.planName": "Plan name",
3657
+ "marketing.content.objective": "Objective",
3658
+ "marketing.content.objectivePlaceholder": "e.g. generate evaluation bookings",
3659
+ "marketing.content.tone": "Tone of voice",
3660
+ "marketing.content.tonePlaceholder": "e.g. approachable authority, plain language",
3661
+ "marketing.content.pillars": "Content pillars",
3662
+ "marketing.content.pillarPlaceholder": "Add pillar\u2026",
3663
+ "marketing.content.formatsLabel": "Formats",
3664
+ "marketing.content.weeklyFrequency": "Posts per week",
3665
+ "marketing.content.weeksCount": "Weeks",
3666
+ "marketing.content.briefBody": "Brief (markdown)",
3667
+ "marketing.content.briefPlaceholder": "Describe the strategy, references and what the content should achieve\u2026",
3668
+ "marketing.content.formatLabel": "Format",
3669
+ "marketing.content.statusLabel": "Status",
3670
+ "marketing.content.scheduledDate": "Planned date",
3671
+ "marketing.content.hook": "Hook",
3672
+ "marketing.content.hookPlaceholder": "The first line that grabs attention\u2026",
3673
+ "marketing.content.cta": "CTA",
3674
+ "marketing.content.ctaPlaceholder": "e.g. Book your evaluation",
3675
+ "marketing.content.titlePlaceholder": "Post title\u2026",
3676
+ "marketing.content.bodyPlaceholder": "Post script / plan in markdown\u2026",
3677
+ "marketing.content.insertTemplate": "Insert script template",
3678
+ "marketing.content.save": "Save",
3679
+ "marketing.content.cancel": "Cancel",
3680
+ "marketing.content.edit": "Edit",
3681
+ "marketing.content.preview": "Preview",
3682
+ "marketing.content.deletePost": "Delete post",
3683
+ "marketing.content.deletePostConfirm": "This post and its script will be permanently removed.",
3684
+ "marketing.content.format.reel": "Reel",
3685
+ "marketing.content.format.static": "Static",
3686
+ "marketing.content.format.carousel": "Carousel",
3687
+ "marketing.content.format.story": "Story",
3688
+ "marketing.content.format.video": "Video",
3689
+ "marketing.content.format.live": "Live",
3690
+ "marketing.content.format.article": "Article",
3691
+ "marketing.content.platformsLabel": "Platforms",
3692
+ "marketing.content.status.idea": "idea",
3693
+ "marketing.content.status.script": "script",
3694
+ "marketing.content.status.recording": "recording",
3695
+ "marketing.content.status.editing": "editing",
3696
+ "marketing.content.status.scheduled": "scheduled",
3697
+ "marketing.content.status.published": "published"
1150
3698
  };
1151
3699
 
1152
3700
  // src/locales/pt-BR.ts
@@ -1222,7 +3770,102 @@ var ptBR = {
1222
3770
  "marketing.settings.autoSyncDesc": "Importar convers\xF5es automaticamente da fonte.",
1223
3771
  "marketing.settings.assisted": "Contar convers\xF5es assistidas",
1224
3772
  "marketing.settings.assistedDesc": "Creditar canais que contribu\xEDram para uma convers\xE3o.",
1225
- "marketing.settings.channelTrackDesc": "Monitorar esta fonte de aquisi\xE7\xE3o."
3773
+ "marketing.settings.channelTrackDesc": "Monitorar esta fonte de aquisi\xE7\xE3o.",
3774
+ // content planner
3775
+ "marketing.content.title": "Conte\xFAdo",
3776
+ "marketing.content.week": "Semana",
3777
+ "marketing.content.month": "M\xEAs",
3778
+ "marketing.content.newPost": "Novo post",
3779
+ "marketing.content.untitled": "Sem t\xEDtulo",
3780
+ "marketing.content.account": "Conta",
3781
+ "marketing.content.addAccount": "Adicionar conta",
3782
+ "marketing.content.previewNow": "Agora",
3783
+ "marketing.content.previewLike": "Curtir",
3784
+ "marketing.content.previewComment": "Comentar",
3785
+ "marketing.content.previewShare": "Compartilhar",
3786
+ "marketing.content.showLess": "mostrar menos",
3787
+ "marketing.content.checklistTitle": "Checklist de grava\xE7\xE3o",
3788
+ "marketing.content.checklistDescription": "Opera\xE7\xE3o do dia de grava\xE7\xE3o",
3789
+ "marketing.content.checklistOpen": "Checklist",
3790
+ "marketing.content.checklistEmpty": "Nenhum item ainda \u2014 gere a partir do roteiro ou adicione o seu abaixo.",
3791
+ "marketing.content.checklistAddPlaceholder": "Novo item\u2026",
3792
+ "marketing.content.checklistAdd": "Adicionar item",
3793
+ "marketing.content.checklistGenerate": "Gerar do roteiro",
3794
+ "marketing.content.scriptLabel": "Roteiro",
3795
+ "marketing.content.captionLabel": "Legenda",
3796
+ "marketing.content.captionPlaceholder": "Escreva a legenda do post\u2026",
3797
+ "marketing.content.mediaLabel": "Arte / m\xEDdia",
3798
+ "marketing.content.mediaUpload": "Enviar m\xEDdia",
3799
+ "marketing.content.mediaHint": "Imagem ou v\xEDdeo deste post",
3800
+ "marketing.content.mediaReplace": "Substituir",
3801
+ "marketing.content.mediaRemove": "Remover",
3802
+ "marketing.content.mediaUnavailable": "O upload de m\xEDdia requer um projeto Supabase conectado.",
3803
+ "marketing.content.editAccountTitle": "Configura\xE7\xF5es da conta",
3804
+ "marketing.content.accountSettings": "Configura\xE7\xF5es da conta",
3805
+ "marketing.content.deleteAccount": "Excluir conta",
3806
+ "marketing.content.deleteAccountConfirm": "A conta e todos os planos e posts de conte\xFAdo dela ser\xE3o exclu\xEDdos permanentemente.",
3807
+ "marketing.content.noAccountsAvailable": "Nenhuma conta dispon\xEDvel",
3808
+ "marketing.content.addAccountTitle": "Nova conta",
3809
+ "marketing.content.nameLabel": "Nome",
3810
+ "marketing.content.handleLabel": "Usu\xE1rio",
3811
+ "marketing.content.handlePlaceholder": "@usuario (opcional)",
3812
+ "marketing.content.viewBoard": "Semanal",
3813
+ "marketing.content.viewCalendar": "Mensal",
3814
+ "marketing.content.viewList": "Lista",
3815
+ "marketing.content.unscheduled": "Sem data",
3816
+ "marketing.content.noPosts": "Nenhum post ainda \u2014 adicione o primeiro em qualquer semana.",
3817
+ "marketing.content.listTitle": "T\xEDtulo",
3818
+ "marketing.content.listWeek": "Semana",
3819
+ "marketing.content.listDate": "Data",
3820
+ "marketing.content.listFormat": "Formato",
3821
+ "marketing.content.listStatus": "Status",
3822
+ "marketing.content.noAccountsHint": "Crie a conta (ex.: o perfil do Instagram) para come\xE7ar a planejar o conte\xFAdo.",
3823
+ "marketing.content.accountNamePlaceholder": "ex.: @minhaclinica",
3824
+ "marketing.content.createAccount": "Criar conta",
3825
+ "marketing.content.defaultPlanName": "Plano de conte\xFAdo \u2014 8 semanas",
3826
+ "marketing.content.brief": "Briefing",
3827
+ "marketing.content.planName": "Nome do plano",
3828
+ "marketing.content.objective": "Objetivo",
3829
+ "marketing.content.objectivePlaceholder": "ex.: gerar agendamentos de avalia\xE7\xE3o",
3830
+ "marketing.content.tone": "Tom de voz",
3831
+ "marketing.content.tonePlaceholder": "ex.: autoridade acess\xEDvel, linguagem simples",
3832
+ "marketing.content.pillars": "Pilares de conte\xFAdo",
3833
+ "marketing.content.pillarPlaceholder": "Adicionar pilar\u2026",
3834
+ "marketing.content.formatsLabel": "Formatos",
3835
+ "marketing.content.weeklyFrequency": "Posts por semana",
3836
+ "marketing.content.weeksCount": "Semanas",
3837
+ "marketing.content.briefBody": "Briefing (markdown)",
3838
+ "marketing.content.briefPlaceholder": "Descreva a estrat\xE9gia, refer\xEAncias e o que se quer com o conte\xFAdo\u2026",
3839
+ "marketing.content.formatLabel": "Formato",
3840
+ "marketing.content.statusLabel": "Status",
3841
+ "marketing.content.scheduledDate": "Data prevista",
3842
+ "marketing.content.hook": "Gancho",
3843
+ "marketing.content.hookPlaceholder": "A primeira frase que segura a aten\xE7\xE3o\u2026",
3844
+ "marketing.content.cta": "CTA",
3845
+ "marketing.content.ctaPlaceholder": "ex.: Agende sua avalia\xE7\xE3o",
3846
+ "marketing.content.titlePlaceholder": "T\xEDtulo do post\u2026",
3847
+ "marketing.content.bodyPlaceholder": "Roteiro / plano do post em markdown\u2026",
3848
+ "marketing.content.insertTemplate": "Inserir modelo de roteiro",
3849
+ "marketing.content.save": "Salvar",
3850
+ "marketing.content.cancel": "Cancelar",
3851
+ "marketing.content.edit": "Editar",
3852
+ "marketing.content.preview": "Visualizar",
3853
+ "marketing.content.deletePost": "Excluir post",
3854
+ "marketing.content.deletePostConfirm": "Este post e seu roteiro ser\xE3o removidos permanentemente.",
3855
+ "marketing.content.format.reel": "Reel",
3856
+ "marketing.content.format.static": "Est\xE1tico",
3857
+ "marketing.content.format.carousel": "Carrossel",
3858
+ "marketing.content.format.story": "Story",
3859
+ "marketing.content.format.video": "V\xEDdeo",
3860
+ "marketing.content.format.live": "Live",
3861
+ "marketing.content.format.article": "Artigo",
3862
+ "marketing.content.platformsLabel": "Plataformas",
3863
+ "marketing.content.status.idea": "ideia",
3864
+ "marketing.content.status.script": "roteiro",
3865
+ "marketing.content.status.recording": "grava\xE7\xE3o",
3866
+ "marketing.content.status.editing": "edi\xE7\xE3o",
3867
+ "marketing.content.status.scheduled": "agendado",
3868
+ "marketing.content.status.published": "publicado"
1226
3869
  };
1227
3870
 
1228
3871
  // src/locales/index.ts
@@ -1240,6 +3883,7 @@ var DEFAULT_LABELS = {
1240
3883
  campaigns: "Campaigns",
1241
3884
  funnel: "Funnel",
1242
3885
  landingPages: "Landing pages",
3886
+ content: "Content",
1243
3887
  settings: "Settings"
1244
3888
  };
1245
3889
  function resolveConfig(options) {
@@ -1264,23 +3908,28 @@ function createMarketingPlugin(options) {
1264
3908
  const provider = options?.dataProvider ?? createSafeProvider(config, domain);
1265
3909
  const store = createMarketingStore(provider);
1266
3910
  const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store });
1267
- const PageComponent = () => React4__default.default.createElement(
3911
+ const contentProvider = options?.contentPlanner?.dataProvider ?? core.createSafeDataProvider(
3912
+ () => createSupabaseContentPlannerProvider(),
3913
+ () => createMockContentPlannerProvider()
3914
+ );
3915
+ const contentStore = createContentPlannerStore(contentProvider);
3916
+ const PageComponent = () => React8__default.default.createElement(
1268
3917
  MarketingContextProvider,
1269
3918
  { config, provider, store },
1270
- React4__default.default.createElement(MarketingPage, { config, provider, store })
3919
+ React8__default.default.createElement(MarketingPage, { config, provider, store, contentProvider, contentStore })
1271
3920
  );
1272
3921
  PageComponent.displayName = "MarketingPage";
1273
3922
  const marketingRegistries = [
1274
3923
  ...buildMarketingRegistries(config.channels),
1275
3924
  ...options?.settingsRegistries ?? []
1276
3925
  ];
1277
- const SettingsComponent = () => React4__default.default.createElement(
3926
+ const SettingsComponent = () => React8__default.default.createElement(
1278
3927
  MarketingContextProvider,
1279
3928
  { config, provider, store },
1280
- React4__default.default.createElement(saas.PluginSettingsPanel, {
3929
+ React8__default.default.createElement(saas.PluginSettingsPanel, {
1281
3930
  title: config.labels.settings ?? config.labels.pageTitle,
1282
3931
  subtitle: config.labels.pageSubtitle,
1283
- generalSettings: React4__default.default.createElement(SettingsView),
3932
+ generalSettings: React8__default.default.createElement(SettingsView),
1284
3933
  registries: marketingRegistries,
1285
3934
  routeBase: "/settings/marketing"
1286
3935
  })
@@ -1293,11 +3942,15 @@ function createMarketingPlugin(options) {
1293
3942
  version: "1.0.0",
1294
3943
  scope: options?.scope ?? "universal",
1295
3944
  verticalId: options?.verticalId,
3945
+ // Admin content-planning surface (campaigns, content calendar) — targets the
3946
+ // saas/admin Panel world, not the ecommerce storefront/operator console.
3947
+ scaffolds: ["saas"],
1296
3948
  defaultEnabled: true,
1297
3949
  dependencies: [],
1298
3950
  declaredFeatures: [
1299
3951
  { id: "marketing", label: config.labels.pageTitle, group: "Engage" },
1300
- ...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : []
3952
+ ...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : [],
3953
+ ...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : []
1301
3954
  ],
1302
3955
  navigation: [
1303
3956
  {
@@ -1322,7 +3975,36 @@ function createMarketingPlugin(options) {
1322
3975
  { name: "marketing.conversion.tracked", description: "A conversion was attributed to a channel/campaign" },
1323
3976
  { name: "marketing.campaign.created", description: "A campaign was created" },
1324
3977
  { name: "marketing.campaign.updated", description: "A campaign was updated" },
1325
- { name: "marketing.channel.synced", description: "A channel pulled fresh attribution data" }
3978
+ { name: "marketing.channel.synced", description: "A channel pulled fresh attribution data" },
3979
+ { name: "marketing.content.plan.updated", description: "A content plan (brief/config) was updated" },
3980
+ { name: "marketing.content.post.created", description: "A content post was created" },
3981
+ { name: "marketing.content.post.updated", description: "A content post was updated" }
3982
+ ],
3983
+ migrations: [
3984
+ {
3985
+ id: "marketing-000-plg-rename",
3986
+ version: "1.0.0",
3987
+ sql: MIGRATION_000_PLG_RENAME,
3988
+ description: "Rename legacy mkt_* tables to plg_marketing_* in-place for pools provisioned before the industry-pool rename (guarded no-op on fresh pools)"
3989
+ },
3990
+ {
3991
+ id: "marketing-001-content-planner",
3992
+ version: "1.0.0",
3993
+ sql: MIGRATION_001_CONTENT_PLANNER,
3994
+ description: "Create plg_marketing_social_accounts, plg_marketing_content_plans and plg_marketing_content_posts"
3995
+ },
3996
+ {
3997
+ id: "marketing-002-multi-platform",
3998
+ version: "1.1.0",
3999
+ sql: MIGRATION_002_MULTI_PLATFORM,
4000
+ description: "Accounts publish to many platforms (platforms[]); posts gain optional platform targets"
4001
+ },
4002
+ {
4003
+ id: "marketing-003-recording-ops",
4004
+ version: "1.2.0",
4005
+ sql: MIGRATION_003_RECORDING_OPS,
4006
+ description: "Recording-day checklist + media asset URL on posts; content-media storage bucket"
4007
+ }
1326
4008
  ],
1327
4009
  aiTools: [
1328
4010
  {
@@ -1396,6 +4078,8 @@ function createMarketingPlugin(options) {
1396
4078
 
1397
4079
  exports.MARKETING_PRESETS = MARKETING_PRESETS;
1398
4080
  exports.createMarketingPlugin = createMarketingPlugin;
4081
+ exports.createMockContentPlannerProvider = createMockContentPlannerProvider;
1399
4082
  exports.createMockMarketingProvider = createMockMarketingProvider;
4083
+ exports.createSupabaseContentPlannerProvider = createSupabaseContentPlannerProvider;
1400
4084
  //# sourceMappingURL=index.cjs.map
1401
4085
  //# sourceMappingURL=index.cjs.map