Merge pull request 'roberto' (#2) from roberto into main
Reviewed-on: #2
This commit is contained in:
commit
1963d4d8d4
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
|
@ -1,10 +1,29 @@
|
|||
import React from "react";
|
||||
import { Document, Page, Text, View, StyleSheet } from "@react-pdf/renderer";
|
||||
import {
|
||||
Document,
|
||||
Page,
|
||||
Text,
|
||||
View,
|
||||
StyleSheet,
|
||||
Image,
|
||||
} from "@react-pdf/renderer";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
page: { padding: 40, fontFamily: "Helvetica" },
|
||||
page: { fontFamily: "Helvetica" },
|
||||
title: { fontSize: 24, textAlign: "center", marginBottom: 20 },
|
||||
section: { marginBottom: 10, fontSize: 14 },
|
||||
nombre: {
|
||||
fontSize: 18,
|
||||
textAlign: "center",
|
||||
marginBottom: 10,
|
||||
fontStyle: "italic",
|
||||
},
|
||||
curso: {
|
||||
fontSize: 30,
|
||||
textAlign: "center",
|
||||
marginBottom: 10,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
section: { padding: 40, fontSize: 14 },
|
||||
competencias: { marginLeft: 20, marginTop: 5 },
|
||||
competencia: { fontSize: 12, marginBottom: 2 },
|
||||
footer: {
|
||||
|
@ -20,36 +39,21 @@ export default function Diploma({ alumno, curso, competencias = [], fecha }) {
|
|||
return (
|
||||
<Document>
|
||||
<Page size="A4" style={styles.page}>
|
||||
<Text style={styles.title}>Diploma</Text>
|
||||
<View style={styles.section}>
|
||||
<Text>
|
||||
<Text style={{ fontWeight: "bold" }}>Alumno: </Text>
|
||||
{alumno?.nombre}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.section}>
|
||||
<Text>
|
||||
<Text style={{ fontWeight: "bold" }}>Curso: </Text>
|
||||
{curso?.nombre || "Sin curso"}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.section}>
|
||||
<Text style={{ fontWeight: "bold" }}>Competencias Acreditadas:</Text>
|
||||
<View style={styles.competencias}>
|
||||
{(competencias || []).map((comp) => (
|
||||
<Text key={comp.id} style={styles.competencia}>
|
||||
- {comp.descripcion}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.section}>
|
||||
<Text>
|
||||
<Text style={{ fontWeight: "bold" }}>Fecha: </Text>
|
||||
{fecha}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.footer}>Generado por SIDAC</Text>
|
||||
<Image src="/encabezado.png" />
|
||||
<Text style={styles.title}>Otorga la presente</Text>
|
||||
<Text style={styles.title}>CONSTANCIA</Text>
|
||||
<Text style={styles.title}>a: </Text>
|
||||
<Text style={styles.nombre}>{alumno?.nombre} </Text>
|
||||
<Text style={styles.title}>
|
||||
Por su asistencia a la píldora educativa
|
||||
</Text>
|
||||
<Text style={styles.curso}>{curso?.nombre || "Sin curso"}</Text>
|
||||
<Text style={styles.title}>
|
||||
con duración de 2 horas, modalidad remota
|
||||
</Text>
|
||||
<Text style={styles.title}>
|
||||
Se expide en la ciudad de Xalapa, Ver., {fecha}
|
||||
</Text>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
|
|
|
@ -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,290 @@
|
|||
import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { cursosSchema } from "@/schemas/CursosSchema";
|
||||
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 function CursosManualForm({ nombreSugerido = "" }) {
|
||||
const [addCompetencia, setAddCompetencia] = useState(false);
|
||||
const [competenciasGuardadas, setCompetenciasGuardadas] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [mostrarDialog, setMostrarDialog] = useState(false);
|
||||
const [mensajeDialog, setMensajeDialog] = useState("");
|
||||
const [mostrarDialogCompetencia, setMostrarDialogCompetencia] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(cursosSchema),
|
||||
defaultValues: {
|
||||
nombre: nombreSugerido,
|
||||
descripcion: "",
|
||||
horas: 0,
|
||||
nuevaCompetencia: "",
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
const handleSaveCompetencia = async (e) => {
|
||||
e.preventDefault();
|
||||
const nuevaCompetencia = getValues("nuevaCompetencia").trim();
|
||||
if (!nuevaCompetencia) return;
|
||||
|
||||
if (competenciasGuardadas.some((c) => c.descripcion === nuevaCompetencia)) {
|
||||
alert("La competencia ya fue agregada.");
|
||||
return;
|
||||
}
|
||||
|
||||
let competenciaId = null;
|
||||
try {
|
||||
const { data: existente } = await supabaseClient
|
||||
.from("competencia")
|
||||
.select("id")
|
||||
.eq("descripcion", nuevaCompetencia)
|
||||
.maybeSingle();
|
||||
|
||||
if (existente && existente.id) {
|
||||
competenciaId = existente.id;
|
||||
} else {
|
||||
const { data: insertada, error } = await supabaseClient
|
||||
.from("competencia")
|
||||
.insert([{ descripcion: nuevaCompetencia }])
|
||||
.select("id")
|
||||
.single();
|
||||
if (error) throw error;
|
||||
competenciaId = insertada.id;
|
||||
}
|
||||
|
||||
setCompetenciasGuardadas([
|
||||
...competenciasGuardadas,
|
||||
{ id: competenciaId, descripcion: nuevaCompetencia },
|
||||
]);
|
||||
setAddCompetencia(false);
|
||||
setValue("nuevaCompetencia", "");
|
||||
setMostrarDialogCompetencia(true);
|
||||
} catch (err) {
|
||||
alert("Error al guardar la competencia: " + (err.message || err));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCompetencia = (index) => {
|
||||
setCompetenciasGuardadas(
|
||||
competenciasGuardadas.filter((_, i) => i !== index)
|
||||
);
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const { nombre, descripcion } = data;
|
||||
const horas = parseInt(data.horas, 10);
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const { data: cursoInsertado, error: errorCurso } = await supabaseClient
|
||||
.from("curso")
|
||||
.insert([{ nombre, descripcion, horas }])
|
||||
.select("id")
|
||||
.single();
|
||||
|
||||
if (errorCurso) {
|
||||
setMensajeDialog("Error al guardar el curso: " + errorCurso.message);
|
||||
setMostrarDialog(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const cursoId = cursoInsertado.id;
|
||||
const relaciones = competenciasGuardadas.map((c) => ({
|
||||
curso_id: cursoId,
|
||||
competencia_id: c.id,
|
||||
}));
|
||||
|
||||
if (relaciones.length > 0) {
|
||||
const { error: errorPivote } = await supabaseClient
|
||||
.from("curso_competencia")
|
||||
.insert(relaciones);
|
||||
if (errorPivote) {
|
||||
setMensajeDialog(
|
||||
"Error al asociar competencias: " + errorPivote.message
|
||||
);
|
||||
setMostrarDialog(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setMensajeDialog("Curso guardado exitosamente");
|
||||
setMostrarDialog(true);
|
||||
form.reset();
|
||||
setCompetenciasGuardadas([]);
|
||||
} catch (err) {
|
||||
alert("Ocurrió un error inesperado");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Nombre del curso"
|
||||
{...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 del curso"
|
||||
{...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>
|
||||
)}
|
||||
|
||||
<h2 className="text-lg font-semibold mb-3 text-black">Competencias</h2>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
Puedes agregar competencias nuevas sin necesidad de crear un nuevo
|
||||
curso. Las competencias se guardarán y podrás asociarlas a otros cursos
|
||||
después.
|
||||
</p>
|
||||
|
||||
{competenciasGuardadas.length > 0 && (
|
||||
<div className="mt-5 w-full flex-wrap">
|
||||
{competenciasGuardadas.map((competencia, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full flex justify-between items-center px-2 mb-2"
|
||||
>
|
||||
<span className="text-black">{competencia.descripcion}</span>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleDeleteCompetencia(index)}
|
||||
className="bg-red-400 hover:bg-red-500 text-white font-bold py-1 px-3 rounded-md"
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{addCompetencia && (
|
||||
<div className="w-full flex flex-col md:flex-row mt-5">
|
||||
<div className="flex flex-col">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Nueva competencia"
|
||||
{...register("nuevaCompetencia")}
|
||||
className="w-80 px-3 py-2 border border-gray-300 rounded-md mb-3 text-black"
|
||||
/>
|
||||
{errors.nuevaCompetencia && (
|
||||
<p className="text-red-500 text-sm">
|
||||
{errors.nuevaCompetencia.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSaveCompetencia}
|
||||
className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-md mr-2"
|
||||
>
|
||||
Guardar
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setAddCompetencia(false);
|
||||
setValue("nuevaCompetencia", "");
|
||||
}}
|
||||
className="bg-gray-400 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded-md"
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setAddCompetencia(true)}
|
||||
className="w-full bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded-md mt-5"
|
||||
>
|
||||
Agregar competencia
|
||||
</Button>
|
||||
|
||||
<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 curso"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog open={mostrarDialog} onOpenChange={setMostrarDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-black">Resultado</DialogTitle>
|
||||
<DialogDescription>{mensajeDialog}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setMostrarDialog(false)}>Cerrar</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={mostrarDialogCompetencia}
|
||||
onOpenChange={setMostrarDialogCompetencia}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-black">
|
||||
Competencia agregada
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
¡La competencia fue agregada exitosamente!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setMostrarDialogCompetencia(false)}>
|
||||
Cerrar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</form>
|
||||
);
|
||||
}
|
|
@ -8,11 +8,16 @@ import {
|
|||
import Diploma from "@/components/Diploma";
|
||||
import { PDFDownloadLink, PDFViewer, pdf } from "@react-pdf/renderer";
|
||||
import { supabaseClient } from "@/utils/supabase";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { mensajesSchema } from "@/schemas/mensajesSchema";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
|
||||
function VistaPreviaDiplomaDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
alumno,
|
||||
curso,
|
||||
competencias: competenciasProp,
|
||||
fecha,
|
||||
competenciasAcreditadas,
|
||||
|
@ -20,8 +25,77 @@ function VistaPreviaDiplomaDialog({
|
|||
const [mostrarVistaPrevia, setMostrarVistaPrevia] = useState(false);
|
||||
const [enviando, setEnviando] = useState(false);
|
||||
const [mensaje, setMensaje] = useState("");
|
||||
const [loadingMensajes, setLoadingMensajes] = useState(false);
|
||||
const [competencias, setCompetencias] = useState([]);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(mensajesSchema),
|
||||
defaultValues: {
|
||||
correo: "",
|
||||
whatsapp: "",
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
// 🔄 Cargar mensajes al abrir el modal
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setLoadingMensajes(true);
|
||||
Promise.all([
|
||||
supabaseClient.from("mensaje_correo").select("id, mensaje").single(),
|
||||
supabaseClient.from("mensaje_whatsapp").select("id, mensaje").single(),
|
||||
])
|
||||
.then(([correoRes, whatsappRes]) => {
|
||||
if (correoRes.data) setValue("correo", correoRes.data.mensaje);
|
||||
if (whatsappRes.data) setValue("whatsapp", whatsappRes.data.mensaje);
|
||||
})
|
||||
.finally(() => setLoadingMensajes(false));
|
||||
}
|
||||
}, [open, setValue]);
|
||||
|
||||
// 📝 Guardar mensajes personalizados
|
||||
const handleGuardarMensajes = async () => {
|
||||
const { correo, whatsapp } = getValues();
|
||||
|
||||
const [{ data: correoExistente }] = await Promise.all([
|
||||
supabaseClient.from("mensaje_correo").select("id").maybeSingle(),
|
||||
]);
|
||||
|
||||
if (correoExistente) {
|
||||
await supabaseClient
|
||||
.from("mensaje_correo")
|
||||
.update({ mensaje: correo })
|
||||
.eq("id", correoExistente.id);
|
||||
} else {
|
||||
await supabaseClient.from("mensaje_correo").insert({ mensaje: correo });
|
||||
}
|
||||
|
||||
const { data: whatsappExistente } = await supabaseClient
|
||||
.from("mensaje_whatsapp")
|
||||
.select("id")
|
||||
.maybeSingle();
|
||||
|
||||
if (whatsappExistente) {
|
||||
await supabaseClient
|
||||
.from("mensaje_whatsapp")
|
||||
.update({ mensaje: whatsapp })
|
||||
.eq("id", whatsappExistente.id);
|
||||
} else {
|
||||
await supabaseClient
|
||||
.from("mensaje_whatsapp")
|
||||
.insert({ mensaje: whatsapp });
|
||||
}
|
||||
|
||||
setMensaje("Mensajes guardados correctamente.");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (alumno && alumno.curso?.id) {
|
||||
supabaseClient
|
||||
|
@ -41,28 +115,25 @@ function VistaPreviaDiplomaDialog({
|
|||
? competencias.filter((comp) => competenciasAcreditadas.includes(comp.id))
|
||||
: competencias;
|
||||
|
||||
// Simulación de envío de PDF por correo y WhatsApp
|
||||
const handleEnviar = async () => {
|
||||
setEnviando(true);
|
||||
setMensaje("");
|
||||
// Genera el PDF como blob
|
||||
|
||||
const blob = await pdf(
|
||||
<Diploma
|
||||
alumno={alumno}
|
||||
curso={alumno.curso}
|
||||
curso={curso}
|
||||
competencias={competenciasMostradas}
|
||||
fecha={fecha || new Date().toLocaleDateString()}
|
||||
/>
|
||||
).toBlob();
|
||||
|
||||
// Convierte el blob a base64
|
||||
const pdfBase64 = await new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result.split(",")[1]);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
// Llama a tu API de Next.js
|
||||
const resp = await fetch("/api/send-diploma", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
@ -71,32 +142,30 @@ function VistaPreviaDiplomaDialog({
|
|||
nombre: alumno.nombre,
|
||||
curso: alumno.curso?.nombre || "Sin curso",
|
||||
pdfBase64,
|
||||
mensajeCorreo: getValues("correo"),
|
||||
}),
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
// WhatsApp real (abre ventana)
|
||||
const telefono = alumno.telefono.replace(/\D/g, "");
|
||||
const mensajeWhatsapp = encodeURIComponent(
|
||||
`Hola ${alumno.nombre}, tu diploma ha sido generado y enviado a tu correo (${alumno.correo}). ¡Felicidades!`
|
||||
);
|
||||
const mensajeWhatsapp = encodeURIComponent(getValues("whatsapp"));
|
||||
window.open(
|
||||
`https://wa.me/${telefono}?text=${mensajeWhatsapp}`,
|
||||
"_blank"
|
||||
);
|
||||
|
||||
setMensaje(
|
||||
`Diploma enviado por correo a ${alumno.correo} y mensaje enviado por WhatsApp al ${alumno.telefono}.`
|
||||
);
|
||||
setMensaje(`Diploma enviado por correo a ${alumno.correo}.`);
|
||||
} else {
|
||||
setMensaje("Error enviando el diploma por correo.");
|
||||
setMensaje("Error enviando el diploma.");
|
||||
}
|
||||
|
||||
setEnviando(false);
|
||||
};
|
||||
|
||||
if (!alumno) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg text-black">
|
||||
<DialogContent className="max-w-lg h-screen text-black overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Diploma</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
@ -117,15 +186,35 @@ function VistaPreviaDiplomaDialog({
|
|||
<div className="text-lg mb-2">
|
||||
<b>Fecha:</b> {fecha || new Date().toLocaleDateString()}
|
||||
</div>
|
||||
<div className="mt-auto text-gray-400 text-xs text-right">
|
||||
Vista previa
|
||||
</div>
|
||||
<div className="mt-4 flex gap-2 justify-center flex-wrap">
|
||||
{/* Campos de mensaje */}
|
||||
<form>
|
||||
<div className="mb-4">
|
||||
<Textarea
|
||||
label="Mensaje para correo"
|
||||
placeholder="Escribe un mensaje personalizado para el correo"
|
||||
{...register("correo")}
|
||||
error={errors.correo?.message}
|
||||
disabled={loadingMensajes}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Textarea
|
||||
label="Mensaje para WhatsApp"
|
||||
placeholder="Escribe un mensaje personalizado para WhatsApp"
|
||||
{...register("whatsapp")}
|
||||
error={errors.whatsapp?.message}
|
||||
disabled={loadingMensajes}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Acciones */}
|
||||
<div className="mt-4 flex flex-wrap gap-2 justify-center">
|
||||
<PDFDownloadLink
|
||||
document={
|
||||
<Diploma
|
||||
alumno={alumno}
|
||||
curso={alumno.curso}
|
||||
curso={curso}
|
||||
competencias={competenciasMostradas}
|
||||
fecha={fecha || new Date().toLocaleDateString()}
|
||||
/>
|
||||
|
@ -144,12 +233,14 @@ function VistaPreviaDiplomaDialog({
|
|||
)
|
||||
}
|
||||
</PDFDownloadLink>
|
||||
|
||||
<button
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2 rounded"
|
||||
onClick={() => setMostrarVistaPrevia(true)}
|
||||
>
|
||||
Ver vista previa PDF
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="bg-purple-600 hover:bg-purple-800 text-white px-4 py-2 rounded"
|
||||
onClick={handleEnviar}
|
||||
|
@ -157,20 +248,31 @@ function VistaPreviaDiplomaDialog({
|
|||
>
|
||||
{enviando ? "Enviando..." : "Enviar por correo y WhatsApp"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="bg-yellow-500 hover:bg-yellow-600 text-white px-4 py-2 rounded"
|
||||
onClick={handleGuardarMensajes}
|
||||
type="button"
|
||||
>
|
||||
Guardar mensajes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{mensaje && (
|
||||
<div className="mt-4 text-green-700 font-semibold text-center">
|
||||
{mensaje}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vista previa PDF */}
|
||||
{mostrarVistaPrevia && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-60 flex flex-col items-center justify-center z-50">
|
||||
<div className="bg-white rounded shadow-lg p-4 flex flex-col items-center">
|
||||
<div className="w-[80vw] h-[80vh] lg:h-[90vh] mb-4 border">
|
||||
<div className="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded shadow-lg p-4">
|
||||
<div className="w-[80vw] h-[90vh] mb-4 border">
|
||||
<PDFViewer width="100%" height="100%">
|
||||
<Diploma
|
||||
alumno={alumno}
|
||||
curso={alumno.curso}
|
||||
curso={curso}
|
||||
competencias={competenciasMostradas}
|
||||
fecha={fecha || new Date().toLocaleDateString()}
|
||||
/>
|
||||
|
|
|
@ -1,7 +1,26 @@
|
|||
"use client";
|
||||
|
||||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
function MainContent({ children }) {
|
||||
const { open } = useSidebar();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`p-4 min-h-screen flex justify-center transition-all duration-200 ${
|
||||
open ? "w-screen md:w-[80vw]" : "w-screen"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
|
@ -9,7 +28,8 @@ export default function Layout({ children }) {
|
|||
<div className="flex">
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<div className="p-4 w-full">{children}</div>
|
||||
<SidebarTrigger className="-ml-1 text-black" />
|
||||
<MainContent>{children}</MainContent>
|
||||
</SidebarInset>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
|
|
|
@ -238,7 +238,7 @@ function SidebarTrigger({ className, onClick, ...props }) {
|
|||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("size-7", className)}
|
||||
className={cn("size-10", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
|
|
|
@ -3,9 +3,18 @@ import Papa from "papaparse";
|
|||
import * as XLSX from "xlsx";
|
||||
import Layout from "@/components/layout/Layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
||||
import { CursosManualForm } from "./cursosManual"; // Importa el formulario sin Layout
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { CursosManualForm } from "@/components/cursosManualForm";
|
||||
import { supabaseClient } from "@/utils/supabase";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function AlumnosArchivo() {
|
||||
const [archivo, setArchivo] = useState(null);
|
||||
|
@ -14,11 +23,32 @@ export default function AlumnosArchivo() {
|
|||
const [mensajeDialogo, setMensajeDialogo] = useState("");
|
||||
const [mostrarDialogCurso, setMostrarDialogCurso] = useState(false);
|
||||
const [cursoFaltante, setCursoFaltante] = useState("");
|
||||
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);
|
||||
// Cancelar navegación
|
||||
throw "Bloqueo de navegación por archivo pendiente";
|
||||
}
|
||||
};
|
||||
|
||||
router.events.on("routeChangeStart", handleRouteChange);
|
||||
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", handleRouteChange);
|
||||
};
|
||||
}, [archivo, datos, router]);
|
||||
|
||||
const registrarAlumnos = async () => {
|
||||
if (datos.length === 0) return;
|
||||
|
||||
|
@ -26,11 +56,12 @@ export default function AlumnosArchivo() {
|
|||
|
||||
for (const alumno of datos) {
|
||||
// 1. Verifica si el curso existe
|
||||
const { data: cursosEncontrados, error: errorCurso } = await supabaseClient
|
||||
.from("curso")
|
||||
.select("id")
|
||||
.eq("nombre", alumno.nombreCurso)
|
||||
.maybeSingle();
|
||||
const { data: cursosEncontrados, error: errorCurso } =
|
||||
await supabaseClient
|
||||
.from("curso")
|
||||
.select("id")
|
||||
.eq("nombre", alumno.nombreCurso)
|
||||
.maybeSingle();
|
||||
|
||||
if (errorCurso) {
|
||||
errores.push({ alumno, error: "Error al buscar el curso" });
|
||||
|
@ -41,7 +72,9 @@ export default function AlumnosArchivo() {
|
|||
// Si no existe el curso, muestra el dialog para registrar el curso
|
||||
setCursoFaltante(alumno.nombreCurso);
|
||||
setMostrarDialogCurso(true);
|
||||
setMensajeDialogo(`El curso "${alumno.nombreCurso}" no existe. Por favor, regístralo primero.`);
|
||||
setMensajeDialogo(
|
||||
`El curso "${alumno.nombreCurso}" no existe. Por favor, regístralo primero.`
|
||||
);
|
||||
setDialogoAbierto(true);
|
||||
return; // Detiene el registro de alumnos
|
||||
}
|
||||
|
@ -54,7 +87,7 @@ export default function AlumnosArchivo() {
|
|||
nombre: alumno.nombre,
|
||||
correo: alumno.correo,
|
||||
telefono: alumno.telefono,
|
||||
curso_id: cursosEncontrados.id, // Usar el id del curso
|
||||
curso_id: cursosEncontrados.id,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -65,7 +98,9 @@ export default function AlumnosArchivo() {
|
|||
}
|
||||
|
||||
if (errores.length > 0) {
|
||||
setMensajeDialogo(`Se registraron algunos errores:\n${JSON.stringify(errores, null, 2)}`);
|
||||
setMensajeDialogo(
|
||||
`Se registraron algunos errores:\n${JSON.stringify(errores, null, 2)}`
|
||||
);
|
||||
} else {
|
||||
setMensajeDialogo("Todos los alumnos fueron registrados correctamente.");
|
||||
setArchivo(null);
|
||||
|
@ -130,8 +165,8 @@ export default function AlumnosArchivo() {
|
|||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="w-[60vw] pt-10 flex flex-col items-end justify-center text-black">
|
||||
<div className="bg-white p-8 font-sans text-center w-[70%] flex flex-col items-center">
|
||||
<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">
|
||||
Nuevo alumno
|
||||
</h1>
|
||||
|
@ -144,7 +179,9 @@ export default function AlumnosArchivo() {
|
|||
{archivo ? (
|
||||
<span className="text-black font-medium">{archivo.name}</span>
|
||||
) : (
|
||||
<span>Arrastra y suelta un archivo o haz clic para seleccionarlo</span>
|
||||
<span>
|
||||
Arrastra y suelta un archivo o haz clic para seleccionarlo
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
|
@ -169,7 +206,9 @@ export default function AlumnosArchivo() {
|
|||
<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>
|
||||
<th key={index} className="border px-4 py-2">
|
||||
{columna}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -177,7 +216,9 @@ export default function AlumnosArchivo() {
|
|||
{datos.map((fila, index) => (
|
||||
<tr key={index}>
|
||||
{Object.values(fila).map((valor, i) => (
|
||||
<td key={i} className="border px-4 py-1">{valor}</td>
|
||||
<td key={i} className="border px-4 py-1">
|
||||
{valor}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
@ -196,7 +237,8 @@ export default function AlumnosArchivo() {
|
|||
Registrar curso faltante
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
El curso <b>{cursoFaltante}</b> no existe. Por favor, regístralo antes de continuar.
|
||||
El curso <b>{cursoFaltante}</b> no existe. Por favor, regístralo
|
||||
antes de continuar.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<CursosManualForm nombreSugerido={cursoFaltante} />
|
||||
|
@ -215,6 +257,38 @@ export default function AlumnosArchivo() {
|
|||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Dialog de advertencia de navegación */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,80 +84,66 @@ export default function AlumnosManual() {
|
|||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="w-[60vw] pt-10 flex flex-col items-end justify-center text-black">
|
||||
<div className="bg-white p-8 font-sans text-center w-[70%]">
|
||||
<h1 className="text-xl font-semibold mb-4 text-black">
|
||||
Nuevo alumno
|
||||
</h1>
|
||||
<form onSubmit={handleSubmit(manejarGuardar)}>
|
||||
<div className="mb-3">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Nombre"
|
||||
{...register("nombre")}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
/>
|
||||
{errors.nombre && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{errors.nombre.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Email"
|
||||
{...register("correo")}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
/>
|
||||
{errors.correo && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{errors.correo.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Teléfono"
|
||||
{...register("telefono")}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
/>
|
||||
{errors.telefono && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{errors.telefono.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Select
|
||||
onValueChange={(value) => setValue("cursoSeleccionado", value)}
|
||||
>
|
||||
<SelectTrigger className="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<SelectValue placeholder="Selecciona un curso" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{cursos.map((curso) => (
|
||||
<SelectItem key={curso.id} value={curso.id.toString()}>
|
||||
{curso.nombre}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.cursoSeleccionado && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{errors.cursoSeleccionado.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-green-400 hover:bg-green-500 font-bold py-2 px-4 rounded-md text-black"
|
||||
>
|
||||
Registrar
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="w-full bg-white font-sans text-center md:w-[80%] pt-10 flex flex-col items-center justify-start text-black">
|
||||
<h1 className="text-xl font-semibold mb-10 text-black my-10">
|
||||
Nuevo alumno
|
||||
</h1>
|
||||
<form onSubmit={handleSubmit(manejarGuardar)} className="w-full">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Nombre"
|
||||
{...register("nombre")}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3"
|
||||
/>
|
||||
{errors.nombre && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.nombre.message}</p>
|
||||
)}
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Email"
|
||||
{...register("correo")}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3"
|
||||
/>
|
||||
{errors.correo && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.correo.message}</p>
|
||||
)}
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Teléfono"
|
||||
{...register("telefono")}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3"
|
||||
/>
|
||||
{errors.telefono && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{errors.telefono.message}
|
||||
</p>
|
||||
)}
|
||||
<Select
|
||||
onValueChange={(value) => setValue("cursoSeleccionado", value)}
|
||||
>
|
||||
<SelectTrigger className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3">
|
||||
<SelectValue placeholder="Selecciona un curso" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{cursos.map((curso) => (
|
||||
<SelectItem key={curso.id} value={curso.id.toString()}>
|
||||
{curso.nombre}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.cursoSeleccionado && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{errors.cursoSeleccionado.message}
|
||||
</p>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-green-400 hover:bg-green-500 font-bold py-2 px-4 rounded-md text-white"
|
||||
>
|
||||
Registrar
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Diálogo de confirmación */}
|
||||
|
|
|
@ -142,124 +142,126 @@ export default function AlumnosVista() {
|
|||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="w-[80vw] pt-10 flex flex-col items-center text-black">
|
||||
<h1 className="text-2xl font-semibold mb-6 text-black">
|
||||
<div className="w-full pt-5 flex flex-col items-center text-black">
|
||||
<h1 className="text-2xl font-semibold mt-5 mb-10 text-black">
|
||||
Lista de Alumnos
|
||||
</h1>
|
||||
<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">Correo</th>
|
||||
<th className="py-2 border-b">Teléfono</th>
|
||||
<th className="py-2 border-b">Curso</th>
|
||||
<th className="py-2 border-b">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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" {...register("nombre")} />
|
||||
{errors.nombre && (
|
||||
<span className="text-red-500 text-xs">
|
||||
{errors.nombre.message}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-4 border-b">
|
||||
<Input type="email" {...register("correo")} />
|
||||
{errors.correo && (
|
||||
<span className="text-red-500 text-xs">
|
||||
{errors.correo.message}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-4 border-b">
|
||||
<Input type="text" {...register("telefono")} />
|
||||
{errors.telefono && (
|
||||
<span className="text-red-500 text-xs">
|
||||
{errors.telefono.message}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-4 border-b">
|
||||
<Select
|
||||
value={undefined}
|
||||
onValueChange={(value) =>
|
||||
setValue("cursoSeleccionado", value)
|
||||
}
|
||||
{...register("cursoSeleccionado")}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecciona un curso" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{cursos.map((curso) => (
|
||||
<SelectItem
|
||||
key={curso.id}
|
||||
value={curso.id.toString()}
|
||||
>
|
||||
{curso.nombre}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.cursoSeleccionado && (
|
||||
<span className="text-red-500 text-xs">
|
||||
{errors.cursoSeleccionado.message}
|
||||
</span>
|
||||
)}
|
||||
</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={handleSubmit(guardarEdicion)}
|
||||
>
|
||||
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">{alumno.telefono}</td>
|
||||
<td className="py-2 px-4 border-b">
|
||||
{alumno.curso?.nombre || "Sin curso"}
|
||||
</td>
|
||||
<td className="py-2 px-4 border-b space-x-2">
|
||||
<Button
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 rounded"
|
||||
onClick={() => iniciarEdicion(alumno)}
|
||||
>
|
||||
Editar
|
||||
</Button>
|
||||
<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">Correo</th>
|
||||
<th className="py-2 border-b">Teléfono</th>
|
||||
<th className="py-2 border-b">Curso</th>
|
||||
<th className="py-2 border-b">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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" {...register("nombre")} />
|
||||
{errors.nombre && (
|
||||
<span className="text-red-500 text-xs">
|
||||
{errors.nombre.message}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-4 border-b">
|
||||
<Input type="email" {...register("correo")} />
|
||||
{errors.correo && (
|
||||
<span className="text-red-500 text-xs">
|
||||
{errors.correo.message}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-4 border-b">
|
||||
<Input type="text" {...register("telefono")} />
|
||||
{errors.telefono && (
|
||||
<span className="text-red-500 text-xs">
|
||||
{errors.telefono.message}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-4 border-b">
|
||||
<Select
|
||||
value={(alumno.curso_id || "").toString()}
|
||||
onValueChange={(value) =>
|
||||
setValue("cursoSeleccionado", value)
|
||||
}
|
||||
{...register("cursoSeleccionado")}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecciona un curso" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{cursos.map((curso) => (
|
||||
<SelectItem
|
||||
key={curso.id}
|
||||
value={curso.id.toString()}
|
||||
>
|
||||
{curso.nombre}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.cursoSeleccionado && (
|
||||
<span className="text-red-500 text-xs">
|
||||
{errors.cursoSeleccionado.message}
|
||||
</span>
|
||||
)}
|
||||
</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-1 rounded"
|
||||
onClick={handleSubmit(guardarEdicion)}
|
||||
>
|
||||
Guardar
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-gray-400 hover:bg-gray-600 text-white font-bold py-1 px-3 m-1 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">{alumno.telefono}</td>
|
||||
<td className="py-2 px-4 border-b">
|
||||
{alumno.curso?.nombre || "Sin curso"}
|
||||
</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(alumno)}
|
||||
>
|
||||
Editar
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 rounded"
|
||||
onClick={() => confirmarEliminacion(alumno.id)}
|
||||
>
|
||||
Eliminar
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<Button
|
||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 m-1 rounded"
|
||||
onClick={() => confirmarEliminacion(alumno.id)}
|
||||
>
|
||||
Eliminar
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal de confirmación */}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
|
@ -5,14 +5,18 @@ sgMail.setApiKey(process.env.SENDGRID_API_KEY);
|
|||
export default async function handler(req, res) {
|
||||
if (req.method !== "POST") return res.status(405).end();
|
||||
|
||||
const { email, nombre, curso, pdfBase64 } = req.body;
|
||||
// Recibe mensajeCorreo desde el body
|
||||
const { email, nombre, curso, pdfBase64, mensajeCorreo } = req.body;
|
||||
|
||||
try {
|
||||
await sgMail.send({
|
||||
to: email,
|
||||
from: "rviverosgonzalez@outlook.com", // Cambia esto por tu correo verificado en SendGrid
|
||||
subject: "Tu diploma",
|
||||
text: `Hola ${nombre}, has concluido tu curso ${curso} por lo que adjuntamos tu diploma.`,
|
||||
// Usa el mensajeCorreo personalizado, si no viene usa el texto por defecto
|
||||
text:
|
||||
mensajeCorreo ||
|
||||
`Hola ${nombre}, has concluido tu curso ${curso} por lo que adjuntamos tu diploma.`,
|
||||
attachments: [
|
||||
{
|
||||
content: pdfBase64,
|
||||
|
|
|
@ -3,7 +3,15 @@ import Papa from "papaparse";
|
|||
import * as XLSX from "xlsx";
|
||||
import Layout from "@/components/layout/Layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function cursosArchivo() {
|
||||
|
@ -53,7 +61,7 @@ export default function cursosArchivo() {
|
|||
horas: curso.horas,
|
||||
descripcion: curso.descripcion,
|
||||
competencias: curso.competencias
|
||||
? curso.competencias.split(",").map(c => c.trim())
|
||||
? curso.competencias.split(",").map((c) => c.trim())
|
||||
: [],
|
||||
}),
|
||||
});
|
||||
|
@ -67,7 +75,9 @@ export default function cursosArchivo() {
|
|||
setDialogoCargando(false); // Ocultar dialogo de carga
|
||||
|
||||
if (errores.length > 0) {
|
||||
setMensajeDialogo(`Se registraron algunos errores:\n${JSON.stringify(errores, null, 2)}`);
|
||||
setMensajeDialogo(
|
||||
`Se registraron algunos errores:\n${JSON.stringify(errores, null, 2)}`
|
||||
);
|
||||
} else {
|
||||
setMensajeDialogo("Todos los cursos fueron registrados correctamente.");
|
||||
setArchivo(null);
|
||||
|
@ -132,11 +142,9 @@ export default function cursosArchivo() {
|
|||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="w-[60vw] pt-10 flex flex-col items-end justify-center text-black">
|
||||
<div className="bg-white p-8 font-sans text-center w-[70%] flex flex-col items-center">
|
||||
<h1 className="text-xl font-semibold mb-4 text-black">
|
||||
Nuevo curso
|
||||
</h1>
|
||||
<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">Nuevo curso</h1>
|
||||
<label
|
||||
htmlFor="archivo"
|
||||
onDrop={manejarSoltar}
|
||||
|
@ -146,7 +154,9 @@ export default function cursosArchivo() {
|
|||
{archivo ? (
|
||||
<span className="text-black font-medium">{archivo.name}</span>
|
||||
) : (
|
||||
<span>Arrastra y suelta un archivo o haz clic para seleccionarlo</span>
|
||||
<span>
|
||||
Arrastra y suelta un archivo o haz clic para seleccionarlo
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
|
@ -171,7 +181,9 @@ export default function cursosArchivo() {
|
|||
<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>
|
||||
<th key={index} className="border px-4 py-2">
|
||||
{columna}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -179,7 +191,9 @@ export default function cursosArchivo() {
|
|||
{datos.map((fila, index) => (
|
||||
<tr key={index}>
|
||||
{Object.values(fila).map((valor, i) => (
|
||||
<td key={i} className="border px-4 py-1">{valor}</td>
|
||||
<td key={i} className="border px-4 py-1">
|
||||
{valor}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
@ -216,7 +230,8 @@ export default function cursosArchivo() {
|
|||
<DialogHeader>
|
||||
<DialogTitle className="text-black">Advertencia</DialogTitle>
|
||||
<DialogDescription>
|
||||
Si cambias de ventana perderás la subida del archivo. ¿Deseas continuar?
|
||||
Si cambias de ventana perderás la subida del archivo. ¿Deseas
|
||||
continuar?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
|
@ -242,4 +257,4 @@ export default function cursosArchivo() {
|
|||
</Dialog>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ 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"; // Importar el cliente de Supabase
|
||||
import { supabaseClient } from "@/utils/supabase";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
@ -17,11 +17,11 @@ import {
|
|||
} from "@/components/ui/dialog";
|
||||
|
||||
export default function CursosManual() {
|
||||
const [addCompetencia, setAddCompetencia] = useState(false);
|
||||
const [competenciasGuardadas, setCompetenciasGuardadas] = useState([]);
|
||||
const [competencias, setCompetencias] = useState([]); // [{id, descripcion}]
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [dialogMsg, setDialogMsg] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [mostrarDialog, setMostrarDialog] = useState(false);
|
||||
const [mensajeDialog, setMensajeDialog] = useState("");
|
||||
const [addCompetencia, setAddCompetencia] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(cursosSchema),
|
||||
|
@ -32,7 +32,6 @@ export default function CursosManual() {
|
|||
nuevaCompetencia: "",
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
|
@ -41,348 +40,211 @@ export default function CursosManual() {
|
|||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
const handleAddCompetencia = () => {
|
||||
setAddCompetencia(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setAddCompetencia(false);
|
||||
setValue("nuevaCompetencia", "");
|
||||
};
|
||||
|
||||
const handleSaveCompetencia = (e) => {
|
||||
e.preventDefault();
|
||||
const nuevaCompetencia = getValues("nuevaCompetencia");
|
||||
if (nuevaCompetencia.trim() !== "") {
|
||||
setCompetenciasGuardadas([
|
||||
...competenciasGuardadas,
|
||||
nuevaCompetencia.trim(),
|
||||
]);
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCompetencia = (index) => {
|
||||
setCompetenciasGuardadas(
|
||||
competenciasGuardadas.filter((_, i) => i !== index)
|
||||
);
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const { nombre, descripcion } = data;
|
||||
const horas = parseInt(data.horas, 10); // Convertir horas a número
|
||||
const competencias = competenciasGuardadas;
|
||||
|
||||
setLoading(true); // Mostrar estado de carga
|
||||
|
||||
try {
|
||||
const { error } = await supabaseClient.from("curso").insert([
|
||||
{
|
||||
nombre,
|
||||
descripcion,
|
||||
horas,
|
||||
competencias, // Guardar competencias como array
|
||||
},
|
||||
]);
|
||||
|
||||
if (error) {
|
||||
console.error("Error al guardar en Supabase:", error.message);
|
||||
alert("Error al guardar el curso: " + error.message);
|
||||
} else {
|
||||
setMensajeDialog("Curso guardado exitosamente");
|
||||
setMostrarDialog(true);
|
||||
form.reset(); // Reiniciar el formulario
|
||||
setCompetenciasGuardadas([]); // Limpiar competencias guardadas
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error inesperado:", err);
|
||||
alert("Ocurrió un error inesperado");
|
||||
} finally {
|
||||
setLoading(false); // Ocultar estado de carga
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="w-[60vw] pt-10 flex flex-col items-end justify-center">
|
||||
<div className="bg-white p-8 font-sans text-center w-[70%] flex flex-col items-center">
|
||||
<h1 className="text-xl font-semibold mb-4 text-black">Nuevo curso</h1>
|
||||
<CursosManualForm nombreSugerido="" />
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export function CursosManualForm({ nombreSugerido = "" }) {
|
||||
const [addCompetencia, setAddCompetencia] = useState(false);
|
||||
const [competenciasGuardadas, setCompetenciasGuardadas] = useState([]); // [{id, descripcion}]
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [mostrarDialog, setMostrarDialog] = useState(false);
|
||||
const [mensajeDialog, setMensajeDialog] = useState("");
|
||||
|
||||
// Estado para dialog de competencia agregada
|
||||
const [mostrarDialogCompetencia, setMostrarDialogCompetencia] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(cursosSchema),
|
||||
defaultValues: {
|
||||
nombre: nombreSugerido,
|
||||
descripcion: "",
|
||||
horas: 0,
|
||||
nuevaCompetencia: "",
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
// Cambia handleSaveCompetencia para mostrar el dialog
|
||||
// Añadir competencia (busca o crea en BD)
|
||||
const handleSaveCompetencia = async (e) => {
|
||||
e.preventDefault();
|
||||
const nuevaCompetencia = getValues("nuevaCompetencia").trim();
|
||||
if (!nuevaCompetencia) return;
|
||||
|
||||
// Verifica si ya existe en el estado
|
||||
if (competenciasGuardadas.some((c) => c.descripcion === nuevaCompetencia)) {
|
||||
alert("La competencia ya fue agregada.");
|
||||
const desc = getValues("nuevaCompetencia").trim();
|
||||
if (!desc) return;
|
||||
if (competencias.some((c) => c.descripcion === desc)) {
|
||||
setDialogMsg("La competencia ya fue agregada.");
|
||||
setShowDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verifica si ya existe en la base de datos
|
||||
let competenciaId = null;
|
||||
try {
|
||||
// Busca si ya existe
|
||||
const { data: existente } = await supabaseClient
|
||||
let { data: existente } = await supabaseClient
|
||||
.from("competencia")
|
||||
.select("id")
|
||||
.eq("descripcion", nuevaCompetencia)
|
||||
.eq("descripcion", desc)
|
||||
.maybeSingle();
|
||||
|
||||
if (existente && existente.id) {
|
||||
competenciaId = existente.id;
|
||||
} else {
|
||||
// Si no existe, la crea
|
||||
let id = existente?.id;
|
||||
if (!id) {
|
||||
const { data: insertada, error } = await supabaseClient
|
||||
.from("competencia")
|
||||
.insert([{ descripcion: nuevaCompetencia }])
|
||||
.insert([{ descripcion: desc }])
|
||||
.select("id")
|
||||
.single();
|
||||
if (error) throw error;
|
||||
competenciaId = insertada.id;
|
||||
id = insertada.id;
|
||||
}
|
||||
|
||||
setCompetenciasGuardadas([
|
||||
...competenciasGuardadas,
|
||||
{ id: competenciaId, descripcion: nuevaCompetencia },
|
||||
]);
|
||||
setCompetencias([...competencias, { id, descripcion: desc }]);
|
||||
setAddCompetencia(false);
|
||||
setValue("nuevaCompetencia", "");
|
||||
setMostrarDialogCompetencia(true); // Mostrar dialog de éxito
|
||||
setDialogMsg("¡La competencia fue agregada exitosamente!");
|
||||
setShowDialog(true);
|
||||
} catch (err) {
|
||||
alert("Error al guardar la competencia: " + (err.message || err));
|
||||
setDialogMsg("Error al guardar la competencia: " + (err.message || err));
|
||||
setShowDialog(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Eliminar competencia
|
||||
const handleDeleteCompetencia = (index) => {
|
||||
setCompetenciasGuardadas(
|
||||
competenciasGuardadas.filter((_, i) => i !== index)
|
||||
);
|
||||
setCompetencias(competencias.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
// Guardar curso y asociar competencias
|
||||
const onSubmit = async (data) => {
|
||||
const { nombre, descripcion } = data;
|
||||
const horas = parseInt(data.horas, 10);
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// 1. Inserta el curso
|
||||
const { data: cursoInsertado, error: errorCurso } = await supabaseClient
|
||||
const { nombre, descripcion, horas } = data;
|
||||
const { data: curso, error: errorCurso } = await supabaseClient
|
||||
.from("curso")
|
||||
.insert([{ nombre, descripcion, horas }])
|
||||
.insert([{ nombre, descripcion, horas: parseInt(horas, 10) }])
|
||||
.select("id")
|
||||
.single();
|
||||
|
||||
if (errorCurso) {
|
||||
setMensajeDialog("Error al guardar el curso: " + errorCurso.message);
|
||||
setMostrarDialog(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Inserta en la tabla pivote curso_competencia
|
||||
const cursoId = cursoInsertado.id;
|
||||
const relaciones = competenciasGuardadas.map((c) => ({
|
||||
curso_id: cursoId,
|
||||
competencia_id: c.id,
|
||||
}));
|
||||
|
||||
if (relaciones.length > 0) {
|
||||
if (errorCurso) throw errorCurso;
|
||||
if (competencias.length) {
|
||||
const relaciones = competencias.map((c) => ({
|
||||
curso_id: curso.id,
|
||||
competencia_id: c.id,
|
||||
}));
|
||||
const { error: errorPivote } = await supabaseClient
|
||||
.from("curso_competencia")
|
||||
.insert(relaciones);
|
||||
if (errorPivote) {
|
||||
setMensajeDialog("Error al asociar competencias: " + errorPivote.message);
|
||||
setMostrarDialog(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (errorPivote) throw errorPivote;
|
||||
}
|
||||
|
||||
setMensajeDialog("Curso guardado exitosamente");
|
||||
setMostrarDialog(true);
|
||||
setDialogMsg("Curso guardado exitosamente");
|
||||
setCompetencias([]);
|
||||
form.reset();
|
||||
setCompetenciasGuardadas([]);
|
||||
} catch (err) {
|
||||
alert("Ocurrió un error inesperado");
|
||||
setDialogMsg("Error: " + (err.message || err));
|
||||
} finally {
|
||||
setShowDialog(true);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Nombre del curso"
|
||||
{...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>
|
||||
)}
|
||||
<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">Nuevo curso</h1>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Nombre del curso"
|
||||
{...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>
|
||||
)}
|
||||
<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 del curso"
|
||||
{...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>
|
||||
)}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Horas del curso"
|
||||
{...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>
|
||||
)}
|
||||
|
||||
<h2 className="text-lg font-semibold mb-3 text-black">Competencias</h2>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
Puedes agregar competencias nuevas sin necesidad de crear un nuevo curso. Las competencias se guardarán y podrás asociarlas a otros cursos después.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold mb-3 text-black">
|
||||
Competencias
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
Puedes agregar competencias nuevas sin necesidad de crear un nuevo
|
||||
curso. Las competencias se guardarán y podrás asociarlas a otros
|
||||
cursos después.
|
||||
</p>
|
||||
|
||||
{competenciasGuardadas.length > 0 && (
|
||||
<div className="mt-5 w-full flex-wrap">
|
||||
{competenciasGuardadas.map((competencia, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full flex justify-between items-center px-2 mb-2"
|
||||
>
|
||||
<span className="text-black">{competencia.descripcion}</span>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleDeleteCompetencia(index)}
|
||||
className="bg-red-400 hover:bg-red-500 text-white font-bold py-1 px-3 rounded-md"
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
{competencias.length > 0 && (
|
||||
<div className="mt-5 w-full flex-wrap">
|
||||
{competencias.map((c, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="w-full flex justify-between items-center px-2 mb-2"
|
||||
>
|
||||
<span className="text-black">{c.descripcion}</span>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleDeleteCompetencia(i)}
|
||||
className="bg-red-400 hover:bg-red-500 text-white font-bold py-1 px-3 rounded-md"
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{addCompetencia && (
|
||||
<div className="w-full flex flex-col md:flex-row mt-5">
|
||||
<div className="flex flex-col">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Nueva competencia"
|
||||
{...register("nuevaCompetencia")}
|
||||
className="w-80 px-3 py-2 border border-gray-300 rounded-md mb-3 text-black"
|
||||
/>
|
||||
{errors.nuevaCompetencia && (
|
||||
<p className="text-red-500 text-sm">
|
||||
{errors.nuevaCompetencia.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
{addCompetencia && (
|
||||
<div className="w-full flex flex-col md:flex-row mt-5">
|
||||
<div className="flex flex-col">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Nueva competencia"
|
||||
{...register("nuevaCompetencia")}
|
||||
className="w-80 px-3 py-2 border border-gray-300 rounded-md mb-3 text-black"
|
||||
/>
|
||||
{errors.nuevaCompetencia && (
|
||||
<p className="text-red-500 text-sm">
|
||||
{errors.nuevaCompetencia.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSaveCompetencia}
|
||||
className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-md mr-2"
|
||||
>
|
||||
Guardar
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setAddCompetencia(false);
|
||||
setValue("nuevaCompetencia", "");
|
||||
}}
|
||||
className="bg-gray-400 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded-md"
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setAddCompetencia(true)}
|
||||
className="w-full bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded-md mt-5"
|
||||
>
|
||||
Agregar competencia
|
||||
</Button>
|
||||
|
||||
<div className="flex justify-center w-full mt-5">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSaveCompetencia}
|
||||
className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-md mr-2"
|
||||
type="submit"
|
||||
className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-md"
|
||||
disabled={loading}
|
||||
>
|
||||
Guardar
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setAddCompetencia(false);
|
||||
setValue("nuevaCompetencia", "");
|
||||
}}
|
||||
className="bg-gray-400 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded-md"
|
||||
>
|
||||
Cancelar
|
||||
{loading ? "Guardando..." : "Guardar curso"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setAddCompetencia(true)}
|
||||
className="w-full bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded-md mt-5"
|
||||
>
|
||||
Agregar competencia
|
||||
</Button>
|
||||
|
||||
<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 curso"}
|
||||
</Button>
|
||||
<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>
|
||||
|
||||
<Dialog open={mostrarDialog} onOpenChange={setMostrarDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-black">Resultado</DialogTitle>
|
||||
<DialogDescription>{mensajeDialog}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setMostrarDialog(false)}>Cerrar</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={mostrarDialogCompetencia} onOpenChange={setMostrarDialogCompetencia}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-black">Competencia agregada</DialogTitle>
|
||||
<DialogDescription>
|
||||
¡La competencia fue agregada exitosamente!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setMostrarDialogCompetencia(false)}>Cerrar</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</form>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -39,7 +39,8 @@ export default function CursosVista() {
|
|||
const cargarCursos = async () => {
|
||||
const { data, error } = await supabaseClient
|
||||
.from("curso")
|
||||
.select(`
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
nombre,
|
||||
descripcion,
|
||||
|
@ -50,7 +51,8 @@ export default function CursosVista() {
|
|||
descripcion
|
||||
)
|
||||
)
|
||||
`)
|
||||
`
|
||||
)
|
||||
.order("id", { ascending: true });
|
||||
if (error) {
|
||||
console.error("Error al cargar cursos:", error.message);
|
||||
|
@ -90,7 +92,7 @@ export default function CursosVista() {
|
|||
// Guardar cambios en curso y competencias
|
||||
const guardarEdicion = async (id) => {
|
||||
// Validar que no haya competencias repetidas
|
||||
const ids = competenciasGuardadas.map(c => c?.id).filter(Boolean);
|
||||
const ids = competenciasGuardadas.map((c) => c?.id).filter(Boolean);
|
||||
const setIds = new Set(ids);
|
||||
if (ids.length !== setIds.size) {
|
||||
setModalMensaje("No puedes repetir competencias en un curso.");
|
||||
|
@ -109,15 +111,12 @@ export default function CursosVista() {
|
|||
|
||||
// Actualiza competencias (tabla pivote)
|
||||
// 1. Elimina todas las competencias actuales del curso
|
||||
await supabaseClient
|
||||
.from("curso_competencia")
|
||||
.delete()
|
||||
.eq("curso_id", id);
|
||||
await supabaseClient.from("curso_competencia").delete().eq("curso_id", id);
|
||||
|
||||
// 2. Inserta las nuevas competencias seleccionadas
|
||||
const competenciasAInsertar = competenciasGuardadas
|
||||
.filter(c => c && c.id)
|
||||
.map(c => ({
|
||||
.filter((c) => c && c.id)
|
||||
.map((c) => ({
|
||||
curso_id: id,
|
||||
competencia_id: c.id,
|
||||
}));
|
||||
|
@ -157,7 +156,9 @@ export default function CursosVista() {
|
|||
}
|
||||
|
||||
if (alumnosInscritos && alumnosInscritos.length > 0) {
|
||||
setModalMensaje("No se puede eliminar el curso porque hay alumnos inscritos a este curso.");
|
||||
setModalMensaje(
|
||||
"No se puede eliminar el curso porque hay alumnos inscritos a este curso."
|
||||
);
|
||||
setConfirmarEliminar(false);
|
||||
setMostrarModal(true);
|
||||
return;
|
||||
|
@ -192,150 +193,167 @@ export default function CursosVista() {
|
|||
};
|
||||
|
||||
const quitarCompetencia = () => {
|
||||
setCompetenciasGuardadas(competenciasGuardadas.filter((_, i) => i !== compAEliminar));
|
||||
setCompetenciasGuardadas(
|
||||
competenciasGuardadas.filter((_, i) => i !== compAEliminar)
|
||||
);
|
||||
setDialogQuitarComp(false);
|
||||
setCompAEliminar(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="w-[80vw] pt-10 flex flex-col items-center text-black">
|
||||
<div className="w-full pt-10 flex flex-col items-center text-black">
|
||||
<h1 className="text-2xl font-semibold mb-6">Lista de Cursos</h1>
|
||||
<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">Competencias</th>
|
||||
<th className="py-2 border-b">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cursos.map((curso) =>
|
||||
cursoEditando === curso.id ? (
|
||||
<tr key={curso.id}>
|
||||
<td className="py-2 px-4 border-b text-center">{curso.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">
|
||||
<div className="flex flex-col gap-2">
|
||||
{competenciasGuardadas.map((comp, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<select
|
||||
className="border rounded px-2 py-1"
|
||||
value={comp?.id || ""}
|
||||
onChange={e => {
|
||||
const nuevaLista = [...competenciasGuardadas];
|
||||
const nuevaComp = todasCompetencias.find(c => c.id === Number(e.target.value));
|
||||
nuevaLista[idx] = nuevaComp;
|
||||
setCompetenciasGuardadas(nuevaLista);
|
||||
}}
|
||||
>
|
||||
<option value="">Selecciona competencia</option>
|
||||
{todasCompetencias.map(tc => (
|
||||
<option
|
||||
key={tc.id}
|
||||
value={tc.id}
|
||||
disabled={
|
||||
// Deshabilita si ya está seleccionada en otro select
|
||||
competenciasGuardadas.some(
|
||||
(c, i) => c && c.id === tc.id && i !== idx
|
||||
)
|
||||
}
|
||||
>
|
||||
{tc.descripcion}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
type="button"
|
||||
className="bg-red-500 hover:bg-red-700 text-white px-2 py-1 rounded"
|
||||
onClick={() => pedirConfirmacionQuitarComp(idx)}
|
||||
>
|
||||
Quitar
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<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">Competencias</th>
|
||||
<th className="py-2 border-b">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cursos.map((curso) =>
|
||||
cursoEditando === curso.id ? (
|
||||
<tr key={curso.id}>
|
||||
<td className="py-2 px-4 border-b text-center">
|
||||
{curso.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">
|
||||
<div className="flex flex-col gap-2">
|
||||
{competenciasGuardadas.map((comp, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<select
|
||||
className="border rounded px-2 py-1"
|
||||
value={comp?.id || ""}
|
||||
onChange={(e) => {
|
||||
const nuevaLista = [...competenciasGuardadas];
|
||||
const nuevaComp = todasCompetencias.find(
|
||||
(c) => c.id === Number(e.target.value)
|
||||
);
|
||||
nuevaLista[idx] = nuevaComp;
|
||||
setCompetenciasGuardadas(nuevaLista);
|
||||
}}
|
||||
>
|
||||
<option value="">Selecciona competencia</option>
|
||||
{todasCompetencias.map((tc) => (
|
||||
<option
|
||||
key={tc.id}
|
||||
value={tc.id}
|
||||
disabled={
|
||||
// Deshabilita si ya está seleccionada en otro select
|
||||
competenciasGuardadas.some(
|
||||
(c, i) => c && c.id === tc.id && i !== idx
|
||||
)
|
||||
}
|
||||
>
|
||||
{tc.descripcion}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
type="button"
|
||||
className="bg-red-500 hover:bg-red-700 text-white px-2 py-1 rounded"
|
||||
onClick={() => pedirConfirmacionQuitarComp(idx)}
|
||||
>
|
||||
Quitar
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white px-2 py-1 rounded mt-2"
|
||||
onClick={() =>
|
||||
setCompetenciasGuardadas([
|
||||
...competenciasGuardadas,
|
||||
null,
|
||||
])
|
||||
}
|
||||
disabled={
|
||||
competenciasGuardadas.length >=
|
||||
todasCompetencias.length
|
||||
}
|
||||
>
|
||||
Agregar competencia
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 px-4 border-b flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white px-2 py-1 rounded mt-2"
|
||||
onClick={() => setCompetenciasGuardadas([...competenciasGuardadas, null])}
|
||||
disabled={competenciasGuardadas.length >= todasCompetencias.length}
|
||||
className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-3 m-2 rounded"
|
||||
onClick={() => guardarEdicion(curso.id)}
|
||||
>
|
||||
Agregar competencia
|
||||
Guardar
|
||||
</Button>
|
||||
</div>
|
||||
</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(curso.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={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">
|
||||
{Array.isArray(curso.competencias) && curso.competencias.length > 0
|
||||
? (
|
||||
<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={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">
|
||||
{Array.isArray(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>
|
||||
<td className="py-2 px-4 border-b space-x-2">
|
||||
<Button
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 rounded"
|
||||
onClick={() => iniciarEdicion(curso)}
|
||||
>
|
||||
Editar
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 rounded"
|
||||
onClick={() => confirmarEliminacion(curso.id)}
|
||||
>
|
||||
Eliminar
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
"Sin competencias"
|
||||
)}
|
||||
</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(curso)}
|
||||
>
|
||||
Editar
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 m-1 rounded"
|
||||
onClick={() => confirmarEliminacion(curso.id)}
|
||||
>
|
||||
Eliminar
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dialog para eliminar curso */}
|
||||
|
@ -371,9 +389,7 @@ export default function CursosVista() {
|
|||
<Dialog open={dialogQuitarComp} onOpenChange={setDialogQuitarComp}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-black">
|
||||
Quitar competencia
|
||||
</DialogTitle>
|
||||
<DialogTitle className="text-black">Quitar competencia</DialogTitle>
|
||||
<DialogDescription>
|
||||
¿Estás seguro de que deseas quitar esta competencia del curso?
|
||||
</DialogDescription>
|
||||
|
@ -411,4 +427,4 @@ export default function CursosVista() {
|
|||
</Dialog>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react";
|
|||
import Layout from "@/components/layout/Layout";
|
||||
import { supabaseClient } from "@/utils/supabase";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import CrearDiplomaDialog from "@/components/dialogs/crearDiplomaDialog";
|
||||
import VistaPreviaDiplomaDialog from "@/components/dialogs/vistaPreviaDiplomaDialog";
|
||||
|
||||
export default function DiplomasVista() {
|
||||
|
@ -53,60 +52,51 @@ export default function DiplomasVista() {
|
|||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="w-[80vw] pt-10 flex flex-col items-center text-black">
|
||||
<div className="w-full pt-10 flex flex-col items-center text-black">
|
||||
<h1 className="text-2xl font-semibold mb-6">Vista de Diplomas</h1>
|
||||
<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">Correo</th>
|
||||
<th className="py-2 border-b">Teléfono</th>
|
||||
<th className="py-2 border-b">Curso</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">{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">{alumno.telefono}</td>
|
||||
<td className="py-2 px-4 border-b">
|
||||
{alumno.curso?.nombre || "Sin curso"}
|
||||
</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={() => {
|
||||
setAlumnoSeleccionado(alumno);
|
||||
setMostrarDialog(true);
|
||||
}}
|
||||
>
|
||||
Crear Diploma
|
||||
</Button>
|
||||
</td>
|
||||
<div className="overflow-x-auto w-full">
|
||||
<table className="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">Correo</th>
|
||||
<th className="py-2 border-b">Teléfono</th>
|
||||
<th className="py-2 border-b">Curso</th>
|
||||
<th className="py-2 border-b">Acciones</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{alumnos.map((alumno) => (
|
||||
<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">{alumno.telefono}</td>
|
||||
<td className="py-2 px-4 border-b">
|
||||
{alumno.curso?.nombre || "Sin curso"}
|
||||
</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={() => {
|
||||
setAlumnoSeleccionado(alumno);
|
||||
setMostrarDialog(true);
|
||||
}}
|
||||
>
|
||||
Crear Diploma
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/* Dialog para crear diploma y vista previa juntos */}
|
||||
{mostrarDialog && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="flex gap-8 bg-black bg-opacity-30 p-8 rounded">
|
||||
{/*<CrearDiplomaDialog
|
||||
open={mostrarDialog}
|
||||
onOpenChange={handleCloseDialog}
|
||||
alumno={alumnoSeleccionado}
|
||||
competencias={competencias}
|
||||
setCompetencias={setCompetencias}
|
||||
competenciasAcreditadas={competenciasAcreditadas}
|
||||
setCompetenciasAcreditadas={setCompetenciasAcreditadas}
|
||||
fecha={fecha}
|
||||
setFecha={setFecha}
|
||||
/>*/}
|
||||
<VistaPreviaDiplomaDialog
|
||||
open={mostrarDialog}
|
||||
onOpenChange={handleCloseDialog}
|
||||
|
|
|
@ -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(),
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const mensajesSchema = z.object({
|
||||
correo: z.string().min(1, "El mensaje de correo es obligatorio"),
|
||||
whatsapp: z.string().min(1, "El mensaje de WhatsApp es obligatorio"),
|
||||
});
|
Loading…
Reference in New Issue