Files
4ventures-client-portal/src/components/CalendarUpload.tsx
4Ventures d073df12d1 Treat Brava-style SEMANA/Story/POST Excel as the official calendar template.
Harden week-grid parsing (placeholders, status rows, month wrap) and ship an October-shaped plantilla for agency uploads.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 16:46:17 +02:00

116 lines
4.1 KiB
TypeScript

'use client';
import React, { useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
import { FileSpreadsheet, Loader2, Upload } from 'lucide-react';
export default function CalendarUpload({
tenantLabel,
}: {
tenantLabel?: string;
}) {
const router = useRouter();
const inputRef = useRef<HTMLInputElement>(null);
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState<string | null>(null);
const [err, setErr] = useState<string | null>(null);
async function onFile(file: File) {
setBusy(true);
setErr(null);
setMsg(null);
try {
const fd = new FormData();
fd.append('file', file);
const r = await fetch('/api/calendar/upload', { method: 'POST', body: fd });
const j = await r.json().catch(() => ({}));
if (!r.ok) {
setErr(j.error || 'No se pudo leer el archivo');
return;
}
const months = (j.months || []) as { month: string; pieces: number }[];
const summary = months
.map((m) => `${m.month}: ${m.pieces} piezas`)
.join(' · ');
setMsg(`Listo para ${j.tenant || tenantLabel}. ${summary}`);
const first = months[0]?.month;
router.refresh();
if (first) {
router.push(`/dashboard/calendario?view=month&month=${first}`);
}
} catch (e) {
setErr(e instanceof Error ? e.message : 'Error al subir');
} finally {
setBusy(false);
}
}
return (
<section className="rounded-2xl border border-dashed border-brand-300 bg-brand-50/40 p-5">
<div className="flex flex-col md:flex-row md:items-center gap-4">
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-white border border-brand-200 text-brand-700">
<FileSpreadsheet className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1">
<h3 className="text-sm font-semibold text-gray-900">
Subir calendario (CSV, Excel o JSON)
</h3>
<p className="text-xs text-gray-500 mt-1">
Plantilla oficial: Excel por mes con filas SEMANA / Story / POST
(como Brava Octubre). También CSV con columnas fecha + título. Si
el mes ya existía, se reemplaza.
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<a
href="/plantillas/calendario-rrss-oficial.xlsx"
className="rounded-xl border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-700 hover:border-brand-300"
>
Plantilla Excel (oficial)
</a>
<a
href="/api/calendar/upload"
className="rounded-xl border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-500 hover:border-brand-300"
>
CSV alternativo
</a>
<button
type="button"
disabled={busy}
onClick={() => inputRef.current?.click()}
className="inline-flex items-center gap-2 rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700 disabled:opacity-60"
>
{busy ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Upload className="h-4 w-4" />
)}
{busy ? 'Leyendo…' : 'Elegir archivo'}
</button>
<input
ref={inputRef}
type="file"
accept=".csv,.xlsx,.xls,.json,text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/json"
className="hidden"
onChange={(e) => {
const f = e.target.files?.[0];
e.target.value = '';
if (f) void onFile(f);
}}
/>
</div>
</div>
{msg ? (
<p className="mt-3 text-xs text-emerald-800 bg-emerald-50 border border-emerald-200 rounded-lg px-3 py-2">
{msg}
</p>
) : null}
{err ? (
<p className="mt-3 text-xs text-red-700 bg-red-50 border border-red-200 rounded-lg px-3 py-2">
{err}
</p>
) : null}
</section>
);
}