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( const curRes = await fetch(
`${supabaseUrl()}/rest/v1/creative_pipeline?id=eq.${encodeURIComponent( `${supabaseUrl()}/rest/v1/creative_pipeline?id=eq.${encodeURIComponent(
id id
)}&tenant_id=eq.${encodeURIComponent(ctx.tenantId)}&select=status,notes`, )}&tenant_id=eq.${encodeURIComponent(ctx.tenantId)}&select=status`,
{ headers: headers || undefined, cache: 'no-store' } { headers: headers || undefined, cache: 'no-store' }
); );
const curRows = curRes.ok ? await curRes.json() : []; const curRows = curRes.ok ? await curRes.json() : [];
@@ -215,20 +215,6 @@ export async function PATCH(req: NextRequest) {
); );
} }
clean.status = next; 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') { if (next === 'aprobado') {
clean.approved_by = user.id; clean.approved_by = user.id;

View File

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

View File

@@ -16,8 +16,6 @@ import {
Filter, Filter,
} from 'lucide-react'; } from 'lucide-react';
import { PageHeader, Surface } from '@/components/ui'; import { PageHeader, Surface } from '@/components/ui';
import { clientNoteFromNotes } from '@/lib/pipeline-calendar';
import Link from 'next/link';
type PipelineItem = { type PipelineItem = {
id: string; id: string;
@@ -110,8 +108,6 @@ export default function CreativePipelineBoard() {
const [selectedMonth, setSelectedMonth] = useState<string>('all'); const [selectedMonth, setSelectedMonth] = useState<string>('all');
const [selectedFormat, setSelectedFormat] = useState<string>('all'); const [selectedFormat, setSelectedFormat] = useState<string>('all');
const [previewItem, setPreviewItem] = useState<PipelineItem | null>(null); const [previewItem, setPreviewItem] = useState<PipelineItem | null>(null);
const [rejectItem, setRejectItem] = useState<PipelineItem | null>(null);
const [rejectNote, setRejectNote] = useState('');
async function load() { async function load() {
setLoading(true); setLoading(true);
@@ -133,7 +129,6 @@ export default function CreativePipelineBoard() {
const monthCounts = useMemo(() => { const monthCounts = useMemo(() => {
const map = new Map<string, number>(); const map = new Map<string, number>();
for (const it of items) { for (const it of items) {
if (it.status === 'publicado') continue;
const key = it.month || 'sin-mes'; const key = it.month || 'sin-mes';
map.set(key, (map.get(key) || 0) + 1); map.set(key, (map.get(key) || 0) + 1);
} }
@@ -143,7 +138,6 @@ export default function CreativePipelineBoard() {
const months = useMemo(() => { const months = useMemo(() => {
const set = new Set<string>(); const set = new Set<string>();
for (const it of items) { for (const it of items) {
if (it.status === 'publicado') continue;
if (it.month) set.add(it.month); if (it.month) set.add(it.month);
} }
return Array.from(set).sort(); return Array.from(set).sort();
@@ -151,7 +145,6 @@ export default function CreativePipelineBoard() {
const filteredItems = useMemo(() => { const filteredItems = useMemo(() => {
return items.filter((it) => { return items.filter((it) => {
if (it.status === 'publicado') return false;
if (selectedMonth !== 'all' && (it.month || 'sin-mes') !== selectedMonth) return false; if (selectedMonth !== 'all' && (it.month || 'sin-mes') !== selectedMonth) return false;
if (selectedFormat !== 'all' && it.format !== selectedFormat) return false; if (selectedFormat !== 'all' && it.format !== selectedFormat) return false;
return true; return true;
@@ -188,14 +181,14 @@ export default function CreativePipelineBoard() {
await load(); await load();
} }
async function setStatus(id: string, status: string, notes?: string) { async function setStatus(id: string, status: string) {
setBusyId(id); setBusyId(id);
setErr(null); setErr(null);
try { try {
const res = await fetch('/api/creative-pipeline', { const res = await fetch('/api/creative-pipeline', {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status, ...(notes ? { notes } : {}) }), body: JSON.stringify({ id, status }),
}); });
const data = await res.json(); const data = await res.json();
if (!res.ok) { if (!res.ok) {
@@ -203,11 +196,7 @@ export default function CreativePipelineBoard() {
return; return;
} }
if (previewItem && previewItem.id === id) { if (previewItem && previewItem.id === id) {
setPreviewItem({ setPreviewItem({ ...previewItem, status });
...previewItem,
status,
notes: notes || previewItem.notes,
});
} }
await load(); await load();
} finally { } finally {
@@ -257,7 +246,7 @@ export default function CreativePipelineBoard() {
<PageHeader <PageHeader
eyebrow="Producción editorial" eyebrow="Producción editorial"
title="Panel de Creatividades" 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={ actions={
isAgency ? ( isAgency ? (
<button <button
@@ -280,20 +269,13 @@ export default function CreativePipelineBoard() {
{err} {err}
</div> </div>
) : null} ) : 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 */} {/* Barra de Filtros por Mes y Formato */}
<div className="mb-6 space-y-3"> <div className="mb-6 space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs font-semibold uppercase tracking-wider text-gray-500"> <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> </span>
</div> </div>
<div className="flex items-center gap-1.5 text-xs text-gray-500"> <div className="flex items-center gap-1.5 text-xs text-gray-500">
@@ -434,14 +416,7 @@ export default function CreativePipelineBoard() {
item={it} item={it}
isAgency={isAgency} isAgency={isAgency}
busy={busyId === it.id} busy={busyId === it.id}
onStatus={(s) => { onStatus={(s) => setStatus(it.id, s)}
if (s === 'rechazado' && !isAgency) {
setRejectItem(it);
setRejectNote('');
return;
}
void setStatus(it.id, s);
}}
onDelete={() => removeItem(it.id)} onDelete={() => removeItem(it.id)}
onUpload={(file, slot, index) => uploadImage(it.id, file, slot, index)} onUpload={(file, slot, index) => uploadImage(it.id, file, slot, index)}
onPreview={() => setPreviewItem(it)} onPreview={() => setPreviewItem(it)}
@@ -460,68 +435,9 @@ export default function CreativePipelineBoard() {
isAgency={isAgency} isAgency={isAgency}
busy={busyId === previewItem.id} busy={busyId === previewItem.id}
onClose={() => setPreviewItem(null)} onClose={() => setPreviewItem(null)}
onStatus={(s) => { onStatus={(s) => setStatus(previewItem.id, s)}
if (s === 'rechazado' && !isAgency) {
setRejectItem(previewItem);
setRejectNote('');
return;
}
void 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> </div>
); );
} }
@@ -701,12 +617,6 @@ function PieceCard({
)} )}
</div> </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"> <div className="mt-auto pt-2">
{isAgency ? ( {isAgency ? (
<select <select
@@ -910,12 +820,6 @@ function DetailModal({
</div> </div>
</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>
<div className="mt-6 border-t border-gray-100 pt-4"> <div className="mt-6 border-t border-gray-100 pt-4">