This commit is contained in:
SirRobert-1 2025-05-07 08:19:51 -06:00
commit 0718f041a0
9 changed files with 973 additions and 94 deletions

File diff suppressed because it is too large Load Diff

View File

@ -21,8 +21,11 @@
"@supabase/supabase-js": "^2.49.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cors": "^2.8.5",
"diplomas": "file:",
"express": "^5.1.0",
"lucide-react": "^0.488.0",
"mysql2": "^3.14.1",
"next": "15.3.0",
"papaparse": "^5.5.2",
"react": "^19.0.0",

View File

@ -52,6 +52,15 @@ const data = {
},
],
},
{
title: "Diplomas",
items: [
{
title: "Creación de diplomas",
url: "/diplomasVista",
},
],
},
],
};

View File

@ -1,34 +1,75 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import Papa from "papaparse";
import * as XLSX from "xlsx";
import Layout from "@/components/layout/Layout";
import { Button } from "@/components/ui/button";
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
export default function AlumnosArchivo() {
const [archivo, setArchivo] = useState(null);
const [datos, setDatos] = useState([]);
const [dialogoAbierto, setDialogoAbierto] = useState(false);
const [mensajeDialogo, setMensajeDialogo] = useState("");
useEffect(() => {
if (archivo) extraerContenido();
}, [archivo]);
const registrarAlumnos = async () => {
if (datos.length === 0) return;
const errores = [];
for (const alumno of datos) {
const res = await fetch("/api/alumno", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
nombre: alumno.nombre,
correo: alumno.correo,
nombreCurso: alumno.nombreCurso,
}),
});
const resultado = await res.json();
if (!res.ok) {
errores.push({ alumno, error: resultado.error || "Error desconocido" });
}
}
if (errores.length > 0) {
setMensajeDialogo(`Se registraron algunos errores:\n${JSON.stringify(errores, null, 2)}`);
} else {
setMensajeDialogo("Todos los alumnos fueron registrados correctamente.");
setArchivo(null);
setDatos([]);
}
setDialogoAbierto(true);
};
const manejarArchivo = (e) => {
const file = e.target.files[0];
if (file && (file.name.endsWith(".csv") || file.name.endsWith(".xlsx"))) {
setArchivo(file);
} else {
alert("Solo se permiten archivos .csv o .xlsx");
}
if (validarArchivo(file)) setArchivo(file);
};
const manejarSoltar = (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file && (file.name.endsWith(".csv") || file.name.endsWith(".xlsx"))) {
setArchivo(file);
} else {
alert("Solo se permiten archivos .csv o .xlsx");
}
if (validarArchivo(file)) setArchivo(file);
};
const manejarArrastrar = (e) => e.preventDefault();
const validarArchivo = (file) => {
if (file && (file.name.endsWith(".csv") || file.name.endsWith(".xlsx"))) {
return true;
} else {
setMensajeDialogo("Solo se permiten archivos .csv o .xlsx");
setDialogoAbierto(true);
return false;
}
};
const extraerContenido = () => {
if (!archivo) return;
@ -39,7 +80,6 @@ export default function AlumnosArchivo() {
header: true,
skipEmptyLines: true,
complete: (result) => {
console.log("Contenido CSV:", result.data);
setDatos(result.data);
},
error: (error) => {
@ -55,7 +95,6 @@ export default function AlumnosArchivo() {
const contenido = XLSX.utils.sheet_to_json(workbook.Sheets[hoja], {
defval: "",
});
console.log("Contenido XLSX:", contenido);
setDatos(contenido);
};
reader.readAsArrayBuffer(archivo);
@ -64,7 +103,7 @@ export default function AlumnosArchivo() {
return (
<Layout>
<div className="w-[60vw] pt-10 flex flex-col items-end justify-center">
<div className="w-[60vw] pt-10 flex flex-col items-end justify-center text-black">
<div className="bg-white p-8 font-sans text-center w-[70%] flex flex-col items-center">
<h1 className="text-xl font-semibold mb-4 text-black">
Nuevo alumno
@ -90,22 +129,47 @@ export default function AlumnosArchivo() {
</label>
<Button
onClick={extraerContenido}
className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-md"
onClick={registrarAlumnos}
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md mt-4"
>
Extraer contenido
Registrar alumnos
</Button>
{datos.length > 0 && (
<div className="mt-6 text-left w-full max-w-md">
<h3 className="font-bold mb-2">Contenido extraído:</h3>
<pre className="bg-gray-100 p-2 rounded overflow-x-auto text-sm">
{JSON.stringify(datos, null, 2)}
</pre>
<div className="mt-6 text-left w-full overflow-auto">
<h3 className="font-bold mb-2">Vista previa del archivo:</h3>
<table className="min-w-full bg-white border border-gray-300 text-sm">
<thead className="bg-gray-100 text-gray-700">
<tr>
{Object.keys(datos[0]).map((columna, index) => (
<th key={index} className="border px-4 py-2">{columna}</th>
))}
</tr>
</thead>
<tbody>
{datos.map((fila, index) => (
<tr key={index}>
{Object.values(fila).map((valor, i) => (
<td key={i} className="border px-4 py-1">{valor}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* Dialog Component */}
<Dialog open={dialogoAbierto} onOpenChange={setDialogoAbierto}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">Información</DialogTitle>
<DialogDescription>{mensajeDialogo}</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</Layout>
);
}
}

View File

@ -20,7 +20,6 @@ import {
SelectValue,
} from "@/components/ui/select";
export default function AlumnosVista() {
const [alumnos, setAlumnos] = useState([]);
const [alumnoEditando, setAlumnoEditando] = useState(null);
@ -28,10 +27,13 @@ export default function AlumnosVista() {
const [nuevoCorreo, setNuevoCorreo] = useState("");
const [mostrarModal, setMostrarModal] = useState(false);
const [modalMensaje, setModalMensaje] = useState("");
const [nuevoCurso, setNuevoCurso] = useState("");
const [nuevoCurso, setNuevoCurso] = useState("");
const [cursos, setCursos] = useState([]);
// Estado para confirmación de eliminación
const [confirmarEliminar, setConfirmarEliminar] = useState(false);
const [alumnoAEliminar, setAlumnoAEliminar] = useState(null);
useEffect(() => {
cargarAlumnos();
cargarCursos();
@ -60,7 +62,7 @@ export default function AlumnosVista() {
setAlumnoEditando(alumno.id);
setNuevoNombre(alumno.nombre);
setNuevoCorreo(alumno.correo);
setNuevoCurso(alumno.nombreCurso); // Asumiendo que tienes un campo cursoId en el alumno
setNuevoCurso(alumno.nombreCurso);
};
// Cancelar edición
@ -78,7 +80,7 @@ export default function AlumnosVista() {
.update({
nombre: nuevoNombre,
correo: nuevoCorreo,
nombreCurso: nuevoCurso, // Asumiendo que tienes un campo cursoId en el alumno
nombreCurso: nuevoCurso,
})
.eq("id", id);
@ -93,9 +95,15 @@ export default function AlumnosVista() {
setMostrarModal(true);
};
// Confirmar eliminación
const confirmarEliminacion = (id) => {
setAlumnoAEliminar(id);
setConfirmarEliminar(true);
};
// Eliminar alumno
const eliminarAlumno = async (id) => {
const { error } = await supabaseClient.from("alumno").delete().eq("id", id);
const eliminarAlumno = async () => {
const { error } = await supabaseClient.from("alumno").delete().eq("id", alumnoAEliminar);
if (error) {
console.error("Error eliminando alumno:", error.message);
setModalMensaje("Error al eliminar el alumno");
@ -103,6 +111,7 @@ export default function AlumnosVista() {
setModalMensaje("Alumno eliminado exitosamente");
await cargarAlumnos();
}
setConfirmarEliminar(false);
setMostrarModal(true);
};
@ -191,7 +200,7 @@ export default function AlumnosVista() {
<Button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 rounded"
onClick={() => eliminarAlumno(alumno.id)}
onClick={() => confirmarEliminacion(alumno.id)}
>
Eliminar
</Button>
@ -203,7 +212,33 @@ export default function AlumnosVista() {
</table>
</div>
{/* Modal */}
{/* Modal de confirmación */}
<Dialog open={confirmarEliminar} onOpenChange={setConfirmarEliminar}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">Confirmar eliminación</DialogTitle>
<DialogDescription>
¿Estás seguro de que deseas eliminar este alumno? Esta acción no se puede deshacer.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
className="bg-red-500 hover:bg-red-700 text-white"
onClick={eliminarAlumno}
>
Eliminar
</Button>
<Button
className="bg-gray-400 hover:bg-gray-600 text-white"
onClick={() => setConfirmarEliminar(false)}
>
Cancelar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Modal de resultado */}
<Dialog open={mostrarModal} onOpenChange={setMostrarModal}>
<DialogContent>
<DialogHeader>
@ -219,4 +254,4 @@ export default function AlumnosVista() {
</Dialog>
</Layout>
);
}
}

View File

@ -0,0 +1,29 @@
// pages/api/alumno.js
import { createClient } from "@/utils/supabase";
export default async function handler(req, res) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Método no permitido" });
}
try {
const supabase = createClient({ req, res });
const { nombre, correo, nombreCurso } = req.body;
if (!nombre || !correo || !nombreCurso) {
return res.status(400).json({ error: "Faltan datos del alumno" });
}
const { data, error } = await supabase.from("alumno").insert([
{ nombre, correo, nombreCurso },
]);
if (error) {
return res.status(500).json({ error: "Error al insertar en Supabase", detalles: error.message });
}
return res.status(200).json({ mensaje: "Alumno registrado", data });
} catch (err) {
return res.status(500).json({ error: "Error interno del servidor", detalles: err.message });
}
}

View File

@ -0,0 +1,29 @@
// pages/api/curso.js
import { createClient } from "@/utils/supabase";
export default async function handler(req, res) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Método no permitido" });
}
try {
const supabase = createClient({ req, res });
const { nombre, horas, descripcion, competencias } = req.body;
if (!nombre || !horas || !descripcion || !competencias) {
return res.status(400).json({ error: "Faltan datos del curso" });
}
const { data, error } = await supabase
.from("curso")
.insert([{ nombre, horas, descripcion, competencias }]);
if (error) {
return res.status(500).json({ error: "Error al insertar en Supabase", detalles: error.message });
}
return res.status(200).json({ mensaje: "Curso registrado", data });
} catch (err) {
return res.status(500).json({ error: "Error interno del servidor", detalles: err.message });
}
}

View File

@ -1,80 +1,178 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import Papa from "papaparse";
import * as XLSX from "xlsx";
import Layout from "@/components/layout/Layout";
import { Button } from "@/components/ui/button";
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
export default function CursosArchivo() {
export default function cursosArchivo() {
const [archivo, setArchivo] = useState(null);
const [datosCSV, setDatosCSV] = useState([]);
const [datos, setDatos] = useState([]);
const [dialogoAbierto, setDialogoAbierto] = useState(false);
const [mensajeDialogo, setMensajeDialogo] = useState("");
useEffect(() => {
if (archivo) extraerContenido();
}, [archivo]);
const registrarCursos = async () => {
if (datos.length === 0) return;
const errores = [];
for (const curso of datos) {
const res = await fetch("/api/curso", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
nombre: curso.nombre,
horas: curso.horas,
descripcion: curso.descripcion,
competencias: curso.competencias
? curso.competencias.split(",").map(c => c.trim())
: [],
}),
});
const resultado = await res.json();
if (!res.ok) {
errores.push({ curso, error: resultado.error || "Error desconocido" });
}
}
if (errores.length > 0) {
setMensajeDialogo(`Se registraron algunos errores:\n${JSON.stringify(errores, null, 2)}`);
} else {
setMensajeDialogo("Todos los cursos fueron registrados correctamente.");
setArchivo(null);
setDatos([]);
}
setDialogoAbierto(true);
};
const manejarArchivo = (e) => {
const file = e.target.files[0];
setArchivo(file);
if (validarArchivo(file)) setArchivo(file);
};
const manejarSoltar = (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
setArchivo(file);
if (validarArchivo(file)) setArchivo(file);
};
const manejarArrastrar = (e) => {
e.preventDefault();
const manejarArrastrar = (e) => e.preventDefault();
const validarArchivo = (file) => {
if (file && (file.name.endsWith(".csv") || file.name.endsWith(".xlsx"))) {
return true;
} else {
setMensajeDialogo("Solo se permiten archivos .csv o .xlsx");
setDialogoAbierto(true);
return false;
}
};
const extraerContenido = () => {
if (!archivo) return;
Papa.parse(archivo, {
header: true,
skipEmptyLines: true,
complete: (result) => {
console.log("Contenido CSV:", result.data);
setDatosCSV(result.data);
},
error: (error) => {
console.error("Error al leer el CSV:", error.message);
},
});
const extension = archivo.name.split(".").pop().toLowerCase();
if (extension === "csv") {
Papa.parse(archivo, {
header: true,
skipEmptyLines: true,
complete: (result) => {
setDatos(result.data);
},
error: (error) => {
console.error("Error al leer el CSV:", error.message);
},
});
} else if (extension === "xlsx") {
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: "array" });
const hoja = workbook.SheetNames[0];
const contenido = XLSX.utils.sheet_to_json(workbook.Sheets[hoja], {
defval: "",
});
setDatos(contenido);
};
reader.readAsArrayBuffer(archivo);
}
};
return (
<Layout>
<div className="w-[60vw] pt-10 flex flex-col items-end justify-center">
<div className="w-[60vw] pt-10 flex flex-col items-end justify-center text-black">
<div className="bg-white p-8 font-sans text-center w-[70%] flex flex-col items-center">
<h1 className="text-xl font-semibold mb-4 text-black">Nuevo curso</h1>
<h1
<h1 className="text-xl font-semibold mb-4 text-black">
Nuevo curso
</h1>
<label
htmlFor="archivo"
onDrop={manejarSoltar}
onDragOver={manejarArrastrar}
className="border-2 border-gray-300 rounded-md p-8 text-gray-600 cursor-pointer w-80 text-center mb-4"
>
Arrastra y suelta un archivo o busca un archivo
{archivo ? (
<span className="text-black font-medium">{archivo.name}</span>
) : (
<span>Arrastra y suelta un archivo o haz clic para seleccionarlo</span>
)}
<input
type="file"
id="archivo"
accept=".csv"
accept=".csv, .xlsx"
onChange={manejarArchivo}
className="hidden"
/>
</h1>
</label>
<Button
onClick={extraerContenido}
className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-md"
onClick={registrarCursos}
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md mt-4"
>
Extraer contenido
Registrar cursos
</Button>
{datosCSV.length > 0 && (
<div className="mt-6 text-left w-full max-w-md">
<h3 className="font-bold mb-2">Contenido extraído:</h3>
<pre className="bg-gray-100 p-2 rounded overflow-x-auto text-sm">
{JSON.stringify(datosCSV, null, 2)}
</pre>
{datos.length > 0 && (
<div className="mt-6 text-left w-full overflow-auto">
<h3 className="font-bold mb-2">Vista previa del archivo:</h3>
<table className="min-w-full bg-white border border-gray-300 text-sm">
<thead className="bg-gray-100 text-gray-700">
<tr>
{Object.keys(datos[0]).map((columna, index) => (
<th key={index} className="border px-4 py-2">{columna}</th>
))}
</tr>
</thead>
<tbody>
{datos.map((fila, index) => (
<tr key={index}>
{Object.values(fila).map((valor, i) => (
<td key={i} className="border px-4 py-1">{valor}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* Dialog Component */}
<Dialog open={dialogoAbierto} onOpenChange={setDialogoAbierto}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">Información</DialogTitle>
<DialogDescription>{mensajeDialogo}</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</Layout>
);
}
}

View File

@ -21,6 +21,10 @@ export default function CursosVista() {
const [mostrarModal, setMostrarModal] = useState(false);
const [modalMensaje, setModalMensaje] = useState("");
// Estado para confirmación de eliminación
const [confirmarEliminar, setConfirmarEliminar] = useState(false);
const [cursoAEliminar, setCursoAEliminar] = useState(null);
useEffect(() => {
cargarCursos();
}, []);
@ -69,8 +73,13 @@ export default function CursosVista() {
setMostrarModal(true);
};
const eliminarCurso = async (id) => {
const { error } = await supabaseClient.from("curso").delete().eq("id", id);
const confirmarEliminacion = (id) => {
setCursoAEliminar(id);
setConfirmarEliminar(true);
};
const eliminarCurso = async () => {
const { error } = await supabaseClient.from("curso").delete().eq("id", cursoAEliminar);
if (error) {
console.error("Error eliminando curso:", error.message);
setModalMensaje("Error al eliminar el curso");
@ -78,6 +87,7 @@ export default function CursosVista() {
setModalMensaje("Curso eliminado exitosamente");
await cargarCursos();
}
setConfirmarEliminar(false);
setMostrarModal(true);
};
@ -149,7 +159,7 @@ export default function CursosVista() {
</Button>
<Button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 rounded"
onClick={() => eliminarCurso(curso.id)}
onClick={() => confirmarEliminacion(curso.id)}
>
Eliminar
</Button>
@ -161,7 +171,33 @@ export default function CursosVista() {
</table>
</div>
{/* Modal */}
{/* Modal de confirmación */}
<Dialog open={confirmarEliminar} onOpenChange={setConfirmarEliminar}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">Confirmar eliminación</DialogTitle>
<DialogDescription>
¿Estás seguro de que deseas eliminar este curso? Esta acción no se puede deshacer.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
className="bg-red-500 hover:bg-red-700 text-white"
onClick={eliminarCurso}
>
Eliminar
</Button>
<Button
className="bg-gray-400 hover:bg-gray-600 text-white"
onClick={() => setConfirmarEliminar(false)}
>
Cancelar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Modal de resultado */}
<Dialog open={mostrarModal} onOpenChange={setMostrarModal}>
<DialogContent>
<DialogHeader>
@ -177,4 +213,4 @@ export default function CursosVista() {
</Dialog>
</Layout>
);
}
}