feat: add manual and file upload pages for inyecciones and pildoras

- Created InyeccionesManual component for manual entry of inyecciones.
- Created InyeccionesVista component for viewing and managing inyecciones.
- Created PildorasArchivo component for uploading pildoras via CSV/XLSX files.
- Created PildorasManual component for manual entry of pildoras.
- Created PildorasVista component for viewing and managing pildoras.
- Created VistaGeneral component to provide an overview of cursos, inyecciones, and pildoras.
- Added validation schemas for inyecciones and pildoras using Zod.
- Updated AlumnosSchema to modify course selection validation.
This commit is contained in:
SirRobert-1 2025-06-03 18:02:10 -06:00
parent 164921592b
commit cddb2291c3
12 changed files with 1542 additions and 1 deletions

View File

@ -36,6 +36,15 @@ const data = {
},
],
},
/*{
title: "Vista general",
items: [
{
title: "Vista general",
url: "/vistaGeneral",
},
],
},*/
{
title: "Cursos",
items: [
@ -53,6 +62,40 @@ const data = {
},
],
},
{
title: "Inyecciones",
items: [
{
title: "Vista de inyecciones",
url: "/inyeccionesVista",
},
{
title: "Agregar manualmente",
url: "/inyeccionesManual",
},
{
title: "Agregar desde archivo",
url: "/inyeccionesArchivo",
},
],
},
{
title: "Pildoras",
items: [
{
title: "Vista de pildoras",
url: "/pildorasVista",
},
{
title: "Agregar manualmente",
url: "/pildorasManual",
},
{
title: "Agregar desde archivo",
url: "/pildorasArchivo",
},
],
},
{
title: "Diplomas",
items: [

View File

@ -0,0 +1,38 @@
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 } = req.body;
if (!nombre || !horas || !descripcion) {
return res.status(400).json({ error: "Faltan datos de la inyección" });
}
// Insertar la inyección
const { error } = await supabase
.from("inyeccion")
.insert([{ nombre, horas, descripcion }]);
if (error) {
return res
.status(500)
.json({
error: "Error al insertar la inyección",
detalles: error.message,
});
}
return res
.status(200)
.json({ mensaje: "Inyección registrada correctamente" });
} catch (err) {
return res
.status(500)
.json({ error: "Error interno del servidor", detalles: err.message });
}
}

View File

@ -0,0 +1,36 @@
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 } = req.body;
if (!nombre || !horas || !descripcion) {
return res.status(400).json({ error: "Faltan datos de la píldora" });
}
// Insertar la píldora
const { error } = await supabase
.from("pildoras")
.insert([{ nombre, horas, descripcion }]);
if (error) {
return res.status(500).json({
error: "Error al insertar la píldora",
detalles: error.message,
});
}
return res
.status(200)
.json({ mensaje: "Píldora registrada correctamente" });
} catch (err) {
return res
.status(500)
.json({ error: "Error interno del servidor", detalles: err.message });
}
}

View File

