Merge pull request 'roberto' (#3) from roberto into main

Reviewed-on: #3
This commit is contained in:
roberto.viveros 2025-06-15 03:24:04 +00:00
commit 5d84bf3350
11 changed files with 1149 additions and 197 deletions

View File

@ -29,6 +29,7 @@
"mysql2": "^3.14.1",
"next": "15.3.0",
"papaparse": "^5.5.2",
"qrcode": "^1.5.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.2",
@ -3084,11 +3085,18 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@ -3509,6 +3517,14 @@
"node": ">=6"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"engines": {
"node": ">=6"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001714",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz",
@ -3572,6 +3588,16 @@
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
@ -3613,7 +3639,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"devOptional": true,
"dependencies": {
"color-name": "~1.1.4"
},
@ -3802,6 +3827,14 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -3893,6 +3926,11 @@
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
},
"node_modules/diplomas": {
"resolved": "",
"link": true
@ -4883,6 +4921,14 @@
"is-property": "^1.0.2"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -5372,6 +5418,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/is-generator-function": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
@ -6494,6 +6548,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@ -6533,7 +6595,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"engines": {
"node": ">=8"
}
@ -6578,6 +6639,14 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@ -6665,6 +6734,22 @@
"node": ">=6"
}
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@ -6876,6 +6961,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@ -6884,6 +6977,11 @@
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -7108,6 +7206,11 @@
"node": ">= 18"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@ -7353,6 +7456,24 @@
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@ -7460,6 +7581,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@ -8028,6 +8160,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
},
"node_modules/which-typed-array": {
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
@ -8074,6 +8211,19 @@
"node": ">=0.10.0"
}
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@ -8119,6 +8269,92 @@
"node": ">=0.8"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/yargs/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yargs/node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -30,6 +30,7 @@
"mysql2": "^3.14.1",
"next": "15.3.0",
"papaparse": "^5.5.2",
"qrcode": "^1.5.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.2",

View File

