Compare commits
218 Commits
d167589c61
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c744b8c89 | |||
| 18ba592378 | |||
| 1ede5bca32 | |||
| 80cc0a9f2b | |||
| 7b437c56e0 | |||
| b37d36fe15 | |||
| d3267e5890 | |||
| b77906e2e9 | |||
| 03cdd24c1d | |||
| a98982d228 | |||
| c710b85090 | |||
| 36b9c559fa | |||
| 434b50cfc3 | |||
| 51f967a955 | |||
| f5a4b0b9af | |||
| 8ae3469e10 | |||
| ddb83782a6 | |||
| 085e6fa0e9 | |||
| b3e9d63833 | |||
| 7a2f16b160 | |||
| 54200986ab | |||
| bfafb8999e | |||
| 883e6e7fa4 | |||
| dd11c2707e | |||
| 60c2e8402d | |||
| 350d7c0f69 | |||
| f2900f3d22 | |||
| 1f27ea6252 | |||
| 0e710f9637 | |||
| 3bb3a11779 | |||
| 75784658bf | |||
| d2f12d96de | |||
| 7f9c3162a0 | |||
| 03ef51d8a6 | |||
| f81b404223 | |||
| 614150f8d1 | |||
| 54a3b74836 | |||
| 0820bf19a5 | |||
| 7ddc5c7f1f | |||
| ad7a741d1e | |||
| 60bfa31186 | |||
| 1bd37c7e27 | |||
| 7eb9ca0bec | |||
| 10dc299311 | |||
| b3954ab16c | |||
| 88a2a28a8d | |||
| 36a11e3793 | |||
| 33efaed03f | |||
| d481e9706c | |||
| ee3b7a56ec | |||
| 2359e38f85 | |||
| ed318fa67b | |||
| c262cd16be | |||
| a08b2abf87 | |||
| a07213d959 | |||
| a2234e5022 | |||
| 658c392f96 | |||
| 4c730fa0ab | |||
| 2abe296b9e | |||
| 1bce226d15 | |||
| b986ec343e | |||
| 9fd816bfa1 | |||
| 379e2d3826 | |||
| cb5422f57c | |||
| 67724181fd | |||
| d9a5cec3c5 | |||
| 96848e1793 | |||
| cbaf96c6b5 | |||
| 0fb831fb58 | |||
| 0d1aa61022 | |||
| 84281a88f2 | |||
| d91018c612 | |||
| 658b2e245c | |||
| 30562fead0 | |||
| 2b91004129 | |||
| 96a045dc67 | |||
| a8229f12d5 | |||
| dd4ac5374a | |||
| 670e0b1d14 | |||
| 93fe247a19 | |||
| 32ebfde9ed | |||
| 32f0c4c4d4 | |||
| 6a520ef6b1 | |||
| 25d451839e | |||
| fe8f1d4753 | |||
| 518b1124d8 | |||
| 8bdaf935ca | |||
| 0d636cbf3b | |||
| 82d047e1c2 | |||
| 674c8a6bee | |||
| 3acea813b6 | |||
| e68954e03c | |||
| 296fbfee79 | |||
| a55910c226 | |||
| 88c6dc6b4d | |||
| 03caa791ad | |||
| 577daaff03 | |||
| f75680e8dd | |||
| 0b7f45c150 | |||
| 56ac8c0155 | |||
| 8ecb0f205a | |||
| ea842ee46c | |||
| 11369ce792 | |||
| 78471c19d9 | |||
| 3e8b8cd011 | |||
| 9eb7aae7d0 | |||
| e5afaa0c7c | |||
| 06bae3ba3e | |||
| 614ef3ffaf | |||
| 2c0c9e0ba4 | |||
| a07304c555 | |||
| ab2510ba1c | |||
| 4624c9add1 | |||
| 1b178dd2a8 | |||
| 203e8608a2 | |||
| a9f38e6d72 | |||
| 2c594fb9f7 | |||
| 98be1a0405 | |||
| 2165d4a976 | |||
| 772f3b6750 | |||
| e84e0abe8d | |||
| 37fab3ead6 | |||
| fa200acbfd | |||
| 020caf4e68 | |||
| 896c694a85 | |||
| 990daf5786 | |||
| c1197413db | |||
| bf2b8a9b6e | |||
| d6ecee7549 | |||
| 66bbf8ae17 | |||
| 6012d0ced8 | |||
| 314a96f2c5 | |||
| 7693f86951 | |||
| 8ad6c8096e | |||
| 28742615d8 | |||
| 0cb467cb78 | |||
| ff5ba3952d | |||
| f6b25ad86a | |||
| d7d4eff523 | |||
| 6773247b03 | |||
| ef614be2f1 | |||
| 2f0005baa7 | |||
| 3c37b5f313 | |||
| 4d1f102acb | |||
| f706456ff6 | |||
| b888bb22cd | |||
| 543e83609e | |||
| 4a8a9e1857 | |||
| d28b32d34e | |||
| 6db7c1c023 | |||
| cfc2153fa2 | |||
| f28804bb5b | |||
| f1d09a37ed | |||
| 3c63fdef69 | |||
| 3dc01c3fba | |||
| a51fa6b1fc | |||
| 1fddb75bf8 | |||
| ec994c9586 | |||
| 1acc37403d | |||
| f7ab1d61f0 | |||
| 6ed5d3541f | |||
| 3188a61431 | |||
| 5912a7c1fb | |||
| 1c45330da6 | |||
| 88ad7d74e3 | |||
| 08afd27f80 | |||
| b0a89ac57f | |||
| 440147edec | |||
| d415344ee7 | |||
| 5a0f7acac3 | |||
| f882d8c89d | |||
| 517b9497f1 | |||
| eb95dec097 | |||
| cf4caa2857 | |||
| 2de1e4237c | |||
| 7472e2cf97 | |||
| 15f60b7751 | |||
| 50c00293cd | |||
| 99ed75b2eb | |||
| cd16b3cb4f | |||
| 8444f2a87e | |||
| 02c415a91d | |||
| 7d45eb4dfa | |||
| 54b22b7adf | |||
| d4a034c2fc | |||
| 56d23f1aa5 | |||
| 13d9f1fe4a | |||
| 2624b0694d | |||
| 04909513bb | |||
| 5f8d758f67 | |||
| 41aecc4a45 | |||
| 1e4a58e4da | |||
| 2157ffe3bc | |||
| c280faef22 | |||
| d6c567195a | |||
| 9c588cfd8f | |||
| 46d8d6142e | |||
| 07d08e1b57 | |||
| ded54c18dd | |||
| 89f264bf5d | |||
| 675c76db74 | |||
| d74807c84e | |||
| 4d0f5815eb | |||
| 2f9e779bce | |||
| 0c57bdfc38 | |||
| 2250a1afd1 | |||
| 9102e756cb | |||
| e788eb788f | |||
| 2ec222694d | |||
| 58d4ee8b6e | |||
| d9a6852f43 | |||
| ba188329dc | |||
| 777be81d2a | |||
| 3afce0de77 | |||
| 4b8ec2c5ab | |||
| 0788002c9b | |||
| c7c631a701 | |||
| 9ba94f2c2c |
@@ -0,0 +1,37 @@
|
|||||||
|
name: Deploy to Azure Static Web Apps
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
VITE_SUPABASE_URL: ${{ vars.VITE_SUPABASE_URL }}
|
||||||
|
VITE_SUPABASE_ANON_KEY: ${{ vars.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
run: bunx --bun vite build
|
||||||
|
|
||||||
|
# No hace falta instalar el CLI globalmente, usamos bunx
|
||||||
|
- name: Deploy to Azure Static Web Apps
|
||||||
|
env:
|
||||||
|
AZURE_SWA_DEPLOYMENT_TOKEN: ${{ secrets.AZURE_SWA_DEPLOYMENT_TOKEN }}
|
||||||
|
run: |
|
||||||
|
bunx @azure/static-web-apps-cli deploy ./dist \
|
||||||
|
--env production \
|
||||||
|
--deployment-token "$AZURE_SWA_DEPLOYMENT_TOKEN"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Ignora los problemas de imports de eslint
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "acad-ia-2",
|
"name": "acad-ia-2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/react": "^0.3.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
@@ -30,18 +31,21 @@
|
|||||||
"@tanstack/router-plugin": "^1.132.0",
|
"@tanstack/router-plugin": "^1.132.0",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
|
"citeproc": "^2.4.63",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"motion": "^12.24.7",
|
"motion": "^12.24.7",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6",
|
||||||
"use-debounce": "^10.1.0",
|
"use-debounce": "^10.1.0",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/devtools-vite": "^0.3.11",
|
"@tanstack/devtools-vite": "^0.3.11",
|
||||||
@@ -135,6 +139,18 @@
|
|||||||
|
|
||||||
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
|
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
|
||||||
|
|
||||||
|
"@dnd-kit/abstract": ["@dnd-kit/abstract@0.3.2", "", { "dependencies": { "@dnd-kit/geometry": "^0.3.2", "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-uvPVK+SZYD6Viddn9M0K0JQdXknuVSxA/EbMlFRanve3P/XTc18oLa5zGftKSGjfQGmuzkZ34E26DSbly1zi3Q=="],
|
||||||
|
|
||||||
|
"@dnd-kit/collision": ["@dnd-kit/collision@0.3.2", "", { "dependencies": { "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/geometry": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-pNmNSLCI8S9fNQ7QJ3fBCDjiT0sqBhUFcKgmyYaGvGCAU+kq0AP8OWlh0JSisc9k5mFyxmRpmFQcnJpILz/RPA=="],
|
||||||
|
|
||||||
|
"@dnd-kit/dom": ["@dnd-kit/dom@0.3.2", "", { "dependencies": { "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/collision": "^0.3.2", "@dnd-kit/geometry": "^0.3.2", "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-cIUAVgt2szQyz6JRy7I+0r+xeyOAGH21Y15hb5bIyHoDEaZBvIDH+OOlD9eoLjCbsxDLN9WloU2CBi3OE6LYDg=="],
|
||||||
|
|
||||||
|
"@dnd-kit/geometry": ["@dnd-kit/geometry@0.3.2", "", { "dependencies": { "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-3UBPuIS7E3oGiHxOE8h810QA+0pnrnCtGxl4Os1z3yy5YkC/BEYGY+TxWPTQaY1/OMV7GCX7ZNMlama2QN3n3w=="],
|
||||||
|
|
||||||
|
"@dnd-kit/react": ["@dnd-kit/react@0.3.2", "", { "dependencies": { "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/dom": "^0.3.2", "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-1Opg1xw6I75Z95c+rF2NJa0pdGb8rLAENtuopKtJ1J0PudWlz+P6yL137xy/6DV43uaRmNGtsdbMbR0yRYJ72g=="],
|
||||||
|
|
||||||
|
"@dnd-kit/state": ["@dnd-kit/state@0.3.2", "", { "dependencies": { "@preact/signals-core": "^1.10.0", "tslib": "^2.6.2" } }, "sha512-dLUIkoYrIJhGXfF2wGLTfb46vUokEsO/OoE21TSfmahYrx7ysTmnwbePsznFaHlwgZhQEh6AlLvthLCeY21b1A=="],
|
||||||
|
|
||||||
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||||
@@ -247,14 +263,22 @@
|
|||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||||
|
|
||||||
|
"@preact/signals-core": ["@preact/signals-core@1.14.0", "", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="],
|
||||||
|
|
||||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
|
||||||
|
|
||||||
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
|
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
|
||||||
|
|
||||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="],
|
||||||
|
|
||||||
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="],
|
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
|
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
|
||||||
@@ -281,12 +305,24 @@
|
|||||||
|
|
||||||
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],
|
||||||
|
|
||||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||||
|
|
||||||
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
|
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
|
||||||
|
|
||||||
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
|
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="],
|
||||||
|
|
||||||
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
|
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
|
||||||
|
|
||||||
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
||||||
@@ -297,6 +333,10 @@
|
|||||||
|
|
||||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
||||||
|
|
||||||
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
|
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
|
||||||
@@ -305,10 +345,22 @@
|
|||||||
|
|
||||||
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
|
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
|
||||||
|
|
||||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="],
|
||||||
|
|
||||||
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||||
@@ -699,6 +751,8 @@
|
|||||||
|
|
||||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||||
|
|
||||||
|
"citeproc": ["citeproc@2.4.63", "", {}, "sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q=="],
|
||||||
|
|
||||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
@@ -1161,6 +1215,8 @@
|
|||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
|
||||||
|
|
||||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
||||||
@@ -1271,7 +1327,7 @@
|
|||||||
|
|
||||||
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
|
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
|
||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||||
|
|
||||||
@@ -1351,6 +1407,8 @@
|
|||||||
|
|
||||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
|
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
|
||||||
|
|
||||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||||
|
|
||||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||||
@@ -1419,6 +1477,8 @@
|
|||||||
|
|
||||||
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-form/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
@@ -1431,6 +1491,8 @@
|
|||||||
|
|
||||||
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toolbar/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
||||||
|
|
||||||
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||||
@@ -1487,6 +1549,14 @@
|
|||||||
|
|
||||||
"is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
|
"radix-ui/@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
|
||||||
|
|
||||||
|
"radix-ui/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
||||||
|
|
||||||
|
"radix-ui/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
||||||
|
|
||||||
|
"radix-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|||||||
+6
-2
@@ -17,6 +17,7 @@
|
|||||||
"ci:verify": "prettier --check . && eslint . && tsc --noEmit"
|
"ci:verify": "prettier --check . && eslint . && tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/react": "^0.3.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
@@ -43,18 +44,21 @@
|
|||||||
"@tanstack/router-plugin": "^1.132.0",
|
"@tanstack/router-plugin": "^1.132.0",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
|
"citeproc": "^2.4.63",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"motion": "^12.24.7",
|
"motion": "^12.24.7",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6",
|
||||||
"use-debounce": "^10.1.0"
|
"use-debounce": "^10.1.0",
|
||||||
|
"vaul": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/devtools-vite": "^0.3.11",
|
"@tanstack/devtools-vite": "^0.3.11",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -0,0 +1,757 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<locale xmlns="http://purl.org/net/xbiblio/csl" version="1.0" xml:lang="es-MX">
|
||||||
|
<info>
|
||||||
|
<translator>
|
||||||
|
<name>Juan Ignacio Flores Salgado</name>
|
||||||
|
<uri>https://www.mendeley.com/profiles/juan-ignacio-flores-salgado/</uri>
|
||||||
|
</translator>
|
||||||
|
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
|
||||||
|
<updated>2025-10-16T03:24:00+00:00</updated>
|
||||||
|
</info>
|
||||||
|
<style-options punctuation-in-quote="false"/>
|
||||||
|
<date form="text">
|
||||||
|
<date-part name="day" prefix="el " suffix=" de "/>
|
||||||
|
<date-part name="month" suffix=" de "/>
|
||||||
|
<date-part name="year"/>
|
||||||
|
</date>
|
||||||
|
<date form="numeric">
|
||||||
|
<date-part name="day" form="numeric-leading-zeros" suffix="/"/>
|
||||||
|
<date-part name="month" form="numeric-leading-zeros" suffix="/"/>
|
||||||
|
<date-part name="year"/>
|
||||||
|
</date>
|
||||||
|
<terms>
|
||||||
|
<!-- LONG GENERAL TERMS -->
|
||||||
|
<term name="accessed">consultado</term>
|
||||||
|
<term name="advance-online-publication">advance online publication</term>
|
||||||
|
<term name="album">album</term>
|
||||||
|
<term name="and">y</term>
|
||||||
|
<term name="and others">et al.</term>
|
||||||
|
<term name="anonymous">anónimo</term>
|
||||||
|
<term name="at">en</term>
|
||||||
|
<term name="audio-recording">audio recording</term>
|
||||||
|
<term name="available at">disponible en</term>
|
||||||
|
<term name="by">de</term>
|
||||||
|
<term name="circa">circa</term>
|
||||||
|
<term name="cited">citado</term>
|
||||||
|
<term name="et-al">et al.</term>
|
||||||
|
<term name="film">film</term>
|
||||||
|
<term name="forthcoming">en preparación</term>
|
||||||
|
<term name="from">a partir de</term>
|
||||||
|
<term name="henceforth">henceforth</term>
|
||||||
|
<term name="ibid">ibid.</term>
|
||||||
|
<term name="in">en</term>
|
||||||
|
<term name="in press">en imprenta</term>
|
||||||
|
<term name="internet">internet</term>
|
||||||
|
<term name="letter">carta</term>
|
||||||
|
<term name="loc-cit">loc. cit.</term> <!-- like ibid., the abbreviated form is the regular form -->
|
||||||
|
<term name="no date">sin fecha</term>
|
||||||
|
<term name="no-place">no place</term>
|
||||||
|
<term name="no-publisher">no publisher</term> <!-- sine nomine -->
|
||||||
|
<term name="on">on</term>
|
||||||
|
<term name="online">en línea</term>
|
||||||
|
<term name="op-cit">op. cit.</term> <!-- like ibid., the abbreviated form is the regular form -->
|
||||||
|
<term name="original-work-published">obra original publicada en</term>
|
||||||
|
<term name="personal-communication">comunicación personal</term>
|
||||||
|
<term name="podcast">podcast</term>
|
||||||
|
<term name="podcast-episode">podcast episode</term>
|
||||||
|
<term name="preprint">preprint</term>
|
||||||
|
<term name="presented at">presentado en</term>
|
||||||
|
<term name="radio-broadcast">radio broadcast</term>
|
||||||
|
<term name="radio-series">radio series</term>
|
||||||
|
<term name="radio-series-episode">radio series episode</term>
|
||||||
|
<term name="reference">
|
||||||
|
<single>referencia</single>
|
||||||
|
<multiple>referencias</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="retrieved">recuperado</term>
|
||||||
|
<term name="review-of">review of</term>
|
||||||
|
<term name="scale">escala</term>
|
||||||
|
<term name="special-issue">special issue</term>
|
||||||
|
<term name="special-section">special section</term>
|
||||||
|
<term name="television-broadcast">television broadcast</term>
|
||||||
|
<term name="television-series">television series</term>
|
||||||
|
<term name="television-series-episode">television series episode</term>
|
||||||
|
<term name="video">video</term>
|
||||||
|
<term name="working-paper">working paper</term>
|
||||||
|
|
||||||
|
<!-- SHORT GENERAL TERMS -->
|
||||||
|
<term name="anonymous" form="short">anón.</term>
|
||||||
|
<term name="circa" form="short">c.</term>
|
||||||
|
<term name="no date" form="short">s/f</term>
|
||||||
|
<term name="no-place" form="short">n.p.</term>
|
||||||
|
<term name="no-publisher" form="short">n.p.</term>
|
||||||
|
<term name="reference" form="short">
|
||||||
|
<single>ref.</single>
|
||||||
|
<multiple>refs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="review-of" form="short">rev. of</term>
|
||||||
|
|
||||||
|
<!-- SYMBOLIC GENERAL FORMS -->
|
||||||
|
|
||||||
|
<!-- LONG ITEM TYPE FORMS -->
|
||||||
|
<term name="article">preprint</term>
|
||||||
|
<term name="article-journal">journal article</term>
|
||||||
|
<term name="article-magazine">magazine article</term>
|
||||||
|
<term name="article-newspaper">newspaper article</term>
|
||||||
|
<term name="bill">bill</term>
|
||||||
|
<!-- book is in the list of locator terms -->
|
||||||
|
<term name="broadcast">broadcast</term>
|
||||||
|
<!-- chapter is in the list of locator terms -->
|
||||||
|
<term name="classic">classic</term>
|
||||||
|
<term name="collection">collection</term>
|
||||||
|
<term name="dataset">dataset</term>
|
||||||
|
<term name="document">document</term>
|
||||||
|
<term name="entry">entry</term>
|
||||||
|
<term name="entry-dictionary">dictionary entry</term>
|
||||||
|
<term name="entry-encyclopedia">encyclopedia entry</term>
|
||||||
|
<term name="event">event</term>
|
||||||
|
<!-- figure is in the list of locator terms -->
|
||||||
|
<term name="graphic">graphic</term>
|
||||||
|
<term name="hearing">hearing</term>
|
||||||
|
<term name="interview">entrevista</term>
|
||||||
|
<term name="legal_case">legal case</term>
|
||||||
|
<term name="legislation">legislation</term>
|
||||||
|
<term name="manuscript">manuscript</term>
|
||||||
|
<term name="map">map</term>
|
||||||
|
<term name="motion_picture">video recording</term>
|
||||||
|
<term name="musical_score">musical score</term>
|
||||||
|
<term name="pamphlet">pamphlet</term>
|
||||||
|
<term name="paper-conference">conference paper</term>
|
||||||
|
<term name="patent">patent</term>
|
||||||
|
<term name="performance">performance</term>
|
||||||
|
<term name="periodical">periodical</term>
|
||||||
|
<term name="personal_communication">comunicación personal</term>
|
||||||
|
<term name="post">post</term>
|
||||||
|
<term name="post-weblog">blog post</term>
|
||||||
|
<term name="regulation">regulation</term>
|
||||||
|
<term name="report">report</term>
|
||||||
|
<term name="review">review</term>
|
||||||
|
<term name="review-book">book review</term>
|
||||||
|
<term name="software">software</term>
|
||||||
|
<term name="song">audio recording</term>
|
||||||
|
<term name="speech">presentation</term>
|
||||||
|
<term name="standard">standard</term>
|
||||||
|
<term name="thesis">thesis</term>
|
||||||
|
<term name="treaty">treaty</term>
|
||||||
|
<term name="webpage">webpage</term>
|
||||||
|
|
||||||
|
<!-- SHORT ITEM TYPE FORMS -->
|
||||||
|
<term name="article-journal" form="short">journal art.</term>
|
||||||
|
<term name="article-magazine" form="short">mag. art.</term>
|
||||||
|
<term name="article-newspaper" form="short">newspaper art.</term>
|
||||||
|
<!-- book is in the list of locator terms -->
|
||||||
|
<!-- chapter is in the list of locator terms -->
|
||||||
|
<term name="document" form="short">doc.</term>
|
||||||
|
<!-- figure is in the list of locator terms -->
|
||||||
|
<term name="graphic" form="short">graph.</term>
|
||||||
|
<term name="interview" form="short">interv.</term>
|
||||||
|
<term name="manuscript" form="short">MS</term>
|
||||||
|
<term name="motion_picture" form="short">video rec.</term>
|
||||||
|
<term name="report" form="short">rep.</term>
|
||||||
|
<term name="review" form="short">rev.</term>
|
||||||
|
<term name="review-book" form="short">bk. rev.</term>
|
||||||
|
<term name="song" form="short">audio rec.</term>
|
||||||
|
|
||||||
|
<!-- LONG VERB ITEM TYPE FORMS -->
|
||||||
|
<!-- Only where applicable -->
|
||||||
|
<term name="hearing" form="verb">testimony of</term>
|
||||||
|
<term name="review" form="verb">review of</term>
|
||||||
|
<term name="review-book" form="verb">review of the book</term>
|
||||||
|
|
||||||
|
<!-- SHORT VERB ITEM TYPE FORMS -->
|
||||||
|
|
||||||
|
<!-- HISTORICAL ERA TERMS -->
|
||||||
|
<term name="ad">d. C.</term>
|
||||||
|
<term name="bc">a. C.</term>
|
||||||
|
<term name="bce">BCE</term>
|
||||||
|
<term name="ce">CE</term>
|
||||||
|
|
||||||
|
<!-- PUNCTUATION -->
|
||||||
|
<term name="open-quote">“</term>
|
||||||
|
<term name="close-quote">”</term>
|
||||||
|
<term name="open-inner-quote">‘</term>
|
||||||
|
<term name="close-inner-quote">’</term>
|
||||||
|
<term name="page-range-delimiter">–</term>
|
||||||
|
<term name="colon">:</term>
|
||||||
|
<term name="comma">,</term>
|
||||||
|
<term name="semicolon">;</term>
|
||||||
|
|
||||||
|
<!-- ORDINALS -->
|
||||||
|
<term name="ordinal">a</term>
|
||||||
|
<term name="ordinal-01" gender-form="feminine" match="whole-number">a</term>
|
||||||
|
<term name="ordinal-01" gender-form="masculine" match="whole-number">o</term>
|
||||||
|
|
||||||
|
<!-- LONG ORDINALS -->
|
||||||
|
<term name="long-ordinal-01">primera</term>
|
||||||
|
<term name="long-ordinal-02">segunda</term>
|
||||||
|
<term name="long-ordinal-03">tercera</term>
|
||||||
|
<term name="long-ordinal-04">cuarta</term>
|
||||||
|
<term name="long-ordinal-05">quinta</term>
|
||||||
|
<term name="long-ordinal-06">sexta</term>
|
||||||
|
<term name="long-ordinal-07">séptima</term>
|
||||||
|
<term name="long-ordinal-08">octava</term>
|
||||||
|
<term name="long-ordinal-09">novena</term>
|
||||||
|
<term name="long-ordinal-10">décima</term>
|
||||||
|
|
||||||
|
<!-- LONG LOCATOR FORMS -->
|
||||||
|
<term name="act">
|
||||||
|
<single>act</single>
|
||||||
|
<multiple>acts</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="appendix">
|
||||||
|
<single>appendix</single>
|
||||||
|
<multiple>appendices</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="article-locator">
|
||||||
|
<single>article</single>
|
||||||
|
<multiple>articles</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="book">
|
||||||
|
<single>libro</single>
|
||||||
|
<multiple>libros</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="canon">
|
||||||
|
<single>canon</single>
|
||||||
|
<multiple>canons</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="chapter">
|
||||||
|
<single>capítulo</single>
|
||||||
|
<multiple>capítulos</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="column">
|
||||||
|
<single>columna</single>
|
||||||
|
<multiple>columnas</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="elocation">
|
||||||
|
<single>location</single>
|
||||||
|
<multiple>locations</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="equation">
|
||||||
|
<single>equation</single>
|
||||||
|
<multiple>equations</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="figure">
|
||||||
|
<single>figura</single>
|
||||||
|
<multiple>figuras</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="folio">
|
||||||
|
<single>folio</single>
|
||||||
|
<multiple>folios</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="issue">
|
||||||
|
<single>número</single>
|
||||||
|
<multiple>números</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="line">
|
||||||
|
<single>línea</single>
|
||||||
|
<multiple>líneas</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="note">
|
||||||
|
<single>nota</single>
|
||||||
|
<multiple>notas</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="opus">
|
||||||
|
<single>opus</single>
|
||||||
|
<multiple>opera</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="page">
|
||||||
|
<single>página</single>
|
||||||
|
<multiple>páginas</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="paragraph">
|
||||||
|
<single>párrafo</single>
|
||||||
|
<multiple>párrafos</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="part">
|
||||||
|
<single>parte</single>
|
||||||
|
<multiple>partes</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="rule">
|
||||||
|
<single>rule</single>
|
||||||
|
<multiple>rules</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="scene">
|
||||||
|
<single>scene</single>
|
||||||
|
<multiple>scenes</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="section">
|
||||||
|
<single>sección</single>
|
||||||
|
<multiple>secciones</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="sub-verbo">
|
||||||
|
<single>sub voce</single>
|
||||||
|
<multiple>sub vocibus</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="supplement">
|
||||||
|
<single>supplement</single>
|
||||||
|
<multiple>supplements</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="table">
|
||||||
|
<single>table</single>
|
||||||
|
<multiple>tables</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="timestamp"> <!-- generally blank -->
|
||||||
|
<single/>
|
||||||
|
<multiple/>
|
||||||
|
</term>
|
||||||
|
<term name="title-locator">
|
||||||
|
<single>title</single>
|
||||||
|
<multiple>titles</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="verse">
|
||||||
|
<single>verso</single>
|
||||||
|
<multiple>versos</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="volume">
|
||||||
|
<single>volumen</single>
|
||||||
|
<multiple>volúmenes</multiple>
|
||||||
|
</term>
|
||||||
|
|
||||||
|
<!-- SHORT LOCATOR FORMS -->
|
||||||
|
<term name="appendix" form="short">
|
||||||
|
<single>app.</single>
|
||||||
|
<multiple>apps.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="article-locator" form="short">
|
||||||
|
<single>art.</single>
|
||||||
|
<multiple>arts.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="book" form="short">
|
||||||
|
<single>lib.</single>
|
||||||
|
<multiple>libs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="chapter" form="short">
|
||||||
|
<single>cap.</single>
|
||||||
|
<multiple>caps.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="column" form="short">
|
||||||
|
<single>col.</single>
|
||||||
|
<multiple>cols.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="elocation" form="short">
|
||||||
|
<single>loc.</single>
|
||||||
|
<multiple>locs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="equation" form="short">
|
||||||
|
<single>eq.</single>
|
||||||
|
<multiple>eqs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="figure" form="short">
|
||||||
|
<single>fig.</single>
|
||||||
|
<multiple>figs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="folio" form="short">
|
||||||
|
<single>f.</single>
|
||||||
|
<multiple>ff.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="issue" form="short">
|
||||||
|
<single>núm.</single>
|
||||||
|
<multiple>núms.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="line" form="short">
|
||||||
|
<single>l.</single>
|
||||||
|
<multiple>ls.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="note" form="short">
|
||||||
|
<single>n.</single>
|
||||||
|
<multiple>nn.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="opus" form="short">
|
||||||
|
<single>op.</single>
|
||||||
|
<multiple>opp.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="page" form="short">
|
||||||
|
<single>p.</single>
|
||||||
|
<multiple>pp.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="paragraph" form="short">
|
||||||
|
<single>párr.</single>
|
||||||
|
<multiple>párrs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="part" form="short">
|
||||||
|
<single>pt.</single>
|
||||||
|
<multiple>pts.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="rule" form="short">
|
||||||
|
<single>r.</single>
|
||||||
|
<multiple>rr.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="scene" form="short">
|
||||||
|
<single>sc.</single>
|
||||||
|
<multiple>scs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="section" form="short">
|
||||||
|
<single>sec.</single>
|
||||||
|
<multiple>secs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="sub-verbo" form="short">
|
||||||
|
<single>s. v.</single>
|
||||||
|
<multiple>s. vv.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="supplement" form="short">
|
||||||
|
<single>supp.</single>
|
||||||
|
<multiple>supps.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="table" form="short">
|
||||||
|
<single>tbl.</single>
|
||||||
|
<multiple>tbls.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="timestamp" form="short"> <!-- generally blank -->
|
||||||
|
<single/>
|
||||||
|
<multiple/>
|
||||||
|
</term>
|
||||||
|
<term name="title-locator" form="short">
|
||||||
|
<single>tit.</single>
|
||||||
|
<multiple>tits.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="verse" form="short">
|
||||||
|
<single>v.</single>
|
||||||
|
<multiple>vv.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="volume" form="short">
|
||||||
|
<single>vol.</single>
|
||||||
|
<multiple>vols.</multiple>
|
||||||
|
</term>
|
||||||
|
|
||||||
|
<!-- SYMBOLIC LOCATOR FORMS -->
|
||||||
|
<term name="paragraph" form="symbol">
|
||||||
|
<single>¶</single>
|
||||||
|
<multiple>¶</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="section" form="symbol">
|
||||||
|
<single>§</single>
|
||||||
|
<multiple>§</multiple>
|
||||||
|
</term>
|
||||||
|
|
||||||
|
<!-- LONG NUMBER VARIABLE FORMS -->
|
||||||
|
<term name="chapter-number">
|
||||||
|
<single>chapter</single>
|
||||||
|
<multiple>chapters</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="citation-number">
|
||||||
|
<single>citation</single>
|
||||||
|
<multiple>citations</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="collection-number">
|
||||||
|
<single>número</single>
|
||||||
|
<multiple>números</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="edition">
|
||||||
|
<single>edición</single>
|
||||||
|
<multiple>ediciones</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="first-reference-note-number">
|
||||||
|
<single>reference</single>
|
||||||
|
<multiple>references</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="number">
|
||||||
|
<single>number</single>
|
||||||
|
<multiple>numbers</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="number-of-pages">
|
||||||
|
<single>página</single>
|
||||||
|
<multiple>páginas</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="number-of-volumes">
|
||||||
|
<single>volume</single>
|
||||||
|
<multiple>volumes</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="page-first">
|
||||||
|
<single>page</single>
|
||||||
|
<multiple>pages</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="printing">
|
||||||
|
<single>printing</single>
|
||||||
|
<multiple>printings</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="version">versión</term>
|
||||||
|
|
||||||
|
<!-- SHORT NUMBER VARIABLE FORMS -->
|
||||||
|
<term name="chapter-number" form="short">
|
||||||
|
<single>chap.</single>
|
||||||
|
<multiple>chaps.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="citation-number" form="short">
|
||||||
|
<single>cit.</single>
|
||||||
|
<multiple>cits.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="collection-number" form="short">
|
||||||
|
<single>núm.</single>
|
||||||
|
<multiple>núms.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="edition" form="short">
|
||||||
|
<single>ed.</single>
|
||||||
|
<multiple>eds.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="first-reference-note-number" form="short">
|
||||||
|
<single>ref.</single>
|
||||||
|
<multiple>refs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="number" form="short">
|
||||||
|
<single>no.</single>
|
||||||
|
<multiple>nos.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="number-of-pages" form="short">
|
||||||
|
<single>p.</single>
|
||||||
|
<multiple>pp.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="number-of-volumes" form="short">
|
||||||
|
<single>vol.</single>
|
||||||
|
<multiple>vols.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="page-first" form="short">
|
||||||
|
<single>p.</single>
|
||||||
|
<multiple>pp.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="printing" form="short">
|
||||||
|
<single>print.</single>
|
||||||
|
<multiple>prints.</multiple>
|
||||||
|
</term>
|
||||||
|
|
||||||
|
<!-- LONG ROLE FORMS -->
|
||||||
|
<term name="author"/> <!-- generally blank -->
|
||||||
|
<term name="chair">
|
||||||
|
<single>chair</single>
|
||||||
|
<multiple>chairs</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="collection-editor">
|
||||||
|
<single>ed.</single>
|
||||||
|
<multiple>eds.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="compiler">
|
||||||
|
<single>compiler</single>
|
||||||
|
<multiple>compilers</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="composer"/> <!-- generally blank -->
|
||||||
|
<term name="container-author"/> <!-- generally blank -->
|
||||||
|
<term name="contributor">
|
||||||
|
<single>contributor</single>
|
||||||
|
<multiple>contributors</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="curator">
|
||||||
|
<single>curator</single>
|
||||||
|
<multiple>curators</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="director">
|
||||||
|
<single>director</single>
|
||||||
|
<multiple>directores</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="editor">
|
||||||
|
<single>editor</single>
|
||||||
|
<multiple>editores</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="editor-translator">
|
||||||
|
<single>editor y traductor</single>
|
||||||
|
<multiple>editores y traductores</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="editortranslator">
|
||||||
|
<single>editor y traductor</single>
|
||||||
|
<multiple>editores y traductores</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="editorial-director">
|
||||||
|
<single>coordinador</single>
|
||||||
|
<multiple>coordinadores</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="executive-producer">
|
||||||
|
<single>executive producer</single>
|
||||||
|
<multiple>executive producers</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="guest">
|
||||||
|
<single>guest</single>
|
||||||
|
<multiple>guests</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="host">
|
||||||
|
<single>host</single>
|
||||||
|
<multiple>hosts</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="illustrator">
|
||||||
|
<single>ilustrador</single>
|
||||||
|
<multiple>ilustradores</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="interviewer"/> <!-- generally blank -->
|
||||||
|
<term name="narrator">
|
||||||
|
<single>narrator</single>
|
||||||
|
<multiple>narrators</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="organizer">
|
||||||
|
<single>organizer</single>
|
||||||
|
<multiple>organizers</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="original-author"/> <!-- generally blank -->
|
||||||
|
<term name="performer">
|
||||||
|
<single>performer</single>
|
||||||
|
<multiple>performers</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="producer">
|
||||||
|
<single>producer</single>
|
||||||
|
<multiple>producers</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="recipient"/> <!-- generally blank -->
|
||||||
|
<term name="reviewed-author"/> <!-- generally blank -->
|
||||||
|
<term name="script-writer">
|
||||||
|
<single>writer</single>
|
||||||
|
<multiple>writers</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="series-creator">
|
||||||
|
<single>series creator</single>
|
||||||
|
<multiple>series creators</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="translator">
|
||||||
|
<single>traductor</single>
|
||||||
|
<multiple>traductores</multiple>
|
||||||
|
</term>
|
||||||
|
|
||||||
|
<!-- SHORT ROLE FORMS -->
|
||||||
|
<term name="compiler" form="short">
|
||||||
|
<single>comp.</single>
|
||||||
|
<multiple>comps.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="contributor" form="short">
|
||||||
|
<single>contrib.</single>
|
||||||
|
<multiple>contribs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="curator" form="short">
|
||||||
|
<single>cur.</single>
|
||||||
|
<multiple>curs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="director" form="short">
|
||||||
|
<single>dir.</single>
|
||||||
|
<multiple>dirs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="editor" form="short">
|
||||||
|
<single>ed.</single>
|
||||||
|
<multiple>eds.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="editor-translator" form="short">
|
||||||
|
<single>ed. y trad.</single>
|
||||||
|
<multiple>eds. y trads.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="editortranslator" form="short">
|
||||||
|
<single>ed. y trad.</single>
|
||||||
|
<multiple>eds. y trads.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="editorial-director" form="short">
|
||||||
|
<single>coord.</single>
|
||||||
|
<multiple>coords.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="executive-producer" form="short">
|
||||||
|
<single>exec. prod.</single>
|
||||||
|
<multiple>exec. prods.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="illustrator" form="short">
|
||||||
|
<single>ilust.</single>
|
||||||
|
<multiple>ilusts.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="narrator" form="short">
|
||||||
|
<single>narr.</single>
|
||||||
|
<multiple>narrs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="organizer" form="short">
|
||||||
|
<single>org.</single>
|
||||||
|
<multiple>orgs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="performer" form="short">
|
||||||
|
<single>perf.</single>
|
||||||
|
<multiple>perfs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="producer" form="short">
|
||||||
|
<single>prod.</single>
|
||||||
|
<multiple>prods.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="script-writer" form="short">
|
||||||
|
<single>writ.</single>
|
||||||
|
<multiple>writs.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="series-creator" form="short">
|
||||||
|
<single>cre.</single>
|
||||||
|
<multiple>cres.</multiple>
|
||||||
|
</term>
|
||||||
|
<term name="translator" form="short">
|
||||||
|
<single>trad.</single>
|
||||||
|
<multiple>trads.</multiple>
|
||||||
|
</term>
|
||||||
|
|
||||||
|
<!-- VERB ROLE FORMS -->
|
||||||
|
<term name="chair" form="verb">chaired by</term>
|
||||||
|
<term name="collection-editor" form="verb">edited by</term>
|
||||||
|
<term name="compiler" form="verb">compiled by</term>
|
||||||
|
<term name="container-author" form="verb">de</term>
|
||||||
|
<term name="contributor" form="verb">with</term>
|
||||||
|
<term name="curator" form="verb">curated by</term>
|
||||||
|
<term name="director" form="verb">dirigido por</term>
|
||||||
|
<term name="editor" form="verb">editado por</term>
|
||||||
|
<term name="editor-translator" form="verb">editado y traducido por</term>
|
||||||
|
<term name="editortranslator" form="verb">editado y traducido por</term>
|
||||||
|
<term name="editorial-director" form="verb">coordinado por</term>
|
||||||
|
<term name="executive-producer" form="verb">executive produced by</term>
|
||||||
|
<term name="guest" form="verb">with guest</term>
|
||||||
|
<term name="host" form="verb">hosted by</term>
|
||||||
|
<term name="illustrator" form="verb">ilustrado por</term>
|
||||||
|
<term name="interviewer" form="verb">entrevistado por</term>
|
||||||
|
<term name="narrator" form="verb">narrated by</term>
|
||||||
|
<term name="organizer" form="verb">organized by</term>
|
||||||
|
<term name="performer" form="verb">performed by</term>
|
||||||
|
<term name="producer" form="verb">produced by</term>
|
||||||
|
<term name="recipient" form="verb">a</term>
|
||||||
|
<term name="reviewed-author" form="verb">por</term>
|
||||||
|
<term name="script-writer" form="verb">written by</term>
|
||||||
|
<term name="series-creator" form="verb">created by</term>
|
||||||
|
<term name="translator" form="verb">traducido por</term>
|
||||||
|
|
||||||
|
<!-- SHORT VERB ROLE FORMS -->
|
||||||
|
<term name="collection-editor" form="verb-short">ed. by</term>
|
||||||
|
<term name="compiler" form="verb-short">comp. by</term>
|
||||||
|
<term name="contributor" form="verb-short">w.</term>
|
||||||
|
<term name="curator" form="verb-short">cur. by</term>
|
||||||
|
<term name="director" form="verb-short">dir.</term>
|
||||||
|
<term name="editor" form="verb-short">ed.</term>
|
||||||
|
<term name="editor-translator" form="verb-short">ed. y trad.</term>
|
||||||
|
<term name="editortranslator" form="verb-short">ed. y trad.</term>
|
||||||
|
<term name="editorial-director" form="verb-short">coord.</term>
|
||||||
|
<term name="executive-producer" form="verb-short">exec. prod. by</term>
|
||||||
|
<term name="guest" form="verb-short">w. guest</term>
|
||||||
|
<term name="host" form="verb-short">hosted by</term>
|
||||||
|
<term name="illustrator" form="verb-short">ilust.</term>
|
||||||
|
<term name="narrator" form="verb-short">narr. by</term>
|
||||||
|
<term name="organizer" form="verb-short">org. by</term>
|
||||||
|
<term name="performer" form="verb-short">perf. by</term>
|
||||||
|
<term name="producer" form="verb-short">prod. by</term>
|
||||||
|
<term name="script-writer" form="verb-short">writ. by</term>
|
||||||
|
<term name="series-creator" form="verb-short">cre. by</term>
|
||||||
|
<term name="translator" form="verb-short">trad.</term>
|
||||||
|
|
||||||
|
<!-- LONG MONTH FORMS -->
|
||||||
|
<term name="month-01">enero</term>
|
||||||
|
<term name="month-02">febrero</term>
|
||||||
|
<term name="month-03">marzo</term>
|
||||||
|
<term name="month-04">abril</term>
|
||||||
|
<term name="month-05">mayo</term>
|
||||||
|
<term name="month-06">junio</term>
|
||||||
|
<term name="month-07">julio</term>
|
||||||
|
<term name="month-08">agosto</term>
|
||||||
|
<term name="month-09">septiembre</term>
|
||||||
|
<term name="month-10">octubre</term>
|
||||||
|
<term name="month-11">noviembre</term>
|
||||||
|
<term name="month-12">diciembre</term>
|
||||||
|
|
||||||
|
<!-- SHORT MONTH FORMS -->
|
||||||
|
<term name="month-01" form="short">ene.</term>
|
||||||
|
<term name="month-02" form="short">feb.</term>
|
||||||
|
<term name="month-03" form="short">mar.</term>
|
||||||
|
<term name="month-04" form="short">abr.</term>
|
||||||
|
<term name="month-05" form="short">may</term>
|
||||||
|
<term name="month-06" form="short">jun.</term>
|
||||||
|
<term name="month-07" form="short">jul.</term>
|
||||||
|
<term name="month-08" form="short">ago.</term>
|
||||||
|
<term name="month-09" form="short">sep.</term>
|
||||||
|
<term name="month-10" form="short">oct.</term>
|
||||||
|
<term name="month-11" form="short">nov.</term>
|
||||||
|
<term name="month-12" form="short">dic.</term>
|
||||||
|
|
||||||
|
<!-- SEASONS -->
|
||||||
|
<term name="season-01">primavera</term>
|
||||||
|
<term name="season-02">verano</term>
|
||||||
|
<term name="season-03">otoño</term>
|
||||||
|
<term name="season-04">invierno</term>
|
||||||
|
</terms>
|
||||||
|
</locale>
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,519 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<style xmlns="http://purl.org/net/xbiblio/csl" class="in-text" version="1.0" demote-non-dropping-particle="sort-only">
|
||||||
|
<info>
|
||||||
|
<title>IEEE Reference Guide version 11.29.2023</title>
|
||||||
|
<title-short>Institute of Electrical and Electronics Engineers</title-short>
|
||||||
|
<id>http://www.zotero.org/styles/ieee</id>
|
||||||
|
<link href="http://www.zotero.org/styles/ieee" rel="self"/>
|
||||||
|
<link href="https://journals.ieeeauthorcenter.ieee.org/your-role-in-article-production/ieee-editorial-style-manual/" rel="documentation"/>
|
||||||
|
<author>
|
||||||
|
<name>Michael Berkowitz</name>
|
||||||
|
<email>mberkowi@gmu.edu</email>
|
||||||
|
</author>
|
||||||
|
<contributor>
|
||||||
|
<name>Julian Onions</name>
|
||||||
|
<email>julian.onions@gmail.com</email>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Rintze Zelle</name>
|
||||||
|
<uri>http://twitter.com/rintzezelle</uri>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Stephen Frank</name>
|
||||||
|
<uri>http://www.zotero.org/sfrank</uri>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Sebastian Karcher</name>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Giuseppe Silano</name>
|
||||||
|
<email>g.silano89@gmail.com</email>
|
||||||
|
<uri>http://giuseppesilano.net</uri>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Patrick O'Brien</name>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Brenton M. Wiernik</name>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Oliver Couch</name>
|
||||||
|
<email>oliver.couch@gmail.com</email>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Andrew Dunning</name>
|
||||||
|
<uri>https://orcid.org/0000-0003-0464-5036</uri>
|
||||||
|
</contributor>
|
||||||
|
<category citation-format="numeric"/>
|
||||||
|
<category field="engineering"/>
|
||||||
|
<category field="generic-base"/>
|
||||||
|
<summary>IEEE style as per the 2023 guidelines.</summary>
|
||||||
|
<updated>2024-03-27T11:41:27+00:00</updated>
|
||||||
|
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
|
||||||
|
</info>
|
||||||
|
<locale xml:lang="en">
|
||||||
|
<date form="text">
|
||||||
|
<date-part name="month" form="short" suffix=" "/>
|
||||||
|
<date-part name="day" form="numeric-leading-zeros" suffix=", "/>
|
||||||
|
<date-part name="year"/>
|
||||||
|
</date>
|
||||||
|
<terms>
|
||||||
|
<term name="chapter" form="short">ch.</term>
|
||||||
|
<term name="chapter-number" form="short">ch.</term>
|
||||||
|
<term name="presented at">presented at the</term>
|
||||||
|
<term name="available at">available</term>
|
||||||
|
<!-- always use three-letter abbreviations for months -->
|
||||||
|
<term name="month-06" form="short">Jun.</term>
|
||||||
|
<term name="month-07" form="short">Jul.</term>
|
||||||
|
<term name="month-09" form="short">Sep.</term>
|
||||||
|
</terms>
|
||||||
|
</locale>
|
||||||
|
<!-- Macros -->
|
||||||
|
<macro name="status">
|
||||||
|
<choose>
|
||||||
|
<if variable="page issue volume" match="none">
|
||||||
|
<text variable="status" text-case="capitalize-first" suffix="" font-weight="bold"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="edition">
|
||||||
|
<choose>
|
||||||
|
<if type="bill book chapter graphic legal_case legislation motion_picture paper-conference report song" match="any">
|
||||||
|
<choose>
|
||||||
|
<if is-numeric="edition">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<number variable="edition" form="ordinal"/>
|
||||||
|
<text term="edition" form="short"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<text variable="edition" text-case="capitalize-first" suffix="."/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="issued">
|
||||||
|
<choose>
|
||||||
|
<if type="article-journal report" match="any">
|
||||||
|
<date variable="issued">
|
||||||
|
<date-part name="month" form="short" suffix=" "/>
|
||||||
|
<date-part name="year" form="long"/>
|
||||||
|
</date>
|
||||||
|
</if>
|
||||||
|
<else-if type="bill book chapter graphic legal_case legislation song thesis" match="any">
|
||||||
|
<date variable="issued">
|
||||||
|
<date-part name="year" form="long"/>
|
||||||
|
</date>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="paper-conference" match="any">
|
||||||
|
<date variable="issued">
|
||||||
|
<date-part name="month" form="short"/>
|
||||||
|
<date-part name="year" prefix=" "/>
|
||||||
|
</date>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="motion_picture" match="any">
|
||||||
|
<date variable="issued" form="text" prefix="(" suffix=")"/>
|
||||||
|
</else-if>
|
||||||
|
<else>
|
||||||
|
<date variable="issued" form="text"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="author">
|
||||||
|
<names variable="author">
|
||||||
|
<name and="text" et-al-min="7" et-al-use-first="1" initialize-with=". "/>
|
||||||
|
<label form="short" prefix=", " text-case="capitalize-first"/>
|
||||||
|
<et-al font-style="italic"/>
|
||||||
|
<substitute>
|
||||||
|
<names variable="editor"/>
|
||||||
|
<names variable="translator"/>
|
||||||
|
<text macro="director"/>
|
||||||
|
</substitute>
|
||||||
|
</names>
|
||||||
|
</macro>
|
||||||
|
<macro name="editor">
|
||||||
|
<names variable="editor">
|
||||||
|
<name initialize-with=". " delimiter=", " and="text"/>
|
||||||
|
<label form="short" prefix=", " text-case="capitalize-first"/>
|
||||||
|
</names>
|
||||||
|
</macro>
|
||||||
|
<macro name="director">
|
||||||
|
<names variable="director">
|
||||||
|
<name and="text" et-al-min="7" et-al-use-first="1" initialize-with=". "/>
|
||||||
|
<et-al font-style="italic"/>
|
||||||
|
</names>
|
||||||
|
</macro>
|
||||||
|
<macro name="locators">
|
||||||
|
<group delimiter=", ">
|
||||||
|
<text macro="edition"/>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text term="volume" form="short"/>
|
||||||
|
<number variable="volume" form="numeric"/>
|
||||||
|
</group>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<number variable="number-of-volumes" form="numeric"/>
|
||||||
|
<text term="volume" form="short" plural="true"/>
|
||||||
|
</group>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text term="issue" form="short"/>
|
||||||
|
<number variable="issue" form="numeric"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="title">
|
||||||
|
<choose>
|
||||||
|
<if type="bill book graphic legal_case legislation motion_picture song standard software" match="any">
|
||||||
|
<text variable="title" font-style="italic"/>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<text variable="title" quotes="true"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="publisher">
|
||||||
|
<choose>
|
||||||
|
<if type="bill book chapter graphic legal_case legislation motion_picture paper-conference song" match="any">
|
||||||
|
<group delimiter=": ">
|
||||||
|
<text variable="publisher-place"/>
|
||||||
|
<text variable="publisher"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<group delimiter=", ">
|
||||||
|
<text variable="publisher"/>
|
||||||
|
<text variable="publisher-place"/>
|
||||||
|
</group>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="event">
|
||||||
|
<choose>
|
||||||
|
<!-- Published Conference Paper -->
|
||||||
|
<if type="paper-conference speech" match="any">
|
||||||
|
<choose>
|
||||||
|
<if variable="container-title" match="any">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text term="in"/>
|
||||||
|
<text variable="container-title" font-style="italic"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
<!-- Unpublished Conference Paper -->
|
||||||
|
<else>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text term="presented at"/>
|
||||||
|
<text variable="event"/>
|
||||||
|
</group>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="access">
|
||||||
|
<choose>
|
||||||
|
<if type="webpage post post-weblog" match="any">
|
||||||
|
<!-- https://url.com/ (accessed Mon. DD, YYYY). -->
|
||||||
|
<choose>
|
||||||
|
<if variable="URL">
|
||||||
|
<group delimiter=". " prefix=" ">
|
||||||
|
<group delimiter=": ">
|
||||||
|
<text term="accessed" text-case="capitalize-first"/>
|
||||||
|
<date variable="accessed" form="text"/>
|
||||||
|
</group>
|
||||||
|
<text term="online" prefix="[" suffix="]" text-case="capitalize-first"/>
|
||||||
|
<group delimiter=": ">
|
||||||
|
<text term="available at" text-case="capitalize-first"/>
|
||||||
|
<text variable="URL"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</if>
|
||||||
|
<else-if match="any" variable="DOI">
|
||||||
|
<!-- doi: 10.1000/xyz123. -->
|
||||||
|
<text variable="DOI" prefix=" doi: " suffix="."/>
|
||||||
|
</else-if>
|
||||||
|
<else-if variable="URL">
|
||||||
|
<!-- Accessed: Mon. DD, YYYY. [Medium]. Available: https://URL.com/ -->
|
||||||
|
<group delimiter=". " prefix=" " suffix=". ">
|
||||||
|
<!-- Accessed: Mon. DD, YYYY. -->
|
||||||
|
<group delimiter=": ">
|
||||||
|
<text term="accessed" text-case="capitalize-first"/>
|
||||||
|
<date variable="accessed" form="text"/>
|
||||||
|
</group>
|
||||||
|
<!-- [Online Video]. -->
|
||||||
|
<group prefix="[" suffix="]" delimiter=" ">
|
||||||
|
<choose>
|
||||||
|
<if variable="medium" match="any">
|
||||||
|
<text variable="medium" text-case="capitalize-first"/>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<text term="online" text-case="capitalize-first"/>
|
||||||
|
<choose>
|
||||||
|
<if type="motion_picture">
|
||||||
|
<text term="video" text-case="capitalize-first"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<!-- Available: https://URL.com/ -->
|
||||||
|
<group delimiter=": " prefix=" ">
|
||||||
|
<text term="available at" text-case="capitalize-first"/>
|
||||||
|
<text variable="URL"/>
|
||||||
|
</group>
|
||||||
|
</else-if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="page">
|
||||||
|
<choose>
|
||||||
|
<if type="article-journal" variable="number" match="all">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text value="Art."/>
|
||||||
|
<text term="issue" form="short"/>
|
||||||
|
<text variable="number"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<label variable="page" form="short"/>
|
||||||
|
<text variable="page"/>
|
||||||
|
</group>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="citation-locator">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<choose>
|
||||||
|
<if locator="page">
|
||||||
|
<label variable="locator" form="short"/>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<label variable="locator" form="short" text-case="capitalize-first"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
<text variable="locator"/>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="geographic-location">
|
||||||
|
<group delimiter=", " suffix=".">
|
||||||
|
<choose>
|
||||||
|
<if variable="publisher-place">
|
||||||
|
<text variable="publisher-place" text-case="title"/>
|
||||||
|
</if>
|
||||||
|
<else-if variable="event-place">
|
||||||
|
<text variable="event-place" text-case="title"/>
|
||||||
|
</else-if>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<!-- Series -->
|
||||||
|
<macro name="collection">
|
||||||
|
<choose>
|
||||||
|
<if variable="collection-title" match="any">
|
||||||
|
<text term="in" suffix=" "/>
|
||||||
|
<group delimiter=", " suffix=". ">
|
||||||
|
<text variable="collection-title"/>
|
||||||
|
<text variable="collection-number" prefix="no. "/>
|
||||||
|
<text variable="volume" prefix="vol. "/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<!-- Citation -->
|
||||||
|
<citation>
|
||||||
|
<sort>
|
||||||
|
<key variable="citation-number"/>
|
||||||
|
</sort>
|
||||||
|
<layout delimiter=", ">
|
||||||
|
<group prefix="[" suffix="]" delimiter=", ">
|
||||||
|
<text variable="citation-number"/>
|
||||||
|
<text macro="citation-locator"/>
|
||||||
|
</group>
|
||||||
|
</layout>
|
||||||
|
</citation>
|
||||||
|
<!-- Bibliography -->
|
||||||
|
<bibliography entry-spacing="0" second-field-align="flush">
|
||||||
|
<layout>
|
||||||
|
<!-- Citation Number -->
|
||||||
|
<text variable="citation-number" prefix="[" suffix="]"/>
|
||||||
|
<!-- Author(s) -->
|
||||||
|
<text macro="author" suffix=", "/>
|
||||||
|
<!-- Rest of Citation -->
|
||||||
|
<choose>
|
||||||
|
<!-- Specific Formats -->
|
||||||
|
<if type="article-journal">
|
||||||
|
<group delimiter=", ">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text variable="container-title" font-style="italic" form="short"/>
|
||||||
|
<text macro="locators"/>
|
||||||
|
<text macro="page"/>
|
||||||
|
<text macro="issued"/>
|
||||||
|
<text macro="status"/>
|
||||||
|
</group>
|
||||||
|
<choose>
|
||||||
|
<if variable="URL DOI" match="none">
|
||||||
|
<text value="."/>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<text value=","/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
<text macro="access"/>
|
||||||
|
</if>
|
||||||
|
<else-if type="paper-conference speech" match="any">
|
||||||
|
<group delimiter=", " suffix=", ">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text macro="event"/>
|
||||||
|
<text macro="editor"/>
|
||||||
|
</group>
|
||||||
|
<text macro="collection"/>
|
||||||
|
<group delimiter=", " suffix=".">
|
||||||
|
<text macro="publisher"/>
|
||||||
|
<text macro="issued"/>
|
||||||
|
<text macro="page"/>
|
||||||
|
<text macro="status"/>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="chapter">
|
||||||
|
<group delimiter=", " suffix=".">
|
||||||
|
<text macro="title"/>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text term="in" suffix=" "/>
|
||||||
|
<text variable="container-title" font-style="italic"/>
|
||||||
|
</group>
|
||||||
|
<text macro="locators"/>
|
||||||
|
<text macro="editor"/>
|
||||||
|
<text macro="collection"/>
|
||||||
|
<text macro="publisher"/>
|
||||||
|
<text macro="issued"/>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<label variable="chapter-number" form="short"/>
|
||||||
|
<text variable="chapter-number"/>
|
||||||
|
</group>
|
||||||
|
<text macro="page"/>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="report">
|
||||||
|
<group delimiter=", " suffix=".">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text macro="publisher"/>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text variable="genre"/>
|
||||||
|
<text variable="number"/>
|
||||||
|
</group>
|
||||||
|
<text macro="issued"/>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="thesis">
|
||||||
|
<group delimiter=", " suffix=".">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text variable="genre"/>
|
||||||
|
<text macro="publisher"/>
|
||||||
|
<text macro="issued"/>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="software">
|
||||||
|
<group delimiter=". " suffix=".">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text macro="issued" prefix="(" suffix=")"/>
|
||||||
|
<text variable="genre"/>
|
||||||
|
<text macro="publisher"/>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="article">
|
||||||
|
<group delimiter=", " suffix=".">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text macro="issued"/>
|
||||||
|
<group delimiter=": ">
|
||||||
|
<text macro="publisher" font-style="italic"/>
|
||||||
|
<text variable="number"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="webpage post-weblog post" match="any">
|
||||||
|
<group delimiter=", " suffix=".">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text variable="container-title"/>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="patent">
|
||||||
|
<group delimiter=", ">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text variable="number"/>
|
||||||
|
<text macro="issued"/>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else-if>
|
||||||
|
<!-- Online Video -->
|
||||||
|
<else-if type="motion_picture">
|
||||||
|
<text macro="geographic-location" suffix=". "/>
|
||||||
|
<group delimiter=", " suffix=".">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text macro="issued"/>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="standard">
|
||||||
|
<group delimiter=", " suffix=".">
|
||||||
|
<text macro="title"/>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text variable="genre"/>
|
||||||
|
<text variable="number"/>
|
||||||
|
</group>
|
||||||
|
<text macro="geographic-location"/>
|
||||||
|
<text macro="issued"/>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else-if>
|
||||||
|
<!-- Generic/Fallback Formats -->
|
||||||
|
<else-if type="bill book graphic legal_case legislation report song" match="any">
|
||||||
|
<group delimiter=", " suffix=". ">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text macro="locators"/>
|
||||||
|
</group>
|
||||||
|
<text macro="collection"/>
|
||||||
|
<group delimiter=", " suffix=".">
|
||||||
|
<text macro="publisher"/>
|
||||||
|
<text macro="issued"/>
|
||||||
|
<text macro="page"/>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="article-magazine article-newspaper broadcast interview manuscript map patent personal_communication song speech thesis webpage" match="any">
|
||||||
|
<group delimiter=", " suffix=".">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text variable="container-title" font-style="italic"/>
|
||||||
|
<text macro="locators"/>
|
||||||
|
<text macro="publisher"/>
|
||||||
|
<text macro="page"/>
|
||||||
|
<text macro="issued"/>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else-if>
|
||||||
|
<else>
|
||||||
|
<group delimiter=", " suffix=". ">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text variable="container-title" font-style="italic"/>
|
||||||
|
<text macro="locators"/>
|
||||||
|
</group>
|
||||||
|
<text macro="collection"/>
|
||||||
|
<group delimiter=", " suffix=".">
|
||||||
|
<text macro="publisher"/>
|
||||||
|
<text macro="page"/>
|
||||||
|
<text macro="issued"/>
|
||||||
|
</group>
|
||||||
|
<text macro="access"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</layout>
|
||||||
|
</bibliography>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,520 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<style xmlns="http://purl.org/net/xbiblio/csl" class="in-text" delimiter-precedes-last="always" demote-non-dropping-particle="sort-only" initialize-with="" initialize-with-hyphen="false" name-as-sort-order="all" name-delimiter=", " names-delimiter=", " page-range-format="minimal" sort-separator=" " version="1.0">
|
||||||
|
<!-- This file was generated by the Style Variant Builder <https://github.com/citation-style-language/style-variant-builder>. To contribute changes, modify the template and regenerate variants. -->
|
||||||
|
<info>
|
||||||
|
<title>NLM/Vancouver: Citing Medicine 2nd edition (citation-sequence)</title>
|
||||||
|
<title-short>National Library of Medicine, ANSI/NISO Z39.29-2005 (R2010), ICMJE Recommendations/URMs (C-S)</title-short>
|
||||||
|
<id>http://www.zotero.org/styles/nlm-citation-sequence</id>
|
||||||
|
<link href="http://www.zotero.org/styles/nlm-citation-sequence" rel="self"/>
|
||||||
|
<link href="https://www.nlm.nih.gov/citingmedicine" rel="documentation"/>
|
||||||
|
<link href="https://www.nlm.nih.gov/bsd/uniform_requirements.html" rel="documentation"/>
|
||||||
|
<link href="https://www.icmje.org/recommendations/" rel="documentation"/>
|
||||||
|
<author>
|
||||||
|
<name>Michael Berkowitz</name>
|
||||||
|
<email>mberkowi@gmu.edu</email>
|
||||||
|
</author>
|
||||||
|
<author>
|
||||||
|
<name>Andrew Dunning</name>
|
||||||
|
<uri>https://orcid.org/0000-0003-0464-5036</uri>
|
||||||
|
</author>
|
||||||
|
<contributor>
|
||||||
|
<name>Petr Hlustik</name>
|
||||||
|
<uri>https://orcid.org/0000-0002-1951-0671</uri>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Sebastian Karcher</name>
|
||||||
|
<uri>https://orcid.org/0000-0001-8249-7388</uri>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Charles Parnot</name>
|
||||||
|
<uri>https://orcid.org/0000-0002-7346-5883</uri>
|
||||||
|
</contributor>
|
||||||
|
<contributor>
|
||||||
|
<name>Sean Takats</name>
|
||||||
|
<uri>https://orcid.org/0000-0002-7851-5069</uri>
|
||||||
|
</contributor>
|
||||||
|
<category citation-format="numeric"/>
|
||||||
|
<category field="generic-base"/>
|
||||||
|
<category field="medicine"/>
|
||||||
|
<category field="science"/>
|
||||||
|
<summary>Citing Medicine: The NLM Style Guide for Authors, Editors, and Publishers, 2nd edition (2015), based on ANSI/NISO Z39.29-2005 (R2010); citation-sequence system.</summary>
|
||||||
|
<updated>2026-02-18T15:24:08+00:00</updated>
|
||||||
|
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
|
||||||
|
</info>
|
||||||
|
<locale xml:lang="en">
|
||||||
|
<date delimiter=" " form="text">
|
||||||
|
<date-part name="year"/>
|
||||||
|
<date-part form="short" name="month" strip-periods="true"/>
|
||||||
|
<date-part name="day"/>
|
||||||
|
</date>
|
||||||
|
<terms>
|
||||||
|
<term name="available at">available from</term>
|
||||||
|
<term name="collection-editor">
|
||||||
|
<single>editor</single>
|
||||||
|
<multiple>editors</multiple>
|
||||||
|
</term>
|
||||||
|
<term form="short" name="month-06">Jun.</term>
|
||||||
|
<term form="short" name="month-07">Jul.</term>
|
||||||
|
<term form="short" name="month-09">Sep.</term>
|
||||||
|
<term name="presented at">presented at</term>
|
||||||
|
<term form="short" name="section">
|
||||||
|
<single>sect.</single>
|
||||||
|
<multiple>sects.</multiple>
|
||||||
|
</term>
|
||||||
|
<term form="short" name="supplement">
|
||||||
|
<single>suppl.</single>
|
||||||
|
<multiple>suppls.</multiple>
|
||||||
|
</term>
|
||||||
|
</terms>
|
||||||
|
</locale>
|
||||||
|
<locale xml:lang="fr">
|
||||||
|
<date delimiter=" " form="text">
|
||||||
|
<date-part name="day"/>
|
||||||
|
<date-part form="short" name="month" strip-periods="true"/>
|
||||||
|
<date-part name="year"/>
|
||||||
|
</date>
|
||||||
|
</locale>
|
||||||
|
<!-- Variable labels -->
|
||||||
|
<macro name="label-collection-number">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<choose>
|
||||||
|
<if is-numeric="collection-number">
|
||||||
|
<label form="short" variable="collection-number"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
<text variable="collection-number"/>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="label-edition">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<choose>
|
||||||
|
<if is-numeric="edition">
|
||||||
|
<number form="ordinal" variable="edition"/>
|
||||||
|
<label form="short" variable="edition"/>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<text variable="edition"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="label-number-of-pages">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text variable="number-of-pages"/>
|
||||||
|
<choose>
|
||||||
|
<if is-numeric="number-of-pages">
|
||||||
|
<label form="short" plural="never" variable="number-of-pages"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="label-page">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<label form="short" plural="never" variable="page"/>
|
||||||
|
<text variable="page"/>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="label-part-number-capitalized">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<choose>
|
||||||
|
<if is-numeric="part-number">
|
||||||
|
<!-- TODO: Replace with `part-number` label when CSL provides one -->
|
||||||
|
<text form="short" term="part" text-case="capitalize-first"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
<text variable="part-number"/>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="label-supplement-number">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<choose>
|
||||||
|
<if is-numeric="supplement-number">
|
||||||
|
<!-- TODO: Replace with `supplement-number` label when CSL provides one -->
|
||||||
|
<text form="short" strip-periods="true" term="supplement" text-case="capitalize-first"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
<text text-case="capitalize-first" variable="supplement-number"/>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="label-volume-capitalized">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<choose>
|
||||||
|
<if is-numeric="volume">
|
||||||
|
<label form="short" text-case="capitalize-first" variable="volume"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
<text variable="volume"/>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="author">
|
||||||
|
<names variable="author">
|
||||||
|
<label prefix=", "/>
|
||||||
|
<substitute>
|
||||||
|
<names variable="editor-translator"/>
|
||||||
|
<names variable="editor translator"/>
|
||||||
|
<names variable="editor"/>
|
||||||
|
<names variable="collection-editor"/>
|
||||||
|
</substitute>
|
||||||
|
</names>
|
||||||
|
</macro>
|
||||||
|
<macro name="title">
|
||||||
|
<choose>
|
||||||
|
<if type="webpage" variable="container-title">
|
||||||
|
<!-- `webpage` listed under `container-title` (Citing Medicine, ch. 25) -->
|
||||||
|
<text variable="container-title"/>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<text variable="title"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="content-type">
|
||||||
|
<text variable="genre"/>
|
||||||
|
</macro>
|
||||||
|
<macro name="type-of-medium">
|
||||||
|
<choose>
|
||||||
|
<if variable="medium">
|
||||||
|
<text text-case="capitalize-first" variable="medium"/>
|
||||||
|
</if>
|
||||||
|
<else-if match="any" type="chapter entry-dictionary entry-encyclopedia paper-conference"/>
|
||||||
|
<else-if variable="URL">
|
||||||
|
<text term="internet" text-case="capitalize-first"/>
|
||||||
|
</else-if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="container-preposition">
|
||||||
|
<choose>
|
||||||
|
<if match="any" type="chapter paper-conference entry-dictionary entry-encyclopedia">
|
||||||
|
<text term="in" text-case="capitalize-first"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="secondary-authors">
|
||||||
|
<names variable="editor">
|
||||||
|
<label prefix=", "/>
|
||||||
|
</names>
|
||||||
|
</macro>
|
||||||
|
<macro name="container-title">
|
||||||
|
<group delimiter=", ">
|
||||||
|
<choose>
|
||||||
|
<if type="webpage"/>
|
||||||
|
<else-if variable="container-title">
|
||||||
|
<group delimiter=". ">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<choose>
|
||||||
|
<if match="any" type="article-journal review review-book">
|
||||||
|
<text form="short" strip-periods="true" variable="container-title"/>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<text variable="container-title"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
<choose>
|
||||||
|
<if type="article-journal" variable="DOI"/>
|
||||||
|
<else-if type="article-journal" variable="PMID"/>
|
||||||
|
<else-if type="article-journal" variable="PMCID"/>
|
||||||
|
<else-if variable="URL">
|
||||||
|
<text prefix="[" suffix="]" term="internet" text-case="capitalize-first"/>
|
||||||
|
</else-if>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
<text macro="label-edition"/>
|
||||||
|
</group>
|
||||||
|
</else-if>
|
||||||
|
<!-- TODO: add `event-name` and `event-place` -->
|
||||||
|
<else-if match="any" type="bill legislation">
|
||||||
|
<group delimiter=". ">
|
||||||
|
<text variable="container-title"/>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text form="short" term="section" text-case="capitalize-first"/>
|
||||||
|
<text variable="section"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<text variable="number"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="speech">
|
||||||
|
<group delimiter=": ">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text text-case="capitalize-first" variable="genre"/>
|
||||||
|
<text term="presented at"/>
|
||||||
|
</group>
|
||||||
|
<text variable="event"/>
|
||||||
|
</group>
|
||||||
|
</else-if>
|
||||||
|
<else>
|
||||||
|
<group delimiter=", ">
|
||||||
|
<text macro="label-volume-capitalized"/>
|
||||||
|
<text variable="volume-title"/>
|
||||||
|
</group>
|
||||||
|
<group delimiter=", ">
|
||||||
|
<text macro="label-part-number-capitalized"/>
|
||||||
|
<text variable="part-title"/>
|
||||||
|
</group>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="place-of-publication">
|
||||||
|
<choose>
|
||||||
|
<if type="thesis">
|
||||||
|
<text prefix="[" suffix="]" variable="publisher-place"/>
|
||||||
|
</if>
|
||||||
|
<else-if type="speech"/>
|
||||||
|
<else>
|
||||||
|
<text variable="publisher-place"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="publisher">
|
||||||
|
<choose>
|
||||||
|
<!-- discard publisher for serial publications -->
|
||||||
|
<if match="none" type="article-journal article-magazine article-newspaper periodical post-weblog review review-book">
|
||||||
|
<group delimiter=": ">
|
||||||
|
<text macro="place-of-publication"/>
|
||||||
|
<text variable="publisher"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="date">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<choose>
|
||||||
|
<if match="any" type="article-journal article-magazine article-newspaper periodical post-weblog review review-book">
|
||||||
|
<group delimiter=":">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<date form="text" variable="issued"/>
|
||||||
|
<choose>
|
||||||
|
<if type="article-journal" variable="DOI"/>
|
||||||
|
<else-if type="article-journal" variable="PMID"/>
|
||||||
|
<else-if type="article-journal" variable="PMCID"/>
|
||||||
|
<else>
|
||||||
|
<text macro="date-of-citation"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
<choose>
|
||||||
|
<if type="article-newspaper">
|
||||||
|
<text variable="page"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
<else-if match="any" type="bill legislation">
|
||||||
|
<date form="text" variable="issued"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="report">
|
||||||
|
<date date-parts="year-month" form="text" variable="issued"/>
|
||||||
|
<text macro="date-of-citation"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="patent">
|
||||||
|
<group delimiter=", ">
|
||||||
|
<text variable="number"/>
|
||||||
|
<date date-parts="year" form="numeric" variable="issued"/>
|
||||||
|
</group>
|
||||||
|
<text macro="date-of-citation"/>
|
||||||
|
</else-if>
|
||||||
|
<else-if type="speech">
|
||||||
|
<group delimiter="; ">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<date form="text" variable="issued"/>
|
||||||
|
<text macro="date-of-citation"/>
|
||||||
|
</group>
|
||||||
|
<text variable="event-place"/>
|
||||||
|
</group>
|
||||||
|
</else-if>
|
||||||
|
<else>
|
||||||
|
<date date-parts="year" form="numeric" variable="issued"/>
|
||||||
|
<text macro="date-of-citation"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="identifier-serial">
|
||||||
|
<choose>
|
||||||
|
<if match="any" type="article-journal article-magazine periodical post-weblog review review-book">
|
||||||
|
<group delimiter=":">
|
||||||
|
<group>
|
||||||
|
<text variable="collection-title"/>
|
||||||
|
<text variable="volume"/>
|
||||||
|
<group delimiter=" " prefix="(" suffix=")">
|
||||||
|
<text variable="issue"/>
|
||||||
|
<text macro="label-supplement-number"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<text macro="location-pagination-serial"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="date-of-citation">
|
||||||
|
<choose>
|
||||||
|
<if variable="URL">
|
||||||
|
<group delimiter=" " prefix="[" suffix="]">
|
||||||
|
<text term="cited"/>
|
||||||
|
<date form="text" variable="accessed"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="location-pagination-monographic">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<choose>
|
||||||
|
<if match="any" type="article-journal article-magazine article-newspaper review review-book"/>
|
||||||
|
<else-if type="book">
|
||||||
|
<text macro="label-number-of-pages"/>
|
||||||
|
</else-if>
|
||||||
|
<else>
|
||||||
|
<text macro="label-page"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="location-pagination-serial">
|
||||||
|
<choose>
|
||||||
|
<if variable="number">
|
||||||
|
<text variable="number"/>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<text variable="page"/>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="webpage-part">
|
||||||
|
<choose>
|
||||||
|
<if type="webpage" variable="container-title">
|
||||||
|
<text variable="title"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="series">
|
||||||
|
<choose>
|
||||||
|
<if match="any" type="article-journal article-magazine article-newspaper periodical post-weblog review review-book"/>
|
||||||
|
<else-if variable="collection-title">
|
||||||
|
<group delimiter=". " prefix="(" suffix=")">
|
||||||
|
<names variable="collection-editor">
|
||||||
|
<label prefix=", "/>
|
||||||
|
</names>
|
||||||
|
<group delimiter="; ">
|
||||||
|
<text variable="collection-title"/>
|
||||||
|
<text macro="label-collection-number"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</else-if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="report-number">
|
||||||
|
<choose>
|
||||||
|
<if type="report">
|
||||||
|
<group delimiter=": ">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text term="report" text-case="capitalize-first"/>
|
||||||
|
<label form="short" text-case="capitalize-first" variable="number"/>
|
||||||
|
</group>
|
||||||
|
<text variable="number"/>
|
||||||
|
</group>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
</macro>
|
||||||
|
<macro name="availability">
|
||||||
|
<group delimiter=". ">
|
||||||
|
<group delimiter=": ">
|
||||||
|
<text text-case="capitalize-first" value="located at"/>
|
||||||
|
<group delimiter="; ">
|
||||||
|
<group delimiter=", ">
|
||||||
|
<text variable="archive_collection"/>
|
||||||
|
<text variable="archive"/>
|
||||||
|
<text variable="archive-place"/>
|
||||||
|
</group>
|
||||||
|
<text variable="archive_location"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<choose>
|
||||||
|
<if type="article-journal" variable="DOI"/>
|
||||||
|
<else-if type="article-journal" variable="PMID"/>
|
||||||
|
<else-if type="article-journal" variable="PMCID"/>
|
||||||
|
<else>
|
||||||
|
<group delimiter=": ">
|
||||||
|
<text term="available at" text-case="capitalize-first"/>
|
||||||
|
<text variable="URL"/>
|
||||||
|
</group>
|
||||||
|
</else>
|
||||||
|
</choose>
|
||||||
|
<text prefix="doi:" variable="DOI"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<macro name="notes">
|
||||||
|
<group delimiter=". " suffix=".">
|
||||||
|
<group delimiter="; ">
|
||||||
|
<group delimiter=": ">
|
||||||
|
<text value="PubMed PMID"/>
|
||||||
|
<text variable="PMID"/>
|
||||||
|
</group>
|
||||||
|
<group delimiter=": ">
|
||||||
|
<text value="PubMed Central PMCID"/>
|
||||||
|
<text variable="PMCID"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<text variable="references"/>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<citation collapse="citation-number">
|
||||||
|
<sort>
|
||||||
|
<key variable="citation-number"/>
|
||||||
|
</sort>
|
||||||
|
<layout delimiter="," prefix="(" suffix=")">
|
||||||
|
<text variable="citation-number"/>
|
||||||
|
</layout>
|
||||||
|
</citation>
|
||||||
|
<macro name="bibliography">
|
||||||
|
<group delimiter=" ">
|
||||||
|
<group delimiter=". " suffix=".">
|
||||||
|
<text macro="author"/>
|
||||||
|
<group delimiter=" ">
|
||||||
|
<text macro="title"/>
|
||||||
|
<text macro="content-type" prefix="[" suffix="]"/>
|
||||||
|
<choose>
|
||||||
|
<if type="webpage" variable="container-title">
|
||||||
|
<text macro="type-of-medium" prefix="[" suffix="]"/>
|
||||||
|
</if>
|
||||||
|
<else-if match="none" variable="container-title">
|
||||||
|
<text macro="type-of-medium" prefix="[" suffix="]"/>
|
||||||
|
</else-if>
|
||||||
|
</choose>
|
||||||
|
</group>
|
||||||
|
<choose>
|
||||||
|
<if match="none" variable="container-title">
|
||||||
|
<text macro="label-edition"/>
|
||||||
|
</if>
|
||||||
|
</choose>
|
||||||
|
<group delimiter=": ">
|
||||||
|
<text macro="container-preposition"/>
|
||||||
|
<group delimiter=". ">
|
||||||
|
<text macro="secondary-authors"/>
|
||||||
|
<text macro="container-title"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group delimiter="; ">
|
||||||
|
<text macro="publisher"/>
|
||||||
|
<group delimiter=";">
|
||||||
|
<text macro="date"/>
|
||||||
|
<text macro="identifier-serial"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<text macro="location-pagination-monographic"/>
|
||||||
|
<text macro="webpage-part"/>
|
||||||
|
<text macro="series"/>
|
||||||
|
<text macro="report-number"/>
|
||||||
|
</group>
|
||||||
|
<text macro="availability"/>
|
||||||
|
<text macro="notes"/>
|
||||||
|
</group>
|
||||||
|
</macro>
|
||||||
|
<bibliography et-al-min="7" et-al-use-first="6" second-field-align="flush">
|
||||||
|
<layout>
|
||||||
|
<text suffix="." variable="citation-number"/>
|
||||||
|
<text macro="bibliography"/>
|
||||||
|
</layout>
|
||||||
|
</bibliography>
|
||||||
|
</style>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 250 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,118 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 192.3 63.4" style="enable-background:new 0 0 192.3 63.4;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
.st1{fill:#FFFFFF;}
|
||||||
|
.st2{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<g id="Group_1247_1_">
|
||||||
|
<path id="Path_477_1_" class="st0" d="M50.7,50.6l4.4-7.8h-8.9l-12-21l-4.4,7.8l12,21C41.8,50.6,50.7,50.6,50.7,50.6z"/>
|
||||||
|
<path id="Path_478_1_" class="st0" d="M34.3,1h-9l4.4,7.8l-12,20.8h9.1l12-20.8L34.3,1z"/>
|
||||||
|
<path id="Path_479_1_" class="st0" d="M0,40.1l4.4,7.8l4.4-7.8h23.9l-4.4-7.8H4.4L0,40.1z"/>
|
||||||
|
<path id="Path_480_1_" class="st1" d="M56.7,40.1l4.4-7.8h-9L40.3,11.4l-4.4,7.8l12,20.8H56.7z"/>
|
||||||
|
<path id="Path_481_1_" class="st1" d="M22.3,1h-8.9l4.4,7.8l-12,20.8h9l12-20.8L22.3,1z"/>
|
||||||
|
<path id="Path_482_1_" class="st1" d="M5.9,50.6l4.4,7.8l4.4-7.8h23.9l-4.4-7.8H10.5L5.9,50.6z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st1" d="M67.9,3.9c0-0.8,0-1.6-0.1-2.4l1.7-0.1l0.1,0.1v6.3c0,0.7,0.1,1.2,0.5,1.6C70.6,9.8,71,10,71.7,10
|
||||||
|
c0.5,0,1.1-0.1,1.3-0.5c0.4-0.4,0.5-0.9,0.5-1.6V3.5c0-0.8,0-1.5-0.1-2.2l1.9-0.1v6.7c0,1.1-0.4,2-1.1,2.6
|
||||||
|
c-0.7,0.5-1.6,0.9-2.7,0.9c-1.1,0-2-0.3-2.7-0.9C68.2,10,67.8,9,67.8,7.9L67.9,3.9L67.9,3.9z"/>
|
||||||
|
<path class="st1" d="M83,11.3h-1.7V6.6c0-0.7-0.1-1.5-0.3-2.3L82.6,4c0.1,0.4,0.3,0.7,0.3,1.1C83.5,4.4,84.3,4,85.1,4
|
||||||
|
c0.7,0,1.1,0.1,1.5,0.5C87,5,87.1,5.5,87.1,6.2v5.1h-1.7V6.6c0-0.8-0.4-1.3-1.1-1.3c-0.4,0-0.9,0.1-1.3,0.5L83,11.3L83,11.3z"/>
|
||||||
|
<path class="st1" d="M95.1,1.6c0,0.3-0.1,0.7-0.4,0.8c-0.3,0.3-0.5,0.4-0.8,0.4S93.3,2.7,93,2.6c-0.1-0.1-0.3-0.4-0.3-0.7
|
||||||
|
s0.1-0.7,0.4-0.8c0.3-0.1,0.5-0.4,0.9-0.4c0.3,0,0.5,0.1,0.7,0.3S95.1,1.4,95.1,1.6z M93,11.3V6.6c0-0.8,0-1.6-0.1-2.4l1.7-0.3
|
||||||
|
L94.8,4v7.1L93,11.3L93,11.3z"/>
|
||||||
|
<path class="st1" d="M106.4,4.3l-2.3,7h-1.9l-2.3-7.1l1.9-0.1l0.9,3.6c0.3,1.1,0.5,1.9,0.5,2.4l0,0c0.1-0.5,0.3-1.3,0.7-2.4
|
||||||
|
l0.9-3.6L106.4,4.3L106.4,4.3z"/>
|
||||||
|
<path class="st1" d="M116.7,7.7l-0.3,0.3h-4c0,0.8,0.3,1.3,0.7,1.7c0.4,0.4,0.8,0.5,1.5,0.5c0.5,0,1.2-0.1,1.7-0.5l0.1,1.2
|
||||||
|
c-0.7,0.4-1.3,0.7-2.4,0.7c-1.1,0-1.9-0.3-2.6-0.9s-0.9-1.5-0.9-2.7s0.3-2,0.9-2.8s1.5-1.1,2.4-1.1c0.8,0,1.5,0.3,2,0.8
|
||||||
|
c0.5,0.5,0.8,1.2,0.8,2.2C116.7,7.3,116.7,7.5,116.7,7.7z M113.9,5.1c-0.4,0-0.8,0.1-0.9,0.5c-0.3,0.4-0.4,0.9-0.4,1.6l2.6-0.1
|
||||||
|
c0-0.1,0-0.3,0-0.5c0-0.4-0.1-0.8-0.3-1.1C114.6,5.3,114.3,5.1,113.9,5.1z"/>
|
||||||
|
<path class="st1" d="M124,11.3h-1.7V6.6c0-0.7-0.1-1.5-0.3-2.3l1.6-0.3c0.1,0.5,0.3,0.9,0.4,1.5c0.5-0.9,1.2-1.5,1.9-1.5
|
||||||
|
c0.3,0,0.5,0,0.7,0.1l-0.1,1.7c-0.3-0.1-0.5-0.1-0.8-0.1c-0.5,0-1.1,0.1-1.6,0.5C124,6.3,124,11.3,124,11.3z"/>
|
||||||
|
<path class="st1" d="M135.8,4.4l-0.1,1.3c-0.7-0.4-1.3-0.5-1.9-0.5c-0.4,0-0.7,0.1-0.8,0.3c-0.3,0.1-0.3,0.3-0.3,0.5
|
||||||
|
c0,0.3,0.1,0.4,0.4,0.7c0.3,0.1,0.5,0.4,0.8,0.5c0.3,0.1,0.7,0.3,1.1,0.4c0.4,0.1,0.7,0.4,0.8,0.7c0.3,0.3,0.4,0.7,0.4,1.1
|
||||||
|
c0,0.7-0.3,1.2-0.8,1.5c-0.5,0.4-1.2,0.5-2.2,0.5s-1.7-0.1-2.4-0.5l0.1-1.3c0.8,0.4,1.5,0.7,2.3,0.7c0.4,0,0.7-0.1,0.8-0.3
|
||||||
|
c0.1-0.1,0.3-0.3,0.3-0.5c0-0.3-0.1-0.4-0.4-0.7c-0.3-0.1-0.5-0.4-0.8-0.5s-0.7-0.3-0.9-0.4s-0.7-0.4-0.8-0.7
|
||||||
|
c-0.3-0.3-0.4-0.7-0.4-1.1c0-0.7,0.3-1.2,0.8-1.6c0.5-0.4,1.2-0.5,2-0.5C134.5,4,135.1,4.2,135.8,4.4z"/>
|
||||||
|
<path class="st1" d="M143.3,1.6c0,0.3-0.1,0.7-0.4,0.8c-0.3,0.1-0.5,0.4-0.8,0.4c-0.3,0-0.5-0.1-0.8-0.3c-0.1-0.1-0.1-0.4-0.1-0.7
|
||||||
|
s0.1-0.7,0.4-0.8c0.3-0.3,0.5-0.4,0.9-0.4c0.3,0,0.5,0.1,0.7,0.3S143.3,1.4,143.3,1.6z M141.3,11.3V6.6c0-0.8,0-1.6-0.1-2.4
|
||||||
|
l1.7-0.3l0.1,0.1v7.1L141.3,11.3L141.3,11.3z"/>
|
||||||
|
<path class="st1" d="M153,0.7l1.7-0.3l0.1,0.1v8.7c0,0.7,0.1,1.3,0.3,1.9l-1.6,0.1c-0.1-0.1-0.3-0.5-0.4-0.9l0,0
|
||||||
|
c-0.5,0.7-1.1,0.9-2,0.9c-0.9,0-1.5-0.3-2-0.9c-0.5-0.7-0.8-1.5-0.8-2.6c0-1.2,0.4-2.2,1.1-3c0.7-0.7,1.6-1.1,2.7-1.1
|
||||||
|
c0.3,0,0.5,0,0.9,0.1V3C153.2,2,153.2,1.4,153,0.7z M150.5,7.7c0,0.8,0.1,1.5,0.4,1.9c0.3,0.5,0.7,0.7,1.2,0.7
|
||||||
|
c0.4,0,0.8-0.1,1.1-0.4V5c-0.3,0-0.5-0.1-0.7-0.1c-0.7,0-1.1,0.3-1.5,0.7C150.6,6.2,150.5,6.9,150.5,7.7z"/>
|
||||||
|
<path class="st1" d="M166.1,9.3c0,0.7,0.1,1.3,0.3,2l-1.5,0.1c-0.1-0.3-0.3-0.5-0.4-0.9l0,0c-0.3,0.3-0.5,0.5-0.9,0.7
|
||||||
|
c-0.4,0.1-0.8,0.3-1.2,0.3c-0.5,0-1.1-0.1-1.3-0.4c-0.4-0.3-0.5-0.7-0.5-1.2c0-0.8,0.4-1.3,1.1-1.7c0.7-0.4,1.6-0.7,2.8-0.7V6.7
|
||||||
|
c0-0.8-0.4-1.3-1.3-1.3c-0.7,0-1.5,0.3-2.2,0.7l-0.1-1.3c0.9-0.4,1.7-0.5,2.7-0.5s1.6,0.3,2,0.7c0.4,0.4,0.7,0.9,0.7,1.7
|
||||||
|
c0,0.4,0,0.8,0,1.5C166.1,8.6,166.1,9,166.1,9.3z M162.2,9.4c0,0.3,0.1,0.5,0.3,0.7c0.1,0.1,0.4,0.3,0.7,0.3
|
||||||
|
c0.4,0,0.8-0.1,1.2-0.5V7.9c-0.7,0-1.1,0.3-1.5,0.4C162.3,8.6,162.2,9,162.2,9.4z"/>
|
||||||
|
<path class="st1" d="M175.6,0.7l1.7-0.3l0.1,0.1v8.7c0,0.7,0.1,1.3,0.3,1.9l-1.6,0.1c-0.1-0.1-0.3-0.5-0.4-0.9l0,0
|
||||||
|
c-0.5,0.7-1.1,0.9-2,0.9s-1.5-0.3-2-0.9c-0.5-0.7-0.8-1.5-0.8-2.6c0-1.2,0.4-2.2,1.1-3c0.7-0.7,1.6-1.1,2.7-1.1
|
||||||
|
c0.3,0,0.5,0,0.9,0.1V3C175.7,2,175.7,1.4,175.6,0.7z M173.1,7.7c0,0.8,0.1,1.5,0.4,1.9c0.3,0.5,0.7,0.7,1.2,0.7
|
||||||
|
c0.4,0,0.8-0.1,1.1-0.4V5c-0.3,0-0.5-0.1-0.7-0.1c-0.7,0-1.1,0.3-1.5,0.7C173.2,6.2,173.1,6.9,173.1,7.7z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st1" d="M78.8,51.2l0.3,0.3l1.2,11h-2l-0.4-4c-0.1-1.2-0.3-3-0.4-5l0,0c-0.3,1.2-0.7,2.8-1.2,5l-1.2,4h-2.3l-1.1-4
|
||||||
|
c-0.5-1.9-0.9-3.5-1.2-5l0,0c-0.1,1.2-0.3,2.8-0.4,5l-0.4,4h-1.7l1.2-11.2l2.7-0.1l1.3,4.6c0.4,1.6,0.8,3.2,1.1,4.8l0,0
|
||||||
|
c0.3-1.6,0.5-3.2,1.1-4.8l1.3-4.4L78.8,51.2z"/>
|
||||||
|
<path class="st1" d="M89.4,58.5l-0.3,0.3h-4.4c0.1,0.8,0.3,1.5,0.8,1.9c0.5,0.4,0.9,0.7,1.6,0.7s1.3-0.1,2-0.5l0.1,1.3
|
||||||
|
c-0.7,0.4-1.6,0.7-2.7,0.7c-1.2,0-2.2-0.4-3-1.1c-0.7-0.7-1.1-1.7-1.1-3c0-1.3,0.4-2.3,1.1-3.1c0.7-0.8,1.6-1.2,2.7-1.2
|
||||||
|
c0.9,0,1.7,0.3,2.3,0.9c0.5,0.5,0.8,1.3,0.8,2.4C89.4,58,89.4,58.4,89.4,58.5z M87.7,50.4l0.1,0.4c-0.7,0.9-1.5,1.9-2.6,2.7
|
||||||
|
l-0.8-0.1c0.7-1.1,1.1-2.2,1.3-3H87.7z M86.3,55.5c-0.4,0-0.8,0.3-1.1,0.7c-0.3,0.4-0.4,1.1-0.5,1.7l2.8-0.1c0-0.1,0-0.3,0-0.5
|
||||||
|
c0-0.5-0.1-0.9-0.3-1.2C87,55.7,86.7,55.5,86.3,55.5z"/>
|
||||||
|
<path class="st1" d="M96.4,62.5l-1.7-3.1L93,62.5h-1.6l-0.1-0.3l2.3-3.8l-2.3-4l2-0.3l1.7,3.4l1.6-3.4l1.6,0.1l0.1,0.3L96,58.4
|
||||||
|
l2.4,4h-2V62.5z"/>
|
||||||
|
<path class="st1" d="M103.1,51.8c0,0.4-0.1,0.7-0.4,0.9s-0.5,0.4-0.9,0.4c-0.4,0-0.7-0.1-0.8-0.4c-0.3-0.3-0.3-0.5-0.3-0.8
|
||||||
|
c0-0.4,0.1-0.7,0.4-0.9c0.3-0.3,0.5-0.4,0.9-0.4c0.3,0,0.5,0.1,0.8,0.3C103,51.1,103.1,51.4,103.1,51.8z M100.8,62.5v-5.2
|
||||||
|
c0-0.9,0-1.9-0.1-2.7l2-0.3l0.3,0.3v7.9H100.8z"/>
|
||||||
|
<path class="st1" d="M112.3,55l-0.4,1.6c-0.7-0.4-1.2-0.7-1.9-0.7c-0.5,0-1.1,0.3-1.5,0.7c-0.4,0.4-0.5,1.1-0.5,1.9
|
||||||
|
c0,0.9,0.1,1.6,0.7,2c0.4,0.5,0.9,0.8,1.7,0.8c0.5,0,1.2-0.1,1.7-0.5l0.1,1.3c-0.7,0.4-1.5,0.7-2.4,0.7c-1.2,0-2.2-0.4-2.8-1.1
|
||||||
|
s-1.1-1.7-1.1-3c0-1.3,0.4-2.3,1.1-3.1c0.8-0.8,1.7-1.2,2.8-1.2C110.8,54.3,111.6,54.6,112.3,55z"/>
|
||||||
|
<path class="st1" d="M121.4,58.5c0,1.3-0.4,2.4-1.1,3.1s-1.6,1.1-2.7,1.1c-1.1,0-1.9-0.4-2.6-1.1s-1.1-1.7-1.1-3
|
||||||
|
c0-1.3,0.4-2.4,1.1-3.1s1.6-1.2,2.7-1.2c1.1,0,2,0.4,2.7,1.1C121.1,56.2,121.4,57.3,121.4,58.5z M116,58.5c0,2,0.5,3,1.6,3
|
||||||
|
c0.5,0,0.9-0.3,1.2-0.8c0.3-0.5,0.4-1.2,0.4-2.2c0-2-0.5-3.1-1.6-3.1c-0.5,0-0.9,0.3-1.2,0.8C116.3,56.8,116,57.6,116,58.5z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st2" d="M83.5,41v3.8H68.3V25.3c0-2.4-0.1-4.6-0.4-6.5l4.6-0.4L73,19v22.2L83.5,41z"/>
|
||||||
|
<path class="st2" d="M100.4,39.5c0,1.9,0.3,3.6,0.8,5.1l-3.9,0.4c-0.4-0.7-0.8-1.6-1.1-2.6h-0.1c-0.5,0.7-1.3,1.3-2.3,1.9
|
||||||
|
c-0.9,0.5-2.2,0.8-3.2,0.8c-1.5,0-2.7-0.4-3.6-1.2c-0.9-0.8-1.3-1.9-1.3-3.4c0-2,0.9-3.6,2.8-4.6c1.9-1.1,4.3-1.6,7.4-1.7v-1.7
|
||||||
|
c0-2.3-1.2-3.4-3.5-3.4c-1.9,0-3.8,0.5-5.6,1.6l-0.3-3.6c2.4-0.9,4.7-1.5,7.1-1.5c2.3,0,4,0.5,5.2,1.6c1.2,1.1,1.7,2.6,1.7,4.6
|
||||||
|
c0,0.9,0,2.3,0,4C100.4,37.7,100.4,38.9,100.4,39.5z M90.2,39.8c0,0.7,0.3,1.3,0.7,1.7c0.4,0.5,1.1,0.7,1.7,0.7
|
||||||
|
c1.2,0,2.2-0.4,3.1-1.3v-5.1c-1.6,0.1-3,0.5-4,1.2C90.8,37.8,90.2,38.7,90.2,39.8z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st2" d="M126.3,37.3c0,2.4-0.8,4.2-2.6,5.5c-1.7,1.3-4,2-6.9,2c-3.1,0-5.5-0.5-7.3-1.5L110,39c0.9,0.5,2,1.1,3.5,1.3
|
||||||
|
c1.3,0.3,2.6,0.4,3.6,0.4c1.3,0,2.4-0.3,3.2-0.9c0.8-0.5,1.1-1.3,1.1-2.4c0-0.3,0-0.5-0.1-0.8c0-0.3-0.1-0.5-0.3-0.7
|
||||||
|
s-0.3-0.4-0.4-0.7c-0.1-0.3-0.4-0.4-0.4-0.5c-0.1-0.1-0.3-0.3-0.7-0.5c-0.3-0.3-0.5-0.4-0.7-0.4c-0.1-0.1-0.4-0.3-0.8-0.4
|
||||||
|
c-0.4-0.3-0.7-0.4-0.8-0.4c-0.1-0.1-0.4-0.3-0.9-0.4c-0.4-0.3-0.7-0.4-0.8-0.4c-0.9-0.4-1.6-0.8-2.2-1.2c-0.5-0.4-1.2-0.8-1.7-1.5
|
||||||
|
c-0.7-0.5-1.1-1.2-1.3-2c-0.3-0.8-0.4-1.6-0.4-2.7c0-2.4,0.9-4.2,2.7-5.5c1.7-1.3,4.2-2,7-2c2.4,0,4.4,0.4,6.2,1.1l-0.7,4.3
|
||||||
|
c-1.7-1.1-3.6-1.5-5.6-1.5c-1.5,0-2.6,0.3-3.4,0.9c-0.8,0.7-1.2,1.3-1.2,2.4c0,0.4,0,0.7,0.1,1.1c0.1,0.3,0.3,0.7,0.5,0.9
|
||||||
|
c0.3,0.3,0.5,0.5,0.7,0.8c0.1,0.1,0.5,0.4,0.9,0.7c0.5,0.3,0.8,0.5,1.1,0.5c0.1,0.1,0.5,0.3,1.2,0.7c0.7,0.3,1.1,0.5,1.2,0.5
|
||||||
|
c0.8,0.4,1.5,0.8,2.2,1.2c0.5,0.4,1.2,0.8,1.7,1.5c0.7,0.7,1.1,1.3,1.3,2.2C126.1,35.4,126.3,36.3,126.3,37.3z"/>
|
||||||
|
<path class="st2" d="M143.9,39.1c0,1.9,0.3,3.8,0.8,5.4l-4,0.4c-0.4-0.7-0.8-1.6-1.1-2.7h-0.1c-0.5,0.8-1.3,1.3-2.4,1.9
|
||||||
|
c-1.1,0.5-2.2,0.8-3.4,0.8c-1.6,0-2.8-0.4-3.8-1.2c-0.9-0.8-1.3-2-1.3-3.5c0-2.2,0.9-3.6,3-4.7c1.9-1.1,4.4-1.6,7.7-1.7v-1.9
|
||||||
|
c0-2.3-1.2-3.5-3.6-3.5c-2,0-3.9,0.5-5.9,1.7l-0.3-3.8c2.4-1.1,4.8-1.5,7.4-1.5c2.4,0,4.2,0.5,5.4,1.6c1.2,1.1,1.9,2.7,1.9,4.8
|
||||||
|
c0,0.9,0,2.3,0,4.2C143.9,37.1,143.9,38.3,143.9,39.1z M133.4,39.3c0,0.7,0.3,1.3,0.7,1.9c0.4,0.5,1.1,0.8,1.9,0.8
|
||||||
|
c1.2,0,2.3-0.4,3.2-1.3v-5.2c-1.7,0.1-3.1,0.5-4.2,1.2S133.4,38.2,133.4,39.3z"/>
|
||||||
|
<path class="st2" d="M153.7,44.4h-4.8V21.9c0-2-0.1-4.2-0.4-6.5l4.8-0.5l0.5,0.5L153.7,44.4L153.7,44.4z"/>
|
||||||
|
<path class="st2" d="M163.4,44.4h-4.8V21.9c0-2-0.1-4.2-0.4-6.5l4.8-0.5l0.5,0.5L163.4,44.4L163.4,44.4z"/>
|
||||||
|
<path class="st2" d="M183.7,34.7l-0.5,0.5h-10.9c0.1,2,0.8,3.6,1.7,4.6c0.9,0.9,2.4,1.5,4,1.5c1.6,0,3.2-0.4,4.8-1.3l0.3,3.2
|
||||||
|
c-1.7,1.1-3.9,1.6-6.5,1.6c-3,0-5.2-0.8-7-2.6s-2.6-4.2-2.6-7.3c0-3.1,0.8-5.6,2.6-7.5c1.7-1.9,3.9-2.8,6.6-2.8
|
||||||
|
c2.3,0,4.2,0.7,5.5,2.2c1.3,1.5,2,3.4,2,5.6C183.8,33.5,183.8,34.2,183.7,34.7z M176,27.6c-1.1,0-2,0.5-2.7,1.6
|
||||||
|
c-0.7,1.1-1.1,2.6-1.2,4.3l6.7-0.3c0-0.3,0-0.8,0-1.3c0-1.2-0.3-2.3-0.8-3.1S176.9,27.6,176,27.6z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path id="Path_483" class="st1" d="M187,39.4h1.5c0.3,0,0.7,0.1,1,0.2c0.3,0.2,0.5,0.5,0.5,0.8c0,0.3-0.1,0.6-0.3,0.8
|
||||||
|
c-0.1,0.1-0.3,0.2-0.5,0.3c0.4,0.1,0.6,0.3,0.6,0.8c0,0.4,0.1,0.9,0.3,1.3h-0.6c-0.1-0.4-0.2-0.7-0.2-1.1
|
||||||
|
c-0.1-0.6-0.2-0.8-0.9-0.8h-0.7v1.9H187V39.4 M187.5,41.2h0.9c0.2,0,0.4,0,0.6-0.1c0.2-0.1,0.3-0.3,0.3-0.6c0-0.7-0.6-0.7-0.8-0.7
|
||||||
|
h-0.9V41.2z"/>
|
||||||
|
<path id="Path_484" class="st1" d="M191.9,41.5c0,1.9-1.6,3.4-3.4,3.3s-3.4-1.6-3.3-3.4c0-1.9,1.5-3.3,3.4-3.3
|
||||||
|
C190.4,38.1,191.9,39.6,191.9,41.5z M188.5,37.8c-2.1,0-3.7,1.6-3.8,3.7c0,2.1,1.6,3.7,3.7,3.8c2.1,0,3.7-1.6,3.8-3.7c0,0,0,0,0,0
|
||||||
|
C192.2,39.4,190.5,37.8,188.5,37.8z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 10 KiB |
@@ -18,11 +18,7 @@ export default function Header() {
|
|||||||
</button>
|
</button>
|
||||||
<h1 className="ml-4 text-xl font-semibold">
|
<h1 className="ml-4 text-xl font-semibold">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<img
|
<img src="/lasalle-logo.svg" alt="La Salle Logo" className="h-10" />
|
||||||
src="/tanstack-word-logo-white.svg"
|
|
||||||
alt="TanStack Logo"
|
|
||||||
className="h-10"
|
|
||||||
/>
|
|
||||||
</Link>
|
</Link>
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,9 @@
|
|||||||
import {
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
Plus,
|
|
||||||
Search,
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
BookOpen,
|
import { useNavigate, useParams } from '@tanstack/react-router'
|
||||||
Trash2,
|
import { Plus, Search, BookOpen, Trash2, Library, Edit3 } from 'lucide-react'
|
||||||
Library,
|
import { useState } from 'react'
|
||||||
Edit3,
|
|
||||||
Save,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -38,40 +34,13 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
|
import {
|
||||||
|
useCreateBibliografia,
|
||||||
|
useDeleteBibliografia,
|
||||||
|
useSubjectBibliografia,
|
||||||
|
useUpdateBibliografia,
|
||||||
|
} from '@/data/hooks/useSubjects'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
// import { toast } from 'sonner';
|
|
||||||
// import { mockLibraryResources } from '@/data/mockAsignaturaData';
|
|
||||||
|
|
||||||
export const mockLibraryResources = [
|
|
||||||
{
|
|
||||||
id: 'lib-1',
|
|
||||||
titulo: 'Deep Learning',
|
|
||||||
autor: 'Goodfellow, I., Bengio, Y., & Courville, A.',
|
|
||||||
editorial: 'MIT Press',
|
|
||||||
anio: 2016,
|
|
||||||
isbn: '9780262035613',
|
|
||||||
disponible: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lib-2',
|
|
||||||
titulo: 'Artificial Intelligence: A Modern Approach',
|
|
||||||
autor: 'Russell, S., & Norvig, P.',
|
|
||||||
editorial: 'Pearson',
|
|
||||||
anio: 2020,
|
|
||||||
isbn: '9780134610993',
|
|
||||||
disponible: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lib-3',
|
|
||||||
titulo: 'Hands-On Machine Learning',
|
|
||||||
autor: 'Aurélien Géron',
|
|
||||||
editorial: "O'Reilly Media",
|
|
||||||
anio: 2019,
|
|
||||||
isbn: '9781492032649',
|
|
||||||
disponible: false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// --- Interfaces ---
|
// --- Interfaces ---
|
||||||
export interface BibliografiaEntry {
|
export interface BibliografiaEntry {
|
||||||
@@ -84,82 +53,80 @@ export interface BibliografiaEntry {
|
|||||||
fuenteBiblioteca?: any
|
fuenteBiblioteca?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BibliografiaTabProps {
|
export function BibliographyItem() {
|
||||||
id: string
|
const navigate = useNavigate()
|
||||||
bibliografia: Array<BibliografiaEntry>
|
const { planId, asignaturaId } = useParams({
|
||||||
onSave: (bibliografia: Array<BibliografiaEntry>) => void
|
from: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
isSaving: boolean
|
})
|
||||||
}
|
|
||||||
|
|
||||||
export function BibliographyItem({
|
// --- 1. Única fuente de verdad: La Query ---
|
||||||
bibliografia,
|
const { data: bibliografia = [], isLoading } =
|
||||||
id,
|
useSubjectBibliografia(asignaturaId)
|
||||||
onSave,
|
|
||||||
isSaving,
|
|
||||||
}: BibliografiaTabProps) {
|
|
||||||
console.log(id)
|
|
||||||
|
|
||||||
const { data: bibliografia2, isLoading: loadinasignatura } =
|
// --- 2. Mutaciones ---
|
||||||
useSubjectBibliografia(id)
|
const { mutate: crearBibliografia } = useCreateBibliografia()
|
||||||
const [entries, setEntries] = useState<Array<BibliografiaEntry>>(bibliografia)
|
const { mutate: actualizarBibliografia } = useUpdateBibliografia(asignaturaId)
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
const { mutate: eliminarBibliografia } = useDeleteBibliografia(asignaturaId)
|
||||||
|
|
||||||
|
// --- 3. Estados de UI (Solo para diálogos y edición) ---
|
||||||
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false)
|
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false)
|
||||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||||
const [editingId, setEditingId] = useState<string | null>(null)
|
const [editingId, setEditingId] = useState<string | null>(null)
|
||||||
const [newEntryType, setNewEntryType] = useState<'BASICA' | 'COMPLEMENTARIA'>(
|
|
||||||
'BASICA',
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
console.log('Datos actuales en el front:', bibliografia)
|
||||||
if (bibliografia2 && Array.isArray(bibliografia2)) {
|
// --- 4. Derivación de datos (Se calculan en cada render) ---
|
||||||
setEntries(bibliografia2)
|
const basicaEntries = bibliografia.filter((e) => e.tipo === 'BASICA')
|
||||||
} else if (bibliografia) {
|
const complementariaEntries = bibliografia.filter(
|
||||||
// Fallback a la prop inicial si la API no devuelve nada
|
|
||||||
setEntries(bibliografia)
|
|
||||||
}
|
|
||||||
}, [bibliografia2, bibliografia])
|
|
||||||
|
|
||||||
const basicaEntries = entries.filter((e) => e.tipo === 'BASICA')
|
|
||||||
const complementariaEntries = entries.filter(
|
|
||||||
(e) => e.tipo === 'COMPLEMENTARIA',
|
(e) => e.tipo === 'COMPLEMENTARIA',
|
||||||
)
|
)
|
||||||
console.log(bibliografia2)
|
|
||||||
|
|
||||||
const handleAddManual = (cita: string) => {
|
// --- Handlers Conectados a la Base de Datos ---
|
||||||
const newEntry: BibliografiaEntry = {
|
|
||||||
id: `manual-${Date.now()}`,
|
|
||||||
tipo: newEntryType,
|
|
||||||
cita,
|
|
||||||
}
|
|
||||||
setEntries([...entries, newEntry])
|
|
||||||
setIsAddDialogOpen(false)
|
|
||||||
// toast.success('Referencia manual añadida');
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddFromLibrary = (
|
const handleAddFromLibrary = (
|
||||||
resource: any,
|
resource: any,
|
||||||
tipo: 'BASICA' | 'COMPLEMENTARIA',
|
tipo: 'BASICA' | 'COMPLEMENTARIA',
|
||||||
) => {
|
) => {
|
||||||
const cita = `${resource.autor} (${resource.anio}). ${resource.titulo}. ${resource.editorial}.`
|
const cita = `${resource.autor} (${resource.anio}). ${resource.titulo}. ${resource.editorial}.`
|
||||||
const newEntry: BibliografiaEntry = {
|
crearBibliografia(
|
||||||
id: `lib-ref-${Date.now()}`,
|
{
|
||||||
tipo,
|
asignatura_id: asignaturaId,
|
||||||
cita,
|
tipo,
|
||||||
fuenteBibliotecaId: resource.id,
|
cita,
|
||||||
fuenteBiblioteca: resource,
|
tipo_fuente: 'BIBLIOTECA',
|
||||||
}
|
biblioteca_item_id: resource.id,
|
||||||
setEntries([...entries, newEntry])
|
},
|
||||||
setIsLibraryDialogOpen(false)
|
{
|
||||||
// toast.success('Añadido desde biblioteca');
|
onSuccess: () => setIsLibraryDialogOpen(false),
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateCita = (id: string, cita: string) => {
|
const handleUpdateCita = (id: string, nuevaCita: string) => {
|
||||||
setEntries(entries.map((e) => (e.id === id ? { ...e, cita } : e)))
|
actualizarBibliografia(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
updates: { cita: nuevaCita },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => setEditingId(null),
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onConfirmDelete = () => {
|
||||||
|
if (deleteId) {
|
||||||
|
eliminarBibliografia(deleteId, {
|
||||||
|
onSuccess: () => setDeleteId(null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return <div className="p-10 text-center">Cargando bibliografía...</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-in fade-in mx-auto max-w-5xl space-y-8 py-10 duration-500">
|
<div className="animate-in fade-in space-y-8 pb-8 duration-500">
|
||||||
<div className="flex items-center justify-between border-b pb-4">
|
<div className="flex flex-col gap-4 border-b pb-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold tracking-tight text-slate-900">
|
<h2 className="text-2xl font-bold tracking-tight text-slate-900">
|
||||||
Bibliografía
|
Bibliografía
|
||||||
@@ -184,34 +151,27 @@ export function BibliographyItem({
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<LibrarySearchDialog
|
<LibrarySearchDialog
|
||||||
|
// CORRECCIÓN: Usamos 'bibliografia' en lugar de 'bibliografia2'
|
||||||
|
resources={[]} // Aquí deberías pasar el catálogo general, no la bibliografía de la asignatura
|
||||||
onSelect={handleAddFromLibrary}
|
onSelect={handleAddFromLibrary}
|
||||||
existingIds={entries.map((e) => e.fuenteBibliotecaId || '')}
|
// CORRECCIÓN: Usamos 'bibliografia' en lugar de 'entries'
|
||||||
/>
|
existingIds={bibliografia.map(
|
||||||
</DialogContent>
|
(e) => e.biblioteca_item_id || '',
|
||||||
</Dialog>
|
)}
|
||||||
|
|
||||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="outline">
|
|
||||||
<Plus className="mr-2 h-4 w-4" /> Añadir manual
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<AddManualDialog
|
|
||||||
tipo={newEntryType}
|
|
||||||
onTypeChange={setNewEntryType}
|
|
||||||
onAdd={handleAddManual}
|
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onSave(entries)}
|
onClick={() =>
|
||||||
disabled={isSaving}
|
navigate({
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
to: `/planes/${planId}/asignaturas/${asignaturaId}/bibliografia/nueva`,
|
||||||
|
resetScroll: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="shadow-md"
|
||||||
>
|
>
|
||||||
<Save className="mr-2 h-4 w-4" />{' '}
|
<Plus className="mr-2 h-4 w-4" /> Agregar Bibliografía
|
||||||
{isSaving ? 'Guardando...' : 'Guardar'}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,13 +234,7 @@ export function BibliographyItem({
|
|||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction onClick={onConfirmDelete} className="bg-red-600">
|
||||||
onClick={() => {
|
|
||||||
setEntries(entries.filter((e) => e.id !== deleteId))
|
|
||||||
setDeleteId(null)
|
|
||||||
}}
|
|
||||||
className="bg-red-600"
|
|
||||||
>
|
|
||||||
Eliminar
|
Eliminar
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
@@ -390,57 +344,16 @@ function BibliografiaCard({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddManualDialog({ tipo, onTypeChange, onAdd }: any) {
|
function LibrarySearchDialog({ resources, onSelect, existingIds }: any) {
|
||||||
const [cita, setCita] = useState('')
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Referencia Manual</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs font-bold text-slate-500 uppercase">
|
|
||||||
Tipo
|
|
||||||
</label>
|
|
||||||
<Select value={tipo} onValueChange={onTypeChange}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="BASICA">Básica</SelectItem>
|
|
||||||
<SelectItem value="COMPLEMENTARIA">Complementaria</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs font-bold text-slate-500 uppercase">
|
|
||||||
Cita APA
|
|
||||||
</label>
|
|
||||||
<Textarea
|
|
||||||
value={cita}
|
|
||||||
onChange={(e) => setCita(e.target.value)}
|
|
||||||
placeholder="Autor, A. (Año). Título..."
|
|
||||||
className="min-h-[120px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() => onAdd(cita)}
|
|
||||||
disabled={!cita.trim()}
|
|
||||||
className="w-full bg-blue-600"
|
|
||||||
>
|
|
||||||
Añadir a la lista
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function LibrarySearchDialog({ onSelect, existingIds }: any) {
|
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [tipo, setTipo] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA')
|
const [tipo, setTipo] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA')
|
||||||
const filtered = mockLibraryResources.filter(
|
const filtered = (resources || []).filter(
|
||||||
(r) =>
|
(r: any) =>
|
||||||
!existingIds.includes(r.id) &&
|
!existingIds.includes(r.id) &&
|
||||||
r.titulo.toLowerCase().includes(search.toLowerCase()),
|
r.titulo?.toLowerCase().includes(search.toLowerCase()),
|
||||||
)
|
)
|
||||||
|
console.log(filtered)
|
||||||
|
console.log(resources)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-2">
|
||||||
@@ -468,7 +381,7 @@ function LibrarySearchDialog({ onSelect, existingIds }: any) {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-[300px] space-y-2 overflow-y-auto pr-2">
|
<div className="max-h-[300px] space-y-2 overflow-y-auto pr-2">
|
||||||
{filtered.map((res) => (
|
{filtered.map((res: any) => (
|
||||||
<div
|
<div
|
||||||
key={res.id}
|
key={res.id}
|
||||||
onClick={() => onSelect(res, tipo)}
|
onClick={() => onSelect(res, tipo)}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,6 @@
|
|||||||
|
import { FileCheck, Download, RefreshCw, Loader2 } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import {
|
|
||||||
FileText,
|
|
||||||
Download,
|
|
||||||
RefreshCw,
|
|
||||||
Calendar,
|
|
||||||
FileCheck,
|
|
||||||
AlertTriangle,
|
|
||||||
Loader2,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -22,54 +12,36 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import type {
|
import { Button } from '@/components/ui/button'
|
||||||
DocumentoAsignatura,
|
import { Card } from '@/components/ui/card'
|
||||||
Asignatura,
|
|
||||||
AsignaturaStructure,
|
|
||||||
} from '@/types/asignatura'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
|
|
||||||
//import { toast } from 'sonner';
|
|
||||||
//import { format } from 'date-fns';
|
|
||||||
//import { es } from 'date-fns/locale';
|
|
||||||
|
|
||||||
interface DocumentoSEPTabProps {
|
interface DocumentoSEPTabProps {
|
||||||
documento: DocumentoAsignatura | null
|
pdfUrl: string | null
|
||||||
asignatura: Asignatura
|
isLoading: boolean
|
||||||
estructura: AsignaturaStructure
|
onDownloadPdf: () => void
|
||||||
datosGenerales: Record<string, any>
|
onDownloadWord: () => void
|
||||||
onRegenerate: () => void
|
onRegenerate: () => void
|
||||||
isRegenerating: boolean
|
isRegenerating: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DocumentoSEPTab({
|
export function DocumentoSEPTab({
|
||||||
documento,
|
pdfUrl,
|
||||||
asignatura,
|
isLoading,
|
||||||
estructura,
|
onDownloadPdf,
|
||||||
datosGenerales,
|
onDownloadWord,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
isRegenerating,
|
isRegenerating,
|
||||||
}: DocumentoSEPTabProps) {
|
}: DocumentoSEPTabProps) {
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
||||||
|
|
||||||
// Check completeness
|
|
||||||
const camposObligatorios = estructura.campos.filter((c) => c.obligatorio)
|
|
||||||
const camposCompletos = camposObligatorios.filter((c) =>
|
|
||||||
datosGenerales[c.id]?.trim(),
|
|
||||||
)
|
|
||||||
const completeness = Math.round(
|
|
||||||
(camposCompletos.length / camposObligatorios.length) * 100,
|
|
||||||
)
|
|
||||||
const isComplete = completeness === 100
|
|
||||||
|
|
||||||
const handleRegenerate = () => {
|
const handleRegenerate = () => {
|
||||||
setShowConfirmDialog(false)
|
setShowConfirmDialog(false)
|
||||||
onRegenerate()
|
onRegenerate()
|
||||||
//toast.success('Regenerando documento...');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in space-y-6">
|
<div className="animate-fade-in space-y-6">
|
||||||
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-display text-foreground flex items-center gap-2 text-2xl font-semibold">
|
<h2 className="font-display text-foreground flex items-center gap-2 text-2xl font-semibold">
|
||||||
@@ -77,45 +49,40 @@ export function DocumentoSEPTab({
|
|||||||
Documento SEP
|
Documento SEP
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground mt-1 text-sm">
|
<p className="text-muted-foreground mt-1 text-sm">
|
||||||
Previsualización del documento oficial para la SEP
|
Previsualización del documento oficial generado
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{documento?.estado === 'listo' && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={
|
|
||||||
() =>
|
|
||||||
console.log('descargando') /*toast.info('Descarga iniciada')*/
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Download className="mr-2 h-4 w-4" />
|
|
||||||
Descargar
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
open={showConfirmDialog}
|
open={showConfirmDialog}
|
||||||
onOpenChange={setShowConfirmDialog}
|
onOpenChange={setShowConfirmDialog}
|
||||||
>
|
>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button disabled={isRegenerating || !isComplete}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
disabled={isRegenerating}
|
||||||
|
>
|
||||||
{isRegenerating ? (
|
{isRegenerating ? (
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
{isRegenerating ? 'Generando...' : 'Regenerar documento'}
|
{isRegenerating ? 'Generando...' : 'Regenerar'}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
|
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>¿Regenerar documento SEP?</AlertDialogTitle>
|
<AlertDialogTitle>¿Regenerar documento SEP?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Se creará una nueva versión del documento con los datos
|
Se generará una nueva versión del documento con la información
|
||||||
actuales de la asignatura. La versión anterior quedará en el
|
actual.
|
||||||
historial.
|
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleRegenerate}>
|
<AlertDialogAction onClick={handleRegenerate}>
|
||||||
@@ -124,311 +91,43 @@ export function DocumentoSEPTab({
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
|
{pdfUrl && !isLoading && (
|
||||||
|
<>
|
||||||
|
<Button size="sm" className="gap-2" onClick={onDownloadWord}>
|
||||||
|
<Download className="h-4 w-4" /> Descargar Word
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
onClick={onDownloadPdf}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" /> Descargar PDF
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
{/* PDF Preview */}
|
||||||
{/* Document preview */}
|
<Card className="h-200 overflow-hidden">
|
||||||
<div className="lg:col-span-2">
|
{isLoading ? (
|
||||||
<Card className="card-elevated h-[700px] overflow-hidden">
|
<div className="flex h-full items-center justify-center">
|
||||||
{documento?.estado === 'listo' ? (
|
<Loader2 className="h-10 w-10 animate-spin" />
|
||||||
<div className="bg-muted/30 flex h-full flex-col">
|
</div>
|
||||||
{/* Simulated document header */}
|
) : pdfUrl ? (
|
||||||
<div className="bg-card border-b p-4">
|
<iframe
|
||||||
<div className="flex items-center justify-between">
|
src={`${pdfUrl}#toolbar=0`}
|
||||||
<div className="flex items-center gap-2">
|
className="h-full w-full border-none"
|
||||||
<FileText className="text-primary h-5 w-5" />
|
title="Documento SEP"
|
||||||
<span className="text-foreground font-medium">
|
/>
|
||||||
Programa de Estudios - {asignatura.clave}
|
) : (
|
||||||
</span>
|
<div className="text-muted-foreground flex h-full items-center justify-center">
|
||||||
</div>
|
No se pudo cargar el documento.
|
||||||
<Badge variant="outline">Versión {documento.version}</Badge>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
{/* Document content simulation */}
|
|
||||||
<div className="flex-1 overflow-y-auto p-8">
|
|
||||||
<div className="bg-card mx-auto max-w-2xl space-y-6 rounded-lg p-8 shadow-lg">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="border-b pb-6 text-center">
|
|
||||||
<p className="text-muted-foreground mb-2 text-xs tracking-wide uppercase">
|
|
||||||
Secretaría de Educación Pública
|
|
||||||
</p>
|
|
||||||
<h1 className="font-display text-primary mb-1 text-2xl font-bold">
|
|
||||||
{asignatura.nombre}
|
|
||||||
</h1>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
Clave: {asignatura.clave} | Créditos:{' '}
|
|
||||||
{asignatura.creditos || 'N/A'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Datos de la institución */}
|
|
||||||
<div className="space-y-1 text-sm">
|
|
||||||
<p>
|
|
||||||
<strong>Carrera:</strong> {asignatura.carrera}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Facultad:</strong> {asignatura.facultad}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Plan de estudios:</strong>{' '}
|
|
||||||
{asignatura.planNombre}
|
|
||||||
</p>
|
|
||||||
{asignatura.ciclo && (
|
|
||||||
<p>
|
|
||||||
<strong>Ciclo:</strong> {asignatura.ciclo}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Campos del documento */}
|
|
||||||
{estructura.campos.map((campo) => {
|
|
||||||
const valor = datosGenerales[campo.id]
|
|
||||||
if (!valor) return null
|
|
||||||
return (
|
|
||||||
<div key={campo.id} className="space-y-2">
|
|
||||||
<h3 className="text-foreground border-b pb-1 font-semibold">
|
|
||||||
{campo.nombre}
|
|
||||||
</h3>
|
|
||||||
<p className="text-foreground text-sm leading-relaxed whitespace-pre-wrap">
|
|
||||||
{valor}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="text-muted-foreground mt-8 border-t pt-6 text-center text-xs">
|
|
||||||
<p>
|
|
||||||
Documento generado el{' '}
|
|
||||||
{/*format(documento.fechaGeneracion, "d 'de' MMMM 'de' yyyy", { locale: es })*/}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1">Universidad La Salle</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : documento?.estado === 'generando' ? (
|
|
||||||
<div className="flex h-full items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<Loader2 className="text-accent mx-auto mb-4 h-12 w-12 animate-spin" />
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Generando documento...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full items-center justify-center">
|
|
||||||
<div className="max-w-sm text-center">
|
|
||||||
<FileText className="text-muted-foreground/50 mx-auto mb-4 h-12 w-12" />
|
|
||||||
<p className="text-muted-foreground mb-4">
|
|
||||||
No hay documento generado aún
|
|
||||||
</p>
|
|
||||||
{!isComplete && (
|
|
||||||
<div className="bg-warning/10 text-warning-foreground rounded-lg p-4 text-sm">
|
|
||||||
<AlertTriangle className="mr-2 inline h-4 w-4" />
|
|
||||||
Completa todos los campos obligatorios para generar el
|
|
||||||
documento
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Info sidebar */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Status */}
|
|
||||||
<Card className="card-elevated">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Estado del documento
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
{documento && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-sm">
|
|
||||||
Versión
|
|
||||||
</span>
|
|
||||||
<Badge variant="outline">{documento.version}</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-sm">
|
|
||||||
Generado
|
|
||||||
</span>
|
|
||||||
<span className="text-sm">
|
|
||||||
{/*format(documento.fechaGeneracion, "d MMM yyyy, HH:mm", { locale: es })*/}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-sm">
|
|
||||||
Estado
|
|
||||||
</span>
|
|
||||||
<Badge
|
|
||||||
className={cn(
|
|
||||||
documento.estado === 'listo' &&
|
|
||||||
'bg-success text-success-foreground',
|
|
||||||
documento.estado === 'generando' &&
|
|
||||||
'bg-info text-info-foreground',
|
|
||||||
documento.estado === 'error' &&
|
|
||||||
'bg-destructive text-destructive-foreground',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{documento.estado === 'listo' && 'Listo'}
|
|
||||||
{documento.estado === 'generando' && 'Generando'}
|
|
||||||
{documento.estado === 'error' && 'Error'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Completeness */}
|
|
||||||
<Card className="card-elevated">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Completitud de datos
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between text-sm">
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Campos obligatorios
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{camposCompletos.length}/{camposObligatorios.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted h-2 overflow-hidden rounded-full">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'h-full transition-all duration-500',
|
|
||||||
completeness === 100 ? 'bg-success' : 'bg-accent',
|
|
||||||
)}
|
|
||||||
style={{ width: `${completeness}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
className={cn(
|
|
||||||
'text-xs',
|
|
||||||
completeness === 100
|
|
||||||
? 'text-success'
|
|
||||||
: 'text-muted-foreground',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{completeness === 100
|
|
||||||
? 'Todos los campos obligatorios están completos'
|
|
||||||
: `Faltan ${camposObligatorios.length - camposCompletos.length} campos por completar`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Missing fields */}
|
|
||||||
{!isComplete && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-muted-foreground text-xs font-medium">
|
|
||||||
Campos faltantes:
|
|
||||||
</p>
|
|
||||||
{camposObligatorios
|
|
||||||
.filter((c) => !datosGenerales[c.id]?.trim())
|
|
||||||
.map((campo) => (
|
|
||||||
<div
|
|
||||||
key={campo.id}
|
|
||||||
className="flex items-center gap-2 text-sm"
|
|
||||||
>
|
|
||||||
<AlertTriangle className="text-warning h-3 w-3" />
|
|
||||||
<span className="text-foreground">{campo.nombre}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Requirements */}
|
|
||||||
<Card className="card-elevated">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Requisitos SEP
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ul className="space-y-2 text-sm">
|
|
||||||
<li className="flex items-start gap-2">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'mt-0.5 flex h-4 w-4 items-center justify-center rounded-full',
|
|
||||||
datosGenerales['objetivo_general']
|
|
||||||
? 'bg-success/20'
|
|
||||||
: 'bg-muted',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{datosGenerales['objetivo_general'] && (
|
|
||||||
<Check className="text-success h-3 w-3" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Objetivo general definido
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start gap-2">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'mt-0.5 flex h-4 w-4 items-center justify-center rounded-full',
|
|
||||||
datosGenerales['competencias']
|
|
||||||
? 'bg-success/20'
|
|
||||||
: 'bg-muted',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{datosGenerales['competencias'] && (
|
|
||||||
<Check className="text-success h-3 w-3" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Competencias especificadas
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start gap-2">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'mt-0.5 flex h-4 w-4 items-center justify-center rounded-full',
|
|
||||||
datosGenerales['evaluacion']
|
|
||||||
? 'bg-success/20'
|
|
||||||
: 'bg-muted',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{datosGenerales['evaluacion'] && (
|
|
||||||
<Check className="text-success h-3 w-3" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Criterios de evaluación
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Check({ className }: { className?: string }) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
className={className}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="3"
|
|
||||||
>
|
|
||||||
<polyline points="20 6 9 17 4 12" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useParams } from '@tanstack/react-router'
|
||||||
import { format, parseISO } from 'date-fns'
|
import { format, parseISO } from 'date-fns'
|
||||||
import { es } from 'date-fns/locale'
|
import { es } from 'date-fns/locale'
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +11,6 @@ import {
|
|||||||
Filter,
|
Filter,
|
||||||
Calendar,
|
Calendar,
|
||||||
Loader2,
|
Loader2,
|
||||||
Eye,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useMemo } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
|
|
||||||
@@ -53,7 +53,10 @@ const tipoConfig: Record<string, { label: string; icon: any; color: string }> =
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HistorialTab({ asignaturaId }) {
|
export function HistorialTab() {
|
||||||
|
const { asignaturaId } = useParams({
|
||||||
|
from: '/planes/$planId/asignaturas/$asignaturaId/historial',
|
||||||
|
})
|
||||||
// 1. Obtenemos los datos directamente dentro del componente
|
// 1. Obtenemos los datos directamente dentro del componente
|
||||||
const { data: rawData, isLoading } = useSubjectHistorial(asignaturaId)
|
const { data: rawData, isLoading } = useSubjectHistorial(asignaturaId)
|
||||||
|
|
||||||
@@ -245,7 +248,16 @@ export function HistorialTab({ asignaturaId }) {
|
|||||||
`bg-current ${config.color}`,
|
`bg-current ${config.color}`,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Card className="card-interactive">
|
<Card
|
||||||
|
className="border-border card-interactive hover:border-primary/50 flex-1 cursor-pointer shadow-none transition-colors"
|
||||||
|
onClick={() => openCompareModal(cambio)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ')
|
||||||
|
openCompareModal(cambio)
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CardContent className="py-4">
|
<CardContent className="py-4">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div
|
<div
|
||||||
@@ -261,16 +273,7 @@ export function HistorialTab({ asignaturaId }) {
|
|||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{cambio.descripcion}
|
{cambio.descripcion}
|
||||||
</p>
|
</p>
|
||||||
{/* BOTÓN PARA VER CAMBIOS */}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="gap-2 text-blue-600 hover:bg-blue-50 hover:text-blue-700"
|
|
||||||
onClick={() => openCompareModal(cambio)}
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
Ver cambios
|
|
||||||
</Button>
|
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{format(cambio.fecha, 'HH:mm')}
|
{format(cambio.fecha, 'HH:mm')}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,209 @@
|
|||||||
|
import { Check, Loader2, BookOpen, Clock, ListChecks } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import type { IASugerencia } from '@/types/asignatura'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
useUpdateAsignatura,
|
||||||
|
useSubject,
|
||||||
|
useUpdateSubjectRecommendation,
|
||||||
|
} from '@/data'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface ImprovementCardProps {
|
||||||
|
sug: IASugerencia
|
||||||
|
asignaturaId: string
|
||||||
|
onApplied: (campoKey: string) => void
|
||||||
|
isSelected?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImprovementCard({
|
||||||
|
sug,
|
||||||
|
asignaturaId,
|
||||||
|
onApplied,
|
||||||
|
}: ImprovementCardProps) {
|
||||||
|
const { data: asignatura } = useSubject(asignaturaId)
|
||||||
|
const updateAsignatura = useUpdateAsignatura()
|
||||||
|
const updateRecommendation = useUpdateSubjectRecommendation()
|
||||||
|
|
||||||
|
const [isApplying, setIsApplying] = useState(false)
|
||||||
|
|
||||||
|
const handleApply = async () => {
|
||||||
|
if (!asignatura) return
|
||||||
|
|
||||||
|
setIsApplying(true)
|
||||||
|
try {
|
||||||
|
// 1. Identificar a qué columna debe ir el guardado
|
||||||
|
let patchData = {}
|
||||||
|
|
||||||
|
if (sug.campoKey === 'contenido_tematico') {
|
||||||
|
// Se guarda directamente en la columna contenido_tematico
|
||||||
|
patchData = { contenido_tematico: sug.valorSugerido }
|
||||||
|
} else if (sug.campoKey === 'criterios_de_evaluacion') {
|
||||||
|
// Se guarda directamente en la columna criterios_de_evaluacion
|
||||||
|
patchData = { criterios_de_evaluacion: sug.valorSugerido }
|
||||||
|
} else {
|
||||||
|
// Otros campos (ciclo, fines, etc.) se siguen guardando en el JSON de la columna 'datos'
|
||||||
|
patchData = {
|
||||||
|
datos: {
|
||||||
|
...asignatura.datos,
|
||||||
|
[sug.campoKey]: sug.valorSugerido,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Ejecutar la actualización con la estructura correcta
|
||||||
|
await updateAsignatura.mutateAsync({
|
||||||
|
asignaturaId: asignaturaId as any,
|
||||||
|
patch: patchData as any,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. Marcar la recomendación como aplicada
|
||||||
|
await updateRecommendation.mutateAsync({
|
||||||
|
mensajeId: sug.messageId,
|
||||||
|
campoAfectado: sug.campoKey,
|
||||||
|
})
|
||||||
|
console.log(sug.campoKey)
|
||||||
|
|
||||||
|
onApplied(sug.campoKey)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al aplicar mejora:', error)
|
||||||
|
} finally {
|
||||||
|
setIsApplying(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FUNCIÓN PARA RENDERIZAR EL CONTENIDO DE FORMA SEGURA ---
|
||||||
|
const renderContenido = (valor: any) => {
|
||||||
|
// Si no es un array, es texto simple
|
||||||
|
if (!Array.isArray(valor)) {
|
||||||
|
return <p className="italic">"{String(valor)}"</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CASO 1: CONTENIDO TEMÁTICO (Detectamos si el primer objeto tiene 'unidad') ---
|
||||||
|
if (valor[0]?.hasOwnProperty('unidad')) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{valor.map((u: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-card border-primary/20 rounded-md border p-2 shadow-sm"
|
||||||
|
>
|
||||||
|
<div className="border-border/50 text-primary mb-1 flex items-center gap-2 border-b pb-1 text-[11px] font-bold">
|
||||||
|
<BookOpen size={12} /> Unidad {u.unidad}: {u.titulo}
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{u.temas?.map((t: any, tidx: number) => (
|
||||||
|
<li
|
||||||
|
key={tidx}
|
||||||
|
className="text-muted-foreground flex items-start justify-between gap-2 text-[10px]"
|
||||||
|
>
|
||||||
|
<span className="leading-tight">• {t.nombre}</span>
|
||||||
|
<span className="text-muted-foreground/70 flex shrink-0 items-center gap-0.5 font-mono">
|
||||||
|
<Clock size={10} /> {t.horasEstimadas}h
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CASO 2: CRITERIOS DE EVALUACIÓN (Detectamos si tiene 'criterio') ---
|
||||||
|
if (valor[0]?.hasOwnProperty('criterio')) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-muted-foreground/70 mb-1 flex items-center gap-2 text-[10px] font-bold uppercase">
|
||||||
|
<ListChecks size={12} /> Desglose de evaluación
|
||||||
|
</div>
|
||||||
|
{valor.map((c: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-card border-border flex items-center justify-between gap-3 rounded-md border p-2 shadow-sm"
|
||||||
|
>
|
||||||
|
<span className="text-foreground text-[11px] leading-tight">
|
||||||
|
{c.criterio}
|
||||||
|
</span>
|
||||||
|
<div className="border-accent/30 bg-accent/10 text-accent flex shrink-0 items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-bold">
|
||||||
|
{c.porcentaje}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{/* Opcional: Suma total para verificar que de 100% */}
|
||||||
|
<div className="text-muted-foreground/70 pt-1 text-right text-[9px] font-medium">
|
||||||
|
Total:{' '}
|
||||||
|
{valor.reduce(
|
||||||
|
(acc: number, curr: any) => acc + (curr.porcentaje || 0),
|
||||||
|
0,
|
||||||
|
)}
|
||||||
|
%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caso por defecto (Array genérico)
|
||||||
|
return (
|
||||||
|
<pre className="text-[10px]">
|
||||||
|
{/* JSON.stringify(valor, null, 2)*/ 'hola'}
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ESTADO APLICADO ---
|
||||||
|
if (sug.aceptada) {
|
||||||
|
return (
|
||||||
|
<div className="bg-card border-border flex flex-col rounded-xl border p-3 opacity-80 shadow-sm">
|
||||||
|
<div className="mb-3 flex items-center justify-between gap-4">
|
||||||
|
<span className="text-foreground text-sm font-bold">
|
||||||
|
{sug.campoNombre}
|
||||||
|
</span>
|
||||||
|
<div className="border-border bg-muted/50 text-muted-foreground flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium">
|
||||||
|
<Check size={14} />
|
||||||
|
Aplicado
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-primary/20 bg-primary/5 text-muted-foreground rounded-lg border p-3 text-xs leading-relaxed">
|
||||||
|
{renderContenido(sug.valorSugerido)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ESTADO PENDIENTE ---
|
||||||
|
return (
|
||||||
|
<div className="bg-card border-primary/20 hover:border-primary/40 group flex flex-col rounded-xl border p-3 shadow-sm transition-all">
|
||||||
|
<div className="mb-3 flex items-center justify-between gap-4">
|
||||||
|
<span className="border-primary/20 bg-primary/10 text-primary max-w-[150px] truncate rounded-lg border px-2.5 py-1 text-[10px] font-bold tracking-wider uppercase">
|
||||||
|
{sug.campoNombre}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
disabled={isApplying || !asignatura}
|
||||||
|
className="h-8 w-auto px-4 text-xs font-semibold shadow-sm"
|
||||||
|
onClick={handleApply}
|
||||||
|
>
|
||||||
|
{isApplying ? (
|
||||||
|
<Loader2 size={14} className="mr-1.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Check size={14} className="mr-1.5" />
|
||||||
|
)}
|
||||||
|
{isApplying ? 'Aplicando...' : 'Aplicar mejora'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'border-border/60 bg-muted/30 text-muted-foreground rounded-lg border border-dashed p-3 text-xs leading-relaxed',
|
||||||
|
!Array.isArray(sug.valorSugerido) && 'line-clamp-4 italic',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderContenido(sug.valorSugerido)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
||||||
|
import { AlertTriangle } from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
onConfirm: () => void
|
||||||
|
titulo?: string
|
||||||
|
descripcion?: string
|
||||||
|
}
|
||||||
|
export const AlertaConflicto = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
onConfirm,
|
||||||
|
titulo,
|
||||||
|
descripcion,
|
||||||
|
}: Props) => {
|
||||||
|
// Intentamos parsear el mensaje si viene como JSON para la lista de materias
|
||||||
|
let contenido
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(descripcion as any)
|
||||||
|
contenido = (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-slate-600">{data.main}</p>
|
||||||
|
<div className="flex flex-wrap gap-2 py-2">
|
||||||
|
{data.materias.map((m: string, i: number) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className="animate-in fade-in zoom-in-95 inline-flex items-center rounded-md border border-red-100 bg-red-50 px-2.5 py-1 text-xs font-medium text-red-700 duration-300"
|
||||||
|
>
|
||||||
|
<AlertTriangle className="mr-1.5 h-3 w-3 shrink-0" />
|
||||||
|
{m}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-xs font-semibold text-slate-500">
|
||||||
|
¿Deseas ignorar la regla y moverla de todos modos (Esto eliminará la
|
||||||
|
seriación)?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
contenido = <p className="text-sm text-slate-600">{descripcion}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog.Root open={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialog.Portal>
|
||||||
|
<AlertDialog.Overlay className="fixed inset-0 z-[200] bg-slate-950/40 backdrop-blur-[2px]" />
|
||||||
|
<AlertDialog.Content className="fixed top-1/2 left-1/2 z-[201] w-[95vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-slate-100 bg-white p-6 shadow-2xl">
|
||||||
|
<div className="mb-4 flex items-center gap-3">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-red-100 text-red-600">
|
||||||
|
<AlertTriangle className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<AlertDialog.Title className="text-xl font-bold tracking-tight text-slate-900">
|
||||||
|
{titulo}
|
||||||
|
</AlertDialog.Title>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlertDialog.Description asChild>{contenido}</AlertDialog.Description>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col-reverse justify-end gap-3 sm:flex-row">
|
||||||
|
<AlertDialog.Cancel asChild>
|
||||||
|
<Button variant="ghost">Cancelar</Button>
|
||||||
|
{/* Radix automáticamente llamará a onOpenChange(false) al hacer clic aquí */}
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
|
||||||
|
<AlertDialog.Action asChild>
|
||||||
|
<Button
|
||||||
|
onClick={onConfirm}
|
||||||
|
className="bg-red-600 font-bold text-white shadow-lg shadow-red-200 hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Mover de todos modos
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</div>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Portal>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
|
|
||||||
|
import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm/PasoBasicosForm'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { useSubject } from '@/data'
|
||||||
|
|
||||||
|
export function PasoBasicosClonadoInterno({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
wizard: NewSubjectWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
|
}) {
|
||||||
|
const sourceId = wizard.clonInterno?.asignaturaOrigenId ?? null
|
||||||
|
const { data: source, isLoading, isError } = useSubject(sourceId)
|
||||||
|
|
||||||
|
const lastAppliedRef = useRef<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!source) return
|
||||||
|
if (lastAppliedRef.current === source.id) return
|
||||||
|
|
||||||
|
lastAppliedRef.current = source.id
|
||||||
|
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
nombre: source.nombre,
|
||||||
|
codigo: source.codigo ?? '',
|
||||||
|
tipo: (source.tipo as any) ?? null,
|
||||||
|
creditos: source.creditos,
|
||||||
|
horasAcademicas: (source as any).horas_academicas ?? null,
|
||||||
|
horasIndependientes: (source as any).horas_independientes ?? null,
|
||||||
|
estructuraId: (source.estructura_id ??
|
||||||
|
w.datosBasicos.estructuraId) as any,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}, [onChange, source])
|
||||||
|
|
||||||
|
if (!sourceId) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Datos básicos</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-muted-foreground text-sm">
|
||||||
|
Selecciona una asignatura fuente para continuar.
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Datos básicos</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-muted-foreground text-sm">
|
||||||
|
Cargando información de la asignatura fuente…
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError || !source) {
|
||||||
|
return (
|
||||||
|
<Card className="border-destructive/40">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-destructive flex items-center gap-2 text-base">
|
||||||
|
<Icons.AlertTriangle className="h-5 w-5" />
|
||||||
|
No se pudo cargar la fuente
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-muted-foreground text-sm">
|
||||||
|
Intenta seleccionar otra asignatura.
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PasoBasicosForm
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={onChange}
|
||||||
|
estructuraFuenteId={source.estructura_id ?? null}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,356 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
|
||||||
import type { Database } from '@/types/supabase'
|
|
||||||
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import { useSubjectEstructuras } from '@/data'
|
|
||||||
import { TIPOS_MATERIA } from '@/features/asignaturas/nueva/catalogs'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
export function PasoBasicosForm({
|
|
||||||
wizard,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
wizard: NewSubjectWizardState
|
|
||||||
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
|
||||||
}) {
|
|
||||||
const { data: estructuras } = useSubjectEstructuras()
|
|
||||||
|
|
||||||
const [creditosInput, setCreditosInput] = useState<string>(() => {
|
|
||||||
const c = Number(wizard.datosBasicos.creditos ?? 0)
|
|
||||||
let newC = c
|
|
||||||
console.log('antes', newC)
|
|
||||||
|
|
||||||
if (Number.isFinite(c) && c > 999) {
|
|
||||||
newC = 999
|
|
||||||
}
|
|
||||||
console.log('desp', newC)
|
|
||||||
return newC > 0 ? newC.toFixed(2) : ''
|
|
||||||
})
|
|
||||||
const [creditosFocused, setCreditosFocused] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (creditosFocused) return
|
|
||||||
const c = Number(wizard.datosBasicos.creditos ?? 0)
|
|
||||||
let newC = c
|
|
||||||
if (Number.isFinite(c) && c > 999) {
|
|
||||||
newC = 999
|
|
||||||
}
|
|
||||||
setCreditosInput(newC > 0 ? newC.toFixed(2) : '')
|
|
||||||
}, [wizard.datosBasicos.creditos, creditosFocused])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div className="grid gap-1 sm:col-span-2">
|
|
||||||
<Label htmlFor="nombre">Nombre de la asignatura</Label>
|
|
||||||
<Input
|
|
||||||
id="nombre"
|
|
||||||
placeholder="Ej. Matemáticas Discretas"
|
|
||||||
maxLength={200}
|
|
||||||
value={wizard.datosBasicos.nombre}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, nombre: e.target.value },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="codigo">
|
|
||||||
Código
|
|
||||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
|
||||||
(Opcional)
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="codigo"
|
|
||||||
placeholder="Ej. MAT-101"
|
|
||||||
maxLength={200}
|
|
||||||
value={wizard.datosBasicos.codigo || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, codigo: e.target.value },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="placeholder:text-muted-foreground/70 placeholder:italicplaceholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="tipo">Tipo</Label>
|
|
||||||
<Select
|
|
||||||
value={(wizard.datosBasicos.tipo ?? '') as string}
|
|
||||||
onValueChange={(value: string) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
tipo: value as NewSubjectWizardState['datosBasicos']['tipo'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="tipo"
|
|
||||||
className={cn(
|
|
||||||
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
|
|
||||||
!wizard.datosBasicos.tipo
|
|
||||||
? 'text-muted-foreground font-normal italic opacity-70'
|
|
||||||
: 'font-medium not-italic',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Ej. Obligatoria" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{TIPOS_MATERIA.map((t) => (
|
|
||||||
<SelectItem key={t.value} value={t.value}>
|
|
||||||
{t.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="creditos">Créditos</Label>
|
|
||||||
<Input
|
|
||||||
id="creditos"
|
|
||||||
type="text"
|
|
||||||
inputMode="decimal"
|
|
||||||
maxLength={6}
|
|
||||||
pattern="^\\d*(?:[.,]\\d{0,2})?$"
|
|
||||||
value={creditosInput}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (['-', 'e', 'E', '+'].includes(e.key)) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFocus={() => setCreditosFocused(true)}
|
|
||||||
onBlur={() => {
|
|
||||||
setCreditosFocused(false)
|
|
||||||
|
|
||||||
const raw = creditosInput.trim()
|
|
||||||
if (!raw) {
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, creditos: 0 },
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalized = raw.replace(',', '.')
|
|
||||||
let asNumber = Number.parseFloat(normalized)
|
|
||||||
if (!Number.isFinite(asNumber) || asNumber <= 0) {
|
|
||||||
setCreditosInput('')
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, creditos: 0 },
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap to 999
|
|
||||||
if (asNumber > 999) asNumber = 999
|
|
||||||
|
|
||||||
const fixed = asNumber.toFixed(2)
|
|
||||||
setCreditosInput(fixed)
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, creditos: Number(fixed) },
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const nextRaw = e.target.value
|
|
||||||
if (nextRaw === '') {
|
|
||||||
setCreditosInput('')
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, creditos: 0 },
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/^\d*(?:[.,]\d{0,2})?$/.test(nextRaw)) return
|
|
||||||
|
|
||||||
// If typed number exceeds 999, cap it immediately (prevents entering >999)
|
|
||||||
const asNumberRaw = Number.parseFloat(nextRaw.replace(',', '.'))
|
|
||||||
if (Number.isFinite(asNumberRaw) && asNumberRaw > 999) {
|
|
||||||
// show capped value to the user
|
|
||||||
const cappedStr = '999.00'
|
|
||||||
setCreditosInput(cappedStr)
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
creditos: 999,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setCreditosInput(nextRaw)
|
|
||||||
|
|
||||||
const asNumber = Number.parseFloat(nextRaw.replace(',', '.'))
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
creditos:
|
|
||||||
Number.isFinite(asNumber) && asNumber > 0 ? asNumber : 0,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
placeholder="Ej. 4.50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="estructura">Estructura de la asignatura</Label>
|
|
||||||
<Select
|
|
||||||
value={wizard.datosBasicos.estructuraId as string}
|
|
||||||
onValueChange={(val) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, estructuraId: val },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="estructura"
|
|
||||||
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Selecciona plantilla..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{estructuras?.map(
|
|
||||||
(
|
|
||||||
e: Database['public']['Tables']['estructuras_asignatura']['Row'],
|
|
||||||
) => (
|
|
||||||
<SelectItem key={e.id} value={e.id}>
|
|
||||||
{e.nombre}
|
|
||||||
</SelectItem>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
Define los campos requeridos (ej. Objetivos, Temario, Evaluación).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="horasAcademicas">
|
|
||||||
Horas Académicas
|
|
||||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
|
||||||
(Opcional)
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="horasAcademicas"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={999}
|
|
||||||
step={1}
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={wizard.datosBasicos.horasAcademicas ?? ''}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
horasAcademicas: (() => {
|
|
||||||
const raw = e.target.value
|
|
||||||
if (raw === '') return null
|
|
||||||
const asNumber = Number(raw)
|
|
||||||
if (Number.isNaN(asNumber)) return null
|
|
||||||
// Coerce to positive integer (natural numbers without zero)
|
|
||||||
const n = Math.floor(Math.abs(asNumber))
|
|
||||||
const capped = Math.min(n >= 1 ? n : 1, 999)
|
|
||||||
return capped
|
|
||||||
})(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
placeholder="Ej. 48"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="horasIndependientes">
|
|
||||||
Horas Independientes
|
|
||||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
|
||||||
(Opcional)
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="horasIndependientes"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={999}
|
|
||||||
step={1}
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={wizard.datosBasicos.horasIndependientes ?? ''}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
horasIndependientes: (() => {
|
|
||||||
const raw = e.target.value
|
|
||||||
if (raw === '') return null
|
|
||||||
const asNumber = Number(raw)
|
|
||||||
if (Number.isNaN(asNumber)) return null
|
|
||||||
// Coerce to positive integer (natural numbers without zero)
|
|
||||||
const n = Math.floor(Math.abs(asNumber))
|
|
||||||
const capped = Math.min(n >= 1 ? n : 1, 999)
|
|
||||||
return capped
|
|
||||||
})(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
placeholder="Ej. 24"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import PasoSugerenciasForm from './PasoSugerenciasForm'
|
||||||
|
|
||||||
|
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
|
import type { Database } from '@/types/supabase'
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import { useSubjectEstructuras } from '@/data'
|
||||||
|
import { TIPOS_MATERIA } from '@/features/asignaturas/nueva/catalogs'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
export function PasoBasicosForm({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
estructuraFuenteId,
|
||||||
|
}: {
|
||||||
|
wizard: NewSubjectWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
|
estructuraFuenteId?: string | null
|
||||||
|
}) {
|
||||||
|
const { data: estructuras } = useSubjectEstructuras()
|
||||||
|
|
||||||
|
const [creditosInput, setCreditosInput] = useState<string>(() => {
|
||||||
|
const c = Number(wizard.datosBasicos.creditos ?? 0)
|
||||||
|
let newC = c
|
||||||
|
console.log('antes', newC)
|
||||||
|
|
||||||
|
if (Number.isFinite(c) && c > 999) {
|
||||||
|
newC = 999
|
||||||
|
}
|
||||||
|
console.log('desp', newC)
|
||||||
|
return newC > 0 ? newC.toFixed(2) : ''
|
||||||
|
})
|
||||||
|
const [creditosFocused, setCreditosFocused] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (creditosFocused) return
|
||||||
|
const c = Number(wizard.datosBasicos.creditos ?? 0)
|
||||||
|
let newC = c
|
||||||
|
if (Number.isFinite(c) && c > 999) {
|
||||||
|
newC = 999
|
||||||
|
}
|
||||||
|
setCreditosInput(newC > 0 ? newC.toFixed(2) : '')
|
||||||
|
}, [wizard.datosBasicos.creditos, creditosFocused])
|
||||||
|
|
||||||
|
if (wizard.tipoOrigen !== 'IA_MULTIPLE') {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="grid gap-1 sm:col-span-2">
|
||||||
|
<Label htmlFor="nombre">Nombre de la asignatura</Label>
|
||||||
|
<Input
|
||||||
|
id="nombre"
|
||||||
|
placeholder="Ej. Matemáticas Discretas"
|
||||||
|
maxLength={200}
|
||||||
|
value={wizard.datosBasicos.nombre}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, nombre: e.target.value },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="codigo">
|
||||||
|
Código
|
||||||
|
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||||
|
(Opcional)
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="codigo"
|
||||||
|
placeholder="Ej. MAT-101"
|
||||||
|
maxLength={200}
|
||||||
|
value={wizard.datosBasicos.codigo || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, codigo: e.target.value },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="placeholder:text-muted-foreground/70 placeholder:italicplaceholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="tipo">Tipo</Label>
|
||||||
|
<Select
|
||||||
|
value={(wizard.datosBasicos.tipo ?? '') as string}
|
||||||
|
onValueChange={(value: string) =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
tipo: value as NewSubjectWizardState['datosBasicos']['tipo'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="tipo"
|
||||||
|
className={cn(
|
||||||
|
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
|
||||||
|
!wizard.datosBasicos.tipo
|
||||||
|
? 'text-muted-foreground font-normal italic opacity-70'
|
||||||
|
: 'font-medium not-italic',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Ej. Obligatoria" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TIPOS_MATERIA.map((t) => (
|
||||||
|
<SelectItem key={t.value} value={t.value}>
|
||||||
|
{t.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="creditos">Créditos</Label>
|
||||||
|
<Input
|
||||||
|
id="creditos"
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
maxLength={6}
|
||||||
|
pattern="^\\d*(?:[.,]\\d{0,2})?$"
|
||||||
|
value={creditosInput}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (['-', 'e', 'E', '+'].includes(e.key)) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocus={() => setCreditosFocused(true)}
|
||||||
|
onBlur={() => {
|
||||||
|
setCreditosFocused(false)
|
||||||
|
|
||||||
|
const raw = creditosInput.trim()
|
||||||
|
if (!raw) {
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, creditos: 0 },
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = raw.replace(',', '.')
|
||||||
|
let asNumber = Number.parseFloat(normalized)
|
||||||
|
if (!Number.isFinite(asNumber) || asNumber <= 0) {
|
||||||
|
setCreditosInput('')
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, creditos: 0 },
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap to 999
|
||||||
|
if (asNumber > 999) asNumber = 999
|
||||||
|
|
||||||
|
const fixed = asNumber.toFixed(2)
|
||||||
|
setCreditosInput(fixed)
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, creditos: Number(fixed) },
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const nextRaw = e.target.value
|
||||||
|
if (nextRaw === '') {
|
||||||
|
setCreditosInput('')
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, creditos: 0 },
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d*(?:[.,]\d{0,2})?$/.test(nextRaw)) return
|
||||||
|
|
||||||
|
// If typed number exceeds 999, cap it immediately (prevents entering >999)
|
||||||
|
const asNumberRaw = Number.parseFloat(nextRaw.replace(',', '.'))
|
||||||
|
if (Number.isFinite(asNumberRaw) && asNumberRaw > 999) {
|
||||||
|
// show capped value to the user
|
||||||
|
const cappedStr = '999.00'
|
||||||
|
setCreditosInput(cappedStr)
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
creditos: 999,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setCreditosInput(nextRaw)
|
||||||
|
|
||||||
|
const asNumber = Number.parseFloat(nextRaw.replace(',', '.'))
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
creditos:
|
||||||
|
Number.isFinite(asNumber) && asNumber > 0 ? asNumber : 0,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||||
|
placeholder="Ej. 4.50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="estructura">Estructura de la asignatura</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.estructuraId as string}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, estructuraId: val },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="estructura"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Selecciona plantilla..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{estructuras?.map(
|
||||||
|
(
|
||||||
|
e: Database['public']['Tables']['estructuras_asignatura']['Row'],
|
||||||
|
) => (
|
||||||
|
<SelectItem key={e.id} value={e.id}>
|
||||||
|
{e.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{estructuraFuenteId &&
|
||||||
|
wizard.datosBasicos.estructuraId &&
|
||||||
|
wizard.datosBasicos.estructuraId !== estructuraFuenteId ? (
|
||||||
|
<div className="border-destructive/40 bg-destructive/5 text-destructive flex items-start gap-2 rounded-md border p-2 text-xs">
|
||||||
|
<Icons.AlertTriangle className="mt-0.5 h-4 w-4 flex-none" />
|
||||||
|
<span>
|
||||||
|
Es posible que se pierdan datos generales al seleccionar otra
|
||||||
|
estructura.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
Define los campos requeridos (ej. Objetivos, Temario, Evaluación).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="horasAcademicas">
|
||||||
|
Horas Académicas
|
||||||
|
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||||
|
(Opcional)
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="horasAcademicas"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={999}
|
||||||
|
step={1}
|
||||||
|
inputMode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
value={wizard.datosBasicos.horasAcademicas ?? ''}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
horasAcademicas: (() => {
|
||||||
|
const raw = e.target.value
|
||||||
|
if (raw === '') return null
|
||||||
|
const asNumber = Number(raw)
|
||||||
|
if (Number.isNaN(asNumber)) return null
|
||||||
|
// Coerce to positive integer (natural numbers without zero)
|
||||||
|
const n = Math.floor(Math.abs(asNumber))
|
||||||
|
const capped = Math.min(n >= 1 ? n : 1, 999)
|
||||||
|
return capped
|
||||||
|
})(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||||
|
placeholder="Ej. 48"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="horasIndependientes">
|
||||||
|
Horas Independientes
|
||||||
|
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||||
|
(Opcional)
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="horasIndependientes"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={999}
|
||||||
|
step={1}
|
||||||
|
inputMode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
value={wizard.datosBasicos.horasIndependientes ?? ''}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
horasIndependientes: (() => {
|
||||||
|
const raw = e.target.value
|
||||||
|
if (raw === '') return null
|
||||||
|
const asNumber = Number(raw)
|
||||||
|
if (Number.isNaN(asNumber)) return null
|
||||||
|
// Coerce to positive integer (natural numbers without zero)
|
||||||
|
const n = Math.floor(Math.abs(asNumber))
|
||||||
|
const capped = Math.min(n >= 1 ? n : 1, 999)
|
||||||
|
return capped
|
||||||
|
})(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||||
|
placeholder="Ej. 24"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PasoSugerenciasForm wizard={wizard} onChange={onChange} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
import { RefreshCw, Sparkles, X } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
|
import type { Dispatch, SetStateAction } from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
|
import { generate_subject_suggestions, usePlan } from '@/data'
|
||||||
|
import { AIProgressLoader } from '@/features/asignaturas/nueva/AIProgressLoader'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
export default function PasoSugerenciasForm({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
wizard: NewSubjectWizardState
|
||||||
|
onChange: Dispatch<SetStateAction<NewSubjectWizardState>>
|
||||||
|
}) {
|
||||||
|
const enfoque = wizard.iaMultiple?.enfoque ?? ''
|
||||||
|
const cantidadDeSugerencias = wizard.iaMultiple?.cantidadDeSugerencias ?? 5
|
||||||
|
const isLoading = wizard.iaMultiple?.isLoading ?? false
|
||||||
|
|
||||||
|
const [showConservacionTooltip, setShowConservacionTooltip] = useState(false)
|
||||||
|
|
||||||
|
const setIaMultiple = (
|
||||||
|
patch: Partial<NonNullable<NewSubjectWizardState['iaMultiple']>>,
|
||||||
|
) =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
iaMultiple: {
|
||||||
|
enfoque: w.iaMultiple?.enfoque ?? '',
|
||||||
|
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
||||||
|
isLoading: w.iaMultiple?.isLoading ?? false,
|
||||||
|
...patch,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: plan } = usePlan(wizard.plan_estudio_id)
|
||||||
|
|
||||||
|
const toggleAsignatura = (id: string, checked: boolean) => {
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
sugerencias: w.sugerencias.map((s) =>
|
||||||
|
s.id === id ? { ...s, selected: checked } : s,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGenerarSugerencias = async () => {
|
||||||
|
const hadNoSugerenciasBefore = wizard.sugerencias.length === 0
|
||||||
|
const sugerenciasConservadas = wizard.sugerencias.filter((s) => s.selected)
|
||||||
|
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
errorMessage: null,
|
||||||
|
sugerencias: sugerenciasConservadas,
|
||||||
|
iaMultiple: {
|
||||||
|
enfoque: w.iaMultiple?.enfoque ?? '',
|
||||||
|
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
||||||
|
isLoading: true,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cantidad = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
|
||||||
|
if (!Number.isFinite(cantidad) || cantidad <= 0 || cantidad > 15) {
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
errorMessage: 'La cantidad de sugerencias debe ser entre 1 y 15.',
|
||||||
|
iaMultiple: {
|
||||||
|
enfoque: w.iaMultiple?.enfoque ?? '',
|
||||||
|
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const enfoqueTrim = wizard.iaMultiple?.enfoque.trim() ?? ''
|
||||||
|
|
||||||
|
const nuevasSugerencias = await generate_subject_suggestions({
|
||||||
|
plan_estudio_id: wizard.plan_estudio_id,
|
||||||
|
enfoque: enfoqueTrim ? enfoqueTrim : undefined,
|
||||||
|
cantidad_de_sugerencias: cantidad,
|
||||||
|
sugerencias_conservadas: sugerenciasConservadas.map((s) => ({
|
||||||
|
nombre: s.nombre,
|
||||||
|
descripcion: s.descripcion,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hadNoSugerenciasBefore && nuevasSugerencias.length > 0) {
|
||||||
|
setShowConservacionTooltip(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
sugerencias: [...nuevasSugerencias, ...sugerenciasConservadas],
|
||||||
|
iaMultiple: {
|
||||||
|
enfoque: w.iaMultiple?.enfoque ?? '',
|
||||||
|
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : 'Error generando sugerencias.'
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
errorMessage: message,
|
||||||
|
iaMultiple: {
|
||||||
|
enfoque: w.iaMultiple?.enfoque ?? '',
|
||||||
|
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* --- BLOQUE SUPERIOR: PARÁMETROS --- */}
|
||||||
|
<div className="border-border/60 bg-muted/30 mb-4 rounded-xl border p-4">
|
||||||
|
<div className="mb-3 flex items-center gap-2">
|
||||||
|
<Sparkles className="text-primary h-4 w-4" />
|
||||||
|
<span className="text-sm font-semibold">
|
||||||
|
Parámetros de sugerencia
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="w-full">
|
||||||
|
<Label className="text-muted-foreground mb-1 block text-xs">
|
||||||
|
Enfoque (opcional)
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Ej. Enfocado en normativa mexicana y tecnología"
|
||||||
|
value={enfoque}
|
||||||
|
maxLength={7000}
|
||||||
|
rows={4}
|
||||||
|
onChange={(e) => setIaMultiple({ enfoque: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex w-full flex-col items-end justify-between gap-3 sm:flex-row">
|
||||||
|
<div className="w-full sm:w-44">
|
||||||
|
<Label className="text-muted-foreground mb-1 block text-xs">
|
||||||
|
Cantidad de sugerencias
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Ej. 5"
|
||||||
|
value={cantidadDeSugerencias}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={15}
|
||||||
|
step={1}
|
||||||
|
inputMode="numeric"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value
|
||||||
|
if (raw === '') return
|
||||||
|
const asNumber = Number(raw)
|
||||||
|
if (!Number.isFinite(asNumber)) return
|
||||||
|
const n = Math.floor(Math.abs(asNumber))
|
||||||
|
const capped = Math.min(n >= 1 ? n : 1, 15)
|
||||||
|
setIaMultiple({ cantidadDeSugerencias: capped })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-9 gap-1.5"
|
||||||
|
onClick={onGenerarSugerencias}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
|
{wizard.sugerencias.length > 0
|
||||||
|
? 'Generar más sugerencias'
|
||||||
|
: 'Generar sugerencias'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AIProgressLoader
|
||||||
|
isLoading={isLoading}
|
||||||
|
cantidadDeSugerencias={cantidadDeSugerencias}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* --- HEADER LISTA --- */}
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-foreground text-base font-semibold">
|
||||||
|
Asignaturas sugeridas
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
Basadas en el plan{' '}
|
||||||
|
{plan ? `${plan.nivel} en ${plan.nombre}` : '...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Tooltip open={showConservacionTooltip}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="bg-muted text-foreground inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-sm font-semibold">
|
||||||
|
<span aria-hidden>📌</span>
|
||||||
|
{wizard.sugerencias.filter((s) => s.selected).length}{' '}
|
||||||
|
seleccionadas
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" sideOffset={8} className="max-w-xs">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="flex-1 text-sm">
|
||||||
|
Al generar más sugerencias, se conservarán las asignaturas
|
||||||
|
seleccionadas.
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-5 w-5"
|
||||||
|
onClick={() => setShowConservacionTooltip(false)}
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* --- LISTA DE ASIGNATURAS --- */}
|
||||||
|
<div className="max-h-100 space-y-1 overflow-y-auto pr-1">
|
||||||
|
{wizard.sugerencias.map((asignatura) => {
|
||||||
|
const isSelected = asignatura.selected
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={asignatura.id}
|
||||||
|
aria-checked={isSelected}
|
||||||
|
className={cn(
|
||||||
|
'border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors has-aria-checked:border-blue-600 has-aria-checked:bg-blue-50 dark:has-aria-checked:border-blue-900 dark:has-aria-checked:bg-blue-950',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={isSelected}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
toggleAsignatura(asignatura.id, !!checked)
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
'peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring mt-0.5 h-5 w-5 shrink-0 border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
// isSelected ? '' : 'invisible',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Contenido de la tarjeta */}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span className="text-foreground text-sm font-medium">
|
||||||
|
{asignatura.nombre}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Badges de Tipo */}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors',
|
||||||
|
asignatura.tipo === 'OBLIGATORIA'
|
||||||
|
? 'border-blue-200 bg-transparent text-blue-700 dark:border-blue-800 dark:text-blue-300'
|
||||||
|
: 'border-yellow-200 bg-transparent text-yellow-700 dark:border-yellow-800 dark:text-yellow-300',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{asignatura.tipo}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
{asignatura.creditos} cred. · {asignatura.horasAcademicas}h
|
||||||
|
acad. · {asignatura.horasIndependientes}h indep.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-1 text-sm">
|
||||||
|
{asignatura.descripcion}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,6 +4,12 @@ import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/
|
|||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
|
|
||||||
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
|
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from '@/components/ui/accordion'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
@@ -20,6 +26,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { usePlan, usePlanLineas, useSubjectEstructuras } from '@/data'
|
||||||
import {
|
import {
|
||||||
FACULTADES,
|
FACULTADES,
|
||||||
MATERIAS_MOCK,
|
MATERIAS_MOCK,
|
||||||
@@ -29,12 +36,14 @@ import {
|
|||||||
export function PasoDetallesPanel({
|
export function PasoDetallesPanel({
|
||||||
wizard,
|
wizard,
|
||||||
onChange,
|
onChange,
|
||||||
onGenerarIA: _onGenerarIA,
|
|
||||||
}: {
|
}: {
|
||||||
wizard: NewSubjectWizardState
|
wizard: NewSubjectWizardState
|
||||||
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
onGenerarIA: () => void
|
|
||||||
}) {
|
}) {
|
||||||
|
const { data: estructurasAsignatura } = useSubjectEstructuras()
|
||||||
|
const { data: plan } = usePlan(wizard.plan_estudio_id)
|
||||||
|
const { data: lineasPlan } = usePlanLineas(wizard.plan_estudio_id)
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'MANUAL') {
|
if (wizard.tipoOrigen === 'MANUAL') {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -49,7 +58,7 @@ export function PasoDetallesPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'IA') {
|
if (wizard.tipoOrigen === 'IA_SIMPLE') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -148,6 +157,171 @@ export function PasoDetallesPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wizard.tipoOrigen === 'IA_MULTIPLE') {
|
||||||
|
const maxCiclos = Math.max(1, plan?.numero_ciclos ?? 1)
|
||||||
|
const sugerenciasSeleccionadas = wizard.sugerencias.filter(
|
||||||
|
(s) => s.selected,
|
||||||
|
)
|
||||||
|
|
||||||
|
const patchSugerencia = (
|
||||||
|
id: string,
|
||||||
|
patch: Partial<NewSubjectWizardState['sugerencias'][number]>,
|
||||||
|
) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
sugerencias: w.sugerencias.map((s) =>
|
||||||
|
s.id === id ? { ...s, ...patch } : s,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="border-border/60 bg-muted/30 rounded-xl border p-4">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label className="text-muted-foreground text-xs">
|
||||||
|
Estructura de la asignatura
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.estructuraId ?? undefined}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
estructuraId: val,
|
||||||
|
datosBasicos: { ...w.datosBasicos, estructuraId: val },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Selecciona una estructura" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(estructurasAsignatura ?? []).map((e) => (
|
||||||
|
<SelectItem key={e.id} value={e.id}>
|
||||||
|
{e.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-border/60 bg-muted/30 rounded-xl border p-4">
|
||||||
|
<h3 className="text-foreground mx-3 mb-2 text-lg font-semibold">
|
||||||
|
Materias seleccionadas
|
||||||
|
</h3>
|
||||||
|
{sugerenciasSeleccionadas.length === 0 ? (
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
Selecciona al menos una sugerencia para configurar su descripción,
|
||||||
|
línea curricular y ciclo.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Accordion type="multiple" className="w-full space-y-2">
|
||||||
|
{sugerenciasSeleccionadas.map((asig) => (
|
||||||
|
<AccordionItem
|
||||||
|
key={asig.id}
|
||||||
|
value={asig.id}
|
||||||
|
className="border-border/60 bg-background/40 rounded-lg border border-b-0 px-3"
|
||||||
|
>
|
||||||
|
<AccordionTrigger className="hover:bg-accent/30 data-[state=open]:bg-accent/20 data-[state=open]:text-accent-foreground -mx-3 px-3">
|
||||||
|
{asig.nombre}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="text-muted-foreground">
|
||||||
|
<div className="mx-1 grid gap-3 sm:grid-cols-2">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label className="text-muted-foreground text-xs">
|
||||||
|
Descripción
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
value={asig.descripcion}
|
||||||
|
maxLength={7000}
|
||||||
|
rows={6}
|
||||||
|
onChange={(e) =>
|
||||||
|
patchSugerencia(asig.id, {
|
||||||
|
descripcion: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid content-start gap-3">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label className="text-muted-foreground text-xs">
|
||||||
|
Ciclo (opcional)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={maxCiclos}
|
||||||
|
step={1}
|
||||||
|
inputMode="numeric"
|
||||||
|
placeholder={`1-${maxCiclos}`}
|
||||||
|
value={asig.numero_ciclo ?? ''}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (
|
||||||
|
['.', ',', '-', 'e', 'E', '+'].includes(e.key)
|
||||||
|
) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value
|
||||||
|
if (raw === '') {
|
||||||
|
patchSugerencia(asig.id, { numero_ciclo: null })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const asNumber = Number(raw)
|
||||||
|
if (!Number.isFinite(asNumber)) return
|
||||||
|
|
||||||
|
const n = Math.floor(Math.abs(asNumber))
|
||||||
|
const capped = Math.min(
|
||||||
|
Math.max(n >= 1 ? n : 1, 1),
|
||||||
|
maxCiclos,
|
||||||
|
)
|
||||||
|
|
||||||
|
patchSugerencia(asig.id, { numero_ciclo: capped })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label className="text-muted-foreground text-xs">
|
||||||
|
Línea curricular (opcional)
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={asig.linea_plan_id ?? '__none__'}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
patchSugerencia(asig.id, {
|
||||||
|
linea_plan_id: val === '__none__' ? null : val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Sin línea" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">Ninguna</SelectItem>
|
||||||
|
{(lineasPlan ?? []).map((l) => (
|
||||||
|
<SelectItem key={l.id} value={l.id}>
|
||||||
|
{l.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
import { useEffect, useMemo } from 'react'
|
||||||
|
import { useDebounce } from 'use-debounce'
|
||||||
|
|
||||||
|
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
|
import type { Database } from '@/types/supabase'
|
||||||
|
|
||||||
|
import Pagination03 from '@/components/shadcn-studio/pagination/pagination-03'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import { supabaseBrowser, useCatalogosPlanes, usePlanes } from '@/data'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
type SourceSubjectRow = {
|
||||||
|
id: string
|
||||||
|
nombre: string
|
||||||
|
codigo: string | null
|
||||||
|
creditos: number
|
||||||
|
tipo: any
|
||||||
|
plan_estudio_id: string
|
||||||
|
estructura_id: string | null
|
||||||
|
rank?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL = '__all__'
|
||||||
|
|
||||||
|
type SearchAsignaturasRow =
|
||||||
|
Database['public']['Functions']['search_asignaturas']['Returns'][number]
|
||||||
|
|
||||||
|
export function PasoFuenteClonadoInterno({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
wizard: NewSubjectWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
|
}) {
|
||||||
|
const pageSize = 20
|
||||||
|
|
||||||
|
const facultadId = wizard.clonInterno?.facultadId ?? null
|
||||||
|
const carreraId = wizard.clonInterno?.carreraId ?? null
|
||||||
|
const planOrigenId = wizard.clonInterno?.planOrigenId ?? null
|
||||||
|
const search = wizard.clonInterno?.search ?? ''
|
||||||
|
const page = Math.max(1, wizard.clonInterno?.page ?? 1)
|
||||||
|
|
||||||
|
const [debouncedSearch] = useDebounce(search, 350)
|
||||||
|
|
||||||
|
const { data: catalogos } = useCatalogosPlanes()
|
||||||
|
|
||||||
|
const carrerasOptions = useMemo(() => {
|
||||||
|
const raw = catalogos?.carreras ?? []
|
||||||
|
return facultadId ? raw.filter((c) => c.facultad_id === facultadId) : raw
|
||||||
|
}, [catalogos?.carreras, facultadId])
|
||||||
|
|
||||||
|
const planesQuery = usePlanes({
|
||||||
|
search: '',
|
||||||
|
facultadId: facultadId ?? 'todas',
|
||||||
|
carreraId: carreraId ?? 'todas',
|
||||||
|
estadoId: 'todos',
|
||||||
|
limit: 500,
|
||||||
|
offset: 0,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const needPlansForFilter = Boolean((facultadId || carreraId) && !planOrigenId)
|
||||||
|
const plansForFilter = planesQuery.data?.data ?? []
|
||||||
|
|
||||||
|
const { data: subjectsPaged, isLoading: subjectsLoading } = useQuery({
|
||||||
|
queryKey: [
|
||||||
|
'asignaturas',
|
||||||
|
'clone-source',
|
||||||
|
{
|
||||||
|
facultadId,
|
||||||
|
carreraId,
|
||||||
|
planOrigenId,
|
||||||
|
search: debouncedSearch,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
planIdsKey: needPlansForFilter
|
||||||
|
? plansForFilter.map((p) => p.id).join(',')
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
enabled: !needPlansForFilter || !planesQuery.isLoading,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
queryFn: async () => {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const from = (page - 1) * pageSize
|
||||||
|
const term = debouncedSearch.trim()
|
||||||
|
|
||||||
|
// Una sola llamada limpia. RPC maneja tanto búsqueda vacía como llena.
|
||||||
|
const { data, error } = await supabase.rpc('search_asignaturas', {
|
||||||
|
p_search: term,
|
||||||
|
p_facultad_id: facultadId ?? undefined, // Corrección TS 2322
|
||||||
|
p_carrera_id: carreraId ?? undefined, // Corrección TS 2322
|
||||||
|
p_plan_estudio_id: planOrigenId ?? undefined, // Corrección TS 2322
|
||||||
|
p_limit: pageSize,
|
||||||
|
p_offset: from,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message)
|
||||||
|
|
||||||
|
// Extraemos el conteo total de la primera fila.
|
||||||
|
// ESLint feliz: "data" ya no puede ser null a este punto.
|
||||||
|
const count = data.length > 0 ? Number(data[0].total_count) : 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
// ESLint feliz: map directo sobre data
|
||||||
|
data: data.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
nombre: r.nombre,
|
||||||
|
codigo: r.codigo,
|
||||||
|
creditos: Number(r.creditos),
|
||||||
|
tipo: r.tipo,
|
||||||
|
plan_estudio_id: r.plan_estudio_id,
|
||||||
|
estructura_id: null,
|
||||||
|
rank: r.rank,
|
||||||
|
})),
|
||||||
|
count,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const subjects = subjectsPaged?.data ?? []
|
||||||
|
const total = subjectsPaged?.count ?? 0
|
||||||
|
const pageCount = Math.max(1, Math.ceil(total / pageSize))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// clamp page if results shrink
|
||||||
|
if (page > pageCount) {
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonInterno: { ...(w.clonInterno ?? {}), page: pageCount },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}, [onChange, page, pageCount])
|
||||||
|
|
||||||
|
const patchClonInterno = (
|
||||||
|
patch: Partial<NonNullable<NewSubjectWizardState['clonInterno']>>,
|
||||||
|
) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonInterno: { ...(w.clonInterno ?? {}), ...patch },
|
||||||
|
}))
|
||||||
|
|
||||||
|
const hasAnyFilter = Boolean(
|
||||||
|
facultadId || carreraId || planOrigenId || search.trim().length,
|
||||||
|
)
|
||||||
|
|
||||||
|
const clearDisabled = !hasAnyFilter
|
||||||
|
|
||||||
|
const selectedId = wizard.clonInterno?.asignaturaOrigenId ?? null
|
||||||
|
|
||||||
|
const resetSelection = () => patchClonInterno({ asignaturaOrigenId: null })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<Card className="gap-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Fuente</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-4">
|
||||||
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label>Facultad</Label>
|
||||||
|
<Select
|
||||||
|
value={facultadId ?? ALL}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
const next = val === ALL ? null : val
|
||||||
|
patchClonInterno({
|
||||||
|
facultadId: next,
|
||||||
|
carreraId: null,
|
||||||
|
planOrigenId: null,
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
resetSelection()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full min-w-0 [&>span]:block! [&>span]:truncate!">
|
||||||
|
<SelectValue placeholder="Todas" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={ALL}>Todas</SelectItem>
|
||||||
|
{(catalogos?.facultades ?? []).map((f) => (
|
||||||
|
<SelectItem key={f.id} value={f.id}>
|
||||||
|
{f.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label>Carrera</Label>
|
||||||
|
<Select
|
||||||
|
value={carreraId ?? ALL}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
const next = val === ALL ? null : val
|
||||||
|
patchClonInterno({
|
||||||
|
carreraId: next,
|
||||||
|
planOrigenId: null,
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
resetSelection()
|
||||||
|
}}
|
||||||
|
disabled={!facultadId}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full min-w-0 [&>span]:block! [&>span]:truncate!">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={facultadId ? 'Todas' : 'Selecciona facultad'}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={ALL}>Todas</SelectItem>
|
||||||
|
{carrerasOptions.map((c) => (
|
||||||
|
<SelectItem key={c.id} value={c.id}>
|
||||||
|
{c.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label>Plan</Label>
|
||||||
|
<Select
|
||||||
|
value={planOrigenId ?? ALL}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
const next = val === ALL ? null : val
|
||||||
|
patchClonInterno({ planOrigenId: next, page: 1 })
|
||||||
|
resetSelection()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
disabled={!carreraId && !facultadId}
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Todos" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={ALL}>Todos</SelectItem>
|
||||||
|
{(planesQuery.data?.data ?? []).map((p) => (
|
||||||
|
<SelectItem key={p.id} value={p.id}>
|
||||||
|
{p.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 sm:grid-cols-[1fr_auto]">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label>Buscar</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Nombre o código..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) =>
|
||||||
|
patchClonInterno({ search: e.target.value, page: 1 })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
patchClonInterno({
|
||||||
|
facultadId: null,
|
||||||
|
carreraId: null,
|
||||||
|
planOrigenId: null,
|
||||||
|
search: '',
|
||||||
|
page: 1,
|
||||||
|
asignaturaOrigenId: null,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
disabled={clearDisabled}
|
||||||
|
>
|
||||||
|
<Icons.X className="mr-2 h-4 w-4" />
|
||||||
|
Limpiar filtros
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
Selecciona una asignatura fuente (solo una).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid max-h-80 gap-2 overflow-y-auto px-1">
|
||||||
|
{subjectsLoading ? (
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
Cargando asignaturas…
|
||||||
|
</div>
|
||||||
|
) : subjects.length === 0 ? (
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
No hay asignaturas con esos filtros.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
subjects.map((m) => {
|
||||||
|
const active = String(selectedId) === String(m.id)
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={m.id}
|
||||||
|
className={cn(
|
||||||
|
'hover:bg-accent flex cursor-pointer items-center justify-between rounded-md border p-3 text-left',
|
||||||
|
active && 'border-primary bg-primary/5 ring-primary ring-1',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="sr-only"
|
||||||
|
type="radio"
|
||||||
|
name="asignaturaFuente"
|
||||||
|
checked={active}
|
||||||
|
onChange={() =>
|
||||||
|
patchClonInterno({ asignaturaOrigenId: m.id, page })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate font-medium">{m.nombre}</div>
|
||||||
|
<div className="text-muted-foreground mt-0.5 text-xs">
|
||||||
|
{(m.codigo ? m.codigo : '—') +
|
||||||
|
' • ' +
|
||||||
|
String(m.creditos) +
|
||||||
|
' créditos'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{active ? (
|
||||||
|
<Icons.CheckCircle2 className="text-primary h-5 w-5 flex-none" />
|
||||||
|
) : (
|
||||||
|
<span className="h-5 w-5 flex-none" aria-hidden />
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pageCount > 1 ? (
|
||||||
|
<Pagination03
|
||||||
|
page={page}
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={(nextPage) => patchClonInterno({ page: nextPage })}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -77,12 +77,93 @@ export function PasoMetodoCardGroup({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Generar contenido automático.</CardDescription>
|
<CardDescription>Generar contenido automático.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
{(wizard.tipoOrigen === 'IA' ||
|
||||||
|
wizard.tipoOrigen === 'IA_SIMPLE' ||
|
||||||
|
wizard.tipoOrigen === 'IA_MULTIPLE') && (
|
||||||
|
<CardContent className="flex flex-col gap-3">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
tipoOrigen: 'IA_SIMPLE',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent) =>
|
||||||
|
handleKeyActivate(e, () =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
tipoOrigen: 'IA_SIMPLE',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
||||||
|
isSelected('IA_SIMPLE')
|
||||||
|
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
||||||
|
: 'border-border text-muted-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icons.Edit3 className="h-6 w-6 flex-none" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">Una asignatura</span>
|
||||||
|
<span className="text-xs opacity-70">
|
||||||
|
Crear una asignatura con control detallado de metadatos.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
tipoOrigen: 'IA_MULTIPLE',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent) =>
|
||||||
|
handleKeyActivate(e, () =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
tipoOrigen: 'IA_MULTIPLE',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
||||||
|
isSelected('IA_MULTIPLE')
|
||||||
|
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
||||||
|
: 'border-border text-muted-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icons.List className="h-6 w-6 flex-none" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">Varias asignaturas</span>
|
||||||
|
<span className="text-xs opacity-70">
|
||||||
|
Generar varias asignaturas a partir de sugerencias de la IA.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
className={isSelected('OTRO') ? 'ring-ring ring-2' : ''}
|
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onChange((w): NewSubjectWizardState => ({ ...w, tipoOrigen: 'OTRO' }))
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({ ...w, tipoOrigen: 'CLONADO' }),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -93,7 +174,7 @@ export function PasoMetodoCardGroup({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>De otra asignatura o archivo Word.</CardDescription>
|
<CardDescription>De otra asignatura o archivo Word.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(wizard.tipoOrigen === 'OTRO' ||
|
{(wizard.tipoOrigen === 'CLONADO' ||
|
||||||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
||||||
<CardContent className="flex flex-col gap-3">
|
<CardContent className="flex flex-col gap-3">
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import { usePlan, useSubjectEstructuras } from '@/data'
|
import { usePlan, usePlanLineas, useSubjectEstructuras } from '@/data'
|
||||||
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
||||||
|
|
||||||
export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
||||||
const { data: plan } = usePlan(wizard.plan_estudio_id)
|
const { data: plan } = usePlan(wizard.plan_estudio_id)
|
||||||
const { data: estructuras } = useSubjectEstructuras()
|
const { data: estructuras } = useSubjectEstructuras()
|
||||||
|
const { data: lineasPlan } = usePlanLineas(wizard.plan_estudio_id)
|
||||||
|
|
||||||
const estructuraNombre = (() => {
|
const estructuraNombre = (() => {
|
||||||
const estructuraId = wizard.datosBasicos.estructuraId
|
const estructuraId = wizard.datosBasicos.estructuraId
|
||||||
@@ -26,6 +27,8 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
const modoLabel = (() => {
|
const modoLabel = (() => {
|
||||||
if (wizard.tipoOrigen === 'MANUAL') return 'Manual (Vacía)'
|
if (wizard.tipoOrigen === 'MANUAL') return 'Manual (Vacía)'
|
||||||
if (wizard.tipoOrigen === 'IA') return 'Generada con IA'
|
if (wizard.tipoOrigen === 'IA') return 'Generada con IA'
|
||||||
|
if (wizard.tipoOrigen === 'IA_SIMPLE') return 'Generada con IA (Simple)'
|
||||||
|
if (wizard.tipoOrigen === 'IA_MULTIPLE') return 'Generación múltiple (IA)'
|
||||||
if (wizard.tipoOrigen === 'CLONADO_INTERNO') return 'Clonada (Sistema)'
|
if (wizard.tipoOrigen === 'CLONADO_INTERNO') return 'Clonada (Sistema)'
|
||||||
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') return 'Clonada (Archivo)'
|
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') return 'Clonada (Archivo)'
|
||||||
return '—'
|
return '—'
|
||||||
@@ -41,6 +44,10 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
const repositoriosRef = wizard.iaConfig?.repositoriosReferencia ?? []
|
const repositoriosRef = wizard.iaConfig?.repositoriosReferencia ?? []
|
||||||
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
|
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
|
||||||
|
|
||||||
|
const materiasSeleccionadas = wizard.sugerencias.filter((s) => s.selected)
|
||||||
|
const iaMultipleEnfoque = wizard.iaMultiple?.enfoque.trim() ?? ''
|
||||||
|
const iaMultipleCantidad = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -72,7 +79,9 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
{wizard.tipoOrigen === 'MANUAL' && (
|
{wizard.tipoOrigen === 'MANUAL' && (
|
||||||
<Icons.Pencil className="h-4 w-4" />
|
<Icons.Pencil className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
{wizard.tipoOrigen === 'IA' && (
|
{(wizard.tipoOrigen === 'IA' ||
|
||||||
|
wizard.tipoOrigen === 'IA_SIMPLE' ||
|
||||||
|
wizard.tipoOrigen === 'IA_MULTIPLE') && (
|
||||||
<Icons.Sparkles className="h-4 w-4" />
|
<Icons.Sparkles className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
{(wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
{(wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||||
@@ -83,112 +92,202 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
{wizard.tipoOrigen === 'IA_MULTIPLE' ? (
|
||||||
<div className="col-span-2">
|
<>
|
||||||
<span className="text-muted-foreground">Nombre: </span>
|
<div className="border-border/60 bg-muted/30 grid gap-3 rounded-xl border p-4">
|
||||||
<span className="font-medium">
|
<div className="flex flex-col gap-1">
|
||||||
{wizard.datosBasicos.nombre || '—'}
|
<div className="text-foreground text-base font-semibold">
|
||||||
</span>
|
Configuración
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="text-muted-foreground text-xs">
|
||||||
<span className="text-muted-foreground">Código: </span>
|
Se crearán {materiasSeleccionadas.length} asignatura(s) a
|
||||||
<span className="font-medium">
|
partir de tus selecciones.
|
||||||
{wizard.datosBasicos.codigo || '—'}
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Tipo: </span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.datosBasicos.tipo || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Créditos: </span>
|
|
||||||
<span className="font-medium">{creditosText}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Estructura: </span>
|
|
||||||
<span className="font-medium">{estructuraNombre}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Horas académicas: </span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.datosBasicos.horasAcademicas ?? '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Horas independientes:{' '}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.datosBasicos.horasIndependientes ?? '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-muted/50 rounded-md p-3">
|
<div className="bg-background/40 border-border/60 rounded-lg border p-3">
|
||||||
<div className="font-medium">Configuración IA</div>
|
<div className="text-muted-foreground text-xs">
|
||||||
<div className="mt-2 grid gap-2">
|
Estructura
|
||||||
<div>
|
</div>
|
||||||
<span className="text-muted-foreground">
|
<div className="text-foreground mt-1 text-sm font-medium">
|
||||||
Enfoque académico:{' '}
|
{estructuraNombre}
|
||||||
</span>
|
</div>
|
||||||
<span className="font-medium">
|
</div>
|
||||||
{wizard.iaConfig?.descripcionEnfoqueAcademico || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Instrucciones adicionales:{' '}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.iaConfig?.instruccionesAdicionalesIA || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2">
|
<div className="border-border/60 bg-muted/30 grid gap-3 rounded-xl border p-4">
|
||||||
<div className="font-medium">Archivos de referencia</div>
|
<div className="flex items-end justify-between gap-2">
|
||||||
{archivosRef.length ? (
|
<div className="text-foreground text-base font-semibold">
|
||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
Materias seleccionadas
|
||||||
{archivosRef.map((id) => (
|
</div>
|
||||||
<li key={id}>{id}</li>
|
<div className="text-muted-foreground text-xs">
|
||||||
))}
|
{materiasSeleccionadas.length} en total
|
||||||
</ul>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{materiasSeleccionadas.length === 0 ? (
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
No hay materias seleccionadas.
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-muted-foreground text-xs">—</div>
|
<div className="grid gap-3">
|
||||||
|
{materiasSeleccionadas.map((m) => {
|
||||||
|
const lineaNombre = m.linea_plan_id
|
||||||
|
? (lineasPlan?.find((l) => l.id === m.linea_plan_id)
|
||||||
|
?.nombre ?? m.linea_plan_id)
|
||||||
|
: '—'
|
||||||
|
|
||||||
|
const cicloText =
|
||||||
|
typeof m.numero_ciclo === 'number' &&
|
||||||
|
Number.isFinite(m.numero_ciclo)
|
||||||
|
? String(m.numero_ciclo)
|
||||||
|
: '—'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
className="bg-background/40 border-border/60 grid gap-2 rounded-lg border p-3"
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="text-foreground text-sm font-semibold">
|
||||||
|
{m.nombre}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
|
||||||
|
<span className="bg-accent/30 text-accent-foreground rounded-full px-2 py-0.5">
|
||||||
|
Línea: {lineaNombre}
|
||||||
|
</span>
|
||||||
|
<span className="bg-accent/30 text-accent-foreground rounded-full px-2 py-0.5">
|
||||||
|
Ciclo: {cicloText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-muted-foreground text-sm whitespace-pre-wrap">
|
||||||
|
{m.descripcion || '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
<div>
|
) : (
|
||||||
<div className="font-medium">Repositorios de referencia</div>
|
<>
|
||||||
{repositoriosRef.length ? (
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
<div className="col-span-2">
|
||||||
{repositoriosRef.map((id) => (
|
<span className="text-muted-foreground">Nombre: </span>
|
||||||
<li key={id}>{id}</li>
|
<span className="font-medium">
|
||||||
))}
|
{wizard.datosBasicos.nombre || '—'}
|
||||||
</ul>
|
</span>
|
||||||
) : (
|
</div>
|
||||||
<div className="text-muted-foreground text-xs">—</div>
|
<div>
|
||||||
)}
|
<span className="text-muted-foreground">Código: </span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.datosBasicos.codigo || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Tipo: </span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.datosBasicos.tipo || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Créditos: </span>
|
||||||
|
<span className="font-medium">{creditosText}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Estructura: </span>
|
||||||
|
<span className="font-medium">{estructuraNombre}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Horas académicas:{' '}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.datosBasicos.horasAcademicas ?? '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Horas independientes:{' '}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.datosBasicos.horasIndependientes ?? '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="bg-muted/50 rounded-md p-3">
|
||||||
<div className="font-medium">Archivos adjuntos</div>
|
<div className="font-medium">Configuración IA</div>
|
||||||
{adjuntos.length ? (
|
<div className="mt-2 grid gap-2">
|
||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
<div>
|
||||||
{adjuntos.map((f) => (
|
<span className="text-muted-foreground">
|
||||||
<li key={f.id}>
|
Enfoque académico:{' '}
|
||||||
<span className="text-foreground">{f.file.name}</span>{' '}
|
</span>
|
||||||
<span>· {formatFileSize(f.file.size)}</span>
|
<span className="font-medium">
|
||||||
</li>
|
{wizard.iaConfig?.descripcionEnfoqueAcademico || '—'}
|
||||||
))}
|
</span>
|
||||||
</ul>
|
</div>
|
||||||
) : (
|
<div>
|
||||||
<div className="text-muted-foreground text-xs">—</div>
|
<span className="text-muted-foreground">
|
||||||
)}
|
Instrucciones adicionales:{' '}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.iaConfig?.instruccionesAdicionalesIA || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="font-medium">Archivos de referencia</div>
|
||||||
|
{archivosRef.length ? (
|
||||||
|
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||||
|
{archivosRef.map((id) => (
|
||||||
|
<li key={id}>{id}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground text-xs">—</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">
|
||||||
|
Repositorios de referencia
|
||||||
|
</div>
|
||||||
|
{repositoriosRef.length ? (
|
||||||
|
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||||
|
{repositoriosRef.map((id) => (
|
||||||
|
<li key={id}>{id}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground text-xs">—</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">Archivos adjuntos</div>
|
||||||
|
{adjuntos.length ? (
|
||||||
|
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||||
|
{adjuntos.map((f) => (
|
||||||
|
<li key={f.id}>
|
||||||
|
<span className="text-foreground">
|
||||||
|
{f.file.name}
|
||||||
|
</span>{' '}
|
||||||
|
<span>· {formatFileSize(f.file.size)}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground text-xs">—</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { Loader2 } from 'lucide-react'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import type { AIGenerateSubjectInput } from '@/data'
|
import type { AISubjectUnifiedInput } from '@/data'
|
||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
|
import type { TablesInsert } from '@/types/supabase'
|
||||||
|
import type { RealtimeChannel } from '@supabase/supabase-js'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useGenerateSubjectAI } from '@/data'
|
import {
|
||||||
|
supabaseBrowser,
|
||||||
|
useGenerateSubjectAI,
|
||||||
|
qk,
|
||||||
|
useCreateSubjectManual,
|
||||||
|
subjects_get_maybe,
|
||||||
|
subjects_get,
|
||||||
|
} from '@/data'
|
||||||
|
|
||||||
export function WizardControls({
|
export function WizardControls({
|
||||||
wizard,
|
wizard,
|
||||||
@@ -28,7 +40,158 @@ export function WizardControls({
|
|||||||
isLastStep: boolean
|
isLastStep: boolean
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const qc = useQueryClient()
|
||||||
const generateSubjectAI = useGenerateSubjectAI()
|
const generateSubjectAI = useGenerateSubjectAI()
|
||||||
|
const createSubjectManual = useCreateSubjectManual()
|
||||||
|
const [isSpinningIA, setIsSpinningIA] = useState(false)
|
||||||
|
const cancelledRef = useRef(false)
|
||||||
|
const realtimeChannelRef = useRef<RealtimeChannel | null>(null)
|
||||||
|
const watchSubjectIdRef = useRef<string | null>(null)
|
||||||
|
const watchTimeoutRef = useRef<number | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cancelledRef.current = false
|
||||||
|
return () => {
|
||||||
|
cancelledRef.current = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const stopSubjectWatch = useCallback(() => {
|
||||||
|
if (watchTimeoutRef.current) {
|
||||||
|
window.clearTimeout(watchTimeoutRef.current)
|
||||||
|
watchTimeoutRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
watchSubjectIdRef.current = null
|
||||||
|
|
||||||
|
const ch = realtimeChannelRef.current
|
||||||
|
if (ch) {
|
||||||
|
realtimeChannelRef.current = null
|
||||||
|
try {
|
||||||
|
supabaseBrowser().removeChannel(ch)
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
stopSubjectWatch()
|
||||||
|
}
|
||||||
|
}, [stopSubjectWatch])
|
||||||
|
|
||||||
|
const handleSubjectReady = (args: {
|
||||||
|
id: string
|
||||||
|
plan_estudio_id: string
|
||||||
|
estado?: unknown
|
||||||
|
}) => {
|
||||||
|
if (cancelledRef.current) return
|
||||||
|
|
||||||
|
const estado = String(args.estado ?? '').toLowerCase()
|
||||||
|
if (estado === 'generando') return
|
||||||
|
|
||||||
|
stopSubjectWatch()
|
||||||
|
setIsSpinningIA(false)
|
||||||
|
setWizard((w) => ({ ...w, isLoading: false }))
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: `/planes/${args.plan_estudio_id}/asignaturas/${args.id}`,
|
||||||
|
state: { showConfetti: true },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const beginSubjectWatch = (args: { subjectId: string; planId: string }) => {
|
||||||
|
stopSubjectWatch()
|
||||||
|
|
||||||
|
watchSubjectIdRef.current = args.subjectId
|
||||||
|
|
||||||
|
// Timeout de seguridad (mismo límite que teníamos con polling)
|
||||||
|
watchTimeoutRef.current = window.setTimeout(
|
||||||
|
() => {
|
||||||
|
if (cancelledRef.current) return
|
||||||
|
if (watchSubjectIdRef.current !== args.subjectId) return
|
||||||
|
|
||||||
|
stopSubjectWatch()
|
||||||
|
setIsSpinningIA(false)
|
||||||
|
setWizard((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage:
|
||||||
|
'La generación está tardando demasiado. Intenta de nuevo en unos minutos.',
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
6 * 60 * 1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const channel = supabase.channel(`asignaturas-status-${args.subjectId}`)
|
||||||
|
realtimeChannelRef.current = channel
|
||||||
|
|
||||||
|
channel.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{
|
||||||
|
event: 'UPDATE',
|
||||||
|
schema: 'public',
|
||||||
|
table: 'asignaturas',
|
||||||
|
filter: `id=eq.${args.subjectId}`,
|
||||||
|
},
|
||||||
|
(payload) => {
|
||||||
|
if (cancelledRef.current) return
|
||||||
|
|
||||||
|
const next: any = (payload as any)?.new
|
||||||
|
if (!next?.id || !next?.plan_estudio_id) return
|
||||||
|
handleSubjectReady({
|
||||||
|
id: String(next.id),
|
||||||
|
plan_estudio_id: String(next.plan_estudio_id),
|
||||||
|
estado: next.estado,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
channel.subscribe((status) => {
|
||||||
|
if (cancelledRef.current) return
|
||||||
|
if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
|
||||||
|
stopSubjectWatch()
|
||||||
|
setIsSpinningIA(false)
|
||||||
|
setWizard((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage:
|
||||||
|
'No se pudo suscribir al estado de la asignatura. Intenta de nuevo.',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadAiAttachments = async (args: {
|
||||||
|
planId: string
|
||||||
|
files: Array<{ file: File }>
|
||||||
|
}): Promise<Array<string>> => {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
if (!args.files.length) return []
|
||||||
|
|
||||||
|
const runId = crypto.randomUUID()
|
||||||
|
const basePath = `planes/${args.planId}/asignaturas/ai/${runId}`
|
||||||
|
|
||||||
|
const keys: Array<string> = []
|
||||||
|
for (const f of args.files) {
|
||||||
|
const safeName = (f.file.name || 'archivo').replace(/[\\/]+/g, '_')
|
||||||
|
const key = `${basePath}/${crypto.randomUUID()}-${safeName}`
|
||||||
|
|
||||||
|
const { error } = await supabase.storage
|
||||||
|
.from('ai-storage')
|
||||||
|
.upload(key, f.file, {
|
||||||
|
contentType: f.file.type || undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message)
|
||||||
|
keys.push(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
setWizard((w) => ({
|
setWizard((w) => ({
|
||||||
...w,
|
...w,
|
||||||
@@ -36,55 +199,324 @@ export function WizardControls({
|
|||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
let startedWaiting = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (wizard.tipoOrigen === 'IA') {
|
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
||||||
const aiInput: AIGenerateSubjectInput = {
|
if (!wizard.plan_estudio_id) {
|
||||||
|
throw new Error('Plan de estudio inválido.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const asignaturaOrigenId = wizard.clonInterno?.asignaturaOrigenId
|
||||||
|
if (!asignaturaOrigenId) {
|
||||||
|
throw new Error('Selecciona una asignatura fuente.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wizard.datosBasicos.estructuraId) {
|
||||||
|
throw new Error('Estructura inválida.')
|
||||||
|
}
|
||||||
|
if (!wizard.datosBasicos.nombre.trim()) {
|
||||||
|
throw new Error('Nombre inválido.')
|
||||||
|
}
|
||||||
|
if (wizard.datosBasicos.tipo == null) {
|
||||||
|
throw new Error('Tipo inválido.')
|
||||||
|
}
|
||||||
|
if (wizard.datosBasicos.creditos == null) {
|
||||||
|
throw new Error('Créditos inválidos.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fuente = await subjects_get(asignaturaOrigenId as any)
|
||||||
|
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const codigo = (wizard.datosBasicos.codigo ?? '').trim()
|
||||||
|
|
||||||
|
const payload: TablesInsert<'asignaturas'> = {
|
||||||
plan_estudio_id: wizard.plan_estudio_id,
|
plan_estudio_id: wizard.plan_estudio_id,
|
||||||
datosBasicos: {
|
estructura_id: wizard.datosBasicos.estructuraId,
|
||||||
|
codigo: codigo ? codigo : null,
|
||||||
|
nombre: wizard.datosBasicos.nombre,
|
||||||
|
tipo: wizard.datosBasicos.tipo,
|
||||||
|
creditos: wizard.datosBasicos.creditos,
|
||||||
|
datos: (fuente as any).datos,
|
||||||
|
contenido_tematico: (fuente as any).contenido_tematico,
|
||||||
|
criterios_de_evaluacion: (fuente as any).criterios_de_evaluacion,
|
||||||
|
tipo_origen: 'CLONADO_INTERNO',
|
||||||
|
meta_origen: {
|
||||||
|
...(fuente as any).meta_origen,
|
||||||
|
asignatura_origen_id: fuente.id,
|
||||||
|
plan_origen_id: (fuente as any).plan_estudio_id,
|
||||||
|
},
|
||||||
|
horas_academicas:
|
||||||
|
wizard.datosBasicos.horasAcademicas ??
|
||||||
|
(fuente as any).horas_academicas ??
|
||||||
|
null,
|
||||||
|
horas_independientes:
|
||||||
|
wizard.datosBasicos.horasIndependientes ??
|
||||||
|
(fuente as any).horas_independientes ??
|
||||||
|
null,
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('payload:', payload)
|
||||||
|
|
||||||
|
const { data: inserted, error: insertError } = await supabase
|
||||||
|
.from('asignaturas')
|
||||||
|
.insert(payload)
|
||||||
|
.select('id,plan_estudio_id')
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (insertError) throw new Error(insertError.message)
|
||||||
|
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: qk.planAsignaturas(wizard.plan_estudio_id),
|
||||||
|
})
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: qk.planHistorial(wizard.plan_estudio_id),
|
||||||
|
})
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: `/planes/${inserted.plan_estudio_id}/asignaturas/${inserted.id}`,
|
||||||
|
state: { showConfetti: true },
|
||||||
|
resetScroll: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wizard.tipoOrigen === 'IA_SIMPLE') {
|
||||||
|
if (!wizard.plan_estudio_id) {
|
||||||
|
throw new Error('Plan de estudio inválido.')
|
||||||
|
}
|
||||||
|
if (!wizard.datosBasicos.estructuraId) {
|
||||||
|
throw new Error('Estructura inválida.')
|
||||||
|
}
|
||||||
|
if (!wizard.datosBasicos.nombre.trim()) {
|
||||||
|
throw new Error('Nombre inválido.')
|
||||||
|
}
|
||||||
|
if (wizard.datosBasicos.creditos == null) {
|
||||||
|
throw new Error('Créditos inválidos.')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${new Date().toISOString()} - Insertando asignatura IA`)
|
||||||
|
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const placeholder: TablesInsert<'asignaturas'> = {
|
||||||
|
plan_estudio_id: wizard.plan_estudio_id,
|
||||||
|
estructura_id: wizard.datosBasicos.estructuraId,
|
||||||
|
nombre: wizard.datosBasicos.nombre,
|
||||||
|
codigo: wizard.datosBasicos.codigo ?? null,
|
||||||
|
tipo: wizard.datosBasicos.tipo ?? undefined,
|
||||||
|
creditos: wizard.datosBasicos.creditos,
|
||||||
|
horas_academicas: wizard.datosBasicos.horasAcademicas ?? null,
|
||||||
|
horas_independientes: wizard.datosBasicos.horasIndependientes ?? null,
|
||||||
|
estado: 'generando',
|
||||||
|
tipo_origen: 'IA',
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: inserted, error: insertError } = await supabase
|
||||||
|
.from('asignaturas')
|
||||||
|
.insert(placeholder)
|
||||||
|
.select('id,plan_estudio_id')
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (insertError) throw new Error(insertError.message)
|
||||||
|
const subjectId = inserted.id
|
||||||
|
|
||||||
|
setIsSpinningIA(true)
|
||||||
|
|
||||||
|
// Inicia watch realtime antes de disparar la Edge para no perder updates.
|
||||||
|
startedWaiting = true
|
||||||
|
beginSubjectWatch({ subjectId, planId: wizard.plan_estudio_id })
|
||||||
|
|
||||||
|
const archivosAdjuntos = await uploadAiAttachments({
|
||||||
|
planId: wizard.plan_estudio_id,
|
||||||
|
files: (wizard.iaConfig?.archivosAdjuntos ?? []).map((x) => ({
|
||||||
|
file: x.file,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
const payload: AISubjectUnifiedInput = {
|
||||||
|
datosUpdate: {
|
||||||
|
id: subjectId,
|
||||||
|
plan_estudio_id: wizard.plan_estudio_id,
|
||||||
|
estructura_id: wizard.datosBasicos.estructuraId,
|
||||||
nombre: wizard.datosBasicos.nombre,
|
nombre: wizard.datosBasicos.nombre,
|
||||||
codigo: wizard.datosBasicos.codigo,
|
codigo: wizard.datosBasicos.codigo ?? null,
|
||||||
tipo: wizard.datosBasicos.tipo!,
|
tipo: wizard.datosBasicos.tipo ?? null,
|
||||||
creditos: wizard.datosBasicos.creditos!,
|
creditos: wizard.datosBasicos.creditos,
|
||||||
horasIndependientes: wizard.datosBasicos.horasIndependientes,
|
horas_academicas: wizard.datosBasicos.horasAcademicas ?? null,
|
||||||
horasAcademicas: wizard.datosBasicos.horasAcademicas,
|
horas_independientes:
|
||||||
estructuraId: wizard.datosBasicos.estructuraId!,
|
wizard.datosBasicos.horasIndependientes ?? null,
|
||||||
},
|
},
|
||||||
iaConfig: {
|
iaConfig: {
|
||||||
descripcionEnfoqueAcademico:
|
descripcionEnfoqueAcademico:
|
||||||
wizard.iaConfig!.descripcionEnfoqueAcademico,
|
wizard.iaConfig?.descripcionEnfoqueAcademico ?? undefined,
|
||||||
instruccionesAdicionalesIA:
|
instruccionesAdicionalesIA:
|
||||||
wizard.iaConfig!.instruccionesAdicionalesIA,
|
wizard.iaConfig?.instruccionesAdicionalesIA ?? undefined,
|
||||||
archivosReferencia: wizard.iaConfig!.archivosReferencia,
|
archivosAdjuntos,
|
||||||
repositoriosReferencia:
|
|
||||||
wizard.iaConfig!.repositoriosReferencia || [],
|
|
||||||
archivosAdjuntos: wizard.iaConfig!.archivosAdjuntos || [],
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`${new Date().toISOString()} - Enviando a generar asignatura con IA`,
|
`${new Date().toISOString()} - Disparando Edge IA asignatura (unified)`,
|
||||||
)
|
)
|
||||||
|
|
||||||
const asignatura = await generateSubjectAI.mutateAsync(aiInput)
|
await generateSubjectAI.mutateAsync(payload as any)
|
||||||
console.log(
|
|
||||||
`${new Date().toISOString()} - Asignatura IA generada`,
|
// Fallback: una lectura puntual por si el UPDATE llegó antes de suscribir.
|
||||||
asignatura,
|
const latest = await subjects_get_maybe(subjectId)
|
||||||
|
if (latest) {
|
||||||
|
handleSubjectReady({
|
||||||
|
id: latest.id as any,
|
||||||
|
plan_estudio_id: latest.plan_estudio_id as any,
|
||||||
|
estado: (latest as any).estado,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wizard.tipoOrigen === 'IA_MULTIPLE') {
|
||||||
|
const selected = wizard.sugerencias.filter((s) => s.selected)
|
||||||
|
|
||||||
|
if (selected.length === 0) {
|
||||||
|
throw new Error('Selecciona al menos una sugerencia.')
|
||||||
|
}
|
||||||
|
if (!wizard.plan_estudio_id) {
|
||||||
|
throw new Error('Plan de estudio inválido.')
|
||||||
|
}
|
||||||
|
if (!wizard.estructuraId) {
|
||||||
|
throw new Error('Selecciona una estructura para continuar.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
setIsSpinningIA(true)
|
||||||
|
|
||||||
|
const archivosAdjuntos = await uploadAiAttachments({
|
||||||
|
planId: wizard.plan_estudio_id,
|
||||||
|
files: (wizard.iaConfig?.archivosAdjuntos ?? []).map((x) => ({
|
||||||
|
file: x.file,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
const placeholders: Array<TablesInsert<'asignaturas'>> = selected.map(
|
||||||
|
(s): TablesInsert<'asignaturas'> => ({
|
||||||
|
plan_estudio_id: wizard.plan_estudio_id,
|
||||||
|
estructura_id: wizard.estructuraId,
|
||||||
|
estado: 'generando',
|
||||||
|
nombre: s.nombre,
|
||||||
|
codigo: s.codigo ?? null,
|
||||||
|
tipo: s.tipo ?? undefined,
|
||||||
|
creditos: s.creditos ?? 0,
|
||||||
|
horas_academicas: s.horasAcademicas ?? null,
|
||||||
|
horas_independientes: s.horasIndependientes ?? null,
|
||||||
|
linea_plan_id: s.linea_plan_id ?? null,
|
||||||
|
numero_ciclo: s.numero_ciclo ?? null,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { data: inserted, error: insertError } = await supabase
|
||||||
|
.from('asignaturas')
|
||||||
|
.insert(placeholders)
|
||||||
|
.select('id')
|
||||||
|
|
||||||
|
if (insertError) {
|
||||||
|
throw new Error(insertError.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertedIds = inserted.map((r) => r.id)
|
||||||
|
if (insertedIds.length !== selected.length) {
|
||||||
|
throw new Error('No se pudieron crear todas las asignaturas.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disparar generación en paralelo (no bloquear navegación)
|
||||||
|
insertedIds.forEach((id, idx) => {
|
||||||
|
const s = selected[idx]
|
||||||
|
const creditosForEdge =
|
||||||
|
typeof s.creditos === 'number' && s.creditos > 0
|
||||||
|
? s.creditos
|
||||||
|
: undefined
|
||||||
|
const payload: AISubjectUnifiedInput = {
|
||||||
|
datosUpdate: {
|
||||||
|
id,
|
||||||
|
plan_estudio_id: wizard.plan_estudio_id,
|
||||||
|
estructura_id: wizard.estructuraId ?? undefined,
|
||||||
|
nombre: s.nombre,
|
||||||
|
codigo: s.codigo ?? null,
|
||||||
|
tipo: s.tipo ?? null,
|
||||||
|
creditos: creditosForEdge,
|
||||||
|
horas_academicas: s.horasAcademicas ?? null,
|
||||||
|
horas_independientes: s.horasIndependientes ?? null,
|
||||||
|
numero_ciclo: s.numero_ciclo ?? null,
|
||||||
|
linea_plan_id: s.linea_plan_id ?? null,
|
||||||
|
},
|
||||||
|
iaConfig: {
|
||||||
|
descripcionEnfoqueAcademico: s.descripcion,
|
||||||
|
instruccionesAdicionalesIA:
|
||||||
|
wizard.iaConfig?.instruccionesAdicionalesIA ?? undefined,
|
||||||
|
archivosAdjuntos,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
void generateSubjectAI.mutateAsync(payload as any).catch((e) => {
|
||||||
|
console.error('Error generando asignatura IA (multiple):', e)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Invalidar la query del listado del plan (una vez) para que la lista
|
||||||
|
// muestre el estado actualizado y recargue cuando lleguen updates.
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: qk.planAsignaturas(wizard.plan_estudio_id),
|
||||||
|
})
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: `/planes/${wizard.plan_estudio_id}/asignaturas`,
|
||||||
|
resetScroll: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsSpinningIA(false)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wizard.tipoOrigen === 'MANUAL') {
|
||||||
|
if (!wizard.plan_estudio_id) {
|
||||||
|
throw new Error('Plan de estudio inválido.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const asignatura = await createSubjectManual.mutateAsync({
|
||||||
|
plan_estudio_id: wizard.plan_estudio_id,
|
||||||
|
estructura_id: wizard.datosBasicos.estructuraId!,
|
||||||
|
nombre: wizard.datosBasicos.nombre,
|
||||||
|
codigo: wizard.datosBasicos.codigo ?? null,
|
||||||
|
tipo: wizard.datosBasicos.tipo ?? undefined,
|
||||||
|
creditos: wizard.datosBasicos.creditos ?? 0,
|
||||||
|
horas_academicas: wizard.datosBasicos.horasAcademicas ?? null,
|
||||||
|
horas_independientes: wizard.datosBasicos.horasIndependientes ?? null,
|
||||||
|
linea_plan_id: null,
|
||||||
|
numero_ciclo: null,
|
||||||
|
})
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
to: `/planes/${wizard.plan_estudio_id}/asignaturas/${asignatura.id}`,
|
to: `/planes/${wizard.plan_estudio_id}/asignaturas/${asignatura.id}`,
|
||||||
state: { showConfetti: true },
|
state: { showConfetti: true },
|
||||||
|
resetScroll: false,
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
setIsSpinningIA(false)
|
||||||
|
stopSubjectWatch()
|
||||||
setWizard((w) => ({
|
setWizard((w) => ({
|
||||||
...w,
|
...w,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errorMessage: err?.message ?? 'Error creando la asignatura',
|
errorMessage: err?.message ?? 'Error creando la asignatura',
|
||||||
}))
|
}))
|
||||||
} finally {
|
} finally {
|
||||||
setWizard((w) => ({ ...w, isLoading: false }))
|
if (!startedWaiting) {
|
||||||
|
setIsSpinningIA(false)
|
||||||
|
setWizard((w) => ({ ...w, isLoading: false }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +526,7 @@ export function WizardControls({
|
|||||||
Anterior
|
Anterior
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="mx-2 flex-1">
|
||||||
{(errorMessage ?? wizard.errorMessage) && (
|
{(errorMessage ?? wizard.errorMessage) && (
|
||||||
<span className="text-destructive text-sm font-medium">
|
<span className="text-destructive text-sm font-medium">
|
||||||
{errorMessage ?? wizard.errorMessage}
|
{errorMessage ?? wizard.errorMessage}
|
||||||
@@ -102,6 +534,17 @@ export function WizardControls({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-2 flex w-5 items-center justify-center">
|
||||||
|
<Loader2
|
||||||
|
className={
|
||||||
|
wizard.tipoOrigen === 'IA_SIMPLE' && isSpinningIA
|
||||||
|
? 'text-muted-foreground h-6 w-6 animate-spin'
|
||||||
|
: 'h-6 w-6 opacity-0'
|
||||||
|
}
|
||||||
|
aria-hidden={!(wizard.tipoOrigen === 'IA_SIMPLE' && isSpinningIA)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isLastStep ? (
|
{isLastStep ? (
|
||||||
<Button onClick={handleCreate} disabled={disableCreate}>
|
<Button onClick={handleCreate} disabled={disableCreate}>
|
||||||
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
|
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ export function LoginCard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
|
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<img
|
||||||
|
src="/4_IMAGOTIPO_LASALLE_MEXICO_COLOR_RGB_2020.png"
|
||||||
|
alt="La Salle México"
|
||||||
|
className="mb-6 h-20 w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<h1 className="mb-1 text-center text-2xl font-semibold">
|
<h1 className="mb-1 text-center text-2xl font-semibold">
|
||||||
Iniciar sesión
|
Iniciar sesión
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -4,20 +4,18 @@ import type { LucideIcon } from 'lucide-react'
|
|||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
|
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
|
||||||
import { cn } from '@/lib/utils' // Asegúrate de tener tu utilidad cn
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface PlanEstudiosCardProps {
|
interface PlanEstudiosCardProps {
|
||||||
/** El componente del ícono importado de lucide-react (ej. BookOpen) */
|
|
||||||
Icono: LucideIcon
|
Icono: LucideIcon
|
||||||
nombrePrograma: string
|
nombrePrograma: string
|
||||||
nivel: string
|
nivel: string
|
||||||
ciclos: string | number // Acepta "8" o "8 semestres"
|
ciclos: string | number
|
||||||
facultad: string
|
facultad: string
|
||||||
estado: string
|
estado: string
|
||||||
/** Código hex o variable CSS (ej. "#ef4444" o "var(--primary)") */
|
|
||||||
claseColorEstado?: string
|
claseColorEstado?: string
|
||||||
|
colorEstadoHex?: string
|
||||||
colorFacultad: string
|
colorFacultad: string
|
||||||
/** Opcional: para manejar el click en la tarjeta */
|
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,52 +27,62 @@ export default function PlanEstudiosCard({
|
|||||||
facultad,
|
facultad,
|
||||||
estado,
|
estado,
|
||||||
claseColorEstado = '',
|
claseColorEstado = '',
|
||||||
|
colorEstadoHex,
|
||||||
colorFacultad,
|
colorFacultad,
|
||||||
onClick,
|
onClick,
|
||||||
}: PlanEstudiosCardProps) {
|
}: PlanEstudiosCardProps) {
|
||||||
|
const badgeStyle = colorEstadoHex
|
||||||
|
? ({
|
||||||
|
backgroundColor: colorEstadoHex,
|
||||||
|
borderColor: colorEstadoHex,
|
||||||
|
} as const)
|
||||||
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex h-full cursor-pointer flex-col justify-between gap-2 overflow-hidden border-l-4 transition-all hover:shadow-lg',
|
'group relative flex h-full cursor-pointer flex-col justify-between overflow-hidden transition-all hover:shadow-lg',
|
||||||
)}
|
)}
|
||||||
// Aplicamos el color de la facultad dinámicamente al borde y un fondo muy sutil
|
|
||||||
style={{
|
|
||||||
borderLeftColor: colorFacultad,
|
|
||||||
backgroundColor: `color-mix(in srgb, ${colorFacultad}, transparent 95%)`, // Truco CSS moderno para fondo tintado
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-2">
|
<div className="flex grow flex-col">
|
||||||
{/* Ícono con el color de la facultad */}
|
<CardHeader className="pb-2">
|
||||||
<div
|
{/* Círculo del ícono con el color de la facultad */}
|
||||||
className="mb-2 w-fit rounded-md p-2"
|
<div
|
||||||
style={{
|
className="mb-2 w-fit rounded-full p-2.5"
|
||||||
backgroundColor: `color-mix(in srgb, ${colorFacultad}, transparent 85%)`,
|
style={{
|
||||||
}}
|
backgroundColor: `color-mix(in srgb, ${colorFacultad}, transparent 85%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icono size={24} style={{ color: colorFacultad }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Título del Programa */}
|
||||||
|
<h4 className="line-clamp-2 text-lg leading-tight font-bold tracking-tight">
|
||||||
|
{nombrePrograma}
|
||||||
|
</h4>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="text-muted-foreground space-y-1 text-sm">
|
||||||
|
<p className="text-foreground font-medium">
|
||||||
|
{nivel} • {ciclos}
|
||||||
|
</p>
|
||||||
|
<p>{facultad}</p>
|
||||||
|
</CardContent>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardFooter className="flex items-center justify-between pt-0 pb-6">
|
||||||
|
<Badge
|
||||||
|
style={badgeStyle}
|
||||||
|
className={cn(
|
||||||
|
'text-sm font-semibold',
|
||||||
|
!colorEstadoHex && claseColorEstado,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Icono size={24} style={{ color: colorFacultad }} />
|
<span className="text-white [text-shadow:1px_1px_0_#000,-1px_-1px_0_#000,1px_-1px_0_#000,-1px_1px_0_#000,0_1px_0_#000,0_-1px_0_#000,1px_0_0_#000,-1px_0_0_#000]">
|
||||||
</div>
|
{estado}
|
||||||
|
</span>
|
||||||
{/* Título del Programa */}
|
|
||||||
<h4 className="line-clamp-2 text-lg leading-tight font-bold tracking-tight">
|
|
||||||
{nombrePrograma}
|
|
||||||
</h4>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="text-muted-foreground space-y-1 text-sm">
|
|
||||||
<p className="text-foreground font-medium">
|
|
||||||
{nivel} • {ciclos}
|
|
||||||
</p>
|
|
||||||
<p>{facultad}</p>
|
|
||||||
</CardContent>
|
|
||||||
|
|
||||||
<CardFooter className="bg-background/50 flex items-center justify-between border-t px-6 pb-3 backdrop-blur-sm [.border-t]:pt-3">
|
|
||||||
<Badge className={`text-sm font-semibold ${claseColorEstado}`}>
|
|
||||||
{estado}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
{/* <span className="text-foreground/80 text-sm font-semibold">
|
|
||||||
{estado}
|
|
||||||
</span> */}
|
|
||||||
|
|
||||||
{/* Flecha animada */}
|
{/* Flecha animada */}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { Check, Loader2 } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { useUpdatePlanFields, useUpdateRecommendationApplied } from '@/data'
|
||||||
|
|
||||||
|
export const ImprovementCard = ({
|
||||||
|
suggestions,
|
||||||
|
onApply,
|
||||||
|
planId,
|
||||||
|
dbMessageId,
|
||||||
|
currentDatos,
|
||||||
|
activeChatId,
|
||||||
|
onApplySuccess,
|
||||||
|
}: {
|
||||||
|
suggestions: Array<any>
|
||||||
|
onApply?: (key: string, value: string) => void
|
||||||
|
planId: string
|
||||||
|
currentDatos: any
|
||||||
|
dbMessageId: string
|
||||||
|
activeChatId: any
|
||||||
|
onApplySuccess?: (key: string) => void
|
||||||
|
}) => {
|
||||||
|
const [localApplied, setLocalApplied] = useState<Array<string>>([])
|
||||||
|
const updatePlan = useUpdatePlanFields()
|
||||||
|
const updateAppliedStatus = useUpdateRecommendationApplied()
|
||||||
|
|
||||||
|
const handleApply = (key: string, newValue: string) => {
|
||||||
|
if (!currentDatos) return
|
||||||
|
const currentValue = currentDatos[key]
|
||||||
|
let finalValue: any
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof currentValue === 'object' &&
|
||||||
|
currentValue !== null &&
|
||||||
|
'description' in currentValue
|
||||||
|
) {
|
||||||
|
finalValue = { ...currentValue, description: newValue }
|
||||||
|
} else {
|
||||||
|
finalValue = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
const datosActualizados = {
|
||||||
|
...currentDatos,
|
||||||
|
[key]: finalValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlan.mutate(
|
||||||
|
{
|
||||||
|
planId: planId as any,
|
||||||
|
patch: { datos: datosActualizados },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setLocalApplied((prev) => [...prev, key])
|
||||||
|
|
||||||
|
if (onApplySuccess) onApplySuccess(key)
|
||||||
|
|
||||||
|
if (dbMessageId) {
|
||||||
|
updateAppliedStatus.mutate({
|
||||||
|
conversacionId: dbMessageId,
|
||||||
|
campoAfectado: key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onApply) onApply(key, newValue)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-2 flex w-full flex-col gap-4">
|
||||||
|
{suggestions.map((sug) => {
|
||||||
|
const isApplied = sug.applied === true || localApplied.includes(sug.key)
|
||||||
|
const isUpdating =
|
||||||
|
updatePlan.isPending &&
|
||||||
|
updatePlan.variables.patch.datos?.[sug.key] !== undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={sug.key}
|
||||||
|
className={`bg-card rounded-2xl border p-5 shadow-sm transition-all ${
|
||||||
|
isApplied ? 'border-primary/30 bg-primary/5' : 'border-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h3 className="text-foreground text-sm font-bold">{sug.label}</h3>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleApply(sug.key, sug.newValue)}
|
||||||
|
disabled={isApplied || !!isUpdating}
|
||||||
|
variant={isApplied ? 'secondary' : 'default'}
|
||||||
|
className="h-8 rounded-full px-4 text-xs transition-all"
|
||||||
|
>
|
||||||
|
{isUpdating ? (
|
||||||
|
<Loader2 size={12} className="animate-spin" />
|
||||||
|
) : isApplied ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Check size={12} /> Aplicado
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'Aplicar mejora'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`rounded-xl border p-3 text-sm transition-colors duration-300 ${
|
||||||
|
isApplied
|
||||||
|
? 'border-primary/20 bg-primary/10 text-foreground'
|
||||||
|
: 'border-border bg-muted/50 text-muted-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{sug.newValue}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { Loader2 } from 'lucide-react'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import type { AIGeneratePlanInput } from '@/data'
|
import type { AIGeneratePlanInput } from '@/data'
|
||||||
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
|
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
|
||||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
||||||
// import type { Database } from '@/types/supabase'
|
// import type { Database } from '@/types/supabase'
|
||||||
|
import type { RealtimeChannel } from '@supabase/supabase-js'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
// import { supabaseBrowser } from '@/data'
|
import { plans_get_maybe } from '@/data/api/plans.api'
|
||||||
import { useCreatePlanManual, useGeneratePlanAI } from '@/data/hooks/usePlans'
|
import {
|
||||||
|
useCreatePlanManual,
|
||||||
|
useDeletePlanEstudio,
|
||||||
|
useGeneratePlanAI,
|
||||||
|
} from '@/data/hooks/usePlans'
|
||||||
|
import { supabaseBrowser } from '@/data/supabase/client'
|
||||||
|
|
||||||
export function WizardControls({
|
export function WizardControls({
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -33,8 +41,152 @@ export function WizardControls({
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const generatePlanAI = useGeneratePlanAI()
|
const generatePlanAI = useGeneratePlanAI()
|
||||||
const createPlanManual = useCreatePlanManual()
|
const createPlanManual = useCreatePlanManual()
|
||||||
// const supabaseClient = supabaseBrowser()
|
const deletePlan = useDeletePlanEstudio()
|
||||||
// const persistPlanFromAI = usePersistPlanFromAI()
|
const [isSpinningIA, setIsSpinningIA] = useState(false)
|
||||||
|
const cancelledRef = useRef(false)
|
||||||
|
const realtimeChannelRef = useRef<RealtimeChannel | null>(null)
|
||||||
|
const watchPlanIdRef = useRef<string | null>(null)
|
||||||
|
const watchTimeoutRef = useRef<number | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cancelledRef.current = false
|
||||||
|
return () => {
|
||||||
|
cancelledRef.current = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const stopPlanWatch = useCallback(() => {
|
||||||
|
if (watchTimeoutRef.current) {
|
||||||
|
window.clearTimeout(watchTimeoutRef.current)
|
||||||
|
watchTimeoutRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
watchPlanIdRef.current = null
|
||||||
|
|
||||||
|
const ch = realtimeChannelRef.current
|
||||||
|
if (ch) {
|
||||||
|
realtimeChannelRef.current = null
|
||||||
|
try {
|
||||||
|
supabaseBrowser().removeChannel(ch)
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
stopPlanWatch()
|
||||||
|
}
|
||||||
|
}, [stopPlanWatch])
|
||||||
|
|
||||||
|
const checkPlanStateAndAct = useCallback(
|
||||||
|
async (planId: string) => {
|
||||||
|
if (cancelledRef.current) return
|
||||||
|
if (watchPlanIdRef.current !== planId) return
|
||||||
|
|
||||||
|
const plan = await plans_get_maybe(planId as any)
|
||||||
|
if (!plan) return
|
||||||
|
|
||||||
|
const clave = String(plan.estados_plan?.clave ?? '').toUpperCase()
|
||||||
|
|
||||||
|
if (clave.startsWith('GENERANDO')) return
|
||||||
|
|
||||||
|
if (clave.startsWith('BORRADOR')) {
|
||||||
|
stopPlanWatch()
|
||||||
|
setIsSpinningIA(false)
|
||||||
|
setWizard((w) => ({ ...w, isLoading: false }))
|
||||||
|
navigate({
|
||||||
|
to: `/planes/${plan.id}`,
|
||||||
|
state: { showConfetti: true },
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clave.startsWith('FALLID')) {
|
||||||
|
stopPlanWatch()
|
||||||
|
setIsSpinningIA(false)
|
||||||
|
|
||||||
|
deletePlan
|
||||||
|
.mutateAsync(plan.id)
|
||||||
|
.catch(() => {
|
||||||
|
// Si falla el borrado, igual mostramos el error.
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setWizard((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: 'La generación del plan falló',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[deletePlan, navigate, setWizard, stopPlanWatch],
|
||||||
|
)
|
||||||
|
|
||||||
|
const beginPlanWatch = useCallback(
|
||||||
|
(planId: string) => {
|
||||||
|
stopPlanWatch()
|
||||||
|
watchPlanIdRef.current = planId
|
||||||
|
|
||||||
|
watchTimeoutRef.current = window.setTimeout(
|
||||||
|
() => {
|
||||||
|
if (cancelledRef.current) return
|
||||||
|
if (watchPlanIdRef.current !== planId) return
|
||||||
|
|
||||||
|
stopPlanWatch()
|
||||||
|
setIsSpinningIA(false)
|
||||||
|
setWizard((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage:
|
||||||
|
'La generación está tardando demasiado. Intenta de nuevo en unos minutos.',
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
6 * 60 * 1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const channel = supabase.channel(`planes-status-${planId}`)
|
||||||
|
realtimeChannelRef.current = channel
|
||||||
|
|
||||||
|
channel.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{
|
||||||
|
event: '*',
|
||||||
|
schema: 'public',
|
||||||
|
table: 'planes_estudio',
|
||||||
|
filter: `id=eq.${planId}`,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
void checkPlanStateAndAct(planId)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
channel.subscribe((status) => {
|
||||||
|
const st = status as
|
||||||
|
| 'SUBSCRIBED'
|
||||||
|
| 'TIMED_OUT'
|
||||||
|
| 'CLOSED'
|
||||||
|
| 'CHANNEL_ERROR'
|
||||||
|
if (cancelledRef.current) return
|
||||||
|
if (st === 'CHANNEL_ERROR' || st === 'TIMED_OUT') {
|
||||||
|
stopPlanWatch()
|
||||||
|
setIsSpinningIA(false)
|
||||||
|
setWizard((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage:
|
||||||
|
'No se pudo suscribir al estado del plan. Intenta de nuevo.',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fallback inmediato por si el plan ya cambió antes de suscribir.
|
||||||
|
void checkPlanStateAndAct(planId)
|
||||||
|
},
|
||||||
|
[checkPlanStateAndAct, setWizard, stopPlanWatch],
|
||||||
|
)
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
// Start loading
|
// Start loading
|
||||||
@@ -78,13 +230,17 @@ export function WizardControls({
|
|||||||
|
|
||||||
console.log(`${new Date().toISOString()} - Enviando a generar plan IA`)
|
console.log(`${new Date().toISOString()} - Enviando a generar plan IA`)
|
||||||
|
|
||||||
const plan = await generatePlanAI.mutateAsync(aiInput as any)
|
setIsSpinningIA(true)
|
||||||
console.log(`${new Date().toISOString()} - Plan IA generado`, plan)
|
const resp: any = await generatePlanAI.mutateAsync(aiInput as any)
|
||||||
|
const planId = resp?.plan?.id ?? resp?.id
|
||||||
|
console.log(`${new Date().toISOString()} - Plan IA generado`, resp)
|
||||||
|
|
||||||
navigate({
|
if (!planId) {
|
||||||
to: `/planes/${plan.id}`,
|
throw new Error('No se pudo obtener el id del plan generado por IA')
|
||||||
state: { showConfetti: true },
|
}
|
||||||
})
|
|
||||||
|
// Inicia realtime; los efectos navegan o marcan error.
|
||||||
|
beginPlanWatch(String(planId))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,13 +264,15 @@ export function WizardControls({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
setIsSpinningIA(false)
|
||||||
|
stopPlanWatch()
|
||||||
setWizard((w) => ({
|
setWizard((w) => ({
|
||||||
...w,
|
...w,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errorMessage: err?.message ?? 'Error generando el plan',
|
errorMessage: err?.message ?? 'Error generando el plan',
|
||||||
}))
|
}))
|
||||||
} finally {
|
} finally {
|
||||||
setWizard((w) => ({ ...w, isLoading: false }))
|
// Si entramos en watch realtime, el loading se corta desde checkPlanStateAndAct.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +288,17 @@ export function WizardControls({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-2 flex w-5 items-center justify-center">
|
||||||
|
<Loader2
|
||||||
|
className={
|
||||||
|
wizard.tipoOrigen === 'IA' && isSpinningIA
|
||||||
|
? 'text-muted-foreground h-6 w-6 animate-spin'
|
||||||
|
: 'h-6 w-6 opacity-0'
|
||||||
|
}
|
||||||
|
aria-hidden={!(wizard.tipoOrigen === 'IA' && isSpinningIA)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{isLastStep ? (
|
{isLastStep ? (
|
||||||
<Button onClick={handleCreate} disabled={disableCreate}>
|
<Button onClick={handleCreate} disabled={disableCreate}>
|
||||||
Crear plan
|
Crear plan
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { StatusBadge } from "./StatusBadge";
|
|
||||||
|
|
||||||
export function PlanCard({ plan }: any) {
|
|
||||||
return (
|
|
||||||
<div className="bg-[#eaf6fa] rounded-2xl p-6 border hover:shadow-md transition">
|
|
||||||
<div className="flex justify-between items-start mb-4">
|
|
||||||
<span className="text-sm text-gray-500">⚙ Ingeniería</span>
|
|
||||||
<StatusBadge status={plan.status} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">
|
|
||||||
{plan.title}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p className="text-sm text-gray-600 mb-6">
|
|
||||||
{plan.subtitle}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex justify-between text-sm text-gray-500">
|
|
||||||
<span>{plan.cycles} ciclos</span>
|
|
||||||
<span>{plan.credits} créditos</span>
|
|
||||||
<span>➜</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { PlanCard } from './PlanCard'
|
|
||||||
|
|
||||||
const mockPlans = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Ingeniería en Sistemas',
|
|
||||||
level: 'Licenciatura',
|
|
||||||
status: 'Activo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Arquitectura',
|
|
||||||
level: 'Licenciatura',
|
|
||||||
status: 'Activo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Maestría en Educación',
|
|
||||||
level: 'Maestría',
|
|
||||||
status: 'Inactivo',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function PlanGrid() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold mb-4">
|
|
||||||
Planes disponibles
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{mockPlans.map(plan => (
|
|
||||||
<PlanCard key={plan.id} plan={plan} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export function StatCard({ icon, value, label }: any) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-xl border p-6">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<span className="text-xl">{icon}</span>
|
|
||||||
<span className="text-2xl font-semibold">{value}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-500">{label}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { StatCard } from "./StatCard";
|
|
||||||
|
|
||||||
export function StatsGrid() {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
||||||
<StatCard icon="📘" value="12" label="Planes Activos" />
|
|
||||||
<StatCard icon="🕒" value="4" label="En Revisión" />
|
|
||||||
<StatCard icon="✅" value="8" label="Aprobados" />
|
|
||||||
<StatCard icon="👥" value="6" label="Carreras" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
export function StatusBadge({ status }: { status: string }) {
|
|
||||||
const styles: any = {
|
|
||||||
revision: 'bg-blue-100 text-blue-700',
|
|
||||||
aprobado: 'bg-green-100 text-green-700',
|
|
||||||
borrador: 'bg-gray-200 text-gray-600',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`text-xs px-3 py-1 rounded-full font-medium ${styles[status]}`}
|
|
||||||
>
|
|
||||||
{status === 'revision'
|
|
||||||
? 'En Revisión'
|
|
||||||
: status === 'aprobado'
|
|
||||||
? 'Aprobado'
|
|
||||||
: 'Borrador'}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from '@/components/ui/pagination'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
export type Pagination03Props = {
|
||||||
|
page: number
|
||||||
|
pageCount: number
|
||||||
|
onPageChange: (page: number) => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const toInt = (n: unknown, fallback: number) => {
|
||||||
|
const x = typeof n === 'number' ? n : Number(n)
|
||||||
|
return Number.isFinite(x) ? Math.floor(x) : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageItems(page: number, pageCount: number): Array<number | '...'> {
|
||||||
|
if (pageCount <= 7) {
|
||||||
|
return Array.from({ length: pageCount }, (_, i) => i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const items: Array<number | '...'> = []
|
||||||
|
const safePage = Math.min(Math.max(page, 1), pageCount)
|
||||||
|
|
||||||
|
items.push(1)
|
||||||
|
|
||||||
|
const start = Math.max(2, safePage - 1)
|
||||||
|
const end = Math.min(pageCount - 1, safePage + 1)
|
||||||
|
|
||||||
|
if (start > 2) items.push('...')
|
||||||
|
for (let p = start; p <= end; p++) items.push(p)
|
||||||
|
if (end < pageCount - 1) items.push('...')
|
||||||
|
|
||||||
|
items.push(pageCount)
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Pagination03({
|
||||||
|
page,
|
||||||
|
pageCount,
|
||||||
|
onPageChange,
|
||||||
|
className,
|
||||||
|
}: Pagination03Props) {
|
||||||
|
const safePageCount = Math.max(1, toInt(pageCount, 1))
|
||||||
|
const safePage = Math.min(Math.max(toInt(page, 1), 1), safePageCount)
|
||||||
|
|
||||||
|
const items = getPageItems(safePage, safePageCount)
|
||||||
|
const canPrev = safePage > 1
|
||||||
|
const canNext = safePage < safePageCount
|
||||||
|
|
||||||
|
const go = (p: number) => {
|
||||||
|
const next = Math.min(Math.max(p, 1), safePageCount)
|
||||||
|
if (next === safePage) return
|
||||||
|
onPageChange(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pagination className={className}>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationPrevious
|
||||||
|
href="#"
|
||||||
|
className={cn(!canPrev && 'pointer-events-none opacity-50')}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!canPrev) return
|
||||||
|
go(safePage - 1)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
|
||||||
|
{items.map((it, idx) =>
|
||||||
|
it === '...' ? (
|
||||||
|
<PaginationItem key={`ellipsis-${idx}`}>
|
||||||
|
<PaginationEllipsis />
|
||||||
|
</PaginationItem>
|
||||||
|
) : (
|
||||||
|
<PaginationItem key={it}>
|
||||||
|
<PaginationLink
|
||||||
|
href="#"
|
||||||
|
isActive={it === safePage}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
go(it)
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
it === safePage
|
||||||
|
? cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: 'default',
|
||||||
|
size: 'icon',
|
||||||
|
}),
|
||||||
|
'hover:text-primary-foreground! dark:bg-primary dark:text-primary-foreground dark:hover:text-primary-foreground dark:hover:bg-primary/90 shadow-none! dark:border-transparent',
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{it}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
href="#"
|
||||||
|
className={cn(!canNext && 'pointer-events-none opacity-50')}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!canNext) return
|
||||||
|
go(safePage + 1)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Pagination03
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
import { Accordion as AccordionPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Accordion({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||||
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
data-slot="accordion-item"
|
||||||
|
className={cn("border-b last:border-b-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
className={cn(
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
data-slot="accordion-content"
|
||||||
|
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||||
@@ -7,7 +7,7 @@ import type { VariantProps } from 'class-variance-authority'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Drawer as DrawerPrimitive } from "vaul"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Drawer({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||||
|
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||||
|
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||||
|
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||||
|
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Overlay
|
||||||
|
data-slot="drawer-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DrawerPortal data-slot="drawer-portal">
|
||||||
|
<DrawerOverlay />
|
||||||
|
<DrawerPrimitive.Content
|
||||||
|
data-slot="drawer-content"
|
||||||
|
className={cn(
|
||||||
|
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||||
|
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||||
|
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||||
|
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||||
|
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||||
|
{children}
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-header"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Title
|
||||||
|
data-slot="drawer-title"
|
||||||
|
className={cn("text-foreground font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Description
|
||||||
|
data-slot="drawer-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Drawer,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerDescription,
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants, type Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
data-slot="pagination"
|
||||||
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="pagination-content"
|
||||||
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||||
|
return <li data-slot="pagination-item" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean
|
||||||
|
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||||
|
React.ComponentProps<"a">
|
||||||
|
|
||||||
|
function PaginationLink({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
data-slot="pagination-link"
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? "outline" : "ghost",
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationPrevious({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
<span className="hidden sm:block">Previous</span>
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationNext({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to next page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="hidden sm:block">Next</span>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot="pagination-ellipsis"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon className="size-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationPrevious,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationEllipsis,
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { CircleIcon } from "lucide-react"
|
||||||
|
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function RadioGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
data-slot="radio-group"
|
||||||
|
className={cn("grid gap-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RadioGroupItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
data-slot="radio-group-item"
|
||||||
|
className={cn(
|
||||||
|
"aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator
|
||||||
|
data-slot="radio-group-indicator"
|
||||||
|
className="relative flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<CircleIcon className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem }
|
||||||
@@ -1,18 +1,24 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
const Textarea = React.forwardRef<
|
||||||
|
HTMLTextAreaElement,
|
||||||
|
React.ComponentProps<'textarea'>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
|
ref={ref}
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
Textarea.displayName = 'Textarea'
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea }
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function WizardLayout({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="z-10 flex-none border-b bg-white">
|
<div className="z-10 flex-none border-b bg-white">
|
||||||
<CardHeader className="flex flex-row items-center justify-between gap-4 p-6 pb-4">
|
<CardHeader className="flex flex-row items-center justify-between gap-4 p-6 pb-3">
|
||||||
<CardTitle>{title}</CardTitle>
|
<CardTitle>{title}</CardTitle>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@@ -36,10 +36,10 @@ export function WizardLayout({
|
|||||||
</button>
|
</button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{headerSlot ? <div className="px-6 pb-6">{headerSlot}</div> : null}
|
{headerSlot ? <div className="px-6 pb-3">{headerSlot}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto bg-gray-50/30 p-6">
|
<div className="flex-1 overflow-y-auto bg-gray-50/30 px-4 py-3 xl:px-6">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,27 @@ import { StepWithTooltip } from '@/components/wizard/StepWithTooltip'
|
|||||||
export function WizardResponsiveHeader({
|
export function WizardResponsiveHeader({
|
||||||
wizard,
|
wizard,
|
||||||
methods,
|
methods,
|
||||||
|
titleOverrides,
|
||||||
|
hiddenStepIds,
|
||||||
}: {
|
}: {
|
||||||
wizard: any
|
wizard: any
|
||||||
methods: any
|
methods: any
|
||||||
|
titleOverrides?: Record<string, string>
|
||||||
|
hiddenStepIds?: Array<string>
|
||||||
}) {
|
}) {
|
||||||
const idx = wizard.utils.getIndex(methods.current.id)
|
const hidden = new Set(hiddenStepIds ?? [])
|
||||||
const totalSteps = wizard.steps.length
|
const visibleSteps = (wizard.steps as Array<any>).filter(
|
||||||
const currentIndex = idx + 1
|
(s) => s && !hidden.has(s.id),
|
||||||
const hasNextStep = idx < totalSteps - 1
|
)
|
||||||
const nextStep = wizard.steps[currentIndex]
|
|
||||||
|
const idx = visibleSteps.findIndex((s) => s.id === methods.current.id)
|
||||||
|
const safeIdx = idx >= 0 ? idx : 0
|
||||||
|
const totalSteps = visibleSteps.length
|
||||||
|
const currentIndex = Math.min(safeIdx + 1, totalSteps)
|
||||||
|
const hasNextStep = safeIdx < totalSteps - 1
|
||||||
|
const nextStep = visibleSteps[safeIdx + 1]
|
||||||
|
|
||||||
|
const resolveTitle = (step: any) => titleOverrides?.[step?.id] ?? step?.title
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -22,13 +34,13 @@ export function WizardResponsiveHeader({
|
|||||||
<div className="flex flex-col justify-center">
|
<div className="flex flex-col justify-center">
|
||||||
<h2 className="text-lg font-bold text-slate-900">
|
<h2 className="text-lg font-bold text-slate-900">
|
||||||
<StepWithTooltip
|
<StepWithTooltip
|
||||||
title={methods.current.title}
|
title={resolveTitle(methods.current)}
|
||||||
desc={methods.current.description}
|
desc={methods.current.description}
|
||||||
/>
|
/>
|
||||||
</h2>
|
</h2>
|
||||||
{hasNextStep && nextStep ? (
|
{hasNextStep && nextStep ? (
|
||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm text-slate-400">
|
||||||
Siguiente: {nextStep.title}
|
Siguiente: {resolveTitle(nextStep)}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm font-medium text-green-500">
|
<p className="text-sm font-medium text-green-500">
|
||||||
@@ -41,14 +53,18 @@ export function WizardResponsiveHeader({
|
|||||||
|
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:block">
|
||||||
<wizard.Stepper.Navigation className="border-border/60 rounded-xl border bg-slate-50 p-2">
|
<wizard.Stepper.Navigation className="border-border/60 rounded-xl border bg-slate-50 p-2">
|
||||||
{wizard.steps.map((step: any) => (
|
{visibleSteps.map((step: any, visibleIdx: number) => (
|
||||||
<wizard.Stepper.Step
|
<wizard.Stepper.Step
|
||||||
key={step.id}
|
key={step.id}
|
||||||
of={step.id}
|
of={step.id}
|
||||||
|
icon={visibleIdx + 1}
|
||||||
className="whitespace-nowrap"
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<wizard.Stepper.Title>
|
<wizard.Stepper.Title>
|
||||||
<StepWithTooltip title={step.title} desc={step.description} />
|
<StepWithTooltip
|
||||||
|
title={resolveTitle(step)}
|
||||||
|
desc={step.description}
|
||||||
|
/>
|
||||||
</wizard.Stepper.Title>
|
</wizard.Stepper.Title>
|
||||||
</wizard.Stepper.Step>
|
</wizard.Stepper.Step>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+350
-54
@@ -1,81 +1,377 @@
|
|||||||
import { invokeEdge } from "../supabase/invokeEdge";
|
import { supabaseBrowser } from '../supabase/client'
|
||||||
import type { InteraccionIA, UUID } from "../types/domain";
|
import { invokeEdge } from '../supabase/invokeEdge'
|
||||||
|
|
||||||
|
import type { InteraccionIA, UUID } from '../types/domain'
|
||||||
|
|
||||||
const EDGE = {
|
const EDGE = {
|
||||||
ai_plan_improve: "ai_plan_improve",
|
ai_plan_improve: 'ai_plan_improve',
|
||||||
ai_plan_chat: "ai_plan_chat",
|
ai_plan_chat: 'ai_plan_chat',
|
||||||
ai_subject_improve: "ai_subject_improve",
|
ai_subject_improve: 'ai_subject_improve',
|
||||||
ai_subject_chat: "ai_subject_chat",
|
ai_subject_chat: 'ai_subject_chat',
|
||||||
|
|
||||||
library_search: "library_search",
|
library_search: 'library_search',
|
||||||
} as const;
|
} as const
|
||||||
|
|
||||||
export async function ai_plan_improve(payload: {
|
export async function ai_plan_improve(payload: {
|
||||||
planId: UUID;
|
planId: UUID
|
||||||
sectionKey: string; // ej: "perfil_de_egreso" o tu key interna
|
sectionKey: string // ej: "perfil_de_egreso" o tu key interna
|
||||||
prompt: string;
|
prompt: string
|
||||||
context?: Record<string, any>;
|
context?: Record<string, any>
|
||||||
fuentes?: {
|
fuentes?: {
|
||||||
archivosIds?: UUID[];
|
archivosIds?: Array<UUID>
|
||||||
vectorStoresIds?: UUID[];
|
vectorStoresIds?: Array<UUID>
|
||||||
usarMCP?: boolean;
|
usarMCP?: boolean
|
||||||
conversacionId?: string;
|
conversacionId?: string
|
||||||
};
|
}
|
||||||
}): Promise<{ interaccion: InteraccionIA; propuesta: any }> {
|
}): Promise<{ interaccion: InteraccionIA; propuesta: any }> {
|
||||||
return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>(EDGE.ai_plan_improve, payload);
|
return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>(
|
||||||
|
EDGE.ai_plan_improve,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ai_plan_chat(payload: {
|
export async function ai_plan_chat(payload: {
|
||||||
planId: UUID;
|
planId: UUID
|
||||||
messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
|
messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>
|
||||||
fuentes?: {
|
fuentes?: {
|
||||||
archivosIds?: UUID[];
|
archivosIds?: Array<UUID>
|
||||||
vectorStoresIds?: UUID[];
|
vectorStoresIds?: Array<UUID>
|
||||||
usarMCP?: boolean;
|
usarMCP?: boolean
|
||||||
conversacionId?: string;
|
conversacionId?: string
|
||||||
};
|
}
|
||||||
}): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> {
|
}): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> {
|
||||||
return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>(EDGE.ai_plan_chat, payload);
|
return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>(
|
||||||
|
EDGE.ai_plan_chat,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ai_subject_improve(payload: {
|
export async function ai_subject_improve(payload: {
|
||||||
subjectId: UUID;
|
subjectId: UUID
|
||||||
sectionKey: string;
|
sectionKey: string
|
||||||
prompt: string;
|
prompt: string
|
||||||
context?: Record<string, any>;
|
context?: Record<string, any>
|
||||||
fuentes?: {
|
fuentes?: {
|
||||||
archivosIds?: UUID[];
|
archivosIds?: Array<UUID>
|
||||||
vectorStoresIds?: UUID[];
|
vectorStoresIds?: Array<UUID>
|
||||||
usarMCP?: boolean;
|
usarMCP?: boolean
|
||||||
conversacionId?: string;
|
conversacionId?: string
|
||||||
};
|
}
|
||||||
}): Promise<{ interaccion: InteraccionIA; propuesta: any }> {
|
}): Promise<{ interaccion: InteraccionIA; propuesta: any }> {
|
||||||
return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>(EDGE.ai_subject_improve, payload);
|
return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>(
|
||||||
|
EDGE.ai_subject_improve,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ai_subject_chat(payload: {
|
export async function ai_subject_chat(payload: {
|
||||||
subjectId: UUID;
|
subjectId: UUID
|
||||||
messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
|
messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>
|
||||||
fuentes?: {
|
fuentes?: {
|
||||||
archivosIds?: UUID[];
|
archivosIds?: Array<UUID>
|
||||||
vectorStoresIds?: UUID[];
|
vectorStoresIds?: Array<UUID>
|
||||||
usarMCP?: boolean;
|
usarMCP?: boolean
|
||||||
conversacionId?: string;
|
conversacionId?: string
|
||||||
};
|
}
|
||||||
}): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> {
|
}): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> {
|
||||||
return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>(EDGE.ai_subject_chat, payload);
|
return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>(
|
||||||
|
EDGE.ai_subject_chat,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Biblioteca (Edge; adapta a tu API real) */
|
/** Biblioteca (Edge; adapta a tu API real) */
|
||||||
export type LibraryItem = {
|
export type LibraryItem = {
|
||||||
id: string;
|
id: string
|
||||||
titulo: string;
|
titulo: string
|
||||||
autor?: string;
|
autor?: string
|
||||||
isbn?: string;
|
isbn?: string
|
||||||
citaSugerida?: string;
|
citaSugerida?: string
|
||||||
disponibilidad?: string;
|
disponibilidad?: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function library_search(payload: { query: string; limit?: number }): Promise<LibraryItem[]> {
|
export async function library_search(payload: {
|
||||||
return invokeEdge<LibraryItem[]>(EDGE.library_search, payload);
|
query: string
|
||||||
|
limit?: number
|
||||||
|
}): Promise<Array<LibraryItem>> {
|
||||||
|
return invokeEdge<Array<LibraryItem>>(EDGE.library_search, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create_conversation(planId: string) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase.functions.invoke(
|
||||||
|
'create-chat-conversation/plan/conversations',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
plan_estudio_id: planId, // O el nombre que confirmamos que funciona
|
||||||
|
instanciador: 'alex',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_chat_history(conversacionId: string) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase.functions.invoke(
|
||||||
|
`create-chat-conversation/conversations/${conversacionId}/messages`,
|
||||||
|
{ method: 'GET' },
|
||||||
|
)
|
||||||
|
if (error) throw error
|
||||||
|
return data // Retorna Array de mensajes
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update_conversation_status(
|
||||||
|
conversacionId: string,
|
||||||
|
nuevoEstado: 'ARCHIVADA' | 'ACTIVA',
|
||||||
|
) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('conversaciones_plan') // Asegúrate que el nombre de la tabla sea exacto
|
||||||
|
.update({ estado: nuevoEstado })
|
||||||
|
.eq('id', conversacionId)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modificamos la función de chat para que use la ruta de mensajes
|
||||||
|
export async function ai_plan_chat_v2(payload: {
|
||||||
|
conversacionId: string
|
||||||
|
content: string
|
||||||
|
campos?: Array<string>
|
||||||
|
}): Promise<{ reply: string; meta?: any }> {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase.functions.invoke(
|
||||||
|
`create-chat-conversation/conversations/plan/${payload.conversacionId}/messages`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
content: payload.content,
|
||||||
|
campos: payload.campos || [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConversationByPlan(planId: string) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('conversaciones_plan')
|
||||||
|
.select('*')
|
||||||
|
.eq('plan_estudio_id', planId)
|
||||||
|
.order('creado_en', { ascending: false })
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
return data ?? []
|
||||||
|
}
|
||||||
|
export async function getMessagesByConversation(conversationId: string) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('plan_mensajes_ia')
|
||||||
|
.select('*')
|
||||||
|
.eq('conversacion_plan_id', conversationId)
|
||||||
|
.order('fecha_creacion', { ascending: true }) // Ascendente para que el chat fluya en orden cronológico
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error al obtener mensajes:', error.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return data ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update_conversation_title(
|
||||||
|
conversacionId: string,
|
||||||
|
nuevoTitulo: string,
|
||||||
|
) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('conversaciones_plan')
|
||||||
|
.update({ nombre: nuevoTitulo }) // Asegúrate que la columna se llame 'title' o 'nombre'
|
||||||
|
.eq('id', conversacionId)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update_recommendation_applied_status(
|
||||||
|
mensajeId: string, // Ahora es más eficiente usar el ID del mensaje directamente
|
||||||
|
campoAfectado: string,
|
||||||
|
) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
// 1. Obtener la propuesta actual de ese mensaje específico
|
||||||
|
const { data: msgData, error: fetchError } = await supabase
|
||||||
|
.from('plan_mensajes_ia')
|
||||||
|
.select('propuesta')
|
||||||
|
.eq('id', mensajeId)
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (fetchError) throw fetchError
|
||||||
|
if (!msgData?.propuesta)
|
||||||
|
throw new Error('No se encontró la propuesta en el mensaje')
|
||||||
|
|
||||||
|
const propuestaActual = msgData.propuesta as any
|
||||||
|
|
||||||
|
// 2. Modificar el array de recommendations dentro de la propuesta
|
||||||
|
// Mantenemos el resto de la propuesta (prompt, respuesta, etc.) intacto
|
||||||
|
const nuevaPropuesta = {
|
||||||
|
...propuestaActual,
|
||||||
|
recommendations: (propuestaActual.recommendations || []).map((rec: any) =>
|
||||||
|
rec.campo_afectado === campoAfectado ? { ...rec, aplicada: true } : rec,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Actualizar la base de datos con el nuevo objeto JSON
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('plan_mensajes_ia')
|
||||||
|
.update({ propuesta: nuevaPropuesta })
|
||||||
|
.eq('id', mensajeId)
|
||||||
|
|
||||||
|
if (updateError) throw updateError
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FUNCIONES DE ASIGNATURA ---
|
||||||
|
|
||||||
|
export async function create_subject_conversation(subjectId: string) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase.functions.invoke(
|
||||||
|
'create-chat-conversation/asignatura/conversations', // Ruta corregida
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
asignatura_id: subjectId,
|
||||||
|
instanciador: 'alex',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (error) throw error
|
||||||
|
return data // Retorna { conversation_asignatura: { id, ... } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ai_subject_chat_v2(payload: {
|
||||||
|
conversacionId: string
|
||||||
|
content: string
|
||||||
|
campos?: Array<string>
|
||||||
|
}) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase.functions.invoke(
|
||||||
|
`create-chat-conversation/conversations/asignatura/${payload.conversacionId}/messages`, // Ruta corregida
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
content: payload.content,
|
||||||
|
campos: payload.campos || [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConversationBySubject(subjectId: string) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('conversaciones_asignatura') // Tabla corregida
|
||||||
|
.select('*')
|
||||||
|
.eq('asignatura_id', subjectId)
|
||||||
|
.order('creado_en', { ascending: false })
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMessagesBySubjectConversation(conversationId: string) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('asignatura_mensajes_ia' as any)
|
||||||
|
.select('*')
|
||||||
|
.eq('conversacion_asignatura_id', conversationId)
|
||||||
|
.order('fecha_creacion', { ascending: true })
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update_subject_recommendation_applied(
|
||||||
|
mensajeId: string,
|
||||||
|
campoAfectado: string,
|
||||||
|
) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
// 1. Obtener propuesta actual
|
||||||
|
const { data: msgData, error: fetchError } = await supabase
|
||||||
|
.from('asignatura_mensajes_ia')
|
||||||
|
.select('propuesta')
|
||||||
|
.eq('id', mensajeId)
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (fetchError) throw fetchError
|
||||||
|
const propuestaActual = msgData?.propuesta as any
|
||||||
|
|
||||||
|
// 2. Marcar como aplicada
|
||||||
|
const nuevaPropuesta = {
|
||||||
|
...propuestaActual,
|
||||||
|
recommendations: (propuestaActual.recommendations || []).map((rec: any) =>
|
||||||
|
rec.campo_afectado === campoAfectado ? { ...rec, aplicada: true } : rec,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Update
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('asignatura_mensajes_ia')
|
||||||
|
.update({ propuesta: nuevaPropuesta })
|
||||||
|
.eq('id', mensajeId)
|
||||||
|
|
||||||
|
if (updateError) throw updateError
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update_subject_conversation_status(
|
||||||
|
conversacionId: string,
|
||||||
|
nuevoEstado: 'ARCHIVADA' | 'ACTIVA',
|
||||||
|
) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('conversaciones_asignatura')
|
||||||
|
.update({ estado: nuevoEstado })
|
||||||
|
.eq('id', conversacionId)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update_subject_conversation_name(
|
||||||
|
conversacionId: string,
|
||||||
|
nuevoNombre: string,
|
||||||
|
) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('conversaciones_asignatura')
|
||||||
|
.update({ nombre: nuevoNombre }) // Asumiendo que la columna es 'titulo' según tu código previo, o cambia a 'nombre'
|
||||||
|
.eq('id', conversacionId)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,86 @@
|
|||||||
// document.api.ts
|
// document.api.ts
|
||||||
|
|
||||||
const DOCUMENT_PDF_URL =
|
import { supabaseBrowser } from '../supabase/client'
|
||||||
'https://n8n.app.lci.ulsa.mx/webhook/62ca84ec-0adb-4006-aba1-32282d27d434'
|
import { invokeEdge } from '../supabase/invokeEdge'
|
||||||
|
|
||||||
|
import { requireData, throwIfError } from './_helpers'
|
||||||
|
|
||||||
|
import type { Tables } from '@/types/supabase'
|
||||||
|
|
||||||
|
const EDGE = {
|
||||||
|
carbone_io_wrapper: 'carbone-io-wrapper',
|
||||||
|
} as const
|
||||||
|
|
||||||
interface GeneratePdfParams {
|
interface GeneratePdfParams {
|
||||||
plan_estudio_id: string
|
plan_estudio_id: string
|
||||||
|
convertTo?: 'pdf'
|
||||||
|
}
|
||||||
|
interface GeneratePdfParamsAsignatura {
|
||||||
|
asignatura_id: string
|
||||||
|
convertTo?: 'pdf'
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPlanPdf({
|
export async function fetchPlanPdf({
|
||||||
plan_estudio_id,
|
plan_estudio_id,
|
||||||
|
convertTo,
|
||||||
}: GeneratePdfParams): Promise<Blob> {
|
}: GeneratePdfParams): Promise<Blob> {
|
||||||
const response = await fetch(DOCUMENT_PDF_URL, {
|
return await invokeEdge<Blob>(
|
||||||
method: 'POST',
|
EDGE.carbone_io_wrapper,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
action: 'downloadReport',
|
||||||
|
plan_estudio_id,
|
||||||
|
body: convertTo ? { convertTo } : {},
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ plan_estudio_id }),
|
{
|
||||||
})
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
if (!response.ok) {
|
},
|
||||||
throw new Error('Error al generar el PDF')
|
responseType: 'blob',
|
||||||
}
|
},
|
||||||
|
)
|
||||||
// n8n devuelve el archivo → lo tratamos como blob
|
}
|
||||||
return await response.blob()
|
|
||||||
|
export async function fetchAsignaturaPdf({
|
||||||
|
asignatura_id,
|
||||||
|
convertTo,
|
||||||
|
}: GeneratePdfParamsAsignatura): Promise<Blob> {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('asignaturas')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', asignatura_id)
|
||||||
|
.single()
|
||||||
|
|
||||||
|
throwIfError(error)
|
||||||
|
|
||||||
|
const row = requireData(
|
||||||
|
data as Pick<
|
||||||
|
Tables<'asignaturas'>,
|
||||||
|
'datos' | 'contenido_tematico' | 'criterios_de_evaluacion'
|
||||||
|
>,
|
||||||
|
'Asignatura no encontrada',
|
||||||
|
)
|
||||||
|
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
data: row,
|
||||||
|
}
|
||||||
|
if (convertTo) body.convertTo = convertTo
|
||||||
|
|
||||||
|
return await invokeEdge<Blob>(
|
||||||
|
EDGE.carbone_io_wrapper,
|
||||||
|
{
|
||||||
|
action: 'downloadReport',
|
||||||
|
asignatura_id,
|
||||||
|
body: {
|
||||||
|
...body,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
responseType: 'blob',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-7
@@ -52,6 +52,35 @@ const cleanText = (text: string) => {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recalculoVectoresAsignaturasInFlight = new Set<string>()
|
||||||
|
|
||||||
|
function triggerRecalculoVectoresAsignaturasNonBlocking(
|
||||||
|
supabase: ReturnType<typeof supabaseBrowser>,
|
||||||
|
planId: UUID,
|
||||||
|
) {
|
||||||
|
const key = String(planId)
|
||||||
|
if (recalculoVectoresAsignaturasInFlight.has(key)) return
|
||||||
|
|
||||||
|
recalculoVectoresAsignaturasInFlight.add(key)
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
const { error } = await supabase.rpc('recalcular_vectores_asignaturas')
|
||||||
|
if (error) {
|
||||||
|
// No debe bloquear ni romper el flujo principal.
|
||||||
|
console.warn(
|
||||||
|
'[recalcular_vectores_asignaturas] RPC error:',
|
||||||
|
error.message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
console.warn('[recalcular_vectores_asignaturas] RPC failed:', err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
recalculoVectoresAsignaturasInFlight.delete(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function plans_list(
|
export async function plans_list(
|
||||||
filters: PlanListFilters = {},
|
filters: PlanListFilters = {},
|
||||||
): Promise<Paged<PlanEstudio>> {
|
): Promise<Paged<PlanEstudio>> {
|
||||||
@@ -144,13 +173,57 @@ export async function plans_get(planId: UUID): Promise<PlanEstudio> {
|
|||||||
return requireData(data, 'Plan no encontrado.')
|
return requireData(data, 'Plan no encontrado.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variante de `plans_get` que NO lanza si no existe (devuelve null).
|
||||||
|
* Útil para flujos de polling donde el plan puede tardar en aparecer.
|
||||||
|
*/
|
||||||
|
export async function plans_get_maybe(
|
||||||
|
planId: UUID,
|
||||||
|
): Promise<PlanEstudio | null> {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('planes_estudio')
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
*,
|
||||||
|
carreras (*, facultades(*)),
|
||||||
|
estructuras_plan (*),
|
||||||
|
estados_plan (*)
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.eq('id', planId)
|
||||||
|
.maybeSingle()
|
||||||
|
|
||||||
|
throwIfError(error)
|
||||||
|
return (data ?? null) as unknown as PlanEstudio | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function plans_delete(planId: UUID): Promise<{ id: UUID }> {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('planes_estudio')
|
||||||
|
.delete()
|
||||||
|
.eq('id', planId)
|
||||||
|
.select('id')
|
||||||
|
.maybeSingle()
|
||||||
|
|
||||||
|
throwIfError(error)
|
||||||
|
|
||||||
|
// Si por alguna razón no retorna fila (RLS / triggers), devolvemos el id solicitado.
|
||||||
|
return { id: ((data as any)?.id ?? planId) as UUID }
|
||||||
|
}
|
||||||
|
|
||||||
export async function plan_lineas_list(
|
export async function plan_lineas_list(
|
||||||
planId: UUID,
|
planId: UUID,
|
||||||
): Promise<Array<LineaPlan>> {
|
): Promise<Array<LineaPlan>> {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('lineas_plan')
|
.from('lineas_plan')
|
||||||
.select('id,plan_estudio_id,nombre,orden,area,creado_en,actualizado_en')
|
.select(
|
||||||
|
'id,plan_estudio_id,nombre,orden,area,creado_en,actualizado_en,color',
|
||||||
|
)
|
||||||
.eq('plan_estudio_id', planId)
|
.eq('plan_estudio_id', planId)
|
||||||
.order('orden', { ascending: true })
|
.order('orden', { ascending: true })
|
||||||
|
|
||||||
@@ -165,7 +238,7 @@ export async function plan_asignaturas_list(
|
|||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('asignaturas')
|
.from('asignaturas')
|
||||||
.select(
|
.select(
|
||||||
'id,plan_estudio_id,horas_academicas, horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en',
|
'id,plan_estudio_id,horas_academicas,horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,criterios_de_evaluacion,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,prerrequisito_asignatura_id,search_vector',
|
||||||
)
|
)
|
||||||
.eq('plan_estudio_id', planId)
|
.eq('plan_estudio_id', planId)
|
||||||
.order('numero_ciclo', { ascending: true, nullsFirst: false })
|
.order('numero_ciclo', { ascending: true, nullsFirst: false })
|
||||||
@@ -173,21 +246,41 @@ export async function plan_asignaturas_list(
|
|||||||
.order('nombre', { ascending: true })
|
.order('nombre', { ascending: true })
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error)
|
||||||
|
|
||||||
|
// No bloqueante: si el primer registro viene sin vector, dispara el recalculo.
|
||||||
|
const first: any = (data as any)?.[0]
|
||||||
|
if (first && first.search_vector === null) {
|
||||||
|
triggerRecalculoVectoresAsignaturasNonBlocking(supabase, planId)
|
||||||
|
}
|
||||||
|
|
||||||
return data ?? []
|
return data ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function plans_history(planId: UUID): Promise<Array<CambioPlan>> {
|
export async function plans_history(
|
||||||
|
planId: UUID,
|
||||||
|
page: number = 0,
|
||||||
|
pageSize: number = 4,
|
||||||
|
): Promise<{ data: Array<CambioPlan>; count: number }> {
|
||||||
|
// Cambiamos el retorno
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
const { data, error } = await supabase
|
const from = page * pageSize
|
||||||
|
const to = from + pageSize - 1
|
||||||
|
|
||||||
|
const { data, error, count } = await supabase
|
||||||
.from('cambios_plan')
|
.from('cambios_plan')
|
||||||
.select(
|
.select(
|
||||||
'id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo',
|
'id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,response_id',
|
||||||
|
{ count: 'exact' }, // <--- Pedimos el conteo exacto
|
||||||
)
|
)
|
||||||
.eq('plan_estudio_id', planId)
|
.eq('plan_estudio_id', planId)
|
||||||
.order('cambiado_en', { ascending: false })
|
.order('cambiado_en', { ascending: false })
|
||||||
|
.range(from, to)
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error)
|
||||||
return data ?? []
|
return {
|
||||||
|
data: data ?? [],
|
||||||
|
count: count ?? 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wizard: crear plan manual (Edge Function) */
|
/** Wizard: crear plan manual (Edge Function) */
|
||||||
@@ -291,7 +384,7 @@ export async function ai_generate_plan(
|
|||||||
archivosAdjuntos: undefined, // los manejamos aparte
|
archivosAdjuntos: undefined, // los manejamos aparte
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
input.iaConfig.archivosAdjuntos.forEach((file, index) => {
|
input.iaConfig.archivosAdjuntos.forEach((file) => {
|
||||||
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+367
-73
@@ -7,48 +7,206 @@ import type { DocumentoResult } from './plans.api'
|
|||||||
import type {
|
import type {
|
||||||
Asignatura,
|
Asignatura,
|
||||||
BibliografiaAsignatura,
|
BibliografiaAsignatura,
|
||||||
|
CarreraRow,
|
||||||
CambioAsignatura,
|
CambioAsignatura,
|
||||||
|
EstructuraAsignatura,
|
||||||
|
FacultadRow,
|
||||||
|
PlanEstudioRow,
|
||||||
TipoAsignatura,
|
TipoAsignatura,
|
||||||
UUID,
|
UUID,
|
||||||
} from '../types/domain'
|
} from '../types/domain'
|
||||||
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
import type {
|
||||||
import type { Database } from '@/types/supabase'
|
AsignaturaSugerida,
|
||||||
|
DataAsignaturaSugerida,
|
||||||
|
} from '@/features/asignaturas/nueva/types'
|
||||||
|
import type { Database, Tables, TablesInsert } from '@/types/supabase'
|
||||||
|
|
||||||
const EDGE = {
|
const EDGE = {
|
||||||
|
generate_subject_suggestions: 'generate-subject-suggestions',
|
||||||
subjects_create_manual: 'subjects_create_manual',
|
subjects_create_manual: 'subjects_create_manual',
|
||||||
ai_generate_subject: 'ai-generate-subject',
|
ai_generate_subject: 'ai-generate-subject',
|
||||||
subjects_persist_from_ai: 'subjects_persist_from_ai',
|
subjects_persist_from_ai: 'subjects_persist_from_ai',
|
||||||
subjects_clone_from_existing: 'subjects_clone_from_existing',
|
subjects_clone_from_existing: 'subjects_clone_from_existing',
|
||||||
subjects_import_from_file: 'subjects_import_from_file',
|
subjects_import_from_file: 'subjects_import_from_file',
|
||||||
|
|
||||||
|
// Bibliografía
|
||||||
|
buscar_bibliografia: 'buscar-bibliografia',
|
||||||
|
|
||||||
subjects_update_fields: 'subjects_update_fields',
|
subjects_update_fields: 'subjects_update_fields',
|
||||||
subjects_update_contenido: 'subjects_update_contenido',
|
|
||||||
subjects_update_bibliografia: 'subjects_update_bibliografia',
|
subjects_update_bibliografia: 'subjects_update_bibliografia',
|
||||||
|
|
||||||
subjects_generate_document: 'subjects_generate_document',
|
subjects_generate_document: 'subjects_generate_document',
|
||||||
subjects_get_document: 'subjects_get_document',
|
subjects_get_document: 'subjects_get_document',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
|
export type BuscarBibliografiaRequest = {
|
||||||
|
searchTerms: {
|
||||||
|
q: string
|
||||||
|
}
|
||||||
|
|
||||||
|
google: {
|
||||||
|
orderBy?: 'newest' | 'relevance'
|
||||||
|
langRestrict?: string
|
||||||
|
startIndex?: number
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
openLibrary: {
|
||||||
|
language?: string
|
||||||
|
page?: number
|
||||||
|
sort?: string
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GoogleBooksVolume = {
|
||||||
|
kind?: 'books#volume'
|
||||||
|
id: string
|
||||||
|
etag?: string
|
||||||
|
selfLink?: string
|
||||||
|
volumeInfo?: {
|
||||||
|
title?: string
|
||||||
|
subtitle?: string
|
||||||
|
authors?: Array<string>
|
||||||
|
publisher?: string
|
||||||
|
publishedDate?: string
|
||||||
|
description?: string
|
||||||
|
industryIdentifiers?: Array<{ type?: string; identifier?: string }>
|
||||||
|
pageCount?: number
|
||||||
|
categories?: Array<string>
|
||||||
|
language?: string
|
||||||
|
previewLink?: string
|
||||||
|
infoLink?: string
|
||||||
|
canonicalVolumeLink?: string
|
||||||
|
imageLinks?: {
|
||||||
|
smallThumbnail?: string
|
||||||
|
thumbnail?: string
|
||||||
|
small?: string
|
||||||
|
medium?: string
|
||||||
|
large?: string
|
||||||
|
extraLarge?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searchInfo?: {
|
||||||
|
textSnippet?: string
|
||||||
|
}
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpenLibraryDoc = Record<string, unknown>
|
||||||
|
|
||||||
|
export type EndpointResult =
|
||||||
|
| { endpoint: 'google'; item: GoogleBooksVolume }
|
||||||
|
| { endpoint: 'open_library'; item: OpenLibraryDoc }
|
||||||
|
|
||||||
|
export async function buscar_bibliografia(
|
||||||
|
input: BuscarBibliografiaRequest,
|
||||||
|
): Promise<Array<EndpointResult>> {
|
||||||
|
const q = input.searchTerms.q
|
||||||
|
|
||||||
|
if (typeof q !== 'string' || q.trim().length < 1) {
|
||||||
|
throw new Error('q es requerido')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await invokeEdge<Array<EndpointResult>>(
|
||||||
|
EDGE.buscar_bibliografia,
|
||||||
|
input,
|
||||||
|
{ headers: { 'Content-Type': 'application/json' } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ContenidoTemaApi =
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
nombre: string
|
||||||
|
horasEstimadas?: number
|
||||||
|
descripcion?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estructura persistida en `asignaturas.contenido_tematico`.
|
||||||
|
* La BDD guarda un arreglo de unidades, cada una con temas (strings u objetos).
|
||||||
|
*/
|
||||||
|
export type ContenidoApi = {
|
||||||
|
unidad: number
|
||||||
|
titulo: string
|
||||||
|
temas: Array<ContenidoTemaApi>
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FacultadInSubject = Pick<
|
||||||
|
FacultadRow,
|
||||||
|
'id' | 'nombre' | 'nombre_corto' | 'color' | 'icono'
|
||||||
|
>
|
||||||
|
|
||||||
|
export type CarreraInSubject = Pick<
|
||||||
|
CarreraRow,
|
||||||
|
'id' | 'facultad_id' | 'nombre' | 'nombre_corto' | 'clave_sep' | 'activa'
|
||||||
|
> & {
|
||||||
|
facultades: FacultadInSubject | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlanEstudioInSubject = Pick<
|
||||||
|
PlanEstudioRow,
|
||||||
|
| 'id'
|
||||||
|
| 'carrera_id'
|
||||||
|
| 'estructura_id'
|
||||||
|
| 'nombre'
|
||||||
|
| 'nivel'
|
||||||
|
| 'tipo_ciclo'
|
||||||
|
| 'numero_ciclos'
|
||||||
|
| 'datos'
|
||||||
|
| 'estado_actual_id'
|
||||||
|
| 'activo'
|
||||||
|
| 'tipo_origen'
|
||||||
|
| 'meta_origen'
|
||||||
|
| 'creado_por'
|
||||||
|
| 'actualizado_por'
|
||||||
|
| 'creado_en'
|
||||||
|
| 'actualizado_en'
|
||||||
|
> & {
|
||||||
|
carreras: CarreraInSubject | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EstructuraAsignaturaInSubject = Pick<
|
||||||
|
EstructuraAsignatura,
|
||||||
|
'id' | 'nombre' | 'definicion'
|
||||||
|
>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo real que devuelve `subjects_get` (asignatura + relaciones seleccionadas).
|
||||||
|
* Nota: `asignaturas_update` (update directo) NO devuelve estas relaciones.
|
||||||
|
*/
|
||||||
|
export type AsignaturaDetail = Omit<Asignatura, 'contenido_tematico'> & {
|
||||||
|
contenido_tematico: Array<ContenidoApi> | null
|
||||||
|
planes_estudio: PlanEstudioInSubject | null
|
||||||
|
estructuras_asignatura: EstructuraAsignaturaInSubject | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function subjects_get(subjectId: UUID): Promise<AsignaturaDetail> {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('asignaturas')
|
.from('asignaturas')
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,criterios_de_evaluacion,prerrequisito_asignatura_id,
|
||||||
planes_estudio(
|
planes_estudio(
|
||||||
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
||||||
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
||||||
),
|
),
|
||||||
estructuras_asignatura(id,nombre,version,definicion)
|
estructuras_asignatura(id,nombre,definicion)
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.eq('id', subjectId)
|
.eq('id', subjectId)
|
||||||
.single()
|
.single()
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error)
|
||||||
return requireData(data, 'Asignatura no encontrada.')
|
return requireData(
|
||||||
|
data,
|
||||||
|
'Asignatura no encontrada.',
|
||||||
|
) as unknown as AsignaturaDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_history(
|
export async function subjects_history(
|
||||||
@@ -74,7 +232,7 @@ export async function subjects_bibliografia_list(
|
|||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('bibliografia_asignatura')
|
.from('bibliografia_asignatura')
|
||||||
.select(
|
.select(
|
||||||
'id,asignatura_id,tipo,cita,tipo_fuente,biblioteca_item_id,creado_por,creado_en,actualizado_en',
|
'id,asignatura_id,tipo,cita,referencia_biblioteca,referencia_en_linea,creado_por,creado_en,actualizado_en',
|
||||||
)
|
)
|
||||||
.eq('asignatura_id', subjectId)
|
.eq('asignatura_id', subjectId)
|
||||||
.order('tipo', { ascending: true })
|
.order('tipo', { ascending: true })
|
||||||
@@ -84,77 +242,105 @@ export async function subjects_bibliografia_list(
|
|||||||
return data ?? []
|
return data ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wizard: crear asignatura manual (Edge Function) */
|
|
||||||
export type SubjectsCreateManualInput = {
|
|
||||||
planId: UUID
|
|
||||||
datosBasicos: {
|
|
||||||
nombre: string
|
|
||||||
clave?: string
|
|
||||||
tipo: TipoAsignatura
|
|
||||||
creditos: number
|
|
||||||
horasSemana?: number
|
|
||||||
estructuraId: UUID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function subjects_create_manual(
|
export async function subjects_create_manual(
|
||||||
payload: SubjectsCreateManualInput,
|
payload: TablesInsert<'asignaturas'>,
|
||||||
): Promise<Asignatura> {
|
): Promise<Asignatura> {
|
||||||
return invokeEdge<Asignatura>(EDGE.subjects_create_manual, payload)
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('asignaturas')
|
||||||
|
.insert(payload)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
throwIfError(error)
|
||||||
|
return requireData(data, 'No se pudo crear la asignatura.')
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AIGenerateSubjectInput = {
|
/**
|
||||||
plan_estudio_id: Asignatura['plan_estudio_id']
|
* Nuevo payload unificado (JSON) para la Edge `ai_generate_subject`.
|
||||||
datosBasicos: {
|
* - Siempre incluye `datosUpdate.plan_estudio_id`.
|
||||||
nombre: Asignatura['nombre']
|
* - `datosUpdate.id` es opcional (si no existe, la Edge puede crear).
|
||||||
codigo?: Asignatura['codigo']
|
* En el frontend, insertamos primero y usamos `id` para actualizar.
|
||||||
tipo: Asignatura['tipo'] | null
|
*/
|
||||||
creditos: Asignatura['creditos'] | null
|
export type AISubjectUnifiedInput = {
|
||||||
horasAcademicas?: Asignatura['horas_academicas'] | null
|
datosUpdate: Partial<{
|
||||||
horasIndependientes?: Asignatura['horas_independientes'] | null
|
id: string
|
||||||
estructuraId: Asignatura['estructura_id'] | null
|
plan_estudio_id: string
|
||||||
|
estructura_id: string
|
||||||
|
nombre: string
|
||||||
|
codigo: string | null
|
||||||
|
tipo: string | null
|
||||||
|
creditos: number
|
||||||
|
horas_academicas: number | null
|
||||||
|
horas_independientes: number | null
|
||||||
|
numero_ciclo: number | null
|
||||||
|
linea_plan_id: string | null
|
||||||
|
orden_celda: number | null
|
||||||
|
}> & {
|
||||||
|
plan_estudio_id: string
|
||||||
}
|
}
|
||||||
// clonInterno?: {
|
|
||||||
// facultadId?: string
|
|
||||||
// carreraId?: string
|
|
||||||
// planOrigenId?: string
|
|
||||||
// asignaturaOrigenId?: string | null
|
|
||||||
// }
|
|
||||||
// clonTradicional?: {
|
|
||||||
// archivoWordAsignaturaId: string | null
|
|
||||||
// archivosAdicionalesIds: Array<string>
|
|
||||||
// }
|
|
||||||
iaConfig?: {
|
iaConfig?: {
|
||||||
descripcionEnfoqueAcademico: string
|
descripcionEnfoqueAcademico?: string
|
||||||
instruccionesAdicionalesIA: string
|
instruccionesAdicionalesIA?: string
|
||||||
archivosReferencia: Array<string>
|
archivosAdjuntos?: Array<string>
|
||||||
repositoriosReferencia?: Array<string>
|
|
||||||
archivosAdjuntos?: Array<UploadedFile>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function subjects_get_maybe(
|
||||||
|
subjectId: UUID,
|
||||||
|
): Promise<Asignatura | null> {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('asignaturas')
|
||||||
|
.select('id,plan_estudio_id,estado')
|
||||||
|
.eq('id', subjectId)
|
||||||
|
.maybeSingle()
|
||||||
|
|
||||||
|
throwIfError(error)
|
||||||
|
return (data ?? null) as unknown as Asignatura | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GenerateSubjectSuggestionsInput = {
|
||||||
|
plan_estudio_id: UUID
|
||||||
|
enfoque?: string
|
||||||
|
cantidad_de_sugerencias: number
|
||||||
|
sugerencias_conservadas: Array<{ nombre: string; descripcion: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generate_subject_suggestions(
|
||||||
|
input: GenerateSubjectSuggestionsInput,
|
||||||
|
): Promise<Array<AsignaturaSugerida>> {
|
||||||
|
const raw = await invokeEdge<Array<DataAsignaturaSugerida>>(
|
||||||
|
EDGE.generate_subject_suggestions,
|
||||||
|
input,
|
||||||
|
{ headers: { 'Content-Type': 'application/json' } },
|
||||||
|
)
|
||||||
|
|
||||||
|
return raw.map(
|
||||||
|
(s): AsignaturaSugerida => ({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
selected: false,
|
||||||
|
source: 'IA',
|
||||||
|
nombre: s.nombre,
|
||||||
|
codigo: s.codigo,
|
||||||
|
tipo: s.tipo ?? null,
|
||||||
|
creditos: s.creditos ?? null,
|
||||||
|
horasAcademicas: s.horasAcademicas ?? null,
|
||||||
|
horasIndependientes: s.horasIndependientes ?? null,
|
||||||
|
descripcion: s.descripcion,
|
||||||
|
linea_plan_id: null,
|
||||||
|
numero_ciclo: null,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export async function ai_generate_subject(
|
export async function ai_generate_subject(
|
||||||
input: AIGenerateSubjectInput,
|
input: AISubjectUnifiedInput,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const edgeFunctionBody = new FormData()
|
return invokeEdge<any>(EDGE.ai_generate_subject, input, {
|
||||||
edgeFunctionBody.append('plan_estudio_id', input.plan_estudio_id)
|
headers: { 'Content-Type': 'application/json' },
|
||||||
edgeFunctionBody.append('datosBasicos', JSON.stringify(input.datosBasicos))
|
|
||||||
edgeFunctionBody.append(
|
|
||||||
'iaConfig',
|
|
||||||
JSON.stringify({
|
|
||||||
...input.iaConfig,
|
|
||||||
archivosAdjuntos: undefined, // los manejamos aparte
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
input.iaConfig?.archivosAdjuntos?.forEach((file, index) => {
|
|
||||||
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
|
||||||
})
|
})
|
||||||
return invokeEdge<any>(
|
|
||||||
EDGE.ai_generate_subject,
|
|
||||||
edgeFunctionBody,
|
|
||||||
undefined,
|
|
||||||
supabaseBrowser(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_persist_from_ai(payload: {
|
export async function subjects_persist_from_ai(payload: {
|
||||||
@@ -211,12 +397,24 @@ export async function subjects_update_fields(
|
|||||||
|
|
||||||
export async function subjects_update_contenido(
|
export async function subjects_update_contenido(
|
||||||
subjectId: UUID,
|
subjectId: UUID,
|
||||||
unidades: Array<any>,
|
unidades: Array<ContenidoApi>,
|
||||||
): Promise<Asignatura> {
|
): Promise<Asignatura> {
|
||||||
return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, {
|
const supabase = supabaseBrowser()
|
||||||
subjectId,
|
|
||||||
unidades,
|
type AsignaturaUpdate = Database['public']['Tables']['asignaturas']['Update']
|
||||||
})
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('asignaturas')
|
||||||
|
.update({
|
||||||
|
contenido_tematico:
|
||||||
|
unidades as unknown as AsignaturaUpdate['contenido_tematico'],
|
||||||
|
})
|
||||||
|
.eq('id', subjectId)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
throwIfError(error)
|
||||||
|
return requireData(data, 'No se pudo actualizar la asignatura.')
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BibliografiaUpsertInput = Array<{
|
export type BibliografiaUpsertInput = Array<{
|
||||||
@@ -301,6 +499,7 @@ export async function lineas_insert(linea: {
|
|||||||
plan_estudio_id: string
|
plan_estudio_id: string
|
||||||
orden: number
|
orden: number
|
||||||
area?: string
|
area?: string
|
||||||
|
color?: string | null
|
||||||
}) {
|
}) {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
@@ -316,7 +515,12 @@ export async function lineas_insert(linea: {
|
|||||||
// Actualizar una línea existente
|
// Actualizar una línea existente
|
||||||
export async function lineas_update(
|
export async function lineas_update(
|
||||||
lineaId: string,
|
lineaId: string,
|
||||||
patch: { nombre?: string; orden?: number; area?: string },
|
patch: {
|
||||||
|
nombre?: string
|
||||||
|
orden?: number
|
||||||
|
area?: string
|
||||||
|
color?: string | null
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
@@ -343,3 +547,93 @@ export async function lineas_delete(lineaId: string) {
|
|||||||
if (error) throw error
|
if (error) throw error
|
||||||
return lineaId
|
return lineaId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function bibliografia_insert(
|
||||||
|
entry: TablesInsert<'bibliografia_asignatura'>,
|
||||||
|
): Promise<Tables<'bibliografia_asignatura'>> {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('bibliografia_asignatura')
|
||||||
|
.insert([entry])
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data as Tables<'bibliografia_asignatura'>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bibliografia_update(
|
||||||
|
id: string,
|
||||||
|
updates: {
|
||||||
|
cita?: string
|
||||||
|
tipo?: 'BASICA' | 'COMPLEMENTARIA'
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('bibliografia_asignatura')
|
||||||
|
.update(updates) // Ahora 'updates' es compatible con lo que espera Supabase
|
||||||
|
.eq('id', id)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bibliografia_delete(id: string) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('bibliografia_asignatura')
|
||||||
|
.delete()
|
||||||
|
.eq('id', id)
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkPrerrequisitoConflicts(
|
||||||
|
asignaturaId: string,
|
||||||
|
nuevoCiclo: number,
|
||||||
|
): Promise<Array<string>> {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
// CORRECCIÓN 1: Agregamos 'id' al select
|
||||||
|
// CORRECCIÓN 2: Quitamos espacios en el .or()
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('asignaturas')
|
||||||
|
.select('id, nombre, numero_ciclo, prerrequisito_asignatura_id')
|
||||||
|
.or(`prerrequisito_asignatura_id.eq.${asignaturaId},id.eq.${asignaturaId}`)
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
// CORRECCIÓN 3: 'data' ahora está disponible porque lo desestructuramos arriba
|
||||||
|
if (!data) return []
|
||||||
|
|
||||||
|
const conflictos: Array<string> = []
|
||||||
|
|
||||||
|
data.forEach((asig) => {
|
||||||
|
// Caso 1: Materias que tienen a esta como prerrequisito (Hijas)
|
||||||
|
// "Si yo me muevo al ciclo 5, y mi hija está en el 4, se rompe la regla"
|
||||||
|
if (asig.prerrequisito_asignatura_id === asignaturaId) {
|
||||||
|
if (asig.numero_ciclo !== null && asig.numero_ciclo <= nuevoCiclo) {
|
||||||
|
conflictos.push(asig.nombre)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caso 2: El prerrequisito de la materia que estoy moviendo (Padre)
|
||||||
|
// "Si yo me muevo al ciclo 2, y mi padre está en el 3, se rompe la regla"
|
||||||
|
if (asig.id === asignaturaId && asig.prerrequisito_asignatura_id) {
|
||||||
|
const padre = data.find((p) => p.id === asig.prerrequisito_asignatura_id)
|
||||||
|
if (
|
||||||
|
padre &&
|
||||||
|
padre.numero_ciclo !== null &&
|
||||||
|
padre.numero_ciclo >= nuevoCiclo
|
||||||
|
) {
|
||||||
|
conflictos.push(padre.nombre)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return conflictos
|
||||||
|
}
|
||||||
|
|||||||
+320
-12
@@ -1,29 +1,337 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ai_plan_chat,
|
ai_plan_chat_v2,
|
||||||
ai_plan_improve,
|
ai_plan_improve,
|
||||||
ai_subject_chat,
|
|
||||||
ai_subject_improve,
|
ai_subject_improve,
|
||||||
|
create_conversation,
|
||||||
|
get_chat_history,
|
||||||
|
getConversationByPlan,
|
||||||
library_search,
|
library_search,
|
||||||
} from "../api/ai.api";
|
update_conversation_status,
|
||||||
|
update_recommendation_applied_status,
|
||||||
|
update_conversation_title,
|
||||||
|
getMessagesByConversation,
|
||||||
|
update_subject_conversation_status,
|
||||||
|
update_subject_recommendation_applied,
|
||||||
|
getMessagesBySubjectConversation,
|
||||||
|
getConversationBySubject,
|
||||||
|
ai_subject_chat_v2,
|
||||||
|
create_subject_conversation,
|
||||||
|
update_subject_conversation_name,
|
||||||
|
} from '../api/ai.api'
|
||||||
|
import { supabaseBrowser } from '../supabase/client'
|
||||||
|
|
||||||
|
import type { UUID } from 'node:crypto'
|
||||||
|
|
||||||
export function useAIPlanImprove() {
|
export function useAIPlanImprove() {
|
||||||
return useMutation({ mutationFn: ai_plan_improve });
|
return useMutation({ mutationFn: ai_plan_improve })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAIPlanChat() {
|
export function useAIPlanChat() {
|
||||||
return useMutation({ mutationFn: ai_plan_chat });
|
return useMutation({
|
||||||
|
mutationFn: async (payload: {
|
||||||
|
planId: UUID
|
||||||
|
content: string
|
||||||
|
campos?: Array<string>
|
||||||
|
conversacionId?: string
|
||||||
|
}) => {
|
||||||
|
let currentId = payload.conversacionId
|
||||||
|
|
||||||
|
// 1. Si no hay ID, creamos la conversación
|
||||||
|
if (!currentId) {
|
||||||
|
const response = await create_conversation(payload.planId)
|
||||||
|
|
||||||
|
// CAMBIO AQUÍ: Accedemos a la estructura correcta según tu consola
|
||||||
|
currentId = response.conversation_plan.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Ahora enviamos el mensaje con el ID garantizado
|
||||||
|
const result = await ai_plan_chat_v2({
|
||||||
|
conversacionId: currentId!,
|
||||||
|
content: payload.content,
|
||||||
|
campos: payload.campos,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Retornamos el resultado del chat y el ID para el estado del componente
|
||||||
|
return { ...result, conversacionId: currentId }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChatHistory(conversacionId?: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['chat-history', conversacionId],
|
||||||
|
queryFn: async () => {
|
||||||
|
return get_chat_history(conversacionId!)
|
||||||
|
},
|
||||||
|
enabled: Boolean(conversacionId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateConversationStatus() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
id,
|
||||||
|
estado,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
estado: 'ARCHIVADA' | 'ACTIVA'
|
||||||
|
}) => update_conversation_status(id, estado),
|
||||||
|
onSuccess: () => {
|
||||||
|
// Esto refresca las listas automáticamente
|
||||||
|
qc.invalidateQueries({ queryKey: ['conversation-by-plan'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConversationByPlan(planId: string | null) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['conversation-by-plan', planId],
|
||||||
|
queryFn: () => getConversationByPlan(planId!),
|
||||||
|
enabled: !!planId, // solo ejecuta si existe planId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMessagesByChat(conversationId: string | null) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['conversation-messages', conversationId],
|
||||||
|
queryFn: () => {
|
||||||
|
if (!conversationId) throw new Error('Conversation ID is required')
|
||||||
|
return getMessagesByConversation(conversationId)
|
||||||
|
},
|
||||||
|
enabled: !!conversationId,
|
||||||
|
placeholderData: (previousData) => previousData,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!conversationId) return
|
||||||
|
|
||||||
|
// Suscribirse a cambios en los mensajes de ESTA conversación
|
||||||
|
const channel = supabase
|
||||||
|
.channel(`realtime-messages-${conversationId}`)
|
||||||
|
.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{
|
||||||
|
event: '*', // Escuchamos INSERT y UPDATE
|
||||||
|
schema: 'public',
|
||||||
|
table: 'plan_mensajes_ia',
|
||||||
|
filter: `conversacion_plan_id=eq.${conversationId}`,
|
||||||
|
},
|
||||||
|
(payload) => {
|
||||||
|
// Opción A: Invalidar la query para que React Query haga refetch (más seguro)
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['conversation-messages', conversationId],
|
||||||
|
})
|
||||||
|
|
||||||
|
/* Opción B: Actualización manual del caché (más rápido/fluido)
|
||||||
|
if (payload.eventType === 'INSERT') {
|
||||||
|
queryClient.setQueryData(['conversation-messages', conversationId], (old: any) => [...old, payload.new])
|
||||||
|
} else if (payload.eventType === 'UPDATE') {
|
||||||
|
queryClient.setQueryData(['conversation-messages', conversationId], (old: any) =>
|
||||||
|
old.map((m: any) => m.id === payload.new.id ? payload.new : m)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.subscribe()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
supabase.removeChannel(channel)
|
||||||
|
}
|
||||||
|
}, [conversationId, queryClient, supabase])
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateRecommendationApplied() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
conversacionId,
|
||||||
|
campoAfectado,
|
||||||
|
}: {
|
||||||
|
conversacionId: string
|
||||||
|
campoAfectado: string
|
||||||
|
}) => update_recommendation_applied_status(conversacionId, campoAfectado),
|
||||||
|
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
// Invalidamos la query para que useConversationByPlan refresque el JSON
|
||||||
|
qc.invalidateQueries({ queryKey: ['conversation-by-plan'] })
|
||||||
|
console.log(
|
||||||
|
`Recomendación ${variables.campoAfectado} marcada como aplicada.`,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Error al actualizar el estado de la recomendación:', error)
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAISubjectImprove() {
|
export function useAISubjectImprove() {
|
||||||
return useMutation({ mutationFn: ai_subject_improve });
|
return useMutation({ mutationFn: ai_subject_improve })
|
||||||
}
|
|
||||||
|
|
||||||
export function useAISubjectChat() {
|
|
||||||
return useMutation({ mutationFn: ai_subject_chat });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLibrarySearch() {
|
export function useLibrarySearch() {
|
||||||
return useMutation({ mutationFn: library_search });
|
return useMutation({ mutationFn: library_search })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateConversationTitle() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, nombre }: { id: string; nombre: string }) =>
|
||||||
|
update_conversation_title(id, nombre),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
// Invalidamos para que la lista de chats se refresque
|
||||||
|
qc.invalidateQueries({ queryKey: ['conversation-by-plan'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asignaturas
|
||||||
|
|
||||||
|
export function useAISubjectChat() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (payload: {
|
||||||
|
subjectId: UUID
|
||||||
|
content: string
|
||||||
|
campos?: Array<string>
|
||||||
|
conversacionId?: string
|
||||||
|
}) => {
|
||||||
|
let currentId = payload.conversacionId
|
||||||
|
|
||||||
|
// 1. Si no hay ID, creamos la conversación de asignatura
|
||||||
|
if (!currentId) {
|
||||||
|
const response = await create_subject_conversation(payload.subjectId)
|
||||||
|
currentId = response.conversation_asignatura.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Enviamos mensaje al endpoint de asignatura
|
||||||
|
const result = await ai_subject_chat_v2({
|
||||||
|
conversacionId: currentId!,
|
||||||
|
content: payload.content,
|
||||||
|
campos: payload.campos,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...result, conversacionId: currentId }
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// Invalidamos mensajes para que se refresque el chat
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: ['subject-messages', data.conversacionId],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConversationBySubject(subjectId: string | null) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['conversation-by-subject', subjectId],
|
||||||
|
queryFn: () => getConversationBySubject(subjectId!),
|
||||||
|
enabled: !!subjectId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMessagesBySubjectChat(conversationId: string | null) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['subject-messages', conversationId],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!conversationId) throw new Error('Conversation ID is required')
|
||||||
|
return getMessagesBySubjectConversation(conversationId)
|
||||||
|
},
|
||||||
|
enabled: !!conversationId,
|
||||||
|
placeholderData: (previousData) => previousData,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!conversationId) return
|
||||||
|
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
// Suscripción a cambios en la tabla específica para esta conversación
|
||||||
|
const channel = supabase
|
||||||
|
.channel(`subject_messages_${conversationId}`)
|
||||||
|
.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{
|
||||||
|
event: 'UPDATE', // Solo nos interesan las actualizaciones (cuando pasa de PROCESANDO a COMPLETADO)
|
||||||
|
schema: 'public',
|
||||||
|
table: 'asignatura_mensajes_ia',
|
||||||
|
filter: `conversacion_asignatura_id=eq.${conversationId}`,
|
||||||
|
},
|
||||||
|
(payload) => {
|
||||||
|
// Si el mensaje se completó o dio error, invalidamos la caché para traer los datos nuevos
|
||||||
|
if (
|
||||||
|
payload.new.estado === 'COMPLETADO' ||
|
||||||
|
payload.new.estado === 'ERROR'
|
||||||
|
) {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['subject-messages', conversationId],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.subscribe()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
supabase.removeChannel(channel)
|
||||||
|
}
|
||||||
|
}, [conversationId, queryClient])
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateSubjectRecommendation() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: { mensajeId: string; campoAfectado: string }) =>
|
||||||
|
update_subject_recommendation_applied(
|
||||||
|
payload.mensajeId,
|
||||||
|
payload.campoAfectado,
|
||||||
|
),
|
||||||
|
onSuccess: () => {
|
||||||
|
// Refrescamos los mensajes para ver el check de "aplicado"
|
||||||
|
qc.invalidateQueries({ queryKey: ['subject-messages'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateSubjectConversationStatus() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: { id: string; estado: 'ARCHIVADA' | 'ACTIVA' }) =>
|
||||||
|
update_subject_conversation_status(payload.id, payload.estado),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['conversation-by-subject'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateSubjectConversationName() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: { id: string; nombre: string }) =>
|
||||||
|
update_subject_conversation_name(payload.id, payload.nombre),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['conversation-by-subject'] })
|
||||||
|
// También invalidamos los mensajes si el título se muestra en la cabecera
|
||||||
|
qc.invalidateQueries({ queryKey: ['subject-messages'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ai_generate_plan,
|
ai_generate_plan,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
plan_lineas_list,
|
plan_lineas_list,
|
||||||
plans_clone_from_existing,
|
plans_clone_from_existing,
|
||||||
plans_create_manual,
|
plans_create_manual,
|
||||||
|
plans_delete,
|
||||||
plans_generate_document,
|
plans_generate_document,
|
||||||
plans_get,
|
plans_get,
|
||||||
plans_get_document,
|
plans_get_document,
|
||||||
@@ -25,6 +27,7 @@ import {
|
|||||||
} from '../api/plans.api'
|
} from '../api/plans.api'
|
||||||
import { lineas_delete } from '../api/subjects.api'
|
import { lineas_delete } from '../api/subjects.api'
|
||||||
import { qk } from '../query/keys'
|
import { qk } from '../query/keys'
|
||||||
|
import { supabaseBrowser } from '../supabase/client'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
PlanListFilters,
|
PlanListFilters,
|
||||||
@@ -71,20 +74,92 @@ export function usePlanLineas(planId: UUID | null | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function usePlanAsignaturas(planId: UUID | null | undefined) {
|
export function usePlanAsignaturas(planId: UUID | null | undefined) {
|
||||||
return useQuery({
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
queryKey: planId
|
queryKey: planId
|
||||||
? qk.planAsignaturas(planId)
|
? qk.planAsignaturas(planId)
|
||||||
: ['planes', 'asignaturas', null],
|
: ['planes', 'asignaturas', null],
|
||||||
queryFn: () => plan_asignaturas_list(planId as UUID),
|
queryFn: () => plan_asignaturas_list(planId as UUID),
|
||||||
enabled: Boolean(planId),
|
enabled: Boolean(planId),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!planId) return
|
||||||
|
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
const channel = supabase.channel(`plan-asignaturas-${planId}`)
|
||||||
|
|
||||||
|
channel.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{
|
||||||
|
event: '*',
|
||||||
|
schema: 'public',
|
||||||
|
table: 'asignaturas',
|
||||||
|
filter: `plan_estudio_id=eq.${planId}`,
|
||||||
|
},
|
||||||
|
(payload: {
|
||||||
|
eventType?: 'INSERT' | 'UPDATE' | 'DELETE'
|
||||||
|
new?: any
|
||||||
|
old?: any
|
||||||
|
}) => {
|
||||||
|
const eventType = payload.eventType
|
||||||
|
|
||||||
|
if (eventType === 'DELETE') {
|
||||||
|
const oldRow: any = payload.old
|
||||||
|
const deletedId = oldRow?.id
|
||||||
|
if (!deletedId) return
|
||||||
|
|
||||||
|
qc.setQueryData(qk.planAsignaturas(planId), (prev) => {
|
||||||
|
if (!Array.isArray(prev)) return prev
|
||||||
|
return prev.filter((a: any) => String(a?.id) !== String(deletedId))
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRow: any = payload.new
|
||||||
|
if (!newRow?.id) return
|
||||||
|
|
||||||
|
qc.setQueryData(qk.planAsignaturas(planId), (prev) => {
|
||||||
|
if (!Array.isArray(prev)) return prev
|
||||||
|
|
||||||
|
const idx = prev.findIndex(
|
||||||
|
(a: any) => String(a?.id) === String(newRow.id),
|
||||||
|
)
|
||||||
|
if (idx === -1) return [...prev, newRow]
|
||||||
|
|
||||||
|
const next = [...prev]
|
||||||
|
next[idx] = { ...prev[idx], ...newRow }
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
channel.subscribe()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
supabase.removeChannel(channel)
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [planId, qc])
|
||||||
|
|
||||||
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePlanHistorial(planId: UUID | null | undefined) {
|
export function usePlanHistorial(
|
||||||
|
planId: UUID | null | undefined,
|
||||||
|
page: number,
|
||||||
|
) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: planId ? qk.planHistorial(planId) : ['planes', 'historial', null],
|
queryKey: planId
|
||||||
queryFn: () => plans_history(planId as UUID),
|
? [...qk.planHistorial(planId), page]
|
||||||
|
: ['planes', 'historial', null, page],
|
||||||
|
queryFn: () => plans_history(planId as UUID, page),
|
||||||
enabled: Boolean(planId),
|
enabled: Boolean(planId),
|
||||||
|
placeholderData: (previousData) => previousData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,6 +322,23 @@ export function useTransitionPlanEstado() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDeletePlanEstudio() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (planId: UUID) => plans_delete(planId),
|
||||||
|
onSuccess: (_ok, planId) => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['planes', 'list'] })
|
||||||
|
qc.removeQueries({ queryKey: qk.plan(planId) })
|
||||||
|
qc.removeQueries({ queryKey: qk.planMaybe(planId) })
|
||||||
|
qc.removeQueries({ queryKey: qk.planAsignaturas(planId) })
|
||||||
|
qc.removeQueries({ queryKey: qk.planLineas(planId) })
|
||||||
|
qc.removeQueries({ queryKey: qk.planHistorial(planId) })
|
||||||
|
qc.removeQueries({ queryKey: qk.planDocumento(planId) })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function useGeneratePlanDocumento() {
|
export function useGeneratePlanDocumento() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
@@ -263,7 +355,7 @@ export function useDeleteLinea() {
|
|||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: lineas_delete,
|
mutationFn: lineas_delete,
|
||||||
onSuccess: (idEliminado) => {
|
onSuccess: (_idEliminado) => {
|
||||||
// Invalidamos para que las materias y líneas se refresquen
|
// Invalidamos para que las materias y líneas se refresquen
|
||||||
qc.invalidateQueries({ queryKey: ['plan_lineas'] })
|
qc.invalidateQueries({ queryKey: ['plan_lineas'] })
|
||||||
qc.invalidateQueries({ queryKey: ['plan_asignaturas'] })
|
qc.invalidateQueries({ queryKey: ['plan_asignaturas'] })
|
||||||
|
|||||||
+103
-13
@@ -1,8 +1,13 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ai_generate_subject,
|
ai_generate_subject,
|
||||||
asignaturas_update,
|
asignaturas_update,
|
||||||
|
bibliografia_delete,
|
||||||
|
bibliografia_insert,
|
||||||
|
bibliografia_update,
|
||||||
|
checkPrerrequisitoConflicts,
|
||||||
lineas_insert,
|
lineas_insert,
|
||||||
lineas_update,
|
lineas_update,
|
||||||
subjects_bibliografia_list,
|
subjects_bibliografia_list,
|
||||||
@@ -23,10 +28,11 @@ import { qk } from '../query/keys'
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
BibliografiaUpsertInput,
|
BibliografiaUpsertInput,
|
||||||
SubjectsCreateManualInput,
|
ContenidoApi,
|
||||||
SubjectsUpdateFieldsPatch,
|
SubjectsUpdateFieldsPatch,
|
||||||
} from '../api/subjects.api'
|
} from '../api/subjects.api'
|
||||||
import type { UUID } from '../types/domain'
|
import type { UUID } from '../types/domain'
|
||||||
|
import type { TablesInsert } from '@/types/supabase'
|
||||||
|
|
||||||
export function useSubject(subjectId: UUID | null | undefined) {
|
export function useSubject(subjectId: UUID | null | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -82,7 +88,7 @@ export function useCreateSubjectManual() {
|
|||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload: SubjectsCreateManualInput) =>
|
mutationFn: (payload: TablesInsert<'asignaturas'>) =>
|
||||||
subjects_create_manual(payload),
|
subjects_create_manual(payload),
|
||||||
onSuccess: (subject) => {
|
onSuccess: (subject) => {
|
||||||
qc.setQueryData(qk.asignatura(subject.id), subject)
|
qc.setQueryData(qk.asignatura(subject.id), subject)
|
||||||
@@ -97,7 +103,6 @@ export function useCreateSubjectManual() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useGenerateSubjectAI() {
|
export function useGenerateSubjectAI() {
|
||||||
const qc = useQueryClient()
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ai_generate_subject,
|
mutationFn: ai_generate_subject,
|
||||||
})
|
})
|
||||||
@@ -162,7 +167,9 @@ export function useUpdateSubjectFields() {
|
|||||||
mutationFn: (vars: { subjectId: UUID; patch: SubjectsUpdateFieldsPatch }) =>
|
mutationFn: (vars: { subjectId: UUID; patch: SubjectsUpdateFieldsPatch }) =>
|
||||||
subjects_update_fields(vars.subjectId, vars.patch),
|
subjects_update_fields(vars.subjectId, vars.patch),
|
||||||
onSuccess: (updated) => {
|
onSuccess: (updated) => {
|
||||||
qc.setQueryData(qk.asignatura(updated.id), updated)
|
qc.setQueryData(qk.asignatura(updated.id), (prev) =>
|
||||||
|
prev ? { ...(prev as any), ...(updated as any) } : updated,
|
||||||
|
)
|
||||||
qc.invalidateQueries({
|
qc.invalidateQueries({
|
||||||
queryKey: qk.planAsignaturas(updated.plan_estudio_id),
|
queryKey: qk.planAsignaturas(updated.plan_estudio_id),
|
||||||
})
|
})
|
||||||
@@ -175,10 +182,19 @@ export function useUpdateSubjectContenido() {
|
|||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (vars: { subjectId: UUID; unidades: Array<any> }) =>
|
mutationFn: (vars: { subjectId: UUID; unidades: Array<ContenidoApi> }) =>
|
||||||
subjects_update_contenido(vars.subjectId, vars.unidades),
|
subjects_update_contenido(vars.subjectId, vars.unidades),
|
||||||
onSuccess: (updated) => {
|
onSuccess: (updated) => {
|
||||||
qc.setQueryData(qk.asignatura(updated.id), updated)
|
qc.setQueryData(qk.asignatura(updated.id), (prev) =>
|
||||||
|
prev ? { ...(prev as any), ...(updated as any) } : updated,
|
||||||
|
)
|
||||||
|
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: qk.planAsignaturas(updated.plan_estudio_id),
|
||||||
|
})
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: qk.planHistorial(updated.plan_estudio_id),
|
||||||
|
})
|
||||||
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -221,17 +237,22 @@ export function useUpdateAsignatura() {
|
|||||||
}) => asignaturas_update(vars.asignaturaId, vars.patch),
|
}) => asignaturas_update(vars.asignaturaId, vars.patch),
|
||||||
|
|
||||||
onSuccess: (updated) => {
|
onSuccess: (updated) => {
|
||||||
// 1. Actualizamos la materia específica en la caché si tienes un query de "detalle"
|
// ✅ Mantener consistencia con las query keys centralizadas (qk)
|
||||||
qc.setQueryData(['asignatura', updated.id], updated)
|
// 1) Actualiza el detalle (esto evita volver a entrar con caché vieja)
|
||||||
|
qc.setQueryData(qk.asignatura(updated.id), (prev) =>
|
||||||
|
prev ? { ...(prev as any), ...(updated as any) } : updated,
|
||||||
|
)
|
||||||
|
|
||||||
// 2. IMPORTANTÍSIMO: Invalidamos la lista de materias del plan
|
// 2) Refresca vistas derivadas del plan
|
||||||
// para que el mapa curricular vea los cambios (créditos, horas, nombre, etc.)
|
|
||||||
qc.invalidateQueries({
|
qc.invalidateQueries({
|
||||||
queryKey: ['plan_asignaturas', updated.plan_estudio_id],
|
queryKey: qk.planAsignaturas(updated.plan_estudio_id),
|
||||||
|
})
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: qk.planHistorial(updated.plan_estudio_id),
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3. Si tienes una lista general de asignaturas, también la invalidamos
|
// 3) Refresca historial de la asignatura si existe
|
||||||
qc.invalidateQueries({ queryKey: ['asignaturas', 'list'] })
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -260,3 +281,72 @@ export function useUpdateLinea() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCreateBibliografia() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: bibliografia_insert,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// USAR LA MISMA LLAVE QUE EL HOOK DE LECTURA
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: qk.asignaturaBibliografia(data.asignatura_id),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateBibliografia(asignaturaId: string) {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, updates }: { id: string; updates: any }) =>
|
||||||
|
bibliografia_update(id, updates),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: qk.asignaturaBibliografia(asignaturaId),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteBibliografia(asignaturaId: string) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => bibliografia_delete(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: qk.asignaturaBibliografia(asignaturaId),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAsignaturaConflictos() {
|
||||||
|
const [isValidating, setIsValidating] = useState(false)
|
||||||
|
|
||||||
|
const validarCambioCiclo = async (
|
||||||
|
asignaturaId: string,
|
||||||
|
nuevoCiclo: number,
|
||||||
|
) => {
|
||||||
|
setIsValidating(true)
|
||||||
|
try {
|
||||||
|
const nombresConflictivos = await checkPrerrequisitoConflicts(
|
||||||
|
asignaturaId,
|
||||||
|
nuevoCiclo,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (nombresConflictivos.length > 0) {
|
||||||
|
const mensaje = `Si mueves esta materia al ciclo ${nuevoCiclo}, se perderá la seriación con:\n\n• ${nombresConflictivos.join('\n• ')}\n\n¿Deseas continuar?`
|
||||||
|
return confirm(mensaje) // Puedes usar un Modal de Shadcn aquí en lugar de confirm
|
||||||
|
}
|
||||||
|
|
||||||
|
return true // Sin conflictos
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
setIsValidating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { validarCambioCiclo, isValidating }
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type {
|
|||||||
BibliografiaEntry,
|
BibliografiaEntry,
|
||||||
CambioAsignatura,
|
CambioAsignatura,
|
||||||
DocumentoAsignatura,
|
DocumentoAsignatura,
|
||||||
LibraryResource,
|
|
||||||
} from '@/types/asignatura'
|
} from '@/types/asignatura'
|
||||||
|
|
||||||
export const mockAsignatura: Asignatura = {
|
export const mockAsignatura: Asignatura = {
|
||||||
@@ -310,67 +309,3 @@ export const mockDocumentoSep: DocumentoAsignatura = {
|
|||||||
fechaGeneracion: new Date('2024-12-06T11:30:00'),
|
fechaGeneracion: new Date('2024-12-06T11:30:00'),
|
||||||
estado: 'listo',
|
estado: 'listo',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mockLibraryResources: Array<LibraryResource> = [
|
|
||||||
{
|
|
||||||
id: 'lib-1',
|
|
||||||
titulo: 'Artificial Intelligence: A Modern Approach',
|
|
||||||
autor: 'Stuart Russell, Peter Norvig',
|
|
||||||
editorial: 'Pearson',
|
|
||||||
anio: 2021,
|
|
||||||
isbn: '978-0134610993',
|
|
||||||
tipo: 'libro',
|
|
||||||
disponible: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lib-2',
|
|
||||||
titulo:
|
|
||||||
'Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow',
|
|
||||||
autor: 'Aurélien Géron',
|
|
||||||
editorial: "O'Reilly Media",
|
|
||||||
anio: 2022,
|
|
||||||
isbn: '978-1098125974',
|
|
||||||
tipo: 'libro',
|
|
||||||
disponible: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lib-3',
|
|
||||||
titulo: 'Pattern Recognition and Machine Learning',
|
|
||||||
autor: 'Christopher Bishop',
|
|
||||||
editorial: 'Springer',
|
|
||||||
anio: 2006,
|
|
||||||
isbn: '978-0387310732',
|
|
||||||
tipo: 'libro',
|
|
||||||
disponible: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lib-4',
|
|
||||||
titulo: 'Deep Learning with Python',
|
|
||||||
autor: 'François Chollet',
|
|
||||||
editorial: 'Manning Publications',
|
|
||||||
anio: 2021,
|
|
||||||
isbn: '978-1617296864',
|
|
||||||
tipo: 'libro',
|
|
||||||
disponible: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lib-5',
|
|
||||||
titulo: 'Neural Networks and Deep Learning: A Textbook',
|
|
||||||
autor: 'Charu C. Aggarwal',
|
|
||||||
editorial: 'Springer',
|
|
||||||
anio: 2023,
|
|
||||||
isbn: '978-3031296413',
|
|
||||||
tipo: 'libro',
|
|
||||||
disponible: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lib-6',
|
|
||||||
titulo: 'Machine Learning: A Probabilistic Perspective',
|
|
||||||
autor: 'Kevin Murphy',
|
|
||||||
editorial: 'MIT Press',
|
|
||||||
anio: 2012,
|
|
||||||
isbn: '978-0262018029',
|
|
||||||
tipo: 'libro',
|
|
||||||
disponible: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -13,14 +13,18 @@ export const qk = {
|
|||||||
|
|
||||||
planesList: (filters: unknown) => ['planes', 'list', filters] as const,
|
planesList: (filters: unknown) => ['planes', 'list', filters] as const,
|
||||||
plan: (planId: string) => ['planes', 'detail', planId] as const,
|
plan: (planId: string) => ['planes', 'detail', planId] as const,
|
||||||
|
planMaybe: (planId: string) => ['planes', 'detail-maybe', planId] as const,
|
||||||
planLineas: (planId: string) => ['planes', planId, 'lineas'] as const,
|
planLineas: (planId: string) => ['planes', planId, 'lineas'] as const,
|
||||||
planAsignaturas: (planId: string) =>
|
planAsignaturas: (planId: string) =>
|
||||||
['planes', planId, 'asignaturas'] as const,
|
['planes', planId, 'asignaturas'] as const,
|
||||||
planHistorial: (planId: string) => ['planes', planId, 'historial'] as const,
|
planHistorial: (planId: string) => ['planes', planId, 'historial'] as const,
|
||||||
planDocumento: (planId: string) => ['planes', planId, 'documento'] as const,
|
planDocumento: (planId: string) => ['planes', planId, 'documento'] as const,
|
||||||
|
|
||||||
|
sugerenciasAsignaturas: () => ['asignaturas', 'sugerencias'] as const,
|
||||||
asignatura: (asignaturaId: string) =>
|
asignatura: (asignaturaId: string) =>
|
||||||
['asignaturas', 'detail', asignaturaId] as const,
|
['asignaturas', 'detail', asignaturaId] as const,
|
||||||
|
asignaturaMaybe: (asignaturaId: string) =>
|
||||||
|
['asignaturas', 'detail-maybe', asignaturaId] as const,
|
||||||
asignaturaBibliografia: (asignaturaId: string) =>
|
asignaturaBibliografia: (asignaturaId: string) =>
|
||||||
['asignaturas', asignaturaId, 'bibliografia'] as const,
|
['asignaturas', asignaturaId, 'bibliografia'] as const,
|
||||||
asignaturaHistorial: (asignaturaId: string) =>
|
asignaturaHistorial: (asignaturaId: string) =>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { SupabaseClient } from '@supabase/supabase-js'
|
|||||||
export type EdgeInvokeOptions = {
|
export type EdgeInvokeOptions = {
|
||||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
|
responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer'
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EdgeFunctionError extends Error {
|
export class EdgeFunctionError extends Error {
|
||||||
@@ -26,6 +27,55 @@ export class EdgeFunctionError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Soporta base64 puro o data:...;base64,...
|
||||||
|
function decodeBase64ToUint8Array(input: string): Uint8Array {
|
||||||
|
const trimmed = input.trim()
|
||||||
|
const base64 = trimmed.startsWith('data:')
|
||||||
|
? trimmed.slice(trimmed.indexOf(',') + 1)
|
||||||
|
: trimmed
|
||||||
|
|
||||||
|
const bin = atob(base64)
|
||||||
|
const bytes = new Uint8Array(bin.length)
|
||||||
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripDataUrlPrefix(input: string): string {
|
||||||
|
const trimmed = input.trim()
|
||||||
|
if (!trimmed.startsWith('data:')) return trimmed
|
||||||
|
const commaIdx = trimmed.indexOf(',')
|
||||||
|
return commaIdx >= 0 ? trimmed.slice(commaIdx + 1) : trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeBase64(s: string): boolean {
|
||||||
|
const t = stripDataUrlPrefix(s).replace(/\s+/g, '').replace(/=+$/g, '')
|
||||||
|
|
||||||
|
// base64 típico: solo chars permitidos y longitud razonable
|
||||||
|
if (t.length < 64) return false
|
||||||
|
return /^[A-Za-z0-9+/]+$/.test(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
function startsWithZip(bytes: Uint8Array): boolean {
|
||||||
|
return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b // "PK"
|
||||||
|
}
|
||||||
|
|
||||||
|
function startsWithPdf(bytes: Uint8Array): boolean {
|
||||||
|
return (
|
||||||
|
bytes.length >= 5 &&
|
||||||
|
bytes[0] === 0x25 &&
|
||||||
|
bytes[1] === 0x50 &&
|
||||||
|
bytes[2] === 0x44 &&
|
||||||
|
bytes[3] === 0x46 &&
|
||||||
|
bytes[4] === 0x2d
|
||||||
|
) // "%PDF-"
|
||||||
|
}
|
||||||
|
|
||||||
|
function binaryStringToUint8Array(input: string): Uint8Array {
|
||||||
|
const bytes = new Uint8Array(input.length)
|
||||||
|
for (let i = 0; i < input.length; i++) bytes[i] = input.charCodeAt(i) & 0xff
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
export async function invokeEdge<TOut>(
|
export async function invokeEdge<TOut>(
|
||||||
functionName: string,
|
functionName: string,
|
||||||
body?:
|
body?:
|
||||||
@@ -42,10 +92,16 @@ export async function invokeEdge<TOut>(
|
|||||||
): Promise<TOut> {
|
): Promise<TOut> {
|
||||||
const supabase = client ?? supabaseBrowser()
|
const supabase = client ?? supabaseBrowser()
|
||||||
|
|
||||||
const { data, error } = await supabase.functions.invoke(functionName, {
|
// Nota: algunas versiones/defs de @supabase/supabase-js no tipan `responseType`
|
||||||
|
// aunque el runtime lo soporte. Usamos `any` para no bloquear el uso de Blob.
|
||||||
|
const invoke: any = (supabase.functions as any).invoke.bind(
|
||||||
|
supabase.functions,
|
||||||
|
)
|
||||||
|
const { data, error } = await invoke(functionName, {
|
||||||
body,
|
body,
|
||||||
method: opts.method ?? 'POST',
|
method: opts.method ?? 'POST',
|
||||||
headers: opts.headers,
|
headers: opts.headers,
|
||||||
|
responseType: opts.responseType,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -104,5 +160,20 @@ export async function invokeEdge<TOut>(
|
|||||||
throw new EdgeFunctionError(message, functionName, status, details)
|
throw new EdgeFunctionError(message, functionName, status, details)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.responseType === 'blob') {
|
||||||
|
const anyData: unknown = data
|
||||||
|
|
||||||
|
if (anyData instanceof Blob) {
|
||||||
|
return anyData as TOut
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new EdgeFunctionError(
|
||||||
|
'La Edge Function no devolvió un binario (Blob) válido.',
|
||||||
|
functionName,
|
||||||
|
undefined,
|
||||||
|
{ receivedType: typeof anyData, received: anyData },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return data as TOut
|
return data as TOut
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react'
|
||||||
|
|
||||||
|
// --- DEFINICIÓN DE MENSAJES ---
|
||||||
|
const MENSAJES_CORTOS = [
|
||||||
|
// Hasta 5 sugerencias (6 mensajes)
|
||||||
|
'Analizando el plan de estudios...',
|
||||||
|
'Identificando áreas de oportunidad...',
|
||||||
|
'Consultando bases de datos académicas...',
|
||||||
|
'Redactando competencias específicas...',
|
||||||
|
'Calculando créditos y horas...',
|
||||||
|
'Afinando los últimos detalles...',
|
||||||
|
]
|
||||||
|
|
||||||
|
const MENSAJES_MEDIOS = [
|
||||||
|
// Hasta 10 sugerencias (10 mensajes)
|
||||||
|
'Conectando con el motor de IA...',
|
||||||
|
'Analizando estructura curricular...',
|
||||||
|
'Buscando asignaturas compatibles...',
|
||||||
|
'Verificando prerrequisitos...',
|
||||||
|
'Generando descripciones detalladas...',
|
||||||
|
'Balanceando cargas académicas...',
|
||||||
|
'Asignando horas independientes...',
|
||||||
|
'Validando coherencia temática...',
|
||||||
|
'Formateando resultados...',
|
||||||
|
'Finalizando generación...',
|
||||||
|
]
|
||||||
|
|
||||||
|
const MENSAJES_LARGOS = [
|
||||||
|
// Más de 10 sugerencias (14 mensajes)
|
||||||
|
'Iniciando procesamiento masivo...',
|
||||||
|
'Escaneando retícula completa...',
|
||||||
|
'Detectando líneas de investigación...',
|
||||||
|
'Generando primer bloque de asignaturas...',
|
||||||
|
'Evaluando pertinencia académica...',
|
||||||
|
'Optimizando créditos por ciclo...',
|
||||||
|
'Redactando objetivos de aprendizaje...',
|
||||||
|
'Generando segundo bloque...',
|
||||||
|
'Revisando duplicidad de contenidos...',
|
||||||
|
'Ajustando tiempos teóricos y prácticos...',
|
||||||
|
'Verificando normatividad...',
|
||||||
|
'Compilando sugerencias...',
|
||||||
|
'Aplicando formato final...',
|
||||||
|
'Casi listo, gracias por tu paciencia...',
|
||||||
|
]
|
||||||
|
|
||||||
|
interface AIProgressLoaderProps {
|
||||||
|
isLoading: boolean
|
||||||
|
cantidadDeSugerencias: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AIProgressLoader: React.FC<AIProgressLoaderProps> = ({
|
||||||
|
isLoading,
|
||||||
|
cantidadDeSugerencias,
|
||||||
|
}) => {
|
||||||
|
const [progress, setProgress] = useState(0)
|
||||||
|
const [currentMessageIndex, setCurrentMessageIndex] = useState(0)
|
||||||
|
|
||||||
|
// 1. Seleccionar el grupo de mensajes según la cantidad
|
||||||
|
const messages = useMemo(() => {
|
||||||
|
if (cantidadDeSugerencias <= 5) return MENSAJES_CORTOS
|
||||||
|
if (cantidadDeSugerencias <= 10) return MENSAJES_MEDIOS
|
||||||
|
return MENSAJES_LARGOS
|
||||||
|
}, [cantidadDeSugerencias])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading) {
|
||||||
|
setProgress(0)
|
||||||
|
setCurrentMessageIndex(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CÁLCULO DEL TIEMPO TOTAL ---
|
||||||
|
// y = 4.07x + 10.93 (en segundos)
|
||||||
|
const estimatedSeconds = 4.07 * cantidadDeSugerencias + 10.93
|
||||||
|
const durationMs = estimatedSeconds * 1000
|
||||||
|
|
||||||
|
// Intervalo de actualización de la barra (cada 50ms para suavidad)
|
||||||
|
const updateInterval = 50
|
||||||
|
const totalSteps = durationMs / updateInterval
|
||||||
|
const incrementPerStep = 99 / totalSteps // Llegamos al 99% para esperar la respuesta real
|
||||||
|
|
||||||
|
// --- TIMER 1: BARRA DE PROGRESO ---
|
||||||
|
const progressTimer = setInterval(() => {
|
||||||
|
setProgress((prev) => {
|
||||||
|
const next = prev + incrementPerStep
|
||||||
|
return next >= 99 ? 99 : next // Topar en 99%
|
||||||
|
})
|
||||||
|
}, updateInterval)
|
||||||
|
|
||||||
|
// --- TIMER 2: MENSAJES (CADA 5 SEGUNDOS) ---
|
||||||
|
const messagesTimer = setInterval(() => {
|
||||||
|
setCurrentMessageIndex((prev) => {
|
||||||
|
// Si ya es el último mensaje, no avanzar más (no ciclar)
|
||||||
|
if (prev >= messages.length - 1) return prev
|
||||||
|
return prev + 1
|
||||||
|
})
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
// Cleanup al desmontar o cuando isLoading cambie
|
||||||
|
return () => {
|
||||||
|
clearInterval(progressTimer)
|
||||||
|
clearInterval(messagesTimer)
|
||||||
|
}
|
||||||
|
}, [isLoading, cantidadDeSugerencias, messages])
|
||||||
|
|
||||||
|
if (!isLoading) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="animate-in fade-in zoom-in m-2 mx-auto w-full max-w-md duration-300">
|
||||||
|
{/* Contenedor de la barra */}
|
||||||
|
<div className="relative pt-1">
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="inline-block rounded-full bg-blue-200 px-2 py-1 text-xs font-semibold text-blue-600 uppercase">
|
||||||
|
Generando IA
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="inline-block text-xs font-semibold text-blue-600">
|
||||||
|
{Math.floor(progress)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Barra de fondo */}
|
||||||
|
<div className="mb-4 flex h-2 overflow-hidden rounded bg-blue-100 text-xs">
|
||||||
|
{/* Barra de progreso dinámica */}
|
||||||
|
<div
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
className="flex flex-col justify-center bg-blue-500 text-center whitespace-nowrap text-white shadow-none transition-all duration-75 ease-linear"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mensajes cambiantes */}
|
||||||
|
<div className="h-6 text-center">
|
||||||
|
{' '}
|
||||||
|
{/* Altura fija para evitar saltos */}
|
||||||
|
<p className="text-sm text-slate-500 italic transition-opacity duration-500">
|
||||||
|
{messages[currentMessageIndex]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nota de tiempo estimado (Opcional, transparencia operacional) */}
|
||||||
|
<p className="mt-2 text-center text-[10px] text-slate-400">
|
||||||
|
Tiempo estimado: ~{Math.ceil(4.07 * cantidadDeSugerencias + 10.93)}{' '}
|
||||||
|
segs
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@ import * as Icons from 'lucide-react'
|
|||||||
|
|
||||||
import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
|
import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
|
||||||
|
|
||||||
import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm'
|
import { PasoBasicosClonadoInterno } from '@/components/asignaturas/wizard/PasoBasicosClonadoInterno.tsx'
|
||||||
|
import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm/PasoBasicosForm'
|
||||||
import { PasoDetallesPanel } from '@/components/asignaturas/wizard/PasoDetallesPanel'
|
import { PasoDetallesPanel } from '@/components/asignaturas/wizard/PasoDetallesPanel'
|
||||||
|
import { PasoFuenteClonadoInterno } from '@/components/asignaturas/wizard/PasoFuenteClonadoInterno.tsx'
|
||||||
import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
|
import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
|
||||||
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
|
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
|
||||||
import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
|
import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
|
||||||
@@ -55,10 +57,21 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
|||||||
canContinueDesdeMetodo,
|
canContinueDesdeMetodo,
|
||||||
canContinueDesdeBasicos,
|
canContinueDesdeBasicos,
|
||||||
canContinueDesdeDetalles,
|
canContinueDesdeDetalles,
|
||||||
simularGeneracionIA,
|
|
||||||
crearAsignatura,
|
|
||||||
} = useNuevaAsignaturaWizard(planId)
|
} = useNuevaAsignaturaWizard(planId)
|
||||||
|
|
||||||
|
const titleOverrides =
|
||||||
|
wizard.tipoOrigen === 'IA_MULTIPLE'
|
||||||
|
? {
|
||||||
|
basicos: 'Sugerencias',
|
||||||
|
detalles: 'Estructura',
|
||||||
|
}
|
||||||
|
: wizard.tipoOrigen === 'CLONADO_INTERNO'
|
||||||
|
? {
|
||||||
|
basicos: 'Fuente',
|
||||||
|
detalles: 'Datos básicos',
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
|
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
|
||||||
}
|
}
|
||||||
@@ -93,13 +106,32 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
|||||||
>
|
>
|
||||||
{({ methods }) => {
|
{({ methods }) => {
|
||||||
const idx = Wizard.utils.getIndex(methods.current.id)
|
const idx = Wizard.utils.getIndex(methods.current.id)
|
||||||
|
const stepId = methods.current.id
|
||||||
|
|
||||||
|
const disableNext =
|
||||||
|
wizard.isLoading ||
|
||||||
|
(stepId === 'metodo'
|
||||||
|
? !canContinueDesdeMetodo
|
||||||
|
: stepId === 'basicos'
|
||||||
|
? wizard.tipoOrigen === 'CLONADO_INTERNO'
|
||||||
|
? !canContinueDesdeDetalles
|
||||||
|
: !canContinueDesdeBasicos
|
||||||
|
: stepId === 'detalles'
|
||||||
|
? wizard.tipoOrigen === 'CLONADO_INTERNO'
|
||||||
|
? !canContinueDesdeBasicos
|
||||||
|
: !canContinueDesdeDetalles
|
||||||
|
: false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WizardLayout
|
<WizardLayout
|
||||||
title="Nueva Asignatura"
|
title="Nueva Asignatura"
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
headerSlot={
|
headerSlot={
|
||||||
<WizardResponsiveHeader wizard={Wizard} methods={methods} />
|
<WizardResponsiveHeader
|
||||||
|
wizard={Wizard}
|
||||||
|
methods={methods}
|
||||||
|
titleOverrides={titleOverrides}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
footerSlot={
|
footerSlot={
|
||||||
<Wizard.Stepper.Controls>
|
<Wizard.Stepper.Controls>
|
||||||
@@ -108,12 +140,7 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
|||||||
onPrev={() => methods.prev()}
|
onPrev={() => methods.prev()}
|
||||||
onNext={() => methods.next()}
|
onNext={() => methods.next()}
|
||||||
disablePrev={idx === 0 || wizard.isLoading}
|
disablePrev={idx === 0 || wizard.isLoading}
|
||||||
disableNext={
|
disableNext={disableNext}
|
||||||
wizard.isLoading ||
|
|
||||||
(idx === 0 && !canContinueDesdeMetodo) ||
|
|
||||||
(idx === 1 && !canContinueDesdeBasicos) ||
|
|
||||||
(idx === 2 && !canContinueDesdeDetalles)
|
|
||||||
}
|
|
||||||
disableCreate={wizard.isLoading}
|
disableCreate={wizard.isLoading}
|
||||||
isLastStep={idx >= Wizard.steps.length - 1}
|
isLastStep={idx >= Wizard.steps.length - 1}
|
||||||
wizard={wizard}
|
wizard={wizard}
|
||||||
@@ -131,17 +158,27 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
|||||||
|
|
||||||
{idx === 1 && (
|
{idx === 1 && (
|
||||||
<Wizard.Stepper.Panel>
|
<Wizard.Stepper.Panel>
|
||||||
<PasoBasicosForm wizard={wizard} onChange={setWizard} />
|
{wizard.tipoOrigen === 'CLONADO_INTERNO' ? (
|
||||||
|
<PasoFuenteClonadoInterno
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PasoBasicosForm wizard={wizard} onChange={setWizard} />
|
||||||
|
)}
|
||||||
</Wizard.Stepper.Panel>
|
</Wizard.Stepper.Panel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{idx === 2 && (
|
{idx === 2 && (
|
||||||
<Wizard.Stepper.Panel>
|
<Wizard.Stepper.Panel>
|
||||||
<PasoDetallesPanel
|
{wizard.tipoOrigen === 'CLONADO_INTERNO' ? (
|
||||||
wizard={wizard}
|
<PasoBasicosClonadoInterno
|
||||||
onChange={setWizard}
|
wizard={wizard}
|
||||||
onGenerarIA={simularGeneracionIA}
|
onChange={setWizard}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<PasoDetallesPanel wizard={wizard} onChange={setWizard} />
|
||||||
|
)}
|
||||||
</Wizard.Stepper.Panel>
|
</Wizard.Stepper.Panel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import type { AsignaturaPreview, NewSubjectWizardState } from '../types'
|
import type { NewSubjectWizardState } from '../types'
|
||||||
|
|
||||||
export function useNuevaAsignaturaWizard(planId: string) {
|
export function useNuevaAsignaturaWizard(planId: string) {
|
||||||
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
||||||
step: 1,
|
step: 1,
|
||||||
plan_estudio_id: planId,
|
plan_estudio_id: planId,
|
||||||
|
estructuraId: null,
|
||||||
tipoOrigen: null,
|
tipoOrigen: null,
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
nombre: '',
|
nombre: '',
|
||||||
@@ -16,6 +17,7 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
horasIndependientes: null,
|
horasIndependientes: null,
|
||||||
estructuraId: '',
|
estructuraId: '',
|
||||||
},
|
},
|
||||||
|
sugerencias: [],
|
||||||
clonInterno: {},
|
clonInterno: {},
|
||||||
clonTradicional: {
|
clonTradicional: {
|
||||||
archivoWordAsignaturaId: null,
|
archivoWordAsignaturaId: null,
|
||||||
@@ -28,6 +30,11 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
repositoriosReferencia: [],
|
repositoriosReferencia: [],
|
||||||
archivosAdjuntos: [],
|
archivosAdjuntos: [],
|
||||||
},
|
},
|
||||||
|
iaMultiple: {
|
||||||
|
enfoque: '',
|
||||||
|
cantidadDeSugerencias: 10,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
resumen: {},
|
resumen: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
@@ -35,20 +42,23 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
|
|
||||||
const canContinueDesdeMetodo =
|
const canContinueDesdeMetodo =
|
||||||
wizard.tipoOrigen === 'MANUAL' ||
|
wizard.tipoOrigen === 'MANUAL' ||
|
||||||
wizard.tipoOrigen === 'IA' ||
|
wizard.tipoOrigen === 'IA_SIMPLE' ||
|
||||||
|
wizard.tipoOrigen === 'IA_MULTIPLE' ||
|
||||||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL'
|
wizard.tipoOrigen === 'CLONADO_TRADICIONAL'
|
||||||
|
|
||||||
const canContinueDesdeBasicos =
|
const canContinueDesdeBasicos =
|
||||||
!!wizard.datosBasicos.nombre &&
|
(!!wizard.datosBasicos.nombre &&
|
||||||
wizard.datosBasicos.tipo !== null &&
|
wizard.datosBasicos.tipo !== null &&
|
||||||
wizard.datosBasicos.creditos !== null &&
|
wizard.datosBasicos.creditos !== null &&
|
||||||
wizard.datosBasicos.creditos > 0 &&
|
wizard.datosBasicos.creditos > 0 &&
|
||||||
!!wizard.datosBasicos.estructuraId
|
!!wizard.datosBasicos.estructuraId) ||
|
||||||
|
(wizard.tipoOrigen === 'IA_MULTIPLE' &&
|
||||||
|
wizard.sugerencias.filter((s) => s.selected).length > 0)
|
||||||
|
|
||||||
const canContinueDesdeDetalles = (() => {
|
const canContinueDesdeDetalles = (() => {
|
||||||
if (wizard.tipoOrigen === 'MANUAL') return true
|
if (wizard.tipoOrigen === 'MANUAL') return true
|
||||||
if (wizard.tipoOrigen === 'IA') {
|
if (wizard.tipoOrigen === 'IA_SIMPLE') {
|
||||||
return !!wizard.iaConfig?.descripcionEnfoqueAcademico
|
return !!wizard.iaConfig?.descripcionEnfoqueAcademico
|
||||||
}
|
}
|
||||||
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
||||||
@@ -57,38 +67,17 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
|
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
|
||||||
return !!wizard.clonTradicional?.archivoWordAsignaturaId
|
return !!wizard.clonTradicional?.archivoWordAsignaturaId
|
||||||
}
|
}
|
||||||
|
if (wizard.tipoOrigen === 'IA_MULTIPLE') {
|
||||||
|
return wizard.estructuraId !== null
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const simularGeneracionIA = async () => {
|
|
||||||
setWizard((w) => ({ ...w, isLoading: true }))
|
|
||||||
await new Promise((r) => setTimeout(r, 1500))
|
|
||||||
setWizard((w) => ({
|
|
||||||
...w,
|
|
||||||
isLoading: false,
|
|
||||||
resumen: {
|
|
||||||
previewAsignatura: {
|
|
||||||
nombre: w.datosBasicos.nombre,
|
|
||||||
objetivo:
|
|
||||||
'Aplicar los fundamentos teóricos para la resolución de problemas...',
|
|
||||||
unidades: 5,
|
|
||||||
bibliografiaCount: 3,
|
|
||||||
} as AsignaturaPreview,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const crearAsignatura = async () => {
|
|
||||||
await new Promise((r) => setTimeout(r, 1000))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
wizard,
|
wizard,
|
||||||
setWizard,
|
setWizard,
|
||||||
canContinueDesdeMetodo,
|
canContinueDesdeMetodo,
|
||||||
canContinueDesdeBasicos,
|
canContinueDesdeBasicos,
|
||||||
canContinueDesdeDetalles,
|
canContinueDesdeDetalles,
|
||||||
simularGeneracionIA,
|
|
||||||
crearAsignatura,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/
|
|||||||
import type { Asignatura } from '@/data'
|
import type { Asignatura } from '@/data'
|
||||||
|
|
||||||
export type ModoCreacion = 'MANUAL' | 'IA' | 'CLONADO'
|
export type ModoCreacion = 'MANUAL' | 'IA' | 'CLONADO'
|
||||||
export type SubModoClonado = 'INTERNO' | 'TRADICIONAL'
|
|
||||||
export type TipoAsignatura = 'OBLIGATORIA' | 'OPTATIVA' | 'TRONCAL' | 'OTRO'
|
export type TipoAsignatura = 'OBLIGATORIA' | 'OPTATIVA' | 'TRONCAL' | 'OTRO'
|
||||||
|
|
||||||
export type AsignaturaPreview = {
|
export type AsignaturaPreview = {
|
||||||
@@ -12,10 +11,34 @@ export type AsignaturaPreview = {
|
|||||||
bibliografiaCount: number
|
bibliografiaCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DataAsignaturaSugerida = {
|
||||||
|
nombre: Asignatura['nombre']
|
||||||
|
codigo?: Asignatura['codigo']
|
||||||
|
tipo: Asignatura['tipo'] | null
|
||||||
|
creditos: Asignatura['creditos'] | null
|
||||||
|
horasAcademicas?: number | null
|
||||||
|
horasIndependientes?: number | null
|
||||||
|
descripcion: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AsignaturaSugerida = {
|
||||||
|
id: string
|
||||||
|
selected: boolean
|
||||||
|
source: 'IA' | 'MANUAL' | 'CLON'
|
||||||
|
linea_plan_id: string | null
|
||||||
|
numero_ciclo: number | null
|
||||||
|
} & DataAsignaturaSugerida
|
||||||
|
|
||||||
export type NewSubjectWizardState = {
|
export type NewSubjectWizardState = {
|
||||||
step: 1 | 2 | 3 | 4
|
step: 1 | 2 | 3 | 4
|
||||||
plan_estudio_id: Asignatura['plan_estudio_id']
|
plan_estudio_id: Asignatura['plan_estudio_id']
|
||||||
tipoOrigen: Asignatura['tipo_origen'] | null
|
estructuraId: Asignatura['estructura_id'] | null
|
||||||
|
tipoOrigen:
|
||||||
|
| Asignatura['tipo_origen']
|
||||||
|
| 'CLONADO'
|
||||||
|
| 'IA_SIMPLE'
|
||||||
|
| 'IA_MULTIPLE'
|
||||||
|
| null
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
nombre: Asignatura['nombre']
|
nombre: Asignatura['nombre']
|
||||||
codigo?: Asignatura['codigo']
|
codigo?: Asignatura['codigo']
|
||||||
@@ -25,11 +48,14 @@ export type NewSubjectWizardState = {
|
|||||||
horasIndependientes?: Asignatura['horas_independientes'] | null
|
horasIndependientes?: Asignatura['horas_independientes'] | null
|
||||||
estructuraId: Asignatura['estructura_id'] | null
|
estructuraId: Asignatura['estructura_id'] | null
|
||||||
}
|
}
|
||||||
|
sugerencias: Array<AsignaturaSugerida>
|
||||||
clonInterno?: {
|
clonInterno?: {
|
||||||
facultadId?: string
|
facultadId?: string | null
|
||||||
carreraId?: string
|
carreraId?: string | null
|
||||||
planOrigenId?: string
|
planOrigenId?: string | null
|
||||||
asignaturaOrigenId?: string | null
|
asignaturaOrigenId?: string | null
|
||||||
|
search?: string
|
||||||
|
page?: number
|
||||||
}
|
}
|
||||||
clonTradicional?: {
|
clonTradicional?: {
|
||||||
archivoWordAsignaturaId: string | null
|
archivoWordAsignaturaId: string | null
|
||||||
@@ -42,6 +68,11 @@ export type NewSubjectWizardState = {
|
|||||||
repositoriosReferencia?: Array<string>
|
repositoriosReferencia?: Array<string>
|
||||||
archivosAdjuntos?: Array<UploadedFile>
|
archivosAdjuntos?: Array<UploadedFile>
|
||||||
}
|
}
|
||||||
|
iaMultiple?: {
|
||||||
|
enfoque: string
|
||||||
|
cantidadDeSugerencias: number
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
resumen: {
|
resumen: {
|
||||||
previewAsignatura?: AsignaturaPreview
|
previewAsignatura?: AsignaturaPreview
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
|||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseContenidoTematicoToPlainText(value: unknown): string {
|
||||||
|
if (!Array.isArray(value)) return ''
|
||||||
|
|
||||||
|
const blocks: Array<string> = []
|
||||||
|
|
||||||
|
for (const item of value) {
|
||||||
|
if (!isRecord(item)) continue
|
||||||
|
|
||||||
|
const unidad =
|
||||||
|
typeof item.unidad === 'number' && Number.isFinite(item.unidad)
|
||||||
|
? item.unidad
|
||||||
|
: undefined
|
||||||
|
const titulo = typeof item.titulo === 'string' ? item.titulo : ''
|
||||||
|
|
||||||
|
const header = `${unidad ?? ''}${unidad ? '.' : ''} ${titulo}`.trim()
|
||||||
|
if (!header) continue
|
||||||
|
|
||||||
|
const lines: Array<string> = [header]
|
||||||
|
|
||||||
|
const temas = Array.isArray(item.temas) ? item.temas : []
|
||||||
|
temas.forEach((tema, idx) => {
|
||||||
|
const temaNombre =
|
||||||
|
typeof tema === 'string'
|
||||||
|
? tema
|
||||||
|
: isRecord(tema) && typeof tema.nombre === 'string'
|
||||||
|
? tema.nombre
|
||||||
|
: ''
|
||||||
|
if (!temaNombre) return
|
||||||
|
|
||||||
|
if (unidad != null) {
|
||||||
|
lines.push(`${unidad}.${idx + 1} ${temaNombre}`.trim())
|
||||||
|
} else {
|
||||||
|
lines.push(`${idx + 1}. ${temaNombre}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
blocks.push(lines.join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks.join('\n\n').trimEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCriteriosEvaluacionToPlainText(value: unknown): string {
|
||||||
|
if (!Array.isArray(value)) return ''
|
||||||
|
|
||||||
|
const lines: Array<string> = []
|
||||||
|
for (const item of value) {
|
||||||
|
if (!isRecord(item)) continue
|
||||||
|
const label = typeof item.criterio === 'string' ? item.criterio.trim() : ''
|
||||||
|
const valueNum =
|
||||||
|
typeof item.porcentaje === 'number'
|
||||||
|
? item.porcentaje
|
||||||
|
: typeof item.porcentaje === 'string'
|
||||||
|
? Number(item.porcentaje)
|
||||||
|
: NaN
|
||||||
|
|
||||||
|
if (!label) continue
|
||||||
|
if (!Number.isFinite(valueNum)) continue
|
||||||
|
|
||||||
|
const v = Math.trunc(valueNum)
|
||||||
|
if (v < 1 || v > 100) continue
|
||||||
|
|
||||||
|
lines.push(`${label}: ${v}%`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const columnParsers: Partial<
|
||||||
|
Record<string, (value: unknown) => string>
|
||||||
|
> = {
|
||||||
|
contenido_tematico: parseContenidoTematicoToPlainText,
|
||||||
|
criterios_de_evaluacion: parseCriteriosEvaluacionToPlainText,
|
||||||
|
}
|
||||||
+227
-23
@@ -17,14 +17,22 @@ import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-qu
|
|||||||
import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo'
|
import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo'
|
||||||
import { Route as PlanesPlanIdDetalleRouteImport } from './routes/planes/$planId/_detalle'
|
import { Route as PlanesPlanIdDetalleRouteImport } from './routes/planes/$planId/_detalle'
|
||||||
import { Route as PlanesPlanIdDetalleIndexRouteImport } from './routes/planes/$planId/_detalle/index'
|
import { Route as PlanesPlanIdDetalleIndexRouteImport } from './routes/planes/$planId/_detalle/index'
|
||||||
import { Route as PlanesPlanIdAsignaturasAsignaturaIdRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId'
|
|
||||||
import { Route as PlanesPlanIdDetalleMapaRouteImport } from './routes/planes/$planId/_detalle/mapa'
|
import { Route as PlanesPlanIdDetalleMapaRouteImport } from './routes/planes/$planId/_detalle/mapa'
|
||||||
import { Route as PlanesPlanIdDetalleIaplanRouteImport } from './routes/planes/$planId/_detalle/iaplan'
|
import { Route as PlanesPlanIdDetalleIaplanRouteImport } from './routes/planes/$planId/_detalle/iaplan'
|
||||||
import { Route as PlanesPlanIdDetalleHistorialRouteImport } from './routes/planes/$planId/_detalle/historial'
|
import { Route as PlanesPlanIdDetalleHistorialRouteImport } from './routes/planes/$planId/_detalle/historial'
|
||||||
import { Route as PlanesPlanIdDetalleFlujoRouteImport } from './routes/planes/$planId/_detalle/flujo'
|
import { Route as PlanesPlanIdDetalleFlujoRouteImport } from './routes/planes/$planId/_detalle/flujo'
|
||||||
import { Route as PlanesPlanIdDetalleDocumentoRouteImport } from './routes/planes/$planId/_detalle/documento'
|
import { Route as PlanesPlanIdDetalleDocumentoRouteImport } from './routes/planes/$planId/_detalle/documento'
|
||||||
import { Route as PlanesPlanIdDetalleAsignaturasRouteImport } from './routes/planes/$planId/_detalle/asignaturas'
|
import { Route as PlanesPlanIdDetalleAsignaturasRouteImport } from './routes/planes/$planId/_detalle/asignaturas'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/route'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasAsignaturaIdIndexRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/index'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/iaasignatura'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasAsignaturaIdHistorialRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/historial'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasAsignaturaIdDocumentoRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/documento'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasAsignaturaIdContenidoRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/contenido'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/bibliografia'
|
||||||
import { Route as PlanesPlanIdDetalleAsignaturasNuevaRouteImport } from './routes/planes/$planId/_detalle/asignaturas/nueva'
|
import { Route as PlanesPlanIdDetalleAsignaturasNuevaRouteImport } from './routes/planes/$planId/_detalle/asignaturas/nueva'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/bibliografia/index'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva'
|
||||||
|
|
||||||
const LoginRoute = LoginRouteImport.update({
|
const LoginRoute = LoginRouteImport.update({
|
||||||
id: '/login',
|
id: '/login',
|
||||||
@@ -67,12 +75,6 @@ const PlanesPlanIdDetalleIndexRoute =
|
|||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => PlanesPlanIdDetalleRoute,
|
getParentRoute: () => PlanesPlanIdDetalleRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const PlanesPlanIdAsignaturasAsignaturaIdRoute =
|
|
||||||
PlanesPlanIdAsignaturasAsignaturaIdRouteImport.update({
|
|
||||||
id: '/planes/$planId/asignaturas/$asignaturaId',
|
|
||||||
path: '/planes/$planId/asignaturas/$asignaturaId',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const PlanesPlanIdDetalleMapaRoute = PlanesPlanIdDetalleMapaRouteImport.update({
|
const PlanesPlanIdDetalleMapaRoute = PlanesPlanIdDetalleMapaRouteImport.update({
|
||||||
id: '/mapa',
|
id: '/mapa',
|
||||||
path: '/mapa',
|
path: '/mapa',
|
||||||
@@ -108,12 +110,66 @@ const PlanesPlanIdDetalleAsignaturasRoute =
|
|||||||
path: '/asignaturas',
|
path: '/asignaturas',
|
||||||
getParentRoute: () => PlanesPlanIdDetalleRoute,
|
getParentRoute: () => PlanesPlanIdDetalleRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdRouteRoute =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport.update({
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
|
path: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdIndexRoute =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdIndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRouteImport.update({
|
||||||
|
id: '/iaasignatura',
|
||||||
|
path: '/iaasignatura',
|
||||||
|
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdHistorialRouteImport.update({
|
||||||
|
id: '/historial',
|
||||||
|
path: '/historial',
|
||||||
|
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdDocumentoRouteImport.update({
|
||||||
|
id: '/documento',
|
||||||
|
path: '/documento',
|
||||||
|
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdContenidoRouteImport.update({
|
||||||
|
id: '/contenido',
|
||||||
|
path: '/contenido',
|
||||||
|
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteImport.update({
|
||||||
|
id: '/bibliografia',
|
||||||
|
path: '/bibliografia',
|
||||||
|
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
|
||||||
|
} as any)
|
||||||
const PlanesPlanIdDetalleAsignaturasNuevaRoute =
|
const PlanesPlanIdDetalleAsignaturasNuevaRoute =
|
||||||
PlanesPlanIdDetalleAsignaturasNuevaRouteImport.update({
|
PlanesPlanIdDetalleAsignaturasNuevaRouteImport.update({
|
||||||
id: '/nueva',
|
id: '/nueva',
|
||||||
path: '/nueva',
|
path: '/nueva',
|
||||||
getParentRoute: () => PlanesPlanIdDetalleAsignaturasRoute,
|
getParentRoute: () => PlanesPlanIdDetalleAsignaturasRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRouteImport.update({
|
||||||
|
id: '/nueva',
|
||||||
|
path: '/nueva',
|
||||||
|
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
@@ -123,15 +179,23 @@ export interface FileRoutesByFullPath {
|
|||||||
'/planes': typeof PlanesListaRouteWithChildren
|
'/planes': typeof PlanesListaRouteWithChildren
|
||||||
'/planes/$planId': typeof PlanesPlanIdDetalleRouteWithChildren
|
'/planes/$planId': typeof PlanesPlanIdDetalleRouteWithChildren
|
||||||
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRouteWithChildren
|
||||||
'/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
|
'/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
|
||||||
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
||||||
'/planes/$planId/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
'/planes/$planId/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
||||||
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
||||||
'/planes/$planId/iaplan': typeof PlanesPlanIdDetalleIaplanRoute
|
'/planes/$planId/iaplan': typeof PlanesPlanIdDetalleIaplanRoute
|
||||||
'/planes/$planId/mapa': typeof PlanesPlanIdDetalleMapaRoute
|
'/planes/$planId/mapa': typeof PlanesPlanIdDetalleMapaRoute
|
||||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
|
||||||
'/planes/$planId/': typeof PlanesPlanIdDetalleIndexRoute
|
'/planes/$planId/': typeof PlanesPlanIdDetalleIndexRoute
|
||||||
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteWithChildren
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/contenido': typeof PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/documento': typeof PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/historial': typeof PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/iaasignatura': typeof PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/': typeof PlanesPlanIdAsignaturasAsignaturaIdIndexRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
@@ -146,9 +210,15 @@ export interface FileRoutesByTo {
|
|||||||
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
||||||
'/planes/$planId/iaplan': typeof PlanesPlanIdDetalleIaplanRoute
|
'/planes/$planId/iaplan': typeof PlanesPlanIdDetalleIaplanRoute
|
||||||
'/planes/$planId/mapa': typeof PlanesPlanIdDetalleMapaRoute
|
'/planes/$planId/mapa': typeof PlanesPlanIdDetalleMapaRoute
|
||||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
|
||||||
'/planes/$planId': typeof PlanesPlanIdDetalleIndexRoute
|
'/planes/$planId': typeof PlanesPlanIdDetalleIndexRoute
|
||||||
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/contenido': typeof PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/documento': typeof PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/historial': typeof PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/iaasignatura': typeof PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdIndexRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
@@ -159,15 +229,23 @@ export interface FileRoutesById {
|
|||||||
'/planes/_lista': typeof PlanesListaRouteWithChildren
|
'/planes/_lista': typeof PlanesListaRouteWithChildren
|
||||||
'/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteWithChildren
|
'/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteWithChildren
|
||||||
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRouteWithChildren
|
||||||
'/planes/$planId/_detalle/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
|
'/planes/$planId/_detalle/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
|
||||||
'/planes/$planId/_detalle/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
'/planes/$planId/_detalle/documento': typeof PlanesPlanIdDetalleDocumentoRoute
|
||||||
'/planes/$planId/_detalle/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
'/planes/$planId/_detalle/flujo': typeof PlanesPlanIdDetalleFlujoRoute
|
||||||
'/planes/$planId/_detalle/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
'/planes/$planId/_detalle/historial': typeof PlanesPlanIdDetalleHistorialRoute
|
||||||
'/planes/$planId/_detalle/iaplan': typeof PlanesPlanIdDetalleIaplanRoute
|
'/planes/$planId/_detalle/iaplan': typeof PlanesPlanIdDetalleIaplanRoute
|
||||||
'/planes/$planId/_detalle/mapa': typeof PlanesPlanIdDetalleMapaRoute
|
'/planes/$planId/_detalle/mapa': typeof PlanesPlanIdDetalleMapaRoute
|
||||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
|
||||||
'/planes/$planId/_detalle/': typeof PlanesPlanIdDetalleIndexRoute
|
'/planes/$planId/_detalle/': typeof PlanesPlanIdDetalleIndexRoute
|
||||||
'/planes/$planId/_detalle/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
'/planes/$planId/_detalle/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteWithChildren
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/contenido': typeof PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/documento': typeof PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/historial': typeof PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/iaasignatura': typeof PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/': typeof PlanesPlanIdAsignaturasAsignaturaIdIndexRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
@@ -179,15 +257,23 @@ export interface FileRouteTypes {
|
|||||||
| '/planes'
|
| '/planes'
|
||||||
| '/planes/$planId'
|
| '/planes/$planId'
|
||||||
| '/planes/nuevo'
|
| '/planes/nuevo'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
| '/planes/$planId/asignaturas'
|
| '/planes/$planId/asignaturas'
|
||||||
| '/planes/$planId/documento'
|
| '/planes/$planId/documento'
|
||||||
| '/planes/$planId/flujo'
|
| '/planes/$planId/flujo'
|
||||||
| '/planes/$planId/historial'
|
| '/planes/$planId/historial'
|
||||||
| '/planes/$planId/iaplan'
|
| '/planes/$planId/iaplan'
|
||||||
| '/planes/$planId/mapa'
|
| '/planes/$planId/mapa'
|
||||||
| '/planes/$planId/asignaturas/$asignaturaId'
|
|
||||||
| '/planes/$planId/'
|
| '/planes/$planId/'
|
||||||
| '/planes/$planId/asignaturas/nueva'
|
| '/planes/$planId/asignaturas/nueva'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/contenido'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/documento'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/historial'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/iaasignatura'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia/'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
@@ -202,9 +288,15 @@ export interface FileRouteTypes {
|
|||||||
| '/planes/$planId/historial'
|
| '/planes/$planId/historial'
|
||||||
| '/planes/$planId/iaplan'
|
| '/planes/$planId/iaplan'
|
||||||
| '/planes/$planId/mapa'
|
| '/planes/$planId/mapa'
|
||||||
| '/planes/$planId/asignaturas/$asignaturaId'
|
|
||||||
| '/planes/$planId'
|
| '/planes/$planId'
|
||||||
| '/planes/$planId/asignaturas/nueva'
|
| '/planes/$planId/asignaturas/nueva'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/contenido'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/documento'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/historial'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/iaasignatura'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
@@ -214,15 +306,23 @@ export interface FileRouteTypes {
|
|||||||
| '/planes/_lista'
|
| '/planes/_lista'
|
||||||
| '/planes/$planId/_detalle'
|
| '/planes/$planId/_detalle'
|
||||||
| '/planes/_lista/nuevo'
|
| '/planes/_lista/nuevo'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
| '/planes/$planId/_detalle/asignaturas'
|
| '/planes/$planId/_detalle/asignaturas'
|
||||||
| '/planes/$planId/_detalle/documento'
|
| '/planes/$planId/_detalle/documento'
|
||||||
| '/planes/$planId/_detalle/flujo'
|
| '/planes/$planId/_detalle/flujo'
|
||||||
| '/planes/$planId/_detalle/historial'
|
| '/planes/$planId/_detalle/historial'
|
||||||
| '/planes/$planId/_detalle/iaplan'
|
| '/planes/$planId/_detalle/iaplan'
|
||||||
| '/planes/$planId/_detalle/mapa'
|
| '/planes/$planId/_detalle/mapa'
|
||||||
| '/planes/$planId/asignaturas/$asignaturaId'
|
|
||||||
| '/planes/$planId/_detalle/'
|
| '/planes/$planId/_detalle/'
|
||||||
| '/planes/$planId/_detalle/asignaturas/nueva'
|
| '/planes/$planId/_detalle/asignaturas/nueva'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/contenido'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/documento'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/historial'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/iaasignatura'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
@@ -232,7 +332,7 @@ export interface RootRouteChildren {
|
|||||||
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
||||||
PlanesListaRoute: typeof PlanesListaRouteWithChildren
|
PlanesListaRoute: typeof PlanesListaRouteWithChildren
|
||||||
PlanesPlanIdDetalleRoute: typeof PlanesPlanIdDetalleRouteWithChildren
|
PlanesPlanIdDetalleRoute: typeof PlanesPlanIdDetalleRouteWithChildren
|
||||||
PlanesPlanIdAsignaturasAsignaturaIdRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRouteWithChildren
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -293,13 +393,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof PlanesPlanIdDetalleIndexRouteImport
|
preLoaderRoute: typeof PlanesPlanIdDetalleIndexRouteImport
|
||||||
parentRoute: typeof PlanesPlanIdDetalleRoute
|
parentRoute: typeof PlanesPlanIdDetalleRoute
|
||||||
}
|
}
|
||||||
'/planes/$planId/asignaturas/$asignaturaId': {
|
|
||||||
id: '/planes/$planId/asignaturas/$asignaturaId'
|
|
||||||
path: '/planes/$planId/asignaturas/$asignaturaId'
|
|
||||||
fullPath: '/planes/$planId/asignaturas/$asignaturaId'
|
|
||||||
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/planes/$planId/_detalle/mapa': {
|
'/planes/$planId/_detalle/mapa': {
|
||||||
id: '/planes/$planId/_detalle/mapa'
|
id: '/planes/$planId/_detalle/mapa'
|
||||||
path: '/mapa'
|
path: '/mapa'
|
||||||
@@ -342,6 +435,55 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasRouteImport
|
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasRouteImport
|
||||||
parentRoute: typeof PlanesPlanIdDetalleRoute
|
parentRoute: typeof PlanesPlanIdDetalleRoute
|
||||||
}
|
}
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId': {
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
path: '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
fullPath: '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/': {
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/planes/$planId/asignaturas/$asignaturaId/'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdIndexRouteImport
|
||||||
|
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
}
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/iaasignatura': {
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId/iaasignatura'
|
||||||
|
path: '/iaasignatura'
|
||||||
|
fullPath: '/planes/$planId/asignaturas/$asignaturaId/iaasignatura'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRouteImport
|
||||||
|
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
}
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/historial': {
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId/historial'
|
||||||
|
path: '/historial'
|
||||||
|
fullPath: '/planes/$planId/asignaturas/$asignaturaId/historial'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdHistorialRouteImport
|
||||||
|
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
}
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/documento': {
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId/documento'
|
||||||
|
path: '/documento'
|
||||||
|
fullPath: '/planes/$planId/asignaturas/$asignaturaId/documento'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdDocumentoRouteImport
|
||||||
|
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
}
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/contenido': {
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId/contenido'
|
||||||
|
path: '/contenido'
|
||||||
|
fullPath: '/planes/$planId/asignaturas/$asignaturaId/contenido'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdContenidoRouteImport
|
||||||
|
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
}
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia': {
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId/bibliografia'
|
||||||
|
path: '/bibliografia'
|
||||||
|
fullPath: '/planes/$planId/asignaturas/$asignaturaId/bibliografia'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteImport
|
||||||
|
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
}
|
||||||
'/planes/$planId/_detalle/asignaturas/nueva': {
|
'/planes/$planId/_detalle/asignaturas/nueva': {
|
||||||
id: '/planes/$planId/_detalle/asignaturas/nueva'
|
id: '/planes/$planId/_detalle/asignaturas/nueva'
|
||||||
path: '/nueva'
|
path: '/nueva'
|
||||||
@@ -349,6 +491,20 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasNuevaRouteImport
|
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasNuevaRouteImport
|
||||||
parentRoute: typeof PlanesPlanIdDetalleAsignaturasRoute
|
parentRoute: typeof PlanesPlanIdDetalleAsignaturasRoute
|
||||||
}
|
}
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/': {
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId/bibliografia/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/planes/$planId/asignaturas/$asignaturaId/bibliografia/'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRouteImport
|
||||||
|
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute
|
||||||
|
}
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva': {
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva'
|
||||||
|
path: '/nueva'
|
||||||
|
fullPath: '/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRouteImport
|
||||||
|
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,6 +559,54 @@ const PlanesPlanIdDetalleRouteChildren: PlanesPlanIdDetalleRouteChildren = {
|
|||||||
const PlanesPlanIdDetalleRouteWithChildren =
|
const PlanesPlanIdDetalleRouteWithChildren =
|
||||||
PlanesPlanIdDetalleRoute._addFileChildren(PlanesPlanIdDetalleRouteChildren)
|
PlanesPlanIdDetalleRoute._addFileChildren(PlanesPlanIdDetalleRouteChildren)
|
||||||
|
|
||||||
|
interface PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteChildren {
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteChildren: PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteChildren =
|
||||||
|
{
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute:
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute,
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute:
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteWithChildren =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute._addFileChildren(
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface PlanesPlanIdAsignaturasAsignaturaIdRouteRouteChildren {
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteWithChildren
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdIndexRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdIndexRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdRouteRouteChildren: PlanesPlanIdAsignaturasAsignaturaIdRouteRouteChildren =
|
||||||
|
{
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute:
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteWithChildren,
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute:
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute,
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute:
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute,
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute:
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute,
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute:
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute,
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdIndexRoute:
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdIndexRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdRouteRouteWithChildren =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute._addFileChildren(
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
DashboardRoute: DashboardRoute,
|
DashboardRoute: DashboardRoute,
|
||||||
@@ -410,8 +614,8 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
||||||
PlanesListaRoute: PlanesListaRouteWithChildren,
|
PlanesListaRoute: PlanesListaRouteWithChildren,
|
||||||
PlanesPlanIdDetalleRoute: PlanesPlanIdDetalleRouteWithChildren,
|
PlanesPlanIdDetalleRoute: PlanesPlanIdDetalleRouteWithChildren,
|
||||||
PlanesPlanIdAsignaturasAsignaturaIdRoute:
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute:
|
||||||
PlanesPlanIdAsignaturasAsignaturaIdRoute,
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRouteWithChildren,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function RouteComponent() {
|
|||||||
4. px-4 md:px-6: Padding RESPONSIVO interno (seguro para móviles y desktop).
|
4. px-4 md:px-6: Padding RESPONSIVO interno (seguro para móviles y desktop).
|
||||||
5. py-6: Padding vertical (opcional, para separarse del header).
|
5. py-6: Padding vertical (opcional, para separarse del header).
|
||||||
*/}
|
*/}
|
||||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-6 lg:px-8">
|
<div className="mx-auto flex w-full flex-col gap-4 p-4 md:px-6 md:pb-6 lg:px-8 lg:pb-8">
|
||||||
<DashboardHeader
|
<DashboardHeader
|
||||||
nombre="Dr. Carlos Mendoza"
|
nombre="Dr. Carlos Mendoza"
|
||||||
rol="Jefe de Carrera"
|
rol="Jefe de Carrera"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const Route = createFileRoute('/login')({
|
|||||||
|
|
||||||
function LoginPage() {
|
function LoginPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-[#7b0f1d] via-[#6b0d1a] to-[#3a050a]">
|
<div className="login-bg flex min-h-screen items-center justify-center bg-cover bg-center bg-no-repeat">
|
||||||
<LoginCard />
|
<LoginCard />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,18 +8,24 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useEffect, forwardRef } from 'react'
|
import { useState, useEffect, forwardRef } from 'react'
|
||||||
|
|
||||||
|
import type { Database } from '@/types/supabase'
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu'
|
|
||||||
import { NotFoundPage } from '@/components/ui/NotFoundPage'
|
import { NotFoundPage } from '@/components/ui/NotFoundPage'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { plans_get } from '@/data/api/plans.api'
|
import { plans_get } from '@/data/api/plans.api'
|
||||||
import { usePlan, useUpdatePlanFields } from '@/data/hooks/usePlans'
|
import { usePlan, useUpdatePlanFields } from '@/data/hooks/usePlans'
|
||||||
import { qk } from '@/data/query/keys'
|
import { qk } from '@/data/query/keys'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
type NivelPlanEstudio = Database['public']['Enums']['nivel_plan_estudio']
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
||||||
loader: async ({ context: { queryClient }, params: { planId } }) => {
|
loader: async ({ context: { queryClient }, params: { planId } }) => {
|
||||||
@@ -54,28 +60,26 @@ function RouteComponent() {
|
|||||||
|
|
||||||
// Estados locales para manejar la edición "en vivo" antes de persistir
|
// Estados locales para manejar la edición "en vivo" antes de persistir
|
||||||
const [nombrePlan, setNombrePlan] = useState('')
|
const [nombrePlan, setNombrePlan] = useState('')
|
||||||
const [nivelPlan, setNivelPlan] = useState('')
|
const [nivelPlan, setNivelPlan] = useState<NivelPlanEstudio | undefined>(
|
||||||
const [isDirty, setIsDirty] = useState(false)
|
undefined,
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
setNombrePlan(data.nombre || '')
|
setNombrePlan(data.nombre || '')
|
||||||
setNivelPlan(data.nivel || '')
|
setNivelPlan(data.nivel)
|
||||||
}
|
}
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
const niveles = [
|
const niveles: Array<NivelPlanEstudio> = [
|
||||||
'Licenciatura',
|
'Licenciatura',
|
||||||
'Maestría',
|
'Maestría',
|
||||||
'Doctorado',
|
'Doctorado',
|
||||||
'Diplomado',
|
|
||||||
'Especialidad',
|
'Especialidad',
|
||||||
|
'Diplomado',
|
||||||
|
'Otro',
|
||||||
]
|
]
|
||||||
|
|
||||||
const persistChange = (patch: any) => {
|
|
||||||
mutate({ planId, patch })
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_CHARACTERS = 200
|
const MAX_CHARACTERS = 200
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
||||||
@@ -114,20 +118,20 @@ function RouteComponent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="bg-background min-h-screen">
|
||||||
{/* 1. Header Superior */}
|
{/* 1. Header Superior */}
|
||||||
<div className="sticky top-0 z-20 border-b bg-white/50 shadow-sm backdrop-blur-sm">
|
<div className="bg-background/80 sticky top-0 z-20 border-b shadow-sm backdrop-blur-sm">
|
||||||
<div className="px-6 py-2">
|
<div className="px-6 py-2">
|
||||||
<Link
|
<Link
|
||||||
to="/planes"
|
to="/planes"
|
||||||
className="flex w-fit items-center gap-1 text-xs text-gray-500 transition-colors hover:text-gray-800"
|
className="text-muted-foreground hover:text-foreground flex w-fit items-center gap-1 text-xs transition-colors"
|
||||||
>
|
>
|
||||||
<ChevronLeft size={14} /> Volver a planes
|
<ChevronLeft size={14} /> Volver a planes
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto max-w-400 space-y-8 p-8">
|
<div className="mx-auto space-y-8 p-4 md:px-6 md:pb-6 lg:px-8 lg:pb-8">
|
||||||
{/* 2. Header del Plan */}
|
{/* 2. Header del Plan */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
/* ===== SKELETON ===== */
|
/* ===== SKELETON ===== */
|
||||||
@@ -139,7 +143,7 @@ function RouteComponent() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="flex flex-wrap items-baseline gap-2 text-3xl leading-tight font-bold tracking-tight text-slate-900">
|
<h1 className="text-foreground flex flex-wrap items-baseline gap-2 text-3xl leading-tight font-bold tracking-tight">
|
||||||
{/* El prefijo "Nivel en" lo mantenemos simple */}
|
{/* El prefijo "Nivel en" lo mantenemos simple */}
|
||||||
<span className="shrink-0">{nivelPlan} en</span>
|
<span className="shrink-0">{nivelPlan} en</span>
|
||||||
<span
|
<span
|
||||||
@@ -148,80 +152,105 @@ function RouteComponent() {
|
|||||||
contentEditable
|
contentEditable
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
aria-label="Nombre del plan"
|
||||||
|
title="Nombre del plan"
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste} // Añadido para controlar lo que pegan
|
onPaste={handlePaste}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
const nuevoNombre =
|
const nuevoNombre = e.currentTarget.textContent.trim()
|
||||||
e.currentTarget.textContent?.trim() || ''
|
|
||||||
setNombrePlan(nuevoNombre)
|
setNombrePlan(nuevoNombre)
|
||||||
if (nuevoNombre !== data?.nombre) {
|
if (nuevoNombre !== data?.nombre) {
|
||||||
mutate({ planId, patch: { nombre: nuevoNombre } })
|
mutate({ planId, patch: { nombre: nuevoNombre } })
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
// Clases añadidas: break-words y whitespace-pre-wrap para el wrap
|
className="hover:border-input focus:border-primary block w-full cursor-text border-b border-transparent wrap-break-word whitespace-pre-wrap no-underline transition-colors outline-none select-text sm:inline-block sm:w-auto"
|
||||||
className="block w-full cursor-text border-b border-transparent break-words whitespace-pre-wrap transition-colors outline-none select-text hover:border-slate-300 focus:border-teal-500 sm:inline-block sm:w-auto"
|
|
||||||
style={{ textDecoration: 'none' }}
|
|
||||||
>
|
>
|
||||||
{nombrePlan}
|
{nombrePlan}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-lg font-medium text-slate-500">
|
<p className="text-muted-foreground mt-1 text-lg font-medium">
|
||||||
{data?.carreras?.facultades?.nombre}{' '}
|
{data?.carreras?.facultades?.nombre}{' '}
|
||||||
{data?.carreras?.nombre_corto}
|
{data?.carreras?.nombre_corto}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
{(() => {
|
||||||
<Badge className="gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100">
|
const estadoColorHex = (data?.estados_plan as any)?.color as
|
||||||
{data?.estados_plan?.etiqueta}
|
| string
|
||||||
</Badge>
|
| undefined
|
||||||
</div>
|
const badgeStyle = estadoColorHex
|
||||||
|
? ({
|
||||||
|
backgroundColor: estadoColorHex,
|
||||||
|
borderColor: estadoColorHex,
|
||||||
|
} as const)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
style={badgeStyle}
|
||||||
|
className={cn(
|
||||||
|
'text-sm font-semibold',
|
||||||
|
!estadoColorHex &&
|
||||||
|
'border-primary/20 bg-primary/10 text-primary hover:bg-primary/20',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-white [text-shadow:1px_1px_0_#000,-1px_-1px_0_#000,1px_-1px_0_#000,-1px_1px_0_#000,0_1px_0_#000,0_-1px_0_#000,1px_0_0_#000,-1px_0_0_#000]">
|
||||||
|
{data?.estados_plan?.etiqueta}
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 3. Cards de Información */}
|
{/* 3. Cards de Información */}
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
||||||
<DropdownMenu>
|
<div className="border-border/60 bg-muted/30 flex h-18 w-full items-center gap-4 rounded-xl border p-4 shadow-sm transition-all">
|
||||||
<DropdownMenuTrigger asChild>
|
<div className="bg-background flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border shadow-sm">
|
||||||
<InfoCard
|
<GraduationCap className="text-muted-foreground" />
|
||||||
icon={<GraduationCap className="text-slate-400" />}
|
</div>
|
||||||
label="Nivel"
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-muted-foreground mb-0.5 truncate text-[10px] font-bold tracking-wider uppercase">
|
||||||
|
Nivel
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
value={nivelPlan}
|
value={nivelPlan}
|
||||||
isEditable
|
onValueChange={(value) => {
|
||||||
/>
|
const nuevoNivel = value as NivelPlanEstudio
|
||||||
</DropdownMenuTrigger>
|
setNivelPlan(nuevoNivel)
|
||||||
|
if (nuevoNivel !== data?.nivel) {
|
||||||
<DropdownMenuContent className="w-48">
|
mutate({ planId, patch: { nivel: nuevoNivel } })
|
||||||
{niveles.map((n) => (
|
}
|
||||||
<DropdownMenuItem
|
}}
|
||||||
key={n}
|
>
|
||||||
onClick={() => {
|
<SelectTrigger className="w-full" size="sm">
|
||||||
setNivelPlan(n)
|
<SelectValue placeholder="---" />
|
||||||
if (n !== data?.nivel) {
|
</SelectTrigger>
|
||||||
mutate({ planId, patch: { nivel: n } })
|
<SelectContent>
|
||||||
}
|
{niveles.map((n) => (
|
||||||
}}
|
<SelectItem key={n} value={n}>
|
||||||
>
|
{n}
|
||||||
{n}
|
</SelectItem>
|
||||||
</DropdownMenuItem>
|
))}
|
||||||
))}
|
</SelectContent>
|
||||||
</DropdownMenuContent>
|
</Select>
|
||||||
</DropdownMenu>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<Clock className="text-slate-400" />}
|
icon={<Clock className="text-muted-foreground" />}
|
||||||
label="Duración"
|
label="Duración"
|
||||||
value={`${data?.numero_ciclos || 0} Ciclos`}
|
value={`${data?.numero_ciclos || 0} Ciclos`}
|
||||||
/>
|
/>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<Hash className="text-slate-400" />}
|
icon={<Hash className="text-muted-foreground" />}
|
||||||
label="Créditos"
|
label="Créditos"
|
||||||
value="320"
|
value="320"
|
||||||
/>
|
/>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<CalendarDays className="text-slate-400" />}
|
icon={<CalendarDays className="text-muted-foreground" />}
|
||||||
label="Creación"
|
label="Creación"
|
||||||
value={data?.creado_en?.split('T')[0]}
|
value={data?.creado_en.split('T')[0]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -276,20 +305,20 @@ const InfoCard = forwardRef<
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
className={`flex h-18 w-full items-center gap-4 rounded-xl border border-slate-200/60 bg-slate-50/50 p-4 shadow-sm transition-all ${
|
className={`border-border/60 bg-muted/30 flex h-18 w-full items-center gap-4 rounded-xl border p-4 shadow-sm transition-all ${
|
||||||
isEditable
|
isEditable
|
||||||
? 'cursor-pointer hover:border-teal-200 hover:bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-teal-500/40'
|
? 'hover:border-primary/50 hover:bg-accent focus-visible:ring-primary/40 cursor-pointer focus:outline-none focus-visible:ring-2'
|
||||||
: ''
|
: ''
|
||||||
} ${className ?? ''}`}
|
} ${className ?? ''}`}
|
||||||
>
|
>
|
||||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border bg-white shadow-sm">
|
<div className="bg-background flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border shadow-sm">
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="mb-0.5 truncate text-[10px] font-bold tracking-wider text-slate-400 uppercase">
|
<p className="text-muted-foreground mb-0.5 truncate text-[10px] font-bold tracking-wider uppercase">
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-sm font-semibold text-slate-700">
|
<p className="text-foreground truncate text-sm font-semibold">
|
||||||
{value || '---'}
|
{value || '---'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,8 +340,8 @@ function Tab({
|
|||||||
<Link
|
<Link
|
||||||
to={to}
|
to={to}
|
||||||
params={params}
|
params={params}
|
||||||
className="border-b-2 border-transparent pb-3 text-sm font-medium text-slate-500 transition-all hover:text-slate-800"
|
className="text-muted-foreground hover:text-foreground border-b-2 border-transparent pb-3 text-sm font-medium transition-all"
|
||||||
activeProps={{ className: 'border-teal-600 text-teal-700 font-bold' }}
|
activeProps={{ className: 'border-primary text-primary font-bold' }}
|
||||||
activeOptions={{
|
activeOptions={{
|
||||||
exact: true,
|
exact: true,
|
||||||
}}
|
}}
|
||||||
@@ -324,7 +353,7 @@ function Tab({
|
|||||||
|
|
||||||
function DatosGeneralesSkeleton() {
|
function DatosGeneralesSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border bg-white">
|
<div className="bg-card rounded-xl border">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between border-b px-5 py-3">
|
<div className="flex items-center justify-between border-b px-5 py-3">
|
||||||
<Skeleton className="h-4 w-40" />
|
<Skeleton className="h-4 w-40" />
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'
|
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Copy,
|
|
||||||
Search,
|
Search,
|
||||||
Filter,
|
Filter,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
Loader2,
|
Loader2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useMemo } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
|
|
||||||
import type { Asignatura, AsignaturaStatus, TipoAsignatura } from '@/types/plan'
|
import type { Asignatura, AsignaturaStatus, TipoAsignatura } from '@/types/plan'
|
||||||
|
import type { Tables } from '@/types/supabase'
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -37,6 +37,11 @@ const statusConfig: Record<
|
|||||||
AsignaturaStatus,
|
AsignaturaStatus,
|
||||||
{ label: string; className: string }
|
{ label: string; className: string }
|
||||||
> = {
|
> = {
|
||||||
|
generando: {
|
||||||
|
label: 'Generando',
|
||||||
|
className:
|
||||||
|
'bg-slate-100 text-slate-600 animate-pulse [animation-duration:2s]',
|
||||||
|
},
|
||||||
borrador: { label: 'Borrador', className: 'bg-slate-100 text-slate-600' },
|
borrador: { label: 'Borrador', className: 'bg-slate-100 text-slate-600' },
|
||||||
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
|
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
|
||||||
aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-700' },
|
aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-700' },
|
||||||
@@ -44,31 +49,31 @@ const statusConfig: Record<
|
|||||||
|
|
||||||
const tipoConfig: Record<TipoAsignatura, { label: string; className: string }> =
|
const tipoConfig: Record<TipoAsignatura, { label: string; className: string }> =
|
||||||
{
|
{
|
||||||
obligatoria: {
|
OBLIGATORIA: {
|
||||||
label: 'Obligatoria',
|
label: 'Obligatoria',
|
||||||
className: 'bg-blue-100 text-blue-700',
|
className: 'bg-blue-100 text-blue-700',
|
||||||
},
|
},
|
||||||
optativa: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
|
OPTATIVA: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
|
||||||
troncal: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
|
TRONCAL: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
|
||||||
|
OTRA: { label: 'Otra', className: 'bg-slate-100 text-slate-700' },
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Mapeadores de API ---
|
// --- Mapeadores de API ---
|
||||||
const mapAsignaturas = (asigApi: Array<any> = []): Array<Asignatura> => {
|
const mapAsignaturas = (
|
||||||
|
asigApi: Array<Tables<'asignaturas'>> = [],
|
||||||
|
): Array<Asignatura> => {
|
||||||
return asigApi.map((asig) => ({
|
return asigApi.map((asig) => ({
|
||||||
id: asig.id,
|
id: asig.id,
|
||||||
clave: asig.codigo,
|
clave: asig.codigo ?? '',
|
||||||
nombre: asig.nombre,
|
nombre: asig.nombre,
|
||||||
creditos: asig.creditos ?? 0,
|
creditos: asig.creditos,
|
||||||
ciclo: asig.numero_ciclo ?? null,
|
ciclo: asig.numero_ciclo ?? null,
|
||||||
lineaCurricularId: asig.linea_plan_id ?? null,
|
lineaCurricularId: asig.linea_plan_id ?? null,
|
||||||
tipo:
|
tipo: asig.tipo,
|
||||||
asig.tipo?.toLowerCase() === 'obligatoria' ? 'obligatoria' : 'optativa',
|
estado: asig.estado,
|
||||||
estado: 'borrador', // O el campo que venga de tu API
|
hd: asig.horas_academicas ?? 0,
|
||||||
hd: Math.floor((asig.horas_semana ?? 0) / 2),
|
hi: asig.horas_independientes ?? 0,
|
||||||
hi: Math.ceil((asig.horas_semana ?? 0) / 2),
|
prerrequisito_asignatura_id: asig.prerrequisito_asignatura_id ?? null,
|
||||||
prerrequisitos: Array.isArray(asig.prerrequisitos)
|
|
||||||
? asig.prerrequisitos
|
|
||||||
: [],
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +86,7 @@ function AsignaturasPage() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// 1. Fetch de datos reales
|
// 1. Fetch de datos reales
|
||||||
const { data: asignaturasApi, isLoading: loadingAsig } =
|
const { data: asignaturaApi, isLoading: loadingAsig } =
|
||||||
usePlanAsignaturas(planId)
|
usePlanAsignaturas(planId)
|
||||||
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
|
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
|
||||||
|
|
||||||
@@ -93,8 +98,8 @@ function AsignaturasPage() {
|
|||||||
|
|
||||||
// 3. Procesamiento de datos
|
// 3. Procesamiento de datos
|
||||||
const asignaturas = useMemo(
|
const asignaturas = useMemo(
|
||||||
() => mapAsignaturas(asignaturasApi),
|
() => mapAsignaturas(asignaturaApi),
|
||||||
[asignaturasApi],
|
[asignaturaApi],
|
||||||
)
|
)
|
||||||
const lineas = useMemo(() => lineasApi || [], [lineasApi])
|
const lineas = useMemo(() => lineasApi || [], [lineasApi])
|
||||||
|
|
||||||
@@ -124,7 +129,7 @@ function AsignaturasPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto space-y-6 px-6 py-6">
|
<div className="w-full space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -138,9 +143,6 @@ function AsignaturasPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<Copy className="mr-2 h-4 w-4" /> Clonar
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log('planId desde asignaturas', planId)
|
console.log('planId desde asignaturas', planId)
|
||||||
@@ -150,7 +152,7 @@ function AsignaturasPage() {
|
|||||||
resetScroll: false,
|
resetScroll: false,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
className="ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-11 items-center justify-center gap-2 rounded-md px-8 text-sm font-medium shadow-md transition-colors"
|
className="shadow-md"
|
||||||
>
|
>
|
||||||
<Plus className="mr-2 h-4 w-4" /> Nueva Asignatura
|
<Plus className="mr-2 h-4 w-4" /> Nueva Asignatura
|
||||||
</Button>
|
</Button>
|
||||||
@@ -216,26 +218,30 @@ function AsignaturasPage() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-slate-50/50">
|
<TableRow className="bg-slate-50/50">
|
||||||
<TableHead className="w-30">Clave</TableHead>
|
<TableHead className="w-30 px-6 py-4">Clave</TableHead>
|
||||||
<TableHead>Nombre</TableHead>
|
<TableHead className="px-6 py-4">Nombre</TableHead>
|
||||||
<TableHead className="text-center">Créditos</TableHead>
|
<TableHead className="px-6 py-4 text-center">Créditos</TableHead>
|
||||||
<TableHead className="text-center">Ciclo</TableHead>
|
<TableHead className="px-6 py-4 text-center">Ciclo</TableHead>
|
||||||
<TableHead>Línea Curricular</TableHead>
|
<TableHead className="px-6 py-4">Línea Curricular</TableHead>
|
||||||
<TableHead>Tipo</TableHead>
|
<TableHead className="px-6 py-4">Tipo</TableHead>
|
||||||
<TableHead>Estado</TableHead>
|
<TableHead className="px-6 py-4">Estado</TableHead>
|
||||||
<TableHead className="w-12.5"></TableHead>
|
<TableHead className="w-12.5 px-6 py-4"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredAsignaturas.length === 0 ? (
|
{filteredAsignaturas.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={8} className="h-40 text-center">
|
<TableCell colSpan={8} className="h-40 px-6 py-8 text-center">
|
||||||
<div className="text-muted-foreground flex flex-col items-center justify-center">
|
<div className="text-muted-foreground flex flex-col items-center justify-center gap-3">
|
||||||
<BookOpen className="mb-2 h-10 w-10 opacity-20" />
|
<BookOpen className="h-10 w-10 opacity-20" />
|
||||||
<p className="font-medium">No se encontraron asignaturas</p>
|
<div>
|
||||||
<p className="text-xs">
|
<p className="font-medium">
|
||||||
Intenta cambiar los filtros de búsqueda
|
No se encontraron asignaturas
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-1 text-xs">
|
||||||
|
Intenta cambiar los filtros de búsqueda
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -249,25 +255,25 @@ function AsignaturasPage() {
|
|||||||
to: '/planes/$planId/asignaturas/$asignaturaId',
|
to: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
params: {
|
params: {
|
||||||
planId,
|
planId,
|
||||||
asignaturaId: asignatura.id, // 👈 puede ser índice, consecutivo o slug
|
asignaturaId: asignatura.id,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
realId: asignatura.id, // 👈 ID largo oculto
|
realId: asignatura.id,
|
||||||
asignaturaId: asignatura.id,
|
asignaturaId: asignatura.id,
|
||||||
} as any,
|
} as any,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TableCell className="font-mono text-xs font-bold text-slate-400">
|
<TableCell className="px-6 py-4 font-mono text-xs font-bold text-slate-400">
|
||||||
{asignatura.clave}
|
{asignatura.clave}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-semibold text-slate-700">
|
<TableCell className="px-6 py-4 font-semibold text-slate-700">
|
||||||
{asignatura.nombre}
|
{asignatura.nombre}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center font-medium">
|
<TableCell className="px-6 py-4 text-center font-medium">
|
||||||
{asignatura.creditos}
|
{asignatura.creditos}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="px-6 py-4 text-center">
|
||||||
{asignatura.ciclo ? (
|
{asignatura.ciclo ? (
|
||||||
<Badge variant="outline" className="font-normal">
|
<Badge variant="outline" className="font-normal">
|
||||||
Ciclo {asignatura.ciclo}
|
Ciclo {asignatura.ciclo}
|
||||||
@@ -276,10 +282,10 @@ function AsignaturasPage() {
|
|||||||
<span className="text-slate-300">—</span>
|
<span className="text-slate-300">—</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm text-slate-600">
|
<TableCell className="px-6 py-4 text-sm text-slate-600">
|
||||||
{getLineaNombre(asignatura.lineaCurricularId)}
|
{getLineaNombre(asignatura.lineaCurricularId)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="px-6 py-4">
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`capitalize shadow-sm ${tipoConfig[asignatura.tipo].className}`}
|
className={`capitalize shadow-sm ${tipoConfig[asignatura.tipo].className}`}
|
||||||
@@ -287,7 +293,7 @@ function AsignaturasPage() {
|
|||||||
{tipoConfig[asignatura.tipo].label}
|
{tipoConfig[asignatura.tipo].label}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="px-6 py-4">
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`capitalize shadow-sm ${statusConfig[asignatura.estado].className}`}
|
className={`capitalize shadow-sm ${statusConfig[asignatura.estado].className}`}
|
||||||
@@ -295,7 +301,7 @@ function AsignaturasPage() {
|
|||||||
{statusConfig[asignatura.estado].label}
|
{statusConfig[asignatura.estado].label}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="px-6 py-4">
|
||||||
<div className="opacity-0 transition-opacity group-hover:opacity-100">
|
<div className="opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
<ChevronRight className="h-5 w-5 text-slate-400" />
|
<ChevronRight className="h-5 w-5 text-slate-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
FileJson,
|
FileJson,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
|
import { usePlan } from '@/data'
|
||||||
import { fetchPlanPdf } from '@/data/api/document.api'
|
import { fetchPlanPdf } from '@/data/api/document.api'
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId/_detalle/documento')({
|
export const Route = createFileRoute('/planes/$planId/_detalle/documento')({
|
||||||
@@ -20,30 +21,41 @@ export const Route = createFileRoute('/planes/$planId/_detalle/documento')({
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = useParams({ from: '/planes/$planId/_detalle/documento' })
|
const { planId } = useParams({ from: '/planes/$planId/_detalle/documento' })
|
||||||
|
const { data: plan } = usePlan(planId)
|
||||||
const [pdfUrl, setPdfUrl] = useState<string | null>(null)
|
const [pdfUrl, setPdfUrl] = useState<string | null>(null)
|
||||||
|
const pdfUrlRef = useRef<string | null>(null)
|
||||||
|
const isMountedRef = useRef<boolean>(false)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
|
const planFileBaseName = sanitizeFileBaseName(plan?.nombre ?? 'plan_estudios')
|
||||||
|
|
||||||
const loadPdfPreview = useCallback(async () => {
|
const loadPdfPreview = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
if (isMountedRef.current) setIsLoading(true)
|
||||||
const pdfBlob = await fetchPlanPdf({ plan_estudio_id: planId })
|
const pdfBlob = await fetchPlanPdf({
|
||||||
|
plan_estudio_id: planId,
|
||||||
|
convertTo: 'pdf',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isMountedRef.current) return
|
||||||
const url = window.URL.createObjectURL(pdfBlob)
|
const url = window.URL.createObjectURL(pdfBlob)
|
||||||
|
|
||||||
// Limpiar URL anterior si existe para evitar fugas de memoria
|
if (pdfUrlRef.current) window.URL.revokeObjectURL(pdfUrlRef.current)
|
||||||
if (pdfUrl) window.URL.revokeObjectURL(pdfUrl)
|
pdfUrlRef.current = url
|
||||||
|
|
||||||
setPdfUrl(url)
|
setPdfUrl(url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error cargando preview:', error)
|
console.error('Error cargando preview:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
if (isMountedRef.current) setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [planId])
|
}, [planId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true
|
||||||
loadPdfPreview()
|
loadPdfPreview()
|
||||||
return () => {
|
return () => {
|
||||||
if (pdfUrl) window.URL.revokeObjectURL(pdfUrl)
|
isMountedRef.current = false
|
||||||
|
if (pdfUrlRef.current) window.URL.revokeObjectURL(pdfUrlRef.current)
|
||||||
}
|
}
|
||||||
}, [loadPdfPreview])
|
}, [loadPdfPreview])
|
||||||
|
|
||||||
@@ -51,12 +63,13 @@ function RouteComponent() {
|
|||||||
try {
|
try {
|
||||||
const pdfBlob = await fetchPlanPdf({
|
const pdfBlob = await fetchPlanPdf({
|
||||||
plan_estudio_id: planId,
|
plan_estudio_id: planId,
|
||||||
|
convertTo: 'pdf',
|
||||||
})
|
})
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(pdfBlob)
|
const url = window.URL.createObjectURL(pdfBlob)
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = url
|
link.href = url
|
||||||
link.download = 'plan_estudios.pdf'
|
link.download = `${planFileBaseName}.pdf`
|
||||||
document.body.appendChild(link)
|
document.body.appendChild(link)
|
||||||
link.click()
|
link.click()
|
||||||
|
|
||||||
@@ -67,8 +80,29 @@ function RouteComponent() {
|
|||||||
alert('No se pudo generar el PDF')
|
alert('No se pudo generar el PDF')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDownloadWord = async () => {
|
||||||
|
try {
|
||||||
|
const docBlob = await fetchPlanPdf({
|
||||||
|
plan_estudio_id: planId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(docBlob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `${planFileBaseName}.docx`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
link.remove()
|
||||||
|
setTimeout(() => window.URL.revokeObjectURL(url), 1000)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
alert('No se pudo generar el Word')
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col gap-6 bg-slate-50/30 p-6">
|
<div className="flex min-h-screen flex-col gap-6 bg-slate-50/30">
|
||||||
{/* HEADER DE ACCIONES */}
|
{/* HEADER DE ACCIONES */}
|
||||||
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
|
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
|
||||||
<div>
|
<div>
|
||||||
@@ -88,12 +122,13 @@ function RouteComponent() {
|
|||||||
>
|
>
|
||||||
<RefreshCcw size={16} /> Regenerar
|
<RefreshCcw size={16} /> Regenerar
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" className="gap-2">
|
<Button size="sm" className="gap-2" onClick={handleDownloadWord}>
|
||||||
<Download size={16} /> Descargar Word
|
<Download size={16} /> Descargar Word
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="gap-2 bg-teal-700 hover:bg-teal-800"
|
className="gap-2"
|
||||||
onClick={handleDownloadPdf}
|
onClick={handleDownloadPdf}
|
||||||
>
|
>
|
||||||
<Download size={16} /> Descargar PDF
|
<Download size={16} /> Descargar PDF
|
||||||
@@ -139,7 +174,7 @@ function RouteComponent() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardContent className="flex min-h-[800px] justify-center bg-slate-500 p-0">
|
<CardContent className="flex min-h-200 justify-center bg-slate-500 p-0">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex flex-col items-center justify-center gap-4 text-white">
|
<div className="flex flex-col items-center justify-center gap-4 text-white">
|
||||||
<RefreshCcw size={40} className="animate-spin opacity-50" />
|
<RefreshCcw size={40} className="animate-spin opacity-50" />
|
||||||
@@ -149,7 +184,7 @@ function RouteComponent() {
|
|||||||
/* 3. VISOR DE PDF REAL */
|
/* 3. VISOR DE PDF REAL */
|
||||||
<iframe
|
<iframe
|
||||||
src={`${pdfUrl}#toolbar=0&navpanes=0`}
|
src={`${pdfUrl}#toolbar=0&navpanes=0`}
|
||||||
className="h-[1000px] w-full max-w-[1000px] border-none shadow-2xl"
|
className="h-250 w-full max-w-250 border-none shadow-2xl"
|
||||||
title="PDF Preview"
|
title="PDF Preview"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -163,6 +198,24 @@ function RouteComponent() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeFileBaseName(input: string): string {
|
||||||
|
const text = String(input)
|
||||||
|
const withoutControlChars = Array.from(text)
|
||||||
|
.filter((ch) => {
|
||||||
|
const code = ch.charCodeAt(0)
|
||||||
|
return code >= 32 && code !== 127
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
const cleaned = withoutControlChars
|
||||||
|
.replace(/[<>:"/\\|?*]+/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.replace(/[. ]+$/g, '')
|
||||||
|
|
||||||
|
return (cleaned || 'documento').slice(0, 150)
|
||||||
|
}
|
||||||
|
|
||||||
// Componente pequeño para las tarjetas de estado superior
|
// Componente pequeño para las tarjetas de estado superior
|
||||||
function StatusCard({
|
function StatusCard({
|
||||||
icon,
|
icon,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function RouteComponent() {
|
|||||||
console.log(rawData)
|
console.log(rawData)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 p-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* Header Informativo (Opcional, si no viene del layout padre) */}
|
{/* Header Informativo (Opcional, si no viene del layout padre) */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -146,7 +146,7 @@ function RouteComponent() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button className="w-full bg-teal-600 hover:bg-teal-700" disabled>
|
<Button className="w-full" disabled>
|
||||||
Avanzar a Revisión Expertos
|
Avanzar a Revisión Expertos
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ import {
|
|||||||
User,
|
User,
|
||||||
Loader2,
|
Loader2,
|
||||||
Clock,
|
Clock,
|
||||||
Eye,
|
|
||||||
History,
|
History,
|
||||||
Calendar,
|
Calendar,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -34,38 +36,37 @@ const getEventConfig = (tipo: string, campo: string) => {
|
|||||||
return {
|
return {
|
||||||
label: 'Creación',
|
label: 'Creación',
|
||||||
icon: <PlusCircle className="h-4 w-4" />,
|
icon: <PlusCircle className="h-4 w-4" />,
|
||||||
color: 'teal',
|
color: 'primary',
|
||||||
}
|
}
|
||||||
if (campo === 'estado')
|
if (campo === 'estado')
|
||||||
return {
|
return {
|
||||||
label: 'Cambio de estado',
|
label: 'Cambio de estado',
|
||||||
icon: <GitBranch className="h-4 w-4" />,
|
icon: <GitBranch className="h-4 w-4" />,
|
||||||
color: 'blue',
|
color: 'secondary',
|
||||||
}
|
}
|
||||||
if (campo === 'datos')
|
if (campo === 'datos')
|
||||||
return {
|
return {
|
||||||
label: 'Edición de Datos',
|
label: 'Edición de Datos',
|
||||||
icon: <Edit3 className="h-4 w-4" />,
|
icon: <Edit3 className="h-4 w-4" />,
|
||||||
color: 'amber',
|
color: 'accent',
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
label: 'Actualización',
|
label: 'Actualización',
|
||||||
icon: <RefreshCw className="h-4 w-4" />,
|
icon: <RefreshCw className="h-4 w-4" />,
|
||||||
color: 'slate',
|
color: 'muted',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
const { data: rawData, isLoading } = usePlanHistorial(planId)
|
const [page, setPage] = useState(0)
|
||||||
|
const pageSize = 4
|
||||||
|
const { data: response, isLoading } = usePlanHistorial(planId, page)
|
||||||
|
const rawData = response?.data ?? []
|
||||||
|
const totalRecords = response?.count ?? 0
|
||||||
|
const totalPages = Math.ceil(totalRecords / pageSize)
|
||||||
const [structure, setStructure] = useState<any>(null)
|
const [structure, setStructure] = useState<any>(null)
|
||||||
const { data } = usePlan(planId)
|
const { data } = usePlan(planId)
|
||||||
console.log('analizando estructura')
|
|
||||||
|
|
||||||
console.log(data?.estructuras_plan?.definicion?.properties)
|
|
||||||
// console.log(structure)
|
|
||||||
|
|
||||||
// ESTADOS PARA EL MODAL
|
|
||||||
const [selectedEvent, setSelectedEvent] = useState<any>(null)
|
const [selectedEvent, setSelectedEvent] = useState<any>(null)
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
|
||||||
@@ -94,14 +95,15 @@ function RouteComponent() {
|
|||||||
}`,
|
}`,
|
||||||
date: parseISO(item.cambiado_en),
|
date: parseISO(item.cambiado_en),
|
||||||
icon: config.icon,
|
icon: config.icon,
|
||||||
campo: item.campo,
|
campo:
|
||||||
|
data?.estructuras_plan?.definicion?.properties?.[item.campo]?.title,
|
||||||
details: {
|
details: {
|
||||||
from: item.valor_anterior,
|
from: item.valor_anterior,
|
||||||
to: item.valor_nuevo,
|
to: item.valor_nuevo,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [rawData])
|
}, [rawData, structure, data])
|
||||||
|
|
||||||
const openCompareModal = (event: any) => {
|
const openCompareModal = (event: any) => {
|
||||||
setSelectedEvent(event)
|
setSelectedEvent(event)
|
||||||
@@ -117,16 +119,16 @@ function RouteComponent() {
|
|||||||
if (isLoading)
|
if (isLoading)
|
||||||
return (
|
return (
|
||||||
<div className="flex h-64 items-center justify-center">
|
<div className="flex h-64 items-center justify-center">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-teal-600" />
|
<Loader2 className="text-primary h-8 w-8 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-5xl p-6">
|
<div className="mx-auto">
|
||||||
<div className="mb-8 flex items-end justify-between">
|
<div className="mb-8 flex items-end justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="flex items-center gap-2 text-xl font-bold text-slate-800">
|
<h1 className="text-foreground flex items-center gap-2 text-xl font-bold">
|
||||||
<Clock className="h-5 w-5 text-teal-600" /> Historial de Cambios del
|
<Clock className="text-primary h-5 w-5" /> Historial de Cambios del
|
||||||
Plan
|
Plan
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
@@ -136,25 +138,39 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative space-y-0">
|
<div className="relative space-y-0">
|
||||||
<div className="absolute top-0 bottom-0 left-9 w-px bg-slate-200" />
|
<div className="bg-border absolute top-0 bottom-0 left-6 w-px md:left-9" />
|
||||||
{historyEvents.length === 0 ? (
|
{historyEvents.length === 0 ? (
|
||||||
<div className="ml-20 py-10 text-slate-500">No hay registros.</div>
|
<div className="text-muted-foreground ml-20 py-10">
|
||||||
|
No hay registros.
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
historyEvents.map((event) => (
|
historyEvents.map((event) => (
|
||||||
<div key={event.id} className="group relative flex gap-6 pb-8">
|
<div
|
||||||
<div className="relative z-10 flex h-18 flex-col items-center">
|
key={event.id}
|
||||||
<div className="flex h-[42px] w-[42px] items-center justify-center rounded-full border-4 border-white bg-slate-100 text-slate-600 shadow-sm transition-colors group-hover:bg-teal-50 group-hover:text-teal-600">
|
className="group relative flex gap-3 pb-8 md:gap-6"
|
||||||
|
>
|
||||||
|
<div className="relative z-10 flex flex-col items-center">
|
||||||
|
<div className="border-background bg-muted text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary flex h-[42px] w-[42px] items-center justify-center rounded-full border-4 shadow-sm transition-colors">
|
||||||
{event.icon}
|
{event.icon}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="flex-1 border-slate-200 shadow-none transition-colors hover:border-teal-200">
|
<Card
|
||||||
|
className="border-border hover:border-primary/50 flex-1 cursor-pointer shadow-none transition-colors"
|
||||||
|
onClick={() => openCompareModal(event)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ')
|
||||||
|
openCompareModal(event)
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-2">
|
||||||
{/* LÍNEA SUPERIOR: Título a la izquierda --- Usuario, Botón y Fecha a la derecha */}
|
{/* LÍNEA SUPERIOR: Título a la izquierda --- Usuario, Botón y Fecha a la derecha */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col justify-between gap-2 md:flex-row md:items-center">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="text-sm font-bold text-slate-800">
|
<span className="text-foreground text-sm font-bold">
|
||||||
{event.type}
|
{event.type}
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
@@ -169,26 +185,17 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grupo de elementos alineados a la derecha */}
|
{/* Grupo de elementos alineados a la derecha */}
|
||||||
<div className="flex items-center gap-4 text-slate-500">
|
<div className="text-muted-foreground flex flex-wrap items-center gap-3 md:gap-4">
|
||||||
{/* Usuario e Icono */}
|
{/* Usuario e Icono */}
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-1.5 text-xs">
|
||||||
<User className="h-3.5 w-3.5" />
|
<User className="h-3.5 w-3.5" />
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{event.user}
|
{event.user}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Botón Ver Cambios */}
|
|
||||||
<button
|
|
||||||
onClick={() => openCompareModal(event)}
|
|
||||||
className="group/btn flex items-center gap-1.5 text-xs transition-colors hover:text-teal-600"
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4 text-slate-400 group-hover/btn:text-teal-600" />
|
|
||||||
<span>Ver cambios</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Fecha exacta (Solo visible en desktop para no amontonar) */}
|
{/* Fecha exacta (Solo visible en desktop para no amontonar) */}
|
||||||
<span className="hidden text-[11px] text-slate-400 md:block">
|
<span className="text-muted-foreground/70 hidden text-[11px] lg:block">
|
||||||
{format(event.date, 'yyyy-MM-dd HH:mm')}
|
{format(event.date, 'yyyy-MM-dd HH:mm')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,7 +203,7 @@ function RouteComponent() {
|
|||||||
|
|
||||||
{/* LÍNEA INFERIOR: Descripción */}
|
{/* LÍNEA INFERIOR: Descripción */}
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<p className="text-sm text-slate-600">
|
<p className="text-muted-foreground text-sm">
|
||||||
{event.description}
|
{event.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -207,16 +214,16 @@ function RouteComponent() {
|
|||||||
<div className="mt-2 flex items-center gap-1.5">
|
<div className="mt-2 flex items-center gap-1.5">
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="bg-red-50 px-1.5 text-[9px] text-red-700"
|
className="bg-destructive/10 text-destructive px-1.5 text-[9px]"
|
||||||
>
|
>
|
||||||
{event.details.from}
|
{event.details.from}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-[10px] text-slate-400">
|
<span className="text-muted-foreground/70 text-[10px]">
|
||||||
→
|
→
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="bg-emerald-50 px-1.5 text-[9px] text-emerald-700"
|
className="bg-primary/10 text-primary px-1.5 text-[9px]"
|
||||||
>
|
>
|
||||||
{event.details.to}
|
{event.details.to}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -229,14 +236,53 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
{historyEvents.length > 0 && (
|
||||||
|
<div className="mt-10 ml-12 flex flex-col gap-3 border-t pt-4 md:ml-20 md:flex-row md:items-center md:justify-between">
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
Mostrando {rawData.length} de {totalRecords} cambios
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setPage((p) => Math.max(0, p - 1))
|
||||||
|
window.scrollTo(0, 0) // Opcional: volver arriba
|
||||||
|
}}
|
||||||
|
disabled={page === 0 || isLoading}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||||
|
Anterior
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<span className="text-foreground text-sm font-medium">
|
||||||
|
Página {page + 1} de {totalPages || 1}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setPage((p) => p + 1)
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}}
|
||||||
|
disabled={page + 1 >= totalPages || isLoading}
|
||||||
|
>
|
||||||
|
Siguiente
|
||||||
|
<ChevronRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* MODAL DE COMPARACIÓN CON SCROLL INTERNO */}
|
{/* MODAL DE COMPARACIÓN CON SCROLL INTERNO */}
|
||||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||||
<DialogContent className="flex max-h-[90vh] max-w-4xl flex-col gap-0 overflow-hidden p-0">
|
<DialogContent className="flex max-h-[90vh] max-w-4xl flex-col gap-0 overflow-hidden p-0">
|
||||||
<DialogHeader className="border-b bg-slate-50/50 p-6">
|
<DialogHeader className="bg-muted/50 border-b p-6">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<History className="h-5 w-5 text-teal-600" /> Comparación de
|
<History className="text-primary h-5 w-5" /> Comparación de
|
||||||
Versiones
|
Versiones
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<div className="text-muted-foreground flex items-center gap-4 pt-2 text-xs">
|
<div className="text-muted-foreground flex items-center gap-4 pt-2 text-xs">
|
||||||
@@ -255,35 +301,37 @@ function RouteComponent() {
|
|||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-6">
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
<div className="grid h-full grid-cols-2 gap-6">
|
<div className="grid h-full grid-cols-2 gap-6">
|
||||||
{/* Lado Antes */}
|
{/* Lado Antes: Solo se renderiza si existe valor_anterior */}
|
||||||
<div className="flex flex-col space-y-2">
|
{selectedEvent?.details.from && (
|
||||||
<div className="sticky top-0 z-10 flex items-center gap-2 bg-white py-1">
|
<div className="flex flex-col space-y-2">
|
||||||
<div className="h-2 w-2 rounded-full bg-red-400" />
|
<div className="bg-background sticky top-0 z-10 flex items-center gap-2 py-1">
|
||||||
<span className="text-muted-foreground text-[10px] font-bold tracking-widest uppercase">
|
<div className="bg-destructive h-2 w-2 rounded-full" />
|
||||||
Versión Anterior
|
<span className="text-muted-foreground text-[10px] font-bold tracking-widest uppercase">
|
||||||
</span>
|
Versión Anterior
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="border-destructive/20 bg-destructive/5 text-foreground max-h-[500px] min-h-[250px] flex-1 overflow-y-auto rounded-lg border p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap">
|
||||||
|
{renderValue(selectedEvent.details.from)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-[500px] min-h-[250px] flex-1 overflow-y-auto rounded-lg border border-red-100 bg-red-50/30 p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap text-slate-700">
|
)}
|
||||||
{renderValue(selectedEvent?.details.from)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lado Después */}
|
{/* Lado Después */}
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<div className="sticky top-0 z-10 flex items-center gap-2 bg-white py-1">
|
<div className="bg-background sticky top-0 z-10 flex items-center gap-2 py-1">
|
||||||
<div className="h-2 w-2 rounded-full bg-emerald-400" />
|
<div className="bg-primary h-2 w-2 rounded-full" />
|
||||||
<span className="text-muted-foreground text-[10px] font-bold tracking-widest uppercase">
|
<span className="text-muted-foreground text-[10px] font-bold tracking-widest uppercase">
|
||||||
Nueva Versión
|
Nueva Versión
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-[500px] min-h-[250px] flex-1 overflow-y-auto rounded-lg border border-emerald-100 bg-emerald-50/30 p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap text-slate-700">
|
<div className="border-primary/20 bg-primary/5 text-foreground max-h-[500px] min-h-[250px] flex-1 overflow-y-auto rounded-lg border p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap">
|
||||||
{renderValue(selectedEvent?.details.to)}
|
{renderValue(selectedEvent?.details.to)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-center border-t bg-slate-50 p-4">
|
<div className="bg-muted/30 flex justify-center border-t p-4">
|
||||||
<Badge variant="outline" className="font-mono text-[10px]">
|
<Badge variant="outline" className="font-mono text-[10px]">
|
||||||
Campo: {selectedEvent?.campo}
|
Campo: {selectedEvent?.campo}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ import {
|
|||||||
useNavigate,
|
useNavigate,
|
||||||
useLocation,
|
useLocation,
|
||||||
} from '@tanstack/react-router'
|
} from '@tanstack/react-router'
|
||||||
// import confetti from 'canvas-confetti'
|
|
||||||
import { Pencil, Check, X, Sparkles, AlertCircle } from 'lucide-react'
|
import { Pencil, Check, X, Sparkles, AlertCircle } from 'lucide-react'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
@@ -20,7 +19,6 @@ import {
|
|||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
import { usePlan, useUpdatePlanFields } from '@/data'
|
import { usePlan, useUpdatePlanFields } from '@/data'
|
||||||
|
|
||||||
// import { toast } from 'sonner' // Asegúrate de tener sonner instalado o quita la línea
|
|
||||||
export const Route = createFileRoute('/planes/$planId/_detalle/')({
|
export const Route = createFileRoute('/planes/$planId/_detalle/')({
|
||||||
component: DatosGeneralesPage,
|
component: DatosGeneralesPage,
|
||||||
})
|
})
|
||||||
@@ -34,21 +32,20 @@ function DatosGeneralesPage() {
|
|||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
const { data, isLoading } = usePlan(planId)
|
const { data, isLoading } = usePlan(planId)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
// Inicializamos campos como un arreglo vacío
|
|
||||||
const [campos, setCampos] = useState<Array<DatosGeneralesField>>([])
|
const [campos, setCampos] = useState<Array<DatosGeneralesField>>([])
|
||||||
const [editingId, setEditingId] = useState<string | null>(null)
|
const [editingId, setEditingId] = useState<string | null>(null)
|
||||||
const [editValue, setEditValue] = useState('')
|
const [editValue, setEditValue] = useState('')
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const updatePlan = useUpdatePlanFields()
|
const updatePlan = useUpdatePlanFields()
|
||||||
// Confetti al llegar desde creación
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.state.showConfetti) {
|
if (location.state.showConfetti) {
|
||||||
lateralConfetti()
|
lateralConfetti()
|
||||||
window.history.replaceState({}, document.title) // Limpiar el estado para que no se repita
|
window.history.replaceState({}, document.title)
|
||||||
}
|
}
|
||||||
}, [location.state])
|
}, [location.state])
|
||||||
|
|
||||||
// Efecto para transformar data?.datos en el arreglo de campos
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const definicion = data?.estructuras_plan?.definicion as any
|
const definicion = data?.estructuras_plan?.definicion as any
|
||||||
const properties = definicion?.properties
|
const properties = definicion?.properties
|
||||||
@@ -59,18 +56,13 @@ function DatosGeneralesPage() {
|
|||||||
if (properties && typeof properties === 'object') {
|
if (properties && typeof properties === 'object') {
|
||||||
let keys = Object.keys(properties)
|
let keys = Object.keys(properties)
|
||||||
|
|
||||||
// Ordenar llaves basado en la lista "required" si existe
|
|
||||||
if (Array.isArray(requiredOrder)) {
|
if (Array.isArray(requiredOrder)) {
|
||||||
keys = keys.sort((a, b) => {
|
keys = keys.sort((a, b) => {
|
||||||
const indexA = requiredOrder.indexOf(a)
|
const indexA = requiredOrder.indexOf(a)
|
||||||
const indexB = requiredOrder.indexOf(b)
|
const indexB = requiredOrder.indexOf(b)
|
||||||
// Si 'a' está en la lista y 'b' no -> 'a' primero (-1)
|
|
||||||
if (indexA !== -1 && indexB === -1) return -1
|
if (indexA !== -1 && indexB === -1) return -1
|
||||||
// Si 'b' está en la lista y 'a' no -> 'b' primero (1)
|
|
||||||
if (indexA === -1 && indexB !== -1) return 1
|
if (indexA === -1 && indexB !== -1) return 1
|
||||||
// Si ambos están, comparar índices
|
|
||||||
if (indexA !== -1 && indexB !== -1) return indexA - indexB
|
if (indexA !== -1 && indexB !== -1) return indexA - indexB
|
||||||
// Ninguno en la lista, mantener orden relativo
|
|
||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -108,18 +100,14 @@ function DatosGeneralesPage() {
|
|||||||
}
|
}
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
// 3. Manejadores de acciones (Ahora como funciones locales)
|
|
||||||
const handleEdit = (nuevoCampo: DatosGeneralesField) => {
|
const handleEdit = (nuevoCampo: DatosGeneralesField) => {
|
||||||
// 1. SI YA ESTÁBAMOS EDITANDO OTRO CAMPO, GUARDAMOS EL ANTERIOR PRIMERO
|
|
||||||
if (editingId && editingId !== nuevoCampo.id) {
|
if (editingId && editingId !== nuevoCampo.id) {
|
||||||
const campoAnterior = campos.find((c) => c.id === editingId)
|
const campoAnterior = campos.find((c) => c.id === editingId)
|
||||||
if (campoAnterior && editValue !== campoAnterior.value) {
|
if (campoAnterior && editValue !== campoAnterior.value) {
|
||||||
// Solo guardamos si el valor realmente cambió
|
|
||||||
ejecutarGuardadoSilencioso(campoAnterior, editValue)
|
ejecutarGuardadoSilencioso(campoAnterior, editValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. ABRIMOS EL NUEVO CAMPO
|
|
||||||
setEditingId(nuevoCampo.id)
|
setEditingId(nuevoCampo.id)
|
||||||
setEditValue(nuevoCampo.value)
|
setEditValue(nuevoCampo.value)
|
||||||
}
|
}
|
||||||
@@ -128,7 +116,7 @@ function DatosGeneralesPage() {
|
|||||||
setEditingId(null)
|
setEditingId(null)
|
||||||
setEditValue('')
|
setEditValue('')
|
||||||
}
|
}
|
||||||
// Función auxiliar para procesar los datos (fuera o dentro del componente)
|
|
||||||
const prepararDatosActualizados = (
|
const prepararDatosActualizados = (
|
||||||
data: any,
|
data: any,
|
||||||
campo: DatosGeneralesField,
|
campo: DatosGeneralesField,
|
||||||
@@ -160,13 +148,13 @@ function DatosGeneralesPage() {
|
|||||||
if (!data?.datos) return
|
if (!data?.datos) return
|
||||||
|
|
||||||
const datosActualizados = prepararDatosActualizados(data, campo, valor)
|
const datosActualizados = prepararDatosActualizados(data, campo, valor)
|
||||||
|
console.log(datosActualizados)
|
||||||
|
|
||||||
updatePlan.mutate({
|
updatePlan.mutate({
|
||||||
planId,
|
planId,
|
||||||
patch: { datos: datosActualizados },
|
patch: { datos: datosActualizados },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Actualizar UI localmente
|
|
||||||
setCampos((prev) =>
|
setCampos((prev) =>
|
||||||
prev.map((c) => (c.id === campo.id ? { ...c, value: valor } : c)),
|
prev.map((c) => (c.id === campo.id ? { ...c, value: valor } : c)),
|
||||||
)
|
)
|
||||||
@@ -184,13 +172,11 @@ function DatosGeneralesPage() {
|
|||||||
currentValue !== null &&
|
currentValue !== null &&
|
||||||
'description' in currentValue
|
'description' in currentValue
|
||||||
) {
|
) {
|
||||||
// Caso 1: objeto con description
|
|
||||||
newValue = {
|
newValue = {
|
||||||
...currentValue,
|
...currentValue,
|
||||||
description: editValue,
|
description: editValue,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Caso 2: valor plano (string, number, etc)
|
|
||||||
newValue = editValue
|
newValue = editValue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +192,6 @@ function DatosGeneralesPage() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// UI optimista
|
|
||||||
setCampos((prev) =>
|
setCampos((prev) =>
|
||||||
prev.map((c) => (c.id === campo.id ? { ...c, value: editValue } : c)),
|
prev.map((c) => (c.id === campo.id ? { ...c, value: editValue } : c)),
|
||||||
)
|
)
|
||||||
@@ -216,19 +201,20 @@ function DatosGeneralesPage() {
|
|||||||
setEditValue('')
|
setEditValue('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIARequest = (clave: string) => {
|
const handleIARequest = (campo: DatosGeneralesField) => {
|
||||||
navigate({
|
navigate({
|
||||||
to: '/planes/$planId/iaplan',
|
to: '/planes/$planId/iaplan',
|
||||||
params: {
|
params: {
|
||||||
planId: planId, // o dinámico
|
planId: planId,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
campo_edit: clave,
|
campo_edit: campo.clave,
|
||||||
} as any,
|
} as any,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-in fade-in container mx-auto px-6 py-6 duration-500">
|
<div className="animate-in fade-in duration-500">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-foreground text-lg font-semibold">
|
<h2 className="text-foreground text-lg font-semibold">
|
||||||
Datos Generales del Plan
|
Datos Generales del Plan
|
||||||
@@ -245,19 +231,19 @@ function DatosGeneralesPage() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={campo.id}
|
key={campo.id}
|
||||||
className={`rounded-xl border transition-all ${
|
className={`bg-card rounded-xl border transition-all ${
|
||||||
isEditing
|
isEditing
|
||||||
? 'border-teal-500 shadow-lg ring-2 ring-teal-50'
|
? 'border-primary ring-primary/20 shadow-lg ring-2'
|
||||||
: 'bg-white hover:shadow-md'
|
: 'hover:shadow-md'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Header de la Card */}
|
{/* Header de la Card */}
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex items-center justify-between border-b bg-slate-50/50 px-5 py-3">
|
<div className="bg-muted/30 flex items-center justify-between border-b px-5 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<h3 className="cursor-help text-sm font-medium text-slate-700">
|
<h3 className="text-foreground cursor-help text-sm font-medium">
|
||||||
{campo.label}
|
{campo.label}
|
||||||
</h3>
|
</h3>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -267,7 +253,7 @@ function DatosGeneralesPage() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{campo.requerido && (
|
{campo.requerido && (
|
||||||
<span className="text-xs text-red-500">*</span>
|
<span className="text-destructive text-xs">*</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -278,7 +264,7 @@ function DatosGeneralesPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 text-teal-600"
|
className="text-primary hover:text-primary/90 h-8 w-8"
|
||||||
onClick={() => handleIARequest(campo)}
|
onClick={() => handleIARequest(campo)}
|
||||||
>
|
>
|
||||||
<Sparkles size={14} />
|
<Sparkles size={14} />
|
||||||
@@ -292,7 +278,7 @@ function DatosGeneralesPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="text-muted-foreground hover:text-foreground h-8 w-8"
|
||||||
onClick={() => handleEdit(campo)}
|
onClick={() => handleEdit(campo)}
|
||||||
>
|
>
|
||||||
<Pencil size={14} />
|
<Pencil size={14} />
|
||||||
@@ -323,11 +309,7 @@ function DatosGeneralesPage() {
|
|||||||
>
|
>
|
||||||
<X size={14} className="mr-1" /> Cancelar
|
<X size={14} className="mr-1" /> Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button size="sm" onClick={() => handleSave(campo)}>
|
||||||
size="sm"
|
|
||||||
className="bg-teal-600 hover:bg-teal-700"
|
|
||||||
onClick={() => handleSave(campo)}
|
|
||||||
>
|
|
||||||
<Check size={14} className="mr-1" /> Guardar
|
<Check size={14} className="mr-1" /> Guardar
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,12 +317,12 @@ function DatosGeneralesPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="min-h-25">
|
<div className="min-h-25">
|
||||||
{campo.value ? (
|
{campo.value ? (
|
||||||
<div className="text-sm leading-relaxed text-slate-600">
|
<div className="text-muted-foreground text-sm leading-relaxed">
|
||||||
{campo.tipo === 'lista' ? (
|
{campo.tipo === 'lista' ? (
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{campo.value.split('\n').map((item, i) => (
|
{campo.value.split('\n').map((item, i) => (
|
||||||
<li key={i} className="flex gap-2">
|
<li key={i} className="flex gap-2">
|
||||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-teal-500" />
|
<span className="bg-primary mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full" />
|
||||||
{item}
|
{item}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -350,7 +332,7 @@ function DatosGeneralesPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2 text-sm text-slate-400">
|
<div className="text-muted-foreground/70 flex items-center gap-2 text-sm">
|
||||||
<AlertCircle size={14} />
|
<AlertCircle size={14} />
|
||||||
<span>Sin contenido.</span>
|
<span>Sin contenido.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,55 +0,0 @@
|
|||||||
import { createFileRoute, notFound, useLocation } from '@tanstack/react-router'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage'
|
|
||||||
import { lateralConfetti } from '@/components/ui/lateral-confetti'
|
|
||||||
import { NotFoundPage } from '@/components/ui/NotFoundPage'
|
|
||||||
import { subjects_get } from '@/data/api/subjects.api'
|
|
||||||
import { qk } from '@/data/query/keys'
|
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
|
||||||
'/planes/$planId/asignaturas/$asignaturaId',
|
|
||||||
)({
|
|
||||||
loader: async ({ context: { queryClient }, params: { asignaturaId } }) => {
|
|
||||||
try {
|
|
||||||
await queryClient.ensureQueryData({
|
|
||||||
queryKey: qk.asignatura(asignaturaId),
|
|
||||||
queryFn: () => subjects_get(asignaturaId),
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
// PGRST116: The result contains 0 rows (Supabase Single response error)
|
|
||||||
if (e?.code === 'PGRST116') {
|
|
||||||
throw notFound()
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
},
|
|
||||||
notFoundComponent: () => {
|
|
||||||
return (
|
|
||||||
<NotFoundPage
|
|
||||||
title="Materia no encontrada"
|
|
||||||
message="La asignatura que buscas no existe o fue eliminada."
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
component: RouteComponent,
|
|
||||||
})
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
// const { planId, asignaturaId } = Route.useParams()
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
// Confetti al llegar desde creación
|
|
||||||
useEffect(() => {
|
|
||||||
if ((location.state as any)?.showConfetti) {
|
|
||||||
lateralConfetti()
|
|
||||||
window.history.replaceState({}, document.title) // Limpiar el estado para que no se repita
|
|
||||||
}
|
|
||||||
}, [location.state])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<AsignaturaDetailPage></AsignaturaDetailPage>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia',
|
||||||
|
)({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <Outlet />
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { BibliographyItem } from '@/components/asignaturas/detalle/BibliographyItem'
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/',
|
||||||
|
)({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <BibliographyItem />
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { NuevaBibliografiaModalContainer } from '@/features/bibliografia/nueva/NuevaBibliografiaModalContainer'
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva',
|
||||||
|
)({
|
||||||
|
component: NuevaBibliografiaModal,
|
||||||
|
})
|
||||||
|
|
||||||
|
function NuevaBibliografiaModal() {
|
||||||
|
const { planId, asignaturaId } = Route.useParams()
|
||||||
|
return (
|
||||||
|
<NuevaBibliografiaModalContainer
|
||||||
|
planId={planId}
|
||||||
|
asignaturaId={asignaturaId}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { ContenidoTematico } from '@/components/asignaturas/detalle/ContenidoTematico'
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/contenido',
|
||||||
|
)({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <ContenidoTematico />
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { createFileRoute, useParams } from '@tanstack/react-router'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { DocumentoSEPTab } from '@/components/asignaturas/detalle/DocumentoSEPTab'
|
||||||
|
import { useSubject } from '@/data'
|
||||||
|
import { fetchAsignaturaPdf } from '@/data/api/document.api'
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/documento',
|
||||||
|
)({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { asignaturaId } = useParams({
|
||||||
|
from: '/planes/$planId/asignaturas/$asignaturaId/documento',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: asignatura } = useSubject(asignaturaId)
|
||||||
|
const asignaturaFileBaseName = sanitizeFileBaseName(
|
||||||
|
asignatura?.nombre ?? 'documento_sep',
|
||||||
|
)
|
||||||
|
|
||||||
|
const [pdfUrl, setPdfUrl] = useState<string | null>(null)
|
||||||
|
const pdfUrlRef = useRef<string | null>(null)
|
||||||
|
const isMountedRef = useRef<boolean>(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [isRegenerating, setIsRegenerating] = useState(false)
|
||||||
|
|
||||||
|
const loadPdfPreview = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
if (isMountedRef.current) setIsLoading(true)
|
||||||
|
|
||||||
|
const pdfBlob = await fetchAsignaturaPdf({
|
||||||
|
asignatura_id: asignaturaId,
|
||||||
|
convertTo: 'pdf',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isMountedRef.current) return
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(pdfBlob)
|
||||||
|
|
||||||
|
if (pdfUrlRef.current) window.URL.revokeObjectURL(pdfUrlRef.current)
|
||||||
|
pdfUrlRef.current = url
|
||||||
|
setPdfUrl(url)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cargando PDF:', error)
|
||||||
|
} finally {
|
||||||
|
if (isMountedRef.current) setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [asignaturaId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true
|
||||||
|
loadPdfPreview()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false
|
||||||
|
if (pdfUrlRef.current) window.URL.revokeObjectURL(pdfUrlRef.current)
|
||||||
|
}
|
||||||
|
}, [loadPdfPreview])
|
||||||
|
|
||||||
|
const handleDownloadPdf = async () => {
|
||||||
|
const pdfBlob = await fetchAsignaturaPdf({
|
||||||
|
asignatura_id: asignaturaId,
|
||||||
|
convertTo: 'pdf',
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(pdfBlob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `${asignaturaFileBaseName}.pdf`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownloadWord = async () => {
|
||||||
|
const docBlob = await fetchAsignaturaPdf({
|
||||||
|
asignatura_id: asignaturaId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(docBlob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `${asignaturaFileBaseName}.docx`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRegenerate = async () => {
|
||||||
|
try {
|
||||||
|
setIsRegenerating(true)
|
||||||
|
|
||||||
|
await loadPdfPreview()
|
||||||
|
} finally {
|
||||||
|
setIsRegenerating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentoSEPTab
|
||||||
|
pdfUrl={pdfUrl}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onDownloadPdf={handleDownloadPdf}
|
||||||
|
onDownloadWord={handleDownloadWord}
|
||||||
|
onRegenerate={handleRegenerate}
|
||||||
|
isRegenerating={isRegenerating}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeFileBaseName(input: string): string {
|
||||||
|
const text = String(input)
|
||||||
|
const withoutControlChars = Array.from(text)
|
||||||
|
.filter((ch) => {
|
||||||
|
const code = ch.charCodeAt(0)
|
||||||
|
return code >= 32 && code !== 127
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
const cleaned = withoutControlChars
|
||||||
|
.replace(/[<>:"/\\|?*]+/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.replace(/[. ]+$/g, '')
|
||||||
|
|
||||||
|
return (cleaned || 'documento').slice(0, 150)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { HistorialTab } from '@/components/asignaturas/detalle/HistorialTab'
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/historial',
|
||||||
|
)({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <HistorialTab />
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { IAAsignaturaTab } from '@/components/asignaturas/detalle/IAAsignaturaTab'
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/iaasignatura',
|
||||||
|
)({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <IAAsignaturaTab />
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage'
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId/',
|
||||||
|
)({
|
||||||
|
component: DatosGeneralesPage,
|
||||||
|
})
|
||||||
|
|
||||||
|
function DatosGeneralesPage() {
|
||||||
|
return <AsignaturaDetailPage />
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user