diff --git a/diplomas/package-lock.json b/diplomas/package-lock.json index c90d0ae..9c0b691 100644 --- a/diplomas/package-lock.json +++ b/diplomas/package-lock.json @@ -28,11 +28,13 @@ "lucide-react": "^0.488.0", "mysql2": "^3.14.1", "next": "15.3.0", + "next-themes": "^0.4.6", "papaparse": "^5.5.2", "qrcode": "^1.5.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.56.2", + "sonner": "^2.0.5", "tailwind-merge": "^3.2.0", "tw-animate-css": "^1.2.5", "xlsx": "^0.18.5", @@ -6317,6 +6319,15 @@ } } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -7399,6 +7410,15 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sonner": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.5.tgz", + "integrity": "sha512-YwbHQO6cSso3HBXlbCkgrgzDNIhws14r4MO87Ofy+cV2X7ES4pOoAK3+veSmVTvqNx1BWUxlhPmZzP00Crk2aQ==", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/diplomas/package.json b/diplomas/package.json index c322137..c6c37ed 100644 --- a/diplomas/package.json +++ b/diplomas/package.json @@ -29,11 +29,13 @@ "lucide-react": "^0.488.0", "mysql2": "^3.14.1", "next": "15.3.0", + "next-themes": "^0.4.6", "papaparse": "^5.5.2", "qrcode": "^1.5.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.56.2", + "sonner": "^2.0.5", "tailwind-merge": "^3.2.0", "tw-animate-css": "^1.2.5", "xlsx": "^0.18.5", diff --git a/diplomas/src/components/Diploma.jsx b/diplomas/src/components/Diploma.jsx index 7e14251..35e3895 100644 --- a/diplomas/src/components/Diploma.jsx +++ b/diplomas/src/components/Diploma.jsx @@ -8,84 +8,149 @@ import { Image, } from "@react-pdf/renderer"; +// Ajusta la ruta de tu logo si es necesario +const LOGO_SRC = "/encabezado.png"; + const styles = StyleSheet.create({ - page: { fontFamily: "Helvetica" }, - title: { fontSize: 24, textAlign: "center", marginBottom: 20 }, - nombre: { + page: { + fontFamily: "Helvetica", + padding: 0, + backgroundColor: "#fff", + position: "relative", + }, + title: { fontSize: 18, textAlign: "center", + marginTop: 40, marginBottom: 10, + fontWeight: "normal", + }, + constancia: { + fontSize: 20, + textAlign: "center", + letterSpacing: 4, + marginBottom: 18, + fontWeight: "normal", + }, + label: { + fontSize: 13, + textAlign: "center", + marginBottom: 6, + fontWeight: "normal", + }, + nombre: { + fontSize: 28, + textAlign: "center", + marginBottom: 16, + fontFamily: "Times-Roman", fontStyle: "italic", }, - curso: { - fontSize: 30, + participacion: { + fontSize: 13, textAlign: "center", - marginBottom: 10, - fontWeight: "bold", + marginBottom: 8, + fontWeight: "normal", }, - section: { padding: 40, fontSize: 14 }, - competencias: { marginLeft: 20, marginTop: 5 }, - competencia: { fontSize: 12, marginBottom: 2 }, - footer: { - position: "absolute", - bottom: 20, - right: 40, + curso: { + fontSize: 15, + textAlign: "center", + fontWeight: "bold", + marginBottom: 6, + }, + detalle: { + fontSize: 11, + textAlign: "center", + marginBottom: 30, + fontWeight: "normal", + }, + nombreDirector: { fontSize: 10, - color: "#888", + textAlign: "center", + marginTop: 40, + marginBottom: 2, + }, + director: { + fontSize: 10, + textAlign: "center", + marginBottom: 18, + }, + footer: { + fontSize: 9, + textAlign: "center", + color: "#444", + position: "absolute", + bottom: 30, + left: 0, + right: 0, }, qr: { - marginTop: 30, - alignSelf: "center", width: 100, height: 100, + alignSelf: "center", + marginTop: 30, }, }); export default function Diploma({ alumno, formacion, fecha, qr }) { - // formacion: { tipo, nombre, competencias } - let tipoTexto = "formación"; - if (formacion?.tipo === "curso") tipoTexto = "curso"; - else if (formacion?.tipo === "inyeccion") tipoTexto = "inyección"; - else if (formacion?.tipo === "pildora") tipoTexto = "píldora educativa"; + // formacion: { tipo, nombre, horas, modalidad } + // Puedes ajustar estos valores según tu modelo de datos + const nombreCurso = + formacion?.nombre || + formacion?.curso?.nombre || + formacion?.inyeccion?.nombre || + formacion?.pildora?.nombre || + "Sin curso"; + const horas = formacion?.horas || 30; + const modalidad = formacion?.modalidad || "remota"; return ( - + {/* Logo */} + + + {/* Título */} Otorga la presente - CONSTANCIA - a: - {alumno?.nombre} - - Por su asistencia{" "} + CONSTANCIA + a: + + {/* Nombre del alumno */} + {alumno?.nombre} + + {/* Participación */} + + Por su{" "} {formacion?.tipo === "curso" - ? "al curso" + ? "participación en el curso" : formacion?.tipo === "inyeccion" - ? "a la inyección" + ? "participación en la Inyección Educativa" : formacion?.tipo === "pildora" - ? "a la píldora educativa" - : "a la formación"} + ? "asistencia a la píldora educativa" + : "participación en la formación"} - {formacion?.nombre || "Sin formación"} - {(formacion?.tipo === "curso" || formacion?.tipo === "inyeccion") && - formacion?.competencias?.length > 0 && ( - - - Competencias acreditadas: - - {formacion.competencias.map((comp) => ( - - - {comp.descripcion} - - ))} - - )} - - Se expide en la ciudad de Xalapa, Ver., {fecha} + + {/* Nombre del curso/formación */} + “{nombreCurso}” + + {/* Detalle de horas y modalidad */} + + con duración de {horas} horas, modalidad {modalidad}. + + {/* Firma */} + + Dr. Juan Manuel Gutiérrez Méndez + + Director de Proyectos + + {/* QR */} {qr && } + + {/* Footer con fecha */} - Verifica este diploma en: http://localhost:3000/alumno/{alumno?.id} + Se expide en la ciudad de Xalapa, Ver., a los {fecha} + {"\n"} + {/*Verifica este diploma en: http://localhost:3000/alumno/{alumno?.id}*/} diff --git a/diplomas/src/components/dialogs/vistaPreviaDiplomaDialog.jsx b/diplomas/src/components/dialogs/vistaPreviaDiplomaDialog.jsx index 25aa7cc..f8be412 100644 --- a/diplomas/src/components/dialogs/vistaPreviaDiplomaDialog.jsx +++ b/diplomas/src/components/dialogs/vistaPreviaDiplomaDialog.jsx @@ -13,6 +13,7 @@ import { useForm } from "react-hook-form"; import { mensajesSchema } from "@/schemas/mensajesSchema"; import { Textarea } from "../ui/textarea"; import QRCode from "qrcode"; +import { toast } from "sonner"; function VistaPreviaDiplomaDialog({ open, @@ -105,6 +106,7 @@ function VistaPreviaDiplomaDialog({ } setMensaje("Mensajes guardados correctamente."); + toast.success("Mensajes guardados correctamente."); }; useEffect(() => { @@ -216,7 +218,7 @@ function VistaPreviaDiplomaDialog({ return ( - + Diploma @@ -331,7 +333,7 @@ function VistaPreviaDiplomaDialog({ {/* Vista previa PDF */} {mostrarVistaPrevia && (
-
+
{ + e.preventDefault(); + // 1. Insertar la inyección + const { data: inyeccion, error } = await supabaseClient + .from("inyeccion") + .insert([ + { + nombre, + descripcion, + horas: horas ? parseInt(horas) : null, + }, + ]) + .select("id") + .single(); + + if (error) { + setMensaje("Error al registrar la inyección: " + error.message); + return; + } + + // 2. Procesar competencias si hay + if (inyeccion && competencias.trim()) { + const competenciasArr = competencias + .split(",") + .map((c) => c.trim()) + .filter(Boolean); + let competenciasIds = []; + for (const desc of competenciasArr) { + // Buscar si ya existe la competencia + let { data: existente } = await supabaseClient + .from("competencia_inyeccion") + .select("id") + .eq("descripcion", desc) + .maybeSingle(); + + let compId = existente?.id; + if (!compId) { + // Insertar si no existe + const { data: insertada, error: errorInsert } = await supabaseClient + .from("competencia_inyeccion") + .insert([{ descripcion: desc }]) + .select("id") + .single(); + if (errorInsert) continue; + compId = insertada.id; + } + competenciasIds.push(compId); + } + // Relacionar competencias con la inyección + if (competenciasIds.length > 0) { + const relaciones = competenciasIds.map((cid) => ({ + inyeccion_id: inyeccion.id, + competencia_inyeccion_id: cid, + })); + await supabaseClient + .from("inyeccion_competencia_inyeccion") + .insert(relaciones); + } + } + + setMensaje("¡Inyección registrada correctamente!"); + setNombre(""); + setDescripcion(""); + setHoras(""); + setCompetencias(""); + }; + + return ( +
+ setNombre(e.target.value)} + required + className="border rounded px-2 py-1" + /> + setDescripcion(e.target.value)} + className="border rounded px-2 py-1" + /> + setHoras(e.target.value)} + className="border rounded px-2 py-1" + /> + setCompetencias(e.target.value)} + className="border rounded px-2 py-1" + /> + + {mensaje &&
{mensaje}
} +
+ ); +} \ No newline at end of file diff --git a/diplomas/src/components/pildorasManualForm.jsx b/diplomas/src/components/pildorasManualForm.jsx new file mode 100644 index 0000000..2b5291d --- /dev/null +++ b/diplomas/src/components/pildorasManualForm.jsx @@ -0,0 +1,60 @@ +import React, { useState } from "react"; +import { supabaseClient } from "@/utils/supabase"; +import { Button } from "@/components/ui/button"; + +export function PildoraManualForm({ nombreSugerido = "" }) { + const [nombre, setNombre] = useState(nombreSugerido); + const [descripcion, setDescripcion] = useState(""); + const [horas, setHoras] = useState(""); + const [mensaje, setMensaje] = useState(""); + + const handleSubmit = async (e) => { + e.preventDefault(); + const { error } = await supabaseClient.from("pildoras").insert([ + { + nombre, + descripcion, + horas: horas ? parseInt(horas) : null, + }, + ]); + if (error) { + setMensaje("Error al registrar la píldora: " + error.message); + } else { + setMensaje("¡Píldora registrada correctamente!"); + setNombre(""); + setDescripcion(""); + setHoras(""); + } + }; + + return ( +
+ setNombre(e.target.value)} + required + className="border rounded px-2 py-1" + /> + setDescripcion(e.target.value)} + className="border rounded px-2 py-1" + /> + setHoras(e.target.value)} + className="border rounded px-2 py-1" + /> + + {mensaje &&
{mensaje}
} +
+ ); +} \ No newline at end of file diff --git a/diplomas/src/components/ui/sonner.jsx b/diplomas/src/components/ui/sonner.jsx new file mode 100644 index 0000000..132fc2e --- /dev/null +++ b/diplomas/src/components/ui/sonner.jsx @@ -0,0 +1,24 @@ +import { useTheme } from "next-themes" +import { Toaster as Sonner } from "sonner"; + +const Toaster = ({ + ...props +}) => { + const { theme = "system" } = useTheme() + + return ( + () + ); +} + +export { Toaster } diff --git a/diplomas/src/pages/_app.js b/diplomas/src/pages/_app.js index b97e52f..79d88a9 100644 --- a/diplomas/src/pages/_app.js +++ b/diplomas/src/pages/_app.js @@ -1,5 +1,11 @@ import "@/styles/globals.css"; +import { Toaster } from "sonner"; export default function App({ Component, pageProps }) { - return ; + return ( + <> + + ; + + ); } diff --git a/diplomas/src/pages/alumnosArchivo.jsx b/diplomas/src/pages/alumnosArchivo.jsx index c201a0a..f5b4652 100644 --- a/diplomas/src/pages/alumnosArchivo.jsx +++ b/diplomas/src/pages/alumnosArchivo.jsx @@ -15,6 +15,8 @@ import { import { CursosManualForm } from "@/components/cursosManualForm"; import { supabaseClient } from "@/utils/supabase"; import { useRouter } from "next/router"; +import { InyeccionManualForm } from "@/components/inyeccionesManualForm"; +import { PildoraManualForm } from "@/components/pildorasManualForm"; export default function AlumnosArchivo() { const [archivo, setArchivo] = useState(null); @@ -25,6 +27,8 @@ export default function AlumnosArchivo() { const [cursoFaltante, setCursoFaltante] = useState(""); const [dialogoAdvertencia, setDialogoAdvertencia] = useState(false); const [rutaPendiente, setRutaPendiente] = useState(null); + const [mostrarDialogFormacion, setMostrarDialogFormacion] = useState(false); + const [formacionFaltante, setFormacionFaltante] = useState(null); const router = useRouter(); useEffect(() => { @@ -55,40 +59,92 @@ export default function AlumnosArchivo() { const errores = []; for (const alumno of datos) { - // 1. Verifica si el curso existe - const { data: cursosEncontrados, error: errorCurso } = - await supabaseClient + let formacionId = null; + let tipo = (alumno.tipo || "").toLowerCase(); + + if (tipo === "curso") { + const { data: curso, error } = await supabaseClient .from("curso") .select("id") - .eq("nombre", alumno.nombreCurso) + .eq("nombre", alumno.formacion) .maybeSingle(); - - if (errorCurso) { - errores.push({ alumno, error: "Error al buscar el curso" }); + if (error) { + errores.push({ alumno, error: "Error al buscar el curso" }); + continue; + } + if (!curso) { + setFormacionFaltante({ tipo: "curso", nombre: alumno.formacion }); + setMostrarDialogFormacion(true); + setMensajeDialogo( + `El curso "${alumno.formacion}" no existe. Por favor, regístralo primero.` + ); + setDialogoAbierto(true); + return; + } + formacionId = curso.id; + } else if (tipo === "inyeccion") { + const { data: inyeccion, error } = await supabaseClient + .from("inyeccion") + .select("id") + .eq("nombre", alumno.formacion) + .maybeSingle(); + if (error) { + errores.push({ alumno, error: "Error al buscar la inyección" }); + continue; + } + if (!inyeccion) { + setFormacionFaltante({ tipo: "inyeccion", nombre: alumno.formacion }); + setMostrarDialogFormacion(true); + setMensajeDialogo( + `La inyección "${alumno.formacion}" no existe. Por favor, regístrala primero.` + ); + setDialogoAbierto(true); + return; + } + formacionId = inyeccion.id; + } else if (tipo === "pildora") { + const { data: pildora, error } = await supabaseClient + .from("pildoras") + .select("id") + .eq("nombre", alumno.formacion) + .maybeSingle(); + if (error) { + errores.push({ alumno, error: "Error al buscar la píldora" }); + continue; + } + if (!pildora) { + setFormacionFaltante({ tipo: "pildora", nombre: alumno.formacion }); + setMostrarDialogFormacion(true); + setMensajeDialogo( + `La píldora "${alumno.formacion}" no existe. Por favor, regístrala primero.` + ); + setDialogoAbierto(true); + return; + } + formacionId = pildora.id; + } else { + errores.push({ alumno, error: "Tipo de formación no válido" }); continue; } - if (!cursosEncontrados) { - // Si no existe el curso, muestra el dialog para registrar el curso - setCursoFaltante(alumno.nombreCurso); - setMostrarDialogCurso(true); - setMensajeDialogo( - `El curso "${alumno.nombreCurso}" no existe. Por favor, regístralo primero.` - ); - setDialogoAbierto(true); - return; // Detiene el registro de alumnos - } + // Registrar alumno con el campo correcto según tipo + let body = { + nombre: alumno.nombre, + correo: alumno.correo, + telefono: alumno.telefono, + tipo_formacion: tipo, + curso_id: null, + inyeccion_id: null, + pildoras_id: null, + }; + if (tipo === "curso") body.curso_id = formacionId; + if (tipo === "inyeccion") body.inyeccion_id = formacionId; + if (tipo === "pildora") body.pildoras_id = formacionId; - // 2. Si existe, registra el alumno con el curso_id correcto const res = await fetch("/api/alumno", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - nombre: alumno.nombre, - correo: alumno.correo, - telefono: alumno.telefono, - curso_id: cursosEncontrados.id, - }), + body: JSON.stringify(body), }); const resultado = await res.json(); @@ -289,6 +345,38 @@ export default function AlumnosArchivo() {
+ + {/* Dialog para formacion faltante */} + + + + + Registrar {formacionFaltante?.tipo} faltante + + + La {formacionFaltante?.tipo} {formacionFaltante?.nombre} no + existe. Por favor, regístrala antes de continuar. + + + {formacionFaltante?.tipo === "curso" && ( + + )} + {formacionFaltante?.tipo === "inyeccion" && ( + + )} + {formacionFaltante?.tipo === "pildora" && ( + + )} + + + + + ); } diff --git a/diplomas/src/pages/alumnosVista.jsx b/diplomas/src/pages/alumnosVista.jsx index c7f607b..0fe726f 100644 --- a/diplomas/src/pages/alumnosVista.jsx +++ b/diplomas/src/pages/alumnosVista.jsx @@ -334,7 +334,7 @@ export default function AlumnosVista() { {alumno.tipo_formacion === "pildora" && alumno.pildoras?.nombre} - +