Compare commits

..

3 Commits

Author SHA1 Message Date
f961f9d8e7 Feat Widgets 1541 2025-09-02 15:39:17 -03:00
da581d9714 Feat Widgets 1540 2025-09-02 15:39:01 -03:00
271a86b632 Feat Widgets 0209 2025-09-02 09:48:46 -03:00
36 changed files with 2586 additions and 372 deletions

View File

@@ -13,12 +13,14 @@
"@tanstack/react-query": "^5.85.5",
"axios": "^1.11.0",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react-dom": "^19.1.1",
"react-select": "^5.10.2"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@types/react-select": "^5.0.0",
"@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
@@ -47,7 +49,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
@@ -103,7 +104,6 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.3",
@@ -137,7 +137,6 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -147,7 +146,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
@@ -189,7 +187,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -199,7 +196,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -233,7 +229,6 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.2"
@@ -277,11 +272,19 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -296,7 +299,6 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
"integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -315,7 +317,6 @@
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -378,6 +379,126 @@
"react": ">=16.8.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"license": "MIT"
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/react": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
"license": "MIT"
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
@@ -974,6 +1095,31 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1044,7 +1190,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -1055,7 +1200,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -1065,14 +1209,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.30",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -1489,11 +1631,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.1.11",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz",
"integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -1509,6 +1656,25 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-select": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-5.0.0.tgz",
"integrity": "sha512-vddLcBpzUMVpVNmnBtpC5cyZ2ajaHx/g6SHUo6lmMw0FIiOzrtmoSQ4UI6TRl+sm8TGGT+Oir8NRMZfYQtgr8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"react-select": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.41.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz",
@@ -1881,6 +2047,21 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1962,7 +2143,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -2052,6 +2232,31 @@
"dev": true,
"license": "MIT"
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"license": "MIT",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cosmiconfig/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2071,14 +2276,12 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -2108,6 +2311,16 @@
"node": ">=0.4.0"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2129,6 +2342,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -2230,7 +2452,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -2504,6 +2725,12 @@
"node": ">=8"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
"license": "MIT"
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -2743,6 +2970,15 @@
"node": ">= 0.4"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2757,7 +2993,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
@@ -2780,6 +3015,27 @@
"node": ">=0.8.19"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"license": "MIT"
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2824,7 +3080,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -2844,7 +3099,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -2860,6 +3114,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -2911,6 +3171,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -2934,6 +3200,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -2953,6 +3231,12 @@
"node": ">= 0.4"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3015,7 +3299,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -3051,6 +3334,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -3105,7 +3397,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
@@ -3114,6 +3405,24 @@
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3134,11 +3443,25 @@
"node": ">=8"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -3193,6 +3516,17 @@
"node": ">= 0.8.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -3251,6 +3585,12 @@
"react": "^19.1.1"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -3261,11 +3601,67 @@
"node": ">=0.10.0"
}
},
"node_modules/react-select": {
"version": "5.10.2",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz",
"integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.8.1",
"@floating-ui/dom": "^1.0.1",
"@types/react-transition-group": "^4.4.0",
"memoize-one": "^6.0.0",
"prop-types": "^15.6.0",
"react-transition-group": "^4.3.0",
"use-isomorphic-layout-effect": "^1.2.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -3385,6 +3781,15 @@
"node": ">=8"
}
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -3408,6 +3813,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -3421,6 +3832,18 @@
"node": ">=8"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
@@ -3593,6 +4016,20 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-isomorphic-layout-effect": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
"integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/vite": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
@@ -3732,6 +4169,21 @@
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -15,12 +15,14 @@
"@tanstack/react-query": "^5.85.5",
"axios": "^1.11.0",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react-dom": "^19.1.1",
"react-select": "^5.10.2"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@types/react-select": "^5.0.0",
"@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",

View File

