import { useEffect, useRef, useState } from 'react'; import { browserClient } from '@/lib/supabase'; import { imgUrl } from '@/lib/materialImg'; import { toast } from '@/lib/toast'; import UnidadesManager from './UnidadesManager'; type Categoria = { id: number; nombre: string }; type Material = { id: number; nombre: string; categoria_id: number | null; descripcion: string | null; cantidad_total: number; cantidad_disponible: number; numero_inventario: string | null; estado: 'disponible' | 'mantenimiento' | 'baja'; imagen_path?: string | null; trackeado_por_unidad?: boolean; }; type Props = { mode: 'create' | 'edit'; material?: Material; categorias: Categoria[]; }; const BUCKET = 'materiales-fotos'; export default function MaterialForm({ mode, material, categorias }: Props) { const dialogRef = useRef(null); const firstFieldRef = useRef(null); const errorRef = useRef(null); const fileInputRef = useRef(null); const [nombre, setNombre] = useState(material?.nombre ?? ''); const [categoriaId, setCategoriaId] = useState( material?.categoria_id != null ? String(material.categoria_id) : '', ); const [descripcion, setDescripcion] = useState(material?.descripcion ?? ''); const [cantidadTotal, setCantidadTotal] = useState( material ? String(material.cantidad_total) : '0', ); const [numeroInventario, setNumeroInventario] = useState(material?.numero_inventario ?? ''); const [estado, setEstado] = useState(material?.estado ?? 'disponible'); const [trackeado, setTrackeado] = useState(material?.trackeado_por_unidad ?? false); const [file, setFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [imagenPath, setImagenPath] = useState(material?.imagen_path ?? null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const titleId = `mat-form-${mode}-${material?.id ?? 'new'}`; // preview URL con revoke en cleanup useEffect(() => { if (!file) { setPreviewUrl(null); return; } const url = URL.createObjectURL(file); setPreviewUrl(url); return () => URL.revokeObjectURL(url); }, [file]); const open = () => { setError(null); if (mode === 'create') { setNombre(''); setCategoriaId(''); setDescripcion(''); setCantidadTotal('0'); setNumeroInventario(''); setEstado('disponible'); setTrackeado(false); setFile(null); setImagenPath(null); } dialogRef.current?.showModal(); queueMicrotask(() => firstFieldRef.current?.focus()); }; const close = () => { if (loading) return; dialogRef.current?.close(); }; useEffect(() => { const dlg = dialogRef.current; if (!dlg) return; const onClick = (e: MouseEvent) => { if (e.target === dlg) close(); }; dlg.addEventListener('click', onClick); return () => dlg.removeEventListener('click', onClick); }); useEffect(() => { if (error) errorRef.current?.focus(); }, [error]); async function uploadFotoFor(id: number): Promise { if (!file) return null; const ext = (file.name.split('.').pop() ?? 'jpg').toLowerCase().replace(/[^a-z0-9]/g, ''); const path = `${id}/${Date.now()}.${ext || 'jpg'}`; const { error: upErr } = await browserClient() .storage.from(BUCKET) .upload(path, file, { upsert: true, contentType: file.type || undefined }); if (upErr) { toast({ kind: 'error', title: 'La foto no se subió', description: 'El material se guardó sin foto.' }); return null; } return path; } const submit = async (e: React.FormEvent) => { e.preventDefault(); if (loading) return; setLoading(true); setError(null); const nombreT = nombre.trim(); if (!nombreT) { setError('El nombre es obligatorio'); setLoading(false); return; } const cantidad = Number(cantidadTotal); if (!trackeado && (!Number.isInteger(cantidad) || cantidad < 0)) { setError('La cantidad total debe ser un entero mayor o igual a 0'); setLoading(false); return; } try { if (mode === 'create') { // 1) crear material sin imagen const res = await fetch('/api/admin/materiales', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nombre: nombreT, categoria_id: categoriaId === '' ? null : Number(categoriaId), descripcion: descripcion.trim() || null, cantidad_total: trackeado ? 0 : cantidad, numero_inventario: numeroInventario.trim() || null, estado, trackeado_por_unidad: trackeado, }), }); const json = await res.json().catch(() => ({})); if (!res.ok) throw new Error(json?.error ?? 'No se pudo crear'); // 2) subir foto (si hay) y PATCH imagen_path if (file && json.id) { const path = await uploadFotoFor(json.id); if (path) { const p = await fetch(`/api/admin/materiales/${json.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imagen_path: path }), }); if (!p.ok) { toast({ kind: 'error', title: 'Foto subida pero no vinculada', description: 'Recarga y vuelve a intentar.' }); } } } } else { // edit — sube nueva foto primero (si hay) y luego PATCH con el path incluido let newImagenPath: string | null | undefined = undefined; if (file && material) { const path = await uploadFotoFor(material.id); if (path) newImagenPath = path; } const patch: Record = { nombre: nombreT, categoria_id: categoriaId === '' ? null : Number(categoriaId), descripcion: descripcion.trim() || null, numero_inventario: numeroInventario.trim() || null, estado, trackeado_por_unidad: trackeado, }; if (!trackeado) patch.cantidad_total = cantidad; if (newImagenPath !== undefined) patch.imagen_path = newImagenPath; const res = await fetch(`/api/admin/materiales/${material!.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch), }); const json = await res.json().catch(() => ({})); if (!res.ok) throw new Error(json?.error ?? 'No se pudo guardar'); } dialogRef.current?.close(); location.reload(); } catch (err) { setError(err instanceof Error ? err.message : 'Error inesperado'); } finally { setLoading(false); } }; const triggerLabel = mode === 'create' ? 'Nuevo material' : 'Editar'; const triggerClass = mode === 'create' ? 'btn btn-primary' : 'btn btn-ghost'; const currentImg = previewUrl ?? imgUrl(imagenPath); return ( <>

{mode === 'create' ? 'Nuevo material' : `Editar: ${material?.nombre}`}

{/* Foto */}
{currentImg ? ( ) : ( )}
setFile(e.target.files?.[0] ?? null)} /> {(file || imagenPath) && ( )}
setNombre(e.target.value)} required autoComplete="off" aria-invalid={error ? true : undefined} aria-describedby={error ? `${titleId}-err` : undefined} />
setNumeroInventario(e.target.value)} autoComplete="off" placeholder="Opcional…" />