import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { toast, toastAfterReload } from '@/lib/toast'; import AgregarMaterial from './AgregarMaterial'; type Material = { id: number; nombre: string; descripcion: string | null; cantidad_disponible: number; cantidad_total: number; numero_inventario: string | null; categoria: { id: number; nombre: string } | null; }; type CartItem = { material_id: number; nombre: string; cantidad: number; cantidad_disponible: number; descripcion: string; }; type CartContextValue = { items: CartItem[]; addItem: (material: { id: number; nombre: string; cantidad_disponible: number }) => void; updateCantidad: (material_id: number, n: number) => void; updateDescripcion: (material_id: number, texto: string) => void; removeItem: (material_id: number) => void; has: (material_id: number) => boolean; }; const CartContext = createContext(null); export function useCart() { const ctx = useContext(CartContext); if (!ctx) throw new Error('useCart debe usarse dentro de CartProvider'); return ctx; } const clamp = (n: number, max: number) => Math.max(1, Math.min(max, n)); export default function CartProvider({ materiales }: { materiales: Material[] }) { const [cart, setCart] = useState>(new Map()); const dialogRef = useRef(null); const firstFieldRef = useRef(null); const [maestroResponsable, setMaestroResponsable] = useState(''); const [notas, setNotas] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [ok, setOk] = useState(false); const items = useMemo(() => Array.from(cart.values()), [cart]); const addItem: CartContextValue['addItem'] = (material) => { setCart((prev) => { const next = new Map(prev); const existing = next.get(material.id); if (existing) { next.set(material.id, { ...existing, cantidad: clamp(existing.cantidad + 1, material.cantidad_disponible) }); } else { next.set(material.id, { material_id: material.id, nombre: material.nombre, cantidad: 1, cantidad_disponible: material.cantidad_disponible, descripcion: '', }); } return next; }); }; const updateCantidad: CartContextValue['updateCantidad'] = (material_id, n) => { setCart((prev) => { const existing = prev.get(material_id); if (!existing) return prev; const next = new Map(prev); next.set(material_id, { ...existing, cantidad: clamp(n, existing.cantidad_disponible) }); return next; }); }; const updateDescripcion: CartContextValue['updateDescripcion'] = (material_id, texto) => { setCart((prev) => { const existing = prev.get(material_id); if (!existing) return prev; const next = new Map(prev); next.set(material_id, { ...existing, descripcion: texto }); return next; }); }; const removeItem: CartContextValue['removeItem'] = (material_id) => { setCart((prev) => { const next = new Map(prev); next.delete(material_id); return next; }); }; const has: CartContextValue['has'] = (material_id) => cart.has(material_id); const value: CartContextValue = { items, addItem, updateCantidad, updateDescripcion, removeItem, has }; const titleId = 'checkout-title'; const openCheckout = () => { setError(null); setOk(false); dialogRef.current?.showModal(); queueMicrotask(() => firstFieldRef.current?.focus()); }; const closeCheckout = () => { if (loading) return; dialogRef.current?.close(); }; // Cerrar al click en backdrop useEffect(() => { const dlg = dialogRef.current; if (!dlg) return; const onClick = (e: MouseEvent) => { if (e.target === dlg) closeCheckout(); }; dlg.addEventListener('click', onClick); return () => dlg.removeEventListener('click', onClick); }); const submit = async (e: React.FormEvent) => { e.preventDefault(); if (loading || items.length === 0) return; setLoading(true); setError(null); try { const res = await fetch('/api/solicitudes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ maestro_responsable: maestroResponsable, notas: notas || undefined, items: items.map((i) => ({ material_id: i.material_id, cantidad: i.cantidad, descripcion: i.descripcion || undefined, })), }), }); const json = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(json?.error ?? 'No se pudo enviar la solicitud'); } setOk(true); toastAfterReload({ kind: 'success', title: 'Solicitud enviada', description: `${items.length} material${items.length === 1 ? '' : 'es'} · pendiente de aprobación`, }); setTimeout(() => { dialogRef.current?.close(); setCart(new Map()); location.reload(); }, 800); } catch (err) { const msg = err instanceof Error ? err.message : 'Error inesperado'; setError(msg); toast({ kind: 'error', title: 'No se pudo enviar', description: msg }); } finally { setLoading(false); } }; return (
{materiales.map((m) => (

{m.nombre}

{m.categoria && ( {m.categoria.nombre} )}
{m.descripcion &&

{m.descripcion}

}
Inventario
{m.numero_inventario ?? '—'}
Disponibles
{m.cantidad_disponible} / {m.cantidad_total}
))}
{items.length > 0 && ( )}

Tu solicitud

{items.length === 0 ? (

Tu solicitud está vacía. Cierra esta ventana y agrega material del catálogo.

) : (
    {items.map((item) => (
  • {item.nombre}

    updateCantidad(item.material_id, Number(e.target.value) || 1)} style={{ width: '4.5rem', minHeight: '36px', paddingBlock: '0.25rem', fontVariantNumeric: 'tabular-nums' }} /> de {item.cantidad_disponible}
    updateDescripcion(item.material_id, e.target.value)} placeholder="Nota opcional: color, con webcam…" style={{ minHeight: '36px', paddingBlock: '0.25rem' }} />
  • ))}
)}
setMaestroResponsable(e.target.value)} />