@ -0,0 +1,263 @@
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,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { useRouter } from "next/router";
export default function InyeccionesArchivo() {
const [archivo, setArchivo] = useState(null);
const [datos, setDatos] = useState([]);
const [dialogoAbierto, setDialogoAbierto] = useState(false);
const [mensajeDialogo, setMensajeDialogo] = useState("");
const [dialogoCargando, setDialogoCargando] = useState(false);
const [dialogoAdvertencia, setDialogoAdvertencia] = useState(false);
const [rutaPendiente, setRutaPendiente] = useState(null);
const router = useRouter();
useEffect(() => {
if (archivo) extraerContenido();
// eslint-disable-next-line
}, [archivo]);
useEffect(() => {
const handleRouteChange = (url) => {
if (archivo && datos.length > 0) {
setDialogoAdvertencia(true);
setRutaPendiente(url);
throw "Bloqueo de navegación por archivo pendiente";
}
};
router.events.on("routeChangeStart", handleRouteChange);
return () => {
router.events.off("routeChangeStart", handleRouteChange);
};
}, [archivo, datos, router]);
const registrarInyecciones = async () => {
if (datos.length === 0) return;
setDialogoCargando(true);
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,
}),
});
const resultado = await res.json();
if (!res.ok) {
errores.push({
inyeccion,
error: resultado.error || "Error desconocido",
});
}
}
setDialogoCargando(false);
if (errores.length > 0) {
setMensajeDialogo(
`Se registraron algunos errores:\n${JSON.stringify(errores, null, 2)}`
);
} else {
setMensajeDialogo(
"Todas las inyecciones fueron registradas correctamente."
);
setArchivo(null);
setDatos([]);
}
setDialogoAbierto(true);
};
const manejarArchivo = (e) => {
const file = e.target.files[0];
if (validarArchivo(file)) setArchivo(file);
};
const manejarSoltar = (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
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;
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-full pt-10 flex flex-col items-start md:items-center md:justify-center text-black">
<div className="bg-white font-sans text-center w-full flex flex-col items-center">
<h1 className="text-xl font-semibold mb-4 text-black">
Nueva inyección (archivo)
</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"
>
{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, .xlsx"
onChange={manejarArchivo}
className="hidden"
/>
</label>
<Button
onClick={registrarInyecciones}
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md mt-4"
>
Registrar inyecciones
</Button>
{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>
<Dialog open={dialogoCargando}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">Cargando...</DialogTitle>
<DialogDescription>
Por favor espera, se están registrando las inyecciones.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
<Dialog open={dialogoAdvertencia} onOpenChange={setDialogoAdvertencia}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">Advertencia</DialogTitle>
<DialogDescription>
Si cambias de ventana perderás la subida del archivo. ¿Deseas
continuar?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
className="bg-red-500 hover:bg-red-700 text-white"
onClick={() => {
setDialogoAdvertencia(false);
setArchivo(null);
setDatos([]);
if (rutaPendiente) router.push(rutaPendiente);
}}
>
, continuar
</Button>
<Button
className="bg-gray-400 hover:bg-gray-600 text-white"
onClick={() => setDialogoAdvertencia(false)}
>
Cancelar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Layout>
);
}

View File

@ -0,0 +1,118 @@
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Schema } from "@/schemas/Schema";
import Layout from "@/components/layout/Layout";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { supabaseClient } from "@/utils/supabase";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
export default function InyeccionesManual() {
const [showDialog, setShowDialog] = useState(false);
const [dialogMsg, setDialogMsg] = useState("");
const [loading, setLoading] = useState(false);
const form = useForm({
resolver: zodResolver(Schema),
defaultValues: {
nombre: "",
descripcion: "",
horas: 0,
},
});
const {
register,
handleSubmit,
formState: { errors },
reset,
} = form;
const onSubmit = async (data) => {
setLoading(true);
try {
const { nombre, descripcion, horas } = data;
const { error } = await supabaseClient
.from("inyeccion")
.insert([{ nombre, descripcion, horas: parseInt(horas, 10) }]);
if (error) throw error;
setDialogMsg("Inyección guardada exitosamente");
reset();
} catch (err) {
setDialogMsg("Error: " + (err.message || err));
} finally {
setShowDialog(true);
setLoading(false);
}
};
return (
<Layout>
<div className="w-full bg-white pt-5 font-sans text-center md:w-[80%] flex flex-col items-center justify-start text-black">
<h1 className="text-xl font-semibold mb-10 text-black">
Nueva inyección
</h1>
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
<Input
type="text"
placeholder="Nombre de la inyección"
{...register("nombre")}
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 text-black"
/>
{errors.nombre && (
<p className="text-red-500 text-sm">{errors.nombre.message}</p>
)}
<Textarea
placeholder="Descripción"
{...register("descripcion")}
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 h-24 text-black"
/>
{errors.descripcion && (
<p className="text-red-500 text-sm">{errors.descripcion.message}</p>
)}
<Input
type="number"
placeholder="Horas"
{...register("horas", { valueAsNumber: true })}
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 text-black"
/>
{errors.horas && (
<p className="text-red-500 text-sm">{errors.horas.message}</p>
)}
<div className="flex justify-center w-full mt-5">
<Button
type="submit"
className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-md"
disabled={loading}
>
{loading ? "Guardando..." : "Guardar inyección"}
</Button>
</div>
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">Resultado</DialogTitle>
<DialogDescription>{dialogMsg}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={() => setShowDialog(false)}>Cerrar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</form>
</div>
</Layout>
);
}

