Revert unsolicited creative pipeline review-only changes.

Restore CreativesPipeline on /creatives and the previous board/API behavior (published pieces visible again; no forced reject note).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-06 16:54:38 +02:00
parent d073df12d1
commit 91380e2285
3 changed files with 12 additions and 122 deletions

View File

@@ -189,7 +189,7 @@ export async function PATCH(req: NextRequest) {
const curRes = await fetch(
`${supabaseUrl()}/rest/v1/creative_pipeline?id=eq.${encodeURIComponent(
id
)}&tenant_id=eq.${encodeURIComponent(ctx.tenantId)}&select=status,notes`,
)}&tenant_id=eq.${encodeURIComponent(ctx.tenantId)}&select=status`,
{ headers: headers || undefined, cache: 'no-store' }
);
const curRows = curRes.ok ? await curRes.json() : [];
@@ -215,20 +215,6 @@ export async function PATCH(req: NextRequest) {
);
}
clean.status = next;
if (next === 'rechazado') {
const note = str(body.notes, 2000);
if (!note) {
return NextResponse.json(
{ error: 'Añade una nota explicando qué hay que cambiar.' },
{ status: 400 }
);
}
const prev = str(curRows?.[0]?.notes, 2000) || '';
const line = `CLIENT_NOTE: ${note}`;
clean.notes = prev.includes(line)
? prev
: [line, prev].filter(Boolean).join('\n');
}
}
if (next === 'aprobado') {
clean.approved_by = user.id;

View File

@@ -1,11 +1,11 @@
/**
* Pipeline de creatividades — cola de revisión (no archivo publicado).
* Lo publicado se abre en el calendario, día a día.
* Pipeline de creatividades — cola de aprobación con preview de placement
* (Meta/Google Ads + RRSS), estilo Ad Tracker.
*/
'use client';
import CreativePipelineBoard from '@/components/CreativePipelineBoard';
import CreativesPipeline from '@/components/CreativesPipeline';
export default function CreativesPage() {
return <CreativePipelineBoard />;
return <CreativesPipeline />;
}

View File

@@ -16,8 +16,6 @@ import {
Filter,
} from 'lucide-react';
import { PageHeader, Surface } from '@/components/ui';
import { clientNoteFromNotes } from '@/lib/pipeline-calendar';
import Link from 'next/link';
type PipelineItem = {
id: string;
@@ -110,8 +108,6 @@ export default function CreativePipelineBoard() {
const [selectedMonth, setSelectedMonth] = useState<string>('all');
const [selectedFormat, setSelectedFormat] = useState<string>('all');
const [previewItem, setPreviewItem] = useState<PipelineItem | null>(null);
const [rejectItem, setRejectItem] = useState<PipelineItem | null>(null);
const [rejectNote, setRejectNote] = useState('');
async function load() {
setLoading(true);
@@ -133,7 +129,6 @@ export default function CreativePipelineBoard() {
const monthCounts = useMemo(() => {
const map = new Map<string, number>();
for (const it of items) {
if (it.status === 'publicado') continue;
const key = it.month || 'sin-mes';
map.set(key, (map.get(key) || 0) + 1);
}
@@ -143,7 +138,6 @@ export default function CreativePipelineBoard() {
const months = useMemo(() => {
const set = new Set<string>();
for (const it of items) {
if (it.status === 'publicado') continue;
if (it.month) set.add(it.month);
}
return Array.from(set).sort();
@@ -151,7 +145,6 @@ export default function CreativePipelineBoard() {
const filteredItems = useMemo(() => {
return items.filter((it) => {
if (it.status === 'publicado') return false;
if (selectedMonth !== 'all' && (it.month || 'sin-mes') !== selectedMonth) return false;
if (selectedFormat !== 'all' && it.format !== selectedFormat) return false;
return true;
@@ -188,14 +181,14 @@ export default function CreativePipelineBoard() {
await load();
}
async function setStatus(id: string, status: string, notes?: string) {
async function setStatus(id: string, status: string) {
setBusyId(id);
setErr(null);
try {
const res = await fetch('/api/creative-pipeline', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status, ...(notes ? { notes } : {}) }),
body: JSON.stringify({ id, status }),
});
const data = await res.json();
if (!res.ok) {
@@ -203,11 +196,7 @@ export default function CreativePipelineBoard() {
return;
}
if (previewItem && previewItem.id === id) {
setPreviewItem({
...previewItem,
status,
notes: notes || previewItem.notes,
});
setPreviewItem({ ...previewItem, status });
}
await load();
} finally {
@@ -257,7 +246,7 @@ export default function CreativePipelineBoard() {
<PageHeader
eyebrow="Producción editorial"
title="Panel de Creatividades"
description="Solo revisión: propuesta, diseño y OK del cliente. Lo publicado se ve en el calendario, día a día."
description="Planificación, diseño y archivo histórico sincronizado con Instagram."
actions={
isAgency ? (
<button
@@ -280,20 +269,13 @@ export default function CreativePipelineBoard() {
{err}
</div>
) : null}
<p className="mb-4 text-xs text-gray-500">
Piezas ya publicadas:{' '}
<Link href="/dashboard/calendario" className="font-semibold text-brand-700 hover:underline">
ábrelas en el calendario, clic en el día
</Link>
.
</p>
{/* Barra de Filtros por Mes y Formato */}
<div className="mb-6 space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold uppercase tracking-wider text-gray-500">
Meses ({items.filter((i) => i.status !== 'publicado').length} en revisión):
Meses ({items.length} piezas):
</span>
</div>
<div className="flex items-center gap-1.5 text-xs text-gray-500">
@@ -434,14 +416,7 @@ export default function CreativePipelineBoard() {
item={it}
isAgency={isAgency}
busy={busyId === it.id}
onStatus={(s) => {
if (s === 'rechazado' && !isAgency) {
setRejectItem(it);
setRejectNote('');
return;
}
void setStatus(it.id, s);
}}
onStatus={(s) => setStatus(it.id, s)}
onDelete={() => removeItem(it.id)}
onUpload={(file, slot, index) => uploadImage(it.id, file, slot, index)}
onPreview={() => setPreviewItem(it)}
@@ -460,68 +435,9 @@ export default function CreativePipelineBoard() {
isAgency={isAgency}
busy={busyId === previewItem.id}
onClose={() => setPreviewItem(null)}
onStatus={(s) => {
if (s === 'rechazado' && !isAgency) {
setRejectItem(previewItem);
setRejectNote('');
return;
}
void setStatus(previewItem.id, s);
}}
onStatus={(s) => setStatus(previewItem.id, s)}
/>
)}
{rejectItem ? (
<div
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 p-4"
onClick={() => setRejectItem(null)}
>
<form
className="w-full max-w-md rounded-2xl bg-white p-5 shadow-xl space-y-3"
onClick={(e) => e.stopPropagation()}
onSubmit={(e) => {
e.preventDefault();
if (!rejectNote.trim()) return;
const id = rejectItem.id;
void setStatus(id, 'rechazado', rejectNote.trim()).then(() => {
setRejectItem(null);
setRejectNote('');
});
}}
>
<h3 className="text-sm font-bold text-gray-900">
No aprobar · {rejectItem.title}
</h3>
<p className="text-xs text-gray-500">
Deja una nota para que 4Ventures sepa qué hay que cambiar.
</p>
<textarea
required
value={rejectNote}
onChange={(e) => setRejectNote(e.target.value)}
rows={4}
className="w-full rounded-xl border border-gray-200 px-3 py-2 text-sm"
placeholder="Ej. Cambia el titular, el logo se ve pequeño…"
/>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => setRejectItem(null)}
className="rounded-lg border border-gray-200 px-3 py-2 text-xs font-semibold"
>
Cancelar
</button>
<button
type="submit"
disabled={!rejectNote.trim() || busyId === rejectItem.id}
className="rounded-lg bg-red-600 px-3 py-2 text-xs font-bold text-white disabled:opacity-50"
>
Enviar nota y rechazar
</button>
</div>
</form>
</div>
) : null}
</div>
);
}
@@ -701,12 +617,6 @@ function PieceCard({
)}
</div>
{item.status === 'rechazado' && clientNoteFromNotes(item.notes) ? (
<p className="text-[11px] text-red-800 bg-red-50 border border-red-100 rounded-lg px-2 py-1.5">
Nota del cliente: {clientNoteFromNotes(item.notes)}
</p>
) : null}
<div className="mt-auto pt-2">
{isAgency ? (
<select
@@ -910,12 +820,6 @@ function DetailModal({
</div>
</div>
)}
{item.status === 'rechazado' && clientNoteFromNotes(item.notes) ? (
<div className="rounded-xl border border-red-200 bg-red-50 p-3 text-xs text-red-900">
<p className="font-semibold mb-1">Nota del cliente</p>
{clientNoteFromNotes(item.notes)}
</div>
) : null}
</div>
<div className="mt-6 border-t border-gray-100 pt-4">