@ -33,9 +33,21 @@ const styles = StyleSheet.create({
fontSize: 10,
color: "#888",
},
qr: {
marginTop: 30,
alignSelf: "center",
width: 100,
height: 100,
},
});
export default function Diploma({ alumno, curso, competencias = [], fecha }) {
export default function Diploma({ alumno, formacion, fecha, qr }) {
// formacion: { tipo, nombre, competencias }
let tipoTexto = "formación";
if (formacion?.tipo === "curso") tipoTexto = "curso";
else if (formacion?.tipo === "inyeccion") tipoTexto = "inyección";
else if (formacion?.tipo === "pildora") tipoTexto = "píldora educativa";
return (
<Document>
<Page size="A4" style={styles.page}>
@ -45,15 +57,36 @@ export default function Diploma({ alumno, curso, competencias = [], fecha }) {
<Text style={styles.title}>a: </Text>
<Text style={styles.nombre}>{alumno?.nombre} </Text>
<Text style={styles.title}>
Por su asistencia a la píldora educativa
</Text>
<Text style={styles.curso}>{curso?.nombre || "Sin curso"}</Text>
<Text style={styles.title}>
con duración de 2 horas, modalidad remota
Por su asistencia{" "}
{formacion?.tipo === "curso"
? "al curso"
: formacion?.tipo === "inyeccion"
? "a la inyección"
: formacion?.tipo === "pildora"
? "a la píldora educativa"
: "a la formación"}
</Text>
<Text style={styles.curso}>{formacion?.nombre || "Sin formación"}</Text>
{(formacion?.tipo === "curso" || formacion?.tipo === "inyeccion") &&
formacion?.competencias?.length > 0 && (
<View style={styles.competencias}>
<Text style={{ fontWeight: "bold", marginBottom: 4 }}>
Competencias acreditadas:
</Text>
{formacion.competencias.map((comp) => (
<Text key={comp.id} style={styles.competencia}>
- {comp.descripcion}
</Text>
))}
</View>
)}
<Text style={styles.title}>
Se expide en la ciudad de Xalapa, Ver., {fecha}
</Text>
{qr && <Image src={qr} style={styles.qr} />}
<Text style={styles.footer}>
Verifica este diploma en: http://localhost:3000/alumno/{alumno?.id}
</Text>
</Page>
</Document>
);

View File

@ -12,6 +12,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { mensajesSchema } from "@/schemas/mensajesSchema";
import { Textarea } from "../ui/textarea";
import QRCode from "qrcode";
function VistaPreviaDiplomaDialog({
open,
@ -27,6 +28,7 @@ function VistaPreviaDiplomaDialog({
const [mensaje, setMensaje] = useState("");
const [loadingMensajes, setLoadingMensajes] = useState(false);
const [competencias, setCompetencias] = useState([]);
const [qrDataUrl, setQrDataUrl] = useState("");
const form = useForm({
resolver: zodResolver(mensajesSchema),
@ -44,6 +46,15 @@ function VistaPreviaDiplomaDialog({
formState: { errors },
} = form;
useEffect(() => {
if (alumno?.id) {
const url = `http://localhost:3000/alumno/${alumno.id}`;
QRCode.toDataURL(url, { width: 200 }, (err, url) => {
if (!err) setQrDataUrl(url);
});
}
}, [alumno]);
// 🔄 Cargar mensajes al abrir el modal
useEffect(() => {
if (open) {
@ -97,23 +108,63 @@ function VistaPreviaDiplomaDialog({
};
useEffect(() => {
if (alumno && alumno.curso?.id) {
supabaseClient
.from("curso_competencia")
.select("competencia(id, descripcion)")
.eq("curso_id", alumno.curso.id)
.then(({ data }) => {
const comps = data?.map((c) => c.competencia).filter(Boolean) || [];
setCompetencias(comps);
});
if (alumno) {
if (alumno.tipo_formacion === "curso" && alumno.curso?.id) {
supabaseClient
.from("curso_competencia")
.select("competencia(id, descripcion)")
.eq("curso_id", alumno.curso.id)
.then(({ data }) => {
const comps = data?.map((c) => c.competencia).filter(Boolean) || [];
setCompetencias(comps);
});
} else if (
alumno.tipo_formacion === "inyeccion" &&
alumno.inyeccion?.id
) {
supabaseClient
.from("inyeccion_competencia_inyeccion")
.select("competencia_inyeccion(id, descripcion)")
.eq("inyeccion_id", alumno.inyeccion.id)
.then(({ data }) => {
const comps =
data?.map((c) => c.competencia_inyeccion).filter(Boolean) || [];
setCompetencias(comps);
});
} else {
setCompetencias([]);
}
}
}, [alumno]);
if (!alumno) return null;
const competenciasMostradas = competenciasAcreditadas
? competencias.filter((comp) => competenciasAcreditadas.includes(comp.id))
: competencias;
// Mostrar solo competencias acreditadas si corresponde
const competenciasMostradas =
alumno.tipo_formacion === "curso" || alumno.tipo_formacion === "inyeccion"
? competenciasAcreditadas
? competencias.filter((comp) =>
competenciasAcreditadas.includes(comp.id)
)
: competencias
: [];
// Obtener nombre de la formación según tipo
let nombreFormacion = "";
if (alumno.tipo_formacion === "curso") {
nombreFormacion = alumno.curso?.nombre || "Sin curso";
} else if (alumno.tipo_formacion === "inyeccion") {
nombreFormacion = alumno.inyeccion?.nombre || "Sin inyección";
} else if (alumno.tipo_formacion === "pildora") {
nombreFormacion = alumno.pildoras?.nombre || "Sin píldora";
}
// Para el PDF, pasar el nombre y tipo de formación
const datosFormacion = {
tipo: alumno.tipo_formacion,
nombre: nombreFormacion,
competencias: competenciasMostradas,
};
const handleEnviar = async () => {
setEnviando(true);
@ -165,7 +216,7 @@ function VistaPreviaDiplomaDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg h-screen text-black overflow-y-auto">
<DialogContent className="w-full h-screen text-black overflow-y-auto">
<DialogHeader>
<DialogTitle>Diploma</DialogTitle>
</DialogHeader>
@ -173,16 +224,29 @@ function VistaPreviaDiplomaDialog({
<b>Alumno:</b> {alumno.nombre}
</div>
<div className="text-lg mb-2">
<b>Curso:</b> {alumno.curso?.nombre || "Sin curso"}
</div>
<div className="text-lg mb-2">
<b>Competencias Acreditadas:</b>
<ul className="list-disc ml-6">
{competenciasMostradas.map((comp) => (
<li key={comp.id}>{comp.descripcion}</li>
))}
</ul>
<b>
{alumno.tipo_formacion === "curso"
? "Curso"
: alumno.tipo_formacion === "inyeccion"
? "Inyección"
: alumno.tipo_formacion === "pildora"
? "Píldora"
: "Formación"}
:
</b>{" "}
{nombreFormacion}
</div>
{(alumno.tipo_formacion === "curso" ||
alumno.tipo_formacion === "inyeccion") && (
<div className="text-lg mb-2">
<b>Competencias Acreditadas:</b>
<ul className="list-disc ml-6">
{competenciasMostradas.map((comp) => (
<li key={comp.id}>{comp.descripcion}</li>
))}
</ul>
</div>
)}
<div className="text-lg mb-2">
<b>Fecha:</b> {fecha || new Date().toLocaleDateString()}
</div>
@ -214,9 +278,9 @@ function VistaPreviaDiplomaDialog({
document={
<Diploma
alumno={alumno}
curso={curso}
competencias={competenciasMostradas}
formacion={datosFormacion}
fecha={fecha || new Date().toLocaleDateString()}
qr={qrDataUrl}
/>
}
fileName={`Diploma_${alumno.nombre}.pdf`}
@ -268,13 +332,13 @@ function VistaPreviaDiplomaDialog({
{mostrarVistaPrevia && (
<div className="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50">
<div className="bg-white rounded shadow-lg p-4">
<div className="w-[80vw] h-[90vh] mb-4 border">
<div className="w-full h-[90vh] mb-4 border">
<PDFViewer width="100%" height="100%">
<Diploma
alumno={alumno}
curso={curso}
competencias={competenciasMostradas}
formacion={datosFormacion}
fecha={fecha || new Date().toLocaleDateString()}
qr={qrDataUrl}
/>
</PDFViewer>
</div>

View File

@ -0,0 +1,132 @@
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { supabaseClient } from "@/utils/supabase";
import { Card } from "@/components/ui/card";
export default function AlumnoId() {
const router = useRouter();
const { id } = router.query;
const [alumno, setAlumno] = useState(null);
const [formacionNombre, setFormacionNombre] = useState("");
const [competencias, setCompetencias] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!id) return;
const fetchAlumno = async () => {
// Traer datos del alumno y su formación
const { data, error } = await supabaseClient
.from("alumno")
.select(
`
id, nombre, correo, tipo_formacion,
curso(id, nombre),
inyeccion(id, nombre),
pildoras(id, nombre)
`
)
.eq("id", id)
.single();
if (data) {
setAlumno(data);
let nombreFormacion = "";
if (data.tipo_formacion === "curso")
nombreFormacion = data.curso?.nombre || "";
if (data.tipo_formacion === "inyeccion")
nombreFormacion = data.inyeccion?.nombre || "";
if (data.tipo_formacion === "pildora")
nombreFormacion = data.pildoras?.nombre || "";
setFormacionNombre(nombreFormacion);
// Traer competencias según tipo
if (data.tipo_formacion === "curso" && data.curso?.id) {
const { data: comps } = await supabaseClient
.from("curso_competencia")
.select("competencia(id, descripcion)")
.eq("curso_id", data.curso.id);
setCompetencias(comps?.map((c) => c.competencia) || []);
} else if (data.tipo_formacion === "inyeccion" && data.inyeccion?.id) {
const { data: comps } = await supabaseClient
.from("inyeccion_competencia_inyeccion")
.select("competencia_inyeccion(id, descripcion)")
.eq("inyeccion_id", data.inyeccion.id);
setCompetencias(comps?.map((c) => c.competencia_inyeccion) || []);
} else {
setCompetencias([]);
}
}
setLoading(false);
};
fetchAlumno();
}, [id]);
if (loading) return <div className="text-black p-10">Cargando...</div>;
if (!alumno)
return <div className="text-black p-10">Alumno no encontrado</div>;
return (
<Card className="max-w-xl mx-auto mt-10 bg-white rounded shadow p-8 text-black">
<img
src="/encabezado.png"
alt="Encabezado"
className="w-full h-32 object-cover mb-6"
/>
<h1 className="text-2xl font-bold mb-4">
Información de {alumno.nombre}
</h1>
<p>
<b>Nombre:</b> {alumno.nombre}
</p>
<p>
<b>Correo:</b> {alumno.correo}
</p>
<p>
<b>Tipo de formación:</b>{" "}
{alumno.tipo_formacion === "curso"
? "Curso"
: alumno.tipo_formacion === "inyeccion"
? "Inyección"
: alumno.tipo_formacion === "pildora"
? "Píldora"
: ""}
</p>
<p>
<b>
{alumno.tipo_formacion === "curso"
? "Curso"
: alumno.tipo_formacion === "inyeccion"
? "Inyección"
: alumno.tipo_formacion === "pildora"
? "Píldora"
: "Formación"}
:
</b>{" "}
{formacionNombre}
</p>
{(alumno.tipo_formacion === "curso" ||
alumno.tipo_formacion === "inyeccion") && (
<div className="mt-4">
<b>Competencias:</b>
<ul className="list-disc ml-6">
{competencias.map((comp) => (
<li key={comp.id}>{comp.descripcion}</li>
))}
</ul>
</div>
)}
<div className="mt-8 text-lg font-semibold text-green-700">
Felicidades {alumno.nombre} por haber concluido tu{" "}
{alumno.tipo_formacion === "curso"
? "curso"
: alumno.tipo_formacion === "inyeccion"
? "inyección"
: alumno.tipo_formacion === "pildora"
? "píldora"
: "formación"}{" "}
{formacionNombre}, agradecemos tu participación, te esperamos pronto por
aquí para seguir formando tu camino.
</div>
</Card>
);
}

View File

@ -11,7 +11,6 @@ import {
} from "@/components/ui/select";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
@ -24,61 +23,99 @@ import { alumnoSchema } from "@/schemas/AlumnosSchema";
export default function AlumnosManual() {
const [cursos, setCursos] = useState([]);
const [isDialogOpen, setIsDialogOpen] = useState(false); // Estado para controlar el diálogo
const [inyecciones, setInyecciones] = useState([]);
const [pildoras, setPildoras] = useState([]);
const [isDialogOpen, setIsDialogOpen] = useState(false);
// Configurar React Hook Form con zod
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
reset,
clearErrors,
} = useForm({
resolver: zodResolver(alumnoSchema),
defaultValues: {
nombre: "",
correo: "",
telefono: "",
tipo: "",
cursoSeleccionado: "",
},
});
// Cargar cursos al iniciar el componente
const tipo = watch("tipo");
const cursoSeleccionado = watch("cursoSeleccionado");
// Cargar opciones según tipo seleccionado
useEffect(() => {
const cargarCursos = async () => {
const { data, error } = await supabaseClient
.from("curso")
.select("id, nombre");
if (error) {
console.error("Error al cargar cursos:", error.message);
} else {
setCursos(data);
}
};
cargarCursos();
}, []);
if (!tipo) {
setValue("cursoSeleccionado", "");
clearErrors("cursoSeleccionado");
return;
}
setValue("cursoSeleccionado", "");
clearErrors("cursoSeleccionado");
if (tipo === "curso") {
cargarCursos();
} else if (tipo === "inyeccion") {
cargarInyecciones();
} else if (tipo === "pildora") {
cargarPildoras();
}
// eslint-disable-next-line
}, [tipo]);
const cargarCursos = async () => {
const { data, error } = await supabaseClient
.from("curso")
.select("id, nombre");
if (!error) setCursos(data || []);
};
const cargarInyecciones = async () => {
const { data, error } = await supabaseClient
.from("inyeccion")
.select("id, nombre");
if (!error) setInyecciones(data || []);
};
const cargarPildoras = async () => {
const { data, error } = await supabaseClient
.from("pildoras")
.select("id, nombre");
if (!error) setPildoras(data || []);
};
// Guardar alumno
const manejarGuardar = async (data) => {
try {
const { error } = await supabaseClient.from("alumno").insert([
{
nombre: data.nombre,
correo: data.correo,
telefono: data.telefono,
curso_id: Number(data.cursoSeleccionado), // Guardar el ID del curso
},
]);
let campos = {
nombre: data.nombre,
correo: data.correo,
telefono: data.telefono,
tipo_formacion: data.tipo,
curso_id: null,
inyeccion_id: null,
pildoras_id: null,
};
if (data.tipo === "curso")
campos.curso_id = Number(data.cursoSeleccionado);
if (data.tipo === "inyeccion")
campos.inyeccion_id = Number(data.cursoSeleccionado);
if (data.tipo === "pildora")
campos.pildoras_id = Number(data.cursoSeleccionado);
const { error } = await supabaseClient.from("alumno").insert([campos]);
if (error) {
console.error("Error al guardar:", error.message);
setIsDialogOpen(false); // Asegurarse de cerrar el diálogo en caso de error
setIsDialogOpen(false);
} else {
setIsDialogOpen(true); // Mostrar el diálogo al guardar exitosamente
reset(); // Reiniciar el formulario
setIsDialogOpen(true);
reset();
}
} catch (err) {
console.error("Error inesperado:", err);
// Manejo de error inesperado
}
};
@ -118,20 +155,77 @@ export default function AlumnosManual() {
{errors.telefono.message}
</p>
)}
{/* Select para tipo de formación */}
<Select
onValueChange={(value) => setValue("cursoSeleccionado", value)}
value={tipo}
onValueChange={(value) => setValue("tipo", value)}
>
<SelectTrigger className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3">
<SelectValue placeholder="Selecciona un curso" />
<SelectValue placeholder="Selecciona tipo de formación" />
</SelectTrigger>
<SelectContent>
{cursos.map((curso) => (
<SelectItem key={curso.id} value={curso.id.toString()}>
{curso.nombre}
</SelectItem>
))}
<SelectItem value="curso">Curso</SelectItem>
<SelectItem value="inyeccion">Inyección</SelectItem>
<SelectItem value="pildora">Píldora</SelectItem>
</SelectContent>
</Select>
{errors.tipo && (
<p className="text-red-500 text-sm mt-1">{errors.tipo.message}</p>
)}
{/* Select para la opción según tipo */}
{tipo === "curso" && (
<Select
value={cursoSeleccionado}
onValueChange={(value) => setValue("cursoSeleccionado", value)}
>
<SelectTrigger className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3">
<SelectValue placeholder="Selecciona un curso" />
</SelectTrigger>
<SelectContent>
{cursos.map((curso) => (
<SelectItem key={curso.id} value={curso.id.toString()}>
{curso.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{tipo === "inyeccion" && (
<Select
value={cursoSeleccionado}
onValueChange={(value) => setValue("cursoSeleccionado", value)}
>
<SelectTrigger className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3">
<SelectValue placeholder="Selecciona una inyección" />
</SelectTrigger>
<SelectContent>
{inyecciones.map((iny) => (
<SelectItem key={iny.id} value={iny.id.toString()}>
{iny.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{tipo === "pildora" && (
<Select
value={cursoSeleccionado}
onValueChange={(value) => setValue("cursoSeleccionado", value)}
>
<SelectTrigger className="w-full px-3 py-2 border border-gray-300 rounded-md mb-3">
<SelectValue placeholder="Selecciona una píldora" />
</SelectTrigger>
<SelectContent>
{pildoras.map((p) => (
<SelectItem key={p.id} value={p.id.toString()}>
{p.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{errors.cursoSeleccionado && (
<p className="text-red-500 text-sm mt-1">
{errors.cursoSeleccionado.message}

View File

@ -33,6 +33,10 @@ export default function AlumnosVista() {
const [modalMensaje, setModalMensaje] = useState("");
const [nuevoCurso, setNuevoCurso] = useState("");
const [cursos, setCursos] = useState([]);
const [nuevoTipo, setNuevoTipo] = useState("");
const [nuevaFormacion, setNuevaFormacion] = useState("");
const [inyecciones, setInyecciones] = useState([]);
const [pildoras, setPildoras] = useState([]);
// Estado para confirmación de eliminación
const [confirmarEliminar, setConfirmarEliminar] = useState(false);
@ -40,7 +44,10 @@ export default function AlumnosVista() {
useEffect(() => {
cargarAlumnos();
// ...cargar cursos, inyecciones, pildoras...
cargarCursos();
cargarInyecciones();
cargarPildoras();
}, []);
const cargarCursos = async () => {
@ -52,16 +59,36 @@ export default function AlumnosVista() {
}
};
const cargarInyecciones = async () => {
const { data, error } = await supabaseClient.from("inyeccion").select("*");
if (!error) setInyecciones(data || []);
};
const cargarPildoras = async () => {
const { data, error } = await supabaseClient.from("pildoras").select("*");
if (!error) setPildoras(data || []);
};
const cargarAlumnos = async () => {
const { data, error } = await supabaseClient
.from("alumno")
.select("id, nombre, correo, telefono, curso_id, curso(nombre)")
.select(
`
id,
nombre,
correo,
telefono,
tipo_formacion,
curso_id,
curso(nombre),
inyeccion_id,
inyeccion(nombre),
pildoras_id,
pildoras(nombre)
`
)
.order("id", { ascending: true });
if (error) {
console.error("Error al cargar alumnos:", error.message);
} else {
setAlumnos(data);
}
setAlumnos(data || []);
};
const {
@ -83,10 +110,18 @@ export default function AlumnosVista() {
// Iniciar edición
const iniciarEdicion = (alumno) => {
setAlumnoEditando(alumno.id);
setValue("nombre", alumno.nombre);
setValue("correo", alumno.correo);
setValue("telefono", alumno.telefono);
setValue("cursoSeleccionado", alumno.curso_id?.toString() || "");
setNuevoNombre(alumno.nombre || "");
setNuevoCorreo(alumno.correo || "");
setNuevoNumero(alumno.telefono || "");
setNuevoTipo(alumno.tipo_formacion || "");
// Detecta la formación actual según el tipo
if (alumno.tipo_formacion === "curso")
setNuevaFormacion(alumno.curso_id ? String(alumno.curso_id) : "");
else if (alumno.tipo_formacion === "inyeccion")
setNuevaFormacion(alumno.inyeccion_id ? String(alumno.inyeccion_id) : "");
else if (alumno.tipo_formacion === "pildora")
setNuevaFormacion(alumno.pildoras_id ? String(alumno.pildoras_id) : "");
else setNuevaFormacion("");
};
// Cancelar edición
@ -96,25 +131,37 @@ export default function AlumnosVista() {
};
// Guardar cambios
const guardarEdicion = async (data) => {
const guardarEdicion = async (id) => {
// Prepara el objeto de actualización
let updateObj = {
tipo_formacion: nuevoTipo,
curso_id: null,
inyeccion_id: null,
pildoras_id: null,
nombre: nuevoNombre,
correo: nuevoCorreo,
telefono: nuevoNumero,
};
if (nuevoTipo === "curso") updateObj.curso_id = Number(nuevaFormacion);
if (nuevoTipo === "inyeccion")
updateObj.inyeccion_id = Number(nuevaFormacion);
if (nuevoTipo === "pildora") updateObj.pildoras_id = Number(nuevaFormacion);
const { error } = await supabaseClient
.from("alumno")
.update({
nombre: data.nombre,
correo: data.correo,
telefono: data.telefono,
curso_id: data.cursoSeleccionado,
})
.eq("id", alumnoEditando);
.update(updateObj)
.eq("id", id);
if (error) {
setModalMensaje("Error al actualizar el alumno");
if (!error) {
setModalMensaje("Alumno actualizado correctamente");
setMostrarModal(true);
setAlumnoEditando(null);
await cargarAlumnos(); // <--- Refresca la tabla
} else {
setModalMensaje("Alumno actualizado exitosamente");
await cargarAlumnos();
cancelarEdicion();
setModalMensaje("Error al actualizar el alumno");
setMostrarModal(true);
}
setMostrarModal(true);
};
// Confirmar eliminación
@ -149,12 +196,13 @@ export default function AlumnosVista() {
<div className="overflow-x-auto w-full">
<table className="min-w-full bg-white border">
<thead>
<tr className="bg-gray-100">
<tr className="bg-gray-100 text-black">
<th className="py-2 border-b">ID</th>
<th className="py-2 border-b">Nombre</th>
<th className="py-2 border-b">Correo</th>
<th className="py-2 border-b">Teléfono</th>
<th className="py-2 border-b">Curso</th>
<th className="py-2 border-b">Tipo</th>
<th className="py-2 border-b">Formación</th>
<th className="py-2 border-b">Acciones</th>
</tr>
</thead>
@ -166,70 +214,101 @@ export default function AlumnosVista() {
{alumno.id}
</td>
<td className="py-2 px-4 border-b">
<Input type="text" {...register("nombre")} />
{errors.nombre && (
<span className="text-red-500 text-xs">
{errors.nombre.message}
</span>
)}
<input
value={nuevoNombre}
onChange={(e) => setNuevoNombre(e.target.value)}
className="border rounded px-2 py-1 text-black"
/>
</td>
<td className="py-2 px-4 border-b">
<Input type="email" {...register("correo")} />
{errors.correo && (
<span className="text-red-500 text-xs">
{errors.correo.message}
</span>
)}
<input
value={nuevoCorreo}
onChange={(e) => setNuevoCorreo(e.target.value)}
className="border rounded px-2 py-1 text-black"
/>
</td>
<td className="py-2 px-4 border-b">
<Input type="text" {...register("telefono")} />
{errors.telefono && (
<span className="text-red-500 text-xs">
{errors.telefono.message}
</span>
)}
<input
value={nuevoNumero}
onChange={(e) => setNuevoNumero(e.target.value)}
className="border rounded px-2 py-1 text-black"
/>
</td>
<td className="py-2 px-4 border-b">
<Select
value={(alumno.curso_id || "").toString()}
onValueChange={(value) =>
setValue("cursoSeleccionado", value)
}
{...register("cursoSeleccionado")}
<select
value={nuevoTipo}
onChange={(e) => {
setNuevoTipo(e.target.value);
setNuevaFormacion("");
}}
className="border rounded px-2 py-1 text-black"
>
<SelectTrigger>
<SelectValue placeholder="Selecciona un curso" />
</SelectTrigger>
<SelectContent>
<option value="">Selecciona tipo</option>
<option value="curso">Curso</option>
<option value="inyeccion">Inyección</option>
<option value="pildora">Píldora</option>
</select>
</td>
<td className="py-2 px-4 border-b">
{nuevoTipo === "curso" && (
<select
value={nuevaFormacion}
onChange={(e) => setNuevaFormacion(e.target.value)}
className="border rounded px-2 py-1 text-black"
required
>
<option value="">Selecciona curso</option>
{cursos.map((curso) => (
<SelectItem
key={curso.id}
value={curso.id.toString()}
>
<option key={curso.id} value={curso.id}>
{curso.nombre}
</SelectItem>
</option>
))}
</SelectContent>
</Select>
{errors.cursoSeleccionado && (
<span className="text-red-500 text-xs">
{errors.cursoSeleccionado.message}
</span>
</select>
)}
{nuevoTipo === "inyeccion" && (
<select
value={nuevaFormacion}
onChange={(e) => setNuevaFormacion(e.target.value)}
className="border rounded px-2 py-1 text-black"
required
>
<option value="">Selecciona inyección</option>
{inyecciones.map((inyeccion) => (
<option key={inyeccion.id} value={inyeccion.id}>
{inyeccion.nombre}
</option>
))}
</select>
)}
{nuevoTipo === "pildora" && (
<select
value={nuevaFormacion}
onChange={(e) => setNuevaFormacion(e.target.value)}
className="border rounded px-2 py-1 text-black"
required
>
<option value="">Selecciona píldora</option>
{pildoras.map((pildora) => (
<option key={pildora.id} value={pildora.id}>
{pildora.nombre}
</option>
))}
</select>
)}
</td>
<td className="py-2 px-4 border-b flex justify-center">
<Button
className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-3 m-1 rounded"
onClick={handleSubmit(guardarEdicion)}
<td className="py-2 px-4 border-b">
<button
onClick={() => guardarEdicion(alumno.id)}
className="bg-green-500 hover:bg-green-700 text-white px-3 py-1 rounded mr-2"
>
Guardar
</Button>
<Button
className="bg-gray-400 hover:bg-gray-600 text-white font-bold py-1 px-3 m-1 rounded"
onClick={cancelarEdicion}
</button>
<button
onClick={() => setAlumnoEditando(null)}
className="bg-gray-400 hover:bg-gray-600 text-white px-3 py-1 rounded"
>
Cancelar
</Button>
</button>
</td>
</tr>
) : (
@ -239,22 +318,35 @@ export default function AlumnosVista() {
<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"}
{alumno.tipo_formacion === "curso"
? "Curso"
: alumno.tipo_formacion === "inyeccion"
? "Inyección"
: alumno.tipo_formacion === "pildora"
? "Píldora"
: ""}
</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"
<td className="py-2 px-4 border-b">
{alumno.tipo_formacion === "curso" &&
alumno.curso?.nombre}
{alumno.tipo_formacion === "inyeccion" &&
alumno.inyeccion?.nombre}
{alumno.tipo_formacion === "pildora" &&
alumno.pildoras?.nombre}
</td>
<td className="py-2 px-4 border-b">
<button
onClick={() => iniciarEdicion(alumno)}
className="bg-blue-500 hover:bg-blue-700 text-white px-3 py-1 rounded mr-2"
>
Editar
</Button>
<Button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 m-1 rounded"
</button>
<button
onClick={() => confirmarEliminacion(alumno.id)}
className="bg-red-500 hover:bg-red-700 text-white px-3 py-1 rounded"
>
Eliminar
</Button>
</button>
</td>
</tr>
)

View File

@ -18,9 +18,23 @@ export default function DiplomasVista() {
const cargarAlumnos = async () => {
const { data, error } = await supabaseClient
.from("alumno")
.select("id, nombre, correo, telefono, curso(id, nombre)")
.select(
`
id,
nombre,
correo,
telefono,
tipo_formacion,
curso_id,
curso(id, nombre),
inyeccion_id,
inyeccion(id, nombre),
pildoras_id,
pildoras(id, nombre)
`
)
.order("id", { ascending: true });
if (!error) setAlumnos(data);
setAlumnos(data || []);
};
cargarAlumnos();
}, []);
@ -37,16 +51,38 @@ export default function DiplomasVista() {
};
useEffect(() => {
if (alumnoSeleccionado && alumnoSeleccionado.curso?.id) {
supabaseClient
.from("curso_competencia")
.select("competencia(id, descripcion)")
.eq("curso_id", alumnoSeleccionado.curso.id)
.then(({ data }) => {
const comps = data?.map((c) => c.competencia).filter(Boolean) || [];
setCompetencias(comps);
setCompetenciasAcreditadas(comps.map((c) => c.id)); // Opcional: selecciona todas por default
});
if (alumnoSeleccionado) {
if (
alumnoSeleccionado.tipo_formacion === "curso" &&
alumnoSeleccionado.curso?.id
) {
supabaseClient
.from("curso_competencia")
.select("competencia(id, descripcion)")
.eq("curso_id", alumnoSeleccionado.curso.id)
.then(({ data }) => {
const comps = data?.map((c) => c.competencia).filter(Boolean) || [];
setCompetencias(comps);
setCompetenciasAcreditadas(comps.map((c) => c.id));
});
} else if (
alumnoSeleccionado.tipo_formacion === "inyeccion" &&
alumnoSeleccionado.inyeccion?.id
) {
supabaseClient
.from("inyeccion_competencia_inyeccion")
.select("competencia_inyeccion(id, descripcion)")
.eq("inyeccion_id", alumnoSeleccionado.inyeccion.id)
.then(({ data }) => {
const comps =
data?.map((c) => c.competencia_inyeccion).filter(Boolean) || [];
setCompetencias(comps);
setCompetenciasAcreditadas(comps.map((c) => c.id));
});
} else {
setCompetencias([]);
setCompetenciasAcreditadas([]);
}
}
}, [alumnoSeleccionado]);
@ -62,7 +98,8 @@ export default function DiplomasVista() {
<th className="py-2 border-b">Nombre</th>
<th className="py-2 border-b">Correo</th>
<th className="py-2 border-b">Teléfono</th>
<th className="py-2 border-b">Curso</th>
<th className="py-2 border-b">Tipo</th>
<th className="py-2 border-b">Formación</th>
<th className="py-2 border-b">Acciones</th>
</tr>
</thead>
@ -74,7 +111,20 @@ export default function DiplomasVista() {
<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"}
{alumno.tipo_formacion === "curso"
? "Curso"
: alumno.tipo_formacion === "inyeccion"
? "Inyección"
: alumno.tipo_formacion === "pildora"
? "Píldora"
: ""}
</td>
<td className="py-2 px-4 border-b">
{alumno.tipo_formacion === "curso" && alumno.curso?.nombre}
{alumno.tipo_formacion === "inyeccion" &&
alumno.inyeccion?.nombre}
{alumno.tipo_formacion === "pildora" &&
alumno.pildoras?.nombre}
</td>
<td className="py-2 px-4 border-b">
<Button
@ -95,7 +145,7 @@ export default function DiplomasVista() {
</div>
{/* Dialog para crear diploma y vista previa juntos */}
{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 w-screen">
<div className="flex gap-8 bg-black bg-opacity-30 p-8 rounded">
<VistaPreviaDiplomaDialog
open={mostrarDialog}

View File

@ -1,7 +1,6 @@
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";
@ -16,35 +15,99 @@ import {
DialogFooter,
} from "@/components/ui/dialog";
// Puedes usar tu propio esquema de validación
const schema = {/* ...tu esquema Zod aquí... */};
export default function InyeccionesManual() {
const [competencias, setCompetencias] = useState([]); // [{id, descripcion}]
const [showDialog, setShowDialog] = useState(false);
const [dialogMsg, setDialogMsg] = useState("");
const [loading, setLoading] = useState(false);
const [addCompetencia, setAddCompetencia] = useState(false);
const form = useForm({
resolver: zodResolver(Schema),
resolver: zodResolver(schema),
defaultValues: {
nombre: "",
descripcion: "",
horas: 0,
nuevaCompetencia: "",
},
});
const {
register,
handleSubmit,
setValue,
getValues,
formState: { errors },
reset,
} = form;
// Añadir competencia (busca o crea en BD)
const handleSaveCompetencia = async (e) => {
e.preventDefault();
const desc = getValues("nuevaCompetencia").trim();
if (!desc) return;
if (competencias.some((c) => c.descripcion === desc)) {
setDialogMsg("La competencia ya fue agregada.");
setShowDialog(true);
return;
}
try {
let { data: existente } = await supabaseClient
.from("competencia_inyeccion")
.select("id")
.eq("descripcion", desc)
.maybeSingle();
let id = existente?.id;
if (!id) {
const { data: insertada, error } = await supabaseClient
.from("competencia_inyeccion")
.insert([{ descripcion: desc }])
.select("id")
.single();
if (error) throw error;
id = insertada.id;
}
setCompetencias([...competencias, { id, descripcion: desc }]);
setAddCompetencia(false);
setValue("nuevaCompetencia", "");
setDialogMsg("¡La competencia fue agregada exitosamente!");
setShowDialog(true);
} catch (err) {
setDialogMsg("Error al guardar la competencia: " + (err.message || err));
setShowDialog(true);
}
};
// Eliminar competencia
const handleDeleteCompetencia = (index) => {
setCompetencias(competencias.filter((_, i) => i !== index));
};
// Guardar inyección y asociar competencias
const onSubmit = async (data) => {
setLoading(true);
try {
const { nombre, descripcion, horas } = data;
const { error } = await supabaseClient
const { data: inyeccion, error: errorIny } = await supabaseClient
.from("inyeccion")
.insert([{ nombre, descripcion, horas: parseInt(horas, 10) }]);
if (error) throw error;
.insert([{ nombre, descripcion, horas: parseInt(horas, 10) }])
.select("id")
.single();
if (errorIny) throw errorIny;
if (competencias.length) {
const relaciones = competencias.map((c) => ({
inyeccion_id: inyeccion.id,
competencia_inyeccion_id: c.id,
}));
const { error: errorPivote } = await supabaseClient
.from("inyeccion_competencia_inyeccion")
.insert(relaciones);
if (errorPivote) throw errorPivote;
}
setDialogMsg("Inyección guardada exitosamente");
setCompetencias([]);
reset();
} catch (err) {
setDialogMsg("Error: " + (err.message || err));
@ -57,9 +120,7 @@ export default function InyeccionesManual() {
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>
<h1 className="text-xl font-semibold mb-10 text-black">Nueva inyección</h1>
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
<Input
type="text"
@ -90,6 +151,78 @@ export default function InyeccionesManual() {
<p className="text-red-500 text-sm">{errors.horas.message}</p>
)}
<h2 className="text-lg font-semibold mb-3 text-black">
Competencias de Inyección
</h2>
<p className="text-xs text-gray-500 mb-2">
Puedes agregar competencias nuevas exclusivas para inyecciones.
</p>
{competencias.length > 0 && (
<div className="mt-5 w-full flex-wrap">
{competencias.map((c, i) => (
<div
key={i}
className="w-full flex justify-between items-center px-2 mb-2"
>
<span className="text-black">{c.descripcion}</span>
<Button
type="button"
onClick={() => handleDeleteCompetencia(i)}
className="bg-red-400 hover:bg-red-500 text-white font-bold py-1 px-3 rounded-md"
>
X
</Button>
</div>
))}
</div>
)}
{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"

View File

@ -22,6 +22,8 @@ export default function InyeccionesVista() {
const [modalMensaje, setModalMensaje] = useState("");
const [confirmarEliminar, setConfirmarEliminar] = useState(false);
const [inyeccionAEliminar, setInyeccionAEliminar] = useState(null);
const [competenciasDisponibles, setCompetenciasDisponibles] = useState([]);
const [competenciasEditando, setCompetenciasEditando] = useState([]);
useEffect(() => {
cargarInyecciones();
@ -30,7 +32,18 @@ export default function InyeccionesVista() {
const cargarInyecciones = async () => {
const { data, error } = await supabaseClient
.from("inyeccion")
.select("*")
.select(`
id,
nombre,
descripcion,
horas,
inyeccion_competencia_inyeccion (
competencia_inyeccion (
id,
descripcion
)
)
`)
.order("id", { ascending: true });
if (error) {
setModalMensaje("Error al cargar inyecciones: " + error.message);
@ -40,11 +53,24 @@ export default function InyeccionesVista() {
}
};
const iniciarEdicion = (inyeccion) => {
const iniciarEdicion = async (inyeccion) => {
setInyeccionEditando(inyeccion.id);
setNuevoNombre(inyeccion.nombre);
setNuevaDescripcion(inyeccion.descripcion);
setNuevaHoras(inyeccion.horas);
// Cargar todas las competencias posibles
const { data: todas, error: errorTodas } = await supabaseClient
.from("competencia_inyeccion")
.select("*");
setCompetenciasDisponibles(todas || []);
// Cargar las competencias ya asociadas a la inyección
setCompetenciasEditando(
(inyeccion.inyeccion_competencia_inyeccion || []).map(
(ic) => ic.competencia_inyeccion?.id
)
);
};
const cancelarEdicion = () => {
@ -52,6 +78,7 @@ export default function InyeccionesVista() {
setNuevoNombre("");
setNuevaDescripcion("");
setNuevaHoras("");
setCompetenciasEditando([]);
};
const guardarEdicion = async (id) => {
@ -64,12 +91,28 @@ export default function InyeccionesVista() {
})
.eq("id", id);
if (error) {
setModalMensaje("Error al actualizar la inyección");
} else {
if (!error) {
// Elimina relaciones viejas
await supabaseClient
.from("inyeccion_competencia_inyeccion")
.delete()
.eq("inyeccion_id", id);
// Inserta las nuevas
if (competenciasEditando.length) {
const relaciones = competenciasEditando.map((cid) => ({
inyeccion_id: id,
competencia_inyeccion_id: cid,
}));
await supabaseClient
.from("inyeccion_competencia_inyeccion")
.insert(relaciones);
}
setModalMensaje("Inyección actualizada exitosamente");
await cargarInyecciones();
cancelarEdicion();
} else {
setModalMensaje("Error al actualizar la inyección");
}
setMostrarModal(true);
};
@ -98,22 +141,25 @@ export default function InyeccionesVista() {
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>
<h1 className="text-2xl font-semibold mb-6 text-black">
Lista de Inyecciones
</h1>
<div className="overflow-x-auto w-full">
<table className="min-w-full bg-white border">
<table className="min-w-full bg-white border text-black">
<thead>
<tr className="bg-gray-100">
<tr className="bg-gray-100 text-black">
<th className="py-2 border-b">ID</th>
<th className="py-2 border-b">Nombre</th>
<th className="py-2 border-b">Descripción</th>
<th className="py-2 border-b">Horas</th>
<th className="py-2 border-b">Competencias</th>
<th className="py-2 border-b">Acciones</th>
</tr>
</thead>
<tbody>
{inyecciones.map((inyeccion) =>
inyeccionEditando === inyeccion.id ? (
<tr key={inyeccion.id}>
<tr key={inyeccion.id} className="text-black">
<td className="py-2 px-4 border-b text-center">
{inyeccion.id}
</td>
@ -121,12 +167,14 @@ export default function InyeccionesVista() {
<Input
value={nuevoNombre}
onChange={(e) => setNuevoNombre(e.target.value)}
className="text-black"
/>
</td>
<td className="py-2 px-4 border-b">
<Input
value={nuevaDescripcion}
onChange={(e) => setNuevaDescripcion(e.target.value)}
className="text-black"
/>
</td>
<td className="py-2 px-4 border-b">
@ -134,8 +182,71 @@ export default function InyeccionesVista() {
type="number"
value={nuevaHoras}
onChange={(e) => setNuevaHoras(e.target.value)}
className="text-black"
/>
</td>
<td className="py-2 px-4 border-b align-top">
<div className="flex flex-col gap-2">
{competenciasEditando.map((compId, idx) => (
<div key={idx} className="flex items-center gap-2 mb-1">
<select
value={compId}
onChange={(e) => {
const nuevaLista = [...competenciasEditando];
nuevaLista[idx] = Number(e.target.value);
setCompetenciasEditando(nuevaLista);
}}
className="border rounded px-2 py-1 text-black"
>
{competenciasDisponibles.map((comp) => (
<option key={comp.id} value={comp.id}>
{comp.descripcion}
</option>
))}
</select>
<button
type="button"
className="bg-red-500 hover:bg-red-700 text-white px-3 py-1 rounded"
onClick={() => {
setCompetenciasEditando(
competenciasEditando.filter((_, i) => i !== idx)
);
}}
>
Quitar
</button>
</div>
))}
<button
type="button"
className="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2 rounded mt-2"
onClick={() => {
if (competenciasEditando.length >= 3) {
setModalMensaje(
"Ya llegaste al límite de 3 competencias para esta inyección."
);
setMostrarModal(true);
return;
}
// Agrega la primera competencia disponible que no esté ya seleccionada
const disponibles = competenciasDisponibles
.map((c) => c.id)
.filter((id) => !competenciasEditando.includes(id));
if (disponibles.length > 0) {
setCompetenciasEditando([
...competenciasEditando,
disponibles[0],
]);
}
}}
disabled={
competenciasEditando.length >= competenciasDisponibles.length
}
>
Agregar competencia
</button>
</div>
</td>
<td className="py-2 px-4 border-b flex justify-center">
<Button
className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-3 m-2 rounded"
@ -152,13 +263,21 @@ export default function InyeccionesVista() {
</td>
</tr>
) : (
<tr key={inyeccion.id}>
<tr key={inyeccion.id} className="text-black">
<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.descripcion}</td>
<td className="py-2 px-4 border-b">{inyeccion.horas}</td>
<td className="py-2 px-4 border-b">
<ul className="list-disc ml-5">
{(inyeccion.inyeccion_competencia_inyeccion || [])
.map((ic) => ic.competencia_inyeccion?.descripcion)
.filter(Boolean)
.map((desc, idx) => (
<li key={idx}>{desc}</li>
))}
</ul>
</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"
@ -188,7 +307,7 @@ export default function InyeccionesVista() {
<DialogTitle className="text-black">
Confirmar eliminación
</DialogTitle>
<DialogDescription>
<DialogDescription className="text-black">
¿Estás seguro de que deseas eliminar esta inyección? Esta acción
no se puede deshacer.
</DialogDescription>
@ -214,10 +333,8 @@ export default function InyeccionesVista() {
<Dialog open={mostrarModal} onOpenChange={setMostrarModal}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-black">
Resultado de la operación
</DialogTitle>
<DialogDescription>{modalMensaje}</DialogDescription>
<DialogTitle className="text-black">Aviso</DialogTitle>
<DialogDescription className="text-black">{modalMensaje}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={() => setMostrarModal(false)}>Cerrar</Button>

View File

@ -12,6 +12,6 @@ export const alumnoSchema = z.object({
.regex(/^\d+$/, "El número de teléfono solo puede contener dígitos")
.min(10, "El número de teléfono debe tener al menos 10 dígitos")
.max(10, "El número de teléfono no puede tener más de 10 dígitos"),
//tipo: z.string().nonempty("Selecciona un tipo de asignación"),
tipo: z.string().nonempty("Selecciona un tipo de formación"),
cursoSeleccionado: z.string().nonempty("Selecciona una opción"),
});