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", "mysql2": "^3.14.1",
"next": "15.3.0", "next": "15.3.0",
"papaparse": "^5.5.2", "papaparse": "^5.5.2",
"qrcode": "^1.5.4",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.56.2", "react-hook-form": "^7.56.2",
@ -3084,11 +3085,18 @@
"url": "https://github.com/sponsors/epoberezkin" "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": { "node_modules/ansi-styles": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
}, },
@ -3509,6 +3517,14 @@
"node": ">=6" "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": { "node_modules/caniuse-lite": {
"version": "1.0.30001714", "version": "1.0.30001714",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz", "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", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" "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": { "node_modules/clone": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
@ -3613,7 +3639,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"devOptional": true,
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "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": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "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", "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" "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": { "node_modules/diplomas": {
"resolved": "", "resolved": "",
"link": true "link": true
@ -4883,6 +4921,14 @@
"is-property": "^1.0.2" "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": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -5372,6 +5418,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/is-generator-function": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", "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" "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": { "node_modules/pako": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@ -6533,7 +6595,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -6578,6 +6639,14 @@
"url": "https://github.com/sponsors/jonschlinkert" "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": { "node_modules/possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@ -6665,6 +6734,22 @@
"node": ">=6" "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": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@ -6876,6 +6961,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/require-from-string": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@ -6884,6 +6977,11 @@
"node": ">=0.10.0" "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": { "node_modules/resolve": {
"version": "1.22.10", "version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -7108,6 +7206,11 @@
"node": ">= 18" "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": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@ -7353,6 +7456,24 @@
"safe-buffer": "~5.2.0" "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": { "node_modules/string.prototype.includes": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", "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" "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": { "node_modules/strip-bom": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@ -8028,6 +8160,11 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/which-typed-array": {
"version": "1.1.19", "version": "1.1.19",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
@ -8074,6 +8211,19 @@
"node": ">=0.10.0" "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": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@ -8119,6 +8269,92 @@
"node": ">=0.8" "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": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

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

View File

@ -33,9 +33,21 @@ const styles = StyleSheet.create({
fontSize: 10, fontSize: 10,
color: "#888", 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 ( return (
<Document> <Document>
<Page size="A4" style={styles.page}> <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.title}>a: </Text>
<Text style={styles.nombre}>{alumno?.nombre} </Text> <Text style={styles.nombre}>{alumno?.nombre} </Text>
<Text style={styles.title}> <Text style={styles.title}>
Por su asistencia a la píldora educativa Por su asistencia{" "}
</Text> {formacion?.tipo === "curso"
<Text style={styles.curso}>{curso?.nombre || "Sin curso"}</Text> ? "al curso"
<Text style={styles.title}> : formacion?.tipo === "inyeccion"
con duración de 2 horas, modalidad remota ? "a la inyección"
: formacion?.tipo === "pildora"
? "a la píldora educativa"
: "a la formación"}
</Text> </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}> <Text style={styles.title}>
Se expide en la ciudad de Xalapa, Ver., {fecha} Se expide en la ciudad de Xalapa, Ver., {fecha}
</Text> </Text>
{qr && <Image src={qr} style={styles.qr} />}
<Text style={styles.footer}>
Verifica este diploma en: http://localhost:3000/alumno/{alumno?.id}
</Text>
</Page> </Page>
</Document> </Document>
); );

View File

@ -12,6 +12,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { mensajesSchema } from "@/schemas/mensajesSchema"; import { mensajesSchema } from "@/schemas/mensajesSchema";
import { Textarea } from "../ui/textarea"; import { Textarea } from "../ui/textarea";
import QRCode from "qrcode";
function VistaPreviaDiplomaDialog({ function VistaPreviaDiplomaDialog({
open, open,
@ -27,6 +28,7 @@ function VistaPreviaDiplomaDialog({
const [mensaje, setMensaje] = useState(""); const [mensaje, setMensaje] = useState("");
const [loadingMensajes, setLoadingMensajes] = useState(false); const [loadingMensajes, setLoadingMensajes] = useState(false);
const [competencias, setCompetencias] = useState([]); const [competencias, setCompetencias] = useState([]);
const [qrDataUrl, setQrDataUrl] = useState("");
const form = useForm({ const form = useForm({
resolver: zodResolver(mensajesSchema), resolver: zodResolver(mensajesSchema),
@ -44,6 +46,15 @@ function VistaPreviaDiplomaDialog({
formState: { errors }, formState: { errors },
} = form; } = 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 // 🔄 Cargar mensajes al abrir el modal
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@ -97,23 +108,63 @@ function VistaPreviaDiplomaDialog({
}; };
useEffect(() => { useEffect(() => {
if (alumno && alumno.curso?.id) { if (alumno) {
supabaseClient if (alumno.tipo_formacion === "curso" && alumno.curso?.id) {
.from("curso_competencia") supabaseClient
.select("competencia(id, descripcion)") .from("curso_competencia")
.eq("curso_id", alumno.curso.id) .select("competencia(id, descripcion)")
.then(({ data }) => { .eq("curso_id", alumno.curso.id)
const comps = data?.map((c) => c.competencia).filter(Boolean) || []; .then(({ data }) => {
setCompetencias(comps); 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]); }, [alumno]);
if (!alumno) return null; if (!alumno) return null;
const competenciasMostradas = competenciasAcreditadas // Mostrar solo competencias acreditadas si corresponde
? competencias.filter((comp) => competenciasAcreditadas.includes(comp.id)) const competenciasMostradas =
: competencias; 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 () => { const handleEnviar = async () => {
setEnviando(true); setEnviando(true);
@ -165,7 +216,7 @@ function VistaPreviaDiplomaDialog({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <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> <DialogHeader>
<DialogTitle>Diploma</DialogTitle> <DialogTitle>Diploma</DialogTitle>
</DialogHeader> </DialogHeader>
@ -173,16 +224,29 @@ function VistaPreviaDiplomaDialog({
<b>Alumno:</b> {alumno.nombre} <b>Alumno:</b> {alumno.nombre}
</div> </div>
<div className="text-lg mb-2"> <div className="text-lg mb-2">
<b>Curso:</b> {alumno.curso?.nombre || "Sin curso"} <b>
</div> {alumno.tipo_formacion === "curso"
<div className="text-lg mb-2"> ? "Curso"
<b>Competencias Acreditadas:</b> : alumno.tipo_formacion === "inyeccion"
<ul className="list-disc ml-6"> ? "Inyección"
{competenciasMostradas.map((comp) => ( : alumno.tipo_formacion === "pildora"
<li key={comp.id}>{comp.descripcion}</li> ? "Píldora"
))} : "Formación"}
</ul> :
</b>{" "}
{nombreFormacion}
</div> </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"> <div className="text-lg mb-2">
<b>Fecha:</b> {fecha || new Date().toLocaleDateString()} <b>Fecha:</b> {fecha || new Date().toLocaleDateString()}
</div> </div>
@ -214,9 +278,9 @@ function VistaPreviaDiplomaDialog({
document={ document={
<Diploma <Diploma
alumno={alumno} alumno={alumno}
curso={curso} formacion={datosFormacion}
competencias={competenciasMostradas}
fecha={fecha || new Date().toLocaleDateString()} fecha={fecha || new Date().toLocaleDateString()}
qr={qrDataUrl}
/> />
} }
fileName={`Diploma_${alumno.nombre}.pdf`} fileName={`Diploma_${alumno.nombre}.pdf`}
@ -268,13 +332,13 @@ function VistaPreviaDiplomaDialog({
{mostrarVistaPrevia && ( {mostrarVistaPrevia && (
<div className="fixed inset-0 bg-black bg-opacity-60 flex 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"> <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%"> <PDFViewer width="100%" height="100%">
<Diploma <Diploma
alumno={alumno} alumno={alumno}
curso={curso} formacion={datosFormacion}
competencias={competenciasMostradas}
fecha={fecha || new Date().toLocaleDateString()} fecha={fecha || new Date().toLocaleDateString()}
qr={qrDataUrl}
/> />
</PDFViewer> </PDFViewer>
</div> </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"; } from "@/components/ui/select";
import { import {
Dialog, Dialog,
DialogTrigger,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
@ -24,61 +23,99 @@ import { alumnoSchema } from "@/schemas/AlumnosSchema";
export default function AlumnosManual() { export default function AlumnosManual() {
const [cursos, setCursos] = useState([]); 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 { const {
register, register,
handleSubmit, handleSubmit,
setValue, setValue,
watch,
formState: { errors }, formState: { errors },
reset, reset,
clearErrors,
} = useForm({ } = useForm({
resolver: zodResolver(alumnoSchema), resolver: zodResolver(alumnoSchema),
defaultValues: { defaultValues: {
nombre: "", nombre: "",
correo: "", correo: "",
telefono: "", telefono: "",
tipo: "",
cursoSeleccionado: "", cursoSeleccionado: "",
}, },
}); });
// Cargar cursos al iniciar el componente const tipo = watch("tipo");
const cursoSeleccionado = watch("cursoSeleccionado");
// Cargar opciones según tipo seleccionado
useEffect(() => { useEffect(() => {
const cargarCursos = async () => { if (!tipo) {
const { data, error } = await supabaseClient setValue("cursoSeleccionado", "");
.from("curso") clearErrors("cursoSeleccionado");
.select("id, nombre"); return;
if (error) { }
console.error("Error al cargar cursos:", error.message); setValue("cursoSeleccionado", "");
} else { clearErrors("cursoSeleccionado");
setCursos(data); if (tipo === "curso") {
} cargarCursos();
}; } else if (tipo === "inyeccion") {
cargarCursos(); 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 // Guardar alumno
const manejarGuardar = async (data) => { const manejarGuardar = async (data) => {
try { try {
const { error } = await supabaseClient.from("alumno").insert([ let campos = {
{ nombre: data.nombre,
nombre: data.nombre, correo: data.correo,
correo: data.correo, telefono: data.telefono,
telefono: data.telefono, tipo_formacion: data.tipo,
curso_id: Number(data.cursoSeleccionado), // Guardar el ID del curso 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) { if (error) {
console.error("Error al guardar:", error.message); setIsDialogOpen(false);
setIsDialogOpen(false); // Asegurarse de cerrar el diálogo en caso de error
} else { } else {
setIsDialogOpen(true); // Mostrar el diálogo al guardar exitosamente setIsDialogOpen(true);
reset(); // Reiniciar el formulario reset();
} }
} catch (err) { } catch (err) {
console.error("Error inesperado:", err); // Manejo de error inesperado
} }
}; };
@ -118,20 +155,77 @@ export default function AlumnosManual() {
{errors.telefono.message} {errors.telefono.message}
</p> </p>
)} )}
{/* Select para tipo de formación */}
<Select <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"> <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> </SelectTrigger>
<SelectContent> <SelectContent>
{cursos.map((curso) => ( <SelectItem value="curso">Curso</SelectItem>
<SelectItem key={curso.id} value={curso.id.toString()}> <SelectItem value="inyeccion">Inyección</SelectItem>
{curso.nombre} <SelectItem value="pildora">Píldora</SelectItem>
</SelectItem>
))}
</SelectContent> </SelectContent>
</Select> </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 && ( {errors.cursoSeleccionado && (
<p className="text-red-500 text-sm mt-1"> <p className="text-red-500 text-sm mt-1">
{errors.cursoSeleccionado.message} {errors.cursoSeleccionado.message}

View File

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

View File

@ -18,9 +18,23 @@ export default function DiplomasVista() {
const cargarAlumnos = async () => { const cargarAlumnos = async () => {
const { data, error } = await supabaseClient const { data, error } = await supabaseClient
.from("alumno") .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 }); .order("id", { ascending: true });
if (!error) setAlumnos(data); setAlumnos(data || []);
}; };
cargarAlumnos(); cargarAlumnos();
}, []); }, []);
@ -37,16 +51,38 @@ export default function DiplomasVista() {
}; };
useEffect(() => { useEffect(() => {
if (alumnoSeleccionado && alumnoSeleccionado.curso?.id) { if (alumnoSeleccionado) {
supabaseClient if (
.from("curso_competencia") alumnoSeleccionado.tipo_formacion === "curso" &&
.select("competencia(id, descripcion)") alumnoSeleccionado.curso?.id
.eq("curso_id", alumnoSeleccionado.curso.id) ) {
.then(({ data }) => { supabaseClient
const comps = data?.map((c) => c.competencia).filter(Boolean) || []; .from("curso_competencia")
setCompetencias(comps); .select("competencia(id, descripcion)")
setCompetenciasAcreditadas(comps.map((c) => c.id)); // Opcional: selecciona todas por default .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]); }, [alumnoSeleccionado]);
@ -62,7 +98,8 @@ export default function DiplomasVista() {
<th className="py-2 border-b">Nombre</th> <th className="py-2 border-b">Nombre</th>
<th className="py-2 border-b">Correo</th> <th className="py-2 border-b">Correo</th>
<th className="py-2 border-b">Teléfono</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> <th className="py-2 border-b">Acciones</th>
</tr> </tr>
</thead> </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.correo}</td>
<td className="py-2 px-4 border-b">{alumno.telefono}</td> <td className="py-2 px-4 border-b">{alumno.telefono}</td>
<td className="py-2 px-4 border-b"> <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>
<td className="py-2 px-4 border-b"> <td className="py-2 px-4 border-b">
<Button <Button
@ -95,7 +145,7 @@ export default function DiplomasVista() {
</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 w-screen">
<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">
<VistaPreviaDiplomaDialog <VistaPreviaDiplomaDialog
open={mostrarDialog} open={mostrarDialog}

View File

@ -1,7 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Schema } from "@/schemas/Schema";
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 { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@ -16,35 +15,99 @@ import {
DialogFooter, DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
// Puedes usar tu propio esquema de validación
const schema = {/* ...tu esquema Zod aquí... */};
export default function InyeccionesManual() { export default function InyeccionesManual() {
const [competencias, setCompetencias] = useState([]); // [{id, descripcion}]
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
const [dialogMsg, setDialogMsg] = useState(""); const [dialogMsg, setDialogMsg] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [addCompetencia, setAddCompetencia] = useState(false);
const form = useForm({ const form = useForm({
resolver: zodResolver(Schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
nombre: "", nombre: "",
descripcion: "", descripcion: "",
horas: 0, horas: 0,
nuevaCompetencia: "",
}, },
}); });
const { const {
register, register,
handleSubmit, handleSubmit,
setValue,
getValues,
formState: { errors }, formState: { errors },
reset, reset,
} = form; } = 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) => { const onSubmit = async (data) => {
setLoading(true); setLoading(true);
try { try {
const { nombre, descripcion, horas } = data; const { nombre, descripcion, horas } = data;
const { error } = await supabaseClient const { data: inyeccion, error: errorIny } = await supabaseClient
.from("inyeccion") .from("inyeccion")
.insert([{ nombre, descripcion, horas: parseInt(horas, 10) }]); .insert([{ nombre, descripcion, horas: parseInt(horas, 10) }])
if (error) throw error; .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"); setDialogMsg("Inyección guardada exitosamente");
setCompetencias([]);
reset(); reset();
} catch (err) { } catch (err) {
setDialogMsg("Error: " + (err.message || err)); setDialogMsg("Error: " + (err.message || err));
@ -57,9 +120,7 @@ export default function InyeccionesManual() {
return ( return (
<Layout> <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"> <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"> <h1 className="text-xl font-semibold mb-10 text-black">Nueva inyección</h1>
Nueva inyección
</h1>
<form onSubmit={handleSubmit(onSubmit)} className="w-full"> <form onSubmit={handleSubmit(onSubmit)} className="w-full">
<Input <Input
type="text" type="text"
@ -90,6 +151,78 @@ export default function InyeccionesManual() {
<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 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"> <div className="flex justify-center w-full mt-5">
<Button <Button
type="submit" type="submit"

View File

@ -22,6 +22,8 @@ export default function InyeccionesVista() {
const [modalMensaje, setModalMensaje] = useState(""); const [modalMensaje, setModalMensaje] = useState("");
const [confirmarEliminar, setConfirmarEliminar] = useState(false); const [confirmarEliminar, setConfirmarEliminar] = useState(false);
const [inyeccionAEliminar, setInyeccionAEliminar] = useState(null); const [inyeccionAEliminar, setInyeccionAEliminar] = useState(null);
const [competenciasDisponibles, setCompetenciasDisponibles] = useState([]);
const [competenciasEditando, setCompetenciasEditando] = useState([]);
useEffect(() => { useEffect(() => {
cargarInyecciones(); cargarInyecciones();
@ -30,7 +32,18 @@ export default function InyeccionesVista() {
const cargarInyecciones = async () => { const cargarInyecciones = async () => {
const { data, error } = await supabaseClient const { data, error } = await supabaseClient
.from("inyeccion") .from("inyeccion")
.select("*") .select(`
id,
nombre,
descripcion,
horas,
inyeccion_competencia_inyeccion (
competencia_inyeccion (
id,
descripcion
)
)
`)
.order("id", { ascending: true }); .order("id", { ascending: true });
if (error) { if (error) {
setModalMensaje("Error al cargar inyecciones: " + error.message); 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); setInyeccionEditando(inyeccion.id);
setNuevoNombre(inyeccion.nombre); setNuevoNombre(inyeccion.nombre);
setNuevaDescripcion(inyeccion.descripcion); setNuevaDescripcion(inyeccion.descripcion);
setNuevaHoras(inyeccion.horas); 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 = () => { const cancelarEdicion = () => {
@ -52,6 +78,7 @@ export default function InyeccionesVista() {
setNuevoNombre(""); setNuevoNombre("");
setNuevaDescripcion(""); setNuevaDescripcion("");
setNuevaHoras(""); setNuevaHoras("");
setCompetenciasEditando([]);
}; };
const guardarEdicion = async (id) => { const guardarEdicion = async (id) => {
@ -64,12 +91,28 @@ export default function InyeccionesVista() {
}) })
.eq("id", id); .eq("id", id);
if (error) { if (!error) {
setModalMensaje("Error al actualizar la inyección"); // Elimina relaciones viejas
} else { 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"); setModalMensaje("Inyección actualizada exitosamente");
await cargarInyecciones(); await cargarInyecciones();
cancelarEdicion(); cancelarEdicion();
} else {
setModalMensaje("Error al actualizar la inyección");
} }
setMostrarModal(true); setMostrarModal(true);
}; };
@ -98,22 +141,25 @@ export default function InyeccionesVista() {
return ( return (
<Layout> <Layout>
<div className="w-full 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 Inyecciones</h1> <h1 className="text-2xl font-semibold mb-6 text-black">
Lista de Inyecciones
</h1>
<div className="overflow-x-auto w-full"> <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> <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">ID</th>
<th className="py-2 border-b">Nombre</th> <th className="py-2 border-b">Nombre</th>
<th className="py-2 border-b">Descripción</th> <th className="py-2 border-b">Descripción</th>
<th className="py-2 border-b">Horas</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> <th className="py-2 border-b">Acciones</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{inyecciones.map((inyeccion) => {inyecciones.map((inyeccion) =>
inyeccionEditando === inyeccion.id ? ( inyeccionEditando === inyeccion.id ? (
<tr key={inyeccion.id}> <tr key={inyeccion.id} className="text-black">
<td className="py-2 px-4 border-b text-center"> <td className="py-2 px-4 border-b text-center">
{inyeccion.id} {inyeccion.id}
</td> </td>
@ -121,12 +167,14 @@ export default function InyeccionesVista() {
<Input <Input
value={nuevoNombre} value={nuevoNombre}
onChange={(e) => setNuevoNombre(e.target.value)} onChange={(e) => setNuevoNombre(e.target.value)}
className="text-black"
/> />
</td> </td>
<td className="py-2 px-4 border-b"> <td className="py-2 px-4 border-b">
<Input <Input
value={nuevaDescripcion} value={nuevaDescripcion}
onChange={(e) => setNuevaDescripcion(e.target.value)} onChange={(e) => setNuevaDescripcion(e.target.value)}
className="text-black"
/> />
</td> </td>
<td className="py-2 px-4 border-b"> <td className="py-2 px-4 border-b">
@ -134,8 +182,71 @@ export default function InyeccionesVista() {
type="number" type="number"
value={nuevaHoras} value={nuevaHoras}
onChange={(e) => setNuevaHoras(e.target.value)} onChange={(e) => setNuevaHoras(e.target.value)}
className="text-black"
/> />
</td> </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"> <td className="py-2 px-4 border-b flex justify-center">
<Button <Button
className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-3 m-2 rounded" 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> </td>
</tr> </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.id}</td>
<td className="py-2 px-4 border-b">{inyeccion.nombre}</td> <td className="py-2 px-4 border-b">{inyeccion.nombre}</td>
<td className="py-2 px-4 border-b"> <td className="py-2 px-4 border-b">{inyeccion.descripcion}</td>
{inyeccion.descripcion}
</td>
<td className="py-2 px-4 border-b">{inyeccion.horas}</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"> <td className="py-2 px-4 border-b flex justify-center">
<Button <Button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-3 m-1 rounded" 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"> <DialogTitle className="text-black">
Confirmar eliminación Confirmar eliminación
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription className="text-black">
¿Estás seguro de que deseas eliminar esta inyección? Esta acción ¿Estás seguro de que deseas eliminar esta inyección? Esta acción
no se puede deshacer. no se puede deshacer.
</DialogDescription> </DialogDescription>
@ -214,10 +333,8 @@ export default function InyeccionesVista() {
<Dialog open={mostrarModal} onOpenChange={setMostrarModal}> <Dialog open={mostrarModal} onOpenChange={setMostrarModal}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle className="text-black"> <DialogTitle className="text-black">Aviso</DialogTitle>
Resultado de la operación <DialogDescription className="text-black">{modalMensaje}</DialogDescription>
</DialogTitle>
<DialogDescription>{modalMensaje}</DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<Button onClick={() => setMostrarModal(false)}>Cerrar</Button> <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") .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"),
//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"), cursoSeleccionado: z.string().nonempty("Selecciona una opción"),
}); });