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 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({
|
const styles = StyleSheet.create({
|
||||||
page: { padding: 40, fontFamily: "Helvetica" },
|
page: { fontFamily: "Helvetica" },
|
||||||
title: { fontSize: 24, textAlign: "center", marginBottom: 20 },
|
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 },
|
competencias: { marginLeft: 20, marginTop: 5 },
|
||||||
competencia: { fontSize: 12, marginBottom: 2 },
|
competencia: { fontSize: 12, marginBottom: 2 },
|
||||||
footer: {
|
footer: {
|
||||||
|
@ -20,36 +39,21 @@ export default function Diploma({ alumno, curso, competencias = [], fecha }) {
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
<Page size="A4" style={styles.page}>
|
<Page size="A4" style={styles.page}>
|
||||||
<Text style={styles.title}>Diploma</Text>
|
<Image src="/encabezado.png" />
|
||||||
<View style={styles.section}>
|
<Text style={styles.title}>Otorga la presente</Text>
|
||||||
<Text>
|
<Text style={styles.title}>CONSTANCIA</Text>
|
||||||
<Text style={{ fontWeight: "bold" }}>Alumno: </Text>
|
<Text style={styles.title}>a: </Text>
|
||||||
{alumno?.nombre}
|
<Text style={styles.nombre}>{alumno?.nombre} </Text>
|
||||||
</Text>
|
<Text style={styles.title}>
|
||||||
</View>
|
Por su asistencia a la píldora educativa
|
||||||
<View style={styles.section}>
|
</Text>
|
||||||
<Text>
|
<Text style={styles.curso}>{curso?.nombre || "Sin curso"}</Text>
|
||||||
<Text style={{ fontWeight: "bold" }}>Curso: </Text>
|
<Text style={styles.title}>
|
||||||
{curso?.nombre || "Sin curso"}
|
con duración de 2 horas, modalidad remota
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
<Text style={styles.title}>
|
||||||
<View style={styles.section}>
|
Se expide en la ciudad de Xalapa, Ver., {fecha}
|
||||||
<Text style={{ fontWeight: "bold" }}>Competencias Acreditadas:</Text>
|
</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>
|
|
||||||
</Page>
|
</Page>
|
||||||
</Document>
|
</Document>
|
||||||
);
|
);
|
||||||
|
|
|
@ -36,6 +36,15 @@ const data = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
/*{
|
||||||
|
title: "Vista general",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Vista general",
|
||||||
|
url: "/vistaGeneral",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},*/
|
||||||
{
|
{
|
||||||
title: "Cursos",
|
title: "Cursos",
|
||||||
items: [
|
items: [
|
||||||
|
@ -53,6 +62,40 @@ const data = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Inyecciones",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Vista de inyecciones",
|
||||||
|
url: "/inyeccionesVista",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Agregar manualmente",
|
||||||
|
url: "/inyeccionesManual",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Agregar desde archivo",
|
||||||
|
url: "/inyeccionesArchivo",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Pildoras",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Vista de pildoras",
|
||||||
|
url: "/pildorasVista",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Agregar manualmente",
|
||||||
|
url: "/pildorasManual",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Agregar desde archivo",
|
||||||
|
url: "/pildorasArchivo",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Diplomas",
|
title: "Diplomas",
|
||||||
items: [
|
items: [
|
||||||
|
|
|
@ -0,0 +1,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 Diploma from "@/components/Diploma";
|
||||||
import { PDFDownloadLink, PDFViewer, pdf } from "@react-pdf/renderer";
|
import { PDFDownloadLink, PDFViewer, pdf } from "@react-pdf/renderer";
|
||||||
import { supabaseClient } from "@/utils/supabase";
|
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({
|
function VistaPreviaDiplomaDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
alumno,
|
alumno,
|
||||||
|
curso,
|
||||||
competencias: competenciasProp,
|
competencias: competenciasProp,
|
||||||
fecha,
|
fecha,
|
||||||
competenciasAcreditadas,
|
competenciasAcreditadas,
|
||||||
|
@ -20,8 +25,77 @@ function VistaPreviaDiplomaDialog({
|
||||||
const [mostrarVistaPrevia, setMostrarVistaPrevia] = useState(false);
|
const [mostrarVistaPrevia, setMostrarVistaPrevia] = useState(false);
|
||||||
const [enviando, setEnviando] = useState(false);
|
const [enviando, setEnviando] = useState(false);
|
||||||
const [mensaje, setMensaje] = useState("");
|
const [mensaje, setMensaje] = useState("");
|
||||||
|
const [loadingMensajes, setLoadingMensajes] = useState(false);
|
||||||
const [competencias, setCompetencias] = useState([]);
|
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(() => {
|
useEffect(() => {
|
||||||
if (alumno && alumno.curso?.id) {
|
if (alumno && alumno.curso?.id) {
|
||||||
supabaseClient
|
supabaseClient
|
||||||
|
@ -41,28 +115,25 @@ function VistaPreviaDiplomaDialog({
|
||||||
? competencias.filter((comp) => competenciasAcreditadas.includes(comp.id))
|
? competencias.filter((comp) => competenciasAcreditadas.includes(comp.id))
|
||||||
: competencias;
|
: competencias;
|
||||||
|
|
||||||
// Simulación de envío de PDF por correo y WhatsApp
|
|
||||||
const handleEnviar = async () => {
|
const handleEnviar = async () => {
|
||||||
setEnviando(true);
|
setEnviando(true);
|
||||||
setMensaje("");
|
setMensaje("");
|
||||||
// Genera el PDF como blob
|
|
||||||
const blob = await pdf(
|
const blob = await pdf(
|
||||||
<Diploma
|
<Diploma
|
||||||
alumno={alumno}
|
alumno={alumno}
|
||||||
curso={alumno.curso}
|
curso={curso}
|
||||||
competencias={competenciasMostradas}
|
competencias={competenciasMostradas}
|
||||||
fecha={fecha || new Date().toLocaleDateString()}
|
fecha={fecha || new Date().toLocaleDateString()}
|
||||||
/>
|
/>
|
||||||
).toBlob();
|
).toBlob();
|
||||||
|
|
||||||
// Convierte el blob a base64
|
|
||||||
const pdfBase64 = await new Promise((resolve) => {
|
const pdfBase64 = await new Promise((resolve) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => resolve(reader.result.split(",")[1]);
|
reader.onloadend = () => resolve(reader.result.split(",")[1]);
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Llama a tu API de Next.js
|
|
||||||
const resp = await fetch("/api/send-diploma", {
|
const resp = await fetch("/api/send-diploma", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
@ -71,32 +142,30 @@ function VistaPreviaDiplomaDialog({
|
||||||
nombre: alumno.nombre,
|
nombre: alumno.nombre,
|
||||||
curso: alumno.curso?.nombre || "Sin curso",
|
curso: alumno.curso?.nombre || "Sin curso",
|
||||||
pdfBase64,
|
pdfBase64,
|
||||||
|
mensajeCorreo: getValues("correo"),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
// WhatsApp real (abre ventana)
|
|
||||||
const telefono = alumno.telefono.replace(/\D/g, "");
|
const telefono = alumno.telefono.replace(/\D/g, "");
|
||||||
const mensajeWhatsapp = encodeURIComponent(
|
const mensajeWhatsapp = encodeURIComponent(getValues("whatsapp"));
|
||||||
`Hola ${alumno.nombre}, tu diploma ha sido generado y enviado a tu correo (${alumno.correo}). ¡Felicidades!`
|
|
||||||
);
|
|
||||||
window.open(
|
window.open(
|
||||||
`https://wa.me/${telefono}?text=${mensajeWhatsapp}`,
|
`https://wa.me/${telefono}?text=${mensajeWhatsapp}`,
|
||||||
"_blank"
|
"_blank"
|
||||||
);
|
);
|
||||||
|
setMensaje(`Diploma enviado por correo a ${alumno.correo}.`);
|
||||||
setMensaje(
|
|
||||||
`Diploma enviado por correo a ${alumno.correo} y mensaje enviado por WhatsApp al ${alumno.telefono}.`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setMensaje("Error enviando el diploma por correo.");
|
setMensaje("Error enviando el diploma.");
|
||||||
}
|
}
|
||||||
|
|
||||||
setEnviando(false);
|
setEnviando(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!alumno) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<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>
|
<DialogHeader>
|
||||||
<DialogTitle>Diploma</DialogTitle>
|
<DialogTitle>Diploma</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
@ -117,15 +186,35 @@ function VistaPreviaDiplomaDialog({
|
||||||
<div className="text-lg mb-2">
|
<div className="text-lg mb-2">
|
||||||
<b>Fecha:</b> {fecha || new Date().toLocaleDateString()}
|
<b>Fecha:</b> {fecha || new Date().toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-auto text-gray-400 text-xs text-right">
|
{/* Campos de mensaje */}
|
||||||
Vista previa
|
<form>
|
||||||
</div>
|
<div className="mb-4">
|
||||||
<div className="mt-4 flex gap-2 justify-center flex-wrap">
|
<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
|
<PDFDownloadLink
|
||||||
document={
|
document={
|
||||||
<Diploma
|
<Diploma
|
||||||
alumno={alumno}
|
alumno={alumno}
|
||||||
curso={alumno.curso}
|
curso={curso}
|
||||||
competencias={competenciasMostradas}
|
competencias={competenciasMostradas}
|
||||||
fecha={fecha || new Date().toLocaleDateString()}
|
fecha={fecha || new Date().toLocaleDateString()}
|
||||||
/>
|
/>
|
||||||
|
@ -144,12 +233,14 @@ function VistaPreviaDiplomaDialog({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</PDFDownloadLink>
|
</PDFDownloadLink>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2 rounded"
|
className="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2 rounded"
|
||||||
onClick={() => setMostrarVistaPrevia(true)}
|
onClick={() => setMostrarVistaPrevia(true)}
|
||||||
>
|
>
|
||||||
Ver vista previa PDF
|
Ver vista previa PDF
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="bg-purple-600 hover:bg-purple-800 text-white px-4 py-2 rounded"
|
className="bg-purple-600 hover:bg-purple-800 text-white px-4 py-2 rounded"
|
||||||
onClick={handleEnviar}
|
onClick={handleEnviar}
|
||||||
|
@ -157,20 +248,31 @@ function VistaPreviaDiplomaDialog({
|
||||||
>
|
>
|
||||||
{enviando ? "Enviando..." : "Enviar por correo y WhatsApp"}
|
{enviando ? "Enviando..." : "Enviar por correo y WhatsApp"}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
{mensaje && (
|
{mensaje && (
|
||||||
<div className="mt-4 text-green-700 font-semibold text-center">
|
<div className="mt-4 text-green-700 font-semibold text-center">
|
||||||
{mensaje}
|
{mensaje}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Vista previa PDF */}
|
||||||
{mostrarVistaPrevia && (
|
{mostrarVistaPrevia && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-60 flex flex-col items-center justify-center z-50">
|
<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 flex flex-col items-center">
|
<div className="bg-white rounded shadow-lg p-4">
|
||||||
<div className="w-[80vw] h-[80vh] lg:h-[90vh] mb-4 border">
|
<div className="w-[80vw] h-[90vh] mb-4 border">
|
||||||
<PDFViewer width="100%" height="100%">
|
<PDFViewer width="100%" height="100%">
|
||||||
<Diploma
|
<Diploma
|
||||||
alumno={alumno}
|
alumno={alumno}
|
||||||
curso={alumno.curso}
|
curso={curso}
|
||||||
competencias={competenciasMostradas}
|
competencias={competenciasMostradas}
|
||||||
fecha={fecha || new Date().toLocaleDateString()}
|
fecha={fecha || new Date().toLocaleDateString()}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,26 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AppSidebar } from "@/components/app-sidebar";
|
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 }) {
|
export default function Layout({ children }) {
|
||||||
return (
|
return (
|
||||||
|
@ -9,7 +28,8 @@ export default function Layout({ children }) {
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<div className="p-4 w-full">{children}</div>
|
<SidebarTrigger className="-ml-1 text-black" />
|
||||||
|
<MainContent>{children}</MainContent>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|
|
@ -238,7 +238,7 @@ function SidebarTrigger({ className, onClick, ...props }) {
|
||||||
data-slot="sidebar-trigger"
|
data-slot="sidebar-trigger"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn("size-7", className)}
|
className={cn("size-10", className)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
onClick?.(event);
|
onClick?.(event);
|
||||||
toggleSidebar();
|
toggleSidebar();
|
||||||
|
|
|
@ -3,9 +3,18 @@ import Papa from "papaparse";
|
||||||
import * as XLSX from "xlsx";
|
import * as XLSX from "xlsx";
|
||||||
import Layout from "@/components/layout/Layout";
|
import Layout from "@/components/layout/Layout";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
import {
|
||||||
import { CursosManualForm } from "./cursosManual"; // Importa el formulario sin Layout
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { CursosManualForm } from "@/components/cursosManualForm";
|
||||||
import { supabaseClient } from "@/utils/supabase";
|
import { supabaseClient } from "@/utils/supabase";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
export default function AlumnosArchivo() {
|
export default function AlumnosArchivo() {
|
||||||
const [archivo, setArchivo] = useState(null);
|
const [archivo, setArchivo] = useState(null);
|
||||||
|
@ -14,11 +23,32 @@ export default function AlumnosArchivo() {
|
||||||
const [mensajeDialogo, setMensajeDialogo] = useState("");
|
const [mensajeDialogo, setMensajeDialogo] = useState("");
|
||||||
const [mostrarDialogCurso, setMostrarDialogCurso] = useState(false);
|
const [mostrarDialogCurso, setMostrarDialogCurso] = useState(false);
|
||||||
const [cursoFaltante, setCursoFaltante] = useState("");
|
const [cursoFaltante, setCursoFaltante] = useState("");
|
||||||
|
const [dialogoAdvertencia, setDialogoAdvertencia] = useState(false);
|
||||||
|
const [rutaPendiente, setRutaPendiente] = useState(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (archivo) extraerContenido();
|
if (archivo) extraerContenido();
|
||||||
|
// eslint-disable-next-line
|
||||||
}, [archivo]);
|
}, [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 () => {
|
const registrarAlumnos = async () => {
|
||||||
if (datos.length === 0) return;
|
if (datos.length === 0) return;
|
||||||
|
|
||||||
|
@ -26,11 +56,12 @@ export default function AlumnosArchivo() {
|
||||||
|
|
||||||
for (const alumno of datos) {
|
for (const alumno of datos) {
|
||||||
// 1. Verifica si el curso existe
|
// 1. Verifica si el curso existe
|
||||||
const { data: cursosEncontrados, error: errorCurso } = await supabaseClient
|
const { data: cursosEncontrados, error: errorCurso } =
|
||||||
.from("curso")
|
await supabaseClient
|
||||||
.select("id")
|
.from("curso")
|
||||||
.eq("nombre", alumno.nombreCurso)
|
.select("id")
|
||||||
.maybeSingle();
|
.eq("nombre", alumno.nombreCurso)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
if (errorCurso) {
|
if (errorCurso) {
|
||||||
errores.push({ alumno, error: "Error al buscar el curso" });
|
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
|
// Si no existe el curso, muestra el dialog para registrar el curso
|
||||||
setCursoFaltante(alumno.nombreCurso);
|
setCursoFaltante(alumno.nombreCurso);
|
||||||
setMostrarDialogCurso(true);
|
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);
|
setDialogoAbierto(true);
|
||||||
return; // Detiene el registro de alumnos
|
return; // Detiene el registro de alumnos
|
||||||
}
|
}
|
||||||
|
@ -54,7 +87,7 @@ export default function AlumnosArchivo() {
|
||||||
nombre: alumno.nombre,
|
nombre: alumno.nombre,
|
||||||
correo: alumno.correo,
|
correo: alumno.correo,
|
||||||
telefono: alumno.telefono,
|
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) {
|
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 {
|
} else {
|
||||||
setMensajeDialogo("Todos los alumnos fueron registrados correctamente.");
|
setMensajeDialogo("Todos los alumnos fueron registrados correctamente.");
|
||||||
setArchivo(null);
|
setArchivo(null);
|
||||||
|
@ -130,8 +165,8 @@ export default function AlumnosArchivo() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="w-[60vw] pt-10 flex flex-col items-end justify-center text-black">
|
<div className="w-full pt-10 flex flex-col items-start md:items-center md:justify-center text-black">
|
||||||
<div className="bg-white p-8 font-sans text-center w-[70%] flex flex-col items-center">
|
<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">
|
<h1 className="text-xl font-semibold mb-4 text-black">
|
||||||
Nuevo alumno
|
Nuevo alumno
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -144,7 +179,9 @@ export default function AlumnosArchivo() {
|
||||||
{archivo ? (
|
{archivo ? (
|
||||||
<span className="text-black font-medium">{archivo.name}</span>
|
<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
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
@ -169,7 +206,9 @@ export default function AlumnosArchivo() {
|
||||||
<thead className="bg-gray-100 text-gray-700">
|
<thead className="bg-gray-100 text-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
{Object.keys(datos[0]).map((columna, index) => (
|
{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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -177,7 +216,9 @@ export default function AlumnosArchivo() {
|
||||||
{datos.map((fila, index) => (
|
{datos.map((fila, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
{Object.values(fila).map((valor, i) => (
|
{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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
@ -196,7 +237,8 @@ export default function AlumnosArchivo() {
|
||||||
Registrar curso faltante
|
Registrar curso faltante
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<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>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<CursosManualForm nombreSugerido={cursoFaltante} />
|
<CursosManualForm nombreSugerido={cursoFaltante} />
|
||||||
|
@ -215,6 +257,38 @@ export default function AlumnosArchivo() {
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,80 +84,66 @@ export default function AlumnosManual() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="w-[60vw] pt-10 flex flex-col items-end justify-center text-black">
|
<div className="w-full bg-white font-sans text-center md:w-[80%] pt-10 flex flex-col items-center justify-start text-black">
|
||||||
<div className="bg-white p-8 font-sans text-center w-[70%]">
|
<h1 className="text-xl font-semibold mb-10 text-black my-10">
|
||||||
<h1 className="text-xl font-semibold mb-4 text-black">
|
Nuevo alumno
|
||||||
Nuevo alumno
|
</h1>
|
||||||
</h1>
|
<form onSubmit={handleSubmit(manejarGuardar)} className="w-full">
|
||||||
<form onSubmit={handleSubmit(manejarGuardar)}>
|
<Input
|
||||||
<div className="mb-3">
|
type="text"
|
||||||
<Input
|
placeholder="Nombre"
|
||||||
type="text"
|
{...register("nombre")}
|
||||||
placeholder="Nombre"
|
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3"
|
||||||
{...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>
|
||||||
{errors.nombre && (
|
)}
|
||||||
<p className="text-red-500 text-sm mt-1">
|
<Input
|
||||||
{errors.nombre.message}
|
type="text"
|
||||||
</p>
|
placeholder="Email"
|
||||||
)}
|
{...register("correo")}
|
||||||
</div>
|
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3"
|
||||||
<div className="mb-3">
|
/>
|
||||||
<Input
|
{errors.correo && (
|
||||||
type="text"
|
<p className="text-red-500 text-sm mt-1">{errors.correo.message}</p>
|
||||||
placeholder="Email"
|
)}
|
||||||
{...register("correo")}
|
<Input
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
type="text"
|
||||||
/>
|
placeholder="Teléfono"
|
||||||
{errors.correo && (
|
{...register("telefono")}
|
||||||
<p className="text-red-500 text-sm mt-1">
|
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3"
|
||||||
{errors.correo.message}
|
/>
|
||||||
</p>
|
{errors.telefono && (
|
||||||
)}
|
<p className="text-red-500 text-sm mt-1">
|
||||||
</div>
|
{errors.telefono.message}
|
||||||
<div className="mb-3">
|
</p>
|
||||||
<Input
|
)}
|
||||||
type="text"
|
<Select
|
||||||
placeholder="Teléfono"
|
onValueChange={(value) => setValue("cursoSeleccionado", value)}
|
||||||
{...register("telefono")}
|
>
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
<SelectTrigger className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3">
|
||||||
/>
|
<SelectValue placeholder="Selecciona un curso" />
|
||||||
{errors.telefono && (
|
</SelectTrigger>
|
||||||
<p className="text-red-500 text-sm mt-1">
|
<SelectContent>
|
||||||
{errors.telefono.message}
|
{cursos.map((curso) => (
|
||||||
</p>
|
<SelectItem key={curso.id} value={curso.id.toString()}>
|
||||||
)}
|
{curso.nombre}
|
||||||
</div>
|
</SelectItem>
|
||||||
<div className="mb-4">
|
))}
|
||||||
<Select
|
</SelectContent>
|
||||||
onValueChange={(value) => setValue("cursoSeleccionado", value)}
|
</Select>
|
||||||
>
|
{errors.cursoSeleccionado && (
|
||||||
<SelectTrigger className="w-full px-3 py-2 border border-gray-300 rounded-md">
|
<p className="text-red-500 text-sm mt-1">
|
||||||
<SelectValue placeholder="Selecciona un curso" />
|
{errors.cursoSeleccionado.message}
|
||||||
</SelectTrigger>
|
</p>
|
||||||
<SelectContent>
|
)}
|
||||||
{cursos.map((curso) => (
|
<Button
|
||||||
<SelectItem key={curso.id} value={curso.id.toString()}>
|
type="submit"
|
||||||
{curso.nombre}
|
className="bg-green-400 hover:bg-green-500 font-bold py-2 px-4 rounded-md text-white"
|
||||||
</SelectItem>
|
>
|
||||||
))}
|
Registrar
|
||||||
</SelectContent>
|
</Button>
|
||||||
</Select>
|
</form>
|
||||||
{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>
|
</div>
|
||||||
|
|
||||||
{/* Diálogo de confirmación */}
|
{/* Diálogo de confirmación */}
|
||||||
|
|
|
@ -142,124 +142,126 @@ export default function AlumnosVista() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="w-[80vw] pt-10 flex flex-col items-center text-black">
|
<div className="w-full pt-5 flex flex-col items-center text-black">
|
||||||
<h1 className="text-2xl font-semibold mb-6 text-black">
|
<h1 className="text-2xl font-semibold mt-5 mb-10 text-black">
|
||||||
Lista de Alumnos
|
Lista de Alumnos
|
||||||
</h1>
|
</h1>
|
||||||
<table className="min-w-full bg-white border">
|
<div className="overflow-x-auto w-full">
|
||||||
<thead>
|
<table className="min-w-full bg-white border">
|
||||||
<tr className="bg-gray-100">
|
<thead>
|
||||||
<th className="py-2 border-b">ID</th>
|
<tr className="bg-gray-100">
|
||||||
<th className="py-2 border-b">Nombre</th>
|
<th className="py-2 border-b">ID</th>
|
||||||
<th className="py-2 border-b">Correo</th>
|
<th className="py-2 border-b">Nombre</th>
|
||||||
<th className="py-2 border-b">Teléfono</th>
|
<th className="py-2 border-b">Correo</th>
|
||||||
<th className="py-2 border-b">Curso</th>
|
<th className="py-2 border-b">Teléfono</th>
|
||||||
<th className="py-2 border-b">Acciones</th>
|
<th className="py-2 border-b">Curso</th>
|
||||||
</tr>
|
<th className="py-2 border-b">Acciones</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{alumnos.map((alumno) =>
|
<tbody>
|
||||||
alumnoEditando === alumno.id ? (
|
{alumnos.map((alumno) =>
|
||||||
<tr key={alumno.id}>
|
alumnoEditando === alumno.id ? (
|
||||||
<td className="py-2 px-4 border-b text-center">
|
<tr key={alumno.id}>
|
||||||
{alumno.id}
|
<td className="py-2 px-4 border-b text-center">
|
||||||
</td>
|
{alumno.id}
|
||||||
<td className="py-2 px-4 border-b">
|
</td>
|
||||||
<Input type="text" {...register("nombre")} />
|
<td className="py-2 px-4 border-b">
|
||||||
{errors.nombre && (
|
<Input type="text" {...register("nombre")} />
|
||||||
<span className="text-red-500 text-xs">
|
{errors.nombre && (
|
||||||
{errors.nombre.message}
|
<span className="text-red-500 text-xs">
|
||||||
</span>
|
{errors.nombre.message}
|
||||||
)}
|
</span>
|
||||||
</td>
|
)}
|
||||||
<td className="py-2 px-4 border-b">
|
</td>
|
||||||
<Input type="email" {...register("correo")} />
|
<td className="py-2 px-4 border-b">
|
||||||
{errors.correo && (
|
<Input type="email" {...register("correo")} />
|
||||||
<span className="text-red-500 text-xs">
|
{errors.correo && (
|
||||||
{errors.correo.message}
|
<span className="text-red-500 text-xs">
|
||||||
</span>
|
{errors.correo.message}
|
||||||
)}
|
</span>
|
||||||
</td>
|
)}
|
||||||
<td className="py-2 px-4 border-b">
|
</td>
|
||||||
<Input type="text" {...register("telefono")} />
|
<td className="py-2 px-4 border-b">
|
||||||
{errors.telefono && (
|
<Input type="text" {...register("telefono")} />
|
||||||
<span className="text-red-500 text-xs">
|
{errors.telefono && (
|
||||||
{errors.telefono.message}
|
<span className="text-red-500 text-xs">
|
||||||
</span>
|
{errors.telefono.message}
|
||||||
)}
|
</span>
|
||||||
</td>
|
)}
|
||||||
<td className="py-2 px-4 border-b">
|
</td>
|
||||||
<Select
|
<td className="py-2 px-4 border-b">
|
||||||
value={undefined}
|
<Select
|
||||||
onValueChange={(value) =>
|
value={(alumno.curso_id || "").toString()}
|
||||||
setValue("cursoSeleccionado", value)
|
onValueChange={(value) =>
|
||||||
}
|
setValue("cursoSeleccionado", value)
|
||||||
{...register("cursoSeleccionado")}
|
}
|
||||||
>
|
{...register("cursoSeleccionado")}
|
||||||
<SelectTrigger>
|
>
|
||||||
<SelectValue placeholder="Selecciona un curso" />
|
<SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Selecciona un curso" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{cursos.map((curso) => (
|
<SelectContent>
|
||||||
<SelectItem
|
{cursos.map((curso) => (
|
||||||
key={curso.id}
|
<SelectItem
|
||||||
value={curso.id.toString()}
|
key={curso.id}
|
||||||
>
|
value={curso.id.toString()}
|
||||||
{curso.nombre}
|
>
|
||||||
</SelectItem>
|
{curso.nombre}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
{errors.cursoSeleccionado && (
|
</Select>
|
||||||
<span className="text-red-500 text-xs">
|
{errors.cursoSeleccionado && (
|
||||||
{errors.cursoSeleccionado.message}
|
<span className="text-red-500 text-xs">
|
||||||
</span>
|
{errors.cursoSeleccionado.message}
|
||||||
)}
|
</span>
|
||||||
</td>
|
)}
|
||||||
<td className="py-2 px-4 border-b flex justify-center">
|
</td>
|
||||||
<Button
|
<td className="py-2 px-4 border-b flex justify-center">
|
||||||
className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-3 m-2 rounded"
|
<Button
|
||||||
onClick={handleSubmit(guardarEdicion)}
|
className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-3 m-1 rounded"
|
||||||
>
|
onClick={handleSubmit(guardarEdicion)}
|
||||||
Guardar
|
>
|
||||||
</Button>
|
Guardar
|
||||||
<Button
|
</Button>
|
||||||
className="bg-gray-400 hover:bg-gray-600 text-white font-bold py-1 px-3 m-2 rounded"
|
<Button
|
||||||
onClick={cancelarEdicion}
|
className="bg-gray-400 hover:bg-gray-600 text-white font-bold py-1 px-3 m-1 rounded"
|
||||||
>
|
onClick={cancelarEdicion}
|
||||||
Cancelar
|
>
|
||||||
</Button>
|
Cancelar
|
||||||
</td>
|
</Button>
|
||||||
</tr>
|
</td>
|
||||||
) : (
|
</tr>
|
||||||
<tr key={alumno.id}>
|
) : (
|
||||||
<td className="py-2 px-4 border-b">{alumno.id}</td>
|
<tr key={alumno.id}>
|
||||||
<td className="py-2 px-4 border-b">{alumno.nombre}</td>
|
<td className="py-2 px-4 border-b">{alumno.id}</td>
|
||||||
<td className="py-2 px-4 border-b">{alumno.correo}</td>
|
<td className="py-2 px-4 border-b">{alumno.nombre}</td>
|
||||||
<td className="py-2 px-4 border-b">{alumno.telefono}</td>
|
<td className="py-2 px-4 border-b">{alumno.correo}</td>
|
||||||
<td className="py-2 px-4 border-b">
|
<td className="py-2 px-4 border-b">{alumno.telefono}</td>
|
||||||
{alumno.curso?.nombre || "Sin curso"}
|
<td className="py-2 px-4 border-b">
|
||||||
</td>
|
{alumno.curso?.nombre || "Sin curso"}
|
||||||
<td className="py-2 px-4 border-b space-x-2">
|
</td>
|
||||||
<Button
|
<td className="py-2 px-4 border-b flex justify-center">
|
||||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 rounded"
|
<Button
|
||||||
onClick={() => iniciarEdicion(alumno)}
|
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 m-1 rounded"
|
||||||
>
|
onClick={() => iniciarEdicion(alumno)}
|
||||||
Editar
|
>
|
||||||
</Button>
|
Editar
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 rounded"
|
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 m-1 rounded"
|
||||||
onClick={() => confirmarEliminacion(alumno.id)}
|
onClick={() => confirmarEliminacion(alumno.id)}
|
||||||
>
|
>
|
||||||
Eliminar
|
Eliminar
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal de confirmación */}
|
{/* 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) {
|
export default async function handler(req, res) {
|
||||||
if (req.method !== "POST") return res.status(405).end();
|
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 {
|
try {
|
||||||
await sgMail.send({
|
await sgMail.send({
|
||||||
to: email,
|
to: email,
|
||||||
from: "rviverosgonzalez@outlook.com", // Cambia esto por tu correo verificado en SendGrid
|
from: "rviverosgonzalez@outlook.com", // Cambia esto por tu correo verificado en SendGrid
|
||||||
subject: "Tu diploma",
|
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: [
|
attachments: [
|
||||||
{
|
{
|
||||||
content: pdfBase64,
|
content: pdfBase64,
|
||||||
|
|
|
@ -3,7 +3,15 @@ import Papa from "papaparse";
|
||||||
import * as XLSX from "xlsx";
|
import * as XLSX from "xlsx";
|
||||||
import Layout from "@/components/layout/Layout";
|
import Layout from "@/components/layout/Layout";
|
||||||
import { Button } from "@/components/ui/button";
|
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";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
export default function cursosArchivo() {
|
export default function cursosArchivo() {
|
||||||
|
@ -53,7 +61,7 @@ export default function cursosArchivo() {
|
||||||
horas: curso.horas,
|
horas: curso.horas,
|
||||||
descripcion: curso.descripcion,
|
descripcion: curso.descripcion,
|
||||||
competencias: curso.competencias
|
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
|
setDialogoCargando(false); // Ocultar dialogo de carga
|
||||||
|
|
||||||
if (errores.length > 0) {
|
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 {
|
} else {
|
||||||
setMensajeDialogo("Todos los cursos fueron registrados correctamente.");
|
setMensajeDialogo("Todos los cursos fueron registrados correctamente.");
|
||||||
setArchivo(null);
|
setArchivo(null);
|
||||||
|
@ -132,11 +142,9 @@ export default function cursosArchivo() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="w-[60vw] pt-10 flex flex-col items-end justify-center text-black">
|
<div className="w-full pt-10 flex flex-col items-start md:items-center md:justify-center text-black">
|
||||||
<div className="bg-white p-8 font-sans text-center w-[70%] flex flex-col items-center">
|
<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">
|
<h1 className="text-xl font-semibold mb-4 text-black">Nuevo curso</h1>
|
||||||
Nuevo curso
|
|
||||||
</h1>
|
|
||||||
<label
|
<label
|
||||||
htmlFor="archivo"
|
htmlFor="archivo"
|
||||||
onDrop={manejarSoltar}
|
onDrop={manejarSoltar}
|
||||||
|
@ -146,7 +154,9 @@ export default function cursosArchivo() {
|
||||||
{archivo ? (
|
{archivo ? (
|
||||||
<span className="text-black font-medium">{archivo.name}</span>
|
<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
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
@ -171,7 +181,9 @@ export default function cursosArchivo() {
|
||||||
<thead className="bg-gray-100 text-gray-700">
|
<thead className="bg-gray-100 text-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
{Object.keys(datos[0]).map((columna, index) => (
|
{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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -179,7 +191,9 @@ export default function cursosArchivo() {
|
||||||
{datos.map((fila, index) => (
|
{datos.map((fila, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
{Object.values(fila).map((valor, i) => (
|
{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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
@ -216,7 +230,8 @@ export default function cursosArchivo() {
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-black">Advertencia</DialogTitle>
|
<DialogTitle className="text-black">Advertencia</DialogTitle>
|
||||||
<DialogDescription>
|
<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>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
@ -242,4 +257,4 @@ export default function cursosArchivo() {
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Layout from "@/components/layout/Layout";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { supabaseClient } from "@/utils/supabase"; // Importar el cliente de Supabase
|
import { supabaseClient } from "@/utils/supabase";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
@ -17,11 +17,11 @@ import {
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
export default function CursosManual() {
|
export default function CursosManual() {
|
||||||
const [addCompetencia, setAddCompetencia] = useState(false);
|
const [competencias, setCompetencias] = useState([]); // [{id, descripcion}]
|
||||||
const [competenciasGuardadas, setCompetenciasGuardadas] = useState([]);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
const [dialogMsg, setDialogMsg] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [mostrarDialog, setMostrarDialog] = useState(false);
|
const [addCompetencia, setAddCompetencia] = useState(false);
|
||||||
const [mensajeDialog, setMensajeDialog] = useState("");
|
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(cursosSchema),
|
resolver: zodResolver(cursosSchema),
|
||||||
|
@ -32,7 +32,6 @@ export default function CursosManual() {
|
||||||
nuevaCompetencia: "",
|
nuevaCompetencia: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
@ -41,348 +40,211 @@ export default function CursosManual() {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const handleAddCompetencia = () => {
|
// Añadir competencia (busca o crea en BD)
|
||||||
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
|
|
||||||
const handleSaveCompetencia = async (e) => {
|
const handleSaveCompetencia = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const nuevaCompetencia = getValues("nuevaCompetencia").trim();
|
const desc = getValues("nuevaCompetencia").trim();
|
||||||
if (!nuevaCompetencia) return;
|
if (!desc) return;
|
||||||
|
if (competencias.some((c) => c.descripcion === desc)) {
|
||||||
// Verifica si ya existe en el estado
|
setDialogMsg("La competencia ya fue agregada.");
|
||||||
if (competenciasGuardadas.some((c) => c.descripcion === nuevaCompetencia)) {
|
setShowDialog(true);
|
||||||
alert("La competencia ya fue agregada.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifica si ya existe en la base de datos
|
|
||||||
let competenciaId = null;
|
|
||||||
try {
|
try {
|
||||||
// Busca si ya existe
|
let { data: existente } = await supabaseClient
|
||||||
const { data: existente } = await supabaseClient
|
|
||||||
.from("competencia")
|
.from("competencia")
|
||||||
.select("id")
|
.select("id")
|
||||||
.eq("descripcion", nuevaCompetencia)
|
.eq("descripcion", desc)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
let id = existente?.id;
|
||||||
if (existente && existente.id) {
|
if (!id) {
|
||||||
competenciaId = existente.id;
|
|
||||||
} else {
|
|
||||||
// Si no existe, la crea
|
|
||||||
const { data: insertada, error } = await supabaseClient
|
const { data: insertada, error } = await supabaseClient
|
||||||
.from("competencia")
|
.from("competencia")
|
||||||
.insert([{ descripcion: nuevaCompetencia }])
|
.insert([{ descripcion: desc }])
|
||||||
.select("id")
|
.select("id")
|
||||||
.single();
|
.single();
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
competenciaId = insertada.id;
|
id = insertada.id;
|
||||||
}
|
}
|
||||||
|
setCompetencias([...competencias, { id, descripcion: desc }]);
|
||||||
setCompetenciasGuardadas([
|
|
||||||
...competenciasGuardadas,
|
|
||||||
{ id: competenciaId, descripcion: nuevaCompetencia },
|
|
||||||
]);
|
|
||||||
setAddCompetencia(false);
|
setAddCompetencia(false);
|
||||||
setValue("nuevaCompetencia", "");
|
setValue("nuevaCompetencia", "");
|
||||||
setMostrarDialogCompetencia(true); // Mostrar dialog de éxito
|
setDialogMsg("¡La competencia fue agregada exitosamente!");
|
||||||
|
setShowDialog(true);
|
||||||
} catch (err) {
|
} 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) => {
|
const handleDeleteCompetencia = (index) => {
|
||||||
setCompetenciasGuardadas(
|
setCompetencias(competencias.filter((_, i) => i !== index));
|
||||||
competenciasGuardadas.filter((_, i) => i !== index)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Guardar curso y asociar competencias
|
// Guardar curso y asociar competencias
|
||||||
const onSubmit = async (data) => {
|
const onSubmit = async (data) => {
|
||||||
const { nombre, descripcion } = data;
|
|
||||||
const horas = parseInt(data.horas, 10);
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Inserta el curso
|
const { nombre, descripcion, horas } = data;
|
||||||
const { data: cursoInsertado, error: errorCurso } = await supabaseClient
|
const { data: curso, error: errorCurso } = await supabaseClient
|
||||||
.from("curso")
|
.from("curso")
|
||||||
.insert([{ nombre, descripcion, horas }])
|
.insert([{ nombre, descripcion, horas: parseInt(horas, 10) }])
|
||||||
.select("id")
|
.select("id")
|
||||||
.single();
|
.single();
|
||||||
|
if (errorCurso) throw errorCurso;
|
||||||
if (errorCurso) {
|
if (competencias.length) {
|
||||||
setMensajeDialog("Error al guardar el curso: " + errorCurso.message);
|
const relaciones = competencias.map((c) => ({
|
||||||
setMostrarDialog(true);
|
curso_id: curso.id,
|
||||||
setLoading(false);
|
competencia_id: c.id,
|
||||||
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) {
|
|
||||||
const { error: errorPivote } = await supabaseClient
|
const { error: errorPivote } = await supabaseClient
|
||||||
.from("curso_competencia")
|
.from("curso_competencia")
|
||||||
.insert(relaciones);
|
.insert(relaciones);
|
||||||
if (errorPivote) {
|
if (errorPivote) throw errorPivote;
|
||||||
setMensajeDialog("Error al asociar competencias: " + errorPivote.message);
|
|
||||||
setMostrarDialog(true);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
setDialogMsg("Curso guardado exitosamente");
|
||||||
setMensajeDialog("Curso guardado exitosamente");
|
setCompetencias([]);
|
||||||
setMostrarDialog(true);
|
|
||||||
form.reset();
|
form.reset();
|
||||||
setCompetenciasGuardadas([]);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Ocurrió un error inesperado");
|
setDialogMsg("Error: " + (err.message || err));
|
||||||
} finally {
|
} finally {
|
||||||
|
setShowDialog(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
|
<Layout>
|
||||||
<Input
|
<div className="w-full bg-white pt-5 font-sans text-center md:w-[80%] flex flex-col items-center justify-start text-black">
|
||||||
type="text"
|
<h1 className="text-xl font-semibold mb-10 text-black">Nuevo curso</h1>
|
||||||
placeholder="Nombre del curso"
|
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
|
||||||
{...register("nombre")}
|
<Input
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 text-black"
|
type="text"
|
||||||
/>
|
placeholder="Nombre del curso"
|
||||||
{errors.nombre && (
|
{...register("nombre")}
|
||||||
<p className="text-red-500 text-sm">{errors.nombre.message}</p>
|
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
|
<Textarea
|
||||||
placeholder="Descripción"
|
placeholder="Descripción"
|
||||||
{...register("descripcion")}
|
{...register("descripcion")}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 h-24 text-black"
|
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 h-24 text-black"
|
||||||
/>
|
/>
|
||||||
{errors.descripcion && (
|
{errors.descripcion && (
|
||||||
<p className="text-red-500 text-sm">{errors.descripcion.message}</p>
|
<p className="text-red-500 text-sm">{errors.descripcion.message}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Horas del curso"
|
placeholder="Horas del curso"
|
||||||
{...register("horas", { valueAsNumber: true })}
|
{...register("horas", { valueAsNumber: true })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 text-black"
|
className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3 text-black"
|
||||||
/>
|
/>
|
||||||
{errors.horas && (
|
{errors.horas && (
|
||||||
<p className="text-red-500 text-sm">{errors.horas.message}</p>
|
<p className="text-red-500 text-sm">{errors.horas.message}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<h2 className="text-lg font-semibold mb-3 text-black">Competencias</h2>
|
<h2 className="text-lg font-semibold mb-3 text-black">
|
||||||
<p className="text-xs text-gray-500 mb-2">
|
Competencias
|
||||||
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.
|
</h2>
|
||||||
</p>
|
<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 && (
|
{competencias.length > 0 && (
|
||||||
<div className="mt-5 w-full flex-wrap">
|
<div className="mt-5 w-full flex-wrap">
|
||||||
{competenciasGuardadas.map((competencia, index) => (
|
{competencias.map((c, i) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={i}
|
||||||
className="w-full flex justify-between items-center px-2 mb-2"
|
className="w-full flex justify-between items-center px-2 mb-2"
|
||||||
>
|
>
|
||||||
<span className="text-black">{competencia.descripcion}</span>
|
<span className="text-black">{c.descripcion}</span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleDeleteCompetencia(index)}
|
onClick={() => handleDeleteCompetencia(i)}
|
||||||
className="bg-red-400 hover:bg-red-500 text-white font-bold py-1 px-3 rounded-md"
|
className="bg-red-400 hover:bg-red-500 text-white font-bold py-1 px-3 rounded-md"
|
||||||
>
|
>
|
||||||
X
|
X
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{addCompetencia && (
|
{addCompetencia && (
|
||||||
<div className="w-full flex flex-col md:flex-row mt-5">
|
<div className="w-full flex flex-col md:flex-row mt-5">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Nueva competencia"
|
placeholder="Nueva competencia"
|
||||||
{...register("nuevaCompetencia")}
|
{...register("nuevaCompetencia")}
|
||||||
className="w-80 px-3 py-2 border border-gray-300 rounded-md mb-3 text-black"
|
className="w-80 px-3 py-2 border border-gray-300 rounded-md mb-3 text-black"
|
||||||
/>
|
/>
|
||||||
{errors.nuevaCompetencia && (
|
{errors.nuevaCompetencia && (
|
||||||
<p className="text-red-500 text-sm">
|
<p className="text-red-500 text-sm">
|
||||||
{errors.nuevaCompetencia.message}
|
{errors.nuevaCompetencia.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row">
|
<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
|
<Button
|
||||||
type="button"
|
type="submit"
|
||||||
onClick={handleSaveCompetencia}
|
className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-md"
|
||||||
className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-md mr-2"
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Guardar
|
{loading ? "Guardando..." : "Guardar curso"}
|
||||||
</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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
||||||
type="button"
|
<DialogContent>
|
||||||
onClick={() => setAddCompetencia(true)}
|
<DialogHeader>
|
||||||
className="w-full bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded-md mt-5"
|
<DialogTitle className="text-black">Resultado</DialogTitle>
|
||||||
>
|
<DialogDescription>{dialogMsg}</DialogDescription>
|
||||||
Agregar competencia
|
</DialogHeader>
|
||||||
</Button>
|
<DialogFooter>
|
||||||
|
<Button onClick={() => setShowDialog(false)}>Cerrar</Button>
|
||||||
<div className="flex justify-center w-full mt-5">
|
</DialogFooter>
|
||||||
<Button
|
</DialogContent>
|
||||||
type="submit"
|
</Dialog>
|
||||||
className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-md"
|
</form>
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? "Guardando..." : "Guardar curso"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Layout>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,8 @@ export default function CursosVista() {
|
||||||
const cargarCursos = async () => {
|
const cargarCursos = async () => {
|
||||||
const { data, error } = await supabaseClient
|
const { data, error } = await supabaseClient
|
||||||
.from("curso")
|
.from("curso")
|
||||||
.select(`
|
.select(
|
||||||
|
`
|
||||||
id,
|
id,
|
||||||
nombre,
|
nombre,
|
||||||
descripcion,
|
descripcion,
|
||||||
|
@ -50,7 +51,8 @@ export default function CursosVista() {
|
||||||
descripcion
|
descripcion
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
`)
|
`
|
||||||
|
)
|
||||||
.order("id", { ascending: true });
|
.order("id", { ascending: true });
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("Error al cargar cursos:", error.message);
|
console.error("Error al cargar cursos:", error.message);
|
||||||
|
@ -90,7 +92,7 @@ export default function CursosVista() {
|
||||||
// Guardar cambios en curso y competencias
|
// Guardar cambios en curso y competencias
|
||||||
const guardarEdicion = async (id) => {
|
const guardarEdicion = async (id) => {
|
||||||
// Validar que no haya competencias repetidas
|
// 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);
|
const setIds = new Set(ids);
|
||||||
if (ids.length !== setIds.size) {
|
if (ids.length !== setIds.size) {
|
||||||
setModalMensaje("No puedes repetir competencias en un curso.");
|
setModalMensaje("No puedes repetir competencias en un curso.");
|
||||||
|
@ -109,15 +111,12 @@ export default function CursosVista() {
|
||||||
|
|
||||||
// Actualiza competencias (tabla pivote)
|
// Actualiza competencias (tabla pivote)
|
||||||
// 1. Elimina todas las competencias actuales del curso
|
// 1. Elimina todas las competencias actuales del curso
|
||||||
await supabaseClient
|
await supabaseClient.from("curso_competencia").delete().eq("curso_id", id);
|
||||||
.from("curso_competencia")
|
|
||||||
.delete()
|
|
||||||
.eq("curso_id", id);
|
|
||||||
|
|
||||||
// 2. Inserta las nuevas competencias seleccionadas
|
// 2. Inserta las nuevas competencias seleccionadas
|
||||||
const competenciasAInsertar = competenciasGuardadas
|
const competenciasAInsertar = competenciasGuardadas
|
||||||
.filter(c => c && c.id)
|
.filter((c) => c && c.id)
|
||||||
.map(c => ({
|
.map((c) => ({
|
||||||
curso_id: id,
|
curso_id: id,
|
||||||
competencia_id: c.id,
|
competencia_id: c.id,
|
||||||
}));
|
}));
|
||||||
|
@ -157,7 +156,9 @@ export default function CursosVista() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alumnosInscritos && alumnosInscritos.length > 0) {
|
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);
|
setConfirmarEliminar(false);
|
||||||
setMostrarModal(true);
|
setMostrarModal(true);
|
||||||
return;
|
return;
|
||||||
|
@ -192,150 +193,167 @@ export default function CursosVista() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const quitarCompetencia = () => {
|
const quitarCompetencia = () => {
|
||||||
setCompetenciasGuardadas(competenciasGuardadas.filter((_, i) => i !== compAEliminar));
|
setCompetenciasGuardadas(
|
||||||
|
competenciasGuardadas.filter((_, i) => i !== compAEliminar)
|
||||||
|
);
|
||||||
setDialogQuitarComp(false);
|
setDialogQuitarComp(false);
|
||||||
setCompAEliminar(null);
|
setCompAEliminar(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<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>
|
<h1 className="text-2xl font-semibold mb-6">Lista de Cursos</h1>
|
||||||
<table className="min-w-full bg-white border">
|
<div className="overflow-x-auto w-full">
|
||||||
<thead>
|
<table className="min-w-full bg-white border">
|
||||||
<tr className="bg-gray-100">
|
<thead>
|
||||||
<th className="py-2 border-b">ID</th>
|
<tr className="bg-gray-100">
|
||||||
<th className="py-2 border-b">Nombre</th>
|
<th className="py-2 border-b">ID</th>
|
||||||
<th className="py-2 border-b">Descripción</th>
|
<th className="py-2 border-b">Nombre</th>
|
||||||
<th className="py-2 border-b">Horas</th>
|
<th className="py-2 border-b">Descripción</th>
|
||||||
<th className="py-2 border-b">Competencias</th>
|
<th className="py-2 border-b">Horas</th>
|
||||||
<th className="py-2 border-b">Acciones</th>
|
<th className="py-2 border-b">Competencias</th>
|
||||||
</tr>
|
<th className="py-2 border-b">Acciones</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{cursos.map((curso) =>
|
<tbody>
|
||||||
cursoEditando === curso.id ? (
|
{cursos.map((curso) =>
|
||||||
<tr key={curso.id}>
|
cursoEditando === curso.id ? (
|
||||||
<td className="py-2 px-4 border-b text-center">{curso.id}</td>
|
<tr key={curso.id}>
|
||||||
<td className="py-2 px-4 border-b">
|
<td className="py-2 px-4 border-b text-center">
|
||||||
<Input
|
{curso.id}
|
||||||
value={nuevoNombre}
|
</td>
|
||||||
onChange={(e) => setNuevoNombre(e.target.value)}
|
<td className="py-2 px-4 border-b">
|
||||||
/>
|
<Input
|
||||||
</td>
|
value={nuevoNombre}
|
||||||
<td className="py-2 px-4 border-b">
|
onChange={(e) => setNuevoNombre(e.target.value)}
|
||||||
<Input
|
/>
|
||||||
value={nuevaDescripcion}
|
</td>
|
||||||
onChange={(e) => setNuevaDescripcion(e.target.value)}
|
<td className="py-2 px-4 border-b">
|
||||||
/>
|
<Input
|
||||||
</td>
|
value={nuevaDescripcion}
|
||||||
<td className="py-2 px-4 border-b">
|
onChange={(e) => setNuevaDescripcion(e.target.value)}
|
||||||
<Input
|
/>
|
||||||
type="number"
|
</td>
|
||||||
value={nuevaHoras}
|
<td className="py-2 px-4 border-b">
|
||||||
onChange={(e) => setNuevaHoras(e.target.value)}
|
<Input
|
||||||
/>
|
type="number"
|
||||||
</td>
|
value={nuevaHoras}
|
||||||
<td className="py-2 px-4 border-b">
|
onChange={(e) => setNuevaHoras(e.target.value)}
|
||||||
<div className="flex flex-col gap-2">
|
/>
|
||||||
{competenciasGuardadas.map((comp, idx) => (
|
</td>
|
||||||
<div key={idx} className="flex items-center gap-2">
|
<td className="py-2 px-4 border-b">
|
||||||
<select
|
<div className="flex flex-col gap-2">
|
||||||
className="border rounded px-2 py-1"
|
{competenciasGuardadas.map((comp, idx) => (
|
||||||
value={comp?.id || ""}
|
<div key={idx} className="flex items-center gap-2">
|
||||||
onChange={e => {
|
<select
|
||||||
const nuevaLista = [...competenciasGuardadas];
|
className="border rounded px-2 py-1"
|
||||||
const nuevaComp = todasCompetencias.find(c => c.id === Number(e.target.value));
|
value={comp?.id || ""}
|
||||||
nuevaLista[idx] = nuevaComp;
|
onChange={(e) => {
|
||||||
setCompetenciasGuardadas(nuevaLista);
|
const nuevaLista = [...competenciasGuardadas];
|
||||||
}}
|
const nuevaComp = todasCompetencias.find(
|
||||||
>
|
(c) => c.id === Number(e.target.value)
|
||||||
<option value="">Selecciona competencia</option>
|
);
|
||||||
{todasCompetencias.map(tc => (
|
nuevaLista[idx] = nuevaComp;
|
||||||
<option
|
setCompetenciasGuardadas(nuevaLista);
|
||||||
key={tc.id}
|
}}
|
||||||
value={tc.id}
|
>
|
||||||
disabled={
|
<option value="">Selecciona competencia</option>
|
||||||
// Deshabilita si ya está seleccionada en otro select
|
{todasCompetencias.map((tc) => (
|
||||||
competenciasGuardadas.some(
|
<option
|
||||||
(c, i) => c && c.id === tc.id && i !== idx
|
key={tc.id}
|
||||||
)
|
value={tc.id}
|
||||||
}
|
disabled={
|
||||||
>
|
// Deshabilita si ya está seleccionada en otro select
|
||||||
{tc.descripcion}
|
competenciasGuardadas.some(
|
||||||
</option>
|
(c, i) => c && c.id === tc.id && i !== idx
|
||||||
))}
|
)
|
||||||
</select>
|
}
|
||||||
<Button
|
>
|
||||||
type="button"
|
{tc.descripcion}
|
||||||
className="bg-red-500 hover:bg-red-700 text-white px-2 py-1 rounded"
|
</option>
|
||||||
onClick={() => pedirConfirmacionQuitarComp(idx)}
|
))}
|
||||||
>
|
</select>
|
||||||
Quitar
|
<Button
|
||||||
</Button>
|
type="button"
|
||||||
</div>
|
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
|
<Button
|
||||||
type="button"
|
className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-3 m-2 rounded"
|
||||||
className="bg-blue-500 hover:bg-blue-700 text-white px-2 py-1 rounded mt-2"
|
onClick={() => guardarEdicion(curso.id)}
|
||||||
onClick={() => setCompetenciasGuardadas([...competenciasGuardadas, null])}
|
|
||||||
disabled={competenciasGuardadas.length >= todasCompetencias.length}
|
|
||||||
>
|
>
|
||||||
Agregar competencia
|
Guardar
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
<Button
|
||||||
</td>
|
className="bg-gray-400 hover:bg-gray-600 text-white font-bold py-1 px-3 m-2 rounded"
|
||||||
<td className="py-2 px-4 border-b flex justify-center">
|
onClick={cancelarEdicion}
|
||||||
<Button
|
>
|
||||||
className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-3 m-2 rounded"
|
Cancelar
|
||||||
onClick={() => guardarEdicion(curso.id)}
|
</Button>
|
||||||
>
|
</td>
|
||||||
Guardar
|
</tr>
|
||||||
</Button>
|
) : (
|
||||||
<Button
|
<tr key={curso.id}>
|
||||||
className="bg-gray-400 hover:bg-gray-600 text-white font-bold py-1 px-3 m-2 rounded"
|
<td className="py-2 px-4 border-b">{curso.id}</td>
|
||||||
onClick={cancelarEdicion}
|
<td className="py-2 px-4 border-b">{curso.nombre}</td>
|
||||||
>
|
<td className="py-2 px-4 border-b">{curso.descripcion}</td>
|
||||||
Cancelar
|
<td className="py-2 px-4 border-b">{curso.horas}</td>
|
||||||
</Button>
|
<td className="py-2 px-4 border-b">
|
||||||
</td>
|
{Array.isArray(curso.competencias) &&
|
||||||
</tr>
|
curso.competencias.length > 0 ? (
|
||||||
) : (
|
|
||||||
<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">
|
<ul className="list-disc pl-4">
|
||||||
{curso.competencias.map((comp) => (
|
{curso.competencias.map((comp) => (
|
||||||
<li key={comp.id}>{comp.descripcion}</li>
|
<li key={comp.id}>{comp.descripcion}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)
|
) : (
|
||||||
: "Sin competencias"}
|
"Sin competencias"
|
||||||
</td>
|
)}
|
||||||
<td className="py-2 px-4 border-b space-x-2">
|
</td>
|
||||||
<Button
|
<td className="py-2 px-4 border-b flex justify-center">
|
||||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 rounded"
|
<Button
|
||||||
onClick={() => iniciarEdicion(curso)}
|
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 m-1 rounded"
|
||||||
>
|
onClick={() => iniciarEdicion(curso)}
|
||||||
Editar
|
>
|
||||||
</Button>
|
Editar
|
||||||
<Button
|
</Button>
|
||||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 rounded"
|
<Button
|
||||||
onClick={() => confirmarEliminacion(curso.id)}
|
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>
|
Eliminar
|
||||||
</td>
|
</Button>
|
||||||
</tr>
|
</td>
|
||||||
)
|
</tr>
|
||||||
)}
|
)
|
||||||
</tbody>
|
)}
|
||||||
</table>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dialog para eliminar curso */}
|
{/* Dialog para eliminar curso */}
|
||||||
|
@ -371,9 +389,7 @@ export default function CursosVista() {
|
||||||
<Dialog open={dialogQuitarComp} onOpenChange={setDialogQuitarComp}>
|
<Dialog open={dialogQuitarComp} onOpenChange={setDialogQuitarComp}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-black">
|
<DialogTitle className="text-black">Quitar competencia</DialogTitle>
|
||||||
Quitar competencia
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
¿Estás seguro de que deseas quitar esta competencia del curso?
|
¿Estás seguro de que deseas quitar esta competencia del curso?
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
@ -411,4 +427,4 @@ export default function CursosVista() {
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react";
|
||||||
import Layout from "@/components/layout/Layout";
|
import Layout from "@/components/layout/Layout";
|
||||||
import { supabaseClient } from "@/utils/supabase";
|
import { supabaseClient } from "@/utils/supabase";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import CrearDiplomaDialog from "@/components/dialogs/crearDiplomaDialog";
|
|
||||||
import VistaPreviaDiplomaDialog from "@/components/dialogs/vistaPreviaDiplomaDialog";
|
import VistaPreviaDiplomaDialog from "@/components/dialogs/vistaPreviaDiplomaDialog";
|
||||||
|
|
||||||
export default function DiplomasVista() {
|
export default function DiplomasVista() {
|
||||||
|
@ -53,60 +52,51 @@ export default function DiplomasVista() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<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>
|
<h1 className="text-2xl font-semibold mb-6">Vista de Diplomas</h1>
|
||||||
<table className="min-w-full bg-white border">
|
<div className="overflow-x-auto w-full">
|
||||||
<thead>
|
<table className="w-full bg-white border">
|
||||||
<tr className="bg-gray-100">
|
<thead>
|
||||||
<th className="py-2 border-b">ID</th>
|
<tr className="bg-gray-100">
|
||||||
<th className="py-2 border-b">Nombre</th>
|
<th className="py-2 border-b">ID</th>
|
||||||
<th className="py-2 border-b">Correo</th>
|
<th className="py-2 border-b">Nombre</th>
|
||||||
<th className="py-2 border-b">Teléfono</th>
|
<th className="py-2 border-b">Correo</th>
|
||||||
<th className="py-2 border-b">Curso</th>
|
<th className="py-2 border-b">Teléfono</th>
|
||||||
<th className="py-2 border-b">Acciones</th>
|
<th className="py-2 border-b">Curso</th>
|
||||||
</tr>
|
<th className="py-2 border-b">Acciones</th>
|
||||||
</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>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{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>
|
</div>
|
||||||
{/* Dialog para crear diploma y vista previa juntos */}
|
{/* Dialog para crear diploma y vista previa juntos */}
|
||||||
{mostrarDialog && (
|
{mostrarDialog && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<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">
|
<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
|
<VistaPreviaDiplomaDialog
|
||||||
open={mostrarDialog}
|
open={mostrarDialog}
|
||||||
onOpenChange={handleCloseDialog}
|
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")
|
.regex(/^\d+$/, "El número de teléfono solo puede contener dígitos")
|
||||||
.min(10, "El número de teléfono debe tener al menos 10 dígitos")
|
.min(10, "El número de teléfono debe tener al menos 10 dígitos")
|
||||||
.max(10, "El número de teléfono no puede tener más de 10 dígitos"),
|
.max(10, "El número de teléfono no puede tener más de 10 dígitos"),
|
||||||
cursoSeleccionado: z.string().nonempty("Selecciona un curso"),
|
//tipo: z.string().nonempty("Selecciona un tipo de asignación"),
|
||||||
|
cursoSeleccionado: z.string().nonempty("Selecciona una opción"),
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const Schema = z.object({
|
||||||
|
nombre: z
|
||||||
|
.string()
|
||||||
|
.nonempty("Escribe el nombre del curso")
|
||||||
|
.regex(/^[\p{L}\s]+$/u, "Solo se permiten letras en el nombre"),
|
||||||
|
descripcion: z.string().nonempty("Escribe una descripción"),
|
||||||
|
horas: z.number().positive("Las horas deben ser un número positivo").int(),
|
||||||
|
});
|
|
@ -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