@fayz-ai/plugin-marketing 0.2.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +2 -0
  2. package/dist/MarketingContext.d.ts +2 -0
  3. package/dist/MarketingContext.d.ts.map +1 -1
  4. package/dist/MarketingPage.d.ts.map +1 -1
  5. package/dist/data/blogEntities.d.ts +28 -0
  6. package/dist/data/blogEntities.d.ts.map +1 -0
  7. package/dist/data/contentSupabase.d.ts.map +1 -1
  8. package/dist/data/tables.d.ts +6 -0
  9. package/dist/data/tables.d.ts.map +1 -0
  10. package/dist/index.cjs +630 -127
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +636 -133
  14. package/dist/index.js.map +1 -1
  15. package/dist/integrations/ranklayer/connectorDef.d.ts +3 -0
  16. package/dist/integrations/ranklayer/connectorDef.d.ts.map +1 -0
  17. package/dist/integrations/ranklayer/data/supabase.d.ts +16 -0
  18. package/dist/integrations/ranklayer/data/supabase.d.ts.map +1 -0
  19. package/dist/integrations/ranklayer/types.d.ts +16 -0
  20. package/dist/integrations/ranklayer/types.d.ts.map +1 -0
  21. package/dist/migrations/index.d.ts +9 -3
  22. package/dist/migrations/index.d.ts.map +1 -1
  23. package/dist/presets.d.ts +2 -0
  24. package/dist/presets.d.ts.map +1 -1
  25. package/dist/schema/index.d.ts +44 -44
  26. package/dist/schema/index.d.ts.map +1 -1
  27. package/dist/schema.cjs +9 -9
  28. package/dist/schema.cjs.map +1 -1
  29. package/dist/schema.js +9 -9
  30. package/dist/schema.js.map +1 -1
  31. package/dist/views/blog/BlogAdminView.d.ts +5 -0
  32. package/dist/views/blog/BlogAdminView.d.ts.map +1 -0
  33. package/dist/views/blog/BlogEntityForm.d.ts +11 -0
  34. package/dist/views/blog/BlogEntityForm.d.ts.map +1 -0
  35. package/dist/views/blog/useEntityCrud.d.ts +29 -0
  36. package/dist/views/blog/useEntityCrud.d.ts.map +1 -0
  37. package/package.json +8 -5
  38. package/src/MarketingContext.tsx +2 -0
  39. package/src/MarketingPage.tsx +22 -1
  40. package/src/data/blogEntities.tsx +91 -0
  41. package/src/data/contentSupabase.ts +18 -17
  42. package/src/data/tables.ts +7 -0
  43. package/src/index.ts +27 -3
  44. package/src/integrations/ranklayer/RANKLAYER.md +40 -0
  45. package/src/integrations/ranklayer/connectorDef.tsx +98 -0
  46. package/src/integrations/ranklayer/data/supabase.ts +106 -0
  47. package/src/integrations/ranklayer/types.ts +16 -0
  48. package/src/migrations/000_plg_rename.sql +15 -0
  49. package/src/migrations/001_content_planner.sql +48 -48
  50. package/src/migrations/002_multi_platform.sql +5 -5
  51. package/src/migrations/003_recording_ops.sql +10 -10
  52. package/src/migrations/004_ranklayer.sql +50 -0
  53. package/src/migrations/index.ts +152 -74
  54. package/src/presets.ts +5 -3
  55. package/src/schema/index.ts +9 -9
  56. package/src/views/blog/BlogAdminView.tsx +99 -0
  57. package/src/views/blog/BlogEntityForm.tsx +92 -0
  58. package/src/views/blog/useEntityCrud.ts +50 -0
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import React8, { useEffect } from 'react';
2
- import { getActiveTenantId, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation } from '@fayz-ai/core';
3
- import { createPluginContext, PluginSettingsPanel, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, SelectRow, ToggleRow, useOrganizationStore } from '@fayz-ai/saas';
4
- import { defineKpiWidget, defineCustomWidget, defineTableWidget, ModulePage, DataTable, SubpageHeader, Button, Skeleton, ConfirmDialog, ContentSplit, ContentSplitTrigger, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MarkdownEditor, DashboardCanvas, SegmentedControl, cn, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, SelectSeparator, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, Checkbox, KpiCard as KpiCard$1, Card, CardHeader, CardTitle, CardContent, TableWidget } from '@fayz-ai/ui';
5
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
- import { ChevronRight, Users, Target, Percent, DollarSign, Trash2, Plus, MousePointerClick, Layers, NotebookPen, ArrowUpRight, ArrowDownRight, Settings2, Columns3, CalendarDays, List, ChevronLeft, Loader2, ImagePlus, X, Sparkles, Bike, Utensils, DoorOpen, MessageCircle, Instagram, Mail, Globe, Megaphone, Search, Link2, AtSign, Pin, Twitter, Linkedin, Facebook, Youtube, Music2, Repeat2, Heart, BarChart2, Share2, ThumbsUp, Send, MoreHorizontal, Bookmark, GripVertical, Image, Trophy } from 'lucide-react';
1
+ import React8, { useState, useEffect, useMemo, useCallback } from 'react';
2
+ import { getActiveTenantId, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, resolveDataProvider } from '@fayz-ai/core';
3
+ import { createPluginContext, PluginSettingsPanel, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, SelectRow, ToggleRow, useOrganizationStore, CrudFormPage, CrudListView } from '@fayz-ai/saas';
4
+ import { defineKpiWidget, defineCustomWidget, defineTableWidget, ModulePage, DataTable, SubpageHeader, Button, Skeleton, ConfirmDialog, ContentSplit, ContentSplitTrigger, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MarkdownEditor, SegmentedControl, DashboardCanvas, cn, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, SelectSeparator, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, Checkbox, toast, KpiCard as KpiCard$1, Card, CardHeader, CardTitle, CardContent, TableWidget } from '@fayz-ai/ui';
5
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
+ import { Sparkles, History, CheckCircle2, AlertCircle, ChevronRight, Users, Target, Percent, DollarSign, Trash2, Plus, MousePointerClick, Layers, NotebookPen, ArrowUpRight, ArrowDownRight, Settings2, Columns3, CalendarDays, List, ChevronLeft, Loader2, ImagePlus, X, Bike, Utensils, DoorOpen, MessageCircle, Instagram, Mail, Globe, Megaphone, Search, Link2, AtSign, Pin, Twitter, Linkedin, Facebook, Youtube, Music2, Repeat2, Heart, BarChart2, Share2, ThumbsUp, Send, MoreHorizontal, Bookmark, GripVertical, Image, Trophy } from 'lucide-react';
7
7
  import { createStore } from 'zustand/vanilla';
8
8
 
9
9
  // src/index.ts
@@ -213,11 +213,11 @@ function ChannelDetailView({ channelId, onBack }) {
213
213
  const range = useMarketingStore((s) => s.range);
214
214
  const channels = useMarketingStore((s) => s.channels);
215
215
  const lookup = useChannelLookup();
216
- const provider = useMarketingProvider();
216
+ const provider2 = useMarketingProvider();
217
217
  const [campaigns, setCampaigns] = React8.useState([]);
218
218
  React8.useEffect(() => {
219
- void provider.listCampaigns({ range, channelId }).then(setCampaigns);
220
- }, [provider, range, channelId]);
219
+ void provider2.listCampaigns({ range, channelId }).then(setCampaigns);
220
+ }, [provider2, range, channelId]);
221
221
  const perf = channels.find((c) => c.channelId === channelId);
222
222
  const channel = lookup.get(channelId);
