diff --git a/diplomas/public/encabezado.png b/diplomas/public/encabezado.png
new file mode 100644
index 0000000..beb13ec
Binary files /dev/null and b/diplomas/public/encabezado.png differ
diff --git a/diplomas/src/components/Diploma.jsx b/diplomas/src/components/Diploma.jsx
index 7325af6..5c2af03 100644
--- a/diplomas/src/components/Diploma.jsx
+++ b/diplomas/src/components/Diploma.jsx
@@ -1,10 +1,29 @@
import React from "react";
-import { Document, Page, Text, View, StyleSheet } from "@react-pdf/renderer";
+import {
+ Document,
+ Page,
+ Text,
+ View,
+ StyleSheet,
+ Image,
+} from "@react-pdf/renderer";
const styles = StyleSheet.create({
- page: { padding: 40, fontFamily: "Helvetica" },
+ page: { fontFamily: "Helvetica" },
title: { fontSize: 24, textAlign: "center", marginBottom: 20 },
- section: { marginBottom: 10, fontSize: 14 },
+ nombre: {
+ fontSize: 18,
+ textAlign: "center",
+ marginBottom: 10,
+ fontStyle: "italic",
+ },
+ curso: {
+ fontSize: 30,
+ textAlign: "center",
+ marginBottom: 10,
+ fontWeight: "bold",
+ },
+ section: { padding: 40, fontSize: 14 },
competencias: { marginLeft: 20, marginTop: 5 },
competencia: { fontSize: 12, marginBottom: 2 },
footer: {
@@ -20,36 +39,21 @@ export default function Diploma({ alumno, curso, competencias = [], fecha }) {
return (
- Diploma
-
-
- Alumno:
- {alumno?.nombre}
-
-
-
-
- Curso:
- {curso?.nombre || "Sin curso"}
-
-
-
- Competencias Acreditadas:
-
- {(competencias || []).map((comp) => (
-
- - {comp.descripcion}
-
- ))}
-
-
-
-
- Fecha:
- {fecha}
-
-
- Generado por SIDAC
+
+ Otorga la presente
+ CONSTANCIA
+ a:
+ {alumno?.nombre}
+
+ Por su asistencia a la píldora educativa
+
+ {curso?.nombre || "Sin curso"}
+
+ con duración de 2 horas, modalidad remota
+
+
+ Se expide en la ciudad de Xalapa, Ver., {fecha}
+
);
diff --git a/diplomas/src/components/app-sidebar.jsx b/diplomas/src/components/app-sidebar.jsx
index 62d21dc..1d5afbe 100644
--- a/diplomas/src/components/app-sidebar.jsx
+++ b/diplomas/src/components/app-sidebar.jsx
@@ -36,6 +36,15 @@ const data = {
},
],
},
+ /*{
+ title: "Vista general",
+ items: [
+ {
+ title: "Vista general",
+ url: "/vistaGeneral",
+ },
+ ],
+ },*/
{
title: "Cursos",
items: [
@@ -53,6 +62,40 @@ const data = {
},
],
},
+ {
+ title: "Inyecciones",
+ items: [
+ {
+ title: "Vista de inyecciones",
+ url: "/inyeccionesVista",
+ },
+ {
+ title: "Agregar manualmente",
+ url: "/inyeccionesManual",
+ },
+ {
+ title: "Agregar desde archivo",
+ url: "/inyeccionesArchivo",
+ },
+ ],
+ },
+ {
+ title: "Pildoras",
+ items: [
+ {
+ title: "Vista de pildoras",
+ url: "/pildorasVista",
+ },
+ {
+ title: "Agregar manualmente",
+ url: "/pildorasManual",
+ },
+ {
+ title: "Agregar desde archivo",
+ url: "/pildorasArchivo",
+ },
+ ],
+ },
{
title: "Diplomas",
items: [
diff --git a/diplomas/src/components/cursosManualForm.jsx b/diplomas/src/components/cursosManualForm.jsx
new file mode 100644
index 0000000..14719c1
--- /dev/null
+++ b/diplomas/src/components/cursosManualForm.jsx
@@ -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 (
+
+ );
+}
diff --git a/diplomas/src/components/dialogs/vistaPreviaDiplomaDialog.jsx b/diplomas/src/components/dialogs/vistaPreviaDiplomaDialog.jsx
index efae999..936cfcc 100644
--- a/diplomas/src/components/dialogs/vistaPreviaDiplomaDialog.jsx
+++ b/diplomas/src/components/dialogs/vistaPreviaDiplomaDialog.jsx
@@ -8,11 +8,16 @@ import {
import Diploma from "@/components/Diploma";
import { PDFDownloadLink, PDFViewer, pdf } from "@react-pdf/renderer";
import { supabaseClient } from "@/utils/supabase";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { mensajesSchema } from "@/schemas/mensajesSchema";
+import { Textarea } from "../ui/textarea";
function VistaPreviaDiplomaDialog({
open,
onOpenChange,
alumno,
+ curso,
competencias: competenciasProp,
fecha,
competenciasAcreditadas,
@@ -20,8 +25,77 @@ function VistaPreviaDiplomaDialog({
const [mostrarVistaPrevia, setMostrarVistaPrevia] = useState(false);
const [enviando, setEnviando] = useState(false);
const [mensaje, setMensaje] = useState("");
+ const [loadingMensajes, setLoadingMensajes] = useState(false);
const [competencias, setCompetencias] = useState([]);
+ const form = useForm({
+ resolver: zodResolver(mensajesSchema),
+ defaultValues: {
+ correo: "",
+ whatsapp: "",
+ },
+ });
+
+ const {
+ register,
+ handleSubmit,
+ setValue,
+ getValues,
+ formState: { errors },
+ } = form;
+
+ // 🔄 Cargar mensajes al abrir el modal
+ useEffect(() => {
+ if (open) {
+ setLoadingMensajes(true);
+ Promise.all([
+ supabaseClient.from("mensaje_correo").select("id, mensaje").single(),
+ supabaseClient.from("mensaje_whatsapp").select("id, mensaje").single(),
+ ])
+ .then(([correoRes, whatsappRes]) => {
+ if (correoRes.data) setValue("correo", correoRes.data.mensaje);
+ if (whatsappRes.data) setValue("whatsapp", whatsappRes.data.mensaje);
+ })
+ .finally(() => setLoadingMensajes(false));
+ }
+ }, [open, setValue]);
+
+ // 📝 Guardar mensajes personalizados
+ const handleGuardarMensajes = async () => {
+ const { correo, whatsapp } = getValues();
+
+ const [{ data: correoExistente }] = await Promise.all([
+ supabaseClient.from("mensaje_correo").select("id").maybeSingle(),
+ ]);
+
+ if (correoExistente) {
+ await supabaseClient
+ .from("mensaje_correo")
+ .update({ mensaje: correo })
+ .eq("id", correoExistente.id);
+ } else {
+ await supabaseClient.from("mensaje_correo").insert({ mensaje: correo });
+ }
+
+ const { data: whatsappExistente } = await supabaseClient
+ .from("mensaje_whatsapp")
+ .select("id")
+ .maybeSingle();
+
+ if (whatsappExistente) {
+ await supabaseClient
+ .from("mensaje_whatsapp")
+ .update({ mensaje: whatsapp })
+ .eq("id", whatsappExistente.id);
+ } else {
+ await supabaseClient
+ .from("mensaje_whatsapp")
+ .insert({ mensaje: whatsapp });
+ }
+
+ setMensaje("Mensajes guardados correctamente.");
+ };
+
useEffect(() => {
if (alumno && alumno.curso?.id) {
supabaseClient
@@ -41,28 +115,25 @@ function VistaPreviaDiplomaDialog({
? competencias.filter((comp) => competenciasAcreditadas.includes(comp.id))
: competencias;
- // Simulación de envío de PDF por correo y WhatsApp
const handleEnviar = async () => {
setEnviando(true);
setMensaje("");
- // Genera el PDF como blob
+
const blob = await pdf(
).toBlob();
- // Convierte el blob a base64
const pdfBase64 = await new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result.split(",")[1]);
reader.readAsDataURL(blob);
});
- // Llama a tu API de Next.js
const resp = await fetch("/api/send-diploma", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -71,32 +142,30 @@ function VistaPreviaDiplomaDialog({
nombre: alumno.nombre,
curso: alumno.curso?.nombre || "Sin curso",
pdfBase64,
+ mensajeCorreo: getValues("correo"),
}),
});
if (resp.ok) {
- // WhatsApp real (abre ventana)
const telefono = alumno.telefono.replace(/\D/g, "");
- const mensajeWhatsapp = encodeURIComponent(
- `Hola ${alumno.nombre}, tu diploma ha sido generado y enviado a tu correo (${alumno.correo}). ¡Felicidades!`
- );
+ const mensajeWhatsapp = encodeURIComponent(getValues("whatsapp"));
window.open(
`https://wa.me/${telefono}?text=${mensajeWhatsapp}`,
"_blank"
);
-
- setMensaje(
- `Diploma enviado por correo a ${alumno.correo} y mensaje enviado por WhatsApp al ${alumno.telefono}.`
- );
+ setMensaje(`Diploma enviado por correo a ${alumno.correo}.`);
} else {
- setMensaje("Error enviando el diploma por correo.");
+ setMensaje("Error enviando el diploma.");
}
+
setEnviando(false);
};
+ if (!alumno) return null;
+
return (