Merge pull request 'roberto' (#4) from roberto into main

Reviewed-on: #4
This commit is contained in:
roberto.viveros 2025-06-16 05:03:23 +00:00
commit 60f334bbcd
15 changed files with 607 additions and 98 deletions

View File

@ -28,11 +28,13 @@
"lucide-react": "^0.488.0", "lucide-react": "^0.488.0",
"mysql2": "^3.14.1", "mysql2": "^3.14.1",
"next": "15.3.0", "next": "15.3.0",
"next-themes": "^0.4.6",
"papaparse": "^5.5.2", "papaparse": "^5.5.2",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.56.2", "react-hook-form": "^7.56.2",
"sonner": "^2.0.5",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
"tw-animate-css": "^1.2.5", "tw-animate-css": "^1.2.5",
"xlsx": "^0.18.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": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@ -7399,6 +7410,15 @@
"is-arrayish": "^0.3.1" "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": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",

View File

@ -29,11 +29,13 @@
"lucide-react": "^0.488.0", "lucide-react": "^0.488.0",
"mysql2": "^3.14.1", "mysql2": "^3.14.1",
"next": "15.3.0", "next": "15.3.0",
"next-themes": "^0.4.6",
"papaparse": "^5.5.2", "papaparse": "^5.5.2",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.56.2", "react-hook-form": "^7.56.2",
"sonner": "^2.0.5",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
"tw-animate-css": "^1.2.5", "tw-animate-css": "^1.2.5",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",

View File

@ -8,84 +8,149 @@ import {
Image, Image,
} from "@react-pdf/renderer"; } from "@react-pdf/renderer";
// Ajusta la ruta de tu logo si es necesario
const LOGO_SRC = "/encabezado.png";
const styles = StyleSheet.create({ const styles = StyleSheet.create({
page: { fontFamily: "Helvetica" }, page: {
title: { fontSize: 24, textAlign: "center", marginBottom: 20 }, fontFamily: "Helvetica",
nombre: { padding: 0,
backgroundColor: "#fff",
position: "relative",
},
title: {
fontSize: 18, fontSize: 18,
textAlign: "center", textAlign: "center",
marginTop: 40,
marginBottom: 10, 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", fontStyle: "italic",
}, },
curso: { participacion: {
fontSize: 30, fontSize: 13,
textAlign: "center", textAlign: "center",
marginBottom: 10, marginBottom: 8,
fontWeight: "bold", fontWeight: "normal",
}, },
section: { padding: 40, fontSize: 14 }, curso: {
competencias: { marginLeft: 20, marginTop: 5 }, fontSize: 15,
competencia: { fontSize: 12, marginBottom: 2 }, textAlign: "center",
footer: { fontWeight: "bold",
position: "absolute", marginBottom: 6,
bottom: 20, },
right: 40, detalle: {
fontSize: 11,
textAlign: "center",
marginBottom: 30,
fontWeight: "normal",
},
nombreDirector: {
fontSize: 10, 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: { qr: {
marginTop: 30,
alignSelf: "center",
width: 100, width: 100,
height: 100, height: 100,
alignSelf: "center",
marginTop: 30,
}, },
}); });
export default function Diploma({ alumno, formacion, fecha, qr }) { export default function Diploma({ alumno, formacion, fecha, qr }) {
// formacion: { tipo, nombre, competencias } // formacion: { tipo, nombre, horas, modalidad }
let tipoTexto = "formación"; // Puedes ajustar estos valores según tu modelo de datos
if (formacion?.tipo === "curso") tipoTexto = "curso"; const nombreCurso =
else if (formacion?.tipo === "inyeccion") tipoTexto = "inyección"; formacion?.nombre ||
else if (formacion?.tipo === "pildora") tipoTexto = "píldora educativa"; formacion?.curso?.nombre ||
formacion?.inyeccion?.nombre ||
formacion?.pildora?.nombre ||
"Sin curso";
const horas = formacion?.horas || 30;
const modalidad = formacion?.modalidad || "remota";
return ( return (
<Document> <Document>
<Page size="A4" style={styles.page}> <Page size="A4" style={styles.page}>
<Image src="/encabezado.png" /> {/* Logo */}
<Image src={LOGO_SRC} />
{/* Título */}
<Text style={styles.title}>Otorga la presente</Text> <Text style={styles.title}>Otorga la presente</Text>
<Text style={styles.title}>CONSTANCIA</Text> <Text style={styles.constancia}>CONSTANCIA</Text>
<Text style={styles.title}>a: </Text> <Text style={styles.label}>a:</Text>
<Text style={styles.nombre}>{alumno?.nombre} </Text>
<Text style={styles.title}> {/* Nombre del alumno */}
Por su asistencia{" "} <Text style={styles.nombre}>{alumno?.nombre}</Text>
{/* Participación */}
<Text style={styles.participacion}>
Por su{" "}
{formacion?.tipo === "curso" {formacion?.tipo === "curso"
? "al curso" ? "participación en el curso"
: formacion?.tipo === "inyeccion" : formacion?.tipo === "inyeccion"
? "a la inyección" ? "participación en la Inyección Educativa"
: formacion?.tipo === "pildora" : formacion?.tipo === "pildora"
? "a la píldora educativa" ? "asistencia a la píldora educativa"
: "a la formación"} : "participación en la formación"}
</Text> </Text>
<Text style={styles.curso}>{formacion?.nombre || "Sin formación"}</Text>
{(formacion?.tipo === "curso" || formacion?.tipo === "inyeccion") && {/* Nombre del curso/formación */}
formacion?.competencias?.length > 0 && ( <Text style={styles.curso}>{nombreCurso}</Text>
<View style={styles.competencias}>
<Text style={{ fontWeight: "bold", marginBottom: 4 }}> {/* Detalle de horas y modalidad */}
Competencias acreditadas: <Text style={styles.detalle}>
</Text> con duración de {horas} horas, modalidad {modalidad}.
{formacion.competencias.map((comp) => (
<Text key={comp.id} style={styles.competencia}>
- {comp.descripcion}
</Text>
))}
</View>
)}
<Text style={styles.title}>
Se expide en la ciudad de Xalapa, Ver., {fecha}
</Text> </Text>
{/* Firma */}
<Text style={styles.nombreDirector}>
Dr. Juan Manuel Gutiérrez Méndez
</Text>
<Text style={styles.director}>Director de Proyectos</Text>
{/* QR */}
{qr && <Image src={qr} style={styles.qr} />} {qr && <Image src={qr} style={styles.qr} />}
{/* Footer con fecha */}
<Text style={styles.footer}> <Text style={styles.footer}>
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}*/}
</Text> </Text>
</Page> </Page>
</Document> </Document>

View File

@ -13,6 +13,7 @@ import { useForm } from "react-hook-form";
import { mensajesSchema } from "@/schemas/mensajesSchema"; import { mensajesSchema } from "@/schemas/mensajesSchema";
import { Textarea } from "../ui/textarea"; import { Textarea } from "../ui/textarea";
import QRCode from "qrcode"; import QRCode from "qrcode";
import { toast } from "sonner";
function VistaPreviaDiplomaDialog({ function VistaPreviaDiplomaDialog({
open, open,
@ -105,6 +106,7 @@ function VistaPreviaDiplomaDialog({
} }
setMensaje("Mensajes guardados correctamente."); setMensaje("Mensajes guardados correctamente.");
toast.success("Mensajes guardados correctamente.");
}; };
useEffect(() => { useEffect(() => {
@ -216,7 +218,7 @@ function VistaPreviaDiplomaDialog({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full h-screen text-black overflow-y-auto"> <DialogContent className="h-screen w-full text-black overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Diploma</DialogTitle> <DialogTitle>Diploma</DialogTitle>
</DialogHeader> </DialogHeader>
@ -331,7 +333,7 @@ function VistaPreviaDiplomaDialog({
{/* Vista previa PDF */} {/* Vista previa PDF */}
{mostrarVistaPrevia && ( {mostrarVistaPrevia && (
<div className="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50">
<div className="bg-white rounded shadow-lg p-4"> <div className="bg-white w-full rounded shadow-lg p-4">
<div className="w-full h-[90vh] mb-4 border"> <div className="w-full h-[90vh] mb-4 border">
<PDFViewer width="100%" height="100%"> <PDFViewer width="100%" height="100%">
<Diploma <Diploma

View File

@ -0,0 +1,116 @@
import React, { useState } from "react";
import { supabaseClient } from "@/utils/supabase";
import { Button } from "@/components/ui/button";
export function InyeccionManualForm({ nombreSugerido = "" }) {
const [nombre, setNombre] = useState(nombreSugerido);
const [descripcion, setDescripcion] = useState("");
const [horas, setHoras] = useState("");
const [competencias, setCompetencias] = useState(""); // Nuevo campo
const [mensaje, setMensaje] = useState("");
const handleSubmit = async (e) => {
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 (
<form onSubmit={handleSubmit} className="flex flex-col gap-2 mt-2 text-black">
<input
type="text"
placeholder="Nombre"
value={nombre}
onChange={(e) => setNombre(e.target.value)}
required
className="border rounded px-2 py-1"
/>
<input
type="text"
placeholder="Descripción"
value={descripcion}
onChange={(e) => setDescripcion(e.target.value)}
className="border rounded px-2 py-1"
/>
<input
type="number"
placeholder="Horas"
value={horas}
onChange={(e) => setHoras(e.target.value)}
className="border rounded px-2 py-1"
/>
<input
type="text"
placeholder="Competencias (separadas por coma)"
value={competencias}
onChange={(e) => setCompetencias(e.target.value)}
className="border rounded px-2 py-1"
/>
<Button type="submit" className="bg-blue-500 text-black mt-2">
Registrar inyección
</Button>
{mensaje && <div className="text-sm mt-1">{mensaje}</div>}
</form>
);
}

View File

@ -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 (
<form onSubmit={handleSubmit} className="flex flex-col gap-2 mt-2 text-black">
<input
type="text"
placeholder="Nombre"
value={nombre}
onChange={(e) => setNombre(e.target.value)}
required
className="border rounded px-2 py-1"
/>
<input
type="text"
placeholder="Descripción"
value={descripcion}
onChange={(e) => setDescripcion(e.target.value)}
className="border rounded px-2 py-1"
/>
<input
type="number"
placeholder="Horas"
value={horas}
onChange={(e) => setHoras(e.target.value)}
className="border rounded px-2 py-1"
/>
<Button type="submit" className="bg-blue-500 text-black mt-2">
Registrar píldora
</Button>
{mensaje && <div className="text-sm mt-1">{mensaje}</div>}
</form>
);
}

View File

@ -0,0 +1,24 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner";
const Toaster = ({
...props
}) => {
const { theme = "system" } = useTheme()
return (
(<Sonner
theme={theme}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)"
}
}
{...props} />)
);
}
export { Toaster }

View File

@ -1,5 +1,11 @@
import "@/styles/globals.css"; import "@/styles/globals.css";
import { Toaster } from "sonner";
export default function App({ Component, pageProps }) { export default function App({ Component, pageProps }) {
return <Component {...pageProps} />; return (
<>
<Toaster richColors position="top-right" />
<Component {...pageProps} />;
</>
);
} }

View File

@ -15,6 +15,8 @@ import {
import { CursosManualForm } from "@/components/cursosManualForm"; import { CursosManualForm } from "@/components/cursosManualForm";
import { supabaseClient } from "@/utils/supabase"; import { supabaseClient } from "@/utils/supabase";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { InyeccionManualForm } from "@/components/inyeccionesManualForm";
import { PildoraManualForm } from "@/components/pildorasManualForm";
export default function AlumnosArchivo() { export default function AlumnosArchivo() {
const [archivo, setArchivo] = useState(null); const [archivo, setArchivo] = useState(null);
@ -25,6 +27,8 @@ export default function AlumnosArchivo() {
const [cursoFaltante, setCursoFaltante] = useState(""); const [cursoFaltante, setCursoFaltante] = useState("");
const [dialogoAdvertencia, setDialogoAdvertencia] = useState(false); const [dialogoAdvertencia, setDialogoAdvertencia] = useState(false);
const [rutaPendiente, setRutaPendiente] = useState(null); const [rutaPendiente, setRutaPendiente] = useState(null);
const [mostrarDialogFormacion, setMostrarDialogFormacion] = useState(false);
const [formacionFaltante, setFormacionFaltante] = useState(null);
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
@ -55,40 +59,92 @@ export default function AlumnosArchivo() {
const errores = []; const errores = [];
for (const alumno of datos) { for (const alumno of datos) {
// 1. Verifica si el curso existe let formacionId = null;
const { data: cursosEncontrados, error: errorCurso } = let tipo = (alumno.tipo || "").toLowerCase();
await supabaseClient
if (tipo === "curso") {
const { data: curso, error } = await supabaseClient
.from("curso") .from("curso")
.select("id") .select("id")
.eq("nombre", alumno.nombreCurso) .eq("nombre", alumno.formacion)
.maybeSingle(); .maybeSingle();
if (error) {
if (errorCurso) { errores.push({ alumno, error: "Error al buscar el curso" });
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; continue;
} }
if (!cursosEncontrados) { // Registrar alumno con el campo correcto según tipo
// Si no existe el curso, muestra el dialog para registrar el curso let body = {
setCursoFaltante(alumno.nombreCurso); nombre: alumno.nombre,
setMostrarDialogCurso(true); correo: alumno.correo,
setMensajeDialogo( telefono: alumno.telefono,
`El curso "${alumno.nombreCurso}" no existe. Por favor, regístralo primero.` tipo_formacion: tipo,
); curso_id: null,
setDialogoAbierto(true); inyeccion_id: null,
return; // Detiene el registro de alumnos 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", { const res = await fetch("/api/alumno", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify(body),
nombre: alumno.nombre,
correo: alumno.correo,
telefono: alumno.telefono,
curso_id: cursosEncontrados.id,
}),
}); });
const resultado = await res.json(); const resultado = await res.json();
@ -289,6 +345,38 @@ export default function AlumnosArchivo() {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Dialog para formacion faltante */}
<Dialog
open={mostrarDialogFormacion}
onOpenChange={setMostrarDialogFormacion}
>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">
Registrar {formacionFaltante?.tipo} faltante
</DialogTitle>
<DialogDescription>
La {formacionFaltante?.tipo} <b>{formacionFaltante?.nombre}</b> no
existe. Por favor, regístrala antes de continuar.
</DialogDescription>
</DialogHeader>
{formacionFaltante?.tipo === "curso" && (
<CursosManualForm nombreSugerido={formacionFaltante.nombre} />
)}
{formacionFaltante?.tipo === "inyeccion" && (
<InyeccionManualForm nombreSugerido={formacionFaltante.nombre} />
)}
{formacionFaltante?.tipo === "pildora" && (
<PildoraManualForm nombreSugerido={formacionFaltante.nombre} />
)}
<DialogFooter>
<Button onClick={() => setMostrarDialogFormacion(false)}>
Cerrar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Layout> </Layout>
); );
} }

View File

@ -334,7 +334,7 @@ export default function AlumnosVista() {
{alumno.tipo_formacion === "pildora" && {alumno.tipo_formacion === "pildora" &&
alumno.pildoras?.nombre} alumno.pildoras?.nombre}
</td> </td>
<td className="py-2 px-4 border-b"> <td className="py-2 px-4 border-b flex justify-center">
<button <button
onClick={() => iniciarEdicion(alumno)} onClick={() => iniciarEdicion(alumno)}
className="bg-blue-500 hover:bg-blue-700 text-white px-3 py-1 rounded mr-2" className="bg-blue-500 hover:bg-blue-700 text-white px-3 py-1 rounded mr-2"

View File

@ -6,23 +6,43 @@ export default async function handler(req, res) {
return res.status(405).json({ error: "Método no permitido" }); return res.status(405).json({ error: "Método no permitido" });
} }
const { nombre, correo, telefono, tipo_formacion, curso_id, inyeccion_id, pildoras_id } = req.body;
if (!nombre || !correo || !telefono || !tipo_formacion) {
return res.status(400).json({ error: "Faltan datos del alumno" });
}
// Validar que llegue el ID correcto según el tipo
if (
(tipo_formacion === "curso" && !curso_id) ||
(tipo_formacion === "inyeccion" && !inyeccion_id) ||
(tipo_formacion === "pildora" && !pildoras_id)
) {
return res.status(400).json({ error: "Faltan datos del alumno" });
}
try { try {
const supabase = createClient({ req, res }); const supabase = createClient({ req, res });
const { nombre, correo, telefono, curso_id } = req.body;
if (!nombre || !correo || !telefono || !curso_id) {
return res.status(400).json({ error: "Faltan datos del alumno" });
}
// Aquí tu lógica para insertar el alumno en la base de datos
// Ejemplo con Supabase:
const { data, error } = await supabase.from("alumno").insert([ const { data, error } = await supabase.from("alumno").insert([
{ nombre, correo, telefono, curso_id }, {
nombre,
correo,
telefono,
tipo_formacion,
curso_id: tipo_formacion === "curso" ? curso_id : null,
inyeccion_id: tipo_formacion === "inyeccion" ? inyeccion_id : null,
pildoras_id: tipo_formacion === "pildora" ? pildoras_id : null,
},
]); ]);
if (error) { if (error) {
return res.status(500).json({ error: "Error al insertar en Supabase", detalles: error.message }); return res.status(500).json({ error: error.message });
} }
return res.status(200).json({ mensaje: "Alumno registrado", data }); return res.status(200).json({ ok: true, data });
} catch (err) { } catch (err) {
return res.status(500).json({ error: "Error interno del servidor", detalles: err.message }); return res.status(500).json({ error: "Error interno del servidor", detalles: err.message });
} }

View File

@ -146,7 +146,7 @@ export default function DiplomasVista() {
{/* Dialog para crear diploma y vista previa juntos */} {/* Dialog para crear diploma y vista previa juntos */}
{mostrarDialog && ( {mostrarDialog && (
<div className="fixed inset-0 z-50 flex items-center justify-center w-screen"> <div className="fixed inset-0 z-50 flex items-center justify-center w-screen">
<div className="flex gap-8 bg-black bg-opacity-30 p-8 rounded"> <div className="flex bg-black bg-opacity-30 p-8 rounded">
<VistaPreviaDiplomaDialog <VistaPreviaDiplomaDialog
open={mostrarDialog} open={mostrarDialog}
onOpenChange={handleCloseDialog} onOpenChange={handleCloseDialog}

View File

@ -3,6 +3,7 @@ import Papa from "papaparse";
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";
import Layout from "@/components/layout/Layout"; import Layout from "@/components/layout/Layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { supabaseClient } from "@/utils/supabase";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -52,22 +53,79 @@ export default function InyeccionesArchivo() {
const errores = []; const errores = [];
for (const inyeccion of datos) { for (const inyeccion of datos) {
const res = await fetch("/api/inyeccion", { // 1. Procesar competencias (si existen)
method: "POST", let competenciasIds = [];
headers: { "Content-Type": "application/json" }, if (inyeccion.competencias) {
body: JSON.stringify({ const competenciasArr = inyeccion.competencias
nombre: inyeccion.nombre, .split(",")
horas: inyeccion.horas, .map((c) => c.trim())
descripcion: inyeccion.descripcion, .filter(Boolean);
}),
});
const resultado = await res.json(); for (const desc of competenciasArr) {
if (!res.ok) { // 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) {
errores.push({
inyeccion,
error: `Error insertando competencia "${desc}": ${errorInsert.message}`,
});
continue;
}
compId = insertada.id;
}
competenciasIds.push(compId);
}
}
// 2. Insertar la inyección
const { data: inyeccionInsertada, error: errorIny } = await supabaseClient
.from("inyeccion")
.insert([
{
nombre: inyeccion.nombre,
horas: inyeccion.horas,
descripcion: inyeccion.descripcion,
},
])
.select("id")
.single();
if (errorIny) {
errores.push({ errores.push({
inyeccion, inyeccion,
error: resultado.error || "Error desconocido", error: errorIny.message || "Error desconocido al insertar inyección",
}); });
continue;
}
// 3. Relacionar competencias con la inyección
if (inyeccionInsertada && competenciasIds.length > 0) {
const relaciones = competenciasIds.map((cid) => ({
inyeccion_id: inyeccionInsertada.id,
competencia_inyeccion_id: cid,
}));
const { error: errorRel } = await supabaseClient
.from("inyeccion_competencia_inyeccion")
.insert(relaciones);
if (errorRel) {
errores.push({
inyeccion,
error: `Error relacionando competencias: ${errorRel.message}`,
});
}
} }
} }

View File

@ -123,6 +123,29 @@ export default function InyeccionesVista() {
}; };
const eliminarInyeccion = async () => { const eliminarInyeccion = async () => {
// 1. Buscar si hay alumnos asociados a esta inyección
const { data: alumnos, error: errorAlumnos } = await supabaseClient
.from("alumno")
.select("id")
.eq("inyeccion_id", inyeccionAEliminar);
if (errorAlumnos) {
setModalMensaje("Error al verificar alumnos asociados: " + errorAlumnos.message);
setConfirmarEliminar(false);
setMostrarModal(true);
return;
}
if (alumnos && alumnos.length > 0) {
setModalMensaje(
"No se puede eliminar la inyección porque hay alumnos inscritos. Primero elimina o reasigna a los alumnos asociados."
);
setConfirmarEliminar(false);
setMostrarModal(true);
return;
}
// 2. Si no hay alumnos, eliminar la inyección
const { error } = await supabaseClient const { error } = await supabaseClient
.from("inyeccion") .from("inyeccion")
.delete() .delete()

View File

@ -80,6 +80,31 @@ export default function PildorasVista() {
}; };
const eliminarPildora = async () => { const eliminarPildora = async () => {
// 1. Buscar si hay alumnos asociados a esta píldora
const { data: alumnos, error: errorAlumnos } = await supabaseClient
.from("alumno")
.select("id")
.eq("pildoras_id", pildoraAEliminar);
if (errorAlumnos) {
setModalMensaje(
"Error al verificar alumnos asociados: " + errorAlumnos.message
);
setConfirmarEliminar(false);
setMostrarModal(true);
return;
}
if (alumnos && alumnos.length > 0) {
setModalMensaje(
"No se puede eliminar la píldora porque hay alumnos inscritos. Primero elimina o reasigna a los alumnos asociados."
);
setConfirmarEliminar(false);
setMostrarModal(true);
return;
}
// 2. Si no hay alumnos, eliminar la píldora
const { error } = await supabaseClient const { error } = await supabaseClient
.from("pildoras") .from("pildoras")
.delete() .delete()