Merge pull request 'roberto' (#4) from roberto into main
Reviewed-on: #4
This commit is contained in:
commit
60f334bbcd
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 }
|
|
@ -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} />;
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue