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>
116 lines
4.1 KiB
TypeScript
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>
|
|
);
|
|
}
|