223
223
  const columns = React8.useMemo(() => [
@@ -1962,7 +1962,7 @@ function toDraft2(post) {
1962
1962
  }
1963
1963
  function PostPage({ postId, onBack }) {
1964
1964
  const t = useTranslation();
1965
- const provider = useContentPlannerProvider();
1965
+ const provider2 = useContentPlannerProvider();
1966
1966
  const storePost = useContentPlannerStore((s) => s.posts.find((p) => p.id === postId));
1967
1967
  const savePost = useContentPlannerStore((s) => s.savePost);
1968
1968
  const deletePost = useContentPlannerStore((s) => s.deletePost);
@@ -1979,13 +1979,13 @@ function PostPage({ postId, onBack }) {
1979
1979
  setDraft(toDraft2(storePost));
1980
1980
  return;
1981
1981
  }
1982
- void provider.getPost(postId).then((fetched) => {
1982
+ void provider2.getPost(postId).then((fetched) => {
1983
1983
  if (fetched) {
1984
1984
  setPost(fetched);
1985
1985
  setDraft(toDraft2(fetched));
1986
1986
  }
1987
1987
  });
1988
- }, [postId, storePost, post?.id, provider]);
1988
+ }, [postId, storePost, post?.id, provider2]);
1989
1989
  if (!post || !draft) {
1990
1990
  return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
1991
1991
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
@@ -2172,6 +2172,239 @@ function PostPage({ postId, onBack }) {
2172
2172
  post.id
2173
2173
  );
2174
2174
  }
2175
+
2176
+ // src/data/blogEntities.tsx
2177
+ var BLOG_POSTS_TABLE = "plg_blog_posts";
2178
+ var BLOG_CATEGORIES_TABLE = "plg_blog_categories";
2179
+ function buildBlogPostEntity() {
2180
+ const fields = [
2181
+ { key: "title", label: "Title", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
2182
+ { key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: false, hint: 'URL identifier, e.g. "cannabis-and-oral-health"' },
2183
+ { key: "status", label: "Status", type: "select", options: ["draft", "published"], showInTable: true, defaultValue: "draft" },
2184
+ { key: "categoryId", label: "Category", type: "relation", relation: { table: BLOG_CATEGORIES_TABLE, valueField: "id", labelField: "name", tenantScoped: true }, showInTable: false },
2185
+ { key: "excerpt", label: "Excerpt", type: "textarea", span: 2, showInTable: false, placeholder: "Short summary shown on cards" },
2186
+ { key: "body", label: "Body", type: "markdown", span: 2, showInTable: false },
2187
+ { key: "coverImage", label: "Cover image", type: "image", showInTable: false, placeholder: "https://\u2026" },
2188
+ { key: "readTime", label: "Read time", type: "text", showInTable: false, placeholder: "5 min" },
2189
+ { key: "publishedAt", label: "Published at", type: "date", showInTable: true, sortable: true },
2190
+ { key: "authorName", label: "Author name", type: "text", showInTable: false },
2191
+ { key: "authorRole", label: "Author role", type: "text", showInTable: false, placeholder: "e.g. Cirurgi\xE3o-Dentista" },
2192
+ { key: "authorAvatarUrl", label: "Author avatar", type: "image", showInTable: false, placeholder: "https://\u2026" },
2193
+ { key: "authorBio", label: "Author bio", type: "textarea", span: 2, showInTable: false }
2194
+ ];
2195
+ return {
2196
+ name: "Post",
2197
+ namePlural: "Posts",
2198
+ icon: "Newspaper",
2199
+ fields,
2200
+ displayField: "title",
2201
+ subtitleField: "slug",
2202
+ facets: [{ field: "status", allLabel: "All" }],
2203
+ data: {
2204
+ table: BLOG_POSTS_TABLE,
2205
+ tenantScoped: true,
2206
+ searchColumns: ["title", "slug", "excerpt"]
2207
+ }
2208
+ };
2209
+ }
2210
+ function buildBlogCategoryEntity() {
2211
+ const fields = [
2212
+ { key: "name", label: "Name", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
2213
+ { key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: true, hint: "URL identifier" },
2214
+ { key: "description", label: "Description", type: "textarea", span: 2, showInTable: false }
2215
+ ];
2216
+ return {
2217
+ name: "Category",
2218
+ namePlural: "Categories",
2219
+ icon: "Tags",
2220
+ fields,
2221
+ displayField: "name",
2222
+ data: {
2223
+ table: BLOG_CATEGORIES_TABLE,
2224
+ tenantScoped: true,
2225
+ searchColumns: ["name", "slug"]
2226
+ }
2227
+ };
2228
+ }
2229
+ function useEntityCrud(entity) {
2230
+ const provider2 = useMemo(() => resolveDataProvider(entity), [entity]);
2231
+ const [items, setItems] = useState(null);
2232
+ const [total, setTotal] = useState(0);
2233
+ const fetch = useCallback(async (query = {}) => {
2234
+ try {
2235
+ const res = await provider2.list(query);
2236
+ setItems(res.data);
2237
+ setTotal(res.total);
2238
+ } catch (err) {
2239
+ console.warn("[blog] list failed; showing empty. Apply the plugin-blog migration to this pool.", err);
2240
+ setItems([]);
2241
+ setTotal(0);
2242
+ }
2243
+ }, [provider2]);
2244
+ const getById = useCallback(async (id) => {
2245
+ const res = await provider2.list({ filters: { id }, page: 1, pageSize: 1 });
2246
+ return res.data[0] ?? null;
2247
+ }, [provider2]);
2248
+ const create = useCallback((data) => provider2.create(data), [provider2]);
2249
+ const update = useCallback((id, data) => provider2.update(id, data), [provider2]);
2250
+ const remove = useCallback((id) => provider2.remove(id), [provider2]);
2251
+ return { items, total, fetch, getById, create, update, remove };
2252
+ }
2253
+ function PostsList({ navigate }) {
2254
+ const entity = useMemo(() => buildBlogPostEntity(), []);
2255
+ const { items, total, fetch } = useEntityCrud(entity);
2256
+ const [search, setSearch] = useState("");
2257
+ const [statusFilter, setStatusFilter] = useState();
2258
+ const [loadedOnce, setLoadedOnce] = useState(false);
2259
+ useEffect(() => {
2260
+ let active = true;
2261
+ Promise.resolve(
2262
+ fetch({ search: search || void 0, filters: statusFilter ? { status: statusFilter } : void 0 })
2263
+ ).finally(() => {
2264
+ if (active) setLoadedOnce(true);
2265
+ });
2266
+ return () => {
2267
+ active = false;
2268
+ };
2269
+ }, [search, statusFilter, fetch]);
2270
+ const facets = useMemo(() => [{
2271
+ field: "status",
2272
+ allLabel: "All",
2273
+ options: [{ value: "draft", label: "Draft" }, { value: "published", label: "Published" }]
2274
+ }], []);
2275
+ return /* @__PURE__ */ jsx(
2276
+ CrudListView,
2277
+ {
2278
+ entityDef: entity,
2279
+ items: loadedOnce ? items : null,
2280
+ total,
2281
+ search,
2282
+ onSearchChange: setSearch,
2283
+ searchPlaceholder: "Search posts\u2026",
2284
+ facets,
2285
+ activeFilters: { status: statusFilter },
2286
+ onFacetChange: (_field, value) => setStatusFilter(value),
2287
+ onNew: () => navigate("blog-new"),
2288
+ addLabel: "New post",
2289
+ onRowClick: (row) => navigate(`blog-post:${row.id}`)
2290
+ }
2291
+ );
2292
+ }
2293
+ function CategoriesList({ navigate }) {
2294
+ const entity = useMemo(() => buildBlogCategoryEntity(), []);
2295
+ const { items, total, fetch } = useEntityCrud(entity);
2296
+ const [search, setSearch] = useState("");
2297
+ const [loadedOnce, setLoadedOnce] = useState(false);
2298
+ useEffect(() => {
2299
+ let active = true;
2300
+ Promise.resolve(fetch({ search: search || void 0 })).finally(() => {
2301
+ if (active) setLoadedOnce(true);
2302
+ });
2303
+ return () => {
2304
+ active = false;
2305
+ };
2306
+ }, [search, fetch]);
2307
+ return /* @__PURE__ */ jsx(
2308
+ CrudListView,
2309
+ {
2310
+ entityDef: entity,
2311
+ items: loadedOnce ? items : null,
2312
+ total,
2313
+ search,
2314
+ onSearchChange: setSearch,
2315
+ searchPlaceholder: "Search categories\u2026",
2316
+ onNew: () => navigate("blog-cat-new"),
2317
+ addLabel: "New category",
2318
+ onRowClick: (row) => navigate(`blog-cat:${row.id}`)
2319
+ }
2320
+ );
2321
+ }
2322
+ function BlogAdminView({ navigate }) {
2323
+ const { labels } = useMarketingConfig();
2324
+ const [lens, setLens] = useState("posts");
2325
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2326
+ /* @__PURE__ */ jsx(
2327
+ SegmentedControl,
2328
+ {
2329
+ "aria-label": "Blog section",
2330
+ options: [
2331
+ { value: "posts", label: labels.blog },
2332
+ { value: "categories", label: labels.blogCategories }
2333
+ ],
2334
+ value: lens,
2335
+ onChange: (v) => setLens(v)
2336
+ }
2337
+ ),
2338
+ lens === "posts" ? /* @__PURE__ */ jsx(PostsList, { navigate }) : /* @__PURE__ */ jsx(CategoriesList, { navigate })
2339
+ ] });
2340
+ }
2341
+ function BlogEntityForm({
2342
+ entity,
2343
+ editId,
2344
+ parentLabel,
2345
+ onDone
2346
+ }) {
2347
+ const { getById, create, update, remove } = useEntityCrud(entity);
2348
+ const isEdit = !!editId;
2349
+ const namePlural = entity.namePlural ?? entity.name + "s";
2350
+ const [initialData, setInitialData] = React8.useState(isEdit ? null : {});
2351
+ React8.useEffect(() => {
2352
+ if (!editId) return;
2353
+ let cancelled = false;
2354
+ (async () => {
2355
+ const item = await getById(editId);
2356
+ if (!cancelled) setInitialData(item ?? {});
2357
+ })();
2358
+ return () => {
2359
+ cancelled = true;
2360
+ };
2361
+ }, [editId, getById]);
2362
+ async function save(values) {
2363
+ try {
2364
+ if (isEdit) await update(editId, values);
2365
+ else await create(values);
2366
+ toast.success(isEdit ? `${entity.name} updated` : `${entity.name} created`);
2367
+ onDone();
2368
+ } catch (err) {
2369
+ toast.error(`Failed to save ${entity.name.toLowerCase()}`);
2370
+ throw err;
2371
+ }
2372
+ }
2373
+ async function del() {
2374
+ if (!editId) return;
2375
+ try {
2376
+ await remove(editId);
2377
+ toast.success(`${entity.name} deleted`);
2378
+ onDone();
2379
+ } catch {
2380
+ toast.error(`Failed to delete ${entity.name.toLowerCase()}`);
2381
+ }
2382
+ }
2383
+ const title = isEdit ? initialData?.[entity.displayField ?? "name"] ?? entity.name : `New ${entity.name.toLowerCase()}`;
2384
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2385
+ /* @__PURE__ */ jsx(
2386
+ SubpageHeader,
2387
+ {
2388
+ title: String(title),
2389
+ onBack: onDone,
2390
+ parentLabel,
2391
+ actions: isEdit ? /* @__PURE__ */ jsx(Button, { variant: "ghost", className: "text-destructive hover:text-destructive", onClick: del, children: "Delete" }) : void 0
2392
+ }
2393
+ ),
2394
+ initialData !== null && /* @__PURE__ */ jsx(
2395
+ CrudFormPage,
2396
+ {
2397
+ entityDef: entity,
2398
+ mode: isEdit ? "edit" : "create",
2399
+ initialData,
2400
+ namePlural,
2401
+ onCancel: onDone,
2402
+ onSubmit: save,
2403
+ hideBreadcrumb: true
2404
+ }
2405
+ )
2406
+ ] });
2407
+ }
2175
2408
  function buildNav(config, view, navigate) {
2176
2409
  const items = [
2177
2410
  { id: "overview", label: config.labels.overview, icon: "BarChart3", active: view === "overview", onClick: () => navigate("overview") }
@@ -2203,9 +2436,18 @@ function buildNav(config, view, navigate) {
2203
2436
  onClick: () => navigate("content")
2204
2437
  });
2205
2438
  }
