feat: suite Orgánico tipo Semrush (SEO + GEO por secciones)
Navegación por visión general, competencia, prompts, percepción, auditoría, keywords, backlinks y prioridades accionables por cliente. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -54,7 +54,7 @@ export async function GET(req: NextRequest) {
|
||||
since && until
|
||||
? `&day=gte.${since}&day=lte.${until}`
|
||||
: '';
|
||||
const [oRes, gRes, iRes] = await Promise.all([
|
||||
const [oRes, gRes, iRes, tRes] = await Promise.all([
|
||||
fetch(
|
||||
`${base}/rest/v1/organic_daily?tenant_id=eq.${encodeURIComponent(tid)}&select=*&order=day.asc${dayFilter}`,
|
||||
{ headers, cache: 'no-store' }
|
||||
@@ -67,12 +67,17 @@ export async function GET(req: NextRequest) {
|
||||
`${base}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(tid)}&provider=in.(ga4,search_console,tag_manager)&select=provider,status,enabled,external_account_id,last_sync_at,error_message`,
|
||||
{ headers, cache: 'no-store' }
|
||||
),
|
||||
fetch(
|
||||
`${base}/rest/v1/tenants?id=eq.${encodeURIComponent(tid)}&select=settings`,
|
||||
{ headers, cache: 'no-store' }
|
||||
),
|
||||
]);
|
||||
|
||||
const warningParts: string[] = [];
|
||||
let organic: OrganicDailyRow[] = [];
|
||||
let geo: GeoSnapshot[] = [];
|
||||
let integrations: unknown[] = [];
|
||||
let workspace = {};
|
||||
|
||||
if (oRes.ok) organic = await oRes.json();
|
||||
else {
|
||||
@@ -91,6 +96,10 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
if (iRes.ok) integrations = await iRes.json();
|
||||
if (tRes.ok) {
|
||||
const rows = await tRes.json();
|
||||
workspace = rows[0]?.settings?.organico || {};
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
portfolioWide: false,
|
||||
@@ -99,6 +108,7 @@ export async function GET(req: NextRequest) {
|
||||
organic,
|
||||
geo,
|
||||
integrations,
|
||||
workspace,
|
||||
warning: warningParts[0] || null,
|
||||
});
|
||||
}
|
||||
@@ -110,7 +120,7 @@ export async function GET(req: NextRequest) {
|
||||
.order('day', { ascending: true });
|
||||
if (since) organicQ = organicQ.gte('day', since);
|
||||
if (until) organicQ = organicQ.lte('day', until);
|
||||
const [o, ge, integ] = await Promise.all([
|
||||
const [o, ge, integ, ten] = await Promise.all([
|
||||
organicQ,
|
||||
supabase
|
||||
.from('geo_snapshots')
|
||||
@@ -123,6 +133,7 @@ export async function GET(req: NextRequest) {
|
||||
'provider,status,enabled,external_account_id,last_sync_at,error_message'
|
||||
)
|
||||
.in('provider', ['ga4', 'search_console', 'tag_manager']),
|
||||
supabase.from('tenants').select('settings').eq('id', tid).maybeSingle(),
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -132,6 +143,7 @@ export async function GET(req: NextRequest) {
|
||||
organic: o.data || [],
|
||||
geo: ge.data || [],
|
||||
integrations: integ.data || [],
|
||||
workspace: (ten.data?.settings as { organico?: unknown } | null)?.organico || {},
|
||||
warning: o.error?.message || ge.error?.message || null,
|
||||
});
|
||||
}
|
||||
@@ -159,9 +171,61 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const kind = body.kind === 'geo' ? 'geo' : 'organic';
|
||||
const kind =
|
||||
body.kind === 'geo'
|
||||
? 'geo'
|
||||
: body.kind === 'workspace'
|
||||
? 'workspace'
|
||||
: 'organic';
|
||||
const base = supabaseUrl();
|
||||
|
||||
if (kind === 'workspace') {
|
||||
// Merge organico workspace into tenants.settings
|
||||
const cur = await fetch(
|
||||
`${base}/rest/v1/tenants?id=eq.${encodeURIComponent(ctx.tenantId)}&select=settings`,
|
||||
{ headers, cache: 'no-store' }
|
||||
);
|
||||
let settings: Record<string, unknown> = {};
|
||||
if (cur.ok) {
|
||||
const rows = await cur.json();
|
||||
settings =
|
||||
rows[0]?.settings && typeof rows[0].settings === 'object'
|
||||
? { ...rows[0].settings }
|
||||
: {};
|
||||
}
|
||||
const prev =
|
||||
settings.organico && typeof settings.organico === 'object'
|
||||
? (settings.organico as Record<string, unknown>)
|
||||
: {};
|
||||
settings.organico = {
|
||||
...prev,
|
||||
...(body.workspace && typeof body.workspace === 'object'
|
||||
? body.workspace
|
||||
: {}),
|
||||
};
|
||||
const r = await fetch(
|
||||
`${base}/rest/v1/tenants?id=eq.${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
Prefer: 'return=representation',
|
||||
},
|
||||
body: JSON.stringify({ settings }),
|
||||
}
|
||||
);
|
||||
if (!r.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: (await r.text()).slice(0, 300) },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json({
|
||||
workspace: (await r.json())[0]?.settings?.organico || settings.organico,
|
||||
});
|
||||
}
|
||||
|
||||
if (kind === 'organic') {
|
||||
const day = typeof body.day === 'string' ? body.day.slice(0, 10) : '';
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) {
|
||||
|
||||
Reference in New Issue
Block a user