@@ -11,7 +11,7 @@ const CONCEJALES_ID = 7;
export const AgrupacionesManager = () => {
const queryClient = useQueryClient();
const [editedAgrupaciones, setEditedAgrupaciones] = useState<Record<string, Partial<AgrupacionPolitica>>>({});
const [editedLogos, setEditedLogos] = useState<LogoAgrupacionCategoria[]>([]);
@@ -52,11 +52,22 @@ export const AgrupacionesManager = () => {
const handleLogoChange = (agrupacionId: string, categoriaId: number, value: string) => {
setEditedLogos(prev => {
const newLogos = [...prev];
const existing = newLogos.find(l => l.agrupacionPoliticaId === agrupacionId && l.categoriaId === categoriaId);
const existing = newLogos.find(l =>
l.agrupacionPoliticaId === agrupacionId &&
l.categoriaId === categoriaId &&
l.ambitoGeograficoId == null
);
if (existing) {
existing.logoUrl = value;
} else {
newLogos.push({ id: 0, agrupacionPoliticaId: agrupacionId, categoriaId, logoUrl: value });
newLogos.push({
id: 0,
agrupacionPoliticaId: agrupacionId,
categoriaId,
logoUrl: value,
ambitoGeograficoId: null
});
}
return newLogos;
});
@@ -75,9 +86,9 @@ export const AgrupacionesManager = () => {
});
const logoPromise = updateLogos(editedLogos);
await Promise.all([...agrupacionPromises, logoPromise]);
queryClient.invalidateQueries({ queryKey: ['agrupaciones'] });
queryClient.invalidateQueries({ queryKey: ['logos'] });
@@ -91,7 +102,11 @@ export const AgrupacionesManager = () => {
const isLoading = isLoadingAgrupaciones || isLoadingLogos;
const getLogoUrl = (agrupacionId: string, categoriaId: number) => {
return editedLogos.find(l => l.agrupacionPoliticaId === agrupacionId && l.categoriaId === categoriaId)?.logoUrl || '';
return editedLogos.find(l =>
l.agrupacionPoliticaId === agrupacionId &&
l.categoriaId === categoriaId &&
l.ambitoGeograficoId == null
)?.logoUrl || '';
};
return (

View File

@@ -104,11 +104,11 @@ export const ConfiguracionGeneral = () => {
</p>
</div>
<div className="form-group" style={{ marginTop: '2rem' }}>
<label htmlFor="ticker-cantidad">Cantidad en Ticker (Dip/Sen)</label>
<label htmlFor="ticker-cantidad">Cantidad en Ticker (Dip/Sen) (Sumar 1 para "Otros")</label>
<input id="ticker-cantidad" type="number" value={tickerCantidad} onChange={e => setTickerCantidad(e.target.value)} />
</div>
<div className="form-group" style={{ marginTop: '2rem' }}>
<label htmlFor="concejales-cantidad">Cantidad en Widget Concejales</label>
<label htmlFor="concejales-cantidad">Cantidad en Widget Concejales (Sumar 1 para "Otros")</label>
<input
id="concejales-cantidad"
type="number"

View File

@@ -5,6 +5,7 @@ import { OrdenDiputadosManager } from './OrdenDiputadosManager';
import { OrdenSenadoresManager } from './OrdenSenadoresManager';
import { ConfiguracionGeneral } from './ConfiguracionGeneral';
import { BancasManager } from './BancasManager';
import { LogoOverridesManager } from './LogoOverridesManager';
export const DashboardPage = () => {
const { logout } = useAuth();
@@ -17,6 +18,7 @@ export const DashboardPage = () => {
</header>
<main style={{ marginTop: '2rem' }}>
<AgrupacionesManager />
<LogoOverridesManager />
<div style={{ display: 'flex', gap: '2rem', flexWrap: 'wrap', marginTop: '2rem' }}>
<div style={{ flex: '1 1 400px' }}>
<OrdenDiputadosManager />

View File

@@ -0,0 +1,83 @@
// src/components/LogoOverridesManager.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import Select from 'react-select';
import { getMunicipiosForAdmin, getAgrupaciones, getLogos, updateLogos } from '../services/apiService';
import type { MunicipioSimple, AgrupacionPolitica, LogoAgrupacionCategoria } from '../types';
// --- AÑADIMOS LAS CATEGORÍAS PARA EL SELECTOR ---
const CATEGORIAS_OPTIONS = [
{ value: 5, label: 'Senadores' },
{ value: 6, label: 'Diputados' },
{ value: 7, label: 'Concejales' }
];
export const LogoOverridesManager = () => {
const queryClient = useQueryClient();
const { data: municipios = [] } = useQuery<MunicipioSimple[]>({ queryKey: ['municipiosForAdmin'], queryFn: getMunicipiosForAdmin });
const { data: agrupaciones = [] } = useQuery<AgrupacionPolitica[]>({ queryKey: ['agrupaciones'], queryFn: getAgrupaciones });
const { data: logos = [] } = useQuery<LogoAgrupacionCategoria[]>({ queryKey: ['logos'], queryFn: getLogos });
// --- NUEVO ESTADO PARA LA CATEGORÍA ---
const [selectedCategoria, setSelectedCategoria] = useState<{ value: number; label: string } | null>(null);
const [selectedMunicipio, setSelectedMunicipio] = useState<{ value: string; label: string } | null>(null);
const [selectedAgrupacion, setSelectedAgrupacion] = useState<{ value: string; label: string } | null>(null);
const [logoUrl, setLogoUrl] = useState('');
const municipioOptions = useMemo(() => municipios.map(m => ({ value: m.id, label: m.nombre })), [municipios]);
const agrupacionOptions = useMemo(() => agrupaciones.map(a => ({ value: a.id, label: a.nombre })), [agrupaciones]);
const currentLogo = useMemo(() => {
// La búsqueda ahora depende de los 3 selectores
if (!selectedMunicipio || !selectedAgrupacion || !selectedCategoria) return '';
return logos.find(l =>
l.ambitoGeograficoId === parseInt(selectedMunicipio.value) &&
l.agrupacionPoliticaId === selectedAgrupacion.value &&
l.categoriaId === selectedCategoria.value
)?.logoUrl || '';
}, [logos, selectedMunicipio, selectedAgrupacion, selectedCategoria]);
useEffect(() => { setLogoUrl(currentLogo) }, [currentLogo]);
const handleSave = async () => {
if (!selectedMunicipio || !selectedAgrupacion || !selectedCategoria) return;
const newLogoEntry: LogoAgrupacionCategoria = {
id: 0,
agrupacionPoliticaId: selectedAgrupacion.value,
categoriaId: selectedCategoria.value,
ambitoGeograficoId: parseInt(selectedMunicipio.value),
logoUrl: logoUrl || null
};
try {
await updateLogos([newLogoEntry]);
queryClient.invalidateQueries({ queryKey: ['logos'] });
alert('Override de logo guardado.');
} catch { alert('Error al guardar.'); }
};
return (
<div className="admin-module">
<h3>Overrides de Logos por Municipio y Categoría</h3>
<p>Configure una imagen específica para un partido en un municipio y categoría determinados.</p>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end' }}>
<div style={{ flex: 1 }}>
<label>Categoría</label>
<Select options={CATEGORIAS_OPTIONS} value={selectedCategoria} onChange={setSelectedCategoria} isClearable placeholder="Seleccione..."/>
</div>
<div style={{ flex: 1 }}>
<label>Municipio</label>
<Select options={municipioOptions} value={selectedMunicipio} onChange={setSelectedMunicipio} isClearable placeholder="Seleccione..."/>
</div>
<div style={{ flex: 1 }}>
<label>Agrupación</label>
<Select options={agrupacionOptions} value={selectedAgrupacion} onChange={setSelectedAgrupacion} isClearable placeholder="Seleccione..."/>
</div>
<div style={{ flex: 2 }}>
<label>URL del Logo Específico</label>
<input type="text" value={logoUrl} onChange={e => setLogoUrl(e.target.value)} style={{ width: '100%' }} disabled={!selectedMunicipio || !selectedAgrupacion || !selectedCategoria} />
</div>
<button onClick={handleSave} disabled={!selectedMunicipio || !selectedAgrupacion || !selectedCategoria}>Guardar</button>
</div>
</div>
);
};

View File

@@ -1,7 +1,7 @@
// src/services/apiService.ts
import axios from 'axios';
import { triggerLogout } from '../context/authUtils';
import type { AgrupacionPolitica, UpdateAgrupacionData, Bancada, LogoAgrupacionCategoria } from '../types';
import type { AgrupacionPolitica, UpdateAgrupacionData, Bancada, LogoAgrupacionCategoria, MunicipioSimple } from '../types';
const AUTH_API_URL = 'http://localhost:5217/api/auth';
const ADMIN_API_URL = 'http://localhost:5217/api/admin';
@@ -10,7 +10,7 @@ const adminApiClient = axios.create({
baseURL: ADMIN_API_URL,
});
// --- INTERCEPTORES (una sola vez) ---
// --- INTERCEPTORES ---
// Interceptor de Peticiones: Añade el token JWT a cada llamada
adminApiClient.interceptors.request.use(
@@ -63,44 +63,51 @@ export const updateAgrupacion = async (id: string, data: UpdateAgrupacionData):
// 3. Ordenamiento de Agrupaciones
export const updateOrden = async (camara: 'diputados' | 'senadores', ids: string[]): Promise<void> => {
await adminApiClient.put(`/agrupaciones/orden-${camara}`, ids);
await adminApiClient.put(`/agrupaciones/orden-${camara}`, ids);
};
// 4. Gestión de Bancas y Ocupantes
export const getBancadas = async (camara: 'diputados' | 'senadores'): Promise<Bancada[]> => {
const camaraId = camara === 'diputados' ? 0 : 1;
const response = await adminApiClient.get(`/bancadas/${camaraId}`);
return response.data;
const camaraId = camara === 'diputados' ? 0 : 1;
const response = await adminApiClient.get(`/bancadas/${camaraId}`);
return response.data;
};
export interface UpdateBancadaData {
agrupacionPoliticaId: string | null;
nombreOcupante: string | null;
fotoUrl: string | null;
periodo: string | null;
agrupacionPoliticaId: string | null;
nombreOcupante: string | null;
fotoUrl: string | null;
periodo: string | null;
}
export const updateBancada = async (bancadaId: number, data: UpdateBancadaData): Promise<void> => {
await adminApiClient.put(`/bancadas/${bancadaId}`, data);
await adminApiClient.put(`/bancadas/${bancadaId}`, data);
};
// 5. Configuración General
export type ConfiguracionResponse = Record<string, string>;
export const getConfiguracion = async (): Promise<ConfiguracionResponse> => {
const response = await adminApiClient.get('/configuracion');
return response.data;
const response = await adminApiClient.get('/configuracion');
return response.data;
};
export const updateConfiguracion = async (data: Record<string, string>): Promise<void> => {
await adminApiClient.put('/configuracion', data);
await adminApiClient.put('/configuracion', data);
};
export const getLogos = async (): Promise<LogoAgrupacionCategoria[]> => {
const response = await adminApiClient.get('/logos');
return response.data;
const response = await adminApiClient.get('/logos');
return response.data;
};
export const updateLogos = async (data: LogoAgrupacionCategoria[]): Promise<void> => {
await adminApiClient.put('/logos', data);
await adminApiClient.put('/logos', data);
};
export const getMunicipiosForAdmin = async (): Promise<MunicipioSimple[]> => {
// Ahora usa adminApiClient, que apunta a /api/admin/
// La URL final será /api/admin/catalogos/municipios
const response = await adminApiClient.get('/catalogos/municipios');
return response.data;
};

View File

@@ -45,4 +45,7 @@ export interface LogoAgrupacionCategoria {
agrupacionPoliticaId: string;
categoriaId: number;
logoUrl: string | null;
}
ambitoGeograficoId: number | null;
}
export interface MunicipioSimple { id: string; nombre: string; }

View File

@@ -18,6 +18,7 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-pdf": "^10.1.0",
"react-select": "^5.10.2",
"react-simple-maps": "github:ozimmortal/react-simple-maps#feat/react-19-support",
"react-tooltip": "^5.29.1"
},
@@ -26,6 +27,7 @@
"@types/geojson": "^7946.0.16",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@types/react-select": "^5.0.0",
"@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
@@ -54,7 +56,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
@@ -110,7 +111,6 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.3",
@@ -144,7 +144,6 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -154,7 +153,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
@@ -196,7 +194,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -206,7 +203,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -240,7 +236,6 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.2"
@@ -284,11 +279,19 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -303,7 +306,6 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
"integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -322,7 +324,6 @@
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -332,6 +333,126 @@
"node": ">=6.9.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"license": "MIT"
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/react": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
"license": "MIT"
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
@@ -1023,7 +1144,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -1034,7 +1154,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -1057,14 +1176,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.30",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -2026,11 +2143,16 @@
"undici-types": "~7.10.0"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.1.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
"integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -2046,6 +2168,25 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-select": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-5.0.0.tgz",
"integrity": "sha512-vddLcBpzUMVpVNmnBtpC5cyZ2ajaHx/g6SHUo6lmMw0FIiOzrtmoSQ4UI6TRl+sm8TGGT+Oir8NRMZfYQtgr8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"react-select": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz",
@@ -2418,6 +2559,21 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2508,7 +2664,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -2613,6 +2768,31 @@
"dev": true,
"license": "MIT"
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"license": "MIT",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cosmiconfig/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2632,7 +2812,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/d3-array": {
@@ -2851,7 +3030,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -2890,6 +3068,16 @@
"node": ">=6"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2911,6 +3099,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -3012,7 +3209,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -3286,6 +3482,12 @@
"node": ">=8"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
"license": "MIT"
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -3525,6 +3727,15 @@
"node": ">= 0.4"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3539,7 +3750,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
@@ -3571,6 +3781,27 @@
"node": ">=12"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"license": "MIT"
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3634,7 +3865,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -3650,6 +3880,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -3701,6 +3937,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -3779,6 +4021,12 @@
"node": ">= 0.4"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/merge-refs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-2.0.0.tgz",
@@ -3858,7 +4106,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -3899,7 +4146,6 @@
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3958,7 +4204,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
@@ -3967,6 +4212,24 @@
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3987,6 +4250,21 @@
"node": ">=8"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pdfjs-dist": {
"version": "5.3.93",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.3.93.tgz",
@@ -4003,7 +4281,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -4063,7 +4340,6 @@
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -4132,8 +4408,7 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/react-pdf": {
"version": "10.1.0",
@@ -4174,6 +4449,27 @@
"node": ">=0.10.0"
}
},
"node_modules/react-select": {
"version": "5.10.2",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz",
"integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.8.1",
"@floating-ui/dom": "^1.0.1",
"@types/react-transition-group": "^4.4.0",
"memoize-one": "^6.0.0",
"prop-types": "^15.6.0",
"react-transition-group": "^4.3.0",
"use-isomorphic-layout-effect": "^1.2.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-simple-maps": {
"version": "3.0.0",
"resolved": "git+ssh://git@github.com/ozimmortal/react-simple-maps.git#f2b342ae277569b751fa1c9b6638e06ad05ab617",
@@ -4206,6 +4502,22 @@
"react-dom": ">=16.14.0"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/react-virtualized-auto-sizer": {
"version": "1.0.26",
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz",
@@ -4216,11 +4528,30 @@
"react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -4340,6 +4671,15 @@
"node": ">=8"
}
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -4388,6 +4728,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -4401,6 +4747,18 @@
"node": ">=8"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/terser": {
"version": "5.43.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
@@ -4644,6 +5002,20 @@
"react": "*"
}
},
"node_modules/use-isomorphic-layout-effect": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
"integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/vite": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
@@ -4792,6 +5164,21 @@
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -20,6 +20,7 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-pdf": "^10.1.0",
"react-select": "^5.10.2",
"react-simple-maps": "github:ozimmortal/react-simple-maps#feat/react-19-support",
"react-tooltip": "^5.29.1"
},
@@ -28,6 +29,7 @@
"@types/geojson": "^7946.0.16",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@types/react-select": "^5.0.0",
"@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",

File diff suppressed because one or more lines are too long

View File

@@ -6,6 +6,7 @@ import MapaBsAs from './components/MapaBsAs'
import { TickerWidget } from './components/TickerWidget'
import { TelegramaWidget } from './components/TelegramaWidget'
import { ConcejalesWidget } from './components/ConcejalesWidget'
import MapaBsAsSecciones from './components/MapaBsAsSecciones'
function App() {
return (
@@ -17,6 +18,7 @@ function App() {
<CongresoWidget />
<BancasWidget />
<MapaBsAs />
<MapaBsAsSecciones />
<TelegramaWidget />
</main>
</>

View File

@@ -60,6 +60,14 @@ export interface ConfiguracionPublica {
// ... otras claves públicas que pueda añadir en el futuro
}
export interface ResultadoDetalleSeccion {
id: string; // ID de la agrupación para la key
nombre: string;
votos: number;
porcentaje: number;
color: string | null;
}
export const getResumenProvincial = async (): Promise<CategoriaResumen[]> => {
const response = await apiClient.get('/resultados/provincia/02');
return response.data;
@@ -129,4 +137,21 @@ export const getConfiguracionPublica = async (): Promise<ConfiguracionPublica> =
export const getResultadosConcejales = async (seccionId: string): Promise<ResultadoTicker[]> => {
const response = await apiClient.get(`/resultados/concejales/${seccionId}`);
return response.data;
};
export const getDetalleSeccion = async (seccionId: string, categoriaId: number): Promise<ResultadoDetalleSeccion[]> => {
const response = await apiClient.get(`/resultados/seccion/${seccionId}?categoriaId=${categoriaId}`);
return response.data;
};
export const getResultadosConcejalesPorMunicipio = async (municipioId: string): Promise<ResultadoTicker[]> => {
// Usamos el endpoint 'partido' que, según la aclaración de la API, busca por municipio
const response = await apiClient.get(`/resultados/partido/${municipioId}`);
// La API devuelve un objeto, nosotros extraemos el array de resultados
return response.data.resultados;
};
export const getMunicipios = async (): Promise<MunicipioSimple[]> => {
const response = await apiClient.get('/catalogos/municipios');
return response.data;
};

View File

@@ -1,102 +1,119 @@
// src/components/ConcejalesWidget.tsx
import { useState, useEffect } from 'react';
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getSeccionesElectorales, getResultadosConcejales, getConfiguracionPublica } from '../apiService';
import Select from 'react-select'; // <-- 1. Importar react-select
import { getMunicipios, getResultadosConcejalesPorMunicipio, getConfiguracionPublica } from '../apiService';
import type { MunicipioSimple, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css'; // Reutilizamos los estilos del ticker
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
export const ConcejalesWidget = () => {
const [secciones, setSecciones] = useState<MunicipioSimple[]>([]);
const [seccionActualId, setSeccionActualId] = useState<string>('');
// Estilos personalizados para que el selector se vea bien
const customSelectStyles = {
control: (base: any) => ({ ...base, minWidth: '220px', border: '1px solid #ced4da' }),
menu: (base: any) => ({ ...base, zIndex: 10 }), // Para que el menú se superponga
};
export const ConcejalesWidget = () => {
// 2. Cambiamos el estado para que se adapte a react-select
const [selectedMunicipio, setSelectedMunicipio] = useState<{ value: string; label: string } | null>(null);
// Query para la configuración (para saber cuántos resultados mostrar)
const { data: configData } = useQuery({
queryKey: ['configuracionPublica'],
queryFn: getConfiguracionPublica,
staleTime: 0,
});
// Calculamos la cantidad a mostrar desde la configuración
const cantidadAMostrar = parseInt(configData?.ConcejalesResultadosCantidad || '5', 10) + 1;
// Usamos la clave de configuración correcta
const cantidadAMostrar = parseInt(configData?.ConcejalesResultadosCantidad || '5', 10);
useEffect(() => {
getSeccionesElectorales().then(seccionesData => {
if (seccionesData && seccionesData.length > 0) {
const orden = new Map([
['Capital', 0], ['Primera', 1], ['Segunda', 2], ['Tercera', 3],
['Cuarta', 4], ['Quinta', 5], ['Sexta', 6], ['Séptima', 7]
]);
const getOrden = (nombre: string) => {
const match = nombre.match(/Capital|Primera|Segunda|Tercera|Cuarta|Quinta|Sexta|Séptima/);
return match ? orden.get(match[0]) ?? 99 : 99;
};
seccionesData.sort((a, b) => getOrden(a.nombre) - getOrden(b.nombre));
setSecciones(seccionesData);
// Al estar los datos ya ordenados, el [0] será "Sección Capital"
setSeccionActualId(seccionesData[0].id);
}
});
}, []); // El array de dependencias vacío asegura que esto solo se ejecute una vez
// Query para obtener los resultados de la sección seleccionada
const { data: resultados, isLoading } = useQuery<ResultadoTicker[]>({
queryKey: ['resultadosConcejales', seccionActualId],
queryFn: () => getResultadosConcejales(seccionActualId),
enabled: !!seccionActualId,
// 3. Query para obtener la lista de MUNICIPIOS
const { data: municipios = [], isLoading: isLoadingMunicipios } = useQuery<MunicipioSimple[]>({
queryKey: ['municipios'],
queryFn: getMunicipios,
});
// --- INICIO DE LA LÓGICA DE PROCESAMIENTO "OTROS" ---
// Este useEffect se encarga de establecer el valor por defecto
useEffect(() => {
// Se ejecuta solo si tenemos la lista de municipios y aún no hemos seleccionado nada
if (municipios.length > 0 && !selectedMunicipio) {
// Buscamos "LA PLATA" en la lista (insensible a mayúsculas/minúsculas)
const laPlata = municipios.find(m => m.nombre.toUpperCase() === 'LA PLATA');
// Si lo encontramos, lo establecemos como el municipio seleccionado
if (laPlata) {
setSelectedMunicipio({ value: laPlata.id, label: laPlata.nombre });
}
}
}, [municipios, selectedMunicipio]); // Se ejecuta cuando 'municipios' o 'selectedMunicipio' cambian
// 4. Transformamos los datos para react-select
const municipioOptions = useMemo(() =>
municipios
.map(m => ({ value: m.id, label: m.nombre }))
.sort((a, b) => a.label.localeCompare(b.label)), // Orden alfabético
[municipios]);
// 5. Query para obtener los resultados del MUNICIPIO seleccionado
const { data: resultados, isLoading: isLoadingResultados } = useQuery<ResultadoTicker[]>({
queryKey: ['resultadosConcejalesPorMunicipio', selectedMunicipio?.value],
queryFn: () => getResultadosConcejalesPorMunicipio(selectedMunicipio!.value),
enabled: !!selectedMunicipio,
});
// 6. Lógica para "Otros" (sin cambios funcionales)
let displayResults: ResultadoTicker[] = resultados || [];
if (resultados && resultados.length > cantidadAMostrar) {
const topParties = resultados.slice(0, cantidadAMostrar - 1);
const otherParties = resultados.slice(cantidadAMostrar - 1);
const otrosPorcentaje = otherParties.reduce((sum, party) => sum + (party.votosPorcentaje || 0), 0);
const otrosPorcentaje = otherParties.reduce((sum, party) => sum + (party.porcentaje || 0), 0);
const otrosEntry: ResultadoTicker = {
id: `otros-concejales-${seccionActualId}`,
id: `otros-concejales-${selectedMunicipio?.value}`,
nombre: 'Otros',
nombreCorto: 'Otros',
color: '#888888',
logoUrl: null,
votos: 0, // No es relevante para la visualización del porcentaje
votosPorcentaje: otrosPorcentaje,
votos: 0,
porcentaje: otrosPorcentaje,
};
displayResults = [...topParties, otrosEntry];
} else if (resultados) {
displayResults = resultados.slice(0, cantidadAMostrar);
}
// --- FIN DE LA LÓGICA DE PROCESAMIENTO "OTROS" ---
return (
<div className="ticker-card" style={{ gridColumn: '1 / -1' }}>
<div className="ticker-header">
<h3>CONCEJALES - LA PLATA</h3>
<select value={seccionActualId} onChange={e => setSeccionActualId(e.target.value)} disabled={secciones.length === 0}>
{secciones.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)}
</select>
<h3>CONCEJALES POR MUNICIPIO</h3>
<Select
options={municipioOptions}
value={selectedMunicipio}
onChange={(option) => setSelectedMunicipio(option)}
isLoading={isLoadingMunicipios}
placeholder="Buscar y seleccionar un municipio..."
styles={customSelectStyles}
/>
</div>
<div className="ticker-results">
{isLoading ? <p>Cargando...</p> :
displayResults.map(partido => (
<div key={partido.id} className="ticker-party">
<div className="party-logo">
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} />
{(isLoadingMunicipios || (isLoadingResultados && selectedMunicipio)) && <p>Cargando...</p>}
{!selectedMunicipio && !isLoadingMunicipios && <p style={{textAlign: 'center', color: '#666'}}>Seleccione un municipio.</p>}
{displayResults.map(partido => (
<div key={partido.id} className="ticker-party">
<div className="party-logo">
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} />
</div>
<div className="party-details">
<div className="party-info">
<span className="party-name">{partido.nombreCorto || partido.nombre}</span>
<span className="party-percent">{formatPercent(partido.porcentaje)}</span>
</div>
<div className="party-details">
<div className="party-info">
<span className="party-name">{partido.nombreCorto || partido.nombre}</span>
<span className="party-percent">{formatPercent(partido.votosPorcentaje)}</span>
</div>
<div className="party-bar-background">
<div className="party-bar-foreground" style={{ width: `${partido.votosPorcentaje}%`, backgroundColor: partido.color || '#888' }}></div>
</div>
<div className="party-bar-background">
<div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div>
</div>
</div>
))
}
</div>
))}
</div>
</div>
);

View File

@@ -76,7 +76,7 @@
min-height: 0;
background-color: var(--background-panel-color);
border-radius: 8px;
padding: 1.5rem;
padding: 1rem;
border: none;
}
@@ -194,4 +194,52 @@
/* Un tamaño de fuente legible en móviles. */
font-size: 1em;
}
}
/* --- ESTILOS PARA EL SELECTOR DE CATEGORÍA --- */
.mapa-categoria-selector {
display: flex;
margin-bottom: 1.5rem;
}
.mapa-categoria-combobox {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1em;
font-weight: 500;
color: var(--text-color);
background-color: #f8f9fa;
border: 1px solid var(--border-color);
border-radius: 6px;
cursor: pointer;
appearance: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%230073e6%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.4-12.9z%22%2F%3E%3C%2Fsvg%3E');
background-repeat: no-repeat;
background-position: right 1rem center;
background-size: 0.8em;
transition: all 0.2s ease-in-out;
}
.mapa-categoria-combobox:hover {
border-color: var(--primary-accent-color);
background-color: #e9ecef;
}
.mapa-categoria-combobox:focus {
outline: none;
border-color: var(--primary-accent-color);
box-shadow: 0 0 0 2px rgba(0, 115, 230, 0.25);
}
/* --- ESTILOS PARA SECCIONES NO CLICLEABLES --- */
.rsm-geography.no-results {
pointer-events: none; /* Ignora todos los eventos del ratón (click, hover, etc.) */
cursor: default; /* Muestra el cursor por defecto en lugar de la mano */
}
/* Opcional pero recomendado: modificar la regla :hover para que no afecte a las secciones no clicleables */
.rsm-geography:not(.no-results):hover {
stroke: var(--primary-accent-color);
stroke-width: 1.5px;
filter: brightness(1.05);
}

View File

@@ -1,6 +1,5 @@
// src/components/MapaBsAs.tsx
import { useState, useMemo, useCallback, useEffect } from 'react';
import type { MouseEvent } from 'react';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import { Tooltip } from 'react-tooltip';
import { useQuery } from '@tanstack/react-query';
@@ -24,7 +23,7 @@ interface ResultadoDetalladoMunicipio {
ultimaActualizacion: string;
porcentajeEscrutado: number;
porcentajeParticipacion: number;
resultados: { nombre: string; votos: number; porcentaje: number }[];
resultados: { id: string; nombre: string; votos: number; porcentaje: number; color: string | null; }[];
votosAdicionales: { enBlanco: number; nulos: number; recurridos: number };
}
@@ -33,11 +32,13 @@ interface Agrupacion {
nombre: string;
}
interface Categoria {
id: number;
nombre: string;
}
interface PartidoProperties {
id: string;
departamento: string;
cabecera: string;
provincia: string;
}
type PartidoGeography = Feature<Geometry, PartidoProperties> & { rsmKey: string };
@@ -50,52 +51,57 @@ const TRANSLATE_EXTENT: [[number, number], [number, number]] = [[-100, -600], [1
const INITIAL_POSITION = { center: [-60.5, -37.2] as PointTuple, zoom: MIN_ZOOM };
const DEFAULT_MAP_COLOR = '#E0E0E0';
const CATEGORIAS: Categoria[] = [
{ id: 5, nombre: 'Senadores' },
{ id: 6, nombre: 'Diputados' },
{ id: 7, nombre: 'Concejales' }
];
// --- Componente Principal ---
const MapaBsAs = () => {
const [position, setPosition] = useState(INITIAL_POSITION);
const [selectedAmbitoId, setSelectedAmbitoId] = useState<number | null>(null);
const [selectedCategoriaId, setSelectedCategoriaId] = useState<number>(6);
const [tooltipContent, setTooltipContent] = useState('');
const { data: resultadosData, isLoading: isLoadingResultados } = useQuery<ResultadoMapa[]>({
queryKey: ['mapaResultados'],
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/mapa`)).data,
queryKey: ['mapaResultadosPorMunicipio', selectedCategoriaId],
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/mapa-por-municipio?categoriaId=${selectedCategoriaId}`)).data,
});
const { data: geoData, isLoading: isLoadingGeo } = useQuery<any>({
queryKey: ['mapaGeoData'],
queryFn: async () => (await axios.get('/partidos-bsas.topojson')).data,
});
const { data: agrupacionesData, isLoading: isLoadingAgrupaciones } = useQuery<Agrupacion[]>({
queryKey: ['catalogoAgrupaciones'],
queryFn: async () => (await axios.get(`${API_BASE_URL}/Catalogos/agrupaciones`)).data,
});
// --- SU SOLUCIÓN CORRECTA INTEGRADA ---
const { nombresAgrupaciones, resultadosPorDepartamento } = useMemo<{
nombresAgrupaciones: Map<string, string>;
resultadosPorDepartamento: Map<string, ResultadoMapa>;
}>(() => {
const nombresMap = new Map<string, string>();
const resultadosMap = new Map<string, ResultadoMapa>();
if (agrupacionesData) {
agrupacionesData.forEach((agrupacion) => {
nombresMap.set(agrupacion.id, agrupacion.nombre);
});
}
if (resultadosData) {
resultadosData.forEach(r => resultadosMap.set(r.departamentoNombre.toUpperCase(), r));
}
return {
nombresAgrupaciones: nombresMap,
resultadosPorDepartamento: resultadosMap
};
const nombresMap = new Map<string, string>();
const resultadosMap = new Map<string, ResultadoMapa>();
if (agrupacionesData) {
agrupacionesData.forEach((agrupacion) => {
nombresMap.set(agrupacion.id, agrupacion.nombre);
});
}
if (resultadosData) {
resultadosData.forEach(r => {
if (r.departamentoNombre) {
resultadosMap.set(r.departamentoNombre.toUpperCase(), r)
}
});
}
return { nombresAgrupaciones: nombresMap, resultadosPorDepartamento: resultadosMap };
}, [agrupacionesData, resultadosData]);
const isLoading = isLoadingResultados || isLoadingAgrupaciones || isLoadingGeo;
// ... (el resto del componente no necesita cambios)
const handleReset = useCallback(() => {
setSelectedAmbitoId(null);
setPosition(INITIAL_POSITION);
@@ -142,75 +148,104 @@ const MapaBsAs = () => {
const getPartyFillColor = (departamentoNombre: string) => {
const resultado = resultadosPorDepartamento.get(departamentoNombre.toUpperCase());
if (!resultado || !resultado.colorGanador) {
return DEFAULT_MAP_COLOR;
}
return resultado.colorGanador;
return resultado?.colorGanador || DEFAULT_MAP_COLOR;
};
const handleMouseEnter = (e: MouseEvent<SVGPathElement>) => {
const path = e.target as SVGPathElement;
if (path.parentNode) {
path.parentNode.appendChild(path);
}
};
// --- Helper de Renderizado ---
const renderGeography = (geo: PartidoGeography, isSelectedGeo: boolean = false) => {
const departamentoNombre = geo.properties.departamento.toUpperCase();
const resultado = resultadosPorDepartamento.get(departamentoNombre);
const isClickable = !!resultado;
const isSelected = isSelectedGeo || (selectedAmbitoId !== null && selectedAmbitoId === resultado?.ambitoId);
const isFaded = !isSelectedGeo && selectedAmbitoId !== null && !isSelected;
const nombreAgrupacionGanadora = resultado ? nombresAgrupaciones.get(resultado.agrupacionGanadoraId) : 'Sin datos';
if (isLoading) return <div className="loading-container">Cargando datos del mapa...</div>;
return (
<Geography
key={geo.rsmKey + (isSelectedGeo ? '-selected' : '')}
geography={geo}
data-tooltip-id="partido-tooltip"
data-tooltip-content={`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`}
className={`rsm-geography ${isSelected ? 'selected' : ''} ${isFaded ? 'faded' : ''} ${!isClickable ? 'no-results' : ''}`}
fill={getPartyFillColor(geo.properties.departamento)}
onClick={isClickable ? () => handleGeographyClick(geo) : undefined}
onMouseEnter={() => setTooltipContent(`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`)}
onMouseLeave={() => setTooltipContent("")}
/>
);
};
return (
<div className="mapa-wrapper">
<div className="mapa-container">
<ComposableMap projection="geoMercator" projectionConfig={{ scale: 4400, center: [-60.5, -37.2] }} className="rsm-svg">
<ZoomableGroup
center={position.center}
zoom={position.zoom}
onMoveEnd={handleMoveEnd}
style={{ transition: "transform 400ms ease-in-out" }}
translateExtent={TRANSLATE_EXTENT}
minZoom={MIN_ZOOM}
maxZoom={MAX_ZOOM}
filterZoomEvent={(e: WheelEvent) => {
if (e.deltaY > 0) {
handleReset();
} else if (e.deltaY < 0) {
handleZoomIn();
}
return true;
}}
{isLoading ? <div className="spinner"></div> : (
<ComposableMap
key={selectedCategoriaId}
projection="geoMercator"
projectionConfig={{ scale: 4400, center: [-60.5, -37.2] }}
className="rsm-svg"
data-tooltip-id="partido-tooltip"
>
{geoData && (
<Geographies geography={geoData}>
{({ geographies }: { geographies: PartidoGeography[] }) =>
geographies.map((geo) => {
const departamentoNombre = geo.properties.departamento.toUpperCase();
const resultado = resultadosPorDepartamento.get(departamentoNombre);
const isSelected = resultado ? selectedAmbitoId === resultado.ambitoId : false;
const isFaded = selectedAmbitoId !== null && !isSelected;
const nombreAgrupacionGanadora = resultado ? nombresAgrupaciones.get(resultado.agrupacionGanadoraId) : 'Sin datos';
<ZoomableGroup
center={position.center}
zoom={position.zoom}
onMoveEnd={handleMoveEnd}
style={{ transition: "transform 400ms ease-in-out" }}
translateExtent={TRANSLATE_EXTENT}
minZoom={MIN_ZOOM}
maxZoom={MAX_ZOOM}
filterZoomEvent={(e: WheelEvent) => {
if (e.deltaY > 0) {
handleReset();
} else if (e.deltaY < 0) {
handleZoomIn();
}
return true;
}}
>
{geoData && (
<Geographies geography={geoData}>
{({ geographies }: { geographies: PartidoGeography[] }) => {
const selectedGeo = selectedAmbitoId
? geographies.find(geo => {
const resultado = resultadosPorDepartamento.get(geo.properties.departamento.toUpperCase());
return resultado?.ambitoId === selectedAmbitoId;
})
: null;
return (
<Geography
key={geo.rsmKey}
geography={geo}
data-tooltip-id="partido-tooltip"
data-tooltip-content={`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`}
className={`rsm-geography ${isSelected ? 'selected' : ''} ${isFaded ? 'faded' : ''}`}
fill={getPartyFillColor(geo.properties.departamento)}
onClick={() => handleGeographyClick(geo)}
onMouseEnter={handleMouseEnter}
/>
<>
{geographies.map(geo => (!selectedGeo || geo.rsmKey !== selectedGeo.rsmKey) ? renderGeography(geo) : null)}
{selectedGeo && renderGeography(selectedGeo, true)}
</>
);
})
}
</Geographies>
)}
</ZoomableGroup>
</ComposableMap>
<Tooltip id="partido-tooltip" variant="light" />
}}
</Geographies>
)}
</ZoomableGroup>
</ComposableMap>
)}
<Tooltip id="partido-tooltip" content={tooltipContent} />
{selectedAmbitoId !== null && <ControlesMapa onReset={handleReset} />}
</div>
<div className="info-panel">
<DetalleMunicipio ambitoId={selectedAmbitoId} onReset={handleReset} />
<div className="mapa-categoria-selector">
<select
className="mapa-categoria-combobox"
value={selectedCategoriaId}
onChange={(e) => {
setSelectedCategoriaId(Number(e.target.value));
handleReset();
}}
>
{CATEGORIAS.map(cat => (
<option key={cat.id} value={cat.id}>
{cat.nombre}
</option>
))}
</select>
</div>
<DetalleMunicipio ambitoId={selectedAmbitoId} onReset={handleReset} categoriaId={selectedCategoriaId} />
<Legend resultados={resultadosPorDepartamento} nombresAgrupaciones={nombresAgrupaciones} />
</div>
</div>
@@ -224,10 +259,10 @@ const ControlesMapa = ({ onReset }: { onReset: () => void }) => (
</div>
);
const DetalleMunicipio = ({ ambitoId, onReset }: { ambitoId: number | null; onReset: () => void }) => {
const DetalleMunicipio = ({ ambitoId, onReset, categoriaId }: { ambitoId: number | null; onReset: () => void; categoriaId: number; }) => {
const { data, isLoading, error } = useQuery<ResultadoDetalladoMunicipio>({
queryKey: ['municipioDetalle', ambitoId],
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/municipio/${ambitoId}`)).data,
queryKey: ['municipioDetalle', ambitoId, categoriaId],
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/municipio/${ambitoId}?categoriaId=${categoriaId}`)).data,
enabled: !!ambitoId,
});
@@ -240,18 +275,24 @@ const DetalleMunicipio = ({ ambitoId, onReset }: { ambitoId: number | null; onRe
<button className="reset-button-panel" onClick={onReset}> VOLVER</button>
<h3>{data?.municipioNombre}</h3>
<div className="detalle-metricas">
<span><strong>Escrutado:</strong> {data?.porcentajeEscrutado.toFixed(2)}%</span>
<span><strong>Participación:</strong> {data?.porcentajeParticipacion.toFixed(2)}%</span>
<span><strong>Escrutado:</strong> {(data?.porcentajeEscrutado ?? 0).toFixed(2)}%</span>
<span><strong>Participación:</strong> {(data?.porcentajeParticipacion ?? 0).toFixed(2)}%</span>
</div>
<ul className="resultados-lista">
{data?.resultados.map((r, index) => (
{(data?.resultados ?? []).map((r, index) => (
<li key={`${r.nombre}-${index}`}>
<div className="resultado-info">
<span className="partido-nombre">{r.nombre}</span>
<span className="partido-votos">{r.votos.toLocaleString('es-AR')} ({r.porcentaje.toFixed(2)}%)</span>
</div>
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${r.porcentaje}%` }}></div>
<div
className="progress-fill"
style={{
width: `${r.porcentaje}%`,
backgroundColor: r.color || DEFAULT_MAP_COLOR
}}
></div>
</div>
</li>
))}
@@ -261,33 +302,33 @@ const DetalleMunicipio = ({ ambitoId, onReset }: { ambitoId: number | null; onRe
};
const Legend = ({ resultados, nombresAgrupaciones }: { resultados: Map<string, ResultadoMapa>, nombresAgrupaciones: Map<string, string> }) => {
const legendItems = useMemo(() => {
const ganadoresUnicos = new Map<string, { nombre: string; color: string }>();
resultados.forEach(resultado => {
if (resultado.colorGanador && !ganadoresUnicos.has(resultado.agrupacionGanadoraId)) {
ganadoresUnicos.set(resultado.agrupacionGanadoraId, {
nombre: nombresAgrupaciones.get(resultado.agrupacionGanadoraId) || 'Desconocido',
color: resultado.colorGanador
});
}
const legendItems = useMemo(() => {
const ganadoresUnicos = new Map<string, { nombre: string; color: string }>();
resultados.forEach(resultado => {
if (resultado.colorGanador && !ganadoresUnicos.has(resultado.agrupacionGanadoraId)) {
ganadoresUnicos.set(resultado.agrupacionGanadoraId, {
nombre: nombresAgrupaciones.get(resultado.agrupacionGanadoraId) || 'Desconocido',
color: resultado.colorGanador
});
}
});
return Array.from(ganadoresUnicos.values());
}, [resultados, nombresAgrupaciones]);
return Array.from(ganadoresUnicos.values());
}, [resultados, nombresAgrupaciones]);
return (
<div className="legend">
<h4>Leyenda de Ganadores</h4>
{legendItems.map(item => (
<div key={item.nombre} className="legend-item">
<div className="legend-color-box" style={{ backgroundColor: item.color }} />
<span>{item.nombre}</span>
</div>
))}
return (
<div className="legend">
<h4>Leyenda de Ganadores</h4>
{legendItems.map(item => (
<div key={item.nombre} className="legend-item">
<div className="legend-color-box" style={{ backgroundColor: item.color }} />
<span>{item.nombre}</span>
</div>
);
))}
</div>
);
};
export default MapaBsAs;

View File

@@ -0,0 +1,303 @@
// src/components/MapaBsAsSecciones.tsx
import { useState, useMemo, useCallback, useEffect } from 'react';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import { Tooltip } from 'react-tooltip';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { geoCentroid } from 'd3-geo';
import { getDetalleSeccion } from '../apiService';
import type { ResultadoDetalleSeccion } from '../apiService';
import './MapaBsAs.css';
// --- Interfaces y Tipos ---
type PointTuple = [number, number];
interface ResultadoMapaSeccion {
seccionId: string;
seccionNombre: string;
agrupacionGanadoraId: string | null;
colorGanador: string | null;
}
interface Agrupacion { id: string; nombre: string; }
interface Categoria { id: number; nombre: string; }
type SeccionGeography = {
rsmKey: string;
properties: { seccion: string; fna: string; };
};
// --- Constantes ---
const API_BASE_URL = 'http://localhost:5217/api';
const DEFAULT_MAP_COLOR = '#E0E0E0';
const CATEGORIAS: Categoria[] = [{ id: 5, nombre: 'Senadores' }, { id: 6, nombre: 'Diputados' }];
const SECCION_ID_TO_ROMAN: Record<string, string> = { '1': 'I', '2': 'II', '3': 'III', '4': 'IV', '5': 'V', '6': 'VI', '7': 'VII', '8': 'VIII' };
const ROMAN_TO_SECCION_ID: Record<string, string> = { 'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5', 'VI': '6', 'VII': '7', 'VIII': '8' };
// --- CORRECCIÓN 1: Mover NOMBRES_SECCIONES aquí para que sea global al archivo ---
const NOMBRES_SECCIONES: Record<string, string> = {
'I': 'Sección Primera', 'II': 'Sección Segunda', 'III': 'Sección Tercera', 'IV': 'Sección Cuarta',
'V': 'Sección Quinta', 'VI': 'Sección Sexta', 'VII': 'Sección Séptima', 'VIII': 'Sección Capital'
};
const MIN_ZOOM = 1;
const MAX_ZOOM = 5;
const TRANSLATE_EXTENT: [[number, number], [number, number]] = [[-100, -600], [1100, 300]];
const INITIAL_POSITION = { center: [-60.5, -37.2] as PointTuple, zoom: MIN_ZOOM };
// --- Componente de Detalle ---
const DetalleSeccion = ({ seccion, categoriaId, onReset }: { seccion: SeccionGeography | null, categoriaId: number, onReset: () => void }) => {
const seccionId = seccion ? ROMAN_TO_SECCION_ID[seccion.properties.seccion] : null;
const { data: resultadosDetalle, isLoading, error } = useQuery<ResultadoDetalleSeccion[]>({
queryKey: ['detalleSeccion', seccionId, categoriaId],
queryFn: () => getDetalleSeccion(seccionId!, categoriaId),
enabled: !!seccionId,
});
if (!seccion) {
return (
<div className="detalle-placeholder">
<h3>Resultados por Sección</h3>
<p>Haga clic en una sección del mapa para ver los resultados detallados.</p>
</div>
);
}
if (isLoading) return (<div className="detalle-loading"><div className="spinner"></div><p>Cargando resultados de la sección...</p></div>);
if (error) return <div className="detalle-error">Error al cargar los datos de la sección.</div>;
const nombreSeccionLegible = NOMBRES_SECCIONES[seccion.properties.seccion] || "Sección Desconocida";
return (
<div className="detalle-content">
<button className="reset-button-panel" onClick={onReset}> VOLVER</button>
<h3>{nombreSeccionLegible}</h3>
<ul className="resultados-lista">
{resultadosDetalle?.map((r) => (
<li key={r.id}>
<div className="resultado-info">
<span className="partido-nombre">{r.nombre}</span>
<span className="partido-votos">{r.votos.toLocaleString('es-AR')} ({r.porcentaje.toFixed(2)}%)</span>
</div>
<div className="progress-bar">
{/* --- CORRECCIÓN 2: Usar el color de la API --- */}
<div className="progress-fill" style={{ width: `${r.porcentaje}%`, backgroundColor: r.color || DEFAULT_MAP_COLOR }}></div>
</div>
</li>
))}
</ul>
</div>
);
};
// --- Componente de Controles del Mapa ---
const ControlesMapa = ({ onReset }: { onReset: () => void }) => (
<div className="map-controls">
<button onClick={onReset}> VOLVER</button>
</div>
);
// --- Componente Principal ---
const MapaBsAsSecciones = () => {
const [position, setPosition] = useState(INITIAL_POSITION);
const [selectedCategoriaId, setSelectedCategoriaId] = useState<number>(6);
const [clickedSeccion, setClickedSeccion] = useState<SeccionGeography | null>(null);
const [tooltipContent, setTooltipContent] = useState('');
const { data: geoData, isLoading: isLoadingGeo } = useQuery<any>({
queryKey: ['mapaGeoDataSecciones'],
queryFn: async () => (await axios.get('./secciones-electorales-pba.topojson')).data,
});
const { data: resultadosData, isLoading: isLoadingResultados } = useQuery<ResultadoMapaSeccion[]>({
queryKey: ['mapaResultadosPorSeccion', selectedCategoriaId],
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/mapa-por-seccion?categoriaId=${selectedCategoriaId}`)).data,
});
const { data: agrupacionesData, isLoading: isLoadingAgrupaciones } = useQuery<Agrupacion[]>({
queryKey: ['catalogoAgrupaciones'],
queryFn: async () => (await axios.get(`${API_BASE_URL}/Catalogos/agrupaciones`)).data,
});
const { nombresAgrupaciones, resultadosPorSeccionRomana } = useMemo<{
nombresAgrupaciones: Map<string, string>;
resultadosPorSeccionRomana: Map<string, ResultadoMapaSeccion>;
}>(() => {
const nombresMap = new Map<string, string>();
const resultadosMap = new Map<string, ResultadoMapaSeccion>();
if (agrupacionesData) {
agrupacionesData.forEach(a => nombresMap.set(a.id, a.nombre));
}
if (resultadosData) {
resultadosData.forEach(r => {
const roman = SECCION_ID_TO_ROMAN[r.seccionId];
if (roman) resultadosMap.set(roman, r);
});
}
return { nombresAgrupaciones: nombresMap, resultadosPorSeccionRomana: resultadosMap };
}, [agrupacionesData, resultadosData]);
const isLoading = isLoadingGeo || isLoadingResultados || isLoadingAgrupaciones;
const handleReset = useCallback(() => {
setClickedSeccion(null);
setPosition(INITIAL_POSITION);
}, []);
const handleGeographyClick = useCallback((geo: SeccionGeography) => {
if (clickedSeccion?.rsmKey === geo.rsmKey) {
handleReset();
} else {
const centroid = geoCentroid(geo as any) as PointTuple;
setPosition({ center: centroid, zoom: 2 });
setClickedSeccion(geo);
}
}, [clickedSeccion, handleReset]);
const handleMoveEnd = (newPosition: { coordinates: PointTuple; zoom: number }) => {
if (newPosition.zoom <= MIN_ZOOM) {
if (position.zoom > MIN_ZOOM || clickedSeccion !== null) {
handleReset();
}
return;
}
if (newPosition.zoom < position.zoom && clickedSeccion !== null) {
setClickedSeccion(null);
}
setPosition({ center: newPosition.coordinates, zoom: newPosition.zoom });
};
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => e.key === 'Escape' && handleReset();
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleReset]);
const getSectionFillColor = (seccionRomana: string) => {
return resultadosPorSeccionRomana.get(seccionRomana)?.colorGanador || DEFAULT_MAP_COLOR;
};
const handleZoomIn = () => {
if (position.zoom < MAX_ZOOM) {
setPosition(p => ({ ...p, zoom: Math.min(p.zoom * 1.5, MAX_ZOOM) }));
}
};
return (
<div className="mapa-wrapper">
<div className="mapa-container">
{isLoading ? <div className="spinner"></div> : (
<ComposableMap
key={selectedCategoriaId}
projection="geoMercator"
projectionConfig={{ scale: 4400, center: [-60.5, -37.2] }}
className="rsm-svg"
data-tooltip-id="seccion-tooltip"
>
<ZoomableGroup
center={position.center}
zoom={position.zoom}
onMoveEnd={handleMoveEnd}
minZoom={MIN_ZOOM}
maxZoom={MAX_ZOOM}
translateExtent={TRANSLATE_EXTENT}
style={{ transition: "transform 400ms ease-in-out" }}
filterZoomEvent={(e: WheelEvent) => {
if (e.deltaY > 0) {
handleReset();
} else if (e.deltaY < 0) {
handleZoomIn();
}
return true;
}}
>
{geoData && (
<Geographies geography={geoData}>
{({ geographies }: { geographies: SeccionGeography[] }) =>
geographies.map((geo) => {
const seccionRomana = geo.properties.seccion;
const resultado = resultadosPorSeccionRomana.get(seccionRomana);
const nombreGanador = resultado?.agrupacionGanadoraId ? nombresAgrupaciones.get(resultado.agrupacionGanadoraId) : 'Sin datos';
const isSelected = clickedSeccion?.rsmKey === geo.rsmKey;
const isFaded = clickedSeccion && !isSelected;
const isClickable = !!resultado;
return (
<Geography
key={geo.rsmKey + (isSelected ? '-selected' : '')}
geography={geo as any}
data-tooltip-id="seccion-tooltip"
onClick={isClickable ? () => handleGeographyClick(geo) : undefined}
onMouseEnter={() => {
if (isClickable) {
// --- CORRECCIÓN 3: Tooltip con nombre de sección ---
const nombreSeccionLegible = NOMBRES_SECCIONES[geo.properties.seccion] || "Sección Desconocida";
setTooltipContent(`${nombreSeccionLegible}: ${nombreGanador}`);
}
}}
onMouseLeave={() => setTooltipContent("")}
className={`rsm-geography ${isSelected ? 'selected' : ''} ${isFaded ? 'faded' : ''} ${!isClickable ? 'no-results' : ''}`}
fill={getSectionFillColor(seccionRomana)}
/>
);
})
}
</Geographies>
)}
</ZoomableGroup>
</ComposableMap>
)}
{clickedSeccion && <ControlesMapa onReset={handleReset} />}
<Tooltip id="seccion-tooltip" content={tooltipContent} />
</div>
<div className="info-panel">
<div className="mapa-categoria-selector">
<select
className="mapa-categoria-combobox"
value={selectedCategoriaId}
onChange={(e) => {
setSelectedCategoriaId(Number(e.target.value));
handleReset();
}}
>
{CATEGORIAS.map(cat => (
<option key={cat.id} value={cat.id}>
{cat.nombre}
</option>
))}
</select>
</div>
<DetalleSeccion seccion={clickedSeccion} categoriaId={selectedCategoriaId} onReset={handleReset} />
<LegendSecciones resultados={resultadosPorSeccionRomana} nombresAgrupaciones={nombresAgrupaciones} />
</div>
</div>
);
};
// --- Sub-componente para la Leyenda ---
const LegendSecciones = ({ resultados, nombresAgrupaciones }: { resultados: Map<string, ResultadoMapaSeccion>, nombresAgrupaciones: Map<string, string> }) => {
const legendItems = useMemo(() => {
const ganadoresUnicos = new Map<string, { nombre: string; color: string }>();
resultados.forEach(resultado => {
if (resultado.agrupacionGanadoraId && resultado.colorGanador && !ganadoresUnicos.has(resultado.agrupacionGanadoraId)) {
ganadoresUnicos.set(resultado.agrupacionGanadoraId, {
nombre: nombresAgrupaciones.get(resultado.agrupacionGanadoraId) || 'Desconocido',
color: resultado.colorGanador
});
}
});
return Array.from(ganadoresUnicos.values());
}, [resultados, nombresAgrupaciones]);
return (
<div className="legend">
<h4>Ganadores por Sección</h4>
{legendItems.map(item => (
<div key={item.nombre} className="legend-item">
<div className="legend-color-box" style={{ backgroundColor: item.color }} />
<span>{item.nombre}</span>
</div>
))}
</div>
);
};
export default MapaBsAsSecciones;

View File

@@ -96,8 +96,8 @@
}
.party-logo {
flex-shrink: 0;
width: 50px;
height: 50px;
width: 75px;
height: 75px;
}
.party-logo img {
width: 100%;

View File

@@ -4,6 +4,7 @@ import { getResumenProvincial, getConfiguracionPublica } from '../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css';
import { useMemo } from 'react';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
@@ -20,7 +21,9 @@ export const TickerWidget = () => {
staleTime: 0,
});
const cantidadAMostrar = parseInt(configData?.TickerResultadosCantidad || '5', 10) + 1;
const cantidadAMostrar = useMemo(() => {
return parseInt(configData?.TickerResultadosCantidad || '5', 10);
}, [configData]);
if (isLoading) return <div className="ticker-wrapper loading">Cargando resumen...</div>;
if (error || !categorias) return <div className="ticker-wrapper error">No hay datos disponibles.</div>;
@@ -36,7 +39,7 @@ export const TickerWidget = () => {
if (categoria.resultados.length > cantidadAMostrar) {
const topParties = categoria.resultados.slice(0, cantidadAMostrar - 1);
const otherParties = categoria.resultados.slice(cantidadAMostrar - 1);
const otrosPorcentaje = otherParties.reduce((sum, party) => sum + party.votosPorcentaje, 0);
const otrosPorcentaje = otherParties.reduce((sum, party) => sum + party.porcentaje, 0);
const otrosEntry: ResultadoTicker = {
id: `otros-${categoria.categoriaId}`,
@@ -45,7 +48,7 @@ export const TickerWidget = () => {
color: '#888888',
logoUrl: null,
votos: 0,
votosPorcentaje: otrosPorcentaje,
porcentaje: otrosPorcentaje,
};
displayResults = [...topParties, otrosEntry];
@@ -75,10 +78,10 @@ export const TickerWidget = () => {
<div className="party-details">
<div className="party-info">
<span className="party-name">{partido.nombreCorto || partido.nombre}</span>
<span className="party-percent">{formatPercent(partido.votosPorcentaje)}</span>
<span className="party-percent">{formatPercent(partido.porcentaje)}</span>
</div>
<div className="party-bar-background">
<div className="party-bar-foreground" style={{ width: `${partido.votosPorcentaje}%`, backgroundColor: partido.color || '#888' }}></div>
<div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div>
</div>
</div>
</div>

View File

@@ -48,7 +48,7 @@ export interface ResultadoTicker {
color: string | null;
logoUrl: string | null;
votos: number;
votosPorcentaje: number;
porcentaje: number;
}
export interface EstadoRecuentoTicker {

View File

@@ -187,22 +187,52 @@ public class AdminController : ControllerBase
[HttpPut("logos")]
public async Task<IActionResult> UpdateLogos([FromBody] List<LogoAgrupacionCategoria> logos)
{
// Lógica de "Upsert"
foreach (var logo in logos)
{
var logoExistente = await _dbContext.LogosAgrupacionesCategorias
.FirstOrDefaultAsync(l => l.AgrupacionPoliticaId == logo.AgrupacionPoliticaId && l.CategoriaId == logo.CategoriaId);
.FirstOrDefaultAsync(l =>
l.AgrupacionPoliticaId == logo.AgrupacionPoliticaId &&
l.CategoriaId == logo.CategoriaId &&
l.AmbitoGeograficoId == logo.AmbitoGeograficoId);
if (logoExistente != null)
{
// Si encontramos el registro exacto, solo actualizamos su URL.
logoExistente.LogoUrl = logo.LogoUrl;
}
else if (!string.IsNullOrEmpty(logo.LogoUrl))
{
_dbContext.LogosAgrupacionesCategorias.Add(logo);
// Si no se encontró un registro exacto (es un override nuevo),
// lo añadimos a la base de datos.
_dbContext.LogosAgrupacionesCategorias.Add(new LogoAgrupacionCategoria
{
AgrupacionPoliticaId = logo.AgrupacionPoliticaId,
CategoriaId = logo.CategoriaId,
AmbitoGeograficoId = logo.AmbitoGeograficoId,
LogoUrl = logo.LogoUrl
});
}
}
await _dbContext.SaveChangesAsync();
return NoContent();
}
[HttpGet("catalogos/municipios")]
public async Task<IActionResult> GetMunicipiosForAdmin()
{
var municipios = await _dbContext.AmbitosGeograficos
.AsNoTracking()
.Where(a => a.NivelId == 30) // Nivel 30 = Municipio
.OrderBy(a => a.Nombre)
.Select(a => new
{
// Devolvemos el ID de la base de datos como un string,
// que es lo que el componente Select espera.
Id = a.Id.ToString(),
Nombre = a.Nombre
})
.ToListAsync();
return Ok(municipios);
}
}

View File

@@ -22,13 +22,11 @@ public class ResultadosController : ControllerBase
_configuration = configuration;
}
[HttpGet("partido/{seccionId}")]
[HttpGet("partido/{seccionId}")] // 'seccionId' es el ID del municipio
public async Task<IActionResult> GetResultadosPorPartido(string seccionId)
{
// 1. Buscamos el ámbito geográfico correspondiente al PARTIDO (Nivel 30)
var ambito = await _dbContext.AmbitosGeograficos
.AsNoTracking()
// CAMBIO CLAVE: Buscamos por SeccionId y NivelId para ser precisos
.FirstOrDefaultAsync(a => a.SeccionId == seccionId && a.NivelId == 30);
if (ambito == null)
@@ -36,14 +34,12 @@ public class ResultadosController : ControllerBase
return NotFound(new { message = $"No se encontró el partido con ID {seccionId}" });
}
// 2. Buscamos el estado del recuento para ese ámbito
var estadoRecuento = await _dbContext.EstadosRecuentos
.AsNoTracking()
.FirstOrDefaultAsync(e => e.AmbitoGeograficoId == ambito.Id);
if (estadoRecuento == null)
{
// Devolvemos una respuesta vacía pero válida para el frontend
return Ok(new MunicipioResultadosDto
{
MunicipioNombre = ambito.Nombre,
@@ -55,28 +51,57 @@ public class ResultadosController : ControllerBase
});
}
// 3. Buscamos todos los votos para ese ámbito
// 1. Obtenemos los IDs de las agrupaciones que tienen resultados en este municipio
var agrupacionIds = await _dbContext.ResultadosVotos
.Where(rv => rv.AmbitoGeograficoId == ambito.Id)
.Select(rv => rv.AgrupacionPoliticaId)
.Distinct()
.ToListAsync();
// 2. Buscamos TODOS los logos relevantes en una sola consulta:
// - Los que son para la categoría Concejales (7)
// - Y pertenecen a los partidos que compiten aquí
// - Y son genéricos (sin ámbito) O específicos para ESTE municipio
var logosRelevantes = await _dbContext.LogosAgrupacionesCategorias
.AsNoTracking()
.Where(l => l.CategoriaId == 7 &&
agrupacionIds.Contains(l.AgrupacionPoliticaId) &&
(l.AmbitoGeograficoId == null || l.AmbitoGeograficoId == ambito.Id))
.ToListAsync();
var resultadosVotos = await _dbContext.ResultadosVotos
.AsNoTracking()
.Include(rv => rv.AgrupacionPolitica)
.Where(rv => rv.AmbitoGeograficoId == ambito.Id)
.ToListAsync();
// 4. Calculamos el total de votos positivos
long totalVotosPositivos = resultadosVotos.Sum(r => r.CantidadVotos);
// 5. Mapeamos al DTO de respuesta
var respuestaDto = new MunicipioResultadosDto
{
MunicipioNombre = ambito.Nombre,
UltimaActualizacion = estadoRecuento.FechaTotalizacion,
PorcentajeEscrutado = estadoRecuento.MesasTotalizadasPorcentaje,
PorcentajeParticipacion = estadoRecuento.ParticipacionPorcentaje,
Resultados = resultadosVotos.Select(rv => new AgrupacionResultadoDto
Resultados = resultadosVotos.Select(rv =>
{
Nombre = rv.AgrupacionPolitica.Nombre,
Votos = rv.CantidadVotos,
Porcentaje = totalVotosPositivos > 0 ? (rv.CantidadVotos * 100.0m / totalVotosPositivos) : 0
// --- LÓGICA DE FALLBACK ---
var logoUrl =
// 1. Buscamos primero el logo específico para este municipio.
logosRelevantes.FirstOrDefault(l => l.AgrupacionPoliticaId == rv.AgrupacionPoliticaId && l.AmbitoGeograficoId == ambito.Id)?.LogoUrl
// 2. Si no lo encontramos, buscamos el logo genérico (sin ámbito).
?? logosRelevantes.FirstOrDefault(l => l.AgrupacionPoliticaId == rv.AgrupacionPoliticaId && l.AmbitoGeograficoId == null)?.LogoUrl;
return new AgrupacionResultadoDto
{
Id = rv.AgrupacionPolitica.Id,
Nombre = rv.AgrupacionPolitica.Nombre,
NombreCorto = rv.AgrupacionPolitica.NombreCorto,
Color = rv.AgrupacionPolitica.Color,
LogoUrl = logoUrl, // Asignamos el logo encontrado
Votos = rv.CantidadVotos,
Porcentaje = totalVotosPositivos > 0 ? (rv.CantidadVotos * 100.0m / totalVotosPositivos) : 0
};
}).OrderByDescending(r => r.Votos).ToList(),
VotosAdicionales = new VotosAdicionalesDto
{
@@ -95,15 +120,10 @@ public class ResultadosController : ControllerBase
var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking()
.FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10);
var todosLosResumenes = await _dbContext.ResumenesVotos.AsNoTracking()
.Include(r => r.AgrupacionPolitica)
.ToListAsync();
// OBTENER TODOS LOS LOGOS EN UNA SOLA CONSULTA
var logosLookup = (await _dbContext.LogosAgrupacionesCategorias.AsNoTracking().ToListAsync())
.ToLookup(l => $"{l.AgrupacionPoliticaId}-{l.CategoriaId}");
if (provincia == null) return NotFound($"No se encontró la provincia con distritoId {distritoId}");
if (provincia == null)
{
return NotFound($"No se encontró la provincia con distritoId {distritoId}");
}
var estadosPorCategoria = await _dbContext.EstadosRecuentosGenerales.AsNoTracking()
.Include(e => e.CategoriaElectoral)
@@ -113,43 +133,50 @@ public class ResultadosController : ControllerBase
var resultadosPorMunicipio = await _dbContext.ResultadosVotos
.AsNoTracking()
.Include(r => r.AgrupacionPolitica)
.Where(r => r.AmbitoGeografico.NivelId == 30)
.Where(r => r.AmbitoGeografico.NivelId == 30) // Nivel 30 = Municipio
.ToListAsync();
var logos = await _dbContext.LogosAgrupacionesCategorias.AsNoTracking().ToListAsync();
var resultadosAgrupados = resultadosPorMunicipio
.GroupBy(r => r.CategoriaId)
.Select(g => new
{
CategoriaId = g.Key,
CategoriaNombre = estadosPorCategoria.ContainsKey(g.Key) ? estadosPorCategoria[g.Key].CategoriaElectoral.Nombre : "Desconocido",
EstadoRecuento = estadosPorCategoria.GetValueOrDefault(g.Key),
TotalVotosCategoria = g.Sum(r => r.CantidadVotos),
// Agrupamos por el ID de la agrupación, no por el objeto
Resultados = g.GroupBy(r => r.AgrupacionPoliticaId)
.Select(partidoGroup => new
{
// El objeto Agrupacion lo tomamos del primer elemento del grupo
Agrupacion = partidoGroup.First().AgrupacionPolitica,
Votos = partidoGroup.Sum(r => r.CantidadVotos)
})
.ToList()
Resultados = g
.GroupBy(r => r.AgrupacionPolitica)
.Select(partidoGroup => new
{
Agrupacion = partidoGroup.Key,
Votos = partidoGroup.Sum(r => r.CantidadVotos)
})
.OrderByDescending(r => r.Votos)
.ToList()
})
.Select(g => new
{
g.CategoriaId,
CategoriaNombre = estadosPorCategoria.ContainsKey(g.CategoriaId) ? estadosPorCategoria[g.CategoriaId].CategoriaElectoral.Nombre : "Desconocido",
EstadoRecuento = estadosPorCategoria.GetValueOrDefault(g.CategoriaId),
Resultados = g.Resultados
.Select(r => new
{
r.Agrupacion.Id,
r.Agrupacion.Nombre,
r.Agrupacion.NombreCorto,
r.Agrupacion.Color,
LogoUrl = logosLookup[$"{r.Agrupacion.Id}-{g.CategoriaId}"].FirstOrDefault()?.LogoUrl,
r.Votos,
VotosPorcentaje = g.TotalVotosCategoria > 0 ? ((decimal)r.Votos * 100 / g.TotalVotosCategoria) : 0
})
.OrderByDescending(r => r.Votos)
.ToList()
g.CategoriaNombre,
g.EstadoRecuento,
Resultados = g.Resultados.Select(r =>
{
var logoUrl = logos.FirstOrDefault(l => l.AgrupacionPoliticaId == r.Agrupacion.Id && l.CategoriaId == g.CategoriaId && l.AmbitoGeograficoId != null)?.LogoUrl
?? logos.FirstOrDefault(l => l.AgrupacionPoliticaId == r.Agrupacion.Id && l.CategoriaId == g.CategoriaId && l.AmbitoGeograficoId == null)?.LogoUrl;
return new
{
Id = r.Agrupacion.Id,
r.Agrupacion.Nombre,
r.Agrupacion.NombreCorto,
r.Agrupacion.Color,
LogoUrl = logoUrl,
r.Votos,
Porcentaje = g.TotalVotosCategoria > 0 ? ((decimal)r.Votos * 100 / g.TotalVotosCategoria) : 0
};
}).ToList()
})
.OrderBy(c => c.CategoriaId)
.ToList();
@@ -235,63 +262,60 @@ public class ResultadosController : ControllerBase
return Ok(resultadosGanadores);
}
[HttpGet("municipio/{ambitoId}")] // Cambiamos el nombre del parámetro de ruta
public async Task<IActionResult> GetResultadosPorMunicipio(int ambitoId) // Cambiamos el tipo de string a int
[HttpGet("municipio/{ambitoId}")]
public async Task<IActionResult> GetResultadosPorMunicipio(int ambitoId, [FromQuery] int categoriaId)
{
_logger.LogInformation("Buscando resultados para AmbitoGeograficoId: {AmbitoId}", ambitoId);
// PASO 1: Buscar el Ámbito Geográfico directamente por su CLAVE PRIMARIA (AmbitoGeograficoId).
var ambito = await _dbContext.AmbitosGeograficos
.AsNoTracking()
.FirstOrDefaultAsync(a => a.Id == ambitoId && a.NivelId == 30); // Usamos a.Id == ambitoId
_logger.LogInformation("Buscando resultados para AmbitoGeograficoId: {AmbitoId}, CategoriaId: {CategoriaId}", ambitoId, categoriaId);
// Validamos que el ámbito exista
var ambito = await _dbContext.AmbitosGeograficos.AsNoTracking()
.FirstOrDefaultAsync(a => a.Id == ambitoId && a.NivelId == 30);
if (ambito == null)
{
_logger.LogWarning("No se encontró el ámbito para el ID interno: {AmbitoId} o no es Nivel 30.", ambitoId);
return NotFound(new { message = $"No se encontró el municipio con ID interno {ambitoId}" });
_logger.LogWarning("No se encontró el municipio con ID: {AmbitoId}", ambitoId);
return NotFound($"No se encontró el municipio con ID {ambitoId}");
}
_logger.LogInformation("Ámbito encontrado: Id={AmbitoId}, Nombre={AmbitoNombre}", ambito.Id, ambito.Nombre);
// PASO 2: Usar la CLAVE PRIMARIA (ambito.Id) para buscar el estado del recuento.
// Obtenemos el estado del recuento para el ámbito
var estadoRecuento = await _dbContext.EstadosRecuentos
.AsNoTracking()
.FirstOrDefaultAsync(e => e.AmbitoGeograficoId == ambito.Id);
if (estadoRecuento == null)
{
_logger.LogWarning("No se encontró EstadoRecuento para AmbitoGeograficoId: {AmbitoId}", ambito.Id);
return NotFound(new { message = $"No se han encontrado resultados de recuento para el municipio {ambito.Nombre}" });
}
// PASO 3: Usar la CLAVE PRIMARIA (ambito.Id) para buscar los votos.
// Obtenemos los votos para ESE municipio y ESA categoría
var resultadosVotos = await _dbContext.ResultadosVotos
.AsNoTracking()
.Include(rv => rv.AgrupacionPolitica) // Incluimos el nombre del partido
.Where(rv => rv.AmbitoGeograficoId == ambito.Id)
.OrderByDescending(rv => rv.CantidadVotos)
.Include(rv => rv.AgrupacionPolitica)
.Where(rv => rv.AmbitoGeograficoId == ambitoId && rv.CategoriaId == categoriaId)
.ToListAsync();
// PASO 4: Calcular el total de votos positivos para el porcentaje.
long totalVotosPositivos = resultadosVotos.Sum(r => r.CantidadVotos);
// Calculamos el total de votos solo para esta selección
var totalVotosPositivos = (decimal)resultadosVotos.Sum(r => r.CantidadVotos);
// PASO 5: Mapear todo al DTO de respuesta que el frontend espera.
// Mapeamos los resultados de los partidos
var resultadosPartidosDto = resultadosVotos
.OrderByDescending(r => r.CantidadVotos)
.Select(rv => new AgrupacionResultadoDto // Assuming AgrupacionResultadoDto is the correct DTO for individual party results
{
Id = rv.AgrupacionPolitica.Id,
Nombre = rv.AgrupacionPolitica.NombreCorto ?? rv.AgrupacionPolitica.Nombre,
Color = rv.AgrupacionPolitica.Color,
Votos = rv.CantidadVotos,
Porcentaje = totalVotosPositivos > 0 ? (rv.CantidadVotos / totalVotosPositivos) * 100 : 0
}).ToList();
// Construimos la respuesta completa del DTO
var respuestaDto = new MunicipioResultadosDto
{
MunicipioNombre = ambito.Nombre,
UltimaActualizacion = estadoRecuento.FechaTotalizacion,
PorcentajeEscrutado = estadoRecuento.MesasTotalizadasPorcentaje,
PorcentajeParticipacion = estadoRecuento.ParticipacionPorcentaje,
Resultados = resultadosVotos.Select(rv => new AgrupacionResultadoDto
UltimaActualizacion = estadoRecuento?.FechaTotalizacion ?? DateTime.UtcNow, // Use null-conditional operator
PorcentajeEscrutado = estadoRecuento?.MesasTotalizadasPorcentaje ?? 0,
PorcentajeParticipacion = estadoRecuento?.ParticipacionPorcentaje ?? 0,
Resultados = resultadosPartidosDto,
VotosAdicionales = new VotosAdicionalesDto // Assuming default constructor is fine
{
Nombre = rv.AgrupacionPolitica.Nombre,
Votos = rv.CantidadVotos,
Porcentaje = totalVotosPositivos > 0 ? (rv.CantidadVotos * 100.0m / totalVotosPositivos) : 0
}).ToList(),
VotosAdicionales = new VotosAdicionalesDto
{
EnBlanco = estadoRecuento.VotosEnBlanco,
Nulos = estadoRecuento.VotosNulos,
Recurridos = estadoRecuento.VotosRecurridos
EnBlanco = estadoRecuento?.VotosEnBlanco ?? 0,
Nulos = estadoRecuento?.VotosNulos ?? 0,
Recurridos = estadoRecuento?.VotosRecurridos ?? 0
}
};
@@ -537,11 +561,10 @@ public class ResultadosController : ControllerBase
[HttpGet("concejales/{seccionId}")]
public async Task<IActionResult> GetResultadosConcejalesPorSeccion(string seccionId)
{
// 1. Encontrar todos los municipios (Nivel 30) que pertenecen a la sección dada (Nivel 20)
var municipiosDeLaSeccion = await _dbContext.AmbitosGeograficos
.AsNoTracking()
.Where(a => a.NivelId == 30 && a.SeccionProvincialId == seccionId)
.Select(a => a.Id) // Solo necesitamos sus IDs
.Select(a => a.Id)
.ToListAsync();
if (!municipiosDeLaSeccion.Any())
@@ -549,7 +572,6 @@ public class ResultadosController : ControllerBase
return Ok(new List<object>());
}
// 2. Obtener todos los resultados de la categoría Concejales (ID 7) para esos municipios
var resultadosMunicipales = await _dbContext.ResultadosVotos
.AsNoTracking()
.Include(r => r.AgrupacionPolitica)
@@ -561,29 +583,159 @@ public class ResultadosController : ControllerBase
.Where(l => l.CategoriaId == 7)
.ToDictionaryAsync(l => l.AgrupacionPoliticaId);
// 3. Agrupar y sumar en memoria para obtener el total por partido para la sección
var totalVotosSeccion = resultadosMunicipales.Sum(r => r.CantidadVotos);
var totalVotosSeccion = (decimal)resultadosMunicipales.Sum(r => r.CantidadVotos);
var resultadosFinales = resultadosMunicipales
.GroupBy(r => r.AgrupacionPolitica)
// 1. Agrupamos por el ID del partido para evitar duplicados.
.GroupBy(r => r.AgrupacionPoliticaId)
.Select(g => new
{
Agrupacion = g.Key,
// 2. Obtenemos la entidad completa del primer elemento del grupo.
Agrupacion = g.First().AgrupacionPolitica,
Votos = g.Sum(r => r.CantidadVotos)
})
.OrderByDescending(r => r.Votos)
.Select(r => new
{
r.Agrupacion.Id,
Id = r.Agrupacion.Id, // Aseguramos que el Id esté en el objeto final
r.Agrupacion.Nombre,
r.Agrupacion.NombreCorto,
r.Agrupacion.Color,
LogoUrl = logosConcejales.GetValueOrDefault(r.Agrupacion.Id)?.LogoUrl,
r.Votos,
votosPorcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0
Votos = r.Votos,
// --- CORRECCIÓN CLAVE ---
// 3. Usamos el nombre de propiedad correcto que el frontend espera: 'votosPorcentaje'
VotosPorcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0
})
.ToList();
return Ok(resultadosFinales);
}
[HttpGet("mapa-por-seccion")]
public async Task<IActionResult> GetResultadosMapaPorSeccion([FromQuery] int categoriaId)
{
// 1. Obtenemos todos los resultados a nivel de MUNICIPIO para la categoría dada.
var resultadosMunicipales = await _dbContext.ResultadosVotos
.AsNoTracking()
.Include(r => r.AmbitoGeografico)
.Include(r => r.AgrupacionPolitica)
.Where(r => r.CategoriaId == categoriaId && r.AmbitoGeografico.NivelId == 30)
.ToListAsync();
// 2. Agrupamos en memoria por Sección Electoral y sumamos los votos.
var ganadoresPorSeccion = resultadosMunicipales
.GroupBy(r => r.AmbitoGeografico.SeccionProvincialId)
.Select(g =>
{
// Para cada sección, encontramos al partido con más votos.
var ganador = g
.GroupBy(r => r.AgrupacionPolitica)
.Select(pg => new { Agrupacion = pg.Key, TotalVotos = pg.Sum(r => r.CantidadVotos) })
.OrderByDescending(x => x.TotalVotos)
.FirstOrDefault();
// Buscamos el nombre de la sección
var seccionInfo = _dbContext.AmbitosGeograficos
.FirstOrDefault(a => a.SeccionProvincialId == g.Key && a.NivelId == 20);
return new
{
SeccionId = g.Key,
SeccionNombre = seccionInfo?.Nombre,
AgrupacionGanadoraId = ganador?.Agrupacion.Id,
ColorGanador = ganador?.Agrupacion.Color
};
})
.Where(r => r.SeccionId != null) // Filtramos cualquier posible nulo
.ToList();
return Ok(ganadoresPorSeccion);
}
[HttpGet("seccion/{seccionId}")]
public async Task<IActionResult> GetResultadosDetallePorSeccion(string seccionId, [FromQuery] int categoriaId)
{
var municipiosDeLaSeccion = await _dbContext.AmbitosGeograficos
.AsNoTracking()
.Where(a => a.NivelId == 30 && a.SeccionProvincialId == seccionId)
.Select(a => a.Id)
.ToListAsync();
if (!municipiosDeLaSeccion.Any()) return Ok(new List<object>());
var resultadosMunicipales = await _dbContext.ResultadosVotos
.AsNoTracking()
.Include(r => r.AgrupacionPolitica)
.Where(r => r.CategoriaId == categoriaId && municipiosDeLaSeccion.Contains(r.AmbitoGeograficoId))
.ToListAsync();
var totalVotosSeccion = (decimal)resultadosMunicipales.Sum(r => r.CantidadVotos);
var resultadosFinales = resultadosMunicipales
// 1. Agrupamos por el ID del partido para evitar duplicados.
.GroupBy(r => r.AgrupacionPoliticaId)
.Select(g => new
{
// 2. Tomamos la entidad completa del primer elemento del grupo.
Agrupacion = g.First().AgrupacionPolitica,
Votos = g.Sum(r => r.CantidadVotos)
})
.OrderByDescending(r => r.Votos)
.Select(r => new
{
id = r.Agrupacion.Id,
nombre = r.Agrupacion.NombreCorto ?? r.Agrupacion.Nombre,
votos = r.Votos,
porcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0,
// 3. Añadimos el color a la respuesta.
color = r.Agrupacion.Color
})
.ToList();
return Ok(resultadosFinales);
}
[HttpGet("mapa-por-municipio")]
public async Task<IActionResult> GetResultadosMapaPorMunicipio([FromQuery] int categoriaId)
{
// Obtenemos los votos primero
var votosPorMunicipio = await _dbContext.ResultadosVotos
.AsNoTracking()
.Where(r => r.CategoriaId == categoriaId && r.AmbitoGeografico.NivelId == 30)
.ToListAsync();
// Luego, los agrupamos en memoria
var ganadores = votosPorMunicipio
.GroupBy(r => r.AmbitoGeograficoId)
.Select(g => g.OrderByDescending(r => r.CantidadVotos).First())
.ToList();
// Ahora, obtenemos los detalles necesarios en una sola consulta adicional
var idsAgrupacionesGanadoras = ganadores.Select(g => g.AgrupacionPoliticaId).ToList();
var idsAmbitosGanadores = ganadores.Select(g => g.AmbitoGeograficoId).ToList();
var agrupacionesInfo = await _dbContext.AgrupacionesPoliticas
.AsNoTracking()
.Where(a => idsAgrupacionesGanadoras.Contains(a.Id))
.ToDictionaryAsync(a => a.Id);
var ambitosInfo = await _dbContext.AmbitosGeograficos
.AsNoTracking()
.Where(a => idsAmbitosGanadores.Contains(a.Id))
.ToDictionaryAsync(a => a.Id);
// Finalmente, unimos todo en memoria
var resultadoFinal = ganadores.Select(g => new
{
AmbitoId = g.AmbitoGeograficoId,
DepartamentoNombre = ambitosInfo.GetValueOrDefault(g.AmbitoGeograficoId)?.Nombre,
AgrupacionGanadoraId = g.AgrupacionPoliticaId,
ColorGanador = agrupacionesInfo.GetValueOrDefault(g.AgrupacionPoliticaId)?.Color
})
.Where(r => r.DepartamentoNombre != null) // Filtramos por si acaso
.ToList();
return Ok(resultadoFinal);
}
}

View File

@@ -1,6 +1,5 @@
@Elecciones.Api_HostAddress = http://localhost:5217
GET {{Elecciones.Api_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -14,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+608ae655bedf6c59be5fec1e14fc308871d2fd62")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+271a86b63211dff88fb7188d1db7b390e6c53efb")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["BNGxWTPjjFD1Fj56FltRDUvsBzgMlQvuqV\u002BraH2IhwQ=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","khGrM2Rl22MsVh9N6\u002B7todRrMuJ6o3ljuHxZF/aubqE=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","6xYke/2SzNspypSwIgizeNUH7b\u002Bfoz3wYfKk6z1tMsw="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["BNGxWTPjjFD1Fj56FltRDUvsBzgMlQvuqV\u002BraH2IhwQ=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","PmrFmFQepAv9NhtsO0l45PPk3BWugccwJ3ysTKcw7bM=","6CAjHexjcmVc1caYyfNvMfhJRU6qtmi57Siv1ysirg0=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","\u002BgTea2HWyPcSQvQyVe0ghcDwqBGkKZIDd2\u002BlGKdxL2U="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["BNGxWTPjjFD1Fj56FltRDUvsBzgMlQvuqV\u002BraH2IhwQ=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","khGrM2Rl22MsVh9N6\u002B7todRrMuJ6o3ljuHxZF/aubqE=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","6xYke/2SzNspypSwIgizeNUH7b\u002Bfoz3wYfKk6z1tMsw="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["BNGxWTPjjFD1Fj56FltRDUvsBzgMlQvuqV\u002BraH2IhwQ=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","PmrFmFQepAv9NhtsO0l45PPk3BWugccwJ3ysTKcw7bM=","6CAjHexjcmVc1caYyfNvMfhJRU6qtmi57Siv1ysirg0=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","\u002BgTea2HWyPcSQvQyVe0ghcDwqBGkKZIDd2\u002BlGKdxL2U="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="GetEFProjectMetadata">
<MSBuild Condition=" '$(TargetFramework)' == '' "
Projects="$(MSBuildProjectFile)"
Targets="GetEFProjectMetadata"
Properties="TargetFramework=$(TargetFrameworks.Split(';')[0]);EFProjectMetadataFile=$(EFProjectMetadataFile)" />
<ItemGroup Condition=" '$(TargetFramework)' != '' ">
<EFProjectMetadata Include="AssemblyName: $(AssemblyName)" />
<EFProjectMetadata Include="Language: $(Language)" />
<EFProjectMetadata Include="OutputPath: $(OutputPath)" />
<EFProjectMetadata Include="Platform: $(Platform)" />
<EFProjectMetadata Include="PlatformTarget: $(PlatformTarget)" />
<EFProjectMetadata Include="ProjectAssetsFile: $(ProjectAssetsFile)" />
<EFProjectMetadata Include="ProjectDir: $(ProjectDir)" />
<EFProjectMetadata Include="RootNamespace: $(RootNamespace)" />
<EFProjectMetadata Include="RuntimeFrameworkVersion: $(RuntimeFrameworkVersion)" />
<EFProjectMetadata Include="TargetFileName: $(TargetFileName)" />
<EFProjectMetadata Include="TargetFrameworkMoniker: $(TargetFrameworkMoniker)" />
<EFProjectMetadata Include="Nullable: $(Nullable)" />
<EFProjectMetadata Include="TargetFramework: $(TargetFramework)" />
<EFProjectMetadata Include="TargetPlatformIdentifier: $(TargetPlatformIdentifier)" />
</ItemGroup>
<WriteLinesToFile Condition=" '$(TargetFramework)' != '' "
File="$(EFProjectMetadataFile)"
Lines="@(EFProjectMetadata)" />
</Target>
</Project>

View File

@@ -3,7 +3,11 @@ namespace Elecciones.Core.DTOs.ApiResponses;
public class AgrupacionResultadoDto
{
public required string? Id { get; set; }
public string Nombre { get; set; } = null!;
public string? NombreCorto { get; set; } = null!;
public string? Color { get; set; } = null!;
public string? LogoUrl { get; set; } = null!;
public long Votos { get; set; }
public decimal Porcentaje { get; set; }
}

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+608ae655bedf6c59be5fec1e14fc308871d2fd62")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+271a86b63211dff88fb7188d1db7b390e6c53efb")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -78,7 +78,8 @@ public class EleccionesDbContext(DbContextOptions<EleccionesDbContext> options)
});
modelBuilder.Entity<LogoAgrupacionCategoria>(entity =>
{
entity.HasIndex(l => new { l.AgrupacionPoliticaId, l.CategoriaId }).IsUnique();
// La combinación de las tres columnas debe ser única.
entity.HasIndex(l => new { l.AgrupacionPoliticaId, l.CategoriaId, l.AmbitoGeograficoId }).IsUnique();
});
}
}

View File

@@ -7,12 +7,10 @@ public class LogoAgrupacionCategoria
{
[Key]
public int Id { get; set; }
[Required]
public string AgrupacionPoliticaId { get; set; } = null!;
[Required]
public int CategoriaId { get; set; }
public string? LogoUrl { get; set; }
public int? AmbitoGeograficoId { get; set; }
}

View File

@@ -0,0 +1,556 @@
// <auto-generated />
using System;
using Elecciones.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Elecciones.Database.Migrations
{
[DbContext(typeof(EleccionesDbContext))]
[Migration("20250902155740_AddAmbitoToLogosWithCorrectIndex")]
partial class AddAmbitoToLogosWithCorrectIndex
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.UseCollation("Modern_Spanish_CI_AS")
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Elecciones.Database.Entities.AdminUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("PasswordSalt")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.HasKey("Id");
b.ToTable("AdminUsers");
});
modelBuilder.Entity("Elecciones.Database.Entities.AgrupacionPolitica", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("Color")
.HasColumnType("nvarchar(max)");
b.Property<string>("IdTelegrama")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Nombre")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("NombreCorto")
.HasColumnType("nvarchar(max)");
b.Property<int?>("OrdenDiputados")
.HasColumnType("int");
b.Property<int?>("OrdenSenadores")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("AgrupacionesPoliticas");
});
modelBuilder.Entity("Elecciones.Database.Entities.AmbitoGeografico", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("CircuitoId")
.HasColumnType("nvarchar(max)");
b.Property<string>("DistritoId")
.HasColumnType("nvarchar(max)");
b.Property<string>("EstablecimientoId")
.HasColumnType("nvarchar(max)");
b.Property<string>("MesaId")
.HasColumnType("nvarchar(max)");
b.Property<string>("MunicipioId")
.HasColumnType("nvarchar(max)");
b.Property<int>("NivelId")
.HasColumnType("int");
b.Property<string>("Nombre")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("SeccionId")
.HasColumnType("nvarchar(max)");
b.Property<string>("SeccionProvincialId")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("AmbitosGeograficos");
});
modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AgrupacionPoliticaId")
.HasColumnType("nvarchar(450)");
b.Property<int>("Camara")
.HasColumnType("int");
b.Property<int>("NumeroBanca")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("AgrupacionPoliticaId");
b.ToTable("Bancadas");
});
modelBuilder.Entity("Elecciones.Database.Entities.CategoriaElectoral", b =>
{
b.Property<int>("Id")
.HasColumnType("int");
b.Property<string>("Nombre")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Orden")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("CategoriasElectorales");
});
modelBuilder.Entity("Elecciones.Database.Entities.Configuracion", b =>
{
b.Property<string>("Clave")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Valor")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.HasKey("Clave");
b.ToTable("Configuraciones");
});
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuento", b =>
{
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<int>("CantidadElectores")
.HasColumnType("int");
b.Property<int>("CantidadVotantes")
.HasColumnType("int");
b.Property<DateTime>("FechaTotalizacion")
.HasColumnType("datetime2");
b.Property<int>("MesasEsperadas")
.HasColumnType("int");
b.Property<int>("MesasTotalizadas")
.HasColumnType("int");
b.Property<decimal>("MesasTotalizadasPorcentaje")
.HasPrecision(5, 2)
.HasColumnType("decimal(5,2)");
b.Property<decimal>("ParticipacionPorcentaje")
.HasPrecision(5, 2)
.HasColumnType("decimal(5,2)");
b.Property<long>("VotosEnBlanco")
.HasColumnType("bigint");
b.Property<decimal>("VotosEnBlancoPorcentaje")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<long>("VotosNulos")
.HasColumnType("bigint");
b.Property<decimal>("VotosNulosPorcentaje")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<long>("VotosRecurridos")
.HasColumnType("bigint");
b.Property<decimal>("VotosRecurridosPorcentaje")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.HasKey("AmbitoGeograficoId", "CategoriaId");
b.ToTable("EstadosRecuentos");
});
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuentoGeneral", b =>
{
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<int>("CantidadElectores")
.HasColumnType("int");
b.Property<int>("CantidadVotantes")
.HasColumnType("int");
b.Property<DateTime>("FechaTotalizacion")
.HasColumnType("datetime2");
b.Property<int>("MesasEsperadas")
.HasColumnType("int");
b.Property<int>("MesasTotalizadas")
.HasColumnType("int");
b.Property<decimal>("MesasTotalizadasPorcentaje")
.HasPrecision(5, 2)
.HasColumnType("decimal(5,2)");
b.Property<decimal>("ParticipacionPorcentaje")
.HasPrecision(5, 2)
.HasColumnType("decimal(5,2)");
b.HasKey("AmbitoGeograficoId", "CategoriaId");
b.HasIndex("CategoriaId");
b.ToTable("EstadosRecuentosGenerales");
});
modelBuilder.Entity("Elecciones.Database.Entities.LogoAgrupacionCategoria", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AgrupacionPoliticaId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int?>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<string>("LogoUrl")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("AgrupacionPoliticaId", "CategoriaId", "AmbitoGeograficoId")
.IsUnique()
.HasFilter("[AmbitoGeograficoId] IS NOT NULL");
b.ToTable("LogosAgrupacionesCategorias");
});
modelBuilder.Entity("Elecciones.Database.Entities.OcupanteBanca", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BancadaId")
.HasColumnType("int");
b.Property<string>("FotoUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("NombreOcupante")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Periodo")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("BancadaId")
.IsUnique();
b.ToTable("OcupantesBancas");
});
modelBuilder.Entity("Elecciones.Database.Entities.ProyeccionBanca", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AgrupacionPoliticaId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<DateTime>("FechaTotalizacion")
.HasColumnType("datetime2");
b.Property<int>("NroBancas")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("AgrupacionPoliticaId");
b.HasIndex("AmbitoGeograficoId", "CategoriaId", "AgrupacionPoliticaId")
.IsUnique();
b.ToTable("ProyeccionesBancas");
});
modelBuilder.Entity("Elecciones.Database.Entities.ResultadoVoto", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("AgrupacionPoliticaId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<long>("CantidadVotos")
.HasColumnType("bigint");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<decimal>("PorcentajeVotos")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.HasKey("Id");
b.HasIndex("AgrupacionPoliticaId");
b.HasIndex("AmbitoGeograficoId", "CategoriaId", "AgrupacionPoliticaId")
.IsUnique();
b.ToTable("ResultadosVotos");
});
modelBuilder.Entity("Elecciones.Database.Entities.ResumenVoto", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AgrupacionPoliticaId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<long>("Votos")
.HasColumnType("bigint");
b.Property<decimal>("VotosPorcentaje")
.HasPrecision(5, 2)
.HasColumnType("decimal(5,2)");
b.HasKey("Id");
b.HasIndex("AgrupacionPoliticaId");
b.ToTable("ResumenesVotos");
});
modelBuilder.Entity("Elecciones.Database.Entities.Telegrama", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<string>("ContenidoBase64")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("FechaEscaneo")
.HasColumnType("datetime2");
b.Property<DateTime>("FechaTotalizacion")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Telegramas");
});
modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b =>
{
b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica")
.WithMany()
.HasForeignKey("AgrupacionPoliticaId");
b.Navigation("AgrupacionPolitica");
});
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuento", b =>
{
b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico")
.WithMany()
.HasForeignKey("AmbitoGeograficoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AmbitoGeografico");
});
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuentoGeneral", b =>
{
b.HasOne("Elecciones.Database.Entities.CategoriaElectoral", "CategoriaElectoral")
.WithMany()
.HasForeignKey("CategoriaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CategoriaElectoral");
});
modelBuilder.Entity("Elecciones.Database.Entities.OcupanteBanca", b =>
{
b.HasOne("Elecciones.Database.Entities.Bancada", "Bancada")
.WithOne("Ocupante")
.HasForeignKey("Elecciones.Database.Entities.OcupanteBanca", "BancadaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Bancada");
});
modelBuilder.Entity("Elecciones.Database.Entities.ProyeccionBanca", b =>
{
b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica")
.WithMany()
.HasForeignKey("AgrupacionPoliticaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico")
.WithMany()
.HasForeignKey("AmbitoGeograficoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AgrupacionPolitica");
b.Navigation("AmbitoGeografico");
});
modelBuilder.Entity("Elecciones.Database.Entities.ResultadoVoto", b =>
{
b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica")
.WithMany()
.HasForeignKey("AgrupacionPoliticaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico")
.WithMany()
.HasForeignKey("AmbitoGeograficoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AgrupacionPolitica");
b.Navigation("AmbitoGeografico");
});
modelBuilder.Entity("Elecciones.Database.Entities.ResumenVoto", b =>
{
b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica")
.WithMany()
.HasForeignKey("AgrupacionPoliticaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AgrupacionPolitica");
});
modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b =>
{
b.Navigation("Ocupante");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Elecciones.Database.Migrations
{
/// <inheritdoc />
public partial class AddAmbitoToLogosWithCorrectIndex : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_LogosAgrupacionesCategorias_AgrupacionPoliticaId_CategoriaId",
table: "LogosAgrupacionesCategorias");
migrationBuilder.AddColumn<int>(
name: "AmbitoGeograficoId",
table: "LogosAgrupacionesCategorias",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_LogosAgrupacionesCategorias_AgrupacionPoliticaId_CategoriaId_AmbitoGeograficoId",
table: "LogosAgrupacionesCategorias",
columns: new[] { "AgrupacionPoliticaId", "CategoriaId", "AmbitoGeograficoId" },
unique: true,
filter: "[AmbitoGeograficoId] IS NOT NULL");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_LogosAgrupacionesCategorias_AgrupacionPoliticaId_CategoriaId_AmbitoGeograficoId",
table: "LogosAgrupacionesCategorias");
migrationBuilder.DropColumn(
name: "AmbitoGeograficoId",
table: "LogosAgrupacionesCategorias");
migrationBuilder.CreateIndex(
name: "IX_LogosAgrupacionesCategorias_AgrupacionPoliticaId_CategoriaId",
table: "LogosAgrupacionesCategorias",
columns: new[] { "AgrupacionPoliticaId", "CategoriaId" },
unique: true);
}
}
}

View File

@@ -284,6 +284,9 @@ namespace Elecciones.Database.Migrations
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int?>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<int>("CategoriaId")
.HasColumnType("int");
@@ -292,8 +295,9 @@ namespace Elecciones.Database.Migrations
b.HasKey("Id");
b.HasIndex("AgrupacionPoliticaId", "CategoriaId")
.IsUnique();
b.HasIndex("AgrupacionPoliticaId", "CategoriaId", "AmbitoGeograficoId")
.IsUnique()
.HasFilter("[AmbitoGeograficoId] IS NOT NULL");
b.ToTable("LogosAgrupacionesCategorias");
});

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+3b8c6bf754cff6ace486ae8fe850ed4d69233280")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+271a86b63211dff88fb7188d1db7b390e6c53efb")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+608ae655bedf6c59be5fec1e14fc308871d2fd62")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+271a86b63211dff88fb7188d1db7b390e6c53efb")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]