diff --git a/.vscode/settings.json b/.vscode/settings.json index 0abbfd2..97b87c8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,7 @@ { - "typescript.preferences.autoImportFileExcludePatterns": ["@ionic/angular/common", "@ionic/angular/standalone"] -} + "typescript.preferences.autoImportFileExcludePatterns": [ + "@ionic/angular/common", + "@ionic/angular/standalone" + ], + "java.configuration.updateBuildConfiguration": "disabled" +} \ No newline at end of file diff --git a/android/.kotlin/sessions/kotlin-compiler-3996273560393522172.salive b/android/.kotlin/sessions/kotlin-compiler-3996273560393522172.salive deleted file mode 100644 index e69de29..0000000 diff --git a/package-lock.json b/package-lock.json index b6839df..b1526cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -601,6 +601,7 @@ "integrity": "sha512-CVskZnF38IIxVVlKWi1VCz7YH/gHMJu2IY9bD1AVoBBGIe0xA4FRXJkW2Y+EDs9vQqZTkZZljhK5gL65Ro1PeQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@angular-eslint/bundled-angular-compiler": "20.7.0", "eslint-scope": "^9.0.0" @@ -630,6 +631,7 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.18.tgz", "integrity": "sha512-XFxgSyjfs0SRD2vQVFJljmM4z9nTvUoI8TRqSre/+l8D2FgzD5pG67Aj2BgDgpSFAUkIcI37G48ijK7a3ZZ3WA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -779,6 +781,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.18.tgz", "integrity": "sha512-M62oQbSTRmnGavIVCwimoadg/PDWadgNhactMm9fgH0eM9rx+iWBAYJk4VufO0bwOhysFpRZpJgXlFjOifz/Jw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -795,6 +798,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.18.tgz", "integrity": "sha512-AaP/LCiDNcYmF135EEozjyR04NRBT38ZfBHQwjhgwiBBTejmvcpHwJaHSkraLpZqZzE4BQqqmgiQ1EJqxEwLVA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -808,6 +812,7 @@ "integrity": "sha512-zsoEgLgnblmRbi47YwMghKirJ8IBKJ3+I8TxLBRIBrhx+KHFp+6oeDeLyu9H+djdyk88zexVd09wzR/YK73F0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "7.28.3", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -840,6 +845,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.18.tgz", "integrity": "sha512-B+NQQngd/aDbcfW0zGLis3wTLDeHTeTYMl/mGKQH+HwdPaRCKI1wEtaXaOYVJXkP2FeThocPevB8gLwNlPQUUw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -865,6 +871,7 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.18.tgz", "integrity": "sha512-x6/99LfxolyZIFUL3Wr0OrtuXHEDwEz/rwx+WzE7NL+n35yO40t3kp0Sn5uMFwI94i91QZJmXHltMpZhrVLuYg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -893,6 +900,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.18.tgz", "integrity": "sha512-q6s5rEN1yYazpHYp+k4pboXRzMsRB9auzTRBEhyXSGYxqzrnn3qHN0DqgsLC9WAdyhCgnIEMFA8kRT+W277DqQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -933,6 +941,7 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.18.tgz", "integrity": "sha512-3CWejsEYr+ze+ktvWN/qHdyq5WLrj96QZpGYJyxh1pchIcpMPE9MmLpdjf0CUrWYB7g/85u0Geq/xsz72JrGng==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -977,6 +986,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2826,6 +2836,7 @@ "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.2.0.tgz", "integrity": "sha512-oKaoNeNtH2iIZMDFVrb1atoyRECDGHcfLMunJ5KWN8DtvpVBeeA4c41e20NTuhMxw1cSYbpq2PV2hb+/9CJxlQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -3957,6 +3968,7 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -7079,6 +7091,7 @@ "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -7219,6 +7232,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -7326,6 +7340,7 @@ "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7398,6 +7413,7 @@ "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", @@ -7682,6 +7698,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7757,6 +7774,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8455,6 +8473,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -10114,6 +10133,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10606,6 +10626,7 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -11394,6 +11415,7 @@ "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -12476,7 +12498,8 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz", "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/jasmine-spec-reporter": { "version": "5.0.2", @@ -12525,6 +12548,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -12665,6 +12689,7 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -13242,6 +13267,7 @@ "integrity": "sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -13386,6 +13412,7 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -15283,6 +15310,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -16115,6 +16143,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -16208,6 +16237,7 @@ "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -17350,6 +17380,7 @@ "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.14.0", @@ -17551,7 +17582,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tuf-js": { "version": "4.1.0", @@ -17687,6 +17719,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17952,6 +17985,7 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -18086,6 +18120,7 @@ "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -18188,6 +18223,7 @@ "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -19170,6 +19206,7 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -19188,7 +19225,8 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT" + "license": "MIT", + "peer": true } } } diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index 0dc53dd..b9b77b1 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -206,9 +206,14 @@
Plan de Estudios / Recorrido - - - +
+ + + + + + +

Selecciona tu carrera para guardar tu avance y ver el recorrido sugerido, toca materias para marcarlas como aprobadas o en curso.

