feat: metrics dashboard with WoW/MoM/YoY and creatives pipeline

Rebuild KPIs as a real dashboard (period deltas, tenant zoom), clarify
objetivos/calendario/activos sources, and turn creatives into a status
pipeline with delete.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-22 13:45:41 +02:00
parent 791976a3d8
commit 8182402d96
11 changed files with 1002 additions and 263 deletions

View File

@@ -140,3 +140,94 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ creative: data }, { status: 201 });
}
const ALLOWED = new Set(['draft', 'in_review', 'approved', 'rejected']);
export async function PATCH(req: NextRequest) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await req.json().catch(() => ({}));
const id = typeof body.id === 'string' ? body.id.trim() : '';
const status = typeof body.status === 'string' ? body.status.trim() : '';
if (!id || !ALLOWED.has(status)) {
return NextResponse.json({ error: 'id y status válidos requeridos' }, { status: 400 });
}
const agency = isAgencyRole(userRole(user));
const headers = serviceHeaders();
if (agency && headers) {
const r = await fetch(
`${supabaseUrl()}/rest/v1/creatives?id=eq.${encodeURIComponent(id)}`,
{
method: 'PATCH',
headers: {
...headers,
'Content-Type': 'application/json',
Prefer: 'return=representation',
},
body: JSON.stringify({
status,
updated_at: new Date().toISOString(),
}),
}
);
if (!r.ok) {
return NextResponse.json({ error: await r.text() }, { status: 500 });
}
const data = await r.json();
return NextResponse.json({ creative: data[0] || data });
}
const { data, error } = await supabase
.from('creatives')
.update({ status, updated_at: new Date().toISOString() })
.eq('id', id)
.select('id,title,status')
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ creative: data });
}
export async function DELETE(req: NextRequest) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const id = req.nextUrl.searchParams.get('id')?.trim() || '';
if (!id) {
return NextResponse.json({ error: 'id requerido' }, { status: 400 });
}
const agency = isAgencyRole(userRole(user));
const headers = serviceHeaders();
if (agency && headers) {
const r = await fetch(
`${supabaseUrl()}/rest/v1/creatives?id=eq.${encodeURIComponent(id)}`,
{ method: 'DELETE', headers }
);
if (!r.ok) {
return NextResponse.json({ error: await r.text() }, { status: 500 });
}
return NextResponse.json({ ok: true });
}
const { error } = await supabase.from('creatives').delete().eq('id', id);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ ok: true });
}