View File

@ -0,0 +1,229 @@
import React, { useEffect, useState } from "react";
import Layout from "@/components/layout/Layout";
import { supabaseClient } from "@/utils/supabase";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export default function InyeccionesVista() {
const [inyecciones, setInyecciones] = useState([]);
const [inyeccionEditando, setInyeccionEditando] = useState(null);
const [nuevoNombre, setNuevoNombre] = useState("");
const [nuevaDescripcion, setNuevaDescripcion] = useState("");
const [nuevaHoras, setNuevaHoras] = useState("");
const [mostrarModal, setMostrarModal] = useState(false);
const [modalMensaje, setModalMensaje] = useState("");
const [confirmarEliminar, setConfirmarEliminar] = useState(false);
const [inyeccionAEliminar, setInyeccionAEliminar] = useState(null);
useEffect(() => {
cargarInyecciones();
}, []);
const cargarInyecciones = async () => {
const { data, error } = await supabaseClient
.from("inyeccion")
.select("*")
.order("id", { ascending: true });
if (error) {
setModalMensaje("Error al cargar inyecciones: " + error.message);
setMostrarModal(true);
} else {
setInyecciones(data);
}
};
const iniciarEdicion = (inyeccion) => {
setInyeccionEditando(inyeccion.id);
setNuevoNombre(inyeccion.nombre);
setNuevaDescripcion(inyeccion.descripcion);
setNuevaHoras(inyeccion.horas);
};
const cancelarEdicion = () => {
setInyeccionEditando(null);
setNuevoNombre("");
setNuevaDescripcion("");
setNuevaHoras("");
};
const guardarEdicion = async (id) => {
const { error } = await supabaseClient
.from("inyeccion")
.update({
nombre: nuevoNombre,
descripcion: nuevaDescripcion,
horas: nuevaHoras,
})
.eq("id", id);
if (error) {
setModalMensaje("Error al actualizar la inyección");
} else {
setModalMensaje("Inyección actualizada exitosamente");
await cargarInyecciones();
cancelarEdicion();
}
setMostrarModal(true);
};
const confirmarEliminacion = (id) => {
setInyeccionAEliminar(id);
setConfirmarEliminar(true);
};
const eliminarInyeccion = async () => {
const { error } = await supabaseClient
.from("inyeccion")
.delete()
.eq("id", inyeccionAEliminar);
if (error) {
setModalMensaje("Error al eliminar la inyección");
} else {
setModalMensaje("Inyección eliminada exitosamente");
await cargarInyecciones();
}
setConfirmarEliminar(false);
setMostrarModal(true);
};
return (
<Layout>
<div className="w-full pt-10 flex flex-col items-center text-black">
<h1 className="text-2xl font-semibold mb-6">Lista de Inyecciones</h1>
<div className="overflow-x-auto w-full">
<table className="min-w-full bg-white border">
<thead>
<tr className="bg-gray-100">
<th className="py-2 border-b">ID</th>
<th className="py-2 border-b">Nombre</th>
<th className="py-2 border-b">Descripción</th>
<th className="py-2 border-b">Horas</th>
<th className="py-2 border-b">Acciones</th>
</tr>
</thead>
<tbody>
{inyecciones.map((inyeccion) =>
inyeccionEditando === inyeccion.id ? (
<tr key={inyeccion.id}>
<td className="py-2 px-4 border-b text-center">
{inyeccion.id}
</td>
<td className="py-2 px-4 border-b">
<Input
value={nuevoNombre}
onChange={(e) => setNuevoNombre(e.target.value)}
/>
</td>
<td className="py-2 px-4 border-b">
<Input
value={nuevaDescripcion}
onChange={(e) => setNuevaDescripcion(e.target.value)}
/>
</td>
<td className="py-2 px-4 border-b">
<Input
type="number"
value={nuevaHoras}
onChange={(e) => setNuevaHoras(e.target.value)}
/>
</td>
<td className="py-2 px-4 border-b flex justify-center">
<Button
className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-3 m-2 rounded"
onClick={() => guardarEdicion(inyeccion.id)}
>
Guardar
</Button>
<Button
className="bg-gray-400 hover:bg-gray-600 text-white font-bold py-1 px-3 m-2 rounded"
onClick={cancelarEdicion}
>
Cancelar
</Button>
</td>
</tr>
) : (
<tr key={inyeccion.id}>
<td className="py-2 px-4 border-b">{inyeccion.id}</td>
<td className="py-2 px-4 border-b">{inyeccion.nombre}</td>
<td className="py-2 px-4 border-b">
{inyeccion.descripcion}
</td>
<td className="py-2 px-4 border-b">{inyeccion.horas}</td>
<td className="py-2 px-4 border-b flex justify-center">
<Button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 m-1 rounded"
onClick={() => iniciarEdicion(inyeccion)}
>
Editar
</Button>
<Button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 m-1 rounded"
onClick={() => confirmarEliminacion(inyeccion.id)}
>
Eliminar
</Button>
</td>
</tr>
)
)}
</tbody>
</table>
</div>
</div>
{/* Dialog para eliminar inyección */}
<Dialog open={confirmarEliminar} onOpenChange={setConfirmarEliminar}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">
Confirmar eliminación
</DialogTitle>
<DialogDescription>
¿Estás seguro de que deseas eliminar esta inyección? Esta acción
no se puede deshacer.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
className="bg-red-500 hover:bg-red-700 text-white"
onClick={eliminarInyeccion}
>
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>
<DialogTitle className="text-black">
Resultado de la operación
</DialogTitle>
<DialogDescription>{modalMensaje}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={() => setMostrarModal(false)}>Cerrar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Layout>
);
}