2439
+ if (config.modules.blog) {
2440
+ items.push({
2441
+ id: "blog",
2442
+ label: config.labels.blog,
2443
+ icon: "Newspaper",
2444
+ active: view === "blog" || view === "blog-new" || view.startsWith("blog-post:") || view === "blog-cat-new" || view.startsWith("blog-cat:"),
2445
+ onClick: () => navigate("blog")
2446
+ });
2447
+ }
2206
2448
  return items;
2207
2449
  }
2208
- function MarketingPage({ config, provider, store, contentProvider, contentStore }) {
2450
+ function MarketingPage({ config, provider: provider2, store, contentProvider, contentStore }) {
2209
2451
  const t = useTranslation();
2210
2452
  const { view, direction, navigate } = useModuleNavigation("/marketing", {
2211
2453
  overview: 0,
@@ -2214,15 +2456,22 @@ function MarketingPage({ config, provider, store, contentProvider, contentStore
2214
2456
  funnel: 0,
2215
2457
  "landing-pages": 0,
2216
2458
  content: 0,
2459
+ blog: 0,
2217
2460
  "channel-detail": 1,
2218
2461
  "content-post": 1,
2219
- "content-brief": 1
2462
+ "content-brief": 1,
2463
+ "blog-new": 1,
2464
+ "blog-post": 1,
2465
+ "blog-cat-new": 1,
2466
+ "blog-cat": 1
2220
2467
  }, "overview");
2221
2468
  React8.useEffect(() => {
2222
2469
  void store.getState().load();
2223
2470
  }, [store]);
2224
2471
  const isOverview = view === "overview";
2225
2472
  const nav = buildNav(config, view, navigate);
2473
+ const blogPostEntity = React8.useMemo(() => buildBlogPostEntity(), []);
2474
+ const blogCategoryEntity = React8.useMemo(() => buildBlogCategoryEntity(), []);
2226
2475
  const quickActions = React8.useMemo(() => [
2227
2476
  { id: "new-campaign", label: t("marketing.campaigns.new"), icon: "Megaphone", description: config.labels.campaigns, action: () => navigate("campaigns") }
2228
2477
  ], [t, config.labels.campaigns]);
@@ -2235,9 +2484,14 @@ function MarketingPage({ config, provider, store, contentProvider, contentStore
2235
2484
  { id: "content", render: () => /* @__PURE__ */ jsx(ContentView, { onOpenPost: (id) => navigate(`content-post:${id}`) }) },
2236
2485
  { id: "content-post", render: ({ id }) => /* @__PURE__ */ jsx(PostPage, { postId: id, onBack: () => navigate("content") }) },
2237
2486
  { id: "content-brief", render: () => /* @__PURE__ */ jsx(PlanBriefView, { onBack: () => navigate("content") }) },
2487
+ { id: "blog", render: () => /* @__PURE__ */ jsx(BlogAdminView, { navigate }) },
2488
+ { id: "blog-new", render: () => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogPostEntity, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
2489
+ { id: "blog-post", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogPostEntity, editId: id, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
2490
+ { id: "blog-cat-new", render: () => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
2491
+ { id: "blog-cat", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, editId: id, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
2238
2492
  { id: "overview", render: () => /* @__PURE__ */ jsx(OverviewView, {}) }
2239
2493
  ], "overview");
2240
- return /* @__PURE__ */ jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsx(
2494
+ return /* @__PURE__ */ jsx(MarketingContextProvider, { config, provider: provider2, store, children: /* @__PURE__ */ jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsx(
2241
2495
  ModulePage,
2242
2496
  {
2243
2497
  title: config.labels.pageTitle,
@@ -2512,7 +2766,7 @@ var AGENCY = {
2512
2766
  { id: "referral", label: "Referral", icon: "Users", kind: "referral" },
2513
2767
  { id: "direct", label: "Direct", icon: "MousePointerClick", kind: "direct" }
2514
2768
  ],
2515
- modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, broadcasts: false, journeys: false }
2769
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
2516
2770
  };
2517
2771
  var BEAUTY = {
2518
2772
  conversion: {
@@ -2530,7 +2784,7 @@ var BEAUTY = {
2530
2784
  { id: "whatsapp", label: "WhatsApp", icon: "MessageCircle", kind: "social" },
2531
2785
  { id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
2532
2786
  ],
2533
- modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, broadcasts: false, journeys: false }
2787
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, blog: false, broadcasts: false, journeys: false }
2534
2788
  };
2535
2789
  var RESTO = {
2536
2790
  conversion: {
@@ -2548,7 +2802,7 @@ var RESTO = {
2548
2802
  { id: "instagram", label: "Instagram", icon: "Instagram", kind: "social" },
2549
2803
  { id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
2550
2804
  ],
2551
- modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, broadcasts: false, journeys: false }
2805
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
2552
2806
  };
2553
2807
  var MARKETING_PRESETS = {
2554
2808
  agency: AGENCY,
@@ -3028,6 +3282,15 @@ function createMockContentPlannerProvider() {
3028
3282
  }
3029
3283
  };
3030
3284
  }
3285
+
3286
+ // src/data/tables.ts
3287
+ var T = {
3288
+ socialAccounts: "plg_marketing_social_accounts",
3289
+ contentPlans: "plg_marketing_content_plans",
3290
+ contentPosts: "plg_marketing_content_posts"
3291
+ };
3292
+
3293
+ // src/data/contentSupabase.ts
3031
3294
  function getClient() {
3032
3295
  const supabase = getSupabaseClientOptional();
3033
3296
  if (!supabase) throw new Error("Supabase not initialized");
@@ -3069,7 +3332,7 @@ function createSupabaseContentPlannerProvider() {
3069
3332
  async listAccounts() {
3070
3333
  const tenantId = getActiveTenantId();
3071
3334
  if (!tenantId) return [];
3072
- const { data } = await getClient().from("mkt_social_accounts").select("*").eq("tenant_id", tenantId).eq("is_active", true).order("created_at", { ascending: true });
3335
+ const { data } = await getClient().from(T.socialAccounts).select("*").eq("tenant_id", tenantId).eq("is_active", true).order("created_at", { ascending: true });
3073
3336
  return (data ?? []).map(toAccount);
3074
3337
  },
3075
3338
  async saveAccount(input) {
@@ -3079,11 +3342,11 @@ function createSupabaseContentPlannerProvider() {
3079
3342
  if (input.handle !== void 0) payload.handle = input.handle;
3080
3343
  if (input.platforms !== void 0) payload.platforms = input.platforms;
3081
3344
  if (input.isActive !== void 0) payload.is_active = input.isActive;
3082
- const { data: data2, error: error2 } = await getClient().from("mkt_social_accounts").update(payload).eq("id", input.id).select("*").single();
3345
+ const { data: data2, error: error2 } = await getClient().from(T.socialAccounts).update(payload).eq("id", input.id).select("*").single();
3083
3346
  if (error2) throw error2;
3084
3347
  return toAccount(data2);
3085
3348
  }
3086
- const { data, error } = await getClient().from("mkt_social_accounts").insert({
3349
+ const { data, error } = await getClient().from(T.socialAccounts).insert({
3087
3350
  tenant_id: requireTenantId(),
3088
3351
  name: input.name,
3089
3352
  handle: input.handle ?? null,
@@ -3094,17 +3357,17 @@ function createSupabaseContentPlannerProvider() {
3094
3357
  return toAccount(data);
3095
3358
  },
3096
3359
  async deleteAccount(id) {
3097
- const { error } = await getClient().from("mkt_social_accounts").delete().eq("id", id);
3360
+ const { error } = await getClient().from(T.socialAccounts).delete().eq("id", id);
3098
3361
  if (error) throw error;
3099
3362
  },
3100
3363
  async listPlans(accountId) {
3101
3364
  const tenantId = getActiveTenantId();
3102
3365
  if (!tenantId) return [];
3103
- const { data } = await getClient().from("mkt_content_plans").select("*").eq("tenant_id", tenantId).eq("account_id", accountId).order("created_at", { ascending: false });
3366
+ const { data } = await getClient().from(T.contentPlans).select("*").eq("tenant_id", tenantId).eq("account_id", accountId).order("created_at", { ascending: false });
3104
3367
  return (data ?? []).map(toPlan);
3105
3368
  },
3106
3369
  async getPlan(id) {
3107
- const { data } = await getClient().from("mkt_content_plans").select("*").eq("id", id).maybeSingle();
3370
+ const { data } = await getClient().from(T.contentPlans).select("*").eq("id", id).maybeSingle();
3108
3371
  return data ? toPlan(data) : null;
3109
3372
  },
3110
3373
  async savePlan(input) {
@@ -3120,12 +3383,12 @@ function createSupabaseContentPlannerProvider() {
3120
3383
  if (input.formats !== void 0) payload.formats = input.formats;
3121
3384
  if (input.weeklyFrequency !== void 0) payload.weekly_frequency = input.weeklyFrequency;
3122
3385
  if (input.briefMd !== void 0) payload.brief_md = input.briefMd;
3123
- const { data: data2, error: error2 } = await getClient().from("mkt_content_plans").update(payload).eq("id", input.id).select("*").single();
3386
+ const { data: data2, error: error2 } = await getClient().from(T.contentPlans).update(payload).eq("id", input.id).select("*").single();
3124
3387
  if (error2) throw error2;
3125
3388
  return toPlan(data2);
3126
3389
  }
3127
3390
  if (!input.accountId) throw new Error("accountId is required");
3128
- const { data, error } = await getClient().from("mkt_content_plans").insert({
3391
+ const { data, error } = await getClient().from(T.contentPlans).insert({
3129
3392
  tenant_id: requireTenantId(),
3130
3393
  account_id: input.accountId,
3131
3394
  name: input.name ?? "Novo plano",
@@ -3143,14 +3406,14 @@ function createSupabaseContentPlannerProvider() {
3143
3406
  return toPlan(data);
3144
3407
  },
3145
3408
  async deletePlan(id) {
3146
- await getClient().from("mkt_content_plans").delete().eq("id", id);
3409
+ await getClient().from(T.contentPlans).delete().eq("id", id);
3147
3410
  },
3148
3411
  async listPosts(planId) {
3149
- const { data } = await getClient().from("mkt_content_posts").select("*").eq("plan_id", planId).order("week_number", { ascending: true }).order("position", { ascending: true });
3412
+ const { data } = await getClient().from(T.contentPosts).select("*").eq("plan_id", planId).order("week_number", { ascending: true }).order("position", { ascending: true });
3150
3413
  return (data ?? []).map(toPost);
3151
3414
  },
3152
3415
  async getPost(id) {
3153
- const { data } = await getClient().from("mkt_content_posts").select("*").eq("id", id).maybeSingle();
3416
+ const { data } = await getClient().from(T.contentPosts).select("*").eq("id", id).maybeSingle();
3154
3417
  return data ? toPost(data) : null;
3155
3418
  },
3156
3419
  async savePost(input) {
@@ -3168,15 +3431,15 @@ function createSupabaseContentPlannerProvider() {
3168
3431
  if (input.hook !== void 0) payload.hook = input.hook;
3169
3432
  if (input.cta !== void 0) payload.cta = input.cta;
3170
3433
  if (input.contentMd !== void 0) payload.content_md = input.contentMd;
3171
- const { data: data2, error: error2 } = await getClient().from("mkt_content_posts").update(payload).eq("id", input.id).select("*").single();
3434
+ const { data: data2, error: error2 } = await getClient().from(T.contentPosts).update(payload).eq("id", input.id).select("*").single();
3172
3435
  if (error2) throw error2;
3173
3436
  return toPost(data2);
3174
3437
  }
3175
3438
  if (!input.planId) throw new Error("planId is required");
3176
3439
  const week = input.weekNumber ?? 1;
3177
- const { data: maxData } = await getClient().from("mkt_content_posts").select("position").eq("plan_id", input.planId).eq("week_number", week).order("position", { ascending: false }).limit(1);
3440
+ const { data: maxData } = await getClient().from(T.contentPosts).select("position").eq("plan_id", input.planId).eq("week_number", week).order("position", { ascending: false }).limit(1);
3178
3441
  const maxPos = maxData?.[0]?.position ?? -1;
3179
- const { data, error } = await getClient().from("mkt_content_posts").insert({
3442
+ const { data, error } = await getClient().from(T.contentPosts).insert({
3180
3443
  tenant_id: requireTenantId(),
3181
3444
  plan_id: input.planId,
3182
3445
  week_number: week,
@@ -3196,11 +3459,11 @@ function createSupabaseContentPlannerProvider() {
3196
3459
  return toPost(data);
3197
3460
  },
3198
3461
  async deletePost(id) {
3199
- await getClient().from("mkt_content_posts").delete().eq("id", id);
3462
+ await getClient().from(T.contentPosts).delete().eq("id", id);
3200
3463
  }
3201
3464
  };
3202
3465
  }
3203
- function createMarketingStore(provider) {
3466
+ function createMarketingStore(provider2) {
3204
3467
  return createStore((set, get) => ({
3205
3468
  range: "30d",
3206
3469
  overview: null,
@@ -3213,11 +3476,11 @@ function createMarketingStore(provider) {
3213
3476
  const range = get().range;
3214
3477
  set({ loading: true });
3215
3478
  const [overview, channels, campaigns, funnel, landingPages] = await Promise.all([
3216
- provider.overview({ range }),
3217
- provider.channelPerformance({ range }),
3218
- provider.listCampaigns({ range }),
3219
- provider.funnel({ range }),
3220
- provider.landingPages({ range })
3479
+ provider2.overview({ range }),
3480
+ provider2.channelPerformance({ range }),
3481
+ provider2.listCampaigns({ range }),
3482
+ provider2.funnel({ range }),
3483
+ provider2.landingPages({ range })
3221
3484
  ]);
3222
3485
  set({ overview, channels, campaigns, funnel, landingPages, loading: false });
3223
3486
  },
@@ -3226,21 +3489,21 @@ function createMarketingStore(provider) {
3226
3489
  await get().load();
3227
3490
  },
3228
3491
  async saveCampaign(input) {
3229
- const created = await provider.saveCampaign(input);
3492
+ const created = await provider2.saveCampaign(input);
3230
3493
  await get().load();
3231
3494
  return created;
3232
3495
  },
3233
3496
  async deleteCampaign(id) {
3234
- await provider.deleteCampaign(id);
3497
+ await provider2.deleteCampaign(id);
3235
3498
  await get().load();
3236
3499
  }
3237
3500
  }));
3238
3501
  }
3239
- function createContentPlannerStore(provider) {
3502
+ function createContentPlannerStore(provider2) {
3240
3503
  async function loadPlansFor(accountId) {
3241
- const plans = await provider.listPlans(accountId);
3504
+ const plans = await provider2.listPlans(accountId);
3242
3505
  const activePlan = plans.find((p) => p.status === "active") ?? plans[0] ?? null;
3243
- const posts = activePlan ? await provider.listPosts(activePlan.id) : [];
3506
+ const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
3244
3507
  return { plans, activePlan, posts };
3245
3508
  }
3246
3509
  return createStore((set, get) => ({
@@ -3260,7 +3523,7 @@ function createContentPlannerStore(provider) {
3260
3523
  if (get().loading) return;
3261
3524
  set({ loading: true });
3262
3525
  try {
3263
- const accounts = await provider.listAccounts();
3526
+ const accounts = await provider2.listAccounts();
3264
3527
  const current = get().activeAccountId;
3265
3528
  const activeAccountId = (current && accounts.some((a) => a.id === current) ? current : accounts[0]?.id) ?? null;
3266
3529
  const { plans, activePlan, posts } = activeAccountId ? await loadPlansFor(activeAccountId) : { plans: [], activePlan: null, posts: [] };
@@ -3291,19 +3554,19 @@ function createContentPlannerStore(provider) {
3291
3554
  },
3292
3555
  async selectPlan(planId) {
3293
3556
  const plan = get().plans.find((p) => p.id === planId) ?? null;
3294
- const posts = plan ? await provider.listPosts(plan.id) : [];
3557
+ const posts = plan ? await provider2.listPosts(plan.id) : [];
3295
3558
  set({ activePlan: plan, posts });
3296
3559
  },
3297
3560
  async saveAccount(input) {
3298
- const account = await provider.saveAccount(input);
3299
- const accounts = await provider.listAccounts();
3561
+ const account = await provider2.saveAccount(input);
3562
+ const accounts = await provider2.listAccounts();
3300
3563
  set({ accounts });
3301
3564
  if (!get().activeAccountId) await get().setAccount(account.id);
3302
3565
  return account;
3303
3566
  },
3304
3567
  async deleteAccount(id) {
3305
- await provider.deleteAccount(id);
3306
- const accounts = await provider.listAccounts();
3568
+ await provider2.deleteAccount(id);
3569
+ const accounts = await provider2.listAccounts();
3307
3570
  const wasActive = get().activeAccountId === id;
3308
3571
  set({ accounts });
3309
3572
  if (wasActive) {
@@ -3316,30 +3579,30 @@ function createContentPlannerStore(provider) {
3316
3579
  }
3317
3580
  },
3318
3581
  async savePlan(input) {
3319
- const plan = await provider.savePlan(input);
3582
+ const plan = await provider2.savePlan(input);
3320
3583
  const accountId = get().activeAccountId;
3321
3584
  if (accountId) {
3322
- const plans = await provider.listPlans(accountId);
3585
+ const plans = await provider2.listPlans(accountId);
3323
3586
  const activePlan = plans.find((p) => p.id === plan.id) ?? get().activePlan;
3324
- const posts = activePlan ? await provider.listPosts(activePlan.id) : [];
3587
+ const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
3325
3588
  set({ plans, activePlan, posts });
3326
3589
  }
3327
3590
  return plan;
3328
3591
  },
3329
3592
  async savePost(input) {
3330
- const post = await provider.savePost(input);
3593
+ const post = await provider2.savePost(input);
3331
3594
  const planId = get().activePlan?.id;
3332
3595
  if (planId && post.planId === planId) {
3333
- const posts = await provider.listPosts(planId);
3596
+ const posts = await provider2.listPosts(planId);
3334
3597
  set({ posts });
3335
3598
  }
3336
3599
  return post;
3337
3600
  },
3338
3601
  async deletePost(id) {
3339
- await provider.deletePost(id);
3602
+ await provider2.deletePost(id);
3340
3603
  const planId = get().activePlan?.id;
3341
3604
  if (planId) {
3342
- const posts = await provider.listPosts(planId);
3605
+ const posts = await provider2.listPosts(planId);
3343
3606
  set({ posts });
3344
3607
  }
3345
3608
  }
@@ -3347,13 +3610,29 @@ function createContentPlannerStore(provider) {
3347
3610
  }
3348
3611
 
3349
3612
  // src/migrations/index.ts
3613
+ var MIGRATION_000_PLG_RENAME = `-- 000_plg_rename.sql \u2014 rename legacy marketing tables to plg_marketing_* for pools
3614
+ -- provisioned before the industry-pool rename. Guarded: fires only when legacy
3615
+ -- name exists and target does not, so fresh pools skip every branch.
3616
+ DO $$
3617
+ BEGIN
3618
+ IF to_regclass('public.mkt_social_accounts') IS NOT NULL AND to_regclass('public.plg_marketing_social_accounts') IS NULL THEN
3619
+ ALTER TABLE public.mkt_social_accounts RENAME TO plg_marketing_social_accounts;
3620
+ END IF;
3621
+ IF to_regclass('public.mkt_content_plans') IS NOT NULL AND to_regclass('public.plg_marketing_content_plans') IS NULL THEN
3622
+ ALTER TABLE public.mkt_content_plans RENAME TO plg_marketing_content_plans;
3623
+ END IF;
3624
+ IF to_regclass('public.mkt_content_posts') IS NOT NULL AND to_regclass('public.plg_marketing_content_posts') IS NULL THEN
3625
+ ALTER TABLE public.mkt_content_posts RENAME TO plg_marketing_content_posts;
3626
+ END IF;
3627
+ END $$;
3628
+ `;
3350
3629
  var MIGRATION_001_CONTENT_PLANNER = `-- Marketing Plugin: Content Planner
3351
- -- Prefix: mkt_
3630
+ -- Prefix: plg_marketing_
3352
3631
  -- Social accounts + content plans + posts (Notion-style markdown pages).
3353
3632
 
3354
- CREATE TABLE IF NOT EXISTS public.mkt_social_accounts (
3633
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_social_accounts (
3355
3634
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3356
- tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
3635
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3357
3636
  name text NOT NULL,
3358
3637
  handle text,
3359
3638
  platform text NOT NULL DEFAULT 'instagram'
@@ -3361,13 +3640,13 @@ CREATE TABLE IF NOT EXISTS public.mkt_social_accounts (
3361
3640
  is_active boolean NOT NULL DEFAULT true,
3362
3641
  created_at timestamptz NOT NULL DEFAULT now()
3363
3642
  );
3364
- ALTER TABLE public.mkt_social_accounts ENABLE ROW LEVEL SECURITY;
3365
- CREATE INDEX IF NOT EXISTS idx_mkt_social_accounts_tenant ON public.mkt_social_accounts(tenant_id);
3643
+ ALTER TABLE public.plg_marketing_social_accounts ENABLE ROW LEVEL SECURITY;
3644
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_social_accounts_tenant ON public.plg_marketing_social_accounts(tenant_id);
3366
3645
 
3367
- CREATE TABLE IF NOT EXISTS public.mkt_content_plans (
3646
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_content_plans (
3368
3647
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3369
- tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
3370
- account_id uuid NOT NULL REFERENCES public.mkt_social_accounts(id) ON DELETE CASCADE,
3648
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3649
+ account_id uuid NOT NULL REFERENCES public.plg_marketing_social_accounts(id) ON DELETE CASCADE,
3371
3650
  name text NOT NULL,
3372
3651
  status text NOT NULL DEFAULT 'active'
3373
3652
  CHECK (status IN ('draft', 'active', 'archived')),
@@ -3382,14 +3661,14 @@ CREATE TABLE IF NOT EXISTS public.mkt_content_plans (
3382
3661
  created_at timestamptz NOT NULL DEFAULT now(),
3383
3662
  updated_at timestamptz NOT NULL DEFAULT now()
3384
3663
  );
3385
- ALTER TABLE public.mkt_content_plans ENABLE ROW LEVEL SECURITY;
3386
- CREATE INDEX IF NOT EXISTS idx_mkt_content_plans_tenant ON public.mkt_content_plans(tenant_id);
3387
- CREATE INDEX IF NOT EXISTS idx_mkt_content_plans_account ON public.mkt_content_plans(account_id);
3664
+ ALTER TABLE public.plg_marketing_content_plans ENABLE ROW LEVEL SECURITY;
3665
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_tenant ON public.plg_marketing_content_plans(tenant_id);
3666
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_account ON public.plg_marketing_content_plans(account_id);
3388
3667
 
3389
- CREATE TABLE IF NOT EXISTS public.mkt_content_posts (
3668
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_content_posts (
3390
3669
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3391
- tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
3392
- plan_id uuid NOT NULL REFERENCES public.mkt_content_plans(id) ON DELETE CASCADE,
3670
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3671
+ plan_id uuid NOT NULL REFERENCES public.plg_marketing_content_plans(id) ON DELETE CASCADE,
3393
3672
  week_number integer NOT NULL DEFAULT 1,
3394
3673
  position integer NOT NULL DEFAULT 0,
3395
3674
  title text NOT NULL,
@@ -3404,49 +3683,50 @@ CREATE TABLE IF NOT EXISTS public.mkt_content_posts (
3404
3683
  created_at timestamptz NOT NULL DEFAULT now(),
3405
3684
  updated_at timestamptz NOT NULL DEFAULT now()
3406
3685
  );
3407
- ALTER TABLE public.mkt_content_posts ENABLE ROW LEVEL SECURITY;
3408
- CREATE INDEX IF NOT EXISTS idx_mkt_content_posts_tenant ON public.mkt_content_posts(tenant_id);
3409
- CREATE INDEX IF NOT EXISTS idx_mkt_content_posts_plan ON public.mkt_content_posts(plan_id, week_number, position);
3410
- CREATE INDEX IF NOT EXISTS idx_mkt_content_posts_status ON public.mkt_content_posts(tenant_id, status);
3411
-
3412
- -- RLS policies for mkt_social_accounts
3413
- DROP POLICY IF EXISTS mkt_social_accounts_select ON public.mkt_social_accounts;
3414
- DROP POLICY IF EXISTS mkt_social_accounts_insert ON public.mkt_social_accounts;
3415
- DROP POLICY IF EXISTS mkt_social_accounts_update ON public.mkt_social_accounts;
3416
- DROP POLICY IF EXISTS mkt_social_accounts_delete ON public.mkt_social_accounts;
3417
- CREATE POLICY mkt_social_accounts_select ON public.mkt_social_accounts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3418
- CREATE POLICY mkt_social_accounts_insert ON public.mkt_social_accounts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3419
- CREATE POLICY mkt_social_accounts_update ON public.mkt_social_accounts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3420
- CREATE POLICY mkt_social_accounts_delete ON public.mkt_social_accounts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3421
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.mkt_social_accounts TO authenticated;
3422
-
3423
- -- RLS policies for mkt_content_plans
3424
- DROP POLICY IF EXISTS mkt_content_plans_select ON public.mkt_content_plans;
3425
- DROP POLICY IF EXISTS mkt_content_plans_insert ON public.mkt_content_plans;
3426
- DROP POLICY IF EXISTS mkt_content_plans_update ON public.mkt_content_plans;
3427
- DROP POLICY IF EXISTS mkt_content_plans_delete ON public.mkt_content_plans;
3428
- CREATE POLICY mkt_content_plans_select ON public.mkt_content_plans FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3429
- CREATE POLICY mkt_content_plans_insert ON public.mkt_content_plans FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3430
- CREATE POLICY mkt_content_plans_update ON public.mkt_content_plans FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3431
- CREATE POLICY mkt_content_plans_delete ON public.mkt_content_plans FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3432
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.mkt_content_plans TO authenticated;
3433
-
3434
- -- RLS policies for mkt_content_posts
3435
- DROP POLICY IF EXISTS mkt_content_posts_select ON public.mkt_content_posts;
3436
- DROP POLICY IF EXISTS mkt_content_posts_insert ON public.mkt_content_posts;
3437
- DROP POLICY IF EXISTS mkt_content_posts_update ON public.mkt_content_posts;
3438
- DROP POLICY IF EXISTS mkt_content_posts_delete ON public.mkt_content_posts;
3439
- CREATE POLICY mkt_content_posts_select ON public.mkt_content_posts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3440
- CREATE POLICY mkt_content_posts_insert ON public.mkt_content_posts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3441
- CREATE POLICY mkt_content_posts_update ON public.mkt_content_posts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3442
- CREATE POLICY mkt_content_posts_delete ON public.mkt_content_posts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3443
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.mkt_content_posts TO authenticated;`;
3686
+ ALTER TABLE public.plg_marketing_content_posts ENABLE ROW LEVEL SECURITY;
3687
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_tenant ON public.plg_marketing_content_posts(tenant_id);
3688
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_plan ON public.plg_marketing_content_posts(plan_id, week_number, position);
3689
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_status ON public.plg_marketing_content_posts(tenant_id, status);
3690
+
3691
+ -- RLS policies for plg_marketing_social_accounts
3692
+ DROP POLICY IF EXISTS plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts;
3693
+ DROP POLICY IF EXISTS plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts;
3694
+ DROP POLICY IF EXISTS plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts;
3695
+ DROP POLICY IF EXISTS plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts;
3696
+ CREATE POLICY plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3697
+ CREATE POLICY plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3698
+ CREATE POLICY plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3699
+ CREATE POLICY plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3700
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_social_accounts TO authenticated;
3701
+
3702
+ -- RLS policies for plg_marketing_content_plans
3703
+ DROP POLICY IF EXISTS plg_marketing_content_plans_select ON public.plg_marketing_content_plans;
3704
+ DROP POLICY IF EXISTS plg_marketing_content_plans_insert ON public.plg_marketing_content_plans;
3705
+ DROP POLICY IF EXISTS plg_marketing_content_plans_update ON public.plg_marketing_content_plans;
3706
+ DROP POLICY IF EXISTS plg_marketing_content_plans_delete ON public.plg_marketing_content_plans;
3707
+ CREATE POLICY plg_marketing_content_plans_select ON public.plg_marketing_content_plans FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3708
+ CREATE POLICY plg_marketing_content_plans_insert ON public.plg_marketing_content_plans FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3709
+ CREATE POLICY plg_marketing_content_plans_update ON public.plg_marketing_content_plans FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3710
+ CREATE POLICY plg_marketing_content_plans_delete ON public.plg_marketing_content_plans FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3711
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_plans TO authenticated;
3712
+
3713
+ -- RLS policies for plg_marketing_content_posts
3714
+ DROP POLICY IF EXISTS plg_marketing_content_posts_select ON public.plg_marketing_content_posts;
3715
+ DROP POLICY IF EXISTS plg_marketing_content_posts_insert ON public.plg_marketing_content_posts;
3716
+ DROP POLICY IF EXISTS plg_marketing_content_posts_update ON public.plg_marketing_content_posts;
3717
+ DROP POLICY IF EXISTS plg_marketing_content_posts_delete ON public.plg_marketing_content_posts;
3718
+ CREATE POLICY plg_marketing_content_posts_select ON public.plg_marketing_content_posts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3719
+ CREATE POLICY plg_marketing_content_posts_insert ON public.plg_marketing_content_posts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3720
+ CREATE POLICY plg_marketing_content_posts_update ON public.plg_marketing_content_posts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3721
+ CREATE POLICY plg_marketing_content_posts_delete ON public.plg_marketing_content_posts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3722
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_posts TO authenticated;
3723
+ `;
3444
3724
  var MIGRATION_002_MULTI_PLATFORM = `-- Marketing Plugin: multi-platform accounts + per-post targets.
3445
3725
  -- An account is a brand/profile that publishes to MANY platforms (Instagram,
3446
3726
  -- YouTube, TikTok, ...). Posts optionally narrow the target platforms; an
3447
3727
  -- empty array means "inherit the account's platforms".
3448
3728
 
3449
- ALTER TABLE public.mkt_social_accounts
3729
+ ALTER TABLE public.plg_marketing_social_accounts
3450
3730
  ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{instagram}';
3451
3731
 
3452
3732
  -- Backfill from the legacy single-platform column, then drop it (also drops
@@ -3455,26 +3735,27 @@ DO $$
3455
3735
  BEGIN
3456
3736
  IF EXISTS (
3457
3737
  SELECT 1 FROM information_schema.columns
3458
- WHERE table_schema = 'public' AND table_name = 'mkt_social_accounts' AND column_name = 'platform'
3738
+ WHERE table_schema = 'public' AND table_name = 'plg_marketing_social_accounts' AND column_name = 'platform'
3459
3739
  ) THEN
3460
- UPDATE public.mkt_social_accounts
3740
+ UPDATE public.plg_marketing_social_accounts
3461
3741
  SET platforms = ARRAY[platform]
3462
3742
  WHERE platform IS NOT NULL AND platforms = '{instagram}';
3463
- ALTER TABLE public.mkt_social_accounts DROP COLUMN platform;
3743
+ ALTER TABLE public.plg_marketing_social_accounts DROP COLUMN platform;
3464
3744
  END IF;
3465
3745
  END $$;
3466
3746
 
3467
- ALTER TABLE public.mkt_content_posts
3468
- ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';`;
3747
+ ALTER TABLE public.plg_marketing_content_posts
3748
+ ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';
3749
+ `;
3469
3750
  var MIGRATION_003_RECORDING_OPS = `-- Marketing Plugin: recording-day ops on posts.
3470
3751
  -- checklist: shooting checklist items [{ id, text, done }] \u2014 the on-set
3471
3752
  -- companion for recording day. media_url: uploaded asset (static posts get a
3472
3753
  -- caption + final art instead of a script).
3473
3754
 
3474
- ALTER TABLE public.mkt_content_posts
3755
+ ALTER TABLE public.plg_marketing_content_posts
3475
3756
  ADD COLUMN IF NOT EXISTS checklist jsonb NOT NULL DEFAULT '[]';
3476
3757
 
3477
- ALTER TABLE public.mkt_content_posts
3758
+ ALTER TABLE public.plg_marketing_content_posts
3478
3759
  ADD COLUMN IF NOT EXISTS media_url text;
3479
3760
 
3480
3761
  -- Public media bucket for content assets (mirrors the avatars-bucket pattern:
@@ -3483,14 +3764,213 @@ INSERT INTO storage.buckets (id, name, public)
3483
3764
  VALUES ('mkt-media', 'mkt-media', true)
3484
3765
  ON CONFLICT (id) DO NOTHING;
3485
3766
 
3486
- DROP POLICY IF EXISTS mkt_media_read ON storage.objects;
3487
- DROP POLICY IF EXISTS mkt_media_insert ON storage.objects;
3488
- DROP POLICY IF EXISTS mkt_media_update ON storage.objects;
3489
- DROP POLICY IF EXISTS mkt_media_delete ON storage.objects;
3490
- CREATE POLICY mkt_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
3491
- CREATE POLICY mkt_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
3492
- CREATE POLICY mkt_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
3493
- CREATE POLICY mkt_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');`;
3767
+ DROP POLICY IF EXISTS plg_marketing_media_read ON storage.objects;
3768
+ DROP POLICY IF EXISTS plg_marketing_media_insert ON storage.objects;
3769
+ DROP POLICY IF EXISTS plg_marketing_media_update ON storage.objects;
3770
+ DROP POLICY IF EXISTS plg_marketing_media_delete ON storage.objects;
3771
+ CREATE POLICY plg_marketing_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
3772
+ CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
3773
+ CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
3774
+ CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
3775
+ `;
3776
+ var MIGRATION_004_RANKLAYER = `-- Marketing Plugin: RankLayer integration (SEO content publishing).
3777
+ -- Control-plane state for the RankLayer connector rendered in Marketing \u2192
3778
+ -- Integra\xE7\xF5es. Prefix: plg_marketing_ranklayer_. Scaffold: the real sync
3779
+ -- (edge function + RankLayer API calls) lands via an external PR \u2014 see
3780
+ -- src/integrations/ranklayer/RANKLAYER.md.
3781
+
3782
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_integrations (
3783
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3784
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3785
+ api_key text,
3786
+ site_domain text,
3787
+ active boolean NOT NULL DEFAULT true,
3788
+ last_sync_at timestamptz,
3789
+ created_at timestamptz NOT NULL DEFAULT now(),
3790
+ updated_at timestamptz NOT NULL DEFAULT now()
3791
+ );
3792
+ ALTER TABLE public.plg_marketing_ranklayer_integrations ENABLE ROW LEVEL SECURITY;
3793
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_marketing_ranklayer_integrations_tenant
3794
+ ON public.plg_marketing_ranklayer_integrations(tenant_id);
3795
+
3796
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_sync_log (
3797
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3798
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3799
+ integration_id uuid REFERENCES public.plg_marketing_ranklayer_integrations(id) ON DELETE CASCADE,
3800
+ status text NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'partial', 'error')),
3801
+ message text,
3802
+ fetched integer NOT NULL DEFAULT 0,
3803
+ written integer NOT NULL DEFAULT 0,
3804
+ created_at timestamptz NOT NULL DEFAULT now()
3805
+ );
3806
+ ALTER TABLE public.plg_marketing_ranklayer_sync_log ENABLE ROW LEVEL SECURITY;
3807
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_ranklayer_sync_log_tenant
3808
+ ON public.plg_marketing_ranklayer_sync_log(tenant_id, created_at DESC);
3809
+
3810
+ -- RLS: authenticated tenant CRUD
3811
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations;
3812
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations;
3813
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations;
3814
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations;
3815
+ CREATE POLICY plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3816
+ CREATE POLICY plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3817
+ CREATE POLICY plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3818
+ CREATE POLICY plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3819
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_ranklayer_integrations TO authenticated;
3820
+
3821
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log;
3822
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log;
3823
+ CREATE POLICY plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3824
+ CREATE POLICY plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3825
+ GRANT SELECT, INSERT ON public.plg_marketing_ranklayer_sync_log TO authenticated;
3826
+ `;
3827
+ var INTEGRATIONS = "plg_marketing_ranklayer_integrations";
3828
+ var SYNC_LOG = "plg_marketing_ranklayer_sync_log";
3829
+ function getClient2() {
3830
+ const supabase = getSupabaseClientOptional();
3831
+ if (!supabase) throw new Error("[ranklayer] Supabase not initialized");
3832
+ return supabase;
3833
+ }
3834
+ function requireTenantId2() {
3835
+ const tenantId = getActiveTenantId();
3836
+ if (!tenantId) throw new Error("[ranklayer] No active tenant");
3837
+ return tenantId;
3838
+ }
3839
+ function toIntegration(row) {
3840
+ return {
3841
+ id: row.id,
3842
+ apiKey: row.api_key ?? void 0,
3843
+ siteDomain: row.site_domain ?? void 0,
3844
+ active: row.active ?? true,
3845
+ lastSyncAt: row.last_sync_at ?? void 0
3846
+ };
3847
+ }
3848
+ function createRankLayerProvider() {
3849
+ return {
3850
+ async getIntegration() {
3851
+ const tenantId = getActiveTenantId();
3852
+ if (!tenantId) return null;
3853
+ const { data } = await getClient2().from(INTEGRATIONS).select("*").eq("tenant_id", tenantId).maybeSingle();
3854
+ return data ? toIntegration(data) : null;
3855
+ },
3856
+ async saveIntegration(values) {
3857
+ const tenantId = requireTenantId2();
3858
+ const payload = {
3859
+ tenant_id: tenantId,
3860
+ api_key: values.apiKey,
3861
+ site_domain: values.siteDomain ?? null,
3862
+ active: true,
3863
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
3864
+ };
3865
+ const { data, error } = await getClient2().from(INTEGRATIONS).upsert(payload, { onConflict: "tenant_id" }).select("*").single();
3866
+ if (error) throw error;
3867
+ return toIntegration(data);
3868
+ },
3869
+ async disconnect() {
3870
+ const tenantId = getActiveTenantId();
3871
+ if (!tenantId) return;
3872
+ await getClient2().from(INTEGRATIONS).delete().eq("tenant_id", tenantId);
3873
+ },
3874
+ // SCAFFOLD: no RankLayer API yet — accept any non-empty key so the connect
3875
+ // flow can be exercised. Replace with a real credential check in the PR.
3876
+ async testConnection(values) {
3877
+ if (!values.apiKey || values.apiKey.trim().length < 8) {
3878
+ return { ok: false, message: "Informe uma API Key v\xE1lida da RankLayer." };
3879
+ }
3880
+ return { ok: true, message: "Chave aceita (valida\xE7\xE3o real pendente \u2014 integra\xE7\xE3o em prepara\xE7\xE3o)." };
3881
+ },
3882
+ async getSyncLog(integrationId) {
3883
+ const { data } = await getClient2().from(SYNC_LOG).select("*").eq("integration_id", integrationId).order("created_at", { ascending: false }).limit(20);
3884
+ return (data ?? []).map((r) => ({
3885
+ id: r.id,
3886
+ createdAt: r.created_at,
3887
+ status: r.status,
3888
+ message: r.message ?? void 0,
3889
+ fetched: r.fetched ?? 0,
3890
+ written: r.written ?? 0
3891
+ }));
3892
+ }
3893
+ };
3894
+ }
3895
+ var provider = createRankLayerProvider();
3896
+ function RankLayerExtraPanel() {
3897
+ const [integration, setIntegration] = useState(null);
3898
+ const [log, setLog] = useState([]);
3899
+ const [loaded, setLoaded] = useState(false);
3900
+ useEffect(() => {
3901
+ let cancelled = false;
3902
+ (async () => {
3903
+ try {
3904
+ const found = await provider.getIntegration();
3905
+ if (cancelled) return;
3906
+ setIntegration(found);
3907
+ if (found) setLog(await provider.getSyncLog(found.id));
3908
+ } catch {
3909
+ } finally {
3910
+ if (!cancelled) setLoaded(true);
3911
+ }
3912
+ })();
3913
+ return () => {
3914
+ cancelled = true;
3915
+ };
3916
+ }, []);
3917
+ if (!loaded || !integration) return null;
3918
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 border-t pt-4", children: [
3919
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 rounded-md border border-dashed p-3", children: [
3920
+ /* @__PURE__ */ jsx(Sparkles, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }),
3921
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
3922
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Sincroniza\xE7\xE3o de conte\xFAdo \u2014 em breve" }),
3923
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
3924
+ "A publica\xE7\xE3o autom\xE1tica de p\xE1ginas e artigos de SEO com a RankLayer est\xE1 em prepara\xE7\xE3o.",
3925
+ integration.siteDomain ? ` Dom\xEDnio configurado: ${integration.siteDomain}.` : "",
3926
+ " Assim que a integra\xE7\xE3o estiver ativa, o hist\xF3rico de sincroniza\xE7\xF5es aparece aqui."
3927
+ ] })
3928
+ ] })
3929
+ ] }),
3930
+ log.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
3931
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3932
+ /* @__PURE__ */ jsx(History, { className: "h-4 w-4 text-muted-foreground" }),
3933
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Hist\xF3rico" })
3934
+ ] }),
3935
+ /* @__PURE__ */ jsx("div", { className: "divide-y rounded-md border text-sm", children: log.map((r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [
3936
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: new Date(r.createdAt).toLocaleString("pt-BR") }),
3937
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto text-xs", children: [
3938
+ r.written,
3939
+ "/",
3940
+ r.fetched,
3941
+ " publicadas"
3942
+ ] }),
3943
+ r.status === "success" ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsx(AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
3944
+ ] }, r.id)) })
3945
+ ] })
3946
+ ] });
3947
+ }
3948
+ var ranklayerConnectorDef = {
3949
+ id: "ranklayer",
3950
+ hostPluginId: "marketing",
3951
+ name: "RankLayer",
3952
+ description: "Publica\xE7\xE3o de conte\xFAdo e p\xE1ginas de SEO por servi\xE7o/bairro para ser encontrado no Google e em assistentes de IA.",
3953
+ icon: "Search",
3954
+ authKind: "api-key",
3955
+ fields: [
3956
+ { key: "apiKey", label: "API Key", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" },
3957
+ { key: "siteDomain", label: "Dom\xEDnio do site", type: "text", placeholder: "exemplo.com.br" }
3958
+ ],
3959
+ async getStatus() {
3960
+ try {
3961
+ const found = await provider.getIntegration();
3962
+ return { connected: !!found?.apiKey, detail: found?.siteDomain };
3963
+ } catch {
3964
+ return { connected: false };
3965
+ }
3966
+ },
3967
+ testConnection: (values) => provider.testConnection({ apiKey: values.apiKey }),
3968
+ saveConnection: async (values) => {
3969
+ await provider.saveIntegration({ apiKey: values.apiKey, siteDomain: values.siteDomain });
3970
+ },
3971
+ disconnect: () => provider.disconnect(),
3972
+ ExtraPanel: RankLayerExtraPanel
3973
+ };
3494
3974
 
3495
3975
  // src/locales/en.ts
3496
3976
  var en = {
@@ -3850,6 +4330,8 @@ var DEFAULT_LABELS = {
3850
4330
  funnel: "Funnel",
3851
4331
  landingPages: "Landing pages",
3852
4332
  content: "Content",
4333
+ blog: "Blog",
4334
+ blogCategories: "Categorias",
3853
4335
  settings: "Settings"
3854
4336
  };
3855
4337
  function resolveConfig(options) {
@@ -3871,9 +4353,9 @@ function createSafeProvider(config, domain) {
3871
4353
  function createMarketingPlugin(options) {
3872
4354
  registerTranslations(marketingLocales);
3873
4355
  const { config, domain } = resolveConfig(options);
3874
- const provider = options?.dataProvider ?? createSafeProvider(config, domain);
3875
- const store = createMarketingStore(provider);
3876
- const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store });
4356
+ const provider2 = options?.dataProvider ?? createSafeProvider(config, domain);
4357
+ const store = createMarketingStore(provider2);
4358
+ const dashboardWidgets = createMarketingDashboardWidgets({ config, provider: provider2, store });
3877
4359
  const contentProvider = options?.contentPlanner?.dataProvider ?? createSafeDataProvider(
3878
4360
  () => createSupabaseContentPlannerProvider(),
3879
4361
  () => createMockContentPlannerProvider()
@@ -3881,8 +4363,8 @@ function createMarketingPlugin(options) {
3881
4363
  const contentStore = createContentPlannerStore(contentProvider);
3882
4364
  const PageComponent = () => React8.createElement(
3883
4365
  MarketingContextProvider,
3884
- { config, provider, store },
3885
- React8.createElement(MarketingPage, { config, provider, store, contentProvider, contentStore })
4366
+ { config, provider: provider2, store },
4367
+ React8.createElement(MarketingPage, { config, provider: provider2, store, contentProvider, contentStore })
3886
4368
  );
3887
4369
  PageComponent.displayName = "MarketingPage";
3888
4370
  const marketingRegistries = [
@@ -3891,12 +4373,14 @@ function createMarketingPlugin(options) {
3891
4373
  ];
3892
4374
  const SettingsComponent = () => React8.createElement(
3893
4375
  MarketingContextProvider,
3894
- { config, provider, store },
4376
+ { config, provider: provider2, store },
3895
4377
  React8.createElement(PluginSettingsPanel, {
3896
4378
  title: config.labels.settings ?? config.labels.pageTitle,
3897
4379
  subtitle: config.labels.pageSubtitle,
3898
4380
  generalSettings: React8.createElement(SettingsView),
3899
4381
  registries: marketingRegistries,
4382
+ // Enables the "Integrações" tab (renders manifest connectors for this host).
4383
+ hostPluginId: "marketing",
3900
4384
  routeBase: "/settings/marketing"
3901
4385
  })
3902
4386
  );
@@ -3908,12 +4392,16 @@ function createMarketingPlugin(options) {
3908
4392
  version: "1.0.0",
3909
4393
  scope: options?.scope ?? "universal",
3910
4394
  verticalId: options?.verticalId,
4395
+ // Admin content-planning surface (campaigns, content calendar) — targets the
4396
+ // saas/admin Panel world, not the ecommerce storefront/operator console.
4397
+ scaffolds: ["saas"],
3911
4398
  defaultEnabled: true,
3912
4399
  dependencies: [],
3913
4400
  declaredFeatures: [
3914
4401
  { id: "marketing", label: config.labels.pageTitle, group: "Engage" },
3915
4402
  ...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : [],
3916
- ...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : []
4403
+ ...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : [],
4404
+ ...config.modules.blog ? [{ id: "marketing.blog", label: config.labels.blog, group: "Engage" }] : []
3917
4405
  ],
3918
4406
  navigation: [
3919
4407
  {
@@ -3944,11 +4432,17 @@ function createMarketingPlugin(options) {
3944
4432
  { name: "marketing.content.post.updated", description: "A content post was updated" }
3945
4433
  ],
3946
4434
  migrations: [
4435
+ {
4436
+ id: "marketing-000-plg-rename",
4437
+ version: "1.0.0",
4438
+ sql: MIGRATION_000_PLG_RENAME,
4439
+ description: "Rename legacy mkt_* tables to plg_marketing_* in-place for pools provisioned before the industry-pool rename (guarded no-op on fresh pools)"
4440
+ },
3947
4441
  {
3948
4442
  id: "marketing-001-content-planner",
3949
4443
  version: "1.0.0",
3950
4444
  sql: MIGRATION_001_CONTENT_PLANNER,
3951
- description: "Create mkt_social_accounts, mkt_content_plans and mkt_content_posts tables"
4445
+ description: "Create plg_marketing_social_accounts, plg_marketing_content_plans and plg_marketing_content_posts"
3952
4446
  },
3953
4447
  {
3954
4448
  id: "marketing-002-multi-platform",
@@ -3960,9 +4454,18 @@ function createMarketingPlugin(options) {
3960
4454
  id: "marketing-003-recording-ops",
3961
4455
  version: "1.2.0",
3962
4456
  sql: MIGRATION_003_RECORDING_OPS,
3963
- description: "Recording-day checklist + media asset URL on posts; mkt-media storage bucket"
4457
+ description: "Recording-day checklist + media asset URL on posts; content-media storage bucket"
4458
+ },
4459
+ {
4460
+ id: "marketing-004-ranklayer",
4461
+ version: "1.3.0",
4462
+ sql: MIGRATION_004_RANKLAYER,
4463
+ description: "RankLayer connector state: plg_marketing_ranklayer_integrations + _sync_log (tenant RLS)"
3964
4464
  }
3965
4465
  ],
4466
+ // RankLayer SEO connector — renders in the Integrações settings tab. Scaffold
4467
+ // (control plane only); the real sync lands via an external PR (RANKLAYER.md).
4468
+ connectors: [ranklayerConnectorDef],
3966
4469
  aiTools: [
3967
4470
  {
3968
4471
  id: "marketing.channel-performance",