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",
"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",

View File

@ -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",

View File

@ -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 (
<Document>
<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}>CONSTANCIA</Text>
<Text style={styles.title}>a: </Text>
<Text style={styles.nombre}>{alumno?.nombre} </Text>
<Text style={styles.title}>
Por su asistencia{" "}
<Text style={styles.constancia}>CONSTANCIA</Text>
<Text style={styles.label}>a:</Text>
{/* Nombre del alumno */}
<Text style={styles.nombre}>{alumno?.nombre}</Text>
{/* Participación */}
<Text style={styles.participacion}>
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"}
</Text>
<Text style={styles.curso}>{formacion?.nombre || "Sin formación"}</Text>
{(formacion?.tipo === "curso" || formacion?.tipo === "inyeccion") &&
formacion?.competencias?.length > 0 && (
<View style={styles.competencias}>
<Text style={{ fontWeight: "bold", marginBottom: 4 }}>
Competencias acreditadas:
</Text>
{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}
{/* Nombre del curso/formación */}
<Text style={styles.curso}>{nombreCurso}</Text>
{/* Detalle de horas y modalidad */}
<Text style={styles.detalle}>
con duración de {horas} horas, modalidad {modalidad}.
</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} />}
{/* Footer con fecha */}
<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>
</Page>
</Document>

View File

@ -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 (
<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>
<DialogTitle>Diploma</DialogTitle>
</DialogHeader>
@ -331,7 +333,7 @@ function VistaPreviaDiplomaDialog({
{/* Vista previa PDF */}
{mostrarVistaPrevia && (
<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">
<PDFViewer width="100%" height="100%">
<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 { Toaster } from "sonner";
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 { 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() {
</DialogFooter>
</DialogContent>
</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>
);
}

View File

@ -334,7 +334,7 @@ export default function AlumnosVista() {
{alumno.tipo_formacion === "pildora" &&
alumno.pildoras?.nombre}
</td>
<td className="py-2 px-4 border-b">
<td className="py-2 px-4 border-b flex justify-center">
<button
onClick={() => iniciarEdicion(alumno)}
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" });
}
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 {
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([
{ 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) {
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) {
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 */}
{mostrarDialog && (
<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
open={mostrarDialog}
onOpenChange={handleCloseDialog}

View File

@ -3,6 +3,7 @@ import Papa from "papaparse";
import * as XLSX from "xlsx";
import Layout from "@/components/layout/Layout";
import { Button } from "@/components/ui/button";
import { supabaseClient } from "@/utils/supabase";
import {
Dialog,
DialogContent,
@ -52,22 +53,79 @@ export default function InyeccionesArchivo() {
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,
}),
});
// 1. Procesar competencias (si existen)
let competenciasIds = [];
if (inyeccion.competencias) {
const competenciasArr = inyeccion.competencias
.split(",")
.map((c) => c.trim())
.filter(Boolean);
const resultado = await res.json();
if (!res.ok) {
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) {
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({
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 () => {
// 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
.from("inyeccion")
.delete()

View File

@ -80,6 +80,31 @@ export default function PildorasVista() {
};
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
.from("pildoras")
.delete()