View File

@ -0,0 +1,261 @@
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,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { useRouter } from "next/router";
export default function PildorasArchivo() {
const [archivo, setArchivo] = useState(null);
const [datos, setDatos] = useState([]);
const [dialogoAbierto, setDialogoAbierto] = useState(false);
const [mensajeDialogo, setMensajeDialogo] = useState("");
const [dialogoCargando, setDialogoCargando] = useState(false);
const [dialogoAdvertencia, setDialogoAdvertencia] = useState(false);
const [rutaPendiente, setRutaPendiente] = useState(null);
const router = useRouter();
useEffect(() => {
if (archivo) extraerContenido();
// eslint-disable-next-line
}, [archivo]);
useEffect(() => {
const handleRouteChange = (url) => {
if (archivo && datos.length > 0) {
setDialogoAdvertencia(true);
setRutaPendiente(url);
throw "Bloqueo de navegación por archivo pendiente";
}
};
router.events.on("routeChangeStart", handleRouteChange);
return () => {
router.events.off("routeChangeStart", handleRouteChange);
};
}, [archivo, datos, router]);
const registrarPildoras = async () => {
if (datos.length === 0) return;
setDialogoCargando(true);
const errores = [];
for (const pildora of datos) {
const res = await fetch("/api/pildora", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
nombre: pildora.nombre,
horas: pildora.horas,
descripcion: pildora.descripcion,
}),
});
const resultado = await res.json();
if (!res.ok) {
errores.push({
pildora,
error: resultado.error || "Error desconocido",
});
}
}
setDialogoCargando(false);
if (errores.length > 0) {
setMensajeDialogo(
`Se registraron algunos errores:\n${JSON.stringify(errores, null, 2)}`
);
} else {
setMensajeDialogo("Todas las píldoras fueron registradas correctamente.");
setArchivo(null);
setDatos([]);
}
setDialogoAbierto(true);
};
const manejarArchivo = (e) => {
const file = e.target.files[0];
if (validarArchivo(file)) setArchivo(file);
};
const manejarSoltar = (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
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;
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-full pt-10 flex flex-col items-start md:items-center md:justify-center text-black">
<div className="bg-white font-sans text-center w-full flex flex-col items-center">
<h1 className="text-xl font-semibold mb-4 text-black">
Nueva píldora (archivo)
</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"
>
{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, .xlsx"
onChange={manejarArchivo}
className="hidden"
/>
</label>
<Button
onClick={registrarPildoras}
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md mt-4"
>
Registrar píldoras
</Button>
{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>
<Dialog open={dialogoCargando}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">Cargando...</DialogTitle>
<DialogDescription>
Por favor espera, se están registrando las píldoras.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
<Dialog open={dialogoAdvertencia} onOpenChange={setDialogoAdvertencia}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">Advertencia</DialogTitle>
<DialogDescription>
Si cambias de ventana perderás la subida del archivo. ¿Deseas
continuar?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
className="bg-red-500 hover:bg-red-700 text-white"
onClick={() => {
setDialogoAdvertencia(false);
setArchivo(null);
setDatos([]);
if (rutaPendiente) router.push(rutaPendiente);
}}
>
, continuar
</Button>
<Button
className="bg-gray-400 hover:bg-gray-600 text-white"
onClick={() => setDialogoAdvertencia(false)}
>
Cancelar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Layout>
);
}

View File

@ -0,0 +1,127 @@
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import Layout from "@/components/layout/Layout";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { supabaseClient } from "@/utils/supabase";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
// Puedes mover este schema a un archivo separado si lo deseas
const pildoraSchema = z.object({
nombre: z.string().nonempty("Escribe el nombre"),
descripcion: z.string().nonempty("Escribe la descripción"),
horas: z
.number({ invalid_type_error: "Las horas deben ser un número" })
.min(1, "Las horas deben ser mayor a 0"),
});
export default function PildorasManual() {
const [showDialog, setShowDialog] = useState(false);
const [dialogMsg, setDialogMsg] = useState("");
const [loading, setLoading] = useState(false);
const form = useForm({
resolver: zodResolver(pildoraSchema),
defaultValues: {
nombre: "",
descripcion: "",
horas: 0,
},
});
const {
register,
handleSubmit,
formState: { errors },
reset,
} = form;
const onSubmit = async (data) => {
setLoading(true);
try {
const { nombre, descripcion, horas } = data;
const { error } = await supabaseClient
.from("pildoras")
.insert([{ nombre, descripcion, horas: parseInt(horas, 10) }]);
if (error) throw error;
setDialogMsg("Píldora guardada exitosamente");
reset();
} catch (err) {
setDialogMsg("Error: " + (err.message || err));
} finally {
setShowDialog(true);
setLoading(false);
}
};
return (
<Layout>
<div className="w-full bg-white pt-5 font-sans text-center md:w-[80%] flex flex-col items-center justify-start text-black">
<h1 className="text-xl font-semibold mb-10 text-black">
Nueva píldora
</h1>
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
<Input
type="text"
placeholder="Nombre de la píldora"
{...register("nombre")}
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 text-black"
/>
{errors.nombre && (
<p className="text-red-500 text-sm">{errors.nombre.message}</p>
)}
<Textarea
placeholder="Descripción"
{...register("descripcion")}
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 h-24 text-black"
/>
{errors.descripcion && (
<p className="text-red-500 text-sm">{errors.descripcion.message}</p>
)}
<Input
type="number"
placeholder="Horas"
{...register("horas", { valueAsNumber: true })}
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 text-black"
/>
{errors.horas && (
<p className="text-red-500 text-sm">{errors.horas.message}</p>
)}
<div className="flex justify-center w-full mt-5">
<Button
type="submit"
className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-md"
disabled={loading}
>
{loading ? "Guardando..." : "Guardar píldora"}
</Button>
</div>
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">Resultado</DialogTitle>
<DialogDescription>{dialogMsg}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={() => setShowDialog(false)}>Cerrar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</form>
</div>
</Layout>
);
}

View File

@ -0,0 +1,229 @@
import React, { useEffect, useState } from "react";
import Layout from "@/components/layout/Layout";
import { supabaseClient } from "@/utils/supabase";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export default function PildorasVista() {
const [pildoras, setPildoras] = useState([]);
const [pildoraEditando, setPildoraEditando] = useState(null);
const [nuevoNombre, setNuevoNombre] = useState("");
const [nuevaDescripcion, setNuevaDescripcion] = useState("");
const [nuevaHoras, setNuevaHoras] = useState("");
const [mostrarModal, setMostrarModal] = useState(false);
const [modalMensaje, setModalMensaje] = useState("");
const [confirmarEliminar, setConfirmarEliminar] = useState(false);
const [pildoraAEliminar, setPildoraAEliminar] = useState(null);
useEffect(() => {
cargarPildoras();
}, []);
const cargarPildoras = async () => {
const { data, error } = await supabaseClient
.from("pildoras")
.select("*")
.order("id", { ascending: true });
if (error) {
setModalMensaje("Error al cargar píldoras: " + error.message);
setMostrarModal(true);
} else {
setPildoras(data);
}
};
const iniciarEdicion = (pildora) => {
setPildoraEditando(pildora.id);
setNuevoNombre(pildora.nombre);
setNuevaDescripcion(pildora.descripcion);
setNuevaHoras(pildora.horas);
};
const cancelarEdicion = () => {
setPildoraEditando(null);
setNuevoNombre("");
setNuevaDescripcion("");
setNuevaHoras("");
};
const guardarEdicion = async (id) => {
const { error } = await supabaseClient
.from("pildoras")
.update({
nombre: nuevoNombre,
descripcion: nuevaDescripcion,
horas: nuevaHoras,
})
.eq("id", id);
if (error) {
setModalMensaje("Error al actualizar la píldora");
} else {
setModalMensaje("Píldora actualizada exitosamente");
await cargarPildoras();
cancelarEdicion();
}
setMostrarModal(true);
};
const confirmarEliminacion = (id) => {
setPildoraAEliminar(id);
setConfirmarEliminar(true);
};
const eliminarPildora = async () => {
const { error } = await supabaseClient
.from("pildoras")
.delete()
.eq("id", pildoraAEliminar);
if (error) {
setModalMensaje("Error al eliminar la píldora");
} else {
setModalMensaje("Píldora eliminada exitosamente");
await cargarPildoras();
}
setConfirmarEliminar(false);
setMostrarModal(true);
};
return (
<Layout>
<div className="w-full pt-10 flex flex-col items-center text-black">
<h1 className="text-2xl font-semibold mb-6">Lista de Píldoras</h1>
<div className="overflow-x-auto w-full">
<table className="min-w-full bg-white border">
<thead>
<tr className="bg-gray-100">
<th className="py-2 border-b">ID</th>
<th className="py-2 border-b">Nombre</th>
<th className="py-2 border-b">Descripción</th>
<th className="py-2 border-b">Horas</th>
<th className="py-2 border-b">Acciones</th>
</tr>
</thead>
<tbody>
{pildoras.map((pildora) =>
pildoraEditando === pildora.id ? (
<tr key={pildora.id}>
<td className="py-2 px-4 border-b text-center">
{pildora.id}
</td>
<td className="py-2 px-4 border-b">
<Input
value={nuevoNombre}
onChange={(e) => setNuevoNombre(e.target.value)}
/>
</td>
<td className="py-2 px-4 border-b">
<Input
value={nuevaDescripcion}
onChange={(e) => setNuevaDescripcion(e.target.value)}
/>
</td>
<td className="py-2 px-4 border-b">
<Input
type="number"
value={nuevaHoras}
onChange={(e) => setNuevaHoras(e.target.value)}
/>
</td>
<td className="py-2 px-4 border-b flex justify-center">
<Button
className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-3 m-2 rounded"
onClick={() => guardarEdicion(pildora.id)}
>
Guardar
</Button>
<Button
className="bg-gray-400 hover:bg-gray-600 text-white font-bold py-1 px-3 m-2 rounded"
onClick={cancelarEdicion}
>
Cancelar
</Button>
</td>
</tr>
) : (
<tr key={pildora.id}>
<td className="py-2 px-4 border-b">{pildora.id}</td>
<td className="py-2 px-4 border-b">{pildora.nombre}</td>
<td className="py-2 px-4 border-b">
{pildora.descripcion}
</td>
<td className="py-2 px-4 border-b">{pildora.horas}</td>
<td className="py-2 px-4 border-b flex justify-center">
<Button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 m-1 rounded"
onClick={() => iniciarEdicion(pildora)}
>
Editar
</Button>
<Button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 m-1 rounded"
onClick={() => confirmarEliminacion(pildora.id)}
>
Eliminar
</Button>
</td>
</tr>
)
)}
</tbody>
</table>
</div>
</div>
{/* Dialog para eliminar píldora */}
<Dialog open={confirmarEliminar} onOpenChange={setConfirmarEliminar}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">
Confirmar eliminación
</DialogTitle>
<DialogDescription>
¿Estás seguro de que deseas eliminar esta píldora? Esta acción no
se puede deshacer.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
className="bg-red-500 hover:bg-red-700 text-white"
onClick={eliminarPildora}
>
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>
<DialogTitle className="text-black">
Resultado de la operación
</DialogTitle>
<DialogDescription>{modalMensaje}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={() => setMostrarModal(false)}>Cerrar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Layout>
);
}

View File

@ -0,0 +1,186 @@
import React, { useEffect, useState } from "react";
import Layout from "@/components/layout/Layout";
import { supabaseClient } from "@/utils/supabase";
export default function VistaGeneral() {
const [cursos, setCursos] = useState([]);
const [inyecciones, setInyecciones] = useState([]);
const [pildoras, setPildoras] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
cargarTodo();
}, []);
const cargarTodo = async () => {
setLoading(true);
const [
{ data: cursosData },
{ data: inyeccionesData },
{ data: pildorasData },
] = await Promise.all([
supabaseClient
.from("curso")
.select(
`
id,
nombre,
descripcion,
horas,
curso_competencia (
competencia (
id,
descripcion
)
)
`
)
.order("id", { ascending: true }),
supabaseClient
.from("inyeccion")
.select("*")
.order("id", { ascending: true }),
supabaseClient
.from("pildoras")
.select("*")
.order("id", { ascending: true }),
]);
const cursosConCompetencias = (cursosData || []).map((curso) => ({
...curso,
competencias: Array.isArray(curso.curso_competencia)
? curso.curso_competencia.map((cc) => cc.competencia)
: [],
}));
setCursos(cursosConCompetencias);
setInyecciones(inyeccionesData || []);
setPildoras(pildorasData || []);
setLoading(false);
};
return (
<Layout>
<div className="w-full pt-10 flex flex-col items-center text-black">
<h1 className="text-2xl font-semibold mb-6">Vista General</h1>
{loading ? (
<div className="text-center text-lg">Cargando...</div>
) : (
<div className="w-full flex flex-col justify-center">
{/* Cursos */}
<div className="flex-1">
<h2 className="text-xl font-bold mb-2">Cursos</h2>
<table className="min-w-full bg-white border mb-8">
<thead>
<tr className="bg-gray-100">
<th className="py-2 border-b">ID</th>
<th className="py-2 border-b">Nombre</th>
<th className="py-2 border-b">Descripción</th>
<th className="py-2 border-b">Horas</th>
<th className="py-2 border-b">Competencias</th>
</tr>
</thead>
<tbody>
{cursos.length === 0 ? (
<tr>
<td colSpan={4} className="text-center py-4">
Sin cursos registrados
</td>
</tr>
) : (
cursos.map((curso) => (
<tr key={curso.id}>
<td className="py-2 px-4 border-b">{curso.id}</td>
<td className="py-2 px-4 border-b">{curso.nombre}</td>
<td className="py-2 px-4 border-b">
{curso.descripcion}
</td>
<td className="py-2 px-4 border-b">{curso.horas}</td>
<td className="py-2 px-4 border-b">
{curso.competencias &&
curso.competencias.length > 0 ? (
<ul className="list-disc pl-4">
{curso.competencias.map((comp) => (
<li key={comp.id}>{comp.descripcion}</li>
))}
</ul>
) : (
"Sin competencias"
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Inyecciones */}
<div className="flex-1">
<h2 className="text-xl font-bold mb-2">Inyecciones</h2>
<table className="min-w-full bg-white border mb-8">
<thead>
<tr className="bg-gray-100">
<th className="py-2 border-b">ID</th>
<th className="py-2 border-b">Nombre</th>
<th className="py-2 border-b">Descripción</th>
<th className="py-2 border-b">Horas</th>
</tr>
</thead>
<tbody>
{inyecciones.length === 0 ? (
<tr>
<td colSpan={4} className="text-center py-4">
Sin inyecciones registradas
</td>
</tr>
) : (
inyecciones.map((iny) => (
<tr key={iny.id}>
<td className="py-2 px-4 border-b">{iny.id}</td>
<td className="py-2 px-4 border-b">{iny.nombre}</td>
<td className="py-2 px-4 border-b">
{iny.descripcion}
</td>
<td className="py-2 px-4 border-b">{iny.horas}</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Píldoras */}
<div className="flex-1">
<h2 className="text-xl font-bold mb-2">Píldoras</h2>
<table className="min-w-full bg-white border mb-8">
<thead>
<tr className="bg-gray-100">
<th className="py-2 border-b">ID</th>
<th className="py-2 border-b">Nombre</th>
<th className="py-2 border-b">Descripción</th>
<th className="py-2 border-b">Horas</th>
</tr>
</thead>
<tbody>
{pildoras.length === 0 ? (
<tr>
<td colSpan={4} className="text-center py-4">
Sin píldoras registradas
</td>
</tr>
) : (
pildoras.map((p) => (
<tr key={p.id}>
<td className="py-2 px-4 border-b">{p.id}</td>
<td className="py-2 px-4 border-b">{p.nombre}</td>
<td className="py-2 px-4 border-b">{p.descripcion}</td>
<td className="py-2 px-4 border-b">{p.horas}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
</div>
</Layout>
);
}

View File

@ -12,5 +12,6 @@ export const alumnoSchema = z.object({
.regex(/^\d+$/, "El número de teléfono solo puede contener dígitos")
.min(10, "El número de teléfono debe tener al menos 10 dígitos")
.max(10, "El número de teléfono no puede tener más de 10 dígitos"),
cursoSeleccionado: z.string().nonempty("Selecciona un curso"),
//tipo: z.string().nonempty("Selecciona un tipo de asignación"),
cursoSeleccionado: z.string().nonempty("Selecciona una opción"),
});

View File

@ -0,0 +1,10 @@
import { z } from "zod";
export const Schema = z.object({
nombre: z
.string()
.nonempty("Escribe el nombre del curso")
.regex(/^[\p{L}\s]+$/u, "Solo se permiten letras en el nombre"),
descripcion: z.string().nonempty("Escribe una descripción"),
horas: z.number().positive("Las horas deben ser un número positivo").int(),
});