From cddb2291c367b40623a7ed096ccdbb5e25ff2aed Mon Sep 17 00:00:00 2001 From: SirRobert-1 Date: Tue, 3 Jun 2025 18:02:10 -0600 Subject: [PATCH] feat: add manual and file upload pages for inyecciones and pildoras - Created InyeccionesManual component for manual entry of inyecciones. - Created InyeccionesVista component for viewing and managing inyecciones. - Created PildorasArchivo component for uploading pildoras via CSV/XLSX files. - Created PildorasManual component for manual entry of pildoras. - Created PildorasVista component for viewing and managing pildoras. - Created VistaGeneral component to provide an overview of cursos, inyecciones, and pildoras. - Added validation schemas for inyecciones and pildoras using Zod. - Updated AlumnosSchema to modify course selection validation. --- diplomas/src/components/app-sidebar.jsx | 43 ++++ diplomas/src/pages/api/inyeccion.js | 38 ++++ diplomas/src/pages/api/pildora.js | 36 +++ diplomas/src/pages/inyeccionesArchivo.jsx | 263 ++++++++++++++++++++++ diplomas/src/pages/inyeccionesManual.jsx | 118 ++++++++++ diplomas/src/pages/inyeccionesVista.jsx | 229 +++++++++++++++++++ diplomas/src/pages/pildorasArchivo.jsx | 261 +++++++++++++++++++++ diplomas/src/pages/pildorasManual.jsx | 127 +++++++++++ diplomas/src/pages/pildorasVista.jsx | 229 +++++++++++++++++++ diplomas/src/pages/vistaGeneral.jsx | 186 +++++++++++++++ diplomas/src/schemas/AlumnosSchema.js | 3 +- diplomas/src/schemas/Schema.js | 10 + 12 files changed, 1542 insertions(+), 1 deletion(-) create mode 100644 diplomas/src/pages/api/inyeccion.js create mode 100644 diplomas/src/pages/api/pildora.js create mode 100644 diplomas/src/pages/inyeccionesArchivo.jsx create mode 100644 diplomas/src/pages/inyeccionesManual.jsx create mode 100644 diplomas/src/pages/inyeccionesVista.jsx create mode 100644 diplomas/src/pages/pildorasArchivo.jsx create mode 100644 diplomas/src/pages/pildorasManual.jsx create mode 100644 diplomas/src/pages/pildorasVista.jsx create mode 100644 diplomas/src/pages/vistaGeneral.jsx create mode 100644 diplomas/src/schemas/Schema.js diff --git a/diplomas/src/components/app-sidebar.jsx b/diplomas/src/components/app-sidebar.jsx index 62d21dc..1d5afbe 100644 --- a/diplomas/src/components/app-sidebar.jsx +++ b/diplomas/src/components/app-sidebar.jsx @@ -36,6 +36,15 @@ const data = { }, ], }, + /*{ + title: "Vista general", + items: [ + { + title: "Vista general", + url: "/vistaGeneral", + }, + ], + },*/ { title: "Cursos", items: [ @@ -53,6 +62,40 @@ const data = { }, ], }, + { + title: "Inyecciones", + items: [ + { + title: "Vista de inyecciones", + url: "/inyeccionesVista", + }, + { + title: "Agregar manualmente", + url: "/inyeccionesManual", + }, + { + title: "Agregar desde archivo", + url: "/inyeccionesArchivo", + }, + ], + }, + { + title: "Pildoras", + items: [ + { + title: "Vista de pildoras", + url: "/pildorasVista", + }, + { + title: "Agregar manualmente", + url: "/pildorasManual", + }, + { + title: "Agregar desde archivo", + url: "/pildorasArchivo", + }, + ], + }, { title: "Diplomas", items: [ diff --git a/diplomas/src/pages/api/inyeccion.js b/diplomas/src/pages/api/inyeccion.js new file mode 100644 index 0000000..b9ba3b0 --- /dev/null +++ b/diplomas/src/pages/api/inyeccion.js @@ -0,0 +1,38 @@ +import { createClient } from "@/utils/supabase"; + +export default async function handler(req, res) { + if (req.method !== "POST") { + return res.status(405).json({ error: "Método no permitido" }); + } + + try { + const supabase = createClient({ req, res }); + const { nombre, horas, descripcion } = req.body; + + if (!nombre || !horas || !descripcion) { + return res.status(400).json({ error: "Faltan datos de la inyección" }); + } + + // Insertar la inyección + const { error } = await supabase + .from("inyeccion") + .insert([{ nombre, horas, descripcion }]); + + if (error) { + return res + .status(500) + .json({ + error: "Error al insertar la inyección", + detalles: error.message, + }); + } + + return res + .status(200) + .json({ mensaje: "Inyección registrada correctamente" }); + } catch (err) { + return res + .status(500) + .json({ error: "Error interno del servidor", detalles: err.message }); + } +} diff --git a/diplomas/src/pages/api/pildora.js b/diplomas/src/pages/api/pildora.js new file mode 100644 index 0000000..039cd37 --- /dev/null +++ b/diplomas/src/pages/api/pildora.js @@ -0,0 +1,36 @@ +import { createClient } from "@/utils/supabase"; + +export default async function handler(req, res) { + if (req.method !== "POST") { + return res.status(405).json({ error: "Método no permitido" }); + } + + try { + const supabase = createClient({ req, res }); + const { nombre, horas, descripcion } = req.body; + + if (!nombre || !horas || !descripcion) { + return res.status(400).json({ error: "Faltan datos de la píldora" }); + } + + // Insertar la píldora + const { error } = await supabase + .from("pildoras") + .insert([{ nombre, horas, descripcion }]); + + if (error) { + return res.status(500).json({ + error: "Error al insertar la píldora", + detalles: error.message, + }); + } + + return res + .status(200) + .json({ mensaje: "Píldora registrada correctamente" }); + } catch (err) { + return res + .status(500) + .json({ error: "Error interno del servidor", detalles: err.message }); + } +} diff --git a/diplomas/src/pages/inyeccionesArchivo.jsx b/diplomas/src/pages/inyeccionesArchivo.jsx new file mode 100644 index 0000000..21f3709 --- /dev/null +++ b/diplomas/src/pages/inyeccionesArchivo.jsx @@ -0,0 +1,263 @@ +import React, { useState, useEffect } from "react"; +import Papa from "papaparse"; +import * as XLSX from "xlsx"; +import Layout from "@/components/layout/Layout"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { useRouter } from "next/router"; + +export default function InyeccionesArchivo() { + const [archivo, setArchivo] = useState(null); + const [datos, setDatos] = useState([]); + const [dialogoAbierto, setDialogoAbierto] = useState(false); + const [mensajeDialogo, setMensajeDialogo] = useState(""); + const [dialogoCargando, setDialogoCargando] = useState(false); + const [dialogoAdvertencia, setDialogoAdvertencia] = useState(false); + const [rutaPendiente, setRutaPendiente] = useState(null); + const router = useRouter(); + + useEffect(() => { + if (archivo) extraerContenido(); + // eslint-disable-next-line + }, [archivo]); + + useEffect(() => { + const handleRouteChange = (url) => { + if (archivo && datos.length > 0) { + setDialogoAdvertencia(true); + setRutaPendiente(url); + throw "Bloqueo de navegación por archivo pendiente"; + } + }; + + router.events.on("routeChangeStart", handleRouteChange); + + return () => { + router.events.off("routeChangeStart", handleRouteChange); + }; + }, [archivo, datos, router]); + + const registrarInyecciones = async () => { + if (datos.length === 0) return; + + setDialogoCargando(true); + + const errores = []; + + for (const inyeccion of datos) { + const res = await fetch("/api/inyeccion", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + nombre: inyeccion.nombre, + horas: inyeccion.horas, + descripcion: inyeccion.descripcion, + }), + }); + + const resultado = await res.json(); + if (!res.ok) { + errores.push({ + inyeccion, + error: resultado.error || "Error desconocido", + }); + } + } + + setDialogoCargando(false); + + if (errores.length > 0) { + setMensajeDialogo( + `Se registraron algunos errores:\n${JSON.stringify(errores, null, 2)}` + ); + } else { + setMensajeDialogo( + "Todas las inyecciones fueron registradas correctamente." + ); + setArchivo(null); + setDatos([]); + } + setDialogoAbierto(true); + }; + + const manejarArchivo = (e) => { + const file = e.target.files[0]; + if (validarArchivo(file)) setArchivo(file); + }; + + const manejarSoltar = (e) => { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + if (validarArchivo(file)) setArchivo(file); + }; + + const manejarArrastrar = (e) => e.preventDefault(); + + const validarArchivo = (file) => { + if (file && (file.name.endsWith(".csv") || file.name.endsWith(".xlsx"))) { + return true; + } else { + setMensajeDialogo("Solo se permiten archivos .csv o .xlsx"); + setDialogoAbierto(true); + return false; + } + }; + + const extraerContenido = () => { + if (!archivo) return; + + const extension = archivo.name.split(".").pop().toLowerCase(); + + if (extension === "csv") { + Papa.parse(archivo, { + header: true, + skipEmptyLines: true, + complete: (result) => { + setDatos(result.data); + }, + error: (error) => { + console.error("Error al leer el CSV:", error.message); + }, + }); + } else if (extension === "xlsx") { + const reader = new FileReader(); + reader.onload = (e) => { + const data = new Uint8Array(e.target.result); + const workbook = XLSX.read(data, { type: "array" }); + const hoja = workbook.SheetNames[0]; + const contenido = XLSX.utils.sheet_to_json(workbook.Sheets[hoja], { + defval: "", + }); + setDatos(contenido); + }; + reader.readAsArrayBuffer(archivo); + } + }; + + return ( + +
+
+

+ Nueva inyección (archivo) +

+ + + + + {datos.length > 0 && ( +
+

Vista previa del archivo:

+ + + + {Object.keys(datos[0]).map((columna, index) => ( + + ))} + + + + {datos.map((fila, index) => ( + + {Object.values(fila).map((valor, i) => ( + + ))} + + ))} + +
+ {columna} +
+ {valor} +
+
+ )} +
+
+ + {/* Dialog Component */} + + + + Información + {mensajeDialogo} + + + + + + + + Cargando... + + Por favor espera, se están registrando las inyecciones. + + + + + + + + + Advertencia + + Si cambias de ventana perderás la subida del archivo. ¿Deseas + continuar? + + + + + + + + +
+ ); +} diff --git a/diplomas/src/pages/inyeccionesManual.jsx b/diplomas/src/pages/inyeccionesManual.jsx new file mode 100644 index 0000000..a0d0b65 --- /dev/null +++ b/diplomas/src/pages/inyeccionesManual.jsx @@ -0,0 +1,118 @@ +import React, { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Schema } from "@/schemas/Schema"; +import Layout from "@/components/layout/Layout"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { supabaseClient } from "@/utils/supabase"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; + +export default function InyeccionesManual() { + const [showDialog, setShowDialog] = useState(false); + const [dialogMsg, setDialogMsg] = useState(""); + const [loading, setLoading] = useState(false); + + const form = useForm({ + resolver: zodResolver(Schema), + defaultValues: { + nombre: "", + descripcion: "", + horas: 0, + }, + }); + const { + register, + handleSubmit, + formState: { errors }, + reset, + } = form; + + const onSubmit = async (data) => { + setLoading(true); + try { + const { nombre, descripcion, horas } = data; + const { error } = await supabaseClient + .from("inyeccion") + .insert([{ nombre, descripcion, horas: parseInt(horas, 10) }]); + if (error) throw error; + setDialogMsg("Inyección guardada exitosamente"); + reset(); + } catch (err) { + setDialogMsg("Error: " + (err.message || err)); + } finally { + setShowDialog(true); + setLoading(false); + } + }; + + return ( + +
+

+ Nueva inyección +

+
+ + {errors.nombre && ( +

{errors.nombre.message}

+ )} + +