"use client" import { useEffect, useState, useCallback, useMemo } from "react" import { DashboardLayout } from "@/components/dashboard-layout" import { useAuth } from "@/lib/auth-context" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { apiFetch } from "@/lib/apiutils" import { Search, Download, RefreshCw, Tag, Pencil, Trash2, Plus, Share2, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, ChevronDown, ChevronUp } from "lucide-react" import { cn } from "@/lib/utils" interface Categoria { id: number nombre: string } interface Producto { id: number codigo: string | null nombre: string categoria_id: number categoria: Categoria precio: number ultima_actualizacion: string } interface ProductForm { codigo: string nombre: string categoria_id: string precio: string } const EMPTY_FORM: ProductForm = { codigo: "", nombre: "", categoria_id: "", precio: "" } const PAGE_SIZE_OPTIONS = [10, 20, 50, 100] function formatPrecio(p: number) { return new Intl.NumberFormat("es-AR", { style: "currency", currency: "ARS", maximumFractionDigits: 0 }).format(p) } export default function PreciosPage() { const { user } = useAuth() const isAdmin = user?.role === "admin" const [productos, setProductos] = useState([]) const [categorias, setCategorias] = useState([]) const [loading, setLoading] = useState(true) const [search, setSearch] = useState("") const [pageSize, setPageSize] = useState(50) const [page, setPage] = useState(1) // ABM State const [dialogOpen, setDialogOpen] = useState(false) const [editTarget, setEditTarget] = useState(null) const [form, setForm] = useState(EMPTY_FORM) const [saving, setSaving] = useState(false) const [errors, setErrors] = useState>({}) const [deleteTarget, setDeleteTarget] = useState(null) const [downloading, setDownloading] = useState(false) // Accordion State const [expandedCats, setExpandedCats] = useState>({}) const fetchData = useCallback(async () => { setLoading(true) try { const [prodRes, catRes] = await Promise.all([ apiFetch("/productos"), apiFetch("/categorias") ]) if (prodRes.ok) setProductos(await prodRes.json()) if (catRes.ok) setCategorias(await catRes.json()) } catch (e) { console.error(e) } finally { setLoading(false) } }, []) useEffect(() => { fetchData() }, [fetchData]) useEffect(() => { setPage(1) }, [search, pageSize]) const filtered = useMemo(() => productos.filter((p) => search === "" || p.nombre.toLowerCase().includes(search.toLowerCase()) || p.categoria?.nombre.toLowerCase().includes(search.toLowerCase()) || (p.codigo ?? "").includes(search) ), [productos, search]) const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)) const safePage = Math.min(page, totalPages) const paginated = useMemo(() => filtered.slice((safePage - 1) * pageSize, safePage * pageSize), [filtered, safePage, pageSize]) const grouped = useMemo(() => paginated.reduce>((acc, p) => { const catName = p.categoria?.nombre || "Sin Categoría" acc[catName] = [...(acc[catName] ?? []), p] return acc }, {}), [paginated]) const toggleCategory = (catName: string) => { setExpandedCats(prev => ({ ...prev, [catName]: !prev[catName] })) } // ABM Methods const openCreate = () => { setEditTarget(null) setForm(EMPTY_FORM) setErrors({}) setDialogOpen(true) } const openEdit = (p: Producto) => { setEditTarget(p) setForm({ codigo: p.codigo ?? "", nombre: p.nombre, categoria_id: String(p.categoria_id), precio: String(p.precio) }) setErrors({}) setDialogOpen(true) } const validate = (): boolean => { const e: Partial = {} if (!form.nombre.trim()) e.nombre = "La descripción es obligatoria." if (!form.categoria_id) e.categoria_id = "La categoría es obligatoria." const precioVal = parseFloat(form.precio) if (!form.precio || isNaN(precioVal) || precioVal <= 0) e.precio = "El precio debe ser un número mayor a 0." setErrors(e) return Object.keys(e).length === 0 } const handleSave = async () => { if (!validate()) return setSaving(true) try { const body = { codigo: form.codigo.trim() || null, nombre: form.nombre.trim(), categoria_id: parseInt(form.categoria_id), precio: parseFloat(form.precio) } const res = editTarget ? await apiFetch(`/productos/${editTarget.id}`, { method: "PUT", body: JSON.stringify(body) }) : await apiFetch("/productos", { method: "POST", body: JSON.stringify(body) }) if (res.ok) { setDialogOpen(false) fetchData() } else { console.error(await res.text()) } } catch (e) { console.error(e) } finally { setSaving(false) } } const handleDelete = async () => { if (!deleteTarget) return try { const res = await apiFetch(`/productos/${deleteTarget.id}`, { method: "DELETE" }) if (res.ok) { setDeleteTarget(null) fetchData() } } catch (e) { console.error(e) } } const downloadPDF = async () => { setDownloading(true) try { const token = typeof window !== "undefined" ? localStorage.getItem("token") : null const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:88/api"}/productos/export/pdf`, { headers: { Authorization: `Bearer ${token}`, Accept: "application/pdf" } } ) if (res.ok) { const blob = await res.blob() const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = `lista_precios_belen_${new Date().toISOString().slice(0, 10)}.pdf` a.click() URL.revokeObjectURL(url) } } catch (e) { console.error(e) } finally { setDownloading(false) } } const sharePDF = async () => { setDownloading(true) try { const token = typeof window !== "undefined" ? localStorage.getItem("token") : null const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:88/api"}/productos/export/pdf`, { headers: { Authorization: `Bearer ${token}`, Accept: "application/pdf" } } ) if (res.ok) { const blob = await res.blob() const filename = `lista_precios_belen_${new Date().toISOString().slice(0, 10)}.pdf` const file = new File([blob], filename, { type: "application/pdf" }) if (navigator.canShare && navigator.canShare({ files: [file] })) { await navigator.share({ files: [file], title: "Lista de Precios - Acoplados Belén", text: "Adjunto la última lista de precios de Acoplados Belén." }) } else { // Fallback if sharing files is not supported alert("Tu dispositivo o navegador no soporta compartir archivos directamente. El archivo será descargado en su lugar.") const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = filename a.click() URL.revokeObjectURL(url) } } } catch (e) { console.error(e) } finally { setDownloading(false) } } return (

Gestión de Productos

{loading ? "Cargando..." : `${filtered.length} de ${productos.length} productos en el catálogo`}

{isAdmin && ( )}
setSearch(e.target.value)} />
Filas por pág:
{loading ? (
Cargando productos...
) : filtered.length === 0 ? (
No se encontraron productos.
) : ( Object.entries(grouped).map(([cat, prods]) => { const isExpanded = expandedCats[cat] === true return ( toggleCategory(cat)} > {cat} {prods.length}
{isExpanded ? : }
{isExpanded && (
{isAdmin && } {prods.map((p) => ( {isAdmin && ( )} ))}
Cód. Descripción Precio Últ. actualiz.Acción
{p.codigo ?? "—"} {p.nombre} {formatPrecio(p.precio)} {new Date(p.ultima_actualizacion).toLocaleDateString("es-AR")}
)}
) }) )} {/* Pagination */} {!loading && filtered.length > 0 && (

Mostrando {(safePage - 1) * pageSize + 1}–{Math.min(safePage * pageSize, filtered.length)} de {filtered.length}

Pág {safePage} / {totalPages}
)}
{/* Create / Edit Dialog */} {editTarget ? "Editar Producto" : "Nuevo Producto"}
setForm(p => ({ ...p, codigo: e.target.value }))} />
setForm(p => ({ ...p, nombre: e.target.value }))} className={cn(errors.nombre && "border-destructive")} /> {errors.nombre &&

{errors.nombre}

}
{errors.categoria_id &&

{errors.categoria_id}

}
setForm(p => ({ ...p, precio: e.target.value }))} className={cn(errors.precio && "border-destructive")} /> {errors.precio &&

{errors.precio}

}
{/* Delete Dialog */} !o && setDeleteTarget(null)}> ¿Eliminar producto? Se eliminará el producto {deleteTarget?.nombre}. Esta acción no se puede deshacer. Cancelar Eliminar
) }