@fayz-ai/plugin-marketing 0.13.33 → 0.13.35

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.
@@ -0,0 +1,2921 @@
1
+ import { buildBlogPostEntity, buildBlogCategoryEntity, ChannelCell, formatNumber, formatPercent, formatCurrency, RangeTabs, StatusBadge, KpiCard, BLOG_POSTS_TABLE, PlatformIcon } from './chunk-P3OYOY5W.js';
2
+ import { MarketingContextProvider, useMarketingConfig, useMarketingStore, useChannelLookup, useMarketingProvider } from './chunk-JHCVIZ52.js';
3
+ import React11, { useState, useMemo, useEffect, useCallback } from 'react';
4
+ import { ModulePage, PageToolbar, DataTable, SubpageHeader, RowActions, PageHeaderActions, Button, Skeleton, ConfirmDialog, ComingSoon, DashboardCanvas, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, SegmentedControl, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, ModalFooter, SheetBody, Textarea, Badge, SheetFooter, SelectSeparator, cn, Checkbox, MarkdownEditor, toast } from '@fayz-ai/ui';
5
+ import { useTranslation, useDataChanged, appHref, getCurrentLocale, resolveDataProvider, getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core';
6
+ import { createPluginContext, useModuleNavigation, createViewRouter, ModuleActionBar, askAI, PageActions, useLimitGuard, useOrganizationStore, CrudFormPage, RightRailPage, invalidateLimit, CrudListView } from '@fayz-ai/admin';
7
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
+ import { ChevronRight, Users, Target, Percent, DollarSign, Trash2, WandSparkles, SlidersHorizontal, Workflow, ExternalLink, Sparkles, Pencil, Plus, Layers, MousePointerClick, NotebookPen, Send, Megaphone, Settings2, Columns3, CalendarDays, List, ChevronLeft, X, GripVertical, Loader2, ImagePlus, MessageCircle, Repeat2, Heart, BarChart2, Share2, ThumbsUp, Globe, MoreHorizontal, Bookmark, Image } from 'lucide-react';
9
+
10
+ function OverviewView() {
11
+ const setRange = useMarketingStore((s) => s.setRange);
12
+ const range = useMarketingStore((s) => s.range);
13
+ return /* @__PURE__ */ jsx(
14
+ DashboardCanvas,
15
+ {
16
+ surface: "plugin-home",
17
+ domain: "marketing",
18
+ showHeader: false,
19
+ className: "mx-auto max-w-6xl space-y-6",
20
+ range: { default: range, onChange: (r) => void setRange(r) }
21
+ }
22
+ );
23
+ }
24
+ function SegmentsView() {
25
+ return /* @__PURE__ */ jsx(
26
+ ComingSoon,
27
+ {
28
+ title: "Segmentos",
29
+ summary: "Recortes salvos da base \u2014 \xABquem comprou nos \xFAltimos 90 dias\xBB, \xABquem nunca voltou\xBB, \xABquem gasta acima de X\xBB \u2014 para a campanha falar com um grupo e n\xE3o com todo mundo.",
30
+ v1Path: "Marketing \u2192 Segmenta\xE7\xE3o \u2192 Segmentos de Clientes",
31
+ measured: "65 segmentos no V1 e 8 da Artorius. \xC9 o que ela mais tem de marketing \u2014 e o que mais falta aqui.",
32
+ missing: [
33
+ "O construtor de recorte: campo, operador, valor \u2014 e a contagem ao vivo de quantos caem nele",
34
+ "O segmento tem de ser DIN\xC2MICO (recalcula ao usar) ou congelado por escolha, e a diferen\xE7a precisa ficar clara na tela",
35
+ "Ligar o segmento \xE0 campanha \xE9 o que d\xE1 sentido aos dois"
36
+ ]
37
+ }
38
+ );
39
+ }
40
+ function MessageTemplatesView() {
41
+ return /* @__PURE__ */ jsx(
42
+ ComingSoon,
43
+ {
44
+ title: "Modelos de mensagem",
45
+ summary: "O texto pronto que a campanha usa, com os campos que se trocam por nome, data ou valor na hora do envio.",
46
+ v1Path: "Marketing \u2192 Campanhas \u2192 Modelos de Mensagem",
47
+ measured: "43 modelos no V1, nenhum da Artorius. Existe tamb\xE9m `whatsapp_templates`, que \xE9 outra coisa: o modelo APROVADO pela Meta.",
48
+ missing: [
49
+ "As vari\xE1veis e a pr\xE9-visualiza\xE7\xE3o com dado real \u2014 modelo sem pr\xE9via erra em produ\xE7\xE3o",
50
+ "Separar o modelo livre (e-mail, SMS) do modelo aprovado pela Meta (WhatsApp): as regras s\xE3o outras"
51
+ ]
52
+ }
53
+ );
54
+ }
55
+ function SendHistoryView() {
56
+ return /* @__PURE__ */ jsx(
57
+ ComingSoon,
58
+ {
59
+ title: "Hist\xF3rico de envios",
60
+ summary: "O que saiu, para quem, quando, e o que aconteceu depois \u2014 entregue, lido, respondido, recusado.",
61
+ v1Path: "Marketing \u2192 Hist\xF3rico de Envios",
62
+ measured: "Vista de leitura no V1 sobre a fila de mensagens.",
63
+ missing: [
64
+ "O evento por destinat\xE1rio, e n\xE3o s\xF3 o total da campanha \u2014 \xE9 onde se descobre que metade da lista tem n\xFAmero errado",
65
+ "O recusado tem de virar regra: quem pediu para n\xE3o receber n\xE3o pode entrar na pr\xF3xima"
66
+ ]
67
+ }
68
+ );
69
+ }
70
+ function QuickTriggersView() {
71
+ return /* @__PURE__ */ jsx(
72
+ ComingSoon,
73
+ {
74
+ title: "Gatilhos r\xE1pidos",
75
+ summary: "O disparo de uma mensagem quando algo acontece \u2014 anivers\xE1rio, primeira compra, sumi\xE7o \u2014 sem montar uma campanha inteira para isso.",
76
+ v1Path: "Marketing \u2192 Gatilhos R\xE1pidos",
77
+ measured: "No V1 \xE9 um atalho para automa\xE7\xF5es j\xE1 prontas, e n\xE3o um cadastro pr\xF3prio.",
78
+ missing: [
79
+ "As Cad\xEAncias daqui j\xE1 fazem a parte dif\xEDcil; falta a porta de um clique",
80
+ "Os aniversariantes do CRM s\xE3o o primeiro gatilho \xF3bvio, e os dois agora est\xE3o no mesmo menu"
81
+ ]
82
+ }
83
+ );
84
+ }
85
+ function BrandView() {
86
+ return /* @__PURE__ */ jsx(
87
+ ComingSoon,
88
+ {
89
+ title: "Marca",
90
+ summary: "As cores, as fontes e o logotipo da casa num lugar s\xF3 \u2014 para a pe\xE7a sair com a cara certa sem ningu\xE9m procurar o arquivo.",
91
+ v1Path: "Marketing \u2192 Marca",
92
+ measured: "6 kits de marca no V1 e UM da Artorius. Ela j\xE1 tem a identidade cadastrada l\xE1.",
93
+ missing: [
94
+ "O kit tem cores, gradientes, fontes e arquivos: s\xE3o quatro tabelas no V1, n\xE3o um formul\xE1rio",
95
+ "As Configura\xE7\xF5es daqui j\xE1 guardam a cor e o logotipo do tenant \u2014 decidir se este \xE9 o mesmo lugar antes de criar o segundo"
96
+ ]
97
+ }
98
+ );
99
+ }
100
+ function AuthorizedImagesView() {
101
+ return /* @__PURE__ */ jsx(
102
+ ComingSoon,
103
+ {
104
+ title: "Imagens autorizadas",
105
+ summary: "As fotos que a casa PODE usar: quem aparece nelas assinou a autoriza\xE7\xE3o, e o prazo dela ainda n\xE3o venceu.",
106
+ v1Path: "Marketing \u2192 Imagens autorizadas",
107
+ measured: "No V1 vive junto do cadastro do cliente, onde a autoriza\xE7\xE3o \xE9 dada.",
108
+ missing: [
109
+ "A autoriza\xE7\xE3o \xE9 jur\xEDdica e tem prazo \u2014 o que falta n\xE3o \xE9 a galeria, \xE9 a data de validade e o pedido",
110
+ "Usar foto de cliente sem isso \xE9 risco real, e \xE9 por isso que a tela existe"
111
+ ]
112
+ }
113
+ );
114
+ }
115
+ function ChannelsView({ onOpen }) {
116
+ const t = useTranslation();
117
+ const { conversion, currency } = useMarketingConfig();
118
+ const channels = useMarketingStore((s) => s.channels);
119
+ const sorted = [...channels].sort((a, b) => b.conversions - a.conversions);
120
+ const columns = React11.useMemo(() => [
121
+ { accessorKey: "channelId", header: t("marketing.col.channel"), cell: ({ getValue }) => /* @__PURE__ */ jsx(ChannelCell, { channelId: getValue() }) },
122
+ { accessorKey: "reach", header: t("marketing.col.reach"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
123
+ { accessorKey: "leads", header: t("marketing.col.leads"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
124
+ { accessorKey: "conversions", header: conversion.labelPlural, cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatNumber(getValue()) }) },
125
+ { accessorKey: "cvr", header: t("marketing.col.cvr"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatPercent(getValue()) }) },
126
+ { accessorKey: "spend", header: t("marketing.col.spend"), cell: ({ getValue }) => {
127
+ const v = getValue();
128
+ return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: v > 0 ? formatCurrency(v, currency) : "\u2014" });
129
+ } },
130
+ { accessorKey: "cpa", header: t("marketing.col.cpa"), cell: ({ getValue }) => {
131
+ const v = getValue();
132
+ return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: v > 0 ? formatCurrency(v, currency) : "\u2014" });
133
+ } },
134
+ { accessorKey: "value", header: conversion.valueLabel, cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatCurrency(getValue(), currency) }) },
135
+ { id: "open", header: "", enableSorting: false, cell: () => /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground" }) }
136
+ ], [t, conversion.labelPlural, conversion.valueLabel, currency]);
137
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
138
+ /* @__PURE__ */ jsx(PageToolbar, { range: /* @__PURE__ */ jsx(RangeTabs, {}) }),
139
+ /* @__PURE__ */ jsx(
140
+ DataTable,
141
+ {
142
+ columns,
143
+ data: sorted,
144
+ onRowClick: (row) => onOpen(row.channelId),
145
+ emptyMessage: t("marketing.channels.subtitle")
146
+ }
147
+ )
148
+ ] });
149
+ }
150
+ function ChannelDetailView({ channelId, onBack }) {
151
+ const t = useTranslation();
152
+ const { conversion, currency, labels } = useMarketingConfig();
153
+ const range = useMarketingStore((s) => s.range);
154
+ const channels = useMarketingStore((s) => s.channels);
155
+ const lookup = useChannelLookup();
156
+ const provider = useMarketingProvider();
157
+ const [campaigns, setCampaigns] = React11.useState([]);
158
+ React11.useEffect(() => {
159
+ void provider.listCampaigns({ range, channelId }).then(setCampaigns);
160
+ }, [provider, range, channelId]);
161
+ const perf = channels.find((c) => c.channelId === channelId);
162
+ const channel = lookup.get(channelId);
163
+ const columns = React11.useMemo(() => [
164
+ { accessorKey: "name", header: t("marketing.col.campaign"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
165
+ { accessorKey: "conversions", header: conversion.labelPlural, cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
166
+ { 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()) }) },
167
+ { accessorKey: "spend", header: t("marketing.col.spend"), cell: ({ getValue }) => {
168
+ const v = getValue();
169
+ return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: v > 0 ? formatCurrency(v, currency) : "\u2014" });
170
+ } },
171
+ { accessorKey: "status", header: t("marketing.col.status"), cell: ({ getValue }) => /* @__PURE__ */ jsx(StatusBadge, { status: getValue() }) }
172
+ ], [t, conversion.labelPlural, currency]);
173
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl space-y-6", children: [
174
+ /* @__PURE__ */ jsx(
175
+ SubpageHeader,
176
+ {
177
+ title: channel?.label ?? channelId,
178
+ subtitle: channel?.kind,
179
+ onBack,
180
+ parentLabel: labels.channels
181
+ }
182
+ ),
183
+ perf && /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4 lg:grid-cols-4", children: [
184
+ /* @__PURE__ */ jsx(KpiCard, { icon: Users, label: t("marketing.metric.reach"), value: formatNumber(perf.reach) }),
185
+ /* @__PURE__ */ jsx(KpiCard, { icon: Target, label: conversion.labelPlural, value: formatNumber(perf.conversions) }),
186
+ /* @__PURE__ */ jsx(KpiCard, { icon: Percent, label: t("marketing.metric.cvr"), value: formatPercent(perf.cvr) }),
187
+ /* @__PURE__ */ jsx(KpiCard, { icon: DollarSign, label: t("marketing.col.cpa"), value: perf.cpa > 0 ? formatCurrency(perf.cpa, currency) : "\u2014", sub: perf.spend > 0 ? `${formatCurrency(perf.spend, currency)} ${t("marketing.metric.spend")}` : "\u2014" })
188
+ ] }),
189
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
190
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-foreground", children: t("marketing.channels.campaignsOn") }),
191
+ /* @__PURE__ */ jsx(DataTable, { columns, data: campaigns, emptyMessage: t("marketing.channels.noCampaigns") })
192
+ ] })
193
+ ] });
194
+ }
195
+ var STATUSES = ["draft", "active", "paused"];
196
+ var PURPOSES = ["promotion", "newsletter", "survey", "transactional"];
197
+ function CampaignComposer({ open, onClose, initialSegmentId }) {
198
+ const t = useTranslation();
199
+ const { channels } = useMarketingConfig();
200
+ const provider = useMarketingProvider();
201
+ const saveCampaign = useMarketingStore((s) => s.saveCampaign);
202
+ const [kind, setKind] = React11.useState("ad_spend");
203
+ const [name, setName] = React11.useState("");
204
+ const [channelId, setChannelId] = React11.useState(channels[0]?.id ?? "");
205
+ const [status, setStatus] = React11.useState("draft");
206
+ const [spend, setSpend] = React11.useState("");
207
+ const [purpose, setPurpose] = React11.useState("promotion");
208
+ const [segmentId, setSegmentId] = React11.useState("");
209
+ const [templateKey, setTemplateKey] = React11.useState("");
210
+ const [segments, setSegments] = React11.useState([]);
211
+ const [saving, setSaving] = React11.useState(false);
212
+ React11.useEffect(() => {
213
+ if (!open) return;
214
+ setName("");
215
+ setChannelId(channels[0]?.id ?? "");
216
+ setSpend("");
217
+ setPurpose("promotion");
218
+ setTemplateKey("");
219
+ setStatus("draft");
220
+ setSegmentId(initialSegmentId ?? "");
221
+ setKind(initialSegmentId ? "outbound" : "ad_spend");
222
+ void provider.listAudienceSegments().then(setSegments).catch(() => setSegments([]));
223
+ }, [open, channels, initialSegmentId, provider]);
224
+ const isOutbound = kind === "outbound";
225
+ const canSave = name.trim().length > 0 && (isOutbound ? Boolean(segmentId && templateKey.trim()) : Boolean(channelId));
226
+ async function handleSave() {
227
+ if (!canSave) return;
228
+ setSaving(true);
229
+ try {
230
+ await saveCampaign({
231
+ name: name.trim(),
232
+ // Um disparo também tem canal: é por ele que o relatório soma o que
233
+ // saiu por WhatsApp e o que saiu por e-mail.
234
+ channelId: channelId || channels[0]?.id || "email",
235
+ status,
236
+ kind,
237
+ purpose: isOutbound ? purpose : "promotion",
238
+ segmentId: isOutbound ? segmentId : void 0,
239
+ templateKey: isOutbound ? templateKey.trim() : void 0,
240
+ start: (/* @__PURE__ */ new Date()).toISOString(),
241
+ spend: isOutbound ? 0 : Number(spend) || 0
242
+ });
243
+ onClose();
244
+ } finally {
245
+ setSaving(false);
246
+ }
247
+ }
248
+ const selectedSegment = segments.find((s) => s.id === segmentId);
249
+ return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (o) => {
250
+ if (!o) onClose();
251
+ }, children: /* @__PURE__ */ jsxs(ModalContent, { size: "md", children: [
252
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: t("marketing.composer.title") }) }),
253
+ /* @__PURE__ */ jsxs(ModalBody, { className: "space-y-4", children: [
254
+ /* @__PURE__ */ jsx(
255
+ SegmentedControl,
256
+ {
257
+ className: "w-full",
258
+ value: kind,
259
+ onChange: setKind,
260
+ "aria-label": t("marketing.composer.kind"),
261
+ options: [
262
+ { value: "ad_spend", label: t("marketing.composer.kindSpend") },
263
+ { value: "outbound", label: t("marketing.composer.kindOutbound") }
264
+ ]
265
+ }
266
+ ),
267
+ /* @__PURE__ */ jsxs("p", { className: "-mt-2 flex items-start gap-1.5 text-[11px] text-muted-foreground", children: [
268
+ isOutbound ? /* @__PURE__ */ jsx(Send, { className: "mt-0.5 h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(Megaphone, { className: "mt-0.5 h-3 w-3 shrink-0" }),
269
+ isOutbound ? t("marketing.composer.kindOutboundHint") : t("marketing.composer.kindSpendHint")
270
+ ] }),
271
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
272
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.name") }),
273
+ /* @__PURE__ */ jsx(Input, { value: name, onChange: (e) => setName(e.target.value), placeholder: t("marketing.composer.namePlaceholder") })
274
+ ] }),
275
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
276
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.channel") }),
277
+ /* @__PURE__ */ jsxs(Select, { value: channelId, onValueChange: setChannelId, children: [
278
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("marketing.composer.channelPlaceholder") }) }),
279
+ /* @__PURE__ */ jsx(SelectContent, { children: channels.map((c) => /* @__PURE__ */ jsx(SelectItem, { value: c.id, children: c.label }, c.id)) })
280
+ ] })
281
+ ] }),
282
+ isOutbound ? /* @__PURE__ */ jsxs(Fragment, { children: [
283
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
284
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.audience") }),
285
+ segments.length > 0 ? /* @__PURE__ */ jsxs(Select, { value: segmentId, onValueChange: setSegmentId, children: [
286
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("marketing.composer.audiencePlaceholder") }) }),
287
+ /* @__PURE__ */ jsx(SelectContent, { children: segments.map((s) => /* @__PURE__ */ jsx(SelectItem, { value: s.id, children: s.name }, s.id)) })
288
+ ] }) : /* @__PURE__ */ jsx("p", { className: "rounded-lg border border-dashed px-3 py-2 text-xs text-muted-foreground", children: t("marketing.composer.noSegments") }),
289
+ selectedSegment?.description && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [
290
+ /* @__PURE__ */ jsx(Users, { className: "h-3 w-3" }),
291
+ selectedSegment.description
292
+ ] })
293
+ ] }),
294
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
295
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.purpose") }),
296
+ /* @__PURE__ */ jsxs(Select, { value: purpose, onValueChange: (v) => setPurpose(v), children: [
297
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
298
+ /* @__PURE__ */ jsx(SelectContent, { children: PURPOSES.map((p) => /* @__PURE__ */ jsx(SelectItem, { value: p, children: t(`marketing.purpose.${p}`) }, p)) })
299
+ ] }),
300
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: t(`marketing.purpose.${purpose}.hint`) })
301
+ ] }),
302
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
303
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.template") }),
304
+ /* @__PURE__ */ jsx(Input, { value: templateKey, onChange: (e) => setTemplateKey(e.target.value), placeholder: "ex.: campanha_reativacao" }),
305
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: t("marketing.composer.templateHint") })
306
+ ] })
307
+ ] }) : /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
308
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.budget") }),
309
+ /* @__PURE__ */ jsx(Input, { type: "number", min: 0, value: spend, onChange: (e) => setSpend(e.target.value), placeholder: "0" })
310
+ ] }),
311
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
312
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.composer.status") }),
313
+ /* @__PURE__ */ jsxs(Select, { value: status, onValueChange: (v) => setStatus(v), children: [
314
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
315
+ /* @__PURE__ */ jsx(SelectContent, { children: STATUSES.map((s) => /* @__PURE__ */ jsx(SelectItem, { value: s, className: "capitalize", children: t(`marketing.status.${s}`) }, s)) })
316
+ ] })
317
+ ] })
318
+ ] }),
319
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
320
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: t("marketing.composer.cancel") }),
321
+ /* @__PURE__ */ jsx(Button, { onClick: handleSave, disabled: saving || !canSave, children: t("marketing.composer.create") })
322
+ ] })
323
+ ] }) });
324
+ }
325
+ function CampaignsView() {
326
+ const t = useTranslation();
327
+ const { conversion, currency } = useMarketingConfig();
328
+ const campaigns = useMarketingStore((s) => s.campaigns);
329
+ const deleteCampaign = useMarketingStore((s) => s.deleteCampaign);
330
+ const [composerOpen, setComposerOpen] = React11.useState(false);
331
+ const columns = React11.useMemo(() => [
332
+ { accessorKey: "name", header: t("marketing.col.campaign"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: getValue() }) },
333
+ { accessorKey: "channelId", header: t("marketing.col.channel"), cell: ({ getValue }) => /* @__PURE__ */ jsx(ChannelCell, { channelId: getValue() }) },
334
+ { accessorKey: "reach", header: t("marketing.col.reach"), cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatNumber(getValue()) }) },
335
+ { accessorKey: "conversions", header: conversion.labelPlural, cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatNumber(getValue()) }) },
336
+ { 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()) }) },
337
+ { accessorKey: "spend", header: t("marketing.col.spend"), cell: ({ getValue }) => {
338
+ const v = getValue();
339
+ return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: v > 0 ? formatCurrency(v, currency) : "\u2014" });
340
+ } },
341
+ { accessorKey: "status", header: t("marketing.col.status"), cell: ({ getValue }) => /* @__PURE__ */ jsx(StatusBadge, { status: getValue() }) },
342
+ {
343
+ id: "actions",
344
+ header: "",
345
+ enableSorting: false,
346
+ cell: ({ row }) => /* @__PURE__ */ jsx(
347
+ RowActions,
348
+ {
349
+ actions: [{
350
+ id: "delete",
351
+ label: t("marketing.campaigns.delete"),
352
+ icon: Trash2,
353
+ intent: "danger",
354
+ onSelect: () => {
355
+ void deleteCampaign(row.original.id);
356
+ }
357
+ }]
358
+ }
359
+ )
360
+ }
361
+ ], [t, conversion.labelPlural, currency, deleteCampaign]);
362
+ const createActions = React11.useMemo(() => [
363
+ {
364
+ id: "with-ai",
365
+ label: t("assistant.askAI"),
366
+ description: t("marketing.campaigns.aiHint"),
367
+ icon: WandSparkles,
368
+ ai: true,
369
+ intent: "secondary",
370
+ // Both entries end in a new campaign, so the circle is a "+" even though
371
+ // neither icon is one — a menu of ways to create is still a create.
372
+ creates: true,
373
+ permission: { feature: "marketing", action: "create" },
374
+ onSelect: () => askAI({
375
+ hint: t("marketing.campaigns.askPlaceholder"),
376
+ intro: {
377
+ role: "growth",
378
+ persona: t("marketing.campaigns.askPersona"),
379
+ pitch: t("marketing.campaigns.askPitch"),
380
+ description: t("marketing.campaigns.askDescription"),
381
+ suggestions: [0, 1, 2].map((i) => ({ label: t(`marketing.campaigns.askSuggestion.${i}`) }))
382
+ }
383
+ })
384
+ },
385
+ {
386
+ id: "by-hand",
387
+ label: t("marketing.campaigns.newByHand"),
388
+ description: t("marketing.campaigns.newByHandHint"),
389
+ icon: SlidersHorizontal,
390
+ intent: "primary",
391
+ creates: true,
392
+ permission: { feature: "marketing", action: "create" },
393
+ onSelect: () => setComposerOpen(true)
394
+ }
395
+ ], [t]);
396
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
397
+ /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(PageActions, { actions: createActions }) }),
398
+ /* @__PURE__ */ jsx(PageToolbar, { range: /* @__PURE__ */ jsx(RangeTabs, {}) }),
399
+ /* @__PURE__ */ jsx(DataTable, { columns, data: campaigns, emptyMessage: t("marketing.campaigns.empty") }),
400
+ /* @__PURE__ */ jsx(CampaignComposer, { open: composerOpen, onClose: () => setComposerOpen(false) })
401
+ ] });
402
+ }
403
+ function FunnelView() {
404
+ const t = useTranslation();
405
+ const { conversion } = useMarketingConfig();
406
+ const funnel = useMarketingStore((s) => s.funnel);
407
+ const max = Math.max(...funnel.map((s) => s.count), 1);
408
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-4xl space-y-4", children: [
409
+ /* @__PURE__ */ jsx(PageToolbar, { range: /* @__PURE__ */ jsx(RangeTabs, {}) }),
410
+ /* @__PURE__ */ jsxs("div", { className: "rounded-card border border-border bg-card p-5", children: [
411
+ /* @__PURE__ */ jsx("div", { className: "space-y-3", children: funnel.map((stage, i) => {
412
+ const pct = max > 0 ? Math.max(stage.count / max * 100, 4) : 0;
413
+ const prev = funnel[i - 1];
414
+ const stepRate = prev && prev.count > 0 ? stage.count / prev.count * 100 : null;
415
+ return /* @__PURE__ */ jsxs("div", { children: [
416
+ /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between text-sm", children: [
417
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: stage.label }),
418
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
419
+ formatNumber(stage.count),
420
+ stepRate != null && /* @__PURE__ */ jsxs("span", { className: "ml-2 text-xs", children: [
421
+ "(",
422
+ formatPercent(stepRate, 0),
423
+ " ",
424
+ t("marketing.funnel.ofPrev"),
425
+ ")"
426
+ ] })
427
+ ] })
428
+ ] }),
429
+ /* @__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}` } }) })
430
+ ] }, stage.id);
431
+ }) }),
432
+ /* @__PURE__ */ jsxs("div", { className: "mt-5 border-t border-border pt-4 text-sm", children: [
433
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
434
+ t("marketing.funnel.overall"),
435
+ ": "
436
+ ] }),
437
+ /* @__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" })
438
+ ] })
439
+ ] })
440
+ ] });
441
+ }
442
+ function CadencesView({ surface: Surface }) {
443
+ const t = useTranslation();
444
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
445
+ /* @__PURE__ */ jsx(SubpageHeader, { title: t("marketing.cadences.title"), subtitle: t("marketing.cadences.subtitle") }),
446
+ Surface ? /* @__PURE__ */ jsx(Surface, {}) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1 rounded-lg border border-dashed py-12 text-center", children: [
447
+ /* @__PURE__ */ jsx(Workflow, { className: "mb-2 h-9 w-9 text-muted-foreground/40", strokeWidth: 1.5 }),
448
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("marketing.cadences.missingTitle") }),
449
+ /* @__PURE__ */ jsx("p", { className: "max-w-md text-xs text-muted-foreground", children: t("marketing.cadences.missingHint") })
450
+ ] })
451
+ ] });
452
+ }
453
+
454
+ // src/types.ts
455
+ var CAPTURE_FIELDS = ["name", "phone", "email", "document", "notes"];
456
+ function slugify(value) {
457
+ return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9:]+/g, "-").replace(/^-+|-+$/g, "");
458
+ }
459
+ function LandingPageComposer({
460
+ open,
461
+ page,
462
+ onClose
463
+ }) {
464
+ const t = useTranslation();
465
+ const sites = useMarketingStore((s) => s.sites);
466
+ const leadTags = useMarketingStore((s) => s.leadTags);
467
+ const saveLandingPage = useMarketingStore((s) => s.saveLandingPage);
468
+ const [siteId, setSiteId] = React11.useState("");
469
+ const [name, setName] = React11.useState("");
470
+ const [route, setRoute] = React11.useState("");
471
+ const [status, setStatus] = React11.useState("live");
472
+ const [prompt, setPrompt] = React11.useState("");
473
+ const [tags, setTags] = React11.useState([]);
474
+ const [newTag, setNewTag] = React11.useState("");
475
+ const [destination, setDestination] = React11.useState("lead");
476
+ const [required, setRequired] = React11.useState(["name"]);
477
+ const [minH, setMinH] = React11.useState("");
478
+ const [maxD, setMaxD] = React11.useState("");
479
+ const [saving, setSaving] = React11.useState(false);
480
+ React11.useEffect(() => {
481
+ if (!open) return;
482
+ setSiteId(page?.siteId ?? sites[0]?.id ?? "");
483
+ setName(page?.name ?? "");
484
+ setRoute(page?.route ?? "");
485
+ setStatus(page?.status ?? "live");
486
+ setPrompt(page?.prompt ?? "");
487
+ setTags(page?.leadTags ?? []);
488
+ setNewTag("");
489
+ setDestination(page?.captureDestination ?? "lead");
490
+ setRequired(page?.captureRequiredFields?.length ? page.captureRequiredFields : ["name"]);
491
+ setMinH(page?.minAdvanceHours != null ? String(page.minAdvanceHours) : "");
492
+ setMaxD(page?.maxAdvanceDays != null ? String(page.maxAdvanceDays) : "");
493
+ }, [open, page, sites]);
494
+ const site = sites.find((s) => s.id === siteId);
495
+ const previewPath = route.trim() ? route.trim().startsWith("/") ? route.trim() : `/${route.trim()}` : "";
496
+ const options = React11.useMemo(() => {
497
+ const byslug = new Map(leadTags.map((tag) => [tag.slug, tag.name]));
498
+ for (const slug of tags) if (!byslug.has(slug)) byslug.set(slug, slug);
499
+ return [...byslug].map(([slug, label]) => ({ slug, label }));
500
+ }, [leadTags, tags]);
501
+ function toggleTag(slug) {
502
+ setTags((current) => current.includes(slug) ? current.filter((s) => s !== slug) : [...current, slug]);
503
+ }
504
+ function addTag() {
505
+ const slug = slugify(newTag);
506
+ if (!slug) return;
507
+ setTags((current) => current.includes(slug) ? current : [...current, slug]);
508
+ setNewTag("");
509
+ }
510
+ async function handleSave() {
511
+ if (!siteId || !name.trim() || !route.trim()) return;
512
+ setSaving(true);
513
+ try {
514
+ await saveLandingPage({
515
+ id: page?.id,
516
+ siteId,
517
+ name: name.trim(),
518
+ route: route.trim(),
519
+ leadTags: tags,
520
+ status,
521
+ prompt: status === "planned" ? prompt.trim() || void 0 : void 0,
522
+ captureDestination: destination,
523
+ captureRequiredFields: required,
524
+ minAdvanceHours: destination !== "lead" && minH ? Number(minH) : null,
525
+ maxAdvanceDays: destination !== "lead" && maxD ? Number(maxD) : null
526
+ });
527
+ onClose();
528
+ } finally {
529
+ setSaving(false);
530
+ }
531
+ }
532
+ return (
533
+ // Beside the list, not over it: the routes already taken are what you check
534
+ // a new one against, and the dialog covered them.
535
+ /* @__PURE__ */ jsxs(
536
+ RightRailPage,
537
+ {
538
+ title: page ? page.name || t("marketing.landing.edit") : t("marketing.landing.new"),
539
+ open,
540
+ onClose,
541
+ width: "max-w-md",
542
+ children: [
543
+ /* @__PURE__ */ jsxs(SheetBody, { className: "space-y-4", children: [
544
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
545
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.landing.site") }),
546
+ /* @__PURE__ */ jsxs(Select, { value: siteId, onValueChange: setSiteId, children: [
547
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("marketing.landing.sitePlaceholder") }) }),
548
+ /* @__PURE__ */ jsx(SelectContent, { children: sites.map((s) => /* @__PURE__ */ jsx(SelectItem, { value: s.id, children: s.url ? `${s.name} \xB7 ${s.url.replace(/^https?:\/\//, "")}` : s.name }, s.id)) })
549
+ ] })
550
+ ] }),
551
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
552
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.landing.name") }),
553
+ /* @__PURE__ */ jsx(Input, { value: name, onChange: (e) => setName(e.target.value), placeholder: t("marketing.landing.namePlaceholder") })
554
+ ] }),
555
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
556
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.landing.route") }),
557
+ /* @__PURE__ */ jsx(Input, { value: route, onChange: (e) => setRoute(e.target.value), placeholder: "/lp-capilar" }),
558
+ site?.url && previewPath ? /* @__PURE__ */ jsxs("span", { className: "block text-xs text-muted-foreground", children: [
559
+ site.url.replace(/\/+$/, ""),
560
+ previewPath
561
+ ] }) : null
562
+ ] }),
563
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
564
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.landing.status") }),
565
+ /* @__PURE__ */ jsx(
566
+ SegmentedControl,
567
+ {
568
+ value: status,
569
+ onChange: (v) => setStatus(v),
570
+ options: [
571
+ { value: "live", label: t("marketing.landing.statusLive") },
572
+ { value: "planned", label: t("marketing.landing.statusPlanned") }
573
+ ]
574
+ }
575
+ )
576
+ ] }),
577
+ status === "planned" ? /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
578
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
579
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.landing.prompt") }),
580
+ site?.editorUrl ? /* @__PURE__ */ jsxs(
581
+ Button,
582
+ {
583
+ variant: "bare",
584
+ size: "bare",
585
+ type: "button",
586
+ onClick: () => window.open(site.editorUrl, "_blank", "noopener,noreferrer"),
587
+ className: "inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline",
588
+ children: [
589
+ /* @__PURE__ */ jsx(Sparkles, { className: "h-3.5 w-3.5" }),
590
+ " ",
591
+ t("marketing.landing.buildWithFayz")
592
+ ]
593
+ }
594
+ ) : null
595
+ ] }),
596
+ /* @__PURE__ */ jsx(
597
+ Textarea,
598
+ {
599
+ value: prompt,
600
+ onChange: (e) => setPrompt(e.target.value),
601
+ rows: 3,
602
+ placeholder: t("marketing.landing.promptPlaceholder"),
603
+ className: "rounded-md border-border bg-background shadow-none focus-visible:ring-offset-0"
604
+ }
605
+ )
606
+ ] }) : null,
607
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
608
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: t("marketing.landing.leadTag") }),
609
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5 rounded-md border border-border bg-background px-2 py-2 focus-within:ring-2 focus-within:ring-ring", children: [
610
+ options.map((opt) => {
611
+ const active = tags.includes(opt.slug);
612
+ return /* @__PURE__ */ jsx(
613
+ Badge,
614
+ {
615
+ variant: active ? "default" : "outline",
616
+ className: "cursor-pointer text-xs",
617
+ onClick: () => toggleTag(opt.slug),
618
+ children: opt.label
619
+ },
620
+ opt.slug
621
+ );
622
+ }),
623
+ /* @__PURE__ */ jsx("div", { className: "min-w-[8rem] flex-1", children: /* @__PURE__ */ jsx(
624
+ Input,
625
+ {
626
+ value: newTag,
627
+ onChange: (e) => setNewTag(e.target.value),
628
+ onKeyDown: (e) => {
629
+ if (e.key === "Enter") {
630
+ e.preventDefault();
631
+ addTag();
632
+ }
633
+ },
634
+ onBlur: addTag,
635
+ placeholder: t("marketing.landing.leadTagPlaceholder"),
636
+ className: "h-auto rounded-none border-0 bg-transparent px-1 py-0 text-xs shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
637
+ }
638
+ ) })
639
+ ] }),
640
+ /* @__PURE__ */ jsx("span", { className: "block text-xs text-muted-foreground", children: t("marketing.landing.leadTagHint") })
641
+ ] }),
642
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-card border bg-muted/20 p-3", "data-testid": "landing-capture", children: [
643
+ /* @__PURE__ */ jsxs("div", { children: [
644
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("marketing.landing.formTitle") }),
645
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.landing.formHint") })
646
+ ] }),
647
+ /* @__PURE__ */ jsxs("div", { children: [
648
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium", children: t("marketing.landing.destination") }),
649
+ /* @__PURE__ */ jsxs(Select, { value: destination, onValueChange: (v) => setDestination(v), children: [
650
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "h-9 text-sm", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
651
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
652
+ /* @__PURE__ */ jsx(SelectItem, { value: "lead", children: t("marketing.landing.dest.lead") }),
653
+ /* @__PURE__ */ jsx(SelectItem, { value: "booking", children: t("marketing.landing.dest.booking") }),
654
+ /* @__PURE__ */ jsx(SelectItem, { value: "reservation", children: t("marketing.landing.dest.reservation") })
655
+ ] })
656
+ ] })
657
+ ] }),
658
+ /* @__PURE__ */ jsxs("div", { children: [
659
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium", children: t("marketing.landing.required") }),
660
+ /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground", children: t("marketing.landing.requiredHint") }),
661
+ /* @__PURE__ */ jsx("div", { className: "mt-2 flex flex-wrap gap-1.5", children: CAPTURE_FIELDS.map((f) => {
662
+ const on = required.includes(f);
663
+ return /* @__PURE__ */ jsx(
664
+ Button,
665
+ {
666
+ variant: "bare",
667
+ size: "bare",
668
+ type: "button",
669
+ className: `font-normal rounded-full border px-2.5 py-1 text-xs ${on ? "border-primary bg-primary/10 text-primary" : "text-muted-foreground"}`,
670
+ onClick: () => setRequired((prev) => on ? prev.filter((x) => x !== f) : [...prev, f]),
671
+ children: t(`marketing.landing.field.${f}`)
672
+ },
673
+ f
674
+ );
675
+ }) })
676
+ ] }),
677
+ destination !== "lead" && /* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-2", "data-testid": "landing-window", children: [
678
+ /* @__PURE__ */ jsxs("div", { children: [
679
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium", children: t("marketing.landing.minAdvance") }),
680
+ /* @__PURE__ */ jsx(Input, { type: "number", min: 0, value: minH, onChange: (e) => setMinH(e.target.value), placeholder: "2" }),
681
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-[10px] text-muted-foreground", children: t("marketing.landing.minAdvanceHint") })
682
+ ] }),
683
+ /* @__PURE__ */ jsxs("div", { children: [
684
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium", children: t("marketing.landing.maxAdvance") }),
685
+ /* @__PURE__ */ jsx(Input, { type: "number", min: 1, value: maxD, onChange: (e) => setMaxD(e.target.value), placeholder: "60" }),
686
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-[10px] text-muted-foreground", children: t("marketing.landing.maxAdvanceHint") })
687
+ ] })
688
+ ] }),
689
+ page?.captureKey && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground", children: t("marketing.landing.publicKey", { key: page.captureKey }) })
690
+ ] })
691
+ ] }),
692
+ /* @__PURE__ */ jsx(SheetFooter, { children: /* @__PURE__ */ jsx(
693
+ Button,
694
+ {
695
+ className: "flex-1",
696
+ onClick: handleSave,
697
+ disabled: saving || !siteId || !name.trim() || !route.trim(),
698
+ children: page ? t("marketing.landing.save") : t("marketing.landing.create")
699
+ }
700
+ ) })
701
+ ]
702
+ }
703
+ )
704
+ );
705
+ }
706
+ function LandingPagesView() {
707
+ const t = useTranslation();
708
+ const pages = useMarketingStore((s) => s.landingPages);
709
+ const deleteLandingPage = useMarketingStore((s) => s.deleteLandingPage);
710
+ const [editing, setEditing] = React11.useState(null);
711
+ const [composerOpen, setComposerOpen] = React11.useState(false);
712
+ const measured = pages.filter((p) => p.visits !== null);
713
+ const totalVisits = measured.reduce((s, p) => s + (p.visits ?? 0), 0);
714
+ const totalLeads = pages.reduce((s, p) => s + p.conversions, 0);
715
+ function openNew() {
716
+ setEditing(null);
717
+ setComposerOpen(true);
718
+ }
719
+ function openEdit(page) {
720
+ setEditing(page);
721
+ setComposerOpen(true);
722
+ }
723
+ const columns = React11.useMemo(() => [
724
+ {
725
+ accessorKey: "name",
726
+ header: t("marketing.col.page"),
727
+ cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
728
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 truncate font-medium text-foreground", children: [
729
+ row.original.name,
730
+ row.original.status === "planned" ? /* @__PURE__ */ jsx("span", { className: "rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold text-amber-800", children: t("marketing.landing.statusPlanned") }) : null
731
+ ] }),
732
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-muted-foreground", children: row.original.siteName })
733
+ ] })
734
+ },
735
+ {
736
+ accessorKey: "route",
737
+ header: t("marketing.col.route"),
738
+ cell: ({ row }) => /* @__PURE__ */ jsxs(
739
+ "a",
740
+ {
741
+ href: row.original.url,
742
+ target: "_blank",
743
+ rel: "noreferrer",
744
+ onClick: (e) => e.stopPropagation(),
745
+ className: "inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground hover:underline",
746
+ children: [
747
+ row.original.route,
748
+ /* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3" })
749
+ ]
750
+ }
751
+ )
752
+ },
753
+ {
754
+ accessorKey: "leadTags",
755
+ header: t("marketing.col.leadTag"),
756
+ cell: ({ getValue }) => {
757
+ const tags = getValue() ?? [];
758
+ if (!tags.length) return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" });
759
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: tags.map((tag) => /* @__PURE__ */ jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground", children: tag }, tag)) });
760
+ }
761
+ },
762
+ {
763
+ accessorKey: "visits",
764
+ header: t("marketing.col.visits"),
765
+ cell: ({ getValue }) => {
766
+ const visits = getValue();
767
+ return visits === null ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", title: t("marketing.landing.visitsPending"), children: "\u2014" }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatNumber(visits) });
768
+ }
769
+ },
770
+ {
771
+ accessorKey: "conversions",
772
+ header: t("marketing.col.leads"),
773
+ cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatNumber(getValue()) })
774
+ },
775
+ {
776
+ accessorKey: "cvr",
777
+ header: t("marketing.col.cvr"),
778
+ cell: ({ getValue }) => {
779
+ const cvr = getValue();
780
+ return cvr === null ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatPercent(cvr) });
781
+ }
782
+ },
783
+ {
784
+ id: "actions",
785
+ header: "",
786
+ enableSorting: false,
787
+ cell: ({ row }) => /* @__PURE__ */ jsx(
788
+ RowActions,
789
+ {
790
+ actions: [
791
+ ...row.original.status === "planned" && row.original.editorUrl ? [{
792
+ id: "build",
793
+ label: t("marketing.landing.buildWithFayz"),
794
+ icon: Sparkles,
795
+ onSelect: () => window.open(row.original.editorUrl, "_blank", "noopener,noreferrer")
796
+ }] : [],
797
+ { id: "edit", label: t("marketing.landing.edit"), icon: Pencil, onSelect: () => openEdit(row.original) },
798
+ {
799
+ id: "delete",
800
+ label: t("marketing.landing.delete"),
801
+ icon: Trash2,
802
+ intent: "danger",
803
+ onSelect: () => {
804
+ void deleteLandingPage(row.original.id);
805
+ }
806
+ }
807
+ ]
808
+ }
809
+ )
810
+ }
811
+ ], [t, deleteLandingPage]);
812
+ const createActions = React11.useMemo(() => [{
813
+ id: "new-page",
814
+ label: t("marketing.landing.new"),
815
+ icon: Plus,
816
+ intent: "primary",
817
+ creates: true,
818
+ permission: { feature: "marketing", action: "create" },
819
+ onSelect: openNew
820
+ }], [t]);
821
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl space-y-4", children: [
822
+ /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(PageActions, { actions: createActions }) }),
823
+ /* @__PURE__ */ jsx(PageToolbar, { range: /* @__PURE__ */ jsx(RangeTabs, {}) }),
824
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-4", children: [
825
+ /* @__PURE__ */ jsx(KpiCard, { icon: Layers, label: t("marketing.metric.pages"), value: String(pages.length) }),
826
+ /* @__PURE__ */ jsx(
827
+ KpiCard,
828
+ {
829
+ icon: MousePointerClick,
830
+ label: t("marketing.metric.totalVisits"),
831
+ value: measured.length > 0 ? formatNumber(totalVisits) : "\u2014"
832
+ }
833
+ ),
834
+ /* @__PURE__ */ jsx(KpiCard, { icon: Users, label: t("marketing.metric.leads"), value: formatNumber(totalLeads) })
835
+ ] }),
836
+ /* @__PURE__ */ jsx(DataTable, { columns, data: pages, emptyMessage: t("marketing.landing.empty") }),
837
+ measured.length < pages.length ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.landing.visitsPending") }) : null,
838
+ /* @__PURE__ */ jsx(LandingPageComposer, { open: composerOpen, page: editing, onClose: () => {
839
+ setComposerOpen(false);
840
+ setEditing(null);
841
+ } })
842
+ ] });
843
+ }
844
+ var ctx = createPluginContext("ContentPlanner");
845
+ var ContentPlannerContextProvider = ctx.ContextProvider;
846
+ ctx.useConfig;
847
+ var useContentPlannerProvider = ctx.useProvider;
848
+ var useContentPlannerStore = ctx.useStore;
849
+
850
+ // src/data/contentTypes.ts
851
+ var PLATFORMS = [
852
+ "instagram",
853
+ "tiktok",
854
+ "youtube",
855
+ "facebook",
856
+ "linkedin",
857
+ "x",
858
+ "pinterest",
859
+ "threads"
860
+ ];
861
+ var POST_STATUS_ORDER = ["idea", "script", "recording", "editing", "scheduled", "published"];
862
+ var POST_FORMATS = ["reel", "static", "carousel", "story", "video", "live", "article"];
863
+ var FORMAT_STYLE = {
864
+ reel: "bg-violet-100 text-violet-700",
865
+ static: "bg-sky-100 text-sky-700",
866
+ carousel: "bg-amber-100 text-amber-700",
867
+ story: "bg-pink-100 text-pink-700",
868
+ video: "bg-red-100 text-red-700",
869
+ live: "bg-rose-100 text-rose-700",
870
+ article: "bg-teal-100 text-teal-700"
871
+ };
872
+ var POST_STATUS_COLOR = {
873
+ idea: "#6b7280",
874
+ script: "#6366f1",
875
+ recording: "#f59e0b",
876
+ editing: "#f97316",
877
+ scheduled: "#3b82f6",
878
+ published: "#10b981"
879
+ };
880
+ function StatusDot({ status, className }) {
881
+ return /* @__PURE__ */ jsx(
882
+ "span",
883
+ {
884
+ className: cn("h-2 w-2 shrink-0 rounded-full", className),
885
+ style: { backgroundColor: POST_STATUS_COLOR[status] }
886
+ }
887
+ );
888
+ }
889
+ var STATUS_STYLE = {
890
+ idea: "bg-slate-100 text-slate-600",
891
+ script: "bg-indigo-100 text-indigo-700",
892
+ recording: "bg-amber-100 text-amber-700",
893
+ editing: "bg-orange-100 text-orange-700",
894
+ scheduled: "bg-sky-100 text-sky-700",
895
+ published: "bg-emerald-100 text-emerald-700"
896
+ };
897
+ function FormatBadge({ format }) {
898
+ const t = useTranslation();
899
+ 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}`) });
900
+ }
901
+ function PostStatusBadge({ status }) {
902
+ const t = useTranslation();
903
+ return /* @__PURE__ */ jsx("span", { className: cn("rounded-full px-2 py-0.5 text-xs font-medium", STATUS_STYLE[status]), children: t(`marketing.content.status.${status}`) });
904
+ }
905
+ function PlatformBadges({ platforms, className }) {
906
+ if (platforms.length === 0) return null;
907
+ 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)) });
908
+ }
909
+ function PlatformPicker({ value, onChange }) {
910
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: PLATFORMS.map((platform) => {
911
+ const selected = value.includes(platform);
912
+ return /* @__PURE__ */ jsxs(
913
+ Button,
914
+ {
915
+ variant: "bare",
916
+ size: "bare",
917
+ type: "button",
918
+ "aria-pressed": selected,
919
+ onClick: () => onChange(selected ? value.filter((p) => p !== platform) : [...value, platform]),
920
+ className: cn(
921
+ "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium capitalize transition-colors",
922
+ selected ? "border-primary bg-primary/10 text-foreground" : "border-border text-muted-foreground hover:bg-muted/40"
923
+ ),
924
+ children: [
925
+ /* @__PURE__ */ jsx(PlatformIcon, { platform, className: "h-3.5 w-3.5" }),
926
+ platform
927
+ ]
928
+ },
929
+ platform
930
+ );
931
+ }) });
932
+ }
933
+ function toDraft(plan) {
934
+ return {
935
+ name: plan.name,
936
+ objective: plan.objective ?? "",
937
+ tone: plan.tone ?? "",
938
+ pillars: plan.pillars,
939
+ formats: plan.formats,
940
+ weeklyFrequency: plan.weeklyFrequency,
941
+ weeksCount: plan.weeksCount,
942
+ briefMd: plan.briefMd
943
+ };
944
+ }
945
+ function Field({ label, children }) {
946
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
947
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: label }),
948
+ children
949
+ ] });
950
+ }
951
+ function useBriefDraft() {
952
+ const plan = useContentPlannerStore((s) => s.activePlan);
953
+ const savePlan = useContentPlannerStore((s) => s.savePlan);
954
+ const [draft, setDraft] = React11.useState(plan ? toDraft(plan) : null);
955
+ const [saving, setSaving] = React11.useState(false);
956
+ React11.useEffect(() => {
957
+ if (plan) setDraft(toDraft(plan));
958
+ }, [plan?.id]);
959
+ const patch = (partial) => setDraft((d) => d ? { ...d, ...partial } : d);
960
+ const dirty = !!plan && !!draft && JSON.stringify(draft) !== JSON.stringify(toDraft(plan));
961
+ const save = async () => {
962
+ if (!plan || !draft) return;
963
+ setSaving(true);
964
+ try {
965
+ await savePlan({
966
+ id: plan.id,
967
+ name: draft.name,
968
+ objective: draft.objective,
969
+ tone: draft.tone,
970
+ pillars: draft.pillars,
971
+ formats: draft.formats,
972
+ weeklyFrequency: draft.weeklyFrequency,
973
+ weeksCount: draft.weeksCount,
974
+ briefMd: draft.briefMd
975
+ });
976
+ } finally {
977
+ setSaving(false);
978
+ }
979
+ };
980
+ return { plan, draft, patch, dirty, saving, save };
981
+ }
982
+ function BriefFields({ draft, patch }) {
983
+ const t = useTranslation();
984
+ const [pillarInput, setPillarInput] = React11.useState("");
985
+ const addPillar = () => {
986
+ const value = pillarInput.trim();
987
+ if (!value || draft.pillars.includes(value)) return;
988
+ patch({ pillars: [...draft.pillars, value] });
989
+ setPillarInput("");
990
+ };
991
+ const toggleFormat = (format) => {
992
+ patch({
993
+ formats: draft.formats.includes(format) ? draft.formats.filter((f) => f !== format) : [...draft.formats, format]
994
+ });
995
+ };
996
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
997
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [
998
+ /* @__PURE__ */ jsx(Field, { label: t("marketing.content.planName"), children: /* @__PURE__ */ jsx(Input, { value: draft.name, onChange: (e) => patch({ name: e.target.value }) }) }),
999
+ /* @__PURE__ */ jsx(Field, { label: t("marketing.content.objective"), children: /* @__PURE__ */ jsx(
1000
+ Input,
1001
+ {
1002
+ value: draft.objective,
1003
+ onChange: (e) => patch({ objective: e.target.value }),
1004
+ placeholder: t("marketing.content.objectivePlaceholder")
1005
+ }
1006
+ ) }),
1007
+ /* @__PURE__ */ jsx(Field, { label: t("marketing.content.tone"), children: /* @__PURE__ */ jsx(
1008
+ Input,
1009
+ {
1010
+ value: draft.tone,
1011
+ onChange: (e) => patch({ tone: e.target.value }),
1012
+ placeholder: t("marketing.content.tonePlaceholder")
1013
+ }
1014
+ ) }),
1015
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
1016
+ /* @__PURE__ */ jsx(Field, { label: t("marketing.content.weeklyFrequency"), children: /* @__PURE__ */ jsx(
1017
+ Input,
1018
+ {
1019
+ type: "number",
1020
+ min: 1,
1021
+ max: 14,
1022
+ value: draft.weeklyFrequency,
1023
+ onChange: (e) => patch({ weeklyFrequency: Math.max(1, Number(e.target.value) || 1) })
1024
+ }
1025
+ ) }),
1026
+ /* @__PURE__ */ jsx(Field, { label: t("marketing.content.weeksCount"), children: /* @__PURE__ */ jsx(
1027
+ Input,
1028
+ {
1029
+ type: "number",
1030
+ min: 1,
1031
+ max: 52,
1032
+ value: draft.weeksCount,
1033
+ onChange: (e) => patch({ weeksCount: Math.max(1, Number(e.target.value) || 1) })
1034
+ }
1035
+ ) })
1036
+ ] })
1037
+ ] }),
1038
+ /* @__PURE__ */ jsx(Field, { label: t("marketing.content.pillars"), children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
1039
+ draft.pillars.map((pillar) => /* @__PURE__ */ jsxs(
1040
+ "span",
1041
+ {
1042
+ className: "inline-flex items-center gap-1 rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium text-foreground",
1043
+ children: [
1044
+ pillar,
1045
+ /* @__PURE__ */ jsx(
1046
+ Button,
1047
+ {
1048
+ variant: "bare",
1049
+ size: "bare",
1050
+ type: "button",
1051
+ onClick: () => patch({ pillars: draft.pillars.filter((p) => p !== pillar) }),
1052
+ className: "text-muted-foreground hover:text-foreground",
1053
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
1054
+ }
1055
+ )
1056
+ ]
1057
+ },
1058
+ pillar
1059
+ )),
1060
+ /* @__PURE__ */ jsx(
1061
+ Input,
1062
+ {
1063
+ value: pillarInput,
1064
+ onChange: (e) => setPillarInput(e.target.value),
1065
+ onKeyDown: (e) => {
1066
+ if (e.key === "Enter") {
1067
+ e.preventDefault();
1068
+ addPillar();
1069
+ }
1070
+ },
1071
+ onBlur: addPillar,
1072
+ placeholder: t("marketing.content.pillarPlaceholder"),
1073
+ className: "h-7 w-44 text-xs"
1074
+ }
1075
+ )
1076
+ ] }) }),
1077
+ /* @__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: [
1078
+ /* @__PURE__ */ jsx(
1079
+ Checkbox,
1080
+ {
1081
+ checked: draft.formats.includes(format),
1082
+ onChange: () => toggleFormat(format)
1083
+ }
1084
+ ),
1085
+ t(`marketing.content.format.${format}`)
1086
+ ] }, format)) }) }),
1087
+ /* @__PURE__ */ jsx(Field, { label: t("marketing.content.briefBody"), children: /* @__PURE__ */ jsx(
1088
+ MarkdownEditor,
1089
+ {
1090
+ value: draft.briefMd,
1091
+ onChange: (briefMd) => patch({ briefMd }),
1092
+ placeholder: t("marketing.content.briefPlaceholder"),
1093
+ editLabel: t("marketing.content.edit"),
1094
+ previewLabel: t("marketing.content.preview"),
1095
+ minRows: 10
1096
+ }
1097
+ ) })
1098
+ ] });
1099
+ }
1100
+ function PlanBriefView({ onBack }) {
1101
+ const t = useTranslation();
1102
+ const { plan, draft, patch, dirty, saving, save } = useBriefDraft();
1103
+ if (!plan || !draft) {
1104
+ return /* @__PURE__ */ jsx("div", { className: "mx-auto max-w-3xl text-sm text-muted-foreground", children: "\u2026" });
1105
+ }
1106
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
1107
+ /* @__PURE__ */ jsx(
1108
+ SubpageHeader,
1109
+ {
1110
+ title: t("marketing.content.brief"),
1111
+ subtitle: plan.name,
1112
+ parentLabel: t("marketing.content.title"),
1113
+ onBack,
1114
+ actions: /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => void save(), disabled: !dirty || saving, children: t("marketing.content.save") })
1115
+ }
1116
+ ),
1117
+ /* @__PURE__ */ jsx(BriefFields, { draft, patch })
1118
+ ] });
1119
+ }
1120
+ function PlanBriefSheet({ open, onClose }) {
1121
+ const t = useTranslation();
1122
+ const { plan, draft, patch, dirty, saving, save } = useBriefDraft();
1123
+ return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (o) => {
1124
+ if (!o) onClose();
1125
+ }, children: /* @__PURE__ */ jsxs(ModalContent, { size: "4xl", children: [
1126
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: plan?.name ?? t("marketing.content.brief") }) }),
1127
+ /* @__PURE__ */ jsx(ModalBody, { children: draft ? /* @__PURE__ */ jsx(BriefFields, { draft, patch }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" }) }),
1128
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
1129
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: t("common.cancel") }),
1130
+ /* @__PURE__ */ jsx(Button, { onClick: () => void save().then(onClose), disabled: !dirty || saving, children: t("marketing.content.save") })
1131
+ ] })
1132
+ ] }) });
1133
+ }
1134
+ function canUploadMedia() {
1135
+ return !!getSupabaseClientOptional();
1136
+ }
1137
+ async function uploadPostMedia(postId, file) {
1138
+ const supabase = getSupabaseClientOptional();
1139
+ if (!supabase) throw new Error("Supabase not initialized");
1140
+ const tenantId = getActiveTenantId();
1141
+ if (!tenantId) throw new Error("No active tenant");
1142
+ const ext = file.name.split(".").pop()?.toLowerCase() ?? "bin";
1143
+ const path = `content/${tenantId}/${postId}-${Date.now()}.${ext}`;
1144
+ const { error } = await supabase.storage.from("mkt-media").upload(path, file, { upsert: true });
1145
+ if (error) throw error;
1146
+ const { data } = supabase.storage.from("mkt-media").getPublicUrl(path);
1147
+ return data.publicUrl;
1148
+ }
1149
+ function newId() {
1150
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
1151
+ }
1152
+ function RecordingChecklist({ post }) {
1153
+ const t = useTranslation();
1154
+ const savePost = useContentPlannerStore((s) => s.savePost);
1155
+ const [items, setItems] = React11.useState(post.checklist);
1156
+ const [text, setText] = React11.useState("");
1157
+ React11.useEffect(() => {
1158
+ setItems(post.checklist);
1159
+ }, [post.id]);
1160
+ const persist = (next) => {
1161
+ setItems(next);
1162
+ void savePost({ id: post.id, checklist: next });
1163
+ };
1164
+ const toggle = (id) => persist(items.map((i) => i.id === id ? { ...i, done: !i.done } : i));
1165
+ const remove = (id) => persist(items.filter((i) => i.id !== id));
1166
+ const add = () => {
1167
+ const value = text.trim();
1168
+ if (!value) return;
1169
+ persist([...items, { id: newId(), text: value, done: false }]);
1170
+ setText("");
1171
+ };
1172
+ const generateFromScript = () => {
1173
+ const takes = [...post.contentMd.matchAll(/^##\s+(TAKE\s+.+)$/gim)].map(
1174
+ (m) => m[1].trim()
1175
+ );
1176
+ const existing = new Set(items.map((i) => i.text));
1177
+ const fresh = takes.filter((take) => !existing.has(take)).map((take) => ({ id: newId(), text: take, done: false }));
1178
+ if (fresh.length > 0) persist([...items, ...fresh]);
1179
+ };
1180
+ const hasTakes = /^##\s+TAKE\s+/im.test(post.contentMd);
1181
+ const done = items.filter((i) => i.done).length;
1182
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1183
+ items.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1184
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [
1185
+ /* @__PURE__ */ jsxs("span", { children: [
1186
+ done,
1187
+ "/",
1188
+ items.length
1189
+ ] }),
1190
+ hasTakes && /* @__PURE__ */ jsxs(
1191
+ Button,
1192
+ {
1193
+ variant: "bare",
1194
+ size: "bare",
1195
+ type: "button",
1196
+ onClick: generateFromScript,
1197
+ className: "flex items-center gap-1 font-medium text-muted-foreground transition-colors hover:text-foreground",
1198
+ children: [
1199
+ /* @__PURE__ */ jsx(Sparkles, { className: "h-3 w-3" }),
1200
+ t("marketing.content.checklistGenerate")
1201
+ ]
1202
+ }
1203
+ )
1204
+ ] }),
1205
+ /* @__PURE__ */ jsx("div", { className: "h-1 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsx(
1206
+ "div",
1207
+ {
1208
+ className: "h-full rounded-full bg-primary transition-[width]",
1209
+ style: { width: items.length ? `${done / items.length * 100}%` : 0 }
1210
+ }
1211
+ ) })
1212
+ ] }),
1213
+ items.length === 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-card border border-dashed border-border p-3 text-center", children: [
1214
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.checklistEmpty") }),
1215
+ hasTakes && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: generateFromScript, children: [
1216
+ /* @__PURE__ */ jsx(Sparkles, { className: "mr-1.5 h-3.5 w-3.5" }),
1217
+ t("marketing.content.checklistGenerate")
1218
+ ] })
1219
+ ] }),
1220
+ /* @__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: [
1221
+ /* @__PURE__ */ jsxs("label", { className: "flex min-w-0 flex-1 cursor-pointer items-start gap-2.5", children: [
1222
+ /* @__PURE__ */ jsx(Checkbox, { checked: item.done, onChange: () => toggle(item.id), className: "mt-0.5" }),
1223
+ /* @__PURE__ */ jsx(
1224
+ "span",
1225
+ {
1226
+ className: cn(
1227
+ "min-w-0 flex-1 text-sm leading-snug",
1228
+ item.done ? "text-muted-foreground line-through" : "text-foreground"
1229
+ ),
1230
+ children: item.text
1231
+ }
1232
+ )
1233
+ ] }),
1234
+ /* @__PURE__ */ jsx(
1235
+ Button,
1236
+ {
1237
+ variant: "bare",
1238
+ size: "bare",
1239
+ type: "button",
1240
+ "aria-label": t("marketing.content.cancel"),
1241
+ onClick: () => remove(item.id),
1242
+ className: "rounded p-0.5 text-muted-foreground/50 opacity-0 transition-opacity hover:text-destructive group-hover/item:opacity-100 focus:opacity-100",
1243
+ children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
1244
+ }
1245
+ )
1246
+ ] }, item.id)) }),
1247
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1248
+ /* @__PURE__ */ jsx(
1249
+ Input,
1250
+ {
1251
+ value: text,
1252
+ onChange: (e) => setText(e.target.value),
1253
+ placeholder: t("marketing.content.checklistAddPlaceholder"),
1254
+ onKeyDown: (e) => {
1255
+ if (e.key === "Enter") add();
1256
+ },
1257
+ className: "h-8 text-sm"
1258
+ }
1259
+ ),
1260
+ /* @__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" }) })
1261
+ ] })
1262
+ ] });
1263
+ }
1264
+ function toCaption(md) {
1265
+ return md.replace(/^#+\s*/gm, "").replace(/\*\*/g, "").replace(/^[-*]\s+/gm, "").trim();
1266
+ }
1267
+ function isVideo(url) {
1268
+ return !!url && /\.(mp4|webm|mov)($|\?)/i.test(url);
1269
+ }
1270
+ function Media({ mediaUrl, className }) {
1271
+ if (!mediaUrl) {
1272
+ 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" }) });
1273
+ }
1274
+ 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) });
1275
+ }
1276
+ function Avatar({ name, ring }) {
1277
+ return /* @__PURE__ */ jsx(
1278
+ "span",
1279
+ {
1280
+ className: cn(
1281
+ "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold text-foreground",
1282
+ ring && "ring-2 ring-offset-2 ring-pink-500/60"
1283
+ ),
1284
+ children: (name.replace(/^@/, "")[0] ?? "?").toUpperCase()
1285
+ }
1286
+ );
1287
+ }
1288
+ function InstagramFrame({ caption, mediaUrl, accountName, accountHandle }) {
1289
+ const t = useTranslation();
1290
+ const handle = (accountHandle || accountName).replace(/^@/, "");
1291
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-sm overflow-hidden rounded-card border border-border bg-card", children: [
1292
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [
1293
+ /* @__PURE__ */ jsx(Avatar, { name: handle, ring: true }),
1294
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-sm font-semibold text-foreground", children: handle }),
1295
+ /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4 text-muted-foreground" })
1296
+ ] }),
1297
+ /* @__PURE__ */ jsx(Media, { mediaUrl }),
1298
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2 px-3 py-2.5", children: [
1299
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 text-foreground", children: [
1300
+ /* @__PURE__ */ jsx(Heart, { className: "h-6 w-6" }),
1301
+ /* @__PURE__ */ jsx(MessageCircle, { className: "h-6 w-6" }),
1302
+ /* @__PURE__ */ jsx(Send, { className: "h-6 w-6" }),
1303
+ /* @__PURE__ */ jsx(Bookmark, { className: "ml-auto h-6 w-6" })
1304
+ ] }),
1305
+ /* @__PURE__ */ jsxs("p", { className: "whitespace-pre-wrap text-sm leading-snug text-foreground", children: [
1306
+ /* @__PURE__ */ jsx("span", { className: "mr-1.5 font-semibold", children: handle }),
1307
+ toCaption(caption)
1308
+ ] }),
1309
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: t("marketing.content.previewNow") })
1310
+ ] })
1311
+ ] });
1312
+ }
1313
+ function FacebookFrame({ caption, mediaUrl, accountName }) {
1314
+ const t = useTranslation();
1315
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-sm overflow-hidden rounded-card border border-border bg-card", children: [
1316
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [
1317
+ /* @__PURE__ */ jsx(Avatar, { name: accountName }),
1318
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1319
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: accountName.replace(/^@/, "") }),
1320
+ /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
1321
+ t("marketing.content.previewNow"),
1322
+ " \xB7 ",
1323
+ /* @__PURE__ */ jsx(Globe, { className: "h-3 w-3" })
1324
+ ] })
1325
+ ] }),
1326
+ /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4 text-muted-foreground" })
1327
+ ] }),
1328
+ /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap px-3 pb-2.5 text-sm leading-snug text-foreground", children: toCaption(caption) }),
1329
+ /* @__PURE__ */ jsx(Media, { mediaUrl, className: "aspect-auto" }),
1330
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-around border-t border-border px-3 py-2 text-muted-foreground", children: [
1331
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
1332
+ /* @__PURE__ */ jsx(ThumbsUp, { className: "h-4 w-4" }),
1333
+ " ",
1334
+ t("marketing.content.previewLike")
1335
+ ] }),
1336
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
1337
+ /* @__PURE__ */ jsx(MessageCircle, { className: "h-4 w-4" }),
1338
+ " ",
1339
+ t("marketing.content.previewComment")
1340
+ ] }),
1341
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
1342
+ /* @__PURE__ */ jsx(Share2, { className: "h-4 w-4" }),
1343
+ " ",
1344
+ t("marketing.content.previewShare")
1345
+ ] })
1346
+ ] })
1347
+ ] });
1348
+ }
1349
+ function LinkedinFrame({ caption, mediaUrl, accountName }) {
1350
+ const t = useTranslation();
1351
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-sm overflow-hidden rounded-card border border-border bg-card", children: [
1352
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [
1353
+ /* @__PURE__ */ jsx(Avatar, { name: accountName }),
1354
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1355
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: accountName.replace(/^@/, "") }),
1356
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.previewNow") })
1357
+ ] })
1358
+ ] }),
1359
+ /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap px-3 pb-2.5 text-sm leading-snug text-foreground", children: toCaption(caption) }),
1360
+ /* @__PURE__ */ jsx(Media, { mediaUrl, className: "aspect-auto" }),
1361
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-around border-t border-border px-3 py-2 text-muted-foreground", children: [
1362
+ /* @__PURE__ */ jsx(ThumbsUp, { className: "h-4 w-4" }),
1363
+ /* @__PURE__ */ jsx(MessageCircle, { className: "h-4 w-4" }),
1364
+ /* @__PURE__ */ jsx(Repeat2, { className: "h-4 w-4" }),
1365
+ /* @__PURE__ */ jsx(Send, { className: "h-4 w-4" })
1366
+ ] })
1367
+ ] });
1368
+ }
1369
+ function XFrame({ caption, mediaUrl, accountName, accountHandle }) {
1370
+ const t = useTranslation();
1371
+ const handle = (accountHandle || accountName).replace(/^@/, "");
1372
+ 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: [
1373
+ /* @__PURE__ */ jsx(Avatar, { name: handle }),
1374
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1375
+ /* @__PURE__ */ jsxs("p", { className: "truncate text-sm text-foreground", children: [
1376
+ /* @__PURE__ */ jsx("span", { className: "font-semibold", children: accountName.replace(/^@/, "") }),
1377
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
1378
+ " @",
1379
+ handle,
1380
+ " \xB7 ",
1381
+ t("marketing.content.previewNow")
1382
+ ] })
1383
+ ] }),
1384
+ /* @__PURE__ */ jsx("p", { className: "mt-0.5 whitespace-pre-wrap text-sm leading-snug text-foreground", children: toCaption(caption) }),
1385
+ /* @__PURE__ */ jsx("div", { className: "mt-2 overflow-hidden rounded-xl border border-border", children: /* @__PURE__ */ jsx(Media, { mediaUrl, className: "aspect-auto" }) }),
1386
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center justify-between pr-6 text-muted-foreground", children: [
1387
+ /* @__PURE__ */ jsx(MessageCircle, { className: "h-4 w-4" }),
1388
+ /* @__PURE__ */ jsx(Repeat2, { className: "h-4 w-4" }),
1389
+ /* @__PURE__ */ jsx(Heart, { className: "h-4 w-4" }),
1390
+ /* @__PURE__ */ jsx(BarChart2, { className: "h-4 w-4" }),
1391
+ /* @__PURE__ */ jsx(Share2, { className: "h-4 w-4" })
1392
+ ] })
1393
+ ] })
1394
+ ] }) });
1395
+ }
1396
+ var FRAMES = {
1397
+ instagram: InstagramFrame,
1398
+ facebook: FacebookFrame,
1399
+ linkedin: LinkedinFrame,
1400
+ x: XFrame
1401
+ };
1402
+ function PostSocialPreview(props) {
1403
+ const platforms = props.platforms.length > 0 ? props.platforms : ["instagram"];
1404
+ const [selected, setSelected] = React11.useState(platforms[0]);
1405
+ const active = platforms.includes(selected) ? selected : platforms[0];
1406
+ const Frame = FRAMES[active] ?? InstagramFrame;
1407
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-md border border-border bg-muted/20 p-4", children: [
1408
+ platforms.length > 1 && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center gap-1", children: platforms.map((platform) => /* @__PURE__ */ jsxs(
1409
+ Button,
1410
+ {
1411
+ variant: "bare",
1412
+ size: "bare",
1413
+ type: "button",
1414
+ onClick: () => setSelected(platform),
1415
+ "aria-pressed": active === platform,
1416
+ className: cn(
1417
+ "flex items-center gap-1.5 rounded-button px-2.5 py-1.5 text-xs font-medium capitalize transition-colors",
1418
+ active === platform ? "bg-card text-foreground shadow-button" : "text-muted-foreground hover:text-foreground"
1419
+ ),
1420
+ children: [
1421
+ /* @__PURE__ */ jsx(PlatformIcon, { platform, className: "h-3.5 w-3.5" }),
1422
+ platform
1423
+ ]
1424
+ },
1425
+ platform
1426
+ )) }),
1427
+ /* @__PURE__ */ jsx(Frame, { ...props })
1428
+ ] });
1429
+ }
1430
+
1431
+ // src/views/content/templates.ts
1432
+ var reel = (title) => `# ${title}
1433
+
1434
+ **TEMA:** ${title.toUpperCase()}
1435
+ **Formato:** Reel \xB7 **Dura\xE7\xE3o:** 45 a 60 segundos
1436
+
1437
+ ---
1438
+
1439
+ ## TAKE 1 \u2013 GANCHO (0s a 5s)
1440
+
1441
+ *Dire\xE7\xE3o de cena: olhando para a c\xE2mera, energia alta.*
1442
+
1443
+ **Texto na tela:** \u2026
1444
+
1445
+ **Fala:** "\u2026"
1446
+
1447
+ ---
1448
+
1449
+ ## TAKE 2 \u2013 DESENVOLVIMENTO (5s a 40s)
1450
+
1451
+ *Dire\xE7\xE3o de cena: \u2026*
1452
+
1453
+ **Texto na tela:** \u2026
1454
+
1455
+ **Fala:** "\u2026"
1456
+
1457
+ ---
1458
+
1459
+ ## TAKE 3 \u2013 CTA (40s a 60s)
1460
+
1461
+ **Fala:** "\u2026"
1462
+
1463
+ **CTA:** \u2026
1464
+ `;
1465
+ var staticPost = (title) => `# ${title}
1466
+
1467
+ **TEMA:** ${title.toUpperCase()}
1468
+ **Formato:** Est\xE1tico
1469
+
1470
+ ## Texto da arte
1471
+
1472
+ \u2026
1473
+
1474
+ ## Legenda
1475
+
1476
+ \u2026
1477
+
1478
+ **CTA:** \u2026
1479
+ `;
1480
+ var carousel = (title) => `# ${title}
1481
+
1482
+ **TEMA:** ${title.toUpperCase()}
1483
+ **Formato:** Carrossel
1484
+
1485
+ ## Slide 1 \u2014 Capa (gancho)
1486
+
1487
+ \u2026
1488
+
1489
+ ## Slide 2
1490
+
1491
+ \u2026
1492
+
1493
+ ## Slide 3
1494
+
1495
+ \u2026
1496
+
1497
+ ## \xDAltimo slide \u2014 CTA
1498
+
1499
+ \u2026
1500
+ `;
1501
+ var story = (title) => `# ${title}
1502
+
1503
+ **TEMA:** ${title.toUpperCase()}
1504
+ **Formato:** Story
1505
+
1506
+ ## Frame 1
1507
+
1508
+ \u2026
1509
+
1510
+ ## Frame 2
1511
+
1512
+ \u2026
1513
+
1514
+ ## Frame final \u2014 CTA
1515
+
1516
+ *Sticker de link / caixa de pergunta / enquete.*
1517
+
1518
+ \u2026
1519
+ `;
1520
+ var video = (title) => `# ${title}
1521
+
1522
+ **TEMA:** ${title.toUpperCase()}
1523
+ **Formato:** V\xEDdeo longo (YouTube) \xB7 **Dura\xE7\xE3o:** 8 a 12 minutos
1524
+
1525
+ ## Gancho (0s a 30s)
1526
+
1527
+ \u2026
1528
+
1529
+ ## Roteiro por blocos
1530
+
1531
+ ### Bloco 1 \u2014 Contexto
1532
+
1533
+ \u2026
1534
+
1535
+ ### Bloco 2 \u2014 Desenvolvimento
1536
+
1537
+ \u2026
1538
+
1539
+ ### Bloco 3 \u2014 Fechamento
1540
+
1541
+ \u2026
1542
+
1543
+ **CTA:** inscreva-se / comente / link na descri\xE7\xE3o
1544
+ `;
1545
+ var live = (title) => `# ${title}
1546
+
1547
+ **TEMA:** ${title.toUpperCase()}
1548
+ **Formato:** Live
1549
+
1550
+ ## Pauta
1551
+
1552
+ 1. Abertura e aquecimento (5 min)
1553
+ 2. \u2026
1554
+ 3. Perguntas e respostas (15 min)
1555
+
1556
+ ## Avisos / CTAs durante a live
1557
+
1558
+ \u2026
1559
+ `;
1560
+ var article = (title) => `# ${title}
1561
+
1562
+ **TEMA:** ${title.toUpperCase()}
1563
+ **Formato:** Artigo / Newsletter
1564
+
1565
+ ## T\xEDtulo de trabalho
1566
+
1567
+ \u2026
1568
+
1569
+ ## Estrutura
1570
+
1571
+ - Abertura (dor / promessa)
1572
+ - Desenvolvimento em 3 pontos
1573
+ - Conclus\xE3o + CTA
1574
+
1575
+ ## Rascunho
1576
+
1577
+ \u2026
1578
+ `;
1579
+ var POST_TEMPLATES = {
1580
+ reel,
1581
+ static: staticPost,
1582
+ carousel,
1583
+ story,
1584
+ video,
1585
+ live,
1586
+ article
1587
+ };
1588
+ function MediaAsset({ post, onChanged }) {
1589
+ const t = useTranslation();
1590
+ const inputRef = React11.useRef(null);
1591
+ const [busy, setBusy] = React11.useState(false);
1592
+ const isVideo2 = post.mediaUrl ? /\.(mp4|webm|mov)($|\?)/i.test(post.mediaUrl) : false;
1593
+ const pick = () => inputRef.current?.click();
1594
+ const upload = async (file) => {
1595
+ setBusy(true);
1596
+ try {
1597
+ const url = await uploadPostMedia(post.id, file);
1598
+ onChanged(url);
1599
+ } finally {
1600
+ setBusy(false);
1601
+ }
1602
+ };
1603
+ if (!canUploadMedia()) {
1604
+ return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.mediaUnavailable") });
1605
+ }
1606
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1607
+ /* @__PURE__ */ jsx(
1608
+ "input",
1609
+ {
1610
+ ref: inputRef,
1611
+ type: "file",
1612
+ accept: "image/*,video/*",
1613
+ className: "hidden",
1614
+ onChange: (e) => {
1615
+ const file = e.target.files?.[0];
1616
+ if (file) void upload(file);
1617
+ e.target.value = "";
1618
+ }
1619
+ }
1620
+ ),
1621
+ post.mediaUrl ? /* @__PURE__ */ jsxs("div", { className: "relative overflow-hidden rounded-card border border-border bg-muted/20", children: [
1622
+ 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" }),
1623
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-border p-2", children: [
1624
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: pick, disabled: busy, children: [
1625
+ 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" }),
1626
+ t("marketing.content.mediaReplace")
1627
+ ] }),
1628
+ /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => onChanged(null), disabled: busy, children: [
1629
+ /* @__PURE__ */ jsx(X, { className: "mr-1 h-3.5 w-3.5" }),
1630
+ t("marketing.content.mediaRemove")
1631
+ ] })
1632
+ ] })
1633
+ ] }) : /* @__PURE__ */ jsxs(
1634
+ Button,
1635
+ {
1636
+ variant: "bare",
1637
+ size: "bare",
1638
+ type: "button",
1639
+ onClick: pick,
1640
+ disabled: busy,
1641
+ 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 font-normal disabled:opacity-100",
1642
+ children: [
1643
+ busy ? /* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 animate-spin" }) : /* @__PURE__ */ jsx(ImagePlus, { className: "h-5 w-5" }),
1644
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: t("marketing.content.mediaUpload") }),
1645
+ /* @__PURE__ */ jsx("span", { className: "text-xs", children: t("marketing.content.mediaHint") })
1646
+ ]
1647
+ }
1648
+ )
1649
+ ] });
1650
+ }
1651
+ function toDraft2(post) {
1652
+ return {
1653
+ title: post.title,
1654
+ format: post.format,
1655
+ status: post.status,
1656
+ scheduledDate: post.scheduledDate ?? "",
1657
+ platforms: post.platforms,
1658
+ hook: post.hook ?? "",
1659
+ cta: post.cta ?? "",
1660
+ contentMd: post.contentMd
1661
+ };
1662
+ }
1663
+ function PostModal({ postId, onClose }) {
1664
+ return /* @__PURE__ */ jsx(Modal, { open: !!postId, onOpenChange: (o) => {
1665
+ if (!o) onClose();
1666
+ }, children: /* @__PURE__ */ jsx(ModalContent, { size: "5xl", children: postId && /* @__PURE__ */ jsx(PostEditor, { postId, onClose }) }) });
1667
+ }
1668
+ function PostEditor({ postId, onClose }) {
1669
+ const t = useTranslation();
1670
+ const provider = useContentPlannerProvider();
1671
+ const storePost = useContentPlannerStore((s) => s.posts.find((p) => p.id === postId));
1672
+ const savePost = useContentPlannerStore((s) => s.savePost);
1673
+ const deletePost = useContentPlannerStore((s) => s.deletePost);
1674
+ const accounts = useContentPlannerStore((s) => s.accounts);
1675
+ const activeAccountId = useContentPlannerStore((s) => s.activeAccountId);
1676
+ const [post, setPost] = React11.useState(storePost ?? null);
1677
+ const [draft, setDraft] = React11.useState(storePost ? toDraft2(storePost) : null);
1678
+ const [saving, setSaving] = React11.useState(false);
1679
+ const [confirmDelete, setConfirmDelete] = React11.useState(false);
1680
+ React11.useEffect(() => {
1681
+ if (post?.id === postId) return;
1682
+ if (storePost) {
1683
+ setPost(storePost);
1684
+ setDraft(toDraft2(storePost));
1685
+ return;
1686
+ }
1687
+ void provider.getPost(postId).then((fetched) => {
1688
+ if (fetched) {
1689
+ setPost(fetched);
1690
+ setDraft(toDraft2(fetched));
1691
+ }
1692
+ });
1693
+ }, [postId, storePost, post?.id, provider]);
1694
+ if (!post || !draft) {
1695
+ return /* @__PURE__ */ jsxs(ModalBody, { className: "space-y-5", children: [
1696
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-56" }),
1697
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }),
1698
+ /* @__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)) }),
1699
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-64 w-full" })
1700
+ ] });
1701
+ }
1702
+ 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;
1703
+ const patch = (partial) => setDraft((d) => d ? { ...d, ...partial } : d);
1704
+ const save = async () => {
1705
+ setSaving(true);
1706
+ try {
1707
+ const updated = await savePost({
1708
+ id: post.id,
1709
+ title: draft.title,
1710
+ format: draft.format,
1711
+ status: draft.status,
1712
+ scheduledDate: draft.scheduledDate || null,
1713
+ platforms: draft.platforms,
1714
+ hook: draft.hook,
1715
+ cta: draft.cta,
1716
+ contentMd: draft.contentMd
1717
+ });
1718
+ setPost(updated);
1719
+ setDraft(toDraft2(updated));
1720
+ } finally {
1721
+ setSaving(false);
1722
+ }
1723
+ };
1724
+ const insertTemplate = () => {
1725
+ patch({ contentMd: POST_TEMPLATES[draft.format](draft.title || t("marketing.content.untitled")) });
1726
+ };
1727
+ const isStatic = draft.format === "static";
1728
+ const account = accounts.find((a) => a.id === activeAccountId) ?? null;
1729
+ const handleMediaChanged = (mediaUrl) => {
1730
+ void savePost({ id: post.id, mediaUrl }).then((updated) => {
1731
+ setPost(updated);
1732
+ setDraft((d) => d ? { ...d } : d);
1733
+ });
1734
+ };
1735
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1736
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: draft.title || t("marketing.content.untitled") }) }),
1737
+ /* @__PURE__ */ jsx(ModalBody, { children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_18rem]", children: [
1738
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-5", children: [
1739
+ /* @__PURE__ */ jsx(
1740
+ Input,
1741
+ {
1742
+ value: draft.title,
1743
+ onChange: (e) => patch({ title: e.target.value }),
1744
+ placeholder: t("marketing.content.titlePlaceholder"),
1745
+ className: "text-base font-medium"
1746
+ }
1747
+ ),
1748
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: [
1749
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1750
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.formatLabel") }),
1751
+ /* @__PURE__ */ jsxs(Select, { value: draft.format, onValueChange: (v) => patch({ format: v }), children: [
1752
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
1753
+ /* @__PURE__ */ jsx(SelectContent, { children: POST_FORMATS.map((f) => /* @__PURE__ */ jsx(SelectItem, { value: f, children: t(`marketing.content.format.${f}`) }, f)) })
1754
+ ] })
1755
+ ] }),
1756
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1757
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.statusLabel") }),
1758
+ /* @__PURE__ */ jsxs(Select, { value: draft.status, onValueChange: (v) => patch({ status: v }), children: [
1759
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
1760
+ /* @__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: [
1761
+ /* @__PURE__ */ jsx(StatusDot, { status: s }),
1762
+ t(`marketing.content.status.${s}`)
1763
+ ] }) }, s)) })
1764
+ ] })
1765
+ ] }),
1766
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1767
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.scheduledDate") }),
1768
+ /* @__PURE__ */ jsx(
1769
+ Input,
1770
+ {
1771
+ type: "date",
1772
+ value: draft.scheduledDate,
1773
+ onChange: (e) => patch({ scheduledDate: e.target.value })
1774
+ }
1775
+ )
1776
+ ] }),
1777
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1 sm:col-span-2", children: [
1778
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.hook") }),
1779
+ /* @__PURE__ */ jsx(
1780
+ Input,
1781
+ {
1782
+ value: draft.hook,
1783
+ onChange: (e) => patch({ hook: e.target.value }),
1784
+ placeholder: t("marketing.content.hookPlaceholder")
1785
+ }
1786
+ )
1787
+ ] }),
1788
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1789
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.cta") }),
1790
+ /* @__PURE__ */ jsx(
1791
+ Input,
1792
+ {
1793
+ value: draft.cta,
1794
+ onChange: (e) => patch({ cta: e.target.value }),
1795
+ placeholder: t("marketing.content.ctaPlaceholder")
1796
+ }
1797
+ )
1798
+ ] }),
1799
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1 sm:col-span-3", children: [
1800
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.platformsLabel") }),
1801
+ /* @__PURE__ */ jsx(PlatformPicker, { value: draft.platforms, onChange: (platforms) => patch({ platforms }) })
1802
+ ] })
1803
+ ] }),
1804
+ isStatic && /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1805
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.mediaLabel") }),
1806
+ /* @__PURE__ */ jsx(MediaAsset, { post, onChanged: handleMediaChanged })
1807
+ ] }),
1808
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1809
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
1810
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: isStatic ? t("marketing.content.captionLabel") : t("marketing.content.scriptLabel") }),
1811
+ draft.contentMd.trim() === "" && /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: insertTemplate, children: t("marketing.content.insertTemplate") })
1812
+ ] }),
1813
+ /* @__PURE__ */ jsx(
1814
+ MarkdownEditor,
1815
+ {
1816
+ value: draft.contentMd,
1817
+ onChange: (contentMd) => patch({ contentMd }),
1818
+ placeholder: isStatic ? t("marketing.content.captionPlaceholder") : t("marketing.content.bodyPlaceholder"),
1819
+ editLabel: t("marketing.content.edit"),
1820
+ previewLabel: t("marketing.content.preview"),
1821
+ renderPreview: isStatic ? (value) => /* @__PURE__ */ jsx(
1822
+ PostSocialPreview,
1823
+ {
1824
+ caption: value,
1825
+ mediaUrl: post.mediaUrl,
1826
+ accountName: account?.name ?? "@conta",
1827
+ accountHandle: account?.handle,
1828
+ platforms: draft.platforms.length > 0 ? draft.platforms : account?.platforms ?? []
1829
+ }
1830
+ ) : void 0
1831
+ }
1832
+ )
1833
+ ] })
1834
+ ] }),
1835
+ /* @__PURE__ */ jsxs("aside", { className: "space-y-2 lg:border-l lg:border-border lg:pl-6", children: [
1836
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: t("marketing.content.checklistTitle") }),
1837
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("marketing.content.checklistDescription") }),
1838
+ /* @__PURE__ */ jsx(RecordingChecklist, { post })
1839
+ ] })
1840
+ ] }) }),
1841
+ /* @__PURE__ */ jsxs(ModalFooter, { className: "sm:justify-between", children: [
1842
+ confirmDelete ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1843
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: t("marketing.content.deletePostConfirm") }),
1844
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => setConfirmDelete(false), children: t("marketing.content.cancel") }),
1845
+ /* @__PURE__ */ jsx(Button, { variant: "destructive", size: "sm", onClick: () => void deletePost(post.id).then(onClose), children: t("marketing.content.deletePost") })
1846
+ ] }) : /* @__PURE__ */ jsx(Button, { variant: "ghost", className: "text-destructive", onClick: () => setConfirmDelete(true), children: t("marketing.content.deletePost") }),
1847
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1848
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: t("marketing.content.cancel") }),
1849
+ /* @__PURE__ */ jsx(Button, { onClick: () => void save().then(onClose), disabled: !dirty || saving, children: t("marketing.content.save") })
1850
+ ] })
1851
+ ] })
1852
+ ] });
1853
+ }
1854
+ var DEFAULT_WEEKS = 8;
1855
+ var ADD_ACCOUNT_VALUE = "__add-account__";
1856
+ var POST_DRAG_MIME = "application/x-mkt-post";
1857
+ function PostCard({ post, onOpen, onDelete }) {
1858
+ const t = useTranslation();
1859
+ const [dragging, setDragging] = React11.useState(false);
1860
+ return /* @__PURE__ */ jsxs(
1861
+ "div",
1862
+ {
1863
+ role: "button",
1864
+ tabIndex: 0,
1865
+ onClick: onOpen,
1866
+ onKeyDown: (e) => {
1867
+ if (e.key === "Enter") onOpen();
1868
+ },
1869
+ draggable: true,
1870
+ onDragStart: (e) => {
1871
+ e.dataTransfer.setData(POST_DRAG_MIME, post.id);
1872
+ e.dataTransfer.effectAllowed = "move";
1873
+ setDragging(true);
1874
+ },
1875
+ onDragEnd: () => setDragging(false),
1876
+ className: cn(
1877
+ "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",
1878
+ dragging && "opacity-50"
1879
+ ),
1880
+ children: [
1881
+ /* @__PURE__ */ jsx(
1882
+ GripVertical,
1883
+ {
1884
+ 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",
1885
+ "aria-hidden": true
1886
+ }
1887
+ ),
1888
+ onDelete && /* @__PURE__ */ jsx(
1889
+ Button,
1890
+ {
1891
+ variant: "bare",
1892
+ size: "bare",
1893
+ type: "button",
1894
+ "aria-label": t("marketing.content.deletePost"),
1895
+ onClick: (e) => {
1896
+ e.stopPropagation();
1897
+ onDelete();
1898
+ },
1899
+ 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",
1900
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
1901
+ }
1902
+ ),
1903
+ /* @__PURE__ */ jsx("p", { className: "pr-5 text-sm font-medium text-foreground", children: post.title || t("marketing.content.untitled") }),
1904
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-1.5", children: [
1905
+ /* @__PURE__ */ jsx(FormatBadge, { format: post.format }),
1906
+ /* @__PURE__ */ jsx(PostStatusBadge, { status: post.status }),
1907
+ post.scheduledDate && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: shortDay(/* @__PURE__ */ new Date(`${post.scheduledDate}T00:00:00`)) }),
1908
+ /* @__PURE__ */ jsx(PlatformBadges, { platforms: post.platforms })
1909
+ ] })
1910
+ ]
1911
+ }
1912
+ );
1913
+ }
1914
+ function WeekColumn({ week, rangeLabel, posts, onOpenPost, onNewPost, onDeletePost, onDropPost, disabled }) {
1915
+ const t = useTranslation();
1916
+ const [isDropTarget, setIsDropTarget] = React11.useState(false);
1917
+ return /* @__PURE__ */ jsxs(
1918
+ "div",
1919
+ {
1920
+ className: cn(
1921
+ "flex flex-col gap-2 rounded-card border border-border bg-muted/20 p-2.5 transition-colors",
1922
+ isDropTarget && "border-primary bg-primary/5 ring-1 ring-primary/40"
1923
+ ),
1924
+ onDragOver: (e) => {
1925
+ if (!onDropPost || !e.dataTransfer.types.includes(POST_DRAG_MIME)) return;
1926
+ e.preventDefault();
1927
+ e.dataTransfer.dropEffect = "move";
1928
+ setIsDropTarget(true);
1929
+ },
1930
+ onDragLeave: () => setIsDropTarget(false),
1931
+ onDrop: (e) => {
1932
+ if (!onDropPost) return;
1933
+ e.preventDefault();
1934
+ setIsDropTarget(false);
1935
+ const postId = e.dataTransfer.getData(POST_DRAG_MIME);
1936
+ if (postId) onDropPost(postId, week);
1937
+ },
1938
+ children: [
1939
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5 px-1", children: [
1940
+ /* @__PURE__ */ jsxs("p", { className: "whitespace-nowrap text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: [
1941
+ t("marketing.content.week"),
1942
+ " ",
1943
+ week
1944
+ ] }),
1945
+ /* @__PURE__ */ jsx("p", { className: "whitespace-nowrap text-xs text-muted-foreground/80", children: rangeLabel })
1946
+ ] }),
1947
+ posts.map((post) => /* @__PURE__ */ jsx(
1948
+ PostCard,
1949
+ {
1950
+ post,
1951
+ onOpen: () => onOpenPost(post.id),
1952
+ onDelete: onDeletePost ? () => onDeletePost(post.id) : void 0
1953
+ },
1954
+ post.id
1955
+ )),
1956
+ /* @__PURE__ */ jsxs(
1957
+ Button,
1958
+ {
1959
+ variant: "bare",
1960
+ size: "bare",
1961
+ type: "button",
1962
+ onClick: () => onNewPost(week),
1963
+ disabled,
1964
+ 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",
1965
+ children: [
1966
+ /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
1967
+ t("marketing.content.newPost")
1968
+ ]
1969
+ }
1970
+ )
1971
+ ]
1972
+ }
1973
+ );
1974
+ }
1975
+ function AccountModal({ open, account, onClose }) {
1976
+ const t = useTranslation();
1977
+ const saveAccount = useContentPlannerStore((s) => s.saveAccount);
1978
+ const deleteAccount = useContentPlannerStore((s) => s.deleteAccount);
1979
+ const isEdit = !!account;
1980
+ const [name, setName] = React11.useState("");
1981
+ const [handle, setHandle] = React11.useState("");
1982
+ const [platforms, setPlatforms] = React11.useState(["instagram"]);
1983
+ const [saving, setSaving] = React11.useState(false);
1984
+ const [confirmDelete, setConfirmDelete] = React11.useState(false);
1985
+ React11.useEffect(() => {
1986
+ if (open) {
1987
+ setName(account?.name ?? "");
1988
+ setHandle(account?.handle ?? "");
1989
+ setPlatforms(account?.platforms?.length ? account.platforms : ["instagram"]);
1990
+ setSaving(false);
1991
+ setConfirmDelete(false);
1992
+ }
1993
+ }, [open, account]);
1994
+ const create = async () => {
1995
+ if (!name.trim() || platforms.length === 0 || saving) return;
1996
+ setSaving(true);
1997
+ try {
1998
+ await saveAccount({
1999
+ ...isEdit ? { id: account.id } : {},
2000
+ name: name.trim(),
2001
+ handle: handle.trim() || void 0,
2002
+ platforms
2003
+ });
2004
+ onClose();
2005
+ } finally {
2006
+ setSaving(false);
2007
+ }
2008
+ };
2009
+ const remove = async () => {
2010
+ if (!account || saving) return;
2011
+ setSaving(true);
2012
+ try {
2013
+ await deleteAccount(account.id);
2014
+ onClose();
2015
+ } finally {
2016
+ setSaving(false);
2017
+ }
2018
+ };
2019
+ return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (o) => {
2020
+ if (!o) onClose();
2021
+ }, children: /* @__PURE__ */ jsxs(ModalContent, { size: "sm", children: [
2022
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: isEdit ? t("marketing.content.editAccountTitle") : t("marketing.content.addAccountTitle") }) }),
2023
+ /* @__PURE__ */ jsxs(ModalBody, { className: "space-y-4", children: [
2024
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
2025
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.nameLabel") }),
2026
+ /* @__PURE__ */ jsx(
2027
+ Input,
2028
+ {
2029
+ autoFocus: true,
2030
+ value: name,
2031
+ onChange: (e) => setName(e.target.value),
2032
+ placeholder: t("marketing.content.accountNamePlaceholder"),
2033
+ onKeyDown: (e) => {
2034
+ if (e.key === "Enter") void create();
2035
+ }
2036
+ }
2037
+ )
2038
+ ] }),
2039
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
2040
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.handleLabel") }),
2041
+ /* @__PURE__ */ jsx(
2042
+ Input,
2043
+ {
2044
+ value: handle,
2045
+ onChange: (e) => setHandle(e.target.value),
2046
+ placeholder: t("marketing.content.handlePlaceholder")
2047
+ }
2048
+ )
2049
+ ] }),
2050
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
2051
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("marketing.content.platformsLabel") }),
2052
+ /* @__PURE__ */ jsx(PlatformPicker, { value: platforms, onChange: setPlatforms })
2053
+ ] })
2054
+ ] }),
2055
+ /* @__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: [
2056
+ /* @__PURE__ */ jsx("p", { className: "flex-1 text-xs text-destructive", children: t("marketing.content.deleteAccountConfirm") }),
2057
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => setConfirmDelete(false), disabled: saving, children: t("marketing.content.cancel") }),
2058
+ /* @__PURE__ */ jsx(Button, { variant: "destructive", size: "sm", onClick: () => void remove(), disabled: saving, children: t("marketing.content.deleteAccount") })
2059
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2060
+ isEdit && /* @__PURE__ */ jsxs(
2061
+ Button,
2062
+ {
2063
+ variant: "outline",
2064
+ onClick: () => setConfirmDelete(true),
2065
+ disabled: saving,
2066
+ className: "mr-auto text-destructive hover:text-destructive",
2067
+ children: [
2068
+ /* @__PURE__ */ jsx(Trash2, { className: "mr-1.5 h-4 w-4" }),
2069
+ t("marketing.content.deleteAccount")
2070
+ ]
2071
+ }
2072
+ ),
2073
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, disabled: saving, children: t("marketing.content.cancel") }),
2074
+ /* @__PURE__ */ jsx(Button, { onClick: () => void create(), disabled: saving || !name.trim() || platforms.length === 0, children: isEdit ? t("marketing.content.save") : t("marketing.content.createAccount") })
2075
+ ] }) })
2076
+ ] }) });
2077
+ }
2078
+ function AccountSelector() {
2079
+ const t = useTranslation();
2080
+ const loaded = useContentPlannerStore((s) => s.loaded);
2081
+ const accounts = useContentPlannerStore((s) => s.accounts);
2082
+ const activeAccountId = useContentPlannerStore((s) => s.activeAccountId);
2083
+ const setAccount = useContentPlannerStore((s) => s.setAccount);
2084
+ const [open, setOpen] = React11.useState(false);
2085
+ const [modal, setModal] = React11.useState(null);
2086
+ const activeAccount = accounts.find((a) => a.id === activeAccountId) ?? null;
2087
+ if (!loaded) {
2088
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2089
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-64" }),
2090
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-9" })
2091
+ ] });
2092
+ }
2093
+ const openSettings = (account) => {
2094
+ setOpen(false);
2095
+ setModal({ mode: "edit", account });
2096
+ };
2097
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2098
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2099
+ /* @__PURE__ */ jsxs(
2100
+ Select,
2101
+ {
2102
+ open,
2103
+ onOpenChange: setOpen,
2104
+ value: activeAccountId ?? void 0,
2105
+ onValueChange: (id) => {
2106
+ if (id === ADD_ACCOUNT_VALUE) {
2107
+ setModal({ mode: "create" });
2108
+ return;
2109
+ }
2110
+ void setAccount(id);
2111
+ },
2112
+ children: [
2113
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "w-64", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("marketing.content.account") }) }),
2114
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
2115
+ accounts.length === 0 ? /* @__PURE__ */ jsx(SelectItem, { value: "__none__", disabled: true, children: t("marketing.content.noAccountsAvailable") }) : accounts.map((acc) => /* @__PURE__ */ jsxs("div", { className: "relative", children: [
2116
+ /* @__PURE__ */ jsx(SelectItem, { value: acc.id, className: "pr-9", children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
2117
+ acc.name,
2118
+ /* @__PURE__ */ jsx(PlatformBadges, { platforms: acc.platforms })
2119
+ ] }) }),
2120
+ /* @__PURE__ */ jsx(
2121
+ Button,
2122
+ {
2123
+ variant: "bare",
2124
+ size: "bare",
2125
+ type: "button",
2126
+ "aria-label": t("marketing.content.accountSettings"),
2127
+ onPointerDown: (e) => {
2128
+ e.stopPropagation();
2129
+ e.preventDefault();
2130
+ },
2131
+ onPointerUp: (e) => {
2132
+ e.stopPropagation();
2133
+ e.preventDefault();
2134
+ },
2135
+ onClick: (e) => {
2136
+ e.stopPropagation();
2137
+ openSettings(acc);
2138
+ },
2139
+ 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",
2140
+ children: /* @__PURE__ */ jsx(Settings2, { className: "h-3.5 w-3.5" })
2141
+ }
2142
+ )
2143
+ ] }, acc.id)),
2144
+ /* @__PURE__ */ jsx(SelectSeparator, {}),
2145
+ /* @__PURE__ */ jsx(SelectItem, { value: ADD_ACCOUNT_VALUE, children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
2146
+ /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
2147
+ t("marketing.content.addAccount")
2148
+ ] }) })
2149
+ ] })
2150
+ ]
2151
+ }
2152
+ ),
2153
+ activeAccount && /* @__PURE__ */ jsx(
2154
+ Button,
2155
+ {
2156
+ variant: "ghost",
2157
+ size: "sm",
2158
+ className: "h-9 w-9 p-0 text-muted-foreground",
2159
+ "aria-label": t("marketing.content.accountSettings"),
2160
+ onClick: () => setModal({ mode: "edit", account: activeAccount }),
2161
+ children: /* @__PURE__ */ jsx(Settings2, { className: "h-4 w-4" })
2162
+ }
2163
+ )
2164
+ ] }),
2165
+ /* @__PURE__ */ jsx(
2166
+ AccountModal,
2167
+ {
2168
+ open: !!modal,
2169
+ account: modal?.mode === "edit" ? modal.account : null,
2170
+ onClose: () => setModal(null)
2171
+ }
2172
+ )
2173
+ ] });
2174
+ }
2175
+ function ViewSwitcher({ mode, onChange }) {
2176
+ const t = useTranslation();
2177
+ return /* @__PURE__ */ jsx(
2178
+ SegmentedControl,
2179
+ {
2180
+ size: "md",
2181
+ "aria-label": t("marketing.content.viewBoard"),
2182
+ value: mode,
2183
+ onChange,
2184
+ options: [
2185
+ { value: "board", label: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
2186
+ /* @__PURE__ */ jsx(Columns3, { className: "h-3.5 w-3.5" }),
2187
+ t("marketing.content.viewBoard")
2188
+ ] }) },
2189
+ { value: "calendar", label: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
2190
+ /* @__PURE__ */ jsx(CalendarDays, { className: "h-3.5 w-3.5" }),
2191
+ t("marketing.content.viewCalendar")
2192
+ ] }) },
2193
+ { value: "list", label: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
2194
+ /* @__PURE__ */ jsx(List, { className: "h-3.5 w-3.5" }),
2195
+ t("marketing.content.viewList")
2196
+ ] }) }
2197
+ ]
2198
+ }
2199
+ );
2200
+ }
2201
+ function shortDay(d) {
2202
+ return d.toLocaleDateString(getCurrentLocale(), { day: "numeric", month: "short" });
2203
+ }
2204
+ function buildBoardWeeks(startDate, weeksCount) {
2205
+ return Array.from({ length: weeksCount }, (_, i) => {
2206
+ const start = new Date(startDate);
2207
+ start.setDate(start.getDate() + i * 7);
2208
+ const end = new Date(start);
2209
+ end.setDate(start.getDate() + 6);
2210
+ return {
2211
+ week: i + 1,
2212
+ rangeLabel: `${shortDay(start)} \u2013 ${shortDay(end)}`,
2213
+ monthLabel: start.toLocaleDateString(getCurrentLocale(), { month: "long", year: "numeric" })
2214
+ };
2215
+ });
2216
+ }
2217
+ function BoardView({ startDate, weeksCount, postsByWeek, onOpenPost, onNewPost, onDeletePost, onDropPost, disabled }) {
2218
+ const months = [];
2219
+ for (const bw of buildBoardWeeks(startDate, weeksCount)) {
2220
+ const current = months[months.length - 1];
2221
+ if (current && current.label === bw.monthLabel) current.weeks.push(bw);
2222
+ else months.push({ label: bw.monthLabel, weeks: [bw] });
2223
+ }
2224
+ return /* @__PURE__ */ jsx("div", { className: "space-y-8", children: months.map(({ label, weeks }) => /* @__PURE__ */ jsxs("section", { className: "space-y-3", children: [
2225
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: label }),
2226
+ /* @__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(
2227
+ WeekColumn,
2228
+ {
2229
+ week,
2230
+ rangeLabel,
2231
+ posts: postsByWeek.get(week) ?? [],
2232
+ onOpenPost,
2233
+ onNewPost,
2234
+ onDeletePost,
2235
+ onDropPost,
2236
+ disabled
2237
+ },
2238
+ week
2239
+ )) })
2240
+ ] }, label)) });
2241
+ }
2242
+ function toISODate(d) {
2243
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
2244
+ }
2245
+ function mondayOfCurrentWeek() {
2246
+ const now = /* @__PURE__ */ new Date();
2247
+ const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
2248
+ monday.setDate(monday.getDate() - (monday.getDay() + 6) % 7);
2249
+ return monday;
2250
+ }
2251
+ function CalendarView({ posts, onOpenPost, onNewPostAt, disabled }) {
2252
+ const t = useTranslation();
2253
+ const [cursor, setCursor] = React11.useState(() => {
2254
+ const first = posts.find((p) => p.scheduledDate)?.scheduledDate;
2255
+ const base = first ? /* @__PURE__ */ new Date(`${first}T00:00:00`) : /* @__PURE__ */ new Date();
2256
+ return new Date(base.getFullYear(), base.getMonth(), 1);
2257
+ });
2258
+ const [expandedDay, setExpandedDay] = React11.useState(null);
2259
+ const MAX_VISIBLE = 3;
2260
+ const postsByDate = React11.useMemo(() => {
2261
+ const map = /* @__PURE__ */ new Map();
2262
+ for (const post of posts) {
2263
+ if (!post.scheduledDate) continue;
2264
+ const list = map.get(post.scheduledDate) ?? [];
2265
+ list.push(post);
2266
+ map.set(post.scheduledDate, list);
2267
+ }
2268
+ return map;
2269
+ }, [posts]);
2270
+ const unscheduled = posts.filter((p) => !p.scheduledDate);
2271
+ const days = React11.useMemo(() => {
2272
+ const firstOfMonth = new Date(cursor.getFullYear(), cursor.getMonth(), 1);
2273
+ const start = new Date(firstOfMonth);
2274
+ start.setDate(start.getDate() - (firstOfMonth.getDay() + 6) % 7);
2275
+ const cells = [];
2276
+ for (let i = 0; i < 42; i++) {
2277
+ const d = new Date(start);
2278
+ d.setDate(start.getDate() + i);
2279
+ cells.push(d);
2280
+ }
2281
+ while (cells.length > 7 && cells[cells.length - 7].getMonth() !== cursor.getMonth()) {
2282
+ cells.splice(cells.length - 7, 7);
2283
+ }
2284
+ return cells;
2285
+ }, [cursor]);
2286
+ const monthLabel = cursor.toLocaleDateString(getCurrentLocale(), { month: "long", year: "numeric" });
2287
+ const weekdayLabels = days.slice(0, 7).map(
2288
+ (d) => d.toLocaleDateString(getCurrentLocale(), { weekday: "short" })
2289
+ );
2290
+ const todayISO = toISODate(/* @__PURE__ */ new Date());
2291
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2292
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
2293
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold capitalize text-foreground", children: monthLabel }),
2294
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2295
+ /* @__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" }) }),
2296
+ /* @__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" }) })
2297
+ ] })
2298
+ ] }),
2299
+ 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: [
2300
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t("marketing.content.unscheduled") }),
2301
+ unscheduled.map((post) => /* @__PURE__ */ jsxs(
2302
+ Button,
2303
+ {
2304
+ variant: "bare",
2305
+ size: "bare",
2306
+ type: "button",
2307
+ onClick: () => onOpenPost(post.id),
2308
+ 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",
2309
+ children: [
2310
+ post.title || t("marketing.content.untitled"),
2311
+ /* @__PURE__ */ jsx(FormatBadge, { format: post.format })
2312
+ ]
2313
+ },
2314
+ post.id
2315
+ ))
2316
+ ] }),
2317
+ /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-card border border-border", children: [
2318
+ /* @__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)) }),
2319
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7", children: days.map((day, i) => {
2320
+ const iso = toISODate(day);
2321
+ const inMonth = day.getMonth() === cursor.getMonth();
2322
+ const dayPosts = postsByDate.get(iso) ?? [];
2323
+ return /* @__PURE__ */ jsxs(
2324
+ "div",
2325
+ {
2326
+ className: cn(
2327
+ "group min-h-24 border-b border-r border-border p-1.5 [&:nth-child(7n)]:border-r-0",
2328
+ !inMonth && "bg-muted/20"
2329
+ ),
2330
+ children: [
2331
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
2332
+ /* @__PURE__ */ jsx(
2333
+ "span",
2334
+ {
2335
+ className: cn(
2336
+ "text-xs",
2337
+ inMonth ? "text-foreground" : "text-muted-foreground/60",
2338
+ iso === todayISO && "flex h-5 w-5 items-center justify-center rounded-full bg-primary font-semibold text-primary-foreground"
2339
+ ),
2340
+ children: day.getDate()
2341
+ }
2342
+ ),
2343
+ !disabled && /* @__PURE__ */ jsx(
2344
+ Button,
2345
+ {
2346
+ variant: "bare",
2347
+ size: "bare",
2348
+ type: "button",
2349
+ onClick: () => onNewPostAt(iso),
2350
+ className: "rounded p-0.5 text-muted-foreground opacity-0 transition-opacity hover:bg-muted/60 hover:text-foreground group-hover:opacity-100",
2351
+ "aria-label": t("marketing.content.newPost"),
2352
+ children: /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" })
2353
+ }
2354
+ )
2355
+ ] }),
2356
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 space-y-1", children: [
2357
+ (expandedDay === iso ? dayPosts : dayPosts.slice(0, MAX_VISIBLE)).map((post) => /* @__PURE__ */ jsx(
2358
+ Button,
2359
+ {
2360
+ variant: "bare",
2361
+ size: "bare",
2362
+ type: "button",
2363
+ onClick: () => onOpenPost(post.id),
2364
+ 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 font-normal",
2365
+ children: post.title || t("marketing.content.untitled")
2366
+ },
2367
+ post.id
2368
+ )),
2369
+ dayPosts.length > MAX_VISIBLE && /* @__PURE__ */ jsx(
2370
+ Button,
2371
+ {
2372
+ variant: "bare",
2373
+ size: "bare",
2374
+ type: "button",
2375
+ onClick: () => setExpandedDay(expandedDay === iso ? null : iso),
2376
+ className: "block w-full px-1.5 py-0.5 text-left text-xs font-medium text-muted-foreground transition-colors hover:text-foreground",
2377
+ children: expandedDay === iso ? t("marketing.content.showLess") : `+${dayPosts.length - MAX_VISIBLE}`
2378
+ }
2379
+ )
2380
+ ] })
2381
+ ]
2382
+ },
2383
+ i
2384
+ );
2385
+ }) })
2386
+ ] })
2387
+ ] });
2388
+ }
2389
+ function ListView({ posts, onOpenPost }) {
2390
+ const t = useTranslation();
2391
+ const columns = React11.useMemo(() => {
2392
+ const head = (key) => () => /* @__PURE__ */ jsx("span", { className: "text-xs uppercase tracking-wide", children: t(key) });
2393
+ return [
2394
+ { id: "week", header: head("marketing.content.listWeek"), cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: row.original.weekNumber }) },
2395
+ {
2396
+ id: "title",
2397
+ header: head("marketing.content.listTitle"),
2398
+ cell: ({ row: { original: post } }) => /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 font-medium text-foreground", children: [
2399
+ post.title || t("marketing.content.untitled"),
2400
+ /* @__PURE__ */ jsx(PlatformBadges, { platforms: post.platforms })
2401
+ ] })
2402
+ },
2403
+ { id: "format", header: head("marketing.content.listFormat"), cell: ({ row }) => /* @__PURE__ */ jsx(FormatBadge, { format: row.original.format }) },
2404
+ { id: "status", header: head("marketing.content.listStatus"), cell: ({ row }) => /* @__PURE__ */ jsx(PostStatusBadge, { status: row.original.status }) },
2405
+ { id: "date", header: head("marketing.content.listDate"), cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: row.original.scheduledDate ?? "\u2014" }) }
2406
+ ];
2407
+ }, [t]);
2408
+ if (posts.length === 0) {
2409
+ return /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.content.noPosts") });
2410
+ }
2411
+ return /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-card border border-border", children: /* @__PURE__ */ jsx(DataTable, { variant: "bare", columns, data: posts, onRowClick: (post) => onOpenPost(post.id) }) });
2412
+ }
2413
+ function ContentView({ initialPostId = null }) {
2414
+ const t = useTranslation();
2415
+ const [openPostId, setOpenPostId] = React11.useState(initialPostId);
2416
+ const onOpenPost = React11.useCallback((id) => {
2417
+ setOpenPostId(id);
2418
+ window.history.replaceState(window.history.state, "", appHref(id ? `/marketing/content/post/${id}` : "/marketing/content"));
2419
+ }, []);
2420
+ const loaded = useContentPlannerStore((s) => s.loaded);
2421
+ const loading = useContentPlannerStore((s) => s.loading);
2422
+ const accounts = useContentPlannerStore((s) => s.accounts);
2423
+ const activeAccountId = useContentPlannerStore((s) => s.activeAccountId);
2424
+ const plan = useContentPlannerStore((s) => s.activePlan);
2425
+ const posts = useContentPlannerStore((s) => s.posts);
2426
+ const load = useContentPlannerStore((s) => s.load);
2427
+ const savePlan = useContentPlannerStore((s) => s.savePlan);
2428
+ const savePost = useContentPlannerStore((s) => s.savePost);
2429
+ const deletePost = useContentPlannerStore((s) => s.deletePost);
2430
+ const viewMode = useContentPlannerStore((s) => s.viewMode);
2431
+ const setViewMode = useContentPlannerStore((s) => s.setViewMode);
2432
+ const loadedTenantId = useContentPlannerStore((s) => s.loadedTenantId);
2433
+ const guardContentPosts = useLimitGuard("content_posts_month");
2434
+ const [briefOpen, setBriefOpen] = React11.useState(false);
2435
+ const [pendingDeleteId, setPendingDeleteId] = React11.useState(null);
2436
+ const tenantId = useOrganizationStore((state) => state.currentOrg?.id);
2437
+ React11.useEffect(() => {
2438
+ if (!loading && (!loaded || tenantId && tenantId !== loadedTenantId)) void load();
2439
+ }, [tenantId, loadedTenantId, loaded, loading, load]);
2440
+ const postsByWeek = React11.useMemo(() => {
2441
+ const map = /* @__PURE__ */ new Map();
2442
+ for (const post of posts) {
2443
+ const list = map.get(post.weekNumber) ?? [];
2444
+ list.push(post);
2445
+ map.set(post.weekNumber, list);
2446
+ }
2447
+ return map;
2448
+ }, [posts]);
2449
+ const ensurePlan = async () => {
2450
+ if (plan) return plan;
2451
+ if (!activeAccountId) return null;
2452
+ return savePlan({
2453
+ accountId: activeAccountId,
2454
+ name: t("marketing.content.defaultPlanName"),
2455
+ status: "active",
2456
+ weeksCount: DEFAULT_WEEKS,
2457
+ startDate: toISODate(mondayOfCurrentWeek())
2458
+ });
2459
+ };
2460
+ const createPost = async (week) => {
2461
+ if (await guardContentPosts() === "blocked") return;
2462
+ const targetPlan = await ensurePlan();
2463
+ if (!targetPlan) return;
2464
+ const start = targetPlan.startDate ? /* @__PURE__ */ new Date(`${targetPlan.startDate}T00:00:00`) : mondayOfCurrentWeek();
2465
+ start.setDate(start.getDate() + (week - 1) * 7);
2466
+ const post = await savePost({
2467
+ planId: targetPlan.id,
2468
+ weekNumber: week,
2469
+ title: "",
2470
+ format: "reel",
2471
+ status: "idea",
2472
+ scheduledDate: toISODate(start)
2473
+ });
2474
+ invalidateLimit("content_posts_month");
2475
+ onOpenPost(post.id);
2476
+ };
2477
+ const movePost = async (postId, week) => {
2478
+ const post = posts.find((p) => p.id === postId);
2479
+ if (!post || post.weekNumber === week) return;
2480
+ await savePost({ id: postId, weekNumber: week });
2481
+ };
2482
+ const createPostAt = async (isoDate) => {
2483
+ if (await guardContentPosts() === "blocked") return;
2484
+ const targetPlan = await ensurePlan();
2485
+ if (!targetPlan) return;
2486
+ let week = 1;
2487
+ if (targetPlan.startDate) {
2488
+ const start = /* @__PURE__ */ new Date(`${targetPlan.startDate}T00:00:00`);
2489
+ const day = /* @__PURE__ */ new Date(`${isoDate}T00:00:00`);
2490
+ const diff = Math.floor((day.getTime() - start.getTime()) / (7 * 24 * 3600 * 1e3)) + 1;
2491
+ week = Math.min(Math.max(diff, 1), targetPlan.weeksCount);
2492
+ }
2493
+ const post = await savePost({
2494
+ planId: targetPlan.id,
2495
+ weekNumber: week,
2496
+ title: "",
2497
+ format: "reel",
2498
+ status: "idea",
2499
+ scheduledDate: isoDate
2500
+ });
2501
+ invalidateLimit("content_posts_month");
2502
+ onOpenPost(post.id);
2503
+ };
2504
+ const busy = !loaded || loading;
2505
+ const hasAccount = accounts.length > 0;
2506
+ const weeksCount = plan?.weeksCount ?? DEFAULT_WEEKS;
2507
+ const boardStart = plan?.startDate ? /* @__PURE__ */ new Date(`${plan.startDate}T00:00:00`) : mondayOfCurrentWeek();
2508
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl space-y-6", children: [
2509
+ plan && /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsxs(Button, { onClick: () => setBriefOpen(true), children: [
2510
+ /* @__PURE__ */ jsx(NotebookPen, { className: "mr-1.5 h-4 w-4" }),
2511
+ t("marketing.content.brief")
2512
+ ] }) }),
2513
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
2514
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
2515
+ /* @__PURE__ */ jsx(AccountSelector, {}),
2516
+ plan && /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: plan.name })
2517
+ ] }),
2518
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx(ViewSwitcher, { mode: viewMode, onChange: setViewMode }) })
2519
+ ] }),
2520
+ !busy && !hasAccount && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("marketing.content.noAccountsHint") }),
2521
+ 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)) }),
2522
+ busy && viewMode === "calendar" && /* @__PURE__ */ jsx(Skeleton, { className: "h-96 w-full" }),
2523
+ 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)) }),
2524
+ !busy && viewMode === "board" && /* @__PURE__ */ jsx(
2525
+ BoardView,
2526
+ {
2527
+ startDate: boardStart,
2528
+ weeksCount,
2529
+ postsByWeek,
2530
+ onOpenPost,
2531
+ onNewPost: (w) => void createPost(w),
2532
+ onDeletePost: setPendingDeleteId,
2533
+ onDropPost: (id, w) => void movePost(id, w),
2534
+ disabled: !hasAccount
2535
+ }
2536
+ ),
2537
+ !busy && viewMode === "calendar" && /* @__PURE__ */ jsx(
2538
+ CalendarView,
2539
+ {
2540
+ posts,
2541
+ onOpenPost,
2542
+ onNewPostAt: (iso) => void createPostAt(iso),
2543
+ disabled: !hasAccount
2544
+ }
2545
+ ),
2546
+ !busy && viewMode === "list" && /* @__PURE__ */ jsx(ListView, { posts, onOpenPost }),
2547
+ /* @__PURE__ */ jsx(PlanBriefSheet, { open: briefOpen, onClose: () => setBriefOpen(false) }),
2548
+ /* @__PURE__ */ jsx(PostModal, { postId: openPostId, onClose: () => onOpenPost(null) }),
2549
+ /* @__PURE__ */ jsx(
2550
+ ConfirmDialog,
2551
+ {
2552
+ open: !!pendingDeleteId,
2553
+ variant: "destructive",
2554
+ title: t("marketing.content.deletePost"),
2555
+ description: t("marketing.content.deletePostConfirm"),
2556
+ confirmLabel: t("marketing.content.deletePost"),
2557
+ cancelLabel: t("marketing.content.cancel"),
2558
+ onCancel: () => setPendingDeleteId(null),
2559
+ onConfirm: () => {
2560
+ const id = pendingDeleteId;
2561
+ setPendingDeleteId(null);
2562
+ if (id) void deletePost(id);
2563
+ }
2564
+ }
2565
+ )
2566
+ ] });
2567
+ }
2568
+ function useEntityCrud(entity) {
2569
+ const provider = useMemo(() => resolveDataProvider(entity), [entity]);
2570
+ const [items, setItems] = useState(null);
2571
+ const [total, setTotal] = useState(0);
2572
+ const fetch = useCallback(async (query = {}) => {
2573
+ try {
2574
+ const res = await provider.list(query);
2575
+ setItems(res.data);
2576
+ setTotal(res.total);
2577
+ } catch (err) {
2578
+ console.warn("[blog] list failed; showing empty. Apply the plugin-blog migration to this pool.", err);
2579
+ setItems([]);
2580
+ setTotal(0);
2581
+ }
2582
+ }, [provider]);
2583
+ const getById = useCallback(async (id) => {
2584
+ const res = await provider.list({ filters: { id }, page: 1, pageSize: 1 });
2585
+ return res.data[0] ?? null;
2586
+ }, [provider]);
2587
+ const create = useCallback((data) => provider.create(data), [provider]);
2588
+ const update = useCallback((id, data) => provider.update(id, data), [provider]);
2589
+ const remove = useCallback((id) => provider.remove(id), [provider]);
2590
+ return { items, total, fetch, getById, create, update, remove };
2591
+ }
2592
+ function PostsList({ navigate, navTabs }) {
2593
+ const entity = useMemo(() => buildBlogPostEntity(), []);
2594
+ const { items, total, fetch } = useEntityCrud(entity);
2595
+ const [search, setSearch] = useState("");
2596
+ const [statusFilter, setStatusFilter] = useState();
2597
+ const [loadedOnce, setLoadedOnce] = useState(false);
2598
+ useEffect(() => {
2599
+ let active = true;
2600
+ Promise.resolve(
2601
+ fetch({ search: search || void 0, filters: statusFilter ? { status: statusFilter } : void 0 })
2602
+ ).finally(() => {
2603
+ if (active) setLoadedOnce(true);
2604
+ });
2605
+ return () => {
2606
+ active = false;
2607
+ };
2608
+ }, [search, statusFilter, fetch]);
2609
+ const facets = useMemo(() => [{
2610
+ field: "status",
2611
+ allLabel: "All",
2612
+ options: [{ value: "draft", label: "Draft" }, { value: "published", label: "Published" }]
2613
+ }], []);
2614
+ return /* @__PURE__ */ jsx(
2615
+ CrudListView,
2616
+ {
2617
+ entityDef: entity,
2618
+ items: loadedOnce ? items : null,
2619
+ total,
2620
+ search,
2621
+ onSearchChange: setSearch,
2622
+ searchPlaceholder: "Search posts\u2026",
2623
+ facets,
2624
+ activeFilters: { status: statusFilter },
2625
+ onFacetChange: (_field, value) => setStatusFilter(value),
2626
+ navTabs,
2627
+ activeNavTab: "posts",
2628
+ onNew: () => navigate("blog-new"),
2629
+ aiCreate: {
2630
+ hint: "Descreva o post\u2026",
2631
+ description: "Descreva o assunto e eu escrevo o rascunho.",
2632
+ intro: {
2633
+ role: "growth",
2634
+ persona: "Redator do blog",
2635
+ pitch: "Diga sobre o que \xE9 o post.",
2636
+ description: "Escrevo t\xEDtulo, resumo e corpo em markdown, com slug e tempo de leitura. Nasce como rascunho \u2014 voc\xEA l\xEA, ajusta e publica quando quiser.",
2637
+ suggestions: [
2638
+ { label: "Um post sobre cuidados p\xF3s-procedimento" },
2639
+ { label: "Cinco d\xFAvidas frequentes dos clientes, respondidas" },
2640
+ { label: "Um post curto para divulgar a promo\xE7\xE3o do m\xEAs" }
2641
+ ]
2642
+ }
2643
+ },
2644
+ addLabel: "New post",
2645
+ onRowClick: (row) => navigate(`blog-post:${row.id}`)
2646
+ }
2647
+ );
2648
+ }
2649
+ function CategoriesList({ navigate, navTabs }) {
2650
+ const entity = useMemo(() => buildBlogCategoryEntity(), []);
2651
+ const { items, total, fetch } = useEntityCrud(entity);
2652
+ const [search, setSearch] = useState("");
2653
+ const [loadedOnce, setLoadedOnce] = useState(false);
2654
+ useEffect(() => {
2655
+ let active = true;
2656
+ Promise.resolve(fetch({ search: search || void 0 })).finally(() => {
2657
+ if (active) setLoadedOnce(true);
2658
+ });
2659
+ return () => {
2660
+ active = false;
2661
+ };
2662
+ }, [search, fetch]);
2663
+ return /* @__PURE__ */ jsx(
2664
+ CrudListView,
2665
+ {
2666
+ entityDef: entity,
2667
+ items: loadedOnce ? items : null,
2668
+ total,
2669
+ search,
2670
+ onSearchChange: setSearch,
2671
+ searchPlaceholder: "Search categories\u2026",
2672
+ navTabs,
2673
+ activeNavTab: "categories",
2674
+ onNew: () => navigate("blog-cat-new"),
2675
+ addLabel: "New category",
2676
+ onRowClick: (row) => navigate(`blog-cat:${row.id}`)
2677
+ }
2678
+ );
2679
+ }
2680
+ function BlogAdminView({ navigate }) {
2681
+ const { labels } = useMarketingConfig();
2682
+ const t = useTranslation();
2683
+ const [lens, setLens] = useState("posts");
2684
+ const navTabs = useMemo(() => [
2685
+ { id: "posts", label: labels.blog ?? t("marketing.nav.blog"), onSelect: () => setLens("posts") },
2686
+ { id: "categories", label: labels.blogCategories ?? t("marketing.nav.blogCategories"), onSelect: () => setLens("categories") }
2687
+ ], [labels.blog, labels.blogCategories, t]);
2688
+ return lens === "posts" ? /* @__PURE__ */ jsx(PostsList, { navigate, navTabs }) : /* @__PURE__ */ jsx(CategoriesList, { navigate, navTabs });
2689
+ }
2690
+ function BlogEntityForm({
2691
+ entity,
2692
+ editId,
2693
+ parentLabel,
2694
+ onDone
2695
+ }) {
2696
+ const { getById, create, update, remove } = useEntityCrud(entity);
2697
+ const isEdit = !!editId;
2698
+ const isPost = entity.data?.table === BLOG_POSTS_TABLE;
2699
+ const guardBlogPosts = useLimitGuard("blog_posts");
2700
+ const namePlural = entity.namePlural ?? entity.name + "s";
2701
+ const [initialData, setInitialData] = React11.useState(isEdit ? null : {});
2702
+ React11.useEffect(() => {
2703
+ if (!editId) return;
2704
+ let cancelled = false;
2705
+ (async () => {
2706
+ const item = await getById(editId);
2707
+ if (!cancelled) setInitialData(item ?? {});
2708
+ })();
2709
+ return () => {
2710
+ cancelled = true;
2711
+ };
2712
+ }, [editId, getById]);
2713
+ async function save(values) {
2714
+ if (!isEdit && isPost && await guardBlogPosts() === "blocked") return;
2715
+ try {
2716
+ if (isEdit) await update(editId, values);
2717
+ else {
2718
+ await create(values);
2719
+ if (isPost) invalidateLimit("blog_posts");
2720
+ }
2721
+ toast.success(isEdit ? `${entity.name} updated` : `${entity.name} created`);
2722
+ onDone();
2723
+ } catch (err) {
2724
+ toast.error(`Failed to save ${entity.name.toLowerCase()}`);
2725
+ throw err;
2726
+ }
2727
+ }
2728
+ async function del() {
2729
+ if (!editId) return;
2730
+ try {
2731
+ await remove(editId);
2732
+ toast.success(`${entity.name} deleted`);
2733
+ onDone();
2734
+ } catch {
2735
+ toast.error(`Failed to delete ${entity.name.toLowerCase()}`);
2736
+ }
2737
+ }
2738
+ const title = isEdit ? initialData?.[entity.displayField ?? "name"] ?? entity.name : `New ${entity.name.toLowerCase()}`;
2739
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2740
+ /* @__PURE__ */ jsx(
2741
+ SubpageHeader,
2742
+ {
2743
+ title: String(title),
2744
+ onBack: onDone,
2745
+ parentLabel,
2746
+ actions: isEdit ? /* @__PURE__ */ jsx(Button, { variant: "ghost", className: "text-destructive hover:text-destructive", onClick: del, children: "Delete" }) : void 0
2747
+ }
2748
+ ),
2749
+ initialData !== null && /* @__PURE__ */ jsx(
2750
+ CrudFormPage,
2751
+ {
2752
+ entityDef: entity,
2753
+ mode: isEdit ? "edit" : "create",
2754
+ initialData,
2755
+ namePlural,
2756
+ onCancel: onDone,
2757
+ onSubmit: save,
2758
+ hideBreadcrumb: true
2759
+ }
2760
+ )
2761
+ ] });
2762
+ }
2763
+ function buildNav(config, view, navigate, hasCadenceSurface, t) {
2764
+ const items = [
2765
+ { id: "overview", label: config.labels.overview ?? t("marketing.nav.overview"), icon: "BarChart3", active: view === "overview", onClick: () => navigate("overview") }
2766
+ ];
2767
+ if (config.modules.channels) {
2768
+ items.push({
2769
+ id: "channels",
2770
+ label: config.labels.channels ?? t("marketing.nav.channels"),
2771
+ icon: "Radio",
2772
+ active: view === "channels" || view.startsWith("channel-detail:"),
2773
+ onClick: () => navigate("channels")
2774
+ });
2775
+ }
2776
+ if (config.modules.campaigns) {
2777
+ items.push({
2778
+ id: "campaigns",
2779
+ label: config.labels.campaigns ?? t("marketing.nav.campaigns"),
2780
+ icon: "Megaphone",
2781
+ active: ["campaigns", "message-templates", "send-history"].includes(view),
2782
+ children: [
2783
+ { id: "campaigns", label: config.labels.campaigns ?? t("marketing.nav.campaigns"), defaultChild: true, active: view === "campaigns", onClick: () => navigate("campaigns") },
2784
+ { id: "message-templates", label: t("marketing.nav.messageTemplates"), active: view === "message-templates", onClick: () => navigate("message-templates") },
2785
+ { id: "send-history", label: t("marketing.nav.sendHistory"), active: view === "send-history", onClick: () => navigate("send-history") }
2786
+ ]
2787
+ });
2788
+ items.push({ id: "segments", label: t("marketing.nav.segments"), icon: "Users", active: view === "segments", onClick: () => navigate("segments") });
2789
+ }
2790
+ if (config.modules.funnel) {
2791
+ items.push({ id: "funnel", label: config.labels.funnel ?? t("marketing.nav.funnel"), icon: "Filter", active: view === "funnel", onClick: () => navigate("funnel") });
2792
+ }
2793
+ if (config.modules.cadences && hasCadenceSurface) {
2794
+ items.push({
2795
+ id: "cadences",
2796
+ label: config.labels.cadences ?? t("marketing.cadences.title"),
2797
+ icon: "Workflow",
2798
+ active: view === "cadences" || view === "quick-triggers",
2799
+ children: [
2800
+ { id: "cadences", label: config.labels.cadences ?? t("marketing.cadences.title"), defaultChild: true, active: view === "cadences", onClick: () => navigate("cadences") },
2801
+ // O gatilho é a cadência de um passo só, com a porta de um clique.
2802
+ { id: "quick-triggers", label: t("marketing.nav.quickTriggers"), active: view === "quick-triggers", onClick: () => navigate("quick-triggers") }
2803
+ ]
2804
+ });
2805
+ }
2806
+ if (config.modules.landingPages) {
2807
+ items.push({ id: "landing-pages", label: config.labels.landingPages ?? t("marketing.nav.landingPages"), icon: "LayoutTemplate", active: view === "landing-pages", onClick: () => navigate("landing-pages") });
2808
+ }
2809
+ if (config.modules.contentPlanner) {
2810
+ items.push({
2811
+ id: "content",
2812
+ label: config.labels.content ?? t("marketing.nav.content"),
2813
+ icon: "Clapperboard",
2814
+ active: view === "content" || view === "content-brief" || view.startsWith("content-post:"),
2815
+ onClick: () => navigate("content")
2816
+ });
2817
+ }
2818
+ if (config.modules.blog) {
2819
+ items.push({
2820
+ id: "blog",
2821
+ label: config.labels.blog ?? t("marketing.nav.blog"),
2822
+ icon: "Newspaper",
2823
+ active: view === "blog" || view === "blog-new" || view.startsWith("blog-post:") || view === "blog-cat-new" || view.startsWith("blog-cat:"),
2824
+ onClick: () => navigate("blog")
2825
+ });
2826
+ }
2827
+ {
2828
+ items.push({
2829
+ id: "brand",
2830
+ label: t("marketing.nav.brand"),
2831
+ icon: "Palette",
2832
+ active: view === "brand" || view === "authorized-images",
2833
+ children: [
2834
+ { id: "brand", label: t("marketing.nav.brand"), defaultChild: true, active: view === "brand", onClick: () => navigate("brand") },
2835
+ { id: "authorized-images", label: t("marketing.nav.authorizedImages"), active: view === "authorized-images", onClick: () => navigate("authorized-images") }
2836
+ ]
2837
+ });
2838
+ }
2839
+ return items;
2840
+ }
2841
+ function MarketingPage({ config, provider, store, contentProvider, contentStore, cadenceSurface }) {
2842
+ const t = useTranslation();
2843
+ const { view, direction, navigate } = useModuleNavigation("/marketing", {
2844
+ overview: 0,
2845
+ channels: 0,
2846
+ campaigns: 0,
2847
+ funnel: 0,
2848
+ cadences: 0,
2849
+ "landing-pages": 0,
2850
+ content: 0,
2851
+ blog: 0,
2852
+ "channel-detail": 1,
2853
+ "content-post": 1,
2854
+ "content-brief": 1,
2855
+ "blog-new": 1,
2856
+ "blog-post": 1,
2857
+ "blog-cat-new": 1,
2858
+ "blog-cat": 1
2859
+ }, "overview");
2860
+ React11.useEffect(() => {
2861
+ void store.getState().load();
2862
+ }, [store]);
2863
+ useDataChanged({ table: "plg_marketing_campaigns" }, () => {
2864
+ void store.getState().refresh();
2865
+ }, [store]);
2866
+ const isOverview = view === "overview";
2867
+ const nav = buildNav(config, view, navigate, Boolean(cadenceSurface), t);
2868
+ const blogPostEntity = React11.useMemo(() => buildBlogPostEntity(), []);
2869
+ const blogCategoryEntity = React11.useMemo(() => buildBlogCategoryEntity(), []);
2870
+ const quickActions = React11.useMemo(() => [
2871
+ { id: "new-campaign", label: t("marketing.campaigns.new"), icon: "Megaphone", description: config.labels.campaigns, action: () => navigate("campaigns") }
2872
+ ], [t, config.labels.campaigns]);
2873
+ const renderView = createViewRouter([
2874
+ { id: "channels", render: () => /* @__PURE__ */ jsx(ChannelsView, { onOpen: (id) => navigate(`channel-detail:${id}`) }) },
2875
+ { id: "channel-detail", render: ({ id }) => /* @__PURE__ */ jsx(ChannelDetailView, { channelId: id, onBack: () => navigate("channels") }) },
2876
+ { id: "campaigns", render: () => /* @__PURE__ */ jsx(CampaignsView, {}) },
2877
+ { id: "funnel", render: () => /* @__PURE__ */ jsx(FunnelView, {}) },
2878
+ { id: "cadences", render: () => /* @__PURE__ */ jsx(CadencesView, { surface: cadenceSurface }) },
2879
+ { id: "landing-pages", render: () => /* @__PURE__ */ jsx(LandingPagesView, {}) },
2880
+ { id: "content", render: () => /* @__PURE__ */ jsx(ContentView, {}) },
2881
+ { id: "content-post", render: ({ id }) => /* @__PURE__ */ jsx(ContentView, { initialPostId: id }) },
2882
+ { id: "content-brief", render: () => /* @__PURE__ */ jsx(PlanBriefView, { onBack: () => navigate("content") }) },
2883
+ { id: "blog", render: () => /* @__PURE__ */ jsx(BlogAdminView, { navigate }) },
2884
+ { id: "blog-new", render: () => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogPostEntity, parentLabel: config.labels.blog ?? t("marketing.nav.blog"), onDone: () => navigate("blog") }) },
2885
+ { id: "blog-post", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogPostEntity, editId: id, parentLabel: config.labels.blog ?? t("marketing.nav.blog"), onDone: () => navigate("blog") }) },
2886
+ { id: "blog-cat-new", render: () => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, parentLabel: config.labels.blogCategories ?? t("marketing.nav.blogCategories"), onDone: () => navigate("blog") }) },
2887
+ { id: "blog-cat", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, editId: id, parentLabel: config.labels.blogCategories ?? t("marketing.nav.blogCategories"), onDone: () => navigate("blog") }) },
2888
+ // As telas que ainda não existem — ver ./GapScreens.
2889
+ { id: "segments", render: () => /* @__PURE__ */ jsx(SegmentsView, {}) },
2890
+ { id: "message-templates", render: () => /* @__PURE__ */ jsx(MessageTemplatesView, {}) },
2891
+ { id: "send-history", render: () => /* @__PURE__ */ jsx(SendHistoryView, {}) },
2892
+ { id: "quick-triggers", render: () => /* @__PURE__ */ jsx(QuickTriggersView, {}) },
2893
+ { id: "brand", render: () => /* @__PURE__ */ jsx(BrandView, {}) },
2894
+ { id: "authorized-images", render: () => /* @__PURE__ */ jsx(AuthorizedImagesView, {}) },
2895
+ { id: "overview", render: () => /* @__PURE__ */ jsx(OverviewView, {}) }
2896
+ ], "overview");
2897
+ return /* @__PURE__ */ jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsx(
2898
+ ModulePage,
2899
+ {
2900
+ title: config.labels.pageTitle,
2901
+ subtitle: config.labels.pageSubtitle,
2902
+ nav,
2903
+ navVariant: config.moduleNav,
2904
+ showHeader: isOverview,
2905
+ viewKey: view,
2906
+ direction,
2907
+ headerAction: /* @__PURE__ */ jsx(
2908
+ ModuleActionBar,
2909
+ {
2910
+ quickActions,
2911
+ settingsPath: "/settings/marketing"
2912
+ }
2913
+ ),
2914
+ children: renderView(view)
2915
+ }
2916
+ ) }) });
2917
+ }
2918
+
2919
+ export { MarketingPage };
2920
+ //# sourceMappingURL=MarketingPage-BE54EZHP.js.map
2921
+ //# sourceMappingURL=MarketingPage-BE54EZHP.js.map