@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.
- package/README.md +2 -0
- package/dist/MarketingContext.d.ts +2 -0
- package/dist/MarketingContext.d.ts.map +1 -1
- package/dist/MarketingPage.d.ts.map +1 -1
- package/dist/data/blogEntities.d.ts +28 -0
- package/dist/data/blogEntities.d.ts.map +1 -0
- package/dist/data/contentSupabase.d.ts.map +1 -1
- package/dist/data/tables.d.ts +6 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/index.cjs +630 -127
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +636 -133
- package/dist/index.js.map +1 -1
- package/dist/integrations/ranklayer/connectorDef.d.ts +3 -0
- package/dist/integrations/ranklayer/connectorDef.d.ts.map +1 -0
- package/dist/integrations/ranklayer/data/supabase.d.ts +16 -0
- package/dist/integrations/ranklayer/data/supabase.d.ts.map +1 -0
- package/dist/integrations/ranklayer/types.d.ts +16 -0
- package/dist/integrations/ranklayer/types.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +9 -3
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/schema/index.d.ts +44 -44
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema.cjs +9 -9
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +9 -9
- package/dist/schema.js.map +1 -1
- package/dist/views/blog/BlogAdminView.d.ts +5 -0
- package/dist/views/blog/BlogAdminView.d.ts.map +1 -0
- package/dist/views/blog/BlogEntityForm.d.ts +11 -0
- package/dist/views/blog/BlogEntityForm.d.ts.map +1 -0
- package/dist/views/blog/useEntityCrud.d.ts +29 -0
- package/dist/views/blog/useEntityCrud.d.ts.map +1 -0
- package/package.json +8 -5
- package/src/MarketingContext.tsx +2 -0
- package/src/MarketingPage.tsx +22 -1
- package/src/data/blogEntities.tsx +91 -0
- package/src/data/contentSupabase.ts +18 -17
- package/src/data/tables.ts +7 -0
- package/src/index.ts +27 -3
- package/src/integrations/ranklayer/RANKLAYER.md +40 -0
- package/src/integrations/ranklayer/connectorDef.tsx +98 -0
- package/src/integrations/ranklayer/data/supabase.ts +106 -0
- package/src/integrations/ranklayer/types.ts +16 -0
- package/src/migrations/000_plg_rename.sql +15 -0
- package/src/migrations/001_content_planner.sql +48 -48
- package/src/migrations/002_multi_platform.sql +5 -5
- package/src/migrations/003_recording_ops.sql +10 -10
- package/src/migrations/004_ranklayer.sql +50 -0
- package/src/migrations/index.ts +152 -74
- package/src/presets.ts +5 -3
- package/src/schema/index.ts +9 -9
- package/src/views/blog/BlogAdminView.tsx +99 -0
- package/src/views/blog/BlogEntityForm.tsx +92 -0
- package/src/views/blog/useEntityCrud.ts +50 -0
package/dist/index.cjs
CHANGED
|
@@ -219,11 +219,11 @@ function ChannelDetailView({ channelId, onBack }) {
|
|
|
219
219
|
const range = useMarketingStore((s) => s.range);
|
|
220
220
|
const channels = useMarketingStore((s) => s.channels);
|
|
221
221
|
const lookup = useChannelLookup();
|
|
222
|
-
const
|
|
222
|
+
const provider2 = useMarketingProvider();
|
|
223
223
|
const [campaigns, setCampaigns] = React8__default.default.useState([]);
|
|
224
224
|
React8__default.default.useEffect(() => {
|
|
225
|
-
void
|
|
226
|
-
}, [
|
|
225
|
+
void provider2.listCampaigns({ range, channelId }).then(setCampaigns);
|
|
226
|
+
}, [provider2, range, channelId]);
|
|
227
227
|
const perf = channels.find((c) => c.channelId === channelId);
|
|
228
228
|
const channel = lookup.get(channelId);
|
|
229
229
|
const columns = React8__default.default.useMemo(() => [
|
|
@@ -1968,7 +1968,7 @@ function toDraft2(post) {
|
|
|
1968
1968
|
}
|
|
1969
1969
|
function PostPage({ postId, onBack }) {
|
|
1970
1970
|
const t = core.useTranslation();
|
|
1971
|
-
const
|
|
1971
|
+
const provider2 = useContentPlannerProvider();
|
|
1972
1972
|
const storePost = useContentPlannerStore((s) => s.posts.find((p) => p.id === postId));
|
|
1973
1973
|
const savePost = useContentPlannerStore((s) => s.savePost);
|
|
1974
1974
|
const deletePost = useContentPlannerStore((s) => s.deletePost);
|
|
@@ -1985,13 +1985,13 @@ function PostPage({ postId, onBack }) {
|
|
|
1985
1985
|
setDraft(toDraft2(storePost));
|
|
1986
1986
|
return;
|
|
1987
1987
|
}
|
|
1988
|
-
void
|
|
1988
|
+
void provider2.getPost(postId).then((fetched) => {
|
|
1989
1989
|
if (fetched) {
|
|
1990
1990
|
setPost(fetched);
|
|
1991
1991
|
setDraft(toDraft2(fetched));
|
|
1992
1992
|
}
|
|
1993
1993
|
});
|
|
1994
|
-
}, [postId, storePost, post?.id,
|
|
1994
|
+
}, [postId, storePost, post?.id, provider2]);
|
|
1995
1995
|
if (!post || !draft) {
|
|
1996
1996
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
|
|
1997
1997
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
@@ -2178,6 +2178,239 @@ function PostPage({ postId, onBack }) {
|
|
|
2178
2178
|
post.id
|
|
2179
2179
|
);
|
|
2180
2180
|
}
|
|
2181
|
+
|
|
2182
|
+
// src/data/blogEntities.tsx
|
|
2183
|
+
var BLOG_POSTS_TABLE = "plg_blog_posts";
|
|
2184
|
+
var BLOG_CATEGORIES_TABLE = "plg_blog_categories";
|
|
2185
|
+
function buildBlogPostEntity() {
|
|
2186
|
+
const fields = [
|
|
2187
|
+
{ key: "title", label: "Title", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
|
|
2188
|
+
{ key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: false, hint: 'URL identifier, e.g. "cannabis-and-oral-health"' },
|
|
2189
|
+
{ key: "status", label: "Status", type: "select", options: ["draft", "published"], showInTable: true, defaultValue: "draft" },
|
|
2190
|
+
{ key: "categoryId", label: "Category", type: "relation", relation: { table: BLOG_CATEGORIES_TABLE, valueField: "id", labelField: "name", tenantScoped: true }, showInTable: false },
|
|
2191
|
+
{ key: "excerpt", label: "Excerpt", type: "textarea", span: 2, showInTable: false, placeholder: "Short summary shown on cards" },
|
|
2192
|
+
{ key: "body", label: "Body", type: "markdown", span: 2, showInTable: false },
|
|
2193
|
+
{ key: "coverImage", label: "Cover image", type: "image", showInTable: false, placeholder: "https://\u2026" },
|
|
2194
|
+
{ key: "readTime", label: "Read time", type: "text", showInTable: false, placeholder: "5 min" },
|
|
2195
|
+
{ key: "publishedAt", label: "Published at", type: "date", showInTable: true, sortable: true },
|
|
2196
|
+
{ key: "authorName", label: "Author name", type: "text", showInTable: false },
|
|
2197
|
+
{ key: "authorRole", label: "Author role", type: "text", showInTable: false, placeholder: "e.g. Cirurgi\xE3o-Dentista" },
|
|
2198
|
+
{ key: "authorAvatarUrl", label: "Author avatar", type: "image", showInTable: false, placeholder: "https://\u2026" },
|
|
2199
|
+
{ key: "authorBio", label: "Author bio", type: "textarea", span: 2, showInTable: false }
|
|
2200
|
+
];
|
|
2201
|
+
return {
|
|
2202
|
+
name: "Post",
|
|
2203
|
+
namePlural: "Posts",
|
|
2204
|
+
icon: "Newspaper",
|
|
2205
|
+
fields,
|
|
2206
|
+
displayField: "title",
|
|
2207
|
+
subtitleField: "slug",
|
|
2208
|
+
facets: [{ field: "status", allLabel: "All" }],
|
|
2209
|
+
data: {
|
|
2210
|
+
table: BLOG_POSTS_TABLE,
|
|
2211
|
+
tenantScoped: true,
|
|
2212
|
+
searchColumns: ["title", "slug", "excerpt"]
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
function buildBlogCategoryEntity() {
|
|
2217
|
+
const fields = [
|
|
2218
|
+
{ key: "name", label: "Name", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
|
|
2219
|
+
{ key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: true, hint: "URL identifier" },
|
|
2220
|
+
{ key: "description", label: "Description", type: "textarea", span: 2, showInTable: false }
|
|
2221
|
+
];
|
|
2222
|
+
return {
|
|
2223
|
+
name: "Category",
|
|
2224
|
+
namePlural: "Categories",
|
|
2225
|
+
icon: "Tags",
|
|
2226
|
+
fields,
|
|
2227
|
+
displayField: "name",
|
|
2228
|
+
data: {
|
|
2229
|
+
table: BLOG_CATEGORIES_TABLE,
|
|
2230
|
+
tenantScoped: true,
|
|
2231
|
+
searchColumns: ["name", "slug"]
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
function useEntityCrud(entity) {
|
|
2236
|
+
const provider2 = React8.useMemo(() => core.resolveDataProvider(entity), [entity]);
|
|
2237
|
+
const [items, setItems] = React8.useState(null);
|
|
2238
|
+
const [total, setTotal] = React8.useState(0);
|
|
2239
|
+
const fetch = React8.useCallback(async (query = {}) => {
|
|
2240
|
+
try {
|
|
2241
|
+
const res = await provider2.list(query);
|
|
2242
|
+
setItems(res.data);
|
|
2243
|
+
setTotal(res.total);
|
|
2244
|
+
} catch (err) {
|
|
2245
|
+
console.warn("[blog] list failed; showing empty. Apply the plugin-blog migration to this pool.", err);
|
|
2246
|
+
setItems([]);
|
|
2247
|
+
setTotal(0);
|
|
2248
|
+
}
|
|
2249
|
+
}, [provider2]);
|
|
2250
|
+
const getById = React8.useCallback(async (id) => {
|
|
2251
|
+
const res = await provider2.list({ filters: { id }, page: 1, pageSize: 1 });
|
|
2252
|
+
return res.data[0] ?? null;
|
|
2253
|
+
}, [provider2]);
|
|
2254
|
+
const create = React8.useCallback((data) => provider2.create(data), [provider2]);
|
|
2255
|
+
const update = React8.useCallback((id, data) => provider2.update(id, data), [provider2]);
|
|
2256
|
+
const remove = React8.useCallback((id) => provider2.remove(id), [provider2]);
|
|
2257
|
+
return { items, total, fetch, getById, create, update, remove };
|
|
2258
|
+
}
|
|
2259
|
+
function PostsList({ navigate }) {
|
|
2260
|
+
const entity = React8.useMemo(() => buildBlogPostEntity(), []);
|
|
2261
|
+
const { items, total, fetch } = useEntityCrud(entity);
|
|
2262
|
+
const [search, setSearch] = React8.useState("");
|
|
2263
|
+
const [statusFilter, setStatusFilter] = React8.useState();
|
|
2264
|
+
const [loadedOnce, setLoadedOnce] = React8.useState(false);
|
|
2265
|
+
React8.useEffect(() => {
|
|
2266
|
+
let active = true;
|
|
2267
|
+
Promise.resolve(
|
|
2268
|
+
fetch({ search: search || void 0, filters: statusFilter ? { status: statusFilter } : void 0 })
|
|
2269
|
+
).finally(() => {
|
|
2270
|
+
if (active) setLoadedOnce(true);
|
|
2271
|
+
});
|
|
2272
|
+
return () => {
|
|
2273
|
+
active = false;
|
|
2274
|
+
};
|
|
2275
|
+
}, [search, statusFilter, fetch]);
|
|
2276
|
+
const facets = React8.useMemo(() => [{
|
|
2277
|
+
field: "status",
|
|
2278
|
+
allLabel: "All",
|
|
2279
|
+
options: [{ value: "draft", label: "Draft" }, { value: "published", label: "Published" }]
|
|
2280
|
+
}], []);
|
|
2281
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2282
|
+
saas.CrudListView,
|
|
2283
|
+
{
|
|
2284
|
+
entityDef: entity,
|
|
2285
|
+
items: loadedOnce ? items : null,
|
|
2286
|
+
total,
|
|
2287
|
+
search,
|
|
2288
|
+
onSearchChange: setSearch,
|
|
2289
|
+
searchPlaceholder: "Search posts\u2026",
|
|
2290
|
+
facets,
|
|
2291
|
+
activeFilters: { status: statusFilter },
|
|
2292
|
+
onFacetChange: (_field, value) => setStatusFilter(value),
|
|
2293
|
+
onNew: () => navigate("blog-new"),
|
|
2294
|
+
addLabel: "New post",
|
|
2295
|
+
onRowClick: (row) => navigate(`blog-post:${row.id}`)
|
|
2296
|
+
}
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
function CategoriesList({ navigate }) {
|
|
2300
|
+
const entity = React8.useMemo(() => buildBlogCategoryEntity(), []);
|
|
2301
|
+
const { items, total, fetch } = useEntityCrud(entity);
|
|
2302
|
+
const [search, setSearch] = React8.useState("");
|
|
2303
|
+
const [loadedOnce, setLoadedOnce] = React8.useState(false);
|
|
2304
|
+
React8.useEffect(() => {
|
|
2305
|
+
let active = true;
|
|
2306
|
+
Promise.resolve(fetch({ search: search || void 0 })).finally(() => {
|
|
2307
|
+
if (active) setLoadedOnce(true);
|
|
2308
|
+
});
|
|
2309
|
+
return () => {
|
|
2310
|
+
active = false;
|
|
2311
|
+
};
|
|
2312
|
+
}, [search, fetch]);
|
|
2313
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2314
|
+
saas.CrudListView,
|
|
2315
|
+
{
|
|
2316
|
+
entityDef: entity,
|
|
2317
|
+
items: loadedOnce ? items : null,
|
|
2318
|
+
total,
|
|
2319
|
+
search,
|
|
2320
|
+
onSearchChange: setSearch,
|
|
2321
|
+
searchPlaceholder: "Search categories\u2026",
|
|
2322
|
+
onNew: () => navigate("blog-cat-new"),
|
|
2323
|
+
addLabel: "New category",
|
|
2324
|
+
onRowClick: (row) => navigate(`blog-cat:${row.id}`)
|
|
2325
|
+
}
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
function BlogAdminView({ navigate }) {
|
|
2329
|
+
const { labels } = useMarketingConfig();
|
|
2330
|
+
const [lens, setLens] = React8.useState("posts");
|
|
2331
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
2332
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2333
|
+
ui.SegmentedControl,
|
|
2334
|
+
{
|
|
2335
|
+
"aria-label": "Blog section",
|
|
2336
|
+
options: [
|
|
2337
|
+
{ value: "posts", label: labels.blog },
|
|
2338
|
+
{ value: "categories", label: labels.blogCategories }
|
|
2339
|
+
],
|
|
2340
|
+
value: lens,
|
|
2341
|
+
onChange: (v) => setLens(v)
|
|
2342
|
+
}
|
|
2343
|
+
),
|
|
2344
|
+
lens === "posts" ? /* @__PURE__ */ jsxRuntime.jsx(PostsList, { navigate }) : /* @__PURE__ */ jsxRuntime.jsx(CategoriesList, { navigate })
|
|
2345
|
+
] });
|
|
2346
|
+
}
|
|
2347
|
+
function BlogEntityForm({
|
|
2348
|
+
entity,
|
|
2349
|
+
editId,
|
|
2350
|
+
parentLabel,
|
|
2351
|
+
onDone
|
|
2352
|
+
}) {
|
|
2353
|
+
const { getById, create, update, remove } = useEntityCrud(entity);
|
|
2354
|
+
const isEdit = !!editId;
|
|
2355
|
+
const namePlural = entity.namePlural ?? entity.name + "s";
|
|
2356
|
+
const [initialData, setInitialData] = React8__default.default.useState(isEdit ? null : {});
|
|
2357
|
+
React8__default.default.useEffect(() => {
|
|
2358
|
+
if (!editId) return;
|
|
2359
|
+
let cancelled = false;
|
|
2360
|
+
(async () => {
|
|
2361
|
+
const item = await getById(editId);
|
|
2362
|
+
if (!cancelled) setInitialData(item ?? {});
|
|
2363
|
+
})();
|
|
2364
|
+
return () => {
|
|
2365
|
+
cancelled = true;
|
|
2366
|
+
};
|
|
2367
|
+
}, [editId, getById]);
|
|
2368
|
+
async function save(values) {
|
|
2369
|
+
try {
|
|
2370
|
+
if (isEdit) await update(editId, values);
|
|
2371
|
+
else await create(values);
|
|
2372
|
+
ui.toast.success(isEdit ? `${entity.name} updated` : `${entity.name} created`);
|
|
2373
|
+
onDone();
|
|
2374
|
+
} catch (err) {
|
|
2375
|
+
ui.toast.error(`Failed to save ${entity.name.toLowerCase()}`);
|
|
2376
|
+
throw err;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
async function del() {
|
|
2380
|
+
if (!editId) return;
|
|
2381
|
+
try {
|
|
2382
|
+
await remove(editId);
|
|
2383
|
+
ui.toast.success(`${entity.name} deleted`);
|
|
2384
|
+
onDone();
|
|
2385
|
+
} catch {
|
|
2386
|
+
ui.toast.error(`Failed to delete ${entity.name.toLowerCase()}`);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
const title = isEdit ? initialData?.[entity.displayField ?? "name"] ?? entity.name : `New ${entity.name.toLowerCase()}`;
|
|
2390
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
2391
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2392
|
+
ui.SubpageHeader,
|
|
2393
|
+
{
|
|
2394
|
+
title: String(title),
|
|
2395
|
+
onBack: onDone,
|
|
2396
|
+
parentLabel,
|
|
2397
|
+
actions: isEdit ? /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", className: "text-destructive hover:text-destructive", onClick: del, children: "Delete" }) : void 0
|
|
2398
|
+
}
|
|
2399
|
+
),
|
|
2400
|
+
initialData !== null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2401
|
+
saas.CrudFormPage,
|
|
2402
|
+
{
|
|
2403
|
+
entityDef: entity,
|
|
2404
|
+
mode: isEdit ? "edit" : "create",
|
|
2405
|
+
initialData,
|
|
2406
|
+
namePlural,
|
|
2407
|
+
onCancel: onDone,
|
|
2408
|
+
onSubmit: save,
|
|
2409
|
+
hideBreadcrumb: true
|
|
2410
|
+
}
|
|
2411
|
+
)
|
|
2412
|
+
] });
|
|
2413
|
+
}
|
|
2181
2414
|
function buildNav(config, view, navigate) {
|
|
2182
2415
|
const items = [
|
|
2183
2416
|
{ id: "overview", label: config.labels.overview, icon: "BarChart3", active: view === "overview", onClick: () => navigate("overview") }
|
|
@@ -2209,9 +2442,18 @@ function buildNav(config, view, navigate) {
|
|
|
2209
2442
|
onClick: () => navigate("content")
|
|
2210
2443
|
});
|
|
2211
2444
|
}
|
|
2445
|
+
if (config.modules.blog) {
|
|
2446
|
+
items.push({
|
|
2447
|
+
id: "blog",
|
|
2448
|
+
label: config.labels.blog,
|
|
2449
|
+
icon: "Newspaper",
|
|
2450
|
+
active: view === "blog" || view === "blog-new" || view.startsWith("blog-post:") || view === "blog-cat-new" || view.startsWith("blog-cat:"),
|
|
2451
|
+
onClick: () => navigate("blog")
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2212
2454
|
return items;
|
|
2213
2455
|
}
|
|
2214
|
-
function MarketingPage({ config, provider, store, contentProvider, contentStore }) {
|
|
2456
|
+
function MarketingPage({ config, provider: provider2, store, contentProvider, contentStore }) {
|
|
2215
2457
|
const t = core.useTranslation();
|
|
2216
2458
|
const { view, direction, navigate } = saas.useModuleNavigation("/marketing", {
|
|
2217
2459
|
overview: 0,
|
|
@@ -2220,15 +2462,22 @@ function MarketingPage({ config, provider, store, contentProvider, contentStore
|
|
|
2220
2462
|
funnel: 0,
|
|
2221
2463
|
"landing-pages": 0,
|
|
2222
2464
|
content: 0,
|
|
2465
|
+
blog: 0,
|
|
2223
2466
|
"channel-detail": 1,
|
|
2224
2467
|
"content-post": 1,
|
|
2225
|
-
"content-brief": 1
|
|
2468
|
+
"content-brief": 1,
|
|
2469
|
+
"blog-new": 1,
|
|
2470
|
+
"blog-post": 1,
|
|
2471
|
+
"blog-cat-new": 1,
|
|
2472
|
+
"blog-cat": 1
|
|
2226
2473
|
}, "overview");
|
|
2227
2474
|
React8__default.default.useEffect(() => {
|
|
2228
2475
|
void store.getState().load();
|
|
2229
2476
|
}, [store]);
|
|
2230
2477
|
const isOverview = view === "overview";
|
|
2231
2478
|
const nav = buildNav(config, view, navigate);
|
|
2479
|
+
const blogPostEntity = React8__default.default.useMemo(() => buildBlogPostEntity(), []);
|
|
2480
|
+
const blogCategoryEntity = React8__default.default.useMemo(() => buildBlogCategoryEntity(), []);
|
|
2232
2481
|
const quickActions = React8__default.default.useMemo(() => [
|
|
2233
2482
|
{ id: "new-campaign", label: t("marketing.campaigns.new"), icon: "Megaphone", description: config.labels.campaigns, action: () => navigate("campaigns") }
|
|
2234
2483
|
], [t, config.labels.campaigns]);
|
|
@@ -2241,9 +2490,14 @@ function MarketingPage({ config, provider, store, contentProvider, contentStore
|
|
|
2241
2490
|
{ id: "content", render: () => /* @__PURE__ */ jsxRuntime.jsx(ContentView, { onOpenPost: (id) => navigate(`content-post:${id}`) }) },
|
|
2242
2491
|
{ id: "content-post", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(PostPage, { postId: id, onBack: () => navigate("content") }) },
|
|
2243
2492
|
{ id: "content-brief", render: () => /* @__PURE__ */ jsxRuntime.jsx(PlanBriefView, { onBack: () => navigate("content") }) },
|
|
2493
|
+
{ id: "blog", render: () => /* @__PURE__ */ jsxRuntime.jsx(BlogAdminView, { navigate }) },
|
|
2494
|
+
{ id: "blog-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogPostEntity, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
|
|
2495
|
+
{ id: "blog-post", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogPostEntity, editId: id, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
|
|
2496
|
+
{ id: "blog-cat-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogCategoryEntity, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
|
|
2497
|
+
{ id: "blog-cat", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogCategoryEntity, editId: id, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
|
|
2244
2498
|
{ id: "overview", render: () => /* @__PURE__ */ jsxRuntime.jsx(OverviewView, {}) }
|
|
2245
2499
|
], "overview");
|
|
2246
|
-
return /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2500
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config, provider: provider2, store, children: /* @__PURE__ */ jsxRuntime.jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2247
2501
|
ui.ModulePage,
|
|
2248
2502
|
{
|
|
2249
2503
|
title: config.labels.pageTitle,
|
|
@@ -2518,7 +2772,7 @@ var AGENCY = {
|
|
|
2518
2772
|
{ id: "referral", label: "Referral", icon: "Users", kind: "referral" },
|
|
2519
2773
|
{ id: "direct", label: "Direct", icon: "MousePointerClick", kind: "direct" }
|
|
2520
2774
|
],
|
|
2521
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, broadcasts: false, journeys: false }
|
|
2775
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
|
|
2522
2776
|
};
|
|
2523
2777
|
var BEAUTY = {
|
|
2524
2778
|
conversion: {
|
|
@@ -2536,7 +2790,7 @@ var BEAUTY = {
|
|
|
2536
2790
|
{ id: "whatsapp", label: "WhatsApp", icon: "MessageCircle", kind: "social" },
|
|
2537
2791
|
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
2538
2792
|
],
|
|
2539
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, broadcasts: false, journeys: false }
|
|
2793
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, blog: false, broadcasts: false, journeys: false }
|
|
2540
2794
|
};
|
|
2541
2795
|
var RESTO = {
|
|
2542
2796
|
conversion: {
|
|
@@ -2554,7 +2808,7 @@ var RESTO = {
|
|
|
2554
2808
|
{ id: "instagram", label: "Instagram", icon: "Instagram", kind: "social" },
|
|
2555
2809
|
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
2556
2810
|
],
|
|
2557
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, broadcasts: false, journeys: false }
|
|
2811
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
|
|
2558
2812
|
};
|
|
2559
2813
|
var MARKETING_PRESETS = {
|
|
2560
2814
|
agency: AGENCY,
|
|
@@ -3034,6 +3288,15 @@ function createMockContentPlannerProvider() {
|
|
|
3034
3288
|
}
|
|
3035
3289
|
};
|
|
3036
3290
|
}
|
|
3291
|
+
|
|
3292
|
+
// src/data/tables.ts
|
|
3293
|
+
var T = {
|
|
3294
|
+
socialAccounts: "plg_marketing_social_accounts",
|
|
3295
|
+
contentPlans: "plg_marketing_content_plans",
|
|
3296
|
+
contentPosts: "plg_marketing_content_posts"
|
|
3297
|
+
};
|
|
3298
|
+
|
|
3299
|
+
// src/data/contentSupabase.ts
|
|
3037
3300
|
function getClient() {
|
|
3038
3301
|
const supabase = core.getSupabaseClientOptional();
|
|
3039
3302
|
if (!supabase) throw new Error("Supabase not initialized");
|
|
@@ -3075,7 +3338,7 @@ function createSupabaseContentPlannerProvider() {
|
|
|
3075
3338
|
async listAccounts() {
|
|
3076
3339
|
const tenantId = core.getActiveTenantId();
|
|
3077
3340
|
if (!tenantId) return [];
|
|
3078
|
-
const { data } = await getClient().from(
|
|
3341
|
+
const { data } = await getClient().from(T.socialAccounts).select("*").eq("tenant_id", tenantId).eq("is_active", true).order("created_at", { ascending: true });
|
|
3079
3342
|
return (data ?? []).map(toAccount);
|
|
3080
3343
|
},
|
|
3081
3344
|
async saveAccount(input) {
|
|
@@ -3085,11 +3348,11 @@ function createSupabaseContentPlannerProvider() {
|
|
|
3085
3348
|
if (input.handle !== void 0) payload.handle = input.handle;
|
|
3086
3349
|
if (input.platforms !== void 0) payload.platforms = input.platforms;
|
|
3087
3350
|
if (input.isActive !== void 0) payload.is_active = input.isActive;
|
|
3088
|
-
const { data: data2, error: error2 } = await getClient().from(
|
|
3351
|
+
const { data: data2, error: error2 } = await getClient().from(T.socialAccounts).update(payload).eq("id", input.id).select("*").single();
|
|
3089
3352
|
if (error2) throw error2;
|
|
3090
3353
|
return toAccount(data2);
|
|
3091
3354
|
}
|
|
3092
|
-
const { data, error } = await getClient().from(
|
|
3355
|
+
const { data, error } = await getClient().from(T.socialAccounts).insert({
|
|
3093
3356
|
tenant_id: requireTenantId(),
|
|
3094
3357
|
name: input.name,
|
|
3095
3358
|
handle: input.handle ?? null,
|
|
@@ -3100,17 +3363,17 @@ function createSupabaseContentPlannerProvider() {
|
|
|
3100
3363
|
return toAccount(data);
|
|
3101
3364
|
},
|
|
3102
3365
|
async deleteAccount(id) {
|
|
3103
|
-
const { error } = await getClient().from(
|
|
3366
|
+
const { error } = await getClient().from(T.socialAccounts).delete().eq("id", id);
|
|
3104
3367
|
if (error) throw error;
|
|
3105
3368
|
},
|
|
3106
3369
|
async listPlans(accountId) {
|
|
3107
3370
|
const tenantId = core.getActiveTenantId();
|
|
3108
3371
|
if (!tenantId) return [];
|
|
3109
|
-
const { data } = await getClient().from(
|
|
3372
|
+
const { data } = await getClient().from(T.contentPlans).select("*").eq("tenant_id", tenantId).eq("account_id", accountId).order("created_at", { ascending: false });
|
|
3110
3373
|
return (data ?? []).map(toPlan);
|
|
3111
3374
|
},
|
|
3112
3375
|
async getPlan(id) {
|
|
3113
|
-
const { data } = await getClient().from(
|
|
3376
|
+
const { data } = await getClient().from(T.contentPlans).select("*").eq("id", id).maybeSingle();
|
|
3114
3377
|
return data ? toPlan(data) : null;
|
|
3115
3378
|
},
|
|
3116
3379
|
async savePlan(input) {
|
|
@@ -3126,12 +3389,12 @@ function createSupabaseContentPlannerProvider() {
|
|
|
3126
3389
|
if (input.formats !== void 0) payload.formats = input.formats;
|
|
3127
3390
|
if (input.weeklyFrequency !== void 0) payload.weekly_frequency = input.weeklyFrequency;
|
|
3128
3391
|
if (input.briefMd !== void 0) payload.brief_md = input.briefMd;
|
|
3129
|
-
const { data: data2, error: error2 } = await getClient().from(
|
|
3392
|
+
const { data: data2, error: error2 } = await getClient().from(T.contentPlans).update(payload).eq("id", input.id).select("*").single();
|
|
3130
3393
|
if (error2) throw error2;
|
|
3131
3394
|
return toPlan(data2);
|
|
3132
3395
|
}
|
|
3133
3396
|
if (!input.accountId) throw new Error("accountId is required");
|
|
3134
|
-
const { data, error } = await getClient().from(
|
|
3397
|
+
const { data, error } = await getClient().from(T.contentPlans).insert({
|
|
3135
3398
|
tenant_id: requireTenantId(),
|
|
3136
3399
|
account_id: input.accountId,
|
|
3137
3400
|
name: input.name ?? "Novo plano",
|
|
@@ -3149,14 +3412,14 @@ function createSupabaseContentPlannerProvider() {
|
|
|
3149
3412
|
return toPlan(data);
|
|
3150
3413
|
},
|
|
3151
3414
|
async deletePlan(id) {
|
|
3152
|
-
await getClient().from(
|
|
3415
|
+
await getClient().from(T.contentPlans).delete().eq("id", id);
|
|
3153
3416
|
},
|
|
3154
3417
|
async listPosts(planId) {
|
|
3155
|
-
const { data } = await getClient().from(
|
|
3418
|
+
const { data } = await getClient().from(T.contentPosts).select("*").eq("plan_id", planId).order("week_number", { ascending: true }).order("position", { ascending: true });
|
|
3156
3419
|
return (data ?? []).map(toPost);
|
|
3157
3420
|
},
|
|
3158
3421
|
async getPost(id) {
|
|
3159
|
-
const { data } = await getClient().from(
|
|
3422
|
+
const { data } = await getClient().from(T.contentPosts).select("*").eq("id", id).maybeSingle();
|
|
3160
3423
|
return data ? toPost(data) : null;
|
|
3161
3424
|
},
|
|
3162
3425
|
async savePost(input) {
|
|
@@ -3174,15 +3437,15 @@ function createSupabaseContentPlannerProvider() {
|
|
|
3174
3437
|
if (input.hook !== void 0) payload.hook = input.hook;
|
|
3175
3438
|
if (input.cta !== void 0) payload.cta = input.cta;
|
|
3176
3439
|
if (input.contentMd !== void 0) payload.content_md = input.contentMd;
|
|
3177
|
-
const { data: data2, error: error2 } = await getClient().from(
|
|
3440
|
+
const { data: data2, error: error2 } = await getClient().from(T.contentPosts).update(payload).eq("id", input.id).select("*").single();
|
|
3178
3441
|
if (error2) throw error2;
|
|
3179
3442
|
return toPost(data2);
|
|
3180
3443
|
}
|
|
3181
3444
|
if (!input.planId) throw new Error("planId is required");
|
|
3182
3445
|
const week = input.weekNumber ?? 1;
|
|
3183
|
-
const { data: maxData } = await getClient().from(
|
|
3446
|
+
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);
|
|
3184
3447
|
const maxPos = maxData?.[0]?.position ?? -1;
|
|
3185
|
-
const { data, error } = await getClient().from(
|
|
3448
|
+
const { data, error } = await getClient().from(T.contentPosts).insert({
|
|
3186
3449
|
tenant_id: requireTenantId(),
|
|
3187
3450
|
plan_id: input.planId,
|
|
3188
3451
|
week_number: week,
|
|
@@ -3202,11 +3465,11 @@ function createSupabaseContentPlannerProvider() {
|
|
|
3202
3465
|
return toPost(data);
|
|
3203
3466
|
},
|
|
3204
3467
|
async deletePost(id) {
|
|
3205
|
-
await getClient().from(
|
|
3468
|
+
await getClient().from(T.contentPosts).delete().eq("id", id);
|
|
3206
3469
|
}
|
|
3207
3470
|
};
|
|
3208
3471
|
}
|
|
3209
|
-
function createMarketingStore(
|
|
3472
|
+
function createMarketingStore(provider2) {
|
|
3210
3473
|
return vanilla.createStore((set, get) => ({
|
|
3211
3474
|
range: "30d",
|
|
3212
3475
|
overview: null,
|
|
@@ -3219,11 +3482,11 @@ function createMarketingStore(provider) {
|
|
|
3219
3482
|
const range = get().range;
|
|
3220
3483
|
set({ loading: true });
|
|
3221
3484
|
const [overview, channels, campaigns, funnel, landingPages] = await Promise.all([
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3485
|
+
provider2.overview({ range }),
|
|
3486
|
+
provider2.channelPerformance({ range }),
|
|
3487
|
+
provider2.listCampaigns({ range }),
|
|
3488
|
+
provider2.funnel({ range }),
|
|
3489
|
+
provider2.landingPages({ range })
|
|
3227
3490
|
]);
|
|
3228
3491
|
set({ overview, channels, campaigns, funnel, landingPages, loading: false });
|
|
3229
3492
|
},
|
|
@@ -3232,21 +3495,21 @@ function createMarketingStore(provider) {
|
|
|
3232
3495
|
await get().load();
|
|
3233
3496
|
},
|
|
3234
3497
|
async saveCampaign(input) {
|
|
3235
|
-
const created = await
|
|
3498
|
+
const created = await provider2.saveCampaign(input);
|
|
3236
3499
|
await get().load();
|
|
3237
3500
|
return created;
|
|
3238
3501
|
},
|
|
3239
3502
|
async deleteCampaign(id) {
|
|
3240
|
-
await
|
|
3503
|
+
await provider2.deleteCampaign(id);
|
|
3241
3504
|
await get().load();
|
|
3242
3505
|
}
|
|
3243
3506
|
}));
|
|
3244
3507
|
}
|
|
3245
|
-
function createContentPlannerStore(
|
|
3508
|
+
function createContentPlannerStore(provider2) {
|
|
3246
3509
|
async function loadPlansFor(accountId) {
|
|
3247
|
-
const plans = await
|
|
3510
|
+
const plans = await provider2.listPlans(accountId);
|
|
3248
3511
|
const activePlan = plans.find((p) => p.status === "active") ?? plans[0] ?? null;
|
|
3249
|
-
const posts = activePlan ? await
|
|
3512
|
+
const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
|
|
3250
3513
|
return { plans, activePlan, posts };
|
|
3251
3514
|
}
|
|
3252
3515
|
return vanilla.createStore((set, get) => ({
|
|
@@ -3266,7 +3529,7 @@ function createContentPlannerStore(provider) {
|
|
|
3266
3529
|
if (get().loading) return;
|
|
3267
3530
|
set({ loading: true });
|
|
3268
3531
|
try {
|
|
3269
|
-
const accounts = await
|
|
3532
|
+
const accounts = await provider2.listAccounts();
|
|
3270
3533
|
const current = get().activeAccountId;
|
|
3271
3534
|
const activeAccountId = (current && accounts.some((a) => a.id === current) ? current : accounts[0]?.id) ?? null;
|
|
3272
3535
|
const { plans, activePlan, posts } = activeAccountId ? await loadPlansFor(activeAccountId) : { plans: [], activePlan: null, posts: [] };
|
|
@@ -3297,19 +3560,19 @@ function createContentPlannerStore(provider) {
|
|
|
3297
3560
|
},
|
|
3298
3561
|
async selectPlan(planId) {
|
|
3299
3562
|
const plan = get().plans.find((p) => p.id === planId) ?? null;
|
|
3300
|
-
const posts = plan ? await
|
|
3563
|
+
const posts = plan ? await provider2.listPosts(plan.id) : [];
|
|
3301
3564
|
set({ activePlan: plan, posts });
|
|
3302
3565
|
},
|
|
3303
3566
|
async saveAccount(input) {
|
|
3304
|
-
const account = await
|
|
3305
|
-
const accounts = await
|
|
3567
|
+
const account = await provider2.saveAccount(input);
|
|
3568
|
+
const accounts = await provider2.listAccounts();
|
|
3306
3569
|
set({ accounts });
|
|
3307
3570
|
if (!get().activeAccountId) await get().setAccount(account.id);
|
|
3308
3571
|
return account;
|
|
3309
3572
|
},
|
|
3310
3573
|
async deleteAccount(id) {
|
|
3311
|
-
await
|
|
3312
|
-
const accounts = await
|
|
3574
|
+
await provider2.deleteAccount(id);
|
|
3575
|
+
const accounts = await provider2.listAccounts();
|
|
3313
3576
|
const wasActive = get().activeAccountId === id;
|
|
3314
3577
|
set({ accounts });
|
|
3315
3578
|
if (wasActive) {
|
|
@@ -3322,30 +3585,30 @@ function createContentPlannerStore(provider) {
|
|
|
3322
3585
|
}
|
|
3323
3586
|
},
|
|
3324
3587
|
async savePlan(input) {
|
|
3325
|
-
const plan = await
|
|
3588
|
+
const plan = await provider2.savePlan(input);
|
|
3326
3589
|
const accountId = get().activeAccountId;
|
|
3327
3590
|
if (accountId) {
|
|
3328
|
-
const plans = await
|
|
3591
|
+
const plans = await provider2.listPlans(accountId);
|
|
3329
3592
|
const activePlan = plans.find((p) => p.id === plan.id) ?? get().activePlan;
|
|
3330
|
-
const posts = activePlan ? await
|
|
3593
|
+
const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
|
|
3331
3594
|
set({ plans, activePlan, posts });
|
|
3332
3595
|
}
|
|
3333
3596
|
return plan;
|
|
3334
3597
|
},
|
|
3335
3598
|
async savePost(input) {
|
|
3336
|
-
const post = await
|
|
3599
|
+
const post = await provider2.savePost(input);
|
|
3337
3600
|
const planId = get().activePlan?.id;
|
|
3338
3601
|
if (planId && post.planId === planId) {
|
|
3339
|
-
const posts = await
|
|
3602
|
+
const posts = await provider2.listPosts(planId);
|
|
3340
3603
|
set({ posts });
|
|
3341
3604
|
}
|
|
3342
3605
|
return post;
|
|
3343
3606
|
},
|
|
3344
3607
|
async deletePost(id) {
|
|
3345
|
-
await
|
|
3608
|
+
await provider2.deletePost(id);
|
|
3346
3609
|
const planId = get().activePlan?.id;
|
|
3347
3610
|
if (planId) {
|
|
3348
|
-
const posts = await
|
|
3611
|
+
const posts = await provider2.listPosts(planId);
|
|
3349
3612
|
set({ posts });
|
|
3350
3613
|
}
|
|
3351
3614
|
}
|
|
@@ -3353,13 +3616,29 @@ function createContentPlannerStore(provider) {
|
|
|
3353
3616
|
}
|
|
3354
3617
|
|
|
3355
3618
|
// src/migrations/index.ts
|
|
3619
|
+
var MIGRATION_000_PLG_RENAME = `-- 000_plg_rename.sql \u2014 rename legacy marketing tables to plg_marketing_* for pools
|
|
3620
|
+
-- provisioned before the industry-pool rename. Guarded: fires only when legacy
|
|
3621
|
+
-- name exists and target does not, so fresh pools skip every branch.
|
|
3622
|
+
DO $$
|
|
3623
|
+
BEGIN
|
|
3624
|
+
IF to_regclass('public.mkt_social_accounts') IS NOT NULL AND to_regclass('public.plg_marketing_social_accounts') IS NULL THEN
|
|
3625
|
+
ALTER TABLE public.mkt_social_accounts RENAME TO plg_marketing_social_accounts;
|
|
3626
|
+
END IF;
|
|
3627
|
+
IF to_regclass('public.mkt_content_plans') IS NOT NULL AND to_regclass('public.plg_marketing_content_plans') IS NULL THEN
|
|
3628
|
+
ALTER TABLE public.mkt_content_plans RENAME TO plg_marketing_content_plans;
|
|
3629
|
+
END IF;
|
|
3630
|
+
IF to_regclass('public.mkt_content_posts') IS NOT NULL AND to_regclass('public.plg_marketing_content_posts') IS NULL THEN
|
|
3631
|
+
ALTER TABLE public.mkt_content_posts RENAME TO plg_marketing_content_posts;
|
|
3632
|
+
END IF;
|
|
3633
|
+
END $$;
|
|
3634
|
+
`;
|
|
3356
3635
|
var MIGRATION_001_CONTENT_PLANNER = `-- Marketing Plugin: Content Planner
|
|
3357
|
-
-- Prefix:
|
|
3636
|
+
-- Prefix: plg_marketing_
|
|
3358
3637
|
-- Social accounts + content plans + posts (Notion-style markdown pages).
|
|
3359
3638
|
|
|
3360
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
3639
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_social_accounts (
|
|
3361
3640
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3362
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
3641
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3363
3642
|
name text NOT NULL,
|
|
3364
3643
|
handle text,
|
|
3365
3644
|
platform text NOT NULL DEFAULT 'instagram'
|
|
@@ -3367,13 +3646,13 @@ CREATE TABLE IF NOT EXISTS public.mkt_social_accounts (
|
|
|
3367
3646
|
is_active boolean NOT NULL DEFAULT true,
|
|
3368
3647
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
3369
3648
|
);
|
|
3370
|
-
ALTER TABLE public.
|
|
3371
|
-
CREATE INDEX IF NOT EXISTS
|
|
3649
|
+
ALTER TABLE public.plg_marketing_social_accounts ENABLE ROW LEVEL SECURITY;
|
|
3650
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_social_accounts_tenant ON public.plg_marketing_social_accounts(tenant_id);
|
|
3372
3651
|
|
|
3373
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
3652
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_content_plans (
|
|
3374
3653
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3375
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
3376
|
-
account_id uuid NOT NULL REFERENCES public.
|
|
3654
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3655
|
+
account_id uuid NOT NULL REFERENCES public.plg_marketing_social_accounts(id) ON DELETE CASCADE,
|
|
3377
3656
|
name text NOT NULL,
|
|
3378
3657
|
status text NOT NULL DEFAULT 'active'
|
|
3379
3658
|
CHECK (status IN ('draft', 'active', 'archived')),
|
|
@@ -3388,14 +3667,14 @@ CREATE TABLE IF NOT EXISTS public.mkt_content_plans (
|
|
|
3388
3667
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3389
3668
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3390
3669
|
);
|
|
3391
|
-
ALTER TABLE public.
|
|
3392
|
-
CREATE INDEX IF NOT EXISTS
|
|
3393
|
-
CREATE INDEX IF NOT EXISTS
|
|
3670
|
+
ALTER TABLE public.plg_marketing_content_plans ENABLE ROW LEVEL SECURITY;
|
|
3671
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_tenant ON public.plg_marketing_content_plans(tenant_id);
|
|
3672
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_account ON public.plg_marketing_content_plans(account_id);
|
|
3394
3673
|
|
|
3395
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
3674
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_content_posts (
|
|
3396
3675
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3397
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
3398
|
-
plan_id uuid NOT NULL REFERENCES public.
|
|
3676
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3677
|
+
plan_id uuid NOT NULL REFERENCES public.plg_marketing_content_plans(id) ON DELETE CASCADE,
|
|
3399
3678
|
week_number integer NOT NULL DEFAULT 1,
|
|
3400
3679
|
position integer NOT NULL DEFAULT 0,
|
|
3401
3680
|
title text NOT NULL,
|
|
@@ -3410,49 +3689,50 @@ CREATE TABLE IF NOT EXISTS public.mkt_content_posts (
|
|
|
3410
3689
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3411
3690
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3412
3691
|
);
|
|
3413
|
-
ALTER TABLE public.
|
|
3414
|
-
CREATE INDEX IF NOT EXISTS
|
|
3415
|
-
CREATE INDEX IF NOT EXISTS
|
|
3416
|
-
CREATE INDEX IF NOT EXISTS
|
|
3417
|
-
|
|
3418
|
-
-- RLS policies for
|
|
3419
|
-
DROP POLICY IF EXISTS
|
|
3420
|
-
DROP POLICY IF EXISTS
|
|
3421
|
-
DROP POLICY IF EXISTS
|
|
3422
|
-
DROP POLICY IF EXISTS
|
|
3423
|
-
CREATE POLICY
|
|
3424
|
-
CREATE POLICY
|
|
3425
|
-
CREATE POLICY
|
|
3426
|
-
CREATE POLICY
|
|
3427
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.
|
|
3428
|
-
|
|
3429
|
-
-- RLS policies for
|
|
3430
|
-
DROP POLICY IF EXISTS
|
|
3431
|
-
DROP POLICY IF EXISTS
|
|
3432
|
-
DROP POLICY IF EXISTS
|
|
3433
|
-
DROP POLICY IF EXISTS
|
|
3434
|
-
CREATE POLICY
|
|
3435
|
-
CREATE POLICY
|
|
3436
|
-
CREATE POLICY
|
|
3437
|
-
CREATE POLICY
|
|
3438
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.
|
|
3439
|
-
|
|
3440
|
-
-- RLS policies for
|
|
3441
|
-
DROP POLICY IF EXISTS
|
|
3442
|
-
DROP POLICY IF EXISTS
|
|
3443
|
-
DROP POLICY IF EXISTS
|
|
3444
|
-
DROP POLICY IF EXISTS
|
|
3445
|
-
CREATE POLICY
|
|
3446
|
-
CREATE POLICY
|
|
3447
|
-
CREATE POLICY
|
|
3448
|
-
CREATE POLICY
|
|
3449
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.
|
|
3692
|
+
ALTER TABLE public.plg_marketing_content_posts ENABLE ROW LEVEL SECURITY;
|
|
3693
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_tenant ON public.plg_marketing_content_posts(tenant_id);
|
|
3694
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_plan ON public.plg_marketing_content_posts(plan_id, week_number, position);
|
|
3695
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_status ON public.plg_marketing_content_posts(tenant_id, status);
|
|
3696
|
+
|
|
3697
|
+
-- RLS policies for plg_marketing_social_accounts
|
|
3698
|
+
DROP POLICY IF EXISTS plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts;
|
|
3699
|
+
DROP POLICY IF EXISTS plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts;
|
|
3700
|
+
DROP POLICY IF EXISTS plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts;
|
|
3701
|
+
DROP POLICY IF EXISTS plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts;
|
|
3702
|
+
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()));
|
|
3703
|
+
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()));
|
|
3704
|
+
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()));
|
|
3705
|
+
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()));
|
|
3706
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_social_accounts TO authenticated;
|
|
3707
|
+
|
|
3708
|
+
-- RLS policies for plg_marketing_content_plans
|
|
3709
|
+
DROP POLICY IF EXISTS plg_marketing_content_plans_select ON public.plg_marketing_content_plans;
|
|
3710
|
+
DROP POLICY IF EXISTS plg_marketing_content_plans_insert ON public.plg_marketing_content_plans;
|
|
3711
|
+
DROP POLICY IF EXISTS plg_marketing_content_plans_update ON public.plg_marketing_content_plans;
|
|
3712
|
+
DROP POLICY IF EXISTS plg_marketing_content_plans_delete ON public.plg_marketing_content_plans;
|
|
3713
|
+
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()));
|
|
3714
|
+
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()));
|
|
3715
|
+
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()));
|
|
3716
|
+
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()));
|
|
3717
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_plans TO authenticated;
|
|
3718
|
+
|
|
3719
|
+
-- RLS policies for plg_marketing_content_posts
|
|
3720
|
+
DROP POLICY IF EXISTS plg_marketing_content_posts_select ON public.plg_marketing_content_posts;
|
|
3721
|
+
DROP POLICY IF EXISTS plg_marketing_content_posts_insert ON public.plg_marketing_content_posts;
|
|
3722
|
+
DROP POLICY IF EXISTS plg_marketing_content_posts_update ON public.plg_marketing_content_posts;
|
|
3723
|
+
DROP POLICY IF EXISTS plg_marketing_content_posts_delete ON public.plg_marketing_content_posts;
|
|
3724
|
+
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()));
|
|
3725
|
+
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()));
|
|
3726
|
+
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()));
|
|
3727
|
+
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()));
|
|
3728
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_posts TO authenticated;
|
|
3729
|
+
`;
|
|
3450
3730
|
var MIGRATION_002_MULTI_PLATFORM = `-- Marketing Plugin: multi-platform accounts + per-post targets.
|
|
3451
3731
|
-- An account is a brand/profile that publishes to MANY platforms (Instagram,
|
|
3452
3732
|
-- YouTube, TikTok, ...). Posts optionally narrow the target platforms; an
|
|
3453
3733
|
-- empty array means "inherit the account's platforms".
|
|
3454
3734
|
|
|
3455
|
-
ALTER TABLE public.
|
|
3735
|
+
ALTER TABLE public.plg_marketing_social_accounts
|
|
3456
3736
|
ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{instagram}';
|
|
3457
3737
|
|
|
3458
3738
|
-- Backfill from the legacy single-platform column, then drop it (also drops
|
|
@@ -3461,26 +3741,27 @@ DO $$
|
|
|
3461
3741
|
BEGIN
|
|
3462
3742
|
IF EXISTS (
|
|
3463
3743
|
SELECT 1 FROM information_schema.columns
|
|
3464
|
-
WHERE table_schema = 'public' AND table_name = '
|
|
3744
|
+
WHERE table_schema = 'public' AND table_name = 'plg_marketing_social_accounts' AND column_name = 'platform'
|
|
3465
3745
|
) THEN
|
|
3466
|
-
UPDATE public.
|
|
3746
|
+
UPDATE public.plg_marketing_social_accounts
|
|
3467
3747
|
SET platforms = ARRAY[platform]
|
|
3468
3748
|
WHERE platform IS NOT NULL AND platforms = '{instagram}';
|
|
3469
|
-
ALTER TABLE public.
|
|
3749
|
+
ALTER TABLE public.plg_marketing_social_accounts DROP COLUMN platform;
|
|
3470
3750
|
END IF;
|
|
3471
3751
|
END $$;
|
|
3472
3752
|
|
|
3473
|
-
ALTER TABLE public.
|
|
3474
|
-
ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}'
|
|
3753
|
+
ALTER TABLE public.plg_marketing_content_posts
|
|
3754
|
+
ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';
|
|
3755
|
+
`;
|
|
3475
3756
|
var MIGRATION_003_RECORDING_OPS = `-- Marketing Plugin: recording-day ops on posts.
|
|
3476
3757
|
-- checklist: shooting checklist items [{ id, text, done }] \u2014 the on-set
|
|
3477
3758
|
-- companion for recording day. media_url: uploaded asset (static posts get a
|
|
3478
3759
|
-- caption + final art instead of a script).
|
|
3479
3760
|
|
|
3480
|
-
ALTER TABLE public.
|
|
3761
|
+
ALTER TABLE public.plg_marketing_content_posts
|
|
3481
3762
|
ADD COLUMN IF NOT EXISTS checklist jsonb NOT NULL DEFAULT '[]';
|
|
3482
3763
|
|
|
3483
|
-
ALTER TABLE public.
|
|
3764
|
+
ALTER TABLE public.plg_marketing_content_posts
|
|
3484
3765
|
ADD COLUMN IF NOT EXISTS media_url text;
|
|
3485
3766
|
|
|
3486
3767
|
-- Public media bucket for content assets (mirrors the avatars-bucket pattern:
|
|
@@ -3489,14 +3770,213 @@ INSERT INTO storage.buckets (id, name, public)
|
|
|
3489
3770
|
VALUES ('mkt-media', 'mkt-media', true)
|
|
3490
3771
|
ON CONFLICT (id) DO NOTHING;
|
|
3491
3772
|
|
|
3492
|
-
DROP POLICY IF EXISTS
|
|
3493
|
-
DROP POLICY IF EXISTS
|
|
3494
|
-
DROP POLICY IF EXISTS
|
|
3495
|
-
DROP POLICY IF EXISTS
|
|
3496
|
-
CREATE POLICY
|
|
3497
|
-
CREATE POLICY
|
|
3498
|
-
CREATE POLICY
|
|
3499
|
-
CREATE POLICY
|
|
3773
|
+
DROP POLICY IF EXISTS plg_marketing_media_read ON storage.objects;
|
|
3774
|
+
DROP POLICY IF EXISTS plg_marketing_media_insert ON storage.objects;
|
|
3775
|
+
DROP POLICY IF EXISTS plg_marketing_media_update ON storage.objects;
|
|
3776
|
+
DROP POLICY IF EXISTS plg_marketing_media_delete ON storage.objects;
|
|
3777
|
+
CREATE POLICY plg_marketing_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
|
|
3778
|
+
CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
|
|
3779
|
+
CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
|
|
3780
|
+
CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
|
|
3781
|
+
`;
|
|
3782
|
+
var MIGRATION_004_RANKLAYER = `-- Marketing Plugin: RankLayer integration (SEO content publishing).
|
|
3783
|
+
-- Control-plane state for the RankLayer connector rendered in Marketing \u2192
|
|
3784
|
+
-- Integra\xE7\xF5es. Prefix: plg_marketing_ranklayer_. Scaffold: the real sync
|
|
3785
|
+
-- (edge function + RankLayer API calls) lands via an external PR \u2014 see
|
|
3786
|
+
-- src/integrations/ranklayer/RANKLAYER.md.
|
|
3787
|
+
|
|
3788
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_integrations (
|
|
3789
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3790
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3791
|
+
api_key text,
|
|
3792
|
+
site_domain text,
|
|
3793
|
+
active boolean NOT NULL DEFAULT true,
|
|
3794
|
+
last_sync_at timestamptz,
|
|
3795
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3796
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3797
|
+
);
|
|
3798
|
+
ALTER TABLE public.plg_marketing_ranklayer_integrations ENABLE ROW LEVEL SECURITY;
|
|
3799
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_marketing_ranklayer_integrations_tenant
|
|
3800
|
+
ON public.plg_marketing_ranklayer_integrations(tenant_id);
|
|
3801
|
+
|
|
3802
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_sync_log (
|
|
3803
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3804
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3805
|
+
integration_id uuid REFERENCES public.plg_marketing_ranklayer_integrations(id) ON DELETE CASCADE,
|
|
3806
|
+
status text NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'partial', 'error')),
|
|
3807
|
+
message text,
|
|
3808
|
+
fetched integer NOT NULL DEFAULT 0,
|
|
3809
|
+
written integer NOT NULL DEFAULT 0,
|
|
3810
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
3811
|
+
);
|
|
3812
|
+
ALTER TABLE public.plg_marketing_ranklayer_sync_log ENABLE ROW LEVEL SECURITY;
|
|
3813
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_ranklayer_sync_log_tenant
|
|
3814
|
+
ON public.plg_marketing_ranklayer_sync_log(tenant_id, created_at DESC);
|
|
3815
|
+
|
|
3816
|
+
-- RLS: authenticated tenant CRUD
|
|
3817
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations;
|
|
3818
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations;
|
|
3819
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations;
|
|
3820
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations;
|
|
3821
|
+
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()));
|
|
3822
|
+
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()));
|
|
3823
|
+
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()));
|
|
3824
|
+
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()));
|
|
3825
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_ranklayer_integrations TO authenticated;
|
|
3826
|
+
|
|
3827
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log;
|
|
3828
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log;
|
|
3829
|
+
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()));
|
|
3830
|
+
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()));
|
|
3831
|
+
GRANT SELECT, INSERT ON public.plg_marketing_ranklayer_sync_log TO authenticated;
|
|
3832
|
+
`;
|
|
3833
|
+
var INTEGRATIONS = "plg_marketing_ranklayer_integrations";
|
|
3834
|
+
var SYNC_LOG = "plg_marketing_ranklayer_sync_log";
|
|
3835
|
+
function getClient2() {
|
|
3836
|
+
const supabase = core.getSupabaseClientOptional();
|
|
3837
|
+
if (!supabase) throw new Error("[ranklayer] Supabase not initialized");
|
|
3838
|
+
return supabase;
|
|
3839
|
+
}
|
|
3840
|
+
function requireTenantId2() {
|
|
3841
|
+
const tenantId = core.getActiveTenantId();
|
|
3842
|
+
if (!tenantId) throw new Error("[ranklayer] No active tenant");
|
|
3843
|
+
return tenantId;
|
|
3844
|
+
}
|
|
3845
|
+
function toIntegration(row) {
|
|
3846
|
+
return {
|
|
3847
|
+
id: row.id,
|
|
3848
|
+
apiKey: row.api_key ?? void 0,
|
|
3849
|
+
siteDomain: row.site_domain ?? void 0,
|
|
3850
|
+
active: row.active ?? true,
|
|
3851
|
+
lastSyncAt: row.last_sync_at ?? void 0
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
function createRankLayerProvider() {
|
|
3855
|
+
return {
|
|
3856
|
+
async getIntegration() {
|
|
3857
|
+
const tenantId = core.getActiveTenantId();
|
|
3858
|
+
if (!tenantId) return null;
|
|
3859
|
+
const { data } = await getClient2().from(INTEGRATIONS).select("*").eq("tenant_id", tenantId).maybeSingle();
|
|
3860
|
+
return data ? toIntegration(data) : null;
|
|
3861
|
+
},
|
|
3862
|
+
async saveIntegration(values) {
|
|
3863
|
+
const tenantId = requireTenantId2();
|
|
3864
|
+
const payload = {
|
|
3865
|
+
tenant_id: tenantId,
|
|
3866
|
+
api_key: values.apiKey,
|
|
3867
|
+
site_domain: values.siteDomain ?? null,
|
|
3868
|
+
active: true,
|
|
3869
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3870
|
+
};
|
|
3871
|
+
const { data, error } = await getClient2().from(INTEGRATIONS).upsert(payload, { onConflict: "tenant_id" }).select("*").single();
|
|
3872
|
+
if (error) throw error;
|
|
3873
|
+
return toIntegration(data);
|
|
3874
|
+
},
|
|
3875
|
+
async disconnect() {
|
|
3876
|
+
const tenantId = core.getActiveTenantId();
|
|
3877
|
+
if (!tenantId) return;
|
|
3878
|
+
await getClient2().from(INTEGRATIONS).delete().eq("tenant_id", tenantId);
|
|
3879
|
+
},
|
|
3880
|
+
// SCAFFOLD: no RankLayer API yet — accept any non-empty key so the connect
|
|
3881
|
+
// flow can be exercised. Replace with a real credential check in the PR.
|
|
3882
|
+
async testConnection(values) {
|
|
3883
|
+
if (!values.apiKey || values.apiKey.trim().length < 8) {
|
|
3884
|
+
return { ok: false, message: "Informe uma API Key v\xE1lida da RankLayer." };
|
|
3885
|
+
}
|
|
3886
|
+
return { ok: true, message: "Chave aceita (valida\xE7\xE3o real pendente \u2014 integra\xE7\xE3o em prepara\xE7\xE3o)." };
|
|
3887
|
+
},
|
|
3888
|
+
async getSyncLog(integrationId) {
|
|
3889
|
+
const { data } = await getClient2().from(SYNC_LOG).select("*").eq("integration_id", integrationId).order("created_at", { ascending: false }).limit(20);
|
|
3890
|
+
return (data ?? []).map((r) => ({
|
|
3891
|
+
id: r.id,
|
|
3892
|
+
createdAt: r.created_at,
|
|
3893
|
+
status: r.status,
|
|
3894
|
+
message: r.message ?? void 0,
|
|
3895
|
+
fetched: r.fetched ?? 0,
|
|
3896
|
+
written: r.written ?? 0
|
|
3897
|
+
}));
|
|
3898
|
+
}
|
|
3899
|
+
};
|
|
3900
|
+
}
|
|
3901
|
+
var provider = createRankLayerProvider();
|
|
3902
|
+
function RankLayerExtraPanel() {
|
|
3903
|
+
const [integration, setIntegration] = React8.useState(null);
|
|
3904
|
+
const [log, setLog] = React8.useState([]);
|
|
3905
|
+
const [loaded, setLoaded] = React8.useState(false);
|
|
3906
|
+
React8.useEffect(() => {
|
|
3907
|
+
let cancelled = false;
|
|
3908
|
+
(async () => {
|
|
3909
|
+
try {
|
|
3910
|
+
const found = await provider.getIntegration();
|
|
3911
|
+
if (cancelled) return;
|
|
3912
|
+
setIntegration(found);
|
|
3913
|
+
if (found) setLog(await provider.getSyncLog(found.id));
|
|
3914
|
+
} catch {
|
|
3915
|
+
} finally {
|
|
3916
|
+
if (!cancelled) setLoaded(true);
|
|
3917
|
+
}
|
|
3918
|
+
})();
|
|
3919
|
+
return () => {
|
|
3920
|
+
cancelled = true;
|
|
3921
|
+
};
|
|
3922
|
+
}, []);
|
|
3923
|
+
if (!loaded || !integration) return null;
|
|
3924
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4 border-t pt-4", children: [
|
|
3925
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3 rounded-md border border-dashed p-3", children: [
|
|
3926
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
3927
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
3928
|
+
/* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold", children: "Sincroniza\xE7\xE3o de conte\xFAdo \u2014 em breve" }),
|
|
3929
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
3930
|
+
"A publica\xE7\xE3o autom\xE1tica de p\xE1ginas e artigos de SEO com a RankLayer est\xE1 em prepara\xE7\xE3o.",
|
|
3931
|
+
integration.siteDomain ? ` Dom\xEDnio configurado: ${integration.siteDomain}.` : "",
|
|
3932
|
+
" Assim que a integra\xE7\xE3o estiver ativa, o hist\xF3rico de sincroniza\xE7\xF5es aparece aqui."
|
|
3933
|
+
] })
|
|
3934
|
+
] })
|
|
3935
|
+
] }),
|
|
3936
|
+
log.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
3937
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
3938
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.History, { className: "h-4 w-4 text-muted-foreground" }),
|
|
3939
|
+
/* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold", children: "Hist\xF3rico" })
|
|
3940
|
+
] }),
|
|
3941
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y rounded-md border text-sm", children: log.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [
|
|
3942
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: new Date(r.createdAt).toLocaleString("pt-BR") }),
|
|
3943
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-auto text-xs", children: [
|
|
3944
|
+
r.written,
|
|
3945
|
+
"/",
|
|
3946
|
+
r.fetched,
|
|
3947
|
+
" publicadas"
|
|
3948
|
+
] }),
|
|
3949
|
+
r.status === "success" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
|
|
3950
|
+
] }, r.id)) })
|
|
3951
|
+
] })
|
|
3952
|
+
] });
|
|
3953
|
+
}
|
|
3954
|
+
var ranklayerConnectorDef = {
|
|
3955
|
+
id: "ranklayer",
|
|
3956
|
+
hostPluginId: "marketing",
|
|
3957
|
+
name: "RankLayer",
|
|
3958
|
+
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.",
|
|
3959
|
+
icon: "Search",
|
|
3960
|
+
authKind: "api-key",
|
|
3961
|
+
fields: [
|
|
3962
|
+
{ key: "apiKey", label: "API Key", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" },
|
|
3963
|
+
{ key: "siteDomain", label: "Dom\xEDnio do site", type: "text", placeholder: "exemplo.com.br" }
|
|
3964
|
+
],
|
|
3965
|
+
async getStatus() {
|
|
3966
|
+
try {
|
|
3967
|
+
const found = await provider.getIntegration();
|
|
3968
|
+
return { connected: !!found?.apiKey, detail: found?.siteDomain };
|
|
3969
|
+
} catch {
|
|
3970
|
+
return { connected: false };
|
|
3971
|
+
}
|
|
3972
|
+
},
|
|
3973
|
+
testConnection: (values) => provider.testConnection({ apiKey: values.apiKey }),
|
|
3974
|
+
saveConnection: async (values) => {
|
|
3975
|
+
await provider.saveIntegration({ apiKey: values.apiKey, siteDomain: values.siteDomain });
|
|
3976
|
+
},
|
|
3977
|
+
disconnect: () => provider.disconnect(),
|
|
3978
|
+
ExtraPanel: RankLayerExtraPanel
|
|
3979
|
+
};
|
|
3500
3980
|
|
|
3501
3981
|
// src/locales/en.ts
|
|
3502
3982
|
var en = {
|
|
@@ -3856,6 +4336,8 @@ var DEFAULT_LABELS = {
|
|
|
3856
4336
|
funnel: "Funnel",
|
|
3857
4337
|
landingPages: "Landing pages",
|
|
3858
4338
|
content: "Content",
|
|
4339
|
+
blog: "Blog",
|
|
4340
|
+
blogCategories: "Categorias",
|
|
3859
4341
|
settings: "Settings"
|
|
3860
4342
|
};
|
|
3861
4343
|
function resolveConfig(options) {
|
|
@@ -3877,9 +4359,9 @@ function createSafeProvider(config, domain) {
|
|
|
3877
4359
|
function createMarketingPlugin(options) {
|
|
3878
4360
|
core.registerTranslations(marketingLocales);
|
|
3879
4361
|
const { config, domain } = resolveConfig(options);
|
|
3880
|
-
const
|
|
3881
|
-
const store = createMarketingStore(
|
|
3882
|
-
const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store });
|
|
4362
|
+
const provider2 = options?.dataProvider ?? createSafeProvider(config, domain);
|
|
4363
|
+
const store = createMarketingStore(provider2);
|
|
4364
|
+
const dashboardWidgets = createMarketingDashboardWidgets({ config, provider: provider2, store });
|
|
3883
4365
|
const contentProvider = options?.contentPlanner?.dataProvider ?? core.createSafeDataProvider(
|
|
3884
4366
|
() => createSupabaseContentPlannerProvider(),
|
|
3885
4367
|
() => createMockContentPlannerProvider()
|
|
@@ -3887,8 +4369,8 @@ function createMarketingPlugin(options) {
|
|
|
3887
4369
|
const contentStore = createContentPlannerStore(contentProvider);
|
|
3888
4370
|
const PageComponent = () => React8__default.default.createElement(
|
|
3889
4371
|
MarketingContextProvider,
|
|
3890
|
-
{ config, provider, store },
|
|
3891
|
-
React8__default.default.createElement(MarketingPage, { config, provider, store, contentProvider, contentStore })
|
|
4372
|
+
{ config, provider: provider2, store },
|
|
4373
|
+
React8__default.default.createElement(MarketingPage, { config, provider: provider2, store, contentProvider, contentStore })
|
|
3892
4374
|
);
|
|
3893
4375
|
PageComponent.displayName = "MarketingPage";
|
|
3894
4376
|
const marketingRegistries = [
|
|
@@ -3897,12 +4379,14 @@ function createMarketingPlugin(options) {
|
|
|
3897
4379
|
];
|
|
3898
4380
|
const SettingsComponent = () => React8__default.default.createElement(
|
|
3899
4381
|
MarketingContextProvider,
|
|
3900
|
-
{ config, provider, store },
|
|
4382
|
+
{ config, provider: provider2, store },
|
|
3901
4383
|
React8__default.default.createElement(saas.PluginSettingsPanel, {
|
|
3902
4384
|
title: config.labels.settings ?? config.labels.pageTitle,
|
|
3903
4385
|
subtitle: config.labels.pageSubtitle,
|
|
3904
4386
|
generalSettings: React8__default.default.createElement(SettingsView),
|
|
3905
4387
|
registries: marketingRegistries,
|
|
4388
|
+
// Enables the "Integrações" tab (renders manifest connectors for this host).
|
|
4389
|
+
hostPluginId: "marketing",
|
|
3906
4390
|
routeBase: "/settings/marketing"
|
|
3907
4391
|
})
|
|
3908
4392
|
);
|
|
@@ -3914,12 +4398,16 @@ function createMarketingPlugin(options) {
|
|
|
3914
4398
|
version: "1.0.0",
|
|
3915
4399
|
scope: options?.scope ?? "universal",
|
|
3916
4400
|
verticalId: options?.verticalId,
|
|
4401
|
+
// Admin content-planning surface (campaigns, content calendar) — targets the
|
|
4402
|
+
// saas/admin Panel world, not the ecommerce storefront/operator console.
|
|
4403
|
+
scaffolds: ["saas"],
|
|
3917
4404
|
defaultEnabled: true,
|
|
3918
4405
|
dependencies: [],
|
|
3919
4406
|
declaredFeatures: [
|
|
3920
4407
|
{ id: "marketing", label: config.labels.pageTitle, group: "Engage" },
|
|
3921
4408
|
...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : [],
|
|
3922
|
-
...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : []
|
|
4409
|
+
...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : [],
|
|
4410
|
+
...config.modules.blog ? [{ id: "marketing.blog", label: config.labels.blog, group: "Engage" }] : []
|
|
3923
4411
|
],
|
|
3924
4412
|
navigation: [
|
|
3925
4413
|
{
|
|
@@ -3950,11 +4438,17 @@ function createMarketingPlugin(options) {
|
|
|
3950
4438
|
{ name: "marketing.content.post.updated", description: "A content post was updated" }
|
|
3951
4439
|
],
|
|
3952
4440
|
migrations: [
|
|
4441
|
+
{
|
|
4442
|
+
id: "marketing-000-plg-rename",
|
|
4443
|
+
version: "1.0.0",
|
|
4444
|
+
sql: MIGRATION_000_PLG_RENAME,
|
|
4445
|
+
description: "Rename legacy mkt_* tables to plg_marketing_* in-place for pools provisioned before the industry-pool rename (guarded no-op on fresh pools)"
|
|
4446
|
+
},
|
|
3953
4447
|
{
|
|
3954
4448
|
id: "marketing-001-content-planner",
|
|
3955
4449
|
version: "1.0.0",
|
|
3956
4450
|
sql: MIGRATION_001_CONTENT_PLANNER,
|
|
3957
|
-
description: "Create
|
|
4451
|
+
description: "Create plg_marketing_social_accounts, plg_marketing_content_plans and plg_marketing_content_posts"
|
|
3958
4452
|
},
|
|
3959
4453
|
{
|
|
3960
4454
|
id: "marketing-002-multi-platform",
|
|
@@ -3966,9 +4460,18 @@ function createMarketingPlugin(options) {
|
|
|
3966
4460
|
id: "marketing-003-recording-ops",
|
|
3967
4461
|
version: "1.2.0",
|
|
3968
4462
|
sql: MIGRATION_003_RECORDING_OPS,
|
|
3969
|
-
description: "Recording-day checklist + media asset URL on posts;
|
|
4463
|
+
description: "Recording-day checklist + media asset URL on posts; content-media storage bucket"
|
|
4464
|
+
},
|
|
4465
|
+
{
|
|
4466
|
+
id: "marketing-004-ranklayer",
|
|
4467
|
+
version: "1.3.0",
|
|
4468
|
+
sql: MIGRATION_004_RANKLAYER,
|
|
4469
|
+
description: "RankLayer connector state: plg_marketing_ranklayer_integrations + _sync_log (tenant RLS)"
|
|
3970
4470
|
}
|
|
3971
4471
|
],
|
|
4472
|
+
// RankLayer SEO connector — renders in the Integrações settings tab. Scaffold
|
|
4473
|
+
// (control plane only); the real sync lands via an external PR (RANKLAYER.md).
|
|
4474
|
+
connectors: [ranklayerConnectorDef],
|
|
3972
4475
|
aiTools: [
|
|
3973
4476
|
{
|
|
3974
4477
|
id: "marketing.channel-performance",
|