1 Commits

Author SHA1 Message Date
da218b1f92 se arregló el estilo visual y comportamiento del grid del mapa curricular
fix #108: ahora se utiliza un único grid para todo el mapa curricular. de esta manera el espaciado se mantiene consistente
2026-02-13 14:12:32 -06:00
56 changed files with 2282 additions and 17004 deletions

View File

@@ -1,37 +0,0 @@
name: Deploy to Azure Static Web Apps
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Build
env:
VITE_SUPABASE_URL: ${{ vars.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ vars.VITE_SUPABASE_ANON_KEY }}
run: bunx --bun vite build
# No hace falta instalar el CLI globalmente, usamos bunx
- name: Deploy to Azure Static Web Apps
env:
AZURE_SWA_DEPLOYMENT_TOKEN: ${{ secrets.AZURE_SWA_DEPLOYMENT_TOKEN }}
run: |
bunx @azure/static-web-apps-cli deploy ./dist \
--env production \
--deployment-token "$AZURE_SWA_DEPLOYMENT_TOKEN"

View File

@@ -1 +0,0 @@
Ignora los problemas de imports de eslint

View File

@@ -4,7 +4,6 @@
"": { "": {
"name": "acad-ia-2", "name": "acad-ia-2",
"dependencies": { "dependencies": {
"@dnd-kit/react": "^0.3.2",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
@@ -31,7 +30,6 @@
"@tanstack/router-plugin": "^1.132.0", "@tanstack/router-plugin": "^1.132.0",
"@types/canvas-confetti": "^1.9.0", "@types/canvas-confetti": "^1.9.0",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
"citeproc": "^2.4.63",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
@@ -139,18 +137,6 @@
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
"@dnd-kit/abstract": ["@dnd-kit/abstract@0.3.2", "", { "dependencies": { "@dnd-kit/geometry": "^0.3.2", "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-uvPVK+SZYD6Viddn9M0K0JQdXknuVSxA/EbMlFRanve3P/XTc18oLa5zGftKSGjfQGmuzkZ34E26DSbly1zi3Q=="],
"@dnd-kit/collision": ["@dnd-kit/collision@0.3.2", "", { "dependencies": { "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/geometry": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-pNmNSLCI8S9fNQ7QJ3fBCDjiT0sqBhUFcKgmyYaGvGCAU+kq0AP8OWlh0JSisc9k5mFyxmRpmFQcnJpILz/RPA=="],
"@dnd-kit/dom": ["@dnd-kit/dom@0.3.2", "", { "dependencies": { "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/collision": "^0.3.2", "@dnd-kit/geometry": "^0.3.2", "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-cIUAVgt2szQyz6JRy7I+0r+xeyOAGH21Y15hb5bIyHoDEaZBvIDH+OOlD9eoLjCbsxDLN9WloU2CBi3OE6LYDg=="],
"@dnd-kit/geometry": ["@dnd-kit/geometry@0.3.2", "", { "dependencies": { "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-3UBPuIS7E3oGiHxOE8h810QA+0pnrnCtGxl4Os1z3yy5YkC/BEYGY+TxWPTQaY1/OMV7GCX7ZNMlama2QN3n3w=="],
"@dnd-kit/react": ["@dnd-kit/react@0.3.2", "", { "dependencies": { "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/dom": "^0.3.2", "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-1Opg1xw6I75Z95c+rF2NJa0pdGb8rLAENtuopKtJ1J0PudWlz+P6yL137xy/6DV43uaRmNGtsdbMbR0yRYJ72g=="],
"@dnd-kit/state": ["@dnd-kit/state@0.3.2", "", { "dependencies": { "@preact/signals-core": "^1.10.0", "tslib": "^2.6.2" } }, "sha512-dLUIkoYrIJhGXfF2wGLTfb46vUokEsO/OoE21TSfmahYrx7ysTmnwbePsznFaHlwgZhQEh6AlLvthLCeY21b1A=="],
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
@@ -263,8 +249,6 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@preact/signals-core": ["@preact/signals-core@1.14.0", "", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
@@ -751,8 +735,6 @@
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"citeproc": ["citeproc@2.4.63", "", {}, "sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],

View File

@@ -17,7 +17,6 @@
"ci:verify": "prettier --check . && eslint . && tsc --noEmit" "ci:verify": "prettier --check . && eslint . && tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/react": "^0.3.2",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
@@ -44,7 +43,6 @@
"@tanstack/router-plugin": "^1.132.0", "@tanstack/router-plugin": "^1.132.0",
"@types/canvas-confetti": "^1.9.0", "@types/canvas-confetti": "^1.9.0",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
"citeproc": "^2.4.63",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",

View File

@@ -1,757 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<locale xmlns="http://purl.org/net/xbiblio/csl" version="1.0" xml:lang="es-MX">
<info>
<translator>
<name>Juan Ignacio Flores Salgado</name>
<uri>https://www.mendeley.com/profiles/juan-ignacio-flores-salgado/</uri>
</translator>
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
<updated>2025-10-16T03:24:00+00:00</updated>
</info>
<style-options punctuation-in-quote="false"/>
<date form="text">
<date-part name="day" prefix="el " suffix=" de "/>
<date-part name="month" suffix=" de "/>
<date-part name="year"/>
</date>
<date form="numeric">
<date-part name="day" form="numeric-leading-zeros" suffix="/"/>
<date-part name="month" form="numeric-leading-zeros" suffix="/"/>
<date-part name="year"/>
</date>
<terms>
<!-- LONG GENERAL TERMS -->
<term name="accessed">consultado</term>
<term name="advance-online-publication">advance online publication</term>
<term name="album">album</term>
<term name="and">y</term>
<term name="and others">et&#160;al.</term>
<term name="anonymous">anónimo</term>
<term name="at">en</term>
<term name="audio-recording">audio recording</term>
<term name="available at">disponible en</term>
<term name="by">de</term>
<term name="circa">circa</term>
<term name="cited">citado</term>
<term name="et-al">et&#160;al.</term>
<term name="film">film</term>
<term name="forthcoming">en preparación</term>
<term name="from">a partir de</term>
<term name="henceforth">henceforth</term>
<term name="ibid">ibid.</term>
<term name="in">en</term>
<term name="in press">en imprenta</term>
<term name="internet">internet</term>
<term name="letter">carta</term>
<term name="loc-cit">loc. cit.</term> <!-- like ibid., the abbreviated form is the regular form -->
<term name="no date">sin fecha</term>
<term name="no-place">no place</term>
<term name="no-publisher">no publisher</term> <!-- sine nomine -->
<term name="on">on</term>
<term name="online">en línea</term>
<term name="op-cit">op. cit.</term> <!-- like ibid., the abbreviated form is the regular form -->
<term name="original-work-published">obra original publicada en</term>
<term name="personal-communication">comunicación personal</term>
<term name="podcast">podcast</term>
<term name="podcast-episode">podcast episode</term>
<term name="preprint">preprint</term>
<term name="presented at">presentado en</term>
<term name="radio-broadcast">radio broadcast</term>
<term name="radio-series">radio series</term>
<term name="radio-series-episode">radio series episode</term>
<term name="reference">
<single>referencia</single>
<multiple>referencias</multiple>
</term>
<term name="retrieved">recuperado</term>
<term name="review-of">review of</term>
<term name="scale">escala</term>
<term name="special-issue">special issue</term>
<term name="special-section">special section</term>
<term name="television-broadcast">television broadcast</term>
<term name="television-series">television series</term>
<term name="television-series-episode">television series episode</term>
<term name="video">video</term>
<term name="working-paper">working paper</term>
<!-- SHORT GENERAL TERMS -->
<term name="anonymous" form="short">anón.</term>
<term name="circa" form="short">c.</term>
<term name="no date" form="short">s/f</term>
<term name="no-place" form="short">n.p.</term>
<term name="no-publisher" form="short">n.p.</term>
<term name="reference" form="short">
<single>ref.</single>
<multiple>refs.</multiple>
</term>
<term name="review-of" form="short">rev. of</term>
<!-- SYMBOLIC GENERAL FORMS -->
<!-- LONG ITEM TYPE FORMS -->
<term name="article">preprint</term>
<term name="article-journal">journal article</term>
<term name="article-magazine">magazine article</term>
<term name="article-newspaper">newspaper article</term>
<term name="bill">bill</term>
<!-- book is in the list of locator terms -->
<term name="broadcast">broadcast</term>
<!-- chapter is in the list of locator terms -->
<term name="classic">classic</term>
<term name="collection">collection</term>
<term name="dataset">dataset</term>
<term name="document">document</term>
<term name="entry">entry</term>
<term name="entry-dictionary">dictionary entry</term>
<term name="entry-encyclopedia">encyclopedia entry</term>
<term name="event">event</term>
<!-- figure is in the list of locator terms -->
<term name="graphic">graphic</term>
<term name="hearing">hearing</term>
<term name="interview">entrevista</term>
<term name="legal_case">legal case</term>
<term name="legislation">legislation</term>
<term name="manuscript">manuscript</term>
<term name="map">map</term>
<term name="motion_picture">video recording</term>
<term name="musical_score">musical score</term>
<term name="pamphlet">pamphlet</term>
<term name="paper-conference">conference paper</term>
<term name="patent">patent</term>
<term name="performance">performance</term>
<term name="periodical">periodical</term>
<term name="personal_communication">comunicación personal</term>
<term name="post">post</term>
<term name="post-weblog">blog post</term>
<term name="regulation">regulation</term>
<term name="report">report</term>
<term name="review">review</term>
<term name="review-book">book review</term>
<term name="software">software</term>
<term name="song">audio recording</term>
<term name="speech">presentation</term>
<term name="standard">standard</term>
<term name="thesis">thesis</term>
<term name="treaty">treaty</term>
<term name="webpage">webpage</term>
<!-- SHORT ITEM TYPE FORMS -->
<term name="article-journal" form="short">journal art.</term>
<term name="article-magazine" form="short">mag. art.</term>
<term name="article-newspaper" form="short">newspaper art.</term>
<!-- book is in the list of locator terms -->
<!-- chapter is in the list of locator terms -->
<term name="document" form="short">doc.</term>
<!-- figure is in the list of locator terms -->
<term name="graphic" form="short">graph.</term>
<term name="interview" form="short">interv.</term>
<term name="manuscript" form="short">MS</term>
<term name="motion_picture" form="short">video rec.</term>
<term name="report" form="short">rep.</term>
<term name="review" form="short">rev.</term>
<term name="review-book" form="short">bk. rev.</term>
<term name="song" form="short">audio rec.</term>
<!-- LONG VERB ITEM TYPE FORMS -->
<!-- Only where applicable -->
<term name="hearing" form="verb">testimony of</term>
<term name="review" form="verb">review of</term>
<term name="review-book" form="verb">review of the book</term>
<!-- SHORT VERB ITEM TYPE FORMS -->
<!-- HISTORICAL ERA TERMS -->
<term name="ad">d.&#160;C.</term>
<term name="bc">a.&#160;C.</term>
<term name="bce">BCE</term>
<term name="ce">CE</term>
<!-- PUNCTUATION -->
<term name="open-quote"></term>
<term name="close-quote"></term>
<term name="open-inner-quote"></term>
<term name="close-inner-quote"></term>
<term name="page-range-delimiter"></term>
<term name="colon">:</term>
<term name="comma">,</term>
<term name="semicolon">;</term>
<!-- ORDINALS -->
<term name="ordinal">a</term>
<term name="ordinal-01" gender-form="feminine" match="whole-number">a</term>
<term name="ordinal-01" gender-form="masculine" match="whole-number">o</term>
<!-- LONG ORDINALS -->
<term name="long-ordinal-01">primera</term>
<term name="long-ordinal-02">segunda</term>
<term name="long-ordinal-03">tercera</term>
<term name="long-ordinal-04">cuarta</term>
<term name="long-ordinal-05">quinta</term>
<term name="long-ordinal-06">sexta</term>
<term name="long-ordinal-07">séptima</term>
<term name="long-ordinal-08">octava</term>
<term name="long-ordinal-09">novena</term>
<term name="long-ordinal-10">décima</term>
<!-- LONG LOCATOR FORMS -->
<term name="act">
<single>act</single>
<multiple>acts</multiple>
</term>
<term name="appendix">
<single>appendix</single>
<multiple>appendices</multiple>
</term>
<term name="article-locator">
<single>article</single>
<multiple>articles</multiple>
</term>
<term name="book">
<single>libro</single>
<multiple>libros</multiple>
</term>
<term name="canon">
<single>canon</single>
<multiple>canons</multiple>
</term>
<term name="chapter">
<single>capítulo</single>
<multiple>capítulos</multiple>
</term>
<term name="column">
<single>columna</single>
<multiple>columnas</multiple>
</term>
<term name="elocation">
<single>location</single>
<multiple>locations</multiple>
</term>
<term name="equation">
<single>equation</single>
<multiple>equations</multiple>
</term>
<term name="figure">
<single>figura</single>
<multiple>figuras</multiple>
</term>
<term name="folio">
<single>folio</single>
<multiple>folios</multiple>
</term>
<term name="issue">
<single>número</single>
<multiple>números</multiple>
</term>
<term name="line">
<single>línea</single>
<multiple>líneas</multiple>
</term>
<term name="note">
<single>nota</single>
<multiple>notas</multiple>
</term>
<term name="opus">
<single>opus</single>
<multiple>opera</multiple>
</term>
<term name="page">
<single>página</single>
<multiple>páginas</multiple>
</term>
<term name="paragraph">
<single>párrafo</single>
<multiple>párrafos</multiple>
</term>
<term name="part">
<single>parte</single>
<multiple>partes</multiple>
</term>
<term name="rule">
<single>rule</single>
<multiple>rules</multiple>
</term>
<term name="scene">
<single>scene</single>
<multiple>scenes</multiple>
</term>
<term name="section">
<single>sección</single>
<multiple>secciones</multiple>
</term>
<term name="sub-verbo">
<single>sub voce</single>
<multiple>sub vocibus</multiple>
</term>
<term name="supplement">
<single>supplement</single>
<multiple>supplements</multiple>
</term>
<term name="table">
<single>table</single>
<multiple>tables</multiple>
</term>
<term name="timestamp"> <!-- generally blank -->
<single/>
<multiple/>
</term>
<term name="title-locator">
<single>title</single>
<multiple>titles</multiple>
</term>
<term name="verse">
<single>verso</single>
<multiple>versos</multiple>
</term>
<term name="volume">
<single>volumen</single>
<multiple>volúmenes</multiple>
</term>
<!-- SHORT LOCATOR FORMS -->
<term name="appendix" form="short">
<single>app.</single>
<multiple>apps.</multiple>
</term>
<term name="article-locator" form="short">
<single>art.</single>
<multiple>arts.</multiple>
</term>
<term name="book" form="short">
<single>lib.</single>
<multiple>libs.</multiple>
</term>
<term name="chapter" form="short">
<single>cap.</single>
<multiple>caps.</multiple>
</term>
<term name="column" form="short">
<single>col.</single>
<multiple>cols.</multiple>
</term>
<term name="elocation" form="short">
<single>loc.</single>
<multiple>locs.</multiple>
</term>
<term name="equation" form="short">
<single>eq.</single>
<multiple>eqs.</multiple>
</term>
<term name="figure" form="short">
<single>fig.</single>
<multiple>figs.</multiple>
</term>
<term name="folio" form="short">
<single>f.</single>
<multiple>ff.</multiple>
</term>
<term name="issue" form="short">
<single>núm.</single>
<multiple>núms.</multiple>
</term>
<term name="line" form="short">
<single>l.</single>
<multiple>ls.</multiple>
</term>
<term name="note" form="short">
<single>n.</single>
<multiple>nn.</multiple>
</term>
<term name="opus" form="short">
<single>op.</single>
<multiple>opp.</multiple>
</term>
<term name="page" form="short">
<single>p.</single>
<multiple>pp.</multiple>
</term>
<term name="paragraph" form="short">
<single>párr.</single>
<multiple>párrs.</multiple>
</term>
<term name="part" form="short">
<single>pt.</single>
<multiple>pts.</multiple>
</term>
<term name="rule" form="short">
<single>r.</single>
<multiple>rr.</multiple>
</term>
<term name="scene" form="short">
<single>sc.</single>
<multiple>scs.</multiple>
</term>
<term name="section" form="short">
<single>sec.</single>
<multiple>secs.</multiple>
</term>
<term name="sub-verbo" form="short">
<single>s.&#160;v.</single>
<multiple>s.&#160;vv.</multiple>
</term>
<term name="supplement" form="short">
<single>supp.</single>
<multiple>supps.</multiple>
</term>
<term name="table" form="short">
<single>tbl.</single>
<multiple>tbls.</multiple>
</term>
<term name="timestamp" form="short"> <!-- generally blank -->
<single/>
<multiple/>
</term>
<term name="title-locator" form="short">
<single>tit.</single>
<multiple>tits.</multiple>
</term>
<term name="verse" form="short">
<single>v.</single>
<multiple>vv.</multiple>
</term>
<term name="volume" form="short">
<single>vol.</single>
<multiple>vols.</multiple>
</term>
<!-- SYMBOLIC LOCATOR FORMS -->
<term name="paragraph" form="symbol">
<single></single>
<multiple></multiple>
</term>
<term name="section" form="symbol">
<single>§</single>
<multiple>§</multiple>
</term>
<!-- LONG NUMBER VARIABLE FORMS -->
<term name="chapter-number">
<single>chapter</single>
<multiple>chapters</multiple>
</term>
<term name="citation-number">
<single>citation</single>
<multiple>citations</multiple>
</term>
<term name="collection-number">
<single>número</single>
<multiple>números</multiple>
</term>
<term name="edition">
<single>edición</single>
<multiple>ediciones</multiple>
</term>
<term name="first-reference-note-number">
<single>reference</single>
<multiple>references</multiple>
</term>
<term name="number">
<single>number</single>
<multiple>numbers</multiple>
</term>
<term name="number-of-pages">
<single>página</single>
<multiple>páginas</multiple>
</term>
<term name="number-of-volumes">
<single>volume</single>
<multiple>volumes</multiple>
</term>
<term name="page-first">
<single>page</single>
<multiple>pages</multiple>
</term>
<term name="printing">
<single>printing</single>
<multiple>printings</multiple>
</term>
<term name="version">versión</term>
<!-- SHORT NUMBER VARIABLE FORMS -->
<term name="chapter-number" form="short">
<single>chap.</single>
<multiple>chaps.</multiple>
</term>
<term name="citation-number" form="short">
<single>cit.</single>
<multiple>cits.</multiple>
</term>
<term name="collection-number" form="short">
<single>núm.</single>
<multiple>núms.</multiple>
</term>
<term name="edition" form="short">
<single>ed.</single>
<multiple>eds.</multiple>
</term>
<term name="first-reference-note-number" form="short">
<single>ref.</single>
<multiple>refs.</multiple>
</term>
<term name="number" form="short">
<single>no.</single>
<multiple>nos.</multiple>
</term>
<term name="number-of-pages" form="short">
<single>p.</single>
<multiple>pp.</multiple>
</term>
<term name="number-of-volumes" form="short">
<single>vol.</single>
<multiple>vols.</multiple>
</term>
<term name="page-first" form="short">
<single>p.</single>
<multiple>pp.</multiple>
</term>
<term name="printing" form="short">
<single>print.</single>
<multiple>prints.</multiple>
</term>
<!-- LONG ROLE FORMS -->
<term name="author"/> <!-- generally blank -->
<term name="chair">
<single>chair</single>
<multiple>chairs</multiple>
</term>
<term name="collection-editor">
<single>ed.</single>
<multiple>eds.</multiple>
</term>
<term name="compiler">
<single>compiler</single>
<multiple>compilers</multiple>
</term>
<term name="composer"/> <!-- generally blank -->
<term name="container-author"/> <!-- generally blank -->
<term name="contributor">
<single>contributor</single>
<multiple>contributors</multiple>
</term>
<term name="curator">
<single>curator</single>
<multiple>curators</multiple>
</term>
<term name="director">
<single>director</single>
<multiple>directores</multiple>
</term>
<term name="editor">
<single>editor</single>
<multiple>editores</multiple>
</term>
<term name="editor-translator">
<single>editor y traductor</single>
<multiple>editores y traductores</multiple>
</term>
<term name="editortranslator">
<single>editor y traductor</single>
<multiple>editores y traductores</multiple>
</term>
<term name="editorial-director">
<single>coordinador</single>
<multiple>coordinadores</multiple>
</term>
<term name="executive-producer">
<single>executive producer</single>
<multiple>executive producers</multiple>
</term>
<term name="guest">
<single>guest</single>
<multiple>guests</multiple>
</term>
<term name="host">
<single>host</single>
<multiple>hosts</multiple>
</term>
<term name="illustrator">
<single>ilustrador</single>
<multiple>ilustradores</multiple>
</term>
<term name="interviewer"/> <!-- generally blank -->
<term name="narrator">
<single>narrator</single>
<multiple>narrators</multiple>
</term>
<term name="organizer">
<single>organizer</single>
<multiple>organizers</multiple>
</term>
<term name="original-author"/> <!-- generally blank -->
<term name="performer">
<single>performer</single>
<multiple>performers</multiple>
</term>
<term name="producer">
<single>producer</single>
<multiple>producers</multiple>
</term>
<term name="recipient"/> <!-- generally blank -->
<term name="reviewed-author"/> <!-- generally blank -->
<term name="script-writer">
<single>writer</single>
<multiple>writers</multiple>
</term>
<term name="series-creator">
<single>series creator</single>
<multiple>series creators</multiple>
</term>
<term name="translator">
<single>traductor</single>
<multiple>traductores</multiple>
</term>
<!-- SHORT ROLE FORMS -->
<term name="compiler" form="short">
<single>comp.</single>
<multiple>comps.</multiple>
</term>
<term name="contributor" form="short">
<single>contrib.</single>
<multiple>contribs.</multiple>
</term>
<term name="curator" form="short">
<single>cur.</single>
<multiple>curs.</multiple>
</term>
<term name="director" form="short">
<single>dir.</single>
<multiple>dirs.</multiple>
</term>
<term name="editor" form="short">
<single>ed.</single>
<multiple>eds.</multiple>
</term>
<term name="editor-translator" form="short">
<single>ed. y trad.</single>
<multiple>eds. y trads.</multiple>
</term>
<term name="editortranslator" form="short">
<single>ed. y trad.</single>
<multiple>eds. y trads.</multiple>
</term>
<term name="editorial-director" form="short">
<single>coord.</single>
<multiple>coords.</multiple>
</term>
<term name="executive-producer" form="short">
<single>exec. prod.</single>
<multiple>exec. prods.</multiple>
</term>
<term name="illustrator" form="short">
<single>ilust.</single>
<multiple>ilusts.</multiple>
</term>
<term name="narrator" form="short">
<single>narr.</single>
<multiple>narrs.</multiple>
</term>
<term name="organizer" form="short">
<single>org.</single>
<multiple>orgs.</multiple>
</term>
<term name="performer" form="short">
<single>perf.</single>
<multiple>perfs.</multiple>
</term>
<term name="producer" form="short">
<single>prod.</single>
<multiple>prods.</multiple>
</term>
<term name="script-writer" form="short">
<single>writ.</single>
<multiple>writs.</multiple>
</term>
<term name="series-creator" form="short">
<single>cre.</single>
<multiple>cres.</multiple>
</term>
<term name="translator" form="short">
<single>trad.</single>
<multiple>trads.</multiple>
</term>
<!-- VERB ROLE FORMS -->
<term name="chair" form="verb">chaired by</term>
<term name="collection-editor" form="verb">edited by</term>
<term name="compiler" form="verb">compiled by</term>
<term name="container-author" form="verb">de</term>
<term name="contributor" form="verb">with</term>
<term name="curator" form="verb">curated by</term>
<term name="director" form="verb">dirigido por</term>
<term name="editor" form="verb">editado por</term>
<term name="editor-translator" form="verb">editado y traducido por</term>
<term name="editortranslator" form="verb">editado y traducido por</term>
<term name="editorial-director" form="verb">coordinado por</term>
<term name="executive-producer" form="verb">executive produced by</term>
<term name="guest" form="verb">with guest</term>
<term name="host" form="verb">hosted by</term>
<term name="illustrator" form="verb">ilustrado por</term>
<term name="interviewer" form="verb">entrevistado por</term>
<term name="narrator" form="verb">narrated by</term>
<term name="organizer" form="verb">organized by</term>
<term name="performer" form="verb">performed by</term>
<term name="producer" form="verb">produced by</term>
<term name="recipient" form="verb">a</term>
<term name="reviewed-author" form="verb">por</term>
<term name="script-writer" form="verb">written by</term>
<term name="series-creator" form="verb">created by</term>
<term name="translator" form="verb">traducido por</term>
<!-- SHORT VERB ROLE FORMS -->
<term name="collection-editor" form="verb-short">ed. by</term>
<term name="compiler" form="verb-short">comp. by</term>
<term name="contributor" form="verb-short">w.</term>
<term name="curator" form="verb-short">cur. by</term>
<term name="director" form="verb-short">dir.</term>
<term name="editor" form="verb-short">ed.</term>
<term name="editor-translator" form="verb-short">ed. y trad.</term>
<term name="editortranslator" form="verb-short">ed. y trad.</term>
<term name="editorial-director" form="verb-short">coord.</term>
<term name="executive-producer" form="verb-short">exec. prod. by</term>
<term name="guest" form="verb-short">w. guest</term>
<term name="host" form="verb-short">hosted by</term>
<term name="illustrator" form="verb-short">ilust.</term>
<term name="narrator" form="verb-short">narr. by</term>
<term name="organizer" form="verb-short">org. by</term>
<term name="performer" form="verb-short">perf. by</term>
<term name="producer" form="verb-short">prod. by</term>
<term name="script-writer" form="verb-short">writ. by</term>
<term name="series-creator" form="verb-short">cre. by</term>
<term name="translator" form="verb-short">trad.</term>
<!-- LONG MONTH FORMS -->
<term name="month-01">enero</term>
<term name="month-02">febrero</term>
<term name="month-03">marzo</term>
<term name="month-04">abril</term>
<term name="month-05">mayo</term>
<term name="month-06">junio</term>
<term name="month-07">julio</term>
<term name="month-08">agosto</term>
<term name="month-09">septiembre</term>
<term name="month-10">octubre</term>
<term name="month-11">noviembre</term>
<term name="month-12">diciembre</term>
<!-- SHORT MONTH FORMS -->
<term name="month-01" form="short">ene.</term>
<term name="month-02" form="short">feb.</term>
<term name="month-03" form="short">mar.</term>
<term name="month-04" form="short">abr.</term>
<term name="month-05" form="short">may</term>
<term name="month-06" form="short">jun.</term>
<term name="month-07" form="short">jul.</term>
<term name="month-08" form="short">ago.</term>
<term name="month-09" form="short">sep.</term>
<term name="month-10" form="short">oct.</term>
<term name="month-11" form="short">nov.</term>
<term name="month-12" form="short">dic.</term>
<!-- SEASONS -->
<term name="season-01">primavera</term>
<term name="season-02">verano</term>
<term name="season-03">otoño</term>
<term name="season-04">invierno</term>
</terms>
</locale>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,519 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<style xmlns="http://purl.org/net/xbiblio/csl" class="in-text" version="1.0" demote-non-dropping-particle="sort-only">
<info>
<title>IEEE Reference Guide version 11.29.2023</title>
<title-short>Institute of Electrical and Electronics Engineers</title-short>
<id>http://www.zotero.org/styles/ieee</id>
<link href="http://www.zotero.org/styles/ieee" rel="self"/>
<link href="https://journals.ieeeauthorcenter.ieee.org/your-role-in-article-production/ieee-editorial-style-manual/" rel="documentation"/>
<author>
<name>Michael Berkowitz</name>
<email>mberkowi@gmu.edu</email>
</author>
<contributor>
<name>Julian Onions</name>
<email>julian.onions@gmail.com</email>
</contributor>
<contributor>
<name>Rintze Zelle</name>
<uri>http://twitter.com/rintzezelle</uri>
</contributor>
<contributor>
<name>Stephen Frank</name>
<uri>http://www.zotero.org/sfrank</uri>
</contributor>
<contributor>
<name>Sebastian Karcher</name>
</contributor>
<contributor>
<name>Giuseppe Silano</name>
<email>g.silano89@gmail.com</email>
<uri>http://giuseppesilano.net</uri>
</contributor>
<contributor>
<name>Patrick O'Brien</name>
</contributor>
<contributor>
<name>Brenton M. Wiernik</name>
</contributor>
<contributor>
<name>Oliver Couch</name>
<email>oliver.couch@gmail.com</email>
</contributor>
<contributor>
<name>Andrew Dunning</name>
<uri>https://orcid.org/0000-0003-0464-5036</uri>
</contributor>
<category citation-format="numeric"/>
<category field="engineering"/>
<category field="generic-base"/>
<summary>IEEE style as per the 2023 guidelines.</summary>
<updated>2024-03-27T11:41:27+00:00</updated>
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
</info>
<locale xml:lang="en">
<date form="text">
<date-part name="month" form="short" suffix=" "/>
<date-part name="day" form="numeric-leading-zeros" suffix=", "/>
<date-part name="year"/>
</date>
<terms>
<term name="chapter" form="short">ch.</term>
<term name="chapter-number" form="short">ch.</term>
<term name="presented at">presented at the</term>
<term name="available at">available</term>
<!-- always use three-letter abbreviations for months -->
<term name="month-06" form="short">Jun.</term>
<term name="month-07" form="short">Jul.</term>
<term name="month-09" form="short">Sep.</term>
</terms>
</locale>
<!-- Macros -->
<macro name="status">
<choose>
<if variable="page issue volume" match="none">
<text variable="status" text-case="capitalize-first" suffix="" font-weight="bold"/>
</if>
</choose>
</macro>
<macro name="edition">
<choose>
<if type="bill book chapter graphic legal_case legislation motion_picture paper-conference report song" match="any">
<choose>
<if is-numeric="edition">
<group delimiter=" ">
<number variable="edition" form="ordinal"/>
<text term="edition" form="short"/>
</group>
</if>
<else>
<text variable="edition" text-case="capitalize-first" suffix="."/>
</else>
</choose>
</if>
</choose>
</macro>
<macro name="issued">
<choose>
<if type="article-journal report" match="any">
<date variable="issued">
<date-part name="month" form="short" suffix=" "/>
<date-part name="year" form="long"/>
</date>
</if>
<else-if type="bill book chapter graphic legal_case legislation song thesis" match="any">
<date variable="issued">
<date-part name="year" form="long"/>
</date>
</else-if>
<else-if type="paper-conference" match="any">
<date variable="issued">
<date-part name="month" form="short"/>
<date-part name="year" prefix=" "/>
</date>
</else-if>
<else-if type="motion_picture" match="any">
<date variable="issued" form="text" prefix="(" suffix=")"/>
</else-if>
<else>
<date variable="issued" form="text"/>
</else>
</choose>
</macro>
<macro name="author">
<names variable="author">
<name and="text" et-al-min="7" et-al-use-first="1" initialize-with=". "/>
<label form="short" prefix=", " text-case="capitalize-first"/>
<et-al font-style="italic"/>
<substitute>
<names variable="editor"/>
<names variable="translator"/>
<text macro="director"/>
</substitute>
</names>
</macro>
<macro name="editor">
<names variable="editor">
<name initialize-with=". " delimiter=", " and="text"/>
<label form="short" prefix=", " text-case="capitalize-first"/>
</names>
</macro>
<macro name="director">
<names variable="director">
<name and="text" et-al-min="7" et-al-use-first="1" initialize-with=". "/>
<et-al font-style="italic"/>
</names>
</macro>
<macro name="locators">
<group delimiter=", ">
<text macro="edition"/>
<group delimiter=" ">
<text term="volume" form="short"/>
<number variable="volume" form="numeric"/>
</group>
<group delimiter=" ">
<number variable="number-of-volumes" form="numeric"/>
<text term="volume" form="short" plural="true"/>
</group>
<group delimiter=" ">
<text term="issue" form="short"/>
<number variable="issue" form="numeric"/>
</group>
</group>
</macro>
<macro name="title">
<choose>
<if type="bill book graphic legal_case legislation motion_picture song standard software" match="any">
<text variable="title" font-style="italic"/>
</if>
<else>
<text variable="title" quotes="true"/>
</else>
</choose>
</macro>
<macro name="publisher">
<choose>
<if type="bill book chapter graphic legal_case legislation motion_picture paper-conference song" match="any">
<group delimiter=": ">
<text variable="publisher-place"/>
<text variable="publisher"/>
</group>
</if>
<else>
<group delimiter=", ">
<text variable="publisher"/>
<text variable="publisher-place"/>
</group>
</else>
</choose>
</macro>
<macro name="event">
<choose>
<!-- Published Conference Paper -->
<if type="paper-conference speech" match="any">
<choose>
<if variable="container-title" match="any">
<group delimiter=" ">
<text term="in"/>
<text variable="container-title" font-style="italic"/>
</group>
</if>
<!-- Unpublished Conference Paper -->
<else>
<group delimiter=" ">
<text term="presented at"/>
<text variable="event"/>
</group>
</else>
</choose>
</if>
</choose>
</macro>
<macro name="access">
<choose>
<if type="webpage post post-weblog" match="any">
<!-- https://url.com/ (accessed Mon. DD, YYYY). -->
<choose>
<if variable="URL">
<group delimiter=". " prefix=" ">
<group delimiter=": ">
<text term="accessed" text-case="capitalize-first"/>
<date variable="accessed" form="text"/>
</group>
<text term="online" prefix="[" suffix="]" text-case="capitalize-first"/>
<group delimiter=": ">
<text term="available at" text-case="capitalize-first"/>
<text variable="URL"/>
</group>
</group>
</if>
</choose>
</if>
<else-if match="any" variable="DOI">
<!-- doi: 10.1000/xyz123. -->
<text variable="DOI" prefix=" doi: " suffix="."/>
</else-if>
<else-if variable="URL">
<!-- Accessed: Mon. DD, YYYY. [Medium]. Available: https://URL.com/ -->
<group delimiter=". " prefix=" " suffix=". ">
<!-- Accessed: Mon. DD, YYYY. -->
<group delimiter=": ">
<text term="accessed" text-case="capitalize-first"/>
<date variable="accessed" form="text"/>
</group>
<!-- [Online Video]. -->
<group prefix="[" suffix="]" delimiter=" ">
<choose>
<if variable="medium" match="any">
<text variable="medium" text-case="capitalize-first"/>
</if>
<else>
<text term="online" text-case="capitalize-first"/>
<choose>
<if type="motion_picture">
<text term="video" text-case="capitalize-first"/>
</if>
</choose>
</else>
</choose>
</group>
</group>
<!-- Available: https://URL.com/ -->
<group delimiter=": " prefix=" ">
<text term="available at" text-case="capitalize-first"/>
<text variable="URL"/>
</group>
</else-if>
</choose>
</macro>
<macro name="page">
<choose>
<if type="article-journal" variable="number" match="all">
<group delimiter=" ">
<text value="Art."/>
<text term="issue" form="short"/>
<text variable="number"/>
</group>
</if>
<else>
<group delimiter=" ">
<label variable="page" form="short"/>
<text variable="page"/>
</group>
</else>
</choose>
</macro>
<macro name="citation-locator">
<group delimiter=" ">
<choose>
<if locator="page">
<label variable="locator" form="short"/>
</if>
<else>
<label variable="locator" form="short" text-case="capitalize-first"/>
</else>
</choose>
<text variable="locator"/>
</group>
</macro>
<macro name="geographic-location">
<group delimiter=", " suffix=".">
<choose>
<if variable="publisher-place">
<text variable="publisher-place" text-case="title"/>
</if>
<else-if variable="event-place">
<text variable="event-place" text-case="title"/>
</else-if>
</choose>
</group>
</macro>
<!-- Series -->
<macro name="collection">
<choose>
<if variable="collection-title" match="any">
<text term="in" suffix=" "/>
<group delimiter=", " suffix=". ">
<text variable="collection-title"/>
<text variable="collection-number" prefix="no. "/>
<text variable="volume" prefix="vol. "/>
</group>
</if>
</choose>
</macro>
<!-- Citation -->
<citation>
<sort>
<key variable="citation-number"/>
</sort>
<layout delimiter=", ">
<group prefix="[" suffix="]" delimiter=", ">
<text variable="citation-number"/>
<text macro="citation-locator"/>
</group>
</layout>
</citation>
<!-- Bibliography -->
<bibliography entry-spacing="0" second-field-align="flush">
<layout>
<!-- Citation Number -->
<text variable="citation-number" prefix="[" suffix="]"/>
<!-- Author(s) -->
<text macro="author" suffix=", "/>
<!-- Rest of Citation -->
<choose>
<!-- Specific Formats -->
<if type="article-journal">
<group delimiter=", ">
<text macro="title"/>
<text variable="container-title" font-style="italic" form="short"/>
<text macro="locators"/>
<text macro="page"/>
<text macro="issued"/>
<text macro="status"/>
</group>
<choose>
<if variable="URL DOI" match="none">
<text value="."/>
</if>
<else>
<text value=","/>
</else>
</choose>
<text macro="access"/>
</if>
<else-if type="paper-conference speech" match="any">
<group delimiter=", " suffix=", ">
<text macro="title"/>
<text macro="event"/>
<text macro="editor"/>
</group>
<text macro="collection"/>
<group delimiter=", " suffix=".">
<text macro="publisher"/>
<text macro="issued"/>
<text macro="page"/>
<text macro="status"/>
</group>
<text macro="access"/>
</else-if>
<else-if type="chapter">
<group delimiter=", " suffix=".">
<text macro="title"/>
<group delimiter=" ">
<text term="in" suffix=" "/>
<text variable="container-title" font-style="italic"/>
</group>
<text macro="locators"/>
<text macro="editor"/>
<text macro="collection"/>
<text macro="publisher"/>
<text macro="issued"/>
<group delimiter=" ">
<label variable="chapter-number" form="short"/>
<text variable="chapter-number"/>
</group>
<text macro="page"/>
</group>
<text macro="access"/>
</else-if>
<else-if type="report">
<group delimiter=", " suffix=".">
<text macro="title"/>
<text macro="publisher"/>
<group delimiter=" ">
<text variable="genre"/>
<text variable="number"/>
</group>
<text macro="issued"/>
</group>
<text macro="access"/>
</else-if>
<else-if type="thesis">
<group delimiter=", " suffix=".">
<text macro="title"/>
<text variable="genre"/>
<text macro="publisher"/>
<text macro="issued"/>
</group>
<text macro="access"/>
</else-if>
<else-if type="software">
<group delimiter=". " suffix=".">
<text macro="title"/>
<text macro="issued" prefix="(" suffix=")"/>
<text variable="genre"/>
<text macro="publisher"/>
</group>
<text macro="access"/>
</else-if>
<else-if type="article">
<group delimiter=", " suffix=".">
<text macro="title"/>
<text macro="issued"/>
<group delimiter=": ">
<text macro="publisher" font-style="italic"/>
<text variable="number"/>
</group>
</group>
<text macro="access"/>
</else-if>
<else-if type="webpage post-weblog post" match="any">
<group delimiter=", " suffix=".">
<text macro="title"/>
<text variable="container-title"/>
</group>
<text macro="access"/>
</else-if>
<else-if type="patent">
<group delimiter=", ">
<text macro="title"/>
<text variable="number"/>
<text macro="issued"/>
</group>
<text macro="access"/>
</else-if>
<!-- Online Video -->
<else-if type="motion_picture">
<text macro="geographic-location" suffix=". "/>
<group delimiter=", " suffix=".">
<text macro="title"/>
<text macro="issued"/>
</group>
<text macro="access"/>
</else-if>
<else-if type="standard">
<group delimiter=", " suffix=".">
<text macro="title"/>
<group delimiter=" ">
<text variable="genre"/>
<text variable="number"/>
</group>
<text macro="geographic-location"/>
<text macro="issued"/>
</group>
<text macro="access"/>
</else-if>
<!-- Generic/Fallback Formats -->
<else-if type="bill book graphic legal_case legislation report song" match="any">
<group delimiter=", " suffix=". ">
<text macro="title"/>
<text macro="locators"/>
</group>
<text macro="collection"/>
<group delimiter=", " suffix=".">
<text macro="publisher"/>
<text macro="issued"/>
<text macro="page"/>
</group>
<text macro="access"/>
</else-if>
<else-if type="article-magazine article-newspaper broadcast interview manuscript map patent personal_communication song speech thesis webpage" match="any">
<group delimiter=", " suffix=".">
<text macro="title"/>
<text variable="container-title" font-style="italic"/>
<text macro="locators"/>
<text macro="publisher"/>
<text macro="page"/>
<text macro="issued"/>
</group>
<text macro="access"/>
</else-if>
<else>
<group delimiter=", " suffix=". ">
<text macro="title"/>
<text variable="container-title" font-style="italic"/>
<text macro="locators"/>
</group>
<text macro="collection"/>
<group delimiter=", " suffix=".">
<text macro="publisher"/>
<text macro="page"/>
<text macro="issued"/>
</group>
<text macro="access"/>
</else>
</choose>
</layout>
</bibliography>
</style>

View File

@@ -1,520 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<style xmlns="http://purl.org/net/xbiblio/csl" class="in-text" delimiter-precedes-last="always" demote-non-dropping-particle="sort-only" initialize-with="" initialize-with-hyphen="false" name-as-sort-order="all" name-delimiter=", " names-delimiter=", " page-range-format="minimal" sort-separator=" " version="1.0">
<!-- This file was generated by the Style Variant Builder <https://github.com/citation-style-language/style-variant-builder>. To contribute changes, modify the template and regenerate variants. -->
<info>
<title>NLM/Vancouver: Citing Medicine 2nd edition (citation-sequence)</title>
<title-short>National Library of Medicine, ANSI/NISO Z39.29-2005 (R2010), ICMJE Recommendations/URMs (C-S)</title-short>
<id>http://www.zotero.org/styles/nlm-citation-sequence</id>
<link href="http://www.zotero.org/styles/nlm-citation-sequence" rel="self"/>
<link href="https://www.nlm.nih.gov/citingmedicine" rel="documentation"/>
<link href="https://www.nlm.nih.gov/bsd/uniform_requirements.html" rel="documentation"/>
<link href="https://www.icmje.org/recommendations/" rel="documentation"/>
<author>
<name>Michael Berkowitz</name>
<email>mberkowi@gmu.edu</email>
</author>
<author>
<name>Andrew Dunning</name>
<uri>https://orcid.org/0000-0003-0464-5036</uri>
</author>
<contributor>
<name>Petr Hlustik</name>
<uri>https://orcid.org/0000-0002-1951-0671</uri>
</contributor>
<contributor>
<name>Sebastian Karcher</name>
<uri>https://orcid.org/0000-0001-8249-7388</uri>
</contributor>
<contributor>
<name>Charles Parnot</name>
<uri>https://orcid.org/0000-0002-7346-5883</uri>
</contributor>
<contributor>
<name>Sean Takats</name>
<uri>https://orcid.org/0000-0002-7851-5069</uri>
</contributor>
<category citation-format="numeric"/>
<category field="generic-base"/>
<category field="medicine"/>
<category field="science"/>
<summary>Citing Medicine: The NLM Style Guide for Authors, Editors, and Publishers, 2nd edition (2015), based on ANSI/NISO Z39.29-2005 (R2010); citation-sequence system.</summary>
<updated>2026-02-18T15:24:08+00:00</updated>
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
</info>
<locale xml:lang="en">
<date delimiter=" " form="text">
<date-part name="year"/>
<date-part form="short" name="month" strip-periods="true"/>
<date-part name="day"/>
</date>
<terms>
<term name="available at">available from</term>
<term name="collection-editor">
<single>editor</single>
<multiple>editors</multiple>
</term>
<term form="short" name="month-06">Jun.</term>
<term form="short" name="month-07">Jul.</term>
<term form="short" name="month-09">Sep.</term>
<term name="presented at">presented at</term>
<term form="short" name="section">
<single>sect.</single>
<multiple>sects.</multiple>
</term>
<term form="short" name="supplement">
<single>suppl.</single>
<multiple>suppls.</multiple>
</term>
</terms>
</locale>
<locale xml:lang="fr">
<date delimiter=" " form="text">
<date-part name="day"/>
<date-part form="short" name="month" strip-periods="true"/>
<date-part name="year"/>
</date>
</locale>
<!-- Variable labels -->
<macro name="label-collection-number">
<group delimiter=" ">
<choose>
<if is-numeric="collection-number">
<label form="short" variable="collection-number"/>
</if>
</choose>
<text variable="collection-number"/>
</group>
</macro>
<macro name="label-edition">
<group delimiter=" ">
<choose>
<if is-numeric="edition">
<number form="ordinal" variable="edition"/>
<label form="short" variable="edition"/>
</if>
<else>
<text variable="edition"/>
</else>
</choose>
</group>
</macro>
<macro name="label-number-of-pages">
<group delimiter=" ">
<text variable="number-of-pages"/>
<choose>
<if is-numeric="number-of-pages">
<label form="short" plural="never" variable="number-of-pages"/>
</if>
</choose>
</group>
</macro>
<macro name="label-page">
<group delimiter=" ">
<label form="short" plural="never" variable="page"/>
<text variable="page"/>
</group>
</macro>
<macro name="label-part-number-capitalized">
<group delimiter=" ">
<choose>
<if is-numeric="part-number">
<!-- TODO: Replace with `part-number` label when CSL provides one -->
<text form="short" term="part" text-case="capitalize-first"/>
</if>
</choose>
<text variable="part-number"/>
</group>
</macro>
<macro name="label-supplement-number">
<group delimiter=" ">
<choose>
<if is-numeric="supplement-number">
<!-- TODO: Replace with `supplement-number` label when CSL provides one -->
<text form="short" strip-periods="true" term="supplement" text-case="capitalize-first"/>
</if>
</choose>
<text text-case="capitalize-first" variable="supplement-number"/>
</group>
</macro>
<macro name="label-volume-capitalized">
<group delimiter=" ">
<choose>
<if is-numeric="volume">
<label form="short" text-case="capitalize-first" variable="volume"/>
</if>
</choose>
<text variable="volume"/>
</group>
</macro>
<macro name="author">
<names variable="author">
<label prefix=", "/>
<substitute>
<names variable="editor-translator"/>
<names variable="editor translator"/>
<names variable="editor"/>
<names variable="collection-editor"/>
</substitute>
</names>
</macro>
<macro name="title">
<choose>
<if type="webpage" variable="container-title">
<!-- `webpage` listed under `container-title` (Citing Medicine, ch. 25) -->
<text variable="container-title"/>
</if>
<else>
<text variable="title"/>
</else>
</choose>
</macro>
<macro name="content-type">
<text variable="genre"/>
</macro>
<macro name="type-of-medium">
<choose>
<if variable="medium">
<text text-case="capitalize-first" variable="medium"/>
</if>
<else-if match="any" type="chapter entry-dictionary entry-encyclopedia paper-conference"/>
<else-if variable="URL">
<text term="internet" text-case="capitalize-first"/>
</else-if>
</choose>
</macro>
<macro name="container-preposition">
<choose>
<if match="any" type="chapter paper-conference entry-dictionary entry-encyclopedia">
<text term="in" text-case="capitalize-first"/>
</if>
</choose>
</macro>
<macro name="secondary-authors">
<names variable="editor">
<label prefix=", "/>
</names>
</macro>
<macro name="container-title">
<group delimiter=", ">
<choose>
<if type="webpage"/>
<else-if variable="container-title">
<group delimiter=". ">
<group delimiter=" ">
<choose>
<if match="any" type="article-journal review review-book">
<text form="short" strip-periods="true" variable="container-title"/>
</if>
<else>
<text variable="container-title"/>
</else>
</choose>
<choose>
<if type="article-journal" variable="DOI"/>
<else-if type="article-journal" variable="PMID"/>
<else-if type="article-journal" variable="PMCID"/>
<else-if variable="URL">
<text prefix="[" suffix="]" term="internet" text-case="capitalize-first"/>
</else-if>
</choose>
</group>
<text macro="label-edition"/>
</group>
</else-if>
<!-- TODO: add `event-name` and `event-place` -->
<else-if match="any" type="bill legislation">
<group delimiter=". ">
<text variable="container-title"/>
<group delimiter=" ">
<text form="short" term="section" text-case="capitalize-first"/>
<text variable="section"/>
</group>
</group>
<text variable="number"/>
</else-if>
<else-if type="speech">
<group delimiter=": ">
<group delimiter=" ">
<text text-case="capitalize-first" variable="genre"/>
<text term="presented at"/>
</group>
<text variable="event"/>
</group>
</else-if>
<else>
<group delimiter=", ">
<text macro="label-volume-capitalized"/>
<text variable="volume-title"/>
</group>
<group delimiter=", ">
<text macro="label-part-number-capitalized"/>
<text variable="part-title"/>
</group>
</else>
</choose>
</group>
</macro>
<macro name="place-of-publication">
<choose>
<if type="thesis">
<text prefix="[" suffix="]" variable="publisher-place"/>
</if>
<else-if type="speech"/>
<else>
<text variable="publisher-place"/>
</else>
</choose>
</macro>
<macro name="publisher">
<choose>
<!-- discard publisher for serial publications -->
<if match="none" type="article-journal article-magazine article-newspaper periodical post-weblog review review-book">
<group delimiter=": ">
<text macro="place-of-publication"/>
<text variable="publisher"/>
</group>
</if>
</choose>
</macro>
<macro name="date">
<group delimiter=" ">
<choose>
<if match="any" type="article-journal article-magazine article-newspaper periodical post-weblog review review-book">
<group delimiter=":">
<group delimiter=" ">
<date form="text" variable="issued"/>
<choose>
<if type="article-journal" variable="DOI"/>
<else-if type="article-journal" variable="PMID"/>
<else-if type="article-journal" variable="PMCID"/>
<else>
<text macro="date-of-citation"/>
</else>
</choose>
</group>
<choose>
<if type="article-newspaper">
<text variable="page"/>
</if>
</choose>
</group>
</if>
<else-if match="any" type="bill legislation">
<date form="text" variable="issued"/>
</else-if>
<else-if type="report">
<date date-parts="year-month" form="text" variable="issued"/>
<text macro="date-of-citation"/>
</else-if>
<else-if type="patent">
<group delimiter=", ">
<text variable="number"/>
<date date-parts="year" form="numeric" variable="issued"/>
</group>
<text macro="date-of-citation"/>
</else-if>
<else-if type="speech">
<group delimiter="; ">
<group delimiter=" ">
<date form="text" variable="issued"/>
<text macro="date-of-citation"/>
</group>
<text variable="event-place"/>
</group>
</else-if>
<else>
<date date-parts="year" form="numeric" variable="issued"/>
<text macro="date-of-citation"/>
</else>
</choose>
</group>
</macro>
<macro name="identifier-serial">
<choose>
<if match="any" type="article-journal article-magazine periodical post-weblog review review-book">
<group delimiter=":">
<group>
<text variable="collection-title"/>
<text variable="volume"/>
<group delimiter=" " prefix="(" suffix=")">
<text variable="issue"/>
<text macro="label-supplement-number"/>
</group>
</group>
<text macro="location-pagination-serial"/>
</group>
</if>
</choose>
</macro>
<macro name="date-of-citation">
<choose>
<if variable="URL">
<group delimiter=" " prefix="[" suffix="]">
<text term="cited"/>
<date form="text" variable="accessed"/>
</group>
</if>
</choose>
</macro>
<macro name="location-pagination-monographic">
<group delimiter=" ">
<choose>
<if match="any" type="article-journal article-magazine article-newspaper review review-book"/>
<else-if type="book">
<text macro="label-number-of-pages"/>
</else-if>
<else>
<text macro="label-page"/>
</else>
</choose>
</group>
</macro>
<macro name="location-pagination-serial">
<choose>
<if variable="number">
<text variable="number"/>
</if>
<else>
<text variable="page"/>
</else>
</choose>
</macro>
<macro name="webpage-part">
<choose>
<if type="webpage" variable="container-title">
<text variable="title"/>
</if>
</choose>
</macro>
<macro name="series">
<choose>
<if match="any" type="article-journal article-magazine article-newspaper periodical post-weblog review review-book"/>
<else-if variable="collection-title">
<group delimiter=". " prefix="(" suffix=")">
<names variable="collection-editor">
<label prefix=", "/>
</names>
<group delimiter="; ">
<text variable="collection-title"/>
<text macro="label-collection-number"/>
</group>
</group>
</else-if>
</choose>
</macro>
<macro name="report-number">
<choose>
<if type="report">
<group delimiter=": ">
<group delimiter=" ">
<text term="report" text-case="capitalize-first"/>
<label form="short" text-case="capitalize-first" variable="number"/>
</group>
<text variable="number"/>
</group>
</if>
</choose>
</macro>
<macro name="availability">
<group delimiter=". ">
<group delimiter=": ">
<text text-case="capitalize-first" value="located at"/>
<group delimiter="; ">
<group delimiter=", ">
<text variable="archive_collection"/>
<text variable="archive"/>
<text variable="archive-place"/>
</group>
<text variable="archive_location"/>
</group>
</group>
<group delimiter=" ">
<choose>
<if type="article-journal" variable="DOI"/>
<else-if type="article-journal" variable="PMID"/>
<else-if type="article-journal" variable="PMCID"/>
<else>
<group delimiter=": ">
<text term="available at" text-case="capitalize-first"/>
<text variable="URL"/>
</group>
</else>
</choose>
<text prefix="doi:" variable="DOI"/>
</group>
</group>
</macro>
<macro name="notes">
<group delimiter=". " suffix=".">
<group delimiter="; ">
<group delimiter=": ">
<text value="PubMed PMID"/>
<text variable="PMID"/>
</group>
<group delimiter=": ">
<text value="PubMed Central PMCID"/>
<text variable="PMCID"/>
</group>
</group>
<text variable="references"/>
</group>
</macro>
<citation collapse="citation-number">
<sort>
<key variable="citation-number"/>
</sort>
<layout delimiter="," prefix="(" suffix=")">
<text variable="citation-number"/>
</layout>
</citation>
<macro name="bibliography">
<group delimiter=" ">
<group delimiter=". " suffix=".">
<text macro="author"/>
<group delimiter=" ">
<text macro="title"/>
<text macro="content-type" prefix="[" suffix="]"/>
<choose>
<if type="webpage" variable="container-title">
<text macro="type-of-medium" prefix="[" suffix="]"/>
</if>
<else-if match="none" variable="container-title">
<text macro="type-of-medium" prefix="[" suffix="]"/>
</else-if>
</choose>
</group>
<choose>
<if match="none" variable="container-title">
<text macro="label-edition"/>
</if>
</choose>
<group delimiter=": ">
<text macro="container-preposition"/>
<group delimiter=". ">
<text macro="secondary-authors"/>
<text macro="container-title"/>
</group>
</group>
<group delimiter="; ">
<text macro="publisher"/>
<group delimiter=";">
<text macro="date"/>
<text macro="identifier-serial"/>
</group>
</group>
<text macro="location-pagination-monographic"/>
<text macro="webpage-part"/>
<text macro="series"/>
<text macro="report-number"/>
</group>
<text macro="availability"/>
<text macro="notes"/>
</group>
</macro>
<bibliography et-al-min="7" et-al-use-first="6" second-field-align="flush">
<layout>
<text suffix="." variable="citation-number"/>
<text macro="bibliography"/>
</layout>
</bibliography>
</style>

View File

@@ -1,118 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 192.3 63.4" style="enable-background:new 0 0 192.3 63.4;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#FFFFFF;}
.st2{fill:#FFFFFF;}
</style>
<g>
<g id="Group_1247_1_">
<path id="Path_477_1_" class="st0" d="M50.7,50.6l4.4-7.8h-8.9l-12-21l-4.4,7.8l12,21C41.8,50.6,50.7,50.6,50.7,50.6z"/>
<path id="Path_478_1_" class="st0" d="M34.3,1h-9l4.4,7.8l-12,20.8h9.1l12-20.8L34.3,1z"/>
<path id="Path_479_1_" class="st0" d="M0,40.1l4.4,7.8l4.4-7.8h23.9l-4.4-7.8H4.4L0,40.1z"/>
<path id="Path_480_1_" class="st1" d="M56.7,40.1l4.4-7.8h-9L40.3,11.4l-4.4,7.8l12,20.8H56.7z"/>
<path id="Path_481_1_" class="st1" d="M22.3,1h-8.9l4.4,7.8l-12,20.8h9l12-20.8L22.3,1z"/>
<path id="Path_482_1_" class="st1" d="M5.9,50.6l4.4,7.8l4.4-7.8h23.9l-4.4-7.8H10.5L5.9,50.6z"/>
</g>
<g>
<path class="st1" d="M67.9,3.9c0-0.8,0-1.6-0.1-2.4l1.7-0.1l0.1,0.1v6.3c0,0.7,0.1,1.2,0.5,1.6C70.6,9.8,71,10,71.7,10
c0.5,0,1.1-0.1,1.3-0.5c0.4-0.4,0.5-0.9,0.5-1.6V3.5c0-0.8,0-1.5-0.1-2.2l1.9-0.1v6.7c0,1.1-0.4,2-1.1,2.6
c-0.7,0.5-1.6,0.9-2.7,0.9c-1.1,0-2-0.3-2.7-0.9C68.2,10,67.8,9,67.8,7.9L67.9,3.9L67.9,3.9z"/>
<path class="st1" d="M83,11.3h-1.7V6.6c0-0.7-0.1-1.5-0.3-2.3L82.6,4c0.1,0.4,0.3,0.7,0.3,1.1C83.5,4.4,84.3,4,85.1,4
c0.7,0,1.1,0.1,1.5,0.5C87,5,87.1,5.5,87.1,6.2v5.1h-1.7V6.6c0-0.8-0.4-1.3-1.1-1.3c-0.4,0-0.9,0.1-1.3,0.5L83,11.3L83,11.3z"/>
<path class="st1" d="M95.1,1.6c0,0.3-0.1,0.7-0.4,0.8c-0.3,0.3-0.5,0.4-0.8,0.4S93.3,2.7,93,2.6c-0.1-0.1-0.3-0.4-0.3-0.7
s0.1-0.7,0.4-0.8c0.3-0.1,0.5-0.4,0.9-0.4c0.3,0,0.5,0.1,0.7,0.3S95.1,1.4,95.1,1.6z M93,11.3V6.6c0-0.8,0-1.6-0.1-2.4l1.7-0.3
L94.8,4v7.1L93,11.3L93,11.3z"/>
<path class="st1" d="M106.4,4.3l-2.3,7h-1.9l-2.3-7.1l1.9-0.1l0.9,3.6c0.3,1.1,0.5,1.9,0.5,2.4l0,0c0.1-0.5,0.3-1.3,0.7-2.4
l0.9-3.6L106.4,4.3L106.4,4.3z"/>
<path class="st1" d="M116.7,7.7l-0.3,0.3h-4c0,0.8,0.3,1.3,0.7,1.7c0.4,0.4,0.8,0.5,1.5,0.5c0.5,0,1.2-0.1,1.7-0.5l0.1,1.2
c-0.7,0.4-1.3,0.7-2.4,0.7c-1.1,0-1.9-0.3-2.6-0.9s-0.9-1.5-0.9-2.7s0.3-2,0.9-2.8s1.5-1.1,2.4-1.1c0.8,0,1.5,0.3,2,0.8
c0.5,0.5,0.8,1.2,0.8,2.2C116.7,7.3,116.7,7.5,116.7,7.7z M113.9,5.1c-0.4,0-0.8,0.1-0.9,0.5c-0.3,0.4-0.4,0.9-0.4,1.6l2.6-0.1
c0-0.1,0-0.3,0-0.5c0-0.4-0.1-0.8-0.3-1.1C114.6,5.3,114.3,5.1,113.9,5.1z"/>
<path class="st1" d="M124,11.3h-1.7V6.6c0-0.7-0.1-1.5-0.3-2.3l1.6-0.3c0.1,0.5,0.3,0.9,0.4,1.5c0.5-0.9,1.2-1.5,1.9-1.5
c0.3,0,0.5,0,0.7,0.1l-0.1,1.7c-0.3-0.1-0.5-0.1-0.8-0.1c-0.5,0-1.1,0.1-1.6,0.5C124,6.3,124,11.3,124,11.3z"/>
<path class="st1" d="M135.8,4.4l-0.1,1.3c-0.7-0.4-1.3-0.5-1.9-0.5c-0.4,0-0.7,0.1-0.8,0.3c-0.3,0.1-0.3,0.3-0.3,0.5
c0,0.3,0.1,0.4,0.4,0.7c0.3,0.1,0.5,0.4,0.8,0.5c0.3,0.1,0.7,0.3,1.1,0.4c0.4,0.1,0.7,0.4,0.8,0.7c0.3,0.3,0.4,0.7,0.4,1.1
c0,0.7-0.3,1.2-0.8,1.5c-0.5,0.4-1.2,0.5-2.2,0.5s-1.7-0.1-2.4-0.5l0.1-1.3c0.8,0.4,1.5,0.7,2.3,0.7c0.4,0,0.7-0.1,0.8-0.3
c0.1-0.1,0.3-0.3,0.3-0.5c0-0.3-0.1-0.4-0.4-0.7c-0.3-0.1-0.5-0.4-0.8-0.5s-0.7-0.3-0.9-0.4s-0.7-0.4-0.8-0.7
c-0.3-0.3-0.4-0.7-0.4-1.1c0-0.7,0.3-1.2,0.8-1.6c0.5-0.4,1.2-0.5,2-0.5C134.5,4,135.1,4.2,135.8,4.4z"/>
<path class="st1" d="M143.3,1.6c0,0.3-0.1,0.7-0.4,0.8c-0.3,0.1-0.5,0.4-0.8,0.4c-0.3,0-0.5-0.1-0.8-0.3c-0.1-0.1-0.1-0.4-0.1-0.7
s0.1-0.7,0.4-0.8c0.3-0.3,0.5-0.4,0.9-0.4c0.3,0,0.5,0.1,0.7,0.3S143.3,1.4,143.3,1.6z M141.3,11.3V6.6c0-0.8,0-1.6-0.1-2.4
l1.7-0.3l0.1,0.1v7.1L141.3,11.3L141.3,11.3z"/>
<path class="st1" d="M153,0.7l1.7-0.3l0.1,0.1v8.7c0,0.7,0.1,1.3,0.3,1.9l-1.6,0.1c-0.1-0.1-0.3-0.5-0.4-0.9l0,0
c-0.5,0.7-1.1,0.9-2,0.9c-0.9,0-1.5-0.3-2-0.9c-0.5-0.7-0.8-1.5-0.8-2.6c0-1.2,0.4-2.2,1.1-3c0.7-0.7,1.6-1.1,2.7-1.1
c0.3,0,0.5,0,0.9,0.1V3C153.2,2,153.2,1.4,153,0.7z M150.5,7.7c0,0.8,0.1,1.5,0.4,1.9c0.3,0.5,0.7,0.7,1.2,0.7
c0.4,0,0.8-0.1,1.1-0.4V5c-0.3,0-0.5-0.1-0.7-0.1c-0.7,0-1.1,0.3-1.5,0.7C150.6,6.2,150.5,6.9,150.5,7.7z"/>
<path class="st1" d="M166.1,9.3c0,0.7,0.1,1.3,0.3,2l-1.5,0.1c-0.1-0.3-0.3-0.5-0.4-0.9l0,0c-0.3,0.3-0.5,0.5-0.9,0.7
c-0.4,0.1-0.8,0.3-1.2,0.3c-0.5,0-1.1-0.1-1.3-0.4c-0.4-0.3-0.5-0.7-0.5-1.2c0-0.8,0.4-1.3,1.1-1.7c0.7-0.4,1.6-0.7,2.8-0.7V6.7
c0-0.8-0.4-1.3-1.3-1.3c-0.7,0-1.5,0.3-2.2,0.7l-0.1-1.3c0.9-0.4,1.7-0.5,2.7-0.5s1.6,0.3,2,0.7c0.4,0.4,0.7,0.9,0.7,1.7
c0,0.4,0,0.8,0,1.5C166.1,8.6,166.1,9,166.1,9.3z M162.2,9.4c0,0.3,0.1,0.5,0.3,0.7c0.1,0.1,0.4,0.3,0.7,0.3
c0.4,0,0.8-0.1,1.2-0.5V7.9c-0.7,0-1.1,0.3-1.5,0.4C162.3,8.6,162.2,9,162.2,9.4z"/>
<path class="st1" d="M175.6,0.7l1.7-0.3l0.1,0.1v8.7c0,0.7,0.1,1.3,0.3,1.9l-1.6,0.1c-0.1-0.1-0.3-0.5-0.4-0.9l0,0
c-0.5,0.7-1.1,0.9-2,0.9s-1.5-0.3-2-0.9c-0.5-0.7-0.8-1.5-0.8-2.6c0-1.2,0.4-2.2,1.1-3c0.7-0.7,1.6-1.1,2.7-1.1
c0.3,0,0.5,0,0.9,0.1V3C175.7,2,175.7,1.4,175.6,0.7z M173.1,7.7c0,0.8,0.1,1.5,0.4,1.9c0.3,0.5,0.7,0.7,1.2,0.7
c0.4,0,0.8-0.1,1.1-0.4V5c-0.3,0-0.5-0.1-0.7-0.1c-0.7,0-1.1,0.3-1.5,0.7C173.2,6.2,173.1,6.9,173.1,7.7z"/>
</g>
<g>
<path class="st1" d="M78.8,51.2l0.3,0.3l1.2,11h-2l-0.4-4c-0.1-1.2-0.3-3-0.4-5l0,0c-0.3,1.2-0.7,2.8-1.2,5l-1.2,4h-2.3l-1.1-4
c-0.5-1.9-0.9-3.5-1.2-5l0,0c-0.1,1.2-0.3,2.8-0.4,5l-0.4,4h-1.7l1.2-11.2l2.7-0.1l1.3,4.6c0.4,1.6,0.8,3.2,1.1,4.8l0,0
c0.3-1.6,0.5-3.2,1.1-4.8l1.3-4.4L78.8,51.2z"/>
<path class="st1" d="M89.4,58.5l-0.3,0.3h-4.4c0.1,0.8,0.3,1.5,0.8,1.9c0.5,0.4,0.9,0.7,1.6,0.7s1.3-0.1,2-0.5l0.1,1.3
c-0.7,0.4-1.6,0.7-2.7,0.7c-1.2,0-2.2-0.4-3-1.1c-0.7-0.7-1.1-1.7-1.1-3c0-1.3,0.4-2.3,1.1-3.1c0.7-0.8,1.6-1.2,2.7-1.2
c0.9,0,1.7,0.3,2.3,0.9c0.5,0.5,0.8,1.3,0.8,2.4C89.4,58,89.4,58.4,89.4,58.5z M87.7,50.4l0.1,0.4c-0.7,0.9-1.5,1.9-2.6,2.7
l-0.8-0.1c0.7-1.1,1.1-2.2,1.3-3H87.7z M86.3,55.5c-0.4,0-0.8,0.3-1.1,0.7c-0.3,0.4-0.4,1.1-0.5,1.7l2.8-0.1c0-0.1,0-0.3,0-0.5
c0-0.5-0.1-0.9-0.3-1.2C87,55.7,86.7,55.5,86.3,55.5z"/>
<path class="st1" d="M96.4,62.5l-1.7-3.1L93,62.5h-1.6l-0.1-0.3l2.3-3.8l-2.3-4l2-0.3l1.7,3.4l1.6-3.4l1.6,0.1l0.1,0.3L96,58.4
l2.4,4h-2V62.5z"/>
<path class="st1" d="M103.1,51.8c0,0.4-0.1,0.7-0.4,0.9s-0.5,0.4-0.9,0.4c-0.4,0-0.7-0.1-0.8-0.4c-0.3-0.3-0.3-0.5-0.3-0.8
c0-0.4,0.1-0.7,0.4-0.9c0.3-0.3,0.5-0.4,0.9-0.4c0.3,0,0.5,0.1,0.8,0.3C103,51.1,103.1,51.4,103.1,51.8z M100.8,62.5v-5.2
c0-0.9,0-1.9-0.1-2.7l2-0.3l0.3,0.3v7.9H100.8z"/>
<path class="st1" d="M112.3,55l-0.4,1.6c-0.7-0.4-1.2-0.7-1.9-0.7c-0.5,0-1.1,0.3-1.5,0.7c-0.4,0.4-0.5,1.1-0.5,1.9
c0,0.9,0.1,1.6,0.7,2c0.4,0.5,0.9,0.8,1.7,0.8c0.5,0,1.2-0.1,1.7-0.5l0.1,1.3c-0.7,0.4-1.5,0.7-2.4,0.7c-1.2,0-2.2-0.4-2.8-1.1
s-1.1-1.7-1.1-3c0-1.3,0.4-2.3,1.1-3.1c0.8-0.8,1.7-1.2,2.8-1.2C110.8,54.3,111.6,54.6,112.3,55z"/>
<path class="st1" d="M121.4,58.5c0,1.3-0.4,2.4-1.1,3.1s-1.6,1.1-2.7,1.1c-1.1,0-1.9-0.4-2.6-1.1s-1.1-1.7-1.1-3
c0-1.3,0.4-2.4,1.1-3.1s1.6-1.2,2.7-1.2c1.1,0,2,0.4,2.7,1.1C121.1,56.2,121.4,57.3,121.4,58.5z M116,58.5c0,2,0.5,3,1.6,3
c0.5,0,0.9-0.3,1.2-0.8c0.3-0.5,0.4-1.2,0.4-2.2c0-2-0.5-3.1-1.6-3.1c-0.5,0-0.9,0.3-1.2,0.8C116.3,56.8,116,57.6,116,58.5z"/>
</g>
<g>
<path class="st2" d="M83.5,41v3.8H68.3V25.3c0-2.4-0.1-4.6-0.4-6.5l4.6-0.4L73,19v22.2L83.5,41z"/>
<path class="st2" d="M100.4,39.5c0,1.9,0.3,3.6,0.8,5.1l-3.9,0.4c-0.4-0.7-0.8-1.6-1.1-2.6h-0.1c-0.5,0.7-1.3,1.3-2.3,1.9
c-0.9,0.5-2.2,0.8-3.2,0.8c-1.5,0-2.7-0.4-3.6-1.2c-0.9-0.8-1.3-1.9-1.3-3.4c0-2,0.9-3.6,2.8-4.6c1.9-1.1,4.3-1.6,7.4-1.7v-1.7
c0-2.3-1.2-3.4-3.5-3.4c-1.9,0-3.8,0.5-5.6,1.6l-0.3-3.6c2.4-0.9,4.7-1.5,7.1-1.5c2.3,0,4,0.5,5.2,1.6c1.2,1.1,1.7,2.6,1.7,4.6
c0,0.9,0,2.3,0,4C100.4,37.7,100.4,38.9,100.4,39.5z M90.2,39.8c0,0.7,0.3,1.3,0.7,1.7c0.4,0.5,1.1,0.7,1.7,0.7
c1.2,0,2.2-0.4,3.1-1.3v-5.1c-1.6,0.1-3,0.5-4,1.2C90.8,37.8,90.2,38.7,90.2,39.8z"/>
</g>
<g>
<path class="st2" d="M126.3,37.3c0,2.4-0.8,4.2-2.6,5.5c-1.7,1.3-4,2-6.9,2c-3.1,0-5.5-0.5-7.3-1.5L110,39c0.9,0.5,2,1.1,3.5,1.3
c1.3,0.3,2.6,0.4,3.6,0.4c1.3,0,2.4-0.3,3.2-0.9c0.8-0.5,1.1-1.3,1.1-2.4c0-0.3,0-0.5-0.1-0.8c0-0.3-0.1-0.5-0.3-0.7
s-0.3-0.4-0.4-0.7c-0.1-0.3-0.4-0.4-0.4-0.5c-0.1-0.1-0.3-0.3-0.7-0.5c-0.3-0.3-0.5-0.4-0.7-0.4c-0.1-0.1-0.4-0.3-0.8-0.4
c-0.4-0.3-0.7-0.4-0.8-0.4c-0.1-0.1-0.4-0.3-0.9-0.4c-0.4-0.3-0.7-0.4-0.8-0.4c-0.9-0.4-1.6-0.8-2.2-1.2c-0.5-0.4-1.2-0.8-1.7-1.5
c-0.7-0.5-1.1-1.2-1.3-2c-0.3-0.8-0.4-1.6-0.4-2.7c0-2.4,0.9-4.2,2.7-5.5c1.7-1.3,4.2-2,7-2c2.4,0,4.4,0.4,6.2,1.1l-0.7,4.3
c-1.7-1.1-3.6-1.5-5.6-1.5c-1.5,0-2.6,0.3-3.4,0.9c-0.8,0.7-1.2,1.3-1.2,2.4c0,0.4,0,0.7,0.1,1.1c0.1,0.3,0.3,0.7,0.5,0.9
c0.3,0.3,0.5,0.5,0.7,0.8c0.1,0.1,0.5,0.4,0.9,0.7c0.5,0.3,0.8,0.5,1.1,0.5c0.1,0.1,0.5,0.3,1.2,0.7c0.7,0.3,1.1,0.5,1.2,0.5
c0.8,0.4,1.5,0.8,2.2,1.2c0.5,0.4,1.2,0.8,1.7,1.5c0.7,0.7,1.1,1.3,1.3,2.2C126.1,35.4,126.3,36.3,126.3,37.3z"/>
<path class="st2" d="M143.9,39.1c0,1.9,0.3,3.8,0.8,5.4l-4,0.4c-0.4-0.7-0.8-1.6-1.1-2.7h-0.1c-0.5,0.8-1.3,1.3-2.4,1.9
c-1.1,0.5-2.2,0.8-3.4,0.8c-1.6,0-2.8-0.4-3.8-1.2c-0.9-0.8-1.3-2-1.3-3.5c0-2.2,0.9-3.6,3-4.7c1.9-1.1,4.4-1.6,7.7-1.7v-1.9
c0-2.3-1.2-3.5-3.6-3.5c-2,0-3.9,0.5-5.9,1.7l-0.3-3.8c2.4-1.1,4.8-1.5,7.4-1.5c2.4,0,4.2,0.5,5.4,1.6c1.2,1.1,1.9,2.7,1.9,4.8
c0,0.9,0,2.3,0,4.2C143.9,37.1,143.9,38.3,143.9,39.1z M133.4,39.3c0,0.7,0.3,1.3,0.7,1.9c0.4,0.5,1.1,0.8,1.9,0.8
c1.2,0,2.3-0.4,3.2-1.3v-5.2c-1.7,0.1-3.1,0.5-4.2,1.2S133.4,38.2,133.4,39.3z"/>
<path class="st2" d="M153.7,44.4h-4.8V21.9c0-2-0.1-4.2-0.4-6.5l4.8-0.5l0.5,0.5L153.7,44.4L153.7,44.4z"/>
<path class="st2" d="M163.4,44.4h-4.8V21.9c0-2-0.1-4.2-0.4-6.5l4.8-0.5l0.5,0.5L163.4,44.4L163.4,44.4z"/>
<path class="st2" d="M183.7,34.7l-0.5,0.5h-10.9c0.1,2,0.8,3.6,1.7,4.6c0.9,0.9,2.4,1.5,4,1.5c1.6,0,3.2-0.4,4.8-1.3l0.3,3.2
c-1.7,1.1-3.9,1.6-6.5,1.6c-3,0-5.2-0.8-7-2.6s-2.6-4.2-2.6-7.3c0-3.1,0.8-5.6,2.6-7.5c1.7-1.9,3.9-2.8,6.6-2.8
c2.3,0,4.2,0.7,5.5,2.2c1.3,1.5,2,3.4,2,5.6C183.8,33.5,183.8,34.2,183.7,34.7z M176,27.6c-1.1,0-2,0.5-2.7,1.6
c-0.7,1.1-1.1,2.6-1.2,4.3l6.7-0.3c0-0.3,0-0.8,0-1.3c0-1.2-0.3-2.3-0.8-3.1S176.9,27.6,176,27.6z"/>
</g>
<g>
<path id="Path_483" class="st1" d="M187,39.4h1.5c0.3,0,0.7,0.1,1,0.2c0.3,0.2,0.5,0.5,0.5,0.8c0,0.3-0.1,0.6-0.3,0.8
c-0.1,0.1-0.3,0.2-0.5,0.3c0.4,0.1,0.6,0.3,0.6,0.8c0,0.4,0.1,0.9,0.3,1.3h-0.6c-0.1-0.4-0.2-0.7-0.2-1.1
c-0.1-0.6-0.2-0.8-0.9-0.8h-0.7v1.9H187V39.4 M187.5,41.2h0.9c0.2,0,0.4,0,0.6-0.1c0.2-0.1,0.3-0.3,0.3-0.6c0-0.7-0.6-0.7-0.8-0.7
h-0.9V41.2z"/>
<path id="Path_484" class="st1" d="M191.9,41.5c0,1.9-1.6,3.4-3.4,3.3s-3.4-1.6-3.3-3.4c0-1.9,1.5-3.3,3.4-3.3
C190.4,38.1,191.9,39.6,191.9,41.5z M188.5,37.8c-2.1,0-3.7,1.6-3.8,3.7c0,2.1,1.6,3.7,3.7,3.8c2.1,0,3.7-1.6,3.8-3.7c0,0,0,0,0,0
C192.2,39.4,190.5,37.8,188.5,37.8z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -18,7 +18,11 @@ export default function Header() {
</button> </button>
<h1 className="ml-4 text-xl font-semibold"> <h1 className="ml-4 text-xl font-semibold">
<Link to="/"> <Link to="/">
<img src="/lasalle-logo.svg" alt="La Salle Logo" className="h-10" /> <img
src="/tanstack-word-logo-white.svg"
alt="TanStack Logo"
className="h-10"
/>
</Link> </Link>
</h1> </h1>
</header> </header>

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,5 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import { useNavigate, useParams } from '@tanstack/react-router'
import { Plus, Search, BookOpen, Trash2, Library, Edit3 } from 'lucide-react' import { Plus, Search, BookOpen, Trash2, Library, Edit3 } from 'lucide-react'
import { useState } from 'react' import { useEffect, useState } from 'react'
import { import {
AlertDialog, AlertDialog,
@@ -34,13 +30,40 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
useCreateBibliografia,
useDeleteBibliografia,
useSubjectBibliografia,
useUpdateBibliografia,
} from '@/data/hooks/useSubjects'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
// import { toast } from 'sonner';
// import { mockLibraryResources } from '@/data/mockAsignaturaData';
export const mockLibraryResources = [
{
id: 'lib-1',
titulo: 'Deep Learning',
autor: 'Goodfellow, I., Bengio, Y., & Courville, A.',
editorial: 'MIT Press',
anio: 2016,
isbn: '9780262035613',
disponible: true,
},
{
id: 'lib-2',
titulo: 'Artificial Intelligence: A Modern Approach',
autor: 'Russell, S., & Norvig, P.',
editorial: 'Pearson',
anio: 2020,
isbn: '9780134610993',
disponible: true,
},
{
id: 'lib-3',
titulo: 'Hands-On Machine Learning',
autor: 'Aurélien Géron',
editorial: "O'Reilly Media",
anio: 2019,
isbn: '9781492032649',
disponible: false,
},
]
// --- Interfaces --- // --- Interfaces ---
export interface BibliografiaEntry { export interface BibliografiaEntry {
@@ -53,77 +76,79 @@ export interface BibliografiaEntry {
fuenteBiblioteca?: any fuenteBiblioteca?: any
} }
export function BibliographyItem() { interface BibliografiaTabProps {
const navigate = useNavigate() id: string
const { planId, asignaturaId } = useParams({ bibliografia: Array<BibliografiaEntry>
from: '/planes/$planId/asignaturas/$asignaturaId', onSave: (bibliografia: Array<BibliografiaEntry>) => void
}) isSaving: boolean
}
// --- 1. Única fuente de verdad: La Query --- export function BibliographyItem({
const { data: bibliografia = [], isLoading } = bibliografia,
useSubjectBibliografia(asignaturaId) id,
onSave,
isSaving,
}: BibliografiaTabProps) {
console.log(id)
// --- 2. Mutaciones --- const { data: bibliografia2, isLoading: loadinasignatura } =
const { mutate: crearBibliografia } = useCreateBibliografia() useSubjectBibliografia(id)
const { mutate: actualizarBibliografia } = useUpdateBibliografia(asignaturaId) const [entries, setEntries] = useState<Array<BibliografiaEntry>>(bibliografia)
const { mutate: eliminarBibliografia } = useDeleteBibliografia(asignaturaId) const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
// --- 3. Estados de UI (Solo para diálogos y edición) ---
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false) const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false)
const [deleteId, setDeleteId] = useState<string | null>(null) const [deleteId, setDeleteId] = useState<string | null>(null)
const [editingId, setEditingId] = useState<string | null>(null) const [editingId, setEditingId] = useState<string | null>(null)
const [newEntryType, setNewEntryType] = useState<'BASICA' | 'COMPLEMENTARIA'>(
console.log('Datos actuales en el front:', bibliografia) 'BASICA',
// --- 4. Derivación de datos (Se calculan en cada render) ---
const basicaEntries = bibliografia.filter((e) => e.tipo === 'BASICA')
const complementariaEntries = bibliografia.filter(
(e) => e.tipo === 'COMPLEMENTARIA',
) )
// --- Handlers Conectados a la Base de Datos --- useEffect(() => {
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 newEntry: BibliografiaEntry = {
id: `manual-${Date.now()}`,
tipo: newEntryType,
cita,
}
setEntries([...entries, newEntry])
setIsAddDialogOpen(false)
// toast.success('Referencia manual añadida');
}
const handleAddFromLibrary = ( const handleAddFromLibrary = (
resource: any, resource: any,
tipo: 'BASICA' | 'COMPLEMENTARIA', tipo: 'BASICA' | 'COMPLEMENTARIA',
) => { ) => {
const cita = `${resource.autor} (${resource.anio}). ${resource.titulo}. ${resource.editorial}.` const cita = `${resource.autor} (${resource.anio}). ${resource.titulo}. ${resource.editorial}.`
crearBibliografia( const newEntry: BibliografiaEntry = {
{ id: `lib-ref-${Date.now()}`,
asignatura_id: asignaturaId,
tipo, tipo,
cita, cita,
tipo_fuente: 'BIBLIOTECA', fuenteBibliotecaId: resource.id,
biblioteca_item_id: resource.id, fuenteBiblioteca: resource,
}, }
{ setEntries([...entries, newEntry])
onSuccess: () => setIsLibraryDialogOpen(false), setIsLibraryDialogOpen(false)
}, // toast.success('Añadido desde biblioteca');
)
} }
const handleUpdateCita = (id: string, nuevaCita: string) => { const handleUpdateCita = (id: string, cita: string) => {
actualizarBibliografia( setEntries(entries.map((e) => (e.id === id ? { ...e, cita } : e)))
{
id,
updates: { cita: nuevaCita },
},
{
onSuccess: () => setEditingId(null),
},
)
} }
const onConfirmDelete = () => {
if (deleteId) {
eliminarBibliografia(deleteId, {
onSuccess: () => setDeleteId(null),
})
}
}
if (isLoading)
return <div className="p-10 text-center">Cargando bibliografía...</div>
return ( return (
<div className="animate-in fade-in mx-auto max-w-5xl space-y-8 py-10 duration-500"> <div className="animate-in fade-in 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">
@@ -151,28 +176,26 @@ export function BibliographyItem() {
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<LibrarySearchDialog <LibrarySearchDialog
// CORRECCIÓN: Usamos 'bibliografia' en lugar de 'bibliografia2'
resources={[]} // Aquí deberías pasar el catálogo general, no la bibliografía de la asignatura
onSelect={handleAddFromLibrary} onSelect={handleAddFromLibrary}
// CORRECCIÓN: Usamos 'bibliografia' en lugar de 'entries' existingIds={entries.map((e) => e.fuenteBibliotecaId || '')}
existingIds={bibliografia.map(
(e) => e.biblioteca_item_id || '',
)}
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Button <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
onClick={() => <DialogTrigger asChild>
navigate({ <Button variant="outline">
to: `/planes/${planId}/asignaturas/${asignaturaId}/bibliografia/nueva`, <Plus className="mr-2 h-4 w-4" /> Añadir manual
resetScroll: false,
})
}
className="ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-11 items-center justify-center gap-2 rounded-md px-8 text-sm font-medium shadow-md transition-colors"
>
<Plus className="mr-2 h-4 w-4" /> Agregar Bibliografía
</Button> </Button>
</DialogTrigger>
<DialogContent>
<AddManualDialog
tipo={newEntryType}
onTypeChange={setNewEntryType}
onAdd={handleAddManual}
/>
</DialogContent>
</Dialog>
</div> </div>
</div> </div>
@@ -234,7 +257,13 @@ export function BibliographyItem() {
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel> <AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={onConfirmDelete} className="bg-red-600"> <AlertDialogAction
onClick={() => {
setEntries(entries.filter((e) => e.id !== deleteId))
setDeleteId(null)
}}
className="bg-red-600"
>
Eliminar Eliminar
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
@@ -344,16 +373,57 @@ function BibliografiaCard({
) )
} }
function LibrarySearchDialog({ resources, onSelect, existingIds }: any) { function AddManualDialog({ tipo, onTypeChange, onAdd }: any) {
const [cita, setCita] = useState('')
return (
<div className="space-y-4 py-4">
<DialogHeader>
<DialogTitle>Referencia Manual</DialogTitle>
</DialogHeader>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase">
Tipo
</label>
<Select value={tipo} onValueChange={onTypeChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="BASICA">Básica</SelectItem>
<SelectItem value="COMPLEMENTARIA">Complementaria</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase">
Cita APA
</label>
<Textarea
value={cita}
onChange={(e) => setCita(e.target.value)}
placeholder="Autor, A. (Año). Título..."
className="min-h-[120px]"
/>
</div>
<Button
onClick={() => onAdd(cita)}
disabled={!cita.trim()}
className="w-full bg-blue-600"
>
Añadir a la lista
</Button>
</div>
)
}
function LibrarySearchDialog({ onSelect, existingIds }: any) {
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [tipo, setTipo] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA') const [tipo, setTipo] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA')
const filtered = (resources || []).filter( const filtered = mockLibraryResources.filter(
(r: any) => (r) =>
!existingIds.includes(r.id) && !existingIds.includes(r.id) &&
r.titulo?.toLowerCase().includes(search.toLowerCase()), r.titulo.toLowerCase().includes(search.toLowerCase()),
) )
console.log(filtered)
console.log(resources)
return ( return (
<div className="space-y-4 py-2"> <div className="space-y-4 py-2">
@@ -381,7 +451,7 @@ function LibrarySearchDialog({ resources, onSelect, existingIds }: any) {
</Select> </Select>
</div> </div>
<div className="max-h-[300px] space-y-2 overflow-y-auto pr-2"> <div className="max-h-[300px] space-y-2 overflow-y-auto pr-2">
{filtered.map((res: any) => ( {filtered.map((res) => (
<div <div
key={res.id} key={res.id}
onClick={() => onSelect(res, tipo)} onClick={() => onSelect(res, tipo)}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,4 +1,3 @@
import { useParams } from '@tanstack/react-router'
import { format, parseISO } from 'date-fns' import { format, parseISO } from 'date-fns'
import { es } from 'date-fns/locale' import { es } from 'date-fns/locale'
import { import {
@@ -54,10 +53,7 @@ const tipoConfig: Record<string, { label: string; icon: any; color: string }> =
}, },
} }
export function HistorialTab() { export function HistorialTab({ asignaturaId }) {
const { asignaturaId } = useParams({
from: '/planes/$planId/asignaturas/$asignaturaId/historial',
})
// 1. Obtenemos los datos directamente dentro del componente // 1. Obtenemos los datos directamente dentro del componente
const { data: rawData, isLoading } = useSubjectHistorial(asignaturaId) const { data: rawData, isLoading } = useSubjectHistorial(asignaturaId)

File diff suppressed because it is too large Load Diff

View File

@@ -1,208 +0,0 @@
import { Check, Loader2, BookOpen, Clock, ListChecks } from 'lucide-react'
import { useState } from 'react'
import type { IASugerencia } from '@/types/asignatura'
import { Button } from '@/components/ui/button'
import {
useUpdateAsignatura,
useSubject,
useUpdateSubjectRecommendation,
} from '@/data'
import { cn } from '@/lib/utils'
interface ImprovementCardProps {
sug: IASugerencia
asignaturaId: string
onApplied: (campoKey: string) => void
}
export function ImprovementCard({
sug,
asignaturaId,
onApplied,
}: ImprovementCardProps) {
const { data: asignatura } = useSubject(asignaturaId)
const updateAsignatura = useUpdateAsignatura()
const updateRecommendation = useUpdateSubjectRecommendation()
const [isApplying, setIsApplying] = useState(false)
const handleApply = async () => {
if (!asignatura) return
setIsApplying(true)
try {
// 1. Identificar a qué columna debe ir el guardado
let patchData = {}
if (sug.campoKey === 'contenido_tematico') {
// Se guarda directamente en la columna contenido_tematico
patchData = { contenido_tematico: sug.valorSugerido }
} else if (sug.campoKey === 'criterios_de_evaluacion') {
// Se guarda directamente en la columna criterios_de_evaluacion
patchData = { criterios_de_evaluacion: sug.valorSugerido }
} else {
// Otros campos (ciclo, fines, etc.) se siguen guardando en el JSON de la columna 'datos'
patchData = {
datos: {
...asignatura.datos,
[sug.campoKey]: sug.valorSugerido,
},
}
}
// 2. Ejecutar la actualización con la estructura correcta
await updateAsignatura.mutateAsync({
asignaturaId: asignaturaId as any,
patch: patchData as any,
})
// 3. Marcar la recomendación como aplicada
await updateRecommendation.mutateAsync({
mensajeId: sug.messageId,
campoAfectado: sug.campoKey,
})
console.log(sug.campoKey)
onApplied(sug.campoKey)
} catch (error) {
console.error('Error al aplicar mejora:', error)
} finally {
setIsApplying(false)
}
}
// --- FUNCIÓN PARA RENDERIZAR EL CONTENIDO DE FORMA SEGURA ---
const renderContenido = (valor: any) => {
// Si no es un array, es texto simple
if (!Array.isArray(valor)) {
return <p className="italic">"{String(valor)}"</p>
}
// --- CASO 1: CONTENIDO TEMÁTICO (Detectamos si el primer objeto tiene 'unidad') ---
if (valor[0]?.hasOwnProperty('unidad')) {
return (
<div className="space-y-3">
{valor.map((u: any, idx: number) => (
<div
key={idx}
className="rounded-md border border-teal-100 bg-white p-2 shadow-sm"
>
<div className="mb-1 flex items-center gap-2 border-b border-slate-50 pb-1 text-[11px] font-bold text-teal-800">
<BookOpen size={12} /> Unidad {u.unidad}: {u.titulo}
</div>
<ul className="space-y-1">
{u.temas?.map((t: any, tidx: number) => (
<li
key={tidx}
className="flex items-start justify-between gap-2 text-[10px] text-slate-600"
>
<span className="leading-tight"> {t.nombre}</span>
<span className="flex shrink-0 items-center gap-0.5 font-mono text-slate-400">
<Clock size={10} /> {t.horasEstimadas}h
</span>
</li>
))}
</ul>
</div>
))}
</div>
)
}
// --- CASO 2: CRITERIOS DE EVALUACIÓN (Detectamos si tiene 'criterio') ---
if (valor[0]?.hasOwnProperty('criterio')) {
return (
<div className="space-y-2">
<div className="mb-1 flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase">
<ListChecks size={12} /> Desglose de evaluación
</div>
{valor.map((c: any, idx: number) => (
<div
key={idx}
className="flex items-center justify-between gap-3 rounded-md border border-slate-100 bg-white p-2 shadow-sm"
>
<span className="text-[11px] leading-tight text-slate-700">
{c.criterio}
</span>
<div className="flex shrink-0 items-center gap-1 rounded-full border border-orange-100 bg-orange-50 px-2 py-0.5 text-[10px] font-bold text-orange-600">
{c.porcentaje}%
</div>
</div>
))}
{/* Opcional: Suma total para verificar que de 100% */}
<div className="pt-1 text-right text-[9px] font-medium text-slate-400">
Total:{' '}
{valor.reduce(
(acc: number, curr: any) => acc + (curr.porcentaje || 0),
0,
)}
%
</div>
</div>
)
}
// Caso por defecto (Array genérico)
return (
<pre className="text-[10px]">
{/* JSON.stringify(valor, null, 2)*/ 'hola'}
</pre>
)
}
// --- ESTADO APLICADO ---
if (sug.aceptada) {
return (
<div className="flex flex-col rounded-xl border border-slate-100 bg-white p-3 opacity-80 shadow-sm">
<div className="mb-3 flex items-center justify-between gap-4">
<span className="text-sm font-bold text-slate-800">
{sug.campoNombre}
</span>
<div className="flex items-center gap-1.5 rounded-full border border-slate-100 bg-slate-50 px-3 py-1 text-xs font-medium text-slate-400">
<Check size={14} />
Aplicado
</div>
</div>
<div className="rounded-lg border border-teal-100 bg-teal-50/30 p-3 text-xs leading-relaxed text-slate-500">
{renderContenido(sug.valorSugerido)}
</div>
</div>
)
}
// --- ESTADO PENDIENTE ---
return (
<div className="group flex flex-col rounded-xl border border-teal-100 bg-white p-3 shadow-sm transition-all hover:border-teal-200">
<div className="mb-3 flex items-center justify-between gap-4">
<span className="max-w-[150px] truncate rounded-lg border border-teal-100 bg-teal-50/50 px-2.5 py-1 text-[10px] font-bold tracking-wider text-teal-700 uppercase">
{sug.campoNombre}
</span>
<Button
size="sm"
disabled={isApplying || !asignatura}
className="h-8 w-auto bg-teal-600 px-4 text-xs font-semibold shadow-sm hover:bg-teal-700"
onClick={handleApply}
>
{isApplying ? (
<Loader2 size={14} className="mr-1.5 animate-spin" />
) : (
<Check size={14} className="mr-1.5" />
)}
{isApplying ? 'Aplicando...' : 'Aplicar mejora'}
</Button>
</div>
<div
className={cn(
'rounded-lg border border-dashed border-slate-200 bg-slate-50/50 p-3 text-xs leading-relaxed text-slate-600',
!Array.isArray(sug.valorSugerido) && 'line-clamp-4 italic',
)}
>
{renderContenido(sug.valorSugerido)}
</div>
</div>
)
}

View File

@@ -26,7 +26,7 @@ export default function PasoSugerenciasForm({
onChange: Dispatch<SetStateAction<NewSubjectWizardState>> onChange: Dispatch<SetStateAction<NewSubjectWizardState>>
}) { }) {
const enfoque = wizard.iaMultiple?.enfoque ?? '' const enfoque = wizard.iaMultiple?.enfoque ?? ''
const cantidadDeSugerencias = wizard.iaMultiple?.cantidadDeSugerencias ?? 5 const cantidadDeSugerencias = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
const isLoading = wizard.iaMultiple?.isLoading ?? false const isLoading = wizard.iaMultiple?.isLoading ?? false
const [showConservacionTooltip, setShowConservacionTooltip] = useState(false) const [showConservacionTooltip, setShowConservacionTooltip] = useState(false)
@@ -163,7 +163,7 @@ export default function PasoSugerenciasForm({
Cantidad de sugerencias Cantidad de sugerencias
</Label> </Label>
<Input <Input
placeholder="Ej. 5" placeholder="Ej. 10"
value={cantidadDeSugerencias} value={cantidadDeSugerencias}
type="number" type="number"
min={1} min={1}

View File

@@ -1,12 +1,11 @@
import { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
import { useNavigate } from '@tanstack/react-router' import { useNavigate } from '@tanstack/react-router'
import { Loader2 } from 'lucide-react' import { Loader2 } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react' import { useState } from 'react'
import type { AISubjectUnifiedInput } from '@/data' import type { AIGenerateSubjectInput, AIGenerateSubjectJsonInput } from '@/data'
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types' import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
import type { TablesInsert } from '@/types/supabase' import type { TablesInsert } from '@/types/supabase'
import type { RealtimeChannel } from '@supabase/supabase-js'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
@@ -14,7 +13,6 @@ import {
useGenerateSubjectAI, useGenerateSubjectAI,
qk, qk,
useCreateSubjectManual, useCreateSubjectManual,
subjects_get_maybe,
} from '@/data' } from '@/data'
export function WizardControls({ export function WizardControls({
@@ -43,154 +41,6 @@ export function WizardControls({
const generateSubjectAI = useGenerateSubjectAI() const generateSubjectAI = useGenerateSubjectAI()
const createSubjectManual = useCreateSubjectManual() const createSubjectManual = useCreateSubjectManual()
const [isSpinningIA, setIsSpinningIA] = useState(false) const [isSpinningIA, setIsSpinningIA] = useState(false)
const cancelledRef = useRef(false)
const realtimeChannelRef = useRef<RealtimeChannel | null>(null)
const watchSubjectIdRef = useRef<string | null>(null)
const watchTimeoutRef = useRef<number | null>(null)
useEffect(() => {
cancelledRef.current = false
return () => {
cancelledRef.current = true
}
}, [])
const stopSubjectWatch = useCallback(() => {
if (watchTimeoutRef.current) {
window.clearTimeout(watchTimeoutRef.current)
watchTimeoutRef.current = null
}
watchSubjectIdRef.current = null
const ch = realtimeChannelRef.current
if (ch) {
realtimeChannelRef.current = null
try {
supabaseBrowser().removeChannel(ch)
} catch {
// noop
}
}
}, [])
useEffect(() => {
return () => {
stopSubjectWatch()
}
}, [stopSubjectWatch])
const handleSubjectReady = (args: {
id: string
plan_estudio_id: string
estado?: unknown
}) => {
if (cancelledRef.current) return
const estado = String(args.estado ?? '').toLowerCase()
if (estado === 'generando') return
stopSubjectWatch()
setIsSpinningIA(false)
setWizard((w) => ({ ...w, isLoading: false }))
navigate({
to: `/planes/${args.plan_estudio_id}/asignaturas/${args.id}`,
state: { showConfetti: true },
})
}
const beginSubjectWatch = (args: { subjectId: string; planId: string }) => {
stopSubjectWatch()
watchSubjectIdRef.current = args.subjectId
// Timeout de seguridad (mismo límite que teníamos con polling)
watchTimeoutRef.current = window.setTimeout(
() => {
if (cancelledRef.current) return
if (watchSubjectIdRef.current !== args.subjectId) return
stopSubjectWatch()
setIsSpinningIA(false)
setWizard((w) => ({
...w,
isLoading: false,
errorMessage:
'La generación está tardando demasiado. Intenta de nuevo en unos minutos.',
}))
},
6 * 60 * 1000,
)
const supabase = supabaseBrowser()
const channel = supabase.channel(`asignaturas-status-${args.subjectId}`)
realtimeChannelRef.current = channel
channel.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'asignaturas',
filter: `id=eq.${args.subjectId}`,
},
(payload) => {
if (cancelledRef.current) return
const next: any = (payload as any)?.new
if (!next?.id || !next?.plan_estudio_id) return
handleSubjectReady({
id: String(next.id),
plan_estudio_id: String(next.plan_estudio_id),
estado: next.estado,
})
},
)
channel.subscribe((status) => {
if (cancelledRef.current) return
if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
stopSubjectWatch()
setIsSpinningIA(false)
setWizard((w) => ({
...w,
isLoading: false,
errorMessage:
'No se pudo suscribir al estado de la asignatura. Intenta de nuevo.',
}))
}
})
}
const uploadAiAttachments = async (args: {
planId: string
files: Array<{ file: File }>
}): Promise<Array<string>> => {
const supabase = supabaseBrowser()
if (!args.files.length) return []
const runId = crypto.randomUUID()
const basePath = `planes/${args.planId}/asignaturas/ai/${runId}`
const keys: Array<string> = []
for (const f of args.files) {
const safeName = (f.file.name || 'archivo').replace(/[\\/]+/g, '_')
const key = `${basePath}/${crypto.randomUUID()}-${safeName}`
const { error } = await supabase.storage
.from('ai-storage')
.upload(key, f.file, {
contentType: f.file.type || undefined,
})
if (error) throw new Error(error.message)
keys.push(key)
}
return keys
}
const handleCreate = async () => { const handleCreate = async () => {
setWizard((w) => ({ setWizard((w) => ({
...w, ...w,
@@ -198,99 +48,48 @@ export function WizardControls({
errorMessage: null, errorMessage: null,
})) }))
let startedWaiting = false
try { try {
if (wizard.tipoOrigen === 'IA_SIMPLE') { if (wizard.tipoOrigen === 'IA_SIMPLE') {
if (!wizard.plan_estudio_id) { const aiInput: AIGenerateSubjectInput = {
throw new Error('Plan de estudio inválido.')
}
if (!wizard.datosBasicos.estructuraId) {
throw new Error('Estructura inválida.')
}
if (!wizard.datosBasicos.nombre.trim()) {
throw new Error('Nombre inválido.')
}
if (wizard.datosBasicos.creditos == null) {
throw new Error('Créditos inválidos.')
}
console.log(`${new Date().toISOString()} - Insertando asignatura IA`)
const supabase = supabaseBrowser()
const placeholder: TablesInsert<'asignaturas'> = {
plan_estudio_id: wizard.plan_estudio_id, plan_estudio_id: wizard.plan_estudio_id,
estructura_id: wizard.datosBasicos.estructuraId, datosBasicos: {
nombre: wizard.datosBasicos.nombre, nombre: wizard.datosBasicos.nombre,
codigo: wizard.datosBasicos.codigo ?? null, codigo: wizard.datosBasicos.codigo,
tipo: wizard.datosBasicos.tipo ?? undefined, tipo: wizard.datosBasicos.tipo!,
creditos: wizard.datosBasicos.creditos, creditos: wizard.datosBasicos.creditos!,
horas_academicas: wizard.datosBasicos.horasAcademicas ?? null, horasIndependientes: wizard.datosBasicos.horasIndependientes,
horas_independientes: wizard.datosBasicos.horasIndependientes ?? null, horasAcademicas: wizard.datosBasicos.horasAcademicas,
estado: 'generando', estructuraId: wizard.datosBasicos.estructuraId!,
tipo_origen: 'IA',
}
const { data: inserted, error: insertError } = await supabase
.from('asignaturas')
.insert(placeholder)
.select('id,plan_estudio_id')
.single()
if (insertError) throw new Error(insertError.message)
const subjectId = inserted.id
setIsSpinningIA(true)
// Inicia watch realtime antes de disparar la Edge para no perder updates.
startedWaiting = true
beginSubjectWatch({ subjectId, planId: wizard.plan_estudio_id })
const archivosAdjuntos = await uploadAiAttachments({
planId: wizard.plan_estudio_id,
files: (wizard.iaConfig?.archivosAdjuntos ?? []).map((x) => ({
file: x.file,
})),
})
const payload: AISubjectUnifiedInput = {
datosUpdate: {
id: subjectId,
plan_estudio_id: wizard.plan_estudio_id,
estructura_id: wizard.datosBasicos.estructuraId,
nombre: wizard.datosBasicos.nombre,
codigo: wizard.datosBasicos.codigo ?? null,
tipo: wizard.datosBasicos.tipo ?? null,
creditos: wizard.datosBasicos.creditos,
horas_academicas: wizard.datosBasicos.horasAcademicas ?? null,
horas_independientes:
wizard.datosBasicos.horasIndependientes ?? null,
}, },
iaConfig: { iaConfig: {
descripcionEnfoqueAcademico: descripcionEnfoqueAcademico:
wizard.iaConfig?.descripcionEnfoqueAcademico ?? undefined, wizard.iaConfig!.descripcionEnfoqueAcademico,
instruccionesAdicionalesIA: instruccionesAdicionalesIA:
wizard.iaConfig?.instruccionesAdicionalesIA ?? undefined, wizard.iaConfig!.instruccionesAdicionalesIA,
archivosAdjuntos, archivosReferencia: wizard.iaConfig!.archivosReferencia,
repositoriosReferencia:
wizard.iaConfig!.repositoriosReferencia || [],
archivosAdjuntos: wizard.iaConfig!.archivosAdjuntos || [],
}, },
} }
console.log( console.log(
`${new Date().toISOString()} - Disparando Edge IA asignatura (unified)`, `${new Date().toISOString()} - Enviando a generar asignatura con IA`,
) )
await generateSubjectAI.mutateAsync(payload as any) setIsSpinningIA(true)
const asignatura = await generateSubjectAI.mutateAsync(aiInput)
// await new Promise((resolve) => setTimeout(resolve, 20000)) // debug
setIsSpinningIA(false)
// console.log(
// `${new Date().toISOString()} - Asignatura IA generada`,
// asignatura,
// )
// Fallback: una lectura puntual por si el UPDATE llegó antes de suscribir. navigate({
const latest = await subjects_get_maybe(subjectId) to: `/planes/${wizard.plan_estudio_id}/asignaturas/${asignatura.id}`,
if (latest) { state: { showConfetti: true },
handleSubjectReady({
id: latest.id as any,
plan_estudio_id: latest.plan_estudio_id as any,
estado: (latest as any).estado,
}) })
}
return return
} }
@@ -309,15 +108,6 @@ export function WizardControls({
const supabase = supabaseBrowser() const supabase = supabaseBrowser()
setIsSpinningIA(true)
const archivosAdjuntos = await uploadAiAttachments({
planId: wizard.plan_estudio_id,
files: (wizard.iaConfig?.archivosAdjuntos ?? []).map((x) => ({
file: x.file,
})),
})
const placeholders: Array<TablesInsert<'asignaturas'>> = selected.map( const placeholders: Array<TablesInsert<'asignaturas'>> = selected.map(
(s): TablesInsert<'asignaturas'> => ({ (s): TablesInsert<'asignaturas'> => ({
plan_estudio_id: wizard.plan_estudio_id, plan_estudio_id: wizard.plan_estudio_id,
@@ -351,33 +141,16 @@ export function WizardControls({
// Disparar generación en paralelo (no bloquear navegación) // Disparar generación en paralelo (no bloquear navegación)
insertedIds.forEach((id, idx) => { insertedIds.forEach((id, idx) => {
const s = selected[idx] const s = selected[idx]
const creditosForEdge = const payload: AIGenerateSubjectJsonInput = {
typeof s.creditos === 'number' && s.creditos > 0
? s.creditos
: undefined
const payload: AISubjectUnifiedInput = {
datosUpdate: {
id, id,
plan_estudio_id: wizard.plan_estudio_id,
estructura_id: wizard.estructuraId ?? undefined,
nombre: s.nombre,
codigo: s.codigo ?? null,
tipo: s.tipo ?? null,
creditos: creditosForEdge,
horas_academicas: s.horasAcademicas ?? null,
horas_independientes: s.horasIndependientes ?? null,
numero_ciclo: s.numero_ciclo ?? null,
linea_plan_id: s.linea_plan_id ?? null,
},
iaConfig: {
descripcionEnfoqueAcademico: s.descripcion, descripcionEnfoqueAcademico: s.descripcion,
instruccionesAdicionalesIA: // (opcionales) parches directos si el edge los usa
wizard.iaConfig?.instruccionesAdicionalesIA ?? undefined, estructura_id: wizard.estructuraId,
archivosAdjuntos, linea_plan_id: s.linea_plan_id,
}, numero_ciclo: s.numero_ciclo,
} }
void generateSubjectAI.mutateAsync(payload as any).catch((e) => { void generateSubjectAI.mutateAsync(payload).catch((e) => {
console.error('Error generando asignatura IA (multiple):', e) console.error('Error generando asignatura IA (multiple):', e)
}) })
}) })
@@ -393,8 +166,6 @@ export function WizardControls({
resetScroll: false, resetScroll: false,
}) })
setIsSpinningIA(false)
return return
} }
@@ -424,19 +195,16 @@ export function WizardControls({
} }
} catch (err: any) { } catch (err: any) {
setIsSpinningIA(false) setIsSpinningIA(false)
stopSubjectWatch()
setWizard((w) => ({ setWizard((w) => ({
...w, ...w,
isLoading: false, isLoading: false,
errorMessage: err?.message ?? 'Error creando la asignatura', errorMessage: err?.message ?? 'Error creando la asignatura',
})) }))
} finally { } finally {
if (!startedWaiting) {
setIsSpinningIA(false) setIsSpinningIA(false)
setWizard((w) => ({ ...w, isLoading: false })) setWizard((w) => ({ ...w, isLoading: false }))
} }
} }
}
return ( return (
<div className="flex grow items-center justify-between"> <div className="flex grow items-center justify-between">

View File

@@ -1,126 +0,0 @@
import { Check, Loader2 } from 'lucide-react'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { useUpdatePlanFields, useUpdateRecommendationApplied } from '@/data'
export const ImprovementCard = ({
suggestions,
onApply,
planId,
dbMessageId,
currentDatos,
activeChatId,
onApplySuccess,
}: {
suggestions: Array<any>
onApply?: (key: string, value: string) => void
planId: string
currentDatos: any
dbMessageId: string
activeChatId: any
onApplySuccess?: (key: string) => void
}) => {
const [localApplied, setLocalApplied] = useState<Array<string>>([])
const updatePlan = useUpdatePlanFields()
const updateAppliedStatus = useUpdateRecommendationApplied()
const handleApply = (key: string, newValue: string) => {
if (!currentDatos) return
const currentValue = currentDatos[key]
let finalValue: any
if (
typeof currentValue === 'object' &&
currentValue !== null &&
'description' in currentValue
) {
finalValue = { ...currentValue, description: newValue }
} else {
finalValue = newValue
}
const datosActualizados = {
...currentDatos,
[key]: finalValue,
}
updatePlan.mutate(
{
planId: planId as any,
patch: { datos: datosActualizados },
},
{
onSuccess: () => {
setLocalApplied((prev) => [...prev, key])
if (onApplySuccess) onApplySuccess(key)
// --- CAMBIO AQUÍ: Ahora enviamos el ID del mensaje ---
if (dbMessageId) {
updateAppliedStatus.mutate({
conversacionId: dbMessageId, // Cambiamos el nombre de la propiedad si es necesario
campoAfectado: key,
})
}
if (onApply) onApply(key, newValue)
},
},
)
}
return (
<div className="mt-2 flex w-full flex-col gap-4">
{suggestions.map((sug) => {
const isApplied = sug.applied === true || localApplied.includes(sug.key)
const isUpdating =
updatePlan.isPending &&
updatePlan.variables.patch.datos?.[sug.key] !== undefined
return (
<div
key={sug.key}
className={`rounded-2xl border bg-white p-5 shadow-sm transition-all ${
isApplied ? 'border-teal-200 bg-teal-50/20' : 'border-slate-100'
}`}
>
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-bold text-slate-900">{sug.label}</h3>
<Button
size="sm"
onClick={() => handleApply(sug.key, sug.newValue)}
disabled={isApplied || !!isUpdating}
className={`h-8 rounded-full px-4 text-xs transition-all ${
isApplied
? 'cursor-not-allowed bg-slate-100 text-slate-400'
: 'bg-[#00a189] text-white hover:bg-[#008f7a]'
}`}
>
{isUpdating ? (
<Loader2 size={12} className="animate-spin" />
) : isApplied ? (
<span className="flex items-center gap-1">
<Check size={12} /> Aplicado
</span>
) : (
'Aplicar mejora'
)}
</Button>
</div>
<div
className={`rounded-xl border p-3 text-sm transition-colors duration-300 ${
isApplied
? 'border-teal-100 bg-teal-50/50 text-slate-700'
: 'border-slate-200 bg-slate-50 text-slate-500'
}`}
>
{sug.newValue}
</div>
</div>
)
})}
</div>
)
}

View File

@@ -1,21 +1,15 @@
import { useNavigate } from '@tanstack/react-router' import { useNavigate } from '@tanstack/react-router'
import { Loader2 } from 'lucide-react' import { Loader2 } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react' import { useState } from 'react'
import type { AIGeneratePlanInput } from '@/data' import type { AIGeneratePlanInput } from '@/data'
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain' import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types' import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
// import type { Database } from '@/types/supabase' // import type { Database } from '@/types/supabase'
import type { RealtimeChannel } from '@supabase/supabase-js'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { plans_get_maybe } from '@/data/api/plans.api' // import { supabaseBrowser } from '@/data'
import { import { useCreatePlanManual, useGeneratePlanAI } from '@/data/hooks/usePlans'
useCreatePlanManual,
useDeletePlanEstudio,
useGeneratePlanAI,
} from '@/data/hooks/usePlans'
import { supabaseBrowser } from '@/data/supabase/client'
export function WizardControls({ export function WizardControls({
errorMessage, errorMessage,
@@ -41,152 +35,9 @@ export function WizardControls({
const navigate = useNavigate() const navigate = useNavigate()
const generatePlanAI = useGeneratePlanAI() const generatePlanAI = useGeneratePlanAI()
const createPlanManual = useCreatePlanManual() const createPlanManual = useCreatePlanManual()
const deletePlan = useDeletePlanEstudio()
const [isSpinningIA, setIsSpinningIA] = useState(false) const [isSpinningIA, setIsSpinningIA] = useState(false)
const cancelledRef = useRef(false) // const supabaseClient = supabaseBrowser()
const realtimeChannelRef = useRef<RealtimeChannel | null>(null) // const persistPlanFromAI = usePersistPlanFromAI()
const watchPlanIdRef = useRef<string | null>(null)
const watchTimeoutRef = useRef<number | null>(null)
useEffect(() => {
cancelledRef.current = false
return () => {
cancelledRef.current = true
}
}, [])
const stopPlanWatch = useCallback(() => {
if (watchTimeoutRef.current) {
window.clearTimeout(watchTimeoutRef.current)
watchTimeoutRef.current = null
}
watchPlanIdRef.current = null
const ch = realtimeChannelRef.current
if (ch) {
realtimeChannelRef.current = null
try {
supabaseBrowser().removeChannel(ch)
} catch {
// noop
}
}
}, [])
useEffect(() => {
return () => {
stopPlanWatch()
}
}, [stopPlanWatch])
const checkPlanStateAndAct = useCallback(
async (planId: string) => {
if (cancelledRef.current) return
if (watchPlanIdRef.current !== planId) return
const plan = await plans_get_maybe(planId as any)
if (!plan) return
const clave = String(plan.estados_plan?.clave ?? '').toUpperCase()
if (clave.startsWith('GENERANDO')) return
if (clave.startsWith('BORRADOR')) {
stopPlanWatch()
setIsSpinningIA(false)
setWizard((w) => ({ ...w, isLoading: false }))
navigate({
to: `/planes/${plan.id}`,
state: { showConfetti: true },
})
return
}
if (clave.startsWith('FALLID')) {
stopPlanWatch()
setIsSpinningIA(false)
deletePlan
.mutateAsync(plan.id)
.catch(() => {
// Si falla el borrado, igual mostramos el error.
})
.finally(() => {
setWizard((w) => ({
...w,
isLoading: false,
errorMessage: 'La generación del plan falló',
}))
})
}
},
[deletePlan, navigate, setWizard, stopPlanWatch],
)
const beginPlanWatch = useCallback(
(planId: string) => {
stopPlanWatch()
watchPlanIdRef.current = planId
watchTimeoutRef.current = window.setTimeout(
() => {
if (cancelledRef.current) return
if (watchPlanIdRef.current !== planId) return
stopPlanWatch()
setIsSpinningIA(false)
setWizard((w) => ({
...w,
isLoading: false,
errorMessage:
'La generación está tardando demasiado. Intenta de nuevo en unos minutos.',
}))
},
6 * 60 * 1000,
)
const supabase = supabaseBrowser()
const channel = supabase.channel(`planes-status-${planId}`)
realtimeChannelRef.current = channel
channel.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'planes_estudio',
filter: `id=eq.${planId}`,
},
() => {
void checkPlanStateAndAct(planId)
},
)
channel.subscribe((status) => {
const st = status as
| 'SUBSCRIBED'
| 'TIMED_OUT'
| 'CLOSED'
| 'CHANNEL_ERROR'
if (cancelledRef.current) return
if (st === 'CHANNEL_ERROR' || st === 'TIMED_OUT') {
stopPlanWatch()
setIsSpinningIA(false)
setWizard((w) => ({
...w,
isLoading: false,
errorMessage:
'No se pudo suscribir al estado del plan. Intenta de nuevo.',
}))
}
})
// Fallback inmediato por si el plan ya cambió antes de suscribir.
void checkPlanStateAndAct(planId)
},
[checkPlanStateAndAct, setWizard, stopPlanWatch],
)
const handleCreate = async () => { const handleCreate = async () => {
// Start loading // Start loading
@@ -231,16 +82,14 @@ export function WizardControls({
console.log(`${new Date().toISOString()} - Enviando a generar plan IA`) console.log(`${new Date().toISOString()} - Enviando a generar plan IA`)
setIsSpinningIA(true) setIsSpinningIA(true)
const resp: any = await generatePlanAI.mutateAsync(aiInput as any) const plan = await generatePlanAI.mutateAsync(aiInput as any)
const planId = resp?.plan?.id ?? resp?.id setIsSpinningIA(false)
console.log(`${new Date().toISOString()} - Plan IA generado`, resp) console.log(`${new Date().toISOString()} - Plan IA generado`, plan)
if (!planId) { navigate({
throw new Error('No se pudo obtener el id del plan generado por IA') to: `/planes/${plan.id}`,
} state: { showConfetti: true },
})
// Inicia realtime; los efectos navegan o marcan error.
beginPlanWatch(String(planId))
return return
} }
@@ -265,14 +114,14 @@ export function WizardControls({
} }
} catch (err: any) { } catch (err: any) {
setIsSpinningIA(false) setIsSpinningIA(false)
stopPlanWatch()
setWizard((w) => ({ setWizard((w) => ({
...w, ...w,
isLoading: false, isLoading: false,
errorMessage: err?.message ?? 'Error generando el plan', errorMessage: err?.message ?? 'Error generando el plan',
})) }))
} finally { } finally {
// Si entramos en watch realtime, el loading se corta desde checkPlanStateAndAct. setIsSpinningIA(false)
setWizard((w) => ({ ...w, isLoading: false }))
} }
} }

View File

@@ -1,43 +0,0 @@
import * as React from "react"
import { CircleIcon } from "lucide-react"
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem }

View File

@@ -1,24 +1,18 @@
import * as React from 'react' import * as React from "react"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
const Textarea = React.forwardRef< function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
HTMLTextAreaElement,
React.ComponentProps<'textarea'>
>(({ className, ...props }, ref) => {
return ( return (
<textarea <textarea
ref={ref}
data-slot="textarea" data-slot="textarea"
className={cn( className={cn(
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className, className
)} )}
{...props} {...props}
/> />
) )
}) }
Textarea.displayName = 'Textarea'
export { Textarea } export { Textarea }

View File

@@ -5,24 +5,16 @@ export function WizardResponsiveHeader({
wizard, wizard,
methods, methods,
titleOverrides, titleOverrides,
hiddenStepIds,
}: { }: {
wizard: any wizard: any
methods: any methods: any
titleOverrides?: Record<string, string> titleOverrides?: Record<string, string>
hiddenStepIds?: Array<string>
}) { }) {
const hidden = new Set(hiddenStepIds ?? []) const idx = wizard.utils.getIndex(methods.current.id)
const visibleSteps = (wizard.steps as Array<any>).filter( const totalSteps = wizard.steps.length
(s) => s && !hidden.has(s.id), const currentIndex = idx + 1
) const hasNextStep = idx < totalSteps - 1
const nextStep = wizard.steps[currentIndex]
const idx = visibleSteps.findIndex((s) => s.id === methods.current.id)
const safeIdx = idx >= 0 ? idx : 0
const totalSteps = visibleSteps.length
const currentIndex = Math.min(safeIdx + 1, totalSteps)
const hasNextStep = safeIdx < totalSteps - 1
const nextStep = visibleSteps[safeIdx + 1]
const resolveTitle = (step: any) => titleOverrides?.[step?.id] ?? step?.title const resolveTitle = (step: any) => titleOverrides?.[step?.id] ?? step?.title
@@ -53,11 +45,10 @@ export function WizardResponsiveHeader({
<div className="hidden sm:block"> <div className="hidden sm:block">
<wizard.Stepper.Navigation className="border-border/60 rounded-xl border bg-slate-50 p-2"> <wizard.Stepper.Navigation className="border-border/60 rounded-xl border bg-slate-50 p-2">
{visibleSteps.map((step: any, visibleIdx: number) => ( {wizard.steps.map((step: any) => (
<wizard.Stepper.Step <wizard.Stepper.Step
key={step.id} key={step.id}
of={step.id} of={step.id}
icon={visibleIdx + 1}
className="whitespace-nowrap" className="whitespace-nowrap"
> >
<wizard.Stepper.Title> <wizard.Stepper.Title>

View File

@@ -1,377 +1,81 @@
import { supabaseBrowser } from '../supabase/client' import { invokeEdge } from "../supabase/invokeEdge";
import { invokeEdge } from '../supabase/invokeEdge' import type { InteraccionIA, UUID } from "../types/domain";
import type { InteraccionIA, UUID } from '../types/domain'
const EDGE = { const EDGE = {
ai_plan_improve: 'ai_plan_improve', ai_plan_improve: "ai_plan_improve",
ai_plan_chat: 'ai_plan_chat', ai_plan_chat: "ai_plan_chat",
ai_subject_improve: 'ai_subject_improve', ai_subject_improve: "ai_subject_improve",
ai_subject_chat: 'ai_subject_chat', ai_subject_chat: "ai_subject_chat",
library_search: 'library_search', library_search: "library_search",
} as const } as const;
export async function ai_plan_improve(payload: { export async function ai_plan_improve(payload: {
planId: UUID planId: UUID;
sectionKey: string // ej: "perfil_de_egreso" o tu key interna sectionKey: string; // ej: "perfil_de_egreso" o tu key interna
prompt: string prompt: string;
context?: Record<string, any> context?: Record<string, any>;
fuentes?: { fuentes?: {
archivosIds?: Array<UUID> archivosIds?: UUID[];
vectorStoresIds?: Array<UUID> vectorStoresIds?: UUID[];
usarMCP?: boolean usarMCP?: boolean;
conversacionId?: string conversacionId?: string;
} };
}): Promise<{ interaccion: InteraccionIA; propuesta: any }> { }): Promise<{ interaccion: InteraccionIA; propuesta: any }> {
return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>( return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>(EDGE.ai_plan_improve, payload);
EDGE.ai_plan_improve,
payload,
)
} }
export async function ai_plan_chat(payload: { export async function ai_plan_chat(payload: {
planId: UUID planId: UUID;
messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }> messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
fuentes?: { fuentes?: {
archivosIds?: Array<UUID> archivosIds?: UUID[];
vectorStoresIds?: Array<UUID> vectorStoresIds?: UUID[];
usarMCP?: boolean usarMCP?: boolean;
conversacionId?: string conversacionId?: string;
} };
}): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> { }): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> {
return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>( return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>(EDGE.ai_plan_chat, payload);
EDGE.ai_plan_chat,
payload,
)
} }
export async function ai_subject_improve(payload: { export async function ai_subject_improve(payload: {
subjectId: UUID subjectId: UUID;
sectionKey: string sectionKey: string;
prompt: string prompt: string;
context?: Record<string, any> context?: Record<string, any>;
fuentes?: { fuentes?: {
archivosIds?: Array<UUID> archivosIds?: UUID[];
vectorStoresIds?: Array<UUID> vectorStoresIds?: UUID[];
usarMCP?: boolean usarMCP?: boolean;
conversacionId?: string conversacionId?: string;
} };
}): Promise<{ interaccion: InteraccionIA; propuesta: any }> { }): Promise<{ interaccion: InteraccionIA; propuesta: any }> {
return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>( return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>(EDGE.ai_subject_improve, payload);
EDGE.ai_subject_improve,
payload,
)
} }
export async function ai_subject_chat(payload: { export async function ai_subject_chat(payload: {
subjectId: UUID subjectId: UUID;
messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }> messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
fuentes?: { fuentes?: {
archivosIds?: Array<UUID> archivosIds?: UUID[];
vectorStoresIds?: Array<UUID> vectorStoresIds?: UUID[];
usarMCP?: boolean usarMCP?: boolean;
conversacionId?: string conversacionId?: string;
} };
}): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> { }): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> {
return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>( return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>(EDGE.ai_subject_chat, payload);
EDGE.ai_subject_chat,
payload,
)
} }
/** Biblioteca (Edge; adapta a tu API real) */ /** Biblioteca (Edge; adapta a tu API real) */
export type LibraryItem = { export type LibraryItem = {
id: string id: string;
titulo: string titulo: string;
autor?: string autor?: string;
isbn?: string isbn?: string;
citaSugerida?: string citaSugerida?: string;
disponibilidad?: string disponibilidad?: string;
} };
export async function library_search(payload: { export async function library_search(payload: { query: string; limit?: number }): Promise<LibraryItem[]> {
query: string return invokeEdge<LibraryItem[]>(EDGE.library_search, payload);
limit?: number
}): Promise<Array<LibraryItem>> {
return invokeEdge<Array<LibraryItem>>(EDGE.library_search, payload)
}
export async function create_conversation(planId: string) {
const supabase = supabaseBrowser()
const { data, error } = await supabase.functions.invoke(
'create-chat-conversation/plan/conversations',
{
method: 'POST',
body: {
plan_estudio_id: planId, // O el nombre que confirmamos que funciona
instanciador: 'alex',
},
},
)
if (error) throw error
return data
}
export async function get_chat_history(conversacionId: string) {
const supabase = supabaseBrowser()
const { data, error } = await supabase.functions.invoke(
`create-chat-conversation/conversations/${conversacionId}/messages`,
{ method: 'GET' },
)
if (error) throw error
return data // Retorna Array de mensajes
}
export async function update_conversation_status(
conversacionId: string,
nuevoEstado: 'ARCHIVADA' | 'ACTIVA',
) {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('conversaciones_plan') // Asegúrate que el nombre de la tabla sea exacto
.update({ estado: nuevoEstado })
.eq('id', conversacionId)
.select()
.single()
if (error) throw error
return data
}
// Modificamos la función de chat para que use la ruta de mensajes
export async function ai_plan_chat_v2(payload: {
conversacionId: string
content: string
campos?: Array<string>
}): Promise<{ reply: string; meta?: any }> {
const supabase = supabaseBrowser()
const { data, error } = await supabase.functions.invoke(
`create-chat-conversation/conversations/plan/${payload.conversacionId}/messages`,
{
method: 'POST',
body: {
content: payload.content,
campos: payload.campos || [],
},
},
)
if (error) throw error
return data
}
export async function getConversationByPlan(planId: string) {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('conversaciones_plan')
.select('*')
.eq('plan_estudio_id', planId)
.order('creado_en', { ascending: false })
if (error) throw error
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return data ?? []
}
export async function getMessagesByConversation(conversationId: string) {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('plan_mensajes_ia')
.select('*')
.eq('conversacion_plan_id', conversationId)
.order('fecha_creacion', { ascending: true }) // Ascendente para que el chat fluya en orden cronológico
if (error) {
console.error('Error al obtener mensajes:', error.message)
throw error
}
return data ?? []
}
export async function update_conversation_title(
conversacionId: string,
nuevoTitulo: string,
) {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('conversaciones_plan')
.update({ nombre: nuevoTitulo }) // Asegúrate que la columna se llame 'title' o 'nombre'
.eq('id', conversacionId)
.select()
.single()
if (error) throw error
return data
}
export async function update_recommendation_applied_status(
mensajeId: string, // Ahora es más eficiente usar el ID del mensaje directamente
campoAfectado: string,
) {
const supabase = supabaseBrowser()
// 1. Obtener la propuesta actual de ese mensaje específico
const { data: msgData, error: fetchError } = await supabase
.from('plan_mensajes_ia')
.select('propuesta')
.eq('id', mensajeId)
.single()
if (fetchError) throw fetchError
if (!msgData?.propuesta)
throw new Error('No se encontró la propuesta en el mensaje')
const propuestaActual = msgData.propuesta as any
// 2. Modificar el array de recommendations dentro de la propuesta
// Mantenemos el resto de la propuesta (prompt, respuesta, etc.) intacto
const nuevaPropuesta = {
...propuestaActual,
recommendations: (propuestaActual.recommendations || []).map((rec: any) =>
rec.campo_afectado === campoAfectado ? { ...rec, aplicada: true } : rec,
),
}
// 3. Actualizar la base de datos con el nuevo objeto JSON
const { error: updateError } = await supabase
.from('plan_mensajes_ia')
.update({ propuesta: nuevaPropuesta })
.eq('id', mensajeId)
if (updateError) throw updateError
return true
}
// --- FUNCIONES DE ASIGNATURA ---
export async function create_subject_conversation(subjectId: string) {
const supabase = supabaseBrowser()
const { data, error } = await supabase.functions.invoke(
'create-chat-conversation/asignatura/conversations', // Ruta corregida
{
method: 'POST',
body: {
asignatura_id: subjectId,
instanciador: 'alex',
},
},
)
if (error) throw error
return data // Retorna { conversation_asignatura: { id, ... } }
}
export async function ai_subject_chat_v2(payload: {
conversacionId: string
content: string
campos?: Array<string>
}) {
const supabase = supabaseBrowser()
const { data, error } = await supabase.functions.invoke(
`create-chat-conversation/conversations/asignatura/${payload.conversacionId}/messages`, // Ruta corregida
{
method: 'POST',
body: {
content: payload.content,
campos: payload.campos || [],
},
},
)
if (error) throw error
return data
}
export async function getConversationBySubject(subjectId: string) {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('conversaciones_asignatura') // Tabla corregida
.select('*')
.eq('asignatura_id', subjectId)
.order('creado_en', { ascending: false })
if (error) throw error
return data ?? []
}
export async function getMessagesBySubjectConversation(conversationId: string) {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('asignatura_mensajes_ia' as any)
.select('*')
.eq('conversacion_asignatura_id', conversationId)
.order('fecha_creacion', { ascending: true })
if (error) throw error
return data ?? []
}
export async function update_subject_recommendation_applied(
mensajeId: string,
campoAfectado: string,
) {
const supabase = supabaseBrowser()
// 1. Obtener propuesta actual
const { data: msgData, error: fetchError } = await supabase
.from('asignatura_mensajes_ia')
.select('propuesta')
.eq('id', mensajeId)
.single()
if (fetchError) throw fetchError
const propuestaActual = msgData?.propuesta as any
// 2. Marcar como aplicada
const nuevaPropuesta = {
...propuestaActual,
recommendations: (propuestaActual.recommendations || []).map((rec: any) =>
rec.campo_afectado === campoAfectado ? { ...rec, aplicada: true } : rec,
),
}
// 3. Update
const { error: updateError } = await supabase
.from('asignatura_mensajes_ia')
.update({ propuesta: nuevaPropuesta })
.eq('id', mensajeId)
if (updateError) throw updateError
return true
}
export async function update_subject_conversation_status(
conversacionId: string,
nuevoEstado: 'ARCHIVADA' | 'ACTIVA',
) {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('conversaciones_asignatura')
.update({ estado: nuevoEstado })
.eq('id', conversacionId)
.select()
.single()
if (error) throw error
return data
}
export async function update_subject_conversation_name(
conversacionId: string,
nuevoNombre: string,
) {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('conversaciones_asignatura')
.update({ nombre: nuevoNombre }) // Asumiendo que la columna es 'titulo' según tu código previo, o cambia a 'nombre'
.eq('id', conversacionId)
.select()
.single()
if (error) throw error
return data
} }

View File

@@ -3,15 +3,9 @@
const DOCUMENT_PDF_URL = const DOCUMENT_PDF_URL =
'https://n8n.app.lci.ulsa.mx/webhook/62ca84ec-0adb-4006-aba1-32282d27d434' 'https://n8n.app.lci.ulsa.mx/webhook/62ca84ec-0adb-4006-aba1-32282d27d434'
const DOCUMENT_PDF_ASIGNATURA_URL =
'https://n8n.app.lci.ulsa.mx/webhook/041a68be-7568-46d0-bc08-09ded12d017d'
interface GeneratePdfParams { interface GeneratePdfParams {
plan_estudio_id: string plan_estudio_id: string
} }
interface GeneratePdfParamsAsignatura {
asignatura_id: string
}
export async function fetchPlanPdf({ export async function fetchPlanPdf({
plan_estudio_id, plan_estudio_id,
@@ -31,22 +25,3 @@ export async function fetchPlanPdf({
// n8n devuelve el archivo → lo tratamos como blob // n8n devuelve el archivo → lo tratamos como blob
return await response.blob() return await response.blob()
} }
export async function fetchAsignaturaPdf({
asignatura_id,
}: GeneratePdfParamsAsignatura): Promise<Blob> {
const response = await fetch(DOCUMENT_PDF_ASIGNATURA_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ asignatura_id }),
})
if (!response.ok) {
throw new Error('Error al generar el PDF')
}
// n8n devuelve el archivo → lo tratamos como blob
return await response.blob()
}

View File

@@ -144,48 +144,6 @@ export async function plans_get(planId: UUID): Promise<PlanEstudio> {
return requireData(data, 'Plan no encontrado.') return requireData(data, 'Plan no encontrado.')
} }
/**
* Variante de `plans_get` que NO lanza si no existe (devuelve null).
* Útil para flujos de polling donde el plan puede tardar en aparecer.
*/
export async function plans_get_maybe(
planId: UUID,
): Promise<PlanEstudio | null> {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('planes_estudio')
.select(
`
*,
carreras (*, facultades(*)),
estructuras_plan (*),
estados_plan (*)
`,
)
.eq('id', planId)
.maybeSingle()
throwIfError(error)
return (data ?? null) as unknown as PlanEstudio | null
}
export async function plans_delete(planId: UUID): Promise<{ id: UUID }> {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('planes_estudio')
.delete()
.eq('id', planId)
.select('id')
.maybeSingle()
throwIfError(error)
// Si por alguna razón no retorna fila (RLS / triggers), devolvemos el id solicitado.
return { id: ((data as any)?.id ?? planId) as UUID }
}
export async function plan_lineas_list( export async function plan_lineas_list(
planId: UUID, planId: UUID,
): Promise<Array<LineaPlan>> { ): Promise<Array<LineaPlan>> {
@@ -207,7 +165,7 @@ export async function plan_asignaturas_list(
const { data, error } = await supabase const { data, error } = await supabase
.from('asignaturas') .from('asignaturas')
.select( .select(
'id,plan_estudio_id,horas_academicas,horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,prerrequisito_asignatura_id', 'id,plan_estudio_id,horas_academicas,horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,asignatura_hash,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 })

View File

@@ -7,19 +7,16 @@ import type { DocumentoResult } from './plans.api'
import type { import type {
Asignatura, Asignatura,
BibliografiaAsignatura, BibliografiaAsignatura,
CarreraRow,
CambioAsignatura, CambioAsignatura,
EstructuraAsignatura,
FacultadRow,
PlanEstudioRow,
TipoAsignatura, TipoAsignatura,
UUID, UUID,
} from '../types/domain' } from '../types/domain'
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
import type { import type {
AsignaturaSugerida, AsignaturaSugerida,
DataAsignaturaSugerida, DataAsignaturaSugerida,
} from '@/features/asignaturas/nueva/types' } from '@/features/asignaturas/nueva/types'
import type { Database, Tables, TablesInsert } from '@/types/supabase' import type { Database, TablesInsert } from '@/types/supabase'
const EDGE = { const EDGE = {
generate_subject_suggestions: 'generate-subject-suggestions', generate_subject_suggestions: 'generate-subject-suggestions',
@@ -29,184 +26,34 @@ const EDGE = {
subjects_clone_from_existing: 'subjects_clone_from_existing', subjects_clone_from_existing: 'subjects_clone_from_existing',
subjects_import_from_file: 'subjects_import_from_file', subjects_import_from_file: 'subjects_import_from_file',
// Bibliografía
buscar_bibliografia: 'buscar-bibliografia',
subjects_update_fields: 'subjects_update_fields', subjects_update_fields: 'subjects_update_fields',
subjects_update_contenido: 'subjects_update_contenido',
subjects_update_bibliografia: 'subjects_update_bibliografia', subjects_update_bibliografia: 'subjects_update_bibliografia',
subjects_generate_document: 'subjects_generate_document', subjects_generate_document: 'subjects_generate_document',
subjects_get_document: 'subjects_get_document', subjects_get_document: 'subjects_get_document',
} as const } as const
export type BuscarBibliografiaRequest = { export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
searchTerms: {
q: string
}
google: {
orderBy?: 'newest' | 'relevance'
langRestrict?: string
startIndex?: number
[k: string]: unknown
}
openLibrary: {
language?: string
page?: number
sort?: string
[k: string]: unknown
}
}
export type GoogleBooksVolume = {
kind?: 'books#volume'
id: string
etag?: string
selfLink?: string
volumeInfo?: {
title?: string
subtitle?: string
authors?: Array<string>
publisher?: string
publishedDate?: string
description?: string
industryIdentifiers?: Array<{ type?: string; identifier?: string }>
pageCount?: number
categories?: Array<string>
language?: string
previewLink?: string
infoLink?: string
canonicalVolumeLink?: string
imageLinks?: {
smallThumbnail?: string
thumbnail?: string
small?: string
medium?: string
large?: string
extraLarge?: string
}
}
searchInfo?: {
textSnippet?: string
}
[k: string]: unknown
}
export type OpenLibraryDoc = Record<string, unknown>
export type EndpointResult =
| { endpoint: 'google'; item: GoogleBooksVolume }
| { endpoint: 'open_library'; item: OpenLibraryDoc }
export async function buscar_bibliografia(
input: BuscarBibliografiaRequest,
): Promise<Array<EndpointResult>> {
const q = input.searchTerms.q
if (typeof q !== 'string' || q.trim().length < 1) {
throw new Error('q es requerido')
}
return await invokeEdge<Array<EndpointResult>>(
EDGE.buscar_bibliografia,
input,
{ headers: { 'Content-Type': 'application/json' } },
)
}
export type ContenidoTemaApi =
| string
| {
nombre: string
horasEstimadas?: number
descripcion?: string
[key: string]: unknown
}
/**
* Estructura persistida en `asignaturas.contenido_tematico`.
* La BDD guarda un arreglo de unidades, cada una con temas (strings u objetos).
*/
export type ContenidoApi = {
unidad: number
titulo: string
temas: Array<ContenidoTemaApi>
[key: string]: unknown
}
export type FacultadInSubject = Pick<
FacultadRow,
'id' | 'nombre' | 'nombre_corto' | 'color' | 'icono'
>
export type CarreraInSubject = Pick<
CarreraRow,
'id' | 'facultad_id' | 'nombre' | 'nombre_corto' | 'clave_sep' | 'activa'
> & {
facultades: FacultadInSubject | null
}
export type PlanEstudioInSubject = Pick<
PlanEstudioRow,
| 'id'
| 'carrera_id'
| 'estructura_id'
| 'nombre'
| 'nivel'
| 'tipo_ciclo'
| 'numero_ciclos'
| 'datos'
| 'estado_actual_id'
| 'activo'
| 'tipo_origen'
| 'meta_origen'
| 'creado_por'
| 'actualizado_por'
| 'creado_en'
| 'actualizado_en'
> & {
carreras: CarreraInSubject | null
}
export type EstructuraAsignaturaInSubject = Pick<
EstructuraAsignatura,
'id' | 'nombre' | 'definicion'
>
/**
* Tipo real que devuelve `subjects_get` (asignatura + relaciones seleccionadas).
* Nota: `asignaturas_update` (update directo) NO devuelve estas relaciones.
*/
export type AsignaturaDetail = Omit<Asignatura, 'contenido_tematico'> & {
contenido_tematico: Array<ContenidoApi> | null
planes_estudio: PlanEstudioInSubject | null
estructuras_asignatura: EstructuraAsignaturaInSubject | null
}
export async function subjects_get(subjectId: UUID): Promise<AsignaturaDetail> {
const supabase = supabaseBrowser() const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from('asignaturas') .from('asignaturas')
.select( .select(
` `
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,criterios_de_evaluacion,prerrequisito_asignatura_id, id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
planes_estudio( planes_estudio(
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en, id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono)) carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
), ),
estructuras_asignatura(id,nombre,definicion) estructuras_asignatura(id,nombre,version,definicion)
`, `,
) )
.eq('id', subjectId) .eq('id', subjectId)
.single() .single()
throwIfError(error) throwIfError(error)
return requireData( return requireData(data, 'Asignatura no encontrada.')
data,
'Asignatura no encontrada.',
) as unknown as AsignaturaDetail
} }
export async function subjects_history( export async function subjects_history(
@@ -232,7 +79,7 @@ export async function subjects_bibliografia_list(
const { data, error } = await supabase const { data, error } = await supabase
.from('bibliografia_asignatura') .from('bibliografia_asignatura')
.select( .select(
'id,asignatura_id,tipo,cita,referencia_biblioteca,referencia_en_linea,creado_por,creado_en,actualizado_en', 'id,asignatura_id,tipo,cita,tipo_fuente,biblioteca_item_id,creado_por,creado_en,actualizado_en',
) )
.eq('asignatura_id', subjectId) .eq('asignatura_id', subjectId)
.order('tipo', { ascending: true }) .order('tipo', { ascending: true })
@@ -256,49 +103,54 @@ export async function subjects_create_manual(
return requireData(data, 'No se pudo crear la asignatura.') return requireData(data, 'No se pudo crear la asignatura.')
} }
/** export type AIGenerateSubjectInput = {
* Nuevo payload unificado (JSON) para la Edge `ai_generate_subject`. plan_estudio_id: Asignatura['plan_estudio_id']
* - Siempre incluye `datosUpdate.plan_estudio_id`. datosBasicos: {
* - `datosUpdate.id` es opcional (si no existe, la Edge puede crear). nombre: Asignatura['nombre']
* En el frontend, insertamos primero y usamos `id` para actualizar. codigo?: Asignatura['codigo']
*/ tipo: Asignatura['tipo'] | null
export type AISubjectUnifiedInput = { creditos: Asignatura['creditos'] | null
datosUpdate: Partial<{ horasAcademicas?: Asignatura['horas_academicas'] | null
id: string horasIndependientes?: Asignatura['horas_independientes'] | null
plan_estudio_id: string estructuraId: Asignatura['estructura_id'] | null
estructura_id: string
nombre: string
codigo: string | null
tipo: string | null
creditos: number
horas_academicas: number | null
horas_independientes: number | null
numero_ciclo: number | null
linea_plan_id: string | null
orden_celda: number | null
}> & {
plan_estudio_id: string
} }
// clonInterno?: {
// facultadId?: string
// carreraId?: string
// planOrigenId?: string
// asignaturaOrigenId?: string | null
// }
// clonTradicional?: {
// archivoWordAsignaturaId: string | null
// archivosAdicionalesIds: Array<string>
// }
iaConfig?: { iaConfig?: {
descripcionEnfoqueAcademico?: string descripcionEnfoqueAcademico: string
instruccionesAdicionalesIA?: string instruccionesAdicionalesIA: string
archivosAdjuntos?: Array<string> archivosReferencia: Array<string>
repositoriosReferencia?: Array<string>
archivosAdjuntos?: Array<UploadedFile>
} }
} }
export async function subjects_get_maybe( /**
subjectId: UUID, * Edge (JSON): actualizar/llenar una asignatura existente por id.
): Promise<Asignatura | null> { * Nota: este flujo NO acepta `instruccionesAdicionalesIA` (solo FormData lo usa).
const supabase = supabaseBrowser() */
export type AIGenerateSubjectJsonInput = Partial<{
const { data, error } = await supabase plan_estudio_id: Asignatura['plan_estudio_id']
.from('asignaturas') nombre: Asignatura['nombre']
.select('id,plan_estudio_id,estado') codigo: Asignatura['codigo']
.eq('id', subjectId) tipo: Asignatura['tipo'] | null
.maybeSingle() creditos: Asignatura['creditos']
horas_academicas: Asignatura['horas_academicas'] | null
throwIfError(error) horas_independientes: Asignatura['horas_independientes'] | null
return (data ?? null) as unknown as Asignatura | null estructura_id: Asignatura['estructura_id'] | null
linea_plan_id: Asignatura['linea_plan_id'] | null
numero_ciclo: Asignatura['numero_ciclo'] | null
descripcionEnfoqueAcademico: string
}> & {
id: Asignatura['id']
} }
export type GenerateSubjectSuggestionsInput = { export type GenerateSubjectSuggestionsInput = {
@@ -336,8 +188,30 @@ export async function generate_subject_suggestions(
} }
export async function ai_generate_subject( export async function ai_generate_subject(
input: AISubjectUnifiedInput, input: AIGenerateSubjectInput | AIGenerateSubjectJsonInput,
): Promise<any> { ): Promise<any> {
if ('datosBasicos' in input) {
const edgeFunctionBody = new FormData()
edgeFunctionBody.append('plan_estudio_id', input.plan_estudio_id)
edgeFunctionBody.append('datosBasicos', JSON.stringify(input.datosBasicos))
edgeFunctionBody.append(
'iaConfig',
JSON.stringify({
...input.iaConfig,
archivosAdjuntos: undefined, // los manejamos aparte
}),
)
input.iaConfig?.archivosAdjuntos?.forEach((file) => {
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
})
return invokeEdge<any>(
EDGE.ai_generate_subject,
edgeFunctionBody,
undefined,
supabaseBrowser(),
)
}
return invokeEdge<any>(EDGE.ai_generate_subject, input, { return invokeEdge<any>(EDGE.ai_generate_subject, input, {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
@@ -397,24 +271,12 @@ export async function subjects_update_fields(
export async function subjects_update_contenido( export async function subjects_update_contenido(
subjectId: UUID, subjectId: UUID,
unidades: Array<ContenidoApi>, unidades: Array<any>,
): Promise<Asignatura> { ): Promise<Asignatura> {
const supabase = supabaseBrowser() return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, {
subjectId,
type AsignaturaUpdate = Database['public']['Tables']['asignaturas']['Update'] unidades,
const { data, error } = await supabase
.from('asignaturas')
.update({
contenido_tematico:
unidades as unknown as AsignaturaUpdate['contenido_tematico'],
}) })
.eq('id', subjectId)
.select()
.single()
throwIfError(error)
return requireData(data, 'No se pudo actualizar la asignatura.')
} }
export type BibliografiaUpsertInput = Array<{ export type BibliografiaUpsertInput = Array<{
@@ -541,47 +403,3 @@ export async function lineas_delete(lineaId: string) {
if (error) throw error if (error) throw error
return lineaId return lineaId
} }
export async function bibliografia_insert(
entry: TablesInsert<'bibliografia_asignatura'>,
): Promise<Tables<'bibliografia_asignatura'>> {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('bibliografia_asignatura')
.insert([entry])
.select()
.single()
if (error) throw error
return data as Tables<'bibliografia_asignatura'>
}
export async function bibliografia_update(
id: string,
updates: {
cita?: string
tipo?: 'BASICA' | 'COMPLEMENTARIA'
},
) {
const supabase = supabaseBrowser()
const { data, error } = await supabase
.from('bibliografia_asignatura')
.update(updates) // Ahora 'updates' es compatible con lo que espera Supabase
.eq('id', id)
.select()
.single()
if (error) throw error
return data
}
export async function bibliografia_delete(id: string) {
const supabase = supabaseBrowser()
const { error } = await supabase
.from('bibliografia_asignatura')
.delete()
.eq('id', id)
if (error) throw error
return id
}

View File

@@ -1,337 +1,29 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation } from "@tanstack/react-query";
import { useEffect } from 'react'
import { import {
ai_plan_chat_v2, ai_plan_chat,
ai_plan_improve, ai_plan_improve,
ai_subject_chat,
ai_subject_improve, ai_subject_improve,
create_conversation,
get_chat_history,
getConversationByPlan,
library_search, library_search,
update_conversation_status, } from "../api/ai.api";
update_recommendation_applied_status,
update_conversation_title,
getMessagesByConversation,
update_subject_conversation_status,
update_subject_recommendation_applied,
getMessagesBySubjectConversation,
getConversationBySubject,
ai_subject_chat_v2,
create_subject_conversation,
update_subject_conversation_name,
} from '../api/ai.api'
import { supabaseBrowser } from '../supabase/client'
import type { UUID } from 'node:crypto'
export function useAIPlanImprove() { export function useAIPlanImprove() {
return useMutation({ mutationFn: ai_plan_improve }) return useMutation({ mutationFn: ai_plan_improve });
} }
export function useAIPlanChat() { export function useAIPlanChat() {
return useMutation({ return useMutation({ mutationFn: ai_plan_chat });
mutationFn: async (payload: {
planId: UUID
content: string
campos?: Array<string>
conversacionId?: string
}) => {
let currentId = payload.conversacionId
// 1. Si no hay ID, creamos la conversación
if (!currentId) {
const response = await create_conversation(payload.planId)
// CAMBIO AQUÍ: Accedemos a la estructura correcta según tu consola
currentId = response.conversation_plan.id
}
// 2. Ahora enviamos el mensaje con el ID garantizado
const result = await ai_plan_chat_v2({
conversacionId: currentId!,
content: payload.content,
campos: payload.campos,
})
// Retornamos el resultado del chat y el ID para el estado del componente
return { ...result, conversacionId: currentId }
},
})
}
export function useChatHistory(conversacionId?: string) {
return useQuery({
queryKey: ['chat-history', conversacionId],
queryFn: async () => {
return get_chat_history(conversacionId!)
},
enabled: Boolean(conversacionId),
})
}
export function useUpdateConversationStatus() {
const qc = useQueryClient()
return useMutation({
mutationFn: ({
id,
estado,
}: {
id: string
estado: 'ARCHIVADA' | 'ACTIVA'
}) => update_conversation_status(id, estado),
onSuccess: () => {
// Esto refresca las listas automáticamente
qc.invalidateQueries({ queryKey: ['conversation-by-plan'] })
},
})
}
export function useConversationByPlan(planId: string | null) {
return useQuery({
queryKey: ['conversation-by-plan', planId],
queryFn: () => getConversationByPlan(planId!),
enabled: !!planId, // solo ejecuta si existe planId
})
}
export function useMessagesByChat(conversationId: string | null) {
const queryClient = useQueryClient()
const supabase = supabaseBrowser()
const query = useQuery({
queryKey: ['conversation-messages', conversationId],
queryFn: () => {
if (!conversationId) throw new Error('Conversation ID is required')
return getMessagesByConversation(conversationId)
},
enabled: !!conversationId,
placeholderData: (previousData) => previousData,
})
useEffect(() => {
if (!conversationId) return
// Suscribirse a cambios en los mensajes de ESTA conversación
const channel = supabase
.channel(`realtime-messages-${conversationId}`)
.on(
'postgres_changes',
{
event: '*', // Escuchamos INSERT y UPDATE
schema: 'public',
table: 'plan_mensajes_ia',
filter: `conversacion_plan_id=eq.${conversationId}`,
},
(payload) => {
// Opción A: Invalidar la query para que React Query haga refetch (más seguro)
queryClient.invalidateQueries({
queryKey: ['conversation-messages', conversationId],
})
/* Opción B: Actualización manual del caché (más rápido/fluido)
if (payload.eventType === 'INSERT') {
queryClient.setQueryData(['conversation-messages', conversationId], (old: any) => [...old, payload.new])
} else if (payload.eventType === 'UPDATE') {
queryClient.setQueryData(['conversation-messages', conversationId], (old: any) =>
old.map((m: any) => m.id === payload.new.id ? payload.new : m)
)
}
*/
},
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [conversationId, queryClient, supabase])
return query
}
export function useUpdateRecommendationApplied() {
const qc = useQueryClient()
return useMutation({
mutationFn: ({
conversacionId,
campoAfectado,
}: {
conversacionId: string
campoAfectado: string
}) => update_recommendation_applied_status(conversacionId, campoAfectado),
onSuccess: (_, variables) => {
// Invalidamos la query para que useConversationByPlan refresque el JSON
qc.invalidateQueries({ queryKey: ['conversation-by-plan'] })
console.log(
`Recomendación ${variables.campoAfectado} marcada como aplicada.`,
)
},
onError: (error) => {
console.error('Error al actualizar el estado de la recomendación:', error)
},
})
} }
export function useAISubjectImprove() { export function useAISubjectImprove() {
return useMutation({ mutationFn: ai_subject_improve }) return useMutation({ mutationFn: ai_subject_improve });
}
export function useAISubjectChat() {
return useMutation({ mutationFn: ai_subject_chat });
} }
export function useLibrarySearch() { export function useLibrarySearch() {
return useMutation({ mutationFn: library_search }) return useMutation({ mutationFn: library_search });
}
export function useUpdateConversationTitle() {
const qc = useQueryClient()
return useMutation({
mutationFn: ({ id, nombre }: { id: string; nombre: string }) =>
update_conversation_title(id, nombre),
onSuccess: (_, variables) => {
// Invalidamos para que la lista de chats se refresque
qc.invalidateQueries({ queryKey: ['conversation-by-plan'] })
},
})
}
// Asignaturas
export function useAISubjectChat() {
const qc = useQueryClient()
return useMutation({
mutationFn: async (payload: {
subjectId: UUID
content: string
campos?: Array<string>
conversacionId?: string
}) => {
let currentId = payload.conversacionId
// 1. Si no hay ID, creamos la conversación de asignatura
if (!currentId) {
const response = await create_subject_conversation(payload.subjectId)
currentId = response.conversation_asignatura.id
}
// 2. Enviamos mensaje al endpoint de asignatura
const result = await ai_subject_chat_v2({
conversacionId: currentId!,
content: payload.content,
campos: payload.campos,
})
return { ...result, conversacionId: currentId }
},
onSuccess: (data) => {
// Invalidamos mensajes para que se refresque el chat
qc.invalidateQueries({
queryKey: ['subject-messages', data.conversacionId],
})
},
})
}
export function useConversationBySubject(subjectId: string | null) {
return useQuery({
queryKey: ['conversation-by-subject', subjectId],
queryFn: () => getConversationBySubject(subjectId!),
enabled: !!subjectId,
})
}
export function useMessagesBySubjectChat(conversationId: string | null) {
const queryClient = useQueryClient()
const query = useQuery({
queryKey: ['subject-messages', conversationId],
queryFn: async () => {
if (!conversationId) throw new Error('Conversation ID is required')
return getMessagesBySubjectConversation(conversationId)
},
enabled: !!conversationId,
placeholderData: (previousData) => previousData,
})
useEffect(() => {
if (!conversationId) return
const supabase = supabaseBrowser()
// Suscripción a cambios en la tabla específica para esta conversación
const channel = supabase
.channel(`subject_messages_${conversationId}`)
.on(
'postgres_changes',
{
event: 'UPDATE', // Solo nos interesan las actualizaciones (cuando pasa de PROCESANDO a COMPLETADO)
schema: 'public',
table: 'asignatura_mensajes_ia',
filter: `conversacion_asignatura_id=eq.${conversationId}`,
},
(payload) => {
// Si el mensaje se completó o dio error, invalidamos la caché para traer los datos nuevos
if (
payload.new.estado === 'COMPLETADO' ||
payload.new.estado === 'ERROR'
) {
queryClient.invalidateQueries({
queryKey: ['subject-messages', conversationId],
})
}
},
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [conversationId, queryClient])
return query
}
export function useUpdateSubjectRecommendation() {
const qc = useQueryClient()
return useMutation({
mutationFn: (payload: { mensajeId: string; campoAfectado: string }) =>
update_subject_recommendation_applied(
payload.mensajeId,
payload.campoAfectado,
),
onSuccess: () => {
// Refrescamos los mensajes para ver el check de "aplicado"
qc.invalidateQueries({ queryKey: ['subject-messages'] })
},
})
}
export function useUpdateSubjectConversationStatus() {
const qc = useQueryClient()
return useMutation({
mutationFn: (payload: { id: string; estado: 'ARCHIVADA' | 'ACTIVA' }) =>
update_subject_conversation_status(payload.id, payload.estado),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['conversation-by-subject'] })
},
})
}
export function useUpdateSubjectConversationName() {
const qc = useQueryClient()
return useMutation({
mutationFn: (payload: { id: string; nombre: string }) =>
update_subject_conversation_name(payload.id, payload.nombre),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['conversation-by-subject'] })
// También invalidamos los mensajes si el título se muestra en la cabecera
qc.invalidateQueries({ queryKey: ['subject-messages'] })
},
})
} }

View File

@@ -4,7 +4,6 @@ import {
useQuery, useQuery,
useQueryClient, useQueryClient,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import { useEffect } from 'react'
import { import {
ai_generate_plan, ai_generate_plan,
@@ -13,7 +12,6 @@ import {
plan_lineas_list, plan_lineas_list,
plans_clone_from_existing, plans_clone_from_existing,
plans_create_manual, plans_create_manual,
plans_delete,
plans_generate_document, plans_generate_document,
plans_get, plans_get,
plans_get_document, plans_get_document,
@@ -27,7 +25,6 @@ import {
} from '../api/plans.api' } from '../api/plans.api'
import { lineas_delete } from '../api/subjects.api' import { lineas_delete } from '../api/subjects.api'
import { qk } from '../query/keys' import { qk } from '../query/keys'
import { supabaseBrowser } from '../supabase/client'
import type { import type {
PlanListFilters, PlanListFilters,
@@ -74,79 +71,23 @@ export function usePlanLineas(planId: UUID | null | undefined) {
} }
export function usePlanAsignaturas(planId: UUID | null | undefined) { export function usePlanAsignaturas(planId: UUID | null | undefined) {
const qc = useQueryClient() return useQuery({
const query = useQuery({
queryKey: planId queryKey: planId
? qk.planAsignaturas(planId) ? qk.planAsignaturas(planId)
: ['planes', 'asignaturas', null], : ['planes', 'asignaturas', null],
queryFn: () => plan_asignaturas_list(planId as UUID), queryFn: () => plan_asignaturas_list(planId as UUID),
enabled: Boolean(planId), enabled: Boolean(planId),
})
useEffect(() => { refetchInterval: (query) => {
if (!planId) return const data = query.state.data
if (!Array.isArray(data)) return false
const supabase = supabaseBrowser() const hayGenerando = data.some(
const channel = supabase.channel(`plan-asignaturas-${planId}`) (a: any) => (a as { estado?: unknown }).estado === 'generando',
channel.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'asignaturas',
filter: `plan_estudio_id=eq.${planId}`,
},
(payload: {
eventType?: 'INSERT' | 'UPDATE' | 'DELETE'
new?: any
old?: any
}) => {
const eventType = payload.eventType
if (eventType === 'DELETE') {
const oldRow: any = payload.old
const deletedId = oldRow?.id
if (!deletedId) return
qc.setQueryData(qk.planAsignaturas(planId), (prev) => {
if (!Array.isArray(prev)) return prev
return prev.filter((a: any) => String(a?.id) !== String(deletedId))
})
return
}
const newRow: any = payload.new
if (!newRow?.id) return
qc.setQueryData(qk.planAsignaturas(planId), (prev) => {
if (!Array.isArray(prev)) return prev
const idx = prev.findIndex(
(a: any) => String(a?.id) === String(newRow.id),
) )
if (idx === -1) return [...prev, newRow] return hayGenerando ? 500 : false
const next = [...prev]
next[idx] = { ...prev[idx], ...newRow }
return next
})
}, },
) refetchIntervalInBackground: true,
})
channel.subscribe()
return () => {
try {
supabase.removeChannel(channel)
} catch {
// noop
}
}
}, [planId, qc])
return query
} }
export function usePlanHistorial( export function usePlanHistorial(
@@ -322,23 +263,6 @@ export function useTransitionPlanEstado() {
}) })
} }
export function useDeletePlanEstudio() {
const qc = useQueryClient()
return useMutation({
mutationFn: (planId: UUID) => plans_delete(planId),
onSuccess: (_ok, planId) => {
qc.invalidateQueries({ queryKey: ['planes', 'list'] })
qc.removeQueries({ queryKey: qk.plan(planId) })
qc.removeQueries({ queryKey: qk.planMaybe(planId) })
qc.removeQueries({ queryKey: qk.planAsignaturas(planId) })
qc.removeQueries({ queryKey: qk.planLineas(planId) })
qc.removeQueries({ queryKey: qk.planHistorial(planId) })
qc.removeQueries({ queryKey: qk.planDocumento(planId) })
},
})
}
export function useGeneratePlanDocumento() { export function useGeneratePlanDocumento() {
const qc = useQueryClient() const qc = useQueryClient()

View File

@@ -3,9 +3,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { import {
ai_generate_subject, ai_generate_subject,
asignaturas_update, asignaturas_update,
bibliografia_delete,
bibliografia_insert,
bibliografia_update,
lineas_insert, lineas_insert,
lineas_update, lineas_update,
subjects_bibliografia_list, subjects_bibliografia_list,
@@ -26,7 +23,6 @@ import { qk } from '../query/keys'
import type { import type {
BibliografiaUpsertInput, BibliografiaUpsertInput,
ContenidoApi,
SubjectsUpdateFieldsPatch, SubjectsUpdateFieldsPatch,
} from '../api/subjects.api' } from '../api/subjects.api'
import type { UUID } from '../types/domain' import type { UUID } from '../types/domain'
@@ -101,6 +97,7 @@ export function useCreateSubjectManual() {
} }
export function useGenerateSubjectAI() { export function useGenerateSubjectAI() {
const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: ai_generate_subject, mutationFn: ai_generate_subject,
}) })
@@ -165,9 +162,7 @@ export function useUpdateSubjectFields() {
mutationFn: (vars: { subjectId: UUID; patch: SubjectsUpdateFieldsPatch }) => mutationFn: (vars: { subjectId: UUID; patch: SubjectsUpdateFieldsPatch }) =>
subjects_update_fields(vars.subjectId, vars.patch), subjects_update_fields(vars.subjectId, vars.patch),
onSuccess: (updated) => { onSuccess: (updated) => {
qc.setQueryData(qk.asignatura(updated.id), (prev) => qc.setQueryData(qk.asignatura(updated.id), updated)
prev ? { ...(prev as any), ...(updated as any) } : updated,
)
qc.invalidateQueries({ qc.invalidateQueries({
queryKey: qk.planAsignaturas(updated.plan_estudio_id), queryKey: qk.planAsignaturas(updated.plan_estudio_id),
}) })
@@ -180,19 +175,10 @@ export function useUpdateSubjectContenido() {
const qc = useQueryClient() const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (vars: { subjectId: UUID; unidades: Array<ContenidoApi> }) => mutationFn: (vars: { subjectId: UUID; unidades: Array<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), (prev) => qc.setQueryData(qk.asignatura(updated.id), updated)
prev ? { ...(prev as any), ...(updated as any) } : updated,
)
qc.invalidateQueries({
queryKey: qk.planAsignaturas(updated.plan_estudio_id),
})
qc.invalidateQueries({
queryKey: qk.planHistorial(updated.plan_estudio_id),
})
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) }) qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
}, },
}) })
@@ -235,22 +221,17 @@ export function useUpdateAsignatura() {
}) => asignaturas_update(vars.asignaturaId, vars.patch), }) => asignaturas_update(vars.asignaturaId, vars.patch),
onSuccess: (updated) => { onSuccess: (updated) => {
// ✅ Mantener consistencia con las query keys centralizadas (qk) // 1. Actualizamos la materia específica en la caché si tienes un query de "detalle"
// 1) Actualiza el detalle (esto evita volver a entrar con caché vieja) qc.setQueryData(['asignatura', updated.id], updated)
qc.setQueryData(qk.asignatura(updated.id), (prev) =>
prev ? { ...(prev as any), ...(updated as any) } : updated,
)
// 2) Refresca vistas derivadas del plan // 2. IMPORTANTÍSIMO: Invalidamos la lista de materias del plan
// para que el mapa curricular vea los cambios (créditos, horas, nombre, etc.)
qc.invalidateQueries({ qc.invalidateQueries({
queryKey: qk.planAsignaturas(updated.plan_estudio_id), queryKey: ['plan_asignaturas', updated.plan_estudio_id],
})
qc.invalidateQueries({
queryKey: qk.planHistorial(updated.plan_estudio_id),
}) })
// 3) Refresca historial de la asignatura si existe // 3. Si tienes una lista general de asignaturas, también la invalidamos
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) }) qc.invalidateQueries({ queryKey: ['asignaturas', 'list'] })
}, },
}) })
} }
@@ -279,41 +260,3 @@ export function useUpdateLinea() {
}, },
}) })
} }
export function useCreateBibliografia() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: bibliografia_insert,
onSuccess: (data) => {
// USAR LA MISMA LLAVE QUE EL HOOK DE LECTURA
queryClient.invalidateQueries({
queryKey: qk.asignaturaBibliografia(data.asignatura_id),
})
},
})
}
export function useUpdateBibliografia(asignaturaId: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: ({ id, updates }: { id: string; updates: any }) =>
bibliografia_update(id, updates),
onSuccess: () => {
qc.invalidateQueries({
queryKey: qk.asignaturaBibliografia(asignaturaId),
})
},
})
}
export function useDeleteBibliografia(asignaturaId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: string) => bibliografia_delete(id),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: qk.asignaturaBibliografia(asignaturaId),
})
},
})
}

View File

@@ -5,6 +5,7 @@ import type {
BibliografiaEntry, BibliografiaEntry,
CambioAsignatura, CambioAsignatura,
DocumentoAsignatura, DocumentoAsignatura,
LibraryResource,
} from '@/types/asignatura' } from '@/types/asignatura'
export const mockAsignatura: Asignatura = { export const mockAsignatura: Asignatura = {
@@ -309,3 +310,67 @@ export const mockDocumentoSep: DocumentoAsignatura = {
fechaGeneracion: new Date('2024-12-06T11:30:00'), fechaGeneracion: new Date('2024-12-06T11:30:00'),
estado: 'listo', estado: 'listo',
} }
export const mockLibraryResources: Array<LibraryResource> = [
{
id: 'lib-1',
titulo: 'Artificial Intelligence: A Modern Approach',
autor: 'Stuart Russell, Peter Norvig',
editorial: 'Pearson',
anio: 2021,
isbn: '978-0134610993',
tipo: 'libro',
disponible: true,
},
{
id: 'lib-2',
titulo:
'Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow',
autor: 'Aurélien Géron',
editorial: "O'Reilly Media",
anio: 2022,
isbn: '978-1098125974',
tipo: 'libro',
disponible: true,
},
{
id: 'lib-3',
titulo: 'Pattern Recognition and Machine Learning',
autor: 'Christopher Bishop',
editorial: 'Springer',
anio: 2006,
isbn: '978-0387310732',
tipo: 'libro',
disponible: true,
},
{
id: 'lib-4',
titulo: 'Deep Learning with Python',
autor: 'François Chollet',
editorial: 'Manning Publications',
anio: 2021,
isbn: '978-1617296864',
tipo: 'libro',
disponible: false,
},
{
id: 'lib-5',
titulo: 'Neural Networks and Deep Learning: A Textbook',
autor: 'Charu C. Aggarwal',
editorial: 'Springer',
anio: 2023,
isbn: '978-3031296413',
tipo: 'libro',
disponible: true,
},
{
id: 'lib-6',
titulo: 'Machine Learning: A Probabilistic Perspective',
autor: 'Kevin Murphy',
editorial: 'MIT Press',
anio: 2012,
isbn: '978-0262018029',
tipo: 'libro',
disponible: true,
},
]

View File

@@ -13,7 +13,6 @@ export const qk = {
planesList: (filters: unknown) => ['planes', 'list', filters] as const, planesList: (filters: unknown) => ['planes', 'list', filters] as const,
plan: (planId: string) => ['planes', 'detail', planId] as const, plan: (planId: string) => ['planes', 'detail', planId] as const,
planMaybe: (planId: string) => ['planes', 'detail-maybe', planId] as const,
planLineas: (planId: string) => ['planes', planId, 'lineas'] as const, planLineas: (planId: string) => ['planes', planId, 'lineas'] as const,
planAsignaturas: (planId: string) => planAsignaturas: (planId: string) =>
['planes', planId, 'asignaturas'] as const, ['planes', planId, 'asignaturas'] as const,
@@ -23,8 +22,6 @@ export const qk = {
sugerenciasAsignaturas: () => ['asignaturas', 'sugerencias'] as const, sugerenciasAsignaturas: () => ['asignaturas', 'sugerencias'] as const,
asignatura: (asignaturaId: string) => asignatura: (asignaturaId: string) =>
['asignaturas', 'detail', asignaturaId] as const, ['asignaturas', 'detail', asignaturaId] as const,
asignaturaMaybe: (asignaturaId: string) =>
['asignaturas', 'detail-maybe', asignaturaId] as const,
asignaturaBibliografia: (asignaturaId: string) => asignaturaBibliografia: (asignaturaId: string) =>
['asignaturas', asignaturaId, 'bibliografia'] as const, ['asignaturas', asignaturaId, 'bibliografia'] as const,
asignaturaHistorial: (asignaturaId: string) => asignaturaHistorial: (asignaturaId: string) =>

View File

@@ -17,22 +17,14 @@ import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-qu
import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo' import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo'
import { Route as PlanesPlanIdDetalleRouteImport } from './routes/planes/$planId/_detalle' import { Route as PlanesPlanIdDetalleRouteImport } from './routes/planes/$planId/_detalle'
import { Route as PlanesPlanIdDetalleIndexRouteImport } from './routes/planes/$planId/_detalle/index' import { Route as PlanesPlanIdDetalleIndexRouteImport } from './routes/planes/$planId/_detalle/index'
import { Route as PlanesPlanIdAsignaturasAsignaturaIdRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId'
import { Route as PlanesPlanIdDetalleMapaRouteImport } from './routes/planes/$planId/_detalle/mapa' import { Route as PlanesPlanIdDetalleMapaRouteImport } from './routes/planes/$planId/_detalle/mapa'
import { Route as PlanesPlanIdDetalleIaplanRouteImport } from './routes/planes/$planId/_detalle/iaplan' import { Route as PlanesPlanIdDetalleIaplanRouteImport } from './routes/planes/$planId/_detalle/iaplan'
import { Route as PlanesPlanIdDetalleHistorialRouteImport } from './routes/planes/$planId/_detalle/historial' import { Route as PlanesPlanIdDetalleHistorialRouteImport } from './routes/planes/$planId/_detalle/historial'
import { Route as PlanesPlanIdDetalleFlujoRouteImport } from './routes/planes/$planId/_detalle/flujo' import { Route as PlanesPlanIdDetalleFlujoRouteImport } from './routes/planes/$planId/_detalle/flujo'
import { Route as PlanesPlanIdDetalleDocumentoRouteImport } from './routes/planes/$planId/_detalle/documento' import { Route as PlanesPlanIdDetalleDocumentoRouteImport } from './routes/planes/$planId/_detalle/documento'
import { Route as PlanesPlanIdDetalleAsignaturasRouteImport } from './routes/planes/$planId/_detalle/asignaturas' import { Route as PlanesPlanIdDetalleAsignaturasRouteImport } from './routes/planes/$planId/_detalle/asignaturas'
import { Route as PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/route'
import { Route as PlanesPlanIdAsignaturasAsignaturaIdIndexRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/index'
import { Route as PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/iaasignatura'
import { Route as PlanesPlanIdAsignaturasAsignaturaIdHistorialRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/historial'
import { Route as PlanesPlanIdAsignaturasAsignaturaIdDocumentoRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/documento'
import { Route as PlanesPlanIdAsignaturasAsignaturaIdContenidoRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/contenido'
import { Route as PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/bibliografia'
import { Route as PlanesPlanIdDetalleAsignaturasNuevaRouteImport } from './routes/planes/$planId/_detalle/asignaturas/nueva' import { Route as PlanesPlanIdDetalleAsignaturasNuevaRouteImport } from './routes/planes/$planId/_detalle/asignaturas/nueva'
import { Route as PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/bibliografia/index'
import { Route as PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva'
const LoginRoute = LoginRouteImport.update({ const LoginRoute = LoginRouteImport.update({
id: '/login', id: '/login',
@@ -75,6 +67,12 @@ const PlanesPlanIdDetalleIndexRoute =
path: '/', path: '/',
getParentRoute: () => PlanesPlanIdDetalleRoute, getParentRoute: () => PlanesPlanIdDetalleRoute,
} as any) } as any)
const PlanesPlanIdAsignaturasAsignaturaIdRoute =
PlanesPlanIdAsignaturasAsignaturaIdRouteImport.update({
id: '/planes/$planId/asignaturas/$asignaturaId',
path: '/planes/$planId/asignaturas/$asignaturaId',
getParentRoute: () => rootRouteImport,
} as any)
const PlanesPlanIdDetalleMapaRoute = PlanesPlanIdDetalleMapaRouteImport.update({ const PlanesPlanIdDetalleMapaRoute = PlanesPlanIdDetalleMapaRouteImport.update({
id: '/mapa', id: '/mapa',
path: '/mapa', path: '/mapa',
@@ -110,66 +108,12 @@ const PlanesPlanIdDetalleAsignaturasRoute =
path: '/asignaturas', path: '/asignaturas',
getParentRoute: () => PlanesPlanIdDetalleRoute, getParentRoute: () => PlanesPlanIdDetalleRoute,
} as any) } as any)
const PlanesPlanIdAsignaturasAsignaturaIdRouteRoute =
PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport.update({
id: '/planes/$planId/asignaturas/$asignaturaId',
path: '/planes/$planId/asignaturas/$asignaturaId',
getParentRoute: () => rootRouteImport,
} as any)
const PlanesPlanIdAsignaturasAsignaturaIdIndexRoute =
PlanesPlanIdAsignaturasAsignaturaIdIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
} as any)
const PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute =
PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRouteImport.update({
id: '/iaasignatura',
path: '/iaasignatura',
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
} as any)
const PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute =
PlanesPlanIdAsignaturasAsignaturaIdHistorialRouteImport.update({
id: '/historial',
path: '/historial',
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
} as any)
const PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute =
PlanesPlanIdAsignaturasAsignaturaIdDocumentoRouteImport.update({
id: '/documento',
path: '/documento',
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
} as any)
const PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute =
PlanesPlanIdAsignaturasAsignaturaIdContenidoRouteImport.update({
id: '/contenido',
path: '/contenido',
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
} as any)
const PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute =
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteImport.update({
id: '/bibliografia',
path: '/bibliografia',
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
} as any)
const PlanesPlanIdDetalleAsignaturasNuevaRoute = const PlanesPlanIdDetalleAsignaturasNuevaRoute =
PlanesPlanIdDetalleAsignaturasNuevaRouteImport.update({ PlanesPlanIdDetalleAsignaturasNuevaRouteImport.update({
id: '/nueva', id: '/nueva',
path: '/nueva', path: '/nueva',
getParentRoute: () => PlanesPlanIdDetalleAsignaturasRoute, getParentRoute: () => PlanesPlanIdDetalleAsignaturasRoute,
} as any) } as any)
const PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute =
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute,
} as any)
const PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute =
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRouteImport.update({
id: '/nueva',
path: '/nueva',
getParentRoute: () => PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
@@ -179,23 +123,15 @@ export interface FileRoutesByFullPath {
'/planes': typeof PlanesListaRouteWithChildren '/planes': typeof PlanesListaRouteWithChildren
'/planes/$planId': typeof PlanesPlanIdDetalleRouteWithChildren '/planes/$planId': typeof PlanesPlanIdDetalleRouteWithChildren
'/planes/nuevo': typeof PlanesListaNuevoRoute '/planes/nuevo': typeof PlanesListaNuevoRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRouteWithChildren
'/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren '/planes/$planId/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
'/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute '/planes/$planId/documento': typeof PlanesPlanIdDetalleDocumentoRoute
'/planes/$planId/flujo': typeof PlanesPlanIdDetalleFlujoRoute '/planes/$planId/flujo': typeof PlanesPlanIdDetalleFlujoRoute
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute '/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
'/planes/$planId/iaplan': typeof PlanesPlanIdDetalleIaplanRoute '/planes/$planId/iaplan': typeof PlanesPlanIdDetalleIaplanRoute
'/planes/$planId/mapa': typeof PlanesPlanIdDetalleMapaRoute '/planes/$planId/mapa': typeof PlanesPlanIdDetalleMapaRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
'/planes/$planId/': typeof PlanesPlanIdDetalleIndexRoute '/planes/$planId/': typeof PlanesPlanIdDetalleIndexRoute
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute '/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
'/planes/$planId/asignaturas/$asignaturaId/bibliografia': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteWithChildren
'/planes/$planId/asignaturas/$asignaturaId/contenido': typeof PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute
'/planes/$planId/asignaturas/$asignaturaId/documento': typeof PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute
'/planes/$planId/asignaturas/$asignaturaId/historial': typeof PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute
'/planes/$planId/asignaturas/$asignaturaId/iaasignatura': typeof PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute
'/planes/$planId/asignaturas/$asignaturaId/': typeof PlanesPlanIdAsignaturasAsignaturaIdIndexRoute
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
@@ -210,15 +146,9 @@ export interface FileRoutesByTo {
'/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute '/planes/$planId/historial': typeof PlanesPlanIdDetalleHistorialRoute
'/planes/$planId/iaplan': typeof PlanesPlanIdDetalleIaplanRoute '/planes/$planId/iaplan': typeof PlanesPlanIdDetalleIaplanRoute
'/planes/$planId/mapa': typeof PlanesPlanIdDetalleMapaRoute '/planes/$planId/mapa': typeof PlanesPlanIdDetalleMapaRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
'/planes/$planId': typeof PlanesPlanIdDetalleIndexRoute '/planes/$planId': typeof PlanesPlanIdDetalleIndexRoute
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute '/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
'/planes/$planId/asignaturas/$asignaturaId/contenido': typeof PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute
'/planes/$planId/asignaturas/$asignaturaId/documento': typeof PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute
'/planes/$planId/asignaturas/$asignaturaId/historial': typeof PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute
'/planes/$planId/asignaturas/$asignaturaId/iaasignatura': typeof PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdIndexRoute
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute
'/planes/$planId/asignaturas/$asignaturaId/bibliografia': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
@@ -229,23 +159,15 @@ export interface FileRoutesById {
'/planes/_lista': typeof PlanesListaRouteWithChildren '/planes/_lista': typeof PlanesListaRouteWithChildren
'/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteWithChildren '/planes/$planId/_detalle': typeof PlanesPlanIdDetalleRouteWithChildren
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute '/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRouteWithChildren
'/planes/$planId/_detalle/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren '/planes/$planId/_detalle/asignaturas': typeof PlanesPlanIdDetalleAsignaturasRouteWithChildren
'/planes/$planId/_detalle/documento': typeof PlanesPlanIdDetalleDocumentoRoute '/planes/$planId/_detalle/documento': typeof PlanesPlanIdDetalleDocumentoRoute
'/planes/$planId/_detalle/flujo': typeof PlanesPlanIdDetalleFlujoRoute '/planes/$planId/_detalle/flujo': typeof PlanesPlanIdDetalleFlujoRoute
'/planes/$planId/_detalle/historial': typeof PlanesPlanIdDetalleHistorialRoute '/planes/$planId/_detalle/historial': typeof PlanesPlanIdDetalleHistorialRoute
'/planes/$planId/_detalle/iaplan': typeof PlanesPlanIdDetalleIaplanRoute '/planes/$planId/_detalle/iaplan': typeof PlanesPlanIdDetalleIaplanRoute
'/planes/$planId/_detalle/mapa': typeof PlanesPlanIdDetalleMapaRoute '/planes/$planId/_detalle/mapa': typeof PlanesPlanIdDetalleMapaRoute
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
'/planes/$planId/_detalle/': typeof PlanesPlanIdDetalleIndexRoute '/planes/$planId/_detalle/': typeof PlanesPlanIdDetalleIndexRoute
'/planes/$planId/_detalle/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute '/planes/$planId/_detalle/asignaturas/nueva': typeof PlanesPlanIdDetalleAsignaturasNuevaRoute
'/planes/$planId/asignaturas/$asignaturaId/bibliografia': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteWithChildren
'/planes/$planId/asignaturas/$asignaturaId/contenido': typeof PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute
'/planes/$planId/asignaturas/$asignaturaId/documento': typeof PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute
'/planes/$planId/asignaturas/$asignaturaId/historial': typeof PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute
'/planes/$planId/asignaturas/$asignaturaId/iaasignatura': typeof PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute
'/planes/$planId/asignaturas/$asignaturaId/': typeof PlanesPlanIdAsignaturasAsignaturaIdIndexRoute
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/': typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
@@ -257,23 +179,15 @@ export interface FileRouteTypes {
| '/planes' | '/planes'
| '/planes/$planId' | '/planes/$planId'
| '/planes/nuevo' | '/planes/nuevo'
| '/planes/$planId/asignaturas/$asignaturaId'
| '/planes/$planId/asignaturas' | '/planes/$planId/asignaturas'
| '/planes/$planId/documento' | '/planes/$planId/documento'
| '/planes/$planId/flujo' | '/planes/$planId/flujo'
| '/planes/$planId/historial' | '/planes/$planId/historial'
| '/planes/$planId/iaplan' | '/planes/$planId/iaplan'
| '/planes/$planId/mapa' | '/planes/$planId/mapa'
| '/planes/$planId/asignaturas/$asignaturaId'
| '/planes/$planId/' | '/planes/$planId/'
| '/planes/$planId/asignaturas/nueva' | '/planes/$planId/asignaturas/nueva'
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia'
| '/planes/$planId/asignaturas/$asignaturaId/contenido'
| '/planes/$planId/asignaturas/$asignaturaId/documento'
| '/planes/$planId/asignaturas/$asignaturaId/historial'
| '/planes/$planId/asignaturas/$asignaturaId/iaasignatura'
| '/planes/$planId/asignaturas/$asignaturaId/'
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva'
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia/'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
@@ -288,15 +202,9 @@ export interface FileRouteTypes {
| '/planes/$planId/historial' | '/planes/$planId/historial'
| '/planes/$planId/iaplan' | '/planes/$planId/iaplan'
| '/planes/$planId/mapa' | '/planes/$planId/mapa'
| '/planes/$planId/asignaturas/$asignaturaId'
| '/planes/$planId' | '/planes/$planId'
| '/planes/$planId/asignaturas/nueva' | '/planes/$planId/asignaturas/nueva'
| '/planes/$planId/asignaturas/$asignaturaId/contenido'
| '/planes/$planId/asignaturas/$asignaturaId/documento'
| '/planes/$planId/asignaturas/$asignaturaId/historial'
| '/planes/$planId/asignaturas/$asignaturaId/iaasignatura'
| '/planes/$planId/asignaturas/$asignaturaId'
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva'
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia'
id: id:
| '__root__' | '__root__'
| '/' | '/'
@@ -306,23 +214,15 @@ export interface FileRouteTypes {
| '/planes/_lista' | '/planes/_lista'
| '/planes/$planId/_detalle' | '/planes/$planId/_detalle'
| '/planes/_lista/nuevo' | '/planes/_lista/nuevo'
| '/planes/$planId/asignaturas/$asignaturaId'
| '/planes/$planId/_detalle/asignaturas' | '/planes/$planId/_detalle/asignaturas'
| '/planes/$planId/_detalle/documento' | '/planes/$planId/_detalle/documento'
| '/planes/$planId/_detalle/flujo' | '/planes/$planId/_detalle/flujo'
| '/planes/$planId/_detalle/historial' | '/planes/$planId/_detalle/historial'
| '/planes/$planId/_detalle/iaplan' | '/planes/$planId/_detalle/iaplan'
| '/planes/$planId/_detalle/mapa' | '/planes/$planId/_detalle/mapa'
| '/planes/$planId/asignaturas/$asignaturaId'
| '/planes/$planId/_detalle/' | '/planes/$planId/_detalle/'
| '/planes/$planId/_detalle/asignaturas/nueva' | '/planes/$planId/_detalle/asignaturas/nueva'
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia'
| '/planes/$planId/asignaturas/$asignaturaId/contenido'
| '/planes/$planId/asignaturas/$asignaturaId/documento'
| '/planes/$planId/asignaturas/$asignaturaId/historial'
| '/planes/$planId/asignaturas/$asignaturaId/iaasignatura'
| '/planes/$planId/asignaturas/$asignaturaId/'
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva'
| '/planes/$planId/asignaturas/$asignaturaId/bibliografia/'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
@@ -332,7 +232,7 @@ export interface RootRouteChildren {
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
PlanesListaRoute: typeof PlanesListaRouteWithChildren PlanesListaRoute: typeof PlanesListaRouteWithChildren
PlanesPlanIdDetalleRoute: typeof PlanesPlanIdDetalleRouteWithChildren PlanesPlanIdDetalleRoute: typeof PlanesPlanIdDetalleRouteWithChildren
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRouteWithChildren PlanesPlanIdAsignaturasAsignaturaIdRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@@ -393,6 +293,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PlanesPlanIdDetalleIndexRouteImport preLoaderRoute: typeof PlanesPlanIdDetalleIndexRouteImport
parentRoute: typeof PlanesPlanIdDetalleRoute parentRoute: typeof PlanesPlanIdDetalleRoute
} }
'/planes/$planId/asignaturas/$asignaturaId': {
id: '/planes/$planId/asignaturas/$asignaturaId'
path: '/planes/$planId/asignaturas/$asignaturaId'
fullPath: '/planes/$planId/asignaturas/$asignaturaId'
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteImport
parentRoute: typeof rootRouteImport
}
'/planes/$planId/_detalle/mapa': { '/planes/$planId/_detalle/mapa': {
id: '/planes/$planId/_detalle/mapa' id: '/planes/$planId/_detalle/mapa'
path: '/mapa' path: '/mapa'
@@ -435,55 +342,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasRouteImport preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasRouteImport
parentRoute: typeof PlanesPlanIdDetalleRoute parentRoute: typeof PlanesPlanIdDetalleRoute
} }
'/planes/$planId/asignaturas/$asignaturaId': {
id: '/planes/$planId/asignaturas/$asignaturaId'
path: '/planes/$planId/asignaturas/$asignaturaId'
fullPath: '/planes/$planId/asignaturas/$asignaturaId'
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/planes/$planId/asignaturas/$asignaturaId/': {
id: '/planes/$planId/asignaturas/$asignaturaId/'
path: '/'
fullPath: '/planes/$planId/asignaturas/$asignaturaId/'
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdIndexRouteImport
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
}
'/planes/$planId/asignaturas/$asignaturaId/iaasignatura': {
id: '/planes/$planId/asignaturas/$asignaturaId/iaasignatura'
path: '/iaasignatura'
fullPath: '/planes/$planId/asignaturas/$asignaturaId/iaasignatura'
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRouteImport
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
}
'/planes/$planId/asignaturas/$asignaturaId/historial': {
id: '/planes/$planId/asignaturas/$asignaturaId/historial'
path: '/historial'
fullPath: '/planes/$planId/asignaturas/$asignaturaId/historial'
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdHistorialRouteImport
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
}
'/planes/$planId/asignaturas/$asignaturaId/documento': {
id: '/planes/$planId/asignaturas/$asignaturaId/documento'
path: '/documento'
fullPath: '/planes/$planId/asignaturas/$asignaturaId/documento'
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdDocumentoRouteImport
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
}
'/planes/$planId/asignaturas/$asignaturaId/contenido': {
id: '/planes/$planId/asignaturas/$asignaturaId/contenido'
path: '/contenido'
fullPath: '/planes/$planId/asignaturas/$asignaturaId/contenido'
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdContenidoRouteImport
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
}
'/planes/$planId/asignaturas/$asignaturaId/bibliografia': {
id: '/planes/$planId/asignaturas/$asignaturaId/bibliografia'
path: '/bibliografia'
fullPath: '/planes/$planId/asignaturas/$asignaturaId/bibliografia'
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteImport
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
}
'/planes/$planId/_detalle/asignaturas/nueva': { '/planes/$planId/_detalle/asignaturas/nueva': {
id: '/planes/$planId/_detalle/asignaturas/nueva' id: '/planes/$planId/_detalle/asignaturas/nueva'
path: '/nueva' path: '/nueva'
@@ -491,20 +349,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasNuevaRouteImport preLoaderRoute: typeof PlanesPlanIdDetalleAsignaturasNuevaRouteImport
parentRoute: typeof PlanesPlanIdDetalleAsignaturasRoute parentRoute: typeof PlanesPlanIdDetalleAsignaturasRoute
} }
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/': {
id: '/planes/$planId/asignaturas/$asignaturaId/bibliografia/'
path: '/'
fullPath: '/planes/$planId/asignaturas/$asignaturaId/bibliografia/'
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRouteImport
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute
}
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva': {
id: '/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva'
path: '/nueva'
fullPath: '/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva'
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRouteImport
parentRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute
}
} }
} }
@@ -559,54 +403,6 @@ const PlanesPlanIdDetalleRouteChildren: PlanesPlanIdDetalleRouteChildren = {
const PlanesPlanIdDetalleRouteWithChildren = const PlanesPlanIdDetalleRouteWithChildren =
PlanesPlanIdDetalleRoute._addFileChildren(PlanesPlanIdDetalleRouteChildren) PlanesPlanIdDetalleRoute._addFileChildren(PlanesPlanIdDetalleRouteChildren)
interface PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteChildren {
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute
}
const PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteChildren: PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteChildren =
{
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute:
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaNuevaRoute,
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute:
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaIndexRoute,
}
const PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteWithChildren =
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute._addFileChildren(
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteChildren,
)
interface PlanesPlanIdAsignaturasAsignaturaIdRouteRouteChildren {
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteWithChildren
PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute
PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute
PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute
PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute
PlanesPlanIdAsignaturasAsignaturaIdIndexRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdIndexRoute
}
const PlanesPlanIdAsignaturasAsignaturaIdRouteRouteChildren: PlanesPlanIdAsignaturasAsignaturaIdRouteRouteChildren =
{
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRoute:
PlanesPlanIdAsignaturasAsignaturaIdBibliografiaRouteWithChildren,
PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute:
PlanesPlanIdAsignaturasAsignaturaIdContenidoRoute,
PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute:
PlanesPlanIdAsignaturasAsignaturaIdDocumentoRoute,
PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute:
PlanesPlanIdAsignaturasAsignaturaIdHistorialRoute,
PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute:
PlanesPlanIdAsignaturasAsignaturaIdIaasignaturaRoute,
PlanesPlanIdAsignaturasAsignaturaIdIndexRoute:
PlanesPlanIdAsignaturasAsignaturaIdIndexRoute,
}
const PlanesPlanIdAsignaturasAsignaturaIdRouteRouteWithChildren =
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute._addFileChildren(
PlanesPlanIdAsignaturasAsignaturaIdRouteRouteChildren,
)
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
DashboardRoute: DashboardRoute, DashboardRoute: DashboardRoute,
@@ -614,8 +410,8 @@ const rootRouteChildren: RootRouteChildren = {
DemoTanstackQueryRoute: DemoTanstackQueryRoute, DemoTanstackQueryRoute: DemoTanstackQueryRoute,
PlanesListaRoute: PlanesListaRouteWithChildren, PlanesListaRoute: PlanesListaRouteWithChildren,
PlanesPlanIdDetalleRoute: PlanesPlanIdDetalleRouteWithChildren, PlanesPlanIdDetalleRoute: PlanesPlanIdDetalleRouteWithChildren,
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute: PlanesPlanIdAsignaturasAsignaturaIdRoute:
PlanesPlanIdAsignaturasAsignaturaIdRouteRouteWithChildren, PlanesPlanIdAsignaturasAsignaturaIdRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)

View File

@@ -1,6 +1,7 @@
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router' import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'
import { import {
Plus, Plus,
Copy,
Search, Search,
Filter, Filter,
ChevronRight, ChevronRight,
@@ -86,7 +87,7 @@ function AsignaturasPage() {
const navigate = useNavigate() const navigate = useNavigate()
// 1. Fetch de datos reales // 1. Fetch de datos reales
const { data: asignaturaApi, isLoading: loadingAsig } = const { data: asignaturasApi, isLoading: loadingAsig } =
usePlanAsignaturas(planId) usePlanAsignaturas(planId)
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId) const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
@@ -98,8 +99,8 @@ function AsignaturasPage() {
// 3. Procesamiento de datos // 3. Procesamiento de datos
const asignaturas = useMemo( const asignaturas = useMemo(
() => mapAsignaturas(asignaturaApi), () => mapAsignaturas(asignaturasApi),
[asignaturaApi], [asignaturasApi],
) )
const lineas = useMemo(() => lineasApi || [], [lineasApi]) const lineas = useMemo(() => lineasApi || [], [lineasApi])
@@ -143,6 +144,9 @@ function AsignaturasPage() {
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline" size="sm">
<Copy className="mr-2 h-4 w-4" /> Clonar
</Button>
<Button <Button
onClick={() => { onClick={() => {
console.log('planId desde asignaturas', planId) console.log('planId desde asignaturas', planId)

View File

@@ -96,8 +96,7 @@ function RouteComponent() {
}`, }`,
date: parseISO(item.cambiado_en), date: parseISO(item.cambiado_en),
icon: config.icon, icon: config.icon,
campo: campo: item.campo,
data?.estructuras_plan?.definicion?.properties?.[item.campo]?.title,
details: { details: {
from: item.valor_anterior, from: item.valor_anterior,
to: item.valor_nuevo, to: item.valor_nuevo,
@@ -299,8 +298,6 @@ function RouteComponent() {
<div className="flex-1 overflow-y-auto p-6"> <div className="flex-1 overflow-y-auto p-6">
<div className="grid h-full grid-cols-2 gap-6"> <div className="grid h-full grid-cols-2 gap-6">
{/* Lado Antes */} {/* Lado Antes */}
{/* Lado Antes: Solo se renderiza si existe valor_anterior */}
{selectedEvent?.details.from && (
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<div className="sticky top-0 z-10 flex items-center gap-2 bg-white py-1"> <div className="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" /> <div className="h-2 w-2 rounded-full bg-red-400" />
@@ -309,10 +306,9 @@ function RouteComponent() {
</span> </span>
</div> </div>
<div className="max-h-[500px] min-h-[250px] flex-1 overflow-y-auto rounded-lg border border-red-100 bg-red-50/30 p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap text-slate-700"> <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)} {renderValue(selectedEvent?.details.from)}
</div> </div>
</div> </div>
)}
{/* Lado Después */} {/* Lado Después */}
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
@@ -332,11 +328,6 @@ function RouteComponent() {
<div className="flex justify-center border-t bg-slate-50 p-4"> <div className="flex justify-center border-t bg-slate-50 p-4">
<Badge variant="outline" className="font-mono text-[10px]"> <Badge variant="outline" className="font-mono text-[10px]">
Campo: {selectedEvent?.campo} Campo: {selectedEvent?.campo}
{console.log(
data?.estructuras_plan?.definicion?.properties?.[
selectedEvent?.campo
]?.title,
)}
</Badge> </Badge>
</div> </div>
</DialogContent> </DialogContent>

File diff suppressed because it is too large Load Diff

View File

@@ -160,7 +160,6 @@ function DatosGeneralesPage() {
if (!data?.datos) return if (!data?.datos) return
const datosActualizados = prepararDatosActualizados(data, campo, valor) const datosActualizados = prepararDatosActualizados(data, campo, valor)
console.log(datosActualizados)
updatePlan.mutate({ updatePlan.mutate({
planId, planId,

View File

@@ -76,7 +76,7 @@ const mapAsignaturasToAsignaturas = (
// Mapeo directo de los nuevos campos de la API // Mapeo directo de los nuevos campos de la API
hd: asig.horas_academicas ?? 0, hd: asig.horas_academicas ?? 0,
hi: asig.horas_independientes ?? 0, hi: asig.horas_independientes ?? 0,
prerrequisito_asignatura_id: asig.prerrequisito_asignatura_id ?? null, prerrequisitos: [],
} }
}) })
} }
@@ -183,7 +183,7 @@ function MapaCurricularPage() {
const { mutate: createLinea } = useCreateLinea() const { mutate: createLinea } = useCreateLinea()
const { mutate: updateLineaApi } = useUpdateLinea() const { mutate: updateLineaApi } = useUpdateLinea()
const { mutate: deleteLineaApi } = useDeleteLinea() const { mutate: deleteLineaApi } = useDeleteLinea()
const { data: asignaturaApi, isLoading: loadingAsig } = const { data: asignaturasApi, isLoading: loadingAsig } =
usePlanAsignaturas(planId) usePlanAsignaturas(planId)
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId) const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
const [asignaturas, setAsignaturas] = useState<Array<Asignatura>>([]) const [asignaturas, setAsignaturas] = useState<Array<Asignatura>>([])
@@ -286,9 +286,9 @@ function MapaCurricularPage() {
}, [lineas]) }, [lineas])
useEffect(() => { useEffect(() => {
if (asignaturaApi) if (asignaturasApi)
setAsignaturas(mapAsignaturasToAsignaturas(asignaturaApi)) setAsignaturas(mapAsignaturasToAsignaturas(asignaturasApi))
}, [asignaturaApi]) }, [asignaturasApi])
useEffect(() => { useEffect(() => {
if (lineasApi) setLineas(mapLineasToLineaCurricular(lineasApi)) if (lineasApi) setLineas(mapLineasToLineaCurricular(lineasApi))
@@ -336,7 +336,6 @@ function MapaCurricularPage() {
horas_independientes?: TablesUpdate<'asignaturas'>['horas_independientes'] horas_independientes?: TablesUpdate<'asignaturas'>['horas_independientes']
numero_ciclo?: TablesUpdate<'asignaturas'>['numero_ciclo'] numero_ciclo?: TablesUpdate<'asignaturas'>['numero_ciclo']
linea_plan_id?: TablesUpdate<'asignaturas'>['linea_plan_id'] linea_plan_id?: TablesUpdate<'asignaturas'>['linea_plan_id']
prerrequisito_asignatura_id?: string | null
} }
const patch: Partial<AsignaturaPatch> = { const patch: Partial<AsignaturaPatch> = {
nombre: editingData.nombre, nombre: editingData.nombre,
@@ -346,7 +345,6 @@ function MapaCurricularPage() {
horas_independientes: editingData.hi, horas_independientes: editingData.hi,
numero_ciclo: editingData.ciclo, numero_ciclo: editingData.ciclo,
linea_plan_id: editingData.lineaCurricularId, linea_plan_id: editingData.lineaCurricularId,
prerrequisito_asignatura_id: editingData.prerrequisito_asignatura_id,
tipo: editingData.tipo.toUpperCase() as TipoAsignatura, // Asegurar que coincida con el ENUM (OBLIGATORIA/OPTATIVA) tipo: editingData.tipo.toUpperCase() as TipoAsignatura, // Asegurar que coincida con el ENUM (OBLIGATORIA/OPTATIVA)
} }
@@ -492,7 +490,7 @@ function MapaCurricularPage() {
e: React.FocusEvent<HTMLSpanElement>, e: React.FocusEvent<HTMLSpanElement>,
id: string, id: string,
) => { ) => {
const nuevoNombre = e.currentTarget.textContent.trim() || '' const nuevoNombre = e.currentTarget.textContent?.trim() || ''
// Buscamos la línea original para comparar // Buscamos la línea original para comparar
const lineaOriginal = lineas.find((l) => l.id === id) const lineaOriginal = lineas.find((l) => l.id === id)
@@ -937,55 +935,65 @@ function MapaCurricularPage() {
{/* Fila 4: Seriación (Prerrequisitos) */} {/* Fila 4: Seriación (Prerrequisitos) */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase"> <label className="text-xs font-bold text-slate-500 uppercase">
Seriación (Prerrequisito) Seriación (Prerrequisitos)
</label> </label>
<Select <Select
// Cambiamos a manejo de valor único basado en el ID de la columna value={seriacionValue}
value={editingData.prerrequisito_asignatura_id || undefined}
onValueChange={(val) => { onValueChange={(val) => {
console.log(editingData) if (val === 'none') {
setSeriacionValue('')
return
}
if (!editingData.prerrequisitos.includes(val)) {
setEditingData({ setEditingData({
...editingData, ...editingData,
prerrequisito_asignatura_id: val === 'none' ? null : val, prerrequisitos: [...editingData.prerrequisitos, val],
}) })
}
setSeriacionValue('')
}} }}
> >
<SelectTrigger className="w-full bg-white"> <SelectTrigger>
<SelectValue placeholder="Seleccionar asignatura..." /> <SelectValue placeholder="Seleccionar asignatura..." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="none">-- Sin Seriación --</SelectItem> <SelectItem value="none">-- Sin Seriación --</SelectItem>
{asignaturas {asignaturas
.filter((asig) => { .filter((m) => m.id !== editingData.id)
// 1. No es la misma materia .map((m) => (
const noEsMisma = asig.id !== editingData.id <SelectItem key={m.id} value={m.id}>
// 2. El ciclo debe ser estrictamente MENOR {m.nombre} ({m.clave})
const esCicloMenor =
asig.ciclo !== null &&
editingData.ciclo !== null &&
asig.ciclo < editingData.ciclo
return noEsMisma && esCicloMenor
})
.sort(
(a, b) =>
(a.ciclo || 0) - (b.ciclo || 0) ||
a.nombre.localeCompare(b.nombre),
)
.map((asig) => (
<SelectItem key={asig.id} value={asig.id}>
<span className="font-bold text-teal-600">
[C{asig.ciclo}]
</span>{' '}
{asig.nombre}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
{/* Visualización del Prerrequisito con el Nombre */} {/* Visualización de los prerrequisitos seleccionados */}
<div className="mt-2 flex flex-wrap gap-2">
{editingData.prerrequisitos.map((pre) => (
<Badge
key={pre}
variant="secondary"
className="bg-slate-100 text-slate-600"
>
{pre}
<button
className="ml-1 hover:text-red-500"
onClick={() => {
setEditingData({
...editingData,
prerrequisitos: editingData.prerrequisitos.filter(
(p) => p !== pre,
),
})
}}
>
×
</button>
</Badge>
))}
</div>
</div> </div>
{/* Fila 5: Tipo */} {/* Fila 5: Tipo */}

View File

@@ -0,0 +1,55 @@
import { createFileRoute, notFound, useLocation } from '@tanstack/react-router'
import { useEffect } from 'react'
import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage'
import { lateralConfetti } from '@/components/ui/lateral-confetti'
import { NotFoundPage } from '@/components/ui/NotFoundPage'
import { subjects_get } from '@/data/api/subjects.api'
import { qk } from '@/data/query/keys'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId',
)({
loader: async ({ context: { queryClient }, params: { asignaturaId } }) => {
try {
await queryClient.ensureQueryData({
queryKey: qk.asignatura(asignaturaId),
queryFn: () => subjects_get(asignaturaId),
})
} catch (e: any) {
// PGRST116: The result contains 0 rows (Supabase Single response error)
if (e?.code === 'PGRST116') {
throw notFound()
}
throw e
}
},
notFoundComponent: () => {
return (
<NotFoundPage
title="Materia no encontrada"
message="La asignatura que buscas no existe o fue eliminada."
/>
)
},
component: RouteComponent,
})
function RouteComponent() {
// const { planId, asignaturaId } = Route.useParams()
const location = useLocation()
// Confetti al llegar desde creación
useEffect(() => {
if ((location.state as any)?.showConfetti) {
lateralConfetti()
window.history.replaceState({}, document.title) // Limpiar el estado para que no se repita
}
}, [location.state])
return (
<div>
<AsignaturaDetailPage></AsignaturaDetailPage>
</div>
)
}

View File

@@ -1,11 +0,0 @@
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId/bibliografia',
)({
component: RouteComponent,
})
function RouteComponent() {
return <Outlet />
}

View File

@@ -1,13 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
import { BibliographyItem } from '@/components/asignaturas/detalle/BibliographyItem'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/',
)({
component: RouteComponent,
})
function RouteComponent() {
return <BibliographyItem />
}

View File

@@ -1,19 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
import { NuevaBibliografiaModalContainer } from '@/features/bibliografia/nueva/NuevaBibliografiaModalContainer'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId/bibliografia/nueva',
)({
component: NuevaBibliografiaModal,
})
function NuevaBibliografiaModal() {
const { planId, asignaturaId } = Route.useParams()
return (
<NuevaBibliografiaModalContainer
planId={planId}
asignaturaId={asignaturaId}
/>
)
}

View File

@@ -1,13 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
import { ContenidoTematico } from '@/components/asignaturas/detalle/ContenidoTematico'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId/contenido',
)({
component: RouteComponent,
})
function RouteComponent() {
return <ContenidoTematico />
}

View File

@@ -1,85 +0,0 @@
import { createFileRoute, useParams } from '@tanstack/react-router'
import { useCallback, useEffect, useState } from 'react'
import { DocumentoSEPTab } from '@/components/asignaturas/detalle/DocumentoSEPTab'
import { fetchAsignaturaPdf } from '@/data/api/document.api'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId/documento',
)({
component: RouteComponent,
})
function RouteComponent() {
const { asignaturaId } = useParams({
from: '/planes/$planId/asignaturas/$asignaturaId/documento',
})
const [pdfUrl, setPdfUrl] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isRegenerating, setIsRegenerating] = useState(false)
const loadPdfPreview = useCallback(async () => {
try {
setIsLoading(true)
const pdfBlob = await fetchAsignaturaPdf({
asignatura_id: asignaturaId,
})
const url = window.URL.createObjectURL(pdfBlob)
setPdfUrl((prev) => {
if (prev) window.URL.revokeObjectURL(prev)
return url
})
} catch (error) {
console.error('Error cargando PDF:', error)
} finally {
setIsLoading(false)
}
}, [asignaturaId])
useEffect(() => {
loadPdfPreview()
return () => {
if (pdfUrl) window.URL.revokeObjectURL(pdfUrl)
}
}, [loadPdfPreview])
const handleDownload = async () => {
const pdfBlob = await fetchAsignaturaPdf({
asignatura_id: asignaturaId,
})
const url = window.URL.createObjectURL(pdfBlob)
const link = document.createElement('a')
link.href = url
link.download = 'documento_sep.pdf'
document.body.appendChild(link)
link.click()
link.remove()
window.URL.revokeObjectURL(url)
}
const handleRegenerate = async () => {
try {
setIsRegenerating(true)
await loadPdfPreview()
} finally {
setIsRegenerating(false)
}
}
return (
<DocumentoSEPTab
pdfUrl={pdfUrl}
isLoading={isLoading}
onDownload={handleDownload}
onRegenerate={handleRegenerate}
isRegenerating={isRegenerating}
/>
)
}

View File

@@ -1,13 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
import { HistorialTab } from '@/components/asignaturas/detalle/HistorialTab'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId/historial',
)({
component: RouteComponent,
})
function RouteComponent() {
return <HistorialTab />
}

View File

@@ -1,13 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
import { IAAsignaturaTab } from '@/components/asignaturas/detalle/IAAsignaturaTab'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId/iaasignatura',
)({
component: RouteComponent,
})
function RouteComponent() {
return <IAAsignaturaTab />
}

View File

@@ -1,13 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId/',
)({
component: DatosGeneralesPage,
})
function DatosGeneralesPage() {
return <AsignaturaDetailPage />
}

View File

@@ -1,263 +0,0 @@
import {
createFileRoute,
Outlet,
Link,
useLocation,
useParams,
useRouterState,
} from '@tanstack/react-router'
import { ArrowLeft, GraduationCap } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { lateralConfetti } from '@/components/ui/lateral-confetti'
import { useSubject, useUpdateAsignatura } from '@/data'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId',
)({
component: AsignaturaLayout,
})
function EditableHeaderField({
value,
onSave,
className,
}: {
value: string | number
onSave: (val: string) => void
className?: string
}) {
const textValue = String(value)
// Manejador para cuando el usuario termina de editar (pierde el foco)
const handleBlur = (e: React.FocusEvent<HTMLSpanElement>) => {
const newValue = e.currentTarget.innerText
if (newValue !== textValue) {
onSave(newValue)
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
e.currentTarget.blur() // Forzamos el guardado al presionar Enter
}
}
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<span
contentEditable
suppressContentEditableWarning={true} // Evita el warning de React por tener hijos y contentEditable
spellCheck={false}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
className={`inline-block cursor-text rounded-sm px-1 transition-all hover:bg-white/10 focus:bg-white/20 focus:ring-2 focus:ring-blue-400/50 focus:outline-none ${className ?? ''} `}
>
{textValue}
</span>
)
}
interface DatosPlan {
nombre?: string
}
function AsignaturaLayout() {
const location = useLocation()
const { asignaturaId } = useParams({
from: '/planes/$planId/asignaturas/$asignaturaId',
})
const { planId } = useParams({
from: '/planes/$planId/asignaturas/$asignaturaId',
})
const { data: asignaturaApi, isLoading: loadingAsig } =
useSubject(asignaturaId)
// 1. Asegúrate de tener estos estados en tu componente principal
const updateAsignatura = useUpdateAsignatura()
// Dentro de AsignaturaDetailPage
const [headerData, setHeaderData] = useState({
codigo: '',
nombre: '',
creditos: 0,
ciclo: 0,
})
// Sincronizar cuando llegue la API
useEffect(() => {
if (asignaturaApi) {
setHeaderData({
codigo: asignaturaApi.codigo ?? '',
nombre: asignaturaApi.nombre,
creditos: asignaturaApi.creditos,
ciclo: asignaturaApi.numero_ciclo ?? 0,
})
}
}, [asignaturaApi])
const handleUpdateHeader = (key: string, value: string | number) => {
const newData = { ...headerData, [key]: value }
setHeaderData(newData)
const patch: Record<string, any> =
key === 'ciclo'
? { numero_ciclo: value }
: {
[key]: value,
}
updateAsignatura.mutate({
asignaturaId,
patch,
})
}
const pathname = useRouterState({
select: (state) => state.location.pathname,
})
// Confetti al llegar desde creación IA
useEffect(() => {
if ((location.state as any)?.showConfetti) {
lateralConfetti()
window.history.replaceState({}, document.title)
}
}, [location.state])
if (loadingAsig) {
return (
<div className="flex h-screen items-center justify-center bg-[#0b1d3a] text-white">
Cargando asignatura...
</div>
)
}
// Si no hay datos y no está cargando, algo falló
if (!asignaturaApi) return null
return (
<div>
<section className="bg-linear-to-b from-[#0b1d3a] to-[#0e2a5c] text-white">
<div className="mx-auto max-w-7xl px-6 py-10">
<Link
to="/planes/$planId/asignaturas"
params={{ planId }}
className="mb-4 flex items-center gap-2 text-sm text-blue-200 hover:text-white"
>
<ArrowLeft className="h-4 w-4" /> Volver al plan
</Link>
<div className="flex items-start justify-between gap-6">
<div className="space-y-3">
{/* CÓDIGO EDITABLE */}
<Badge className="border border-blue-700 bg-blue-900/50">
<EditableHeaderField
value={headerData.codigo}
onSave={(val) => handleUpdateHeader('codigo', val)}
/>
</Badge>
{/* NOMBRE EDITABLE */}
<h1 className="text-3xl font-bold">
<EditableHeaderField
value={headerData.nombre}
onSave={(val) => handleUpdateHeader('nombre', val)}
/>
</h1>
{
// console.log(headerData),
console.log(asignaturaApi.planes_estudio?.nombre)
}
<div className="flex flex-wrap gap-4 text-sm text-blue-200">
<span className="flex items-center gap-1">
<GraduationCap className="h-4 w-4 shrink-0" />
Pertenece al plan:{' '}
<span className="text-blue-100">
{(asignaturaApi.planes_estudio as DatosPlan).nombre || ''}
</span>
</span>
</div>
</div>
<div className="flex flex-col items-end gap-2 text-right">
{/* CRÉDITOS EDITABLES */}
<Badge variant="secondary" className="gap-1">
<span className="inline-flex max-w-fit">
<EditableHeaderField
value={headerData.creditos}
onSave={(val) =>
handleUpdateHeader('creditos', parseInt(val) || 0)
}
/>
</span>
<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">{asignaturaApi.tipo}</Badge>
</div>
</div>
</div>
</section>
{/* TABS */}
<nav className="border-b bg-white">
<div className="mx-auto max-w-7xl px-6">
<div className="flex justify-center gap-8">
{[
{ label: 'Datos', to: '' },
{ label: 'Contenido', to: 'contenido' },
{ label: 'Bibliografía', to: 'bibliografia' },
{ label: 'IA', to: 'iaasignatura' },
{ label: 'Documento SEP', to: 'documento' },
{ label: 'Historial', to: 'historial' },
].map((tab) => {
const isActive =
tab.to === ''
? pathname === `/planes/${planId}/asignaturas/${asignaturaId}`
: pathname.includes(tab.to)
return (
<Link
key={tab.label}
to={
(tab.to === ''
? '/planes/$planId/asignaturas/$asignaturaId'
: `/planes/$planId/asignaturas/$asignaturaId/${tab.to}`) as any
}
from="/planes/$planId/asignaturas/$asignaturaId"
params={{ planId, asignaturaId }}
className={`border-b-2 py-3 text-sm font-medium ${
isActive
? 'border-blue-600 text-blue-600'
: 'border-transparent text-slate-500 hover:border-slate-300 hover:text-slate-700'
}`}
>
{tab.label}
</Link>
)
})}
</div>
</div>
</nav>
<div className="mx-auto max-w-7xl px-6 py-8">
<Outlet />
</div>
</div>
)
}

View File

@@ -1,12 +0,0 @@
declare module 'citeproc' {
const CSL: {
Engine: new (
sys: any,
style: string,
lang?: string,
forceLang?: boolean,
) => any
}
export default CSL
}

View File

@@ -50,7 +50,7 @@ export interface Asignatura {
orden?: number orden?: number
hd: number // <--- Añadir hd: number // <--- Añadir
hi: number // <--- Añadir hi: number // <--- Añadir
prerrequisito_asignatura_id: string | null prerrequisitos: Array<string>
} }
export interface Plan { export interface Plan {

View File

@@ -1,4 +1,4 @@
export type Json = export type Json =
| string | string
| number | number
| boolean | boolean
@@ -81,56 +81,6 @@ export type Database = {
}, },
] ]
} }
asignatura_mensajes_ia: {
Row: {
campos: Array<string>
conversacion_asignatura_id: string
enviado_por: string
estado: Database['public']['Enums']['estado_mensaje_ia']
fecha_actualizacion: string
fecha_creacion: string
id: string
is_refusal: boolean
mensaje: string
propuesta: Json | null
respuesta: string | null
}
Insert: {
campos?: Array<string>
conversacion_asignatura_id: string
enviado_por?: string
estado?: Database['public']['Enums']['estado_mensaje_ia']
fecha_actualizacion?: string
fecha_creacion?: string
id?: string
is_refusal?: boolean
mensaje: string
propuesta?: Json | null
respuesta?: string | null
}
Update: {
campos?: Array<string>
conversacion_asignatura_id?: string
enviado_por?: string
estado?: Database['public']['Enums']['estado_mensaje_ia']
fecha_actualizacion?: string
fecha_creacion?: string
id?: string
is_refusal?: boolean
mensaje?: string
propuesta?: Json | null
respuesta?: string | null
}
Relationships: [
{
foreignKeyName: 'asignatura_mensajes_ia_conversacion_asignatura_id_fkey'
columns: ['conversacion_asignatura_id']
isOneToOne: false
referencedRelation: 'conversaciones_asignatura'
referencedColumns: ['id']
},
]
}
asignaturas: { asignaturas: {
Row: { Row: {
actualizado_en: string actualizado_en: string
@@ -141,7 +91,6 @@ export type Database = {
creado_en: string creado_en: string
creado_por: string | null creado_por: string | null
creditos: number creditos: number
criterios_de_evaluacion: Json
datos: Json datos: Json
estado: Database['public']['Enums']['estado_asignatura'] estado: Database['public']['Enums']['estado_asignatura']
estructura_id: string | null estructura_id: string | null
@@ -156,7 +105,6 @@ export type Database = {
plan_estudio_id: string plan_estudio_id: string
tipo: Database['public']['Enums']['tipo_asignatura'] tipo: Database['public']['Enums']['tipo_asignatura']
tipo_origen: Database['public']['Enums']['tipo_origen'] | null tipo_origen: Database['public']['Enums']['tipo_origen'] | null
prerrequisito_asignatura_id?: string
} }
Insert: { Insert: {
actualizado_en?: string actualizado_en?: string
@@ -167,7 +115,6 @@ export type Database = {
creado_en?: string creado_en?: string
creado_por?: string | null creado_por?: string | null
creditos: number creditos: number
criterios_de_evaluacion?: Json
datos?: Json datos?: Json
estado?: Database['public']['Enums']['estado_asignatura'] estado?: Database['public']['Enums']['estado_asignatura']
estructura_id?: string | null estructura_id?: string | null
@@ -192,7 +139,6 @@ export type Database = {
creado_en?: string creado_en?: string
creado_por?: string | null creado_por?: string | null
creditos?: number creditos?: number
criterios_de_evaluacion?: Json
datos?: Json datos?: Json
estado?: Database['public']['Enums']['estado_asignatura'] estado?: Database['public']['Enums']['estado_asignatura']
estructura_id?: string | null estructura_id?: string | null
@@ -230,13 +176,6 @@ export type Database = {
referencedRelation: 'estructuras_asignatura' referencedRelation: 'estructuras_asignatura'
referencedColumns: ['id'] referencedColumns: ['id']
}, },
{
foreignKeyName: 'asignaturas_estructura_id_fkey'
columns: ['estructura_id']
isOneToOne: false
referencedRelation: 'plantilla_asignatura'
referencedColumns: ['estructura_id']
},
{ {
foreignKeyName: 'asignaturas_linea_plan_fk_compuesta' foreignKeyName: 'asignaturas_linea_plan_fk_compuesta'
columns: ['linea_plan_id', 'plan_estudio_id'] columns: ['linea_plan_id', 'plan_estudio_id']
@@ -264,35 +203,35 @@ export type Database = {
Row: { Row: {
actualizado_en: string actualizado_en: string
asignatura_id: string asignatura_id: string
biblioteca_item_id: string | null
cita: string cita: string
creado_en: string creado_en: string
creado_por: string | null creado_por: string | null
id: string id: string
referencia_biblioteca: string | null
referencia_en_linea: string | null
tipo: Database['public']['Enums']['tipo_bibliografia'] tipo: Database['public']['Enums']['tipo_bibliografia']
tipo_fuente: Database['public']['Enums']['tipo_fuente_bibliografia']
} }
Insert: { Insert: {
actualizado_en?: string actualizado_en?: string
asignatura_id: string asignatura_id: string
biblioteca_item_id?: string | null
cita: string cita: string
creado_en?: string creado_en?: string
creado_por?: string | null creado_por?: string | null
id?: string id?: string
referencia_biblioteca?: string | null
referencia_en_linea?: string | null
tipo: Database['public']['Enums']['tipo_bibliografia'] tipo: Database['public']['Enums']['tipo_bibliografia']
tipo_fuente?: Database['public']['Enums']['tipo_fuente_bibliografia']
} }
Update: { Update: {
actualizado_en?: string actualizado_en?: string
asignatura_id?: string asignatura_id?: string
biblioteca_item_id?: string | null
cita?: string cita?: string
creado_en?: string creado_en?: string
creado_por?: string | null creado_por?: string | null
id?: string id?: string
referencia_biblioteca?: string | null
referencia_en_linea?: string | null
tipo?: Database['public']['Enums']['tipo_bibliografia'] tipo?: Database['public']['Enums']['tipo_bibliografia']
tipo_fuente?: Database['public']['Enums']['tipo_fuente_bibliografia']
} }
Relationships: [ Relationships: [
{ {
@@ -302,13 +241,6 @@ export type Database = {
referencedRelation: 'asignaturas' referencedRelation: 'asignaturas'
referencedColumns: ['id'] referencedColumns: ['id']
}, },
{
foreignKeyName: 'bibliografia_asignatura_asignatura_id_fkey'
columns: ['asignatura_id']
isOneToOne: false
referencedRelation: 'plantilla_asignatura'
referencedColumns: ['asignatura_id']
},
{ {
foreignKeyName: 'bibliografia_asignatura_creado_por_fkey' foreignKeyName: 'bibliografia_asignatura_creado_por_fkey'
columns: ['creado_por'] columns: ['creado_por']
@@ -363,13 +295,6 @@ export type Database = {
referencedRelation: 'asignaturas' referencedRelation: 'asignaturas'
referencedColumns: ['id'] referencedColumns: ['id']
}, },
{
foreignKeyName: 'cambios_asignatura_asignatura_id_fkey'
columns: ['asignatura_id']
isOneToOne: false
referencedRelation: 'plantilla_asignatura'
referencedColumns: ['asignatura_id']
},
{ {
foreignKeyName: 'cambios_asignatura_cambiado_por_fkey' foreignKeyName: 'cambios_asignatura_cambiado_por_fkey'
columns: ['cambiado_por'] columns: ['cambiado_por']
@@ -475,7 +400,6 @@ export type Database = {
estado: Database['public']['Enums']['estado_conversacion'] estado: Database['public']['Enums']['estado_conversacion']
id: string id: string
intento_archivado: number intento_archivado: number
nombre: string | null
openai_conversation_id: string openai_conversation_id: string
} }
Insert: { Insert: {
@@ -488,7 +412,6 @@ export type Database = {
estado?: Database['public']['Enums']['estado_conversacion'] estado?: Database['public']['Enums']['estado_conversacion']
id?: string id?: string
intento_archivado?: number intento_archivado?: number
nombre?: string | null
openai_conversation_id: string openai_conversation_id: string
} }
Update: { Update: {
@@ -501,7 +424,6 @@ export type Database = {
estado?: Database['public']['Enums']['estado_conversacion'] estado?: Database['public']['Enums']['estado_conversacion']
id?: string id?: string
intento_archivado?: number intento_archivado?: number
nombre?: string | null
openai_conversation_id?: string openai_conversation_id?: string
} }
Relationships: [ Relationships: [
@@ -519,13 +441,6 @@ export type Database = {
referencedRelation: 'asignaturas' referencedRelation: 'asignaturas'
referencedColumns: ['id'] referencedColumns: ['id']
}, },
{
foreignKeyName: 'conversaciones_asignatura_asignatura_id_fkey'
columns: ['asignatura_id']
isOneToOne: false
referencedRelation: 'plantilla_asignatura'
referencedColumns: ['asignatura_id']
},
{ {
foreignKeyName: 'conversaciones_asignatura_creado_por_fkey' foreignKeyName: 'conversaciones_asignatura_creado_por_fkey'
columns: ['creado_por'] columns: ['creado_por']
@@ -545,7 +460,6 @@ export type Database = {
estado: Database['public']['Enums']['estado_conversacion'] estado: Database['public']['Enums']['estado_conversacion']
id: string id: string
intento_archivado: number intento_archivado: number
nombre: string | null
openai_conversation_id: string openai_conversation_id: string
plan_estudio_id: string plan_estudio_id: string
} }
@@ -558,7 +472,6 @@ export type Database = {
estado?: Database['public']['Enums']['estado_conversacion'] estado?: Database['public']['Enums']['estado_conversacion']
id?: string id?: string
intento_archivado?: number intento_archivado?: number
nombre?: string | null
openai_conversation_id: string openai_conversation_id: string
plan_estudio_id: string plan_estudio_id: string
} }
@@ -571,7 +484,6 @@ export type Database = {
estado?: Database['public']['Enums']['estado_conversacion'] estado?: Database['public']['Enums']['estado_conversacion']
id?: string id?: string
intento_archivado?: number intento_archivado?: number
nombre?: string | null
openai_conversation_id?: string openai_conversation_id?: string
plan_estudio_id?: string plan_estudio_id?: string
} }
@@ -637,8 +549,7 @@ export type Database = {
definicion: Json definicion: Json
id: string id: string
nombre: string nombre: string
template_id: string | null version: string | null
tipo: Database['public']['Enums']['tipo_estructura_plan'] | null
} }
Insert: { Insert: {
actualizado_en?: string actualizado_en?: string
@@ -646,8 +557,7 @@ export type Database = {
definicion?: Json definicion?: Json
id?: string id?: string
nombre: string nombre: string
template_id?: string | null version?: string | null
tipo?: Database['public']['Enums']['tipo_estructura_plan'] | null
} }
Update: { Update: {
actualizado_en?: string actualizado_en?: string
@@ -655,8 +565,7 @@ export type Database = {
definicion?: Json definicion?: Json
id?: string id?: string
nombre?: string nombre?: string
template_id?: string | null version?: string | null
tipo?: Database['public']['Enums']['tipo_estructura_plan'] | null
} }
Relationships: [] Relationships: []
} }
@@ -780,13 +689,6 @@ export type Database = {
referencedRelation: 'asignaturas' referencedRelation: 'asignaturas'
referencedColumns: ['id'] referencedColumns: ['id']
}, },
{
foreignKeyName: 'interacciones_ia_asignatura_id_fkey'
columns: ['asignatura_id']
isOneToOne: false
referencedRelation: 'plantilla_asignatura'
referencedColumns: ['asignatura_id']
},
{ {
foreignKeyName: 'interacciones_ia_plan_estudio_id_fkey' foreignKeyName: 'interacciones_ia_plan_estudio_id_fkey'
columns: ['plan_estudio_id'] columns: ['plan_estudio_id']
@@ -893,56 +795,6 @@ export type Database = {
}, },
] ]
} }
plan_mensajes_ia: {
Row: {
campos: Array<string>
conversacion_plan_id: string
enviado_por: string
estado: Database['public']['Enums']['estado_mensaje_ia']
fecha_actualizacion: string
fecha_creacion: string
id: string
is_refusal: boolean
mensaje: string
propuesta: Json | null
respuesta: string | null
}
Insert: {
campos?: Array<string>
conversacion_plan_id: string
enviado_por?: string
estado?: Database['public']['Enums']['estado_mensaje_ia']
fecha_actualizacion?: string
fecha_creacion?: string
id?: string
is_refusal?: boolean
mensaje: string
propuesta?: Json | null
respuesta?: string | null
}
Update: {
campos?: Array<string>
conversacion_plan_id?: string
enviado_por?: string
estado?: Database['public']['Enums']['estado_mensaje_ia']
fecha_actualizacion?: string
fecha_creacion?: string
id?: string
is_refusal?: boolean
mensaje?: string
propuesta?: Json | null
respuesta?: string | null
}
Relationships: [
{
foreignKeyName: 'plan_mensajes_ia_conversacion_plan_id_fkey'
columns: ['conversacion_plan_id']
isOneToOne: false
referencedRelation: 'conversaciones_plan'
referencedColumns: ['id']
},
]
}
planes_estudio: { planes_estudio: {
Row: { Row: {
activo: boolean activo: boolean
@@ -1079,13 +931,6 @@ export type Database = {
referencedRelation: 'asignaturas' referencedRelation: 'asignaturas'
referencedColumns: ['id'] referencedColumns: ['id']
}, },
{
foreignKeyName: 'responsables_asignatura_asignatura_id_fkey'
columns: ['asignatura_id']
isOneToOne: false
referencedRelation: 'plantilla_asignatura'
referencedColumns: ['asignatura_id']
},
{ {
foreignKeyName: 'responsables_asignatura_usuario_id_fkey' foreignKeyName: 'responsables_asignatura_usuario_id_fkey'
columns: ['usuario_id'] columns: ['usuario_id']
@@ -1351,14 +1196,6 @@ export type Database = {
} }
} }
Views: { Views: {
plantilla_asignatura: {
Row: {
asignatura_id: string | null
estructura_id: string | null
template_id: string | null
}
Relationships: []
}
plantilla_plan: { plantilla_plan: {
Row: { Row: {
estructura_id: string | null estructura_id: string | null
@@ -1369,21 +1206,12 @@ export type Database = {
} }
} }
Functions: { Functions: {
append_conversacion_asignatura: {
Args: { p_append: Json; p_id: string }
Returns: undefined
}
append_conversacion_plan: {
Args: { p_append: Json; p_id: string }
Returns: undefined
}
unaccent: { Args: { '': string }; Returns: string } unaccent: { Args: { '': string }; Returns: string }
unaccent_immutable: { Args: { '': string }; Returns: string } unaccent_immutable: { Args: { '': string }; Returns: string }
} }
Enums: { Enums: {
estado_asignatura: 'borrador' | 'revisada' | 'aprobada' | 'generando' estado_asignatura: 'borrador' | 'revisada' | 'aprobada' | 'generando'
estado_conversacion: 'ACTIVA' | 'ARCHIVANDO' | 'ARCHIVADA' | 'ERROR' estado_conversacion: 'ACTIVA' | 'ARCHIVANDO' | 'ARCHIVADA' | 'ERROR'
estado_mensaje_ia: 'PROCESANDO' | 'COMPLETADO' | 'ERROR'
estado_tarea_revision: 'PENDIENTE' | 'COMPLETADA' | 'OMITIDA' estado_tarea_revision: 'PENDIENTE' | 'COMPLETADA' | 'OMITIDA'
fuente_cambio: 'HUMANO' | 'IA' fuente_cambio: 'HUMANO' | 'IA'
nivel_plan_estudio: nivel_plan_estudio:
@@ -1558,7 +1386,6 @@ export const Constants = {
Enums: { Enums: {
estado_asignatura: ['borrador', 'revisada', 'aprobada', 'generando'], estado_asignatura: ['borrador', 'revisada', 'aprobada', 'generando'],
estado_conversacion: ['ACTIVA', 'ARCHIVANDO', 'ARCHIVADA', 'ERROR'], estado_conversacion: ['ACTIVA', 'ARCHIVANDO', 'ARCHIVADA', 'ERROR'],
estado_mensaje_ia: ['PROCESANDO', 'COMPLETADO', 'ERROR'],
estado_tarea_revision: ['PENDIENTE', 'COMPLETADA', 'OMITIDA'], estado_tarea_revision: ['PENDIENTE', 'COMPLETADA', 'OMITIDA'],
fuente_cambio: ['HUMANO', 'IA'], fuente_cambio: ['HUMANO', 'IA'],
nivel_plan_estudio: [ nivel_plan_estudio: [

View File

@@ -1,14 +0,0 @@
{
"navigationFallback": {
"rewrite": "/index.html",
"exclude": [
"/assets/*",
"/*.css",
"/*.js",
"/*.ico",
"/*.png",
"/*.jpg",
"/*.svg"
]
}
}

View File

@@ -2,7 +2,6 @@
"include": [ "include": [
"**/*.ts", "**/*.ts",
"**/*.tsx", "**/*.tsx",
"**/*.d.ts",
"eslint.config.js", "eslint.config.js",
"prettier.config.js", "prettier.config.js",
"vite.config.ts" "vite.config.ts"