diff --git a/diplomas/package-lock.json b/diplomas/package-lock.json index ca5ad0d..5709836 100644 --- a/diplomas/package-lock.json +++ b/diplomas/package-lock.json @@ -8,6 +8,7 @@ "name": "diplomas", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^5.0.1", "@radix-ui/react-dialog": "^1.1.11", "@radix-ui/react-label": "^2.1.4", "@radix-ui/react-select": "^2.1.7", @@ -25,8 +26,10 @@ "papaparse": "^5.5.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.56.2", "tailwind-merge": "^3.2.0", - "tw-animate-css": "^1.2.5" + "tw-animate-css": "^1.2.5", + "zod": "^3.24.3" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -252,6 +255,17 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" }, + "node_modules/@hookform/resolvers": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.0.1.tgz", + "integrity": "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1961,6 +1975,11 @@ "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", "dev": true }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" + }, "node_modules/@supabase/auth-js": { "version": "2.69.1", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz", @@ -5769,6 +5788,21 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.56.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.2.tgz", + "integrity": "sha512-vpfuHuQMF/L6GpuQ4c3ZDo+pRYxIi40gQqsCmmfUBwm+oqvBhKhwghCuj2o00YCgSfU6bR9KC/xnQGWm3Gr08A==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6881,6 +6915,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/diplomas/package.json b/diplomas/package.json index 7390529..1ae39da 100644 --- a/diplomas/package.json +++ b/diplomas/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^5.0.1", "@radix-ui/react-dialog": "^1.1.11", "@radix-ui/react-label": "^2.1.4", "@radix-ui/react-select": "^2.1.7", @@ -26,8 +27,10 @@ "papaparse": "^5.5.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.56.2", "tailwind-merge": "^3.2.0", - "tw-animate-css": "^1.2.5" + "tw-animate-css": "^1.2.5", + "zod": "^3.24.3" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/diplomas/src/components/app-sidebar.jsx b/diplomas/src/components/app-sidebar.jsx index a1aa06f..eef192b 100644 --- a/diplomas/src/components/app-sidebar.jsx +++ b/diplomas/src/components/app-sidebar.jsx @@ -81,7 +81,7 @@ export function AppSidebar({ ...props }) { {data.navMain.map((item) => ( <SidebarMenuItem key={item.title}> <SidebarMenuButton asChild> - <a href={item.url} className="font-medium"> + <a href={item.url} className="text-xl font-medium"> {item.title} </a> </SidebarMenuButton> diff --git a/diplomas/src/pages/alumnosManual.jsx b/diplomas/src/pages/alumnosManual.jsx index d99a093..3a3e459 100644 --- a/diplomas/src/pages/alumnosManual.jsx +++ b/diplomas/src/pages/alumnosManual.jsx @@ -9,7 +9,10 @@ import { SelectContent, SelectItem, } from "@/components/ui/select"; -import { supabaseClient } from "@/util/supabase"; +import { supabaseClient } from "@/utils/supabase"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { alumnoSchema } from "@/schemas/AlumnosSchema"; export default function AlumnosManual() { const [nombre, setNombre] = useState(""); @@ -96,7 +99,7 @@ export default function AlumnosManual() { <Button onClick={manejarGuardar} - className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-md text-black" + className="bg-green-400 hover:bg-green-500 font-bold py-2 px-4 rounded-md text-black" > Guardar </Button> diff --git a/diplomas/src/pages/alumnosVista.jsx b/diplomas/src/pages/alumnosVista.jsx index 76de15a..125aee4 100644 --- a/diplomas/src/pages/alumnosVista.jsx +++ b/diplomas/src/pages/alumnosVista.jsx @@ -1,9 +1,25 @@ import React, { useEffect, useState } from "react"; import Layout from "@/components/layout/Layout"; import { supabaseClient } from "@/utils/supabase"; +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; export default function AlumnosVista() { const [alumnos, setAlumnos] = useState([]); + const [alumnoEditando, setAlumnoEditando] = useState(null); + const [nuevoNombre, setNuevoNombre] = useState(""); + const [nuevoCorreo, setNuevoCorreo] = useState(""); + const [mostrarModal, setMostrarModal] = useState(false); + const [modalMensaje, setModalMensaje] = useState(""); useEffect(() => { const cargarAlumnos = async () => { @@ -17,6 +33,43 @@ export default function AlumnosVista() { cargarAlumnos(); }, []); + // Iniciar edición + const iniciarEdicion = (alumno) => { + setAlumnoEditando(alumno.id); + setNuevoNombre(alumno.nombre); + setNuevoCorreo(alumno.correo); + }; + + // Cancelar edición + const cancelarEdicion = () => { + setAlumnoEditando(null); + setNuevoNombre(""); + setNuevoCorreo(""); + }; + + // Guardar cambios + const guardarEdicion = async (id) => { + const { error } = await supabaseClient + .from("alumno") + .update({ + nombre: nuevoNombre, + correo: nuevoCorreo, + }) + .eq("id", id); + + if (error) { + console.error("Error actualizando alumno:", error.message); + setModalMensaje("Error al actualizar el alumno"); + } else { + setModalMensaje("Alumno actualizado exitosamente"); + // Recargar alumnos + const { data } = await supabaseClient.from("alumno").select("*"); + setAlumnos(data); + cancelarEdicion(); + } + setMostrarModal(true); + }; + return ( <Layout> <div className="w-[80vw] pt-10 flex flex-col items-center text-black"> @@ -26,22 +79,84 @@ export default function AlumnosVista() { <table className="min-w-full bg-white border"> <thead> <tr> - <th className="py-2 px-4 border-b">ID</th> - <th className="py-2 px-4 border-b">Nombre</th> - <th className="py-2 px-4 border-b">Correo</th> + <th className="py-2 border-b">ID</th> + <th className="py-2 border-b">Nombre</th> + <th className="py-2 border-b">Correo</th> + <th className="py-2 border-b">Acciones</th> </tr> </thead> <tbody> - {alumnos.map((alumno) => ( - <tr key={alumno.id}> - <td className="py-2 px-4 border-b text-center">{alumno.id}</td> - <td className="py-2 px-4 border-b">{alumno.nombre}</td> - <td className="py-2 px-4 border-b">{alumno.correo}</td> - </tr> - ))} + {alumnos.map((alumno) => + alumnoEditando === alumno.id ? ( + <tr key={alumno.id}> + <td className="py-2 px-4 border-b text-center"> + {alumno.id} + </td> + <td className="py-2 px-4 border-b"> + <Input + type="text" + value={nuevoNombre} + onChange={(e) => setNuevoNombre(e.target.value)} + className="border rounded px-2 py-1 w-full" + /> + </td> + <td className="py-2 px-4 border-b"> + <Input + type="email" + value={nuevoCorreo} + onChange={(e) => setNuevoCorreo(e.target.value)} + className="border rounded px-2 py-1 w-full" + /> + </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(alumno.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={alumno.id}> + <td className="py-2 px-4 border-b">{alumno.id}</td> + <td className="py-2 px-4 border-b">{alumno.nombre}</td> + <td className="py-2 px-4 border-b">{alumno.correo}</td> + <td className="py-2 px-4 border-b"> + <Button + className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 rounded" + onClick={() => iniciarEdicion(alumno)} + > + Editar + </Button> + </td> + </tr> + ) + )} </tbody> </table> </div> + + {/* Modal */} + <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> ); } diff --git a/diplomas/src/pages/cursosVista.jsx b/diplomas/src/pages/cursosVista.jsx index b98e955..ac755dc 100644 --- a/diplomas/src/pages/cursosVista.jsx +++ b/diplomas/src/pages/cursosVista.jsx @@ -1,6 +1,17 @@ import React, { useEffect, useState } from "react"; import Layout from "@/components/layout/Layout"; import { supabaseClient } from "@/utils/supabase"; +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; export default function CursosVista() { const [cursos, setCursos] = useState([]); @@ -8,6 +19,8 @@ export default function CursosVista() { const [nuevoNombre, setNuevoNombre] = useState(""); const [nuevaDescripcion, setNuevaDescripcion] = useState(""); const [nuevaHoras, setNuevaHoras] = useState(""); + const [mostrarModal, setMostrarModal] = useState(false); + const [modalMensaje, setModalMensaje] = useState(""); // Cargar cursos al iniciar useEffect(() => { @@ -40,7 +53,7 @@ export default function CursosVista() { // Guardar cambios const guardarEdicion = async (id) => { - const { data, error } = await supabaseClient + const { error } = await supabaseClient .from("curso") .update({ nombre: nuevoNombre, @@ -51,14 +64,15 @@ export default function CursosVista() { if (error) { console.error("Error actualizando curso:", error.message); - alert("Error al actualizar el curso"); + setModalMensaje("Error al actualizar el curso"); } else { - alert("Curso actualizado exitosamente"); + setModalMensaje("Curso actualizado exitosamente"); // Recargar cursos const { data } = await supabaseClient.from("curso").select("*"); setCursos(data); cancelarEdicion(); } + setMostrarModal(true); }; return ( @@ -86,7 +100,7 @@ export default function CursosVista() { {curso.id} </td> <td className="py-2 px-4 border-b"> - <input + <Input type="text" value={nuevoNombre} onChange={(e) => setNuevoNombre(e.target.value)} @@ -94,7 +108,7 @@ export default function CursosVista() { /> </td> <td className="py-2 px-4 border-b"> - <input + <Input type="text" value={nuevaDescripcion} onChange={(e) => setNuevaDescripcion(e.target.value)} @@ -110,18 +124,18 @@ export default function CursosVista() { /> </td> <td className="py-2 px-4 border-b flex gap-2 justify-center"> - <button + <Button className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-3 rounded" onClick={() => guardarEdicion(curso.id)} > Guardar - </button> - <button + </Button> + <Button className="bg-gray-400 hover:bg-gray-600 text-white font-bold py-1 px-3 rounded" onClick={cancelarEdicion} > Cancelar - </button> + </Button> </td> </tr> ) : ( @@ -133,12 +147,12 @@ export default function CursosVista() { <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 text-center"> - <button + <Button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 rounded" onClick={() => iniciarEdicion(curso)} > Editar - </button> + </Button> </td> </tr> ) @@ -147,6 +161,21 @@ export default function CursosVista() { </table> </div> </div> + + {/* Modal */} + <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> ); } diff --git a/diplomas/src/schemas/AlumnosSchema.js b/diplomas/src/schemas/AlumnosSchema.js new file mode 100644 index 0000000..01a389a --- /dev/null +++ b/diplomas/src/schemas/AlumnosSchema.js @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export const alumnoSchema = z.object({ + nombre: z + .string() + .nonempty("Escribe el nombre") + .regex(/^[\p{L}\s]+$/u, "Solo se permiten letras en el nombre"), + correo: z.string().email("Escribe un correo válido"), + cursoSeleccionado: z.string().nonempty("Selecciona un curso"), +}); diff --git a/diplomas/src/schemas/CursosSchema.js b/diplomas/src/schemas/CursosSchema.js new file mode 100644 index 0000000..f097b88 --- /dev/null +++ b/diplomas/src/schemas/CursosSchema.js @@ -0,0 +1,14 @@ +import { z } from "zod"; + +export const cursosSchema = 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"), + competencias: z + .string() + .nonempty("Escribe las competencias") + .regex(/^[\p{L}\s]+$/u, "Solo se permiten letras en las competencias"), +});