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:
@@ -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;
|
||||||
|
|||||||
@@ -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 />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user