diff --git a/src/app/(app)/catalogo/CatalogoClient.tsx b/src/app/(app)/catalogo/CatalogoClient.tsx index 08ab561..244be88 100644 --- a/src/app/(app)/catalogo/CatalogoClient.tsx +++ b/src/app/(app)/catalogo/CatalogoClient.tsx @@ -17,6 +17,7 @@ import { Upload, } from "lucide-react"; import { formatCurrency, FASES as FASES_LABELS } from "@/lib/calculators"; +import { useConfirm, usePrompt, useToast } from "@/components/ui/DialogProvider"; import clsx from "clsx"; interface Servicio { @@ -71,6 +72,9 @@ type Tab = "categorias" | "paquetes" | "servicios"; const FASES = FASES_LABELS; export function CatalogoClient({ initialServicios, initialCategorias, initialPaquetes }: Props) { + const showConfirm = useConfirm(); + const showPrompt = usePrompt(); + const showToast = useToast(); const [tab, setTab] = useState("servicios"); const [servicios, setServicios] = useState(initialServicios); const [categorias, setCategorias] = useState(initialCategorias); @@ -106,14 +110,14 @@ export function CatalogoClient({ initialServicios, initialCategorias, initialPaq if (res.ok) { let msg = `${data.creados} servicio(s) importado(s).`; if (data.omitidos > 0) msg += ` ${data.omitidos} omitido(s).`; - if (data.errores?.length) msg += `\n\nErrores:\n${data.errores.join("\n")}`; - alert(msg); + showToast(msg, "success"); + if (data.errores?.length) showToast(`Errores:\n${data.errores.join("\n")}`, "error"); await refresh(); } else { - alert(data.error || "Error al importar"); + showToast(data.error || "Error al importar", "error"); } } catch { - alert("Error al importar CSV"); + showToast("Error al importar CSV", "error"); } finally { setImporting(false); e.target.value = ""; @@ -123,7 +127,7 @@ export function CatalogoClient({ initialServicios, initialCategorias, initialPaq const deleteCategoria = async (id: string) => { const res = await fetch(`/api/categorias/${id}`, { method: "DELETE" }); if (res.ok) setCategorias((p) => p.filter((c) => c.id !== id)); - else { const e = await res.json(); alert(e.error); } + else { const e = await res.json(); showToast(e.error, "error"); } }; const toggleCategoriaActivo = async (cat: Categoria) => { @@ -139,13 +143,24 @@ export function CatalogoClient({ initialServicios, initialCategorias, initialPaq }; const deletePaquete = async (id: string) => { - if (!confirm("Eliminar este paquete y todas sus fases?")) return; + const ok = await showConfirm({ + title: "Eliminar paquete", + message: "¿Eliminar este paquete y todas sus fases?", + confirmText: "Eliminar", + danger: true, + }); + if (!ok) return; const res = await fetch(`/api/paquetes/${id}`, { method: "DELETE" }); if (res.ok) setPaquetes((p) => p.filter((pk) => pk.id !== id)); }; const handleCreatePaquete = async () => { - const nombre = prompt("Nombre del paquete:"); + const nombre = await showPrompt({ + title: "Nuevo paquete", + label: "Nombre del paquete", + placeholder: "Ej. Paquete Emprendedor", + confirmText: "Crear", + }); if (!nombre) return; const res = await fetch("/api/paquetes", { method: "POST", @@ -159,7 +174,12 @@ export function CatalogoClient({ initialServicios, initialCategorias, initialPaq }; const addFaseToPaquete = async (paqueteId: string) => { - const nombre = prompt("Nombre de la fase:"); + const nombre = await showPrompt({ + title: "Nueva fase", + label: "Nombre de la fase", + placeholder: "Ej. Fase 1 - Setup", + confirmText: "Agregar", + }); if (!nombre) return; const pk = paquetes.find((p) => p.id === paqueteId); const orden = pk ? pk.fases.length : 0; @@ -179,7 +199,13 @@ export function CatalogoClient({ initialServicios, initialCategorias, initialPaq }; const deleteFase = async (paqueteId: string, faseId: string) => { - if (!confirm("Eliminar esta fase?")) return; + const ok = await showConfirm({ + title: "Eliminar fase", + message: "¿Eliminar esta fase?", + confirmText: "Eliminar", + danger: true, + }); + if (!ok) return; const res = await fetch(`/api/paquetes/${paqueteId}/manage`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -343,7 +369,8 @@ export function CatalogoClient({ initialServicios, initialCategorias, initialPaq + + + + ); +} + +function PromptBody({ + opts, + value, + setValue, + inputRef, + onCancel, + onSubmit, +}: { + opts: PromptOptions; + value: string; + setValue: (v: string) => void; + inputRef: React.RefObject; + onCancel: () => void; + onSubmit: () => void; +}) { + return ( +
{ + e.preventDefault(); + onSubmit(); + }} + > + {opts.title &&

{opts.title}

} + {opts.message &&

{opts.message}

} + {opts.label && } + setValue(e.target.value)} + placeholder={opts.placeholder} + className="w-full px-3 py-2 border border-border rounded-lg text-sm focus:outline-none focus:ring-1 focus:ring-primary" + /> +
+ + +
+
+ ); +} + +const TOAST_STYLE: Record = { + success: { cls: "border-green-200 bg-green-50 text-green-800", icon: }, + error: { cls: "border-red-200 bg-red-50 text-red-800", icon: }, + info: { cls: "border-blue-200 bg-blue-50 text-blue-800", icon: }, +}; + +function ToastCard({ item, onClose }: { item: ToastItem; onClose: () => void }) { + const s = TOAST_STYLE[item.kind]; + return ( +
+ {s.icon} + {item.message} + +
+ ); +} + +export function useDialogs(): DialogContextValue { + const ctx = useContext(DialogContext); + if (!ctx) throw new Error("useDialogs debe usarse dentro de "); + return ctx; +} + +export const useConfirm = () => useDialogs().confirm; +export const usePrompt = () => useDialogs().prompt; +export const useToast = () => useDialogs().toast;