7 Commits

32 changed files with 3570 additions and 2078 deletions

263
bun.lock
View File

@@ -4,12 +4,9 @@
"": { "": {
"name": "acad-ia-2", "name": "acad-ia-2",
"dependencies": { "dependencies": {
"@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",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-scroll-area": "^1.2.10",
@@ -19,7 +16,6 @@
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@stepperize/react": "^5.1.9", "@stepperize/react": "^5.1.9",
"@supabase/supabase-js": "^2.90.1",
"@tailwindcss/vite": "^4.0.6", "@tailwindcss/vite": "^4.0.6",
"@tanstack/react-devtools": "^0.7.0", "@tanstack/react-devtools": "^0.7.0",
"@tanstack/react-query": "^5.66.5", "@tanstack/react-query": "^5.66.5",
@@ -30,7 +26,6 @@
"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",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"motion": "^12.24.7", "motion": "^12.24.7",
"react": "^19.2.0", "react": "^19.2.0",
@@ -38,7 +33,6 @@
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.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",
}, },
"devDependencies": { "devDependencies": {
"@tanstack/devtools-vite": "^0.3.11", "@tanstack/devtools-vite": "^0.3.11",
@@ -58,7 +52,6 @@
"jsdom": "^27.0.0", "jsdom": "^27.0.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.7.2", "prettier-plugin-tailwindcss": "^0.7.2",
"supabase": "^2.72.2",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^7.1.7", "vite": "^7.1.7",
"vitest": "^3.0.5", "vitest": "^3.0.5",
@@ -67,7 +60,7 @@
}, },
}, },
"packages": { "packages": {
"@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="], "@acemir/cssom": ["@acemir/cssom@0.9.30", "", {}, "sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg=="],
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.4" } }, "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ=="], "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.4" } }, "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ=="],
@@ -75,23 +68,23 @@
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
"@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
"@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
"@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
@@ -99,25 +92,25 @@
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
"@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="],
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
"@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
"@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
@@ -127,7 +120,7 @@
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.25", "", {}, "sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q=="], "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.22", "", {}, "sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw=="],
"@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=="],
@@ -229,8 +222,6 @@
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -247,21 +238,17 @@
"@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-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-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=="],
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "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-presence": "1.1.5", "@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" }, "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-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@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-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-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@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-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-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
"@radix-ui/react-compose-refs": ["@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-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], "@radix-ui/react-compose-refs": ["@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-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
"@radix-ui/react-dialog": ["@radix-ui/react-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-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-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-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], "@radix-ui/react-dialog": ["@radix-ui/react-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-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-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-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
@@ -269,8 +256,6 @@
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "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-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "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-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "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-menu": "2.1.16", "@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-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
"@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=="],
@@ -279,8 +264,6 @@
"@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-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=="],
@@ -289,12 +272,10 @@
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@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-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@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-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@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.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-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-select": ["@radix-ui/react-select@2.2.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-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-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.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", "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-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], "@radix-ui/react-select": ["@radix-ui/react-select@2.2.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-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-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.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", "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-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
"@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=="],
@@ -397,19 +378,7 @@
"@stepperize/react": ["@stepperize/react@5.1.9", "", { "dependencies": { "@stepperize/core": "1.2.7" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yBgw1I5Tx6/qZB4xTdVBaPGfTqH5aYS1WFB5vtR8+fwPeqd3YNuOnQ1pJM6w/xV/gvryuy31hbFw080lZc+/hw=="], "@stepperize/react": ["@stepperize/react@5.1.9", "", { "dependencies": { "@stepperize/core": "1.2.7" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yBgw1I5Tx6/qZB4xTdVBaPGfTqH5aYS1WFB5vtR8+fwPeqd3YNuOnQ1pJM6w/xV/gvryuy31hbFw080lZc+/hw=="],
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.7.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/types": "^8.52.0", "eslint-visitor-keys": "^5.0.0", "espree": "^11.0.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-PsSugIf9ip1H/mWKj4bi/BlEoerxXAda9ByRFsYuwsmr6af9NxJL0AaiNXs8Le7R21QR5KMiD/KdxZZ71LjAxQ=="], "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.6.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.47.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw=="],
"@supabase/auth-js": ["@supabase/auth-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-vxb66dgo6h3yyPbR06735Ps+dK3hj0JwS8w9fdQPVZQmocSTlKUW5MfxSy99mN0XqCCuLMQ3jCEiIIUU23e9ng=="],
"@supabase/functions-js": ["@supabase/functions-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-x9mV9dF1Lam9qL3zlpP6mSM5C9iqMPtF5B/tU1Jj/F0ufX5mjDf9ghVBaErVxmrQJRL4+iMKWKY2GnODkpS8tw=="],
"@supabase/postgrest-js": ["@supabase/postgrest-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-jh6vqzaYzoFn3raaC0hcFt9h+Bt+uxNRBSdc7PfToQeRGk7PDPoweHsbdiPWREtDVTGKfu+PyPW9e2jbK+BCgQ=="],
"@supabase/realtime-js": ["@supabase/realtime-js@2.90.1", "", { "dependencies": { "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" } }, "sha512-PWbnEMkcQRuor8jhObp4+Snufkq8C6fBp+MchVp2qBPY1NXk/c3Iv3YyiFYVzo0Dzuw4nAlT4+ahuPggy4r32w=="],
"@supabase/storage-js": ["@supabase/storage-js@2.90.1", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-GHY+Ps/K/RBfRj7kwx+iVf2HIdqOS43rM2iDOIDpapyUnGA9CCBFzFV/XvfzznGykd//z2dkGZhlZZprsVFqGg=="],
"@supabase/supabase-js": ["@supabase/supabase-js@2.90.1", "", { "dependencies": { "@supabase/auth-js": "2.90.1", "@supabase/functions-js": "2.90.1", "@supabase/postgrest-js": "2.90.1", "@supabase/realtime-js": "2.90.1", "@supabase/storage-js": "2.90.1" } }, "sha512-U8KaKGLUgTIFHtwEW1dgw1gK7XrdpvvYo7nzzqPx721GqPe8WZbAiLh/hmyKLGBYQ/mmQNr20vU9tWSDZpii3w=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
@@ -467,19 +436,19 @@
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.2", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.14", "react": "^18 || ^19" } }, "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg=="], "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.2", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.14", "react": "^18 || ^19" } }, "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg=="],
"@tanstack/react-router": ["@tanstack/react-router@1.147.3", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.147.1", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Fp9DoszYiIJclwxU43kyP/cqcWD418DPmV6yhmIOuVedsSMnfh2g7uRQ+bOoaWn996JjuU9yt/x48h66aCQSQA=="], "@tanstack/react-router": ["@tanstack/react-router@1.145.7", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.145.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-0O+a4TjJSPXd2BsvDPwDPBKRQKYqNIBg5TAg9NzCteqJ0NXRxwohyqCksHqCEEtJe/uItwqmHoqkK4q5MDhEsA=="],
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.149.0", "", { "dependencies": { "@tanstack/router-devtools-core": "1.149.0" }, "peerDependencies": { "@tanstack/react-router": "^1.147.3", "@tanstack/router-core": "^1.147.1", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-QJ6epMhRKTS8WrBmcMFjK1v+jDaimMQuySCSNA8NR1ZROKv3xx0gY8AjyVVgQ1h78HSXXRMYH3aql2kWYjc31g=="], "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.145.7", "", { "dependencies": { "@tanstack/router-devtools-core": "1.145.7" }, "peerDependencies": { "@tanstack/react-router": "^1.145.7", "@tanstack/router-core": "^1.145.7", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-crzHSQ/rcGX7RfuYsmm1XG5quurNMDTIApU7jfwDx5J9HnUxCOSJrbFX0L3w0o0VRCw5xhrL2EdCnW78Ic86hg=="],
"@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="], "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="],
"@tanstack/router-core": ["@tanstack/router-core@1.147.1", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-yf8o3CNgJVGO5JnIqiTe0y2eChxEM0w7TrEs1VSumL/zz2bQroYGNr1mOXJ2VeN+7YfJJwjEqq71P5CzWwMzRg=="], "@tanstack/router-core": ["@tanstack/router-core@1.145.7", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-v6jx6JqVUBM0/FcBq1tX22xiPq8Ufc0PDEP582/4deYoq2/RYd+bZstANp3mGSsqdxE/luhoLYuuSQiwi/j1wA=="],
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.149.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.147.1", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-dy9xb8U9VWAavqKM0sTFhAs2ufVs3d/cGSbqczIgBcAKCjjbsAng1gV4ezPXmfF1pa+2MW6n7SViXsxxvtCRiw=="], "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.145.7", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.145.7", "csstype": "^3.0.10", "solid-js": ">=1.9.5" }, "optionalPeers": ["csstype"] }, "sha512-oKeq/6QvN49THCh++FJyPv1X65i20qGS4aJHQTNsl4cu1piW1zWUhab2L3DZVr3G8C40FW3xb6hVw92N/fzZbQ=="],
"@tanstack/router-generator": ["@tanstack/router-generator@1.149.0", "", { "dependencies": { "@tanstack/router-core": "1.147.1", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-H+SZbJ9j4G+y/329LlRLb//4sBdPNQpuMddb/rgkfoRZpdztm9Ejm9EEbMJB0rkNDrSgfSPOZ6VtJbndYH/AQA=="], "@tanstack/router-generator": ["@tanstack/router-generator@1.145.7", "", { "dependencies": { "@tanstack/router-core": "1.145.7", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-xg71c1WTku0ro0rgpJWh3Dt+ognV9qWe2KJHAPzrqfOYdUYu9sGq7Ri4jo8Rk0luXWZrWsrFdBP+9Jx6JH6zWA=="],
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.149.0", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.147.1", "@tanstack/router-generator": "1.149.0", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.147.3", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-DYPScneHZ0fm3FJyDhkUCW0w0dOopAKvep57n/Ft2b3RrHSeSeJB/cJWgsUvpcYJfpywkyOLyqVLMoiDvLoG/A=="], "@tanstack/router-plugin": ["@tanstack/router-plugin@1.145.7", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.145.7", "@tanstack/router-generator": "1.145.7", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.145.7", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-Rimo0NragYKHwjoYX9JBLS8VkZD4D/LqzzLIlX9yz93lmWFRu/DbuS7fDZNqX1Ea8naNvo18DlySszYLzC8XDg=="],
"@tanstack/router-utils": ["@tanstack/router-utils@1.143.11", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-N24G4LpfyK8dOlnP8BvNdkuxg1xQljkyl6PcrdiPSA301pOjatRT1y8wuCCJZKVVD8gkd0MpCZ0VEjRMGILOtA=="], "@tanstack/router-utils": ["@tanstack/router-utils@1.143.11", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-N24G4LpfyK8dOlnP8BvNdkuxg1xQljkyl6PcrdiPSA301pOjatRT1y8wuCCJZKVVD8gkd0MpCZ0VEjRMGILOtA=="],
@@ -513,35 +482,31 @@
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@types/node": ["@types/node@22.19.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q=="], "@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="],
"@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="], "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
"@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/type-utils": "8.53.0", "@typescript-eslint/utils": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.52.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg=="], "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.52.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.52.0", "@typescript-eslint/types": "^8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.0", "@typescript-eslint/types": "^8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="], "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.52.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="], "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.52.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.0", "@typescript-eslint/tsconfig-utils": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw=="],
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
@@ -641,7 +606,7 @@
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"axe-core": ["axe-core@4.11.1", "", {}, "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A=="], "axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@@ -649,12 +614,10 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
"bin-links": ["bin-links@6.0.0", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
@@ -673,7 +636,7 @@
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="], "caniuse-lite": ["caniuse-lite@1.0.30001762", "", {}, "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw=="],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
@@ -683,14 +646,10 @@
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"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=="],
"cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="],
"cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -709,14 +668,12 @@
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
"cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="], "cssstyle": ["cssstyle@5.3.6", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-legscpSpgSAeGEe0TNcai97DKt9Vd9AsAdOL7Uoetb52Ar/8eJm3LIa39qpv8wWzLFlNG4vVvppQM+teaMPj3A=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"data-urls": ["data-urls@6.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="], "data-urls": ["data-urls@6.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="],
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
@@ -725,8 +682,6 @@
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
@@ -745,7 +700,7 @@
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
@@ -813,9 +768,9 @@
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.0", "", {}, "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q=="], "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"espree": ["espree@11.0.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-+gMeWRrIh/NsG+3NaLeWHuyeyk70p2tbvZIWBYcqQ4/7Xvars6GYTZNhF1sIeLcc6Wb11He5ffz3hsHyXFrw5A=="], "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
@@ -839,8 +794,6 @@
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -853,9 +806,7 @@
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "framer-motion": ["framer-motion@12.24.7", "", { "dependencies": { "motion-dom": "^12.24.3", "motion-utils": "^12.23.28", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-EolFLm7NdEMhWO/VTMZ0LlR4fLHGDiJItTx3i8dlyQooOOBoYAaysK4paGD4PrwqnoDdeDOS+TxnSBIAnNHs3w=="],
"framer-motion": ["framer-motion@12.26.1", "", { "dependencies": { "motion-dom": "^12.24.11", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Uzc8wGldU4FpmGotthjjcj0SZhigcODjqvKT7lzVZHsmYkzQMFfMIv0vHQoXCeoe/Ahxqp4by4A6QbzFA/lblw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -911,14 +862,12 @@
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"iceberg-js": ["iceberg-js@0.8.1", "", {}, "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@@ -1065,15 +1014,11 @@
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "motion": ["motion@12.24.7", "", { "dependencies": { "framer-motion": "^12.24.7", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-0jOoqFlQ7JBvAcRhRv28pwUgZ1xw9WS4+tCU6aqYjxgiNVZCVi34ED2cihW3EgjIIWPBoZJis5og1mx/LmQWVQ=="],
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], "motion-dom": ["motion-dom@12.24.3", "", { "dependencies": { "motion-utils": "^12.23.28" } }, "sha512-ZjMZCwhTglim0LM64kC1iFdm4o+2P9IKk3rl/Nb4RKsb5p4O9HJ1C2LWZXOFdsRtp6twpqWRXaFKOduF30ntow=="],
"motion": ["motion@12.26.1", "", { "dependencies": { "framer-motion": "^12.26.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-IVhzx9HOQTiJ9ykthMOlZPnLwrkXziN5Q/yebsqBYlFJb2rHP8yhmKc8O/YUT9byPJlxOeqkzfNYCrVKZx8vqg=="], "motion-utils": ["motion-utils@12.23.28", "", {}, "sha512-0W6cWd5Okoyf8jmessVK3spOmbyE0yTdNKujHctHH9XdAE4QDuZ1/LjSXC68rrhsJU+TkzXURC5OdSWh9ibOwQ=="],
"motion-dom": ["motion-dom@12.24.11", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-DlWOmsXMJrV8lzZyd+LKjG2CXULUs++bkq8GZ2Sr0R0RRhs30K2wtY+LKiTjhmJU3W61HK+rB0GLz6XmPvTA1A=="],
"motion-utils": ["motion-utils@12.24.10", "", {}, "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -1083,16 +1028,10 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"npm-normalize-package-bin": ["npm-normalize-package-bin@5.0.0", "", {}, "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
@@ -1143,8 +1082,6 @@
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
@@ -1161,8 +1098,6 @@
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
@@ -1219,8 +1154,6 @@
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="], "solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
@@ -1249,8 +1182,6 @@
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
"supabase": ["supabase@2.72.4", "", { "dependencies": { "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", "tar": "7.5.2" }, "bin": { "supabase": "bin/supabase" } }, "sha512-7jbpr9svviXihYhUqBK7k7U3aRo4x8OVSkzxrl+cpH4svDS5+Rl605DW9ijYFeRuNQZEvBkRaJQ93bGORWNFqQ=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
@@ -1263,8 +1194,6 @@
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
@@ -1315,7 +1244,7 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.53.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.0", "@typescript-eslint/parser": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw=="], "typescript-eslint": ["typescript-eslint@8.52.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.52.0", "@typescript-eslint/parser": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA=="],
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
@@ -1331,13 +1260,11 @@
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-debounce": ["use-debounce@10.1.0", "", { "peerDependencies": { "react": "*" } }, "sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"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=="],
"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.0", "", { "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-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
"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=="],
@@ -1347,8 +1274,6 @@
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="], "web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="],
"webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
@@ -1373,15 +1298,13 @@
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"write-file-atomic": ["write-file-atomic@7.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg=="],
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
@@ -1393,34 +1316,64 @@
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@radix-ui/react-alert-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-arrow/@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-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="], "@radix-ui/react-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-avatar/@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-checkbox/@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-collection/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-collection/@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-collection/@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-collection/@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-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-dialog/@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-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-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-dismissable-layer/@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-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-focus-scope/@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-popover/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-popover/@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-popover/@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-popover/@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-primitive/@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-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-popper/@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-portal/@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-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-roving-focus/@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-select/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-select/@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-select/@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-select/@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-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-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-tabs/@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-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-tooltip/@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-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=="],
"@radix-ui/react-visually-hidden/@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=="],
"@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=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
@@ -1441,20 +1394,12 @@
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"cmdk/@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=="],
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"eslint/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"eslint/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"eslint-compat-utils/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "eslint-compat-utils/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
@@ -1487,15 +1432,25 @@
"tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
"vue-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"vue-eslint-parser/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"vue-eslint-parser/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "vue-eslint-parser/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@radix-ui/react-arrow/@radix-ui/react-primitive/@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=="],
"@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], "@radix-ui/react-checkbox/@radix-ui/react-primitive/@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-dismissable-layer/@radix-ui/react-primitive/@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-focus-scope/@radix-ui/react-primitive/@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-popper/@radix-ui/react-primitive/@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-portal/@radix-ui/react-primitive/@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-roving-focus/@radix-ui/react-primitive/@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-tabs/@radix-ui/react-primitive/@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-visually-hidden/@radix-ui/react-primitive/@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=="],
"@tanstack/devtools/@tanstack/devtools-client/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.5", "", {}, "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw=="], "@tanstack/devtools/@tanstack/devtools-client/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.5", "", {}, "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw=="],

View File

@@ -58,6 +58,7 @@
"@tanstack/eslint-config": "^0.3.0", "@tanstack/eslint-config": "^0.3.0",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.2.0",
"@types/bun": "^1.3.6",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/react": "^19.2.0", "@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0", "@types/react-dom": "^19.2.0",

19
scripts/update-types.ts Normal file
View File

@@ -0,0 +1,19 @@
// scripts/update-types.ts
/* Uso:
bun run scripts/update-types.ts
*/
import { $ } from "bun";
console.log("🔄 Generando tipos de Supabase...");
try {
// Ejecutamos el comando y capturamos la salida como texto
const output = await $`supabase gen types typescript --linked`.text();
// Escribimos el archivo directamente con Bun (garantiza UTF-8)
await Bun.write("src/types/supabase.ts", output);
console.log("✅ Tipos actualizados correctamente con acentos.");
} catch (error) {
console.error("❌ Error generando tipos:", error);
}

View File

@@ -1,14 +1,44 @@
import { useState } from 'react'; import { useEffect, useState } from 'react'
import { Plus, Search, BookOpen, Trash2, Library, Edit3, Save } from 'lucide-react'; import {
import { Card, CardContent } from '@/components/ui/card'; Plus,
import { Button } from '@/components/ui/button'; Search,
import { Input } from '@/components/ui/input'; BookOpen,
import { Textarea } from '@/components/ui/textarea'; Trash2,
import { Badge } from '@/components/ui/badge'; Library,
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; Edit3,
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; Save,
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; } from 'lucide-react'
import { cn } from '@/lib/utils'; import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { cn } from '@/lib/utils'
import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
//import { toast } from 'sonner'; //import { toast } from 'sonner';
//import { mockLibraryResources } from '@/data/mockMateriaData'; //import { mockLibraryResources } from '@/data/mockMateriaData';
@@ -20,7 +50,7 @@ export const mockLibraryResources = [
editorial: 'MIT Press', editorial: 'MIT Press',
anio: 2016, anio: 2016,
isbn: '9780262035613', isbn: '9780262035613',
disponible: true disponible: true,
}, },
{ {
id: 'lib-2', id: 'lib-2',
@@ -29,102 +59,154 @@ export const mockLibraryResources = [
editorial: 'Pearson', editorial: 'Pearson',
anio: 2020, anio: 2020,
isbn: '9780134610993', isbn: '9780134610993',
disponible: true disponible: true,
}, },
{ {
id: 'lib-3', id: 'lib-3',
titulo: 'Hands-On Machine Learning', titulo: 'Hands-On Machine Learning',
autor: 'Aurélien Géron', autor: 'Aurélien Géron',
editorial: 'O\'Reilly Media', editorial: "O'Reilly Media",
anio: 2019, anio: 2019,
isbn: '9781492032649', isbn: '9781492032649',
disponible: false disponible: false,
} },
]; ]
// --- Interfaces --- // --- Interfaces ---
export interface BibliografiaEntry { export interface BibliografiaEntry {
id: string; id: string
tipo: 'BASICA' | 'COMPLEMENTARIA'; tipo: 'BASICA' | 'COMPLEMENTARIA'
cita: string; cita: string
fuenteBibliotecaId?: string; tipo_fuente?: 'MANUAL' | 'BIBLIOTECA'
fuenteBiblioteca?: any; biblioteca_item_id?: string | null
fuenteBibliotecaId?: string
fuenteBiblioteca?: any
} }
interface BibliografiaTabProps { interface BibliografiaTabProps {
bibliografia: BibliografiaEntry[]; bibliografia: BibliografiaEntry[]
onSave: (bibliografia: BibliografiaEntry[]) => void; onSave: (bibliografia: BibliografiaEntry[]) => void
isSaving: boolean; isSaving: boolean
} }
export function BibliographyItem({ bibliografia, onSave, isSaving }: BibliografiaTabProps) { export function BibliographyItem({
const [entries, setEntries] = useState<BibliografiaEntry[]>(bibliografia); bibliografia,
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); onSave,
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false); isSaving,
const [deleteId, setDeleteId] = useState<string | null>(null); }: BibliografiaTabProps) {
const [editingId, setEditingId] = useState<string | null>(null); const { data: bibliografia2, isLoading: loadinmateria } =
const [newEntryType, setNewEntryType] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA'); useSubjectBibliografia('9d4dda6a-488f-428a-8a07-38081592a641')
const [entries, setEntries] = useState<BibliografiaEntry[]>(bibliografia)
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false)
const [deleteId, setDeleteId] = useState<string | null>(null)
const [editingId, setEditingId] = useState<string | null>(null)
const [newEntryType, setNewEntryType] = useState<'BASICA' | 'COMPLEMENTARIA'>(
'BASICA',
)
const basicaEntries = entries.filter(e => e.tipo === 'BASICA'); useEffect(() => {
const complementariaEntries = entries.filter(e => e.tipo === 'COMPLEMENTARIA'); if (bibliografia2 && Array.isArray(bibliografia2)) {
setEntries(bibliografia2)
} else if (bibliografia) {
// 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',
)
console.log(bibliografia2)
const handleAddManual = (cita: string) => { const handleAddManual = (cita: string) => {
const newEntry: BibliografiaEntry = { id: `manual-${Date.now()}`, tipo: newEntryType, cita }; const newEntry: BibliografiaEntry = {
setEntries([...entries, newEntry]); id: `manual-${Date.now()}`,
setIsAddDialogOpen(false); tipo: newEntryType,
cita,
}
setEntries([...entries, newEntry])
setIsAddDialogOpen(false)
//toast.success('Referencia manual añadida'); //toast.success('Referencia manual añadida');
}; }
const handleAddFromLibrary = (resource: any, tipo: 'BASICA' | 'COMPLEMENTARIA') => { const handleAddFromLibrary = (
const cita = `${resource.autor} (${resource.anio}). ${resource.titulo}. ${resource.editorial}.`; resource: any,
tipo: 'BASICA' | 'COMPLEMENTARIA',
) => {
const cita = `${resource.autor} (${resource.anio}). ${resource.titulo}. ${resource.editorial}.`
const newEntry: BibliografiaEntry = { const newEntry: BibliografiaEntry = {
id: `lib-ref-${Date.now()}`, id: `lib-ref-${Date.now()}`,
tipo, tipo,
cita, cita,
fuenteBibliotecaId: resource.id, fuenteBibliotecaId: resource.id,
fuenteBiblioteca: resource, fuenteBiblioteca: resource,
}; }
setEntries([...entries, newEntry]); setEntries([...entries, newEntry])
setIsLibraryDialogOpen(false); setIsLibraryDialogOpen(false)
//toast.success('Añadido desde biblioteca'); //toast.success('Añadido desde biblioteca');
}; }
const handleUpdateCita = (id: string, cita: string) => { const handleUpdateCita = (id: string, cita: string) => {
setEntries(entries.map(e => e.id === id ? { ...e, cita } : e)); setEntries(entries.map((e) => (e.id === id ? { ...e, cita } : e)))
}; }
return ( return (
<div className="max-w-5xl mx-auto py-10 space-y-8 animate-in fade-in duration-500"> <div className="animate-in fade-in mx-auto max-w-5xl space-y-8 py-10 duration-500">
<div className="flex items-center justify-between border-b pb-4"> <div className="flex items-center justify-between border-b pb-4">
<div> <div>
<h2 className="text-2xl font-bold text-slate-900 tracking-tight">Bibliografía</h2> <h2 className="text-2xl font-bold tracking-tight text-slate-900">
<p className="text-sm text-slate-500 mt-1"> Bibliografía
{basicaEntries.length} básica {complementariaEntries.length} complementaria </h2>
<p className="mt-1 text-sm text-slate-500">
{basicaEntries.length} básica {complementariaEntries.length}{' '}
complementaria
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Dialog open={isLibraryDialogOpen} onOpenChange={setIsLibraryDialogOpen}> <Dialog
open={isLibraryDialogOpen}
onOpenChange={setIsLibraryDialogOpen}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" className="border-blue-200 text-blue-700 hover:bg-blue-50"> <Button
<Library className="w-4 h-4 mr-2" /> Buscar en biblioteca variant="outline"
className="border-blue-200 text-blue-700 hover:bg-blue-50"
>
<Library className="mr-2 h-4 w-4" /> Buscar en biblioteca
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<LibrarySearchDialog onSelect={handleAddFromLibrary} existingIds={entries.map(e => e.fuenteBibliotecaId || '')} /> <LibrarySearchDialog
onSelect={handleAddFromLibrary}
existingIds={entries.map((e) => e.fuenteBibliotecaId || '')}
/>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}> <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline"><Plus className="w-4 h-4 mr-2" /> Añadir manual</Button> <Button variant="outline">
<Plus className="mr-2 h-4 w-4" /> Añadir manual
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<AddManualDialog tipo={newEntryType} onTypeChange={setNewEntryType} onAdd={handleAddManual} /> <AddManualDialog
tipo={newEntryType}
onTypeChange={setNewEntryType}
onAdd={handleAddManual}
/>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Button onClick={() => onSave(entries)} disabled={isSaving} className="bg-blue-600 hover:bg-blue-700"> <Button
<Save className="w-4 h-4 mr-2" /> {isSaving ? 'Guardando...' : 'Guardar'} onClick={() => onSave(entries)}
disabled={isSaving}
className="bg-blue-600 hover:bg-blue-700"
>
<Save className="mr-2 h-4 w-4" />{' '}
{isSaving ? 'Guardando...' : 'Guardar'}
</Button> </Button>
</div> </div>
</div> </div>
@@ -133,11 +215,13 @@ export function BibliographyItem({ bibliografia, onSave, isSaving }: Bibliografi
{/* BASICA */} {/* BASICA */}
<section className="space-y-4"> <section className="space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-4 w-1 bg-blue-600 rounded-full" /> <div className="h-4 w-1 rounded-full bg-blue-600" />
<h3 className="font-semibold text-slate-800">Bibliografía Básica</h3> <h3 className="font-semibold text-slate-800">
Bibliografía Básica
</h3>
</div> </div>
<div className="grid gap-3"> <div className="grid gap-3">
{basicaEntries.map(entry => ( {basicaEntries.map((entry) => (
<BibliografiaCard <BibliografiaCard
key={entry.id} key={entry.id}
entry={entry} entry={entry}
@@ -154,11 +238,13 @@ export function BibliographyItem({ bibliografia, onSave, isSaving }: Bibliografi
{/* COMPLEMENTARIA */} {/* COMPLEMENTARIA */}
<section className="space-y-4"> <section className="space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-4 w-1 bg-slate-400 rounded-full" /> <div className="h-4 w-1 rounded-full bg-slate-400" />
<h3 className="font-semibold text-slate-800">Bibliografía Complementaria</h3> <h3 className="font-semibold text-slate-800">
Bibliografía Complementaria
</h3>
</div> </div>
<div className="grid gap-3"> <div className="grid gap-3">
{complementariaEntries.map(entry => ( {complementariaEntries.map((entry) => (
<BibliografiaCard <BibliografiaCard
key={entry.id} key={entry.id}
entry={entry} entry={entry}
@@ -177,70 +263,143 @@ export function BibliographyItem({ bibliografia, onSave, isSaving }: Bibliografi
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>¿Eliminar referencia?</AlertDialogTitle> <AlertDialogTitle>¿Eliminar referencia?</AlertDialogTitle>
<AlertDialogDescription>La referencia será quitada del plan de estudios.</AlertDialogDescription> <AlertDialogDescription>
La referencia será quitada del plan de estudios.
</AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel> <AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={() => { setEntries(entries.filter(e => e.id !== deleteId)); setDeleteId(null); }} className="bg-red-600">Eliminar</AlertDialogAction> <AlertDialogAction
onClick={() => {
setEntries(entries.filter((e) => e.id !== deleteId))
setDeleteId(null)
}}
className="bg-red-600"
>
Eliminar
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div> </div>
); )
} }
// --- Subcomponentes --- // --- Subcomponentes ---
function BibliografiaCard({ entry, isEditing, onEdit, onStopEditing, onUpdateCita, onDelete }: any) { function BibliografiaCard({
const [localCita, setLocalCita] = useState(entry.cita); entry,
isEditing,
onEdit,
onStopEditing,
onUpdateCita,
onDelete,
}: any) {
const [localCita, setLocalCita] = useState(entry.cita)
return ( return (
<Card className={cn("group transition-all hover:shadow-md", isEditing && "ring-2 ring-blue-500")}> <Card
className={cn(
'group transition-all hover:shadow-md',
isEditing && 'ring-2 ring-blue-500',
)}
>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<BookOpen className={cn("w-5 h-5 mt-1", entry.tipo === 'BASICA' ? "text-blue-600" : "text-slate-400")} /> <BookOpen
<div className="flex-1 min-w-0"> className={cn(
'mt-1 h-5 w-5',
entry.tipo === 'BASICA' ? 'text-blue-600' : 'text-slate-400',
)}
/>
<div className="min-w-0 flex-1">
{isEditing ? ( {isEditing ? (
<div className="space-y-2"> <div className="space-y-2">
<Textarea value={localCita} onChange={(e) => setLocalCita(e.target.value)} className="min-h-[80px]" /> <Textarea
value={localCita}
onChange={(e) => setLocalCita(e.target.value)}
className="min-h-[80px]"
/>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="ghost" size="sm" onClick={onStopEditing}>Cancelar</Button> <Button variant="ghost" size="sm" onClick={onStopEditing}>
<Button size="sm" className="bg-emerald-600" onClick={() => { onUpdateCita(entry.id, localCita); onStopEditing(); }}>Guardar</Button> Cancelar
</Button>
<Button
size="sm"
className="bg-emerald-600"
onClick={() => {
onUpdateCita(entry.id, localCita)
onStopEditing()
}}
>
Guardar
</Button>
</div> </div>
</div> </div>
) : ( ) : (
<div onClick={onEdit} className="cursor-pointer"> <div onClick={onEdit} className="cursor-pointer">
<p className="text-sm leading-relaxed text-slate-700">{entry.cita}</p> <p className="text-sm leading-relaxed text-slate-700">
{entry.cita}
</p>
{entry.fuenteBiblioteca && ( {entry.fuenteBiblioteca && (
<div className="flex gap-2 mt-2"> <div className="mt-2 flex gap-2">
<Badge variant="secondary" className="text-[10px] bg-slate-100 text-slate-600">Biblioteca</Badge> <Badge
{entry.fuenteBiblioteca.disponible && <Badge className="text-[10px] bg-emerald-50 text-emerald-700 border-emerald-100">Disponible</Badge>} variant="secondary"
className="bg-slate-100 text-[10px] text-slate-600"
>
Biblioteca
</Badge>
{entry.fuenteBiblioteca.disponible && (
<Badge className="border-emerald-100 bg-emerald-50 text-[10px] text-emerald-700">
Disponible
</Badge>
)}
</div> </div>
)} )}
</div> </div>
)} )}
</div> </div>
{!isEditing && ( {!isEditing && (
<div className="flex opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex opacity-0 transition-opacity group-hover:opacity-100">
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-blue-600" onClick={onEdit}><Edit3 className="w-4 h-4" /></Button> <Button
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-red-500" onClick={onDelete}><Trash2 className="w-4 h-4" /></Button> variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-blue-600"
onClick={onEdit}
>
<Edit3 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-red-500"
onClick={onDelete}
>
<Trash2 className="h-4 w-4" />
</Button>
</div> </div>
)} )}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
); )
} }
function AddManualDialog({ tipo, onTypeChange, onAdd }: any) { function AddManualDialog({ tipo, onTypeChange, onAdd }: any) {
const [cita, setCita] = useState(''); const [cita, setCita] = useState('')
return ( return (
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<DialogHeader><DialogTitle>Referencia Manual</DialogTitle></DialogHeader> <DialogHeader>
<DialogTitle>Referencia Manual</DialogTitle>
</DialogHeader>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-bold uppercase text-slate-500">Tipo</label> <label className="text-xs font-bold text-slate-500 uppercase">
Tipo
</label>
<Select value={tipo} onValueChange={onTypeChange}> <Select value={tipo} onValueChange={onTypeChange}>
<SelectTrigger><SelectValue /></SelectTrigger> <SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="BASICA">Básica</SelectItem> <SelectItem value="BASICA">Básica</SelectItem>
<SelectItem value="COMPLEMENTARIA">Complementaria</SelectItem> <SelectItem value="COMPLEMENTARIA">Complementaria</SelectItem>
@@ -248,44 +407,78 @@ function AddManualDialog({ tipo, onTypeChange, onAdd }: any) {
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-bold uppercase text-slate-500">Cita APA</label> <label className="text-xs font-bold text-slate-500 uppercase">
<Textarea value={cita} onChange={(e) => setCita(e.target.value)} placeholder="Autor, A. (Año). Título..." className="min-h-[120px]" /> Cita APA
</label>
<Textarea
value={cita}
onChange={(e) => setCita(e.target.value)}
placeholder="Autor, A. (Año). Título..."
className="min-h-[120px]"
/>
</div> </div>
<Button onClick={() => onAdd(cita)} disabled={!cita.trim()} className="w-full bg-blue-600">Añadir a la lista</Button> <Button
onClick={() => onAdd(cita)}
disabled={!cita.trim()}
className="w-full bg-blue-600"
>
Añadir a la lista
</Button>
</div> </div>
); )
} }
function LibrarySearchDialog({ onSelect, existingIds }: any) { 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(r => const filtered = mockLibraryResources.filter(
!existingIds.includes(r.id) && r.titulo.toLowerCase().includes(search.toLowerCase()) (r) =>
); !existingIds.includes(r.id) &&
r.titulo.toLowerCase().includes(search.toLowerCase()),
)
return ( return (
<div className="space-y-4 py-2"> <div className="space-y-4 py-2">
<DialogHeader><DialogTitle>Catálogo de Biblioteca</DialogTitle></DialogHeader> <DialogHeader>
<DialogTitle>Catálogo de Biblioteca</DialogTitle>
</DialogHeader>
<div className="flex gap-2"> <div className="flex gap-2">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" /> <Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Buscar por título o autor..." className="pl-10" /> <Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Buscar por título o autor..."
className="pl-10"
/>
</div> </div>
<Select value={tipo} onValueChange={(v:any) => setTipo(v)}><SelectTrigger className="w-36"><SelectValue /></SelectTrigger> <Select value={tipo} onValueChange={(v: any) => setTipo(v)}>
<SelectContent><SelectItem value="BASICA">Básica</SelectItem><SelectItem value="COMPLEMENTARIA">Complem.</SelectItem></SelectContent> <SelectTrigger className="w-36">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="BASICA">Básica</SelectItem>
<SelectItem value="COMPLEMENTARIA">Complem.</SelectItem>
</SelectContent>
</Select> </Select>
</div> </div>
<div className="max-h-[300px] overflow-y-auto pr-2 space-y-2"> <div className="max-h-[300px] space-y-2 overflow-y-auto pr-2">
{filtered.map(res => ( {filtered.map((res) => (
<div key={res.id} onClick={() => onSelect(res, tipo)} className="p-3 border rounded-lg hover:bg-slate-50 cursor-pointer flex justify-between items-center group"> <div
key={res.id}
onClick={() => onSelect(res, tipo)}
className="group flex cursor-pointer items-center justify-between rounded-lg border p-3 hover:bg-slate-50"
>
<div> <div>
<p className="text-sm font-semibold text-slate-700">{res.titulo}</p> <p className="text-sm font-semibold text-slate-700">
{res.titulo}
</p>
<p className="text-xs text-slate-500">{res.autor}</p> <p className="text-xs text-slate-500">{res.autor}</p>
</div> </div>
<Plus className="w-4 h-4 text-blue-600 opacity-0 group-hover:opacity-100 transition-opacity" /> <Plus className="h-4 w-4 text-blue-600 opacity-0 transition-opacity group-hover:opacity-100" />
</div> </div>
))} ))}
</div> </div>
</div> </div>
); )
} }

View File

@@ -1,10 +1,23 @@
import { useState } from 'react'; import { useEffect, useState } from 'react'
import { Plus, GripVertical, ChevronDown, ChevronRight, Edit3, Trash2, Clock, Save } from 'lucide-react'; import {
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; Plus,
import { Button } from '@/components/ui/button'; GripVertical,
import { Input } from '@/components/ui/input'; ChevronDown,
import { Badge } from '@/components/ui/badge'; ChevronRight,
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; Edit3,
Trash2,
Clock,
Save,
} from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -14,24 +27,22 @@ import {
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog'
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils'
//import { toast } from 'sonner'; //import { toast } from 'sonner';
export interface Tema { export interface Tema {
id: string; id: string
nombre: string; nombre: string
descripcion?: string; descripcion?: string
horasEstimadas?: number; horasEstimadas?: number
} }
export interface UnidadTematica { export interface UnidadTematica {
id: string; id: string
nombre: string; nombre: string
numero: number; numero: number
temas: Tema[]; temas: Tema[]
} }
const initialData: UnidadTematica[] = [ const initialData: UnidadTematica[] = [
@@ -42,152 +53,297 @@ const initialData: UnidadTematica[] = [
temas: [ temas: [
{ id: 't1', nombre: 'Tipos de IA y aplicaciones', horasEstimadas: 6 }, { id: 't1', nombre: 'Tipos de IA y aplicaciones', horasEstimadas: 6 },
{ id: 't2', nombre: 'Ética en IA', horasEstimadas: 3 }, { id: 't2', nombre: 'Ética en IA', horasEstimadas: 3 },
],
},
] ]
}
];
export function ContenidoTematico() { // Estructura que viene de tu JSON/API
const [unidades, setUnidades] = useState<UnidadTematica[]>(initialData); interface ContenidoApi {
const [expandedUnits, setExpandedUnits] = useState<Set<string>>(new Set(['u1'])); unidad: number
const [deleteDialog, setDeleteDialog] = useState<{ type: 'unidad' | 'tema'; id: string; parentId?: string } | null>(null); titulo: string
const [editingUnit, setEditingUnit] = useState<string | null>(null); temas: string[] | any[] // Acepta strings o objetos
const [editingTema, setEditingTema] = useState<{ unitId: string; temaId: string } | null>(null); [key: string]: any // Esta línea permite que haya más claves desconocidas
const [isSaving, setIsSaving] = useState(false); }
// Props del componente
interface ContenidoTematicoProps {
data: {
contenido_tematico: ContenidoApi[]
}
isLoading: boolean
}
export function ContenidoTematico({ data, isLoading }: ContenidoTematicoProps) {
const [unidades, setUnidades] = useState<UnidadTematica[]>([])
const [expandedUnits, setExpandedUnits] = useState<Set<string>>(
new Set(['u1']),
)
const [deleteDialog, setDeleteDialog] = useState<{
type: 'unidad' | 'tema'
id: string
parentId?: string
} | null>(null)
const [editingUnit, setEditingUnit] = useState<string | null>(null)
const [editingTema, setEditingTema] = useState<{
unitId: string
temaId: string
} | null>(null)
const [isSaving, setIsSaving] = useState(false)
useEffect(() => {
if (data?.contenido_tematico) {
const transformed = data.contenido_tematico.map(
(u: any, idx: number) => ({
id: `u-${idx}`,
numero: u.unidad || idx + 1,
nombre: u.titulo || 'Sin título',
temas: Array.isArray(u.temas)
? u.temas.map((t: any, tidx: number) => ({
id: `t-${idx}-${tidx}`,
nombre: typeof t === 'string' ? t : t.nombre || 'Tema',
horasEstimadas: t.horasEstimadas || 0,
}))
: [],
}),
)
setUnidades(transformed)
// Expandir la primera unidad automáticamente
if (transformed.length > 0) {
setExpandedUnits(new Set([transformed[0].id]))
}
}
}, [data])
if (isLoading)
return <div className="p-10 text-center">Cargando contenido...</div>
// 3. Cálculo de horas (ahora dinámico basado en los nuevos datos)
const totalHoras = unidades.reduce(
(acc, u) =>
acc + u.temas.reduce((sum, t) => sum + (t.horasEstimadas || 0), 0),
0,
)
// --- Lógica de Unidades --- // --- Lógica de Unidades ---
const toggleUnit = (id: string) => { const toggleUnit = (id: string) => {
const newExpanded = new Set(expandedUnits); const newExpanded = new Set(expandedUnits)
newExpanded.has(id) ? newExpanded.delete(id) : newExpanded.add(id); newExpanded.has(id) ? newExpanded.delete(id) : newExpanded.add(id)
setExpandedUnits(newExpanded); setExpandedUnits(newExpanded)
}; }
const addUnidad = () => { const addUnidad = () => {
const newId = `u-${Date.now()}`; const newId = `u-${Date.now()}`
const newUnidad: UnidadTematica = { const newUnidad: UnidadTematica = {
id: newId, id: newId,
nombre: 'Nueva Unidad', nombre: 'Nueva Unidad',
numero: unidades.length + 1, numero: unidades.length + 1,
temas: [], temas: [],
}; }
setUnidades([...unidades, newUnidad]); setUnidades([...unidades, newUnidad])
setExpandedUnits(new Set([...expandedUnits, newId])); setExpandedUnits(new Set([...expandedUnits, newId]))
setEditingUnit(newId); setEditingUnit(newId)
}; }
const updateUnidadNombre = (id: string, nombre: string) => { const updateUnidadNombre = (id: string, nombre: string) => {
setUnidades(unidades.map(u => u.id === id ? { ...u, nombre } : u)); setUnidades(unidades.map((u) => (u.id === id ? { ...u, nombre } : u)))
}; }
// --- Lógica de Temas --- // --- Lógica de Temas ---
const addTema = (unidadId: string) => { const addTema = (unidadId: string) => {
setUnidades(unidades.map(u => { setUnidades(
unidades.map((u) => {
if (u.id === unidadId) { if (u.id === unidadId) {
const newTemaId = `t-${Date.now()}`; const newTemaId = `t-${Date.now()}`
const newTema: Tema = { id: newTemaId, nombre: 'Nuevo tema', horasEstimadas: 2 }; const newTema: Tema = {
setEditingTema({ unitId: unidadId, temaId: newTemaId }); id: newTemaId,
return { ...u, temas: [...u.temas, newTema] }; nombre: 'Nuevo tema',
horasEstimadas: 2,
}
setEditingTema({ unitId: unidadId, temaId: newTemaId })
return { ...u, temas: [...u.temas, newTema] }
}
return u
}),
)
} }
return u;
}));
};
const updateTema = (unidadId: string, temaId: string, updates: Partial<Tema>) => { const updateTema = (
setUnidades(unidades.map(u => { unidadId: string,
temaId: string,
updates: Partial<Tema>,
) => {
setUnidades(
unidades.map((u) => {
if (u.id === unidadId) { if (u.id === unidadId) {
return { ...u, temas: u.temas.map(t => t.id === temaId ? { ...t, ...updates } : t) }; return {
...u,
temas: u.temas.map((t) =>
t.id === temaId ? { ...t, ...updates } : t,
),
}
}
return u
}),
)
} }
return u;
}));
};
const handleDelete = () => { const handleDelete = () => {
if (!deleteDialog) return; if (!deleteDialog) return
if (deleteDialog.type === 'unidad') { if (deleteDialog.type === 'unidad') {
setUnidades(unidades.filter(u => u.id !== deleteDialog.id).map((u, i) => ({ ...u, numero: i + 1 }))); setUnidades(
unidades
.filter((u) => u.id !== deleteDialog.id)
.map((u, i) => ({ ...u, numero: i + 1 })),
)
} else if (deleteDialog.parentId) { } else if (deleteDialog.parentId) {
setUnidades(unidades.map(u => u.id === deleteDialog.parentId ? { ...u, temas: u.temas.filter(t => t.id !== deleteDialog.id) } : u)); setUnidades(
unidades.map((u) =>
u.id === deleteDialog.parentId
? { ...u, temas: u.temas.filter((t) => t.id !== deleteDialog.id) }
: u,
),
)
} }
setDeleteDialog(null); setDeleteDialog(null)
//toast.success("Eliminado correctamente"); //toast.success("Eliminado correctamente");
}; }
const totalHoras = unidades.reduce((acc, u) => acc + u.temas.reduce((sum, t) => sum + (t.horasEstimadas || 0), 0), 0);
return ( return (
<div className="max-w-5xl mx-auto py-10 space-y-6 animate-in fade-in duration-500"> <div className="animate-in fade-in mx-auto max-w-5xl space-y-6 py-10 duration-500">
<div className="flex items-center justify-between border-b pb-4"> <div className="flex items-center justify-between border-b pb-4">
<div> <div>
<h2 className="text-2xl font-bold tracking-tight text-slate-900">Contenido Temático</h2> <h2 className="text-2xl font-bold tracking-tight text-slate-900">
<p className="text-sm text-slate-500 mt-1"> Contenido Temático
</h2>
<p className="mt-1 text-sm text-slate-500">
{unidades.length} unidades {totalHoras} horas estimadas totales {unidades.length} unidades {totalHoras} horas estimadas totales
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" onClick={addUnidad} className="gap-2"> <Button variant="outline" onClick={addUnidad} className="gap-2">
<Plus className="w-4 h-4" /> Nueva unidad <Plus className="h-4 w-4" /> Nueva unidad
</Button> </Button>
<Button onClick={() => { setIsSaving(true); setTimeout(() => { setIsSaving(false); /*toast.success("Guardado")*/; }, 1000); }} disabled={isSaving} className="bg-blue-600 hover:bg-blue-700"> <Button
<Save className="w-4 h-4 mr-2" /> {isSaving ? 'Guardando...' : 'Guardar'} onClick={() => {
setIsSaving(true)
setTimeout(() => {
setIsSaving(false) /*toast.success("Guardado")*/
}, 1000)
}}
disabled={isSaving}
className="bg-blue-600 hover:bg-blue-700"
>
<Save className="mr-2 h-4 w-4" />{' '}
{isSaving ? 'Guardando...' : 'Guardar'}
</Button> </Button>
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{unidades.map((unidad) => ( {unidades.map((unidad) => (
<Card key={unidad.id} className="overflow-hidden border-slate-200 shadow-sm"> <Card
<Collapsible open={expandedUnits.has(unidad.id)} onOpenChange={() => toggleUnit(unidad.id)}> key={unidad.id}
<CardHeader className="bg-slate-50/50 py-3 border-b border-slate-100"> className="overflow-hidden border-slate-200 shadow-sm"
>
<Collapsible
open={expandedUnits.has(unidad.id)}
onOpenChange={() => toggleUnit(unidad.id)}
>
<CardHeader className="border-b border-slate-100 bg-slate-50/50 py-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<GripVertical className="w-4 h-4 text-slate-300 cursor-grab" /> <GripVertical className="h-4 w-4 cursor-grab text-slate-300" />
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="p-0 h-auto"> <Button variant="ghost" size="sm" className="h-auto p-0">
{expandedUnits.has(unidad.id) ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />} {expandedUnits.has(unidad.id) ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>
<Badge className="bg-blue-600 font-mono">Unidad {unidad.numero}</Badge> <Badge className="bg-blue-600 font-mono">
Unidad {unidad.numero}
</Badge>
{editingUnit === unidad.id ? ( {editingUnit === unidad.id ? (
<Input <Input
value={unidad.nombre} value={unidad.nombre}
onChange={(e) => updateUnidadNombre(unidad.id, e.target.value)} onChange={(e) =>
updateUnidadNombre(unidad.id, e.target.value)
}
onBlur={() => setEditingUnit(null)} onBlur={() => setEditingUnit(null)}
onKeyDown={(e) => e.key === 'Enter' && setEditingUnit(null)} onKeyDown={(e) =>
className="max-w-md h-8 bg-white" e.key === 'Enter' && setEditingUnit(null)
}
className="h-8 max-w-md bg-white"
autoFocus autoFocus
/> />
) : ( ) : (
<CardTitle className="text-base font-semibold cursor-pointer hover:text-blue-600 transition-colors" onClick={() => setEditingUnit(unidad.id)}> <CardTitle
className="cursor-pointer text-base font-semibold transition-colors hover:text-blue-600"
onClick={() => setEditingUnit(unidad.id)}
>
{unidad.nombre} {unidad.nombre}
</CardTitle> </CardTitle>
)} )}
<div className="ml-auto flex items-center gap-3"> <div className="ml-auto flex items-center gap-3">
<span className="text-xs font-medium text-slate-400 flex items-center gap-1"> <span className="flex items-center gap-1 text-xs font-medium text-slate-400">
<Clock className="w-3 h-3" /> {unidad.temas.reduce((sum, t) => sum + (t.horasEstimadas || 0), 0)}h <Clock className="h-3 w-3" />{' '}
{unidad.temas.reduce(
(sum, t) => sum + (t.horasEstimadas || 0),
0,
)}
h
</span> </span>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-red-500" onClick={() => setDeleteDialog({ type: 'unidad', id: unidad.id })}> <Button
<Trash2 className="w-4 h-4" /> variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-red-500"
onClick={() =>
setDeleteDialog({ type: 'unidad', id: unidad.id })
}
>
<Trash2 className="h-4 w-4" />
</Button> </Button>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CollapsibleContent> <CollapsibleContent>
<CardContent className="pt-4 bg-white"> <CardContent className="bg-white pt-4">
<div className="space-y-1 ml-10 border-l-2 border-slate-50 pl-4"> <div className="ml-10 space-y-1 border-l-2 border-slate-50 pl-4">
{unidad.temas.map((tema, idx) => ( {unidad.temas.map((tema, idx) => (
<TemaRow <TemaRow
key={tema.id} key={tema.id}
tema={tema} tema={tema}
index={idx + 1} index={idx + 1}
isEditing={editingTema?.unitId === unidad.id && editingTema?.temaId === tema.id} isEditing={
onEdit={() => setEditingTema({ unitId: unidad.id, temaId: tema.id })} editingTema?.unitId === unidad.id &&
editingTema?.temaId === tema.id
}
onEdit={() =>
setEditingTema({ unitId: unidad.id, temaId: tema.id })
}
onStopEditing={() => setEditingTema(null)} onStopEditing={() => setEditingTema(null)}
onUpdate={(updates) => updateTema(unidad.id, tema.id, updates)} onUpdate={(updates) =>
onDelete={() => setDeleteDialog({ type: 'tema', id: tema.id, parentId: unidad.id })} updateTema(unidad.id, tema.id, updates)
}
onDelete={() =>
setDeleteDialog({
type: 'tema',
id: tema.id,
parentId: unidad.id,
})
}
/> />
))} ))}
<Button variant="ghost" size="sm" className="text-blue-600 hover:text-blue-700 hover:bg-blue-50 w-full justify-start mt-2" onClick={() => addTema(unidad.id)}> <Button
<Plus className="w-3 h-3 mr-2" /> Añadir subtema variant="ghost"
size="sm"
className="mt-2 w-full justify-start text-blue-600 hover:bg-blue-50 hover:text-blue-700"
onClick={() => addTema(unidad.id)}
>
<Plus className="mr-2 h-3 w-3" /> Añadir subtema
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@@ -197,81 +353,137 @@ export function ContenidoTematico() {
))} ))}
</div> </div>
<DeleteConfirmDialog dialog={deleteDialog} setDialog={setDeleteDialog} onConfirm={handleDelete} /> <DeleteConfirmDialog
dialog={deleteDialog}
setDialog={setDeleteDialog}
onConfirm={handleDelete}
/>
</div> </div>
); )
} }
// --- Componentes Auxiliares --- // --- Componentes Auxiliares ---
interface TemaRowProps { interface TemaRowProps {
tema: Tema; tema: Tema
index: number; index: number
isEditing: boolean; isEditing: boolean
onEdit: () => void; onEdit: () => void
onStopEditing: () => void; onStopEditing: () => void
onUpdate: (updates: Partial<Tema>) => void; onUpdate: (updates: Partial<Tema>) => void
onDelete: () => void; onDelete: () => void
} }
function TemaRow({ tema, index, isEditing, onEdit, onStopEditing, onUpdate, onDelete }: TemaRowProps) { function TemaRow({
tema,
index,
isEditing,
onEdit,
onStopEditing,
onUpdate,
onDelete,
}: TemaRowProps) {
return ( return (
<div className={cn("flex items-center gap-3 p-2 rounded-md group transition-all", isEditing ? "bg-blue-50 ring-1 ring-blue-100" : "hover:bg-slate-50")}> <div
<span className="text-xs font-mono text-slate-400 w-4">{index}.</span> className={cn(
'group flex items-center gap-3 rounded-md p-2 transition-all',
isEditing ? 'bg-blue-50 ring-1 ring-blue-100' : 'hover:bg-slate-50',
)}
>
<span className="w-4 font-mono text-xs text-slate-400">{index}.</span>
{isEditing ? ( {isEditing ? (
<div className="flex-1 flex items-center gap-2 animate-in slide-in-from-left-2"> <div className="animate-in slide-in-from-left-2 flex flex-1 items-center gap-2">
<Input value={tema.nombre} onChange={(e) => onUpdate({ nombre: e.target.value })} className="h-8 flex-1 bg-white" placeholder="Nombre" autoFocus /> <Input
<Input type="number" value={tema.horasEstimadas} onChange={(e) => onUpdate({ horasEstimadas: parseInt(e.target.value) || 0 })} className="h-8 w-16 bg-white" /> value={tema.nombre}
<Button size="sm" className="bg-emerald-600 h-8" onClick={onStopEditing}>Listo</Button> onChange={(e) => onUpdate({ nombre: e.target.value })}
className="h-8 flex-1 bg-white"
placeholder="Nombre"
autoFocus
/>
<Input
type="number"
value={tema.horasEstimadas}
onChange={(e) =>
onUpdate({ horasEstimadas: parseInt(e.target.value) || 0 })
}
className="h-8 w-16 bg-white"
/>
<Button
size="sm"
className="h-8 bg-emerald-600"
onClick={onStopEditing}
>
Listo
</Button>
</div> </div>
) : ( ) : (
<> <>
<div className="flex-1 cursor-pointer" onClick={onEdit}> <div className="flex-1 cursor-pointer" onClick={onEdit}>
<p className="text-sm font-medium text-slate-700">{tema.nombre}</p> <p className="text-sm font-medium text-slate-700">{tema.nombre}</p>
</div> </div>
<Badge variant="secondary" className="text-[10px] opacity-60">{tema.horasEstimadas}h</Badge> <Badge variant="secondary" className="text-[10px] opacity-60">
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> {tema.horasEstimadas}h
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-600" onClick={onEdit}><Edit3 className="w-3 h-3" /></Button> </Badge>
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-red-500" onClick={onDelete}><Trash2 className="w-3 h-3" /></Button> <div className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-400 hover:text-blue-600"
onClick={onEdit}
>
<Edit3 className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-400 hover:text-red-500"
onClick={onDelete}
>
<Trash2 className="h-3 w-3" />
</Button>
</div> </div>
</> </>
)} )}
</div> </div>
); )
} }
interface DeleteDialogState { interface DeleteDialogState {
type: 'unidad' | 'tema'; type: 'unidad' | 'tema'
id: string; id: string
parentId?: string; parentId?: string
} }
interface DeleteConfirmDialogProps { interface DeleteConfirmDialogProps {
dialog: DeleteDialogState | null; dialog: DeleteDialogState | null
setDialog: (value: DeleteDialogState | null) => void; setDialog: (value: DeleteDialogState | null) => void
onConfirm: () => void; onConfirm: () => void
} }
function DeleteConfirmDialog({ function DeleteConfirmDialog({
dialog, dialog,
setDialog, setDialog,
onConfirm, onConfirm,
}: DeleteConfirmDialogProps) { }: DeleteConfirmDialogProps) {
return ( return (
<AlertDialog open={!!dialog} onOpenChange={() => setDialog(null)}> <AlertDialog open={!!dialog} onOpenChange={() => setDialog(null)}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>¿Confirmar eliminación?</AlertDialogTitle> <AlertDialogTitle>¿Confirmar eliminación?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Estás a punto de borrar un {dialog?.type}. Esta acción no se puede deshacer. Estás a punto de borrar un {dialog?.type}. Esta acción no se puede
deshacer.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel> <AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm} className="bg-red-600 hover:bg-red-700 text-white">Eliminar</AlertDialogAction> <AlertDialogAction
onClick={onConfirm}
className="bg-red-600 text-white hover:bg-red-700"
>
Eliminar
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
); )
} }

View File

@@ -1,8 +1,16 @@
import { useState } from 'react'; import { useState } from 'react'
import { FileText, Download, RefreshCw, Calendar, FileCheck, AlertTriangle, Loader2 } from 'lucide-react'; import {
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; FileText,
import { Button } from '@/components/ui/button'; Download,
import { Badge } from '@/components/ui/badge'; 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,
@@ -13,63 +21,88 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog'
import type { DocumentoMateria, Materia, MateriaStructure } from '@/types/materia'; import type {
import { cn } from '@/lib/utils'; DocumentoMateria,
Materia,
MateriaStructure,
} from '@/types/materia'
import { cn } from '@/lib/utils'
import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
//import { toast } from 'sonner'; //import { toast } from 'sonner';
//import { format } from 'date-fns'; //import { format } from 'date-fns';
//import { es } from 'date-fns/locale'; //import { es } from 'date-fns/locale';
interface DocumentoSEPTabProps { interface DocumentoSEPTabProps {
documento: DocumentoMateria | null; documento: DocumentoMateria | null
materia: Materia; materia: Materia
estructura: MateriaStructure; estructura: MateriaStructure
datosGenerales: Record<string, any>; datosGenerales: Record<string, any>
onRegenerate: () => void; onRegenerate: () => void
isRegenerating: boolean; isRegenerating: boolean
} }
export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales, onRegenerate, isRegenerating }: DocumentoSEPTabProps) { export function DocumentoSEPTab({
const [showConfirmDialog, setShowConfirmDialog] = useState(false); documento,
materia,
estructura,
datosGenerales,
onRegenerate,
isRegenerating,
}: DocumentoSEPTabProps) {
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
// Check completeness // Check completeness
const camposObligatorios = estructura.campos.filter(c => c.obligatorio); const camposObligatorios = estructura.campos.filter((c) => c.obligatorio)
const camposCompletos = camposObligatorios.filter(c => datosGenerales[c.id]?.trim()); const camposCompletos = camposObligatorios.filter((c) =>
const completeness = Math.round((camposCompletos.length / camposObligatorios.length) * 100); datosGenerales[c.id]?.trim(),
const isComplete = completeness === 100; )
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...'); //toast.success('Regenerando documento...');
}; }
return ( return (
<div className="space-y-6 animate-fade-in"> <div className="animate-fade-in space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="font-display text-2xl font-semibold text-foreground flex items-center gap-2"> <h2 className="font-display text-foreground flex items-center gap-2 text-2xl font-semibold">
<FileCheck className="w-6 h-6 text-accent" /> <FileCheck className="text-accent h-6 w-6" />
Documento SEP Documento SEP
</h2> </h2>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1 text-sm">
Previsualización del documento oficial para la SEP Previsualización del documento oficial para la SEP
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{documento?.estado === 'listo' && ( {documento?.estado === 'listo' && (
<Button variant="outline" onClick={() => console.log("descargando") /*toast.info('Descarga iniciada')*/}> <Button
<Download className="w-4 h-4 mr-2" /> variant="outline"
onClick={
() =>
console.log('descargando') /*toast.info('Descarga iniciada')*/
}
>
<Download className="mr-2 h-4 w-4" />
Descargar Descargar
</Button> </Button>
)} )}
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}> <AlertDialog
open={showConfirmDialog}
onOpenChange={setShowConfirmDialog}
>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button disabled={isRegenerating || !isComplete}> <Button disabled={isRegenerating || !isComplete}>
{isRegenerating ? ( {isRegenerating ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : ( ) : (
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="mr-2 h-4 w-4" />
)} )}
{isRegenerating ? 'Generando...' : 'Regenerar documento'} {isRegenerating ? 'Generando...' : 'Regenerar documento'}
</Button> </Button>
@@ -78,8 +111,9 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
<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 actuales de la materia. Se creará una nueva versión del documento con los datos
La versión anterior quedará en el historial. actuales de la materia. La versión anterior quedará en el
historial.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
@@ -93,18 +127,18 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
</div> </div>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Document preview */} {/* Document preview */}
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<Card className="card-elevated h-[700px] overflow-hidden"> <Card className="card-elevated h-[700px] overflow-hidden">
{documento?.estado === 'listo' ? ( {documento?.estado === 'listo' ? (
<div className="h-full bg-muted/30 flex flex-col"> <div className="bg-muted/30 flex h-full flex-col">
{/* Simulated document header */} {/* Simulated document header */}
<div className="bg-card border-b p-4"> <div className="bg-card border-b p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FileText className="w-5 h-5 text-primary" /> <FileText className="text-primary h-5 w-5" />
<span className="font-medium text-foreground"> <span className="text-foreground font-medium">
Programa de Estudios - {materia.clave} Programa de Estudios - {materia.clave}
</span> </span>
</div> </div>
@@ -114,70 +148,87 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
{/* Document content simulation */} {/* Document content simulation */}
<div className="flex-1 overflow-y-auto p-8"> <div className="flex-1 overflow-y-auto p-8">
<div className="max-w-2xl mx-auto bg-card rounded-lg shadow-lg p-8 space-y-6"> <div className="bg-card mx-auto max-w-2xl space-y-6 rounded-lg p-8 shadow-lg">
{/* Header */} {/* Header */}
<div className="text-center border-b pb-6"> <div className="border-b pb-6 text-center">
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-2"> <p className="text-muted-foreground mb-2 text-xs tracking-wide uppercase">
Secretaría de Educación Pública Secretaría de Educación Pública
</p> </p>
<h1 className="font-display text-2xl font-bold text-primary mb-1"> <h1 className="font-display text-primary mb-1 text-2xl font-bold">
{materia.nombre} {materia.nombre}
</h1> </h1>
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
Clave: {materia.clave} | Créditos: {materia.creditos || 'N/A'} Clave: {materia.clave} | Créditos:{' '}
{materia.creditos || 'N/A'}
</p> </p>
</div> </div>
{/* Datos de la institución */} {/* Datos de la institución */}
<div className="space-y-1 text-sm"> <div className="space-y-1 text-sm">
<p><strong>Carrera:</strong> {materia.carrera}</p> <p>
<p><strong>Facultad:</strong> {materia.facultad}</p> <strong>Carrera:</strong> {materia.carrera}
<p><strong>Plan de estudios:</strong> {materia.planNombre}</p> </p>
{materia.ciclo && <p><strong>Ciclo:</strong> {materia.ciclo}</p>} <p>
<strong>Facultad:</strong> {materia.facultad}
</p>
<p>
<strong>Plan de estudios:</strong> {materia.planNombre}
</p>
{materia.ciclo && (
<p>
<strong>Ciclo:</strong> {materia.ciclo}
</p>
)}
</div> </div>
{/* Campos del documento */} {/* Campos del documento */}
{estructura.campos.map((campo) => { {estructura.campos.map((campo) => {
const valor = datosGenerales[campo.id]; const valor = datosGenerales[campo.id]
if (!valor) return null; if (!valor) return null
return ( return (
<div key={campo.id} className="space-y-2"> <div key={campo.id} className="space-y-2">
<h3 className="font-semibold text-foreground border-b pb-1"> <h3 className="text-foreground border-b pb-1 font-semibold">
{campo.nombre} {campo.nombre}
</h3> </h3>
<p className="text-sm text-foreground whitespace-pre-wrap leading-relaxed"> <p className="text-foreground text-sm leading-relaxed whitespace-pre-wrap">
{valor} {valor}
</p> </p>
</div> </div>
); )
})} })}
{/* Footer */} {/* Footer */}
<div className="border-t pt-6 mt-8 text-center text-xs text-muted-foreground"> <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>
Documento generado el{' '}
{/*format(documento.fechaGeneracion, "d 'de' MMMM 'de' yyyy", { locale: es })*/}
</p>
<p className="mt-1">Universidad La Salle</p> <p className="mt-1">Universidad La Salle</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
) : documento?.estado === 'generando' ? ( ) : documento?.estado === 'generando' ? (
<div className="h-full flex items-center justify-center"> <div className="flex h-full items-center justify-center">
<div className="text-center"> <div className="text-center">
<Loader2 className="w-12 h-12 mx-auto text-accent animate-spin mb-4" /> <Loader2 className="text-accent mx-auto mb-4 h-12 w-12 animate-spin" />
<p className="text-muted-foreground">Generando documento...</p> <p className="text-muted-foreground">
Generando documento...
</p>
</div> </div>
</div> </div>
) : ( ) : (
<div className="h-full flex items-center justify-center"> <div className="flex h-full items-center justify-center">
<div className="text-center max-w-sm"> <div className="max-w-sm text-center">
<FileText className="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" /> <FileText className="text-muted-foreground/50 mx-auto mb-4 h-12 w-12" />
<p className="text-muted-foreground mb-4"> <p className="text-muted-foreground mb-4">
No hay documento generado aún No hay documento generado aún
</p> </p>
{!isComplete && ( {!isComplete && (
<div className="p-4 bg-warning/10 rounded-lg text-sm text-warning-foreground"> <div className="bg-warning/10 text-warning-foreground rounded-lg p-4 text-sm">
<AlertTriangle className="w-4 h-4 inline mr-2" /> <AlertTriangle className="mr-2 inline h-4 w-4" />
Completa todos los campos obligatorios para generar el documento Completa todos los campos obligatorios para generar el
documento
</div> </div>
)} )}
</div> </div>
@@ -191,28 +242,41 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
{/* Status */} {/* Status */}
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Estado del documento</CardTitle> <CardTitle className="text-sm font-medium">
Estado del documento
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{documento && ( {documento && (
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Versión</span> <span className="text-muted-foreground text-sm">
Versión
</span>
<Badge variant="outline">{documento.version}</Badge> <Badge variant="outline">{documento.version}</Badge>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Generado</span> <span className="text-muted-foreground text-sm">
Generado
</span>
<span className="text-sm"> <span className="text-sm">
{/*format(documento.fechaGeneracion, "d MMM yyyy, HH:mm", { locale: es })*/} {/*format(documento.fechaGeneracion, "d MMM yyyy, HH:mm", { locale: es })*/}
</span> </span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Estado</span> <span className="text-muted-foreground text-sm">
<Badge className={cn( Estado
documento.estado === 'listo' && "bg-success text-success-foreground", </span>
documento.estado === 'generando' && "bg-info text-info-foreground", <Badge
documento.estado === 'error' && "bg-destructive text-destructive-foreground" 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 === 'listo' && 'Listo'}
{documento.estado === 'generando' && 'Generando'} {documento.estado === 'generando' && 'Generando'}
{documento.estado === 'error' && 'Error'} {documento.estado === 'error' && 'Error'}
@@ -226,41 +290,57 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
{/* Completeness */} {/* Completeness */}
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Completitud de datos</CardTitle> <CardTitle className="text-sm font-medium">
Completitud de datos
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Campos obligatorios</span> <span className="text-muted-foreground">
<span className="font-medium">{camposCompletos.length}/{camposObligatorios.length}</span> Campos obligatorios
</span>
<span className="font-medium">
{camposCompletos.length}/{camposObligatorios.length}
</span>
</div> </div>
<div className="h-2 bg-muted rounded-full overflow-hidden"> <div className="bg-muted h-2 overflow-hidden rounded-full">
<div <div
className={cn( className={cn(
"h-full transition-all duration-500", 'h-full transition-all duration-500',
completeness === 100 ? "bg-success" : "bg-accent" completeness === 100 ? 'bg-success' : 'bg-accent',
)} )}
style={{ width: `${completeness}%` }} style={{ width: `${completeness}%` }}
/> />
</div> </div>
<p className={cn( <p
"text-xs", className={cn(
completeness === 100 ? "text-success" : "text-muted-foreground" 'text-xs',
)}> completeness === 100
? 'text-success'
: 'text-muted-foreground',
)}
>
{completeness === 100 {completeness === 100
? 'Todos los campos obligatorios están completos' ? 'Todos los campos obligatorios están completos'
: `Faltan ${camposObligatorios.length - camposCompletos.length} campos por completar` : `Faltan ${camposObligatorios.length - camposCompletos.length} campos por completar`}
}
</p> </p>
</div> </div>
{/* Missing fields */} {/* Missing fields */}
{!isComplete && ( {!isComplete && (
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Campos faltantes:</p> <p className="text-muted-foreground text-xs font-medium">
{camposObligatorios.filter(c => !datosGenerales[c.id]?.trim()).map((campo) => ( Campos faltantes:
<div key={campo.id} className="flex items-center gap-2 text-sm"> </p>
<AlertTriangle className="w-3 h-3 text-warning" /> {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> <span className="text-foreground">{campo.nombre}</span>
</div> </div>
))} ))}
@@ -272,36 +352,62 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
{/* Requirements */} {/* Requirements */}
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Requisitos SEP</CardTitle> <CardTitle className="text-sm font-medium">
Requisitos SEP
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ul className="space-y-2 text-sm"> <ul className="space-y-2 text-sm">
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
<div className={cn( <div
"w-4 h-4 rounded-full flex items-center justify-center mt-0.5", className={cn(
datosGenerales['objetivo_general'] ? "bg-success/20" : "bg-muted" 'mt-0.5 flex h-4 w-4 items-center justify-center rounded-full',
)}> datosGenerales['objetivo_general']
{datosGenerales['objetivo_general'] && <Check className="w-3 h-3 text-success" />} ? 'bg-success/20'
: 'bg-muted',
)}
>
{datosGenerales['objetivo_general'] && (
<Check className="text-success h-3 w-3" />
)}
</div> </div>
<span className="text-muted-foreground">Objetivo general definido</span> <span className="text-muted-foreground">
Objetivo general definido
</span>
</li> </li>
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
<div className={cn( <div
"w-4 h-4 rounded-full flex items-center justify-center mt-0.5", className={cn(
datosGenerales['competencias'] ? "bg-success/20" : "bg-muted" 'mt-0.5 flex h-4 w-4 items-center justify-center rounded-full',
)}> datosGenerales['competencias']
{datosGenerales['competencias'] && <Check className="w-3 h-3 text-success" />} ? 'bg-success/20'
: 'bg-muted',
)}
>
{datosGenerales['competencias'] && (
<Check className="text-success h-3 w-3" />
)}
</div> </div>
<span className="text-muted-foreground">Competencias especificadas</span> <span className="text-muted-foreground">
Competencias especificadas
</span>
</li> </li>
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
<div className={cn( <div
"w-4 h-4 rounded-full flex items-center justify-center mt-0.5", className={cn(
datosGenerales['evaluacion'] ? "bg-success/20" : "bg-muted" 'mt-0.5 flex h-4 w-4 items-center justify-center rounded-full',
)}> datosGenerales['evaluacion']
{datosGenerales['evaluacion'] && <Check className="w-3 h-3 text-success" />} ? 'bg-success/20'
: 'bg-muted',
)}
>
{datosGenerales['evaluacion'] && (
<Check className="text-success h-3 w-3" />
)}
</div> </div>
<span className="text-muted-foreground">Criterios de evaluación</span> <span className="text-muted-foreground">
Criterios de evaluación
</span>
</li> </li>
</ul> </ul>
</CardContent> </CardContent>
@@ -309,13 +415,19 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
</div> </div>
</div> </div>
</div> </div>
); )
} }
function Check({ className }: { className?: string }) { function Check({ className }: { className?: string }) {
return ( return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"> <svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3"
>
<polyline points="20 6 9 17 4 12" /> <polyline points="20 6 9 17 4 12" />
</svg> </svg>
); )
} }

View File

@@ -1,181 +1,291 @@
import { useState } from 'react'; import { useState, useMemo } from 'react'
import { History, FileText, List, BookMarked, Sparkles, FileCheck, User, Filter, Calendar } from 'lucide-react'; import {
import { Card, CardContent } from '@/components/ui/card'; History,
import { Button } from '@/components/ui/button'; FileText,
import { Badge } from '@/components/ui/badge'; List,
BookMarked,
Sparkles,
FileCheck,
User,
Filter,
Calendar,
Loader2,
Eye,
} from 'lucide-react'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu'
import type { CambioMateria } from '@/types/materia'; import { cn } from '@/lib/utils'
import { cn } from '@/lib/utils'; import { format, parseISO } from 'date-fns'
import { format, formatDistanceToNow } from 'date-fns'; import { es } from 'date-fns/locale'
import { es } from 'date-fns/locale'; import { useSubjectHistorial } from '@/data/hooks/useSubjects'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
interface HistorialTabProps { const tipoConfig: Record<string, { label: string; icon: any; color: string }> =
historial: CambioMateria[]; {
datos: { label: 'Datos generales', icon: FileText, color: 'text-info' },
contenido: {
label: 'Contenido temático',
icon: List,
color: 'text-accent',
},
bibliografia: {
label: 'Bibliografía',
icon: BookMarked,
color: 'text-success',
},
ia: { label: 'IA', icon: Sparkles, color: 'text-amber-500' },
documento: {
label: 'Documento SEP',
icon: FileCheck,
color: 'text-primary',
},
} }
const tipoConfig: Record<string, { label: string; icon: React.ComponentType<{ className?: string }>; color: string }> = { export function HistorialTab() {
datos: { label: 'Datos generales', icon: FileText, color: 'text-info' }, // 1. Obtenemos los datos directamente dentro del componente
contenido: { label: 'Contenido temático', icon: List, color: 'text-accent' }, const { data: rawData, isLoading } = useSubjectHistorial(
bibliografia: { label: 'Bibliografía', icon: BookMarked, color: 'text-success' }, '9d4dda6a-488f-428a-8a07-38081592a641',
ia: { label: 'IA', icon: Sparkles, color: 'text-amber-500' }, )
documento: { label: 'Documento SEP', icon: FileCheck, color: 'text-primary' },
};
export function HistorialTab({ historial }: HistorialTabProps) { const [filtros, setFiltros] = useState<Set<string>>(
const [filtros, setFiltros] = useState<Set<string>>(new Set(['datos', 'contenido', 'bibliografia', 'ia', 'documento'])); new Set(['datos', 'contenido', 'bibliografia', 'ia', 'documento']),
)
// ESTADOS PARA EL MODAL
const [selectedChange, setSelectedChange] = useState<any>(null)
const [isModalOpen, setIsModalOpen] = useState(false)
const RenderValue = ({ value }: { value: any }) => {
// 1. Caso: Nulo o vacío
if (
value === null ||
value === undefined ||
value === 'Sin información previa'
) {
return (
<span className="text-muted-foreground italic">Sin información</span>
)
}
// 2. Caso: Es un ARRAY (como tu lista de unidades)
if (Array.isArray(value)) {
return (
<div className="space-y-4">
{value.map((item, index) => (
<div
key={index}
className="rounded-lg border bg-white/50 p-3 shadow-sm"
>
<RenderValue value={item} />
</div>
))}
</div>
)
}
// 3. Caso: Es un OBJETO (como cada unidad con titulo, temas, etc.)
if (typeof value === 'object') {
return (
<div className="grid gap-2">
{Object.entries(value).map(([key, val]) => (
<div key={key} className="flex flex-col">
<span className="text-[10px] font-bold tracking-wider text-slate-400 uppercase">
{key.replace(/_/g, ' ')}
</span>
<div className="text-sm text-slate-700">
{/* Llamada recursiva para manejar lo que haya dentro del valor */}
{typeof val === 'object' ? (
<div className="mt-1 border-l-2 border-slate-100 pl-2">
<RenderValue value={val} />
</div>
) : (
String(val)
)}
</div>
</div>
))}
</div>
)
}
// 4. Caso: Texto o número simple
return <span className="text-sm leading-relaxed">{String(value)}</span>
}
const historialTransformado = useMemo(() => {
if (!rawData) return []
return rawData.map((item: any) => ({
id: item.id,
tipo: item.campo === 'contenido_tematico' ? 'contenido' : 'datos',
descripcion: `Se actualizó el campo ${item.campo.replace('_', ' ')}`,
fecha: parseISO(item.cambiado_en),
usuario: item.fuente === 'HUMANO' ? 'Usuario Staff' : 'Sistema IA',
detalles: {
campo: item.campo,
valor_anterior: item.valor_anterior || 'Sin datos previos', // Asumiendo que existe en tu API
valor_nuevo: item.valor_nuevo,
},
}))
}, [rawData])
const openCompareModal = (cambio: any) => {
setSelectedChange(cambio)
setIsModalOpen(true)
}
const toggleFiltro = (tipo: string) => { const toggleFiltro = (tipo: string) => {
const newFiltros = new Set(filtros); const newFiltros = new Set(filtros)
if (newFiltros.has(tipo)) { if (newFiltros.has(tipo)) newFiltros.delete(tipo)
newFiltros.delete(tipo); else newFiltros.add(tipo)
} else { setFiltros(newFiltros)
newFiltros.add(tipo);
} }
setFiltros(newFiltros);
};
const filteredHistorial = historial.filter(cambio => filtros.has(cambio.tipo)); // 3. Aplicamos filtros y agrupamiento sobre los datos transformados
const filteredHistorial = historialTransformado.filter((cambio) =>
filtros.has(cambio.tipo),
)
// Group by date const groupedHistorial = filteredHistorial.reduce(
const groupedHistorial = filteredHistorial.reduce((groups, cambio) => { (groups, cambio) => {
const dateKey = format(cambio.fecha, 'yyyy-MM-dd'); const dateKey = format(cambio.fecha, 'yyyy-MM-dd')
if (!groups[dateKey]) { if (!groups[dateKey]) groups[dateKey] = []
groups[dateKey] = []; groups[dateKey].push(cambio)
return groups
},
{} as Record<string, any[]>,
)
const sortedDates = Object.keys(groupedHistorial).sort((a, b) =>
b.localeCompare(a),
)
if (isLoading) {
return (
<div className="flex h-48 items-center justify-center">
<Loader2 className="text-primary h-8 w-8 animate-spin" />
</div>
)
} }
groups[dateKey].push(cambio);
return groups;
}, {} as Record<string, CambioMateria[]>);
const sortedDates = Object.keys(groupedHistorial).sort((a, b) => b.localeCompare(a));
return ( return (
<div className="space-y-6 animate-fade-in"> <div className="animate-fade-in space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="font-display text-2xl font-semibold text-foreground flex items-center gap-2"> <h2 className="font-display text-foreground flex items-center gap-2 text-2xl font-semibold">
<History className="w-6 h-6 text-accent" /> <History className="text-accent h-6 w-6" />
Historial de cambios Historial de cambios
</h2> </h2>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1 text-sm">
{historial.length} cambios registrados {historialTransformado.length} cambios registrados
</p> </p>
</div> </div>
{/* Dropdown de Filtros (Igual al anterior) */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline"> <Button variant="outline">
<Filter className="w-4 h-4 mr-2" /> <Filter className="mr-2 h-4 w-4" />
Filtrar ({filtros.size}) Filtrar ({filtros.size})
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48"> <DropdownMenuContent align="end" className="w-48">
{Object.entries(tipoConfig).map(([tipo, config]) => { {Object.entries(tipoConfig).map(([tipo, config]) => (
const Icon = config.icon;
return (
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
key={tipo} key={tipo}
checked={filtros.has(tipo)} checked={filtros.has(tipo)}
onCheckedChange={() => toggleFiltro(tipo)} onCheckedChange={() => toggleFiltro(tipo)}
> >
<Icon className={cn("w-4 h-4 mr-2", config.color)} /> <config.icon className={cn('mr-2 h-4 w-4', config.color)} />
{config.label} {config.label}
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
); ))}
})}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
{filteredHistorial.length === 0 ? ( {filteredHistorial.length === 0 ? (
<Card className="card-elevated"> <Card>
<CardContent className="py-12 text-center"> <CardContent className="py-12 text-center">
<History className="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" /> <History className="text-muted-foreground/50 mx-auto mb-4 h-12 w-12" />
<p className="text-muted-foreground"> <p className="text-muted-foreground">No se encontraron cambios.</p>
{historial.length === 0
? 'No hay cambios registrados aún'
: 'No hay cambios con los filtros seleccionados'
}
</p>
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<div className="space-y-8"> <div className="space-y-8">
{sortedDates.map((dateKey) => { {sortedDates.map((dateKey) => (
const cambios = groupedHistorial[dateKey];
const date = new Date(dateKey);
const isToday = format(new Date(), 'yyyy-MM-dd') === dateKey;
const isYesterday = format(new Date(Date.now() - 86400000), 'yyyy-MM-dd') === dateKey;
return (
<div key={dateKey}> <div key={dateKey}>
{/* Date header */} <div className="mb-4 flex items-center gap-3">
<div className="flex items-center gap-3 mb-4"> <Calendar className="text-muted-foreground h-4 w-4" />
<div className="p-2 rounded-lg bg-muted"> <h3 className="text-foreground font-semibold">
<Calendar className="w-4 h-4 text-muted-foreground" /> {format(parseISO(dateKey), "EEEE, d 'de' MMMM", {
</div> locale: es,
<div> })}
<h3 className="font-semibold text-foreground">
{isToday ? 'Hoy' : isYesterday ? 'Ayer' : format(date, "EEEE, d 'de' MMMM", { locale: es })}
</h3> </h3>
<p className="text-xs text-muted-foreground">
{cambios.length} {cambios.length === 1 ? 'cambio' : 'cambios'}
</p>
</div>
</div> </div>
{/* Timeline */} <div className="border-border ml-4 space-y-4 border-l-2 pl-6">
<div className="ml-4 border-l-2 border-border pl-6 space-y-4"> {groupedHistorial[dateKey].map((cambio) => {
{cambios.map((cambio) => { const config = tipoConfig[cambio.tipo] || tipoConfig.datos
const config = tipoConfig[cambio.tipo]; const Icon = config.icon
const Icon = config.icon;
return ( return (
<div key={cambio.id} className="relative"> <div key={cambio.id} className="relative">
{/* Timeline dot */} <div
<div className={cn( className={cn(
"absolute -left-[31px] w-4 h-4 rounded-full border-2 border-background", 'border-background absolute -left-[31px] h-4 w-4 rounded-full border-2',
`bg-current ${config.color}` `bg-current ${config.color}`,
)} /> )}
/>
<Card className="card-interactive"> <Card className="card-interactive">
<CardContent className="py-4"> <CardContent className="py-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className={cn( <div
"p-2 rounded-lg bg-muted flex-shrink-0", className={cn(
config.color 'bg-muted rounded-lg p-2',
)}> config.color,
<Icon className="w-4 h-4" /> )}
>
<Icon className="h-4 w-4" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1">
<div className="flex items-start justify-between gap-2"> <div className="flex justify-between">
<div> <p className="font-medium">
<p className="font-medium text-foreground">
{cambio.descripcion} {cambio.descripcion}
</p> </p>
<div className="flex items-center gap-2 mt-1"> {/* BOTÓN PARA VER CAMBIOS */}
<Badge variant="outline" className="text-xs"> <Button
{config.label} variant="ghost"
</Badge> size="sm"
{cambio.detalles?.campo && ( className="gap-2 text-blue-600 hover:bg-blue-50 hover:text-blue-700"
<span className="text-xs text-muted-foreground"> onClick={() => openCompareModal(cambio)}
Campo: {cambio.detalles.campo} >
</span> <Eye className="h-4 w-4" />
)} Ver cambios
</div> </Button>
</div> <span className="text-muted-foreground text-xs">
<span className="text-xs text-muted-foreground whitespace-nowrap">
{format(cambio.fecha, 'HH:mm')} {format(cambio.fecha, 'HH:mm')}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 mt-3 text-xs text-muted-foreground"> <div className="mt-2 flex items-center gap-2">
<User className="w-3 h-3" /> <Badge
<span>{cambio.usuario}</span> variant="outline"
<span className="text-muted-foreground/50"></span> className="text-[10px]"
<span> >
{formatDistanceToNow(cambio.fecha, { addSuffix: true, locale: es })} {config.label}
</Badge>
<span className="text-muted-foreground text-xs italic">
por {cambio.usuario}
</span> </span>
</div> </div>
</div> </div>
@@ -183,14 +293,62 @@ export function HistorialTab({ historial }: HistorialTabProps) {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
); )
})} })}
</div> </div>
</div> </div>
); ))}
})}
</div> </div>
)} )}
{/* MODAL DE COMPARACIÓN */}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="flex max-h-[90vh] max-w-4xl flex-col overflow-hidden">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2 text-xl">
<History className="h-5 w-5 text-blue-500" />
Comparación de cambios
</DialogTitle>
{/* ... info de usuario y fecha */}
</DialogHeader>
<div className="custom-scrollbar mt-4 flex-1 overflow-y-auto pr-2">
<div className="grid h-full grid-cols-2 gap-6">
{/* Lado Antes */}
<div className="flex flex-col space-y-3">
<div className="sticky top-0 z-10 flex items-center gap-2 bg-white pb-2">
<div className="h-2 w-2 rounded-full bg-red-400" />
<span className="text-xs font-bold text-slate-500 uppercase">
Versión Anterior
</span>
</div> </div>
); <div className="flex-1 rounded-xl border border-red-100 bg-red-50/30 p-4">
<RenderValue
value={selectedChange?.detalles.valor_anterior}
/>
</div>
</div>
{/* Lado Después */}
<div className="flex flex-col space-y-3">
<div className="sticky top-0 z-10 flex items-center gap-2 bg-white pb-2">
<div className="h-2 w-2 rounded-full bg-emerald-400" />
<span className="text-xs font-bold text-slate-500 uppercase">
Nueva Versión
</span>
</div>
<div className="flex-1 rounded-xl border border-emerald-100 bg-emerald-50/40 p-4">
<RenderValue value={selectedChange?.detalles.valor_nuevo} />
</div>
</div>
</div>
</div>
<div className="mt-4 flex flex-shrink-0 items-center justify-center gap-2 rounded-lg border border-slate-100 bg-slate-50 p-3 text-xs text-slate-500">
Campo modificado:{' '}
<Badge variant="secondary">{selectedChange?.detalles.campo}</Badge>
</div>
</DialogContent>
</Dialog>
</div>
)
} }

View File

@@ -1,5 +1,4 @@
import { useCallback, useState, useEffect } from 'react'
import { useCallback, useState } from 'react'
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
@@ -10,8 +9,10 @@ import { Textarea } from '@/components/ui/textarea'
import { import {
ArrowLeft, ArrowLeft,
GraduationCap, GraduationCap,
Edit2, Save, Edit2,
Pencil Save,
Pencil,
Sparkles,
} from 'lucide-react' } from 'lucide-react'
import { ContenidoTematico } from './ContenidoTematico' import { ContenidoTematico } from './ContenidoTematico'
import { BibliographyItem } from './BibliographyItem' import { BibliographyItem } from './BibliographyItem'
@@ -21,35 +22,113 @@ import type {
IAMessage, IAMessage,
IASugerencia, IASugerencia,
UnidadTematica, UnidadTematica,
} from '@/types/materia'; } from '@/types/materia'
import { import {
mockMateria, mockMateria,
mockEstructura, mockEstructura,
mockDocumentoSep, mockDocumentoSep,
mockHistorial mockHistorial,
} from '@/data/mockMateriaData'; } from '@/data/mockMateriaData'
import { DocumentoSEPTab } from './DocumentoSEPTab' import { DocumentoSEPTab } from './DocumentoSEPTab'
import { HistorialTab } from './HistorialTab' import { HistorialTab } from './HistorialTab'
import { useSubject } from '@/data/hooks/useSubjects'
export interface BibliografiaEntry { export interface BibliografiaEntry {
id: string; id: string
tipo: 'BASICA' | 'COMPLEMENTARIA'; tipo: 'BASICA' | 'COMPLEMENTARIA'
cita: string; cita: string
fuenteBibliotecaId?: string; fuenteBibliotecaId?: string
fuenteBiblioteca?: any; fuenteBiblioteca?: any
} }
export interface BibliografiaTabProps { export interface BibliografiaTabProps {
bibliografia: BibliografiaEntry[]; bibliografia: BibliografiaEntry[]
onSave: (bibliografia: BibliografiaEntry[]) => void; onSave: (bibliografia: BibliografiaEntry[]) => void
isSaving: boolean; isSaving: boolean
} }
export default function MateriaDetailPage() { export interface AsignaturaDatos {
[key: string]: string
}
export interface AsignaturaResponse {
datos: AsignaturaDatos
}
function EditableHeaderField({
value,
onSave,
className,
}: {
value: string | number
onSave: (val: string) => void
className?: string
}) {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
;(e.currentTarget as HTMLElement).blur() // Quita el foco
}
}
const handleBlur = (e: React.FocusEvent<HTMLElement>) => {
const newValue = e.currentTarget.textContent || ''
if (newValue !== value.toString()) {
onSave(newValue)
}
}
return (
<span
contentEditable
suppressContentEditableWarning
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={`cursor-text rounded px-1 transition-all outline-none focus:ring-2 focus:ring-blue-400 ${className}`}
>
{value}
</span>
)
}
export default function MateriaDetailPage() {
const { data: asignaturasApi, isLoading: loadingAsig } = useSubject(
'9d4dda6a-488f-428a-8a07-38081592a641',
)
// 1. Asegúrate de tener estos estados en tu componente principal // 1. Asegúrate de tener estos estados en tu componente principal
const [messages, setMessages] = useState<IAMessage[]>([]); const [messages, setMessages] = useState<IAMessage[]>([])
const [datosGenerales, setDatosGenerales] = useState({}); const [datosGenerales, setDatosGenerales] = useState({})
const [campos, setCampos] = useState<CampoEstructura[]>([]); const [campos, setCampos] = useState<CampoEstructura[]>([])
// Dentro de MateriaDetailPage
const [headerData, setHeaderData] = useState({
codigo: '',
nombre: '',
creditos: 0,
ciclo: 0,
})
// Sincronizar cuando llegue la API
useEffect(() => {
if (asignaturasApi) {
setHeaderData({
codigo: asignaturasApi?.codigo ?? '',
nombre: asignaturasApi?.nombre ?? '',
creditos: asignaturasApi?.creditos ?? '',
ciclo: asignaturasApi?.numero_ciclo ?? 0,
})
}
}, [asignaturasApi])
const handleUpdateHeader = (key: string, value: string | number) => {
const newData = { ...headerData, [key]: value }
setHeaderData(newData)
console.log('💾 Guardando en estado y base de datos:', key, value)
}
/* ---------- sincronizar API ---------- */
useEffect(() => {
if (asignaturasApi?.datos) {
setDatosGenerales(asignaturasApi.datos)
}
}, [asignaturasApi])
// 2. Funciones de manejo para la IA // 2. Funciones de manejo para la IA
const handleSendMessage = (text: string, campoId?: string) => { const handleSendMessage = (text: string, campoId?: string) => {
@@ -58,104 +137,132 @@ const handleSendMessage = (text: string, campoId?: string) => {
role: 'user', role: 'user',
content: text, content: text,
timestamp: new Date(), timestamp: new Date(),
campoAfectado: campoId campoAfectado: campoId,
}; }
setMessages([...messages, newMessage]); setMessages([...messages, newMessage])
// Aquí llamarías a tu API de OpenAI/Claude // Aquí llamarías a tu API de OpenAI/Claude
//toast.info("Enviando consulta a la IA..."); //toast.info("Enviando consulta a la IA...");
}; }
const handleAcceptSuggestion = (sugerencia: IASugerencia) => { const handleAcceptSuggestion = (sugerencia: IASugerencia) => {
// Lógica para actualizar el valor del campo en tu estado de datosGenerales // Lógica para actualizar el valor del campo en tu estado de datosGenerales
//toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`); //toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`);
}; }
// Dentro de tu componente principal (donde están los Tabs) // Dentro de tu componente principal (donde están los Tabs)
const [bibliografia, setBibliografia] = useState<BibliografiaEntry[]>([ const [bibliografia, setBibliografia] = useState<BibliografiaEntry[]>([
{ {
id: '1', id: '1',
tipo: 'BASICA', tipo: 'BASICA',
cita: 'Russell, S., & Norvig, P. (2020). Artificial Intelligence: A Modern Approach. Pearson.' cita: 'Russell, S., & Norvig, P. (2020). Artificial Intelligence: A Modern Approach. Pearson.',
} },
]); ])
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false)
const handleSaveBibliografia = (data: BibliografiaEntry[]) => { const handleSaveBibliografia = (data: BibliografiaEntry[]) => {
setIsSaving(true); setIsSaving(true)
// Aquí iría tu llamada a la API // Aquí iría tu llamada a la API
setBibliografia(data); setBibliografia(data)
// Simulamos un guardado // Simulamos un guardado
setTimeout(() => { setTimeout(() => {
setIsSaving(false); setIsSaving(false)
//toast.success("Cambios guardados"); //toast.success("Cambios guardados");
}, 1000); }, 1000)
}; }
const [isRegenerating, setIsRegenerating] = useState(false); const [isRegenerating, setIsRegenerating] = useState(false)
const handleRegenerateDocument = useCallback(() => { const handleRegenerateDocument = useCallback(() => {
setIsRegenerating(true); setIsRegenerating(true)
setTimeout(() => { setTimeout(() => {
setIsRegenerating(false); setIsRegenerating(false)
}, 2000); }, 2000)
}, []); }, [])
return ( return (
<div className="w-full"> <div className="w-full">
{/* ================= HEADER ================= */} {/* ================= HEADER ACTUALIZADO ================= */}
<section className="bg-gradient-to-b from-[#0b1d3a] to-[#0e2a5c] text-white"> <section className="bg-gradient-to-b from-[#0b1d3a] to-[#0e2a5c] text-white">
<div className="max-w-7xl mx-auto px-6 py-10"> <div className="mx-auto max-w-7xl px-6 py-10">
<Link <Link
to="/planes" to="/planes"
className="flex items-center gap-2 text-sm text-blue-200 hover:text-white mb-4" className="mb-4 flex items-center gap-2 text-sm text-blue-200 hover:text-white"
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="h-4 w-4" /> Volver al plan
Volver al plan
</Link> </Link>
<div className="flex items-start justify-between gap-6"> <div className="flex items-start justify-between gap-6">
<div className="space-y-3"> <div className="space-y-3">
<Badge className="bg-blue-900/50 border border-blue-700"> {/* CÓDIGO EDITABLE */}
IA-401 <Badge className="border border-blue-700 bg-blue-900/50">
<EditableHeaderField
value={headerData.codigo}
onSave={(val) => handleUpdateHeader('codigo', val)}
/>
</Badge> </Badge>
{/* NOMBRE EDITABLE */}
<h1 className="text-3xl font-bold"> <h1 className="text-3xl font-bold">
Inteligencia Artificial Aplicada <EditableHeaderField
value={headerData.nombre}
onSave={(val) => handleUpdateHeader('nombre', val)}
/>
</h1> </h1>
<div className="flex flex-wrap gap-4 text-sm text-blue-200"> <div className="flex flex-wrap gap-4 text-sm text-blue-200">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<GraduationCap className="w-4 h-4" /> <GraduationCap className="h-4 w-4" />
Ingeniería en Sistemas Computacionales {asignaturasApi?.planes_estudio?.datos?.nombre}
</span>
<span>
{asignaturasApi?.planes_estudio?.carreras?.facultades?.nombre}
</span> </span>
<span>Facultad de Ingeniería</span>
</div> </div>
<p className="text-sm text-blue-300"> <p className="text-sm text-blue-300">
Pertenece al plan:{' '} Pertenece al plan:{' '}
<span className="underline cursor-pointer"> <span className="cursor-pointer underline">
Licenciatura en Ingeniería en Sistemas Computacionales 2024 {asignaturasApi?.planes_estudio?.nombre}
</span> </span>
</p> </p>
</div> </div>
<div className="flex flex-col gap-2 items-end"> <div className="flex flex-col items-end gap-2 text-right">
<Badge variant="secondary">8 créditos</Badge> {/* CRÉDITOS EDITABLES */}
<Badge variant="secondary">7° semestre</Badge> <Badge variant="secondary" className="gap-1">
<Badge variant="secondary">Sistemas Inteligentes</Badge> <EditableHeaderField
value={headerData.creditos}
onSave={(val) =>
handleUpdateHeader('creditos', parseInt(val) || 0)
}
/>
<span>créditos</span>
</Badge>
{/* SEMESTRE EDITABLE */}
<Badge variant="secondary" className="gap-1">
<EditableHeaderField
value={headerData.ciclo}
onSave={(val) =>
handleUpdateHeader('ciclo', parseInt(val) || 0)
}
/>
<span>° ciclo</span>
</Badge>
<Badge variant="secondary">{asignaturasApi?.tipo}</Badge>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
{/* ================= TABS ================= */} {/* ================= TABS ================= */}
<section className="bg-white border-b"> <section className="border-b bg-white">
<div className="max-w-7xl mx-auto px-6"> <div className="mx-auto max-w-7xl px-6">
<Tabs defaultValue="datos"> <Tabs defaultValue="datos">
<TabsList className="h-auto bg-transparent p-0 gap-6"> <TabsList className="h-auto gap-6 bg-transparent p-0">
<TabsTrigger value="datos">Datos generales</TabsTrigger> <TabsTrigger value="datos">Datos generales</TabsTrigger>
<TabsTrigger value="contenido">Contenido temático</TabsTrigger> <TabsTrigger value="contenido">Contenido temático</TabsTrigger>
<TabsTrigger value="bibliografia">Bibliografía</TabsTrigger> <TabsTrigger value="bibliografia">Bibliografía</TabsTrigger>
@@ -168,11 +275,14 @@ const handleRegenerateDocument = useCallback(() => {
{/* ================= TAB: DATOS GENERALES ================= */} {/* ================= TAB: DATOS GENERALES ================= */}
<TabsContent value="datos"> <TabsContent value="datos">
<DatosGenerales /> <DatosGenerales data={datosGenerales} isLoading={loadingAsig} />
</TabsContent> </TabsContent>
<TabsContent value="contenido"> <TabsContent value="contenido">
<ContenidoTematico></ContenidoTematico> <ContenidoTematico
data={asignaturasApi}
isLoading={loadingAsig}
></ContenidoTematico>
</TabsContent> </TabsContent>
<TabsContent value="bibliografia"> <TabsContent value="bibliografia">
@@ -190,7 +300,12 @@ const handleRegenerateDocument = useCallback(() => {
messages={messages} messages={messages}
onSendMessage={handleSendMessage} onSendMessage={handleSendMessage}
onAcceptSuggestion={handleAcceptSuggestion} onAcceptSuggestion={handleAcceptSuggestion}
onRejectSuggestion={(id) => console.log("Rechazada") /*toast.error("Sugerencia rechazada")*/} onRejectSuggestion={
(id) =>
console.log(
'Rechazada',
) /*toast.error("Sugerencia rechazada")*/
}
/> />
</TabsContent> </TabsContent>
@@ -206,7 +321,7 @@ const handleRegenerateDocument = useCallback(() => {
</TabsContent> </TabsContent>
<TabsContent value="historial"> <TabsContent value="historial">
<HistorialTab historial={mockHistorial} /> <HistorialTab />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>
@@ -216,54 +331,46 @@ const handleRegenerateDocument = useCallback(() => {
} }
/* ================= TAB CONTENT ================= */ /* ================= TAB CONTENT ================= */
interface DatosGeneralesProps {
data: AsignaturaDatos
isLoading: boolean
}
function DatosGenerales({ data, isLoading }: DatosGeneralesProps) {
const formatTitle = (key: string): string =>
key.replace(/_/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase())
function DatosGenerales() {
return ( return (
<div className="max-w-7xl mx-auto py-8 px-4 space-y-8 animate-in fade-in duration-500"> <div className="animate-in fade-in mx-auto max-w-7xl space-y-8 px-4 py-8 duration-500">
{/* Encabezado de la Sección */} {/* Encabezado de la Sección */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b pb-6"> <div className="flex flex-col justify-between gap-4 border-b pb-6 md:flex-row md:items-center">
<div> <div>
<h2 className="text-2xl font-bold tracking-tight text-slate-900">Datos Generales</h2> <h2 className="text-2xl font-bold tracking-tight text-slate-900">
<p className="text-slate-500 mt-1"> Datos Generales
</h2>
<p className="mt-1 text-slate-500">
Información oficial estructurada bajo los lineamientos de la SEP. Información oficial estructurada bajo los lineamientos de la SEP.
</p> </p>
</div> </div>
<div className="flex gap-3">
<Button variant="outline" size="sm" className="gap-2">
<Edit2 className="w-4 h-4" /> Editar borrador
</Button>
<Button size="sm" className="gap-2 bg-blue-600 hover:bg-blue-700">
<Save className="w-4 h-4" /> Guardar cambios
</Button>
</div>
</div> </div>
{/* Grid de Información */} {/* Grid de Información */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 gap-6 md:grid-cols-3">
{/* Columna Principal (Más ancha) */} {/* Columna Principal (Más ancha) */}
<div className="md:col-span-2 space-y-6"> <div className="space-y-6 md:col-span-2">
<div className="md:col-span-2 space-y-6"> {isLoading && <p>Cargando información...</p>}
<InfoCard
title="Competencias a Desarrollar"
subtitle="Competencias profesionales que se desarrollarán"
isList={true}
initialContent={`• Diseñar algoritmos de machine learning para clasificación y predicción\n• Implementar redes neuronales profundas para procesamiento de imágenes\n• Evaluar y optimizar modelos de IA considerando métricas`}
/>
{!isLoading &&
Object.entries(data).map(([key, value]) => (
<InfoCard <InfoCard
title="Objetivo General" key={key}
initialContent="Formar profesionales capaces de diseñar, implementar y evaluar sistemas de inteligencia artificial que resuelvan problemas complejos." title={formatTitle(key)}
initialContent={value}
onEnhanceAI={(contenido) => {
console.log('Llevar a IA:', contenido)
// Aquí tu lógica: setPestañaActiva('mejorar-con-ia');
}}
/> />
</div> ))}
<div className="space-y-6">
<InfoCard
title="Justificación"
initialContent="La inteligencia artificial es una de las tecnologías más disruptivas..."
/>
</div>
</div> </div>
{/* Columna Lateral (Información Secundaria) */} {/* Columna Lateral (Información Secundaria) */}
@@ -274,8 +381,16 @@ function DatosGenerales() {
title="Requisitos y Seriación" title="Requisitos y Seriación"
type="requirements" type="requirements"
initialContent={[ initialContent={[
{ type: "Pre-requisito", code: "PA-301", name: "Programación Avanzada" }, {
{ type: "Co-requisito", code: "MAT-201", name: "Matemáticas Discretas" } type: 'Pre-requisito',
code: 'PA-301',
name: 'Programación Avanzada',
},
{
type: 'Co-requisito',
code: 'MAT-201',
name: 'Matemáticas Discretas',
},
]} ]}
/> />
@@ -284,10 +399,10 @@ function DatosGenerales() {
title="Sistema de Evaluación" title="Sistema de Evaluación"
type="evaluation" type="evaluation"
initialContent={[ initialContent={[
{ label: "Exámenes parciales", value: "30%" }, { label: 'Exámenes parciales', value: '30%' },
{ label: "Proyecto integrador", value: "35%" }, { label: 'Proyecto integrador', value: '35%' },
{ label: "Prácticas de laboratorio", value: "20%" }, { label: 'Prácticas de laboratorio', value: '20%' },
{ label: "Participación", value: "15%" }, { label: 'Participación', value: '15%' },
]} ]}
/> />
</div> </div>
@@ -298,37 +413,59 @@ function DatosGenerales() {
} }
interface InfoCardProps { interface InfoCardProps {
title: string, title: string
subtitle?: string initialContent: any
isList?:boolean type?: 'text' | 'requirements' | 'evaluation'
initialContent: any // Puede ser string o array de objetos onEnhanceAI?: (content: any) => void // Nueva prop para la acción de IA
type?: 'text' | 'list' | 'requirements' | 'evaluation'
} }
function InfoCard({ title, initialContent, type = 'text' }: InfoCardProps) { function InfoCard({
title,
initialContent,
type = 'text',
onEnhanceAI,
}: InfoCardProps) {
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [data, setData] = useState(initialContent) const [data, setData] = useState(initialContent)
// Estado temporal para el área de texto (siempre editamos como texto por simplicidad)
const [tempText, setTempText] = useState( const [tempText, setTempText] = useState(
type === 'text' || type === 'list' type === 'text' ? initialContent : JSON.stringify(initialContent, null, 2),
? initialContent
: JSON.stringify(initialContent, null, 2) // O un formato legible
) )
const handleSave = () => { const handleSave = () => {
// Aquí podrías parsear el texto de vuelta si es necesario
setData(tempText) setData(tempText)
setIsEditing(false) setIsEditing(false)
} }
return ( return (
<Card className="transition-all hover:border-slate-300"> <Card className="transition-all hover:border-slate-300">
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0"> <CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
<CardTitle className="text-sm font-bold text-slate-700">{title}</CardTitle> <CardTitle className="text-sm font-bold text-slate-700">
{title}
</CardTitle>
{!isEditing && ( {!isEditing && (
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400" onClick={() => setIsEditing(true)}> <div className="flex gap-1">
{/* NUEVO: Botón de Mejorar con IA */}
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-blue-500 hover:bg-blue-50 hover:text-blue-600"
onClick={() => onEnhanceAI?.(data)} // Enviamos la data actual a la IA
title="Mejorar con IA"
>
<Sparkles className="h-4 w-4" />
</Button>
{/* Botón de Editar original */}
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400"
onClick={() => setIsEditing(true)}
>
<Pencil className="h-3 w-3" /> <Pencil className="h-3 w-3" />
</Button> </Button>
</div>
)} )}
</CardHeader> </CardHeader>
@@ -338,11 +475,23 @@ function InfoCard({ title, initialContent, type = 'text' }: InfoCardProps) {
<Textarea <Textarea
value={tempText} value={tempText}
onChange={(e) => setTempText(e.target.value)} onChange={(e) => setTempText(e.target.value)}
className="text-xs min-h-[100px]" className="min-h-[100px] text-xs"
/> />
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button size="sm" variant="ghost" onClick={() => setIsEditing(false)}>Cancelar</Button> <Button
<Button size="sm" className="bg-[#00a878]" onClick={handleSave}>Guardar</Button> size="sm"
variant="ghost"
onClick={() => setIsEditing(false)}
>
Cancelar
</Button>
<Button
size="sm"
className="bg-[#00a878] hover:bg-[#008f66]"
onClick={handleSave}
>
Guardar
</Button>
</div> </div>
</div> </div>
) : ( ) : (
@@ -362,9 +511,16 @@ function RequirementsView({ items }: { items: any[] }) {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{items.map((req, i) => ( {items.map((req, i) => (
<div key={i} className="p-3 bg-slate-50 rounded-lg border border-slate-100"> <div
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-tight">{req.type}</p> key={i}
<p className="text-sm font-medium text-slate-700">{req.code} {req.name}</p> className="rounded-lg border border-slate-100 bg-slate-50 p-3"
>
<p className="text-[10px] font-bold tracking-tight text-slate-400 uppercase">
{req.type}
</p>
<p className="text-sm font-medium text-slate-700">
{req.code} {req.name}
</p>
</div> </div>
))} ))}
</div> </div>
@@ -376,7 +532,10 @@ function EvaluationView({ items }: { items: any[] }) {
return ( return (
<div className="space-y-2"> <div className="space-y-2">
{items.map((item, i) => ( {items.map((item, i) => (
<div key={i} className="flex justify-between text-sm border-b border-slate-50 pb-1.5 italic"> <div
key={i}
className="flex justify-between border-b border-slate-50 pb-1.5 text-sm italic"
>
<span className="text-slate-500">{item.label}</span> <span className="text-slate-500">{item.label}</span>
<span className="font-bold text-blue-600">{item.value}</span> <span className="font-bold text-blue-600">{item.value}</span>
</div> </div>
@@ -384,13 +543,3 @@ function EvaluationView({ items }: { items: any[] }) {
</div> </div>
) )
} }
function EmptyTab({ title }: { title: string }) {
return (
<div className="py-16 text-center text-muted-foreground">
{title} (pendiente)
</div>
)
}

View File

@@ -1,6 +1,9 @@
import { TemplateSelectorCard } from './TemplateSelectorCard' import type {
EstructuraPlanRow,
import type { CARRERAS } from '@/features/planes/nuevo/catalogs' FacultadRow,
NivelPlanEstudio,
TipoCiclo,
} from '@/data/types/domain'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types' import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -12,25 +15,30 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator' import { useCatalogosPlanes } from '@/data/hooks/usePlans'
import { import { NIVELES, TIPOS_CICLO } from '@/features/planes/nuevo/catalogs'
FACULTADES,
NIVELES,
TIPOS_CICLO,
PLANTILLAS_ANEXO_1,
PLANTILLAS_ANEXO_2,
} from '@/features/planes/nuevo/catalogs'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
export function PasoBasicosForm({ export function PasoBasicosForm({
wizard, wizard,
onChange, onChange,
carrerasFiltradas,
}: { }: {
wizard: NewPlanWizardState wizard: NewPlanWizardState
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>> onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
carrerasFiltradas: typeof CARRERAS
}) { }) {
const { data: catalogos } = useCatalogosPlanes()
// Preferir los catálogos remotos si están disponibles; si no, usar los locales
const facultadesList = catalogos?.facultades ?? []
const rawCarreras = catalogos?.carreras ?? []
const estructurasPlanList = catalogos?.estructurasPlan ?? []
const filteredCarreras = rawCarreras.filter((c: any) => {
const facId = wizard.datosBasicos.facultadId
if (!facId) return true
// soportar ambos shapes: `facultad_id` (BD) o `facultadId` (local)
return c.facultad_id ? c.facultad_id === facId : c.facultadId === facId
})
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
@@ -40,13 +48,18 @@ export function PasoBasicosForm({
</Label> </Label>
<Input <Input
id="nombrePlan" id="nombrePlan"
placeholder="Ej. Ingeniería en Sistemas 2026" placeholder="Ej. Ingeniería en Sistemas (2026)"
value={wizard.datosBasicos.nombrePlan} value={wizard.datosBasicos.nombrePlan}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange((w) => ({ onChange(
(w): NewPlanWizardState => ({
...w, ...w,
datosBasicos: { ...w.datosBasicos, nombrePlan: e.target.value }, datosBasicos: {
})) ...w.datosBasicos,
nombrePlan: e.target.value,
},
}),
)
} }
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic" className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
/> />
@@ -57,14 +70,16 @@ export function PasoBasicosForm({
<Select <Select
value={wizard.datosBasicos.facultadId} value={wizard.datosBasicos.facultadId}
onValueChange={(value) => onValueChange={(value) =>
onChange((w) => ({ onChange(
(w): NewPlanWizardState => ({
...w, ...w,
datosBasicos: { datosBasicos: {
...w.datosBasicos, ...w.datosBasicos,
facultadId: value, facultadId: value,
carreraId: '', carreraId: '',
}, },
})) }),
)
} }
> >
<SelectTrigger <SelectTrigger
@@ -79,7 +94,7 @@ export function PasoBasicosForm({
<SelectValue placeholder="Ej. Facultad de Ingeniería" /> <SelectValue placeholder="Ej. Facultad de Ingeniería" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{FACULTADES.map((f) => ( {facultadesList.map((f: FacultadRow) => (
<SelectItem key={f.id} value={f.id}> <SelectItem key={f.id} value={f.id}>
{f.nombre} {f.nombre}
</SelectItem> </SelectItem>
@@ -93,10 +108,12 @@ export function PasoBasicosForm({
<Select <Select
value={wizard.datosBasicos.carreraId} value={wizard.datosBasicos.carreraId}
onValueChange={(value) => onValueChange={(value) =>
onChange((w) => ({ onChange(
(w): NewPlanWizardState => ({
...w, ...w,
datosBasicos: { ...w.datosBasicos, carreraId: value }, datosBasicos: { ...w.datosBasicos, carreraId: value },
})) }),
)
} }
disabled={!wizard.datosBasicos.facultadId} disabled={!wizard.datosBasicos.facultadId}
> >
@@ -112,7 +129,7 @@ export function PasoBasicosForm({
<SelectValue placeholder="Ej. Ingeniería en Cibernética y Sistemas Computacionales" /> <SelectValue placeholder="Ej. Ingeniería en Cibernética y Sistemas Computacionales" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{carrerasFiltradas.map((c) => ( {filteredCarreras.map((c: any) => (
<SelectItem key={c.id} value={c.id}> <SelectItem key={c.id} value={c.id}>
{c.nombre} {c.nombre}
</SelectItem> </SelectItem>
@@ -125,11 +142,13 @@ export function PasoBasicosForm({
<Label htmlFor="nivel">Nivel</Label> <Label htmlFor="nivel">Nivel</Label>
<Select <Select
value={wizard.datosBasicos.nivel} value={wizard.datosBasicos.nivel}
onValueChange={(value) => onValueChange={(value: NivelPlanEstudio) =>
onChange((w) => ({ onChange(
(w): NewPlanWizardState => ({
...w, ...w,
datosBasicos: { ...w.datosBasicos, nivel: value }, datosBasicos: { ...w.datosBasicos, nivel: value },
})) }),
)
} }
> >
<SelectTrigger <SelectTrigger
@@ -157,14 +176,16 @@ export function PasoBasicosForm({
<Label htmlFor="tipoCiclo">Tipo de ciclo</Label> <Label htmlFor="tipoCiclo">Tipo de ciclo</Label>
<Select <Select
value={wizard.datosBasicos.tipoCiclo} value={wizard.datosBasicos.tipoCiclo}
onValueChange={(value) => onValueChange={(value: TipoCiclo) =>
onChange((w) => ({ onChange(
(w): NewPlanWizardState => ({
...w, ...w,
datosBasicos: { datosBasicos: {
...w.datosBasicos, ...w.datosBasicos,
tipoCiclo: value as any, tipoCiclo: value as any,
}, },
})) }),
)
} }
> >
<SelectTrigger <SelectTrigger
@@ -180,8 +201,8 @@ export function PasoBasicosForm({
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{TIPOS_CICLO.map((t) => ( {TIPOS_CICLO.map((t) => (
<SelectItem key={t.value} value={t.value}> <SelectItem key={t} value={t}>
{t.label} {t}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -196,22 +217,63 @@ export function PasoBasicosForm({
min={1} min={1}
value={wizard.datosBasicos.numCiclos ?? ''} value={wizard.datosBasicos.numCiclos ?? ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange((w) => ({ onChange(
(w): NewPlanWizardState => ({
...w, ...w,
datosBasicos: { datosBasicos: {
...w.datosBasicos, ...w.datosBasicos,
// Keep undefined when the input is empty so the field stays optional // Keep undefined when the input is empty so the field stays optional
numCiclos: numCiclos:
e.target.value === '' ? undefined : Number(e.target.value), e.target.value === ''
? undefined
: Number(e.target.value),
}, },
})) }),
)
} }
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic" className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
placeholder="Ej. 8" placeholder="Ej. 8"
/> />
</div> </div>
<div className="grid gap-1">
<Label htmlFor="estructuraPlan">Estructura de plan de estudios</Label>
<Select
value={wizard.datosBasicos.estructuraPlanId ?? ''}
onValueChange={(value: string) =>
onChange(
(w): NewPlanWizardState => ({
...w,
datosBasicos: {
...w.datosBasicos,
estructuraPlanId: value,
},
}),
)
}
>
<SelectTrigger
id="tipoCiclo"
className={cn(
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
!wizard.datosBasicos.estructuraPlanId
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
: 'font-medium not-italic', // Tiene Valor (Medium)
)}
>
<SelectValue placeholder="Ej. Plan base SEP/ULSA (2026)" />
</SelectTrigger>
<SelectContent>
{estructurasPlanList.map((t: EstructuraPlanRow) => (
<SelectItem key={t.id} value={t.id}>
{t.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
<Separator className="my-3" /> </div>
{/* <Separator className="my-3" />
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
<TemplateSelectorCard <TemplateSelectorCard
cardTitle="Plantilla de plan de estudios" cardTitle="Plantilla de plan de estudios"
@@ -247,7 +309,7 @@ export function PasoBasicosForm({
})) }))
} }
/> />
</div> </div> */}
</div> </div>
) )
} }

View File

@@ -2,13 +2,13 @@ import { Upload, File, X, FileText } from 'lucide-react'
import { useState, useCallback, useEffect, useRef } from 'react' import { useState, useCallback, useEffect, useRef } from 'react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { formatFileSize } from '@/features/planes/utils/format-file-size'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
interface UploadedFile { export interface UploadedFile {
id: string id: string // Necesario para React (key)
name: string file: File // La fuente de verdad (contiene name, size, type)
size: string preview?: string // Opcional: si fueran imágenes
type: string
} }
interface FileDropzoneProps { interface FileDropzoneProps {
@@ -37,9 +37,7 @@ export function FileDropzone({
typeof crypto !== 'undefined' && 'randomUUID' in crypto typeof crypto !== 'undefined' && 'randomUUID' in crypto
? (crypto as any).randomUUID() ? (crypto as any).randomUUID()
: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, : `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
name: file.name, file,
size: formatFileSize(file.size),
type: file.name.split('.').pop() || 'file',
})) }))
setFiles((prev) => { setFiles((prev) => {
const room = Math.max(0, maxFiles - prev.length) const room = Math.max(0, maxFiles - prev.length)
@@ -97,12 +95,6 @@ export function FileDropzone({
if (onFilesChangeRef.current) onFilesChangeRef.current(files) if (onFilesChangeRef.current) onFilesChangeRef.current(files)
}, [files]) }, [files])
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
const getFileIcon = (type: string) => { const getFileIcon = (type: string) => {
switch (type.toLowerCase()) { switch (type.toLowerCase()) {
case 'pdf': case 'pdf':
@@ -170,23 +162,25 @@ export function FileDropzone({
{/* Uploaded files list */} {/* Uploaded files list */}
{files.length > 0 && ( {files.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
{files.map((file) => ( {files.map((item) => (
<div <div
key={file.id} key={item.id}
className="bg-accent/50 border-border fade-in flex items-center gap-3 rounded-lg border p-3" className="bg-accent/50 border-border fade-in flex items-center gap-3 rounded-lg border p-3"
> >
{getFileIcon(file.type)} {getFileIcon(item.file.type)}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-foreground truncate text-sm font-medium"> <p className="text-foreground truncate text-sm font-medium">
{file.name} {item.file.name}
</p>
<p className="text-muted-foreground text-xs">
{formatFileSize(item.file.size)}
</p> </p>
<p className="text-muted-foreground text-xs">{file.size}</p>
</div> </div>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="text-muted-foreground hover:text-destructive h-8 w-8" className="text-muted-foreground hover:text-destructive h-8 w-8"
onClick={() => removeFile(file.id)} onClick={() => removeFile(item.id)}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>

View File

@@ -1,6 +1,7 @@
import { FileDropzone } from './FileDropZone' import { FileDropzone } from './FileDropZone'
import ReferenciasParaIA from './ReferenciasParaIA' import ReferenciasParaIA from './ReferenciasParaIA'
import type { UploadedFile } from './FileDropZone'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types' import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -30,7 +31,7 @@ export function PasoDetallesPanel({
onGenerarIA: () => void onGenerarIA: () => void
isLoading: boolean isLoading: boolean
}) { }) {
if (wizard.modoCreacion === 'MANUAL') { if (wizard.tipoOrigen === 'MANUAL') {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
@@ -43,7 +44,7 @@ export function PasoDetallesPanel({
) )
} }
if (wizard.modoCreacion === 'IA') { if (wizard.tipoOrigen === 'IA') {
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">
@@ -116,14 +117,16 @@ export function PasoDetallesPanel({
} }
}) })
} }
onFilesChange={(files) => onFilesChange={(files: Array<UploadedFile>) =>
onChange((w) => ({ onChange(
(w): NewPlanWizardState => ({
...w, ...w,
iaConfig: { iaConfig: {
...(w.iaConfig || ({} as any)), ...(w.iaConfig || ({} as any)),
archivosAdjuntos: files, archivosAdjuntos: files,
}, },
})) }),
)
} }
/> />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -162,10 +165,7 @@ export function PasoDetallesPanel({
) )
} }
if ( if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
wizard.modoCreacion === 'CLONADO' &&
wizard.subModoClonado === 'INTERNO'
) {
return ( return (
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid gap-3 sm:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-3">
@@ -269,10 +269,7 @@ export function PasoDetallesPanel({
) )
} }
if ( if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
wizard.modoCreacion === 'CLONADO' &&
wizard.subModoClonado === 'TRADICIONAL'
) {
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">

View File

@@ -5,6 +5,8 @@ import BarraBusqueda from '../../BarraBusqueda'
import { FileDropzone } from './FileDropZone' import { FileDropzone } from './FileDropZone'
import type { UploadedFile } from './FileDropZone'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { import {
@@ -27,9 +29,7 @@ const ReferenciasParaIA = ({
selectedRepositorioIds?: Array<string> selectedRepositorioIds?: Array<string>
onToggleArchivo?: (id: string, checked: boolean) => void onToggleArchivo?: (id: string, checked: boolean) => void
onToggleRepositorio?: (id: string, checked: boolean) => void onToggleRepositorio?: (id: string, checked: boolean) => void
onFilesChange?: ( onFilesChange?: (files: Array<UploadedFile>) => void
files: Array<{ id: string; name: string; size: string; type: string }>,
) => void
}) => { }) => {
const [busquedaArchivos, setBusquedaArchivos] = useState('') const [busquedaArchivos, setBusquedaArchivos] = useState('')
const [busquedaRepositorios, setBusquedaRepositorios] = useState('') const [busquedaRepositorios, setBusquedaRepositorios] = useState('')

View File

@@ -1,10 +1,7 @@
import * as Icons from 'lucide-react' import * as Icons from 'lucide-react'
import type { import type { TipoOrigen } from '@/data/types/domain'
NewPlanWizardState, import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
ModoCreacion,
SubModoClonado,
} from '@/features/planes/nuevo/types'
import { import {
Card, Card,
@@ -21,8 +18,7 @@ export function PasoModoCardGroup({
wizard: NewPlanWizardState wizard: NewPlanWizardState
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>> onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
}) { }) {
const isSelected = (m: ModoCreacion) => wizard.modoCreacion === m const isSelected = (m: TipoOrigen) => wizard.tipoOrigen === m
const isSubSelected = (s: SubModoClonado) => wizard.subModoClonado === s
const handleKeyActivate = (e: React.KeyboardEvent, cb: () => void) => { const handleKeyActivate = (e: React.KeyboardEvent, cb: () => void) => {
const key = e.key const key = e.key
if ( if (
@@ -41,19 +37,21 @@ export function PasoModoCardGroup({
<Card <Card
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''} className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
onClick={() => onClick={() =>
onChange((w) => ({ onChange(
(w): NewPlanWizardState => ({
...w, ...w,
modoCreacion: 'MANUAL', tipoOrigen: 'MANUAL',
subModoClonado: undefined, }),
})) )
} }
onKeyDown={(e: React.KeyboardEvent) => onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () => handleKeyActivate(e, () =>
onChange((w) => ({ onChange(
(w): NewPlanWizardState => ({
...w, ...w,
modoCreacion: 'MANUAL', tipoOrigen: 'MANUAL',
subModoClonado: undefined, }),
})), ),
) )
} }
role="button" role="button"
@@ -70,19 +68,21 @@ export function PasoModoCardGroup({
<Card <Card
className={isSelected('IA') ? 'ring-ring ring-2' : ''} className={isSelected('IA') ? 'ring-ring ring-2' : ''}
onClick={() => onClick={() =>
onChange((w) => ({ onChange(
(w): NewPlanWizardState => ({
...w, ...w,
modoCreacion: 'IA', tipoOrigen: 'IA',
subModoClonado: undefined, }),
})) )
} }
onKeyDown={(e: React.KeyboardEvent) => onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () => handleKeyActivate(e, () =>
onChange((w) => ({ onChange(
(w): NewPlanWizardState => ({
...w, ...w,
modoCreacion: 'IA', tipoOrigen: 'IA',
subModoClonado: undefined, }),
})), ),
) )
} }
role="button" role="button"
@@ -99,11 +99,13 @@ export function PasoModoCardGroup({
</Card> </Card>
<Card <Card
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''} className={isSelected('OTRO') ? 'ring-ring ring-2' : ''}
onClick={() => onChange((w) => ({ ...w, modoCreacion: 'CLONADO' }))} onClick={() =>
onChange((w): NewPlanWizardState => ({ ...w, tipoOrigen: 'OTRO' }))
}
onKeyDown={(e: React.KeyboardEvent) => onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () => handleKeyActivate(e, () =>
onChange((w) => ({ ...w, modoCreacion: 'CLONADO' })), onChange((w): NewPlanWizardState => ({ ...w, tipoOrigen: 'OTRO' })),
) )
} }
role="button" role="button"
@@ -115,22 +117,34 @@ export function PasoModoCardGroup({
</CardTitle> </CardTitle>
<CardDescription>Desde un plan existente o archivos.</CardDescription> <CardDescription>Desde un plan existente o archivos.</CardDescription>
</CardHeader> </CardHeader>
{wizard.modoCreacion === 'CLONADO' && ( {(wizard.tipoOrigen === 'OTRO' ||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
<CardContent className="flex flex-col gap-3"> <CardContent className="flex flex-col gap-3">
<div <div
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' })) onChange(
(w): NewPlanWizardState => ({
...w,
tipoOrigen: 'CLONADO_INTERNO',
}),
)
}} }}
onKeyDown={(e: React.KeyboardEvent) => onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () => handleKeyActivate(e, () =>
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' })), onChange(
(w): NewPlanWizardState => ({
...w,
tipoOrigen: 'CLONADO_INTERNO',
}),
),
) )
} }
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${ className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
isSubSelected('INTERNO') isSelected('CLONADO_INTERNO')
? 'border-primary bg-primary/5 ring-primary text-primary ring-1' ? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
: 'border-border text-muted-foreground' : 'border-border text-muted-foreground'
} `} } `}
@@ -144,15 +158,25 @@ export function PasoModoCardGroup({
tabIndex={0} tabIndex={0}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' })) onChange(
(w): NewPlanWizardState => ({
...w,
tipoOrigen: 'CLONADO_TRADICIONAL',
}),
)
}} }}
onKeyDown={(e: React.KeyboardEvent) => onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () => handleKeyActivate(e, () =>
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' })), onChange(
(w): NewPlanWizardState => ({
...w,
tipoOrigen: 'CLONADO_TRADICIONAL',
}),
),
) )
} }
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${ className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
isSubSelected('TRADICIONAL') isSelected('CLONADO_TRADICIONAL')
? 'border-primary bg-primary/5 ring-primary text-primary ring-1' ? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
: 'border-border text-muted-foreground' : 'border-border text-muted-foreground'
} `} } `}

View File

@@ -8,12 +8,11 @@ import {
CardTitle, CardTitle,
} from '@/components/ui/card' } from '@/components/ui/card'
import { import {
PLANTILLAS_ANEXO_1,
PLANTILLAS_ANEXO_2,
PLANES_EXISTENTES, PLANES_EXISTENTES,
ARCHIVOS, ARCHIVOS,
REPOSITORIOS, REPOSITORIOS,
} from '@/features/planes/nuevo/catalogs' } from '@/features/planes/nuevo/catalogs'
import { formatFileSize } from '@/features/planes/utils/format-file-size'
export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) { export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
return ( return (
@@ -32,12 +31,6 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
const repositoriosRef = const repositoriosRef =
wizard.iaConfig?.repositoriosReferencia ?? [] wizard.iaConfig?.repositoriosReferencia ?? []
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? [] const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
const plantillaPlan = PLANTILLAS_ANEXO_1.find(
(x) => x.id === wizard.datosBasicos.plantillaPlanId,
)
const plantillaMapa = PLANTILLAS_ANEXO_2.find(
(x) => x.id === wizard.datosBasicos.plantillaMapaId,
)
const contenido = ( const contenido = (
<> <>
<div> <div>
@@ -68,49 +61,20 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
{wizard.datosBasicos.tipoCiclo}) {wizard.datosBasicos.tipoCiclo})
</span> </span>
</div> </div>
<div className="mt-2">
<span className="text-muted-foreground">
Plantilla plan:{' '}
</span>
<span className="font-medium">
{(plantillaPlan?.name ||
wizard.datosBasicos.plantillaPlanId ||
'—') +
' · ' +
(wizard.datosBasicos.plantillaPlanVersion || '—')}
</span>
</div>
<div>
<span className="text-muted-foreground">
Mapa curricular:{' '}
</span>
<span className="font-medium">
{(plantillaMapa?.name ||
wizard.datosBasicos.plantillaMapaId ||
'—') +
' · ' +
(wizard.datosBasicos.plantillaMapaVersion || '—')}
</span>
</div>
<div className="mt-2"> <div className="mt-2">
<span className="text-muted-foreground">Modo: </span> <span className="text-muted-foreground">Modo: </span>
<span className="font-medium"> <span className="font-medium">
{wizard.modoCreacion === 'MANUAL' && 'Manual'} {wizard.tipoOrigen === 'MANUAL' && 'Manual'}
{wizard.modoCreacion === 'IA' && 'Generado con IA'} {wizard.tipoOrigen === 'IA' && 'Generado con IA'}
{wizard.modoCreacion === 'CLONADO' && {wizard.tipoOrigen === 'CLONADO_INTERNO' &&
wizard.subModoClonado === 'INTERNO' &&
'Clonado desde plan del sistema'} 'Clonado desde plan del sistema'}
{wizard.modoCreacion === 'CLONADO' && {wizard.tipoOrigen === 'CLONADO_TRADICIONAL' &&
wizard.subModoClonado === 'TRADICIONAL' &&
'Importado desde documentos tradicionales'} 'Importado desde documentos tradicionales'}
</span> </span>
</div> </div>
{wizard.modoCreacion === 'CLONADO' && {wizard.tipoOrigen === 'CLONADO_INTERNO' && (
wizard.subModoClonado === 'INTERNO' && (
<div className="mt-2"> <div className="mt-2">
<span className="text-muted-foreground"> <span className="text-muted-foreground">Plan origen: </span>
Plan origen:{' '}
</span>
<span className="font-medium"> <span className="font-medium">
{(() => { {(() => {
const p = PLANES_EXISTENTES.find( const p = PLANES_EXISTENTES.find(
@@ -123,17 +87,13 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
</span> </span>
</div> </div>
)} )}
{wizard.modoCreacion === 'CLONADO' && {wizard.tipoOrigen === 'CLONADO_TRADICIONAL' && (
wizard.subModoClonado === 'TRADICIONAL' && (
<div className="mt-2"> <div className="mt-2">
<div className="font-medium">Documentos adjuntos</div> <div className="font-medium">Documentos adjuntos</div>
<ul className="text-muted-foreground list-disc pl-5 text-xs"> <ul className="text-muted-foreground list-disc pl-5 text-xs">
<li> <li>
<span className="text-foreground"> <span className="text-foreground">Word del plan:</span>{' '}
Word del plan: {wizard.clonTradicional?.archivoWordPlanId?.name || '—'}
</span>{' '}
{wizard.clonTradicional?.archivoWordPlanId?.name ||
'—'}
</li> </li>
<li> <li>
<span className="text-foreground"> <span className="text-foreground">
@@ -150,7 +110,7 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
</ul> </ul>
</div> </div>
)} )}
{wizard.modoCreacion === 'IA' && ( {wizard.tipoOrigen === 'IA' && (
<div className="bg-muted/50 mt-2 rounded-md p-3"> <div className="bg-muted/50 mt-2 rounded-md p-3">
<div> <div>
<span className="text-muted-foreground">Enfoque: </span> <span className="text-muted-foreground">Enfoque: </span>
@@ -208,8 +168,10 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
<ul className="text-muted-foreground list-disc pl-5 text-xs"> <ul className="text-muted-foreground list-disc pl-5 text-xs">
{adjuntos.map((f) => ( {adjuntos.map((f) => (
<li key={f.id}> <li key={f.id}>
<span className="text-foreground">{f.name}</span>{' '} <span className="text-foreground">
<span>· {f.size}</span> {f.file.name}
</span>{' '}
<span>· {formatFileSize(f.file.size)}</span>
</li> </li>
))} ))}
</ul> </ul>

View File

@@ -1,7 +1,7 @@
import { supabaseBrowser } from "../supabase/client"; import { supabaseBrowser } from '../supabase/client'
import { invokeEdge } from "../supabase/invokeEdge"; import { invokeEdge } from '../supabase/invokeEdge'
import { buildRange, requireData, throwIfError } from "./_helpers"; import { buildRange, requireData, throwIfError } from './_helpers'
import type { import type {
Asignatura, Asignatura,
@@ -13,59 +13,59 @@ import type {
PlanEstudio, PlanEstudio,
TipoCiclo, TipoCiclo,
UUID, UUID,
} from "../types/domain"; } from '../types/domain'
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
const EDGE = { const EDGE = {
plans_create_manual: "plans_create_manual", plans_create_manual: 'plans_create_manual',
ai_generate_plan: "ai_generate_plan", ai_generate_plan: 'ai_generate_plan',
plans_persist_from_ai: "plans_persist_from_ai", plans_persist_from_ai: 'plans_persist_from_ai',
plans_clone_from_existing: "plans_clone_from_existing", plans_clone_from_existing: 'plans_clone_from_existing',
plans_import_from_files: "plans_import_from_files", plans_import_from_files: 'plans_import_from_files',
plans_update_fields: "plans_update_fields", plans_update_fields: 'plans_update_fields',
plans_update_map: "plans_update_map", plans_update_map: 'plans_update_map',
plans_transition_state: "plans_transition_state", plans_transition_state: 'plans_transition_state',
plans_generate_document: "plans_generate_document", plans_generate_document: 'plans_generate_document',
plans_get_document: "plans_get_document", plans_get_document: 'plans_get_document',
} as const; } as const
export type PlanListFilters = { export type PlanListFilters = {
search?: string; search?: string
carreraId?: UUID; carreraId?: UUID
facultadId?: UUID; // filtra por carreras.facultad_id facultadId?: UUID // filtra por carreras.facultad_id
estadoId?: UUID; estadoId?: UUID
activo?: boolean; activo?: boolean
limit?: number; limit?: number
offset?: number; offset?: number
}; }
// Helper para limpiar texto (lo movemos fuera para reutilizar o lo dejas en un utils) // Helper para limpiar texto (lo movemos fuera para reutilizar o lo dejas en un utils)
const cleanText = (text: string) => { const cleanText = (text: string) => {
return text return text
.normalize("NFD") .normalize('NFD')
.replace(/[\u0300-\u036f]/g, "") .replace(/[\u0300-\u036f]/g, '')
.toLowerCase(); .toLowerCase()
}; }
export async function plans_list( export async function plans_list(
filters: PlanListFilters = {}, filters: PlanListFilters = {},
): Promise<Paged<PlanEstudio>> { ): Promise<Paged<PlanEstudio>> {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser()
// 1. Construimos la query base // 1. Construimos la query base
// NOTA IMPORTANTE: Para filtrar planes basados en facultad (que está en carreras), // NOTA IMPORTANTE: Para filtrar planes basados en facultad (que está en carreras),
// necesitamos hacer un INNER JOIN. En Supabase se usa "!inner". // necesitamos hacer un INNER JOIN. En Supabase se usa "!inner".
// Si filters.facultadId existe, forzamos el inner join, si no, lo dejamos normal. // Si filters.facultadId existe, forzamos el inner join, si no, lo dejamos normal.
const carreraModifier = filters.facultadId && filters.facultadId !== "todas" const carreraModifier =
? "!inner" filters.facultadId && filters.facultadId !== 'todas' ? '!inner' : ''
: "";
let q = supabase let q = supabase
.from("planes_estudio") .from('planes_estudio')
.select( .select(
` `
*, *,
@@ -76,56 +76,56 @@ export async function plans_list(
estructuras_plan (*), estructuras_plan (*),
estados_plan (*) estados_plan (*)
`, `,
{ count: "exact" }, { count: 'exact' },
) )
.order("actualizado_en", { ascending: false }); .order('actualizado_en', { ascending: false })
// 2. Aplicamos filtros dinámicos // 2. Aplicamos filtros dinámicos
// SOLUCIÓN SEARCH: Limpiamos el input y buscamos en la columna generada // SOLUCIÓN SEARCH: Limpiamos el input y buscamos en la columna generada
if (filters.search?.trim()) { if (filters.search?.trim()) {
const cleanTerm = cleanText(filters.search.trim()); const cleanTerm = cleanText(filters.search.trim())
// Usamos la columna nueva creada en el Paso 1 // Usamos la columna nueva creada en el Paso 1
q = q.ilike("nombre_search", `%${cleanTerm}%`); q = q.ilike('nombre_search', `%${cleanTerm}%`)
} }
if (filters.carreraId && filters.carreraId !== "todas") { if (filters.carreraId && filters.carreraId !== 'todas') {
q = q.eq("carrera_id", filters.carreraId); q = q.eq('carrera_id', filters.carreraId)
} }
if (filters.estadoId && filters.estadoId !== "todos") { if (filters.estadoId && filters.estadoId !== 'todos') {
q = q.eq("estado_actual_id", filters.estadoId); q = q.eq('estado_actual_id', filters.estadoId)
} }
if (typeof filters.activo === "boolean") { if (typeof filters.activo === 'boolean') {
q = q.eq("activo", filters.activo); q = q.eq('activo', filters.activo)
} }
// Filtro por facultad (gracias al !inner arriba, esto filtrará los planes) // Filtro por facultad (gracias al !inner arriba, esto filtrará los planes)
if (filters.facultadId && filters.facultadId !== "todas") { if (filters.facultadId && filters.facultadId !== 'todas') {
q = q.eq("carreras.facultad_id", filters.facultadId); q = q.eq('carreras.facultad_id', filters.facultadId)
} }
// 3. Paginación // 3. Paginación
const { from, to } = buildRange(filters.limit, filters.offset); const { from, to } = buildRange(filters.limit, filters.offset)
if (from !== undefined && to !== undefined) q = q.range(from, to); if (from !== undefined && to !== undefined) q = q.range(from, to)
const { data, error, count } = await q; const { data, error, count } = await q
throwIfError(error); throwIfError(error)
return { return {
// 1. Si data es null, usa []. // 1. Si data es null, usa [].
// 2. Luego dile a TS que el resultado es tu Array tipado. // 2. Luego dile a TS que el resultado es tu Array tipado.
data: (data ?? []) as unknown as Array<PlanEstudio>, data: (data ?? []) as unknown as Array<PlanEstudio>,
count: count ?? 0, count: count ?? 0,
}; }
} }
export async function plans_get(planId: UUID): Promise<PlanEstudio> { export async function plans_get(planId: UUID): Promise<PlanEstudio> {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from("planes_estudio") .from('planes_estudio')
.select( .select(
` `
*, *,
@@ -134,198 +134,197 @@ export async function plans_get(planId: UUID): Promise<PlanEstudio> {
estados_plan (*) estados_plan (*)
`, `,
) )
.eq("id", planId) .eq('id', planId)
.single(); .single()
throwIfError(error); throwIfError(error)
return requireData(data, "Plan no encontrado."); return requireData(data, 'Plan no encontrado.')
} }
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')
.eq("plan_estudio_id", planId) .eq('plan_estudio_id', planId)
.order("orden", { ascending: true }); .order('orden', { ascending: true })
throwIfError(error); throwIfError(error)
return data ?? []; return data ?? []
} }
export async function plan_asignaturas_list( export async function plan_asignaturas_list(
planId: UUID, planId: UUID,
): Promise<Array<Asignatura>> { ): Promise<Array<Asignatura>> {
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,facultad_propietaria_id,codigo,nombre,tipo,creditos,horas_semana,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,facultad_propietaria_id,codigo,nombre,tipo,creditos,horas_semana,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en',
) )
.eq("plan_estudio_id", planId) .eq('plan_estudio_id', planId)
.order("numero_ciclo", { ascending: true, nullsFirst: false }) .order('numero_ciclo', { ascending: true, nullsFirst: false })
.order("orden_celda", { ascending: true, nullsFirst: false }) .order('orden_celda', { ascending: true, nullsFirst: false })
.order("nombre", { ascending: true }); .order('nombre', { ascending: true })
throwIfError(error); throwIfError(error)
return data ?? []; return data ?? []
} }
export async function plans_history(planId: UUID): Promise<Array<CambioPlan>> { export async function plans_history(planId: UUID): Promise<Array<CambioPlan>> {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from("cambios_plan") .from('cambios_plan')
.select( .select(
"id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,interaccion_ia_id", 'id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,interaccion_ia_id',
) )
.eq("plan_estudio_id", planId) .eq('plan_estudio_id', planId)
.order("cambiado_en", { ascending: false }); .order('cambiado_en', { ascending: false })
throwIfError(error); throwIfError(error)
return data ?? []; return data ?? []
} }
/** Wizard: crear plan manual (Edge Function) */ /** Wizard: crear plan manual (Edge Function) */
export type PlansCreateManualInput = { export type PlansCreateManualInput = {
carreraId: UUID; carreraId: UUID
estructuraId: UUID; estructuraId: UUID
nombre: string; nombre: string
nivel: NivelPlanEstudio; nivel: NivelPlanEstudio
tipoCiclo: TipoCiclo; tipoCiclo: TipoCiclo
numCiclos: number; numCiclos: number
datos?: Partial<PlanDatosSep> & Record<string, any>; datos?: Partial<PlanDatosSep> & Record<string, any>
}; }
export async function plans_create_manual( export async function plans_create_manual(
input: PlansCreateManualInput, input: PlansCreateManualInput,
): Promise<PlanEstudio> { ): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_create_manual, input); return invokeEdge<PlanEstudio>(EDGE.plans_create_manual, input)
} }
/** Wizard: IA genera preview JSON (Edge Function) */ /** Wizard: IA genera preview JSON (Edge Function) */
export type AIGeneratePlanInput = { export type AIGeneratePlanInput = {
datosBasicos: { datosBasicos: {
nombrePlan: string; nombrePlan: string
carreraId: UUID; carreraId: UUID
facultadId?: UUID; facultadId?: UUID
nivel: string; nivel: string
tipoCiclo: TipoCiclo; tipoCiclo: TipoCiclo
numCiclos: number; numCiclos: number
}; }
iaConfig: { iaConfig: {
descripcionEnfoque: string; descripcionEnfoque: string
poblacionObjetivo?: string; poblacionObjetivo?: string
notasAdicionales?: string; notasAdicionales?: string
archivosReferencia?: Array<UUID>; archivosReferencia?: Array<UUID>
repositoriosIds?: Array<UUID>; repositoriosIds?: Array<UUID>
usarMCP?: boolean; archivosAdjuntos: Array<UploadedFile>
}; usarMCP?: boolean
}; }
}
export async function ai_generate_plan( export async function ai_generate_plan(
input: AIGeneratePlanInput, input: AIGeneratePlanInput,
): Promise<any> { ): Promise<any> {
return invokeEdge<any>(EDGE.ai_generate_plan, input); return invokeEdge<any>(EDGE.ai_generate_plan, input)
} }
export async function plans_persist_from_ai( export async function plans_persist_from_ai(payload: {
payload: { jsonPlan: any }, jsonPlan: any
): Promise<PlanEstudio> { }): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_persist_from_ai, payload); return invokeEdge<PlanEstudio>(EDGE.plans_persist_from_ai, payload)
} }
export async function plans_clone_from_existing(payload: { export async function plans_clone_from_existing(payload: {
planOrigenId: UUID; planOrigenId: UUID
overrides: overrides: Partial<
& Partial< Pick<PlanEstudio, 'nombre' | 'nivel' | 'tipo_ciclo' | 'numero_ciclos'>
Pick<PlanEstudio, "nombre" | "nivel" | "tipo_ciclo" | "numero_ciclos"> > & {
> carrera_id?: UUID
& { estructura_id?: UUID
carrera_id?: UUID; datos?: Partial<PlanDatosSep> & Record<string, any>
estructura_id?: UUID; }
datos?: Partial<PlanDatosSep> & Record<string, any>;
};
}): Promise<PlanEstudio> { }): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_clone_from_existing, payload); return invokeEdge<PlanEstudio>(EDGE.plans_clone_from_existing, payload)
} }
export async function plans_import_from_files(payload: { export async function plans_import_from_files(payload: {
datosBasicos: { datosBasicos: {
nombrePlan: string; nombrePlan: string
carreraId: UUID; carreraId: UUID
estructuraId: UUID; estructuraId: UUID
nivel: string; nivel: string
tipoCiclo: TipoCiclo; tipoCiclo: TipoCiclo
numCiclos: number; numCiclos: number
}; }
archivoWordPlanId: UUID; archivoWordPlanId: UUID
archivoMapaExcelId?: UUID | null; archivoMapaExcelId?: UUID | null
archivoMateriasExcelId?: UUID | null; archivoMateriasExcelId?: UUID | null
}): Promise<PlanEstudio> { }): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_import_from_files, payload); return invokeEdge<PlanEstudio>(EDGE.plans_import_from_files, payload)
} }
/** Update de tarjetas/fields del plan (Edge Function: merge server-side) */ /** Update de tarjetas/fields del plan (Edge Function: merge server-side) */
export type PlansUpdateFieldsPatch = { export type PlansUpdateFieldsPatch = {
nombre?: string; nombre?: string
nivel?: NivelPlanEstudio; nivel?: NivelPlanEstudio
tipo_ciclo?: TipoCiclo; tipo_ciclo?: TipoCiclo
numero_ciclos?: number; numero_ciclos?: number
datos?: Partial<PlanDatosSep> & Record<string, any>; datos?: Partial<PlanDatosSep> & Record<string, any>
}; }
export async function plans_update_fields( export async function plans_update_fields(
planId: UUID, planId: UUID,
patch: PlansUpdateFieldsPatch, patch: PlansUpdateFieldsPatch,
): Promise<PlanEstudio> { ): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_update_fields, { planId, patch }); return invokeEdge<PlanEstudio>(EDGE.plans_update_fields, { planId, patch })
} }
/** Operaciones del mapa curricular (mover/reordenar) */ /** Operaciones del mapa curricular (mover/reordenar) */
export type PlanMapOperation = export type PlanMapOperation =
| { | {
op: "MOVE_ASIGNATURA"; op: 'MOVE_ASIGNATURA'
asignaturaId: UUID; asignaturaId: UUID
numero_ciclo: number | null; numero_ciclo: number | null
linea_plan_id: UUID | null; linea_plan_id: UUID | null
orden_celda?: number | null; orden_celda?: number | null
} }
| { | {
op: "REORDER_CELDA"; op: 'REORDER_CELDA'
linea_plan_id: UUID; linea_plan_id: UUID
numero_ciclo: number; numero_ciclo: number
asignaturaIdsOrdenados: Array<UUID>; asignaturaIdsOrdenados: Array<UUID>
}; }
export async function plans_update_map( export async function plans_update_map(
planId: UUID, planId: UUID,
ops: Array<PlanMapOperation>, ops: Array<PlanMapOperation>,
): Promise<{ ok: true }> { ): Promise<{ ok: true }> {
return invokeEdge<{ ok: true }>(EDGE.plans_update_map, { planId, ops }); return invokeEdge<{ ok: true }>(EDGE.plans_update_map, { planId, ops })
} }
export async function plans_transition_state(payload: { export async function plans_transition_state(payload: {
planId: UUID; planId: UUID
haciaEstadoId: UUID; haciaEstadoId: UUID
comentario?: string; comentario?: string
}): Promise<{ ok: true }> { }): Promise<{ ok: true }> {
return invokeEdge<{ ok: true }>(EDGE.plans_transition_state, payload); return invokeEdge<{ ok: true }>(EDGE.plans_transition_state, payload)
} }
/** Documento (Edge Function: genera y devuelve URL firmada o metadata) */ /** Documento (Edge Function: genera y devuelve URL firmada o metadata) */
export type DocumentoResult = { export type DocumentoResult = {
archivoId: UUID; archivoId: UUID
signedUrl: string; signedUrl: string
mimeType?: string; mimeType?: string
nombre?: string; nombre?: string
}; }
export async function plans_generate_document( export async function plans_generate_document(
planId: UUID, planId: UUID,
): Promise<DocumentoResult> { ): Promise<DocumentoResult> {
return invokeEdge<DocumentoResult>(EDGE.plans_generate_document, { planId }); return invokeEdge<DocumentoResult>(EDGE.plans_generate_document, { planId })
} }
export async function plans_get_document( export async function plans_get_document(
@@ -333,21 +332,26 @@ export async function plans_get_document(
): Promise<DocumentoResult | null> { ): Promise<DocumentoResult | null> {
return invokeEdge<DocumentoResult | null>(EDGE.plans_get_document, { return invokeEdge<DocumentoResult | null>(EDGE.plans_get_document, {
planId, planId,
}); })
} }
export async function getCatalogos() { export async function getCatalogos() {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser()
const [facRes, carRes, estRes] = await Promise.all([ const [facultadesRes, carrerasRes, estadosRes, estructurasPlanRes] =
supabase.from("facultades").select("*").order("nombre"), await Promise.all([
supabase.from("carreras").select("*").order("nombre"), supabase.from('facultades').select('*').order('nombre'),
supabase.from("estados_plan").select("*").order("orden"), supabase.from('carreras').select('*').order('nombre'),
]); supabase.from('estados_plan').select('*').order('orden'),
supabase.from('estructuras_plan').select('*').order('creado_en', {
ascending: true,
}),
])
return { return {
facultades: facRes.data ?? [], facultades: facultadesRes.data ?? [],
carreras: carRes.data ?? [], carreras: carrerasRes.data ?? [],
estados: estRes.data ?? [], estados: estadosRes.data ?? [],
}; estructurasPlan: estructurasPlanRes.data ?? [],
}
} }

View File

@@ -3,8 +3,15 @@ import {
useMutation, useMutation,
useQuery, useQuery,
useQueryClient, useQueryClient,
} from "@tanstack/react-query"; } from '@tanstack/react-query'
import { qk } from '../query/keys'
import type { PlanEstudio, UUID } from '../types/domain'
import type {
PlanListFilters,
PlanMapOperation,
PlansCreateManualInput,
PlansUpdateFieldsPatch,
} from '../api/plans.api'
import { import {
ai_generate_plan, ai_generate_plan,
getCatalogos, getCatalogos,
@@ -22,16 +29,7 @@ import {
plans_transition_state, plans_transition_state,
plans_update_fields, plans_update_fields,
plans_update_map, plans_update_map,
} from "../api/plans.api"; } from '../api/plans.api'
import { qk } from "../query/keys";
import type {
PlanListFilters,
PlanMapOperation,
PlansCreateManualInput,
PlansUpdateFieldsPatch,
} from "../api/plans.api";
import type { UUID } from "../types/domain";
export function usePlanes(filters: PlanListFilters) { export function usePlanes(filters: PlanListFilters) {
// 🧠 Tip: memoiza "filters" (useMemo) para que queryKey sea estable. // 🧠 Tip: memoiza "filters" (useMemo) para que queryKey sea estable.
@@ -44,197 +42,188 @@ export function usePlanes(filters: PlanListFilters) {
// UX: Mantiene los datos viejos mientras carga la paginación nueva // UX: Mantiene los datos viejos mientras carga la paginación nueva
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
})
// Opcional: Tiempo que la data se considera fresca
staleTime: 1000 * 60 * 5, // 5 minutos
});
} }
export function usePlan(planId: UUID | null | undefined) { export function usePlan(planId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: planId ? qk.plan(planId) : ["planes", "detail", null], queryKey: planId ? qk.plan(planId) : ['planes', 'detail', null],
queryFn: () => plans_get(planId as UUID), queryFn: () => plans_get(planId as UUID),
enabled: Boolean(planId), enabled: Boolean(planId),
}); })
} }
export function usePlanLineas(planId: UUID | null | undefined) { export function usePlanLineas(planId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: planId ? qk.planLineas(planId) : ["planes", "lineas", null], queryKey: planId ? qk.planLineas(planId) : ['planes', 'lineas', null],
queryFn: () => plan_lineas_list(planId as UUID), queryFn: () => plan_lineas_list(planId as UUID),
enabled: Boolean(planId), enabled: Boolean(planId),
}); })
} }
export function usePlanAsignaturas(planId: UUID | null | undefined) { export function usePlanAsignaturas(planId: UUID | null | undefined) {
return useQuery({ return 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),
}); })
} }
export function usePlanHistorial(planId: UUID | null | undefined) { export function usePlanHistorial(planId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: planId ? qk.planHistorial(planId) : ["planes", "historial", null], queryKey: planId ? qk.planHistorial(planId) : ['planes', 'historial', null],
queryFn: () => plans_history(planId as UUID), queryFn: () => plans_history(planId as UUID),
enabled: Boolean(planId), enabled: Boolean(planId),
}); })
} }
export function usePlanDocumento(planId: UUID | null | undefined) { export function usePlanDocumento(planId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: planId ? qk.planDocumento(planId) : ["planes", "documento", null], queryKey: planId ? qk.planDocumento(planId) : ['planes', 'documento', null],
queryFn: () => plans_get_document(planId as UUID), queryFn: () => plans_get_document(planId as UUID),
enabled: Boolean(planId), enabled: Boolean(planId),
staleTime: 30_000, staleTime: 30_000,
}); })
}
export function useCatalogosPlanes() {
return useQuery({
queryKey: ["catalogos_planes"],
queryFn: getCatalogos,
staleTime: 1000 * 60 * 60, // 1 hora de caché (estos datos casi no cambian)
});
} }
/* ------------------ Mutations ------------------ */ /* ------------------ Mutations ------------------ */
export function useCreatePlanManual() { export function useCreatePlanManual() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (input: PlansCreateManualInput) => plans_create_manual(input), mutationFn: (input: PlansCreateManualInput) => plans_create_manual(input),
onSuccess: (plan) => { onSuccess: (plan) => {
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
qc.setQueryData(qk.plan(plan.id), plan); qc.setQueryData(qk.plan(plan.id), plan)
}, },
}); })
} }
export function useGeneratePlanAI() { export function useGeneratePlanAI() {
return useMutation({ return useMutation({
mutationFn: ai_generate_plan, mutationFn: ai_generate_plan,
}); })
} }
export function usePersistPlanFromAI() { export function usePersistPlanFromAI() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (payload: { jsonPlan: any }) => plans_persist_from_ai(payload), mutationFn: (payload: { jsonPlan: any }) => plans_persist_from_ai(payload),
onSuccess: (plan) => { onSuccess: (plan) => {
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
qc.setQueryData(qk.plan(plan.id), plan); qc.setQueryData(qk.plan(plan.id), plan)
}, },
}); })
} }
export function useClonePlan() { export function useClonePlan() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: plans_clone_from_existing, mutationFn: plans_clone_from_existing,
onSuccess: (plan) => { onSuccess: (plan) => {
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
qc.setQueryData(qk.plan(plan.id), plan); qc.setQueryData(qk.plan(plan.id), plan)
}, },
}); })
} }
export function useImportPlanFromFiles() { export function useImportPlanFromFiles() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: plans_import_from_files, mutationFn: plans_import_from_files,
onSuccess: (plan) => { onSuccess: (plan) => {
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
qc.setQueryData(qk.plan(plan.id), plan); qc.setQueryData(qk.plan(plan.id), plan)
}, },
}); })
} }
export function useUpdatePlanFields() { export function useUpdatePlanFields() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (vars: { planId: UUID; patch: PlansUpdateFieldsPatch }) => mutationFn: (vars: { planId: UUID; patch: PlansUpdateFieldsPatch }) =>
plans_update_fields(vars.planId, vars.patch), plans_update_fields(vars.planId, vars.patch),
onSuccess: (updated) => { onSuccess: (updated) => {
qc.setQueryData(qk.plan(updated.id), updated); qc.setQueryData(qk.plan(updated.id), updated)
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
qc.invalidateQueries({ queryKey: qk.planHistorial(updated.id) }); qc.invalidateQueries({ queryKey: qk.planHistorial(updated.id) })
}, },
}); })
} }
export function useUpdatePlanMapa() { export function useUpdatePlanMapa() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (vars: { planId: UUID; ops: Array<PlanMapOperation> }) => mutationFn: (vars: { planId: UUID; ops: PlanMapOperation[] }) =>
plans_update_map(vars.planId, vars.ops), plans_update_map(vars.planId, vars.ops),
// ✅ Optimista (rápida) para el caso MOVE_ASIGNATURA // ✅ Optimista (rápida) para el caso MOVE_ASIGNATURA
onMutate: async (vars) => { onMutate: async (vars) => {
await qc.cancelQueries({ queryKey: qk.planAsignaturas(vars.planId) }); await qc.cancelQueries({ queryKey: qk.planAsignaturas(vars.planId) })
const prev = qc.getQueryData<any>(qk.planAsignaturas(vars.planId)); const prev = qc.getQueryData<any>(qk.planAsignaturas(vars.planId))
// solo optimizamos MOVEs simples // solo optimizamos MOVEs simples
const moves = vars.ops.filter((x) => x.op === "MOVE_ASIGNATURA"); const moves = vars.ops.filter((x) => x.op === 'MOVE_ASIGNATURA') as Array<
Extract<PlanMapOperation, { op: 'MOVE_ASIGNATURA' }>
>
if (prev && Array.isArray(prev) && moves.length) { if (prev && Array.isArray(prev) && moves.length) {
const next = prev.map((a: any) => { const next = prev.map((a: any) => {
const m = moves.find((x) => x.asignaturaId === a.id); const m = moves.find((x) => x.asignaturaId === a.id)
if (!m) return a; if (!m) return a
return { return {
...a, ...a,
numero_ciclo: m.numero_ciclo, numero_ciclo: m.numero_ciclo,
linea_plan_id: m.linea_plan_id, linea_plan_id: m.linea_plan_id,
orden_celda: m.orden_celda ?? a.orden_celda, orden_celda: m.orden_celda ?? a.orden_celda,
}; }
}); })
qc.setQueryData(qk.planAsignaturas(vars.planId), next); qc.setQueryData(qk.planAsignaturas(vars.planId), next)
} }
return { prev }; return { prev }
}, },
onError: (_err, vars, ctx) => { onError: (_err, vars, ctx) => {
if (ctx?.prev) qc.setQueryData(qk.planAsignaturas(vars.planId), ctx.prev); if (ctx?.prev) qc.setQueryData(qk.planAsignaturas(vars.planId), ctx.prev)
}, },
onSuccess: (_ok, vars) => { onSuccess: (_ok, vars) => {
qc.invalidateQueries({ queryKey: qk.planAsignaturas(vars.planId) }); qc.invalidateQueries({ queryKey: qk.planAsignaturas(vars.planId) })
qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) }); qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) })
}, },
}); })
} }
export function useTransitionPlanEstado() { export function useTransitionPlanEstado() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: plans_transition_state, mutationFn: plans_transition_state,
onSuccess: (_ok, vars) => { onSuccess: (_ok, vars) => {
qc.invalidateQueries({ queryKey: qk.plan(vars.planId) }); qc.invalidateQueries({ queryKey: qk.plan(vars.planId) })
qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) }); qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) })
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
}, },
}); })
} }
export function useGeneratePlanDocumento() { export function useGeneratePlanDocumento() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (planId: UUID) => plans_generate_document(planId), mutationFn: (planId: UUID) => plans_generate_document(planId),
onSuccess: (_doc, planId) => { onSuccess: (_doc, planId) => {
qc.invalidateQueries({ queryKey: qk.planDocumento(planId) }); qc.invalidateQueries({ queryKey: qk.planDocumento(planId) })
qc.invalidateQueries({ queryKey: qk.planHistorial(planId) }); qc.invalidateQueries({ queryKey: qk.planHistorial(planId) })
}, },
}); })
} }

View File

@@ -1,11 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { qk } from "../query/keys"; import { qk } from '../query/keys'
import type { UUID } from "../types/domain"; import type { UUID } from '../types/domain'
import type { import type {
BibliografiaUpsertInput, BibliografiaUpsertInput,
SubjectsCreateManualInput, SubjectsCreateManualInput,
SubjectsUpdateFieldsPatch, SubjectsUpdateFieldsPatch,
} from "../api/subjects.api"; } from '../api/subjects.api'
import { import {
ai_generate_subject, ai_generate_subject,
subjects_bibliografia_list, subjects_bibliografia_list,
@@ -20,147 +20,177 @@ import {
subjects_update_bibliografia, subjects_update_bibliografia,
subjects_update_contenido, subjects_update_contenido,
subjects_update_fields, subjects_update_fields,
} from "../api/subjects.api"; } from '../api/subjects.api'
export function useSubject(subjectId: UUID | null | undefined) { export function useSubject(subjectId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: subjectId ? qk.asignatura(subjectId) : ["asignaturas", "detail", null], queryKey: subjectId
? qk.asignatura(subjectId)
: ['asignaturas', 'detail', null],
queryFn: () => subjects_get(subjectId as UUID), queryFn: () => subjects_get(subjectId as UUID),
enabled: Boolean(subjectId), enabled: Boolean(subjectId),
}); })
} }
export function useSubjectBibliografia(subjectId: UUID | null | undefined) { export function useSubjectBibliografia(subjectId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: subjectId ? qk.asignaturaBibliografia(subjectId) : ["asignaturas", "bibliografia", null], queryKey: subjectId
? qk.asignaturaBibliografia(subjectId)
: ['asignaturas', 'bibliografia', null],
queryFn: () => subjects_bibliografia_list(subjectId as UUID), queryFn: () => subjects_bibliografia_list(subjectId as UUID),
enabled: Boolean(subjectId), enabled: Boolean(subjectId),
}); })
} }
export function useSubjectHistorial(subjectId: UUID | null | undefined) { export function useSubjectHistorial(subjectId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: subjectId ? qk.asignaturaHistorial(subjectId) : ["asignaturas", "historial", null], queryKey: subjectId
? qk.asignaturaHistorial(subjectId)
: ['asignaturas', 'historial', null],
queryFn: () => subjects_history(subjectId as UUID), queryFn: () => subjects_history(subjectId as UUID),
enabled: Boolean(subjectId), enabled: Boolean(subjectId),
}); })
} }
export function useSubjectDocumento(subjectId: UUID | null | undefined) { export function useSubjectDocumento(subjectId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: subjectId ? qk.asignaturaDocumento(subjectId) : ["asignaturas", "documento", null], queryKey: subjectId
? qk.asignaturaDocumento(subjectId)
: ['asignaturas', 'documento', null],
queryFn: () => subjects_get_document(subjectId as UUID), queryFn: () => subjects_get_document(subjectId as UUID),
enabled: Boolean(subjectId), enabled: Boolean(subjectId),
staleTime: 30_000, staleTime: 30_000,
}); })
} }
/* ------------------ Mutations ------------------ */ /* ------------------ Mutations ------------------ */
export function useCreateSubjectManual() { export function useCreateSubjectManual() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (payload: SubjectsCreateManualInput) => subjects_create_manual(payload), mutationFn: (payload: SubjectsCreateManualInput) =>
subjects_create_manual(payload),
onSuccess: (subject) => { onSuccess: (subject) => {
qc.setQueryData(qk.asignatura(subject.id), subject); qc.setQueryData(qk.asignatura(subject.id), subject)
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) }); qc.invalidateQueries({
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) }); queryKey: qk.planAsignaturas(subject.plan_estudio_id),
})
qc.invalidateQueries({
queryKey: qk.planHistorial(subject.plan_estudio_id),
})
}, },
}); })
} }
export function useGenerateSubjectAI() { export function useGenerateSubjectAI() {
return useMutation({ mutationFn: ai_generate_subject }); return useMutation({ mutationFn: ai_generate_subject })
} }
export function usePersistSubjectFromAI() { export function usePersistSubjectFromAI() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (payload: { planId: UUID; jsonMateria: any }) => subjects_persist_from_ai(payload), mutationFn: (payload: { planId: UUID; jsonMateria: any }) =>
subjects_persist_from_ai(payload),
onSuccess: (subject) => { onSuccess: (subject) => {
qc.setQueryData(qk.asignatura(subject.id), subject); qc.setQueryData(qk.asignatura(subject.id), subject)
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) }); qc.invalidateQueries({
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) }); queryKey: qk.planAsignaturas(subject.plan_estudio_id),
})
qc.invalidateQueries({
queryKey: qk.planHistorial(subject.plan_estudio_id),
})
}, },
}); })
} }
export function useCloneSubject() { export function useCloneSubject() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: subjects_clone_from_existing, mutationFn: subjects_clone_from_existing,
onSuccess: (subject) => { onSuccess: (subject) => {
qc.setQueryData(qk.asignatura(subject.id), subject); qc.setQueryData(qk.asignatura(subject.id), subject)
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) }); qc.invalidateQueries({
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) }); queryKey: qk.planAsignaturas(subject.plan_estudio_id),
})
qc.invalidateQueries({
queryKey: qk.planHistorial(subject.plan_estudio_id),
})
}, },
}); })
} }
export function useImportSubjectFromFile() { export function useImportSubjectFromFile() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: subjects_import_from_file, mutationFn: subjects_import_from_file,
onSuccess: (subject) => { onSuccess: (subject) => {
qc.setQueryData(qk.asignatura(subject.id), subject); qc.setQueryData(qk.asignatura(subject.id), subject)
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) }); qc.invalidateQueries({
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) }); queryKey: qk.planAsignaturas(subject.plan_estudio_id),
})
qc.invalidateQueries({
queryKey: qk.planHistorial(subject.plan_estudio_id),
})
}, },
}); })
} }
export function useUpdateSubjectFields() { export function useUpdateSubjectFields() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
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), updated)
qc.invalidateQueries({ queryKey: qk.planAsignaturas(updated.plan_estudio_id) }); qc.invalidateQueries({
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) }); queryKey: qk.planAsignaturas(updated.plan_estudio_id),
})
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
}, },
}); })
} }
export function useUpdateSubjectContenido() { export function useUpdateSubjectContenido() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (vars: { subjectId: UUID; unidades: any[] }) => mutationFn: (vars: { subjectId: UUID; unidades: any[] }) =>
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), updated)
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) }); qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
}, },
}); })
} }
export function useUpdateSubjectBibliografia() { export function useUpdateSubjectBibliografia() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (vars: { subjectId: UUID; entries: BibliografiaUpsertInput }) => mutationFn: (vars: { subjectId: UUID; entries: BibliografiaUpsertInput }) =>
subjects_update_bibliografia(vars.subjectId, vars.entries), subjects_update_bibliografia(vars.subjectId, vars.entries),
onSuccess: (_ok, vars) => { onSuccess: (_ok, vars) => {
qc.invalidateQueries({ queryKey: qk.asignaturaBibliografia(vars.subjectId) }); qc.invalidateQueries({
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(vars.subjectId) }); queryKey: qk.asignaturaBibliografia(vars.subjectId),
})
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(vars.subjectId) })
}, },
}); })
} }
export function useGenerateSubjectDocumento() { export function useGenerateSubjectDocumento() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (subjectId: UUID) => subjects_generate_document(subjectId), mutationFn: (subjectId: UUID) => subjects_generate_document(subjectId),
onSuccess: (_doc, subjectId) => { onSuccess: (_doc, subjectId) => {
qc.invalidateQueries({ queryKey: qk.asignaturaDocumento(subjectId) }); qc.invalidateQueries({ queryKey: qk.asignaturaDocumento(subjectId) })
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(subjectId) }); qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(subjectId) })
}, },
}); })
} }

View File

@@ -3,7 +3,7 @@ import { createClient } from "@supabase/supabase-js";
import { getEnv } from "./env"; import { getEnv } from "./env";
import type { SupabaseClient } from "@supabase/supabase-js"; import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "src/types/supabase.js"; import type { Database } from "src/types/supabase";
let _client: SupabaseClient<Database> | null = null; let _client: SupabaseClient<Database> | null = null;

View File

@@ -1,4 +1,4 @@
import type { Database, Enums, Tables } from "../../types/supabase"; import type { Enums, Tables } from "../../types/supabase";
export type UUID = string; export type UUID = string;
@@ -52,14 +52,14 @@ export type PlanDatosSep = {
}; };
export type PlanEstudioWithRel = export type PlanEstudioWithRel =
& Database["public"]["Tables"]["planes_estudio"]["Row"] & Tables<"planes_estudio">
& { & {
carreras: carreras:
| Database["public"]["Tables"]["carreras"]["Row"] & { | Tables<"carreras"> & {
facultades: Database["public"]["Tables"]["facultades"]["Row"] | null; facultades: Tables<"facultades"> | null;
} }
| null; | null;
estados_plan: Database["public"]["Tables"]["estados_plan"]["Row"] | null; estados_plan: Tables<"estados_plan"> | null;
}; };
export type Paged<T> = { data: Array<T>; count: number | null }; export type Paged<T> = { data: Array<T>; count: number | null };

View File

@@ -3,6 +3,8 @@ import * as Icons from 'lucide-react'
import { useNuevoPlanWizard } from './hooks/useNuevoPlanWizard' import { useNuevoPlanWizard } from './hooks/useNuevoPlanWizard'
import type { NewPlanWizardState } from './types'
import { PasoBasicosForm } from '@/components/planes/wizard/PasoBasicosForm/PasoBasicosForm' import { PasoBasicosForm } from '@/components/planes/wizard/PasoBasicosForm/PasoBasicosForm'
import { PasoDetallesPanel } from '@/components/planes/wizard/PasoDetallesPanel/PasoDetallesPanel' import { PasoDetallesPanel } from '@/components/planes/wizard/PasoDetallesPanel/PasoDetallesPanel'
import { PasoModoCardGroup } from '@/components/planes/wizard/PasoModoCardGroup' import { PasoModoCardGroup } from '@/components/planes/wizard/PasoModoCardGroup'
@@ -49,7 +51,6 @@ export default function NuevoPlanModalContainer() {
const { const {
wizard, wizard,
setWizard, setWizard,
carrerasFiltradas,
canContinueDesdeModo, canContinueDesdeModo,
canContinueDesdeBasicos, canContinueDesdeBasicos,
canContinueDesdeDetalles, canContinueDesdeDetalles,
@@ -61,12 +62,20 @@ export default function NuevoPlanModalContainer() {
} }
const crearPlan = async () => { const crearPlan = async () => {
setWizard((w) => ({ ...w, isLoading: true, errorMessage: null })) setWizard((w: NewPlanWizardState) => ({
...w,
isLoading: true,
errorMessage: null,
}))
await new Promise((r) => setTimeout(r, 900)) await new Promise((r) => setTimeout(r, 900))
const nuevoId = (() => { const nuevoId = (() => {
if (wizard.modoCreacion === 'MANUAL') return 'plan_new_manual_001' if (wizard.tipoOrigen === 'MANUAL') return 'plan_new_manual_001'
if (wizard.modoCreacion === 'IA') return 'plan_new_ai_001' if (wizard.tipoOrigen === 'IA') return 'plan_new_ai_001'
if (wizard.subModoClonado === 'INTERNO') return 'plan_new_clone_001' if (
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL'
)
return 'plan_new_clone_001'
return 'plan_new_import_001' return 'plan_new_import_001'
})() })()
navigate({ to: `/planes/${nuevoId}` }) navigate({ to: `/planes/${nuevoId}` })
@@ -115,7 +124,10 @@ export default function NuevoPlanModalContainer() {
{({ methods }) => { {({ methods }) => {
const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1 const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1
const totalSteps = Wizard.steps.length const totalSteps = Wizard.steps.length
const nextStep = Wizard.steps[currentIndex] const nextStep = Wizard.steps[currentIndex] ?? {
title: '',
description: '',
}
return ( return (
<> <>
@@ -124,7 +136,7 @@ export default function NuevoPlanModalContainer() {
totalSteps={totalSteps} totalSteps={totalSteps}
currentTitle={methods.current.title} currentTitle={methods.current.title}
currentDescription={methods.current.description} currentDescription={methods.current.description}
nextTitle={nextStep?.title} nextTitle={nextStep.title}
onClose={handleClose} onClose={handleClose}
Wizard={Wizard} Wizard={Wizard}
/> />
@@ -144,7 +156,6 @@ export default function NuevoPlanModalContainer() {
<PasoBasicosForm <PasoBasicosForm
wizard={wizard} wizard={wizard}
onChange={setWizard} onChange={setWizard}
carrerasFiltradas={carrerasFiltradas}
/> />
</Wizard.Stepper.Panel> </Wizard.Stepper.Panel>
)} )}

View File

@@ -1,4 +1,4 @@
import type { TipoCiclo } from "./types"; import type { NivelPlanEstudio, TipoCiclo } from "@/data/types/domain";
export const FACULTADES = [ export const FACULTADES = [
{ id: "ing", nombre: "Facultad de Ingeniería" }, { id: "ing", nombre: "Facultad de Ingeniería" },
@@ -16,16 +16,20 @@ export const CARRERAS = [
{ id: "act", nombre: "Actuaría", facultadId: "neg" }, { id: "act", nombre: "Actuaría", facultadId: "neg" },
]; ];
export const NIVELES = [ export const NIVELES: Array<NivelPlanEstudio> = [
"Licenciatura", "Licenciatura",
"Especialidad",
"Maestría", "Maestría",
"Doctorado", "Doctorado",
"Especialidad",
"Diplomado",
"Otro",
]; ];
export const TIPOS_CICLO: Array<{ value: TipoCiclo; label: string }> = [
{ value: "SEMESTRE", label: "Semestre" }, export const TIPOS_CICLO: Array<TipoCiclo> = [
{ value: "CUATRIMESTRE", label: "Cuatrimestre" }, "Semestre",
{ value: "TRIMESTRE", label: "Trimestre" }, "Cuatrimestre",
"Trimestre",
"Otro",
]; ];
export const PLANES_EXISTENTES = [ export const PLANES_EXISTENTES = [

View File

@@ -1,13 +1,12 @@
import { useMemo, useState } from "react"; import { useState } from "react";
import { CARRERAS } from "../catalogs"; import type { NewPlanWizardState, PlanPreview } from "../types";
import type { NivelPlanEstudio, TipoCiclo } from "@/data/types/domain";
import type { NewPlanWizardState, PlanPreview, TipoCiclo } from "../types";
export function useNuevoPlanWizard() { export function useNuevoPlanWizard() {
const [wizard, setWizard] = useState<NewPlanWizardState>({ const [wizard, setWizard] = useState<NewPlanWizardState>({
step: 1, step: 1,
modoCreacion: null, tipoOrigen: null,
datosBasicos: { datosBasicos: {
nombrePlan: "", nombrePlan: "",
carreraId: "", carreraId: "",
@@ -15,10 +14,7 @@ export function useNuevoPlanWizard() {
nivel: "", nivel: "",
tipoCiclo: "", tipoCiclo: "",
numCiclos: undefined, numCiclos: undefined,
plantillaPlanId: "", estructuraPlanId: null,
plantillaPlanVersion: "",
plantillaMapaId: "",
plantillaMapaVersion: "",
}, },
// datosBasicos: { // datosBasicos: {
// nombrePlan: "Medicina", // nombrePlan: "Medicina",
@@ -40,7 +36,6 @@ export function useNuevoPlanWizard() {
}, },
iaConfig: { iaConfig: {
descripcionEnfoque: "", descripcionEnfoque: "",
poblacionObjetivo: "",
notasAdicionales: "", notasAdicionales: "",
archivosReferencia: [], archivosReferencia: [],
repositoriosReferencia: [], repositoriosReferencia: [],
@@ -51,14 +46,10 @@ export function useNuevoPlanWizard() {
errorMessage: null, errorMessage: null,
}); });
const carrerasFiltradas = useMemo(() => { const canContinueDesdeModo = wizard.tipoOrigen === "MANUAL" ||
const fac = wizard.datosBasicos.facultadId; wizard.tipoOrigen === "IA" ||
return fac ? CARRERAS.filter((c) => c.facultadId === fac) : CARRERAS; (wizard.tipoOrigen === "CLONADO_INTERNO" ||
}, [wizard.datosBasicos.facultadId]); wizard.tipoOrigen === "CLONADO_TRADICIONAL");
const canContinueDesdeModo = wizard.modoCreacion === "MANUAL" ||
wizard.modoCreacion === "IA" ||
(wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado);
const canContinueDesdeBasicos = !!wizard.datosBasicos.nombrePlan && const canContinueDesdeBasicos = !!wizard.datosBasicos.nombrePlan &&
!!wizard.datosBasicos.carreraId && !!wizard.datosBasicos.carreraId &&
@@ -67,23 +58,19 @@ export function useNuevoPlanWizard() {
(wizard.datosBasicos.numCiclos !== undefined && (wizard.datosBasicos.numCiclos !== undefined &&
wizard.datosBasicos.numCiclos > 0) && wizard.datosBasicos.numCiclos > 0) &&
// Requerir ambas plantillas (plan y mapa) con versión // Requerir ambas plantillas (plan y mapa) con versión
!!wizard.datosBasicos.plantillaPlanId && !!wizard.datosBasicos.estructuraPlanId;
!!wizard.datosBasicos.plantillaPlanVersion &&
!!wizard.datosBasicos.plantillaMapaId &&
!!wizard.datosBasicos.plantillaMapaVersion;
const canContinueDesdeDetalles = (() => { const canContinueDesdeDetalles = (() => {
if (wizard.modoCreacion === "MANUAL") return true; if (wizard.tipoOrigen === "MANUAL") return true;
if (wizard.modoCreacion === "IA") { if (wizard.tipoOrigen === "IA") {
// Requerimos descripción del enfoque y notas adicionales // Requerimos descripción del enfoque y notas adicionales
return !!wizard.iaConfig?.descripcionEnfoque && return !!wizard.iaConfig?.descripcionEnfoque &&
!!wizard.iaConfig?.notasAdicionales; !!wizard.iaConfig.notasAdicionales;
} }
if (wizard.modoCreacion === "CLONADO") { if (wizard.tipoOrigen === "CLONADO_INTERNO") {
if (wizard.subModoClonado === "INTERNO") {
return !!wizard.clonInterno?.planOrigenId; return !!wizard.clonInterno?.planOrigenId;
} }
if (wizard.subModoClonado === "TRADICIONAL") { if (wizard.tipoOrigen === "CLONADO_TRADICIONAL") {
const t = wizard.clonTradicional; const t = wizard.clonTradicional;
if (!t) return false; if (!t) return false;
const tieneWord = !!t.archivoWordPlanId; const tieneWord = !!t.archivoWordPlanId;
@@ -91,7 +78,6 @@ export function useNuevoPlanWizard() {
!!t.archivoAsignaturasExcelId; !!t.archivoAsignaturasExcelId;
return tieneWord && tieneAlMenosUnExcel; return tieneWord && tieneAlMenosUnExcel;
} }
}
return false; return false;
})(); })();
@@ -101,7 +87,7 @@ export function useNuevoPlanWizard() {
// Ensure preview has the stricter types required by `PlanPreview`. // Ensure preview has the stricter types required by `PlanPreview`.
let tipoCicloSafe: TipoCiclo; let tipoCicloSafe: TipoCiclo;
if (wizard.datosBasicos.tipoCiclo === "") { if (wizard.datosBasicos.tipoCiclo === "") {
tipoCicloSafe = "SEMESTRE"; tipoCicloSafe = "Semestre";
} else { } else {
tipoCicloSafe = wizard.datosBasicos.tipoCiclo; tipoCicloSafe = wizard.datosBasicos.tipoCiclo;
} }
@@ -112,7 +98,7 @@ export function useNuevoPlanWizard() {
const preview: PlanPreview = { const preview: PlanPreview = {
nombrePlan: wizard.datosBasicos.nombrePlan || "Plan sin nombre", nombrePlan: wizard.datosBasicos.nombrePlan || "Plan sin nombre",
nivel: wizard.datosBasicos.nivel || "Licenciatura", nivel: wizard.datosBasicos.nivel as NivelPlanEstudio,
tipoCiclo: tipoCicloSafe, tipoCiclo: tipoCicloSafe,
numCiclos: numCiclosSafe, numCiclos: numCiclosSafe,
numAsignaturasAprox: numCiclosSafe * 6, numAsignaturasAprox: numCiclosSafe * 6,
@@ -121,7 +107,7 @@ export function useNuevoPlanWizard() {
{ id: "perfil", titulo: "Perfil de egreso", resumen: "Borrador…" }, { id: "perfil", titulo: "Perfil de egreso", resumen: "Borrador…" },
], ],
}; };
setWizard((w) => ({ setWizard((w: NewPlanWizardState) => ({
...w, ...w,
isLoading: false, isLoading: false,
resumen: { previewPlan: preview }, resumen: { previewPlan: preview },
@@ -131,7 +117,6 @@ export function useNuevoPlanWizard() {
return { return {
wizard, wizard,
setWizard, setWizard,
carrerasFiltradas,
canContinueDesdeModo, canContinueDesdeModo,
canContinueDesdeBasicos, canContinueDesdeBasicos,
canContinueDesdeDetalles, canContinueDesdeDetalles,

View File

@@ -1,10 +1,13 @@
export type TipoCiclo = "SEMESTRE" | "CUATRIMESTRE" | "TRIMESTRE"; import type { UploadedFile } from "@/components/planes/wizard/PasoDetallesPanel/FileDropZone";
export type ModoCreacion = "MANUAL" | "IA" | "CLONADO"; import type {
export type SubModoClonado = "INTERNO" | "TRADICIONAL"; NivelPlanEstudio,
TipoCiclo,
TipoOrigen,
} from "@/data/types/domain";
export type PlanPreview = { export type PlanPreview = {
nombrePlan: string; nombrePlan: string;
nivel: string; nivel: NivelPlanEstudio;
tipoCiclo: TipoCiclo; tipoCiclo: TipoCiclo;
numCiclos: number; numCiclos: number;
numAsignaturasAprox?: number; numAsignaturasAprox?: number;
@@ -13,20 +16,16 @@ export type PlanPreview = {
export type NewPlanWizardState = { export type NewPlanWizardState = {
step: 1 | 2 | 3 | 4; step: 1 | 2 | 3 | 4;
modoCreacion: ModoCreacion | null; tipoOrigen: TipoOrigen | null;
subModoClonado?: SubModoClonado;
datosBasicos: { datosBasicos: {
nombrePlan: string; nombrePlan: string;
carreraId: string; carreraId: string;
facultadId: string; facultadId: string;
nivel: string; nivel: NivelPlanEstudio | "";
tipoCiclo: TipoCiclo | ""; tipoCiclo: TipoCiclo | "";
numCiclos: number | undefined; numCiclos: number | undefined;
// Selección de plantillas (obligatorias) // Selección de plantillas (obligatorias)
plantillaPlanId?: string; estructuraPlanId: string | null;
plantillaPlanVersion?: string;
plantillaMapaId?: string;
plantillaMapaVersion?: string;
}; };
clonInterno?: { planOrigenId: string | null }; clonInterno?: { planOrigenId: string | null };
clonTradicional?: { clonTradicional?: {
@@ -53,12 +52,11 @@ export type NewPlanWizardState = {
}; };
iaConfig?: { iaConfig?: {
descripcionEnfoque: string; descripcionEnfoque: string;
poblacionObjetivo: string;
notasAdicionales: string; notasAdicionales: string;
archivosReferencia: Array<string>; archivosReferencia: Array<string>;
repositoriosReferencia?: Array<string>; repositoriosReferencia?: Array<string>;
archivosAdjuntos?: Array< archivosAdjuntos?: Array<
{ id: string; name: string; size: string; type: string } UploadedFile
>; >;
}; };
resumen: { previewPlan?: PlanPreview }; resumen: { previewPlan?: PlanPreview };

View File

@@ -0,0 +1,5 @@
export const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
};

View File

@@ -30,4 +30,29 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
/> />
</> </>
), ),
errorComponent: ({ error, reset }) => {
return (
<div className="flex min-h-[50vh] flex-col items-center justify-center space-y-4 p-6 text-center">
<h2 className="text-2xl font-bold text-red-600">
¡Ups! Algo salió mal
</h2>
<p className="max-w-md text-gray-600">
Ocurrió un error inesperado al cargar esta sección.
</p>
{/* Opcional: Mostrar el detalle técnico en desarrollo */}
<pre className="max-w-full overflow-auto rounded border border-gray-300 bg-gray-100 p-4 text-left text-xs">
{error.message}
</pre>
<button
onClick={reset}
className="rounded bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700"
>
Intentar de nuevo
</button>
</div>
)
},
}) })

View File

@@ -1,40 +1,50 @@
import { usePlan } from '@/data'; import { usePlan } from '@/data'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react' import { useState, useEffect } from 'react'
import type { DatosGeneralesField } from '@/types/plan' import type { DatosGeneralesField } from '@/types/plan'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { import { Pencil, Check, X, Sparkles, AlertCircle } from 'lucide-react'
Pencil,
Check,
X,
Sparkles,
AlertCircle
} from 'lucide-react'
//import { toast } from 'sonner' // Asegúrate de tener sonner instalado o quita la línea //import { toast } from 'sonner' // Asegúrate de tener sonner instalado o quita la línea
export const Route = createFileRoute('/planes/$planId/_detalle/datos')({ export const Route = createFileRoute('/planes/$planId/_detalle/datos')({
component: DatosGeneralesPage, component: DatosGeneralesPage,
}) })
function DatosGeneralesPage() { const formatLabel = (key: string) => {
const {data, isFetching} = usePlan('0e0aea4d-b8b4-4e75-8279-6224c3ac769f'); const result = key.replace(/_/g, ' ')
if(!isFetching && !data) { return result.charAt(0).toUpperCase() + result.slice(1)
return <div>No se encontró el plan de estudios.</div>
} }
console.log(data);
// 1. Definimos los DATOS iniciales (Lo que antes venía por props) function DatosGeneralesPage() {
const [campos, setCampos] = useState<DatosGeneralesField[]>([ const { data } = usePlan('0e0aea4d-b8b4-4e75-8279-6224c3ac769f')
{ id: '1', label: 'Objetivo General', value: 'Formar profesionales...', requerido: true, tipo: 'texto' },
{ id: '2', label: 'Perfil de Ingreso', value: 'Interés por la tecnología...', requerido: true, tipo: 'lista' },
{ id: '3', label: 'Perfil de Egreso', value: '', requerido: true, tipo: 'texto' },
])
// 2. Estados de edición // Inicializamos campos como un arreglo vacío
const [campos, setCampos] = useState<DatosGeneralesField[]>([])
const [editingId, setEditingId] = useState<string | null>(null) const [editingId, setEditingId] = useState<string | null>(null)
const [editValue, setEditValue] = useState('') const [editValue, setEditValue] = useState('')
// Efecto para transformar data?.datos en el arreglo de campos
useEffect(() => {
// 2. Validación de seguridad para sourceData
const sourceData = data?.datos
if (sourceData && typeof sourceData === 'object') {
const datosTransformados: DatosGeneralesField[] = Object.entries(
sourceData,
).map(([key, value], index) => ({
id: (index + 1).toString(),
label: formatLabel(key),
// Forzamos el valor a string de forma segura
value: typeof value === 'string' ? value : value?.toString() || '',
requerido: true,
tipo: 'texto',
}))
setCampos(datosTransformados)
}
}, [data])
// 3. Manejadores de acciones (Ahora como funciones locales) // 3. Manejadores de acciones (Ahora como funciones locales)
const handleEdit = (campo: DatosGeneralesField) => { const handleEdit = (campo: DatosGeneralesField) => {
setEditingId(campo.id) setEditingId(campo.id)
@@ -48,9 +58,9 @@ function DatosGeneralesPage() {
const handleSave = (id: string) => { const handleSave = (id: string) => {
// Actualizamos el estado local de la lista // Actualizamos el estado local de la lista
setCampos(prev => prev.map(c => setCampos((prev) =>
c.id === id ? { ...c, value: editValue } : c prev.map((c) => (c.id === id ? { ...c, value: editValue } : c)),
)) )
setEditingId(null) setEditingId(null)
setEditValue('') setEditValue('')
//toast.success('Cambios guardados localmente') //toast.success('Cambios guardados localmente')
@@ -62,40 +72,56 @@ function DatosGeneralesPage() {
} }
return ( return (
<div className="container mx-auto px-6 py-6 animate-in fade-in duration-500"> <div className="animate-in fade-in container mx-auto px-6 py-6 duration-500">
<div className="mb-6"> <div className="mb-6">
<h2 className="text-lg font-semibold text-foreground"> <h2 className="text-foreground text-lg font-semibold">
Datos Generales del Plan Datos Generales del Plan
</h2> </h2>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1 text-sm">
Información estructural y descriptiva del plan de estudios Información estructural y descriptiva del plan de estudios
</p> </p>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{campos.map((campo) => { {campos.map((campo) => {
const isEditing = editingId === campo.id const isEditing = editingId === campo.id
return ( return (
<div <div
key={campo.id} key={campo.id}
className={`border rounded-xl transition-all ${ className={`rounded-xl border transition-all ${
isEditing ? 'border-teal-500 ring-2 ring-teal-50 shadow-lg' : 'bg-white hover:shadow-md' isEditing
? 'border-teal-500 shadow-lg ring-2 ring-teal-50'
: 'bg-white hover:shadow-md'
}`} }`}
> >
{/* Header de la Card */} {/* Header de la Card */}
<div className="flex items-center justify-between px-5 py-3 border-b bg-slate-50/50"> <div className="flex items-center justify-between border-b bg-slate-50/50 px-5 py-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="font-medium text-sm text-slate-700">{campo.label}</h3> <h3 className="text-sm font-medium text-slate-700">
{campo.requerido && <span className="text-red-500 text-xs">*</span>} {campo.label}
</h3>
{campo.requerido && (
<span className="text-xs text-red-500">*</span>
)}
</div> </div>
{!isEditing && ( {!isEditing && (
<div className="flex gap-1"> <div className="flex gap-1">
<Button variant="ghost" size="icon" className="h-8 w-8 text-teal-600" onClick={() => handleIARequest(campo.id)}> <Button
variant="ghost"
size="icon"
className="h-8 w-8 text-teal-600"
onClick={() => handleIARequest(campo.id)}
>
<Sparkles size={14} /> <Sparkles size={14} />
</Button> </Button>
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => handleEdit(campo)}> <Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => handleEdit(campo)}
>
<Pencil size={14} /> <Pencil size={14} />
</Button> </Button>
</div> </div>
@@ -112,10 +138,18 @@ function DatosGeneralesPage() {
className="min-h-[120px]" className="min-h-[120px]"
/> />
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={handleCancel}> <Button
variant="outline"
size="sm"
onClick={handleCancel}
>
<X size={14} className="mr-1" /> Cancelar <X size={14} className="mr-1" /> Cancelar
</Button> </Button>
<Button size="sm" className="bg-teal-600 hover:bg-teal-700" onClick={() => handleSave(campo.id)}> <Button
size="sm"
className="bg-teal-600 hover:bg-teal-700"
onClick={() => handleSave(campo.id)}
>
<Check size={14} className="mr-1" /> Guardar <Check size={14} className="mr-1" /> Guardar
</Button> </Button>
</div> </div>
@@ -123,12 +157,12 @@ function DatosGeneralesPage() {
) : ( ) : (
<div className="min-h-[100px]"> <div className="min-h-[100px]">
{campo.value ? ( {campo.value ? (
<div className="text-sm text-slate-600 leading-relaxed"> <div className="text-sm leading-relaxed text-slate-600">
{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 rounded-full bg-teal-500 shrink-0" /> <span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-teal-500" />
{item} {item}
</li> </li>
))} ))}
@@ -138,7 +172,7 @@ function DatosGeneralesPage() {
)} )}
</div> </div>
) : ( ) : (
<div className="flex items-center gap-2 text-slate-400 text-sm"> <div className="flex items-center gap-2 text-sm text-slate-400">
<AlertCircle size={14} /> <AlertCircle size={14} />
<span>Sin contenido.</span> <span>Sin contenido.</span>
</div> </div>

View File

@@ -1,49 +1,64 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { CheckCircle2, Circle, Clock } from "lucide-react" import { CheckCircle2, Circle, Clock } from 'lucide-react'
import { Badge } from "@/components/ui/badge" import { Badge } from '@/components/ui/badge'
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Textarea } from "@/components/ui/textarea" import { Textarea } from '@/components/ui/textarea'
import { usePlanHistorial } from '@/data/hooks/usePlans'
export const Route = createFileRoute('/planes/$planId/_detalle/flujo')({ export const Route = createFileRoute('/planes/$planId/_detalle/flujo')({
component: RouteComponent, component: RouteComponent,
}) })
function RouteComponent() { function RouteComponent() {
const { data: rawData, isLoading } = usePlanHistorial(
'0e0aea4d-b8b4-4e75-8279-6224c3ac769f',
)
console.log(rawData)
return ( return (
<div className="flex flex-col gap-6 p-6"> <div className="flex flex-col gap-6 p-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>
<h1 className="text-2xl font-bold italic">Flujo de Aprobación</h1> <h1 className="text-2xl font-bold italic">Flujo de Aprobación</h1>
<p className="text-sm text-muted-foreground">Gestiona el proceso de revisión y aprobación del plan</p> <p className="text-muted-foreground text-sm">
Gestiona el proceso de revisión y aprobación del plan
</p>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
{/* LADO IZQUIERDO: Timeline del Flujo */} {/* LADO IZQUIERDO: Timeline del Flujo */}
<div className="lg:col-span-2 space-y-4"> <div className="space-y-4 lg:col-span-2">
{/* Estado: Completado */} {/* Estado: Completado */}
<div className="relative flex gap-4 pb-4"> <div className="relative flex gap-4 pb-4">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="rounded-full bg-green-100 p-1 text-green-600"> <div className="rounded-full bg-green-100 p-1 text-green-600">
<CheckCircle2 className="h-6 w-6" /> <CheckCircle2 className="h-6 w-6" />
</div> </div>
<div className="w-px flex-1 bg-green-200 mt-2" /> <div className="mt-2 w-px flex-1 bg-green-200" />
</div> </div>
<Card className="flex-1"> <Card className="flex-1">
<CardHeader className="flex flex-row items-center justify-between py-3"> <CardHeader className="flex flex-row items-center justify-between py-3">
<div> <div>
<CardTitle className="text-lg">Borrador</CardTitle> <CardTitle className="text-lg">Borrador</CardTitle>
<p className="text-xs text-muted-foreground">14 de enero de 2024</p> <p className="text-muted-foreground text-xs">
14 de enero de 2024
</p>
</div> </div>
<Badge variant="secondary" className="bg-green-100 text-green-700">Completado</Badge> <Badge
variant="secondary"
className="bg-green-100 text-green-700"
>
Completado
</Badge>
</CardHeader> </CardHeader>
<CardContent className="text-sm border-t pt-3"> <CardContent className="border-t pt-3 text-sm">
<p className="font-semibold text-muted-foreground mb-2">Comentarios</p> <p className="text-muted-foreground mb-2 font-semibold">
<ul className="list-disc list-inside space-y-1 text-muted-foreground"> Comentarios
</p>
<ul className="text-muted-foreground list-inside list-disc space-y-1">
<li>Documento inicial creado</li> <li>Documento inicial creado</li>
<li>Estructura base definida</li> <li>Estructura base definida</li>
</ul> </ul>
@@ -57,19 +72,27 @@ function RouteComponent() {
<div className="rounded-full bg-blue-100 p-1 text-blue-600 ring-2 ring-blue-500 ring-offset-2"> <div className="rounded-full bg-blue-100 p-1 text-blue-600 ring-2 ring-blue-500 ring-offset-2">
<Clock className="h-6 w-6" /> <Clock className="h-6 w-6" />
</div> </div>
<div className="w-px flex-1 bg-slate-200 mt-2" /> <div className="mt-2 w-px flex-1 bg-slate-200" />
</div> </div>
<Card className="flex-1 border-blue-500 bg-blue-50/10"> <Card className="flex-1 border-blue-500 bg-blue-50/10">
<CardHeader className="flex flex-row items-center justify-between py-3"> <CardHeader className="flex flex-row items-center justify-between py-3">
<div> <div>
<CardTitle className="text-lg text-blue-700">En Revisión</CardTitle> <CardTitle className="text-lg text-blue-700">
<p className="text-xs text-muted-foreground">19 de febrero de 2024</p> En Revisión
</CardTitle>
<p className="text-muted-foreground text-xs">
19 de febrero de 2024
</p>
</div> </div>
<Badge variant="default" className="bg-blue-500">En curso</Badge> <Badge variant="default" className="bg-blue-500">
En curso
</Badge>
</CardHeader> </CardHeader>
<CardContent className="text-sm border-t border-blue-100 pt-3"> <CardContent className="border-t border-blue-100 pt-3 text-sm">
<p className="font-semibold text-muted-foreground mb-2">Comentarios</p> <p className="text-muted-foreground mb-2 font-semibold">
<ul className="list-disc list-inside space-y-1 text-muted-foreground"> Comentarios
</p>
<ul className="text-muted-foreground list-inside list-disc space-y-1">
<li>Revisión de objetivo general pendiente</li> <li>Revisión de objetivo general pendiente</li>
<li>Mapa curricular aprobado preliminarmente</li> <li>Mapa curricular aprobado preliminarmente</li>
</ul> </ul>
@@ -91,7 +114,6 @@ function RouteComponent() {
</CardHeader> </CardHeader>
</Card> </Card>
</div> </div>
</div> </div>
{/* LADO DERECHO: Formulario de Transición */} {/* LADO DERECHO: Formulario de Transición */}
@@ -101,20 +123,22 @@ function RouteComponent() {
<CardTitle className="text-lg">Transición de Estado</CardTitle> <CardTitle className="text-lg">Transición de Estado</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<div className="flex items-center justify-between p-3 bg-slate-50 rounded-lg text-sm border"> <div className="flex items-center justify-between rounded-lg border bg-slate-50 p-3 text-sm">
<div className="text-center"> <div className="text-center">
<p className="text-xs text-muted-foreground">Estado actual</p> <p className="text-muted-foreground text-xs">Estado actual</p>
<p className="font-bold">En Revisión</p> <p className="font-bold">En Revisión</p>
</div> </div>
<div className="h-px flex-1 bg-slate-300 mx-4" /> <div className="mx-4 h-px flex-1 bg-slate-300" />
<div className="text-center"> <div className="text-center">
<p className="text-xs text-muted-foreground">Siguiente</p> <p className="text-muted-foreground text-xs">Siguiente</p>
<p className="font-bold text-primary">Revisión Expertos</p> <p className="text-primary font-bold">Revisión Expertos</p>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Comentario de transición</label> <label className="text-sm font-medium">
Comentario de transición
</label>
<Textarea <Textarea
placeholder="Agrega un comentario para la transición..." placeholder="Agrega un comentario para la transición..."
className="min-h-[120px]" className="min-h-[120px]"
@@ -127,7 +151,6 @@ function RouteComponent() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</div> </div>
</div> </div>
) )

View File

@@ -1,142 +1,284 @@
import { useMemo, useState } from 'react'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { import {
GitBranch, GitBranch,
Edit3, Edit3,
PlusCircle, PlusCircle,
FileText,
RefreshCw, RefreshCw,
User User,
} from "lucide-react" Loader2,
import { Badge } from "@/components/ui/badge" Clock,
import { Card, CardContent } from "@/components/ui/card" Eye,
import { Avatar, AvatarFallback } from "@/components/ui/avatar" History,
Calendar,
} from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { usePlanHistorial } from '@/data/hooks/usePlans'
import { format, formatDistanceToNow, parseISO } from 'date-fns'
import { es } from 'date-fns/locale'
export const Route = createFileRoute('/planes/$planId/_detalle/historial')({ export const Route = createFileRoute('/planes/$planId/_detalle/historial')({
component: RouteComponent, component: RouteComponent,
}) })
function RouteComponent() { const getEventConfig = (tipo: string, campo: string) => {
const historyEvents = [ if (tipo === 'CREACION')
{ return {
id: 1, label: 'Creación',
type: 'Cambio de estado',
user: 'Dr. Juan Pérez',
description: 'Plan pasado de Borrador a En Revisión',
date: 'Hace 2 días',
icon: <GitBranch className="h-4 w-4" />,
details: { from: 'Borrador', to: 'En Revisión' }
},
{
id: 2,
type: 'Edición',
user: 'Lic. María García',
description: 'Actualizado perfil de egreso',
date: 'Hace 3 días',
icon: <Edit3 className="h-4 w-4" />,
},
{
id: 3,
type: 'Reorganización',
user: 'Ing. Carlos López',
description: 'Movida materia BD102 de ciclo 3 a ciclo 4',
date: 'Hace 5 días',
icon: <RefreshCw className="h-4 w-4" />,
details: { from: 'Ciclo 3', to: 'Ciclo 4' }
},
{
id: 4,
type: 'Creación',
user: 'Dr. Juan Pérez',
description: 'Añadida nueva materia: Inteligencia Artificial',
date: 'Hace 1 semana',
icon: <PlusCircle className="h-4 w-4" />, icon: <PlusCircle className="h-4 w-4" />,
}, color: 'teal',
{
id: 5,
type: 'Documento',
user: 'Lic. María García',
description: 'Generado documento oficial v1.0',
date: 'Hace 1 semana',
icon: <FileText className="h-4 w-4" />,
} }
] if (campo === 'estado')
return {
label: 'Cambio de estado',
icon: <GitBranch className="h-4 w-4" />,
color: 'blue',
}
if (campo === 'datos')
return {
label: 'Edición de Datos',
icon: <Edit3 className="h-4 w-4" />,
color: 'amber',
}
return {
label: 'Actualización',
icon: <RefreshCw className="h-4 w-4" />,
color: 'slate',
}
}
function RouteComponent() {
const { planId } = Route.useParams()
const { data: rawData, isLoading } = usePlanHistorial(
'0e0aea4d-b8b4-4e75-8279-6224c3ac769f',
)
// ESTADOS PARA EL MODAL
const [selectedEvent, setSelectedEvent] = useState<any>(null)
const [isModalOpen, setIsModalOpen] = useState(false)
const historyEvents = useMemo(() => {
if (!rawData) return []
return rawData.map((item: any) => {
const config = getEventConfig(item.tipo, item.campo)
return {
id: item.id,
type: config.label,
user:
item.cambiado_por === '11111111-1111-1111-1111-111111111111'
? 'Administrador'
: 'Usuario Staff',
description:
item.campo === 'datos'
? `Actualización general de: ${item.valor_nuevo?.nombre || 'información del plan'}`
: `Se modificó el campo ${item.campo}`,
date: parseISO(item.cambiado_en),
icon: config.icon,
campo: item.campo,
details: {
from: item.valor_anterior,
to: item.valor_nuevo,
},
}
})
}, [rawData])
const openCompareModal = (event: any) => {
setSelectedEvent(event)
setIsModalOpen(true)
}
const renderValue = (val: any) => {
if (!val) return 'Sin información'
if (typeof val === 'object') return JSON.stringify(val, null, 2)
return String(val)
}
if (isLoading)
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-teal-600" />
</div>
)
return ( return (
<div className="p-6 max-w-5xl mx-auto"> <div className="mx-auto max-w-5xl p-6">
<div className="mb-8"> <div className="mb-8 flex items-end justify-between">
<h1 className="text-xl font-bold text-slate-800">Historial de Cambios</h1> <div>
<p className="text-sm text-muted-foreground">Registro de todas las modificaciones realizadas al plan</p> <h1 className="flex items-center gap-2 text-xl font-bold text-slate-800">
<Clock className="h-5 w-5 text-teal-600" /> Historial de Cambios del
Plan
</h1>
<p className="text-muted-foreground text-sm">
Registro cronológico de modificaciones realizadas
</p>
</div>
</div> </div>
<div className="relative space-y-0"> <div className="relative space-y-0">
{/* Línea vertical de fondo */} <div className="absolute top-0 bottom-0 left-9 w-px bg-slate-200" />
<div className="absolute left-9 top-0 bottom-0 w-px bg-slate-200" /> {historyEvents.length === 0 ? (
<div className="ml-20 py-10 text-slate-500">No hay registros.</div>
{historyEvents.map((event) => ( ) : (
<div key={event.id} className="relative flex gap-6 pb-8 group"> historyEvents.map((event) => (
<div key={event.id} className="group relative flex gap-6 pb-8">
{/* Indicador con Icono */}
<div className="relative z-10 flex h-18 flex-col items-center"> <div className="relative z-10 flex h-18 flex-col items-center">
<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 group-hover:bg-teal-50 group-hover:text-teal-600 transition-colors"> <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">
{event.icon} {event.icon}
</div> </div>
</div> </div>
{/* Tarjeta de Contenido */} <Card className="flex-1 border-slate-200 shadow-none transition-colors hover:border-teal-200">
<Card className="flex-1 shadow-none border-slate-200 hover:border-teal-200 transition-colors">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-2 mb-2"> <div className="flex flex-col gap-1">
{/* 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 items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-bold text-slate-800 text-sm">{event.type}</span> <span className="text-sm font-bold text-slate-800">
<Badge variant="outline" className="text-[10px] font-normal py-0"> {event.type}
{event.date} </span>
<Badge
variant="outline"
className="h-5 py-0 text-[10px] font-normal"
>
{formatDistanceToNow(event.date, {
addSuffix: true,
locale: es,
})}
</Badge> </Badge>
</div> </div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Avatar className="h-5 w-5 border"> {/* Grupo de elementos alineados a la derecha */}
<AvatarFallback className="text-[8px] bg-slate-50"><User size={10}/></AvatarFallback> <div className="flex items-center gap-4 text-slate-500">
</Avatar> {/* Usuario e Icono */}
<div className="flex items-center gap-2 text-xs">
<User className="h-3.5 w-3.5" />
<span className="text-muted-foreground">
{event.user} {event.user}
</span>
</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) */}
<span className="hidden text-[11px] text-slate-400 md:block">
{format(event.date, 'yyyy-MM-dd HH:mm')}
</span>
</div> </div>
</div> </div>
<p className="text-sm text-slate-600 mb-3">{event.description}</p> {/* LÍNEA INFERIOR: Descripción */}
<div className="mt-1">
<p className="text-sm text-slate-600">
{event.description}
</p>
{/* Badges de transición (si existen) */} {/* Badges de transición opcionales (de estado) */}
{event.details && ( {event.details &&
<div className="flex items-center gap-2 mt-2"> typeof event.details.from === 'string' &&
<Badge variant="secondary" className="bg-orange-50 text-orange-700 hover:bg-orange-50 border-orange-100 text-[10px]"> event.campo === 'estado' && (
<div className="mt-2 flex items-center gap-1.5">
<Badge
variant="secondary"
className="bg-red-50 px-1.5 text-[9px] text-red-700"
>
{event.details.from} {event.details.from}
</Badge> </Badge>
<span className="text-slate-400 text-xs"></span> <span className="text-[10px] text-slate-400">
<Badge variant="secondary" className="bg-green-50 text-green-700 hover:bg-green-50 border-green-100 text-[10px]">
</span>
<Badge
variant="secondary"
className="bg-emerald-50 px-1.5 text-[9px] text-emerald-700"
>
{event.details.to} {event.details.to}
</Badge> </Badge>
</div> </div>
)} )}
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
))} ))
)}
</div>
{/* Evento inicial de creación */} {/* MODAL DE COMPARACIÓN CON SCROLL INTERNO */}
<div className="relative flex gap-6 group"> <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<div className="relative z-10 flex items-center"> <DialogContent className="flex max-h-[90vh] max-w-4xl flex-col gap-0 overflow-hidden p-0">
<div className="flex h-[42px] w-[42px] items-center justify-center rounded-full border-4 border-white bg-teal-600 text-white shadow-sm"> <DialogHeader className="border-b bg-slate-50/50 p-6">
<PlusCircle className="h-4 w-4" /> <DialogTitle className="flex items-center gap-2">
<History className="h-5 w-5 text-teal-600" /> Comparación de
Versiones
</DialogTitle>
<div className="text-muted-foreground flex items-center gap-4 pt-2 text-xs">
<span className="flex items-center gap-1">
<User className="h-3 w-3" /> {selectedEvent?.user}
</span>
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />{' '}
{selectedEvent &&
format(selectedEvent.date, "d 'de' MMMM, HH:mm", {
locale: es,
})}
</span>
</div>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-6">
<div className="grid h-full grid-cols-2 gap-6">
{/* Lado Antes */}
<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="h-2 w-2 rounded-full bg-red-400" />
<span className="text-muted-foreground text-[10px] font-bold tracking-widest uppercase">
Versión Anterior
</span>
</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>
</div> </div>
<Card className="flex-1 bg-teal-50/30 border-teal-100 shadow-none">
<CardContent className="p-4"> {/* Lado Después */}
<div className="flex items-center justify-between mb-1"> <div className="flex flex-col space-y-2">
<span className="font-bold text-teal-900 text-sm">Creación</span> <div className="sticky top-0 z-10 flex items-center gap-2 bg-white py-1">
<span className="text-[10px] text-teal-600 font-medium">14 Ene 2024</span> <div className="h-2 w-2 rounded-full bg-emerald-400" />
<span className="text-muted-foreground text-[10px] font-bold tracking-widest uppercase">
Nueva Versión
</span>
</div> </div>
<p className="text-sm text-teal-800/80">Plan de estudios creado</p> <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">
</CardContent> {renderValue(selectedEvent?.details.to)}
</Card>
</div> </div>
</div> </div>
</div> </div>
</div>
<div className="flex justify-center border-t bg-slate-50 p-4">
<Badge variant="outline" className="font-mono text-[10px]">
Campo: {selectedEvent?.campo}
</Badge>
</div>
</DialogContent>
</Dialog>
</div>
) )
} }

View File

@@ -1,118 +1,271 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { Sparkles, Send, Paperclip, Target, UserCheck, Lightbulb, FileText } from "lucide-react" import { useState, useEffect, useRef } from 'react'
import { useState } from 'react' // Importamos useState import {
import { Button } from "@/components/ui/button" Sparkles,
import { Input } from "@/components/ui/input" Send,
import { ScrollArea } from "@/components/ui/scroll-area" Paperclip,
import { Avatar, AvatarFallback } from "@/components/ui/avatar" Target,
UserCheck,
Lightbulb,
FileText,
Users,
GraduationCap,
BookOpen,
Check,
X,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
const PRESETS = [
{
id: 'objetivo',
label: 'Mejorar objetivo general',
icon: Target,
prompt: 'Mejora la redacción del objetivo general...',
},
{
id: 'perfil-egreso',
label: 'Redactar perfil de egreso',
icon: GraduationCap,
prompt: 'Genera un perfil de egreso detallado...',
},
{
id: 'competencias',
label: 'Sugerir competencias',
icon: BookOpen,
prompt: 'Genera una lista de competencias...',
},
{
id: 'pertinencia',
label: 'Justificar pertinencia',
icon: FileText,
prompt: 'Redacta una justificación de pertinencia...',
},
]
export const Route = createFileRoute('/planes/$planId/_detalle/iaplan')({ export const Route = createFileRoute('/planes/$planId/_detalle/iaplan')({
component: RouteComponent, component: RouteComponent,
}) })
function RouteComponent() { interface Message {
// 1. Estado para el texto del input id: string
const [inputValue, setInputValue] = useState('') role: 'user' | 'assistant'
content: string
// 2. Estado para la lista de mensajes (iniciamos con los de la imagen)
const [messages, setMessages] = useState([
{ id: 1, role: 'ai', text: 'Hola, soy tu asistente de IA para el diseño del plan de estudios...' },
{ id: 2, role: 'user', text: 'jkasakj' },
{ id: 3, role: 'ai', text: 'Entendido. Estoy procesando tu solicitud.' },
])
// 3. Función para enviar el mensaje
const handleSend = () => {
if (!inputValue.trim()) return
// Agregamos el mensaje del usuario
const newMessage = {
id: Date.now(),
role: 'user',
text: inputValue
} }
setMessages([...messages, newMessage]) function RouteComponent() {
setInputValue('') // Limpiamos el input const [messages, setMessages] = useState<Message[]>([
{
id: '1',
role: 'assistant',
content: '¡Hola! Soy tu asistente de IA. ¿En qué puedo ayudarte hoy?',
},
])
const [input, setInput] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [pendingSuggestion, setPendingSuggestion] = useState<{
field: string
text: string
} | null>(null)
const scrollRef = useRef<HTMLDivElement>(null)
// Función de scroll corregida para Radix
const scrollToBottom = () => {
const viewport = scrollRef.current?.querySelector(
'[data-radix-scroll-area-viewport]',
)
if (viewport) {
viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'smooth' })
}
}
useEffect(() => {
const timer = setTimeout(scrollToBottom, 100)
return () => clearTimeout(timer)
}, [messages, isLoading])
const handleSend = async (prompt?: string) => {
const messageText = prompt || input
if (!messageText.trim()) return
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: messageText,
}
setMessages((prev) => [...prev, userMessage])
setInput('')
setIsLoading(true)
setTimeout(() => {
const mockText =
'He analizado tu solicitud. Basado en los estándares actuales, sugiero fortalecer las competencias técnicas...'
const aiResponse: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: `He analizado tu solicitud. Aquí está mi sugerencia:\n\n"${mockText}"\n\n¿Te gustaría aplicar este texto al plan?`,
}
setMessages((prev) => [...prev, aiResponse])
setPendingSuggestion({ field: 'seccion-plan', text: mockText })
setIsLoading(false)
}, 1200)
} }
return ( return (
<div className="flex h-[calc(100vh-200px)] gap-6 p-4"> /* CAMBIO CLAVE 1:
<div className="flex flex-col flex-1 bg-slate-50/50 rounded-xl border relative overflow-hidden"> Aseguramos que el contenedor padre ocupe el espacio disponible pero NO MÁS.
'max-h-full' y 'flex-1' evitan que el chat empuje el layout hacia abajo.
*/
<div className="flex h-[calc(100vh-160px)] max-h-[calc(100vh-160px)] w-full gap-6 overflow-hidden p-4">
{/* PANEL DE CHAT */}
<div className="relative flex min-w-0 flex-[3] flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-50/50 shadow-sm">
{/* Header Fijo (shrink-0 es vital para que no se aplaste) */}
<div className="flex shrink-0 items-center justify-between border-b bg-white p-4">
<div className="flex flex-col">
<h3 className="flex items-center gap-2 text-sm font-bold text-slate-700">
<Sparkles className="h-4 w-4 text-teal-600" />
Asistente de Diseño Curricular
</h3>
<p className="text-left text-[11px] text-slate-500">
Optimizado con IA
</p>
</div>
</div>
<ScrollArea className="flex-1 p-6"> {/* CAMBIO CLAVE 2:
<div className="space-y-6 max-w-3xl mx-auto"> El ScrollArea debe tener 'flex-1' y 'h-full'.
{/* 4. Mapeamos los mensajes dinámicamente */} Esto obliga al componente a colapsar su altura y activar el scroll.
*/}
<div className="relative min-h-0 flex-1">
<ScrollArea ref={scrollRef} className="h-full w-full">
<div className="mx-auto max-w-3xl space-y-6 p-6">
{messages.map((msg) => ( {messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'} gap-3`}> <div
{msg.role === 'ai' && ( key={msg.id}
<Avatar className="h-8 w-8 border bg-teal-50"> className={`flex ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'} items-start gap-3`}
<AvatarFallback className="text-teal-600"><Sparkles size={16}/></AvatarFallback> >
</Avatar> <Avatar
className={`h-8 w-8 shrink-0 border ${msg.role === 'assistant' ? 'bg-teal-50' : 'bg-slate-200'}`}
>
<AvatarFallback className="text-[10px]">
{msg.role === 'assistant' ? (
<Sparkles size={14} className="text-teal-600" />
) : (
<UserCheck size={14} />
)} )}
</AvatarFallback>
<div className={msg.role === 'ai' ? 'space-y-2' : ''}> </Avatar>
{msg.role === 'ai' && <p className="text-xs font-bold text-teal-700 uppercase tracking-wider">Asistente IA</p>} <div
<div className={`p-4 rounded-2xl text-sm shadow-sm ${ className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
>
{msg.role === 'assistant' && (
<span className="mb-1 ml-1 text-[9px] font-bold text-teal-700 uppercase">
Asistente IA
</span>
)}
<div
className={`rounded-2xl p-3 text-left text-sm whitespace-pre-wrap shadow-sm ${
msg.role === 'user' msg.role === 'user'
? 'bg-teal-600 text-white rounded-tr-none' ? 'rounded-tr-none bg-teal-600 text-white'
: 'bg-white border text-slate-700 rounded-tl-none' : 'rounded-tl-none border border-slate-200 bg-white text-slate-700'
}`}> }`}
{msg.text} >
{msg.content}
</div> </div>
</div> </div>
</div> </div>
))} ))}
{isLoading && (
<div className="flex gap-2 p-4">
<div className="h-2 w-2 animate-bounce rounded-full bg-teal-400" />
<div className="h-2 w-2 animate-bounce rounded-full bg-teal-400 [animation-delay:0.2s]" />
<div className="h-2 w-2 animate-bounce rounded-full bg-teal-400 [animation-delay:0.4s]" />
</div>
)}
</div> </div>
</ScrollArea> </ScrollArea>
{/* 5. Input vinculado al estado */} {/* Barra de aplicación flotante (dentro del contenedor relativo del scroll) */}
<div className="p-4 bg-white border-t"> {pendingSuggestion && !isLoading && (
<div className="max-w-4xl mx-auto flex gap-2 items-center bg-slate-50 border rounded-lg px-3 py-1 shadow-sm focus-within:ring-1 focus-within:ring-teal-500 transition-all"> <div className="animate-in fade-in slide-in-from-bottom-2 absolute bottom-4 left-1/2 flex -translate-x-1/2 gap-2 rounded-full border bg-white p-1.5 shadow-2xl">
<Input <Button
value={inputValue} variant="ghost"
onChange={(e) => setInputValue(e.target.value)} size="sm"
onKeyDown={(e) => e.key === 'Enter' && handleSend()} // Enviar con Enter onClick={() => setPendingSuggestion(null)}
className="border-none bg-transparent focus-visible:ring-0 text-sm" className="h-8 rounded-full text-xs"
placeholder='Escribe tu solicitud... Usa ":" para mencionar campos' >
/> <X className="mr-1 h-3 w-3" /> Descartar
<Button variant="ghost" size="icon" className="text-slate-400">
<Paperclip size={18} />
</Button> </Button>
<Button <Button
onClick={handleSend} size="sm"
size="icon" onClick={() => {}}
className="bg-teal-600 hover:bg-teal-700 h-8 w-8" className="h-8 rounded-full bg-teal-600 text-xs text-white hover:bg-teal-700"
> >
<Send size={16} /> <Check className="mr-1 h-3 w-3" /> Aplicar cambios
</Button>
</div>
)}
</div>
{/* INPUT FIJO AL FONDO */}
<div className="shrink-0 border-t bg-white p-4">
<div className="relative mx-auto max-w-4xl">
<div className="flex items-end gap-2 rounded-xl border bg-slate-50 p-2 transition-all focus-within:bg-white focus-within:ring-1 focus-within:ring-teal-500">
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}}
placeholder="Escribe tu solicitud aquí..."
className="max-h-[120px] min-h-[40px] flex-1 resize-none border-none bg-transparent py-2 text-left text-sm focus-visible:ring-0"
/>
<Button
onClick={() => handleSend()}
disabled={!input.trim() || isLoading}
size="icon"
className="h-9 w-9 shrink-0 bg-teal-600 hover:bg-teal-700"
>
<Send size={16} className="text-white" />
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
{/* Panel lateral (se mantiene igual) */}
<div className="w-72 space-y-4">
<div className="flex items-center gap-2 text-orange-500 font-semibold text-sm mb-4">
<Lightbulb size={18} />
Acciones rápidas
</div> </div>
{/* PANEL LATERAL */}
<div className="flex flex-[1] flex-col gap-4 overflow-y-auto pr-2">
<h4 className="flex items-center gap-2 text-left text-sm font-bold text-slate-800">
<Lightbulb size={18} className="text-orange-500" /> Acciones rápidas
</h4>
<div className="space-y-2"> <div className="space-y-2">
<ActionButton icon={<Target className="text-teal-500" size={18} />} text="Mejorar objetivo general" /> {PRESETS.map((preset) => (
<ActionButton icon={<UserCheck className="text-slate-500" size={18} />} text="Redactar perfil de egreso" /> <button
<ActionButton icon={<Lightbulb className="text-blue-500" size={18} />} text="Sugerir competencias" /> key={preset.id}
<ActionButton icon={<FileText className="text-teal-500" size={18} />} text="Justificar pertinencia" /> onClick={() => handleSend(preset.prompt)}
className="group flex w-full items-center gap-3 rounded-xl border bg-white p-3 text-left text-sm shadow-sm transition-all hover:border-teal-500 hover:bg-teal-50"
>
<div className="rounded-lg bg-slate-100 p-2 text-slate-500 group-hover:bg-teal-100 group-hover:text-teal-600">
<preset.icon size={16} />
</div>
<span className="leading-tight font-medium text-slate-700">
{preset.label}
</span>
</button>
))}
</div> </div>
</div> </div>
</div> </div>
) )
} }
function ActionButton({ icon, text }: { icon: React.ReactNode, text: string }) { function generateMockResponse(prompt: string) {
return ( return 'Mock response content...'
<Button variant="outline" className="w-full justify-start gap-3 h-auto py-3 px-4 text-sm font-normal hover:bg-slate-50 border-slate-200 shadow-sm text-slate-700">
{icon}
{text}
</Button>
)
} }

View File

@@ -1,5 +1,5 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react' import { useMemo, useState, useEffect } from 'react'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { import {
@@ -7,65 +7,101 @@ import {
ChevronDown, ChevronDown,
AlertTriangle, AlertTriangle,
GripVertical, GripVertical,
Trash2 Trash2,
} from 'lucide-react' } from 'lucide-react'
import type { Materia, LineaCurricular } from '@/types/plan' import type { Materia, LineaCurricular } from '@/types/plan'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog, DialogContent, DialogHeader, DialogTitle Dialog,
} from "@/components/ui/dialog" DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from '@/components/ui/dropdown-menu'
import { usePlanAsignaturas, usePlanLineas } from '@/data'
export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({ // --- Mapeadores (Fuera del componente para mayor limpieza) ---
component: MapaCurricularPage, const mapLineasToLineaCurricular = (
}) lineasApi: any[] = [],
): LineaCurricular[] => {
return lineasApi.map((linea) => ({
id: linea.id,
nombre: linea.nombre,
orden: linea.orden ?? 0,
color: '#1976d2',
}))
}
// --- Constantes de Estilo y Datos --- const mapAsignaturasToMaterias = (asigApi: any[] = []): Materia[] => {
const INITIAL_LINEAS: LineaCurricular[] = [ return asigApi.map((asig) => ({
{ id: 'l1', nombre: 'Formación Básica', orden: 1 }, id: asig.id,
{ id: 'l2', nombre: 'Ciencias de la Computación', orden: 2 }, clave: asig.codigo,
]; nombre: asig.nombre,
creditos: asig.creditos ?? 0,
const INITIAL_MATERIAS: Materia[] = [ ciclo: asig.numero_ciclo ?? null,
{ id: "1", clave: 'MAT101', nombre: 'Cálculo Diferencial', creditos: 8, hd: 4, hi: 4, ciclo: 1, lineaCurricularId: 'l1', tipo: 'obligatoria', estado: 'aprobada' }, lineaCurricularId: asig.linea_plan_id ?? null,
{ id: "2", clave: 'FIS101', nombre: 'Física Mecánica', creditos: 6, hd: 3, hi: 3, ciclo: 1, lineaCurricularId: 'l1', tipo: 'obligatoria', estado: 'aprobada' }, tipo: asig.tipo === 'OBLIGATORIA' ? 'obligatoria' : 'optativa',
{ id: "3", clave: 'PRO101', nombre: 'Fundamentos de Programación', creditos: 8, hd: 4, hi: 4, ciclo: null, lineaCurricularId: null, tipo: 'obligatoria', estado: 'borrador' }, estado: 'borrador',
]; orden: asig.orden_celda ?? 0,
hd: Math.floor((asig.horas_semana ?? 0) / 2),
hi: Math.ceil((asig.horas_semana ?? 0) / 2),
}))
}
const lineColors = [ const lineColors = [
'bg-blue-50 border-blue-200 text-blue-700', 'bg-blue-50 border-blue-200 text-blue-700',
'bg-purple-50 border-purple-200 text-purple-700', 'bg-purple-50 border-purple-200 text-purple-700',
'bg-orange-50 border-orange-200 text-orange-700', 'bg-orange-50 border-orange-200 text-orange-700',
'bg-emerald-50 border-emerald-200 text-emerald-700', 'bg-emerald-50 border-emerald-200 text-emerald-700',
]; ]
const statusBadge: Record<string, string> = { const statusBadge: Record<string, string> = {
borrador: 'bg-slate-100 text-slate-600', borrador: 'bg-slate-100 text-slate-600',
revisada: 'bg-amber-100 text-amber-700', revisada: 'bg-amber-100 text-amber-700',
aprobada: 'bg-emerald-100 text-emerald-700', aprobada: 'bg-emerald-100 text-emerald-700',
}; }
// --- Subcomponentes --- // --- Subcomponentes ---
function StatItem({ label, value, total }: { label: string, value: number, total?: number }) { function StatItem({
label,
value,
total,
}: {
label: string
value: number
total?: number
}) {
return ( return (
<div className="flex items-baseline gap-1.5"> <div className="flex items-baseline gap-1.5">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">{label}:</span> <span className="text-[10px] font-bold tracking-wider text-slate-400 uppercase">
{label}:
</span>
<span className="text-sm font-bold text-slate-700"> <span className="text-sm font-bold text-slate-700">
{value}{total ? <span className="text-slate-400 font-normal">/{total}</span> : ''} {value}
{total ? (
<span className="font-normal text-slate-400">/{total}</span>
) : (
''
)}
</span> </span>
</div> </div>
) )
} }
function MateriaCardItem({ materia, onDragStart, isDragging, onClick }: { function MateriaCardItem({
materia: Materia, materia,
onDragStart: (e: React.DragEvent, id: string) => void, onDragStart,
isDragging: boolean, isDragging,
onClick,
}: {
materia: Materia
onDragStart: (e: React.DragEvent, id: string) => void
isDragging: boolean
onClick: () => void onClick: () => void
}) { }) {
return ( return (
@@ -73,230 +109,386 @@ function MateriaCardItem({ materia, onDragStart, isDragging, onClick }: {
draggable draggable
onDragStart={(e) => onDragStart(e, materia.id)} onDragStart={(e) => onDragStart(e, materia.id)}
onClick={onClick} onClick={onClick}
className={`group p-3 rounded-lg border bg-white shadow-sm cursor-grab active:cursor-grabbing transition-all ${ className={`group cursor-grab rounded-lg border bg-white p-3 shadow-sm transition-all active:cursor-grabbing ${
isDragging ? 'opacity-40 scale-95' : 'hover:border-teal-400 hover:shadow-md' isDragging
? 'scale-95 opacity-40'
: 'hover:border-teal-400 hover:shadow-md'
}`} }`}
> >
<div className="flex justify-between items-start mb-1"> <div className="mb-1 flex items-start justify-between">
<span className="text-[10px] font-mono font-bold text-slate-400">{materia.clave}</span> <span className="font-mono text-[10px] font-bold text-slate-400">
<Badge variant="outline" className={`text-[9px] px-1 py-0 uppercase ${statusBadge[materia.estado] || ''}`}> {materia.clave}
</span>
<Badge
variant="outline"
className={`px-1 py-0 text-[9px] uppercase ${statusBadge[materia.estado] || ''}`}
>
{materia.estado} {materia.estado}
</Badge> </Badge>
</div> </div>
<p className="text-xs font-bold text-slate-700 leading-tight mb-1">{materia.nombre}</p> <p className="mb-1 text-xs leading-tight font-bold text-slate-700">
<div className="flex items-center justify-between mt-2"> {materia.nombre}
<span className="text-[10px] text-slate-500">{materia.creditos} CR HD:{materia.hd} HI:{materia.hi}</span> </p>
<GripVertical size={12} className="text-slate-300 opacity-0 group-hover:opacity-100 transition-opacity" /> <div className="mt-2 flex items-center justify-between">
<span className="text-[10px] text-slate-500">
{materia.creditos} CR HD:{materia.hd} HI:{materia.hi}
</span>
<GripVertical
size={12}
className="text-slate-300 opacity-0 transition-opacity group-hover:opacity-100"
/>
</div> </div>
</div> </div>
) )
} }
// --- Componente Principal --- export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({
function MapaCurricularPage() { component: MapaCurricularPage,
const [materias, setMaterias] = useState<Materia[]>(INITIAL_MATERIAS); })
const [lineas, setLineas] = useState<LineaCurricular[]>(INITIAL_LINEAS);
const [draggedMateria, setDraggedMateria] = useState<string | null>(null);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [selectedMateria, setSelectedMateria] = useState<Materia | null>(null);
const ciclosTotales = 9; function MapaCurricularPage() {
const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1); const { planId } = Route.useParams() // Idealmente usa el ID de la ruta
// 1. Fetch de Datos
const { data: asignaturasApi, isLoading: loadingAsig } = usePlanAsignaturas(
/*planId*/ '0e0aea4d-b8b4-4e75-8279-6224c3ac769f',
)
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(
/*planId*/ '0e0aea4d-b8b4-4e75-8279-6224c3ac769f',
)
// 2. Estado Local (Para interactividad)
const [materias, setMaterias] = useState<Materia[]>([])
const [lineas, setLineas] = useState<LineaCurricular[]>([])
const [draggedMateria, setDraggedMateria] = useState<string | null>(null)
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [selectedMateria, setSelectedMateria] = useState<Materia | null>(null)
// 3. Sincronizar API -> Estado Local
useEffect(() => {
if (asignaturasApi) setMaterias(mapAsignaturasToMaterias(asignaturasApi))
}, [asignaturasApi])
useEffect(() => {
if (lineasApi) setLineas(mapLineasToLineaCurricular(lineasApi))
}, [lineasApi])
const ciclosTotales = 9
const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1)
// --- Lógica de Gestión --- // --- Lógica de Gestión ---
const agregarLinea = (nombre: string) => { const agregarLinea = (nombre: string) => {
const nueva = { id: crypto.randomUUID(), nombre, orden: lineas.length + 1 }; const nueva = { id: crypto.randomUUID(), nombre, orden: lineas.length + 1 }
setLineas([...lineas, nueva]); setLineas([...lineas, nueva])
}; }
const borrarLinea = (id: string) => { const borrarLinea = (id: string) => {
setMaterias(prev => prev.map(m => m.lineaCurricularId === id ? { ...m, ciclo: null, lineaCurricularId: null } : m)); setMaterias((prev) =>
setLineas(prev => prev.filter(l => l.id !== id)); prev.map((m) =>
}; m.lineaCurricularId === id
? { ...m, ciclo: null, lineaCurricularId: null }
: m,
),
)
setLineas((prev) => prev.filter((l) => l.id !== id))
}
// --- Selectores/Cálculos ---
const getTotalesCiclo = (ciclo: number) => { const getTotalesCiclo = (ciclo: number) => {
return materias.filter(m => m.ciclo === ciclo).reduce((acc, m) => ({ return materias
cr: acc.cr + (m.creditos || 0), hd: acc.hd + (m.hd || 0), hi: acc.hi + (m.hi || 0) .filter((m) => m.ciclo === ciclo)
}), { cr: 0, hd: 0, hi: 0 }); .reduce(
}; (acc, m) => ({
cr: acc.cr + (m.creditos || 0),
hd: acc.hd + (m.hd || 0),
hi: acc.hi + (m.hi || 0),
}),
{ cr: 0, hd: 0, hi: 0 },
)
}
const getSubtotalLinea = (lineaId: string) => { const getSubtotalLinea = (lineaId: string) => {
return materias.filter(m => m.lineaCurricularId === lineaId && m.ciclo !== null).reduce((acc, m) => ({ return materias
cr: acc.cr + (m.creditos || 0), hd: acc.hd + (m.hd || 0), hi: acc.hi + (m.hi || 0) .filter((m) => m.lineaCurricularId === lineaId && m.ciclo !== null)
}), { cr: 0, hd: 0, hi: 0 }); .reduce(
}; (acc, m) => ({
cr: acc.cr + (m.creditos || 0),
hd: acc.hd + (m.hd || 0),
hi: acc.hi + (m.hi || 0),
}),
{ cr: 0, hd: 0, hi: 0 },
)
}
// --- Handlers Drag & Drop --- const handleDragStart = (e: React.DragEvent, id: string) => {
const handleDragStart = (e: React.DragEvent, id: string) => { setDraggedMateria(id); e.dataTransfer.effectAllowed = 'move'; }; setDraggedMateria(id)
const handleDragOver = (e: React.DragEvent) => e.preventDefault(); e.dataTransfer.effectAllowed = 'move'
const handleDrop = (e: React.DragEvent, ciclo: number | null, lineaId: string | null) => { }
e.preventDefault(); const handleDragOver = (e: React.DragEvent) => e.preventDefault()
const handleDrop = (
e: React.DragEvent,
ciclo: number | null,
lineaId: string | null,
) => {
e.preventDefault()
if (draggedMateria) { if (draggedMateria) {
setMaterias(prev => prev.map(m => m.id === draggedMateria ? { ...m, ciclo, lineaCurricularId: lineaId } : m)); setMaterias((prev) =>
setDraggedMateria(null); prev.map((m) =>
m.id === draggedMateria
? { ...m, ciclo, lineaCurricularId: lineaId }
: m,
),
)
setDraggedMateria(null)
}
} }
};
// --- Estadísticas Generales --- const stats = useMemo(
const stats = materias.reduce((acc, m) => { () =>
materias.reduce(
(acc, m) => {
if (m.ciclo !== null) { if (m.ciclo !== null) {
acc.cr += m.creditos || 0; acc.hd += m.hd || 0; acc.hi += m.hi || 0; acc.cr += m.creditos || 0
acc.hd += m.hd || 0
acc.hi += m.hi || 0
} }
return acc; return acc
}, { cr: 0, hd: 0, hi: 0 }); },
{ cr: 0, hd: 0, hi: 0 },
),
[materias],
)
if (loadingAsig || loadingLineas)
return <div className="p-10 text-center">Cargando mapa curricular...</div>
return ( return (
<div className="container mx-auto px-2 py-6"> <div className="container mx-auto px-2 py-6">
{/* Header */} {/* Header */}
<div className="flex justify-between items-center mb-6"> <div className="mb-6 flex items-center justify-between">
<div> <div>
<h2 className="text-xl font-bold">Mapa Curricular</h2> <h2 className="text-xl font-bold">Mapa Curricular</h2>
<p className="text-sm text-slate-500">Organiza las materias por línea curricular y ciclo</p> <p className="text-sm text-slate-500">
Organiza las materias de la petición por línea y ciclo
</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{materias.filter(m => !m.ciclo).length > 0 && ( {materias.filter((m) => !m.ciclo).length > 0 && (
<Badge className="bg-amber-50 text-amber-600 border-amber-100 hover:bg-amber-50"> <Badge className="border-amber-100 bg-amber-50 text-amber-600 hover:bg-amber-50">
<AlertTriangle size={14} className="mr-1" /> {materias.filter(m => !m.ciclo).length} materias sin asignar <AlertTriangle size={14} className="mr-1" />{' '}
{materias.filter((m) => !m.ciclo).length} sin asignar
</Badge> </Badge>
)} )}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button className="bg-teal-700 hover:bg-teal-800 text-white"> <Button className="bg-teal-700 text-white hover:bg-teal-800">
<Plus size={16} className="mr-2" /> Agregar <ChevronDown size={14} className="ml-2" /> <Plus size={16} className="mr-2" /> Agregar{' '}
<ChevronDown size={14} className="ml-2" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => agregarLinea("Nueva Línea")}>Nueva Línea Curricular</DropdownMenuItem> <DropdownMenuItem onClick={() => agregarLinea('Nueva Línea')}>
<DropdownMenuItem onClick={() => agregarLinea("Área Común")}>Agregar Área Común</DropdownMenuItem> Nueva Línea Curricular
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
</div> </div>
{/* Barra Totales */} {/* Barra Totales */}
<div className="bg-slate-50/80 border border-slate-200 rounded-xl p-4 mb-8 flex gap-10"> <div className="mb-8 flex gap-10 rounded-xl border border-slate-200 bg-slate-50/80 p-4">
<StatItem label="Total Créditos" value={stats.cr} total={320} /> <StatItem label="Total Créditos" value={stats.cr} total={320} />
<StatItem label="Total HD" value={stats.hd} /> <StatItem label="Total HD" value={stats.hd} />
<StatItem label="Total HI" value={stats.hi} /> <StatItem label="Total HI" value={stats.hi} />
<StatItem label="Total Horas" value={stats.hd + stats.hi} /> <StatItem label="Total Horas" value={stats.hd + stats.hi} />
</div> </div>
{/* Grid Principal */}
<div className="overflow-x-auto pb-6"> <div className="overflow-x-auto pb-6">
<div className="min-w-[1500px]"> <div className="min-w-[1500px]">
{/* Header Ciclos */} <div
<div className="grid gap-3 mb-4" style={{ gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px` }}> className="mb-4 grid gap-3"
<div className="text-xs font-bold text-slate-400 self-end px-2">LÍNEA CURRICULAR</div> style={{
{ciclosArray.map(n => <div key={n} className="bg-slate-100 rounded-lg p-2 text-center text-sm font-bold text-slate-600">Ciclo {n}</div>)} gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px`,
<div className="text-xs font-bold text-slate-400 self-end text-center">SUBTOTAL</div> }}
>
<div className="self-end px-2 text-xs font-bold text-slate-400">
LÍNEA CURRICULAR
</div>
{ciclosArray.map((n) => (
<div
key={n}
className="rounded-lg bg-slate-100 p-2 text-center text-sm font-bold text-slate-600"
>
Ciclo {n}
</div>
))}
<div className="self-end text-center text-xs font-bold text-slate-400">
SUBTOTAL
</div>
</div> </div>
{/* Filas por Línea */}
{lineas.map((linea, idx) => { {lineas.map((linea, idx) => {
const sub = getSubtotalLinea(linea.id); const sub = getSubtotalLinea(linea.id)
return ( return (
<div key={linea.id} className="grid gap-3 mb-3" style={{ gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px` }}> <div
<div className={`p-4 rounded-xl border-l-4 flex justify-between items-center ${lineColors[idx % lineColors.length]}`}> key={linea.id}
className="mb-3 grid gap-3"
style={{
gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px`,
}}
>
<div
className={`flex items-center justify-between rounded-xl border-l-4 p-4 ${lineColors[idx % lineColors.length]}`}
>
<span className="text-xs font-bold">{linea.nombre}</span> <span className="text-xs font-bold">{linea.nombre}</span>
<Trash2 size={14} className="text-slate-400 hover:text-red-500 cursor-pointer" onClick={() => borrarLinea(linea.id)} /> <Trash2
size={14}
className="cursor-pointer text-slate-400 hover:text-red-500"
onClick={() => borrarLinea(linea.id)}
/>
</div> </div>
{ciclosArray.map(ciclo => ( {ciclosArray.map((ciclo) => (
<div <div
key={ciclo} key={ciclo}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, ciclo, linea.id)} onDrop={(e) => handleDrop(e, ciclo, linea.id)}
className="min-h-[140px] p-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 space-y-2" className="min-h-[140px] space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 p-2"
>
{materias.filter(m => m.ciclo === ciclo && m.lineaCurricularId === linea.id).map(m => (
<MateriaCardItem key={m.id} materia={m} isDragging={draggedMateria === m.id} onDragStart={handleDragStart} onClick={() => { setSelectedMateria(m); setIsEditModalOpen(true); }} />
))}
</div>
))}
<div className="p-4 bg-slate-50 rounded-xl flex flex-col justify-center text-[10px] text-slate-500 font-medium border border-slate-100">
<div>Cr: {sub.cr}</div><div>HD: {sub.hd}</div><div>HI: {sub.hi}</div>
</div>
</div>
)
})}
{/* Fila Totales Ciclo */}
<div className="grid gap-3 mt-6 border-t pt-4" style={{ gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px` }}>
<div className="p-2 font-bold text-slate-600">Totales por Ciclo</div>
{ciclosArray.map(ciclo => {
const t = getTotalesCiclo(ciclo);
return (
<div key={ciclo} className="text-[10px] text-center p-2 bg-slate-50 rounded-lg">
<div className="font-bold text-slate-700">Cr: {t.cr}</div>
<div>HD: {t.hd} HI: {t.hi}</div>
</div>
)
})}
<div className="bg-teal-50 rounded-lg p-2 text-center text-teal-800 font-bold text-xs flex flex-col justify-center">
<div>{stats.cr} Cr</div><div>{stats.hd + stats.hi} Hrs</div>
</div>
</div>
</div>
</div>
{/* Modal de Edición */}
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader><DialogTitle>Editar Materia</DialogTitle></DialogHeader>
{selectedMateria && (
<div className="grid grid-cols-2 gap-4 py-4">
<div className="space-y-2"><label className="text-xs font-bold uppercase">Clave</label><Input defaultValue={selectedMateria.clave} /></div>
<div className="space-y-2"><label className="text-xs font-bold uppercase">Nombre</label><Input defaultValue={selectedMateria.nombre} /></div>
<div className="space-y-2"><label className="text-xs font-bold uppercase">Créditos</label><Input type="number" defaultValue={selectedMateria.creditos} /></div>
<div className="flex gap-2">
<div className="space-y-2"><label className="text-xs font-bold uppercase">HD</label><Input type="number" defaultValue={selectedMateria.hd} /></div>
<div className="space-y-2"><label className="text-xs font-bold uppercase">HI</label><Input type="number" defaultValue={selectedMateria.hi} /></div>
</div>
</div>
)}
<div className="flex justify-end gap-3 mt-4">
<Button variant="outline" onClick={() => setIsEditModalOpen(false)}>Cancelar</Button>
<Button className="bg-teal-700 text-white">Guardar Cambios</Button>
</div>
</DialogContent>
</Dialog>
{/* 4. Materias Pendientes (Sin Asignar) */}
{materias.filter(m => m.ciclo === null).length > 0 && (
<div className="mt-10 p-6 bg-slate-50 rounded-2xl border border-slate-200 shadow-sm animate-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center gap-2 mb-4 text-amber-600">
<AlertTriangle size={20} />
<h3 className="font-bold text-sm uppercase tracking-tight">
Materias pendientes de asignar ({materias.filter(m => m.ciclo === null).length})
</h3>
</div>
<div
className={`flex flex-wrap gap-4 min-h-[100px] p-4 rounded-xl border-2 border-dashed transition-all ${
draggedMateria ? 'border-amber-200 bg-amber-50/50' : 'border-slate-200 bg-white/50'
}`}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, null, null)} // null devuelve la materia al estado "sin asignar"
> >
{materias {materias
.filter(m => m.ciclo === null) .filter(
.map(m => ( (m) =>
m.ciclo === ciclo && m.lineaCurricularId === linea.id,
)
.map((m) => (
<MateriaCardItem
key={m.id}
materia={m}
isDragging={draggedMateria === m.id}
onDragStart={handleDragStart}
onClick={() => {
setSelectedMateria(m)
setIsEditModalOpen(true)
}}
/>
))}
</div>
))}
<div className="flex flex-col justify-center rounded-xl border border-slate-100 bg-slate-50 p-4 text-[10px] font-medium text-slate-500">
<div>Cr: {sub.cr}</div>
<div>HD: {sub.hd}</div>
<div>HI: {sub.hi}</div>
</div>
</div>
)
})}
<div
className="mt-6 grid gap-3 border-t pt-4"
style={{
gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px`,
}}
>
<div className="p-2 font-bold text-slate-600">
Totales por Ciclo
</div>
{ciclosArray.map((ciclo) => {
const t = getTotalesCiclo(ciclo)
return (
<div
key={ciclo}
className="rounded-lg bg-slate-50 p-2 text-center text-[10px]"
>
<div className="font-bold text-slate-700">Cr: {t.cr}</div>
<div>
HD: {t.hd} HI: {t.hi}
</div>
</div>
)
})}
<div className="flex flex-col justify-center rounded-lg bg-teal-50 p-2 text-center text-xs font-bold text-teal-800">
<div>{stats.cr} Cr</div>
<div>{stats.hd + stats.hi} Hrs</div>
</div>
</div>
</div>
</div>
{/* Materias Sin Asignar */}
{materias.filter((m) => m.ciclo === null).length > 0 && (
<div className="mt-10 rounded-2xl border border-slate-200 bg-slate-50 p-6">
<div className="mb-4 flex items-center gap-2 text-amber-600">
<AlertTriangle size={20} />
<h3 className="text-sm font-bold uppercase">
Materias pendientes (
{materias.filter((m) => m.ciclo === null).length})
</h3>
</div>
<div
className="flex min-h-[100px] flex-wrap gap-4 rounded-xl border-2 border-dashed bg-white/50 p-4"
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, null, null)}
>
{materias
.filter((m) => m.ciclo === null)
.map((m) => (
<div key={m.id} className="w-[200px]"> <div key={m.id} className="w-[200px]">
<MateriaCardItem <MateriaCardItem
materia={m} materia={m}
isDragging={draggedMateria === m.id} isDragging={draggedMateria === m.id}
onDragStart={handleDragStart} onDragStart={handleDragStart}
onClick={() => { setSelectedMateria(m); setIsEditModalOpen(true); }} onClick={() => {
setSelectedMateria(m)
setIsEditModalOpen(true)
}}
/> />
</div> </div>
))} ))}
</div> </div>
<p className="mt-3 text-[11px] text-slate-400 italic text-center">
Arrastra las materias desde aquí hacia cualquier ciclo y línea del mapa curricular.
</p>
</div> </div>
)} )}
{/* Modal de Edición */}
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Editar Materia</DialogTitle>
</DialogHeader>
{selectedMateria && (
<div className="grid grid-cols-2 gap-4 py-4">
<div className="space-y-2">
<label className="text-xs font-bold uppercase">Clave</label>
<Input defaultValue={selectedMateria.clave} />
</div>
<div className="space-y-2">
<label className="text-xs font-bold uppercase">Nombre</label>
<Input defaultValue={selectedMateria.nombre} />
</div>
<div className="space-y-2">
<label className="text-xs font-bold uppercase">Créditos</label>
<Input type="number" defaultValue={selectedMateria.creditos} />
</div>
<div className="flex gap-2">
<div className="space-y-2">
<label className="text-xs font-bold uppercase">HD</label>
<Input type="number" defaultValue={selectedMateria.hd} />
</div>
<div className="space-y-2">
<label className="text-xs font-bold uppercase">HI</label>
<Input type="number" defaultValue={selectedMateria.hi} />
</div>
</div>
</div>
)}
<div className="mt-4 flex justify-end gap-3">
<Button variant="outline" onClick={() => setIsEditModalOpen(false)}>
Cancelar
</Button>
<Button className="bg-teal-700 text-white">Guardar Cambios</Button>
</div>
</DialogContent>
</Dialog>
</div> </div>
) )
} }

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react' import { useState, useMemo } from 'react'
import type { Materia, LineaCurricular } from '@/types/plan'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
@@ -12,204 +12,263 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
Plus,
Copy,
Search,
Filter,
ChevronRight,
BookOpen,
Loader2,
} from 'lucide-react'
import { usePlanAsignaturas, usePlanLineas } from '@/data'
export const Route = createFileRoute('/planes/$planId/_detalle/materias')({ // --- Configuración de Estilos ---
component: Materias, const statusConfig: Record<string, { label: string; className: string }> = {
}) borrador: { label: 'Borrador', className: 'bg-slate-100 text-slate-600' },
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
type Materia = { aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-700' },
id: string;
clave: string
nombre: string
creditos: number
hd: number
hi: number
ciclo: string
linea: string
tipo: 'Obligatoria' | 'Optativa' | 'Troncal'
estado: 'Aprobada' | 'Revisada' | 'Borrador'
} }
const MATERIAS: Materia[] = [ const tipoConfig: Record<string, { label: string; className: string }> = {
{ obligatoria: { label: 'Obligatoria', className: 'bg-blue-100 text-blue-700' },
id: "1", optativa: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
clave: 'MAT101', troncal: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
nombre: 'Cálculo Diferencial', }
creditos: 8,
hd: 4,
hi: 4,
ciclo: 'Ciclo 1',
linea: 'Formación Básica',
tipo: 'Obligatoria',
estado: 'Aprobada',
},
{
id: "2",
clave: 'FIS101',
nombre: 'Física Mecánica',
creditos: 6,
hd: 3,
hi: 3,
ciclo: 'Ciclo 1',
linea: 'Formación Básica',
tipo: 'Obligatoria',
estado: 'Aprobada',
},
{
id: "3",
clave: 'PRO101',
nombre: 'Fundamentos de Programación',
creditos: 8,
hd: 4,
hi: 4,
ciclo: 'Ciclo 1',
linea: 'Ciencias de la Computación',
tipo: 'Obligatoria',
estado: 'Revisada',
},
{
id: "4",
clave: 'EST101',
nombre: 'Estructura de Datos',
creditos: 6,
hd: 3,
hi: 3,
ciclo: 'Ciclo 2',
linea: 'Ciencias de la Computación',
tipo: 'Obligatoria',
estado: 'Borrador',
},
]
function Materias() { // --- Mapeadores de API ---
const [search, setSearch] = useState('') const mapAsignaturas = (asigApi: any[] = []): Materia[] => {
const [filtro, setFiltro] = useState<'Todas' | Materia['tipo']>('Todas') return asigApi.map((asig) => ({
id: asig.id,
clave: asig.codigo,
nombre: asig.nombre,
creditos: asig.creditos ?? 0,
ciclo: asig.numero_ciclo ?? null,
lineaCurricularId: asig.linea_plan_id ?? null,
tipo:
asig.tipo?.toLowerCase() === 'obligatoria' ? 'obligatoria' : 'optativa',
estado: 'borrador', // O el campo que venga de tu API
hd: Math.floor((asig.horas_semana ?? 0) / 2),
hi: Math.ceil((asig.horas_semana ?? 0) / 2),
}))
}
const materiasFiltradas = MATERIAS.filter((m) => { export const Route = createFileRoute('/planes/$planId/_detalle/materias')({
const okFiltro = filtro === 'Todas' || m.tipo === filtro component: MateriasPage,
const okSearch =
m.nombre.toLowerCase().includes(search.toLowerCase()) ||
m.clave.toLowerCase().includes(search.toLowerCase())
return okFiltro && okSearch
}) })
const totalCreditos = materiasFiltradas.reduce( function MateriasPage() {
(acc, m) => acc + m.creditos, const { planId } = Route.useParams()
0
// 1. Fetch de datos reales
const { data: asignaturasApi, isLoading: loadingAsig } = usePlanAsignaturas(
'0e0aea4d-b8b4-4e75-8279-6224c3ac769f',
)
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(
'0e0aea4d-b8b4-4e75-8279-6224c3ac769f',
) )
// 2. Estados de filtrado
const [searchTerm, setSearchTerm] = useState('')
const [filterTipo, setFilterTipo] = useState<string>('all')
const [filterEstado, setFilterEstado] = useState<string>('all')
const [filterLinea, setFilterLinea] = useState<string>('all')
// 3. Procesamiento de datos
const materias = useMemo(
() => mapAsignaturas(asignaturasApi),
[asignaturasApi],
)
const lineas = useMemo(() => lineasApi || [], [lineasApi])
const filteredMaterias = materias.filter((m) => {
const matchesSearch =
m.nombre.toLowerCase().includes(searchTerm.toLowerCase()) ||
m.clave.toLowerCase().includes(searchTerm.toLowerCase())
const matchesTipo = filterTipo === 'all' || m.tipo === filterTipo
const matchesEstado = filterEstado === 'all' || m.estado === filterEstado
const matchesLinea =
filterLinea === 'all' || m.lineaCurricularId === filterLinea
return matchesSearch && matchesTipo && matchesEstado && matchesLinea
})
const getLineaNombre = (lineaId: string | null) => {
if (!lineaId) return 'Sin asignar'
return lineas.find((l: any) => l.id === lineaId)?.nombre || 'Desconocida'
}
if (loadingAsig || loadingLineas) {
return ( return (
<div className="space-y-6"> <div className="flex h-64 items-center justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
)
}
return (
<div className="container mx-auto space-y-6 px-6 py-6">
{/* Header */} {/* Header */}
<div className="flex justify-between items-start"> <div className="flex flex-wrap items-center justify-between gap-4">
<div> <div>
<h2 className="text-xl font-semibold">Materias del Plan</h2> <h2 className="text-foreground text-xl font-bold">
<p className="text-sm text-muted-foreground"> Materias del Plan
{materiasFiltradas.length} materias · {totalCreditos} créditos </h2>
<p className="text-muted-foreground mt-1 text-sm">
{materias.length} materias en total {filteredMaterias.length}{' '}
filtradas
</p> </p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline">Clonar de mi Facultad</Button> <Button variant="outline" size="sm">
<Button variant="outline">Clonar de otra Facultad</Button> <Copy className="mr-2 h-4 w-4" /> Clonar
</Button>
<Button className="bg-emerald-700 hover:bg-emerald-800"> <Button className="bg-emerald-700 hover:bg-emerald-800">
+ Nueva Materia <Plus className="mr-2 h-4 w-4" /> Nueva Materia
</Button> </Button>
</div> </div>
</div> </div>
{/* Buscador y filtros */} {/* Barra de Filtros Avanzada */}
<div className="flex items-center gap-4"> <div className="flex flex-wrap items-center gap-3 rounded-xl border bg-slate-50 p-4">
<div className="relative min-w-[240px] flex-1">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input <Input
placeholder="Buscar por nombre o clave..." placeholder="Buscar por nombre o clave..."
value={search} value={searchTerm}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-64" className="bg-white pl-9"
/> />
</div>
<div className="flex gap-2"> <div className="flex flex-wrap items-center gap-2">
{['Todas', 'Obligatoria', 'Optativa', 'Troncal'].map((t) => ( <Filter className="text-muted-foreground mr-1 h-4 w-4" />
<Button
key={t} <Select value={filterTipo} onValueChange={setFilterTipo}>
variant={filtro === t ? 'secondary' : 'ghost'} <SelectTrigger className="w-[140px] bg-white">
size="sm" <SelectValue placeholder="Tipo" />
onClick={() => setFiltro(t as any)} </SelectTrigger>
> <SelectContent>
{t === 'Obligatoria' ? 'Obligatorias' : t} <SelectItem value="all">Todos los tipos</SelectItem>
</Button> <SelectItem value="obligatoria">Obligatoria</SelectItem>
<SelectItem value="optativa">Optativa</SelectItem>
</SelectContent>
</Select>
<Select value={filterEstado} onValueChange={setFilterEstado}>
<SelectTrigger className="w-[140px] bg-white">
<SelectValue placeholder="Estado" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos los estados</SelectItem>
<SelectItem value="borrador">Borrador</SelectItem>
<SelectItem value="revisada">Revisada</SelectItem>
<SelectItem value="aprobada">Aprobada</SelectItem>
</SelectContent>
</Select>
<Select value={filterLinea} onValueChange={setFilterLinea}>
<SelectTrigger className="w-[180px] bg-white">
<SelectValue placeholder="Línea" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todas las líneas</SelectItem>
{lineas.map((linea: any) => (
<SelectItem key={linea.id} value={linea.id}>
{linea.nombre}
</SelectItem>
))} ))}
</SelectContent>
</Select>
</div> </div>
</div> </div>
{/* Tabla */} {/* Tabla Pro */}
<div className="rounded-md border"> <div className="overflow-hidden rounded-xl border bg-white shadow-sm">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow className="bg-slate-50/50">
<TableHead>Clave</TableHead> <TableHead className="w-[120px]">Clave</TableHead>
<TableHead>Nombre</TableHead> <TableHead>Nombre</TableHead>
<TableHead className="text-center">Créditos</TableHead> <TableHead className="text-center">Créditos</TableHead>
<TableHead className="text-center">HD</TableHead> <TableHead className="text-center">Ciclo</TableHead>
<TableHead className="text-center">HI</TableHead> <TableHead>Línea Curricular</TableHead>
<TableHead>Ciclo</TableHead>
<TableHead>Línea</TableHead>
<TableHead>Tipo</TableHead> <TableHead>Tipo</TableHead>
<TableHead>Estado</TableHead> <TableHead>Estado</TableHead>
<TableHead className="text-center">Acciones</TableHead> <TableHead className="w-[50px]"></TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{materiasFiltradas.map((m) => ( {filteredMaterias.length === 0 ? (
<TableRow key={m.clave}> <TableRow>
<TableCell className="text-muted-foreground"> <TableCell colSpan={8} className="h-40 text-center">
{m.clave} <div className="text-muted-foreground flex flex-col items-center justify-center">
<BookOpen className="mb-2 h-10 w-10 opacity-20" />
<p className="font-medium">No se encontraron materias</p>
<p className="text-xs">
Intenta cambiar los filtros de búsqueda
</p>
</div>
</TableCell> </TableCell>
<TableCell className="font-medium">{m.nombre}</TableCell> </TableRow>
<TableCell className="text-center">{m.creditos}</TableCell> ) : (
<TableCell className="text-center">{m.hd}</TableCell> filteredMaterias.map((materia) => (
<TableCell className="text-center">{m.hi}</TableCell> <TableRow
<TableCell>{m.ciclo}</TableCell> key={materia.id}
<TableCell>{m.linea}</TableCell> className="group cursor-pointer transition-colors hover:bg-slate-50/80"
>
<TableCell> <TableCell className="font-mono text-xs font-bold text-slate-400">
<Badge variant="secondary">{m.tipo}</Badge> {materia.clave}
</TableCell>
<TableCell className="font-semibold text-slate-700">
{materia.nombre}
</TableCell>
<TableCell className="text-center font-medium">
{materia.creditos}
</TableCell>
<TableCell className="text-center">
{materia.ciclo ? (
<Badge variant="outline" className="font-normal">
Ciclo {materia.ciclo}
</Badge>
) : (
<span className="text-slate-300"></span>
)}
</TableCell>
<TableCell className="text-sm text-slate-600">
{getLineaNombre(materia.lineaCurricularId)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge <Badge
variant="secondary" variant="outline"
className={ className={`capitalize shadow-sm ${tipoConfig[materia.tipo]?.className}`}
m.estado === 'Aprobada'
? 'bg-emerald-100 text-emerald-700'
: m.estado === 'Revisada'
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-500'
}
> >
{m.estado} {tipoConfig[materia.tipo]?.label}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell>
<TableCell className="text-center"> <Badge
<Button variant="ghost" size="icon"> variant="outline"
className={`capitalize shadow-sm ${statusConfig[materia.estado]?.className}`}
</Button>
</TableCell>
</TableRow>
))}
{materiasFiltradas.length === 0 && (
<TableRow>
<TableCell
colSpan={10}
className="text-center py-6 text-muted-foreground"
> >
No se encontraron materias {statusConfig[materia.estado]?.label}
</Badge>
</TableCell>
<TableCell>
<div className="opacity-0 transition-opacity group-hover:opacity-100">
<ChevronRight className="h-5 w-5 text-slate-400" />
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
))
)} )}
</TableBody> </TableBody>
</Table> </Table>

Binary file not shown.