@@ -238,29 +243,84 @@
- {{ row.c1.name }} + {{ abreviarMateria(row.c1.name) }} + + {{ getSiuBadgeText(siuMat) }} +
- {{ row.c2.name }} + {{ abreviarMateria(row.c2.name) }} + + {{ getSiuBadgeText(siuMat) }} +
- {{ row.c3.name }} + {{ abreviarMateria(row.c3.name) }} + + {{ getSiuBadgeText(siuMat) }} +
+ + + + + + + Conectar con SIU + + Cerrar + + + + + + + diff --git a/src/app/home/home.page.scss b/src/app/home/home.page.scss index c0c7ba3..1b47669 100644 --- a/src/app/home/home.page.scss +++ b/src/app/home/home.page.scss @@ -1,517 +1,719 @@ -.filter-section { - background: var(--ion-background-color); - padding: 5px 0; - - ion-item { - --background: transparent; - } -} - -.loading-container { - display: flex; - flex-direction: column; - align-items: center; - margin-top: 50px; - color: var(--ion-color-medium); -} - -.empty-state { - text-align: center; - padding: 30px 20px; - color: var(--ion-color-medium); -} - -.error-state { - text-align: center; - padding: 30px 20px; - color: var(--ion-color-danger); - font-weight: bold; -} - -// Header styles -.header-content { - display: flex; - align-items: center; - padding: 0 10px; - - .header-logo { - height: 36px; - width: auto; - background-color: white; - padding: 4px; - border-radius: 5px; - position: absolute; - left: 10px; - z-index: 10; - } -} - -// Footer replica styles -.footer-toolbar { - --background: var(--ion-background-color); - --border-width: 0; - padding: 8px 14px; - padding-bottom: calc(8px + var(--ion-safe-area-bottom, 0px)); -} - -.modern-footer-btn { - display: flex; - align-items: center; - background: linear-gradient(135deg, var(--ion-color-primary), var(--ion-color-secondary)); - border-radius: 16px; - padding: 12px 16px; - color: white; - box-shadow: 0 4px 15px rgba(var(--ion-color-primary-rgb), 0.3); - transition: transform 0.2s, box-shadow 0.2s; - cursor: pointer; - - &:active { - transform: scale(0.97); - box-shadow: 0 2px 8px rgba(var(--ion-color-primary-rgb), 0.2); - } - - .location-icon { - font-size: 24px; - margin-right: 12px; - background: rgba(255, 255, 255, 0.2); - padding: 8px; - border-radius: 12px; - flex-shrink: 0; - } - - .footer-text-column { - flex: 1; - display: flex; - flex-direction: column; - - .footer-title { - font-size: 14px; - font-weight: 700; - letter-spacing: 0.5px; - margin-bottom: 2px; - } - - .footer-address { - font-size: 12px; - opacity: 0.9; - } - } - - .nav-icon { - font-size: 28px; - opacity: 0.95; - margin-left: 10px; - flex-shrink: 0; - } -} - -.view-segment { - background: var(--ion-background-color); - padding: 10px; -} - -.card-header-flex { - display: flex; - justify-content: space-between; - align-items: flex-start; - - .header-titles { - flex: 1; - margin-right: 15px; - } - - .fav-icon { - font-size: 28px; - cursor: pointer; - flex-shrink: 0; - } -} - -.aula-container { - margin: 15px 0; - - .aula-tag { - color: #682cad; - font-size: 1.3em; - font-weight: bold; - display: inline-block; - } -} - -ion-card { - margin: 8px 10px; - - ion-card-header { - padding: 10px 14px 0; - - ion-card-title { - font-size: 1.15rem; - } - } - - ion-card-content { - padding: 10px 14px 14px; - - p { - margin-top: 0; - margin-bottom: 4px; - font-size: 0.9em; - } - - .aula-container { - margin: 8px 0; - - .aula-tag { - font-size: 1.2em; - } - } - } -} - -ion-card.subject-card { - margin: 8px 12px; - position: relative; - overflow: hidden; - transition: transform 0.1s; - - &:active { - transform: scale(0.98); - } -} - -.card-actions { - display: flex; - align-items: center; - gap: 12px; - - .share-icon, - .menu-icon { - font-size: 1.6rem; - cursor: pointer; - transition: transform 0.2s; - - &:active { - transform: scale(0.9); - } - } -} - - -.link-container { - margin-top: 10px; - - ion-button { - --border-radius: 8px; - font-weight: 600; - letter-spacing: 0.3px; - } -} - -.schedule-text { - color: #7B2049; - /* Secondary Uva color */ - font-size: 0.98rem !important; - font-weight: bold !important; - margin-top: 8px !important; - margin-bottom: 8px !important; -} - -.highlight-card { - border: 2px solid var(--ion-color-secondary) !important; - animation: pulse-border 1s alternate infinite; -} - -@keyframes pulse-border { - from { - box-shadow: 0 0 5px var(--ion-color-secondary); - } - to { - box-shadow: 0 0 20px var(--ion-color-secondary); - } -} - -/* Dark mode overwrites for custom colored text */ -@media (prefers-color-scheme: dark) { - .aula-container .aula-tag { - color: #ae7fff; /* Lighter vivid purple for dark background */ - } - - .schedule-text { - color: #ff8ab6 !important; /* Lighter magenta for dark background */ - } - - .highlight-card { - border-color: var(--ion-color-primary) !important; - animation: pulse-border-dark 1s alternate infinite; - } - - @keyframes pulse-border-dark { - from { - box-shadow: 0 0 5px var(--ion-color-primary); - } - to { - box-shadow: 0 0 20px var(--ion-color-primary); - } - } - - .proxima-clase-card { - background: linear-gradient(135deg, #1e2a4a, #1a1f3c) !important; - border-color: rgba(var(--ion-color-primary-rgb), 0.6) !important; - - - - .proxima-label { color: rgba(255, 255, 255, 0.6); } - .proxima-asignatura { color: #fff; } - .proxima-detalles { color: rgba(255, 255, 255, 0.75); } - } - - .evento-badge { - --background: rgba(var(--ion-color-secondary-rgb), 0.15); - color: var(--ion-color-secondary-contrast); - } -} - -// ─── Hitos Académicos (Agenda) ────────────────────────────────────── - -.evento-badge-container { - margin-top: 5px; - padding: 0 0 10px; -} - -.evento-badge { - font-size: 0.8rem; - padding: 6px 10px; - border-radius: 6px; - display: inline-flex; - align-items: center; - gap: 6px; - font-weight: 600; - letter-spacing: 0.3px; - box-shadow: 0 2px 8px rgba(0,0,0,0.05); - - ion-icon { - font-size: 0.95rem; - } -} - - -// ─── Banner: Próxima Clase ─────────────────────────────────────────── - -.proxima-clase-container { - padding: 8px 12px 0; -} - -.proxima-clase-card { - margin: 0; - border-radius: 14px; - border: 1.5px solid rgba(var(--ion-color-primary-rgb), 0.35); - background: linear-gradient(135deg, #eef2ff, #f5f0ff); - box-shadow: 0 4px 18px rgba(var(--ion-color-primary-rgb), 0.15); - transition: border-color 0.3s, box-shadow 0.3s; - - ion-card-content { - padding: 14px 16px; - } - - &.urgente { - border-color: rgba(var(--ion-color-danger-rgb), 0.5); - background: linear-gradient(135deg, #fff0f3, #ffe8ed); - box-shadow: 0 4px 18px rgba(var(--ion-color-danger-rgb), 0.18); - animation: pulse-urgente 1.4s ease-in-out infinite; - } -} - -@keyframes pulse-urgente { - 0%, 100% { box-shadow: 0 4px 18px rgba(var(--ion-color-danger-rgb), 0.15); } - 50% { box-shadow: 0 4px 28px rgba(var(--ion-color-danger-rgb), 0.35); } -} - -.proxima-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - - .proxima-label { - display: flex; - align-items: center; - gap: 5px; - font-size: 0.78rem; - font-weight: 600; - letter-spacing: 0.5px; - text-transform: uppercase; - color: var(--ion-color-medium); - - ion-icon { font-size: 1rem; } - } -} - -.countdown-chip { - font-weight: 700; - font-size: 0.82rem; - height: 28px; - --padding-start: 10px; - --padding-end: 10px; - - ion-icon { margin-right: 4px; } -} - -@keyframes pulse-chip { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.75; transform: scale(1.05); } -} - -.pulse-chip { - animation: pulse-chip 1s ease-in-out infinite; -} - -.proxima-asignatura { - font-size: 1.1rem; - font-weight: 700; - color: var(--ion-color-dark); - margin: 0 0 10px; - line-height: 1.3; -} - -.proxima-detalles { - display: flex; - gap: 16px; - flex-wrap: wrap; - - span { - display: flex; - align-items: center; - gap: 4px; - font-size: 0.88rem; - color: var(--ion-color-medium-shade); - font-weight: 500; - - ion-icon { font-size: 1rem; flex-shrink: 0; } - } -} - -// ─── Recorrido Académico (Plan de Estudios) ─────────────────────────── - -/* --- Vista: Mi Carrera (Plan de Estudios) --- */ - -.content-plan { - background: var(--ion-color-light); -} - -.carrera-selector-card { - margin: 10px 0 20px 0; - border-radius: 12px; - box-shadow: 0 4px 12px rgba(0,0,0,0.05); - - ion-card-content { - padding: 16px; - } - - .selector-title { - display: block; - font-size: 1.15rem; - font-weight: 700; - color: var(--ion-color-dark); - margin-bottom: 6px; - } - - .selector-desc { - font-size: 0.88rem; - color: var(--ion-color-medium); - margin-bottom: 16px; - line-height: 1.4; - } - - .carrera-item { - --background: var(--ion-color-light); - border-radius: 8px; - --padding-start: 10px; - --inner-padding-end: 10px; - - ion-select { - font-weight: 600; - color: var(--ion-color-primary); - width: 100%; - } - } -} - -.fade-in { - animation: fadeIn 0.3s ease-in; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(5px); } - to { opacity: 1; transform: translateY(0); } -} - -.recorrido-table { - width: 100%; - border-collapse: collapse; - font-size: 0.75rem; - - th { - background: var(--ion-color-primary); - color: white; - padding: 8px 4px; - text-align: center; - font-weight: 600; - border-radius: 4px 4px 0 0; - } - - td { - padding: 4px; - vertical-align: top; - width: 33.33%; - } - - .materia-cell { - padding: 8px 4px; - height: 100%; - min-height: 60px; - background: var(--ion-color-light); - border-radius: 6px; - text-align: center; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); - cursor: pointer; - transition: all 0.2s ease; - user-select: none; - display: flex; - align-items: center; - justify-content: center; - border: 1.5px solid transparent; - - &:active { - transform: scale(0.95); - } - - &.aprobada { - background: #682cad; - color: white; - font-weight: 600; - box-shadow: 0 2px 6px rgba(104, 44, 173, 0.3); - border-color: rgba(0,0,0,0.1); - } - - &.encurso { - background: rgba(var(--ion-color-secondary-rgb), 0.15); // Subtono Cyan/Celeste - color: var(--ion-color-secondary); - font-weight: 600; - border: 1.5px solid var(--ion-color-secondary); - } - - &.optativa-cell:not(.aprobada):not(.encurso) { - border: 1.5px dashed var(--ion-color-medium); - color: var(--ion-color-primary); - background: transparent; - font-weight: 600; - } - } -} - -.instruccion-marcar { - text-align: center; - color: var(--ion-color-medium); - margin-top: 12px; - font-style: italic; -} \ No newline at end of file +.filter-section { + background: var(--ion-background-color); + padding: 5px 0; + + ion-item { + --background: transparent; + } +} + +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 50px; + color: var(--ion-color-medium); +} + +.empty-state { + text-align: center; + padding: 30px 20px; + color: var(--ion-color-medium); +} + +.error-state { + text-align: center; + padding: 30px 20px; + color: var(--ion-color-danger); + font-weight: bold; +} + +// Header styles +.header-content { + display: flex; + align-items: center; + padding: 0 10px; + + .header-logo { + height: 36px; + width: auto; + background-color: white; + padding: 4px; + border-radius: 5px; + position: absolute; + left: 10px; + z-index: 10; + } +} + +// Footer replica styles +.footer-toolbar { + --background: var(--ion-background-color); + --border-width: 0; + padding: 8px 14px; + padding-bottom: calc(8px + var(--ion-safe-area-bottom, 0px)); +} + +.modern-footer-btn { + display: flex; + align-items: center; + background: linear-gradient(135deg, var(--ion-color-primary), var(--ion-color-secondary)); + border-radius: 16px; + padding: 12px 16px; + color: white; + box-shadow: 0 4px 15px rgba(var(--ion-color-primary-rgb), 0.3); + transition: transform 0.2s, box-shadow 0.2s; + cursor: pointer; + + &:active { + transform: scale(0.97); + box-shadow: 0 2px 8px rgba(var(--ion-color-primary-rgb), 0.2); + } + + .location-icon { + font-size: 24px; + margin-right: 12px; + background: rgba(255, 255, 255, 0.2); + padding: 8px; + border-radius: 12px; + flex-shrink: 0; + } + + .footer-text-column { + flex: 1; + display: flex; + flex-direction: column; + + .footer-title { + font-size: 14px; + font-weight: 700; + letter-spacing: 0.5px; + margin-bottom: 2px; + } + + .footer-address { + font-size: 12px; + opacity: 0.9; + } + } + + .nav-icon { + font-size: 28px; + opacity: 0.95; + margin-left: 10px; + flex-shrink: 0; + } +} + +.view-segment { + background: var(--ion-background-color); + padding: 10px; +} + +.card-header-flex { + display: flex; + justify-content: space-between; + align-items: flex-start; + + .header-titles { + flex: 1; + margin-right: 15px; + } + + .fav-icon { + font-size: 28px; + cursor: pointer; + flex-shrink: 0; + } +} + +.aula-container { + margin: 15px 0; + + .aula-tag { + color: #682cad; + font-size: 1.3em; + font-weight: bold; + display: inline-block; + } +} + +ion-card { + margin: 8px 10px; + + ion-card-header { + padding: 10px 14px 0; + + ion-card-title { + font-size: 1.15rem; + } + } + + ion-card-content { + padding: 10px 14px 14px; + + p { + margin-top: 0; + margin-bottom: 4px; + font-size: 0.9em; + } + + .aula-container { + margin: 8px 0; + + .aula-tag { + font-size: 1.2em; + } + } + } +} + +ion-card.subject-card { + margin: 8px 12px; + position: relative; + overflow: hidden; + transition: transform 0.1s; + + &:active { + transform: scale(0.98); + } +} + +.card-actions { + display: flex; + align-items: center; + gap: 12px; + + .share-icon, + .menu-icon { + font-size: 1.6rem; + cursor: pointer; + transition: transform 0.2s; + + &:active { + transform: scale(0.9); + } + } +} + + +.link-container { + margin-top: 10px; + + ion-button { + --border-radius: 8px; + font-weight: 600; + letter-spacing: 0.3px; + } +} + +.schedule-text { + color: #7B2049; + /* Secondary Uva color */ + font-size: 0.98rem !important; + font-weight: bold !important; + margin-top: 8px !important; + margin-bottom: 8px !important; +} + +.highlight-card { + border: 2px solid var(--ion-color-secondary) !important; + animation: pulse-border 1s alternate infinite; +} + +@keyframes pulse-border { + from { + box-shadow: 0 0 5px var(--ion-color-secondary); + } + + to { + box-shadow: 0 0 20px var(--ion-color-secondary); + } +} + +/* Dark mode overwrites for custom colored text */ +@media (prefers-color-scheme: dark) { + .aula-container .aula-tag { + color: #ae7fff; + /* Lighter vivid purple for dark background */ + } + + .schedule-text { + color: #ff8ab6 !important; + /* Lighter magenta for dark background */ + } + + .highlight-card { + border-color: var(--ion-color-primary) !important; + animation: pulse-border-dark 1s alternate infinite; + } + + @keyframes pulse-border-dark { + from { + box-shadow: 0 0 5px var(--ion-color-primary); + } + + to { + box-shadow: 0 0 20px var(--ion-color-primary); + } + } + + .proxima-clase-card { + background: linear-gradient(135deg, #1e2a4a, #1a1f3c) !important; + border-color: rgba(var(--ion-color-primary-rgb), 0.6) !important; + + + + .proxima-label { + color: rgba(255, 255, 255, 0.6); + } + + .proxima-asignatura { + color: #fff; + } + + .proxima-detalles { + color: rgba(255, 255, 255, 0.75); + } + } + + .evento-badge { + --background: rgba(var(--ion-color-secondary-rgb), 0.15); + color: var(--ion-color-secondary-contrast); + } +} + +// ─── Hitos Académicos (Agenda) ────────────────────────────────────── + +.evento-badge-container { + margin-top: 5px; + padding: 0 0 10px; +} + +.evento-badge { + font-size: 0.8rem; + padding: 6px 10px; + border-radius: 6px; + display: inline-flex; + align-items: center; + gap: 6px; + font-weight: 600; + letter-spacing: 0.3px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + + ion-icon { + font-size: 0.95rem; + } +} + + +// ─── Banner: Próxima Clase ─────────────────────────────────────────── + +.proxima-clase-container { + padding: 8px 12px 0; +} + +.proxima-clase-card { + margin: 0; + border-radius: 14px; + border: 1.5px solid rgba(var(--ion-color-primary-rgb), 0.35); + background: linear-gradient(135deg, #eef2ff, #f5f0ff); + box-shadow: 0 4px 18px rgba(var(--ion-color-primary-rgb), 0.15); + transition: border-color 0.3s, box-shadow 0.3s; + + ion-card-content { + padding: 14px 16px; + } + + &.urgente { + border-color: rgba(var(--ion-color-danger-rgb), 0.5); + background: linear-gradient(135deg, #fff0f3, #ffe8ed); + box-shadow: 0 4px 18px rgba(var(--ion-color-danger-rgb), 0.18); + animation: pulse-urgente 1.4s ease-in-out infinite; + } +} + +@keyframes pulse-urgente { + + 0%, + 100% { + box-shadow: 0 4px 18px rgba(var(--ion-color-danger-rgb), 0.15); + } + + 50% { + box-shadow: 0 4px 28px rgba(var(--ion-color-danger-rgb), 0.35); + } +} + +.proxima-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + + .proxima-label { + display: flex; + align-items: center; + gap: 5px; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + color: var(--ion-color-medium); + + ion-icon { + font-size: 1rem; + } + } +} + +.countdown-chip { + font-weight: 700; + font-size: 0.82rem; + height: 28px; + --padding-start: 10px; + --padding-end: 10px; + + ion-icon { + margin-right: 4px; + } +} + +@keyframes pulse-chip { + + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: 0.75; + transform: scale(1.05); + } +} + +.pulse-chip { + animation: pulse-chip 1s ease-in-out infinite; +} + +.proxima-asignatura { + font-size: 1.1rem; + font-weight: 700; + color: var(--ion-color-dark); + margin: 0 0 10px; + line-height: 1.3; +} + +.proxima-detalles { + display: flex; + gap: 16px; + flex-wrap: wrap; + + span { + display: flex; + align-items: center; + gap: 4px; + font-size: 0.88rem; + color: var(--ion-color-medium-shade); + font-weight: 500; + + ion-icon { + font-size: 1rem; + flex-shrink: 0; + } + } +} + +// ─── Recorrido Académico (Plan de Estudios) ─────────────────────────── + +/* --- Vista: Mi Carrera (Plan de Estudios) --- */ + +.content-plan { + background: var(--ion-color-light); +} + +.carrera-selector-card { + margin: 10px 0 20px 0; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + + ion-card-content { + padding: 16px; + } + + .selector-title { + display: block; + font-size: 1.15rem; + font-weight: 700; + color: var(--ion-color-dark); + margin-bottom: 6px; + } + + .selector-desc { + font-size: 0.88rem; + color: var(--ion-color-medium); + margin-bottom: 16px; + line-height: 1.4; + } + + .carrera-item { + --background: var(--ion-color-light); + border-radius: 8px; + --padding-start: 10px; + --inner-padding-end: 10px; + + ion-select { + font-weight: 600; + color: var(--ion-color-primary); + width: 100%; + } + } +} + +.fade-in { + animation: fadeIn 0.3s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(5px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.recorrido-table { + width: 100%; + border-collapse: collapse; + font-size: 0.70rem; + /* Fuente más chica para nombres largos */ + + th { + background: var(--ion-color-primary); + color: white; + padding: 8px 4px; + text-align: center; + font-weight: 600; + border-radius: 4px 4px 0 0; + } + + td { + padding: 4px; + vertical-align: top; + width: 33.33%; + } + + .materia-cell { + padding: 6px 4px; + width: 100%; + aspect-ratio: 1 / 0.65; + /* Reducimos 15% el alto respecto al ancho */ + background: var(--ion-color-light); + border-radius: 6px; + text-align: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: all 0.2s ease; + user-select: none; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border: 1.5px solid transparent; + + .m-name { + display: flex; + flex: 1; + align-items: center; + } + + .siu-cell-badge { + margin-top: 4px; + font-size: 0.75rem; + padding: 3px 8px; + border-radius: 4px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25); + + &.custom-badge-success { + --background: rgba(255, 255, 255, 0.95); + color: #a31c62; + /* Rosa oscuro legible sobre blanco */ + font-weight: 800; + letter-spacing: 0.5px; + } + + &.custom-badge-warning { + --background: rgba(255, 255, 255, 0.95); + color: #0b5f7e; + /* Azul/Cyan oscuro legible sobre blanco */ + font-weight: 800; + letter-spacing: 0.5px; + } + + &.custom-badge-danger { + --background: rgba(var(--ion-color-danger-rgb), 0.15); + color: var(--ion-color-danger); + } + } + + &:active { + transform: scale(0.95); + } + + &.aprobada { + background: #682cad; + color: white; + font-weight: 600; + box-shadow: 0 2px 6px rgba(104, 44, 173, 0.3); + border-color: rgba(0, 0, 0, 0.1); + } + + &.encurso { + background: rgba(var(--ion-color-secondary-rgb), 0.15); // Subtono Cyan/Celeste + color: var(--ion-color-secondary); + font-weight: 600; + border: 1.5px solid var(--ion-color-secondary); + } + + &.optativa-cell:not(.aprobada):not(.encurso) { + border: 1.5px dashed var(--ion-color-medium); + color: var(--ion-color-primary); + background: transparent; + font-weight: 600; + } + } +} + +.instruccion-marcar { + text-align: center; + color: var(--ion-color-medium); + margin-top: 12px; + font-style: italic; +} + +// ─── SIU Guaraní ─────────────────────────────────────────── + +.siu-container { + padding: 10px 0; + + .siu-header { + padding: 0 10px; + } + + .ultima-sync { + padding: 0 10px; + } +} + +.siu-card { + margin: 10px 0; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); + + ion-card-header { + padding: 12px 16px 4px; + + ion-card-title { + color: var(--ion-color-dark); + font-weight: 700; + } + } + + .siu-card-content { + padding: 10px 16px 16px; + + ion-badge { + font-size: 0.9rem; + padding: 6px 10px; + border-radius: 6px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + } +} + +.siu-login-modal { + --border-radius: 16px 16px 0 0; + + .siu-logo-container { + padding-top: 20px; + + h2 { + font-weight: 800; + color: var(--ion-color-primary); + } + } + + .siu-form-list { + background: transparent; + + ion-item { + --background: rgba(var(--ion-color-light-rgb), 0.5); + --border-radius: 8px; + margin-bottom: 10px; + --padding-start: 12px; + + ion-input { + margin-left: 10px; + font-weight: 500; + } + } + } + + ion-button[expand="block"] { + margin-top: 20px; + height: 48px; + --border-radius: 12px; + font-weight: bold; + font-size: 1.05rem; + letter-spacing: 0.5px; + } +} + +@media (prefers-color-scheme: dark) { + .siu-card { + background: var(--ion-color-step-100); + } + + .siu-login-modal { + .siu-form-list ion-item { + --background: var(--ion-color-step-100); + } + + .siu-info { + background: var(--ion-color-step-100) !important; + } + } + + /* Mejorar el contraste de los botones de Optativa en modo oscuro */ + .recorrido-table .materia-cell.optativa-cell:not(.aprobada):not(.encurso) { + border-color: rgba(255, 255, 255, 0.45); + color: #ff8ab6; + /* Lighter magenta for dark background */ + } + + /* Mejorar íconos y bordes de encabezado (botones SIU/Guardar) */ + ion-button.header-btn { + --border-width: 1.5px; + } + + ion-button.header-btn[color="primary"] { + --color: #ff8ab6 !important; + --border-color: #ff8ab6 !important; + } + + ion-button.header-btn[color="tertiary"] { + --color: #ae7fff !important; + --border-color: #ae7fff !important; + } + + ion-button.header-btn[color="success"] { + --color: #68f4a1 !important; + --border-color: #68f4a1 !important; + } +} \ No newline at end of file diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index 585151e..3a85e36 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -13,6 +13,7 @@ import { LocalStorageService } from '../services/local-storage.service'; import { EventosService } from '../services/eventos.service'; import { EventoModalComponent } from '../components/evento-modal/evento-modal.component'; import { AgendaModalComponent } from '../components/agenda-modal/agenda-modal.component'; +import { SiuService } from '../services/siu-guarani.service'; export interface ProximaClaseInfo { @@ -66,6 +67,14 @@ export class HomePage implements OnInit { carreraSeleccionada: string = 'tecnologias'; + // SIU Guaraní State + siuMaterias: any[] = []; + siuLastSync: string | null = null; + isSiuModalOpen: boolean = false; + siuLoading: boolean = false; + siuDni: string = ''; + siuPass: string = ''; + // Mapa de días en español al getDay() de JS (0=Dom) private readonly DIAS_DOW: Record = { @@ -81,7 +90,8 @@ export class HomePage implements OnInit { private alertCtrl: AlertController, private localStorageService: LocalStorageService, private actionSheetCtrl: ActionSheetController, - private eventosService: EventosService + private eventosService: EventosService, + private siuService: SiuService ) { } @@ -118,6 +128,14 @@ export class HomePage implements OnInit { this.aprobadas = userData.aprobadas || []; this.encurso = userData.encurso || []; this.optativasSeleccionadas = userData.optativas || []; + this.siuMaterias = userData.siuMaterias || []; + this.siuLastSync = userData.siuLastSync || null; + + // Auto-completar desde SIU si hay datos previamente cacheados + if (this.siuMaterias.length > 0) { + this.autoCompletarDesdeSiu(); + } + // Sincronizar eventos cargados desde el CSV con el servicio de eventos for (const ev of userData.eventos) { await this.eventosService.saveEvento(ev); @@ -219,7 +237,9 @@ export class HomePage implements OnInit { this.aprobadas = imported.aprobadas || []; this.encurso = imported.encurso || []; this.optativasSeleccionadas = imported.optativas || []; - + this.siuMaterias = imported.siuMaterias || []; + this.siuLastSync = imported.siuLastSync || null; + // Sincronizar eventos cargados desde el CSV con el servicio de eventos for (const ev of imported.eventos) { await this.eventosService.saveEvento(ev); @@ -274,12 +294,12 @@ export class HomePage implements OnInit { const ahora = new Date(); const hoyIdx = ahora.getDay(); // 0-6 const horaActualStr = ahora.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }); - + let candidata: ProximaClaseInfo | null = null; // 1. Buscar si hay alguna clase en curso AHORA const favsHoy = this.horarios.filter(h => this.isFavorito(h.codigo) && this.DIAS_DOW[h.dia || ''] === hoyIdx); - + for (const item of favsHoy) { const parts = (item.horario || '').split('-'); if (parts.length >= 2) { @@ -304,7 +324,7 @@ export class HomePage implements OnInit { if (tInicio > horaActualStr) { const targetDate = new Date(ahora); targetDate.setHours(parseInt(mInicio[1], 10), parseInt(mInicio[2], 10), 0, 0); - + const diff = targetDate.getTime() - ahora.getTime(); if (!candidata || diff < candidata.diffMs) { candidata = { item, diffMs: diff, label: 'Próxima Clase' }; @@ -317,14 +337,14 @@ export class HomePage implements OnInit { if (!candidata) { const mananaIdx = (hoyIdx + 1) % 7; const favsManana = this.horarios.filter(h => this.isFavorito(h.codigo) && this.DIAS_DOW[h.dia || ''] === mananaIdx); - + for (const item of favsManana) { const mInicio = (item.horario || '').match(/(\d{1,2}):(\d{2})/); if (mInicio) { const targetDate = new Date(ahora); targetDate.setDate(ahora.getDate() + 1); targetDate.setHours(parseInt(mInicio[1], 10), parseInt(mInicio[2], 10), 0, 0); - + const diff = targetDate.getTime() - ahora.getTime(); if (!candidata || diff < candidata.diffMs) { candidata = { item, diffMs: diff, label: 'Próxima Clase' }; @@ -527,7 +547,7 @@ export class HomePage implements OnInit { openMap() { const address = 'Tte. Gral. Juan Domingo Perón 802, C1063, Cdad. Autónoma de Buenos Aires'; const encodedAddress = encodeURIComponent(address); - + // Si estamos en un dispositivo móvil, preferimos los esquemas nativos para invocar al OS const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); const isAndroid = /android/i.test(navigator.userAgent); @@ -632,7 +652,7 @@ export class HomePage implements OnInit { const ahora = new Date().getTime(); const futuros = this.getEventosMateria(codigo) .filter(e => new Date(e.fecha).getTime() > ahora); - + return futuros.length > 0 ? futuros[0] : null; } @@ -667,13 +687,13 @@ export class HomePage implements OnInit { await modal.present(); await modal.onWillDismiss(); - + // Al cerrar la agenda, recargamos los eventos locales por si se agregó/editó alguno this.eventos = await this.eventosService.loadEventos(); - + // Y aseguramos que la próxima clase y los badges se actualicen this.actualizarProximaClase(); - + // Guardamos progreso por si hubo cambios this.guardarProgreso(); } @@ -697,14 +717,14 @@ export class HomePage implements OnInit { const selected = this.optativasSeleccionadas.find(o => o.slot === slotId); return { id: slotId, name: selected ? selected.materia : label, isOptativa: true }; }; - + const mat = (name: string) => ({ id: null, name: name, isOptativa: false }); if (this.carreraSeleccionada === 'tecnologias') { return [ { c1: mat('Gestión de Sistemas de Información'), c2: mat('Administración de Negocios Digitales'), c3: mat('Seguridad de la Información') }, { c1: mat('Análisis de Datos I'), c2: mat('Sistemas Digitales'), c3: mat('Programación de Vanguardia') }, - { c1: mat('Taller: Proyectos Digitales'), c2: mat('Taller: Emprendedurismo en Innovación Digital'), c3: mat('Taller: Control de calidad de Software') }, + { c1: mat('Taller: Gestión de Proyectos Digitales'), c2: mat('Taller: Emprendedurismo en Innovación Digital'), c3: mat('Taller: Control de Calidad de Software') }, { c1: getSlot('tec_opt1', 'Optativa 1'), c2: getSlot('tec_opt2', 'Optativa 2'), c3: getSlot('tec_opt3', 'Optativa 3') } ]; } else if (this.carreraSeleccionada === 'datos') { @@ -729,13 +749,13 @@ export class HomePage implements OnInit { const modes: ('favoritos' | 'todas' | 'plan')[] = ['favoritos', 'todas', 'plan']; const currentIndex = modes.indexOf(this.viewMode); - if (event.direction === 2) { + if (event.direction === 2) { // Swipe Izquierda (Siguiente Tab) if (currentIndex < modes.length - 1) { this.viewMode = modes[currentIndex + 1]; this.onViewModeChange(); } - } else if (event.direction === 4) { + } else if (event.direction === 4) { // Swipe Derecha (Tab Anterior) if (currentIndex > 0) { this.viewMode = modes[currentIndex - 1]; @@ -821,7 +841,7 @@ export class HomePage implements OnInit { subHeader: cell.isOptativa ? 'Gestión de Materia Optativa' : 'Gestión de Materia', buttons: buttons }); - + await actionSheet.present(); } @@ -887,7 +907,9 @@ export class HomePage implements OnInit { eventos: this.eventos, aprobadas: this.aprobadas, optativas: this.optativasSeleccionadas, - encurso: this.encurso + encurso: this.encurso, + siuMaterias: this.siuMaterias, + siuLastSync: this.siuLastSync }); } @@ -898,9 +920,11 @@ export class HomePage implements OnInit { eventos: this.eventos, aprobadas: this.aprobadas, optativas: this.optativasSeleccionadas, - encurso: this.encurso + encurso: this.encurso, + siuMaterias: this.siuMaterias, + siuLastSync: this.siuLastSync }; - + const actionSheet = await this.actionSheetCtrl.create({ header: 'Guardar Respaldo', subHeader: '¿A dónde querés exportar tus materias guardadas?', @@ -909,14 +933,14 @@ export class HomePage implements OnInit { text: 'Compartir (Drive, WhatsApp, Mail...)', icon: 'share-social-outline', handler: async () => { - await this.ejecutarExportacion(data, false); + await this.ejecutarExportacion(data, false); } }, { text: 'Descargar Offline (A la carpeta Descargas)', icon: 'download-outline', handler: async () => { - await this.ejecutarExportacion(data, true); + await this.ejecutarExportacion(data, true); } }, { @@ -939,44 +963,271 @@ export class HomePage implements OnInit { icon: 'save' }); toast.present(); - await Haptics.impact({ style: ImpactStyle.Heavy }).catch(() => {}); + await Haptics.impact({ style: ImpactStyle.Heavy }).catch(() => { }); } } async importarRespaldoManual() { const imported = await this.localStorageService.importBackup(); if (imported && (imported.favoritos.length > 0 || imported.eventos.length > 0 || imported.aprobadas?.length > 0 || imported.optativas?.length > 0 || imported.encurso?.length > 0)) { - this.favoritos = imported.favoritos; - this.aprobadas = imported.aprobadas || []; - this.encurso = imported.encurso || []; - this.optativasSeleccionadas = imported.optativas || []; - - for (const ev of imported.eventos) { - await this.eventosService.saveEvento(ev); - } - this.eventos = await this.eventosService.loadEventos(); - - await Preferences.set({ key: 'favoritos', value: JSON.stringify(this.favoritos) }); - this.guardarProgreso(); - this.filterHorarios(); - - const toast = await this.toastCtrl.create({ - message: '¡Respaldo importado correctamente!', - duration: 3000, - color: 'success', - icon: 'checkmark-circle' - }); - toast.present(); - await Haptics.impact({ style: ImpactStyle.Heavy }).catch(() => {}); + this.favoritos = imported.favoritos; + this.aprobadas = imported.aprobadas || []; + this.encurso = imported.encurso || []; + this.optativasSeleccionadas = imported.optativas || []; + this.siuMaterias = imported.siuMaterias || []; + this.siuLastSync = imported.siuLastSync || null; + + for (const ev of imported.eventos) { + await this.eventosService.saveEvento(ev); + } + this.eventos = await this.eventosService.loadEventos(); + + await Preferences.set({ key: 'favoritos', value: JSON.stringify(this.favoritos) }); + this.guardarProgreso(); + this.filterHorarios(); + + const toast = await this.toastCtrl.create({ + message: '¡Respaldo importado correctamente!', + duration: 3000, + color: 'success', + icon: 'checkmark-circle' + }); + toast.present(); + await Haptics.impact({ style: ImpactStyle.Heavy }).catch(() => { }); } else { - const toast = await this.toastCtrl.create({ - message: 'No se procesó ningún archivo válido.', - duration: 2500, - color: 'warning', - icon: 'alert-circle' - }); - toast.present(); + const toast = await this.toastCtrl.create({ + message: 'No se procesó ningún archivo válido.', + duration: 2500, + color: 'warning', + icon: 'alert-circle' + }); + toast.present(); } } + + // --- SIU Guaraní --- + + openSiuModal() { + this.isSiuModalOpen = true; + } + + closeSiuModal() { + this.isSiuModalOpen = false; + this.siuDni = ''; + this.siuPass = ''; + } + + async sincronizarSiu() { + if (!this.siuDni || !this.siuPass) { + const toast = await this.toastCtrl.create({ + message: 'Por favor, ingresá DNI y Contraseña para conectar', + duration: 2500, + color: 'warning' + }); + toast.present(); + return; + } + + this.siuLoading = true; + try { + const materias = await this.siuService.getAcademicHistory(this.siuDni, this.siuPass); + this.siuMaterias = materias; + this.siuLastSync = new Date().toISOString(); + + this.autoCompletarDesdeSiu(); + this.guardarProgreso(); + + const toast = await this.toastCtrl.create({ + message: 'Historial del SIU cargado con éxito!', + duration: 3000, + color: 'success' + }); + toast.present(); + this.closeSiuModal(); + } catch (error) { + console.error(error); + const toast = await this.toastCtrl.create({ + message: 'Error al conectar con SIU. Comprobá tus datos o intentá más tarde.', + duration: 4000, + color: 'danger' + }); + toast.present(); + } finally { + this.siuLoading = false; + } + } + + getNotaColor(nota: string): string { + if (!nota) return 'medium'; + + const n = parseInt(nota, 10); + if (!isNaN(n)) { + if (n >= 4) return 'success'; + return 'danger'; + } + + // Fallback if not numeric + const lower = nota.toLowerCase(); + if (lower.includes('aprobado') || lower.includes('promocionado') || lower.includes('equivalencia')) return 'success'; + if (lower.includes('regular') || lower.includes('en curso')) return 'warning'; + return 'medium'; + } + + // --- SIU Guaraní Mapeo y Auto-Sync --- + + getCleanSubjectName(name: string): string { + if (!name) return ''; + return name.split(' (')[0].trim(); + } + + matcheaSiu(siuName: string, appName: string): boolean { + const siuN = this.normalizeText(siuName); + const appN = this.normalizeText(this.getCleanSubjectName(appName)); + if (!siuN || !appN) return false; + return siuN === appN || appN.includes(siuN) || siuN.includes(appN); + } + + getSiuMateriaParaCelda(materiaNombre: string): any { + if (!materiaNombre || !this.siuMaterias) return null; + const siuMat = this.siuMaterias.find(m => this.matcheaSiu(m.nombre, materiaNombre)); + if (siuMat && this.determinarEstadoSiu(siuMat.nota, siuMat.origen) !== null) { + return siuMat; + } + return null; + } + + getSiuBadgeText(siuMat: any): string { + if (!siuMat) return ''; + const notaValid = siuMat.nota && siuMat.nota.trim() !== '' && siuMat.nota !== 'SIN NOTA' && siuMat.nota !== '-'; + let txt = notaValid ? siuMat.nota : ''; + + const estado = this.determinarEstadoSiu(siuMat.nota, siuMat.origen); + if (estado === 'encurso') { + if (txt) { + return txt.toLowerCase().includes('curso') ? txt : `${txt} (En Curso)`; + } + return 'En Curso'; + } + + let badgeText = txt || 'Aprobado'; + badgeText = badgeText.replace(/Promocionado/gi, 'Prom.'); + badgeText = badgeText.replace(/Aprobado/gi, 'Aprob.'); + return badgeText; + } + + abreviarMateria(nombre: string): string { + if (!nombre) return ''; + let n = nombre; + // Sustituciones específicas para encajar mejor en los zócalos + n = n.replace('Gestión de Sistemas de Información', 'Gest. Sist. de Info.'); + n = n.replace('Administración de Negocios Digitales', 'Admin. Negocios Dig.'); + n = n.replace('Seguridad de la Información', 'Seguridad Info.'); + n = n.replace('Programación de Vanguardia', 'Prog. de Vanguardia'); + n = n.replace('Taller: Emprendedurismo en Innovación Digital', 'Taller Emprendedurismo'); + n = n.replace('Taller: Gestión de Proyectos Digitales', 'Taller Proyectos'); + n = n.replace('Taller: Control de Calidad de Software', 'Taller QA Software'); + n = n.replace(/Gestión del Talento Humano en la Industria [dD]igital/, 'Gest. Talento Humano'); + n = n.replace('Gestión de Redes Informáticas', 'Redes Informáticas'); + n = n.replace('Desarrollo en Dispositivos Móviles', 'Des. Dispositivos Móviles'); + n = n.replace('Diseño y Producción de Videojuegos', 'Diseño y Prod. Videojuegos'); + return n; + } + + get siuMateriasFiltradas() { + if (!this.siuMaterias) return []; + const allCareerSubjects: string[] = []; + for (const row of this.getRecorridoData()) { + allCareerSubjects.push(row.c1.name, row.c2.name, row.c3.name); + } + return this.siuMaterias.filter(mat => { + const inCareer = allCareerSubjects.some(appName => this.matcheaSiu(mat.nombre, appName)); + const hasState = this.determinarEstadoSiu(mat.nota, mat.origen) !== null; + return inCareer && hasState; + }); + } + + determinarEstadoSiu(nota: string, origen: string): 'aprobada' | 'encurso' | null { + const n = parseInt(nota, 10); + const lowerNota = (nota || '').toLowerCase(); + const lowerOrigen = (origen || '').toLowerCase(); + + // Si la nota es numérica y es mayor o igual a 4, está aprobada + if (!isNaN(n) && n >= 4) return 'aprobada'; + if (lowerNota.includes('aprobado') || lowerNota.includes('promocionado') || lowerNota.includes('equivalencia')) return 'aprobada'; + if (lowerOrigen.includes('aprobado') || lowerOrigen.includes('promocionado')) return 'aprobada'; + + // Si no está aprobada, pero cursa regular o está anotado + if (lowerOrigen.includes('en curso') || lowerNota.includes('en curso') || lowerOrigen.includes('regular')) return 'encurso'; + + return null; + } + + autoCompletarDesdeSiu() { + let changed = false; + + // Primero, pre-mapear optativas huérfanas + const newlyMapped = this.autoAsignarOptativasSiu(); + if (newlyMapped) changed = true; + + const allCareerSubjects: string[] = []; + for (const row of this.getRecorridoData()) { + allCareerSubjects.push(row.c1.name, row.c2.name, row.c3.name); + } + + for (const appName of allCareerSubjects) { + if (appName.startsWith('Optativa') && !appName.includes('ASIG')) continue; // Optativa no seleccionada + + const siuMat = this.getSiuMateriaParaCelda(appName); + if (siuMat) { + const estadoSiu = this.determinarEstadoSiu(siuMat.nota, siuMat.origen); + if (estadoSiu === 'aprobada' && !this.aprobadas.includes(appName)) { + this.aprobadas = [...this.aprobadas, appName]; + this.encurso = this.encurso.filter(x => x !== appName); + changed = true; + } else if (estadoSiu === 'encurso' && !this.aprobadas.includes(appName) && !this.encurso.includes(appName)) { + this.encurso = [...this.encurso, appName]; + changed = true; + } + } + } + if (changed) this.guardarProgreso(); + } + + autoAsignarOptativasSiu(): boolean { + if (!this.siuMaterias) return false; + let mappedAlgo = false; + + const allCareerSubjects: string[] = []; + for (const row of this.getRecorridoData()) { + allCareerSubjects.push(row.c1.name, row.c2.name, row.c3.name); + } + + const optativasSiu = this.siuMaterias.filter(mat => { + const estado = this.determinarEstadoSiu(mat.nota, mat.origen); + if (!estado) return false; + return !allCareerSubjects.some(appName => this.matcheaSiu(mat.nombre, appName)); + }); + + if (optativasSiu.length > 0) { + let slots = ['tec_opt1', 'tec_opt2', 'tec_opt3']; + if (this.carreraSeleccionada === 'datos') slots = ['dat_opt1', 'dat_opt2', 'dat_opt3']; + else if (this.carreraSeleccionada === 'videojuegos') slots = ['vid_opt1', 'vid_opt2', 'vid_opt3']; + + for (const siuOpt of optativasSiu) { + const cleanName = this.getCleanSubjectName(siuOpt.nombre); + const alreadySelected = this.optativasSeleccionadas.find(o => this.matcheaSiu(siuOpt.nombre, o.materia)); + if (!alreadySelected) { + const freeSlot = slots.find(s => !this.optativasSeleccionadas.find(o => o.slot === s)); + if (freeSlot) { + this.optativasSeleccionadas.push({ slot: freeSlot, materia: cleanName }); + // actualizamos el map de todo para que no intente la próxima vuelta + allCareerSubjects.push(cleanName); + mappedAlgo = true; + } + } + } + } + return mappedAlgo; + } } diff --git a/src/app/services/local-storage.service.ts b/src/app/services/local-storage.service.ts index faafbd3..389d344 100644 --- a/src/app/services/local-storage.service.ts +++ b/src/app/services/local-storage.service.ts @@ -12,6 +12,8 @@ export interface AppData { aprobadas: string[]; optativas: {slot: string, materia: string}[]; encurso: string[]; + siuMaterias?: {nombre: string, nota: string, origen: string}[]; + siuLastSync?: string | null; } @Injectable({ @@ -55,7 +57,9 @@ export class LocalStorageService { eventos: parsed.eventos || [], aprobadas: parsed.aprobadas || [], optativas: parsed.optativas || [], - encurso: parsed.encurso || [] + encurso: parsed.encurso || [], + siuMaterias: parsed.siuMaterias || [], + siuLastSync: parsed.siuLastSync || null }; } return null; @@ -130,7 +134,9 @@ export class LocalStorageService { eventos: parsed.eventos || [], aprobadas: parsed.aprobadas || [], optativas: parsed.optativas || [], - encurso: parsed.encurso || [] + encurso: parsed.encurso || [], + siuMaterias: parsed.siuMaterias || [], + siuLastSync: parsed.siuLastSync || null }; await this.saveData(importedData); return importedData; @@ -208,7 +214,7 @@ export class LocalStorageService { } const importedData: AppData = { - favoritos: favs, eventos: evs, aprobadas: aprobadas, optativas: optativasMap, encurso: encurso + favoritos: favs, eventos: evs, aprobadas: aprobadas, optativas: optativasMap, encurso: encurso, siuMaterias: [], siuLastSync: null }; if (saveAfter) { diff --git a/src/app/services/siu-guarani.service.ts b/src/app/services/siu-guarani.service.ts new file mode 100644 index 0000000..2fc51ae --- /dev/null +++ b/src/app/services/siu-guarani.service.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@angular/core'; +import { CapacitorHttp, HttpResponse } from '@capacitor/core'; + +@Injectable({ providedIn: 'root' }) +export class SiuService { + private readonly BASE_URL = 'https://siuuniciu.eastus2.cloudapp.azure.com/caba'; + + async getAcademicHistory(dni: string, pass: string, carreraId: string = '30'): Promise { + try { + // 1. LOGIN POST + const loginData = `usuario=${dni}&password=${pass}&login=Ingresar`; + await CapacitorHttp.post({ + url: `${this.BASE_URL}/acceso?auth=form`, + data: loginData, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } + }); + + // 2. CAMBIO DE CARRERA (Simulando el GET que capturamos) + // Esto es lo que destrabó el cuello de botella + await CapacitorHttp.get({ + url: `${this.BASE_URL}/acceso/cambiar_carrera?id=${carreraId}&op=inicio_alumno`, + }); + + // 3. OBTENER PLAN DE ESTUDIO + const resPlan: HttpResponse = await CapacitorHttp.get({ + url: `${this.BASE_URL}/plan_estudio`, + }); + + // 4. PARSEO DEL JSON EMBEBIDO + // Buscamos el bloque on_arrival que contiene "info_plan" + const rawHtml = resPlan.data; + const regex = /on_arrival\((\{.*?"id":"info_plan".*?\})\);/s; + const match = rawHtml.match(regex); + + if (match && match[1]) { + const jsonData = JSON.parse(match[1]); + const contentHtml = jsonData.content; + return this.parseHtmlSubjects(contentHtml); + } + + throw new Error('No se encontró el bloque de materias'); + } catch (error) { + console.error('Error SIU:', error); + throw error; + } + } + + private parseHtmlSubjects(html: string): any[] { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const rows = Array.from(doc.querySelectorAll('tr')); + + return rows.map(row => { + const cols = row.querySelectorAll('td'); + if (cols.length >= 6) { + const nombreRaw = cols[0].textContent?.trim() || ''; + return { + nombre: nombreRaw.split(' (')[0], + nota: cols[4].textContent?.trim(), + origen: cols[5].textContent?.trim() + }; + } + return null; + }).filter(item => item && item.nombre); + } +} \ No newline at end of file