feat: restaurar todas las secciones en Creatividades Ads con sincronización bidireccional en tiempo real con Zona de Marca
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* GET / PUT brandbook for active tenant (documents type=brandbook).
|
* GET / PUT brandbook for active tenant (documents type=brandbook).
|
||||||
|
* Sincronización bidireccional permanente con tenants.creative_style.
|
||||||
*/
|
*/
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createClient } from '@/lib/supabase/server';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
|
import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
|
||||||
import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency';
|
import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency';
|
||||||
|
import { createClient as createAdminClient } from '@supabase/supabase-js';
|
||||||
import {
|
import {
|
||||||
BRANDBOOK_DOC_TITLE,
|
BRANDBOOK_DOC_TITLE,
|
||||||
BRANDBOOK_DOC_TYPE,
|
BRANDBOOK_DOC_TYPE,
|
||||||
@@ -17,6 +19,12 @@ import {
|
|||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
function getAdmin() {
|
||||||
|
const url = supabaseUrl();
|
||||||
|
const key = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||||
|
return createAdminClient(url, key);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadDoc(tenantId: string): Promise<{
|
async function loadDoc(tenantId: string): Promise<{
|
||||||
id: string | null;
|
id: string | null;
|
||||||
book: Brandbook | null;
|
book: Brandbook | null;
|
||||||
@@ -31,7 +39,7 @@ async function loadDoc(tenantId: string): Promise<{
|
|||||||
return { id: row.id, book: parseBrandbook(row.content) };
|
return { id: row.id, book: parseBrandbook(row.content) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(req: NextRequest) {
|
||||||
const supabase = await createClient();
|
const supabase = await createClient();
|
||||||
const {
|
const {
|
||||||
data: { user },
|
data: { user },
|
||||||
@@ -39,22 +47,48 @@ export async function GET() {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
const ctx = await resolveActiveTenant(user);
|
|
||||||
if (!ctx.tenantId || ctx.portfolioWide) {
|
const isAgency = isAgencyRole(userRole(user));
|
||||||
return NextResponse.json(
|
const queryTenantId = req.nextUrl.searchParams.get('tenant_id') || req.nextUrl.searchParams.get('tenantId');
|
||||||
{ error: 'Selecciona un cliente en Contexto.' },
|
|
||||||
{ status: 400 }
|
let tenantId: string | null = null;
|
||||||
);
|
let label = 'Cliente';
|
||||||
|
let slug = 'cliente';
|
||||||
|
|
||||||
|
if (isAgency && queryTenantId) {
|
||||||
|
const admin = getAdmin();
|
||||||
|
const { data: tenant } = await admin
|
||||||
|
.from('tenants')
|
||||||
|
.select('id, name, slug')
|
||||||
|
.eq('id', queryTenantId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (tenant) {
|
||||||
|
tenantId = tenant.id;
|
||||||
|
label = tenant.name || 'Cliente';
|
||||||
|
slug = tenant.slug || 'cliente';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let slug = 'cliente';
|
if (!tenantId) {
|
||||||
const t = await restGet<{ slug?: string }[]>(
|
const ctx = await resolveActiveTenant(user);
|
||||||
`/rest/v1/tenants?id=eq.${encodeURIComponent(ctx.tenantId)}&select=slug`
|
if (!ctx.tenantId || ctx.portfolioWide) {
|
||||||
);
|
return NextResponse.json(
|
||||||
slug = t.data?.[0]?.slug || slug;
|
{ error: 'Selecciona un cliente en Contexto.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tenantId = ctx.tenantId;
|
||||||
|
label = ctx.label;
|
||||||
|
|
||||||
const { id, book } = await loadDoc(ctx.tenantId);
|
const t = await restGet<{ slug?: string }[]>(
|
||||||
const current = book || emptyBrandbook(ctx.label);
|
`/rest/v1/tenants?id=eq.${encodeURIComponent(tenantId)}&select=slug`
|
||||||
|
);
|
||||||
|
slug = t.data?.[0]?.slug || slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, book } = await loadDoc(tenantId);
|
||||||
|
const current = book || emptyBrandbook(label);
|
||||||
const completeness = brandbookCompleteness(current);
|
const completeness = brandbookCompleteness(current);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -62,7 +96,7 @@ export async function GET() {
|
|||||||
id,
|
id,
|
||||||
brandbook: current,
|
brandbook: current,
|
||||||
slug,
|
slug,
|
||||||
label: ctx.label,
|
label,
|
||||||
completeness,
|
completeness,
|
||||||
exists: !!book,
|
exists: !!book,
|
||||||
});
|
});
|
||||||
@@ -79,28 +113,59 @@ export async function PUT(req: NextRequest) {
|
|||||||
if (!isAgencyRole(userRole(user))) {
|
if (!isAgencyRole(userRole(user))) {
|
||||||
return NextResponse.json({ error: 'Solo agencia' }, { status: 403 });
|
return NextResponse.json({ error: 'Solo agencia' }, { status: 403 });
|
||||||
}
|
}
|
||||||
const ctx = await resolveActiveTenant(user);
|
|
||||||
if (!ctx.tenantId || ctx.portfolioWide) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Selecciona un cliente en Contexto.' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = (await req.json().catch(() => ({}))) as {
|
const body = (await req.json().catch(() => ({}))) as {
|
||||||
brandbook?: unknown;
|
brandbook?: unknown;
|
||||||
seed?: boolean;
|
seed?: boolean;
|
||||||
|
tenant_id?: string;
|
||||||
|
tenantId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queryTenantId =
|
||||||
|
req.nextUrl.searchParams.get('tenant_id') ||
|
||||||
|
req.nextUrl.searchParams.get('tenantId') ||
|
||||||
|
body.tenant_id ||
|
||||||
|
body.tenantId;
|
||||||
|
|
||||||
|
let tenantId: string | null = null;
|
||||||
|
let label = 'Cliente';
|
||||||
let slug = 'cliente';
|
let slug = 'cliente';
|
||||||
const t = await restGet<{ slug?: string }[]>(
|
|
||||||
`/rest/v1/tenants?id=eq.${encodeURIComponent(ctx.tenantId)}&select=slug`
|
if (queryTenantId) {
|
||||||
);
|
const admin = getAdmin();
|
||||||
slug = t.data?.[0]?.slug || slug;
|
const { data: tenant } = await admin
|
||||||
|
.from('tenants')
|
||||||
|
.select('id, name, slug')
|
||||||
|
.eq('id', queryTenantId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (tenant) {
|
||||||
|
tenantId = tenant.id;
|
||||||
|
label = tenant.name || 'Cliente';
|
||||||
|
slug = tenant.slug || 'cliente';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tenantId) {
|
||||||
|
const ctx = await resolveActiveTenant(user);
|
||||||
|
if (!ctx.tenantId || ctx.portfolioWide) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Selecciona un cliente en Contexto.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tenantId = ctx.tenantId;
|
||||||
|
label = ctx.label;
|
||||||
|
|
||||||
|
const t = await restGet<{ slug?: string }[]>(
|
||||||
|
`/rest/v1/tenants?id=eq.${encodeURIComponent(tenantId)}&select=slug`
|
||||||
|
);
|
||||||
|
slug = t.data?.[0]?.slug || slug;
|
||||||
|
}
|
||||||
|
|
||||||
let book: Brandbook | null = null;
|
let book: Brandbook | null = null;
|
||||||
if (body.seed) {
|
if (body.seed) {
|
||||||
book = seedBrandbook(slug, ctx.label);
|
book = seedBrandbook(slug, label);
|
||||||
} else {
|
} else {
|
||||||
book = parseBrandbook(body.brandbook);
|
book = parseBrandbook(body.brandbook);
|
||||||
}
|
}
|
||||||
@@ -108,68 +173,118 @@ export async function PUT(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'brandbook inválido' }, { status: 400 });
|
return NextResponse.json({ error: 'brandbook inválido' }, { status: 400 });
|
||||||
}
|
}
|
||||||
book.updated_at = new Date().toISOString();
|
book.updated_at = new Date().toISOString();
|
||||||
book.brand_name = book.brand_name || ctx.label;
|
book.brand_name = book.brand_name || label;
|
||||||
|
|
||||||
const headers = serviceHeaders();
|
const admin = getAdmin();
|
||||||
const base = supabaseUrl();
|
const { id } = await loadDoc(tenantId);
|
||||||
if (!headers || !base) {
|
|
||||||
return NextResponse.json({ error: 'No service role' }, { status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await loadDoc(ctx.tenantId);
|
let docId = id;
|
||||||
if (id) {
|
if (id) {
|
||||||
const patch = await fetch(`${base}/rest/v1/documents?id=eq.${id}`, {
|
const { error: patchErr } = await admin
|
||||||
method: 'PATCH',
|
.from('documents')
|
||||||
headers: {
|
.update({
|
||||||
...headers,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Prefer: 'return=representation',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
content: book,
|
content: book,
|
||||||
title: BRANDBOOK_DOC_TITLE,
|
title: BRANDBOOK_DOC_TITLE,
|
||||||
updated_at: book.updated_at,
|
updated_at: book.updated_at,
|
||||||
}),
|
})
|
||||||
});
|
.eq('id', id);
|
||||||
if (!patch.ok) {
|
|
||||||
return NextResponse.json(
|
if (patchErr) {
|
||||||
{ error: await patch.text() },
|
return NextResponse.json({ error: patchErr.message }, { status: 500 });
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const rows = (await patch.json()) as { id: string }[];
|
} else {
|
||||||
return NextResponse.json({
|
const { data: insRow, error: insErr } = await admin
|
||||||
ok: true,
|
.from('documents')
|
||||||
id: rows[0]?.id || id,
|
.insert({
|
||||||
brandbook: book,
|
tenant_id: tenantId,
|
||||||
completeness: brandbookCompleteness(book),
|
type: BRANDBOOK_DOC_TYPE,
|
||||||
});
|
title: BRANDBOOK_DOC_TITLE,
|
||||||
|
status: 'approved',
|
||||||
|
content: book,
|
||||||
|
updated_at: book.updated_at,
|
||||||
|
})
|
||||||
|
.select('id')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (insErr) {
|
||||||
|
return NextResponse.json({ error: insErr.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
docId = insRow.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ins = await fetch(`${base}/rest/v1/documents`, {
|
// Sincronizar simultáneamente con tenants.creative_style para consistencia 100%
|
||||||
method: 'POST',
|
const { data: tenantRow } = await admin
|
||||||
headers: {
|
.from('tenants')
|
||||||
...headers,
|
.select('creative_style, settings')
|
||||||
'Content-Type': 'application/json',
|
.eq('id', tenantId)
|
||||||
Prefer: 'return=representation',
|
.maybeSingle();
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
if (tenantRow) {
|
||||||
tenant_id: ctx.tenantId,
|
const existingStyle = (tenantRow.creative_style && typeof tenantRow.creative_style === 'object') ? tenantRow.creative_style : {};
|
||||||
type: BRANDBOOK_DOC_TYPE,
|
const updatedStyle = {
|
||||||
title: BRANDBOOK_DOC_TITLE,
|
...existingStyle,
|
||||||
status: 'approved',
|
identity: {
|
||||||
content: book,
|
...(existingStyle.identity || {}),
|
||||||
}),
|
summary: book.elevator_pitch || existingStyle.identity?.summary || '',
|
||||||
});
|
website: book.website || existingStyle.identity?.website || '',
|
||||||
if (!ins.ok) {
|
b2b_offering: book.b2b_offering || existingStyle.identity?.b2b_offering || '',
|
||||||
return NextResponse.json({ error: await ins.text() }, { status: 500 });
|
b2c_offering: book.b2c_offering || existingStyle.identity?.b2c_offering || '',
|
||||||
|
usp_differentiators: book.usp_differentiators || existingStyle.identity?.usp_differentiators || '',
|
||||||
|
current_priorities: book.current_priorities || existingStyle.identity?.current_priorities || '',
|
||||||
|
price_and_offer_framing: book.price_and_offer_framing || existingStyle.identity?.price_and_offer_framing || '',
|
||||||
|
},
|
||||||
|
voice: {
|
||||||
|
...(existingStyle.voice || {}),
|
||||||
|
tone: book.tone || existingStyle.voice?.tone || '',
|
||||||
|
copy_structure_rules: book.copy_structure_rules || existingStyle.voice?.copy_structure_rules || '',
|
||||||
|
english_terms_policy: book.english_terms_policy || existingStyle.voice?.english_terms_policy || '',
|
||||||
|
power_words: book.power_words?.length ? book.power_words : existingStyle.voice?.power_words || [],
|
||||||
|
allowed_ctas: book.allowed_ctas?.length ? book.allowed_ctas : existingStyle.voice?.allowed_ctas || [],
|
||||||
|
},
|
||||||
|
visual: {
|
||||||
|
...(existingStyle.visual || {}),
|
||||||
|
surface_rules: book.surface_rules || existingStyle.visual?.surface_rules || '',
|
||||||
|
image_text_rules: book.image_text_rules || existingStyle.visual?.image_text_rules || '',
|
||||||
|
typography_headline: book.typography_headline || existingStyle.visual?.typography_headline || '',
|
||||||
|
typography_body: book.typography_body || existingStyle.visual?.typography_body || '',
|
||||||
|
},
|
||||||
|
audiences: {
|
||||||
|
...(existingStyle.audiences || {}),
|
||||||
|
b2b_personas: book.b2b_personas?.length ? book.b2b_personas : existingStyle.audiences?.b2b_personas || [],
|
||||||
|
b2c_personas: book.b2c_personas?.length ? book.b2c_personas : existingStyle.audiences?.b2c_personas || [],
|
||||||
|
customer_frustrations: book.customer_frustrations || existingStyle.audiences?.customer_frustrations || '',
|
||||||
|
desired_transformation: book.desired_transformation || existingStyle.audiences?.desired_transformation || '',
|
||||||
|
},
|
||||||
|
arguments: {
|
||||||
|
...(existingStyle.arguments || {}),
|
||||||
|
objections: book.objections?.length ? book.objections : existingStyle.arguments?.objections || [],
|
||||||
|
winning_hooks: book.winning_hooks?.length ? book.winning_hooks : existingStyle.arguments?.winning_hooks || [],
|
||||||
|
preferred_angles: book.preferred_angles?.length ? book.preferred_angles : existingStyle.arguments?.preferred_angles || [],
|
||||||
|
},
|
||||||
|
prohibitions: {
|
||||||
|
...(existingStyle.prohibitions || {}),
|
||||||
|
emoji_policy: book.emoji_policy || existingStyle.prohibitions?.emoji_policy || '',
|
||||||
|
testimonials_policy: book.testimonials_policy || existingStyle.prohibitions?.testimonials_policy || '',
|
||||||
|
competitors_never_mention: book.competitors_never_mention?.length ? book.competitors_never_mention : existingStyle.prohibitions?.competitors_never_mention || [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await admin
|
||||||
|
.from('tenants')
|
||||||
|
.update({
|
||||||
|
creative_style: updatedStyle,
|
||||||
|
settings: {
|
||||||
|
...(tenantRow.settings || {}),
|
||||||
|
creative_style: updatedStyle,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.eq('id', tenantId);
|
||||||
}
|
}
|
||||||
const rows = (await ins.json()) as { id: string }[];
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
id: rows[0]?.id,
|
id: docId,
|
||||||
brandbook: book,
|
brandbook: book,
|
||||||
completeness: brandbookCompleteness(book),
|
completeness: brandbookCompleteness(book),
|
||||||
created: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,10 +241,10 @@ export async function PUT(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: updErr.message }, { status: 500 });
|
return NextResponse.json({ error: updErr.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cargar brandbook de documents para compilar skill actualizado
|
// Cargar brandbook de documents para compilar skill actualizado y mantener sincronización
|
||||||
const { data: brandDoc } = await admin
|
const { data: brandDoc } = await admin
|
||||||
.from('documents')
|
.from('documents')
|
||||||
.select('content')
|
.select('id, content')
|
||||||
.eq('tenant_id', tenantId)
|
.eq('tenant_id', tenantId)
|
||||||
.eq('type', 'brandbook')
|
.eq('type', 'brandbook')
|
||||||
.order('updated_at', { ascending: false })
|
.order('updated_at', { ascending: false })
|
||||||
@@ -267,6 +267,60 @@ export async function PUT(req: NextRequest) {
|
|||||||
brandbook = seedBrandbook(existing.slug || 'cliente', existing.name || 'Cliente');
|
brandbook = seedBrandbook(existing.slug || 'cliente', existing.name || 'Cliente');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sincronizar campos de normalizedStyle hacia brandbook
|
||||||
|
if (normalizedStyle.identity) {
|
||||||
|
if (normalizedStyle.identity.summary) brandbook.elevator_pitch = normalizedStyle.identity.summary;
|
||||||
|
if (normalizedStyle.identity.website) brandbook.website = normalizedStyle.identity.website;
|
||||||
|
if (normalizedStyle.identity.b2b_offering) brandbook.b2b_offering = normalizedStyle.identity.b2b_offering;
|
||||||
|
if (normalizedStyle.identity.b2c_offering) brandbook.b2c_offering = normalizedStyle.identity.b2c_offering;
|
||||||
|
if (normalizedStyle.identity.usp_differentiators) brandbook.usp_differentiators = normalizedStyle.identity.usp_differentiators;
|
||||||
|
if (normalizedStyle.identity.current_priorities) brandbook.current_priorities = normalizedStyle.identity.current_priorities;
|
||||||
|
if (normalizedStyle.identity.price_and_offer_framing) brandbook.price_and_offer_framing = normalizedStyle.identity.price_and_offer_framing;
|
||||||
|
}
|
||||||
|
if (normalizedStyle.voice) {
|
||||||
|
if (normalizedStyle.voice.tone) brandbook.tone = normalizedStyle.voice.tone;
|
||||||
|
if (normalizedStyle.voice.copy_structure_rules) brandbook.copy_structure_rules = normalizedStyle.voice.copy_structure_rules;
|
||||||
|
if (normalizedStyle.voice.english_terms_policy) brandbook.english_terms_policy = normalizedStyle.voice.english_terms_policy;
|
||||||
|
if (normalizedStyle.voice.power_words?.length) brandbook.power_words = normalizedStyle.voice.power_words;
|
||||||
|
if (normalizedStyle.voice.allowed_ctas?.length) brandbook.allowed_ctas = normalizedStyle.voice.allowed_ctas;
|
||||||
|
}
|
||||||
|
if (normalizedStyle.visual) {
|
||||||
|
if (normalizedStyle.visual.surface_rules) brandbook.surface_rules = normalizedStyle.visual.surface_rules;
|
||||||
|
if (normalizedStyle.visual.image_text_rules) brandbook.image_text_rules = normalizedStyle.visual.image_text_rules;
|
||||||
|
if (normalizedStyle.visual.typography_headline) brandbook.typography_headline = normalizedStyle.visual.typography_headline;
|
||||||
|
if (normalizedStyle.visual.typography_body) brandbook.typography_body = normalizedStyle.visual.typography_body;
|
||||||
|
}
|
||||||
|
if (normalizedStyle.audiences) {
|
||||||
|
if (normalizedStyle.audiences.b2b_personas?.length) brandbook.b2b_personas = normalizedStyle.audiences.b2b_personas;
|
||||||
|
if (normalizedStyle.audiences.b2c_personas?.length) brandbook.b2c_personas = normalizedStyle.audiences.b2c_personas;
|
||||||
|
if (normalizedStyle.audiences.customer_frustrations) brandbook.customer_frustrations = normalizedStyle.audiences.customer_frustrations;
|
||||||
|
if (normalizedStyle.audiences.desired_transformation) brandbook.desired_transformation = normalizedStyle.audiences.desired_transformation;
|
||||||
|
}
|
||||||
|
if (normalizedStyle.arguments) {
|
||||||
|
if (normalizedStyle.arguments.objections?.length) brandbook.objections = normalizedStyle.arguments.objections;
|
||||||
|
if (normalizedStyle.arguments.winning_hooks?.length) brandbook.winning_hooks = normalizedStyle.arguments.winning_hooks;
|
||||||
|
if (normalizedStyle.arguments.preferred_angles?.length) brandbook.preferred_angles = normalizedStyle.arguments.preferred_angles;
|
||||||
|
}
|
||||||
|
if (normalizedStyle.prohibitions) {
|
||||||
|
if (normalizedStyle.prohibitions.emoji_policy) brandbook.emoji_policy = normalizedStyle.prohibitions.emoji_policy;
|
||||||
|
if (normalizedStyle.prohibitions.testimonials_policy) brandbook.testimonials_policy = normalizedStyle.prohibitions.testimonials_policy;
|
||||||
|
if (normalizedStyle.prohibitions.competitors_never_mention?.length) brandbook.competitors_never_mention = normalizedStyle.prohibitions.competitors_never_mention;
|
||||||
|
}
|
||||||
|
if (normalizedStyle.image_ai?.photo_style) {
|
||||||
|
brandbook.photography_style = normalizedStyle.image_ai.photo_style;
|
||||||
|
}
|
||||||
|
brandbook.updated_at = new Date().toISOString();
|
||||||
|
|
||||||
|
if (brandDoc?.id) {
|
||||||
|
await admin
|
||||||
|
.from('documents')
|
||||||
|
.update({
|
||||||
|
content: brandbook,
|
||||||
|
updated_at: brandbook.updated_at,
|
||||||
|
})
|
||||||
|
.eq('id', brandDoc.id);
|
||||||
|
}
|
||||||
|
|
||||||
const compiledMarkdown = compileClientSkillFromBrandbook(
|
const compiledMarkdown = compileClientSkillFromBrandbook(
|
||||||
brandbook,
|
brandbook,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,5 +32,5 @@ export default async function MarcaPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <BrandZone tenantLabel={ctx.label} />;
|
return <BrandZone tenantId={ctx.tenantId || ''} tenantLabel={ctx.label} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,15 @@ function KitThumb({ assetFileId, alt }: { assetFileId: string; alt: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
export default function BrandZone({
|
||||||
|
tenantId,
|
||||||
|
tenantLabel,
|
||||||
|
tenantSlug,
|
||||||
|
}: {
|
||||||
|
tenantId?: string;
|
||||||
|
tenantLabel: string;
|
||||||
|
tenantSlug?: string;
|
||||||
|
}) {
|
||||||
const [viewMode, setViewMode] = useState<'overview' | 'edit'>('overview');
|
const [viewMode, setViewMode] = useState<'overview' | 'edit'>('overview');
|
||||||
const [section, setSection] = useState<BrandbookSectionId>('identidad');
|
const [section, setSection] = useState<BrandbookSectionId>('identidad');
|
||||||
const [book, setBook] = useState<Brandbook>(() => emptyBrandbook(tenantLabel));
|
const [book, setBook] = useState<Brandbook>(() => emptyBrandbook(tenantLabel));
|
||||||
@@ -120,11 +128,12 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/brandbook');
|
const query = tenantId ? `?tenant_id=${encodeURIComponent(tenantId)}` : '';
|
||||||
|
const r = await fetch(`/api/brandbook${query}`);
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (!r.ok) throw new Error(j.error || 'No se pudo cargar');
|
if (!r.ok) throw new Error(j.error || 'No se pudo cargar');
|
||||||
setBook(j.brandbook || emptyBrandbook(tenantLabel));
|
setBook(j.brandbook || emptyBrandbook(tenantLabel));
|
||||||
setSlug(j.slug || '');
|
setSlug(j.slug || tenantSlug || '');
|
||||||
setScore(j.completeness?.score ?? 0);
|
setScore(j.completeness?.score ?? 0);
|
||||||
setMissing(j.completeness?.missing || []);
|
setMissing(j.completeness?.missing || []);
|
||||||
setExists(!!j.exists);
|
setExists(!!j.exists);
|
||||||
@@ -133,7 +142,7 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [tenantLabel]);
|
}, [tenantId, tenantLabel, tenantSlug]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void load();
|
void load();
|
||||||
@@ -149,10 +158,11 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
|||||||
setErr(null);
|
setErr(null);
|
||||||
setOkMsg(null);
|
setOkMsg(null);
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/brandbook', {
|
const query = tenantId ? `?tenant_id=${encodeURIComponent(tenantId)}` : '';
|
||||||
|
const r = await fetch(`/api/brandbook${query}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ brandbook: book }),
|
body: JSON.stringify({ brandbook: book, tenant_id: tenantId }),
|
||||||
});
|
});
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (!r.ok) throw new Error(j.error || 'No se pudo guardar');
|
if (!r.ok) throw new Error(j.error || 'No se pudo guardar');
|
||||||
@@ -160,7 +170,7 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
|||||||
setScore(j.completeness?.score ?? 0);
|
setScore(j.completeness?.score ?? 0);
|
||||||
setMissing(j.completeness?.missing || []);
|
setMissing(j.completeness?.missing || []);
|
||||||
setExists(true);
|
setExists(true);
|
||||||
setOkMsg('Brand kit guardado');
|
setOkMsg('Brand kit guardado correctamente y sincronizado');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErr(e instanceof Error ? e.message : 'Error al guardar');
|
setErr(e instanceof Error ? e.message : 'Error al guardar');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -172,10 +182,11 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
|
|||||||
setSeeding(true);
|
setSeeding(true);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/brandbook', {
|
const query = tenantId ? `?tenant_id=${encodeURIComponent(tenantId)}` : '';
|
||||||
|
const r = await fetch(`/api/brandbook${query}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ seed: true }),
|
body: JSON.stringify({ seed: true, tenant_id: tenantId }),
|
||||||
});
|
});
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (!r.ok) throw new Error(j.error || 'No se pudo sembrar');
|
if (!r.ok) throw new Error(j.error || 'No se pudo sembrar');
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user