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