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:
parent
164921592b
commit
cddb2291c3
|
@ -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: [
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}}
|
||||
>
|
||||
Sí, continuar
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-gray-400 hover:bg-gray-600 text-white"
|
||||
onClick={() => setDialogoAdvertencia(false)}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Layout>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}}
|
||||
>
|
||||
Sí, continuar
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-gray-400 hover:bg-gray-600 text-white"
|
||||
onClick={() => setDialogoAdvertencia(false)}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Layout>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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"),
|
||||
});
|
||||
|
|
|
@ -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(),
|
||||
});
|
Loading…
Reference in New Issue