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",
|
title: "Cursos",
|
||||||
items: [
|
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",
|
title: "Diplomas",
|
||||||
items: [
|
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")
|
.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")
|
.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"),
|
.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