Showing
21 changed files
with
37 additions
and
7218 deletions
| ... | @@ -9,7 +9,6 @@ | ... | @@ -9,7 +9,6 @@ |
| 9 | "version": "0.0.1", | 9 | "version": "0.0.1", |
| 10 | "dependencies": { | 10 | "dependencies": { |
| 11 | "@observablehq/plot": "^0.6.17", | 11 | "@observablehq/plot": "^0.6.17", |
| 12 | - "@supabase/supabase-js": "^2.98.0", | ||
| 13 | "@sveltejs/adapter-node": "^5.5.4", | 12 | "@sveltejs/adapter-node": "^5.5.4", |
| 14 | "d3": "^7.9.0", | 13 | "d3": "^7.9.0", |
| 15 | "fuse.js": "^7.1.0", | 14 | "fuse.js": "^7.1.0", |
| ... | @@ -939,86 +938,6 @@ | ... | @@ -939,86 +938,6 @@ |
| 939 | "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", | 938 | "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", |
| 940 | "license": "MIT" | 939 | "license": "MIT" |
| 941 | }, | 940 | }, |
| 942 | - "node_modules/@supabase/auth-js": { | ||
| 943 | - "version": "2.98.0", | ||
| 944 | - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.98.0.tgz", | ||
| 945 | - "integrity": "sha512-GBH361T0peHU91AQNzOlIrjUZw9TZbB9YDRiyFgk/3Kvr3/Z1NWUZ2athWTfHhwNNi8IrW00foyFxQD9IO/Trg==", | ||
| 946 | - "license": "MIT", | ||
| 947 | - "dependencies": { | ||
| 948 | - "tslib": "2.8.1" | ||
| 949 | - }, | ||
| 950 | - "engines": { | ||
| 951 | - "node": ">=20.0.0" | ||
| 952 | - } | ||
| 953 | - }, | ||
| 954 | - "node_modules/@supabase/functions-js": { | ||
| 955 | - "version": "2.98.0", | ||
| 956 | - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.98.0.tgz", | ||
| 957 | - "integrity": "sha512-N/xEyiNU5Org+d+PNCpv+TWniAXRzxIURxDYsS/m2I/sfAB/HcM9aM2Dmf5edj5oWb9GxID1OBaZ8NMmPXL+Lg==", | ||
| 958 | - "license": "MIT", | ||
| 959 | - "dependencies": { | ||
| 960 | - "tslib": "2.8.1" | ||
| 961 | - }, | ||
| 962 | - "engines": { | ||
| 963 | - "node": ">=20.0.0" | ||
| 964 | - } | ||
| 965 | - }, | ||
| 966 | - "node_modules/@supabase/postgrest-js": { | ||
| 967 | - "version": "2.98.0", | ||
| 968 | - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.98.0.tgz", | ||
| 969 | - "integrity": "sha512-v6e9WeZuJijzUut8HyXu6gMqWFepIbaeaMIm1uKzei4yLg9bC9OtEW9O14LE/9ezqNbSAnSLO5GtOLFdm7Bpkg==", | ||
| 970 | - "license": "MIT", | ||
| 971 | - "dependencies": { | ||
| 972 | - "tslib": "2.8.1" | ||
| 973 | - }, | ||
| 974 | - "engines": { | ||
| 975 | - "node": ">=20.0.0" | ||
| 976 | - } | ||
| 977 | - }, | ||
| 978 | - "node_modules/@supabase/realtime-js": { | ||
| 979 | - "version": "2.98.0", | ||
| 980 | - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.98.0.tgz", | ||
| 981 | - "integrity": "sha512-rOWt28uGyFipWOSd+n0WVMr9kUXiWaa7J4hvyLCIHjRFqWm1z9CaaKAoYyfYMC1Exn3WT8WePCgiVhlAtWC2yw==", | ||
| 982 | - "license": "MIT", | ||
| 983 | - "dependencies": { | ||
| 984 | - "@types/phoenix": "^1.6.6", | ||
| 985 | - "@types/ws": "^8.18.1", | ||
| 986 | - "tslib": "2.8.1", | ||
| 987 | - "ws": "^8.18.2" | ||
| 988 | - }, | ||
| 989 | - "engines": { | ||
| 990 | - "node": ">=20.0.0" | ||
| 991 | - } | ||
| 992 | - }, | ||
| 993 | - "node_modules/@supabase/storage-js": { | ||
| 994 | - "version": "2.98.0", | ||
| 995 | - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.98.0.tgz", | ||
| 996 | - "integrity": "sha512-tzr2mG+v7ILSAZSfZMSL9OPyIH4z1ikgQ8EcQTKfMRz4EwmlFt3UnJaGzSOxyvF5b+fc9So7qdSUWTqGgeLokQ==", | ||
| 997 | - "license": "MIT", | ||
| 998 | - "dependencies": { | ||
| 999 | - "iceberg-js": "^0.8.1", | ||
| 1000 | - "tslib": "2.8.1" | ||
| 1001 | - }, | ||
| 1002 | - "engines": { | ||
| 1003 | - "node": ">=20.0.0" | ||
| 1004 | - } | ||
| 1005 | - }, | ||
| 1006 | - "node_modules/@supabase/supabase-js": { | ||
| 1007 | - "version": "2.98.0", | ||
| 1008 | - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.98.0.tgz", | ||
| 1009 | - "integrity": "sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw==", | ||
| 1010 | - "license": "MIT", | ||
| 1011 | - "dependencies": { | ||
| 1012 | - "@supabase/auth-js": "2.98.0", | ||
| 1013 | - "@supabase/functions-js": "2.98.0", | ||
| 1014 | - "@supabase/postgrest-js": "2.98.0", | ||
| 1015 | - "@supabase/realtime-js": "2.98.0", | ||
| 1016 | - "@supabase/storage-js": "2.98.0" | ||
| 1017 | - }, | ||
| 1018 | - "engines": { | ||
| 1019 | - "node": ">=20.0.0" | ||
| 1020 | - } | ||
| 1021 | - }, | ||
| 1022 | "node_modules/@sveltejs/acorn-typescript": { | 941 | "node_modules/@sveltejs/acorn-typescript": { |
| 1023 | "version": "1.0.9", | 942 | "version": "1.0.9", |
| 1024 | "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", | 943 | "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", |
| ... | @@ -1415,21 +1334,6 @@ | ... | @@ -1415,21 +1334,6 @@ |
| 1415 | "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", | 1334 | "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", |
| 1416 | "license": "MIT" | 1335 | "license": "MIT" |
| 1417 | }, | 1336 | }, |
| 1418 | - "node_modules/@types/node": { | ||
| 1419 | - "version": "25.3.2", | ||
| 1420 | - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz", | ||
| 1421 | - "integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==", | ||
| 1422 | - "license": "MIT", | ||
| 1423 | - "dependencies": { | ||
| 1424 | - "undici-types": "~7.18.0" | ||
| 1425 | - } | ||
| 1426 | - }, | ||
| 1427 | - "node_modules/@types/phoenix": { | ||
| 1428 | - "version": "1.6.7", | ||
| 1429 | - "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", | ||
| 1430 | - "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", | ||
| 1431 | - "license": "MIT" | ||
| 1432 | - }, | ||
| 1433 | "node_modules/@types/resolve": { | 1337 | "node_modules/@types/resolve": { |
| 1434 | "version": "1.20.2", | 1338 | "version": "1.20.2", |
| 1435 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", | 1339 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", |
| ... | @@ -1442,15 +1346,6 @@ | ... | @@ -1442,15 +1346,6 @@ |
| 1442 | "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", | 1346 | "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", |
| 1443 | "license": "MIT" | 1347 | "license": "MIT" |
| 1444 | }, | 1348 | }, |
| 1445 | - "node_modules/@types/ws": { | ||
| 1446 | - "version": "8.18.1", | ||
| 1447 | - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", | ||
| 1448 | - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", | ||
| 1449 | - "license": "MIT", | ||
| 1450 | - "dependencies": { | ||
| 1451 | - "@types/node": "*" | ||
| 1452 | - } | ||
| 1453 | - }, | ||
| 1454 | "node_modules/acorn": { | 1349 | "node_modules/acorn": { |
| 1455 | "version": "8.16.0", | 1350 | "version": "8.16.0", |
| 1456 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", | 1351 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", |
| ... | @@ -2114,15 +2009,6 @@ | ... | @@ -2114,15 +2009,6 @@ |
| 2114 | "integrity": "sha512-CXcN/u6RdQqsK8IphUptpAEqY8IzgwzHY+MuXX+2wpoWTumfxPVr6JYbbywsNsiAl9aEbM5sRtxkwRBa22b49w==", | 2009 | "integrity": "sha512-CXcN/u6RdQqsK8IphUptpAEqY8IzgwzHY+MuXX+2wpoWTumfxPVr6JYbbywsNsiAl9aEbM5sRtxkwRBa22b49w==", |
| 2115 | "license": "MIT" | 2010 | "license": "MIT" |
| 2116 | }, | 2011 | }, |
| 2117 | - "node_modules/iceberg-js": { | ||
| 2118 | - "version": "0.8.1", | ||
| 2119 | - "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", | ||
| 2120 | - "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", | ||
| 2121 | - "license": "MIT", | ||
| 2122 | - "engines": { | ||
| 2123 | - "node": ">=20.0.0" | ||
| 2124 | - } | ||
| 2125 | - }, | ||
| 2126 | "node_modules/iconv-lite": { | 2012 | "node_modules/iconv-lite": { |
| 2127 | "version": "0.6.3", | 2013 | "version": "0.6.3", |
| 2128 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", | 2014 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", |
| ... | @@ -2775,13 +2661,9 @@ | ... | @@ -2775,13 +2661,9 @@ |
| 2775 | "version": "2.8.1", | 2661 | "version": "2.8.1", |
| 2776 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", | 2662 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", |
| 2777 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", | 2663 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", |
| 2778 | - "license": "0BSD" | 2664 | + "dev": true, |
| 2779 | - }, | 2665 | + "license": "0BSD", |
| 2780 | - "node_modules/undici-types": { | 2666 | + "optional": true |
| 2781 | - "version": "7.18.2", | ||
| 2782 | - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", | ||
| 2783 | - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", | ||
| 2784 | - "license": "MIT" | ||
| 2785 | }, | 2667 | }, |
| 2786 | "node_modules/vite": { | 2668 | "node_modules/vite": { |
| 2787 | "version": "7.3.1", | 2669 | "version": "7.3.1", |
| ... | @@ -2876,27 +2758,6 @@ | ... | @@ -2876,27 +2758,6 @@ |
| 2876 | } | 2758 | } |
| 2877 | } | 2759 | } |
| 2878 | }, | 2760 | }, |
| 2879 | - "node_modules/ws": { | ||
| 2880 | - "version": "8.19.0", | ||
| 2881 | - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", | ||
| 2882 | - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", | ||
| 2883 | - "license": "MIT", | ||
| 2884 | - "engines": { | ||
| 2885 | - "node": ">=10.0.0" | ||
| 2886 | - }, | ||
| 2887 | - "peerDependencies": { | ||
| 2888 | - "bufferutil": "^4.0.1", | ||
| 2889 | - "utf-8-validate": ">=5.0.2" | ||
| 2890 | - }, | ||
| 2891 | - "peerDependenciesMeta": { | ||
| 2892 | - "bufferutil": { | ||
| 2893 | - "optional": true | ||
| 2894 | - }, | ||
| 2895 | - "utf-8-validate": { | ||
| 2896 | - "optional": true | ||
| 2897 | - } | ||
| 2898 | - } | ||
| 2899 | - }, | ||
| 2900 | "node_modules/zimmerframe": { | 2761 | "node_modules/zimmerframe": { |
| 2901 | "version": "1.1.4", | 2762 | "version": "1.1.4", |
| 2902 | "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", | 2763 | "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", | ... | ... |
| ... | @@ -20,7 +20,6 @@ | ... | @@ -20,7 +20,6 @@ |
| 20 | }, | 20 | }, |
| 21 | "dependencies": { | 21 | "dependencies": { |
| 22 | "@observablehq/plot": "^0.6.17", | 22 | "@observablehq/plot": "^0.6.17", |
| 23 | - "@supabase/supabase-js": "^2.98.0", | ||
| 24 | "@sveltejs/adapter-node": "^5.5.4", | 23 | "@sveltejs/adapter-node": "^5.5.4", |
| 25 | "d3": "^7.9.0", | 24 | "d3": "^7.9.0", |
| 26 | "fuse.js": "^7.1.0", | 25 | "fuse.js": "^7.1.0", | ... | ... |
src/lib/services/search.js
deleted
100644 → 0
| 1 | -import Fuse from 'fuse.js'; | ||
| 2 | -import { supabase } from '$lib/supabase'; | ||
| 3 | -import { normalizeText } from '$lib/utils/normalize'; | ||
| 4 | - | ||
| 5 | -let fuseInstance = null; | ||
| 6 | -let searchData = []; | ||
| 7 | - | ||
| 8 | -/** | ||
| 9 | - * Configuración de Fuse.js | ||
| 10 | - */ | ||
| 11 | -const fuseOptions = { | ||
| 12 | - keys: [ | ||
| 13 | - { name: 'nombre_normalizado', weight: 0.6 }, | ||
| 14 | - { name: 'sigla_normalizada', weight: 0.2 }, | ||
| 15 | - { name: 'codigo_normalizado', weight: 0.2 } | ||
| 16 | - ], | ||
| 17 | - threshold: 0.4, // 0 = exacto, 1 = muy fuzzy | ||
| 18 | - distance: 100, | ||
| 19 | - includeScore: true, | ||
| 20 | - includeMatches: true, | ||
| 21 | - minMatchCharLength: 2, | ||
| 22 | - ignoreLocation: true | ||
| 23 | -}; | ||
| 24 | - | ||
| 25 | -/** | ||
| 26 | - * Carga las entidades y objetos de gasto desde Supabase y construye el índice | ||
| 27 | - */ | ||
| 28 | -export async function initSearchIndex() { | ||
| 29 | - if (fuseInstance) return fuseInstance; | ||
| 30 | - | ||
| 31 | - // Cargar entidades | ||
| 32 | - const { data: entidades, error: errorEntidades } = await supabase | ||
| 33 | - .schema('ppto') | ||
| 34 | - .from('clas_institucional') | ||
| 35 | - .select('entidad, desc_entidad, sigla_entidad, desc_area, n_gestiones'); | ||
| 36 | - | ||
| 37 | - if (errorEntidades) { | ||
| 38 | - console.error('Error cargando entidades:', errorEntidades); | ||
| 39 | - throw errorEntidades; | ||
| 40 | - } | ||
| 41 | - | ||
| 42 | - // Cargar objetos de gasto | ||
| 43 | - const { data: objetos, error: errorObjetos } = await supabase | ||
| 44 | - .schema('ppto') | ||
| 45 | - .from('clas_objetos') | ||
| 46 | - .select('objeto, desc_objeto, nivel'); | ||
| 47 | - | ||
| 48 | - if (errorObjetos) { | ||
| 49 | - console.error('Error cargando objetos:', errorObjetos); | ||
| 50 | - throw errorObjetos; | ||
| 51 | - } | ||
| 52 | - | ||
| 53 | - // Cargar rubros | ||
| 54 | - const { data: rubros, error: errorRubros } = await supabase | ||
| 55 | - .schema('ppto') | ||
| 56 | - .from('clas_rubros') | ||
| 57 | - .select('rubro, desc_rubro, nivel'); | ||
| 58 | - | ||
| 59 | - if (errorRubros) { | ||
| 60 | - console.error('Error cargando rubros:', errorRubros); | ||
| 61 | - throw errorRubros; | ||
| 62 | - } | ||
| 63 | - | ||
| 64 | - // Cargar finalidad y función | ||
| 65 | - const { data: finfun, error: errorFinfun } = await supabase | ||
| 66 | - .schema('ppto') | ||
| 67 | - .from('clas_finfun') | ||
| 68 | - .select('finfun, desc_finfun, nivel'); | ||
| 69 | - | ||
| 70 | - if (errorFinfun) { | ||
| 71 | - console.error('Error cargando finalidad/función:', errorFinfun); | ||
| 72 | - throw errorFinfun; | ||
| 73 | - } | ||
| 74 | - | ||
| 75 | - // Cargar organismos (no bloquea si falla) | ||
| 76 | - let organismos = []; | ||
| 77 | - const { data: organismosData, error: errorOrganismos } = await supabase | ||
| 78 | - .schema('ppto') | ||
| 79 | - .from('clas_organismos') | ||
| 80 | - .select('organismo, organismo_desc_organismo, organismo_grupo, organismo_sub_grupo'); | ||
| 81 | - | ||
| 82 | - if (errorOrganismos) { | ||
| 83 | - console.warn('Tabla clas_organismos no disponible:', errorOrganismos.message); | ||
| 84 | - } else { | ||
| 85 | - organismos = organismosData || []; | ||
| 86 | - } | ||
| 87 | - | ||
| 88 | - // Cargar fuentes (no bloquea si falla) | ||
| 89 | - let fuentes = []; | ||
| 90 | - const { data: fuentesData, error: errorFuentes } = await supabase | ||
| 91 | - .schema('ppto') | ||
| 92 | - .from('clas_fuentes') | ||
| 93 | - .select('fuente, desc_fuente, grupo_fuente, desc_grupo_fuente'); | ||
| 94 | - | ||
| 95 | - if (errorFuentes) { | ||
| 96 | - console.warn('Tabla clas_fuentes no disponible:', errorFuentes.message); | ||
| 97 | - } else { | ||
| 98 | - fuentes = fuentesData || []; | ||
| 99 | - } | ||
| 100 | - | ||
| 101 | - // Transformar entidades para el índice | ||
| 102 | - const entidadesIndex = entidades.map(item => ({ | ||
| 103 | - tipo: 'entidad', | ||
| 104 | - codigo: item.entidad, | ||
| 105 | - nombre: item.desc_entidad, | ||
| 106 | - sigla: item.sigla_entidad, | ||
| 107 | - contexto: item.desc_area, | ||
| 108 | - años: item.n_gestiones, | ||
| 109 | - nombre_normalizado: normalizeText(item.desc_entidad), | ||
| 110 | - sigla_normalizada: normalizeText(item.sigla_entidad), | ||
| 111 | - codigo_normalizado: item.entidad?.toString() || '' | ||
| 112 | - })); | ||
| 113 | - | ||
| 114 | - // Transformar objetos de gasto para el índice | ||
| 115 | - const nivelLabelsObjetos = { grupo: 'Grupo', subgrupo: 'Subgrupo', partida: 'Partida', subpartida: 'Subpartida' }; | ||
| 116 | - const objetosIndex = objetos.map(item => ({ | ||
| 117 | - tipo: 'objeto_gasto', | ||
| 118 | - codigo: item.objeto, | ||
| 119 | - nombre: item.desc_objeto, | ||
| 120 | - sigla: null, | ||
| 121 | - contexto: `Objeto del Gasto · ${nivelLabelsObjetos[item.nivel] || item.nivel}`, | ||
| 122 | - nivel: item.nivel, | ||
| 123 | - nombre_normalizado: normalizeText(item.desc_objeto), | ||
| 124 | - sigla_normalizada: '', | ||
| 125 | - codigo_normalizado: item.objeto || '' | ||
| 126 | - })); | ||
| 127 | - | ||
| 128 | - // Transformar rubros para el índice | ||
| 129 | - const nivelLabelsRubros = { tipo: 'Tipo', clase: 'Clase', cuenta: 'Cuenta', sub_cuenta: 'Subcuenta' }; | ||
| 130 | - const rubrosIndex = rubros.map(item => ({ | ||
| 131 | - tipo: 'rubro', | ||
| 132 | - codigo: item.rubro, | ||
| 133 | - nombre: item.desc_rubro, | ||
| 134 | - sigla: null, | ||
| 135 | - contexto: `Rubro · ${nivelLabelsRubros[item.nivel] || item.nivel}`, | ||
| 136 | - nivel: item.nivel, | ||
| 137 | - nombre_normalizado: normalizeText(item.desc_rubro), | ||
| 138 | - sigla_normalizada: '', | ||
| 139 | - codigo_normalizado: item.rubro?.toString() || '' | ||
| 140 | - })); | ||
| 141 | - | ||
| 142 | - // Transformar finalidad y función para el índice | ||
| 143 | - const nivelLabelsFinfun = { finalidad: 'Finalidad', grpfuncion: 'Grupo Función', funcion: 'Función' }; | ||
| 144 | - const finfunIndex = finfun.map(item => ({ | ||
| 145 | - tipo: 'finfun', | ||
| 146 | - codigo: item.finfun, | ||
| 147 | - nombre: item.desc_finfun, | ||
| 148 | - sigla: null, | ||
| 149 | - contexto: `Finalidad y Función · ${nivelLabelsFinfun[item.nivel] || item.nivel}`, | ||
| 150 | - nivel: item.nivel, | ||
| 151 | - nombre_normalizado: normalizeText(item.desc_finfun), | ||
| 152 | - sigla_normalizada: '', | ||
| 153 | - codigo_normalizado: item.finfun?.toString() || '' | ||
| 154 | - })); | ||
| 155 | - | ||
| 156 | - // Transformar organismos para el índice | ||
| 157 | - // Todos los registros en la tabla son organismos (nivel hoja) | ||
| 158 | - const organismosIndex = organismos.map(item => ({ | ||
| 159 | - tipo: 'organismo', | ||
| 160 | - codigo: item.organismo, | ||
| 161 | - nombre: item.organismo_desc_organismo, | ||
| 162 | - sigla: null, | ||
| 163 | - contexto: `Organismo · Grupo ${item.organismo_grupo}`, | ||
| 164 | - nivel: 'organismo', | ||
| 165 | - nombre_normalizado: normalizeText(item.organismo_desc_organismo), | ||
| 166 | - sigla_normalizada: '', | ||
| 167 | - codigo_normalizado: item.organismo?.toString() || '' | ||
| 168 | - })); | ||
| 169 | - | ||
| 170 | - // Transformar fuentes para el índice | ||
| 171 | - const fuentesIndex = fuentes.map(item => ({ | ||
| 172 | - tipo: 'fuente', | ||
| 173 | - codigo: item.fuente, | ||
| 174 | - nombre: item.desc_fuente, | ||
| 175 | - sigla: null, | ||
| 176 | - contexto: `Fuente · ${item.desc_grupo_fuente}`, | ||
| 177 | - nivel: 'fuente', | ||
| 178 | - nombre_normalizado: normalizeText(item.desc_fuente), | ||
| 179 | - sigla_normalizada: '', | ||
| 180 | - codigo_normalizado: item.fuente?.toString() || '' | ||
| 181 | - })); | ||
| 182 | - | ||
| 183 | - // Combinar todos los conjuntos | ||
| 184 | - searchData = [...entidadesIndex, ...objetosIndex, ...rubrosIndex, ...finfunIndex, ...organismosIndex, ...fuentesIndex]; | ||
| 185 | - | ||
| 186 | - fuseInstance = new Fuse(searchData, fuseOptions); | ||
| 187 | - | ||
| 188 | - return fuseInstance; | ||
| 189 | -} | ||
| 190 | - | ||
| 191 | -/** | ||
| 192 | - * Busca en el índice | ||
| 193 | - * @param {string} query - Texto de búsqueda | ||
| 194 | - * @param {number} limit - Máximo de resultados (default 30) | ||
| 195 | - * @returns {Array} Resultados de búsqueda | ||
| 196 | - */ | ||
| 197 | -export function search(query, limit = 30) { | ||
| 198 | - if (!fuseInstance) { | ||
| 199 | - console.warn('Índice no inicializado. Llama initSearchIndex() primero.'); | ||
| 200 | - return []; | ||
| 201 | - } | ||
| 202 | - | ||
| 203 | - if (!query || query.length < 2) { | ||
| 204 | - return []; | ||
| 205 | - } | ||
| 206 | - | ||
| 207 | - const normalizedQuery = normalizeText(query); | ||
| 208 | - const results = fuseInstance.search(normalizedQuery, { limit }); | ||
| 209 | - | ||
| 210 | - return results.map(result => ({ | ||
| 211 | - ...result.item, | ||
| 212 | - score: result.score, | ||
| 213 | - matches: result.matches | ||
| 214 | - })); | ||
| 215 | -} | ||
| 216 | - | ||
| 217 | -/** | ||
| 218 | - * Obtiene todas las entidades (sin búsqueda) | ||
| 219 | - */ | ||
| 220 | -export function getAllEntidades() { | ||
| 221 | - return searchData.filter(item => item.tipo === 'entidad'); | ||
| 222 | -} | ||
| 223 | - | ||
| 224 | -/** | ||
| 225 | - * Obtiene todos los objetos de gasto (sin búsqueda) | ||
| 226 | - */ | ||
| 227 | -export function getAllObjetos() { | ||
| 228 | - return searchData.filter(item => item.tipo === 'objeto_gasto'); | ||
| 229 | -} | ||
| 230 | - | ||
| 231 | -/** | ||
| 232 | - * Verifica si el índice está cargado | ||
| 233 | - */ | ||
| 234 | -export function isIndexReady() { | ||
| 235 | - return fuseInstance !== null; | ||
| 236 | -} |
| 1 | -import { writable, derived, get } from 'svelte/store'; | 1 | +import { writable, derived } from 'svelte/store'; |
| 2 | -import { initSearchIndex, search, isIndexReady } from '$lib/services/search'; | ||
| 3 | 2 | ||
| 4 | -// Estado del buscador | ||
| 5 | export const query = writable(''); | 3 | export const query = writable(''); |
| 6 | export const results = writable([]); | 4 | export const results = writable([]); |
| 7 | export const isLoading = writable(false); | 5 | export const isLoading = writable(false); |
| 8 | -export const indexLoaded = writable(false); | 6 | +export const indexLoaded = writable(true); |
| 9 | export const error = writable(null); | 7 | export const error = writable(null); |
| 10 | export const selectedIndex = writable(-1); | 8 | export const selectedIndex = writable(-1); |
| 11 | -export const filterType = writable('todos'); // todos, entidad, objeto_gasto, rubro, finfun | 9 | +export const filterType = writable('todos'); |
| 12 | 10 | ||
| 13 | -// Estado derivado: si está mostrando resultados | ||
| 14 | export const showResults = derived( | 11 | export const showResults = derived( |
| 15 | [query, results, indexLoaded], | 12 | [query, results, indexLoaded], |
| 16 | ([$query, $results, $indexLoaded]) => $indexLoaded && $query.length >= 2 | 13 | ([$query, $results, $indexLoaded]) => $indexLoaded && $query.length >= 2 |
| 17 | ); | 14 | ); |
| 18 | 15 | ||
| 19 | -/** | ||
| 20 | - * Inicializa el índice de búsqueda | ||
| 21 | - */ | ||
| 22 | export async function initIndex() { | 16 | export async function initIndex() { |
| 23 | - if (isIndexReady()) { | 17 | + indexLoaded.set(true); |
| 24 | - indexLoaded.set(true); | ||
| 25 | - return; | ||
| 26 | - } | ||
| 27 | - | ||
| 28 | - isLoading.set(true); | ||
| 29 | - error.set(null); | ||
| 30 | - | ||
| 31 | - try { | ||
| 32 | - await initSearchIndex(); | ||
| 33 | - indexLoaded.set(true); | ||
| 34 | - } catch (e) { | ||
| 35 | - error.set('Error al cargar el índice de búsqueda'); | ||
| 36 | - console.error(e); | ||
| 37 | - } finally { | ||
| 38 | - isLoading.set(false); | ||
| 39 | - } | ||
| 40 | } | 18 | } |
| 41 | 19 | ||
| 42 | -/** | 20 | +export function performSearch() {} |
| 43 | - * Realiza la búsqueda | ||
| 44 | - */ | ||
| 45 | -export function performSearch(searchQuery, filter = null) { | ||
| 46 | - query.set(searchQuery); | ||
| 47 | - selectedIndex.set(-1); | ||
| 48 | - | ||
| 49 | - if (searchQuery.length < 2) { | ||
| 50 | - results.set([]); | ||
| 51 | - return; | ||
| 52 | - } | ||
| 53 | - | ||
| 54 | - let searchResults = search(searchQuery, 30); | ||
| 55 | - | ||
| 56 | - // Filtrar por tipo si no es "todos" | ||
| 57 | - const currentFilter = filter !== null ? filter : get(filterType); | ||
| 58 | - if (currentFilter !== 'todos') { | ||
| 59 | - searchResults = searchResults.filter(r => r.tipo === currentFilter); | ||
| 60 | - } | ||
| 61 | - | ||
| 62 | - // Limitar resultados | ||
| 63 | - results.set(searchResults.slice(0, 15)); | ||
| 64 | -} | ||
| 65 | - | ||
| 66 | -/** | ||
| 67 | - * Limpia la búsqueda | ||
| 68 | - */ | ||
| 69 | export function clearSearch() { | 21 | export function clearSearch() { |
| 70 | query.set(''); | 22 | query.set(''); |
| 71 | results.set([]); | 23 | results.set([]); |
| 72 | selectedIndex.set(-1); | 24 | selectedIndex.set(-1); |
| 73 | } | 25 | } |
| 74 | - | 26 | +export function setFilter() {} |
| 75 | -/** | 27 | +export function navigateResults() {} |
| 76 | - * Cambia el filtro de tipo | ||
| 77 | - */ | ||
| 78 | -export function setFilter(type) { | ||
| 79 | - filterType.set(type); | ||
| 80 | - // Re-ejecutar búsqueda con el nuevo filtro | ||
| 81 | - const currentQuery = get(query); | ||
| 82 | - if (currentQuery.length >= 2) { | ||
| 83 | - performSearch(currentQuery, type); | ||
| 84 | - } | ||
| 85 | -} | ||
| 86 | - | ||
| 87 | -/** | ||
| 88 | - * Navegación por teclado | ||
| 89 | - */ | ||
| 90 | -export function navigateResults(direction, totalResults) { | ||
| 91 | - selectedIndex.update(current => { | ||
| 92 | - if (direction === 'down') { | ||
| 93 | - return current < totalResults - 1 ? current + 1 : 0; | ||
| 94 | - } else if (direction === 'up') { | ||
| 95 | - return current > 0 ? current - 1 : totalResults - 1; | ||
| 96 | - } | ||
| 97 | - return current; | ||
| 98 | - }); | ||
| 99 | -} | ... | ... |
src/lib/supabase.js
deleted
100644 → 0
src/routes/api/test/+server.js
deleted
100644 → 0
| 1 | -import { supabase } from '$lib/supabase'; | ||
| 2 | -import { json } from '@sveltejs/kit'; | ||
| 3 | - | ||
| 4 | -export async function GET() { | ||
| 5 | - const { data, error } = await supabase | ||
| 6 | - .schema('ppto') | ||
| 7 | - .from('clas_institucional') | ||
| 8 | - .select('*') | ||
| 9 | - .limit(5); | ||
| 10 | - | ||
| 11 | - if (error) { | ||
| 12 | - return json({ error: error.message }, { status: 500 }); | ||
| 13 | - } | ||
| 14 | - | ||
| 15 | - return json({ | ||
| 16 | - success: true, | ||
| 17 | - count: data.length, | ||
| 18 | - columns: data.length > 0 ? Object.keys(data[0]) : [], | ||
| 19 | - sample: data | ||
| 20 | - }); | ||
| 21 | -} |
| 1 | -<script> | ||
| 2 | - import { supabase } from '$lib/supabase'; | ||
| 3 | - import { onMount } from 'svelte'; | ||
| 4 | - import { fade, scale } from 'svelte/transition'; | ||
| 5 | - import { cubicOut } from 'svelte/easing'; | ||
| 6 | - | ||
| 7 | - let loading = $state(true); | ||
| 8 | - let searchQuery = $state(''); | ||
| 9 | - let grupos = $state([]); | ||
| 10 | - let allItems = $state([]); | ||
| 11 | - let selectedGrupo = $state(null); | ||
| 12 | - let selectedItem = $state(null); | ||
| 13 | - let highlightedItem = $state(null); | ||
| 14 | - let sidebarOpen = $state(false); | ||
| 15 | - | ||
| 16 | - onMount(async () => { | ||
| 17 | - // Forzar recálculo de layout para navegación cliente | ||
| 18 | - requestAnimationFrame(() => { | ||
| 19 | - document.body.offsetHeight; // Force reflow | ||
| 20 | - }); | ||
| 21 | - | ||
| 22 | - const { data, error } = await supabase | ||
| 23 | - .schema('ppto') | ||
| 24 | - .from('clas_fuentes') | ||
| 25 | - .select('*') | ||
| 26 | - .order('fuente'); | ||
| 27 | - | ||
| 28 | - if (!error && data) { | ||
| 29 | - allItems = data; | ||
| 30 | - | ||
| 31 | - // Crear grupos sintéticos desde valores únicos de grupo_fuente | ||
| 32 | - const gruposUnicos = [...new Set(data.map(item => item.grupo_fuente))].filter(g => g != null); | ||
| 33 | - grupos = gruposUnicos.map(grupoNum => { | ||
| 34 | - const sample = data.find(item => item.grupo_fuente === grupoNum); | ||
| 35 | - return { | ||
| 36 | - grupo_fuente: grupoNum, | ||
| 37 | - desc_grupo_fuente: sample?.desc_grupo_fuente || `Grupo ${grupoNum}` | ||
| 38 | - }; | ||
| 39 | - }).sort((a, b) => Number(a.grupo_fuente) - Number(b.grupo_fuente)); | ||
| 40 | - | ||
| 41 | - if (grupos.length > 0) { | ||
| 42 | - selectedGrupo = grupos[0]; | ||
| 43 | - } | ||
| 44 | - } | ||
| 45 | - | ||
| 46 | - loading = false; | ||
| 47 | - }); | ||
| 48 | - | ||
| 49 | - // Obtener fuentes para un grupo específico | ||
| 50 | - function getFuentesForGrupo(grupo) { | ||
| 51 | - if (!grupo) return []; | ||
| 52 | - return allItems | ||
| 53 | - .filter(item => item.grupo_fuente === grupo.grupo_fuente) | ||
| 54 | - .sort((a, b) => String(a.fuente).localeCompare(String(b.fuente))); | ||
| 55 | - } | ||
| 56 | - | ||
| 57 | - // Búsqueda global | ||
| 58 | - function searchGlobal(query) { | ||
| 59 | - if (!query || query.length < 2) return []; | ||
| 60 | - const q = query.toLowerCase(); | ||
| 61 | - | ||
| 62 | - return allItems | ||
| 63 | - .filter(item => | ||
| 64 | - item.fuente?.toString().includes(q) || | ||
| 65 | - item.desc_fuente?.toLowerCase().includes(q) || | ||
| 66 | - item.desc_grupo_fuente?.toLowerCase().includes(q) | ||
| 67 | - ) | ||
| 68 | - .slice(0, 20); | ||
| 69 | - } | ||
| 70 | - | ||
| 71 | - function parseDescripciones(descripcionesStr) { | ||
| 72 | - if (!descripcionesStr) return []; | ||
| 73 | - try { | ||
| 74 | - const parsed = typeof descripcionesStr === 'string' | ||
| 75 | - ? JSON.parse(descripcionesStr) | ||
| 76 | - : descripcionesStr; | ||
| 77 | - return parsed.sort((a, b) => { | ||
| 78 | - const maxYearA = getMaxYear(a.rangos); | ||
| 79 | - const maxYearB = getMaxYear(b.rangos); | ||
| 80 | - return maxYearB - maxYearA; | ||
| 81 | - }); | ||
| 82 | - } catch { | ||
| 83 | - return []; | ||
| 84 | - } | ||
| 85 | - } | ||
| 86 | - | ||
| 87 | - function getMaxYear(rangos) { | ||
| 88 | - if (!rangos) return 0; | ||
| 89 | - const years = rangos.match(/\d{4}/g); | ||
| 90 | - if (!years) return 0; | ||
| 91 | - return Math.max(...years.map(y => parseInt(y))); | ||
| 92 | - } | ||
| 93 | - | ||
| 94 | - function selectGrupo(grupo) { | ||
| 95 | - selectedGrupo = grupo; | ||
| 96 | - selectedItem = null; | ||
| 97 | - searchQuery = ''; | ||
| 98 | - highlightedItem = null; | ||
| 99 | - sidebarOpen = false; | ||
| 100 | - } | ||
| 101 | - | ||
| 102 | - function openDetail(item) { | ||
| 103 | - selectedItem = item; | ||
| 104 | - } | ||
| 105 | - | ||
| 106 | - function closeDetail() { | ||
| 107 | - selectedItem = null; | ||
| 108 | - } | ||
| 109 | - | ||
| 110 | - function goToSearchResult(item) { | ||
| 111 | - const targetGrupo = grupos.find(g => g.grupo_fuente === item.grupo_fuente); | ||
| 112 | - | ||
| 113 | - if (targetGrupo) { | ||
| 114 | - selectedGrupo = targetGrupo; | ||
| 115 | - highlightedItem = item.fuente; | ||
| 116 | - searchQuery = ''; | ||
| 117 | - sidebarOpen = false; | ||
| 118 | - | ||
| 119 | - setTimeout(() => { | ||
| 120 | - const element = document.getElementById(`f-${item.fuente}`); | ||
| 121 | - if (element) { | ||
| 122 | - element.scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||
| 123 | - } | ||
| 124 | - }, 100); | ||
| 125 | - | ||
| 126 | - setTimeout(() => { | ||
| 127 | - highlightedItem = null; | ||
| 128 | - }, 2000); | ||
| 129 | - } | ||
| 130 | - } | ||
| 131 | - | ||
| 132 | - let grupoContent = $derived(selectedGrupo ? getFuentesForGrupo(selectedGrupo) : []); | ||
| 133 | - let searchResults = $derived(searchGlobal(searchQuery)); | ||
| 134 | - let isSearching = $derived(searchQuery.length >= 2); | ||
| 135 | - | ||
| 136 | - // Contadores | ||
| 137 | - let totalItems = $derived(allItems.length); | ||
| 138 | - let totalGrupos = $derived(grupos.length); | ||
| 139 | -</script> | ||
| 140 | - | ||
| 141 | -<svelte:head> | ||
| 142 | - <title>Fuentes de Financiamiento | Presupuesto Público</title> | ||
| 143 | - <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| 144 | - <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@400;500&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" /> | ||
| 145 | -</svelte:head> | ||
| 146 | - | ||
| 147 | -<div class="min-h-screen" style="font-family: 'Instrument Sans', sans-serif; background-color: var(--theme-body); color: var(--theme-titulo); transition: background-color 0.2s, color 0.2s;"> | ||
| 148 | - <!-- Header pedagógico --> | ||
| 149 | - <header class="border-b" style="border-color: var(--theme-borde); background-color: var(--theme-body);"> | ||
| 150 | - <div class="max-w-screen-xl mx-auto px-4 sm:px-6 py-6"> | ||
| 151 | - <!-- Breadcrumb: responsive --> | ||
| 152 | - <nav class="mb-4" style="font-family: 'DM Mono', monospace; font-size: 0.75rem;"> | ||
| 153 | - <!-- Móvil: solo padre --> | ||
| 154 | - <a href="/clasificadores" class="sm:hidden transition-colors" style="color: var(--theme-texto);"> | ||
| 155 | - ← Clasificadores | ||
| 156 | - </a> | ||
| 157 | - <!-- Desktop: ruta completa --> | ||
| 158 | - <div class="hidden sm:flex items-center gap-2" style="color: var(--theme-texto);"> | ||
| 159 | - <a href="/" class="transition-colors hover:opacity-80">Inicio</a> | ||
| 160 | - <span style="opacity: 0.5;">/</span> | ||
| 161 | - <a href="/clasificadores" class="transition-colors hover:opacity-80">Clasificadores</a> | ||
| 162 | - </div> | ||
| 163 | - </nav> | ||
| 164 | - | ||
| 165 | - <div class="max-w-3xl"> | ||
| 166 | - <p class="text-xs uppercase tracking-widest mb-2" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);"> | ||
| 167 | - Clasificador 02 | ||
| 168 | - </p> | ||
| 169 | - <h1 class="text-3xl mb-3" style="font-family: 'DM Serif Display', serif; color: var(--theme-titulo);"> | ||
| 170 | - ¿Con qué recursos se financia? | ||
| 171 | - </h1> | ||
| 172 | - | ||
| 173 | - <p class="leading-relaxed mb-3" style="color: var(--theme-texto);"> | ||
| 174 | - Organiza los recursos según su <strong style="color: var(--theme-titulo);">origen de financiamiento</strong>: | ||
| 175 | - Tesoro General, recursos específicos, créditos externos e internos, donaciones. | ||
| 176 | - Permite entender de dónde provienen los fondos para el gasto público. | ||
| 177 | - </p> | ||
| 178 | - | ||
| 179 | - {#if totalItems > 0} | ||
| 180 | - <p class="text-sm mb-5" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);"> | ||
| 181 | - <span style="color: var(--theme-titulo); font-weight: 500;">{totalItems}</span> fuentes de financiamiento en | ||
| 182 | - <span style="color: var(--theme-titulo); font-weight: 500;">{totalGrupos}</span> grupos | ||
| 183 | - </p> | ||
| 184 | - {/if} | ||
| 185 | - | ||
| 186 | - <!-- Jerarquía --> | ||
| 187 | - <div class="hidden sm:flex flex-wrap items-center gap-2 sm:gap-3 text-xs mb-6" style="font-family: 'DM Mono', monospace;"> | ||
| 188 | - <span class="px-2.5 py-1 rounded-full font-medium" style="background-color: var(--theme-accent); color: var(--theme-body); opacity: 0.9;"> | ||
| 189 | - Grupo | ||
| 190 | - </span> | ||
| 191 | - <span style="color: var(--theme-texto);">→</span> | ||
| 192 | - <span class="px-2.5 py-1 rounded-full" style="background-color: var(--theme-fill); color: var(--theme-titulo); border: 1px solid var(--theme-borde);"> | ||
| 193 | - Fuente | ||
| 194 | - </span> | ||
| 195 | - </div> | ||
| 196 | - <!-- Versión móvil simplificada --> | ||
| 197 | - <p class="sm:hidden text-sm mb-4" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);"> | ||
| 198 | - Jerarquía: Grupo → Fuente | ||
| 199 | - </p> | ||
| 200 | - </div> | ||
| 201 | - </div> | ||
| 202 | - </header> | ||
| 203 | - | ||
| 204 | - {#if loading} | ||
| 205 | - <div class="flex items-center justify-center py-20"> | ||
| 206 | - <p style="color: var(--theme-texto);">Cargando clasificador...</p> | ||
| 207 | - </div> | ||
| 208 | - {:else} | ||
| 209 | - <!-- Botón móvil para abrir sidebar --> | ||
| 210 | - <div class="lg:hidden px-4 py-3 border-b" style="border-color: var(--theme-borde); background-color: var(--theme-fill);"> | ||
| 211 | - <button | ||
| 212 | - onclick={() => sidebarOpen = !sidebarOpen} | ||
| 213 | - class="flex items-center gap-2 text-sm" | ||
| 214 | - style="color: var(--theme-texto);" | ||
| 215 | - > | ||
| 216 | - <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 217 | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> | ||
| 218 | - </svg> | ||
| 219 | - <span class="font-medium">{selectedGrupo ? selectedGrupo.desc_grupo_fuente : 'Seleccionar grupo'}</span> | ||
| 220 | - <svg class="w-4 h-4 ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 221 | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> | ||
| 222 | - </svg> | ||
| 223 | - </button> | ||
| 224 | - </div> | ||
| 225 | - | ||
| 226 | - <div class="clasificador-layout max-w-screen-xl mx-auto flex px-4" style="display: flex !important; flex-direction: row;"> | ||
| 227 | - <!-- Sidebar izquierda: Grupos --> | ||
| 228 | - {#if sidebarOpen} | ||
| 229 | - <div | ||
| 230 | - transition:fade={{ duration: 150 }} | ||
| 231 | - class="fixed inset-0 bg-black/30 z-40 lg:hidden" | ||
| 232 | - onclick={() => sidebarOpen = false} | ||
| 233 | - ></div> | ||
| 234 | - {/if} | ||
| 235 | - <aside class=" | ||
| 236 | - {sidebarOpen ? 'translate-x-0' : '-translate-x-full'} | ||
| 237 | - lg:translate-x-0 | ||
| 238 | - fixed lg:relative | ||
| 239 | - inset-y-0 left-0 | ||
| 240 | - w-72 lg:w-64 | ||
| 241 | - z-50 lg:z-auto | ||
| 242 | - transition-transform duration-200 ease-in-out | ||
| 243 | - lg:flex-shrink-0 | ||
| 244 | - shadow-xl lg:shadow-none | ||
| 245 | - sidebar-left | ||
| 246 | - "> | ||
| 247 | - <div class="h-full lg:h-screen lg:sticky lg:top-0 overflow-y-auto py-6 px-4 lg:px-0 lg:pr-6"> | ||
| 248 | - <!-- Cerrar en móvil --> | ||
| 249 | - <div class="flex justify-between items-center mb-4 lg:hidden"> | ||
| 250 | - <span class="text-sm font-medium" style="color: var(--theme-titulo);">Grupos de fuentes</span> | ||
| 251 | - <button onclick={() => sidebarOpen = false} style="color: var(--theme-texto);"> | ||
| 252 | - <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 253 | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | ||
| 254 | - </svg> | ||
| 255 | - </button> | ||
| 256 | - </div> | ||
| 257 | - | ||
| 258 | - <!-- Buscador --> | ||
| 259 | - <div class="mb-6"> | ||
| 260 | - <input | ||
| 261 | - type="text" | ||
| 262 | - bind:value={searchQuery} | ||
| 263 | - placeholder="Buscar..." | ||
| 264 | - class="w-full px-3 py-2 text-sm rounded-md focus:outline-none focus:ring-2" | ||
| 265 | - style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);" | ||
| 266 | - /> | ||
| 267 | - </div> | ||
| 268 | - | ||
| 269 | - <!-- Resultados de búsqueda --> | ||
| 270 | - {#if isSearching} | ||
| 271 | - <div class="mb-4"> | ||
| 272 | - <p class="text-xs uppercase tracking-wide mb-2" style="color: var(--theme-texto);"> | ||
| 273 | - {searchResults.length} resultados | ||
| 274 | - </p> | ||
| 275 | - <div class="space-y-1"> | ||
| 276 | - {#each searchResults as result} | ||
| 277 | - <button | ||
| 278 | - class="w-full text-left px-2 py-2 text-sm rounded transition-all" | ||
| 279 | - style="color: var(--theme-titulo);" | ||
| 280 | - onclick={() => goToSearchResult(result)} | ||
| 281 | - > | ||
| 282 | - <span class="text-xs block" style="color: var(--theme-texto);">Fuente</span> | ||
| 283 | - <span class="font-mono text-xs font-medium" style="color: var(--theme-accent);">{result.fuente}</span> | ||
| 284 | - <span class="ml-1">{result.desc_fuente}</span> | ||
| 285 | - </button> | ||
| 286 | - {/each} | ||
| 287 | - {#if searchResults.length === 0} | ||
| 288 | - <p class="text-sm px-2" style="color: var(--theme-texto);">Sin resultados</p> | ||
| 289 | - {/if} | ||
| 290 | - </div> | ||
| 291 | - </div> | ||
| 292 | - {:else} | ||
| 293 | - <!-- Lista de grupos --> | ||
| 294 | - <nav> | ||
| 295 | - <p class="text-xs uppercase tracking-wide mb-3 px-2 hidden lg:block" style="color: var(--theme-texto);">Grupos</p> | ||
| 296 | - <ul class="space-y-1"> | ||
| 297 | - {#each grupos as grupo} | ||
| 298 | - <li> | ||
| 299 | - <button | ||
| 300 | - class="w-full text-left px-3 py-2 rounded-md text-sm transition-all" | ||
| 301 | - style="{selectedGrupo?.grupo_fuente === grupo.grupo_fuente | ||
| 302 | - ? `background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent); color: var(--theme-titulo); font-weight: 500; border-left: 2px solid var(--theme-accent);` | ||
| 303 | - : `color: var(--theme-texto);`}" | ||
| 304 | - onclick={() => selectGrupo(grupo)} | ||
| 305 | - > | ||
| 306 | - <span class="font-mono text-xs block" style="color: var(--theme-texto);">Grupo {grupo.grupo_fuente}</span> | ||
| 307 | - {grupo.desc_grupo_fuente} | ||
| 308 | - </button> | ||
| 309 | - </li> | ||
| 310 | - {/each} | ||
| 311 | - </ul> | ||
| 312 | - </nav> | ||
| 313 | - {/if} | ||
| 314 | - </div> | ||
| 315 | - </aside> | ||
| 316 | - | ||
| 317 | - <!-- Contenido principal --> | ||
| 318 | - <main class="flex-1 min-w-0 lg:border-l xl:border-r" style="border-color: var(--theme-borde);"> | ||
| 319 | - <div class="px-4 sm:px-6 lg:px-10 py-6 lg:py-8"> | ||
| 320 | - {#if selectedGrupo} | ||
| 321 | - {#key selectedGrupo.grupo_fuente} | ||
| 322 | - <div in:fade={{ duration: 200, delay: 50 }}> | ||
| 323 | - <!-- Título del grupo --> | ||
| 324 | - <div class="mb-8 lg:mb-10 pb-6 lg:pb-8 border-b" style="border-color: var(--theme-borde);"> | ||
| 325 | - <p class="text-sm font-mono mb-1" style="color: var(--theme-texto);">Grupo {selectedGrupo.grupo_fuente}</p> | ||
| 326 | - <h2 class="text-lg sm:text-xl font-medium mb-2 flex items-center gap-2 sm:gap-3 flex-wrap" style="color: var(--theme-titulo);"> | ||
| 327 | - {selectedGrupo.desc_grupo_fuente} | ||
| 328 | - </h2> | ||
| 329 | - <p class="text-sm" style="color: var(--theme-texto);"> | ||
| 330 | - {grupoContent.length} fuentes de financiamiento en este grupo | ||
| 331 | - </p> | ||
| 332 | - </div> | ||
| 333 | - | ||
| 334 | - <!-- Fuentes --> | ||
| 335 | - <div class="space-y-8"> | ||
| 336 | - {#each grupoContent as fuente} | ||
| 337 | - {@const fuenteDescs = parseDescripciones(fuente.descripciones)} | ||
| 338 | - <section | ||
| 339 | - id="f-{fuente.fuente}" | ||
| 340 | - class="scroll-mt-4 {highlightedItem === fuente.fuente ? 'highlighted' : ''}" | ||
| 341 | - > | ||
| 342 | - <div class="flex items-start gap-2 sm:gap-4 mb-3"> | ||
| 343 | - <span class="font-mono text-xs sm:text-sm pt-1" style="color: var(--theme-texto);">{fuente.fuente}</span> | ||
| 344 | - <div class="flex-1"> | ||
| 345 | - <h3 class="text-base sm:text-lg font-medium flex items-center gap-2 flex-wrap" style="color: var(--theme-titulo);"> | ||
| 346 | - <span class="text-left">{fuente.desc_fuente}</span> | ||
| 347 | - {#if fuente.n_variaciones > 1} | ||
| 348 | - <button | ||
| 349 | - class="text-xs font-normal text-orange-500 hover:text-orange-700 transition-colors underline decoration-dotted decoration-orange-300 hover:decoration-orange-500 cursor-pointer" | ||
| 350 | - onclick={() => openDetail(fuente)} | ||
| 351 | - title="Ver variaciones de descripción" | ||
| 352 | - > | ||
| 353 | - {fuente.n_variaciones} var. | ||
| 354 | - </button> | ||
| 355 | - {/if} | ||
| 356 | - <a | ||
| 357 | - href="/fuente/{fuente.fuente}" | ||
| 358 | - class="transition-colors hover:text-[var(--theme-accent)]" | ||
| 359 | - style="color: var(--theme-texto);" | ||
| 360 | - title="Ver página de detalle" | ||
| 361 | - > | ||
| 362 | - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 363 | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> | ||
| 364 | - </svg> | ||
| 365 | - </a> | ||
| 366 | - </h3> | ||
| 367 | - {#if fuenteDescs.length > 0} | ||
| 368 | - <p class="text-sm mt-1 leading-relaxed" style="color: var(--theme-texto);">{fuenteDescs[0].descripcion}</p> | ||
| 369 | - {/if} | ||
| 370 | - <!-- Vigencia temporal --> | ||
| 371 | - {#if fuente.gestiones} | ||
| 372 | - <p class="text-xs mt-3 font-mono" style="color: var(--theme-texto); opacity: 0.7;"> | ||
| 373 | - Vigente: {fuente.gestiones} | ||
| 374 | - </p> | ||
| 375 | - {/if} | ||
| 376 | - </div> | ||
| 377 | - </div> | ||
| 378 | - </section> | ||
| 379 | - {/each} | ||
| 380 | - </div> | ||
| 381 | - </div> | ||
| 382 | - {/key} | ||
| 383 | - {/if} | ||
| 384 | - </div> | ||
| 385 | - </main> | ||
| 386 | - | ||
| 387 | - <!-- Sidebar derecha: En esta página --> | ||
| 388 | - <aside class="w-56 flex-shrink-0 hidden xl:block"> | ||
| 389 | - <div class="sticky top-0 h-screen overflow-y-auto p-4 border-l" style="border-color: var(--theme-borde);"> | ||
| 390 | - <p class="text-xs uppercase tracking-wide mb-3" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">En esta página</p> | ||
| 391 | - {#if selectedGrupo && !isSearching} | ||
| 392 | - <nav class="space-y-2"> | ||
| 393 | - {#each grupoContent as fuente} | ||
| 394 | - <a | ||
| 395 | - href="#f-{fuente.fuente}" | ||
| 396 | - class="block text-sm truncate transition-colors hover:text-[var(--theme-accent)]" | ||
| 397 | - style="color: var(--theme-texto);" | ||
| 398 | - title="{fuente.desc_fuente}" | ||
| 399 | - > | ||
| 400 | - <span class="font-mono text-xs" style="opacity: 0.6;">{fuente.fuente}</span> | ||
| 401 | - {fuente.desc_fuente} | ||
| 402 | - </a> | ||
| 403 | - {/each} | ||
| 404 | - </nav> | ||
| 405 | - {/if} | ||
| 406 | - </div> | ||
| 407 | - </aside> | ||
| 408 | - </div> | ||
| 409 | - {/if} | ||
| 410 | -</div> | ||
| 411 | - | ||
| 412 | -<!-- Modal de detalle --> | ||
| 413 | -{#if selectedItem} | ||
| 414 | - <div | ||
| 415 | - transition:fade={{ duration: 150 }} | ||
| 416 | - class="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50" | ||
| 417 | - onclick={closeDetail} | ||
| 418 | - > | ||
| 419 | - <div | ||
| 420 | - transition:scale={{ duration: 200, start: 0.95, easing: cubicOut }} | ||
| 421 | - class="rounded-xl shadow-2xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col" | ||
| 422 | - style="background-color: var(--theme-surface);" | ||
| 423 | - onclick={(e) => e.stopPropagation()} | ||
| 424 | - > | ||
| 425 | - <div class="px-6 py-4 border-b flex justify-between items-start" style="background-color: var(--theme-fill); border-color: var(--theme-borde);"> | ||
| 426 | - <div> | ||
| 427 | - <p class="text-xs uppercase tracking-wide font-medium" style="color: var(--theme-texto);">Fuente de Financiamiento</p> | ||
| 428 | - <h3 class="text-lg font-medium mt-1" style="color: var(--theme-titulo);"> | ||
| 429 | - <span class="font-mono" style="color: var(--theme-texto);">{selectedItem.fuente}</span> | ||
| 430 | - <span class="mx-2" style="color: var(--theme-texto); opacity: 0.5;">·</span> | ||
| 431 | - {selectedItem.desc_fuente} | ||
| 432 | - </h3> | ||
| 433 | - </div> | ||
| 434 | - <button | ||
| 435 | - onclick={closeDetail} | ||
| 436 | - class="text-2xl leading-none" | ||
| 437 | - style="color: var(--theme-texto);" | ||
| 438 | - > | ||
| 439 | - × | ||
| 440 | - </button> | ||
| 441 | - </div> | ||
| 442 | - | ||
| 443 | - <div class="p-6 overflow-y-auto flex-1"> | ||
| 444 | - <h4 class="text-sm font-medium mb-4" style="color: var(--theme-titulo);"> | ||
| 445 | - {#if selectedItem.n_variaciones > 1} | ||
| 446 | - Descripciones ({selectedItem.n_variaciones} variaciones) | ||
| 447 | - {:else} | ||
| 448 | - Descripción | ||
| 449 | - {/if} | ||
| 450 | - </h4> | ||
| 451 | - | ||
| 452 | - <div class="space-y-4"> | ||
| 453 | - {#each parseDescripciones(selectedItem.descripciones) as desc, i} | ||
| 454 | - <div class="border-l-2 pl-4 py-3 rounded-r" style="{i === 0 ? `border-color: var(--theme-accent); background-color: color-mix(in srgb, var(--theme-accent) 10%, transparent);` : `border-color: var(--theme-borde);`}"> | ||
| 455 | - <p class="text-sm mb-2" style="color: var(--theme-texto);"> | ||
| 456 | - {#if i === 0 && selectedItem.n_variaciones > 1} | ||
| 457 | - <span class="font-medium" style="color: var(--theme-accent);">Vigente</span> | ||
| 458 | - <span class="mx-1" style="opacity: 0.5;">·</span> | ||
| 459 | - {/if} | ||
| 460 | - <span class="font-mono">{desc.rangos}</span> | ||
| 461 | - </p> | ||
| 462 | - <p class="text-base leading-relaxed" style="color: var(--theme-titulo);">{desc.descripcion}</p> | ||
| 463 | - </div> | ||
| 464 | - {/each} | ||
| 465 | - </div> | ||
| 466 | - | ||
| 467 | - <!-- Vigencia temporal --> | ||
| 468 | - {#if selectedItem.gestiones} | ||
| 469 | - <div class="mt-6 pt-4 border-t" style="border-color: var(--theme-borde);"> | ||
| 470 | - <p class="text-xs font-mono" style="color: var(--theme-texto);"> | ||
| 471 | - <span class="font-medium">Años con datos:</span> {selectedItem.gestiones} | ||
| 472 | - </p> | ||
| 473 | - </div> | ||
| 474 | - {/if} | ||
| 475 | - </div> | ||
| 476 | - </div> | ||
| 477 | - </div> | ||
| 478 | -{/if} | ||
| 479 | - | ||
| 480 | -<style> | ||
| 481 | - /* Sidebar izquierdo: con fondo en móvil, transparente en desktop */ | ||
| 482 | - .sidebar-left { | ||
| 483 | - background-color: var(--theme-surface); | ||
| 484 | - } | ||
| 485 | - @media (min-width: 1024px) { | ||
| 486 | - .sidebar-left { | ||
| 487 | - background-color: transparent; | ||
| 488 | - } | ||
| 489 | - } | ||
| 490 | - | ||
| 491 | - /* Highlight pulse animation para resultados de búsqueda */ | ||
| 492 | - @keyframes highlight-pulse { | ||
| 493 | - 0%, 100% { | ||
| 494 | - box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme-accent) 30%, transparent); | ||
| 495 | - } | ||
| 496 | - 50% { | ||
| 497 | - box-shadow: 0 0 0 4px color-mix(in srgb, var(--theme-accent) 50%, transparent); | ||
| 498 | - } | ||
| 499 | - } | ||
| 500 | - | ||
| 501 | - .highlighted { | ||
| 502 | - animation: highlight-pulse 0.8s ease-in-out 2; | ||
| 503 | - border-radius: 0.5rem; | ||
| 504 | - padding: 0.5rem; | ||
| 505 | - margin-left: -0.5rem; | ||
| 506 | - } | ||
| 507 | - | ||
| 508 | - /* Layout principal - fallback nativo */ | ||
| 509 | - .clasificador-layout { | ||
| 510 | - display: flex !important; | ||
| 511 | - flex-direction: row !important; | ||
| 512 | - } | ||
| 513 | - | ||
| 514 | - /* Sidebar - fallback para responsive */ | ||
| 515 | - .sidebar-left { | ||
| 516 | - position: fixed; | ||
| 517 | - transform: translateX(-100%); | ||
| 518 | - } | ||
| 519 | - | ||
| 520 | - @media (min-width: 1024px) { | ||
| 521 | - .sidebar-left { | ||
| 522 | - position: relative !important; | ||
| 523 | - transform: translateX(0) !important; | ||
| 524 | - flex-shrink: 0; | ||
| 525 | - width: 16rem; | ||
| 526 | - z-index: auto !important; | ||
| 527 | - box-shadow: none !important; | ||
| 528 | - } | ||
| 529 | - } | ||
| 530 | - | ||
| 531 | - @media (max-width: 1023px) { | ||
| 532 | - .clasificador-layout { | ||
| 533 | - display: block !important; | ||
| 534 | - } | ||
| 535 | - } | ||
| 536 | -</style> |
| 1 | -import { supabase } from '$lib/supabase'; | ||
| 2 | -import { error } from '@sveltejs/kit'; | ||
| 3 | - | ||
| 4 | -export async function load({ params }) { | ||
| 5 | - console.time('[SERVER] Total load fuente'); | ||
| 6 | - const { codigo } = params; | ||
| 7 | - | ||
| 8 | - console.time('[SERVER] Query clas_fuentes'); | ||
| 9 | - const { data, error: dbError } = await supabase | ||
| 10 | - .schema('ppto') | ||
| 11 | - .from('clas_fuentes') | ||
| 12 | - .select('*') | ||
| 13 | - .eq('fuente', codigo); | ||
| 14 | - console.timeEnd('[SERVER] Query clas_fuentes'); | ||
| 15 | - | ||
| 16 | - if (dbError || !data || data.length === 0) { | ||
| 17 | - throw error(404, 'Fuente no encontrada'); | ||
| 18 | - } | ||
| 19 | - | ||
| 20 | - const fuente = data[0]; | ||
| 21 | - | ||
| 22 | - // Obtener hermanos (otras fuentes del mismo grupo) | ||
| 23 | - let hermanos = []; | ||
| 24 | - | ||
| 25 | - console.time('[SERVER] Query hermanos'); | ||
| 26 | - const { data: hermanosData } = await supabase | ||
| 27 | - .schema('ppto') | ||
| 28 | - .from('clas_fuentes') | ||
| 29 | - .select('*') | ||
| 30 | - .eq('grupo_fuente', fuente.grupo_fuente) | ||
| 31 | - .neq('fuente', codigo) | ||
| 32 | - .order('fuente'); | ||
| 33 | - | ||
| 34 | - if (hermanosData) { | ||
| 35 | - hermanos = hermanosData; | ||
| 36 | - } | ||
| 37 | - console.timeEnd('[SERVER] Query hermanos'); | ||
| 38 | - | ||
| 39 | - console.timeEnd('[SERVER] Total load fuente'); | ||
| 40 | - return { | ||
| 41 | - fuente, | ||
| 42 | - hermanos | ||
| 43 | - }; | ||
| 44 | -} |
| 1 | -<script> | ||
| 2 | - import { onMount } from 'svelte'; | ||
| 3 | - | ||
| 4 | - // Datos del servidor (reactivos) | ||
| 5 | - let { data } = $props(); | ||
| 6 | - let fuenteData = $derived(data.fuente); | ||
| 7 | - let hermanos = $derived(data.hermanos); | ||
| 8 | - | ||
| 9 | - // Estado | ||
| 10 | - let mounted = $state(false); | ||
| 11 | - | ||
| 12 | - onMount(() => { | ||
| 13 | - setTimeout(() => { mounted = true; }, 50); | ||
| 14 | - }); | ||
| 15 | - | ||
| 16 | - function parseDescripciones(descripcionesStr) { | ||
| 17 | - if (!descripcionesStr) return []; | ||
| 18 | - try { | ||
| 19 | - const parsed = typeof descripcionesStr === 'string' | ||
| 20 | - ? JSON.parse(descripcionesStr) | ||
| 21 | - : descripcionesStr; | ||
| 22 | - return parsed.sort((a, b) => { | ||
| 23 | - const maxYearA = getMaxYear(a.rangos); | ||
| 24 | - const maxYearB = getMaxYear(b.rangos); | ||
| 25 | - return maxYearB - maxYearA; | ||
| 26 | - }); | ||
| 27 | - } catch { | ||
| 28 | - return []; | ||
| 29 | - } | ||
| 30 | - } | ||
| 31 | - | ||
| 32 | - function getMaxYear(rangos) { | ||
| 33 | - if (!rangos) return 0; | ||
| 34 | - const years = rangos.match(/\d{4}/g); | ||
| 35 | - if (!years) return 0; | ||
| 36 | - return Math.max(...years.map(y => parseInt(y))); | ||
| 37 | - } | ||
| 38 | - | ||
| 39 | - let descripciones = $derived(parseDescripciones(fuenteData.descripciones)); | ||
| 40 | -</script> | ||
| 41 | - | ||
| 42 | -<svelte:head> | ||
| 43 | - <title>{fuenteData.desc_fuente || fuenteData.fuente} | Clasificador de Fuentes</title> | ||
| 44 | - <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| 45 | - <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@400;500&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" /> | ||
| 46 | -</svelte:head> | ||
| 47 | - | ||
| 48 | -<div class="page" class:mounted> | ||
| 49 | - <!-- Link a clasificadores --> | ||
| 50 | - <nav class="page-nav"> | ||
| 51 | - <a href="/clasificadores/fuentes" class="back-link"> | ||
| 52 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 53 | - <path d="M19 12H5M12 19l-7-7 7-7"/> | ||
| 54 | - </svg> | ||
| 55 | - Clasificador | ||
| 56 | - </a> | ||
| 57 | - </nav> | ||
| 58 | - | ||
| 59 | - <div class="dashboard-layout"> | ||
| 60 | - <!-- CONTEXTO IZQUIERDO --> | ||
| 61 | - <aside class="dashboard-context"> | ||
| 62 | - <div class="context-card"> | ||
| 63 | - <h2 class="context-main-title">Contexto</h2> | ||
| 64 | - | ||
| 65 | - <!-- Info del grupo --> | ||
| 66 | - <div class="context-section"> | ||
| 67 | - <h3 class="context-subtitle">Ubicación</h3> | ||
| 68 | - <div class="ubicacion-info"> | ||
| 69 | - <div class="ubicacion-item"> | ||
| 70 | - <span class="ubicacion-label">Grupo {fuenteData.grupo_fuente}</span> | ||
| 71 | - <span class="ubicacion-value">{fuenteData.desc_grupo_fuente}</span> | ||
| 72 | - </div> | ||
| 73 | - </div> | ||
| 74 | - </div> | ||
| 75 | - | ||
| 76 | - <!-- Hermanos --> | ||
| 77 | - {#if hermanos.length > 0} | ||
| 78 | - <div class="context-separator"></div> | ||
| 79 | - <div class="context-section"> | ||
| 80 | - <h3 class="context-subtitle">Mismo grupo ({hermanos.length})</h3> | ||
| 81 | - <p class="context-hint">Otras fuentes del mismo grupo.</p> | ||
| 82 | - <div class="hermanos-list"> | ||
| 83 | - {#each hermanos.slice(0, 10) as hermano} | ||
| 84 | - <a href="/fuente/{hermano.fuente}" class="hermano-link"> | ||
| 85 | - <span class="hermano-code">{hermano.fuente}</span> | ||
| 86 | - <span class="hermano-name">{hermano.desc_fuente}</span> | ||
| 87 | - </a> | ||
| 88 | - {/each} | ||
| 89 | - {#if hermanos.length > 10} | ||
| 90 | - <span class="hermanos-more">+{hermanos.length - 10} más</span> | ||
| 91 | - {/if} | ||
| 92 | - </div> | ||
| 93 | - </div> | ||
| 94 | - {/if} | ||
| 95 | - </div> | ||
| 96 | - </aside> | ||
| 97 | - | ||
| 98 | - <!-- CONTENIDO PRINCIPAL --> | ||
| 99 | - <main class="dashboard-main"> | ||
| 100 | - <div class="main-content"> | ||
| 101 | - <!-- Header --> | ||
| 102 | - <div class="main-header"> | ||
| 103 | - <div class="main-title-pills"> | ||
| 104 | - <span class="pill pill-codigo">{fuenteData.fuente}</span> | ||
| 105 | - </div> | ||
| 106 | - <h1 class="main-title-name">{fuenteData.desc_fuente}</h1> | ||
| 107 | - </div> | ||
| 108 | - | ||
| 109 | - <!-- Info cards --> | ||
| 110 | - <div class="info-grid"> | ||
| 111 | - <div class="info-card"> | ||
| 112 | - <span class="info-label">Grupo</span> | ||
| 113 | - <span class="info-value">{fuenteData.grupo_fuente}</span> | ||
| 114 | - <span class="info-desc">{fuenteData.desc_grupo_fuente}</span> | ||
| 115 | - </div> | ||
| 116 | - | ||
| 117 | - {#if fuenteData.gestiones} | ||
| 118 | - <div class="info-card"> | ||
| 119 | - <span class="info-label">Gestiones</span> | ||
| 120 | - <span class="info-value">{fuenteData.n_gestiones || ''} años</span> | ||
| 121 | - <span class="info-desc">{fuenteData.gestiones}</span> | ||
| 122 | - </div> | ||
| 123 | - {/if} | ||
| 124 | - | ||
| 125 | - {#if fuenteData.n_variaciones > 1} | ||
| 126 | - <div class="info-card"> | ||
| 127 | - <span class="info-label">Variaciones</span> | ||
| 128 | - <span class="info-value">{fuenteData.n_variaciones}</span> | ||
| 129 | - <span class="info-desc">Cambios en la descripción a lo largo del tiempo</span> | ||
| 130 | - </div> | ||
| 131 | - {/if} | ||
| 132 | - </div> | ||
| 133 | - | ||
| 134 | - <!-- Descripciones --> | ||
| 135 | - {#if descripciones.length > 0} | ||
| 136 | - <div class="descripciones-section"> | ||
| 137 | - <h2 class="descripciones-title"> | ||
| 138 | - {#if descripciones.length > 1} | ||
| 139 | - Descripciones ({descripciones.length} variaciones) | ||
| 140 | - {:else} | ||
| 141 | - Descripción | ||
| 142 | - {/if} | ||
| 143 | - </h2> | ||
| 144 | - <div class="descripciones-list"> | ||
| 145 | - {#each descripciones as desc, i} | ||
| 146 | - <div class="descripcion-item" class:vigente={i === 0}> | ||
| 147 | - <p class="descripcion-rangos"> | ||
| 148 | - {#if i === 0 && descripciones.length > 1} | ||
| 149 | - <span class="vigente-badge">Vigente</span> | ||
| 150 | - {/if} | ||
| 151 | - {desc.rangos} | ||
| 152 | - </p> | ||
| 153 | - <p class="descripcion-texto">{desc.descripcion}</p> | ||
| 154 | - </div> | ||
| 155 | - {/each} | ||
| 156 | - </div> | ||
| 157 | - </div> | ||
| 158 | - {/if} | ||
| 159 | - </div> | ||
| 160 | - </main> | ||
| 161 | - </div> | ||
| 162 | -</div> | ||
| 163 | - | ||
| 164 | -<style> | ||
| 165 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 166 | - BASE | ||
| 167 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 168 | - .page { | ||
| 169 | - min-height: 100vh; | ||
| 170 | - padding: 1rem; | ||
| 171 | - opacity: 0; | ||
| 172 | - transform: translateY(10px); | ||
| 173 | - transition: opacity 0.4s ease, transform 0.4s ease; | ||
| 174 | - } | ||
| 175 | - | ||
| 176 | - .page.mounted { | ||
| 177 | - opacity: 1; | ||
| 178 | - transform: translateY(0); | ||
| 179 | - } | ||
| 180 | - | ||
| 181 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 182 | - NAV | ||
| 183 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 184 | - .page-nav { | ||
| 185 | - max-width: 1400px; | ||
| 186 | - margin: 0 auto 1.5rem; | ||
| 187 | - } | ||
| 188 | - | ||
| 189 | - .back-link { | ||
| 190 | - display: inline-flex; | ||
| 191 | - align-items: center; | ||
| 192 | - gap: 0.5rem; | ||
| 193 | - color: var(--text-secondary, #888); | ||
| 194 | - text-decoration: none; | ||
| 195 | - font-size: 0.875rem; | ||
| 196 | - transition: color 0.2s; | ||
| 197 | - } | ||
| 198 | - | ||
| 199 | - .back-link:hover { | ||
| 200 | - color: var(--text-primary, #fff); | ||
| 201 | - } | ||
| 202 | - | ||
| 203 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 204 | - LAYOUT | ||
| 205 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 206 | - .dashboard-layout { | ||
| 207 | - display: grid; | ||
| 208 | - grid-template-columns: 300px 1fr; | ||
| 209 | - gap: 2rem; | ||
| 210 | - max-width: 1400px; | ||
| 211 | - margin: 0 auto; | ||
| 212 | - } | ||
| 213 | - | ||
| 214 | - @media (max-width: 900px) { | ||
| 215 | - .dashboard-layout { | ||
| 216 | - grid-template-columns: 1fr; | ||
| 217 | - } | ||
| 218 | - | ||
| 219 | - .dashboard-context { | ||
| 220 | - order: 2; | ||
| 221 | - } | ||
| 222 | - } | ||
| 223 | - | ||
| 224 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 225 | - SIDEBAR | ||
| 226 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 227 | - .dashboard-context { | ||
| 228 | - position: sticky; | ||
| 229 | - top: 5rem; | ||
| 230 | - height: fit-content; | ||
| 231 | - } | ||
| 232 | - | ||
| 233 | - .context-card { | ||
| 234 | - background: var(--card-bg, rgba(255, 255, 255, 0.03)); | ||
| 235 | - border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08)); | ||
| 236 | - border-radius: 16px; | ||
| 237 | - padding: 1.5rem; | ||
| 238 | - } | ||
| 239 | - | ||
| 240 | - .context-main-title { | ||
| 241 | - font-family: 'Instrument Sans', sans-serif; | ||
| 242 | - font-size: 0.75rem; | ||
| 243 | - font-weight: 600; | ||
| 244 | - text-transform: uppercase; | ||
| 245 | - letter-spacing: 0.05em; | ||
| 246 | - color: var(--text-tertiary, #666); | ||
| 247 | - margin-bottom: 1.5rem; | ||
| 248 | - } | ||
| 249 | - | ||
| 250 | - .context-section { | ||
| 251 | - margin-bottom: 1.5rem; | ||
| 252 | - } | ||
| 253 | - | ||
| 254 | - .context-subtitle { | ||
| 255 | - font-family: 'Instrument Sans', sans-serif; | ||
| 256 | - font-size: 0.8rem; | ||
| 257 | - font-weight: 500; | ||
| 258 | - color: var(--text-secondary, #aaa); | ||
| 259 | - margin-bottom: 0.75rem; | ||
| 260 | - } | ||
| 261 | - | ||
| 262 | - .context-hint { | ||
| 263 | - font-size: 0.75rem; | ||
| 264 | - color: var(--text-tertiary, #666); | ||
| 265 | - margin-bottom: 1rem; | ||
| 266 | - } | ||
| 267 | - | ||
| 268 | - .context-separator { | ||
| 269 | - height: 1px; | ||
| 270 | - background: var(--border-color, rgba(255, 255, 255, 0.08)); | ||
| 271 | - margin: 1.5rem 0; | ||
| 272 | - } | ||
| 273 | - | ||
| 274 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 275 | - UBICACION INFO | ||
| 276 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 277 | - .ubicacion-info { | ||
| 278 | - display: flex; | ||
| 279 | - flex-direction: column; | ||
| 280 | - gap: 0.75rem; | ||
| 281 | - } | ||
| 282 | - | ||
| 283 | - .ubicacion-item { | ||
| 284 | - display: flex; | ||
| 285 | - flex-direction: column; | ||
| 286 | - gap: 0.25rem; | ||
| 287 | - padding: 0.75rem; | ||
| 288 | - background: var(--hover-bg, rgba(255, 255, 255, 0.03)); | ||
| 289 | - border-radius: 8px; | ||
| 290 | - } | ||
| 291 | - | ||
| 292 | - .ubicacion-label { | ||
| 293 | - font-family: 'DM Mono', monospace; | ||
| 294 | - font-size: 0.7rem; | ||
| 295 | - color: var(--text-tertiary, #666); | ||
| 296 | - text-transform: uppercase; | ||
| 297 | - } | ||
| 298 | - | ||
| 299 | - .ubicacion-value { | ||
| 300 | - font-size: 0.85rem; | ||
| 301 | - color: var(--text-primary, #fff); | ||
| 302 | - } | ||
| 303 | - | ||
| 304 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 305 | - HERMANOS LIST | ||
| 306 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 307 | - .hermanos-list { | ||
| 308 | - display: flex; | ||
| 309 | - flex-direction: column; | ||
| 310 | - gap: 0.25rem; | ||
| 311 | - } | ||
| 312 | - | ||
| 313 | - .hermano-link { | ||
| 314 | - display: flex; | ||
| 315 | - align-items: center; | ||
| 316 | - gap: 0.5rem; | ||
| 317 | - padding: 0.5rem 0.75rem; | ||
| 318 | - border-radius: 6px; | ||
| 319 | - text-decoration: none; | ||
| 320 | - color: var(--text-secondary, #aaa); | ||
| 321 | - font-size: 0.8rem; | ||
| 322 | - transition: all 0.2s ease; | ||
| 323 | - } | ||
| 324 | - | ||
| 325 | - .hermano-link:hover { | ||
| 326 | - background: var(--hover-bg, rgba(255, 255, 255, 0.05)); | ||
| 327 | - color: var(--text-primary, #fff); | ||
| 328 | - } | ||
| 329 | - | ||
| 330 | - .hermano-code { | ||
| 331 | - font-family: 'DM Mono', monospace; | ||
| 332 | - font-size: 0.7rem; | ||
| 333 | - opacity: 0.6; | ||
| 334 | - flex-shrink: 0; | ||
| 335 | - min-width: 2.5rem; | ||
| 336 | - } | ||
| 337 | - | ||
| 338 | - .hermano-name { | ||
| 339 | - white-space: nowrap; | ||
| 340 | - overflow: hidden; | ||
| 341 | - text-overflow: ellipsis; | ||
| 342 | - } | ||
| 343 | - | ||
| 344 | - .hermanos-more { | ||
| 345 | - font-size: 0.75rem; | ||
| 346 | - color: var(--text-tertiary, #666); | ||
| 347 | - padding: 0.5rem 0.75rem; | ||
| 348 | - } | ||
| 349 | - | ||
| 350 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 351 | - MAIN CONTENT | ||
| 352 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 353 | - .dashboard-main { | ||
| 354 | - min-width: 0; | ||
| 355 | - } | ||
| 356 | - | ||
| 357 | - .main-content { | ||
| 358 | - background: var(--card-bg, rgba(255, 255, 255, 0.03)); | ||
| 359 | - border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08)); | ||
| 360 | - border-radius: 16px; | ||
| 361 | - padding: 2rem; | ||
| 362 | - } | ||
| 363 | - | ||
| 364 | - .main-header { | ||
| 365 | - margin-bottom: 2rem; | ||
| 366 | - } | ||
| 367 | - | ||
| 368 | - .main-title-pills { | ||
| 369 | - display: flex; | ||
| 370 | - gap: 0.5rem; | ||
| 371 | - margin-bottom: 0.75rem; | ||
| 372 | - } | ||
| 373 | - | ||
| 374 | - .pill { | ||
| 375 | - padding: 0.25rem 0.75rem; | ||
| 376 | - border-radius: 999px; | ||
| 377 | - font-size: 0.75rem; | ||
| 378 | - font-weight: 500; | ||
| 379 | - } | ||
| 380 | - | ||
| 381 | - .pill-codigo { | ||
| 382 | - background: var(--hover-bg, rgba(255, 255, 255, 0.05)); | ||
| 383 | - color: var(--text-secondary, #aaa); | ||
| 384 | - font-family: 'DM Mono', monospace; | ||
| 385 | - } | ||
| 386 | - | ||
| 387 | - .main-title-name { | ||
| 388 | - font-family: 'DM Serif Display', serif; | ||
| 389 | - font-size: 2rem; | ||
| 390 | - font-weight: 400; | ||
| 391 | - color: var(--text-primary, #fff); | ||
| 392 | - line-height: 1.2; | ||
| 393 | - margin: 0; | ||
| 394 | - } | ||
| 395 | - | ||
| 396 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 397 | - INFO GRID | ||
| 398 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 399 | - .info-grid { | ||
| 400 | - display: grid; | ||
| 401 | - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | ||
| 402 | - gap: 1rem; | ||
| 403 | - margin-bottom: 2rem; | ||
| 404 | - } | ||
| 405 | - | ||
| 406 | - .info-card { | ||
| 407 | - display: flex; | ||
| 408 | - flex-direction: column; | ||
| 409 | - gap: 0.25rem; | ||
| 410 | - padding: 1rem; | ||
| 411 | - background: var(--hover-bg, rgba(255, 255, 255, 0.03)); | ||
| 412 | - border-radius: 12px; | ||
| 413 | - border: 1px solid var(--border-color, rgba(255, 255, 255, 0.05)); | ||
| 414 | - } | ||
| 415 | - | ||
| 416 | - .info-label { | ||
| 417 | - font-size: 0.75rem; | ||
| 418 | - text-transform: uppercase; | ||
| 419 | - letter-spacing: 0.05em; | ||
| 420 | - color: var(--text-tertiary, #666); | ||
| 421 | - } | ||
| 422 | - | ||
| 423 | - .info-value { | ||
| 424 | - font-family: 'DM Mono', monospace; | ||
| 425 | - font-size: 1.25rem; | ||
| 426 | - color: var(--text-primary, #fff); | ||
| 427 | - } | ||
| 428 | - | ||
| 429 | - .info-desc { | ||
| 430 | - font-size: 0.8rem; | ||
| 431 | - color: var(--text-secondary, #aaa); | ||
| 432 | - line-height: 1.4; | ||
| 433 | - } | ||
| 434 | - | ||
| 435 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 436 | - DESCRIPCIONES | ||
| 437 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 438 | - .descripciones-section { | ||
| 439 | - border-top: 1px solid var(--border-color, rgba(255, 255, 255, 0.08)); | ||
| 440 | - padding-top: 2rem; | ||
| 441 | - } | ||
| 442 | - | ||
| 443 | - .descripciones-title { | ||
| 444 | - font-family: 'Instrument Sans', sans-serif; | ||
| 445 | - font-size: 0.875rem; | ||
| 446 | - font-weight: 600; | ||
| 447 | - color: var(--text-secondary, #aaa); | ||
| 448 | - margin-bottom: 1rem; | ||
| 449 | - } | ||
| 450 | - | ||
| 451 | - .descripciones-list { | ||
| 452 | - display: flex; | ||
| 453 | - flex-direction: column; | ||
| 454 | - gap: 1rem; | ||
| 455 | - } | ||
| 456 | - | ||
| 457 | - .descripcion-item { | ||
| 458 | - padding: 1rem; | ||
| 459 | - border-left: 2px solid var(--border-color, rgba(255, 255, 255, 0.1)); | ||
| 460 | - border-radius: 0 8px 8px 0; | ||
| 461 | - background: var(--hover-bg, rgba(255, 255, 255, 0.02)); | ||
| 462 | - } | ||
| 463 | - | ||
| 464 | - .descripcion-item.vigente { | ||
| 465 | - border-left-color: var(--theme-accent, #3b82f6); | ||
| 466 | - background: color-mix(in srgb, var(--theme-accent, #3b82f6) 10%, transparent); | ||
| 467 | - } | ||
| 468 | - | ||
| 469 | - .descripcion-rangos { | ||
| 470 | - font-family: 'DM Mono', monospace; | ||
| 471 | - font-size: 0.75rem; | ||
| 472 | - color: var(--text-tertiary, #666); | ||
| 473 | - margin-bottom: 0.5rem; | ||
| 474 | - } | ||
| 475 | - | ||
| 476 | - .vigente-badge { | ||
| 477 | - display: inline-block; | ||
| 478 | - padding: 0.125rem 0.5rem; | ||
| 479 | - border-radius: 999px; | ||
| 480 | - background: var(--theme-accent, #3b82f6); | ||
| 481 | - color: white; | ||
| 482 | - font-size: 0.65rem; | ||
| 483 | - font-weight: 600; | ||
| 484 | - text-transform: uppercase; | ||
| 485 | - letter-spacing: 0.05em; | ||
| 486 | - margin-right: 0.5rem; | ||
| 487 | - } | ||
| 488 | - | ||
| 489 | - .descripcion-texto { | ||
| 490 | - font-size: 0.9rem; | ||
| 491 | - color: var(--text-primary, #fff); | ||
| 492 | - line-height: 1.6; | ||
| 493 | - } | ||
| 494 | -</style> |
| 1 | -<script> | ||
| 2 | - import { onMount } from 'svelte'; | ||
| 3 | - import { goto } from '$app/navigation'; | ||
| 4 | - import { supabase } from '$lib/supabase'; | ||
| 5 | - import { | ||
| 6 | - query, | ||
| 7 | - results, | ||
| 8 | - isLoading, | ||
| 9 | - indexLoaded, | ||
| 10 | - error, | ||
| 11 | - selectedIndex, | ||
| 12 | - showResults, | ||
| 13 | - initIndex, | ||
| 14 | - performSearch, | ||
| 15 | - clearSearch, | ||
| 16 | - navigateResults | ||
| 17 | - } from '$lib/stores/searchStore'; | ||
| 18 | - | ||
| 19 | - // Pre-warm Supabase connection (evita cold start al navegar) | ||
| 20 | - async function warmupSupabase() { | ||
| 21 | - console.time('[WARMUP] Supabase'); | ||
| 22 | - await supabase.schema('ppto').from('vista_objeto_estado').select('objeto').limit(1); | ||
| 23 | - console.timeEnd('[WARMUP] Supabase'); | ||
| 24 | - } | ||
| 25 | - | ||
| 26 | - let mounted = false; | ||
| 27 | - let mode = 'archivo'; | ||
| 28 | - let searchFocused = false; | ||
| 29 | - let searchVal = ''; | ||
| 30 | - let hDiario = false; | ||
| 31 | - let hArchivo = false; | ||
| 32 | - let hVis = null; | ||
| 33 | - let hHist = null; | ||
| 34 | - let hWidget = null; | ||
| 35 | - let hGear = false; | ||
| 36 | - let mobileSettingsOpen = false; | ||
| 37 | - let gearMenuOpen = false; | ||
| 38 | - let searchInput; | ||
| 39 | - let debounceTimer; | ||
| 40 | - let autoFocused = false; | ||
| 41 | - | ||
| 42 | - // Search filters - which types to include | ||
| 43 | - let searchFilters = { | ||
| 44 | - entidad: true, | ||
| 45 | - objeto_gasto: true, | ||
| 46 | - rubro: true | ||
| 47 | - }; | ||
| 48 | - | ||
| 49 | - // Filtered results based on selected filters | ||
| 50 | - $: filteredResults = $results.filter(item => searchFilters[item.tipo]); | ||
| 51 | - | ||
| 52 | - function toggleFilter(tipo) { | ||
| 53 | - searchFilters[tipo] = !searchFilters[tipo]; | ||
| 54 | - searchFilters = searchFilters; // trigger reactivity | ||
| 55 | - } | ||
| 56 | - | ||
| 57 | - function toggleGearMenu() { | ||
| 58 | - gearMenuOpen = !gearMenuOpen; | ||
| 59 | - } | ||
| 60 | - | ||
| 61 | - onMount(() => { | ||
| 62 | - setTimeout(() => { mounted = true; }, 80); | ||
| 63 | - initIndex(); | ||
| 64 | - // Pre-calentar Supabase en background para evitar cold start | ||
| 65 | - warmupSupabase(); | ||
| 66 | - | ||
| 67 | - // Cerrar gear menu al hacer click fuera | ||
| 68 | - const handleClickOutside = (e) => { | ||
| 69 | - if (gearMenuOpen && !e.target.closest('.gear-wrap')) { | ||
| 70 | - gearMenuOpen = false; | ||
| 71 | - } | ||
| 72 | - }; | ||
| 73 | - document.addEventListener('click', handleClickOutside); | ||
| 74 | - return () => document.removeEventListener('click', handleClickOutside); | ||
| 75 | - }); | ||
| 76 | - | ||
| 77 | - // Auto-focus en el buscador cuando el índice esté listo (solo desktop) | ||
| 78 | - $: if ($indexLoaded && searchInput && !autoFocused) { | ||
| 79 | - autoFocused = true; | ||
| 80 | - // No auto-focus en móvil para evitar que aparezca el teclado | ||
| 81 | - const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; | ||
| 82 | - if (!isMobile) { | ||
| 83 | - setTimeout(() => { | ||
| 84 | - searchInput?.focus(); | ||
| 85 | - }, 50); | ||
| 86 | - } | ||
| 87 | - } | ||
| 88 | - | ||
| 89 | - function toggleMobileSettings() { | ||
| 90 | - mobileSettingsOpen = !mobileSettingsOpen; | ||
| 91 | - } | ||
| 92 | - | ||
| 93 | - $: isD = mode === 'diario'; | ||
| 94 | - $: accent = isD ? '#4A8B6E' : '#C9A751'; | ||
| 95 | - $: placeholder = isD | ||
| 96 | - ? 'Busca una entidad, un ministerio, un municipio...' | ||
| 97 | - : 'Busca instituciones, gastos, ingresos, sectores...'; | ||
| 98 | - $: examples = isD | ||
| 99 | - ? [ | ||
| 100 | - { label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' }, | ||
| 101 | - { label: 'Min. Educación', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' }, | ||
| 102 | - { label: 'GOB. La Paz', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' }, | ||
| 103 | - ] | ||
| 104 | - : [ | ||
| 105 | - { label: 'Sueldos', bg: 'rgba(201,167,81,0.12)', color: '#d4b962', href: '/objeto/11700' }, | ||
| 106 | - { label: 'Regalías', bg: 'rgba(160,150,170,0.09)', color: '#aaa0b0', href: '/rubro/14000' }, | ||
| 107 | - { label: 'BOA', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' }, | ||
| 108 | - { label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' }, | ||
| 109 | - { label: 'Publicidad', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' }, | ||
| 110 | - ]; | ||
| 111 | - | ||
| 112 | - function switchMode(m) { | ||
| 113 | - mode = m; | ||
| 114 | - searchVal = ''; | ||
| 115 | - clearSearch(); | ||
| 116 | - if (searchInput) searchInput.value = ''; | ||
| 117 | - } | ||
| 118 | - | ||
| 119 | - function handleBlur() { | ||
| 120 | - setTimeout(() => { searchFocused = false; }, 200); | ||
| 121 | - } | ||
| 122 | - | ||
| 123 | - function handleInput(e) { | ||
| 124 | - const value = e.target.value; | ||
| 125 | - searchVal = value; | ||
| 126 | - clearTimeout(debounceTimer); | ||
| 127 | - debounceTimer = setTimeout(() => { | ||
| 128 | - performSearch(value); | ||
| 129 | - }, 250); | ||
| 130 | - } | ||
| 131 | - | ||
| 132 | - function handleKeydown(e) { | ||
| 133 | - if (!$showResults) return; | ||
| 134 | - switch (e.key) { | ||
| 135 | - case 'ArrowDown': | ||
| 136 | - e.preventDefault(); | ||
| 137 | - navigateResults('down', filteredResults.length); | ||
| 138 | - break; | ||
| 139 | - case 'ArrowUp': | ||
| 140 | - e.preventDefault(); | ||
| 141 | - navigateResults('up', filteredResults.length); | ||
| 142 | - break; | ||
| 143 | - case 'Enter': | ||
| 144 | - e.preventDefault(); | ||
| 145 | - // Si hay un resultado seleccionado, navegar a él | ||
| 146 | - if ($selectedIndex >= 0 && filteredResults[$selectedIndex]) { | ||
| 147 | - handleSelect(filteredResults[$selectedIndex]); | ||
| 148 | - } else if (filteredResults.length > 0) { | ||
| 149 | - // Si no hay selección pero hay resultados, ir al primero | ||
| 150 | - handleSelect(filteredResults[0]); | ||
| 151 | - } | ||
| 152 | - break; | ||
| 153 | - case 'Escape': | ||
| 154 | - clearSearch(); | ||
| 155 | - searchVal = ''; | ||
| 156 | - if (searchInput) searchInput.value = ''; | ||
| 157 | - searchInput?.blur(); | ||
| 158 | - break; | ||
| 159 | - } | ||
| 160 | - } | ||
| 161 | - | ||
| 162 | - function handleSelect(item) { | ||
| 163 | - if (item.tipo === 'entidad') { | ||
| 164 | - goto(`/entidad/${item.codigo}`); | ||
| 165 | - } else if (item.tipo === 'objeto_gasto') { | ||
| 166 | - goto(`/objeto/${item.codigo}`); | ||
| 167 | - } else if (item.tipo === 'rubro') { | ||
| 168 | - goto(`/rubro/${item.codigo}`); | ||
| 169 | - } | ||
| 170 | - clearSearch(); | ||
| 171 | - searchVal = ''; | ||
| 172 | - if (searchInput) searchInput.value = ''; | ||
| 173 | - } | ||
| 174 | - | ||
| 175 | - function getTypeLabel(tipo) { | ||
| 176 | - switch (tipo) { | ||
| 177 | - case 'entidad': return 'ENTIDAD'; | ||
| 178 | - case 'objeto_gasto': return 'OBJETO'; | ||
| 179 | - case 'rubro': return 'RUBRO'; | ||
| 180 | - default: return tipo?.toUpperCase() || ''; | ||
| 181 | - } | ||
| 182 | - } | ||
| 183 | - | ||
| 184 | - function getTypeColor(tipo) { | ||
| 185 | - switch (tipo) { | ||
| 186 | - case 'entidad': return '#4A8B6E'; | ||
| 187 | - case 'objeto_gasto': return '#C9A751'; | ||
| 188 | - case 'rubro': return '#8B6E9B'; | ||
| 189 | - default: return '#6B6860'; | ||
| 190 | - } | ||
| 191 | - } | ||
| 192 | - | ||
| 193 | - const WIDGETS_INGRESOS = [ | ||
| 194 | - { id: 'rubros', label: 'tipos de ingreso', d: 'Impuestos, regalías, ventas, créditos', t: 'Recursos por rubros', n: '324', href: '/clasificadores/rubros' }, | ||
| 195 | - { id: 'fuentes', label: 'fuentes de financiamiento', d: 'TGN, crédito externo, donaciones', t: 'Fuentes', n: '22', href: '/clasificadores/fuente' }, | ||
| 196 | - { id: 'organismos', label: 'organismos financiadores', d: 'BID, Banco Mundial, cooperación', t: 'Organismos', n: '228', href: '/clasificadores/organismos' }, | ||
| 197 | - ]; | ||
| 198 | - | ||
| 199 | - const WIDGETS_GASTOS_QUE = [ | ||
| 200 | - { id: 'objeto', label: 'objetos de gasto', d: 'Sueldos, consultorías, materiales', t: 'Objeto del gasto', n: '324', href: '/clasificadores/objeto-gasto' }, | ||
| 201 | - { id: 'sector', label: 'sectores económicos', d: 'Industria, construcción, servicios', t: 'Sectores', n: '488', href: '/clasificadores/sectores' }, | ||
| 202 | - ]; | ||
| 203 | - | ||
| 204 | - const WIDGETS_GASTOS_BOTTOM = [ | ||
| 205 | - { id: 'finalidad', label: 'finalidades y funciones', d: 'Educación, salud, defensa', t: 'Finalidad', n: '132', href: '/clasificadores/funcional' }, | ||
| 206 | - { id: 'donde', label: 'ubicaciones geográficas', d: 'Departamentos, municipios', t: 'Geográfico', n: '352', href: '/clasificadores/geografico' }, | ||
| 207 | - ]; | ||
| 208 | - | ||
| 209 | - const HISTORIAS = [ | ||
| 210 | - { color: '#C9A751', title: '¿Cuánto gasta Bolivia en consultorías cada año?', desc: 'Un análisis de 20 años revela patrones que todo ciudadano debería conocer. La evolución del gasto en servicios profesionales muestra tendencias inesperadas.', tag: 'INVESTIGACIÓN', tall: true }, | ||
| 211 | - { color: '#C44B3F', title: 'Salud vs. Defensa: 20 años', desc: 'Cómo se redistribuyó el presupuesto entre sectores.', tag: 'COMPARATIVA', tall: false }, | ||
| 212 | - { color: '#4A8B6E', title: 'El boom del gasto en infraestructura', desc: 'De 2006 a 2014 Bolivia multiplicó por 8 su inversión pública. Un recorrido por las grandes obras y su impacto fiscal.', tag: 'CRÓNICA', tall: true }, | ||
| 213 | - { color: '#6B8CA6', title: '¿Quién financia a Bolivia?', desc: 'Un mapa de los organismos internacionales que prestan al país.', tag: 'ANÁLISIS', tall: false }, | ||
| 214 | - ]; | ||
| 215 | - | ||
| 216 | - const VISUALIZACIONES = [ | ||
| 217 | - { color: '#4A8B6E', title: 'Evolución del gasto en salud', desc: 'Cómo cambiaron las prioridades en salud a lo largo de dos décadas.', type: 'Serie temporal', href: null }, | ||
| 218 | - { color: '#C9A751', title: '¿En qué se gasta?', desc: 'Explora el presupuesto por objeto de gasto.', type: 'Treemap', href: '/clasificadores/objeto-gasto?modo=mapa' }, | ||
| 219 | - { color: '#8B6E9B', title: 'Mapa de inversión por departamento', desc: 'Distribución geográfica de la inversión pública.', type: 'Mapa', href: null }, | ||
| 220 | - ]; | ||
| 221 | - | ||
| 222 | - /* Reveal action */ | ||
| 223 | - let revealStates = {}; | ||
| 224 | - function reveal(node, { id, delay = 0 }) { | ||
| 225 | - revealStates[id] = false; | ||
| 226 | - revealStates = revealStates; | ||
| 227 | - const obs = new IntersectionObserver(([e]) => { | ||
| 228 | - if (e.isIntersecting) { | ||
| 229 | - setTimeout(() => { revealStates[id] = true; revealStates = revealStates; }, delay); | ||
| 230 | - obs.disconnect(); | ||
| 231 | - } | ||
| 232 | - }, { threshold: 0.08 }); | ||
| 233 | - obs.observe(node); | ||
| 234 | - return { destroy() { obs.disconnect(); } }; | ||
| 235 | - } | ||
| 236 | - | ||
| 237 | - /* Section navigation */ | ||
| 238 | - const SECTIONS_NAV = [ | ||
| 239 | - { id: 'hero', label: 'Inicio' }, | ||
| 240 | - { id: 'datos', label: 'Datos' }, | ||
| 241 | - { id: 'clasificadores', label: 'Clasificadores' }, | ||
| 242 | - { id: 'historias', label: 'Historias' }, | ||
| 243 | - { id: 'visualizaciones', label: 'Visualizaciones' }, | ||
| 244 | - { id: 'manifiesto', label: 'Manifiesto' }, | ||
| 245 | - { id: 'descargas', label: 'Descargas' }, | ||
| 246 | - ]; | ||
| 247 | - let currentSection = 'hero'; | ||
| 248 | - | ||
| 249 | - onMount(() => { | ||
| 250 | - const sections = document.querySelectorAll('[data-section]'); | ||
| 251 | - const observer = new IntersectionObserver((entries) => { | ||
| 252 | - entries.forEach(entry => { | ||
| 253 | - if (entry.isIntersecting) { | ||
| 254 | - currentSection = entry.target.dataset.section; | ||
| 255 | - } | ||
| 256 | - }); | ||
| 257 | - }, { threshold: 0.5 }); | ||
| 258 | - sections.forEach(section => observer.observe(section)); | ||
| 259 | - return () => observer.disconnect(); | ||
| 260 | - }); | ||
| 261 | - | ||
| 262 | - function scrollToSection(id) { | ||
| 263 | - const el = document.querySelector(`[data-section="${id}"]`); | ||
| 264 | - if (el) el.scrollIntoView({ behavior: 'smooth' }); | ||
| 265 | - } | ||
| 266 | -</script> | ||
| 267 | - | ||
| 268 | -<div class="page" class:mounted> | ||
| 269 | - | ||
| 270 | - <!-- Section Navigation Dots --> | ||
| 271 | - <nav class="section-dots"> | ||
| 272 | - {#each SECTIONS_NAV as s} | ||
| 273 | - <button | ||
| 274 | - class="section-dot" | ||
| 275 | - class:section-dot-active={currentSection === s.id} | ||
| 276 | - on:click={() => scrollToSection(s.id)} | ||
| 277 | - aria-label={s.label} | ||
| 278 | - > | ||
| 279 | - <span class="section-dot-label">{s.label}</span> | ||
| 280 | - </button> | ||
| 281 | - {/each} | ||
| 282 | - </nav> | ||
| 283 | - | ||
| 284 | - <!-- ═══ LANDING SCREEN (NAV + HERO) ═══ --> | ||
| 285 | - <section class="landing-screen" data-section="hero"> | ||
| 286 | - <nav> | ||
| 287 | - <div class="nav-logo"><img src="/logos_oscuro/05_Imago MEFP horizontal espacio negativo.png" alt="MEFP" /></div> | ||
| 288 | - </nav> | ||
| 289 | - | ||
| 290 | - <!-- ═══ HERO ═══ --> | ||
| 291 | - <section class="hero"> | ||
| 292 | - <div class="dot-pattern"></div> | ||
| 293 | - <div class="hero-inner"> | ||
| 294 | - <!-- Bloque 1: Identidad --> | ||
| 295 | - <div class="hero-identity"> | ||
| 296 | - <h1 class="hero-title anim" style="--d:200ms">Presupuesto <span class="gold">Abierto</span></h1> | ||
| 297 | - <p class="hero-sub anim" style="--d:340ms"><a href="/vista-diaria" class="hero-sub-link">Información diaria</a> e histórica de todo el Estado Boliviano.</p> | ||
| 298 | - <div class="hero-stats anim" style="--d:420ms"> | ||
| 299 | - <span class="stat-num">21</span> años<span class="stat-dot">·</span><span class="stat-num">8.3M</span> registros<span class="stat-dot">·</span><span class="stat-num">647</span> instituciones<span class="stat-dot">·</span><span class="stat-num">7</span> <a href="#clasificadores" class="stat-link">clasificadores</a><span class="stat-dot">·</span><span class="stat-num">10</span> <a href="#visualizaciones" class="stat-link">visualizaciones</a><span class="stat-dot">·</span><span class="stat-num">4</span> <a href="#historias" class="stat-link">historias</a> | ||
| 300 | - </div> | ||
| 301 | - </div> | ||
| 302 | - | ||
| 303 | - <!-- Bloque 2: Búsqueda --> | ||
| 304 | - <div class="hero-search anim" style="--d:520ms"> | ||
| 305 | - <div class="search-wrap"> | ||
| 306 | - <!-- Search bar unificado --> | ||
| 307 | - <div class="search-bar" class:search-focused={searchFocused} class:search-has-results={searchFocused && $query.length >= 2 && ($results.length > 0 || $results.length === 0 || $isLoading)} style="--accent:{accent}"> | ||
| 308 | - <!-- Link al diario + Badge informativo (desktop) --> | ||
| 309 | - <div class="search-meta toggle-desktop"> | ||
| 310 | - <a href="/vista-diaria" class="search-meta-item search-diario-link"> | ||
| 311 | - Ver gasto de hoy | ||
| 312 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 313 | - <line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/> | ||
| 314 | - </svg> | ||
| 315 | - </a> | ||
| 316 | - <span class="search-meta-item search-badge"> | ||
| 317 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 318 | - <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/> | ||
| 319 | - </svg> | ||
| 320 | - Viendo 2005 – 2025 | ||
| 321 | - </span> | ||
| 322 | - </div> | ||
| 323 | - <div class="search-divider"></div> | ||
| 324 | - | ||
| 325 | - <!-- Mobile: mode indicator + settings button --> | ||
| 326 | - <button class="mobile-settings-btn" class:mobile-settings-open={mobileSettingsOpen} on:click={toggleMobileSettings}> | ||
| 327 | - <span class="mobile-mode-indicator" style="background:{accent}"></span> | ||
| 328 | - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={mobileSettingsOpen ? accent : '#B8B5AD'} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 329 | - <circle cx="12" cy="12" r="3"/> | ||
| 330 | - <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> | ||
| 331 | - </svg> | ||
| 332 | - </button> | ||
| 333 | - | ||
| 334 | - <svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={searchFocused ? accent : '#B8B5AD'} stroke-width="2" stroke-linecap="round"> | ||
| 335 | - <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/> | ||
| 336 | - </svg> | ||
| 337 | - <div class="search-input-wrap"> | ||
| 338 | - <input | ||
| 339 | - type="text" | ||
| 340 | - bind:this={searchInput} | ||
| 341 | - bind:value={searchVal} | ||
| 342 | - on:input={handleInput} | ||
| 343 | - on:keydown={handleKeydown} | ||
| 344 | - on:focus={() => searchFocused = true} | ||
| 345 | - on:blur={handleBlur} | ||
| 346 | - disabled={!$indexLoaded} | ||
| 347 | - /> | ||
| 348 | - {#if !searchVal} | ||
| 349 | - <div class="search-placeholder"> | ||
| 350 | - <span class="blink-cursor" class:cursor-diario={isD} class:cursor-archivo={!isD}></span> | ||
| 351 | - <span class="placeholder-text placeholder-desktop">{placeholder}</span> | ||
| 352 | - <span class="placeholder-text placeholder-mobile">{isD ? 'MEFP, Min. Salud, GAM...' : 'BOA, Publicidad, IDH...'}</span> | ||
| 353 | - </div> | ||
| 354 | - {/if} | ||
| 355 | - {#if searchVal && !$isLoading} | ||
| 356 | - <button class="search-clear" on:click={() => { clearSearch(); searchVal = ''; if (searchInput) searchInput.value = ''; }}> | ||
| 357 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> | ||
| 358 | - </button> | ||
| 359 | - {/if} | ||
| 360 | - </div> | ||
| 361 | - <!-- Gear button with filter dropdown (desktop) --> | ||
| 362 | - <div class="gear-wrap gear-desktop" on:click|stopPropagation> | ||
| 363 | - <button class="gear-btn" class:gear-active={gearMenuOpen} on:click={toggleGearMenu}> | ||
| 364 | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={gearMenuOpen ? accent : '#B8B5AD'} stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"> | ||
| 365 | - <circle cx="12" cy="12" r="3"/> | ||
| 366 | - <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> | ||
| 367 | - </svg> | ||
| 368 | - </button> | ||
| 369 | - {#if gearMenuOpen} | ||
| 370 | - <div class="gear-menu"> | ||
| 371 | - <div class="gear-menu-title">Ajusta tu búsqueda</div> | ||
| 372 | - <button class="gear-row" on:click={() => toggleFilter('entidad')}> | ||
| 373 | - <div class="gear-check" class:gear-check-on={searchFilters.entidad}> | ||
| 374 | - {#if searchFilters.entidad} | ||
| 375 | - <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> | ||
| 376 | - {/if} | ||
| 377 | - </div> | ||
| 378 | - <div class="gear-row-content"> | ||
| 379 | - <span class="gear-row-question">¿Quién?</span> | ||
| 380 | - <span class="gear-row-label" class:gear-row-label-on={searchFilters.entidad}>Instituciones</span> | ||
| 381 | - </div> | ||
| 382 | - <span class="gear-row-count">756</span> | ||
| 383 | - </button> | ||
| 384 | - <button class="gear-row" on:click={() => toggleFilter('objeto_gasto')}> | ||
| 385 | - <div class="gear-check" class:gear-check-on={searchFilters.objeto_gasto}> | ||
| 386 | - {#if searchFilters.objeto_gasto} | ||
| 387 | - <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> | ||
| 388 | - {/if} | ||
| 389 | - </div> | ||
| 390 | - <div class="gear-row-content"> | ||
| 391 | - <span class="gear-row-question">¿En qué?</span> | ||
| 392 | - <span class="gear-row-label" class:gear-row-label-on={searchFilters.objeto_gasto}>Objetos de gasto</span> | ||
| 393 | - </div> | ||
| 394 | - <span class="gear-row-count">324</span> | ||
| 395 | - </button> | ||
| 396 | - <button class="gear-row" on:click={() => toggleFilter('rubro')}> | ||
| 397 | - <div class="gear-check" class:gear-check-on={searchFilters.rubro}> | ||
| 398 | - {#if searchFilters.rubro} | ||
| 399 | - <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> | ||
| 400 | - {/if} | ||
| 401 | - </div> | ||
| 402 | - <div class="gear-row-content"> | ||
| 403 | - <span class="gear-row-question">¿De dónde?</span> | ||
| 404 | - <span class="gear-row-label" class:gear-row-label-on={searchFilters.rubro}>Rubros de ingreso</span> | ||
| 405 | - </div> | ||
| 406 | - <span class="gear-row-count">324</span> | ||
| 407 | - </button> | ||
| 408 | - </div> | ||
| 409 | - {/if} | ||
| 410 | - </div> | ||
| 411 | - </div> | ||
| 412 | - | ||
| 413 | - <!-- Search Results Dropdown --> | ||
| 414 | - {#if $isLoading && $query.length >= 2} | ||
| 415 | - <div class="search-results"> | ||
| 416 | - <div class="search-loading">Buscando...</div> | ||
| 417 | - </div> | ||
| 418 | - {:else if searchFocused && filteredResults.length > 0} | ||
| 419 | - <div class="search-results"> | ||
| 420 | - {#each filteredResults as item, i} | ||
| 421 | - <button | ||
| 422 | - type="button" | ||
| 423 | - class="search-result-item" | ||
| 424 | - class:result-selected={$selectedIndex === i} | ||
| 425 | - on:click={() => handleSelect(item)} | ||
| 426 | - > | ||
| 427 | - <div class="result-main"> | ||
| 428 | - <span class="result-name">{item.nombre}</span> | ||
| 429 | - {#if item.sigla} | ||
| 430 | - <span class="result-sigla">({item.sigla})</span> | ||
| 431 | - {/if} | ||
| 432 | - </div> | ||
| 433 | - <div class="result-meta"> | ||
| 434 | - {#if item.codigo} | ||
| 435 | - <span class="result-code">{item.codigo}</span> | ||
| 436 | - {/if} | ||
| 437 | - <span class="result-type" style="color:{getTypeColor(item.tipo)}">{getTypeLabel(item.tipo)}</span> | ||
| 438 | - </div> | ||
| 439 | - </button> | ||
| 440 | - {/each} | ||
| 441 | - </div> | ||
| 442 | - {:else if searchFocused && $query.length >= 2 && filteredResults.length === 0} | ||
| 443 | - <div class="search-results"> | ||
| 444 | - <div class="search-no-results">Sin resultados para "{$query}"</div> | ||
| 445 | - </div> | ||
| 446 | - {/if} | ||
| 447 | - | ||
| 448 | - {#if $error} | ||
| 449 | - <div class="search-error">{$error}</div> | ||
| 450 | - {/if} | ||
| 451 | - | ||
| 452 | - <!-- Mobile settings panel --> | ||
| 453 | - <div class="mobile-settings-panel" class:mobile-settings-panel-open={mobileSettingsOpen}> | ||
| 454 | - <div class="mobile-meta-bar"> | ||
| 455 | - <a href="/vista-diaria" class="mobile-meta-item mobile-diario-link"> | ||
| 456 | - Ver hoy | ||
| 457 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 458 | - <line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/> | ||
| 459 | - </svg> | ||
| 460 | - </a> | ||
| 461 | - <span class="mobile-meta-item mobile-search-badge"> | ||
| 462 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 463 | - <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/> | ||
| 464 | - </svg> | ||
| 465 | - Viendo 2005 – 2025 | ||
| 466 | - </span> | ||
| 467 | - </div> | ||
| 468 | - <div class="mobile-filter-section"> | ||
| 469 | - <div class="mobile-filter-title">Buscar</div> | ||
| 470 | - <div class="mobile-filter-bar"> | ||
| 471 | - <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.entidad} on:click={() => toggleFilter('entidad')}> | ||
| 472 | - <span class="filter-chip-dot" style="background:#4A8B6E"></span> | ||
| 473 | - Instituciones | ||
| 474 | - </button> | ||
| 475 | - <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.objeto_gasto} on:click={() => toggleFilter('objeto_gasto')}> | ||
| 476 | - <span class="filter-chip-dot" style="background:#C9A751"></span> | ||
| 477 | - Gastos | ||
| 478 | - </button> | ||
| 479 | - <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.rubro} on:click={() => toggleFilter('rubro')}> | ||
| 480 | - <span class="filter-chip-dot" style="background:#8B6E9B"></span> | ||
| 481 | - Ingresos | ||
| 482 | - </button> | ||
| 483 | - </div> | ||
| 484 | - </div> | ||
| 485 | - </div> | ||
| 486 | - | ||
| 487 | - <!-- Pills --> | ||
| 488 | - <div class="pills"> | ||
| 489 | - <span class="pills-label">Ej:</span> | ||
| 490 | - {#each examples as ex} | ||
| 491 | - {#if ex.href} | ||
| 492 | - <a class="pill" style="color:{ex.color};background:{ex.bg}" href={ex.href}>{ex.label}</a> | ||
| 493 | - {:else} | ||
| 494 | - <button class="pill" style="color:{ex.color};background:{ex.bg}" on:click={() => searchVal = ex.label}>{ex.label}</button> | ||
| 495 | - {/if} | ||
| 496 | - {/each} | ||
| 497 | - </div> | ||
| 498 | - </div> | ||
| 499 | - </div> | ||
| 500 | - </div> | ||
| 501 | - | ||
| 502 | - <!-- Scroll hint --> | ||
| 503 | - <div class="scroll-hint anim" style="--d:700ms" class:scroll-hint-hidden={searchFocused && $results.length > 0}> | ||
| 504 | - <svg width="18" height="28" viewBox="0 0 18 28" fill="none" stroke="#B8B5AD" stroke-width="1.2" stroke-linecap="round"> | ||
| 505 | - <rect x="1" y="1" width="16" height="26" rx="8"/><line x1="9" y1="7" x2="9" y2="12" opacity="0.7"/> | ||
| 506 | - </svg> | ||
| 507 | - <span>Explora el portal</span> | ||
| 508 | - </div> | ||
| 509 | - </section> | ||
| 510 | - </section> | ||
| 511 | - | ||
| 512 | - <!-- ═══ PORTAL DIRECTORY ═══ --> | ||
| 513 | - <section class="directory" data-section="datos"> | ||
| 514 | - <div class="directory-inner"> | ||
| 515 | - <!-- Left: Title --> | ||
| 516 | - <div class="directory-left"> | ||
| 517 | - <div use:reveal={{ id: 'dir-head' }} class="reveal" class:revealed={revealStates['dir-head']}> | ||
| 518 | - <p class="section-tag">LOS DATOS</p> | ||
| 519 | - <h2 class="section-title">Dos formas<br/>de ver el<br/>presupuesto.</h2> | ||
| 520 | - </div> | ||
| 521 | - </div> | ||
| 522 | - | ||
| 523 | - <!-- Right: Cards --> | ||
| 524 | - <div class="directory-right"> | ||
| 525 | - <!-- Diario --> | ||
| 526 | - <div use:reveal={{ id: 'door-d', delay: 60 }} class="reveal" class:revealed={revealStates['door-d']}> | ||
| 527 | - <div class="door door-warm" class:door-hovered={hDiario} on:mouseenter={() => hDiario = true} on:mouseleave={() => hDiario = false} role="button" tabindex="0"> | ||
| 528 | - <div class="door-content"> | ||
| 529 | - <div class="door-header"> | ||
| 530 | - <div class="door-icon" style="opacity:{hDiario ? 1 : 0.4}"> | ||
| 531 | - <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#4A8B6E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 12 7 12 9 6 12 18 15 9 17 12 21 12"/></svg> | ||
| 532 | - </div> | ||
| 533 | - <div class="door-badge"> | ||
| 534 | - <div class="pulse-dot-sm"></div> | ||
| 535 | - <span class="door-badge-text" style="color:#4A8B6E">EN VIVO</span> | ||
| 536 | - </div> | ||
| 537 | - </div> | ||
| 538 | - <h3 class="door-title door-title-dark">Diariamente</h3> | ||
| 539 | - <div class="door-pills"> | ||
| 540 | - <span class="door-pill door-pill-green">¿Cuánto se ejecutó hoy?</span> | ||
| 541 | - <span class="door-pill door-pill-green">¿Quién gastó más?</span> | ||
| 542 | - <span class="door-pill door-pill-green">¿Qué cambió desde ayer?</span> | ||
| 543 | - <span class="door-pill door-pill-green">Panorama del presupuesto en curso</span> | ||
| 544 | - <span class="door-pill door-pill-green">Bitácora de movimientos</span> | ||
| 545 | - <span class="door-pill door-pill-green">Descarga de datos diaria</span> | ||
| 546 | - <span class="door-pill door-pill-green">Vista de todo el Estado</span> | ||
| 547 | - <span class="door-pill door-pill-green">Vista por Institución</span> | ||
| 548 | - </div> | ||
| 549 | - </div> | ||
| 550 | - <div class="door-footer"> | ||
| 551 | - <span class="door-meta">ACTUALIZADO HACE 2 HORAS</span> | ||
| 552 | - <span class="door-cta" style="color:{hDiario ? '#4A8B6E' : '#6B6860'}">Explorar →</span> | ||
| 553 | - </div> | ||
| 554 | - </div> | ||
| 555 | - </div> | ||
| 556 | - | ||
| 557 | - <!-- Archivo --> | ||
| 558 | - <div use:reveal={{ id: 'door-a', delay: 160 }} class="reveal" class:revealed={revealStates['door-a']}> | ||
| 559 | - <div class="door door-warm" class:door-hovered={hArchivo} on:mouseenter={() => hArchivo = true} on:mouseleave={() => hArchivo = false} role="button" tabindex="0"> | ||
| 560 | - <div class="door-content"> | ||
| 561 | - <div class="door-header"> | ||
| 562 | - <div class="door-icon" style="opacity:{hArchivo ? 1 : 0.4}"> | ||
| 563 | - <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#A68A3E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="6" width="14" height="14" rx="2"/><path d="M7 6V4.5A1.5 1.5 0 0 1 8.5 3h7A1.5 1.5 0 0 1 17 4.5V6"/><line x1="8" y1="11" x2="14" y2="11"/><line x1="8" y1="14.5" x2="12" y2="14.5"/></svg> | ||
| 564 | - </div> | ||
| 565 | - <span class="door-badge-text" style="color:#A68A3E">2005 — 2025</span> | ||
| 566 | - </div> | ||
| 567 | - <h3 class="door-title door-title-dark">Históricamente</h3> | ||
| 568 | - <div class="door-pills"> | ||
| 569 | - <span class="door-pill door-pill-light">Tendencias</span> | ||
| 570 | - <span class="door-pill door-pill-light">Tipos de ingreso</span> | ||
| 571 | - <span class="door-pill door-pill-light">Tipos de gasto</span> | ||
| 572 | - <span class="door-pill door-pill-light">Por institución</span> | ||
| 573 | - <span class="door-pill door-pill-light">Por sector</span> | ||
| 574 | - <span class="door-pill door-pill-light">Por autonomías</span> | ||
| 575 | - <span class="door-pill door-pill-light">Comparar entidades</span> | ||
| 576 | - </div> | ||
| 577 | - </div> | ||
| 578 | - <div class="door-footer"> | ||
| 579 | - <span class="door-meta">8M REGISTROS · 7 CLASIFICADORES</span> | ||
| 580 | - <span class="door-cta" style="color:{hArchivo ? '#C9A751' : '#6B6860'}">Explorar →</span> | ||
| 581 | - </div> | ||
| 582 | - </div> | ||
| 583 | - </div> | ||
| 584 | - </div> | ||
| 585 | - </div> | ||
| 586 | - </section> | ||
| 587 | - | ||
| 588 | - <!-- ═══ CLASIFICADORES ═══ --> | ||
| 589 | - <section id="clasificadores" class="clasificadores" data-section="clasificadores"> | ||
| 590 | - <div class="clas-container"> | ||
| 591 | - <!-- Header --> | ||
| 592 | - <div use:reveal={{ id: 'clas-head' }} class="reveal clas-header-section" class:revealed={revealStates['clas-head']}> | ||
| 593 | - <div class="clas-header-row"> | ||
| 594 | - <div> | ||
| 595 | - <p class="section-tag">CLASIFICADORES</p> | ||
| 596 | - <h2 class="section-title">Elige cómo explorar el presupuesto</h2> | ||
| 597 | - <p class="clas-subtitle">Navega por cada una de las 2,626 piezas de los 8 clasificadores.</p> | ||
| 598 | - </div> | ||
| 599 | - <a href="/clasificadores" class="clas-ver-todos">Ver todos →</a> | ||
| 600 | - </div> | ||
| 601 | - </div> | ||
| 602 | - | ||
| 603 | - <!-- ¿Quién? --> | ||
| 604 | - <div use:reveal={{ id: 'clas-quien', delay: 100 }} class="reveal" class:revealed={revealStates['clas-quien']}> | ||
| 605 | - <div class="clas-group-card"> | ||
| 606 | - <div class="clas-group-header"> | ||
| 607 | - <div class="clas-group-icon" style="--c:#1C1C1A"> | ||
| 608 | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> | ||
| 609 | - </div> | ||
| 610 | - <div> | ||
| 611 | - <div class="clas-group-label">¿QUIÉN?</div> | ||
| 612 | - <div class="clas-group-sub">La entidad que gasta o recibe</div> | ||
| 613 | - </div> | ||
| 614 | - </div> | ||
| 615 | - <a href="/clasificadores/institucional" class="widget-card widget-h" class:widget-hover={hWidget === 'quien'} on:mouseenter={() => hWidget = 'quien'} on:mouseleave={() => hWidget = null}> | ||
| 616 | - <div class="widget-left"> | ||
| 617 | - <div class="widget-headline"> | ||
| 618 | - <span class="widget-num-big">756</span> | ||
| 619 | - <span class="widget-label">instituciones públicas</span> | ||
| 620 | - </div> | ||
| 621 | - <div class="widget-desc">Ministerios, gobernaciones, municipios, empresas públicas, universidades</div> | ||
| 622 | - </div> | ||
| 623 | - <div class="widget-right"> | ||
| 624 | - <span class="widget-tech">INSTITUCIONAL</span> | ||
| 625 | - <span class="widget-arrow" class:arrow-show={hWidget === 'quien'}>→</span> | ||
| 626 | - </div> | ||
| 627 | - </a> | ||
| 628 | - </div> | ||
| 629 | - </div> | ||
| 630 | - | ||
| 631 | - <!-- Grid: Ingresos + Gastos --> | ||
| 632 | - <div class="clas-main-grid"> | ||
| 633 | - <!-- INGRESOS --> | ||
| 634 | - <div use:reveal={{ id: 'clas-ing', delay: 200 }} class="reveal clas-col" class:revealed={revealStates['clas-ing']}> | ||
| 635 | - <div class="clas-group-card"> | ||
| 636 | - <div class="clas-group-header"> | ||
| 637 | - <div class="clas-group-icon" style="--c:#C9A751"> | ||
| 638 | - <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg> | ||
| 639 | - </div> | ||
| 640 | - <div> | ||
| 641 | - <div class="clas-group-label" style="color:#C9A751">INGRESOS</div> | ||
| 642 | - </div> | ||
| 643 | - </div> | ||
| 644 | - <div class="widgets-col"> | ||
| 645 | - {#each WIDGETS_INGRESOS as w} | ||
| 646 | - <a href={w.href} class="widget-card widget-grow" class:widget-hover={hWidget === w.id} on:mouseenter={() => hWidget = w.id} on:mouseleave={() => hWidget = null}> | ||
| 647 | - <div class="widget-headline"> | ||
| 648 | - <span class="widget-num-big widget-num-gold">{w.n}</span> | ||
| 649 | - <span class="widget-label">{w.label}</span> | ||
| 650 | - </div> | ||
| 651 | - <p class="widget-desc">{w.d}</p> | ||
| 652 | - <div class="widget-footer"> | ||
| 653 | - <span class="widget-tech">{w.t}</span> | ||
| 654 | - <span class="widget-arrow" class:arrow-show={hWidget === w.id}>→</span> | ||
| 655 | - </div> | ||
| 656 | - </a> | ||
| 657 | - {/each} | ||
| 658 | - </div> | ||
| 659 | - </div> | ||
| 660 | - </div> | ||
| 661 | - | ||
| 662 | - <!-- GASTOS --> | ||
| 663 | - <div use:reveal={{ id: 'clas-gas', delay: 300 }} class="reveal clas-col" class:revealed={revealStates['clas-gas']}> | ||
| 664 | - <div class="clas-group-card"> | ||
| 665 | - <div class="clas-group-header"> | ||
| 666 | - <div class="clas-group-icon" style="--c:#4A8B6E"> | ||
| 667 | - <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12l7 7 7-7"/></svg> | ||
| 668 | - </div> | ||
| 669 | - <div> | ||
| 670 | - <div class="clas-group-label" style="color:#4A8B6E">GASTOS</div> | ||
| 671 | - </div> | ||
| 672 | - </div> | ||
| 673 | - | ||
| 674 | - <!-- Gastos content wrapper --> | ||
| 675 | - <div class="gastos-content"> | ||
| 676 | - <!-- ¿En qué? sub-group --> | ||
| 677 | - <div class="clas-subgroup"> | ||
| 678 | - <div class="clas-subgroup-header"> | ||
| 679 | - <div class="clas-subgroup-icon" style="--c:#4A8B6E"> | ||
| 680 | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg> | ||
| 681 | - </div> | ||
| 682 | - <div> | ||
| 683 | - <div class="clas-subgroup-label">¿EN QUÉ?</div> | ||
| 684 | - </div> | ||
| 685 | - </div> | ||
| 686 | - <div class="widgets-row"> | ||
| 687 | - {#each WIDGETS_GASTOS_QUE as w} | ||
| 688 | - <a href={w.href} class="widget-card widget-sm" class:widget-hover={hWidget === w.id} on:mouseenter={() => hWidget = w.id} on:mouseleave={() => hWidget = null}> | ||
| 689 | - <div class="widget-headline"> | ||
| 690 | - <span class="widget-num-big widget-num-green">{w.n}</span> | ||
| 691 | - <span class="widget-label">{w.label}</span> | ||
| 692 | - </div> | ||
| 693 | - <p class="widget-desc widget-desc-sm">{w.d}</p> | ||
| 694 | - <div class="widget-footer"> | ||
| 695 | - <span class="widget-tech">{w.t}</span> | ||
| 696 | - <span class="widget-arrow" class:arrow-show={hWidget === w.id}>→</span> | ||
| 697 | - </div> | ||
| 698 | - </a> | ||
| 699 | - {/each} | ||
| 700 | - </div> | ||
| 701 | - </div> | ||
| 702 | - | ||
| 703 | - <!-- Bottom: Para qué + Dónde --> | ||
| 704 | - <div class="widgets-row"> | ||
| 705 | - <!-- ¿Para qué? --> | ||
| 706 | - <div class="clas-subgroup"> | ||
| 707 | - <div class="clas-subgroup-header"> | ||
| 708 | - <div class="clas-subgroup-icon" style="--c:#4A8B6E"> | ||
| 709 | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg> | ||
| 710 | - </div> | ||
| 711 | - <div> | ||
| 712 | - <div class="clas-subgroup-label">¿PARA QUÉ?</div> | ||
| 713 | - </div> | ||
| 714 | - </div> | ||
| 715 | - <a href="/clasificadores/funcional" class="widget-card widget-full" class:widget-hover={hWidget === 'finalidad'} on:mouseenter={() => hWidget = 'finalidad'} on:mouseleave={() => hWidget = null}> | ||
| 716 | - <div class="widget-headline"> | ||
| 717 | - <span class="widget-num-big widget-num-green">132</span> | ||
| 718 | - <span class="widget-label">finalidades y funciones</span> | ||
| 719 | - </div> | ||
| 720 | - <p class="widget-desc">Educación, salud, defensa</p> | ||
| 721 | - <div class="widget-footer"> | ||
| 722 | - <span class="widget-tech">Finalidad</span> | ||
| 723 | - <span class="widget-arrow" class:arrow-show={hWidget === 'finalidad'}>→</span> | ||
| 724 | - </div> | ||
| 725 | - </a> | ||
| 726 | - </div> | ||
| 727 | - | ||
| 728 | - <!-- ¿Dónde? --> | ||
| 729 | - <div class="clas-subgroup"> | ||
| 730 | - <div class="clas-subgroup-header"> | ||
| 731 | - <div class="clas-subgroup-icon" style="--c:#4A8B6E"> | ||
| 732 | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5"/></svg> | ||
| 733 | - </div> | ||
| 734 | - <div> | ||
| 735 | - <div class="clas-subgroup-label">¿DÓNDE?</div> | ||
| 736 | - </div> | ||
| 737 | - </div> | ||
| 738 | - <a href="/clasificadores/geografico" class="widget-card widget-full" class:widget-hover={hWidget === 'donde'} on:mouseenter={() => hWidget = 'donde'} on:mouseleave={() => hWidget = null}> | ||
| 739 | - <div class="widget-headline"> | ||
| 740 | - <span class="widget-num-big widget-num-green">352</span> | ||
| 741 | - <span class="widget-label">ubicaciones geográficas</span> | ||
| 742 | - </div> | ||
| 743 | - <p class="widget-desc">Departamentos, municipios</p> | ||
| 744 | - <div class="widget-footer"> | ||
| 745 | - <span class="widget-tech">Geográfico</span> | ||
| 746 | - <span class="widget-arrow" class:arrow-show={hWidget === 'donde'}>→</span> | ||
| 747 | - </div> | ||
| 748 | - </a> | ||
| 749 | - </div> | ||
| 750 | - </div> | ||
| 751 | - </div> | ||
| 752 | - </div> | ||
| 753 | - </div> | ||
| 754 | - </div> | ||
| 755 | - </div> | ||
| 756 | - </section> | ||
| 757 | - | ||
| 758 | - <!-- ═══ HISTORIAS ═══ --> | ||
| 759 | - <section id="historias" class="historias" data-section="historias"> | ||
| 760 | - <div class="container"> | ||
| 761 | - <div use:reveal={{ id: 'hist-head' }} class="reveal" class:revealed={revealStates['hist-head']}> | ||
| 762 | - <div class="hist-header"> | ||
| 763 | - <div> | ||
| 764 | - <div class="hist-tag-row"> | ||
| 765 | - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#6B6860" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg> | ||
| 766 | - <p class="section-tag">HISTORIAS</p> | ||
| 767 | - </div> | ||
| 768 | - <h2 class="section-title">Los datos, contados.</h2> | ||
| 769 | - <p class="hist-subtitle">Investigaciones y análisis narrativos sobre el presupuesto público.</p> | ||
| 770 | - </div> | ||
| 771 | - <a href="/historias" class="gold-link">Ver todas →</a> | ||
| 772 | - </div> | ||
| 773 | - </div> | ||
| 774 | - <div class="hist-grid"> | ||
| 775 | - {#each HISTORIAS as h, i} | ||
| 776 | - <div use:reveal={{ id: `hist-${i}`, delay: 60 + i * 80 }} class="reveal hist-cell" class:revealed={revealStates[`hist-${i}`]} class:hist-tall={h.tall}> | ||
| 777 | - <div class="hist-card" class:hist-card-hovered={hHist === i} on:mouseenter={() => hHist = i} on:mouseleave={() => hHist = null} role="button" tabindex="0" style="--card-color:{h.color}"> | ||
| 778 | - <div class="hist-accent" style="background:{h.color};transform:scaleX({hHist === i ? 1 : 0})"></div> | ||
| 779 | - <div class="hist-content"> | ||
| 780 | - <span class="hist-tag" style="color:{h.color}">{h.tag}</span> | ||
| 781 | - <h4 class="hist-title">{h.title}</h4> | ||
| 782 | - <p class="hist-desc">{h.desc}</p> | ||
| 783 | - </div> | ||
| 784 | - <div class="hist-cta" style="color:{hHist === i ? h.color : '#6B6860'}"> | ||
| 785 | - Leer historia <span class="hist-arrow" class:hist-arrow-moved={hHist === i}>→</span> | ||
| 786 | - </div> | ||
| 787 | - </div> | ||
| 788 | - </div> | ||
| 789 | - {/each} | ||
| 790 | - </div> | ||
| 791 | - </div> | ||
| 792 | - </section> | ||
| 793 | - | ||
| 794 | - <!-- ═══ VISUALIZACIONES ═══ --> | ||
| 795 | - <section id="visualizaciones" class="visualizaciones" data-section="visualizaciones"> | ||
| 796 | - <div class="container"> | ||
| 797 | - <div use:reveal={{ id: 'vis-head' }} class="reveal" class:revealed={revealStates['vis-head']}> | ||
| 798 | - <div class="vis-header"> | ||
| 799 | - <div> | ||
| 800 | - <div class="vis-tag-row"> | ||
| 801 | - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#6B6860" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg> | ||
| 802 | - <p class="section-tag">VISUALIZACIONES</p> | ||
| 803 | - </div> | ||
| 804 | - <h2 class="section-title">Explora los datos.</h2> | ||
| 805 | - <p class="vis-subtitle">Herramientas interactivas para navegar el presupuesto.</p> | ||
| 806 | - </div> | ||
| 807 | - <a href="/visualizaciones" class="gold-link">Ver todas →</a> | ||
| 808 | - </div> | ||
| 809 | - </div> | ||
| 810 | - <div class="vis-grid-simple"> | ||
| 811 | - {#each VISUALIZACIONES as v, i} | ||
| 812 | - <div use:reveal={{ id: `vis-${i}`, delay: 60 + i * 80 }} class="reveal" class:revealed={revealStates[`vis-${i}`]}> | ||
| 813 | - {#if v.href} | ||
| 814 | - <a href={v.href} class="vis-card vis-card-link" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null}> | ||
| 815 | - <div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div> | ||
| 816 | - <div> | ||
| 817 | - <span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span> | ||
| 818 | - <h4 class="vis-title">{v.title}</h4> | ||
| 819 | - <p class="vis-desc">{v.desc}</p> | ||
| 820 | - </div> | ||
| 821 | - <div class="vis-cta" style="color:{hVis === i ? v.color : '#6B6860'}"> | ||
| 822 | - Explorar <span class="vis-arrow" class:vis-arrow-moved={hVis === i}>→</span> | ||
| 823 | - </div> | ||
| 824 | - </a> | ||
| 825 | - {:else} | ||
| 826 | - <div class="vis-card vis-card-disabled" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null} role="button" tabindex="0"> | ||
| 827 | - <div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div> | ||
| 828 | - <div> | ||
| 829 | - <span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span> | ||
| 830 | - <h4 class="vis-title">{v.title}</h4> | ||
| 831 | - <p class="vis-desc">{v.desc}</p> | ||
| 832 | - </div> | ||
| 833 | - <div class="vis-cta" style="color:#6B6860"> | ||
| 834 | - Próximamente | ||
| 835 | - </div> | ||
| 836 | - </div> | ||
| 837 | - {/if} | ||
| 838 | - </div> | ||
| 839 | - {/each} | ||
| 840 | - </div> | ||
| 841 | - </div> | ||
| 842 | - </section> | ||
| 843 | - | ||
| 844 | - <!-- ═══ MANIFIESTO ═══ --> | ||
| 845 | - <section class="manifiesto" data-section="manifiesto"> | ||
| 846 | - <div class="dot-pattern"></div> | ||
| 847 | - <div class="manifiesto-inner"> | ||
| 848 | - <div use:reveal={{ id: 'man' }} class="reveal" class:revealed={revealStates['man']}> | ||
| 849 | - <p class="section-tag-light">POR QUÉ EXISTE ESTO</p> | ||
| 850 | - <h2 class="manifiesto-title">El presupuesto público no es de los gobiernos.<br/><span class="gold">Es de todos.</span></h2> | ||
| 851 | - </div> | ||
| 852 | - </div> | ||
| 853 | - </section> | ||
| 854 | - | ||
| 855 | - <!-- ═══ DESCARGAS ═══ --> | ||
| 856 | - <section id="descargas" class="descargas" data-section="descargas"> | ||
| 857 | - <div class="descargas-inner"> | ||
| 858 | - <div use:reveal={{ id: 'desc' }} class="reveal" class:revealed={revealStates['desc']}> | ||
| 859 | - <h2 class="descargas-title">Descarga los datos.</h2> | ||
| 860 | - <div class="descargas-formats"> | ||
| 861 | - <span class="format-pill">.csv</span> | ||
| 862 | - <span class="format-pill">.parquet</span> | ||
| 863 | - <span class="format-pill">.gpkg</span> | ||
| 864 | - </div> | ||
| 865 | - <a href="/descargas" class="descargas-link"> | ||
| 866 | - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v12"/><polyline points="8 12 12 16 16 12"/><path d="M4 18h16"/></svg> | ||
| 867 | - Ir a página de descargas | ||
| 868 | - </a> | ||
| 869 | - </div> | ||
| 870 | - </div> | ||
| 871 | - </section> | ||
| 872 | - | ||
| 873 | - <!-- ═══ FOOTER ═══ --> | ||
| 874 | - <footer> | ||
| 875 | - <div class="footer-inner"> | ||
| 876 | - <div class="footer-org">MINISTERIO DE ECONOMÍA Y FINANZAS PÚBLICAS</div> | ||
| 877 | - <div class="footer-right">DATOS ABIERTOS · 2005 — 2026</div> | ||
| 878 | - </div> | ||
| 879 | - </footer> | ||
| 880 | -</div> | ||
| 881 | - | ||
| 882 | -<style> | ||
| 883 | - @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap'); | ||
| 884 | - :global(*){margin:0;padding:0;box-sizing:border-box} | ||
| 885 | - :global(html){width:100%;height:100%;margin:0;padding:0;overflow-x:hidden;background:#1C1C1A;scroll-behavior:smooth} | ||
| 886 | - :global(body){width:100%;min-height:100%;margin:0;padding:0;overflow-x:hidden;background:#1C1C1A} | ||
| 887 | - :global(::selection){background:#C9A751;color:#1C1C1A} | ||
| 888 | - :global(::-webkit-scrollbar){width:5px} | ||
| 889 | - :global(::-webkit-scrollbar-track){background:#1C1C1A} | ||
| 890 | - :global(::-webkit-scrollbar-thumb){background:#444;border-radius:3px} | ||
| 891 | - :global(a){color:inherit;text-decoration:none} | ||
| 892 | - | ||
| 893 | - @keyframes pulse{0%,100%{opacity:0.35}50%{opacity:1}} | ||
| 894 | - @keyframes blink{0%,100%{opacity:1}50%{opacity:0}} | ||
| 895 | - @keyframes scrollHint{0%,100%{transform:translateY(0);opacity:0.6}50%{transform:translateY(5px);opacity:1}} | ||
| 896 | - @keyframes heartbeat{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:48}} | ||
| 897 | - | ||
| 898 | - .page{--serif:'Qanelas',system-ui,sans-serif;--sans:'Qanelas',system-ui,sans-serif;--mono:'JetBrains Mono',monospace;font-family:var(--serif);color:#1C1C1A;width:100%;min-width:100%;overflow-x:hidden;scroll-snap-type:y mandatory;overflow-y:scroll;height:100vh} | ||
| 899 | - | ||
| 900 | - /* Section Navigation Dots */ | ||
| 901 | - .section-dots{position:fixed;right:24px;top:50%;transform:translateY(-50%);z-index:100;display:flex;flex-direction:column;gap:12px;background:rgba(255,255,255,0.1);backdrop-filter:blur(8px);padding:12px 8px;border-radius:20px} | ||
| 902 | - .section-dot{width:10px;height:10px;border-radius:50%;border:1.5px solid rgba(100,100,100,0.4);background:rgba(150,150,150,0.2);cursor:pointer;transition:all 0.3s;padding:0;position:relative} | ||
| 903 | - .section-dot:hover{border-color:rgba(100,100,100,0.6);background:rgba(150,150,150,0.4);transform:scale(1.2)} | ||
| 904 | - .section-dot-active{background:#C9A751;border-color:#C9A751} | ||
| 905 | - .section-dot-active:hover{transform:scale(1.2)} | ||
| 906 | - .section-dot-label{position:absolute;right:20px;top:50%;transform:translateY(-50%);font-family:var(--mono);font-size:10px;letter-spacing:0.05em;color:#6B6860;white-space:nowrap;opacity:0;transition:opacity 0.2s;pointer-events:none} | ||
| 907 | - .section-dot:hover .section-dot-label{opacity:1} | ||
| 908 | - .gold{color:#C9A751} | ||
| 909 | - .container{max-width:1060px;margin:0 auto} | ||
| 910 | - | ||
| 911 | - /* Reveal */ | ||
| 912 | - .reveal{opacity:0;transform:translateY(28px);transition:all 0.8s cubic-bezier(0.16,1,0.3,1)} | ||
| 913 | - .revealed{opacity:1!important;transform:translateY(0)!important} | ||
| 914 | - .anim{opacity:0;transform:translateY(18px);transition:all 0.8s cubic-bezier(0.16,1,0.3,1);transition-delay:var(--d,0ms)} | ||
| 915 | - .mounted .anim{opacity:1;transform:translateY(0)} | ||
| 916 | - | ||
| 917 | - /* ── NAV ── */ | ||
| 918 | - nav{background:#1C1C1A;padding:12px 48px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0} | ||
| 919 | - .nav-left{display:flex;align-items:center} | ||
| 920 | - .nav-right{display:flex;align-items:center;gap:24px} | ||
| 921 | - .nav-link{font-family:var(--sans);font-size:14px;color:#B8B5AD;letter-spacing:0.01em;font-weight:400;transition:color 0.2s} | ||
| 922 | - .nav-link:hover{color:#F5F0E8} | ||
| 923 | - .nav-link-icon{display:flex;align-items:center;gap:5px} | ||
| 924 | - .heartbeat-line{animation:heartbeat 3s ease-in-out infinite} | ||
| 925 | - .nav-date{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:9px;letter-spacing:0.06em;color:#B8B5AD;font-weight:400} | ||
| 926 | - .date-label{color:#9B9890} | ||
| 927 | - .date-sep{margin:0 3px;opacity:0.3} | ||
| 928 | - .pulse-dot{width:5px;height:5px;border-radius:50%;background:#4A8B6E;animation:pulse 2s ease-in-out infinite} | ||
| 929 | - .pulse-dot-sm{width:6px;height:6px;border-radius:50%;background:#4A8B6E;animation:pulse 2s ease-in-out infinite;box-shadow:0 0 8px rgba(74,139,110,0.37)} | ||
| 930 | - .nav-sep{width:1px;height:16px;background:#2A2A27} | ||
| 931 | - .nav-logo{height:88px;display:flex;align-items:center;flex-shrink:0} | ||
| 932 | - .nav-logo img{height:100%;width:auto;object-fit:contain} | ||
| 933 | - | ||
| 934 | - /* ── HERO ── */ | ||
| 935 | - .landing-screen{height:100vh;background:#1C1C1A;display:flex;flex-direction:column;scroll-snap-align:start;scroll-snap-stop:always;overflow:hidden} | ||
| 936 | - .hero{background:#1C1C1A;color:#F5F0E8;flex:1;display:flex;flex-direction:column;justify-content:center;padding:40px 48px 32px;position:relative;overflow:hidden} | ||
| 937 | - .dot-pattern{position:absolute;inset:0;opacity:0.02;background-image:radial-gradient(circle at 1px 1px,#B8B5AD 0.5px,transparent 0);background-size:40px 40px} | ||
| 938 | - .dot-pattern-sm{position:absolute;inset:0;opacity:0.015;background-image:radial-gradient(circle at 1px 1px,#fff 0.5px,transparent 0);background-size:32px 32px} | ||
| 939 | - .hero-inner{max-width:1060px;margin:0 auto;width:100%;position:relative;margin-top:-80px;text-align:center} | ||
| 940 | - .hero-identity{margin-bottom:20px} | ||
| 941 | - .hero-search{display:flex;justify-content:center;position:relative;z-index:50} | ||
| 942 | - .hero-tag{font-family:var(--mono);font-size:12px;letter-spacing:0.25em;color:#B8B5AD} | ||
| 943 | - .hero-title{font-size:clamp(44px,5.5vw,72px);font-weight:900;line-height:1.02;letter-spacing:-0.035em;max-width:720px;margin:14px auto 0} | ||
| 944 | - .hero-sub{font-family:var(--sans);font-size:clamp(16px,1.6vw,18px);color:#A5A29A;max-width:100%;margin-top:14px;line-height:1.6} | ||
| 945 | - .hero-sub-link{color:inherit;text-decoration:none;border-bottom:1px dotted rgba(165,162,154,0.5);transition:all 0.2s;padding-bottom:1px} | ||
| 946 | - .hero-sub-link:hover{color:#4A8B6E;border-bottom-color:#4A8B6E} | ||
| 947 | - .hero-stats{font-family:var(--mono);font-size:13px;letter-spacing:0.04em;color:#B8B5AD;margin-top:16px;line-height:1} | ||
| 948 | - .stat-num{color:#F5F0E8} | ||
| 949 | - .stat-dot{margin:0 10px;opacity:0.3} | ||
| 950 | - .stat-link{color:inherit;text-decoration:none;border-bottom:1px dotted rgba(184,181,173,0.4);transition:all 0.2s;padding-bottom:1px} | ||
| 951 | - .stat-link:hover{color:#C9A751;border-bottom-color:#C9A751} | ||
| 952 | - | ||
| 953 | - /* Search */ | ||
| 954 | - .search-wrap{max-width:960px;width:100%;margin:0 auto;position:relative;z-index:9999} | ||
| 955 | - .search-bar{display:flex;align-items:center;background:rgba(255,255,255,0.04);border-radius:16px;padding:8px 14px 8px 8px;transition:all 0.4s cubic-bezier(0.16,1,0.3,1);box-shadow:0 0 0 1px rgba(201,167,81,0.12),0 8px 32px rgba(0,0,0,0.25);position:relative} | ||
| 956 | - .search-focused{background:rgba(255,255,255,0.07);box-shadow:0 0 0 1.5px rgba(201,167,81,0.35),0 8px 40px rgba(0,0,0,0.3)} | ||
| 957 | - .search-has-results{border-radius:16px 16px 0 0} | ||
| 958 | - .search-meta{display:flex;align-items:center;gap:8px;flex-shrink:0;padding:0 4px} | ||
| 959 | - .search-meta-item{display:flex;align-items:center;gap:7px;font-family:var(--sans);font-size:13px;font-weight:500;padding:10px 14px;border-radius:10px;text-decoration:none;transition:all 0.2s} | ||
| 960 | - .search-meta-item svg{display:block;flex-shrink:0} | ||
| 961 | - .search-badge{color:#4A8B6E;background:rgba(74,139,110,0.12)} | ||
| 962 | - .search-diario-link{color:#C9A751;background:rgba(201,167,81,0.12)} | ||
| 963 | - .search-diario-link:hover{background:rgba(201,167,81,0.22);color:#D4B65E} | ||
| 964 | - .search-diario-link svg{transition:transform 0.2s} | ||
| 965 | - .search-diario-link:hover svg{transform:translateX(3px)} | ||
| 966 | - .search-divider{width:1px;height:32px;background:rgba(255,255,255,0.08);margin:0 16px;flex-shrink:0} | ||
| 967 | - .search-icon{flex-shrink:0;transition:stroke 0.3s} | ||
| 968 | - .search-input-wrap{flex:1;position:relative;display:flex;align-items:center} | ||
| 969 | - .search-input-wrap input{width:100%;background:transparent;border:none;outline:none;color:#F5F0E8;font-size:16px;font-family:var(--sans);padding:12px 12px 12px 10px;caret-color:#C9A751} | ||
| 970 | - .search-placeholder{position:absolute;left:10px;top:50%;transform:translateY(-50%);display:flex;align-items:center;pointer-events:none} | ||
| 971 | - .blink-cursor{width:1.5px;height:18px;margin-right:1px;animation:blink 1s step-end infinite;border-radius:1px;flex-shrink:0} | ||
| 972 | - .cursor-diario{background:#4A8B6E} | ||
| 973 | - .cursor-archivo{background:#C9A751} | ||
| 974 | - .placeholder-text{font-family:var(--sans);font-size:16px;color:#B8B5AD} | ||
| 975 | - .placeholder-mobile{display:none} | ||
| 976 | - .gear-btn{width:38px;height:38px;border-radius:10px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.06);cursor:pointer;transition:all 0.3s;flex-shrink:0;margin-left:8px} | ||
| 977 | - .gear-btn svg{transition:transform 0.3s ease} | ||
| 978 | - .gear-btn:hover svg{transform:rotate(45deg)} | ||
| 979 | - .gear-btn:hover,.gear-active{background:rgba(255,255,255,0.12)} | ||
| 980 | - .gear-wrap{position:relative} | ||
| 981 | - .gear-desktop{display:block} | ||
| 982 | - .gear-menu{position:absolute;top:calc(100% + 6px);right:0;background:rgba(28,28,26,0.92);backdrop-filter:blur(32px) saturate(1.2);-webkit-backdrop-filter:blur(32px) saturate(1.2);border-radius:16px;min-width:320px;z-index:9999;box-shadow:0 12px 48px rgba(0,0,0,0.5);padding:4px;animation:gearFadeIn 0.15s ease-out} | ||
| 983 | - @keyframes gearFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}} | ||
| 984 | - .gear-menu-title{font-family:var(--sans);font-size:11px;font-weight:400;color:#6B6860;padding:10px 14px 6px;letter-spacing:0.02em} | ||
| 985 | - .gear-row{display:flex;align-items:center;gap:12px;width:100%;background:transparent;border:none;padding:12px 14px;border-radius:12px;cursor:pointer;transition:background 0.15s;text-align:left} | ||
| 986 | - .gear-row:hover{background:rgba(255,255,255,0.04)} | ||
| 987 | - .gear-check{width:18px;height:18px;border-radius:6px;border:1.5px solid rgba(255,255,255,0.15);background:transparent;display:flex;align-items:center;justify-content:center;transition:all 0.2s;flex-shrink:0;color:transparent} | ||
| 988 | - .gear-check-on{border-color:transparent;background:#C9A751;color:#1C1C1A} | ||
| 989 | - .gear-row-content{flex:1;display:flex;align-items:baseline;gap:8px} | ||
| 990 | - .gear-row-question{font-family:var(--sans);font-size:12px;color:#5A5850;font-weight:400} | ||
| 991 | - .gear-row-label{font-family:var(--sans);font-size:14px;color:#8B8880;transition:color 0.15s} | ||
| 992 | - .gear-row-label-on{color:#F5F0E8} | ||
| 993 | - .gear-row-count{font-family:var(--mono);font-size:10px;color:#4A4840} | ||
| 994 | - | ||
| 995 | - /* Mobile settings button (hidden on desktop) */ | ||
| 996 | - .mobile-settings-btn{display:none;width:40px;height:40px;border-radius:10px;background:rgba(255,255,255,0.06);border:none;cursor:pointer;align-items:center;justify-content:center;position:relative;transition:all 0.3s;flex-shrink:0} | ||
| 997 | - .mobile-settings-btn:hover,.mobile-settings-open{background:rgba(255,255,255,0.12)} | ||
| 998 | - .mobile-mode-indicator{position:absolute;top:6px;right:6px;width:6px;height:6px;border-radius:50%} | ||
| 999 | - | ||
| 1000 | - /* Mobile settings panel (hidden on desktop) */ | ||
| 1001 | - .mobile-settings-panel{display:none;overflow:hidden;max-height:0;opacity:0;transition:all 0.3s cubic-bezier(0.16,1,0.3,1)} | ||
| 1002 | - .mobile-settings-panel-open{max-height:300px;opacity:1;margin-top:12px} | ||
| 1003 | - .mobile-meta-bar{display:flex;gap:10px;padding:4px 0} | ||
| 1004 | - .mobile-meta-item{flex:1;display:flex;align-items:center;justify-content:center;gap:8px;font-family:var(--sans);font-size:13px;font-weight:500;padding:12px 14px;border-radius:10px;text-decoration:none;transition:all 0.2s} | ||
| 1005 | - .mobile-meta-item svg{flex-shrink:0} | ||
| 1006 | - .mobile-search-badge{color:#4A8B6E;background:rgba(74,139,110,0.12)} | ||
| 1007 | - .mobile-diario-link{color:#C9A751;background:rgba(201,167,81,0.12)} | ||
| 1008 | - .mobile-diario-link:hover,.mobile-diario-link:active{background:rgba(201,167,81,0.22)} | ||
| 1009 | - .mobile-filter-section{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.06)} | ||
| 1010 | - .mobile-filter-title{font-family:var(--mono);font-size:10px;font-weight:600;letter-spacing:0.1em;color:#6B6860;text-transform:uppercase;margin-bottom:8px} | ||
| 1011 | - .mobile-filter-bar{display:flex;gap:8px;flex-wrap:wrap} | ||
| 1012 | - .mobile-filter-chip{display:flex;align-items:center;gap:6px;padding:8px 12px;border-radius:20px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);cursor:pointer;transition:all 0.2s;font-family:var(--sans);font-size:13px;color:#6B6860} | ||
| 1013 | - .mobile-filter-chip.filter-chip-active{background:rgba(255,255,255,0.1);border-color:rgba(255,255,255,0.15);color:#F5F0E8} | ||
| 1014 | - .filter-chip-dot{width:6px;height:6px;border-radius:50%;opacity:0.4;transition:opacity 0.2s} | ||
| 1015 | - .mobile-filter-chip.filter-chip-active .filter-chip-dot{opacity:1} | ||
| 1016 | - | ||
| 1017 | - /* Search clear button */ | ||
| 1018 | - .search-clear{background:none;border:none;cursor:pointer;color:#6B6860;padding:4px;display:flex;align-items:center;justify-content:center;transition:color 0.2s;flex-shrink:0} | ||
| 1019 | - .search-clear:hover{color:#F5F0E8} | ||
| 1020 | - | ||
| 1021 | - /* Search Results Dropdown */ | ||
| 1022 | - .search-results{position:absolute;top:100%;left:0;right:0;background:rgba(28,28,26,0.98);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.1);border-top:none;border-radius:0 0 16px 16px;max-height:320px;overflow-y:auto;z-index:9999} | ||
| 1023 | - .search-loading,.search-no-results{padding:16px 20px;font-family:var(--sans);font-size:14px;color:#9B9890;text-align:center} | ||
| 1024 | - .search-error{margin-top:8px;font-family:var(--sans);font-size:13px;color:#C44B3F;text-align:center} | ||
| 1025 | - | ||
| 1026 | - .search-result-item{width:100%;text-align:left;background:none;border:none;border-top:1px solid rgba(255,255,255,0.06);padding:14px 20px;display:flex;justify-content:space-between;align-items:center;cursor:pointer;transition:background 0.2s} | ||
| 1027 | - .search-result-item:first-child{border-top:none} | ||
| 1028 | - .search-result-item:hover,.result-selected{background:rgba(255,255,255,0.06)} | ||
| 1029 | - | ||
| 1030 | - .result-main{display:flex;align-items:center;gap:8px;flex:1;min-width:0} | ||
| 1031 | - .result-name{font-family:var(--sans);font-size:14px;color:#F5F0E8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} | ||
| 1032 | - .result-sigla{font-family:var(--sans);font-size:12px;color:#9B9890;flex-shrink:0} | ||
| 1033 | - | ||
| 1034 | - .result-meta{display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:12px} | ||
| 1035 | - .result-code{font-family:var(--mono);font-size:10px;color:#9B9890;background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px} | ||
| 1036 | - .result-type{font-family:var(--mono);font-size:9px;letter-spacing:0.1em;font-weight:600} | ||
| 1037 | - .pills{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap;align-items:center;justify-content:center} | ||
| 1038 | - .pills-label{font-family:var(--mono);font-size:11px;letter-spacing:0.1em;color:#B8B5AD;margin-right:2px} | ||
| 1039 | - .pill{font-family:var(--mono);font-size:12px;font-weight:300;letter-spacing:0.03em;border:none;border-radius:6px;padding:5px 12px;cursor:pointer;transition:all 0.25s;line-height:1.4;position:relative;overflow:hidden;text-decoration:none} | ||
| 1040 | - .pill::before{content:'';position:absolute;inset:0;background:rgba(255,255,255,0.12);opacity:0;transition:opacity 0.25s} | ||
| 1041 | - .pill:hover::before{opacity:1} | ||
| 1042 | - .pill:hover{transform:translateY(-1px)} | ||
| 1043 | - | ||
| 1044 | - .scroll-hint{position:absolute;bottom:28px;left:0;right:0;margin:0 auto;width:fit-content;display:flex;flex-direction:column;align-items:center;gap:10px;animation:scrollHint 2.5s ease-in-out infinite;z-index:1;transition:opacity 0.3s} | ||
| 1045 | - .scroll-hint-hidden{opacity:0;pointer-events:none} | ||
| 1046 | - .scroll-hint span{font-family:var(--mono);font-size:10px;letter-spacing:0.2em;color:#B8B5AD;opacity:0.7} | ||
| 1047 | - | ||
| 1048 | - /* ── DIRECTORY ── */ | ||
| 1049 | - .directory{background:#f5f5f7;min-height:100vh;display:flex;align-items:center;scroll-snap-align:start;scroll-snap-stop:always;padding:48px} | ||
| 1050 | - .directory-inner{display:grid;grid-template-columns:280px 1fr;gap:48px;width:100%;max-width:1200px;margin:0 auto;align-items:center} | ||
| 1051 | - .directory-left{} | ||
| 1052 | - .directory-right{display:flex;flex-direction:column;gap:20px} | ||
| 1053 | - .section-tag{font-family:var(--mono);font-size:12px;letter-spacing:0.2em;color:#6B6860;margin-bottom:10px} | ||
| 1054 | - .section-tag-light{font-family:var(--mono);font-size:12px;letter-spacing:0.2em;color:#B8B5AD} | ||
| 1055 | - .section-title{font-size:clamp(40px,4.5vw,56px);font-weight:900;letter-spacing:-0.025em;line-height:1.05;color:#1C1C1A} | ||
| 1056 | - .section-title-light{font-size:clamp(32px,4vw,46px);font-weight:900;letter-spacing:-0.02em;line-height:1.08;color:#F5F0E8} | ||
| 1057 | - | ||
| 1058 | - /* Door cards */ | ||
| 1059 | - .door{border-radius:24px;padding:44px 40px 40px;cursor:pointer;transition:all 0.5s cubic-bezier(0.16,1,0.3,1);position:relative;overflow:hidden;display:flex;flex-direction:column;justify-content:space-between;min-height:260px} | ||
| 1060 | - .door-hovered{transform:translateY(-4px)} | ||
| 1061 | - .door-dark{background:#1C1C1A;color:#F5F0E8;box-shadow:none} | ||
| 1062 | - .door-dark.door-hovered{box-shadow:0 24px 64px rgba(0,0,0,0.2)} | ||
| 1063 | - .door-warm{background:#ffffff;color:#1C1C1A;box-shadow:none} | ||
| 1064 | - .door-warm.door-hovered{box-shadow:0 24px 64px rgba(201,167,81,0.12)} | ||
| 1065 | - .door-accent{position:absolute;top:0;left:0;right:0;height:3px;transform-origin:left;transition:transform 0.6s cubic-bezier(0.16,1,0.3,1)} | ||
| 1066 | - .door-content{position:relative} | ||
| 1067 | - .door-header{display:flex;align-items:center;gap:10px;margin-bottom:16px} | ||
| 1068 | - .door-icon{transition:opacity 0.3s} | ||
| 1069 | - .door-badge{display:flex;align-items:center;gap:6px} | ||
| 1070 | - .door-badge-text{font-family:var(--mono);font-size:11px;letter-spacing:0.16em;font-weight:500} | ||
| 1071 | - .door-title{font-size:clamp(28px,3vw,38px);font-weight:900;letter-spacing:-0.02em;line-height:1.08;color:#F5F0E8} | ||
| 1072 | - .door-title-dark{color:#1C1C1A} | ||
| 1073 | - .door-desc{font-family:var(--sans);font-size:17px;color:#6B6860;line-height:1.55;margin-top:10px;max-width:480px} | ||
| 1074 | - .door-desc-light{color:#9B9890} | ||
| 1075 | - .door-pills{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px} | ||
| 1076 | - .door-pill{font-family:var(--sans);font-size:16px;padding:10px 16px;border-radius:8px;line-height:1.3} | ||
| 1077 | - .door-pill-dark{background:rgba(74,139,110,0.12);color:#8ac4a8;font-weight:500} | ||
| 1078 | - .door-pill-light{background:rgba(166,138,62,0.1);color:#a68a3e;font-weight:500} | ||
| 1079 | - .door-pill-green{background:rgba(74,139,110,0.1);color:#3d7a5e;font-weight:500} | ||
| 1080 | - .door-footer{position:relative;display:flex;justify-content:space-between;align-items:center;margin-top:24px} | ||
| 1081 | - .door-meta{font-family:var(--mono);font-size:11px;letter-spacing:0.1em;color:#4A4843} | ||
| 1082 | - .door-meta-light{color:#B8B5AD} | ||
| 1083 | - .door-cta{font-family:var(--sans);font-size:16px;font-weight:400;transition:color 0.3s} | ||
| 1084 | - | ||
| 1085 | - /* ── CLASIFICADORES ── */ | ||
| 1086 | - .clasificadores{background:#e8e8ea;color:#1C1C1A;padding:48px;height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always;overflow:hidden} | ||
| 1087 | - .clas-container{max-width:1100px;margin:0 auto;width:100%;max-height:calc(100vh - 96px);display:flex;flex-direction:column} | ||
| 1088 | - .clas-header-section{margin-bottom:24px;flex-shrink:0} | ||
| 1089 | - .clas-header-row{display:flex;justify-content:space-between;align-items:flex-end} | ||
| 1090 | - .clas-subtitle{font-family:var(--sans);font-size:17px;color:#6B6860;margin-top:10px} | ||
| 1091 | - .clas-ver-todos{font-family:var(--sans);font-size:15px;font-weight:500;color:#1C1C1A;transition:color 0.2s} | ||
| 1092 | - .clas-ver-todos:hover{color:#4A8B6E} | ||
| 1093 | - | ||
| 1094 | - /* Group cards */ | ||
| 1095 | - .clas-group-card{background:#f2f2f4;border:1px solid rgba(0,0,0,0.04);border-radius:20px;padding:16px;margin-bottom:12px;height:100%;display:flex;flex-direction:column;transition:all 0.3s} | ||
| 1096 | - /* Parent reacts to child hover */ | ||
| 1097 | - .clas-group-card:has(.widget-card:hover){background:#efefef;border-color:rgba(0,0,0,0.06)} | ||
| 1098 | - .clas-group-card:has(.widget-card:hover) > .clas-group-header .clas-group-label{transform:scale(1.05);transform-origin:left} | ||
| 1099 | - /* Parent hover highlights children */ | ||
| 1100 | - .clas-group-card:hover .widget-card{background:#fdfdfd;border-color:rgba(0,0,0,0.05)} | ||
| 1101 | - .clas-group-card:hover .clas-subgroup{background:#f5f5f7} | ||
| 1102 | - .gastos-content{display:flex;flex-direction:column;flex:1;gap:10px} | ||
| 1103 | - .gastos-content .clas-subgroup{flex:1;margin-bottom:0} | ||
| 1104 | - .gastos-content > .widgets-row{flex:1} | ||
| 1105 | - .clas-group-header{display:flex;align-items:center;gap:10px;margin-bottom:10px} | ||
| 1106 | - .clas-group-icon{width:32px;height:32px;border-radius:10px;background:color-mix(in srgb, var(--c) 10%, transparent);border:1px solid color-mix(in srgb, var(--c) 15%, transparent);display:flex;align-items:center;justify-content:center;color:var(--c);flex-shrink:0} | ||
| 1107 | - .clas-group-label{font-family:var(--mono);font-size:12px;letter-spacing:0.15em;font-weight:600;color:#1C1C1A;transition:transform 0.3s cubic-bezier(0.16,1,0.3,1)} | ||
| 1108 | - .clas-group-sub{font-family:var(--sans);font-size:13px;color:#6B6860;margin-top:2px} | ||
| 1109 | - | ||
| 1110 | - /* Subgroup */ | ||
| 1111 | - .clas-subgroup{background:#f7f7f9;border:1px solid rgba(0,0,0,0.03);border-radius:14px;padding:12px;margin-bottom:10px;display:flex;flex-direction:column;transition:all 0.3s} | ||
| 1112 | - /* Subgroup parent reacts to child hover */ | ||
| 1113 | - .clas-subgroup:has(.widget-card:hover){background:#f3f3f5;border-color:rgba(0,0,0,0.05)} | ||
| 1114 | - .clas-subgroup:has(.widget-card:hover) > .clas-subgroup-header .clas-subgroup-label{transform:scale(1.05);transform-origin:left} | ||
| 1115 | - /* Subgroup hover highlights children */ | ||
| 1116 | - .clas-subgroup:hover .widget-card{background:#fdfdfd;border-color:rgba(0,0,0,0.05)} | ||
| 1117 | - .clas-subgroup .widgets-row{flex:1} | ||
| 1118 | - .clas-subgroup-header{display:flex;align-items:center;gap:10px;margin-bottom:10px} | ||
| 1119 | - .clas-subgroup-icon{width:32px;height:32px;border-radius:10px;background:color-mix(in srgb, var(--c) 10%, transparent);border:1px solid color-mix(in srgb, var(--c) 15%, transparent);display:flex;align-items:center;justify-content:center;color:var(--c);flex-shrink:0} | ||
| 1120 | - .clas-subgroup-label{font-family:var(--mono);font-size:12px;letter-spacing:0.15em;font-weight:600;color:#1C1C1A;transition:transform 0.3s cubic-bezier(0.16,1,0.3,1)} | ||
| 1121 | - .clas-subgroup-tag{font-family:var(--sans);font-size:13px;color:#6B6860;margin-top:2px} | ||
| 1122 | - | ||
| 1123 | - /* Main grid */ | ||
| 1124 | - .clas-main-grid{display:grid;grid-template-columns:1fr 2fr;gap:12px;align-items:stretch;flex:1;min-height:0} | ||
| 1125 | - .clas-col{display:flex;flex-direction:column} | ||
| 1126 | - .clas-col .clas-group-card{flex:1} | ||
| 1127 | - | ||
| 1128 | - /* Widget cards */ | ||
| 1129 | - .widget-card{display:block;background:#fafafa;border:1px solid rgba(0,0,0,0.04);border-radius:14px;padding:14px;cursor:pointer;text-decoration:none;color:#1C1C1A;transition:all 0.35s cubic-bezier(0.16,1,0.3,1);box-shadow:0 1px 3px rgba(0,0,0,0.02)} | ||
| 1130 | - .widget-hover{background:#ffffff;border-color:rgba(0,0,0,0.06);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,0.05)} | ||
| 1131 | - .widget-h{display:flex;justify-content:space-between;align-items:center} | ||
| 1132 | - .widget-left{flex:1} | ||
| 1133 | - .widget-right{display:flex;align-items:center;gap:12px;flex-shrink:0} | ||
| 1134 | - .widget-grow{flex:1} | ||
| 1135 | - .widget-sm{padding:12px} | ||
| 1136 | - .widget-full{flex:1} | ||
| 1137 | - .widgets-col{display:flex;flex-direction:column;gap:8px;flex:1} | ||
| 1138 | - .widgets-col .widget-card{flex:1} | ||
| 1139 | - .widgets-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;flex:1} | ||
| 1140 | - .widgets-row .widget-card{display:flex;flex-direction:column;justify-content:space-between} | ||
| 1141 | - .widget-headline{display:flex;align-items:baseline;gap:8px;margin-bottom:6px} | ||
| 1142 | - .widget-num-big{font-family:var(--mono);font-size:24px;font-weight:700;line-height:1} | ||
| 1143 | - .widget-num-gold{color:#a68a3e} | ||
| 1144 | - .widget-num-green{color:#4A8B6E} | ||
| 1145 | - .widget-label{font-family:var(--sans);font-size:14px;font-weight:700;color:#1C1C1A;line-height:1.2} | ||
| 1146 | - .widget-desc{font-family:var(--sans);font-size:14px;color:#6B6860;line-height:1.4;font-weight:400} | ||
| 1147 | - .widget-desc-sm{font-size:13px} | ||
| 1148 | - .widget-footer{display:flex;justify-content:space-between;align-items:center;margin-top:8px} | ||
| 1149 | - .widget-tech{font-family:var(--mono);font-size:9px;letter-spacing:0.1em;color:#9B9890;text-transform:uppercase} | ||
| 1150 | - .widget-arrow{font-family:var(--sans);font-size:14px;color:transparent;transform:translateX(-6px);transition:all 0.35s cubic-bezier(0.16,1,0.3,1)} | ||
| 1151 | - .arrow-show{color:#1C1C1A;transform:translateX(0)} | ||
| 1152 | - | ||
| 1153 | - /* ── HISTORIAS ── */ | ||
| 1154 | - .historias{background:#1C1C1A;padding:80px 48px;color:#F5F0E8;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always} | ||
| 1155 | - .historias .container{width:100%;max-width:1060px;margin:0 auto} | ||
| 1156 | - .hist-header{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:36px} | ||
| 1157 | - .hist-tag-row{display:flex;align-items:center;gap:8px;margin-bottom:8px} | ||
| 1158 | - .hist-tag-row svg{stroke:#9B9890} | ||
| 1159 | - .hist-tag-row .section-tag{color:#9B9890} | ||
| 1160 | - .historias .section-title{color:#F5F0E8} | ||
| 1161 | - .hist-subtitle{font-family:var(--sans);font-size:17px;color:#9B9890;margin-top:10px} | ||
| 1162 | - .hist-grid{display:grid;grid-template-columns:repeat(3, 1fr);grid-auto-rows:minmax(120px, auto);gap:16px} | ||
| 1163 | - .hist-cell{grid-row:span 1} | ||
| 1164 | - .hist-tall{grid-row:span 2} | ||
| 1165 | - .hist-card{background:color-mix(in srgb, var(--card-color) 15%, #1C1C1A);border:1px solid color-mix(in srgb, var(--card-color) 25%, transparent);border-radius:18px;padding:28px 24px 24px;cursor:pointer;transition:all 0.5s cubic-bezier(0.16,1,0.3,1);position:relative;overflow:hidden;height:100%;display:flex;flex-direction:column;justify-content:space-between} | ||
| 1166 | - .hist-card-hovered{background:color-mix(in srgb, var(--card-color) 22%, #1C1C1A);border-color:color-mix(in srgb, var(--card-color) 35%, transparent);transform:translateY(-3px)} | ||
| 1167 | - .hist-accent{position:absolute;top:0;left:0;right:0;height:2px;transform-origin:left;transition:transform 0.5s cubic-bezier(0.16,1,0.3,1)} | ||
| 1168 | - .hist-content{flex:1} | ||
| 1169 | - .hist-tag{font-family:var(--mono);font-size:10px;letter-spacing:0.14em;font-weight:500} | ||
| 1170 | - .hist-title{font-family:var(--serif);font-size:18px;font-weight:800;margin-top:12px;line-height:1.25;letter-spacing:-0.01em;color:#F5F0E8} | ||
| 1171 | - .hist-tall .hist-title{font-size:22px} | ||
| 1172 | - .hist-desc{font-family:var(--sans);font-size:14px;color:#9B9890;line-height:1.5;margin-top:10px} | ||
| 1173 | - .hist-cta{font-family:var(--sans);font-size:14px;margin-top:20px;transition:color 0.3s;display:flex;align-items:center;gap:3px;color:#6B6860} | ||
| 1174 | - .hist-arrow{transition:transform 0.3s;display:inline-block} | ||
| 1175 | - .hist-arrow-moved{transform:translateX(3px)} | ||
| 1176 | - | ||
| 1177 | - /* ── VISUALIZACIONES ── */ | ||
| 1178 | - .visualizaciones{background:#f5f5f7;padding:80px 48px;color:#1C1C1A;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always} | ||
| 1179 | - .visualizaciones .container{width:100%;max-width:1060px;margin:0 auto} | ||
| 1180 | - .vis-header{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:36px} | ||
| 1181 | - .vis-tag-row{display:flex;align-items:center;gap:8px;margin-bottom:8px} | ||
| 1182 | - .vis-subtitle{font-family:var(--sans);font-size:17px;color:#6B6860;margin-top:10px} | ||
| 1183 | - .gold-link{font-family:var(--sans);font-size:15px;color:#C9A751;font-weight:600} | ||
| 1184 | - .vis-grid-simple{display:grid;grid-template-columns:repeat(3, 1fr);gap:16px} | ||
| 1185 | - .vis-card{background:#fafafc;border-radius:18px;padding:28px 24px 24px;cursor:pointer;transition:all 0.5s cubic-bezier(0.16,1,0.3,1);position:relative;overflow:hidden;height:100%;display:flex;flex-direction:column;justify-content:space-between} | ||
| 1186 | - .vis-card-hovered{transform:translateY(-2px);box-shadow:0 16px 40px rgba(0,0,0,0.05)} | ||
| 1187 | - .vis-accent{position:absolute;top:0;left:0;right:0;height:2px;transform-origin:left;transition:transform 0.5s cubic-bezier(0.16,1,0.3,1)} | ||
| 1188 | - .vis-type{font-family:var(--mono);font-size:10px;letter-spacing:0.1em;font-weight:500;padding:5px 10px;border-radius:6px;display:inline-block} | ||
| 1189 | - .vis-title{font-family:var(--serif);font-size:18px;font-weight:800;margin-top:12px;line-height:1.25;letter-spacing:-0.01em;color:#1C1C1A} | ||
| 1190 | - .vis-desc{font-family:var(--sans);font-size:14px;color:#6B6860;line-height:1.5;margin-top:10px} | ||
| 1191 | - .vis-cta{font-family:var(--sans);font-size:14px;margin-top:20px;transition:color 0.3s;display:flex;align-items:center;gap:3px;color:#6B6860} | ||
| 1192 | - .vis-arrow{transition:transform 0.3s;display:inline-block} | ||
| 1193 | - .vis-arrow-moved{transform:translateX(3px)} | ||
| 1194 | - .vis-card-link{text-decoration:none;color:inherit} | ||
| 1195 | - .vis-card-disabled{opacity:0.6;cursor:default} | ||
| 1196 | - .vis-card-disabled:hover{transform:none;box-shadow:none} | ||
| 1197 | - | ||
| 1198 | - /* ── MANIFIESTO ── */ | ||
| 1199 | - .manifiesto{background:#1C1C1A;color:#F5F0E8;padding:96px 48px;position:relative;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always} | ||
| 1200 | - .manifiesto-inner{max-width:640px;width:100%;margin:0 auto;position:relative;text-align:center} | ||
| 1201 | - .manifiesto-title{font-size:clamp(36px,4.5vw,56px);font-weight:900;line-height:1.08;letter-spacing:-0.025em;margin-top:18px} | ||
| 1202 | - .manifiesto-text{font-family:var(--sans);font-size:17px;color:#9B9890;margin-top:20px;line-height:1.7} | ||
| 1203 | - | ||
| 1204 | - /* ── DESCARGAS ── */ | ||
| 1205 | - .descargas{background:#1C1C1A;padding:80px 48px;color:#F5F0E8;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always} | ||
| 1206 | - .descargas-inner{max-width:640px;width:100%;margin:0 auto;text-align:center} | ||
| 1207 | - .descargas-title{font-size:clamp(32px,4vw,48px);font-weight:900;line-height:1.1;letter-spacing:-0.02em;color:#F5F0E8} | ||
| 1208 | - .descargas-formats{display:flex;justify-content:center;gap:12px;margin-top:24px} | ||
| 1209 | - .format-pill{font-family:var(--mono);font-size:14px;letter-spacing:0.05em;color:#9B9890;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);padding:8px 16px;border-radius:8px} | ||
| 1210 | - .descargas-link{display:inline-flex;align-items:center;gap:8px;margin-top:32px;font-family:var(--sans);font-size:15px;color:#C9A751;transition:all 0.3s} | ||
| 1211 | - .descargas-link:hover{color:#d4b45a;gap:10px} | ||
| 1212 | - | ||
| 1213 | - /* ── FOOTER ── */ | ||
| 1214 | - footer{background:#1C1C1A;padding:40px 48px;border-top:1px solid #2A2A27} | ||
| 1215 | - .footer-inner{max-width:1060px;margin:0 auto;display:flex;justify-content:space-between;align-items:center} | ||
| 1216 | - .footer-org{font-family:var(--mono);font-size:12px;letter-spacing:0.12em;color:#6B6860} | ||
| 1217 | - .footer-unit{font-family:var(--sans);font-size:13px;color:#6B6860;margin-top:3px} | ||
| 1218 | - .footer-right{font-family:var(--mono);font-size:12px;letter-spacing:0.1em;color:#6B6860} | ||
| 1219 | - | ||
| 1220 | - /* ══════════════════════════════════════════════════════════════════════════ | ||
| 1221 | - RESPONSIVE - TABLET (768px) | ||
| 1222 | - ══════════════════════════════════════════════════════════════════════════ */ | ||
| 1223 | - @media(max-width:768px){ | ||
| 1224 | - .page{scroll-snap-type:y proximity} | ||
| 1225 | - .landing-screen,.directory,.clasificadores,.historias,.visualizaciones,.manifiesto,.descargas{scroll-snap-stop:normal} | ||
| 1226 | - | ||
| 1227 | - /* Section dots - bottom horizontal bar */ | ||
| 1228 | - .section-dots{position:fixed;right:auto;left:50%;top:auto;bottom:20px;transform:translateX(-50%);flex-direction:row;gap:10px;padding:12px 20px;border-radius:24px;background:rgba(28,28,26,0.7);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.1);z-index:1000} | ||
| 1229 | - .section-dot{width:8px;height:8px} | ||
| 1230 | - .section-dot-label{display:none} | ||
| 1231 | - | ||
| 1232 | - /* Nav */ | ||
| 1233 | - nav{padding:12px 24px} | ||
| 1234 | - .nav-logo{height:60px} | ||
| 1235 | - | ||
| 1236 | - /* Hero */ | ||
| 1237 | - .hero{padding:32px 24px} | ||
| 1238 | - .hero-inner{margin-top:0} | ||
| 1239 | - .hero-title{font-size:clamp(36px,8vw,48px)} | ||
| 1240 | - .hero-stats{font-size:11px;line-height:1.6} | ||
| 1241 | - .stat-dot{margin:0 6px} | ||
| 1242 | - | ||
| 1243 | - /* Search bar - mobile compact */ | ||
| 1244 | - .search-bar{flex-wrap:nowrap;padding:8px 12px;gap:8px} | ||
| 1245 | - .toggle-desktop{display:none} | ||
| 1246 | - .search-divider{display:none} | ||
| 1247 | - .gear-desktop{display:none} | ||
| 1248 | - .mobile-settings-btn{display:flex} | ||
| 1249 | - .mobile-settings-panel{display:block} | ||
| 1250 | - .search-icon{margin-left:0} | ||
| 1251 | - .search-input-wrap{order:1} | ||
| 1252 | - .search-input-wrap input{font-size:15px;padding:10px 8px} | ||
| 1253 | - .placeholder-desktop{display:none} | ||
| 1254 | - .placeholder-mobile{display:inline;font-size:14px} | ||
| 1255 | - | ||
| 1256 | - /* Search results mobile */ | ||
| 1257 | - .search-results{max-height:260px;border-radius:0 0 12px 12px} | ||
| 1258 | - .search-result-item{padding:12px 16px} | ||
| 1259 | - .result-name{font-size:13px} | ||
| 1260 | - .result-meta{flex-direction:column;align-items:flex-end;gap:4px} | ||
| 1261 | - .result-code{font-size:9px} | ||
| 1262 | - .result-type{font-size:8px} | ||
| 1263 | - .pills{gap:6px} | ||
| 1264 | - .pill{font-size:11px;padding:4px 10px} | ||
| 1265 | - .scroll-hint{bottom:20px} | ||
| 1266 | - | ||
| 1267 | - /* Directory */ | ||
| 1268 | - .directory{padding:48px 24px} | ||
| 1269 | - .directory-inner{grid-template-columns:1fr;gap:32px} | ||
| 1270 | - .directory-left{text-align:center} | ||
| 1271 | - .section-title{font-size:clamp(32px,7vw,42px)} | ||
| 1272 | - .door{padding:28px 24px 24px;min-height:auto} | ||
| 1273 | - .door-title{font-size:clamp(24px,5vw,32px)} | ||
| 1274 | - .door-pills{gap:6px} | ||
| 1275 | - .door-pill{font-size:14px;padding:8px 12px} | ||
| 1276 | - .door-meta{font-size:10px} | ||
| 1277 | - | ||
| 1278 | - /* Clasificadores */ | ||
| 1279 | - .clasificadores{padding:32px 20px;height:auto;min-height:100vh;overflow-y:auto} | ||
| 1280 | - .clas-container{max-height:none} | ||
| 1281 | - .clas-header-row{flex-direction:column;align-items:flex-start;gap:12px} | ||
| 1282 | - .clas-ver-todos{align-self:flex-start} | ||
| 1283 | - .clas-main-grid{grid-template-columns:1fr;gap:12px} | ||
| 1284 | - .widgets-row{grid-template-columns:1fr} | ||
| 1285 | - .widget-h{flex-direction:column;align-items:flex-start;gap:12px} | ||
| 1286 | - .widget-right{width:100%;justify-content:space-between} | ||
| 1287 | - .widget-num-big{font-size:22px} | ||
| 1288 | - .clas-group-icon,.clas-subgroup-icon{width:28px;height:28px} | ||
| 1289 | - | ||
| 1290 | - /* Historias */ | ||
| 1291 | - .historias{padding:60px 24px} | ||
| 1292 | - .hist-header{flex-direction:column;align-items:flex-start;gap:16px} | ||
| 1293 | - .hist-grid{grid-template-columns:1fr 1fr;grid-auto-rows:auto;gap:12px} | ||
| 1294 | - .hist-tall{grid-row:span 1} | ||
| 1295 | - .hist-card{padding:20px 18px 18px} | ||
| 1296 | - .hist-title{font-size:16px} | ||
| 1297 | - .hist-tall .hist-title{font-size:18px} | ||
| 1298 | - .hist-desc{font-size:13px} | ||
| 1299 | - | ||
| 1300 | - /* Visualizaciones */ | ||
| 1301 | - .visualizaciones{padding:60px 24px} | ||
| 1302 | - .vis-header{flex-direction:column;align-items:flex-start;gap:16px} | ||
| 1303 | - .vis-grid-simple{grid-template-columns:1fr 1fr;gap:12px} | ||
| 1304 | - .vis-card{padding:20px 18px 18px} | ||
| 1305 | - .vis-title{font-size:16px} | ||
| 1306 | - | ||
| 1307 | - /* Manifiesto */ | ||
| 1308 | - .manifiesto{padding:60px 24px} | ||
| 1309 | - .manifiesto-title{font-size:clamp(28px,6vw,40px)} | ||
| 1310 | - | ||
| 1311 | - /* Descargas */ | ||
| 1312 | - .descargas{padding:60px 24px} | ||
| 1313 | - .descargas-title{font-size:clamp(28px,6vw,40px)} | ||
| 1314 | - .descargas-formats{gap:8px} | ||
| 1315 | - .format-pill{font-size:12px;padding:6px 12px} | ||
| 1316 | - | ||
| 1317 | - /* Footer */ | ||
| 1318 | - footer{padding:32px 24px} | ||
| 1319 | - .footer-inner{flex-direction:column;gap:12px;text-align:center} | ||
| 1320 | - .footer-org,.footer-right{font-size:10px} | ||
| 1321 | - } | ||
| 1322 | - | ||
| 1323 | - /* ══════════════════════════════════════════════════════════════════════════ | ||
| 1324 | - RESPONSIVE - MOBILE (480px) | ||
| 1325 | - ══════════════════════════════════════════════════════════════════════════ */ | ||
| 1326 | - @media(max-width:480px){ | ||
| 1327 | - /* Section dots - bottom bar smaller */ | ||
| 1328 | - .section-dots{bottom:16px;gap:8px;padding:10px 16px;border-radius:20px} | ||
| 1329 | - .section-dot{width:7px;height:7px} | ||
| 1330 | - | ||
| 1331 | - /* Nav */ | ||
| 1332 | - nav{padding:10px 16px} | ||
| 1333 | - .nav-logo{height:48px} | ||
| 1334 | - | ||
| 1335 | - /* Hero */ | ||
| 1336 | - .hero{padding:24px 16px} | ||
| 1337 | - .hero-title{font-size:clamp(32px,9vw,40px)} | ||
| 1338 | - .hero-sub{font-size:15px} | ||
| 1339 | - .hero-stats{font-size:10px;display:flex;flex-wrap:wrap;justify-content:center;gap:4px 0} | ||
| 1340 | - .stat-dot{margin:0 4px} | ||
| 1341 | - | ||
| 1342 | - /* Search */ | ||
| 1343 | - .search-wrap{padding:0} | ||
| 1344 | - .mobile-settings-btn{width:36px;height:36px} | ||
| 1345 | - .mobile-meta-item{padding:10px 12px;font-size:12px} | ||
| 1346 | - .search-results{max-height:220px} | ||
| 1347 | - .search-result-item{padding:10px 14px} | ||
| 1348 | - .result-name{font-size:12px} | ||
| 1349 | - .result-sigla{font-size:10px} | ||
| 1350 | - .pills-label{display:none} | ||
| 1351 | - .pill{font-size:10px;padding:4px 8px} | ||
| 1352 | - .scroll-hint{display:none} | ||
| 1353 | - | ||
| 1354 | - /* Directory */ | ||
| 1355 | - .directory{padding:40px 16px} | ||
| 1356 | - .directory-inner{gap:24px} | ||
| 1357 | - .section-tag{font-size:10px} | ||
| 1358 | - .section-title{font-size:clamp(28px,8vw,36px)} | ||
| 1359 | - .door{padding:24px 20px 20px;border-radius:18px} | ||
| 1360 | - .door-icon svg{width:22px;height:22px} | ||
| 1361 | - .door-title{font-size:clamp(22px,6vw,28px)} | ||
| 1362 | - .door-pill{font-size:13px;padding:7px 10px} | ||
| 1363 | - .door-footer{flex-direction:column;align-items:flex-start;gap:8px} | ||
| 1364 | - .door-meta{font-size:9px} | ||
| 1365 | - .door-cta{font-size:14px} | ||
| 1366 | - | ||
| 1367 | - /* Clasificadores */ | ||
| 1368 | - .clasificadores{padding:24px 16px} | ||
| 1369 | - .clas-header-section{margin-bottom:16px} | ||
| 1370 | - .section-title{font-size:clamp(24px,6vw,32px)} | ||
| 1371 | - .clas-subtitle{font-size:14px} | ||
| 1372 | - .clas-group-card{padding:12px;border-radius:14px;margin-bottom:10px} | ||
| 1373 | - .clas-subgroup{padding:10px;border-radius:10px} | ||
| 1374 | - .clas-group-header,.clas-subgroup-header{margin-bottom:8px} | ||
| 1375 | - .clas-group-label,.clas-subgroup-label{font-size:10px} | ||
| 1376 | - .clas-group-sub{font-size:12px} | ||
| 1377 | - .widget-card{padding:12px;border-radius:10px} | ||
| 1378 | - .widget-num-big{font-size:20px} | ||
| 1379 | - .widget-label{font-size:13px} | ||
| 1380 | - .widget-desc{font-size:12px} | ||
| 1381 | - .widget-tech{font-size:8px} | ||
| 1382 | - | ||
| 1383 | - /* Historias */ | ||
| 1384 | - .historias{padding:48px 16px} | ||
| 1385 | - .hist-subtitle{font-size:14px} | ||
| 1386 | - .hist-grid{grid-template-columns:1fr;gap:10px} | ||
| 1387 | - .hist-card{padding:18px 16px 16px;border-radius:14px} | ||
| 1388 | - .hist-tag{font-size:9px} | ||
| 1389 | - .hist-title{font-size:16px} | ||
| 1390 | - .hist-desc{font-size:12px;margin-top:8px} | ||
| 1391 | - .hist-cta{font-size:13px;margin-top:14px} | ||
| 1392 | - | ||
| 1393 | - /* Visualizaciones */ | ||
| 1394 | - .visualizaciones{padding:48px 16px} | ||
| 1395 | - .vis-subtitle{font-size:14px} | ||
| 1396 | - .vis-grid-simple{grid-template-columns:1fr;gap:10px} | ||
| 1397 | - .vis-card{padding:18px 16px 16px;border-radius:14px} | ||
| 1398 | - .vis-type{font-size:9px;padding:4px 8px} | ||
| 1399 | - .vis-title{font-size:16px} | ||
| 1400 | - .vis-desc{font-size:12px} | ||
| 1401 | - .vis-cta{font-size:13px;margin-top:14px} | ||
| 1402 | - | ||
| 1403 | - /* Manifiesto */ | ||
| 1404 | - .manifiesto{padding:48px 16px} | ||
| 1405 | - .section-tag-light{font-size:10px} | ||
| 1406 | - .manifiesto-title{font-size:clamp(24px,7vw,32px)} | ||
| 1407 | - | ||
| 1408 | - /* Descargas */ | ||
| 1409 | - .descargas{padding:48px 16px} | ||
| 1410 | - .descargas-title{font-size:clamp(24px,7vw,32px)} | ||
| 1411 | - .descargas-formats{flex-wrap:wrap;gap:6px} | ||
| 1412 | - .format-pill{font-size:11px;padding:5px 10px} | ||
| 1413 | - .descargas-link{font-size:14px;margin-top:24px} | ||
| 1414 | - | ||
| 1415 | - /* Footer */ | ||
| 1416 | - footer{padding:24px 16px} | ||
| 1417 | - .footer-org{font-size:9px;letter-spacing:0.08em} | ||
| 1418 | - .footer-right{font-size:9px} | ||
| 1419 | - } | ||
| 1420 | -</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 1 | -<script> | ||
| 2 | - import { onMount } from 'svelte'; | ||
| 3 | - | ||
| 4 | - // Estado base | ||
| 5 | - let mounted = false; | ||
| 6 | - let searchFocused = false; | ||
| 7 | - let searchVal = ''; | ||
| 8 | - let searchInput; | ||
| 9 | - let isLoading = false; | ||
| 10 | - let results = null; | ||
| 11 | - let error = null; | ||
| 12 | - | ||
| 13 | - // Ordenamiento | ||
| 14 | - let sortBy = 'amount'; | ||
| 15 | - let sortOrder = 'desc'; | ||
| 16 | - | ||
| 17 | - // Paginación | ||
| 18 | - let currentPage = 1; | ||
| 19 | - let isLoadingMore = false; | ||
| 20 | - let allHits = []; | ||
| 21 | - | ||
| 22 | - function toggleSort(field) { | ||
| 23 | - if (sortBy === field) { | ||
| 24 | - sortOrder = sortOrder === 'desc' ? 'asc' : 'desc'; | ||
| 25 | - } else { | ||
| 26 | - sortBy = field; | ||
| 27 | - sortOrder = 'desc'; | ||
| 28 | - } | ||
| 29 | - } | ||
| 30 | - | ||
| 31 | - $: sortedHits = allHits.length > 0 ? [...allHits].sort((a, b) => { | ||
| 32 | - let valA = sortBy === 'year' ? (a.document.gestion || 0) : (a.document.devengado || 0); | ||
| 33 | - let valB = sortBy === 'year' ? (b.document.gestion || 0) : (b.document.devengado || 0); | ||
| 34 | - return sortOrder === 'desc' ? valB - valA : valA - valB; | ||
| 35 | - }) : []; | ||
| 36 | - | ||
| 37 | - $: hasMoreResults = results && allHits.length < results.found; | ||
| 38 | - | ||
| 39 | - // Modo de búsqueda | ||
| 40 | - let searchMode = 'gastos'; // 'gastos' | 'ingresos' | ||
| 41 | - let searchSubmode = 'programas'; // 'programas' | 'clasificadores' | ||
| 42 | - let selectedClassifiers = []; | ||
| 43 | - | ||
| 44 | - const CLASIFICADORES_GASTOS = [ | ||
| 45 | - { id: 'finfun', label: 'Finalidad y función' }, | ||
| 46 | - { id: 'objeto', label: 'Objetos de gasto' }, | ||
| 47 | - { id: 'acteco', label: 'Actividad económica' }, | ||
| 48 | - { id: 'entidad', label: 'Entidades' }, | ||
| 49 | - { id: 'geografico', label: 'Geográfico', disabled: true }, | ||
| 50 | - ]; | ||
| 51 | - | ||
| 52 | - const CLASIFICADORES_INGRESOS = [ | ||
| 53 | - { id: 'rubro', label: 'Rubro', disabled: true }, | ||
| 54 | - { id: 'organismo', label: 'Organismo', disabled: true }, | ||
| 55 | - { id: 'fuente', label: 'Fuente', disabled: true }, | ||
| 56 | - { id: 'entidad', label: 'Entidades' }, | ||
| 57 | - { id: 'geografico',label: 'Geográfico', disabled: true }, | ||
| 58 | - ]; | ||
| 59 | - | ||
| 60 | - $: activeClasificadores = searchMode === 'gastos' ? CLASIFICADORES_GASTOS : CLASIFICADORES_INGRESOS; | ||
| 61 | - | ||
| 62 | - function toggleClassifier(id) { | ||
| 63 | - if (selectedClassifiers.includes(id)) { | ||
| 64 | - selectedClassifiers = selectedClassifiers.filter(c => c !== id); | ||
| 65 | - } else { | ||
| 66 | - selectedClassifiers = [...selectedClassifiers, id]; | ||
| 67 | - } | ||
| 68 | - } | ||
| 69 | - | ||
| 70 | - function setMode(mode) { | ||
| 71 | - searchMode = mode; | ||
| 72 | - selectedClassifiers = []; | ||
| 73 | - if (mode === 'gastos') { | ||
| 74 | - searchSubmode = 'programas'; | ||
| 75 | - } else { | ||
| 76 | - searchSubmode = 'clasificadores'; | ||
| 77 | - } | ||
| 78 | - } | ||
| 79 | - | ||
| 80 | - function setSubmode(sub) { | ||
| 81 | - searchSubmode = sub; | ||
| 82 | - selectedClassifiers = []; | ||
| 83 | - } | ||
| 84 | - | ||
| 85 | - // Placeholder dinámico | ||
| 86 | - $: placeholderText = getPlaceholderText(searchMode, searchSubmode, selectedClassifiers); | ||
| 87 | - | ||
| 88 | - function getPlaceholderText(mode, submode, classifiers) { | ||
| 89 | - if (mode === 'gastos') { | ||
| 90 | - if (submode === 'programas') return 'Buscar en programas y proyectos...'; | ||
| 91 | - if (classifiers.length === 0) return 'Selecciona al menos un clasificador...'; | ||
| 92 | - if (classifiers.length === CLASIFICADORES_GASTOS.length) return 'Buscar en todos los clasificadores de gasto...'; | ||
| 93 | - return `Buscar en ${classifiers.length} clasificador${classifiers.length > 1 ? 'es' : ''} de gasto...`; | ||
| 94 | - } else { | ||
| 95 | - if (classifiers.length === 0) return 'Selecciona al menos un clasificador de ingreso...'; | ||
| 96 | - return `Buscar en ${classifiers.length} clasificador${classifiers.length > 1 ? 'es' : ''} de ingreso...`; | ||
| 97 | - } | ||
| 98 | - } | ||
| 99 | - | ||
| 100 | - // Debounce | ||
| 101 | - let debounceTimer; | ||
| 102 | - let hintTimer; | ||
| 103 | - let showMinLengthHint = false; | ||
| 104 | - const DEBOUNCE_MS = 300; | ||
| 105 | - const HINT_DELAY_MS = 1500; | ||
| 106 | - | ||
| 107 | - const POBLACION = 12000000; | ||
| 108 | - const API_BASE = '/api/search'; | ||
| 109 | - | ||
| 110 | - // Hint diario | ||
| 111 | - let dailyData = null; | ||
| 112 | - let dailyLoading = true; | ||
| 113 | - | ||
| 114 | - async function fetchDaily() { | ||
| 115 | - try { | ||
| 116 | - const res = await fetch('/api/daily-summary'); | ||
| 117 | - if (res.ok) dailyData = await res.json(); | ||
| 118 | - } catch { | ||
| 119 | - // silencioso — es un hint, no crítico | ||
| 120 | - } finally { | ||
| 121 | - dailyLoading = false; | ||
| 122 | - } | ||
| 123 | - } | ||
| 124 | - | ||
| 125 | - // Ejemplos | ||
| 126 | - const EXAMPLES = [ | ||
| 127 | - { label: 'Vía férrea Bulo Bulo', color: 'rgba(201,167,81,0.12)', textColor: '#d4b962' }, | ||
| 128 | - { label: 'Viru Viru', color: 'rgba(150,155,175,0.10)', textColor: '#a0a5b8' }, | ||
| 129 | - { label: 'Cancha de fútbol', color: 'rgba(180,130,100,0.10)', textColor: '#b8947a' }, | ||
| 130 | - ]; | ||
| 131 | - | ||
| 132 | - // Navegación | ||
| 133 | - const SECTIONS_NAV = [ | ||
| 134 | - { id: 'hero', label: 'Inicio' }, | ||
| 135 | - { id: 'datos', label: 'Datos' }, | ||
| 136 | - { id: 'clasificadores', label: 'Clasificadores' }, | ||
| 137 | - { id: 'historias', label: 'Historias' }, | ||
| 138 | - { id: 'visualizaciones',label: 'Visualizaciones' }, | ||
| 139 | - ]; | ||
| 140 | - let currentSection = 'hero'; | ||
| 141 | - | ||
| 142 | - function scrollToSection(id) { | ||
| 143 | - document.querySelector(`[data-section="${id}"]`)?.scrollIntoView({ behavior: 'smooth' }); | ||
| 144 | - } | ||
| 145 | - | ||
| 146 | - onMount(() => { | ||
| 147 | - setTimeout(() => { mounted = true; }, 80); | ||
| 148 | - fetchDaily(); | ||
| 149 | - | ||
| 150 | - const isMobile = window.innerWidth < 768; | ||
| 151 | - if (!isMobile) setTimeout(() => searchInput?.focus(), 300); | ||
| 152 | - | ||
| 153 | - const sections = document.querySelectorAll('[data-section]'); | ||
| 154 | - const observer = new IntersectionObserver(entries => { | ||
| 155 | - entries.forEach(e => { if (e.isIntersecting) currentSection = e.target.dataset.section; }); | ||
| 156 | - }, { threshold: 0.5 }); | ||
| 157 | - sections.forEach(s => observer.observe(s)); | ||
| 158 | - | ||
| 159 | - return () => observer.disconnect(); | ||
| 160 | - }); | ||
| 161 | - | ||
| 162 | - function searchExample(label) { | ||
| 163 | - searchVal = label; | ||
| 164 | - performSearch(label); | ||
| 165 | - searchInput?.focus(); | ||
| 166 | - } | ||
| 167 | - | ||
| 168 | - async function handleInput(e) { | ||
| 169 | - searchVal = e.target.value; | ||
| 170 | - clearTimeout(debounceTimer); | ||
| 171 | - clearTimeout(hintTimer); | ||
| 172 | - showMinLengthHint = false; | ||
| 173 | - | ||
| 174 | - if (searchVal.length < 3) { | ||
| 175 | - results = null; | ||
| 176 | - error = null; | ||
| 177 | - if (searchVal.length > 0) { | ||
| 178 | - hintTimer = setTimeout(() => { showMinLengthHint = true; }, HINT_DELAY_MS); | ||
| 179 | - } | ||
| 180 | - return; | ||
| 181 | - } | ||
| 182 | - | ||
| 183 | - const needsClassifiers = searchMode === 'ingresos' || | ||
| 184 | - (searchMode === 'gastos' && searchSubmode === 'clasificadores'); | ||
| 185 | - if (needsClassifiers && selectedClassifiers.length === 0) { | ||
| 186 | - results = null; | ||
| 187 | - error = null; | ||
| 188 | - return; | ||
| 189 | - } | ||
| 190 | - | ||
| 191 | - debounceTimer = setTimeout(() => performSearch(searchVal), DEBOUNCE_MS); | ||
| 192 | - } | ||
| 193 | - | ||
| 194 | - async function performSearch(query, page = 1) { | ||
| 195 | - if (page === 1) { | ||
| 196 | - isLoading = true; | ||
| 197 | - allHits = []; | ||
| 198 | - sortBy = 'amount'; | ||
| 199 | - sortOrder = 'desc'; | ||
| 200 | - } else { | ||
| 201 | - isLoadingMore = true; | ||
| 202 | - } | ||
| 203 | - error = null; | ||
| 204 | - currentPage = page; | ||
| 205 | - | ||
| 206 | - try { | ||
| 207 | - const isClassSearch = searchMode === 'ingresos' || | ||
| 208 | - (searchMode === 'gastos' && searchSubmode === 'clasificadores'); | ||
| 209 | - const params = new URLSearchParams({ | ||
| 210 | - q: query, | ||
| 211 | - is_class: isClassSearch ? 'true' : 'false', | ||
| 212 | - mode: searchMode, | ||
| 213 | - page: page.toString() | ||
| 214 | - }); | ||
| 215 | - if (isClassSearch && selectedClassifiers.length > 0) { | ||
| 216 | - params.set('class_', selectedClassifiers.join(',')); | ||
| 217 | - } | ||
| 218 | - | ||
| 219 | - const res = await fetch(`${API_BASE}?${params}`); | ||
| 220 | - if (!res.ok) throw new Error(`Error ${res.status}`); | ||
| 221 | - const data = await res.json(); | ||
| 222 | - results = data; | ||
| 223 | - allHits = page === 1 ? data.hits : [...allHits, ...data.hits]; | ||
| 224 | - } catch (err) { | ||
| 225 | - console.error(err); | ||
| 226 | - error = 'Error al buscar. Intenta de nuevo.'; | ||
| 227 | - if (page === 1) { results = null; allHits = []; } | ||
| 228 | - } | ||
| 229 | - | ||
| 230 | - isLoading = false; | ||
| 231 | - isLoadingMore = false; | ||
| 232 | - } | ||
| 233 | - | ||
| 234 | - function loadMore() { | ||
| 235 | - if (hasMoreResults && !isLoadingMore) performSearch(searchVal, currentPage + 1); | ||
| 236 | - } | ||
| 237 | - | ||
| 238 | - function handleKeydown(e) { | ||
| 239 | - if (e.key === 'Escape') { clearSearch(); searchInput?.blur(); } | ||
| 240 | - } | ||
| 241 | - | ||
| 242 | - function clearSearch() { | ||
| 243 | - searchVal = ''; | ||
| 244 | - results = null; | ||
| 245 | - allHits = []; | ||
| 246 | - currentPage = 1; | ||
| 247 | - error = null; | ||
| 248 | - showMinLengthHint = false; | ||
| 249 | - clearTimeout(debounceTimer); | ||
| 250 | - clearTimeout(hintTimer); | ||
| 251 | - searchInput?.focus(); | ||
| 252 | - } | ||
| 253 | - | ||
| 254 | - // Helpers | ||
| 255 | - function extractAfterHyphen(text) { | ||
| 256 | - if (!text) return ''; | ||
| 257 | - const parts = text.split(' - '); | ||
| 258 | - return parts.length > 1 ? parts.slice(1).join(' - ').trim() : text; | ||
| 259 | - } | ||
| 260 | - | ||
| 261 | - function parseMetadatos(meta) { | ||
| 262 | - if (!meta) return {}; | ||
| 263 | - if (typeof meta === 'object') return meta; | ||
| 264 | - try { return JSON.parse(meta); } catch { return {}; } | ||
| 265 | - } | ||
| 266 | - | ||
| 267 | - function isClassifierSearch(data) { return data?.hits?.[0]?.document?.is_class === true; } | ||
| 268 | - function isEntidadSearch(data) { return data?.hits?.[0]?.document?.class_ === 'entidad'; } | ||
| 269 | - | ||
| 270 | - function getStats(data) { | ||
| 271 | - if (!data?.hits?.length) return null; | ||
| 272 | - const hits = data.hits; | ||
| 273 | - const isClassifier = isClassifierSearch(data); | ||
| 274 | - const montoTotal = hits.reduce((s, h) => s + (h.document.devengado || 0), 0); | ||
| 275 | - | ||
| 276 | - if (isClassifier) { | ||
| 277 | - const isEntidad = isEntidadSearch(data); | ||
| 278 | - const entidadesSet = new Set(); | ||
| 279 | - const subareasCount = {}; | ||
| 280 | - hits.forEach(h => { | ||
| 281 | - const m = parseMetadatos(h.document.metadatos); | ||
| 282 | - if (m.entidad) entidadesSet.add(m.entidad); | ||
| 283 | - if (isEntidad) { | ||
| 284 | - const sub = extractAfterHyphen(m.entidad_desc_subarea); | ||
| 285 | - if (sub) subareasCount[sub] = (subareasCount[sub] || 0) + 1; | ||
| 286 | - } | ||
| 287 | - }); | ||
| 288 | - return { found: data.found, montoTotal, perCapita: montoTotal / POBLACION, | ||
| 289 | - numEntidades: entidadesSet.size, subareasCount: isEntidad ? subareasCount : null, | ||
| 290 | - isClassifier: true, classType: hits[0]?.document?.class_ || null }; | ||
| 291 | - } else { | ||
| 292 | - const entidadesSet = new Set(); | ||
| 293 | - hits.forEach(h => { const m = parseMetadatos(h.document.metadatos); if (m.entidad) entidadesSet.add(m.entidad); }); | ||
| 294 | - const years = hits.map(h => h.document.gestion).filter(Boolean); | ||
| 295 | - const minYear = Math.min(...years); | ||
| 296 | - const maxYear = Math.max(...years); | ||
| 297 | - return { found: data.found, montoTotal, perCapita: montoTotal / POBLACION, | ||
| 298 | - numEntidades: entidadesSet.size, | ||
| 299 | - yearRange: minYear === maxYear ? `${minYear}` : `${minYear} – ${maxYear}`, | ||
| 300 | - isClassifier: false }; | ||
| 301 | - } | ||
| 302 | - } | ||
| 303 | - | ||
| 304 | - function formatMonto(num) { | ||
| 305 | - if (num >= 1e9) return `${(num/1e9).toFixed(1)} mil millones de Bs`; | ||
| 306 | - if (num >= 1e6) return `${(num/1e6).toFixed(1)} millones de Bs`; | ||
| 307 | - if (num >= 1e3) return `${(num/1e3).toFixed(0)} mil Bs`; | ||
| 308 | - return `${num.toFixed(0)} Bs`; | ||
| 309 | - } | ||
| 310 | - | ||
| 311 | - function formatMontoFull(num) { | ||
| 312 | - return `${num.toLocaleString('es-BO', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} Bs`; | ||
| 313 | - } | ||
| 314 | - | ||
| 315 | - function generateCodigo(hit) { | ||
| 316 | - const m = parseMetadatos(hit.document.metadatos); | ||
| 317 | - return `${m.entidad}-${m.programa}-${m.proyecto}-${m.actividad}-${hit.document.gestion}`; | ||
| 318 | - } | ||
| 319 | - | ||
| 320 | - $: stats = results ? getStats(results) : null; | ||
| 321 | - | ||
| 322 | - $: { | ||
| 323 | - const _mode = searchMode; | ||
| 324 | - const _submode = searchSubmode; | ||
| 325 | - const _classifiers = selectedClassifiers; | ||
| 326 | - if (searchVal.length >= 3 && mounted) { | ||
| 327 | - clearTimeout(debounceTimer); | ||
| 328 | - const needsClassifiers = _mode === 'ingresos' || (_mode === 'gastos' && _submode === 'clasificadores'); | ||
| 329 | - if (!needsClassifiers || _classifiers.length > 0) { | ||
| 330 | - debounceTimer = setTimeout(() => performSearch(searchVal), 150); | ||
| 331 | - } else { | ||
| 332 | - results = null; | ||
| 333 | - } | ||
| 334 | - } | ||
| 335 | - } | ||
| 336 | - | ||
| 337 | - // ¿Mostrar chips de clasificadores? | ||
| 338 | - $: showClassifierChips = searchMode === 'ingresos' || | ||
| 339 | - (searchMode === 'gastos' && searchSubmode === 'clasificadores'); | ||
| 340 | - | ||
| 341 | - // ¿Hay búsqueda activa? | ||
| 342 | - $: isSearching = searchVal.length >= 3; | ||
| 343 | -</script> | ||
| 344 | - | ||
| 345 | -<svelte:head> | ||
| 346 | - <title>Buscador | Presupuesto Abierto</title> | ||
| 347 | -</svelte:head> | ||
| 348 | - | ||
| 349 | -<div class="page" class:mounted> | ||
| 350 | - | ||
| 351 | - <!-- Dots de navegación --> | ||
| 352 | - <nav class="section-dots"> | ||
| 353 | - {#each SECTIONS_NAV as s} | ||
| 354 | - <button | ||
| 355 | - class="section-dot" | ||
| 356 | - class:section-dot-active={currentSection === s.id} | ||
| 357 | - on:click={() => scrollToSection(s.id)} | ||
| 358 | - aria-label={s.label} | ||
| 359 | - > | ||
| 360 | - <span class="section-dot-label">{s.label}</span> | ||
| 361 | - </button> | ||
| 362 | - {/each} | ||
| 363 | - </nav> | ||
| 364 | - | ||
| 365 | - <!-- Hero --> | ||
| 366 | - <main data-section="hero"> | ||
| 367 | - | ||
| 368 | - <div class="nav-logo"> | ||
| 369 | - <img src="/logos_oscuro/05_Imago MEFP horizontal espacio negativo.png" alt="MEFP" /> | ||
| 370 | - </div> | ||
| 371 | - | ||
| 372 | - <button class="nav-menu-btn" aria-label="Menú"> | ||
| 373 | - <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"> | ||
| 374 | - <line x1="3" y1="6" x2="21" y2="6"/> | ||
| 375 | - <line x1="3" y1="12" x2="21" y2="12"/> | ||
| 376 | - <line x1="3" y1="18" x2="21" y2="18"/> | ||
| 377 | - </svg> | ||
| 378 | - </button> | ||
| 379 | - | ||
| 380 | - <div class="search-container anim" style="--d:200ms"> | ||
| 381 | - | ||
| 382 | - <!-- Título --> | ||
| 383 | - <h1 class="hero-title" class:hero-collapsed={allHits.length > 0}> | ||
| 384 | - Presupuesto <span class="gold">Abierto</span> | ||
| 385 | - </h1> | ||
| 386 | - <p class="hero-sub" class:hero-collapsed={allHits.length > 0}> | ||
| 387 | - <a href="/vista-diaria" class="hero-sub-link">Información diaria</a> e histórica de todo el Estado Boliviano. | ||
| 388 | - </p> | ||
| 389 | - | ||
| 390 | - <!-- Buscador --> | ||
| 391 | - <div class="search-wrap"> | ||
| 392 | - <div class="search-bar" class:search-focused={searchFocused} class:has-results={isSearching}> | ||
| 393 | - <svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" | ||
| 394 | - stroke={searchFocused ? '#D4B254' : '#8C8880'} stroke-width="2" stroke-linecap="round"> | ||
| 395 | - <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/> | ||
| 396 | - </svg> | ||
| 397 | - | ||
| 398 | - <div class="search-input-wrap"> | ||
| 399 | - <input | ||
| 400 | - type="text" | ||
| 401 | - bind:this={searchInput} | ||
| 402 | - bind:value={searchVal} | ||
| 403 | - on:input={handleInput} | ||
| 404 | - on:keydown={handleKeydown} | ||
| 405 | - on:focus={() => searchFocused = true} | ||
| 406 | - on:blur={() => searchFocused = false} | ||
| 407 | - placeholder="" | ||
| 408 | - /> | ||
| 409 | - {#if !searchVal} | ||
| 410 | - <div class="search-placeholder"> | ||
| 411 | - <span class="blink-cursor" class:cursor-hidden={searchFocused}></span> | ||
| 412 | - <span class="placeholder-text">{placeholderText}</span> | ||
| 413 | - </div> | ||
| 414 | - {/if} | ||
| 415 | - {#if searchVal && !isLoading} | ||
| 416 | - <button class="search-clear" on:click={clearSearch}> | ||
| 417 | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"> | ||
| 418 | - <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> | ||
| 419 | - </svg> | ||
| 420 | - </button> | ||
| 421 | - {/if} | ||
| 422 | - {#if isLoading} | ||
| 423 | - <div class="search-loading"><span class="loading-dot"></span></div> | ||
| 424 | - {/if} | ||
| 425 | - </div> | ||
| 426 | - </div> | ||
| 427 | - | ||
| 428 | - <!-- ─── FILTROS INLINE ─── --> | ||
| 429 | - <div class="filters-row" class:filters-hidden={isSearching}> | ||
| 430 | - | ||
| 431 | - <!-- Fila 1: Gastos / Ingresos --> | ||
| 432 | - <div class="filter-group"> | ||
| 433 | - <button | ||
| 434 | - class="filter-chip filter-chip-mode" | ||
| 435 | - class:filter-chip-active={searchMode === 'gastos'} | ||
| 436 | - on:click={() => setMode('gastos')} | ||
| 437 | - > | ||
| 438 | - Gastos | ||
| 439 | - </button> | ||
| 440 | - <button | ||
| 441 | - class="filter-chip filter-chip-mode" | ||
| 442 | - class:filter-chip-active={searchMode === 'ingresos'} | ||
| 443 | - on:click={() => setMode('ingresos')} | ||
| 444 | - > | ||
| 445 | - Ingresos | ||
| 446 | - </button> | ||
| 447 | - | ||
| 448 | - <span class="filter-divider"></span> | ||
| 449 | - | ||
| 450 | - <!-- Sub-modo (solo cuando Gastos está activo) --> | ||
| 451 | - {#if searchMode === 'gastos'} | ||
| 452 | - <button | ||
| 453 | - class="filter-chip" | ||
| 454 | - class:filter-chip-active={searchSubmode === 'programas'} | ||
| 455 | - on:click={() => setSubmode('programas')} | ||
| 456 | - > | ||
| 457 | - Programas y proyectos | ||
| 458 | - </button> | ||
| 459 | - <button | ||
| 460 | - class="filter-chip" | ||
| 461 | - class:filter-chip-active={searchSubmode === 'clasificadores'} | ||
| 462 | - on:click={() => setSubmode('clasificadores')} | ||
| 463 | - > | ||
| 464 | - Clasificadores | ||
| 465 | - </button> | ||
| 466 | - {/if} | ||
| 467 | - </div> | ||
| 468 | - | ||
| 469 | - <!-- Fila 2: Clasificadores (cuando aplica) --> | ||
| 470 | - {#if showClassifierChips} | ||
| 471 | - <div class="filter-group filter-group-classifiers"> | ||
| 472 | - {#each activeClasificadores as clf} | ||
| 473 | - <button | ||
| 474 | - class="filter-chip filter-chip-classifier" | ||
| 475 | - class:filter-chip-active={selectedClassifiers.includes(clf.id)} | ||
| 476 | - class:filter-chip-disabled={clf.disabled} | ||
| 477 | - on:click={() => { if (!clf.disabled) toggleClassifier(clf.id); }} | ||
| 478 | - disabled={clf.disabled} | ||
| 479 | - > | ||
| 480 | - {clf.label} | ||
| 481 | - {#if clf.disabled} | ||
| 482 | - <span class="chip-soon">próx.</span> | ||
| 483 | - {/if} | ||
| 484 | - </button> | ||
| 485 | - {/each} | ||
| 486 | - </div> | ||
| 487 | - {/if} | ||
| 488 | - </div> | ||
| 489 | - | ||
| 490 | - <!-- ─── ÁREA DE RESULTADOS / EJEMPLOS / HINT ─── --> | ||
| 491 | - <div class="results-area"> | ||
| 492 | - | ||
| 493 | - <!-- Ejemplos --> | ||
| 494 | - <div class="examples" class:examples-hidden={isSearching}> | ||
| 495 | - <span class="examples-label">Prueba con:</span> | ||
| 496 | - {#each EXAMPLES as ex} | ||
| 497 | - <button | ||
| 498 | - class="example-chip" | ||
| 499 | - style="background:{ex.color}; color:{ex.textColor}" | ||
| 500 | - on:click={() => searchExample(ex.label)} | ||
| 501 | - > | ||
| 502 | - {ex.label} | ||
| 503 | - </button> | ||
| 504 | - {/each} | ||
| 505 | - </div> | ||
| 506 | - | ||
| 507 | - <!-- Hint diario --> | ||
| 508 | - {#if !isSearching && !dailyLoading} | ||
| 509 | - <a | ||
| 510 | - href="/vista-diaria" | ||
| 511 | - class="daily-hint" | ||
| 512 | - class:daily-hint-visible={!isSearching} | ||
| 513 | - > | ||
| 514 | - <span class="daily-dot"></span> | ||
| 515 | - <span class="daily-text"> | ||
| 516 | - {#if dailyData} | ||
| 517 | - Ayer el Estado ejecutó <strong>{formatMonto(dailyData.monto)}</strong> | ||
| 518 | - {:else} | ||
| 519 | - Ver el resumen de ejecución de ayer | ||
| 520 | - {/if} | ||
| 521 | - </span> | ||
| 522 | - <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 523 | - <path d="M5 12h14M12 5l7 7-7 7"/> | ||
| 524 | - </svg> | ||
| 525 | - </a> | ||
| 526 | - {/if} | ||
| 527 | - | ||
| 528 | - <!-- Estados de búsqueda --> | ||
| 529 | - {#if showMinLengthHint && searchVal.length > 0 && searchVal.length < 3} | ||
| 530 | - <div class="results-content"> | ||
| 531 | - <div class="results-hint">Escribe al menos 3 letras para buscar</div> | ||
| 532 | - </div> | ||
| 533 | - {:else if error && !results} | ||
| 534 | - <div class="results-content"> | ||
| 535 | - <div class="results-error">{error}</div> | ||
| 536 | - </div> | ||
| 537 | - {:else if isLoading && !results} | ||
| 538 | - <div class="results-content"> | ||
| 539 | - <div class="results-loading">Buscando...</div> | ||
| 540 | - </div> | ||
| 541 | - {:else if allHits.length > 0} | ||
| 542 | - <div class="results-content results-content-filled"> | ||
| 543 | - | ||
| 544 | - {#if stats} | ||
| 545 | - <div class="results-summary"> | ||
| 546 | - <div class="summary-row"> | ||
| 547 | - <div class="summary-left"> | ||
| 548 | - <div class="summary-header"> | ||
| 549 | - <span class="summary-count">{sortedHits.length}</span> | ||
| 550 | - <span class="summary-header-text">de {stats.found.toLocaleString()} resultados</span> | ||
| 551 | - {#if isLoading}<span class="updating-dot"></span>{/if} | ||
| 552 | - </div> | ||
| 553 | - <div class="summary-meta"> | ||
| 554 | - <span class="summary-tag summary-tag-gold"> | ||
| 555 | - {formatMonto(stats.montoTotal)} <span class="summary-period">· 2006–2025</span> | ||
| 556 | - </span> | ||
| 557 | - {#if stats.isClassifier} | ||
| 558 | - {#if stats.subareasCount} | ||
| 559 | - <span class="summary-tag summary-tag-entities">{stats.numEntidades} {stats.numEntidades === 1 ? 'entidad' : 'entidades'}</span> | ||
| 560 | - <span class="summary-arrow">→</span> | ||
| 561 | - {#each Object.entries(stats.subareasCount).sort((a,b) => b[1]-a[1]) as [sub, count]} | ||
| 562 | - <span class="summary-tag summary-tag-subarea">{sub}: {count}</span> | ||
| 563 | - {/each} | ||
| 564 | - {:else} | ||
| 565 | - <span class="summary-tag">{stats.found} resultados en {stats.classType}</span> | ||
| 566 | - {/if} | ||
| 567 | - {:else} | ||
| 568 | - <span class="summary-tag">{stats.numEntidades} {stats.numEntidades === 1 ? 'institución' : 'instituciones'}</span> | ||
| 569 | - {#if stats.yearRange} | ||
| 570 | - <span class="summary-tag">{stats.yearRange}</span> | ||
| 571 | - {/if} | ||
| 572 | - {/if} | ||
| 573 | - </div> | ||
| 574 | - </div> | ||
| 575 | - <div class="sort-buttons"> | ||
| 576 | - {#if !stats.isClassifier} | ||
| 577 | - <button class="sort-btn" class:sort-btn-active={sortBy === 'year'} on:click={() => toggleSort('year')}> | ||
| 578 | - Año | ||
| 579 | - {#if sortBy === 'year'} | ||
| 580 | - <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 581 | - {#if sortOrder === 'desc'}<path d="M12 5v14M5 12l7 7 7-7"/> | ||
| 582 | - {:else}<path d="M12 19V5M5 12l7-7 7 7"/>{/if} | ||
| 583 | - </svg> | ||
| 584 | - {/if} | ||
| 585 | - </button> | ||
| 586 | - {/if} | ||
| 587 | - <button class="sort-btn" class:sort-btn-active={sortBy === 'amount'} on:click={() => toggleSort('amount')}> | ||
| 588 | - Monto | ||
| 589 | - {#if sortBy === 'amount'} | ||
| 590 | - <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 591 | - {#if sortOrder === 'desc'}<path d="M12 5v14M5 12l7 7 7-7"/> | ||
| 592 | - {:else}<path d="M12 19V5M5 12l7-7 7 7"/>{/if} | ||
| 593 | - </svg> | ||
| 594 | - {/if} | ||
| 595 | - </button> | ||
| 596 | - </div> | ||
| 597 | - </div> | ||
| 598 | - </div> | ||
| 599 | - {/if} | ||
| 600 | - | ||
| 601 | - <div class="results-list"> | ||
| 602 | - {#each sortedHits as hit, i} | ||
| 603 | - {@const meta = parseMetadatos(hit.document.metadatos)} | ||
| 604 | - {@const isEntidadClass = hit.document.class_ === 'entidad'} | ||
| 605 | - {@const isDA = isEntidadClass && meta.da && meta.entidad_desc_entidad} | ||
| 606 | - {@const subarea = isEntidadClass ? extractAfterHyphen(meta.entidad_desc_subarea) : null} | ||
| 607 | - {@const parentEntity = isDA ? extractAfterHyphen(meta.entidad_desc_entidad) : null} | ||
| 608 | - {@const isClassResult = hit.document.is_class === true} | ||
| 609 | - <a | ||
| 610 | - href={isEntidadClass | ||
| 611 | - ? `/entidad/${meta.entidad}` | ||
| 612 | - : (isClassResult ? `/clasificador/${hit.document.class_}/${hit.document.id}` : `/proyecto/${generateCodigo(hit)}`)} | ||
| 613 | - class="result-card" | ||
| 614 | - style="--delay: {i * 30}ms" | ||
| 615 | - > | ||
| 616 | - <div class="result-content"> | ||
| 617 | - <div class="result-text"> | ||
| 618 | - {#if hit.highlights?.[0]?.snippet} | ||
| 619 | - {@html hit.highlights[0].snippet} | ||
| 620 | - {:else} | ||
| 621 | - {hit.document.texto} | ||
| 622 | - {/if} | ||
| 623 | - </div> | ||
| 624 | - <div class="result-meta"> | ||
| 625 | - {#if isEntidadClass} | ||
| 626 | - {#if isDA}<span class="result-da-badge">DA</span><span class="result-parent">de {parentEntity}</span>{/if} | ||
| 627 | - {#if subarea}<span class="result-subarea">{subarea}</span>{/if} | ||
| 628 | - {:else if isClassResult} | ||
| 629 | - <span class="result-class-type">{hit.document.class_}</span> | ||
| 630 | - {:else} | ||
| 631 | - <span class="result-year">{hit.document.gestion}</span> | ||
| 632 | - {/if} | ||
| 633 | - <span class="result-monto">{formatMontoFull(hit.document.devengado)}</span> | ||
| 634 | - </div> | ||
| 635 | - </div> | ||
| 636 | - <svg class="result-link-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 637 | - <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/> | ||
| 638 | - <polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/> | ||
| 639 | - </svg> | ||
| 640 | - </a> | ||
| 641 | - {/each} | ||
| 642 | - </div> | ||
| 643 | - | ||
| 644 | - {#if hasMoreResults} | ||
| 645 | - <div class="load-more-container"> | ||
| 646 | - <button class="load-more-btn" on:click={loadMore} disabled={isLoadingMore}> | ||
| 647 | - {#if isLoadingMore} | ||
| 648 | - <span class="loading-dot"></span> Cargando... | ||
| 649 | - {:else} | ||
| 650 | - Cargar más ({allHits.length} de {results.found}) | ||
| 651 | - {/if} | ||
| 652 | - </button> | ||
| 653 | - </div> | ||
| 654 | - {/if} | ||
| 655 | - </div> | ||
| 656 | - | ||
| 657 | - {:else if results && results.hits?.length === 0} | ||
| 658 | - <div class="results-content"> | ||
| 659 | - <div class="results-empty">Sin resultados para "{searchVal}"</div> | ||
| 660 | - </div> | ||
| 661 | - {/if} | ||
| 662 | - | ||
| 663 | - <div class="area-spacer" class:area-spacer-hidden={allHits.length > 0}></div> | ||
| 664 | - </div> | ||
| 665 | - </div> | ||
| 666 | - </div> | ||
| 667 | - | ||
| 668 | - <!-- Scroll hint --> | ||
| 669 | - <div class="scroll-hint anim" style="--d:800ms" class:scroll-hint-hidden={isSearching}> | ||
| 670 | - <svg class="scroll-icon-mouse" width="18" height="28" viewBox="0 0 18 28" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"> | ||
| 671 | - <rect x="1" y="1" width="16" height="26" rx="8"/><line x1="9" y1="7" x2="9" y2="12" opacity="0.7"/> | ||
| 672 | - </svg> | ||
| 673 | - <svg class="scroll-icon-touch" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 674 | - <path d="M6 9l6 6 6-6"/> | ||
| 675 | - </svg> | ||
| 676 | - <span>Explora la página</span> | ||
| 677 | - </div> | ||
| 678 | - | ||
| 679 | - </main> | ||
| 680 | -</div> | ||
| 681 | - | ||
| 682 | -<style> | ||
| 683 | - @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&display=swap'); | ||
| 684 | - | ||
| 685 | - :global(*) { margin: 0; padding: 0; box-sizing: border-box; } | ||
| 686 | - :global(html), :global(body) { overflow-x: hidden; } | ||
| 687 | - | ||
| 688 | - .page { | ||
| 689 | - --serif: 'Qanelas', system-ui, sans-serif; | ||
| 690 | - --sans: 'Qanelas', system-ui, sans-serif; | ||
| 691 | - --mono: 'JetBrains Mono', monospace; | ||
| 692 | - --gold: #D4B254; | ||
| 693 | - --gold-bright: #EAD06A; | ||
| 694 | - --gold-soft: rgba(212,178,84,0.15); | ||
| 695 | - --green: #4A8B6E; | ||
| 696 | - --bg: #1A1A18; | ||
| 697 | - --bg-elevated: #232321; | ||
| 698 | - --bg-surface: #2A2A28; | ||
| 699 | - --text: #F7F3EB; | ||
| 700 | - --text-secondary: #E0DCD4; | ||
| 701 | - --text-muted: #B5B1A9; | ||
| 702 | - --text-dim: #8C8880; | ||
| 703 | - | ||
| 704 | - font-family: var(--sans); | ||
| 705 | - background: var(--bg); | ||
| 706 | - color: var(--text); | ||
| 707 | - height: 100vh; | ||
| 708 | - max-height: 100vh; | ||
| 709 | - overflow: hidden; | ||
| 710 | - display: flex; | ||
| 711 | - flex-direction: column; | ||
| 712 | - -webkit-font-smoothing: antialiased; | ||
| 713 | - -moz-osx-font-smoothing: grayscale; | ||
| 714 | - text-rendering: optimizeLegibility; | ||
| 715 | - } | ||
| 716 | - | ||
| 717 | - @keyframes pulse { 0%,100% { opacity:0.4; } 50% { opacity:1; } } | ||
| 718 | - @keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0; } } | ||
| 719 | - | ||
| 720 | - /* ── Dots ── */ | ||
| 721 | - .section-dots { | ||
| 722 | - position: fixed; right: 24px; top: 50%; transform: translateY(-50%); | ||
| 723 | - z-index: 100; display: flex; flex-direction: column; gap: 12px; | ||
| 724 | - background: rgba(255,255,255,0.06); backdrop-filter: blur(8px); | ||
| 725 | - padding: 12px 8px; border-radius: 20px; | ||
| 726 | - } | ||
| 727 | - .section-dot { | ||
| 728 | - width:10px; height:10px; border-radius:50%; | ||
| 729 | - border:1.5px solid rgba(180,177,169,0.4); | ||
| 730 | - background:rgba(180,177,169,0.15); | ||
| 731 | - cursor:pointer; transition:all 0.3s; padding:0; position:relative; | ||
| 732 | - } | ||
| 733 | - .section-dot:hover { border-color:rgba(180,177,169,0.6); background:rgba(180,177,169,0.3); transform:scale(1.2); } | ||
| 734 | - .section-dot-active { background:var(--gold); border-color:var(--gold); } | ||
| 735 | - .section-dot-label { | ||
| 736 | - position:absolute; right:20px; top:50%; transform:translateY(-50%); | ||
| 737 | - font-family:var(--mono); font-size:10px; letter-spacing:0.05em; | ||
| 738 | - color:var(--text-dim); white-space:nowrap; opacity:0; transition:opacity 0.2s; pointer-events:none; | ||
| 739 | - } | ||
| 740 | - .section-dot:hover .section-dot-label { opacity:1; } | ||
| 741 | - | ||
| 742 | - /* ── Anim ── */ | ||
| 743 | - .anim { opacity:0; transform:translateY(18px); transition:all 0.8s cubic-bezier(0.16,1,0.3,1); transition-delay:var(--d,0ms); } | ||
| 744 | - .mounted .anim { opacity:1; transform:translateY(0); } | ||
| 745 | - | ||
| 746 | - /* ── Nav ── */ | ||
| 747 | - .nav-logo { position:absolute; top:24px; left:32px; z-index:50; opacity:0.75; pointer-events:none; } | ||
| 748 | - .nav-logo img { height:100px; width:auto; object-fit:contain; } | ||
| 749 | - .nav-menu-btn { | ||
| 750 | - position:absolute; top:24px; right:24px; z-index:50; | ||
| 751 | - background:none; border:none; cursor:pointer; color:var(--text-dim); | ||
| 752 | - opacity:0.6; padding:8px; border-radius:8px; transition:all 0.2s; | ||
| 753 | - } | ||
| 754 | - .nav-menu-btn:hover { opacity:1; color:var(--text); background:rgba(255,255,255,0.05); } | ||
| 755 | - | ||
| 756 | - /* ── Main ── */ | ||
| 757 | - main { | ||
| 758 | - flex:1; display:flex; flex-direction:column; align-items:center; | ||
| 759 | - justify-content:flex-start; padding:28vh 48px 0; | ||
| 760 | - position:relative; overflow:hidden; | ||
| 761 | - } | ||
| 762 | - | ||
| 763 | - .search-container { width:100%; max-width:820px; text-align:center; position:relative; } | ||
| 764 | - | ||
| 765 | - /* ── Título ── */ | ||
| 766 | - .hero-title { | ||
| 767 | - font-size:clamp(40px,5.5vw,64px); font-weight:900; | ||
| 768 | - line-height:1.05; letter-spacing:-0.035em; color:var(--text); | ||
| 769 | - margin-top:40px; transition:all 0.35s cubic-bezier(0.16,1,0.3,1); | ||
| 770 | - } | ||
| 771 | - .hero-title.hero-collapsed { opacity:0; height:0; overflow:hidden; margin:0; } | ||
| 772 | - | ||
| 773 | - .hero-sub { | ||
| 774 | - font-size:clamp(16px,1.6vw,18px); color:#A5A29A; | ||
| 775 | - margin-top:10px; margin-bottom:16px; line-height:1.6; | ||
| 776 | - transition:all 0.35s cubic-bezier(0.16,1,0.3,1); | ||
| 777 | - } | ||
| 778 | - .hero-sub.hero-collapsed { opacity:0; height:0; overflow:hidden; margin:0; } | ||
| 779 | - | ||
| 780 | - .hero-sub-link { color:inherit; text-decoration:none; border-bottom:1px dotted rgba(165,162,154,0.5); transition:all 0.2s; padding-bottom:1px; } | ||
| 781 | - .hero-sub-link:hover { color:#4A8B6E; border-bottom-color:#4A8B6E; } | ||
| 782 | - .gold { color:var(--gold); } | ||
| 783 | - | ||
| 784 | - /* ── Search bar ── */ | ||
| 785 | - .search-wrap { position:relative; border-radius:16px; transition:all 0.35s cubic-bezier(0.16,1,0.3,1); z-index:20; } | ||
| 786 | - .search-bar { | ||
| 787 | - display:flex; align-items:center; background:var(--bg-elevated); | ||
| 788 | - border-radius:16px; padding:8px 20px; | ||
| 789 | - border:1px solid transparent; transition:all 0.3s cubic-bezier(0.16,1,0.3,1); | ||
| 790 | - } | ||
| 791 | - .search-focused { background:var(--bg-surface); border:1px solid rgba(255,255,255,0.08); box-shadow:0 12px 40px rgba(0,0,0,0.25); } | ||
| 792 | - .search-bar.has-results { border-radius:16px 16px 0 0; background:var(--bg-surface); border:1px solid rgba(255,255,255,0.08); border-bottom:none; box-shadow:0 12px 40px rgba(0,0,0,0.25); } | ||
| 793 | - .search-icon { flex-shrink:0; transition:stroke 0.3s; } | ||
| 794 | - .search-input-wrap { flex:1; position:relative; display:flex; align-items:center; } | ||
| 795 | - .search-input-wrap input { | ||
| 796 | - width:100%; background:transparent; border:none; outline:none; | ||
| 797 | - color:var(--text); font-size:17px; font-weight:500; font-family:var(--sans); | ||
| 798 | - padding:14px 16px 14px 10px; caret-color:var(--gold); letter-spacing:-0.01em; | ||
| 799 | - } | ||
| 800 | - .search-placeholder { position:absolute; left:10px; top:50%; transform:translateY(-50%); display:flex; align-items:center; pointer-events:none; } | ||
| 801 | - .blink-cursor { width:2px; height:20px; background:var(--gold); margin-right:4px; animation:blink 1s step-end infinite; border-radius:1px; flex-shrink:0; } | ||
| 802 | - .blink-cursor.cursor-hidden { opacity:0; animation:none; } | ||
| 803 | - .placeholder-text { font-family:var(--sans); font-size:16px; color:#B8B5AD; } | ||
| 804 | - .search-clear { background:rgba(255,255,255,0.08); border:none; border-radius:8px; width:32px; height:32px; display:flex; align-items:center; justify-content:center; cursor:pointer; color:var(--text-muted); transition:all 0.2s; } | ||
| 805 | - .search-clear:hover { background:rgba(255,255,255,0.15); color:var(--text); } | ||
| 806 | - .search-loading { display:flex; align-items:center; padding-right:8px; } | ||
| 807 | - .loading-dot { width:8px; height:8px; border-radius:50%; background:var(--gold); animation:pulse 1s ease-in-out infinite; } | ||
| 808 | - | ||
| 809 | - /* ───────────────────────────────────────── | ||
| 810 | - FILTROS INLINE — el cambio principal | ||
| 811 | - ───────────────────────────────────────── */ | ||
| 812 | - .filters-row { | ||
| 813 | - display:flex; flex-direction:column; gap:8px; | ||
| 814 | - padding:10px 2px 0; | ||
| 815 | - transition:opacity 0.25s ease, max-height 0.35s cubic-bezier(0.16,1,0.3,1); | ||
| 816 | - max-height:120px; overflow:hidden; | ||
| 817 | - } | ||
| 818 | - .filters-hidden { opacity:0; max-height:0; pointer-events:none; } | ||
| 819 | - | ||
| 820 | - .filter-group { | ||
| 821 | - display:flex; align-items:center; gap:6px; flex-wrap:wrap; | ||
| 822 | - justify-content:center; | ||
| 823 | - } | ||
| 824 | - .filter-group-classifiers { padding-left:0; } | ||
| 825 | - | ||
| 826 | - .filter-divider { | ||
| 827 | - width:1px; height:14px; background:rgba(255,255,255,0.12); | ||
| 828 | - margin:0 4px; flex-shrink:0; | ||
| 829 | - } | ||
| 830 | - | ||
| 831 | - .filter-chip { | ||
| 832 | - font-family:var(--sans); font-size:12px; font-weight:500; | ||
| 833 | - padding:5px 12px; border-radius:20px; border:1px solid rgba(255,255,255,0.1); | ||
| 834 | - background:rgba(255,255,255,0.04); color:var(--text-muted); | ||
| 835 | - cursor:pointer; transition:all 0.18s; white-space:nowrap; | ||
| 836 | - display:flex; align-items:center; gap:5px; | ||
| 837 | - } | ||
| 838 | - .filter-chip:hover:not(:disabled) { | ||
| 839 | - background:rgba(255,255,255,0.08); color:var(--text); | ||
| 840 | - border-color:rgba(255,255,255,0.18); | ||
| 841 | - } | ||
| 842 | - .filter-chip-active { | ||
| 843 | - background:rgba(212,178,84,0.12); color:var(--gold); | ||
| 844 | - border-color:rgba(212,178,84,0.35); | ||
| 845 | - } | ||
| 846 | - .filter-chip-active:hover { background:rgba(212,178,84,0.18); } | ||
| 847 | - | ||
| 848 | - .filter-chip-mode { font-weight:600; font-size:12.5px; } | ||
| 849 | - | ||
| 850 | - .filter-chip-disabled { | ||
| 851 | - opacity:0.4; cursor:default; pointer-events:none; | ||
| 852 | - } | ||
| 853 | - | ||
| 854 | - .chip-soon { | ||
| 855 | - font-family:var(--mono); font-size:9px; letter-spacing:0.04em; | ||
| 856 | - color:var(--text-dim); background:rgba(255,255,255,0.06); | ||
| 857 | - padding:1px 5px; border-radius:4px; | ||
| 858 | - } | ||
| 859 | - | ||
| 860 | - /* ───────────────────────────────────────── | ||
| 861 | - HINT DIARIO | ||
| 862 | - ───────────────────────────────────────── */ | ||
| 863 | - .daily-hint { | ||
| 864 | - display:inline-flex; align-items:center; gap:8px; | ||
| 865 | - padding:8px 16px; border-radius:20px; | ||
| 866 | - border:1px solid rgba(74,139,110,0.25); | ||
| 867 | - background:rgba(74,139,110,0.07); | ||
| 868 | - color:var(--text-muted); text-decoration:none; | ||
| 869 | - font-size:13px; transition:all 0.2s; | ||
| 870 | - opacity:0; pointer-events:none; | ||
| 871 | - transform:translateY(4px); | ||
| 872 | - } | ||
| 873 | - .daily-hint-visible { opacity:1; pointer-events:auto; transform:translateY(0); transition:opacity 0.4s ease 0.1s, transform 0.4s ease 0.1s, background 0.2s, border-color 0.2s; } | ||
| 874 | - .daily-hint:hover { background:rgba(74,139,110,0.12); border-color:rgba(74,139,110,0.4); color:var(--text); } | ||
| 875 | - .daily-hint strong { color:#6BBF97; font-weight:600; } | ||
| 876 | - | ||
| 877 | - .daily-dot { | ||
| 878 | - width:7px; height:7px; border-radius:50%; | ||
| 879 | - background:#4A8B6E; flex-shrink:0; | ||
| 880 | - box-shadow:0 0 0 3px rgba(74,139,110,0.2); | ||
| 881 | - animation:pulse 2s ease-in-out infinite; | ||
| 882 | - } | ||
| 883 | - | ||
| 884 | - /* ── Results area ── */ | ||
| 885 | - .results-area { margin-top:16px; min-height:240px; position:relative; } | ||
| 886 | - .area-spacer { position:absolute; bottom:0; left:50%; transform:translateX(-50%); height:80px; pointer-events:none; transition:opacity 0.4s; } | ||
| 887 | - .area-spacer-hidden { opacity:0; } | ||
| 888 | - | ||
| 889 | - /* ── Examples ── */ | ||
| 890 | - .examples { | ||
| 891 | - display:flex; align-items:center; justify-content:center; | ||
| 892 | - gap:10px; flex-wrap:wrap; padding:12px 0 16px; | ||
| 893 | - transition:opacity 0.25s ease; | ||
| 894 | - } | ||
| 895 | - .examples-label { font-size:13px; color:var(--text-dim); } | ||
| 896 | - .example-chip { font-family:var(--sans); font-size:13px; padding:8px 14px; border-radius:20px; border:none; cursor:pointer; transition:all 0.2s; font-weight:500; } | ||
| 897 | - .example-chip:hover { transform:translateY(-1px); filter:brightness(1.1); } | ||
| 898 | - .examples-hidden { opacity:0; pointer-events:none; } | ||
| 899 | - | ||
| 900 | - /* ── Results ── */ | ||
| 901 | - .results-content { position:absolute; top:-16px; left:0; right:0; z-index:10; text-align:left; } | ||
| 902 | - .results-content-filled { | ||
| 903 | - background:var(--bg-surface); border-radius:0 0 16px 16px; | ||
| 904 | - border:1px solid rgba(255,255,255,0.08); border-top:none; | ||
| 905 | - box-shadow:0 12px 40px rgba(0,0,0,0.25); max-height:450px; overflow-y:auto; | ||
| 906 | - } | ||
| 907 | - .results-loading, .results-error, .results-empty, .results-hint { | ||
| 908 | - padding:16px 24px; text-align:center; color:var(--text-muted); font-size:14px; line-height:1.5; | ||
| 909 | - } | ||
| 910 | - .results-error { color:#F28B82; } | ||
| 911 | - .results-empty { color:var(--text-dim); } | ||
| 912 | - | ||
| 913 | - /* Summary */ | ||
| 914 | - .results-summary { padding:12px 16px; border-bottom:1px solid rgba(255,255,255,0.06); } | ||
| 915 | - .summary-row { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; } | ||
| 916 | - .summary-left { flex:1; min-width:0; } | ||
| 917 | - .summary-header { display:flex; align-items:baseline; gap:6px; margin-bottom:4px; flex-wrap:wrap; } | ||
| 918 | - .summary-count { font-weight:700; color:var(--text); font-size:20px; font-family:var(--mono); letter-spacing:-0.02em; } | ||
| 919 | - .summary-header-text { color:var(--text-secondary); font-size:15px; } | ||
| 920 | - .updating-dot { display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--gold); animation:pulse 0.8s ease-in-out infinite; margin-left:6px; } | ||
| 921 | - .summary-meta { display:flex; flex-wrap:wrap; gap:6px; align-items:center; } | ||
| 922 | - .summary-tag { font-family:var(--mono); font-size:12px; color:var(--text-dim); background:rgba(255,255,255,0.05); padding:3px 8px; border-radius:4px; } | ||
| 923 | - .summary-tag-gold { color:var(--gold); background:rgba(212,178,84,0.1); } | ||
| 924 | - .summary-tag-entities { color:#7EB8D8; background:rgba(126,184,216,0.1); } | ||
| 925 | - .summary-tag-subarea { color:#A8C4A0; background:rgba(168,196,160,0.1); } | ||
| 926 | - .summary-period { color:var(--text-dim); font-weight:300; } | ||
| 927 | - .summary-arrow { color:var(--text-dim); font-size:12px; } | ||
| 928 | - | ||
| 929 | - /* Sort */ | ||
| 930 | - .sort-buttons { display:flex; gap:6px; flex-shrink:0; align-items:flex-start; padding-top:2px; } | ||
| 931 | - .sort-btn { display:flex; align-items:center; gap:4px; padding:5px 10px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.08); border-radius:6px; font-family:var(--mono); font-size:11px; color:var(--text-muted); cursor:pointer; transition:all 0.15s; } | ||
| 932 | - .sort-btn:hover { background:rgba(255,255,255,0.09); color:var(--text); } | ||
| 933 | - .sort-btn-active { background:rgba(212,178,84,0.1); border-color:rgba(212,178,84,0.3); color:var(--gold); } | ||
| 934 | - | ||
| 935 | - /* Results list */ | ||
| 936 | - .results-list { padding:4px 0; } | ||
| 937 | - .result-card { display:flex; align-items:center; gap:10px; padding:10px 16px; text-decoration:none; color:inherit; border-left:3px solid transparent; transition:all 0.2s ease; animation:fadeSlideIn 0.25s ease both; animation-delay:var(--delay,0ms); } | ||
| 938 | - @keyframes fadeSlideIn { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } } | ||
| 939 | - .result-card:hover { background:rgba(255,255,255,0.04); border-left-color:var(--gold); transform:translateX(4px); } | ||
| 940 | - .result-link-icon { flex-shrink:0; opacity:0; color:var(--gold); transition:opacity 0.2s; } | ||
| 941 | - .result-card:hover .result-link-icon { opacity:1; } | ||
| 942 | - .result-content { flex:1; min-width:0; } | ||
| 943 | - .result-text { font-size:14px; color:var(--text); line-height:1.4; margin-bottom:4px; } | ||
| 944 | - .result-text :global(mark) { background:#F5E047; color:#1A1A18; padding:1px 4px; margin:0 1px; border-radius:3px; font-weight:600; box-decoration-break:clone; -webkit-box-decoration-break:clone; } | ||
| 945 | - .result-meta { display:flex; flex-wrap:wrap; gap:10px; font-family:var(--mono); font-size:12px; } | ||
| 946 | - .result-year { color:var(--text-dim); } | ||
| 947 | - .result-monto { color:var(--gold); font-weight:500; } | ||
| 948 | - .result-da-badge { background:rgba(212,133,106,0.2); color:#D4856A; padding:2px 6px; border-radius:4px; font-size:10px; font-weight:600; letter-spacing:0.05em; } | ||
| 949 | - .result-parent { color:var(--text-muted); font-size:11px; } | ||
| 950 | - .result-subarea { color:#7EB8D8; font-size:11px; } | ||
| 951 | - .result-class-type { color:var(--text-dim); font-size:10px; text-transform:uppercase; letter-spacing:0.05em; } | ||
| 952 | - | ||
| 953 | - /* Load more */ | ||
| 954 | - .load-more-container { padding:8px 16px 16px; border-top:1px solid rgba(255,255,255,0.04); margin-top:4px; } | ||
| 955 | - .load-more-btn { display:flex; align-items:center; justify-content:center; gap:8px; width:100%; padding:12px; background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.08); border-radius:8px; font-family:var(--mono); font-size:13px; color:var(--text-muted); cursor:pointer; transition:all 0.2s; } | ||
| 956 | - .load-more-btn:hover:not(:disabled) { background:rgba(255,255,255,0.08); border-color:rgba(255,255,255,0.12); color:var(--text); } | ||
| 957 | - .load-more-btn:disabled { cursor:wait; opacity:0.7; } | ||
| 958 | - .load-more-btn .loading-dot { width:6px; height:6px; } | ||
| 959 | - | ||
| 960 | - /* Scroll hint */ | ||
| 961 | - .scroll-hint { position:absolute; bottom:28px; left:0; right:0; margin:0 auto; width:fit-content; display:flex; flex-direction:column; align-items:center; gap:10px; color:var(--text-dim); animation:scrollHint 2.5s ease-in-out infinite; z-index:1; transition:opacity 0.3s; } | ||
| 962 | - .scroll-hint-hidden { opacity:0; pointer-events:none; } | ||
| 963 | - .scroll-hint span { font-family:var(--mono); font-size:10px; letter-spacing:0.2em; text-transform:uppercase; opacity:0.7; } | ||
| 964 | - .scroll-icon-mouse { display:block; } | ||
| 965 | - .scroll-icon-touch { display:none; } | ||
| 966 | - @keyframes scrollHint { 0%,100% { transform:translateY(0); opacity:0.6; } 50% { transform:translateY(5px); opacity:1; } } | ||
| 967 | - | ||
| 968 | - /* ── Mobile ── */ | ||
| 969 | - @media (max-width: 768px) { | ||
| 970 | - .page { height:100vh; height:100dvh; } | ||
| 971 | - .section-dots { display:none; } | ||
| 972 | - .nav-logo { top:16px; left:20px; opacity:1; } | ||
| 973 | - .nav-logo img { height:60px; } | ||
| 974 | - .nav-menu-btn { top:16px; right:16px; } | ||
| 975 | - main { padding:0 20px; justify-content:center; min-height:100%; } | ||
| 976 | - .search-container { padding-top:80px; } | ||
| 977 | - .hero-title { margin-top:0; font-size:clamp(28px,8vw,40px); } | ||
| 978 | - .hero-sub { font-size:14px; margin-bottom:12px; } | ||
| 979 | - .search-bar { padding:6px 12px; } | ||
| 980 | - .search-input-wrap input { font-size:14px; padding:10px 10px 10px 6px; } | ||
| 981 | - .search-placeholder { left:6px; } | ||
| 982 | - .placeholder-text { font-size:13px; } | ||
| 983 | - .search-icon { width:18px; height:18px; } | ||
| 984 | - .filters-row { gap:6px; padding-top:8px; } | ||
| 985 | - .filter-chip { font-size:11px; padding:4px 10px; } | ||
| 986 | - .filter-chip-mode { font-size:11.5px; } | ||
| 987 | - .examples { gap:8px; } | ||
| 988 | - .example-chip { font-size:12px; padding:6px 12px; } | ||
| 989 | - .daily-hint { font-size:12px; padding:7px 12px; } | ||
| 990 | - .results-area { min-height:200px; } | ||
| 991 | - .results-content-filled { max-height:45vh; } | ||
| 992 | - .result-card { padding:8px 14px; } | ||
| 993 | - .result-card:hover { transform:none; } | ||
| 994 | - .result-text { font-size:14px; line-height:1.45; } | ||
| 995 | - .result-meta { font-size:12px; gap:10px; } | ||
| 996 | - .summary-count { font-size:16px; } | ||
| 997 | - .summary-meta { font-size:12px; } | ||
| 998 | - .scroll-icon-mouse { display:none; } | ||
| 999 | - .scroll-icon-touch { display:block; } | ||
| 1000 | - .scroll-hint { position:fixed; bottom:24px; } | ||
| 1001 | - } | ||
| 1002 | - | ||
| 1003 | - @media (max-width: 400px) { | ||
| 1004 | - .nav-logo img { height:50px; } | ||
| 1005 | - main { padding:0 16px; } | ||
| 1006 | - .search-container { padding-top:70px; } | ||
| 1007 | - .search-bar { padding:5px 10px; } | ||
| 1008 | - .search-input-wrap input { font-size:13px; padding:8px 8px 8px 4px; } | ||
| 1009 | - .placeholder-text { font-size:12px; } | ||
| 1010 | - .hero-title { font-size:26px; } | ||
| 1011 | - .hero-sub { font-size:13px; margin-bottom:10px; } | ||
| 1012 | - .results-area { min-height:180px; } | ||
| 1013 | - .results-content-filled { max-height:40vh; } | ||
| 1014 | - .scroll-hint { display:none; } | ||
| 1015 | - .examples { flex-direction:column; gap:6px; } | ||
| 1016 | - .filter-group { gap:5px; } | ||
| 1017 | - .filter-divider { display:none; } | ||
| 1018 | - } | ||
| 1019 | -</style> |
This diff could not be displayed because it is too large.
src/routes/test/+page.svelte
deleted
100644 → 0
| 1 | -<script> | ||
| 2 | - import { onMount } from 'svelte'; | ||
| 3 | - import * as d3 from 'd3'; | ||
| 4 | - import * as Plot from '@observablehq/plot'; | ||
| 5 | - | ||
| 6 | - // Estado reactivo | ||
| 7 | - let codigoSeleccionado = $state('1901'); | ||
| 8 | - let gestionSeleccionada = $state(2025); | ||
| 9 | - let resumenData = $state([]); | ||
| 10 | - let distribucionesData = $state([]); | ||
| 11 | - let codigosDisponibles = $state([]); | ||
| 12 | - let cargando = $state(true); | ||
| 13 | - | ||
| 14 | - // Datos derivados | ||
| 15 | - let tipoCodigo = $derived.by(() => { | ||
| 16 | - const row = resumenData.find(d => String(d.codigo) === codigoSeleccionado); | ||
| 17 | - return row?.tipo_codigo || 'entidad'; | ||
| 18 | - }); | ||
| 19 | - | ||
| 20 | - let esEntidad = $derived(tipoCodigo === 'entidad'); | ||
| 21 | - | ||
| 22 | - let resumenFiltrado = $derived( | ||
| 23 | - resumenData.filter(d => String(d.codigo) === codigoSeleccionado) | ||
| 24 | - ); | ||
| 25 | - | ||
| 26 | - let historiaGastos = $derived( | ||
| 27 | - resumenFiltrado | ||
| 28 | - .filter(d => d.tipo === 'gastos') | ||
| 29 | - .sort((a, b) => a.gestion - b.gestion) | ||
| 30 | - ); | ||
| 31 | - | ||
| 32 | - let historiaIngresos = $derived( | ||
| 33 | - resumenFiltrado | ||
| 34 | - .filter(d => d.tipo === 'ingresos') | ||
| 35 | - .sort((a, b) => a.gestion - b.gestion) | ||
| 36 | - ); | ||
| 37 | - | ||
| 38 | - let gestiones = $derived(() => { | ||
| 39 | - const years = [...new Set(resumenFiltrado.map(d => d.gestion))].sort(); | ||
| 40 | - return years; | ||
| 41 | - }); | ||
| 42 | - | ||
| 43 | - let rankingGastos = $derived(() => { | ||
| 44 | - const row = historiaGastos.find(d => d.gestion === gestionSeleccionada); | ||
| 45 | - return row ? { posicion: row.ranking, total: '~600' } : null; | ||
| 46 | - }); | ||
| 47 | - | ||
| 48 | - let rankingIngresos = $derived(() => { | ||
| 49 | - const row = historiaIngresos.find(d => d.gestion === gestionSeleccionada); | ||
| 50 | - return row ? { posicion: row.ranking, total: '~600' } : null; | ||
| 51 | - }); | ||
| 52 | - | ||
| 53 | - // Distribuciones filtradas por codigo y gestion | ||
| 54 | - let distFiltradas = $derived( | ||
| 55 | - distribucionesData.filter(d => | ||
| 56 | - String(d.codigo) === codigoSeleccionado && | ||
| 57 | - d.gestion === gestionSeleccionada | ||
| 58 | - ) | ||
| 59 | - ); | ||
| 60 | - | ||
| 61 | - // Clasificadores: cada hijo es un segmento de la barra | ||
| 62 | - function prepararSegmentos(data) { | ||
| 63 | - return data | ||
| 64 | - .map(d => ({ | ||
| 65 | - codigo: d.hijo, | ||
| 66 | - nombre: d.desc_hijo, | ||
| 67 | - monto: d.devengado, | ||
| 68 | - padre: d.desc_padre | ||
| 69 | - })) | ||
| 70 | - .filter(d => d.monto > 0) | ||
| 71 | - .sort((a, b) => b.monto - a.monto); | ||
| 72 | - } | ||
| 73 | - | ||
| 74 | - let clasificadoresGasto = $derived.by(() => { | ||
| 75 | - const gastos = distFiltradas.filter(d => d.tipo === 'gastos'); | ||
| 76 | - const dims = [ | ||
| 77 | - { key: 'objeto', label: 'Objetos de gasto' }, | ||
| 78 | - { key: 'finfun', label: 'Finalidad y función' }, | ||
| 79 | - { key: 'acteco', label: 'Sectores económicos' } | ||
| 80 | - ]; | ||
| 81 | - return dims | ||
| 82 | - .map(dim => ({ | ||
| 83 | - ...dim, | ||
| 84 | - data: prepararSegmentos(gastos.filter(d => d.dimension === dim.key)) | ||
| 85 | - })) | ||
| 86 | - .filter(dim => dim.data.length > 0); | ||
| 87 | - }); | ||
| 88 | - | ||
| 89 | - let clasificadoresIngreso = $derived.by(() => { | ||
| 90 | - if (!esEntidad) return []; | ||
| 91 | - const ingresos = distFiltradas.filter(d => d.tipo === 'ingresos'); | ||
| 92 | - const dims = [ | ||
| 93 | - { key: 'rubro', label: 'Rubros de ingreso' }, | ||
| 94 | - { key: 'organismo', label: 'Organismos financiadores' } | ||
| 95 | - ]; | ||
| 96 | - return dims | ||
| 97 | - .map(dim => ({ | ||
| 98 | - ...dim, | ||
| 99 | - data: prepararSegmentos(ingresos.filter(d => d.dimension === dim.key)) | ||
| 100 | - })) | ||
| 101 | - .filter(dim => dim.data.length > 0); | ||
| 102 | - }); | ||
| 103 | - | ||
| 104 | - // Contenedores D3 | ||
| 105 | - let barContainers = $state({}); | ||
| 106 | - let hoverData = $state({}); | ||
| 107 | - let chartIngresosContainer = $state(null); | ||
| 108 | - let chartGastosContainer = $state(null); | ||
| 109 | - | ||
| 110 | - // Totales | ||
| 111 | - function getTotal(data) { | ||
| 112 | - return data.reduce((sum, d) => sum + d.monto, 0); | ||
| 113 | - } | ||
| 114 | - | ||
| 115 | - // Formatear montos | ||
| 116 | - function formatearMonto(valor) { | ||
| 117 | - if (valor >= 1e9) return `${(valor / 1e9).toFixed(1)} mil millones`; | ||
| 118 | - if (valor >= 1e6) return `${(valor / 1e6).toFixed(0)} millones`; | ||
| 119 | - if (valor >= 1e3) return `${(valor / 1e3).toFixed(0)} mil`; | ||
| 120 | - return valor.toFixed(0); | ||
| 121 | - } | ||
| 122 | - | ||
| 123 | - function formatearMontoCorto(valor) { | ||
| 124 | - if (valor >= 1e9) return `${(valor / 1e9).toFixed(1)}B`; | ||
| 125 | - if (valor >= 1e6) return `${(valor / 1e6).toFixed(0)}M`; | ||
| 126 | - if (valor >= 1e3) return `${(valor / 1e3).toFixed(0)}K`; | ||
| 127 | - return valor.toFixed(0); | ||
| 128 | - } | ||
| 129 | - | ||
| 130 | - // Colores del tema | ||
| 131 | - function getChartColors() { | ||
| 132 | - const root = document.documentElement; | ||
| 133 | - const isDark = root.classList.contains('dark'); | ||
| 134 | - return { | ||
| 135 | - barDefault: isDark ? '#3a3a38' : '#e0e0e0', | ||
| 136 | - barHighlight: isDark ? '#C9A751' : '#F3EAD8', | ||
| 137 | - barStroke: isDark ? '#5a5a55' : '#b0b0b0', | ||
| 138 | - barStrokeDefault: isDark ? '#2a2a28' : 'white' | ||
| 139 | - }; | ||
| 140 | - } | ||
| 141 | - | ||
| 142 | - // Renderizar barras D3 | ||
| 143 | - function renderizarBarras(key, data) { | ||
| 144 | - const container = barContainers[key]; | ||
| 145 | - if (!container || data.length === 0) return; | ||
| 146 | - | ||
| 147 | - const total = getTotal(data); | ||
| 148 | - const height = 48; | ||
| 149 | - const radius = 4; | ||
| 150 | - | ||
| 151 | - d3.select(container).selectAll('*').remove(); | ||
| 152 | - | ||
| 153 | - const containerRect = container.getBoundingClientRect(); | ||
| 154 | - const width = containerRect.width || 800; | ||
| 155 | - | ||
| 156 | - const svg = d3.select(container) | ||
| 157 | - .append('svg') | ||
| 158 | - .attr('width', '100%') | ||
| 159 | - .attr('height', height) | ||
| 160 | - .attr('viewBox', `0 0 ${width} ${height}`) | ||
| 161 | - .attr('preserveAspectRatio', 'none') | ||
| 162 | - .style('display', 'block'); | ||
| 163 | - | ||
| 164 | - let cumulative = 0; | ||
| 165 | - const segments = data.map(d => { | ||
| 166 | - const segWidth = (d.monto / total) * width; | ||
| 167 | - const segment = { ...d, x: cumulative, width: segWidth }; | ||
| 168 | - cumulative += segWidth; | ||
| 169 | - return segment; | ||
| 170 | - }); | ||
| 171 | - | ||
| 172 | - const chartColors = getChartColors(); | ||
| 173 | - const colorDefault = chartColors.barDefault; | ||
| 174 | - const colorHover = chartColors.barHighlight; | ||
| 175 | - const colorFirst = chartColors.barHighlight; | ||
| 176 | - | ||
| 177 | - const bars = svg.selectAll('rect') | ||
| 178 | - .data(segments) | ||
| 179 | - .enter() | ||
| 180 | - .append('rect') | ||
| 181 | - .attr('x', d => d.x) | ||
| 182 | - .attr('y', 2) | ||
| 183 | - .attr('width', d => Math.max(d.width - 1, 1)) | ||
| 184 | - .attr('height', height - 4) | ||
| 185 | - .attr('rx', (d, i) => { | ||
| 186 | - if (i === 0) return radius; | ||
| 187 | - if (i === segments.length - 1) return radius; | ||
| 188 | - return 0; | ||
| 189 | - }) | ||
| 190 | - .attr('fill', (d, i) => i === 0 ? colorFirst : colorDefault) | ||
| 191 | - .attr('stroke', (d, i) => i === 0 ? chartColors.barStroke : chartColors.barStrokeDefault) | ||
| 192 | - .attr('stroke-width', (d, i) => i === 0 ? 1 : 0.5) | ||
| 193 | - .style('cursor', 'pointer') | ||
| 194 | - .on('mouseenter', function(event, d) { | ||
| 195 | - bars | ||
| 196 | - .attr('fill', colorDefault) | ||
| 197 | - .attr('stroke', chartColors.barStrokeDefault) | ||
| 198 | - .attr('stroke-width', 0.5); | ||
| 199 | - d3.select(this) | ||
| 200 | - .attr('fill', colorHover) | ||
| 201 | - .attr('stroke', chartColors.barStroke) | ||
| 202 | - .attr('stroke-width', 1); | ||
| 203 | - hoverData[key] = d; | ||
| 204 | - hoverData = { ...hoverData }; | ||
| 205 | - }) | ||
| 206 | - .on('mouseleave', function() { | ||
| 207 | - bars | ||
| 208 | - .attr('fill', (d, i) => i === 0 ? colorFirst : colorDefault) | ||
| 209 | - .attr('stroke', (d, i) => i === 0 ? chartColors.barStroke : chartColors.barStrokeDefault) | ||
| 210 | - .attr('stroke-width', (d, i) => i === 0 ? 1 : 0.5); | ||
| 211 | - hoverData[key] = null; | ||
| 212 | - hoverData = { ...hoverData }; | ||
| 213 | - }); | ||
| 214 | - } | ||
| 215 | - | ||
| 216 | - // Item a mostrar (hover o primero) | ||
| 217 | - function getDisplayItem(key, data) { | ||
| 218 | - const hover = hoverData[key]; | ||
| 219 | - if (hover) return hover; | ||
| 220 | - return data[0] || null; | ||
| 221 | - } | ||
| 222 | - | ||
| 223 | - // Renderizar gráficos de historia | ||
| 224 | - function renderizarHistoria() { | ||
| 225 | - if (esEntidad && chartIngresosContainer && historiaIngresos.length > 0) { | ||
| 226 | - chartIngresosContainer.innerHTML = ''; | ||
| 227 | - const plot = Plot.plot({ | ||
| 228 | - width: chartIngresosContainer.clientWidth, | ||
| 229 | - height: 180, | ||
| 230 | - marginLeft: 50, | ||
| 231 | - marginRight: 20, | ||
| 232 | - marginTop: 20, | ||
| 233 | - marginBottom: 30, | ||
| 234 | - style: { background: 'transparent', fontFamily: 'var(--font-sans)' }, | ||
| 235 | - x: { label: null, tickFormat: d => d }, | ||
| 236 | - y: { label: null, tickFormat: d => formatearMontoCorto(d), grid: true }, | ||
| 237 | - marks: [ | ||
| 238 | - Plot.areaY(historiaIngresos, { x: 'gestion', y: 'devengado', fill: '#4A8B6E', fillOpacity: 0.3 }), | ||
| 239 | - Plot.lineY(historiaIngresos, { x: 'gestion', y: 'devengado', stroke: '#4A8B6E', strokeWidth: 2 }), | ||
| 240 | - Plot.dot(historiaIngresos, { x: 'gestion', y: 'devengado', fill: '#4A8B6E', r: 3 }) | ||
| 241 | - ] | ||
| 242 | - }); | ||
| 243 | - chartIngresosContainer.appendChild(plot); | ||
| 244 | - } | ||
| 245 | - | ||
| 246 | - if (chartGastosContainer && historiaGastos.length > 0) { | ||
| 247 | - chartGastosContainer.innerHTML = ''; | ||
| 248 | - const plot = Plot.plot({ | ||
| 249 | - width: chartGastosContainer.clientWidth, | ||
| 250 | - height: 180, | ||
| 251 | - marginLeft: 50, | ||
| 252 | - marginRight: 20, | ||
| 253 | - marginTop: 20, | ||
| 254 | - marginBottom: 30, | ||
| 255 | - style: { background: 'transparent', fontFamily: 'var(--font-sans)' }, | ||
| 256 | - x: { label: null, tickFormat: d => d }, | ||
| 257 | - y: { label: null, tickFormat: d => formatearMontoCorto(d), grid: true }, | ||
| 258 | - marks: [ | ||
| 259 | - Plot.areaY(historiaGastos, { x: 'gestion', y: 'devengado', fill: '#C9A751', fillOpacity: 0.3 }), | ||
| 260 | - Plot.lineY(historiaGastos, { x: 'gestion', y: 'devengado', stroke: '#C9A751', strokeWidth: 2 }), | ||
| 261 | - Plot.dot(historiaGastos, { x: 'gestion', y: 'devengado', fill: '#C9A751', r: 3 }) | ||
| 262 | - ] | ||
| 263 | - }); | ||
| 264 | - chartGastosContainer.appendChild(plot); | ||
| 265 | - } | ||
| 266 | - } | ||
| 267 | - | ||
| 268 | - // Renderizar todas las barras | ||
| 269 | - function renderizarTodasLasBarras() { | ||
| 270 | - setTimeout(() => { | ||
| 271 | - clasificadoresGasto.forEach(({ key, data }) => { | ||
| 272 | - if (barContainers[key]) renderizarBarras(key, data); | ||
| 273 | - }); | ||
| 274 | - clasificadoresIngreso.forEach(({ key, data }) => { | ||
| 275 | - if (barContainers[key]) renderizarBarras(key, data); | ||
| 276 | - }); | ||
| 277 | - }, 100); | ||
| 278 | - } | ||
| 279 | - | ||
| 280 | - // Efectos | ||
| 281 | - $effect(() => { | ||
| 282 | - // Trigger on data changes | ||
| 283 | - clasificadoresGasto; | ||
| 284 | - clasificadoresIngreso; | ||
| 285 | - renderizarTodasLasBarras(); | ||
| 286 | - }); | ||
| 287 | - | ||
| 288 | - $effect(() => { | ||
| 289 | - if (chartGastosContainer) { | ||
| 290 | - // Trigger on history data changes | ||
| 291 | - historiaGastos; | ||
| 292 | - historiaIngresos; | ||
| 293 | - esEntidad; | ||
| 294 | - setTimeout(renderizarHistoria, 150); | ||
| 295 | - } | ||
| 296 | - }); | ||
| 297 | - | ||
| 298 | - onMount(async () => { | ||
| 299 | - // Cargar CSVs | ||
| 300 | - const [resumenRaw, distRaw] = await Promise.all([ | ||
| 301 | - fetch('/resumen.csv').then(r => r.text()), | ||
| 302 | - fetch('/distribuciones.csv').then(r => r.text()) | ||
| 303 | - ]); | ||
| 304 | - | ||
| 305 | - resumenData = d3.csvParse(resumenRaw, d => ({ | ||
| 306 | - tipo: d.tipo, | ||
| 307 | - codigo: d.codigo, | ||
| 308 | - gestion: +d.gestion, | ||
| 309 | - devengado: +d.devengado, | ||
| 310 | - ranking: +d.ranking, | ||
| 311 | - tipo_codigo: d.tipo_codigo | ||
| 312 | - })); | ||
| 313 | - | ||
| 314 | - distribucionesData = d3.csvParse(distRaw, d => ({ | ||
| 315 | - tipo: d.tipo, | ||
| 316 | - codigo: d.codigo, | ||
| 317 | - dimension: d.dimension, | ||
| 318 | - gestion: +d.gestion, | ||
| 319 | - padre: d.padre, | ||
| 320 | - desc_padre: d.desc_padre, | ||
| 321 | - hijo: d.hijo, | ||
| 322 | - desc_hijo: d.desc_hijo, | ||
| 323 | - devengado: +d.devengado, | ||
| 324 | - tipo_codigo: d.tipo_codigo | ||
| 325 | - })); | ||
| 326 | - | ||
| 327 | - // Extraer codigos únicos | ||
| 328 | - codigosDisponibles = [...new Set(resumenData.map(d => d.codigo))].sort((a, b) => { | ||
| 329 | - return parseFloat(a) - parseFloat(b); | ||
| 330 | - }); | ||
| 331 | - | ||
| 332 | - cargando = false; | ||
| 333 | - | ||
| 334 | - // Observer para tema | ||
| 335 | - const observer = new MutationObserver(() => { | ||
| 336 | - renderizarTodasLasBarras(); | ||
| 337 | - }); | ||
| 338 | - observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); | ||
| 339 | - | ||
| 340 | - // Resize observer para charts | ||
| 341 | - const resizeObserver = new ResizeObserver(() => { | ||
| 342 | - renderizarHistoria(); | ||
| 343 | - }); | ||
| 344 | - if (chartIngresosContainer) resizeObserver.observe(chartIngresosContainer); | ||
| 345 | - if (chartGastosContainer) resizeObserver.observe(chartGastosContainer); | ||
| 346 | - | ||
| 347 | - return () => { | ||
| 348 | - observer.disconnect(); | ||
| 349 | - resizeObserver.disconnect(); | ||
| 350 | - }; | ||
| 351 | - }); | ||
| 352 | -</script> | ||
| 353 | - | ||
| 354 | -<svelte:head> | ||
| 355 | - <title>Radiografía - {codigoSeleccionado}</title> | ||
| 356 | -</svelte:head> | ||
| 357 | - | ||
| 358 | -{#if cargando} | ||
| 359 | - <div class="dashboard"> | ||
| 360 | - <p>Cargando datos...</p> | ||
| 361 | - </div> | ||
| 362 | -{:else} | ||
| 363 | -<div class="dashboard"> | ||
| 364 | - <!-- Header sticky --> | ||
| 365 | - <header class="sticky-header"> | ||
| 366 | - <div class="sticky-row"> | ||
| 367 | - <div class="sticky-left"> | ||
| 368 | - <h1 class="entidad-nombre">Código: {codigoSeleccionado}</h1> | ||
| 369 | - <div class="entidad-meta"> | ||
| 370 | - <span class="tipo-badge" class:da={!esEntidad}>{tipoCodigo}</span> | ||
| 371 | - <span class="entidad-sector">{esEntidad ? 'Gastos e Ingresos' : 'Solo Gastos'}</span> | ||
| 372 | - </div> | ||
| 373 | - </div> | ||
| 374 | - <div class="sticky-controls"> | ||
| 375 | - <select class="control-select" bind:value={codigoSeleccionado}> | ||
| 376 | - {#each codigosDisponibles as cod} | ||
| 377 | - <option value={cod}>{cod}</option> | ||
| 378 | - {/each} | ||
| 379 | - </select> | ||
| 380 | - <select class="control-select" bind:value={gestionSeleccionada}> | ||
| 381 | - {#each gestiones() as g} | ||
| 382 | - <option value={g}>{g}</option> | ||
| 383 | - {/each} | ||
| 384 | - </select> | ||
| 385 | - </div> | ||
| 386 | - </div> | ||
| 387 | - </header> | ||
| 388 | - | ||
| 389 | - <!-- Ranking --> | ||
| 390 | - <div class="ranking-bar"> | ||
| 391 | - {#if rankingGastos()} | ||
| 392 | - <div class="ranking-item"> | ||
| 393 | - <span class="ranking-label">Ranking gastos</span> | ||
| 394 | - <span class="ranking-value">#{rankingGastos().posicion}</span> | ||
| 395 | - <span class="ranking-total">de {rankingGastos().total}</span> | ||
| 396 | - </div> | ||
| 397 | - {/if} | ||
| 398 | - {#if esEntidad && rankingIngresos()} | ||
| 399 | - <div class="ranking-sep"></div> | ||
| 400 | - <div class="ranking-item"> | ||
| 401 | - <span class="ranking-label">Ranking ingresos</span> | ||
| 402 | - <span class="ranking-value">#{rankingIngresos().posicion}</span> | ||
| 403 | - <span class="ranking-total">de {rankingIngresos().total}</span> | ||
| 404 | - </div> | ||
| 405 | - {/if} | ||
| 406 | - </div> | ||
| 407 | - | ||
| 408 | - <!-- Historia temporal --> | ||
| 409 | - <section class="seccion"> | ||
| 410 | - <h2 class="seccion-titulo">{historiaGastos.length > 0 ? `${historiaGastos[0].gestion} – ${historiaGastos[historiaGastos.length - 1].gestion}` : ''}</h2> | ||
| 411 | - <div class="historia-grid" class:single={!esEntidad}> | ||
| 412 | - {#if esEntidad} | ||
| 413 | - <div class="historia-card"> | ||
| 414 | - <div class="historia-header"> | ||
| 415 | - <span class="historia-label">Ingresos</span> | ||
| 416 | - {#if historiaIngresos.length > 0} | ||
| 417 | - <span class="historia-monto ingresos">{formatearMonto(historiaIngresos[historiaIngresos.length - 1].devengado)} Bs</span> | ||
| 418 | - {/if} | ||
| 419 | - </div> | ||
| 420 | - <div class="historia-chart" bind:this={chartIngresosContainer}></div> | ||
| 421 | - </div> | ||
| 422 | - {/if} | ||
| 423 | - <div class="historia-card"> | ||
| 424 | - <div class="historia-header"> | ||
| 425 | - <span class="historia-label">Gastos</span> | ||
| 426 | - {#if historiaGastos.length > 0} | ||
| 427 | - <span class="historia-monto gastos">{formatearMonto(historiaGastos[historiaGastos.length - 1].devengado)} Bs</span> | ||
| 428 | - {/if} | ||
| 429 | - </div> | ||
| 430 | - <div class="historia-chart" bind:this={chartGastosContainer}></div> | ||
| 431 | - </div> | ||
| 432 | - </div> | ||
| 433 | - </section> | ||
| 434 | - | ||
| 435 | - <!-- Composición del gasto --> | ||
| 436 | - {#if clasificadoresGasto.length > 0} | ||
| 437 | - <section class="seccion"> | ||
| 438 | - <h2 class="seccion-titulo">¿En qué gasta? <span class="gestion-badge">{gestionSeleccionada}</span></h2> | ||
| 439 | - <div class="clasificadores-grid"> | ||
| 440 | - {#each clasificadoresGasto as { key, label, data }} | ||
| 441 | - {@const displayItem = getDisplayItem(key, data)} | ||
| 442 | - <div class="clasificador-card"> | ||
| 443 | - <div class="clasificador-header"> | ||
| 444 | - <div class="clasificador-info"> | ||
| 445 | - <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} Bs</span> | ||
| 446 | - <span class="clasificador-padre">{displayItem?.padre || ''}</span> | ||
| 447 | - <span class="clasificador-nombre">{displayItem?.nombre || ''}</span> | ||
| 448 | - </div> | ||
| 449 | - <span class="clasificador-label">{label}</span> | ||
| 450 | - </div> | ||
| 451 | - <div class="barra-container" bind:this={barContainers[key]}></div> | ||
| 452 | - </div> | ||
| 453 | - {/each} | ||
| 454 | - </div> | ||
| 455 | - </section> | ||
| 456 | - {/if} | ||
| 457 | - | ||
| 458 | - <!-- Origen del dinero (solo entidad) --> | ||
| 459 | - {#if esEntidad && clasificadoresIngreso.length > 0} | ||
| 460 | - <section class="seccion"> | ||
| 461 | - <h2 class="seccion-titulo">Fuentes de ingreso y organismos financiadores <span class="gestion-badge">{gestionSeleccionada}</span></h2> | ||
| 462 | - <div class="clasificadores-grid"> | ||
| 463 | - {#each clasificadoresIngreso as { key, label, data }} | ||
| 464 | - {@const displayItem = getDisplayItem(key, data)} | ||
| 465 | - <div class="clasificador-card"> | ||
| 466 | - <div class="clasificador-header"> | ||
| 467 | - <div class="clasificador-info"> | ||
| 468 | - <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} Bs</span> | ||
| 469 | - <span class="clasificador-padre">{displayItem?.padre || ''}</span> | ||
| 470 | - <span class="clasificador-nombre">{displayItem?.nombre || ''}</span> | ||
| 471 | - </div> | ||
| 472 | - <span class="clasificador-label">{label}</span> | ||
| 473 | - </div> | ||
| 474 | - <div class="barra-container" bind:this={barContainers[key]}></div> | ||
| 475 | - </div> | ||
| 476 | - {/each} | ||
| 477 | - </div> | ||
| 478 | - </section> | ||
| 479 | - {/if} | ||
| 480 | -</div> | ||
| 481 | -{/if} | ||
| 482 | - | ||
| 483 | -<style> | ||
| 484 | - .dashboard { | ||
| 485 | - min-height: 100vh; | ||
| 486 | - background: var(--theme-fondo); | ||
| 487 | - color: var(--theme-texto); | ||
| 488 | - padding: 2rem; | ||
| 489 | - padding-top: 2rem; | ||
| 490 | - font-family: var(--font-sans); | ||
| 491 | - } | ||
| 492 | - | ||
| 493 | - /* Sticky header */ | ||
| 494 | - .sticky-header { | ||
| 495 | - position: sticky; | ||
| 496 | - top: 0; | ||
| 497 | - z-index: 50; | ||
| 498 | - padding: 0.75rem 2rem; | ||
| 499 | - margin: 0 -2rem 1.5rem -2rem; | ||
| 500 | - background: color-mix(in srgb, var(--theme-fondo) 90%, transparent); | ||
| 501 | - border-bottom: 1px solid var(--theme-borde); | ||
| 502 | - backdrop-filter: blur(12px); | ||
| 503 | - -webkit-backdrop-filter: blur(12px); | ||
| 504 | - } | ||
| 505 | - | ||
| 506 | - .sticky-row { | ||
| 507 | - display: flex; | ||
| 508 | - align-items: center; | ||
| 509 | - gap: 1rem; | ||
| 510 | - max-width: calc(100% - 220px); /* dejar espacio para el navbar fixed */ | ||
| 511 | - } | ||
| 512 | - | ||
| 513 | - .sticky-left { | ||
| 514 | - display: flex; | ||
| 515 | - align-items: baseline; | ||
| 516 | - gap: 0.75rem; | ||
| 517 | - min-width: 0; | ||
| 518 | - } | ||
| 519 | - | ||
| 520 | - .entidad-nombre { | ||
| 521 | - font-size: 1.1rem; | ||
| 522 | - font-weight: 700; | ||
| 523 | - color: var(--theme-titulo); | ||
| 524 | - margin: 0; | ||
| 525 | - line-height: 1.2; | ||
| 526 | - white-space: nowrap; | ||
| 527 | - overflow: hidden; | ||
| 528 | - text-overflow: ellipsis; | ||
| 529 | - } | ||
| 530 | - | ||
| 531 | - .entidad-meta { | ||
| 532 | - display: flex; | ||
| 533 | - align-items: center; | ||
| 534 | - gap: 0.4rem; | ||
| 535 | - flex-shrink: 0; | ||
| 536 | - } | ||
| 537 | - | ||
| 538 | - .entidad-sector { | ||
| 539 | - font-size: 0.7rem; | ||
| 540 | - color: var(--theme-texto); | ||
| 541 | - opacity: 0.6; | ||
| 542 | - } | ||
| 543 | - | ||
| 544 | - .tipo-badge { | ||
| 545 | - font-size: 0.6rem; | ||
| 546 | - font-weight: 600; | ||
| 547 | - padding: 0.15rem 0.45rem; | ||
| 548 | - background: rgba(74, 139, 110, 0.15); | ||
| 549 | - color: #4A8B6E; | ||
| 550 | - border-radius: 4px; | ||
| 551 | - text-transform: uppercase; | ||
| 552 | - letter-spacing: 0.03em; | ||
| 553 | - } | ||
| 554 | - | ||
| 555 | - .tipo-badge.da { | ||
| 556 | - background: rgba(201, 167, 81, 0.15); | ||
| 557 | - color: #C9A751; | ||
| 558 | - } | ||
| 559 | - | ||
| 560 | - .sticky-controls { | ||
| 561 | - display: flex; | ||
| 562 | - align-items: center; | ||
| 563 | - gap: 0.4rem; | ||
| 564 | - flex-shrink: 0; | ||
| 565 | - } | ||
| 566 | - | ||
| 567 | - .control-select { | ||
| 568 | - padding: 0.35rem 0.6rem; | ||
| 569 | - font-size: 0.8rem; | ||
| 570 | - font-weight: 600; | ||
| 571 | - background: var(--theme-tarjeta); | ||
| 572 | - border: 1px solid var(--theme-borde); | ||
| 573 | - border-radius: 6px; | ||
| 574 | - color: var(--theme-titulo); | ||
| 575 | - cursor: pointer; | ||
| 576 | - appearance: none; | ||
| 577 | - -webkit-appearance: none; | ||
| 578 | - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M3 5l3 3 3-3'/%3E%3C/svg%3E"); | ||
| 579 | - background-repeat: no-repeat; | ||
| 580 | - background-position: right 0.4rem center; | ||
| 581 | - padding-right: 1.5rem; | ||
| 582 | - } | ||
| 583 | - | ||
| 584 | - /* Ranking */ | ||
| 585 | - .ranking-bar { | ||
| 586 | - display: flex; | ||
| 587 | - align-items: center; | ||
| 588 | - gap: 1.5rem; | ||
| 589 | - padding: 1rem 1.5rem; | ||
| 590 | - background: var(--theme-tarjeta); | ||
| 591 | - border-radius: 12px; | ||
| 592 | - margin-bottom: 2rem; | ||
| 593 | - } | ||
| 594 | - | ||
| 595 | - .ranking-item { | ||
| 596 | - display: flex; | ||
| 597 | - align-items: baseline; | ||
| 598 | - gap: 0.5rem; | ||
| 599 | - } | ||
| 600 | - | ||
| 601 | - .ranking-label { | ||
| 602 | - font-size: 0.8rem; | ||
| 603 | - color: var(--theme-texto); | ||
| 604 | - opacity: 0.7; | ||
| 605 | - } | ||
| 606 | - | ||
| 607 | - .ranking-value { | ||
| 608 | - font-size: 1.25rem; | ||
| 609 | - font-weight: 700; | ||
| 610 | - color: #C9A751; | ||
| 611 | - } | ||
| 612 | - | ||
| 613 | - .ranking-total { | ||
| 614 | - font-size: 0.8rem; | ||
| 615 | - color: var(--theme-texto); | ||
| 616 | - opacity: 0.5; | ||
| 617 | - } | ||
| 618 | - | ||
| 619 | - .ranking-sep { | ||
| 620 | - width: 1px; | ||
| 621 | - height: 24px; | ||
| 622 | - background: var(--theme-borde); | ||
| 623 | - } | ||
| 624 | - | ||
| 625 | - /* Secciones */ | ||
| 626 | - .seccion { | ||
| 627 | - margin-bottom: 2.5rem; | ||
| 628 | - } | ||
| 629 | - | ||
| 630 | - .seccion-titulo { | ||
| 631 | - font-size: 1.1rem; | ||
| 632 | - font-weight: 600; | ||
| 633 | - color: var(--theme-titulo); | ||
| 634 | - margin: 0 0 1rem 0; | ||
| 635 | - display: flex; | ||
| 636 | - align-items: center; | ||
| 637 | - gap: 0.75rem; | ||
| 638 | - } | ||
| 639 | - | ||
| 640 | - .gestion-badge { | ||
| 641 | - font-size: 0.75rem; | ||
| 642 | - font-weight: 500; | ||
| 643 | - padding: 0.25rem 0.6rem; | ||
| 644 | - background: rgba(201, 167, 81, 0.15); | ||
| 645 | - color: #C9A751; | ||
| 646 | - border-radius: 4px; | ||
| 647 | - } | ||
| 648 | - | ||
| 649 | - /* Historia */ | ||
| 650 | - .historia-grid { | ||
| 651 | - display: grid; | ||
| 652 | - grid-template-columns: repeat(2, 1fr); | ||
| 653 | - gap: 1.5rem; | ||
| 654 | - } | ||
| 655 | - | ||
| 656 | - .historia-grid.single { | ||
| 657 | - grid-template-columns: 1fr; | ||
| 658 | - } | ||
| 659 | - | ||
| 660 | - .historia-card { | ||
| 661 | - background: var(--theme-tarjeta); | ||
| 662 | - border-radius: 12px; | ||
| 663 | - padding: 1rem; | ||
| 664 | - } | ||
| 665 | - | ||
| 666 | - .historia-header { | ||
| 667 | - display: flex; | ||
| 668 | - justify-content: space-between; | ||
| 669 | - align-items: baseline; | ||
| 670 | - margin-bottom: 0.75rem; | ||
| 671 | - } | ||
| 672 | - | ||
| 673 | - .historia-label { | ||
| 674 | - font-size: 0.85rem; | ||
| 675 | - font-weight: 500; | ||
| 676 | - color: var(--theme-texto); | ||
| 677 | - } | ||
| 678 | - | ||
| 679 | - .historia-monto { | ||
| 680 | - font-size: 0.95rem; | ||
| 681 | - font-weight: 600; | ||
| 682 | - } | ||
| 683 | - | ||
| 684 | - .historia-monto.ingresos { | ||
| 685 | - color: #4A8B6E; | ||
| 686 | - } | ||
| 687 | - | ||
| 688 | - .historia-monto.gastos { | ||
| 689 | - color: #C9A751; | ||
| 690 | - } | ||
| 691 | - | ||
| 692 | - .historia-chart { | ||
| 693 | - width: 100%; | ||
| 694 | - height: 180px; | ||
| 695 | - } | ||
| 696 | - | ||
| 697 | - /* Clasificadores */ | ||
| 698 | - .clasificadores-grid { | ||
| 699 | - display: flex; | ||
| 700 | - flex-direction: column; | ||
| 701 | - gap: 1rem; | ||
| 702 | - } | ||
| 703 | - | ||
| 704 | - .clasificador-card { | ||
| 705 | - display: flex; | ||
| 706 | - flex-direction: column; | ||
| 707 | - gap: 0.35rem; | ||
| 708 | - } | ||
| 709 | - | ||
| 710 | - .clasificador-header { | ||
| 711 | - display: flex; | ||
| 712 | - align-items: baseline; | ||
| 713 | - justify-content: space-between; | ||
| 714 | - gap: 1rem; | ||
| 715 | - } | ||
| 716 | - | ||
| 717 | - .clasificador-info { | ||
| 718 | - display: flex; | ||
| 719 | - align-items: baseline; | ||
| 720 | - gap: 0.4rem; | ||
| 721 | - flex-wrap: wrap; | ||
| 722 | - } | ||
| 723 | - | ||
| 724 | - .clasificador-monto { | ||
| 725 | - font-size: 0.95rem; | ||
| 726 | - font-weight: 600; | ||
| 727 | - color: var(--theme-titulo); | ||
| 728 | - } | ||
| 729 | - | ||
| 730 | - .clasificador-padre { | ||
| 731 | - font-size: 0.8rem; | ||
| 732 | - font-weight: 400; | ||
| 733 | - color: var(--theme-texto); | ||
| 734 | - opacity: 0.5; | ||
| 735 | - } | ||
| 736 | - | ||
| 737 | - .clasificador-nombre { | ||
| 738 | - font-size: 0.85rem; | ||
| 739 | - font-weight: 500; | ||
| 740 | - color: var(--theme-titulo); | ||
| 741 | - } | ||
| 742 | - | ||
| 743 | - .clasificador-label { | ||
| 744 | - font-size: 0.65rem; | ||
| 745 | - text-transform: uppercase; | ||
| 746 | - letter-spacing: 0.05em; | ||
| 747 | - color: var(--theme-texto); | ||
| 748 | - opacity: 0.6; | ||
| 749 | - flex-shrink: 0; | ||
| 750 | - } | ||
| 751 | - | ||
| 752 | - .barra-container { | ||
| 753 | - width: 100%; | ||
| 754 | - height: 48px; | ||
| 755 | - border-radius: 4px; | ||
| 756 | - overflow: hidden; | ||
| 757 | - } | ||
| 758 | - | ||
| 759 | - /* Responsive */ | ||
| 760 | - @media (max-width: 768px) { | ||
| 761 | - .dashboard { | ||
| 762 | - padding: 1rem; | ||
| 763 | - } | ||
| 764 | - | ||
| 765 | - .sticky-header { | ||
| 766 | - margin: 0 -1rem 1rem -1rem; | ||
| 767 | - padding: 0.5rem 1rem; | ||
| 768 | - } | ||
| 769 | - | ||
| 770 | - .sticky-row { | ||
| 771 | - flex-direction: column; | ||
| 772 | - align-items: flex-start; | ||
| 773 | - gap: 0.5rem; | ||
| 774 | - max-width: calc(100% - 160px); | ||
| 775 | - } | ||
| 776 | - | ||
| 777 | - .sticky-left { | ||
| 778 | - flex-direction: column; | ||
| 779 | - gap: 0.25rem; | ||
| 780 | - } | ||
| 781 | - | ||
| 782 | - .entidad-nombre { | ||
| 783 | - font-size: 0.95rem; | ||
| 784 | - } | ||
| 785 | - | ||
| 786 | - .sticky-controls { | ||
| 787 | - width: 100%; | ||
| 788 | - } | ||
| 789 | - | ||
| 790 | - .control-select { | ||
| 791 | - flex: 1; | ||
| 792 | - } | ||
| 793 | - | ||
| 794 | - .historia-grid { | ||
| 795 | - grid-template-columns: 1fr; | ||
| 796 | - } | ||
| 797 | - | ||
| 798 | - .ranking-bar { | ||
| 799 | - flex-direction: column; | ||
| 800 | - align-items: flex-start; | ||
| 801 | - gap: 0.75rem; | ||
| 802 | - } | ||
| 803 | - | ||
| 804 | - .ranking-sep { | ||
| 805 | - width: 100%; | ||
| 806 | - height: 1px; | ||
| 807 | - } | ||
| 808 | - } | ||
| 809 | -</style> |
| 1 | -import { supabase } from '$lib/supabase'; | ||
| 2 | - | ||
| 3 | -// Hardcoded a objeto 11700 (Sueldos) para testing de layout | ||
| 4 | -const CODIGO = '11700'; | ||
| 5 | - | ||
| 6 | -function getGrupoCode(objeto) { return objeto.charAt(0) + '0000'; } | ||
| 7 | -function getSubgrupoCode(objeto) { return objeto.substring(0, 2) + '000'; } | ||
| 8 | -function getPartidaCode(objeto) { return objeto.substring(0, 3) + '00'; } | ||
| 9 | - | ||
| 10 | -export async function load() { | ||
| 11 | - const { data } = await supabase.schema('ppto').from('clas_objetos').select('*').eq('objeto', CODIGO); | ||
| 12 | - const objeto = data[0]; | ||
| 13 | - | ||
| 14 | - const grupoCode = getGrupoCode(CODIGO); | ||
| 15 | - const subgrupoCode = getSubgrupoCode(CODIGO); | ||
| 16 | - const partidaCode = getPartidaCode(CODIGO); | ||
| 17 | - | ||
| 18 | - const { data: padresData } = await supabase.schema('ppto').from('clas_objetos').select('*').in('objeto', [grupoCode, subgrupoCode, partidaCode]).order('objeto'); | ||
| 19 | - const padres = padresData || []; | ||
| 20 | - | ||
| 21 | - // Contar entidades | ||
| 22 | - const nEntidades = objeto.n_entidades || 0; | ||
| 23 | - | ||
| 24 | - return { objeto, padres, hijos: [], nEntidades }; | ||
| 25 | -} |
src/routes/test/layout/+page.svelte
deleted
100644 → 0
| 1 | -<script> | ||
| 2 | - import { onMount } from 'svelte'; | ||
| 3 | - import { tweened } from 'svelte/motion'; | ||
| 4 | - import { cubicOut } from 'svelte/easing'; | ||
| 5 | - import { supabase } from '$lib/supabase'; | ||
| 6 | - import ObjetoSearch from '$lib/components/objeto/ObjetoSearch.svelte'; | ||
| 7 | - import BarChart from '$lib/components/objeto/BarChart.svelte'; | ||
| 8 | - import EntitySelector from '$lib/components/objeto/EntitySelector.svelte'; | ||
| 9 | - import { initIndex } from '$lib/stores/searchStore'; | ||
| 10 | - | ||
| 11 | - let { data } = $props(); | ||
| 12 | - let objetoData = $derived(data.objeto); | ||
| 13 | - let padres = $derived(data.padres); | ||
| 14 | - let hijos = $derived(data.hijos); | ||
| 15 | - | ||
| 16 | - let mounted = $state(false); | ||
| 17 | - let loading = $state(true); | ||
| 18 | - let hoveredYear = $state(null); | ||
| 19 | - | ||
| 20 | - // Estado selector entidad | ||
| 21 | - let entidades = $state([]); | ||
| 22 | - let selectedEntity = $state(null); | ||
| 23 | - let nEntidades = $derived(data.nEntidades); | ||
| 24 | - | ||
| 25 | - // Datos | ||
| 26 | - let datosAnuales = $state([]); | ||
| 27 | - let datosHistorico = $state(null); | ||
| 28 | - let topEntidadesPorAño = $state({}); | ||
| 29 | - | ||
| 30 | - const POBLACION = 12000000; | ||
| 31 | - let objetoCodigo = $derived(objetoData.objeto); | ||
| 32 | - let objetoNivel = $derived(objetoData.nivel); | ||
| 33 | - | ||
| 34 | - let gastoPerCapita = $derived( | ||
| 35 | - datosAnuales.map(d => ({ | ||
| 36 | - año: d.gestion, | ||
| 37 | - monto: selectedEntity ? (d.monto ?? d.devengado) : d.total, | ||
| 38 | - perCapita: d.per_capita ?? (d.devengado ? d.devengado / POBLACION : 0) | ||
| 39 | - })) | ||
| 40 | - ); | ||
| 41 | - | ||
| 42 | - let primerAño = $derived(gastoPerCapita.length > 0 ? gastoPerCapita[0].año : 2005); | ||
| 43 | - let ultimoAño = $derived(gastoPerCapita.length > 0 ? gastoPerCapita[gastoPerCapita.length - 1].año : 2025); | ||
| 44 | - | ||
| 45 | - let totalHistorico = $derived(datosHistorico ? (selectedEntity ? datosHistorico.monto : datosHistorico.total) : 0); | ||
| 46 | - let promedioPerCapita = $derived(datosHistorico?.per_capita || 0); | ||
| 47 | - let totalPorcentaje = $derived(datosHistorico?.prop || 0); | ||
| 48 | - let ranking = $derived(datosHistorico?.ranking || 0); | ||
| 49 | - let nPares = $derived(datosHistorico?.n_pares || 0); | ||
| 50 | - | ||
| 51 | - let currentData = $derived( | ||
| 52 | - hoveredYear ? gastoPerCapita.find(g => g.año === hoveredYear) : null | ||
| 53 | - ); | ||
| 54 | - | ||
| 55 | - let displayMonto = $derived(currentData ? currentData.monto : totalHistorico); | ||
| 56 | - let displayMontoLabel = $derived(currentData ? `gastados en ${currentData.año}` : `gastados ${primerAño}-${ultimoAño}`); | ||
| 57 | - let displayPerCapita = $derived(currentData ? currentData.perCapita : promedioPerCapita); | ||
| 58 | - let displayPerCapitaLabel = $derived(currentData ? `por cada boliviano en ${currentData.año}` : 'por cada boliviano'); | ||
| 59 | - let displayPorcentaje = $derived(currentData ? (totalHistorico > 0 ? totalPorcentaje * (currentData.monto / (totalHistorico / gastoPerCapita.length)) : 0) : totalPorcentaje); | ||
| 60 | - let displayPeriodo = $derived(currentData ? currentData.año : `${primerAño}-${ultimoAño}`); | ||
| 61 | - let displayRanking = $derived(`${currentData ? (datosAnuales.find(d => d.gestion === hoveredYear)?.ranking || ranking) : ranking} de ${selectedEntity ? (datosHistorico?.n_entidades || nEntidades) : nPares}`); | ||
| 62 | - let displayPorcentajeLabel = $derived(currentData ? `del gasto en ${currentData.año}` : 'del gasto total'); | ||
| 63 | - let displayRankingLabel = $derived(currentData ? `partidas más gastadas en ${currentData.año}` : 'partidas más gastadas'); | ||
| 64 | - | ||
| 65 | - // Top entidades | ||
| 66 | - let topEntidadesTotal = $derived( | ||
| 67 | - datosHistorico && !selectedEntity ? [ | ||
| 68 | - { nombre: datosHistorico.top1_entidad, porcentaje: datosHistorico.top1_pct }, | ||
| 69 | - { nombre: datosHistorico.top2_entidad, porcentaje: datosHistorico.top2_pct }, | ||
| 70 | - { nombre: datosHistorico.top3_entidad, porcentaje: datosHistorico.top3_pct }, | ||
| 71 | - ].filter(e => e.nombre) : [] | ||
| 72 | - ); | ||
| 73 | - let displayTopEntidades = $derived( | ||
| 74 | - hoveredYear && topEntidadesPorAño[hoveredYear] ? topEntidadesPorAño[hoveredYear] : topEntidadesTotal | ||
| 75 | - ); | ||
| 76 | - | ||
| 77 | - // Tweens | ||
| 78 | - const tw = { duration: 300, easing: cubicOut }; | ||
| 79 | - const twMonto = tweened(0, tw); | ||
| 80 | - const twPerCapita = tweened(0, tw); | ||
| 81 | - const twPorcentaje = tweened(0, tw); | ||
| 82 | - const twTop1 = tweened(0, tw); | ||
| 83 | - const twTop2 = tweened(0, tw); | ||
| 84 | - const twTop3 = tweened(0, tw); | ||
| 85 | - | ||
| 86 | - $effect(() => { twMonto.set(displayMonto); }); | ||
| 87 | - $effect(() => { twPerCapita.set(displayPerCapita); }); | ||
| 88 | - $effect(() => { twPorcentaje.set(displayPorcentaje); }); | ||
| 89 | - $effect(() => { if (displayTopEntidades[0]) twTop1.set(displayTopEntidades[0].porcentaje); }); | ||
| 90 | - $effect(() => { if (displayTopEntidades[1]) twTop2.set(displayTopEntidades[1].porcentaje); }); | ||
| 91 | - $effect(() => { if (displayTopEntidades[2]) twTop3.set(displayTopEntidades[2].porcentaje); }); | ||
| 92 | - | ||
| 93 | - function formatMontoLargo(n) { | ||
| 94 | - if (n >= 1e9) return `${(n / 1e9).toFixed(1)} mil mill.`; | ||
| 95 | - if (n >= 1e6) return `${(n / 1e6).toFixed(1)} mill.`; | ||
| 96 | - if (n >= 1e3) return `${(n / 1e3).toFixed(0)} mil`; | ||
| 97 | - return n.toLocaleString('es-BO'); | ||
| 98 | - } | ||
| 99 | - | ||
| 100 | - function formatPerCapita(n) { | ||
| 101 | - return n.toLocaleString('es-BO', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); | ||
| 102 | - } | ||
| 103 | - | ||
| 104 | - async function loadData() { | ||
| 105 | - loading = true; | ||
| 106 | - if (selectedEntity) { | ||
| 107 | - const { data } = await supabase.schema('ppto').from('vista_objeto_entidad').select('*') | ||
| 108 | - .eq('objeto', objetoCodigo).eq('nivel', objetoNivel).eq('entidad', selectedEntity.entidad).order('gestion'); | ||
| 109 | - if (data) { | ||
| 110 | - datosHistorico = data.find(d => d.gestion === 0) || null; | ||
| 111 | - datosAnuales = data.filter(d => d.gestion > 0); | ||
| 112 | - topEntidadesPorAño = {}; | ||
| 113 | - } | ||
| 114 | - } else { | ||
| 115 | - const { data } = await supabase.schema('ppto').from('vista_objeto_estado').select('*') | ||
| 116 | - .eq('objeto', objetoCodigo).order('gestion'); | ||
| 117 | - if (data) { | ||
| 118 | - datosHistorico = data.find(d => d.gestion === 0) || null; | ||
| 119 | - datosAnuales = data.filter(d => d.gestion > 0); | ||
| 120 | - topEntidadesPorAño = {}; | ||
| 121 | - datosAnuales.forEach(d => { | ||
| 122 | - topEntidadesPorAño[d.gestion] = [ | ||
| 123 | - { nombre: d.top1_entidad, porcentaje: d.top1_pct }, | ||
| 124 | - { nombre: d.top2_entidad, porcentaje: d.top2_pct }, | ||
| 125 | - { nombre: d.top3_entidad, porcentaje: d.top3_pct }, | ||
| 126 | - ].filter(e => e.nombre); | ||
| 127 | - }); | ||
| 128 | - } | ||
| 129 | - } | ||
| 130 | - loading = false; | ||
| 131 | - } | ||
| 132 | - | ||
| 133 | - async function loadEntidades() { | ||
| 134 | - const { data } = await supabase.schema('ppto').from('entidades_por_objeto').select('entidad, entidad_desc') | ||
| 135 | - .eq('objeto', objetoCodigo).eq('nivel', objetoNivel).order('entidad_desc'); | ||
| 136 | - if (data) entidades = data; | ||
| 137 | - } | ||
| 138 | - | ||
| 139 | - onMount(async () => { | ||
| 140 | - mounted = true; | ||
| 141 | - initIndex(); | ||
| 142 | - await Promise.all([loadData(), loadEntidades()]); | ||
| 143 | - }); | ||
| 144 | -</script> | ||
| 145 | - | ||
| 146 | -<svelte:head> | ||
| 147 | - <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| 148 | - <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@400;500&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" /> | ||
| 149 | -</svelte:head> | ||
| 150 | - | ||
| 151 | -<div class="page" class:mounted> | ||
| 152 | - <nav class="page-nav"> | ||
| 153 | - <a href="/clasificadores/objeto-gasto" class="back-link"> | ||
| 154 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 155 | - <path d="M19 12H5M12 19l-7-7 7-7"/> | ||
| 156 | - </svg> | ||
| 157 | - Clasificador | ||
| 158 | - </a> | ||
| 159 | - </nav> | ||
| 160 | - | ||
| 161 | - <div class="dashboard-layout"> | ||
| 162 | - <!-- Sidebar --> | ||
| 163 | - <aside class="dashboard-context"> | ||
| 164 | - <div class="context-card"> | ||
| 165 | - <h2 class="context-main-title">Herramientas</h2> | ||
| 166 | - | ||
| 167 | - <!-- Buscar --> | ||
| 168 | - <div class="context-section"> | ||
| 169 | - <h3 class="context-subtitle">Buscar</h3> | ||
| 170 | - <p class="context-hint">Buscar otro grupo, subgrupo, partida o subpartida</p> | ||
| 171 | - <ObjetoSearch /> | ||
| 172 | - </div> | ||
| 173 | - | ||
| 174 | - <div class="context-separator"></div> | ||
| 175 | - | ||
| 176 | - <!-- Navegación --> | ||
| 177 | - {#if padres.length > 0 || hijos.length > 0} | ||
| 178 | - <div class="context-section"> | ||
| 179 | - <h3 class="context-subtitle">Navegación</h3> | ||
| 180 | - <p class="context-hint">Partidas relacionadas. Haz click para explorar.</p> | ||
| 181 | - <div class="tree"> | ||
| 182 | - {#each padres as padre, i} | ||
| 183 | - <a href="/objeto/{padre.objeto}" class="tree-node has-tooltip" style="--indent: {i}" data-tooltip="{padre.desc_objeto}"> | ||
| 184 | - <span class="tree-dot"></span> | ||
| 185 | - <span class="tree-code">{padre.objeto}</span> | ||
| 186 | - <span class="tree-name">{padre.desc_objeto}</span> | ||
| 187 | - </a> | ||
| 188 | - {/each} | ||
| 189 | - | ||
| 190 | - <div class="tree-node tree-node-current has-tooltip" style="--indent: {padres.length}" data-tooltip="{objetoData.desc_objeto}"> | ||
| 191 | - <span class="tree-dot"></span> | ||
| 192 | - <span class="tree-code">{objetoData.objeto}</span> | ||
| 193 | - <span class="tree-name">{objetoData.desc_objeto}</span> | ||
| 194 | - </div> | ||
| 195 | - | ||
| 196 | - {#if hijos.length > 0} | ||
| 197 | - <div class="tree-children" style="--indent: {padres.length + 1}"> | ||
| 198 | - {#each hijos as hijo} | ||
| 199 | - <a href="/objeto/{hijo.objeto}" class="tree-node tree-node-child has-tooltip" data-tooltip="{hijo.desc_objeto}"> | ||
| 200 | - <span class="tree-dot"></span> | ||
| 201 | - <span class="tree-code">{hijo.objeto}</span> | ||
| 202 | - <span class="tree-name">{hijo.desc_objeto}</span> | ||
| 203 | - </a> | ||
| 204 | - {/each} | ||
| 205 | - </div> | ||
| 206 | - {/if} | ||
| 207 | - </div> | ||
| 208 | - </div> | ||
| 209 | - | ||
| 210 | - <div class="context-separator"></div> | ||
| 211 | - {/if} | ||
| 212 | - | ||
| 213 | - <!-- Explorar --> | ||
| 214 | - <div class="context-section"> | ||
| 215 | - <h3 class="context-subtitle">Explorar visualizaciones</h3> | ||
| 216 | - <div class="explore-scroll"> | ||
| 217 | - <a href="/objeto/{objetoData.objeto}?vista=comparar" class="explore-card"> | ||
| 218 | - <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 219 | - <rect x="4" y="14" width="4" height="6"/><rect x="10" y="8" width="4" height="12"/><rect x="16" y="4" width="4" height="16"/> | ||
| 220 | - </svg> | ||
| 221 | - <span class="explore-title">Comparar dos entidades</span> | ||
| 222 | - <span class="explore-desc">¿Quién gasta más en sueldos, el Min. de Salud o Defensa?</span> | ||
| 223 | - </a> | ||
| 224 | - <a href="/clasificadores/objeto-gasto" class="explore-card"> | ||
| 225 | - <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 226 | - <path d="M3 20h18M5 17h2v3H5zM9 14h2v6H9zM13 11h2v9h-2zM17 8h2v12h-2z"/> | ||
| 227 | - </svg> | ||
| 228 | - <span class="explore-title">Ranking</span> | ||
| 229 | - <span class="explore-desc">{nEntidades} entidades que gastan en esta partida</span> | ||
| 230 | - </a> | ||
| 231 | - <a href="/ubicacion" class="explore-card explore-card-disabled"> | ||
| 232 | - <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 233 | - <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/> | ||
| 234 | - </svg> | ||
| 235 | - <span class="explore-title">Mapa</span> | ||
| 236 | - <span class="explore-desc">Dónde se concentra el gasto en esta partida</span> | ||
| 237 | - </a> | ||
| 238 | - <a href="/clasificadores/objeto-gasto?modo=mapa" class="explore-card"> | ||
| 239 | - <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 240 | - <circle cx="12" cy="12" r="10"/><path d="M12 2a15 15 0 0 1 4 10 15 15 0 0 1-4 10 15 15 0 0 1-4-10A15 15 0 0 1 12 2z"/> | ||
| 241 | - </svg> | ||
| 242 | - <span class="explore-title">Peso relativo</span> | ||
| 243 | - <span class="explore-desc">Cuánto pesan los sueldos respecto a otros gastos</span> | ||
| 244 | - </a> | ||
| 245 | - <a href="/clasificadores/objeto-gasto" class="explore-card"> | ||
| 246 | - <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 247 | - <path d="M4 6h16M8 10h12M12 14h8M8 18h12"/> | ||
| 248 | - </svg> | ||
| 249 | - <span class="explore-title">Clasificador</span> | ||
| 250 | - <span class="explore-desc">Ver todas las partidas de gasto y navegar entre ellas</span> | ||
| 251 | - </a> | ||
| 252 | - </div> | ||
| 253 | - </div> | ||
| 254 | - | ||
| 255 | - <div class="context-separator"></div> | ||
| 256 | - | ||
| 257 | - <!-- Comparabilidad --> | ||
| 258 | - <div class="context-section"> | ||
| 259 | - <h3 class="context-subtitle">Comparabilidad</h3> | ||
| 260 | - <div class="vigencia-compact"> | ||
| 261 | - <div class="vigencia-status-icon vigencia-full"> | ||
| 262 | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 263 | - <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> | ||
| 264 | - <polyline points="22 4 12 14.01 9 11.01"/> | ||
| 265 | - </svg> | ||
| 266 | - </div> | ||
| 267 | - <div class="vigencia-text"> | ||
| 268 | - <span class="vigencia-main">Comparable en todo el período</span> | ||
| 269 | - <span class="vigencia-sub">2005 – 2025</span> | ||
| 270 | - </div> | ||
| 271 | - </div> | ||
| 272 | - </div> | ||
| 273 | - | ||
| 274 | - <div class="context-separator"></div> | ||
| 275 | - | ||
| 276 | - <!-- Definición --> | ||
| 277 | - <div class="context-section"> | ||
| 278 | - <h3 class="context-subtitle">Definición</h3> | ||
| 279 | - <button class="def-link"> | ||
| 280 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 281 | - <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/> | ||
| 282 | - </svg> | ||
| 283 | - Ver variaciones en la definición | ||
| 284 | - </button> | ||
| 285 | - </div> | ||
| 286 | - </div> | ||
| 287 | - </aside> | ||
| 288 | - | ||
| 289 | - <!-- Main --> | ||
| 290 | - <main class="dashboard-main"> | ||
| 291 | - <div class="chart-protagonista"> | ||
| 292 | - <!-- Header --> | ||
| 293 | - <div class="chart-top-row"> | ||
| 294 | - <div class="main-title-pills"> | ||
| 295 | - <span class="pill pill-nivel">Subpartida</span> | ||
| 296 | - <span class="pill pill-codigo">{objetoData.objeto}</span> | ||
| 297 | - </div> | ||
| 298 | - </div> | ||
| 299 | - | ||
| 300 | - <h2 class="main-title-name">{objetoData.desc_objeto}</h2> | ||
| 301 | - | ||
| 302 | - <div class="chart-controls"> | ||
| 303 | - <EntitySelector {entidades} bind:selectedEntity {nEntidades} /> | ||
| 304 | - </div> | ||
| 305 | - | ||
| 306 | - <div class="chart-header"> | ||
| 307 | - <span class="chart-title">Bs/habitante <span class="chart-period">· {displayPeriodo}</span></span> | ||
| 308 | - </div> | ||
| 309 | - | ||
| 310 | - {#if loading} | ||
| 311 | - <div class="chart-loading" style="flex: 1; min-height: 100px;"> | ||
| 312 | - <span>Cargando...</span> | ||
| 313 | - </div> | ||
| 314 | - {:else} | ||
| 315 | - <div class="chart-flex-wrapper"> | ||
| 316 | - <BarChart data={gastoPerCapita} bind:hoveredYear height={200} fill={true} /> | ||
| 317 | - </div> | ||
| 318 | - {/if} | ||
| 319 | - | ||
| 320 | - <!-- KPIs --> | ||
| 321 | - <div class="kpis"> | ||
| 322 | - <div class="kpi"> | ||
| 323 | - <span class="kpi-value">{formatMontoLargo(Math.round($twMonto))}</span> | ||
| 324 | - <span class="kpi-label">{displayMontoLabel}</span> | ||
| 325 | - </div> | ||
| 326 | - <div class="kpi"> | ||
| 327 | - <span class="kpi-value">{formatPerCapita($twPerCapita)} Bs.</span> | ||
| 328 | - <span class="kpi-label">{displayPerCapitaLabel}</span> | ||
| 329 | - </div> | ||
| 330 | - <div class="kpi"> | ||
| 331 | - <span class="kpi-value">{$twPorcentaje.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</span> | ||
| 332 | - <span class="kpi-label">{displayPorcentajeLabel}</span> | ||
| 333 | - </div> | ||
| 334 | - <div class="kpi"> | ||
| 335 | - <span class="kpi-value">{displayRanking}</span> | ||
| 336 | - <span class="kpi-label">{displayRankingLabel}</span> | ||
| 337 | - </div> | ||
| 338 | - </div> | ||
| 339 | - </div> | ||
| 340 | - | ||
| 341 | - {#if !selectedEntity && displayTopEntidades.length > 0} | ||
| 342 | - <div class="ranking-section"> | ||
| 343 | - <h3 class="ranking-title">Concentración del gasto</h3> | ||
| 344 | - <p class="ranking-desc">Porcentaje del total gastado en {objetoData.desc_objeto?.toLowerCase()} por todo el Estado {hoveredYear ? `en ${hoveredYear}` : `(${primerAño}-${ultimoAño})`}</p> | ||
| 345 | - <div class="ranking-cards"> | ||
| 346 | - {#each displayTopEntidades as ent, i} | ||
| 347 | - {@const pct = i === 0 ? $twTop1 : i === 1 ? $twTop2 : $twTop3} | ||
| 348 | - <div class="ranking-card"> | ||
| 349 | - <span class="ranking-position">#{i + 1}</span> | ||
| 350 | - <span class="ranking-name">{ent.nombre}</span> | ||
| 351 | - <span class="ranking-pct">{pct.toFixed(1)}%</span> | ||
| 352 | - </div> | ||
| 353 | - {/each} | ||
| 354 | - </div> | ||
| 355 | - </div> | ||
| 356 | - {/if} | ||
| 357 | - </main> | ||
| 358 | - </div> | ||
| 359 | -</div> | ||
| 360 | - | ||
| 361 | -<style> | ||
| 362 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 363 | - PAGE | ||
| 364 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 365 | - .page { | ||
| 366 | - min-height: 100vh; | ||
| 367 | - width: 100%; | ||
| 368 | - max-width: 100vw; | ||
| 369 | - overflow-x: hidden; | ||
| 370 | - background: var(--theme-body); | ||
| 371 | - color: var(--theme-titulo); | ||
| 372 | - font-family: var(--font-sans), -apple-system, sans-serif; | ||
| 373 | - opacity: 0; | ||
| 374 | - transition: opacity 0.4s ease; | ||
| 375 | - } | ||
| 376 | - .page.mounted { opacity: 1; } | ||
| 377 | - | ||
| 378 | - :global(html:not(.dark)) .page { | ||
| 379 | - background: #f5f5f7; | ||
| 380 | - } | ||
| 381 | - | ||
| 382 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 383 | - NAV | ||
| 384 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 385 | - .page-nav { | ||
| 386 | - padding: 2rem 2rem 0.5rem; | ||
| 387 | - max-width: 1400px; | ||
| 388 | - margin: 0 auto; | ||
| 389 | - } | ||
| 390 | - | ||
| 391 | - .back-link { | ||
| 392 | - display: inline-flex; | ||
| 393 | - align-items: center; | ||
| 394 | - gap: 0.375rem; | ||
| 395 | - font-family: 'DM Mono', monospace; | ||
| 396 | - font-size: 0.6875rem; | ||
| 397 | - color: var(--theme-texto); | ||
| 398 | - text-decoration: none; | ||
| 399 | - transition: color 0.2s; | ||
| 400 | - } | ||
| 401 | - .back-link:hover { color: var(--theme-accent); } | ||
| 402 | - | ||
| 403 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 404 | - LAYOUT | ||
| 405 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 406 | - .dashboard-layout { | ||
| 407 | - display: grid; | ||
| 408 | - grid-template-columns: minmax(200px, 30%) 1fr; | ||
| 409 | - gap: 1.5rem; | ||
| 410 | - max-width: 1400px; | ||
| 411 | - margin: 0 auto; | ||
| 412 | - padding: 1.5rem 2rem 2rem; | ||
| 413 | - height: calc(100vh - 4rem); | ||
| 414 | - overflow: hidden; | ||
| 415 | - } | ||
| 416 | - | ||
| 417 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 418 | - SIDEBAR | ||
| 419 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 420 | - .dashboard-context { | ||
| 421 | - display: flex; | ||
| 422 | - flex-direction: column; | ||
| 423 | - min-height: 0; | ||
| 424 | - } | ||
| 425 | - | ||
| 426 | - .context-card { | ||
| 427 | - background: var(--theme-surface); | ||
| 428 | - border-radius: 12px; | ||
| 429 | - padding: 0.75rem; | ||
| 430 | - display: flex; | ||
| 431 | - flex-direction: column; | ||
| 432 | - gap: 8px; | ||
| 433 | - flex: 1; | ||
| 434 | - min-height: 0; | ||
| 435 | - overflow-y: auto; | ||
| 436 | - scrollbar-width: thin; | ||
| 437 | - } | ||
| 438 | - | ||
| 439 | - .context-card::-webkit-scrollbar { width: 4px; } | ||
| 440 | - .context-card::-webkit-scrollbar-thumb { | ||
| 441 | - background: rgba(128, 128, 128, 0.3); | ||
| 442 | - border-radius: 2px; | ||
| 443 | - } | ||
| 444 | - | ||
| 445 | - .context-main-title { | ||
| 446 | - font-size: 1.375rem; | ||
| 447 | - font-weight: 600; | ||
| 448 | - color: var(--theme-titulo); | ||
| 449 | - margin: 0; | ||
| 450 | - line-height: 1.2; | ||
| 451 | - } | ||
| 452 | - | ||
| 453 | - .context-subtitle { | ||
| 454 | - font-family: 'Qanelas', var(--font-sans); | ||
| 455 | - font-size: 0.875rem; | ||
| 456 | - font-weight: 600; | ||
| 457 | - color: var(--theme-titulo); | ||
| 458 | - margin: 0; | ||
| 459 | - } | ||
| 460 | - | ||
| 461 | - .context-hint { | ||
| 462 | - font-size: 0.75rem; | ||
| 463 | - color: #666666; | ||
| 464 | - margin: 0; | ||
| 465 | - line-height: 1.4; | ||
| 466 | - } | ||
| 467 | - | ||
| 468 | - .context-section { | ||
| 469 | - display: flex; | ||
| 470 | - flex-direction: column; | ||
| 471 | - gap: 0.5rem; | ||
| 472 | - } | ||
| 473 | - | ||
| 474 | - .context-separator { | ||
| 475 | - border-top: 1px solid var(--theme-borde); | ||
| 476 | - margin: 0.25rem 0; | ||
| 477 | - } | ||
| 478 | - | ||
| 479 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 480 | - TREE | ||
| 481 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 482 | - .tree { | ||
| 483 | - font-size: 0.75rem; | ||
| 484 | - line-height: 1.6; | ||
| 485 | - } | ||
| 486 | - | ||
| 487 | - .tree-node { | ||
| 488 | - display: flex; | ||
| 489 | - align-items: center; | ||
| 490 | - gap: 0.375rem; | ||
| 491 | - padding: 0.375rem 0.5rem; | ||
| 492 | - margin-left: calc(var(--indent, 0) * 1rem); | ||
| 493 | - border-left: 2px solid transparent; | ||
| 494 | - border-radius: 0 6px 6px 0; | ||
| 495 | - text-decoration: none; | ||
| 496 | - color: var(--theme-texto); | ||
| 497 | - transition: all 0.15s; | ||
| 498 | - } | ||
| 499 | - .tree-node:hover { color: var(--theme-accent); } | ||
| 500 | - | ||
| 501 | - .tree-node-current { | ||
| 502 | - color: var(--theme-titulo); | ||
| 503 | - font-weight: 600; | ||
| 504 | - border-left-color: var(--theme-accent); | ||
| 505 | - } | ||
| 506 | - | ||
| 507 | - .tree-children { | ||
| 508 | - margin-left: calc(var(--indent, 0) * 1rem); | ||
| 509 | - } | ||
| 510 | - | ||
| 511 | - .tree-node-child { | ||
| 512 | - padding-left: 0.75rem; | ||
| 513 | - } | ||
| 514 | - | ||
| 515 | - .tree-dot { | ||
| 516 | - width: 4px; | ||
| 517 | - height: 4px; | ||
| 518 | - border-radius: 50%; | ||
| 519 | - background: currentColor; | ||
| 520 | - flex-shrink: 0; | ||
| 521 | - opacity: 0.4; | ||
| 522 | - } | ||
| 523 | - | ||
| 524 | - .tree-code { | ||
| 525 | - font-family: 'DM Mono', monospace; | ||
| 526 | - font-size: 0.6875rem; | ||
| 527 | - min-width: 2.5rem; | ||
| 528 | - opacity: 0.6; | ||
| 529 | - } | ||
| 530 | - | ||
| 531 | - .tree-name { | ||
| 532 | - font-size: 0.75rem; | ||
| 533 | - white-space: nowrap; | ||
| 534 | - overflow: hidden; | ||
| 535 | - text-overflow: ellipsis; | ||
| 536 | - max-width: 140px; | ||
| 537 | - } | ||
| 538 | - | ||
| 539 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 540 | - EXPLORE | ||
| 541 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 542 | - .explore-scroll { | ||
| 543 | - display: flex; | ||
| 544 | - gap: 8px; | ||
| 545 | - overflow-x: auto; | ||
| 546 | - scrollbar-width: none; | ||
| 547 | - padding-bottom: 2px; | ||
| 548 | - } | ||
| 549 | - .explore-scroll::-webkit-scrollbar { display: none; } | ||
| 550 | - | ||
| 551 | - .explore-card { | ||
| 552 | - position: relative; | ||
| 553 | - display: flex; | ||
| 554 | - flex-direction: column; | ||
| 555 | - justify-content: flex-start; | ||
| 556 | - padding: 0.375rem 0.5rem; | ||
| 557 | - width: 120px; | ||
| 558 | - height: 95px; | ||
| 559 | - border-radius: 8px; | ||
| 560 | - background: var(--theme-borde); | ||
| 561 | - text-decoration: none; | ||
| 562 | - color: var(--theme-texto); | ||
| 563 | - transition: all 0.15s; | ||
| 564 | - flex-shrink: 0; | ||
| 565 | - overflow: hidden; | ||
| 566 | - } | ||
| 567 | - .explore-card:hover { | ||
| 568 | - background: rgba(201, 167, 81, 0.15); | ||
| 569 | - color: var(--theme-accent); | ||
| 570 | - } | ||
| 571 | - .explore-card:hover .explore-watermark { | ||
| 572 | - opacity: 0.18; | ||
| 573 | - color: var(--theme-accent); | ||
| 574 | - } | ||
| 575 | - .explore-card:hover .explore-title { | ||
| 576 | - color: var(--theme-accent); | ||
| 577 | - } | ||
| 578 | - | ||
| 579 | - .explore-watermark { | ||
| 580 | - position: absolute; | ||
| 581 | - bottom: -4px; | ||
| 582 | - right: -4px; | ||
| 583 | - width: 44px; | ||
| 584 | - height: 44px; | ||
| 585 | - opacity: 0.15; | ||
| 586 | - pointer-events: none; | ||
| 587 | - color: var(--theme-accent, #C9A751); | ||
| 588 | - } | ||
| 589 | - | ||
| 590 | - .explore-title { | ||
| 591 | - position: relative; | ||
| 592 | - font-size: 0.75rem; | ||
| 593 | - font-weight: 600; | ||
| 594 | - line-height: 1.3; | ||
| 595 | - color: var(--theme-titulo); | ||
| 596 | - z-index: 1; | ||
| 597 | - } | ||
| 598 | - | ||
| 599 | - .explore-desc { | ||
| 600 | - position: relative; | ||
| 601 | - font-size: 0.625rem; | ||
| 602 | - line-height: 1.35; | ||
| 603 | - opacity: 0.6; | ||
| 604 | - margin-top: 3px; | ||
| 605 | - z-index: 1; | ||
| 606 | - } | ||
| 607 | - | ||
| 608 | - .explore-card-disabled { | ||
| 609 | - opacity: 0.3; | ||
| 610 | - pointer-events: none; | ||
| 611 | - } | ||
| 612 | - | ||
| 613 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 614 | - VIGENCIA | ||
| 615 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 616 | - .vigencia-compact { | ||
| 617 | - display: flex; | ||
| 618 | - align-items: center; | ||
| 619 | - gap: 0.75rem; | ||
| 620 | - } | ||
| 621 | - | ||
| 622 | - .vigencia-status-icon { | ||
| 623 | - width: 32px; | ||
| 624 | - height: 32px; | ||
| 625 | - border-radius: 8px; | ||
| 626 | - display: flex; | ||
| 627 | - align-items: center; | ||
| 628 | - justify-content: center; | ||
| 629 | - flex-shrink: 0; | ||
| 630 | - } | ||
| 631 | - | ||
| 632 | - .vigencia-status-icon.vigencia-full { | ||
| 633 | - background: rgba(74, 158, 107, 0.15); | ||
| 634 | - color: #4A9E6B; | ||
| 635 | - } | ||
| 636 | - | ||
| 637 | - .vigencia-text { | ||
| 638 | - display: flex; | ||
| 639 | - flex-direction: column; | ||
| 640 | - gap: 0.125rem; | ||
| 641 | - } | ||
| 642 | - | ||
| 643 | - .vigencia-main { | ||
| 644 | - font-size: 0.875rem; | ||
| 645 | - font-weight: 500; | ||
| 646 | - color: var(--theme-titulo); | ||
| 647 | - } | ||
| 648 | - | ||
| 649 | - .vigencia-sub { | ||
| 650 | - font-size: 0.6875rem; | ||
| 651 | - color: var(--theme-texto); | ||
| 652 | - opacity: 0.7; | ||
| 653 | - } | ||
| 654 | - | ||
| 655 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 656 | - DEFINICIÓN | ||
| 657 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 658 | - .def-link { | ||
| 659 | - display: flex; | ||
| 660 | - align-items: center; | ||
| 661 | - gap: 0.375rem; | ||
| 662 | - font-size: 0.75rem; | ||
| 663 | - color: var(--theme-accent, #C9A751); | ||
| 664 | - background: none; | ||
| 665 | - border: none; | ||
| 666 | - padding: 0; | ||
| 667 | - cursor: pointer; | ||
| 668 | - transition: opacity 0.15s; | ||
| 669 | - } | ||
| 670 | - .def-link:hover { opacity: 0.7; } | ||
| 671 | - .def-link svg { flex-shrink: 0; opacity: 0.8; } | ||
| 672 | - | ||
| 673 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 674 | - MAIN | ||
| 675 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 676 | - .dashboard-main { | ||
| 677 | - display: flex; | ||
| 678 | - flex-direction: column; | ||
| 679 | - gap: 1.5rem; | ||
| 680 | - min-height: 0; | ||
| 681 | - overflow: hidden; | ||
| 682 | - } | ||
| 683 | - | ||
| 684 | - .chart-protagonista { | ||
| 685 | - background: var(--theme-surface); | ||
| 686 | - border-radius: 12px; | ||
| 687 | - padding: 0.75rem; | ||
| 688 | - display: flex; | ||
| 689 | - flex-direction: column; | ||
| 690 | - min-height: 0; | ||
| 691 | - flex: 2; | ||
| 692 | - overflow: hidden; | ||
| 693 | - } | ||
| 694 | - | ||
| 695 | - .ranking-section { | ||
| 696 | - background: var(--theme-surface); | ||
| 697 | - border-radius: 12px; | ||
| 698 | - padding: 0.75rem; | ||
| 699 | - flex: 1; | ||
| 700 | - display: flex; | ||
| 701 | - flex-direction: column; | ||
| 702 | - min-height: 0; | ||
| 703 | - } | ||
| 704 | - | ||
| 705 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 706 | - CHART & KPIs | ||
| 707 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 708 | - .chart-top-row { | ||
| 709 | - display: flex; | ||
| 710 | - justify-content: space-between; | ||
| 711 | - align-items: center; | ||
| 712 | - margin-bottom: 0.375rem; | ||
| 713 | - } | ||
| 714 | - | ||
| 715 | - .main-title-pills { | ||
| 716 | - display: flex; | ||
| 717 | - gap: 0.375rem; | ||
| 718 | - } | ||
| 719 | - | ||
| 720 | - .pill { | ||
| 721 | - font-family: 'DM Mono', monospace; | ||
| 722 | - font-size: 0.625rem; | ||
| 723 | - padding: 0.125rem 0.5rem; | ||
| 724 | - border-radius: 4px; | ||
| 725 | - letter-spacing: 0.03em; | ||
| 726 | - } | ||
| 727 | - | ||
| 728 | - .pill-nivel { | ||
| 729 | - background: rgba(201, 167, 81, 0.15); | ||
| 730 | - color: var(--theme-accent); | ||
| 731 | - } | ||
| 732 | - | ||
| 733 | - .pill-codigo { | ||
| 734 | - background: var(--theme-borde); | ||
| 735 | - color: var(--theme-texto); | ||
| 736 | - } | ||
| 737 | - | ||
| 738 | - .main-title-name { | ||
| 739 | - font-family: 'DM Serif Display', serif; | ||
| 740 | - font-size: 1.75rem; | ||
| 741 | - font-weight: 400; | ||
| 742 | - margin: 0 0 0.5rem; | ||
| 743 | - line-height: 1.2; | ||
| 744 | - color: var(--theme-titulo); | ||
| 745 | - } | ||
| 746 | - | ||
| 747 | - .chart-controls { | ||
| 748 | - margin-bottom: 0.5rem; | ||
| 749 | - } | ||
| 750 | - | ||
| 751 | - .chart-header { | ||
| 752 | - margin-bottom: 0.25rem; | ||
| 753 | - } | ||
| 754 | - | ||
| 755 | - .chart-title { | ||
| 756 | - font-family: 'DM Mono', monospace; | ||
| 757 | - font-size: 0.6875rem; | ||
| 758 | - color: var(--theme-texto); | ||
| 759 | - opacity: 0.7; | ||
| 760 | - } | ||
| 761 | - | ||
| 762 | - .chart-period { | ||
| 763 | - opacity: 0.5; | ||
| 764 | - } | ||
| 765 | - | ||
| 766 | - .chart-loading { | ||
| 767 | - display: flex; | ||
| 768 | - align-items: center; | ||
| 769 | - justify-content: center; | ||
| 770 | - color: var(--theme-texto); | ||
| 771 | - opacity: 0.5; | ||
| 772 | - } | ||
| 773 | - | ||
| 774 | - .chart-flex-wrapper { | ||
| 775 | - flex: 1; | ||
| 776 | - min-height: 0; | ||
| 777 | - } | ||
| 778 | - | ||
| 779 | - .kpis { | ||
| 780 | - display: grid; | ||
| 781 | - grid-template-columns: repeat(4, 1fr); | ||
| 782 | - gap: 0.5rem; | ||
| 783 | - margin-top: 0.5rem; | ||
| 784 | - padding-top: 0.75rem; | ||
| 785 | - border-top: 1px solid var(--theme-borde); | ||
| 786 | - } | ||
| 787 | - | ||
| 788 | - .kpi { | ||
| 789 | - display: flex; | ||
| 790 | - flex-direction: column; | ||
| 791 | - gap: 0.125rem; | ||
| 792 | - } | ||
| 793 | - | ||
| 794 | - .kpi-value { | ||
| 795 | - font-family: 'DM Mono', monospace; | ||
| 796 | - font-size: 0.9375rem; | ||
| 797 | - font-weight: 600; | ||
| 798 | - color: var(--theme-titulo); | ||
| 799 | - } | ||
| 800 | - | ||
| 801 | - .kpi-label { | ||
| 802 | - font-size: 0.5625rem; | ||
| 803 | - color: var(--theme-texto); | ||
| 804 | - opacity: 0.6; | ||
| 805 | - line-height: 1.3; | ||
| 806 | - } | ||
| 807 | - | ||
| 808 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 809 | - RANKING | ||
| 810 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 811 | - .ranking-title { | ||
| 812 | - font-size: 1.125rem; | ||
| 813 | - font-weight: 600; | ||
| 814 | - color: var(--theme-titulo); | ||
| 815 | - margin: 0 0 0.25rem; | ||
| 816 | - } | ||
| 817 | - | ||
| 818 | - .ranking-desc { | ||
| 819 | - font-size: 0.75rem; | ||
| 820 | - color: var(--theme-texto); | ||
| 821 | - opacity: 0.6; | ||
| 822 | - margin: 0 0 0.75rem; | ||
| 823 | - } | ||
| 824 | - | ||
| 825 | - .ranking-cards { | ||
| 826 | - display: flex; | ||
| 827 | - flex-direction: column; | ||
| 828 | - gap: 0.25rem; | ||
| 829 | - } | ||
| 830 | - | ||
| 831 | - .ranking-card { | ||
| 832 | - display: flex; | ||
| 833 | - align-items: center; | ||
| 834 | - gap: 0.75rem; | ||
| 835 | - padding: 0.5rem 0.625rem; | ||
| 836 | - border-radius: 8px; | ||
| 837 | - background: var(--theme-borde); | ||
| 838 | - } | ||
| 839 | - | ||
| 840 | - .ranking-position { | ||
| 841 | - font-family: 'DM Mono', monospace; | ||
| 842 | - font-size: 0.6875rem; | ||
| 843 | - color: var(--theme-accent); | ||
| 844 | - font-weight: 600; | ||
| 845 | - min-width: 1.5rem; | ||
| 846 | - } | ||
| 847 | - | ||
| 848 | - .ranking-name { | ||
| 849 | - flex: 1; | ||
| 850 | - font-size: 0.75rem; | ||
| 851 | - color: var(--theme-titulo); | ||
| 852 | - } | ||
| 853 | - | ||
| 854 | - .ranking-pct { | ||
| 855 | - font-family: 'DM Mono', monospace; | ||
| 856 | - font-size: 0.8125rem; | ||
| 857 | - font-weight: 600; | ||
| 858 | - color: var(--theme-titulo); | ||
| 859 | - } | ||
| 860 | - | ||
| 861 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 862 | - DARK MODE | ||
| 863 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 864 | - :global(html.dark) .context-hint { | ||
| 865 | - color: rgba(255, 255, 255, 0.4); | ||
| 866 | - } | ||
| 867 | - | ||
| 868 | - :global(html.dark) .def-link { | ||
| 869 | - color: var(--theme-accent, #C9A751); | ||
| 870 | - } | ||
| 871 | -</style> |
| 1 | -import { supabase } from '$lib/supabase'; | ||
| 2 | -import { error } from '@sveltejs/kit'; | ||
| 3 | - | ||
| 4 | -// Funciones para derivar jerarquía del código objeto | ||
| 5 | -function getGrupoCode(objeto) { | ||
| 6 | - return objeto.charAt(0) + '0000'; | ||
| 7 | -} | ||
| 8 | - | ||
| 9 | -function getSubgrupoCode(objeto) { | ||
| 10 | - return objeto.substring(0, 2) + '000'; | ||
| 11 | -} | ||
| 12 | - | ||
| 13 | -function getPartidaCode(objeto) { | ||
| 14 | - return objeto.substring(0, 3) + '00'; | ||
| 15 | -} | ||
| 16 | - | ||
| 17 | -function getNivel(objeto) { | ||
| 18 | - if (objeto.endsWith('0000')) return 'grupo'; | ||
| 19 | - if (objeto.endsWith('000')) return 'subgrupo'; | ||
| 20 | - if (objeto.endsWith('00')) return 'partida'; | ||
| 21 | - return 'subpartida'; | ||
| 22 | -} | ||
| 23 | - | ||
| 24 | -export async function load({ params }) { | ||
| 25 | - console.time('[SERVER] Total load objeto'); | ||
| 26 | - const { codigo } = params; | ||
| 27 | - | ||
| 28 | - console.time('[SERVER] Query clas_objetos'); | ||
| 29 | - const { data, error: dbError } = await supabase | ||
| 30 | - .schema('ppto') | ||
| 31 | - .from('clas_objetos') | ||
| 32 | - .select('*') | ||
| 33 | - .eq('objeto', codigo); | ||
| 34 | - console.timeEnd('[SERVER] Query clas_objetos'); | ||
| 35 | - | ||
| 36 | - if (dbError || !data || data.length === 0) { | ||
| 37 | - throw error(404, 'Objeto no encontrado'); | ||
| 38 | - } | ||
| 39 | - | ||
| 40 | - // Tomar el primer resultado | ||
| 41 | - const objeto = data[0]; | ||
| 42 | - const nivel = objeto.nivel || getNivel(codigo); | ||
| 43 | - | ||
| 44 | - // Obtener jerarquía (padres e hijos) | ||
| 45 | - let padres = []; | ||
| 46 | - let hijos = []; | ||
| 47 | - | ||
| 48 | - console.time('[SERVER] Query padres'); | ||
| 49 | - | ||
| 50 | - // Buscar padres según nivel | ||
| 51 | - if (nivel === 'subpartida') { | ||
| 52 | - // Padres: grupo, subgrupo, partida | ||
| 53 | - const grupoCode = getGrupoCode(codigo); | ||
| 54 | - const subgrupoCode = getSubgrupoCode(codigo); | ||
| 55 | - const partidaCode = getPartidaCode(codigo); | ||
| 56 | - | ||
| 57 | - const { data: padresData } = await supabase | ||
| 58 | - .schema('ppto') | ||
| 59 | - .from('clas_objetos') | ||
| 60 | - .select('*') | ||
| 61 | - .in('objeto', [grupoCode, subgrupoCode, partidaCode]) | ||
| 62 | - .order('objeto'); | ||
| 63 | - | ||
| 64 | - if (padresData) padres = padresData; | ||
| 65 | - } else if (nivel === 'partida') { | ||
| 66 | - // Padres: grupo, subgrupo | ||
| 67 | - const grupoCode = getGrupoCode(codigo); | ||
| 68 | - const subgrupoCode = getSubgrupoCode(codigo); | ||
| 69 | - | ||
| 70 | - const { data: padresData } = await supabase | ||
| 71 | - .schema('ppto') | ||
| 72 | - .from('clas_objetos') | ||
| 73 | - .select('*') | ||
| 74 | - .in('objeto', [grupoCode, subgrupoCode]) | ||
| 75 | - .order('objeto'); | ||
| 76 | - | ||
| 77 | - if (padresData) padres = padresData; | ||
| 78 | - } else if (nivel === 'subgrupo') { | ||
| 79 | - // Padre: grupo | ||
| 80 | - const grupoCode = getGrupoCode(codigo); | ||
| 81 | - | ||
| 82 | - const { data: padresData } = await supabase | ||
| 83 | - .schema('ppto') | ||
| 84 | - .from('clas_objetos') | ||
| 85 | - .select('*') | ||
| 86 | - .eq('objeto', grupoCode); | ||
| 87 | - | ||
| 88 | - if (padresData) padres = padresData; | ||
| 89 | - } | ||
| 90 | - // grupo no tiene padres | ||
| 91 | - | ||
| 92 | - console.timeEnd('[SERVER] Query padres'); | ||
| 93 | - | ||
| 94 | - console.time('[SERVER] Query hijos'); | ||
| 95 | - | ||
| 96 | - // Buscar hijos según nivel | ||
| 97 | - if (nivel === 'grupo') { | ||
| 98 | - // Hijos: subgrupos que empiecen con el mismo dígito | ||
| 99 | - const prefix = codigo.charAt(0); | ||
| 100 | - const { data: hijosData } = await supabase | ||
| 101 | - .schema('ppto') | ||
| 102 | - .from('clas_objetos') | ||
| 103 | - .select('*') | ||
| 104 | - .eq('nivel', 'subgrupo') | ||
| 105 | - .like('objeto', `${prefix}%`) | ||
| 106 | - .order('objeto'); | ||
| 107 | - | ||
| 108 | - hijos = hijosData || []; | ||
| 109 | - } else if (nivel === 'subgrupo') { | ||
| 110 | - // Hijos: partidas que empiecen con los mismos 2 dígitos | ||
| 111 | - const prefix = codigo.substring(0, 2); | ||
| 112 | - const { data: hijosData } = await supabase | ||
| 113 | - .schema('ppto') | ||
| 114 | - .from('clas_objetos') | ||
| 115 | - .select('*') | ||
| 116 | - .eq('nivel', 'partida') | ||
| 117 | - .like('objeto', `${prefix}%`) | ||
| 118 | - .order('objeto'); | ||
| 119 | - | ||
| 120 | - hijos = hijosData || []; | ||
| 121 | - } else if (nivel === 'partida') { | ||
| 122 | - // Hijos: subpartidas que empiecen con los mismos 3 dígitos | ||
| 123 | - const prefix = codigo.substring(0, 3); | ||
| 124 | - const { data: hijosData } = await supabase | ||
| 125 | - .schema('ppto') | ||
| 126 | - .from('clas_objetos') | ||
| 127 | - .select('*') | ||
| 128 | - .eq('nivel', 'subpartida') | ||
| 129 | - .like('objeto', `${prefix}%`) | ||
| 130 | - .order('objeto'); | ||
| 131 | - | ||
| 132 | - hijos = hijosData || []; | ||
| 133 | - } | ||
| 134 | - // subpartida no tiene hijos | ||
| 135 | - | ||
| 136 | - console.timeEnd('[SERVER] Query hijos'); | ||
| 137 | - | ||
| 138 | - console.timeEnd('[SERVER] Total load objeto'); | ||
| 139 | - return { | ||
| 140 | - objeto, | ||
| 141 | - padres, | ||
| 142 | - hijos | ||
| 143 | - }; | ||
| 144 | -} |
| 1 | -<script> | ||
| 2 | - import { onMount } from 'svelte'; | ||
| 3 | - import { tweened } from 'svelte/motion'; | ||
| 4 | - import { cubicOut } from 'svelte/easing'; | ||
| 5 | - import { supabase } from '$lib/supabase'; | ||
| 6 | - import ObjetoSearch from '$lib/components/objeto/ObjetoSearch.svelte'; | ||
| 7 | - import BarChart from '$lib/components/objeto/BarChart.svelte'; | ||
| 8 | - import EntitySelector from '$lib/components/objeto/EntitySelector.svelte'; | ||
| 9 | - import { initIndex } from '$lib/stores/searchStore'; | ||
| 10 | - | ||
| 11 | - let { data } = $props(); | ||
| 12 | - let objetoData = $derived(data.objeto); | ||
| 13 | - let padres = $derived(data.padres); | ||
| 14 | - let hijos = $derived(data.hijos); | ||
| 15 | - | ||
| 16 | - let mounted = $state(false); | ||
| 17 | - let loading = $state(true); | ||
| 18 | - let hoveredYear = $state(null); | ||
| 19 | - let showDefinicionesModal = $state(false); | ||
| 20 | - let showMobileSidebar = $state(false); | ||
| 21 | - let metrica = $state('percapita'); // 'percapita' | 'monto' | ||
| 22 | - | ||
| 23 | - // Parsear descripciones | ||
| 24 | - function parseDescripciones(str) { | ||
| 25 | - if (!str) return []; | ||
| 26 | - try { | ||
| 27 | - const parsed = JSON.parse(str); | ||
| 28 | - return parsed.sort((a, b) => { | ||
| 29 | - const yA = a.rangos?.match(/\d{4}/g) || []; | ||
| 30 | - const yB = b.rangos?.match(/\d{4}/g) || []; | ||
| 31 | - return Math.max(...yB.map(Number), 0) - Math.max(...yA.map(Number), 0); | ||
| 32 | - }); | ||
| 33 | - } catch { return []; } | ||
| 34 | - } | ||
| 35 | - | ||
| 36 | - let descripciones = $derived(parseDescripciones(objetoData.descripciones)); | ||
| 37 | - let variaciones = $derived(descripciones.map(d => ({ rango: d.rangos, texto: d.descripcion }))); | ||
| 38 | - | ||
| 39 | - // Estado selector entidad | ||
| 40 | - let entidades = $state([]); | ||
| 41 | - let selectedEntity = $state(null); | ||
| 42 | - let nEntidades = $derived(data.nEntidades); | ||
| 43 | - | ||
| 44 | - // Datos | ||
| 45 | - let datosAnuales = $state([]); | ||
| 46 | - let datosHistorico = $state(null); | ||
| 47 | - let topEntidadesPorAño = $state({}); | ||
| 48 | - | ||
| 49 | - const POBLACION = 12000000; | ||
| 50 | - let objetoCodigo = $derived(objetoData.objeto); | ||
| 51 | - let objetoNivel = $derived(objetoData.nivel); | ||
| 52 | - | ||
| 53 | - let gastoPerCapita = $derived( | ||
| 54 | - datosAnuales.map(d => { | ||
| 55 | - const monto = selectedEntity ? (d.monto ?? d.devengado) : d.total; | ||
| 56 | - const perCapita = d.per_capita ?? (d.devengado ? d.devengado / POBLACION : 0); | ||
| 57 | - return { | ||
| 58 | - año: d.gestion, | ||
| 59 | - monto, | ||
| 60 | - perCapita: metrica === 'percapita' ? perCapita : monto | ||
| 61 | - }; | ||
| 62 | - }) | ||
| 63 | - ); | ||
| 64 | - | ||
| 65 | - let primerAño = $derived(gastoPerCapita.length > 0 ? gastoPerCapita[0].año : 2005); | ||
| 66 | - let ultimoAño = $derived(gastoPerCapita.length > 0 ? gastoPerCapita[gastoPerCapita.length - 1].año : 2025); | ||
| 67 | - | ||
| 68 | - let totalHistorico = $derived(datosHistorico ? (selectedEntity ? datosHistorico.monto : datosHistorico.total) : 0); | ||
| 69 | - let promedioPerCapita = $derived(datosHistorico?.per_capita || 0); | ||
| 70 | - let totalPorcentaje = $derived(datosHistorico?.prop || 0); | ||
| 71 | - let ranking = $derived(datosHistorico?.ranking || 0); | ||
| 72 | - let nPares = $derived(datosHistorico?.n_pares || 0); | ||
| 73 | - | ||
| 74 | - let currentData = $derived( | ||
| 75 | - hoveredYear ? gastoPerCapita.find(g => g.año === hoveredYear) : null | ||
| 76 | - ); | ||
| 77 | - | ||
| 78 | - let displayMonto = $derived(currentData ? currentData.monto : totalHistorico); | ||
| 79 | - let displayMontoLabel = $derived(currentData ? `gastados en ${currentData.año}` : `gastados ${primerAño}-${ultimoAño}`); | ||
| 80 | - let displayPerCapita = $derived(currentData ? currentData.perCapita : promedioPerCapita); | ||
| 81 | - let displayPerCapitaLabel = $derived(currentData ? `por cada boliviano en ${currentData.año}` : 'por cada boliviano'); | ||
| 82 | - let displayPorcentaje = $derived(currentData ? (totalHistorico > 0 ? totalPorcentaje * (currentData.monto / (totalHistorico / gastoPerCapita.length)) : 0) : totalPorcentaje); | ||
| 83 | - let displayPeriodo = $derived(currentData ? currentData.año : `${primerAño}-${ultimoAño}`); | ||
| 84 | - let displayRanking = $derived(`${currentData ? (datosAnuales.find(d => d.gestion === hoveredYear)?.ranking || ranking) : ranking} de ${selectedEntity ? (datosHistorico?.n_entidades || nEntidades) : nPares}`); | ||
| 85 | - let displayPorcentajeLabel = $derived(currentData ? `del gasto en ${currentData.año}` : 'del gasto total'); | ||
| 86 | - let nivelKey = $derived(objetoData.nivel?.toLowerCase()); | ||
| 87 | - let nivelPlural = $derived({ grupo: 'grupos', subgrupo: 'subgrupos', partida: 'partidas', subpartida: 'subpartidas' }[nivelKey] || 'objetos'); | ||
| 88 | - let nivelLabel = $derived({ grupo: 'Grupo', subgrupo: 'Subgrupo', partida: 'Partida', subpartida: 'Subpartida' }[nivelKey] || objetoData.nivel); | ||
| 89 | - let displayRankingLabel = $derived(currentData ? `${nivelPlural} más gastadas en ${currentData.año}` : `${nivelPlural} más gastadas`); | ||
| 90 | - | ||
| 91 | - // Top entidades | ||
| 92 | - let topEntidadesTotal = $derived( | ||
| 93 | - datosHistorico && !selectedEntity ? [ | ||
| 94 | - { nombre: datosHistorico.top1_entidad, porcentaje: datosHistorico.top1_pct }, | ||
| 95 | - { nombre: datosHistorico.top2_entidad, porcentaje: datosHistorico.top2_pct }, | ||
| 96 | - { nombre: datosHistorico.top3_entidad, porcentaje: datosHistorico.top3_pct }, | ||
| 97 | - ].filter(e => e.nombre) : [] | ||
| 98 | - ); | ||
| 99 | - let displayTopEntidades = $derived( | ||
| 100 | - hoveredYear && topEntidadesPorAño[hoveredYear] ? topEntidadesPorAño[hoveredYear] : topEntidadesTotal | ||
| 101 | - ); | ||
| 102 | - | ||
| 103 | - // Tweens | ||
| 104 | - const tw = { duration: 300, easing: cubicOut }; | ||
| 105 | - const twMonto = tweened(0, tw); | ||
| 106 | - const twPerCapita = tweened(0, tw); | ||
| 107 | - const twPorcentaje = tweened(0, tw); | ||
| 108 | - const twTop1 = tweened(0, tw); | ||
| 109 | - const twTop2 = tweened(0, tw); | ||
| 110 | - const twTop3 = tweened(0, tw); | ||
| 111 | - | ||
| 112 | - $effect(() => { twMonto.set(displayMonto); }); | ||
| 113 | - $effect(() => { twPerCapita.set(displayPerCapita); }); | ||
| 114 | - $effect(() => { twPorcentaje.set(displayPorcentaje); }); | ||
| 115 | - $effect(() => { if (displayTopEntidades[0]) twTop1.set(displayTopEntidades[0].porcentaje); }); | ||
| 116 | - $effect(() => { if (displayTopEntidades[1]) twTop2.set(displayTopEntidades[1].porcentaje); }); | ||
| 117 | - $effect(() => { if (displayTopEntidades[2]) twTop3.set(displayTopEntidades[2].porcentaje); }); | ||
| 118 | - | ||
| 119 | - function formatMontoLargo(n) { | ||
| 120 | - if (n >= 1e9) return `${(n / 1e9).toFixed(1)} mil mill.`; | ||
| 121 | - if (n >= 1e6) return `${(n / 1e6).toFixed(1)} mill.`; | ||
| 122 | - if (n >= 1e3) return `${(n / 1e3).toFixed(0)} mil`; | ||
| 123 | - return n.toLocaleString('es-BO'); | ||
| 124 | - } | ||
| 125 | - | ||
| 126 | - function formatPerCapita(n) { | ||
| 127 | - return n.toLocaleString('es-BO', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); | ||
| 128 | - } | ||
| 129 | - | ||
| 130 | - async function loadData() { | ||
| 131 | - loading = true; | ||
| 132 | - if (selectedEntity) { | ||
| 133 | - const { data } = await supabase.schema('ppto').from('vista_objeto_entidad').select('*') | ||
| 134 | - .eq('objeto', objetoCodigo).eq('nivel', objetoNivel).eq('entidad', selectedEntity.entidad).order('gestion'); | ||
| 135 | - if (data) { | ||
| 136 | - datosHistorico = data.find(d => d.gestion === 0) || null; | ||
| 137 | - datosAnuales = data.filter(d => d.gestion > 0); | ||
| 138 | - topEntidadesPorAño = {}; | ||
| 139 | - } | ||
| 140 | - } else { | ||
| 141 | - const { data } = await supabase.schema('ppto').from('vista_objeto_estado').select('*') | ||
| 142 | - .eq('objeto', objetoCodigo).order('gestion'); | ||
| 143 | - if (data) { | ||
| 144 | - datosHistorico = data.find(d => d.gestion === 0) || null; | ||
| 145 | - datosAnuales = data.filter(d => d.gestion > 0); | ||
| 146 | - topEntidadesPorAño = {}; | ||
| 147 | - datosAnuales.forEach(d => { | ||
| 148 | - topEntidadesPorAño[d.gestion] = [ | ||
| 149 | - { nombre: d.top1_entidad, porcentaje: d.top1_pct }, | ||
| 150 | - { nombre: d.top2_entidad, porcentaje: d.top2_pct }, | ||
| 151 | - { nombre: d.top3_entidad, porcentaje: d.top3_pct }, | ||
| 152 | - ].filter(e => e.nombre); | ||
| 153 | - }); | ||
| 154 | - } | ||
| 155 | - } | ||
| 156 | - loading = false; | ||
| 157 | - } | ||
| 158 | - | ||
| 159 | - async function loadEntidades() { | ||
| 160 | - const { data } = await supabase.schema('ppto').from('entidades_por_objeto').select('entidad, entidad_desc') | ||
| 161 | - .eq('objeto', objetoCodigo).eq('nivel', objetoNivel).order('entidad_desc'); | ||
| 162 | - if (data) entidades = data; | ||
| 163 | - } | ||
| 164 | - | ||
| 165 | - onMount(async () => { | ||
| 166 | - mounted = true; | ||
| 167 | - initIndex(); | ||
| 168 | - await Promise.all([loadData(), loadEntidades()]); | ||
| 169 | - }); | ||
| 170 | -</script> | ||
| 171 | - | ||
| 172 | -<svelte:head> | ||
| 173 | - <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| 174 | - <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@400;500&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" /> | ||
| 175 | -</svelte:head> | ||
| 176 | - | ||
| 177 | -<div class="page" class:mounted> | ||
| 178 | - <nav class="page-nav"> | ||
| 179 | - <a href="/clasificadores/objeto-gasto" class="back-link"> | ||
| 180 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 181 | - <path d="M19 12H5M12 19l-7-7 7-7"/> | ||
| 182 | - </svg> | ||
| 183 | - Clasificador | ||
| 184 | - </a> | ||
| 185 | - </nav> | ||
| 186 | - | ||
| 187 | - <div class="dashboard-layout"> | ||
| 188 | - <!-- Sidebar --> | ||
| 189 | - <aside class="dashboard-context" class:mobile-open={showMobileSidebar}> | ||
| 190 | - <!-- Mobile toggle --> | ||
| 191 | - <button class="mobile-sidebar-toggle" onclick={() => showMobileSidebar = !showMobileSidebar}> | ||
| 192 | - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 193 | - <circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> | ||
| 194 | - </svg> | ||
| 195 | - <span>{showMobileSidebar ? 'Ocultar herramientas' : 'Herramientas'}</span> | ||
| 196 | - </button> | ||
| 197 | - | ||
| 198 | - <div class="context-card"> | ||
| 199 | - <h2 class="context-main-title">Herramientas</h2> | ||
| 200 | - | ||
| 201 | - <!-- Buscar --> | ||
| 202 | - <div class="context-section"> | ||
| 203 | - <h3 class="context-subtitle">Buscar</h3> | ||
| 204 | - <p class="context-hint">Buscar otro objeto de gasto</p> | ||
| 205 | - <ObjetoSearch /> | ||
| 206 | - </div> | ||
| 207 | - | ||
| 208 | - <div class="context-separator"></div> | ||
| 209 | - | ||
| 210 | - <!-- Navegación --> | ||
| 211 | - {#if padres.length > 0 || hijos.length > 0} | ||
| 212 | - <div class="context-section"> | ||
| 213 | - <h3 class="context-subtitle">Navegación</h3> | ||
| 214 | - <p class="context-hint">Partidas relacionadas. Haz click para explorar.</p> | ||
| 215 | - <div class="tree"> | ||
| 216 | - {#each padres as padre, i} | ||
| 217 | - <a href="/objeto/{padre.objeto}" class="tree-node has-tooltip" style="--indent: {i}" data-tooltip="{padre.desc_objeto}"> | ||
| 218 | - <span class="tree-dot"></span> | ||
| 219 | - <span class="tree-code">{padre.objeto}</span> | ||
| 220 | - <span class="tree-name">{padre.desc_objeto}</span> | ||
| 221 | - </a> | ||
| 222 | - {/each} | ||
| 223 | - | ||
| 224 | - <div class="tree-node tree-node-current has-tooltip" style="--indent: {padres.length}" data-tooltip="{objetoData.desc_objeto}"> | ||
| 225 | - <span class="tree-dot"></span> | ||
| 226 | - <span class="tree-code">{objetoData.objeto}</span> | ||
| 227 | - <span class="tree-name">{objetoData.desc_objeto}</span> | ||
| 228 | - </div> | ||
| 229 | - | ||
| 230 | - {#if hijos.length > 0} | ||
| 231 | - <div class="tree-children" style="--indent: {padres.length + 1}"> | ||
| 232 | - {#each hijos as hijo} | ||
| 233 | - <a href="/objeto/{hijo.objeto}" class="tree-node tree-node-child has-tooltip" data-tooltip="{hijo.desc_objeto}"> | ||
| 234 | - <span class="tree-dot"></span> | ||
| 235 | - <span class="tree-code">{hijo.objeto}</span> | ||
| 236 | - <span class="tree-name">{hijo.desc_objeto}</span> | ||
| 237 | - </a> | ||
| 238 | - {/each} | ||
| 239 | - </div> | ||
| 240 | - {/if} | ||
| 241 | - </div> | ||
| 242 | - </div> | ||
| 243 | - | ||
| 244 | - <div class="context-separator"></div> | ||
| 245 | - {/if} | ||
| 246 | - | ||
| 247 | - <!-- Explorar --> | ||
| 248 | - <div class="context-section"> | ||
| 249 | - <h3 class="context-subtitle">Explorar visualizaciones</h3> | ||
| 250 | - <div class="explore-scroll"> | ||
| 251 | - <a href="/objeto/{objetoData.objeto}?vista=comparar" class="explore-card"> | ||
| 252 | - <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 253 | - <rect x="4" y="14" width="4" height="6"/><rect x="10" y="8" width="4" height="12"/><rect x="16" y="4" width="4" height="16"/> | ||
| 254 | - </svg> | ||
| 255 | - <span class="explore-title">Comparar dos entidades</span> | ||
| 256 | - <span class="explore-desc">¿Quién gasta más en {objetoData.desc_objeto?.toLowerCase()}?</span> | ||
| 257 | - </a> | ||
| 258 | - <a href="/clasificadores/objeto-gasto" class="explore-card"> | ||
| 259 | - <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 260 | - <path d="M3 20h18M5 17h2v3H5zM9 14h2v6H9zM13 11h2v9h-2zM17 8h2v12h-2z"/> | ||
| 261 | - </svg> | ||
| 262 | - <span class="explore-title">Ranking</span> | ||
| 263 | - <span class="explore-desc">{nEntidades} entidades que gastan en {objetoData.desc_objeto?.toLowerCase()}</span> | ||
| 264 | - </a> | ||
| 265 | - <a href="/ubicacion" class="explore-card explore-card-disabled"> | ||
| 266 | - <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 267 | - <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/> | ||
| 268 | - </svg> | ||
| 269 | - <span class="explore-title">Mapa</span> | ||
| 270 | - <span class="explore-desc">Dónde se concentra el gasto en {objetoData.desc_objeto?.toLowerCase()}</span> | ||
| 271 | - </a> | ||
| 272 | - <a href="/clasificadores/objeto-gasto?modo=mapa" class="explore-card"> | ||
| 273 | - <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 274 | - <circle cx="12" cy="12" r="10"/><path d="M12 2a15 15 0 0 1 4 10 15 15 0 0 1-4 10 15 15 0 0 1-4-10A15 15 0 0 1 12 2z"/> | ||
| 275 | - </svg> | ||
| 276 | - <span class="explore-title">Peso relativo</span> | ||
| 277 | - <span class="explore-desc">Cuánto pesa {objetoData.desc_objeto?.toLowerCase()} respecto a otros gastos</span> | ||
| 278 | - </a> | ||
| 279 | - <a href="/clasificadores/objeto-gasto" class="explore-card"> | ||
| 280 | - <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 281 | - <path d="M4 6h16M8 10h12M12 14h8M8 18h12"/> | ||
| 282 | - </svg> | ||
| 283 | - <span class="explore-title">Clasificador</span> | ||
| 284 | - <span class="explore-desc">Ver todos los objetos de gasto y navegar entre ellos</span> | ||
| 285 | - </a> | ||
| 286 | - </div> | ||
| 287 | - </div> | ||
| 288 | - | ||
| 289 | - <div class="context-separator"></div> | ||
| 290 | - | ||
| 291 | - <!-- Comparabilidad --> | ||
| 292 | - <div class="context-section"> | ||
| 293 | - <h3 class="context-subtitle">Comparabilidad</h3> | ||
| 294 | - <div class="vigencia-compact"> | ||
| 295 | - <div class="vigencia-status-icon vigencia-full"> | ||
| 296 | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 297 | - <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> | ||
| 298 | - <polyline points="22 4 12 14.01 9 11.01"/> | ||
| 299 | - </svg> | ||
| 300 | - </div> | ||
| 301 | - <div class="vigencia-text"> | ||
| 302 | - <span class="vigencia-main">Comparable en todo el período</span> | ||
| 303 | - <span class="vigencia-sub">2005 – 2025</span> | ||
| 304 | - </div> | ||
| 305 | - </div> | ||
| 306 | - </div> | ||
| 307 | - | ||
| 308 | - <div class="context-separator"></div> | ||
| 309 | - | ||
| 310 | - <!-- Definición --> | ||
| 311 | - <div class="context-section"> | ||
| 312 | - <h3 class="context-subtitle">Definición</h3> | ||
| 313 | - <button class="def-link" onclick={() => showDefinicionesModal = true}> | ||
| 314 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 315 | - <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/> | ||
| 316 | - </svg> | ||
| 317 | - {#if variaciones.length === 1} | ||
| 318 | - Ver definición oficial | ||
| 319 | - {:else} | ||
| 320 | - Ver {variaciones.length} variaciones en la definición | ||
| 321 | - {/if} | ||
| 322 | - </button> | ||
| 323 | - </div> | ||
| 324 | - </div> | ||
| 325 | - </aside> | ||
| 326 | - | ||
| 327 | - <!-- Main --> | ||
| 328 | - <main class="dashboard-main"> | ||
| 329 | - <div class="chart-protagonista"> | ||
| 330 | - <!-- Header --> | ||
| 331 | - <div class="chart-top-row"> | ||
| 332 | - <div class="main-title-pills"> | ||
| 333 | - <span class="pill pill-nivel">{nivelLabel}</span> | ||
| 334 | - <span class="pill pill-codigo">{objetoData.objeto}</span> | ||
| 335 | - </div> | ||
| 336 | - </div> | ||
| 337 | - | ||
| 338 | - <h2 class="main-title-name">{objetoData.desc_objeto}</h2> | ||
| 339 | - | ||
| 340 | - <div class="chart-controls"> | ||
| 341 | - <EntitySelector {entidades} bind:selectedEntity {nEntidades} /> | ||
| 342 | - </div> | ||
| 343 | - | ||
| 344 | - <div class="chart-header"> | ||
| 345 | - <span class="chart-title"> | ||
| 346 | - {metrica === 'percapita' ? 'Bs/habitante' : 'Bs (monto total)'} | ||
| 347 | - <span class="chart-period">· {displayPeriodo}</span> | ||
| 348 | - </span> | ||
| 349 | - <button class="metrica-toggle" onclick={() => metrica = metrica === 'percapita' ? 'monto' : 'percapita'}> | ||
| 350 | - {metrica === 'percapita' ? 'Ver monto real' : 'Ver per cápita'} | ||
| 351 | - </button> | ||
| 352 | - </div> | ||
| 353 | - | ||
| 354 | - {#if loading} | ||
| 355 | - <div class="chart-loading" style="flex: 1; min-height: 100px;"> | ||
| 356 | - <span>Cargando...</span> | ||
| 357 | - </div> | ||
| 358 | - {:else} | ||
| 359 | - <div class="chart-flex-wrapper"> | ||
| 360 | - <BarChart data={gastoPerCapita} bind:hoveredYear height={200} fill={true} /> | ||
| 361 | - </div> | ||
| 362 | - {/if} | ||
| 363 | - | ||
| 364 | - <!-- KPIs --> | ||
| 365 | - <div class="kpis"> | ||
| 366 | - <div class="kpi"> | ||
| 367 | - <span class="kpi-value">{formatMontoLargo(Math.round($twMonto))}</span> | ||
| 368 | - <span class="kpi-label">{displayMontoLabel}</span> | ||
| 369 | - </div> | ||
| 370 | - <div class="kpi"> | ||
| 371 | - <span class="kpi-value">{formatPerCapita($twPerCapita)} Bs.</span> | ||
| 372 | - <span class="kpi-label">{displayPerCapitaLabel}</span> | ||
| 373 | - </div> | ||
| 374 | - <div class="kpi"> | ||
| 375 | - <span class="kpi-value">{$twPorcentaje.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</span> | ||
| 376 | - <span class="kpi-label">{displayPorcentajeLabel}</span> | ||
| 377 | - </div> | ||
| 378 | - <div class="kpi"> | ||
| 379 | - <span class="kpi-value">{displayRanking}</span> | ||
| 380 | - <span class="kpi-label">{displayRankingLabel}</span> | ||
| 381 | - </div> | ||
| 382 | - </div> | ||
| 383 | - </div> | ||
| 384 | - | ||
| 385 | - {#if !selectedEntity && displayTopEntidades.length > 0} | ||
| 386 | - <div class="ranking-section"> | ||
| 387 | - <h3 class="ranking-title">Concentración del gasto</h3> | ||
| 388 | - <p class="ranking-desc">Porcentaje del total gastado en {objetoData.desc_objeto?.toLowerCase()} por todo el Estado {hoveredYear ? `en ${hoveredYear}` : `(${primerAño}-${ultimoAño})`}</p> | ||
| 389 | - <div class="ranking-cards"> | ||
| 390 | - {#each displayTopEntidades as ent, i} | ||
| 391 | - {@const pct = i === 0 ? $twTop1 : i === 1 ? $twTop2 : $twTop3} | ||
| 392 | - <div class="ranking-card"> | ||
| 393 | - <span class="ranking-position">#{i + 1}</span> | ||
| 394 | - <span class="ranking-name">{ent.nombre}</span> | ||
| 395 | - <span class="ranking-pct">{pct.toFixed(1)}%</span> | ||
| 396 | - </div> | ||
| 397 | - {/each} | ||
| 398 | - </div> | ||
| 399 | - </div> | ||
| 400 | - {/if} | ||
| 401 | - </main> | ||
| 402 | - </div> | ||
| 403 | - | ||
| 404 | - <!-- Modal de definiciones --> | ||
| 405 | - {#if showDefinicionesModal} | ||
| 406 | - <div class="modal-overlay" onclick={() => showDefinicionesModal = false}> | ||
| 407 | - <div class="modal-content" onclick={(e) => e.stopPropagation()}> | ||
| 408 | - <div class="modal-header"> | ||
| 409 | - <h3>Definiciones de "{objetoData.desc_objeto}"</h3> | ||
| 410 | - <button class="modal-close" onclick={() => showDefinicionesModal = false}> | ||
| 411 | - <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 412 | - <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> | ||
| 413 | - </svg> | ||
| 414 | - </button> | ||
| 415 | - </div> | ||
| 416 | - <div class="modal-body"> | ||
| 417 | - {#each variaciones as v, i} | ||
| 418 | - <div class="def-item" class:def-current={i === 0}> | ||
| 419 | - <div class="def-header"> | ||
| 420 | - <span class="def-period">{v.rango}</span> | ||
| 421 | - {#if i === 0} | ||
| 422 | - <span class="def-badge">Vigente</span> | ||
| 423 | - {/if} | ||
| 424 | - </div> | ||
| 425 | - <p class="def-text">{v.texto}</p> | ||
| 426 | - </div> | ||
| 427 | - {/each} | ||
| 428 | - </div> | ||
| 429 | - </div> | ||
| 430 | - </div> | ||
| 431 | - {/if} | ||
| 432 | -</div> | ||
| 433 | - | ||
| 434 | -<style> | ||
| 435 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 436 | - PAGE | ||
| 437 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 438 | - .page { | ||
| 439 | - min-height: 100vh; | ||
| 440 | - width: 100%; | ||
| 441 | - max-width: 100%; | ||
| 442 | - overflow-x: hidden; | ||
| 443 | - background: var(--theme-body); | ||
| 444 | - color: var(--theme-titulo); | ||
| 445 | - font-family: var(--font-sans), -apple-system, sans-serif; | ||
| 446 | - opacity: 0; | ||
| 447 | - transition: opacity 0.4s ease; | ||
| 448 | - } | ||
| 449 | - .page.mounted { opacity: 1; } | ||
| 450 | - | ||
| 451 | - :global(html:not(.dark)) .page { | ||
| 452 | - background: #f5f5f7; | ||
| 453 | - } | ||
| 454 | - | ||
| 455 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 456 | - NAV | ||
| 457 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 458 | - .page-nav { | ||
| 459 | - padding: 2rem 2rem 0.5rem; | ||
| 460 | - max-width: 1400px; | ||
| 461 | - margin: 0 auto; | ||
| 462 | - } | ||
| 463 | - | ||
| 464 | - .back-link { | ||
| 465 | - display: inline-flex; | ||
| 466 | - align-items: center; | ||
| 467 | - gap: 0.375rem; | ||
| 468 | - font-family: 'DM Mono', monospace; | ||
| 469 | - font-size: 0.6875rem; | ||
| 470 | - color: var(--theme-texto); | ||
| 471 | - text-decoration: none; | ||
| 472 | - transition: color 0.2s; | ||
| 473 | - } | ||
| 474 | - .back-link:hover { color: var(--theme-accent); } | ||
| 475 | - | ||
| 476 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 477 | - LAYOUT | ||
| 478 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 479 | - .dashboard-layout { | ||
| 480 | - display: grid; | ||
| 481 | - grid-template-columns: minmax(200px, 30%) 1fr; | ||
| 482 | - gap: 1.5rem; | ||
| 483 | - max-width: 1400px; | ||
| 484 | - margin: 0 auto; | ||
| 485 | - padding: 30px 2rem; | ||
| 486 | - height: calc(100vh - 4rem); | ||
| 487 | - overflow: hidden; | ||
| 488 | - } | ||
| 489 | - | ||
| 490 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 491 | - SIDEBAR | ||
| 492 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 493 | - .dashboard-context { | ||
| 494 | - display: flex; | ||
| 495 | - flex-direction: column; | ||
| 496 | - min-height: 0; | ||
| 497 | - } | ||
| 498 | - | ||
| 499 | - .context-card { | ||
| 500 | - background: var(--theme-surface); | ||
| 501 | - border-radius: 12px; | ||
| 502 | - padding: 0.75rem; | ||
| 503 | - display: flex; | ||
| 504 | - flex-direction: column; | ||
| 505 | - gap: 8px; | ||
| 506 | - flex: 1; | ||
| 507 | - min-height: 0; | ||
| 508 | - overflow-y: auto; | ||
| 509 | - scrollbar-width: thin; | ||
| 510 | - } | ||
| 511 | - | ||
| 512 | - .context-card::-webkit-scrollbar { width: 4px; } | ||
| 513 | - .context-card::-webkit-scrollbar-thumb { | ||
| 514 | - background: rgba(128, 128, 128, 0.3); | ||
| 515 | - border-radius: 2px; | ||
| 516 | - } | ||
| 517 | - | ||
| 518 | - .context-main-title { | ||
| 519 | - font-size: 1rem; | ||
| 520 | - font-weight: 600; | ||
| 521 | - color: var(--theme-titulo); | ||
| 522 | - margin: 0; | ||
| 523 | - line-height: 1.2; | ||
| 524 | - text-transform: uppercase; | ||
| 525 | - letter-spacing: 0.05em; | ||
| 526 | - opacity: 0.5; | ||
| 527 | - } | ||
| 528 | - | ||
| 529 | - .context-subtitle { | ||
| 530 | - font-family: 'Qanelas', var(--font-sans); | ||
| 531 | - font-size: 0.875rem; | ||
| 532 | - font-weight: 700; | ||
| 533 | - color: var(--theme-titulo); | ||
| 534 | - margin: 0; | ||
| 535 | - } | ||
| 536 | - | ||
| 537 | - .context-hint { | ||
| 538 | - font-size: 0.75rem; | ||
| 539 | - color: var(--theme-texto); | ||
| 540 | - opacity: 0.7; | ||
| 541 | - margin: 0; | ||
| 542 | - line-height: 1.4; | ||
| 543 | - } | ||
| 544 | - | ||
| 545 | - .context-section { | ||
| 546 | - display: flex; | ||
| 547 | - flex-direction: column; | ||
| 548 | - gap: 0.5rem; | ||
| 549 | - } | ||
| 550 | - | ||
| 551 | - .context-separator { | ||
| 552 | - border-top: 1px solid var(--theme-borde); | ||
| 553 | - margin: 0.25rem 0; | ||
| 554 | - } | ||
| 555 | - | ||
| 556 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 557 | - TREE | ||
| 558 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 559 | - .tree { | ||
| 560 | - font-size: 0.75rem; | ||
| 561 | - line-height: 1.6; | ||
| 562 | - } | ||
| 563 | - | ||
| 564 | - .tree-node { | ||
| 565 | - display: flex; | ||
| 566 | - align-items: center; | ||
| 567 | - gap: 0.375rem; | ||
| 568 | - padding: 0.375rem 0.5rem; | ||
| 569 | - margin-left: calc(var(--indent, 0) * 1rem); | ||
| 570 | - border-left: 2px solid transparent; | ||
| 571 | - border-radius: 0 6px 6px 0; | ||
| 572 | - text-decoration: none; | ||
| 573 | - color: var(--theme-texto); | ||
| 574 | - transition: all 0.15s; | ||
| 575 | - } | ||
| 576 | - .tree-node:hover { color: var(--theme-accent); } | ||
| 577 | - | ||
| 578 | - .tree-node-current { | ||
| 579 | - color: var(--theme-titulo); | ||
| 580 | - font-weight: 600; | ||
| 581 | - border-left-color: var(--theme-accent); | ||
| 582 | - background: rgba(201, 167, 81, 0.1); | ||
| 583 | - } | ||
| 584 | - | ||
| 585 | - .tree-children { | ||
| 586 | - margin-left: calc(var(--indent, 0) * 1rem); | ||
| 587 | - } | ||
| 588 | - | ||
| 589 | - .tree-node-child { | ||
| 590 | - padding-left: 0.75rem; | ||
| 591 | - } | ||
| 592 | - | ||
| 593 | - .tree-dot { | ||
| 594 | - width: 4px; | ||
| 595 | - height: 4px; | ||
| 596 | - border-radius: 50%; | ||
| 597 | - background: currentColor; | ||
| 598 | - flex-shrink: 0; | ||
| 599 | - opacity: 0.4; | ||
| 600 | - } | ||
| 601 | - | ||
| 602 | - .tree-code { | ||
| 603 | - font-family: 'DM Mono', monospace; | ||
| 604 | - font-size: 0.6875rem; | ||
| 605 | - min-width: 2.5rem; | ||
| 606 | - opacity: 0.6; | ||
| 607 | - } | ||
| 608 | - | ||
| 609 | - .tree-name { | ||
| 610 | - font-size: 0.6875rem; | ||
| 611 | - white-space: nowrap; | ||
| 612 | - overflow: hidden; | ||
| 613 | - text-overflow: ellipsis; | ||
| 614 | - } | ||
| 615 | - | ||
| 616 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 617 | - EXPLORE | ||
| 618 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 619 | - .explore-scroll { | ||
| 620 | - display: flex; | ||
| 621 | - gap: 8px; | ||
| 622 | - overflow-x: auto; | ||
| 623 | - scrollbar-width: none; | ||
| 624 | - padding-bottom: 2px; | ||
| 625 | - } | ||
| 626 | - .explore-scroll::-webkit-scrollbar { display: none; } | ||
| 627 | - | ||
| 628 | - .explore-card { | ||
| 629 | - position: relative; | ||
| 630 | - display: flex; | ||
| 631 | - flex-direction: column; | ||
| 632 | - justify-content: flex-start; | ||
| 633 | - padding: 0.375rem 0.5rem; | ||
| 634 | - width: 120px; | ||
| 635 | - height: 95px; | ||
| 636 | - border-radius: 8px; | ||
| 637 | - background: var(--theme-borde); | ||
| 638 | - text-decoration: none; | ||
| 639 | - color: var(--theme-texto); | ||
| 640 | - transition: all 0.15s; | ||
| 641 | - flex-shrink: 0; | ||
| 642 | - overflow: hidden; | ||
| 643 | - } | ||
| 644 | - .explore-card:hover { | ||
| 645 | - background: rgba(201, 167, 81, 0.15); | ||
| 646 | - color: var(--theme-accent); | ||
| 647 | - } | ||
| 648 | - .explore-card:hover .explore-watermark { | ||
| 649 | - opacity: 0.18; | ||
| 650 | - color: var(--theme-accent); | ||
| 651 | - } | ||
| 652 | - .explore-card:hover .explore-title { | ||
| 653 | - color: var(--theme-accent); | ||
| 654 | - } | ||
| 655 | - | ||
| 656 | - .explore-watermark { | ||
| 657 | - position: absolute; | ||
| 658 | - bottom: -4px; | ||
| 659 | - right: -4px; | ||
| 660 | - width: 44px; | ||
| 661 | - height: 44px; | ||
| 662 | - opacity: 0.15; | ||
| 663 | - pointer-events: none; | ||
| 664 | - color: var(--theme-accent, #C9A751); | ||
| 665 | - } | ||
| 666 | - | ||
| 667 | - .explore-title { | ||
| 668 | - position: relative; | ||
| 669 | - font-size: 0.75rem; | ||
| 670 | - font-weight: 600; | ||
| 671 | - line-height: 1.3; | ||
| 672 | - color: var(--theme-titulo); | ||
| 673 | - z-index: 1; | ||
| 674 | - } | ||
| 675 | - | ||
| 676 | - .explore-desc { | ||
| 677 | - position: relative; | ||
| 678 | - font-size: 0.625rem; | ||
| 679 | - line-height: 1.35; | ||
| 680 | - opacity: 0.6; | ||
| 681 | - margin-top: 3px; | ||
| 682 | - z-index: 1; | ||
| 683 | - } | ||
| 684 | - | ||
| 685 | - .explore-card-disabled { | ||
| 686 | - opacity: 0.3; | ||
| 687 | - pointer-events: none; | ||
| 688 | - } | ||
| 689 | - | ||
| 690 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 691 | - VIGENCIA | ||
| 692 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 693 | - .vigencia-compact { | ||
| 694 | - display: flex; | ||
| 695 | - align-items: center; | ||
| 696 | - gap: 0.75rem; | ||
| 697 | - } | ||
| 698 | - | ||
| 699 | - .vigencia-status-icon { | ||
| 700 | - width: 32px; | ||
| 701 | - height: 32px; | ||
| 702 | - border-radius: 8px; | ||
| 703 | - display: flex; | ||
| 704 | - align-items: center; | ||
| 705 | - justify-content: center; | ||
| 706 | - flex-shrink: 0; | ||
| 707 | - } | ||
| 708 | - | ||
| 709 | - .vigencia-status-icon.vigencia-full { | ||
| 710 | - background: rgba(74, 158, 107, 0.15); | ||
| 711 | - color: #4A9E6B; | ||
| 712 | - } | ||
| 713 | - | ||
| 714 | - .vigencia-text { | ||
| 715 | - display: flex; | ||
| 716 | - flex-direction: column; | ||
| 717 | - gap: 0.125rem; | ||
| 718 | - } | ||
| 719 | - | ||
| 720 | - .vigencia-main { | ||
| 721 | - font-size: 0.75rem; | ||
| 722 | - font-weight: 500; | ||
| 723 | - color: var(--theme-titulo); | ||
| 724 | - } | ||
| 725 | - | ||
| 726 | - .vigencia-sub { | ||
| 727 | - font-size: 0.625rem; | ||
| 728 | - color: var(--theme-texto); | ||
| 729 | - opacity: 0.5; | ||
| 730 | - } | ||
| 731 | - | ||
| 732 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 733 | - DEFINICIÓN | ||
| 734 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 735 | - .def-link { | ||
| 736 | - display: flex; | ||
| 737 | - align-items: center; | ||
| 738 | - gap: 0.375rem; | ||
| 739 | - font-size: 0.75rem; | ||
| 740 | - color: var(--theme-accent, #C9A751); | ||
| 741 | - background: none; | ||
| 742 | - border: none; | ||
| 743 | - padding: 0; | ||
| 744 | - cursor: pointer; | ||
| 745 | - transition: opacity 0.15s; | ||
| 746 | - } | ||
| 747 | - .def-link:hover { opacity: 0.7; } | ||
| 748 | - .def-link svg { flex-shrink: 0; opacity: 0.8; } | ||
| 749 | - | ||
| 750 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 751 | - MAIN | ||
| 752 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 753 | - .dashboard-main { | ||
| 754 | - display: flex; | ||
| 755 | - flex-direction: column; | ||
| 756 | - gap: 1.5rem; | ||
| 757 | - min-height: 0; | ||
| 758 | - overflow: hidden; | ||
| 759 | - } | ||
| 760 | - | ||
| 761 | - .chart-protagonista { | ||
| 762 | - background: var(--theme-surface); | ||
| 763 | - border-radius: 12px; | ||
| 764 | - padding: 0.75rem; | ||
| 765 | - display: flex; | ||
| 766 | - flex-direction: column; | ||
| 767 | - min-height: 0; | ||
| 768 | - flex: 2; | ||
| 769 | - overflow: hidden; | ||
| 770 | - } | ||
| 771 | - | ||
| 772 | - .ranking-section { | ||
| 773 | - background: var(--theme-surface); | ||
| 774 | - border-radius: 12px; | ||
| 775 | - padding: 0.75rem; | ||
| 776 | - flex: 1; | ||
| 777 | - display: flex; | ||
| 778 | - flex-direction: column; | ||
| 779 | - min-height: 0; | ||
| 780 | - } | ||
| 781 | - | ||
| 782 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 783 | - CHART & KPIs | ||
| 784 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 785 | - .chart-top-row { | ||
| 786 | - display: flex; | ||
| 787 | - justify-content: space-between; | ||
| 788 | - align-items: center; | ||
| 789 | - margin-bottom: 0.375rem; | ||
| 790 | - } | ||
| 791 | - | ||
| 792 | - .main-title-pills { | ||
| 793 | - display: flex; | ||
| 794 | - gap: 0.375rem; | ||
| 795 | - } | ||
| 796 | - | ||
| 797 | - .pill { | ||
| 798 | - font-family: 'DM Mono', monospace; | ||
| 799 | - font-size: 0.625rem; | ||
| 800 | - padding: 0.125rem 0.5rem; | ||
| 801 | - border-radius: 4px; | ||
| 802 | - letter-spacing: 0.03em; | ||
| 803 | - } | ||
| 804 | - | ||
| 805 | - .pill-nivel { | ||
| 806 | - background: rgba(201, 167, 81, 0.15); | ||
| 807 | - color: var(--theme-accent); | ||
| 808 | - } | ||
| 809 | - | ||
| 810 | - .pill-codigo { | ||
| 811 | - background: var(--theme-borde); | ||
| 812 | - color: var(--theme-texto); | ||
| 813 | - } | ||
| 814 | - | ||
| 815 | - .main-title-name { | ||
| 816 | - font-family: 'DM Serif Display', serif; | ||
| 817 | - font-size: 2rem; | ||
| 818 | - font-weight: 400; | ||
| 819 | - margin: 0 0 0.5rem; | ||
| 820 | - line-height: 1.15; | ||
| 821 | - color: var(--theme-titulo); | ||
| 822 | - } | ||
| 823 | - | ||
| 824 | - .chart-controls { | ||
| 825 | - margin-bottom: 0.5rem; | ||
| 826 | - } | ||
| 827 | - | ||
| 828 | - .chart-header { | ||
| 829 | - display: flex; | ||
| 830 | - align-items: center; | ||
| 831 | - justify-content: space-between; | ||
| 832 | - margin-bottom: 0.25rem; | ||
| 833 | - } | ||
| 834 | - | ||
| 835 | - .chart-title { | ||
| 836 | - font-family: 'DM Mono', monospace; | ||
| 837 | - font-size: 0.6875rem; | ||
| 838 | - color: var(--theme-texto); | ||
| 839 | - opacity: 0.7; | ||
| 840 | - } | ||
| 841 | - | ||
| 842 | - .chart-period { | ||
| 843 | - opacity: 0.5; | ||
| 844 | - } | ||
| 845 | - | ||
| 846 | - .metrica-toggle { | ||
| 847 | - font-family: 'DM Mono', monospace; | ||
| 848 | - font-size: 0.625rem; | ||
| 849 | - color: var(--theme-accent); | ||
| 850 | - background: rgba(201, 167, 81, 0.1); | ||
| 851 | - border: 1px solid rgba(201, 167, 81, 0.2); | ||
| 852 | - padding: 0.2rem 0.5rem; | ||
| 853 | - border-radius: 4px; | ||
| 854 | - cursor: pointer; | ||
| 855 | - transition: all 0.15s; | ||
| 856 | - } | ||
| 857 | - | ||
| 858 | - .metrica-toggle:hover { | ||
| 859 | - background: rgba(201, 167, 81, 0.2); | ||
| 860 | - } | ||
| 861 | - | ||
| 862 | - .chart-loading { | ||
| 863 | - display: flex; | ||
| 864 | - align-items: center; | ||
| 865 | - justify-content: center; | ||
| 866 | - color: var(--theme-texto); | ||
| 867 | - opacity: 0.5; | ||
| 868 | - } | ||
| 869 | - | ||
| 870 | - .chart-flex-wrapper { | ||
| 871 | - flex: 1; | ||
| 872 | - min-height: 0; | ||
| 873 | - } | ||
| 874 | - | ||
| 875 | - .kpis { | ||
| 876 | - display: grid; | ||
| 877 | - grid-template-columns: repeat(4, 1fr); | ||
| 878 | - gap: 0.5rem; | ||
| 879 | - margin-top: 0.5rem; | ||
| 880 | - padding-top: 0.75rem; | ||
| 881 | - border-top: 1px solid var(--theme-borde); | ||
| 882 | - } | ||
| 883 | - | ||
| 884 | - .kpi { | ||
| 885 | - display: flex; | ||
| 886 | - flex-direction: column; | ||
| 887 | - align-items: center; | ||
| 888 | - gap: 0.125rem; | ||
| 889 | - text-align: center; | ||
| 890 | - } | ||
| 891 | - | ||
| 892 | - .kpi-value { | ||
| 893 | - font-family: 'Qanelas', var(--font-sans); | ||
| 894 | - font-size: 1rem; | ||
| 895 | - font-weight: 600; | ||
| 896 | - color: var(--theme-titulo); | ||
| 897 | - } | ||
| 898 | - | ||
| 899 | - .kpi-label { | ||
| 900 | - font-size: 0.625rem; | ||
| 901 | - color: var(--theme-texto); | ||
| 902 | - opacity: 0.5; | ||
| 903 | - line-height: 1.35; | ||
| 904 | - } | ||
| 905 | - | ||
| 906 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 907 | - RANKING | ||
| 908 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 909 | - .ranking-title { | ||
| 910 | - font-size: 1rem; | ||
| 911 | - font-weight: 600; | ||
| 912 | - color: var(--theme-titulo); | ||
| 913 | - margin: 0 0 0.25rem; | ||
| 914 | - } | ||
| 915 | - | ||
| 916 | - .ranking-desc { | ||
| 917 | - font-size: 0.6875rem; | ||
| 918 | - color: var(--theme-texto); | ||
| 919 | - opacity: 0.5; | ||
| 920 | - margin: 0 0 0.5rem; | ||
| 921 | - } | ||
| 922 | - | ||
| 923 | - .ranking-cards { | ||
| 924 | - display: flex; | ||
| 925 | - flex-direction: column; | ||
| 926 | - gap: 0.25rem; | ||
| 927 | - } | ||
| 928 | - | ||
| 929 | - .ranking-card { | ||
| 930 | - display: flex; | ||
| 931 | - align-items: center; | ||
| 932 | - gap: 0.75rem; | ||
| 933 | - padding: 0.5rem 0.625rem; | ||
| 934 | - border-radius: 8px; | ||
| 935 | - background: rgba(128, 128, 128, 0.06); | ||
| 936 | - } | ||
| 937 | - | ||
| 938 | - .ranking-position { | ||
| 939 | - font-family: 'DM Mono', monospace; | ||
| 940 | - font-size: 0.6875rem; | ||
| 941 | - color: var(--theme-accent); | ||
| 942 | - font-weight: 600; | ||
| 943 | - min-width: 1.5rem; | ||
| 944 | - } | ||
| 945 | - | ||
| 946 | - .ranking-name { | ||
| 947 | - flex: 1; | ||
| 948 | - font-size: 0.8125rem; | ||
| 949 | - color: var(--theme-titulo); | ||
| 950 | - white-space: nowrap; | ||
| 951 | - overflow: hidden; | ||
| 952 | - text-overflow: ellipsis; | ||
| 953 | - } | ||
| 954 | - | ||
| 955 | - .ranking-pct { | ||
| 956 | - font-family: 'DM Mono', monospace; | ||
| 957 | - font-size: 0.875rem; | ||
| 958 | - font-weight: 600; | ||
| 959 | - color: var(--theme-titulo); | ||
| 960 | - } | ||
| 961 | - | ||
| 962 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 963 | - MODAL | ||
| 964 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 965 | - .modal-overlay { | ||
| 966 | - position: fixed; | ||
| 967 | - inset: 0; | ||
| 968 | - background: rgba(0, 0, 0, 0.6); | ||
| 969 | - backdrop-filter: blur(4px); | ||
| 970 | - display: flex; | ||
| 971 | - align-items: center; | ||
| 972 | - justify-content: center; | ||
| 973 | - z-index: 1000; | ||
| 974 | - padding: 1rem; | ||
| 975 | - } | ||
| 976 | - | ||
| 977 | - .modal-content { | ||
| 978 | - background: var(--theme-surface); | ||
| 979 | - border-radius: 16px; | ||
| 980 | - max-width: 560px; | ||
| 981 | - width: 100%; | ||
| 982 | - max-height: 80vh; | ||
| 983 | - overflow: hidden; | ||
| 984 | - display: flex; | ||
| 985 | - flex-direction: column; | ||
| 986 | - box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2); | ||
| 987 | - } | ||
| 988 | - | ||
| 989 | - .modal-header { | ||
| 990 | - display: flex; | ||
| 991 | - align-items: center; | ||
| 992 | - justify-content: space-between; | ||
| 993 | - padding: 1.25rem 1.5rem; | ||
| 994 | - border-bottom: 1px solid var(--theme-borde); | ||
| 995 | - } | ||
| 996 | - | ||
| 997 | - .modal-header h3 { | ||
| 998 | - font-size: 1rem; | ||
| 999 | - font-weight: 600; | ||
| 1000 | - color: var(--theme-titulo); | ||
| 1001 | - margin: 0; | ||
| 1002 | - } | ||
| 1003 | - | ||
| 1004 | - .modal-close { | ||
| 1005 | - color: var(--theme-texto); | ||
| 1006 | - background: transparent; | ||
| 1007 | - border: none; | ||
| 1008 | - padding: 0.25rem; | ||
| 1009 | - cursor: pointer; | ||
| 1010 | - border-radius: 6px; | ||
| 1011 | - transition: background 0.2s; | ||
| 1012 | - } | ||
| 1013 | - | ||
| 1014 | - .modal-close:hover { | ||
| 1015 | - background: var(--theme-borde); | ||
| 1016 | - } | ||
| 1017 | - | ||
| 1018 | - .modal-body { | ||
| 1019 | - padding: 1.5rem; | ||
| 1020 | - overflow-y: auto; | ||
| 1021 | - display: flex; | ||
| 1022 | - flex-direction: column; | ||
| 1023 | - gap: 1rem; | ||
| 1024 | - } | ||
| 1025 | - | ||
| 1026 | - .def-item { | ||
| 1027 | - padding: 1rem; | ||
| 1028 | - background: var(--theme-body); | ||
| 1029 | - border-radius: 10px; | ||
| 1030 | - border-left: 3px solid var(--theme-borde); | ||
| 1031 | - } | ||
| 1032 | - | ||
| 1033 | - .def-current { | ||
| 1034 | - border-left-color: #6B9FD4; | ||
| 1035 | - } | ||
| 1036 | - | ||
| 1037 | - .def-header { | ||
| 1038 | - display: flex; | ||
| 1039 | - align-items: center; | ||
| 1040 | - gap: 0.5rem; | ||
| 1041 | - margin-bottom: 0.5rem; | ||
| 1042 | - } | ||
| 1043 | - | ||
| 1044 | - .def-period { | ||
| 1045 | - font-family: 'DM Mono', monospace; | ||
| 1046 | - font-size: 0.75rem; | ||
| 1047 | - font-weight: 500; | ||
| 1048 | - color: #6B9FD4; | ||
| 1049 | - } | ||
| 1050 | - | ||
| 1051 | - .def-badge { | ||
| 1052 | - font-size: 0.625rem; | ||
| 1053 | - font-weight: 600; | ||
| 1054 | - text-transform: uppercase; | ||
| 1055 | - letter-spacing: 0.05em; | ||
| 1056 | - color: white; | ||
| 1057 | - background: #4A9E6B; | ||
| 1058 | - padding: 0.125rem 0.5rem; | ||
| 1059 | - border-radius: 4px; | ||
| 1060 | - } | ||
| 1061 | - | ||
| 1062 | - .def-text { | ||
| 1063 | - font-size: 0.875rem; | ||
| 1064 | - color: var(--theme-texto); | ||
| 1065 | - line-height: 1.6; | ||
| 1066 | - margin: 0; | ||
| 1067 | - } | ||
| 1068 | - | ||
| 1069 | - :global(html.dark) .def-current { | ||
| 1070 | - border-left-color: var(--theme-accent); | ||
| 1071 | - } | ||
| 1072 | - | ||
| 1073 | - :global(html.dark) .def-period { | ||
| 1074 | - color: var(--theme-accent); | ||
| 1075 | - } | ||
| 1076 | - | ||
| 1077 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 1078 | - MOBILE SIDEBAR | ||
| 1079 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 1080 | - .mobile-sidebar-toggle { | ||
| 1081 | - display: none; | ||
| 1082 | - } | ||
| 1083 | - | ||
| 1084 | - @media (max-width: 900px) { | ||
| 1085 | - .dashboard-layout { | ||
| 1086 | - grid-template-columns: 1fr; | ||
| 1087 | - gap: 0.5rem; | ||
| 1088 | - height: auto; | ||
| 1089 | - overflow: visible; | ||
| 1090 | - padding: 1.5rem 1rem 2rem; | ||
| 1091 | - align-content: center; | ||
| 1092 | - min-height: calc(100vh - 4rem); | ||
| 1093 | - } | ||
| 1094 | - | ||
| 1095 | - .dashboard-main { | ||
| 1096 | - overflow: visible; | ||
| 1097 | - } | ||
| 1098 | - | ||
| 1099 | - .chart-protagonista { | ||
| 1100 | - flex: none; | ||
| 1101 | - } | ||
| 1102 | - | ||
| 1103 | - .ranking-section { | ||
| 1104 | - flex: none; | ||
| 1105 | - } | ||
| 1106 | - | ||
| 1107 | - .mobile-sidebar-toggle { | ||
| 1108 | - display: flex; | ||
| 1109 | - align-items: center; | ||
| 1110 | - gap: 0.5rem; | ||
| 1111 | - padding: 0.625rem 1rem; | ||
| 1112 | - background: var(--theme-surface); | ||
| 1113 | - border: 1px solid var(--theme-borde); | ||
| 1114 | - border-radius: 10px; | ||
| 1115 | - color: var(--theme-titulo); | ||
| 1116 | - font-family: 'Qanelas', var(--font-sans); | ||
| 1117 | - font-size: 0.8125rem; | ||
| 1118 | - font-weight: 500; | ||
| 1119 | - cursor: pointer; | ||
| 1120 | - transition: all 0.2s; | ||
| 1121 | - margin-bottom: 0.25rem; | ||
| 1122 | - width: 100%; | ||
| 1123 | - } | ||
| 1124 | - | ||
| 1125 | - .mobile-sidebar-toggle:hover { | ||
| 1126 | - background: var(--theme-surface-hover); | ||
| 1127 | - } | ||
| 1128 | - | ||
| 1129 | - .mobile-sidebar-toggle svg { | ||
| 1130 | - color: var(--theme-texto); | ||
| 1131 | - opacity: 0.6; | ||
| 1132 | - } | ||
| 1133 | - | ||
| 1134 | - .dashboard-context .context-card { | ||
| 1135 | - display: none; | ||
| 1136 | - } | ||
| 1137 | - | ||
| 1138 | - .dashboard-context.mobile-open .context-card { | ||
| 1139 | - display: flex; | ||
| 1140 | - animation: slideDown 0.25s ease; | ||
| 1141 | - } | ||
| 1142 | - | ||
| 1143 | - @keyframes slideDown { | ||
| 1144 | - from { opacity: 0; transform: translateY(-8px); } | ||
| 1145 | - to { opacity: 1; transform: translateY(0); } | ||
| 1146 | - } | ||
| 1147 | - | ||
| 1148 | - .context-card { | ||
| 1149 | - height: auto; | ||
| 1150 | - overflow-y: visible; | ||
| 1151 | - } | ||
| 1152 | - | ||
| 1153 | - .page-nav { | ||
| 1154 | - padding: 0.75rem 1rem; | ||
| 1155 | - } | ||
| 1156 | - | ||
| 1157 | - .kpis { | ||
| 1158 | - grid-template-columns: repeat(2, 1fr); | ||
| 1159 | - } | ||
| 1160 | - } | ||
| 1161 | - | ||
| 1162 | - @media (max-width: 480px) { | ||
| 1163 | - .dashboard-layout { | ||
| 1164 | - padding: 0.5rem; | ||
| 1165 | - } | ||
| 1166 | - | ||
| 1167 | - .page-nav { | ||
| 1168 | - padding: 0.5rem; | ||
| 1169 | - } | ||
| 1170 | - | ||
| 1171 | - .chart-protagonista, | ||
| 1172 | - .ranking-section, | ||
| 1173 | - .context-card { | ||
| 1174 | - padding: 0.5rem; | ||
| 1175 | - border-radius: 8px; | ||
| 1176 | - } | ||
| 1177 | - | ||
| 1178 | - .main-title-name { | ||
| 1179 | - font-size: 1.375rem; | ||
| 1180 | - } | ||
| 1181 | - | ||
| 1182 | - .chart-header { | ||
| 1183 | - flex-direction: column; | ||
| 1184 | - align-items: flex-start; | ||
| 1185 | - gap: 0.25rem; | ||
| 1186 | - } | ||
| 1187 | - | ||
| 1188 | - .kpis { | ||
| 1189 | - grid-template-columns: repeat(2, 1fr); | ||
| 1190 | - gap: 0.375rem; | ||
| 1191 | - } | ||
| 1192 | - | ||
| 1193 | - .kpi-value { | ||
| 1194 | - font-size: 0.8125rem; | ||
| 1195 | - } | ||
| 1196 | - | ||
| 1197 | - .kpi-label { | ||
| 1198 | - font-size: 0.5625rem; | ||
| 1199 | - } | ||
| 1200 | - | ||
| 1201 | - .explore-scroll { | ||
| 1202 | - gap: 6px; | ||
| 1203 | - } | ||
| 1204 | - | ||
| 1205 | - .explore-card { | ||
| 1206 | - width: 100px; | ||
| 1207 | - height: 80px; | ||
| 1208 | - } | ||
| 1209 | - | ||
| 1210 | - .explore-title { | ||
| 1211 | - font-size: 0.625rem; | ||
| 1212 | - } | ||
| 1213 | - | ||
| 1214 | - .explore-desc { | ||
| 1215 | - font-size: 0.5rem; | ||
| 1216 | - } | ||
| 1217 | - | ||
| 1218 | - .ranking-title { | ||
| 1219 | - font-size: 0.875rem; | ||
| 1220 | - } | ||
| 1221 | - | ||
| 1222 | - .ranking-name { | ||
| 1223 | - font-size: 0.6875rem; | ||
| 1224 | - } | ||
| 1225 | - | ||
| 1226 | - .ranking-pct { | ||
| 1227 | - font-size: 0.75rem; | ||
| 1228 | - } | ||
| 1229 | - | ||
| 1230 | - .modal-content { | ||
| 1231 | - margin: 0.5rem; | ||
| 1232 | - max-height: 90vh; | ||
| 1233 | - } | ||
| 1234 | - | ||
| 1235 | - .tree-name { | ||
| 1236 | - max-width: 150px; | ||
| 1237 | - } | ||
| 1238 | - } | ||
| 1239 | - | ||
| 1240 | - /* ═══════════════════════════════════════════════════════════════ | ||
| 1241 | - DARK MODE | ||
| 1242 | - ═══════════════════════════════════════════════════════════════ */ | ||
| 1243 | - :global(html.dark) .context-hint { | ||
| 1244 | - color: rgba(255, 255, 255, 0.5); | ||
| 1245 | - } | ||
| 1246 | - | ||
| 1247 | - :global(html.dark) .def-link { | ||
| 1248 | - color: var(--theme-accent, #C9A751); | ||
| 1249 | - } | ||
| 1250 | -</style> |
src/routes/test/mapa/+page.svelte
deleted
100644 → 0
| 1 | -<script> | ||
| 2 | - import { onMount } from 'svelte'; | ||
| 3 | - import * as d3 from 'd3'; | ||
| 4 | - | ||
| 5 | - let mapaData = $state(null); | ||
| 6 | - let boliviaPath = $state(''); | ||
| 7 | - let deptoPath = $state(''); | ||
| 8 | - let muniPath = $state(''); | ||
| 9 | - let error = $state(''); | ||
| 10 | - let debug = $state(''); | ||
| 11 | - | ||
| 12 | - onMount(async () => { | ||
| 13 | - try { | ||
| 14 | - const res = await fetch('/mapa.json'); | ||
| 15 | - mapaData = await res.json(); | ||
| 16 | - | ||
| 17 | - debug = `bolivia: ${mapaData.bolivia.features.length} features, `; | ||
| 18 | - debug += `deptos: ${mapaData.departamentos.features.length}, `; | ||
| 19 | - debug += `munis: ${mapaData.municipios.features.length}`; | ||
| 20 | - | ||
| 21 | - // Quitar CRS y corregir winding order | ||
| 22 | - delete mapaData.bolivia.crs; | ||
| 23 | - delete mapaData.departamentos.crs; | ||
| 24 | - delete mapaData.municipios.crs; | ||
| 25 | - | ||
| 26 | - // Corregir winding order: invertir todos los anillos | ||
| 27 | - function fixWinding(geojson) { | ||
| 28 | - const fix = (coords, isHole) => { | ||
| 29 | - // Para geo coords: exterior debe ser counterclockwise (area < 0 en D3) | ||
| 30 | - // Simplemente invertimos todos los anillos exteriores | ||
| 31 | - coords.forEach((ring, i) => { | ||
| 32 | - if (i === 0) ring.reverse(); // exterior | ||
| 33 | - // holes quedan como están | ||
| 34 | - }); | ||
| 35 | - }; | ||
| 36 | - for (const feat of geojson.features || [geojson]) { | ||
| 37 | - const geom = (feat.geometry || feat); | ||
| 38 | - if (geom.type === 'Polygon') { | ||
| 39 | - fix(geom.coordinates); | ||
| 40 | - } else if (geom.type === 'MultiPolygon') { | ||
| 41 | - geom.coordinates.forEach(poly => fix(poly)); | ||
| 42 | - } | ||
| 43 | - } | ||
| 44 | - } | ||
| 45 | - | ||
| 46 | - fixWinding(mapaData.bolivia); | ||
| 47 | - fixWinding(mapaData.departamentos); | ||
| 48 | - fixWinding(mapaData.municipios); | ||
| 49 | - | ||
| 50 | - const bounds = d3.geoBounds(mapaData.bolivia); | ||
| 51 | - debug += ` | bounds: ${JSON.stringify(bounds)}`; | ||
| 52 | - | ||
| 53 | - const projection = d3.geoMercator().fitSize([380, 480], mapaData.bolivia); | ||
| 54 | - const pathGen = d3.geoPath(projection); | ||
| 55 | - | ||
| 56 | - boliviaPath = pathGen(mapaData.bolivia) || ''; | ||
| 57 | - debug += ` | boliviaPath length: ${boliviaPath.length}`; | ||
| 58 | - | ||
| 59 | - deptoPath = mapaData.departamentos.features.map(f => pathGen(f)).join(' '); | ||
| 60 | - debug += ` | deptoPath length: ${deptoPath.length}`; | ||
| 61 | - | ||
| 62 | - const muni = mapaData.municipios.features[0]; | ||
| 63 | - muniPath = pathGen(muni) || ''; | ||
| 64 | - debug += ` | muni code: ${muni.properties.codigo}`; | ||
| 65 | - | ||
| 66 | - } catch (e) { | ||
| 67 | - error = e.message; | ||
| 68 | - } | ||
| 69 | - }); | ||
| 70 | -</script> | ||
| 71 | - | ||
| 72 | -<div style="padding: 2rem; background: #1C1C1A; color: #F5F0E8; min-height: 100vh;"> | ||
| 73 | - <h1>Test Mapa</h1> | ||
| 74 | - <p style="font-size: 12px; color: #888;">{debug}</p> | ||
| 75 | - {#if error} | ||
| 76 | - <p style="color: red;">{error}</p> | ||
| 77 | - {/if} | ||
| 78 | - | ||
| 79 | - <svg width="400" height="500" viewBox="0 0 400 500" style="border: 1px solid #333;"> | ||
| 80 | - {#if boliviaPath} | ||
| 81 | - <path d={boliviaPath} fill="none" stroke="#555" stroke-width="1" /> | ||
| 82 | - {/if} | ||
| 83 | - {#if deptoPath} | ||
| 84 | - <path d={deptoPath} fill="rgba(255,255,255,0.1)" stroke="#777" stroke-width="0.5" /> | ||
| 85 | - {/if} | ||
| 86 | - {#if muniPath} | ||
| 87 | - <path d={muniPath} fill="#D4A574" stroke="#D4A574" stroke-width="1" /> | ||
| 88 | - {/if} | ||
| 89 | - </svg> | ||
| 90 | -</div> |
| 1 | -import { supabase } from '$lib/supabase'; | 1 | +export const ssr = false; |
| 2 | 2 | ||
| 3 | export async function load({ fetch }) { | 3 | export async function load({ fetch }) { |
| 4 | - // Cargar resumen y población en paralelo | 4 | + const pobRes = await fetch('/poblacion.csv').then(r => r.text()); |
| 5 | - const [resumenRes, pobRes] = await Promise.all([ | ||
| 6 | - supabase | ||
| 7 | - .schema('ppto') | ||
| 8 | - .from('entidad_resumen') | ||
| 9 | - .select('codigo, desc, gestion, devengado, ranking') | ||
| 10 | - .eq('tipo', 'gastos') | ||
| 11 | - .eq('tipo_codigo', 'municipio_ubigeo') | ||
| 12 | - .eq('gestion', 2025) | ||
| 13 | - .order('devengado', { ascending: false }) | ||
| 14 | - .limit(500), | ||
| 15 | - | ||
| 16 | - fetch('/poblacion.csv').then(r => r.text()) | ||
| 17 | - ]); | ||
| 18 | 5 | ||
| 19 | const poblacionMap = {}; | 6 | const poblacionMap = {}; |
| 20 | pobRes.split('\n').slice(1).forEach(line => { | 7 | pobRes.split('\n').slice(1).forEach(line => { |
| ... | @@ -27,7 +14,7 @@ export async function load({ fetch }) { | ... | @@ -27,7 +14,7 @@ export async function load({ fetch }) { |
| 27 | const gestiones = [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]; | 14 | const gestiones = [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]; |
| 28 | 15 | ||
| 29 | return { | 16 | return { |
| 30 | - resumen: resumenRes.data || [], | 17 | + resumen: [], |
| 31 | poblacionMap, | 18 | poblacionMap, |
| 32 | gestiones, | 19 | gestiones, |
| 33 | gestionInicial: 2025 | 20 | gestionInicial: 2025 | ... | ... |
| ... | @@ -4,7 +4,6 @@ | ... | @@ -4,7 +4,6 @@ |
| 4 | import { goto } from '$app/navigation'; | 4 | import { goto } from '$app/navigation'; |
| 5 | import { page } from '$app/stores'; | 5 | import { page } from '$app/stores'; |
| 6 | import * as d3 from 'd3'; | 6 | import * as d3 from 'd3'; |
| 7 | - import { supabase } from '$lib/supabase'; | ||
| 8 | import { mapaCache } from '$lib/stores/mapaCache'; | 7 | import { mapaCache } from '$lib/stores/mapaCache'; |
| 9 | 8 | ||
| 10 | let { data } = $props(); | 9 | let { data } = $props(); |
| ... | @@ -38,6 +37,22 @@ | ... | @@ -38,6 +37,22 @@ |
| 38 | let clasSearchVal = $state(''); | 37 | let clasSearchVal = $state(''); |
| 39 | let clasSearchResults = $state([]); | 38 | let clasSearchResults = $state([]); |
| 40 | let clasSearchFocused = $state(false); | 39 | let clasSearchFocused = $state(false); |
| 40 | + let clasSelectedIdx = $state(-1); | ||
| 41 | + | ||
| 42 | + function handleClasKeydown(e) { | ||
| 43 | + if (e.key === 'ArrowDown') { | ||
| 44 | + e.preventDefault(); | ||
| 45 | + clasSelectedIdx = clasSelectedIdx < clasSearchResults.length - 1 ? clasSelectedIdx + 1 : 0; | ||
| 46 | + } else if (e.key === 'ArrowUp') { | ||
| 47 | + e.preventDefault(); | ||
| 48 | + clasSelectedIdx = clasSelectedIdx > 0 ? clasSelectedIdx - 1 : clasSearchResults.length - 1; | ||
| 49 | + } else if (e.key === 'Enter' && clasSelectedIdx >= 0) { | ||
| 50 | + e.preventDefault(); | ||
| 51 | + seleccionarClasificador(clasSearchResults[clasSelectedIdx]); | ||
| 52 | + } else if (e.key === 'Escape') { | ||
| 53 | + clasSearchFocused = false; | ||
| 54 | + } | ||
| 55 | + } | ||
| 41 | let clasSearchLoading = $state(false); | 56 | let clasSearchLoading = $state(false); |
| 42 | let clasSeleccionado = $state(null); | 57 | let clasSeleccionado = $state(null); |
| 43 | let clasDebounce; | 58 | let clasDebounce; |
| ... | @@ -89,6 +104,7 @@ | ... | @@ -89,6 +104,7 @@ |
| 89 | 104 | ||
| 90 | function handleClasInput(e) { | 105 | function handleClasInput(e) { |
| 91 | clasSearchVal = e.target.value; | 106 | clasSearchVal = e.target.value; |
| 107 | + clasSelectedIdx = -1; | ||
| 92 | clearTimeout(clasDebounce); | 108 | clearTimeout(clasDebounce); |
| 93 | if (clasSearchVal.length >= 2) { | 109 | if (clasSearchVal.length >= 2) { |
| 94 | clasDebounce = setTimeout(() => buscarClasificador(clasSearchVal), 200); | 110 | clasDebounce = setTimeout(() => buscarClasificador(clasSearchVal), 200); |
| ... | @@ -362,20 +378,9 @@ | ... | @@ -362,20 +378,9 @@ |
| 362 | return; | 378 | return; |
| 363 | } | 379 | } |
| 364 | 380 | ||
| 365 | - // Modo total: cargar desde Supabase | 381 | + // Modo total: datos no disponibles sin Supabase |
| 366 | - if (!resumenCache[gestion]) { | 382 | + // TODO: implementar endpoint API para resumen por municipio |
| 367 | - const { data: rows } = await supabase | 383 | + resumen = []; |
| 368 | - .schema('ppto') | ||
| 369 | - .from('entidad_resumen') | ||
| 370 | - .select('codigo, desc, gestion, devengado, ranking') | ||
| 371 | - .eq('tipo', 'gastos') | ||
| 372 | - .eq('tipo_codigo', 'municipio_ubigeo') | ||
| 373 | - .eq('gestion', gestion) | ||
| 374 | - .order('devengado', { ascending: false }) | ||
| 375 | - .limit(500); | ||
| 376 | - resumenCache[gestion] = rows || []; | ||
| 377 | - } | ||
| 378 | - resumen = resumenCache[gestion]; | ||
| 379 | poblacionMap = await getPoblacionAno(gestion); | 384 | poblacionMap = await getPoblacionAno(gestion); |
| 380 | } | 385 | } |
| 381 | 386 | ||
| ... | @@ -588,6 +593,7 @@ | ... | @@ -588,6 +593,7 @@ |
| 588 | type="text" | 593 | type="text" |
| 589 | value={clasSearchVal} | 594 | value={clasSearchVal} |
| 590 | oninput={handleClasInput} | 595 | oninput={handleClasInput} |
| 596 | + onkeydown={handleClasKeydown} | ||
| 591 | onfocus={() => clasSearchFocused = true} | 597 | onfocus={() => clasSearchFocused = true} |
| 592 | onblur={() => setTimeout(() => clasSearchFocused = false, 150)} | 598 | onblur={() => setTimeout(() => clasSearchFocused = false, 150)} |
| 593 | placeholder="Buscar {clasificadorSeleccionado === 'objeto' ? 'partida' : clasificadorSeleccionado === 'finfun' ? 'finalidad o función' : 'sector'}..." | 599 | placeholder="Buscar {clasificadorSeleccionado === 'objeto' ? 'partida' : clasificadorSeleccionado === 'finfun' ? 'finalidad o función' : 'sector'}..." |
| ... | @@ -599,8 +605,8 @@ | ... | @@ -599,8 +605,8 @@ |
| 599 | {:else if clasSearchResults.length === 0} | 605 | {:else if clasSearchResults.length === 0} |
| 600 | <div class="objeto-search-msg">Sin resultados</div> | 606 | <div class="objeto-search-msg">Sin resultados</div> |
| 601 | {:else} | 607 | {:else} |
| 602 | - {#each clasSearchResults as result} | 608 | + {#each clasSearchResults as result, i} |
| 603 | - <button class="objeto-search-item" onclick={() => seleccionarClasificador(result)}> | 609 | + <button class="objeto-search-item" class:active={clasSelectedIdx === i} onclick={() => seleccionarClasificador(result)}> |
| 604 | <span class="objeto-search-code">{result.codigo}</span> | 610 | <span class="objeto-search-code">{result.codigo}</span> |
| 605 | <span class="objeto-search-name">{@html result.highlight}</span> | 611 | <span class="objeto-search-name">{@html result.highlight}</span> |
| 606 | </button> | 612 | </button> | ... | ... |
-
Please register or login to post a comment