diff --git a/frontend/index.html b/frontend/index.html
index e4b78ea..7dc52fb 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,9 +2,9 @@
-
+
- Vite + React + TS
+ Mercadors - El Día
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 5e593a5..c849938 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -12,9 +12,12 @@
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.2.0",
"@mui/material": "^7.2.0",
+ "@types/recharts": "^1.8.29",
"axios": "^1.10.0",
"react": "^19.1.0",
- "react-dom": "^19.1.0"
+ "react-dom": "^19.1.0",
+ "react-icons": "^5.5.0",
+ "recharts": "^3.0.2"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
@@ -1451,6 +1454,32 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz",
+ "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.19",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz",
@@ -1738,6 +1767,18 @@
"win32"
]
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1783,6 +1824,69 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz",
+ "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "1.3.12",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz",
+ "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "^1"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1837,6 +1941,22 @@
"@types/react": "*"
}
},
+ "node_modules/@types/recharts": {
+ "version": "1.8.29",
+ "resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.29.tgz",
+ "integrity": "sha512-ulKklaVsnFIIhTQsQw226TnOibrddW1qUQNFVhoQEyY1Z7FRQrNecFCGt7msRuJseudzE9czVawZb17dK/aPXw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-shape": "^1",
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
@@ -2448,6 +2568,127 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -2465,6 +2706,12 @@
}
}
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2566,6 +2813,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.39.5",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.5.tgz",
+ "integrity": "sha512-z9V0qU4lx1TBXDNFWfAASWk6RNU6c6+TJBKE+FLIg8u0XJ6Yw58Hi0yX8ftEouj6p1QARRlXLFfHbIli93BdQQ==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
"node_modules/esbuild": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
@@ -2807,6 +3064,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3164,6 +3427,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
+ "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -3190,6 +3463,15 @@
"node": ">=0.8.19"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -3760,12 +4042,44 @@
"react": "^19.1.0"
}
},
+ "node_modules/react-icons": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
+ "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-is": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
"license": "MIT"
},
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -3792,6 +4106,54 @@
"react-dom": ">=16.6.0"
}
},
+ "node_modules/recharts": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.0.2.tgz",
+ "integrity": "sha512-eDc3ile9qJU9Dp/EekSthQPhAVPG48/uM47jk+PF7VBQngxeW3cwQpPHb/GHC1uqwyCRWXcIrDzuHRVrnRryoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@reduxjs/toolkit": "1.x.x || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -3998,6 +4360,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
@@ -4160,6 +4528,46 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
+ "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "node_modules/victory-vendor/node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
"node_modules/vite": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index b6a2ed3..b703220 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
- "dev": "vite",
+ "dev": "vite --host",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
@@ -14,9 +14,12 @@
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.2.0",
"@mui/material": "^7.2.0",
+ "@types/recharts": "^1.8.29",
"axios": "^1.10.0",
"react": "^19.1.0",
- "react-dom": "^19.1.0"
+ "react-dom": "^19.1.0",
+ "react-icons": "^5.5.0",
+ "recharts": "^3.0.2"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
diff --git a/frontend/public/eldia.svg b/frontend/public/eldia.svg
new file mode 100644
index 0000000..91ce072
--- /dev/null
+++ b/frontend/public/eldia.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg
deleted file mode 100644
index e7b8dfb..0000000
--- a/frontend/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 3e9e629..bb8f687 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -3,6 +3,9 @@ import { BolsaLocalWidget } from './components/BolsaLocalWidget';
import { MercadoAgroWidget } from './components/MercadoAgroWidget';
import { GranosWidget } from './components/GranosWidget';
import { BolsaUsaWidget } from './components/BolsaUsaWidget';
+import { MercadoAgroCardWidget } from './components/MercadoAgroCardWidget';
+import { GranosCardWidget } from './components/GranosCardWidget';
+import { Divider } from '@mui/material';
function App() {
return (
@@ -11,7 +14,7 @@ function App() {
- Mercados Modernos - Demo
+ Mercados - El Día
@@ -27,16 +30,36 @@ function App() {
+ {/* --- Sección 1: Mercado Agroganadero de Cañuelas --- */}
+
+ Mercado Agroganadero de Cañuelas
+
+
+ Datos Detallados
+
+
+
+
{/* --- Sección 2: Granos - Bolsa de Comercio de Rosario --- */}
Granos - Bolsa de Comercio de Rosario
-
+
+
+ Datos Detallados
+
+
+
+
+ {/* --- Sección 2: Granos - Bolsa de Comercio de Rosario --- */}
+
+ Granos - Bolsa de Comercio de Rosario
+
{/* --- Sección 3: Mercado de Valores de Estados Unidos --- */}
Mercado de Valores de Estados Unidos
-
+
{/* --- Sección 4: Mercado de Valores Local --- */}
@@ -47,9 +70,9 @@ function App() {
-
- Desarrollado por El Día - {new Date().getFullYear()}
-
+
+ Desarrollado por El Día - {new Date().getFullYear()}
+
>
);
diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts
index 254e120..1391bdb 100644
--- a/frontend/src/api/apiClient.ts
+++ b/frontend/src/api/apiClient.ts
@@ -2,7 +2,7 @@ import axios from 'axios';
// Durante el desarrollo, nuestra API corre en un puerto específico (ej. 5045).
// En producción, esto debería apuntar a la URL real del servidor donde se despliegue la API.
-const API_BASE_URL = 'http://localhost:5045/api';
+const API_BASE_URL = 'http://192.168.10.78:5045/api';
const apiClient = axios.create({
baseURL: API_BASE_URL,
diff --git a/frontend/src/components/BolsaLocalWidget.tsx b/frontend/src/components/BolsaLocalWidget.tsx
index 10f95d9..a9a8e3e 100644
--- a/frontend/src/components/BolsaLocalWidget.tsx
+++ b/frontend/src/components/BolsaLocalWidget.tsx
@@ -1,38 +1,42 @@
+import { useState } from 'react';
import {
- Box, CircularProgress, Alert, Table, TableBody, TableCell,
- TableContainer, TableHead, TableRow, Paper, Typography, Tooltip
+ Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer,
+ TableHead, TableRow, Paper, Typography, Dialog, DialogTitle,
+ DialogContent, IconButton
} from '@mui/material';
-import type { CotizacionBolsa } from '../models/mercadoModels';
-import { useApiData } from '../hooks/useApiData';
+import CloseIcon from '@mui/icons-material/Close';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
-import RemoveIcon from '@mui/icons-material/Remove'; // Para cambios neutros
+import RemoveIcon from '@mui/icons-material/Remove';
+import { formatFullDateTime } from '../utils/formatters';
+import type { CotizacionBolsa } from '../models/mercadoModels';
+import { useApiData } from '../hooks/useApiData';
+import { HistoricalChartWidget } from './HistoricalChartWidget';
-// Función para formatear números
-const formatNumber = (num: number) => {
- return new Intl.NumberFormat('es-AR', {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- }).format(num);
-};
+const formatNumber = (num: number) => new Intl.NumberFormat('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(num);
-// Componente para mostrar la variación con color e icono
const Variacion = ({ value }: { value: number }) => {
const color = value > 0 ? 'success.main' : value < 0 ? 'error.main' : 'text.secondary';
const Icon = value > 0 ? ArrowUpwardIcon : value < 0 ? ArrowDownwardIcon : RemoveIcon;
-
return (
-
- {formatNumber(value)}%
-
+ {formatNumber(value)}%
);
};
export const BolsaLocalWidget = () => {
const { data, loading, error } = useApiData('/mercados/bolsa/local');
+ const [selectedTicker, setSelectedTicker] = useState(null);
+
+ const handleRowClick = (ticker: string) => {
+ setSelectedTicker(ticker);
+ };
+
+ const handleCloseDialog = () => {
+ setSelectedTicker(null);
+ };
if (loading) {
return ;
@@ -47,38 +51,52 @@ export const BolsaLocalWidget = () => {
}
return (
-
-
-
-
- Símbolo
- Precio Actual
- Apertura
- Cierre Anterior
- % Cambio
-
-
-
- {data.map((row) => (
-
-
- {row.ticker}
-
- ${formatNumber(row.precioActual)}
- ${formatNumber(row.apertura)}
- ${formatNumber(row.cierreAnterior)}
-
-
-
+ <>
+
+
+
+ Última actualización: {formatFullDateTime(data[0].fechaRegistro)}
+
+
+
+
+
+ Símbolo
+ Precio Actual
+ Apertura
+ Cierre Anterior
+ % Cambio
- ))}
-
-
-
-
- Fuente: Yahoo Finance
-
-
-
+
+
+ {data.map((row) => (
+ handleRowClick(row.ticker)}>
+ {row.ticker}
+ ${formatNumber(row.precioActual)}
+ ${formatNumber(row.apertura)}
+ ${formatNumber(row.cierreAnterior)}
+
+
+ ))}
+
+
+
+
+
+ >
);
};
\ No newline at end of file
diff --git a/frontend/src/components/BolsaUsaWidget.tsx b/frontend/src/components/BolsaUsaWidget.tsx
index a854a1f..c11ed61 100644
--- a/frontend/src/components/BolsaUsaWidget.tsx
+++ b/frontend/src/components/BolsaUsaWidget.tsx
@@ -1,36 +1,44 @@
+import { useState } from 'react';
import {
- Box, CircularProgress, Alert, Table, TableBody, TableCell,
- TableContainer, TableHead, TableRow, Paper, Typography, Tooltip
+ Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer,
+ TableHead, TableRow, Paper, Typography, Dialog, DialogTitle,
+ DialogContent, IconButton
} from '@mui/material';
-import type { CotizacionBolsa } from '../models/mercadoModels';
-import { useApiData } from '../hooks/useApiData';
+import CloseIcon from '@mui/icons-material/Close';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import RemoveIcon from '@mui/icons-material/Remove';
+import { formatFullDateTime } from '../utils/formatters';
+import type { CotizacionBolsa } from '../models/mercadoModels';
+import { useApiData } from '../hooks/useApiData';
+import { HistoricalChartWidget } from './HistoricalChartWidget';
-const formatNumber = (num: number) => {
- return new Intl.NumberFormat('en-US', { // Usamos formato de EEUU
- style: 'currency',
- currency: 'USD',
- }).format(num);
-};
+// Usamos el formato de EEUU para los precios en dólares
+const formatCurrency = (num: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(num);
+const formatPercentage = (num: number) => num.toFixed(2);
const Variacion = ({ value }: { value: number }) => {
const color = value > 0 ? 'success.main' : value < 0 ? 'error.main' : 'text.secondary';
const Icon = value > 0 ? ArrowUpwardIcon : value < 0 ? ArrowDownwardIcon : RemoveIcon;
-
return (
-
- {value.toFixed(2)}%
-
+ {formatPercentage(value)}%
);
};
export const BolsaUsaWidget = () => {
const { data, loading, error } = useApiData('/mercados/bolsa/eeuu');
+ const [selectedTicker, setSelectedTicker] = useState(null);
+
+ const handleRowClick = (ticker: string) => {
+ setSelectedTicker(ticker);
+ };
+
+ const handleCloseDialog = () => {
+ setSelectedTicker(null);
+ };
if (loading) {
return ;
@@ -40,43 +48,58 @@ export const BolsaUsaWidget = () => {
return {error};
}
+ // Recordatorio de que el fetcher puede estar desactivado
if (!data || data.length === 0) {
- return No hay datos disponibles para el mercado de EEUU. (El fetcher puede estar desactivado);
+ return No hay datos disponibles para el mercado de EEUU. (El fetcher podría estar desactivado en el Worker).;
}
return (
-
-
-
-
- Símbolo
- Precio Actual
- Apertura
- Cierre Anterior
- % Cambio
-
-
-
- {data.map((row) => (
-
-
- {row.ticker}
-
- {formatNumber(row.precioActual)}
- {formatNumber(row.apertura)}
- {formatNumber(row.cierreAnterior)}
-
-
-
+ <>
+
+
+
+ Última actualización: {formatFullDateTime(data[0].fechaRegistro)}
+
+
+
+
+
+ Símbolo
+ Precio Actual
+ Apertura
+ Cierre Anterior
+ % Cambio
- ))}
-
-
-
-
- Fuente: Finnhub
-
-
-
+
+
+ {data.map((row) => (
+ handleRowClick(row.ticker)}>
+ {row.ticker}
+ {formatCurrency(row.precioActual)}
+ {formatCurrency(row.apertura)}
+ {formatCurrency(row.cierreAnterior)}
+
+
+ ))}
+
+
+
+
+
+ >
);
};
\ No newline at end of file
diff --git a/frontend/src/components/GranosCardWidget.tsx b/frontend/src/components/GranosCardWidget.tsx
new file mode 100644
index 0000000..cf532d5
--- /dev/null
+++ b/frontend/src/components/GranosCardWidget.tsx
@@ -0,0 +1,102 @@
+import { Box, CircularProgress, Alert, Paper, Typography } from '@mui/material';
+import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
+import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
+import RemoveIcon from '@mui/icons-material/Remove';
+// Iconos de react-icons para cada grano
+import { GiSunflower, GiWheat, GiCorn, GiGrain } from "react-icons/gi";
+import { TbGrain } from "react-icons/tb";
+
+import type { CotizacionGrano } from '../models/mercadoModels';
+import { useApiData } from '../hooks/useApiData';
+import { formatCurrency, formatDateOnly } from '../utils/formatters';
+
+// Función para elegir el icono según el nombre del grano
+const getGrainIcon = (nombre: string) => {
+ switch (nombre.toLowerCase()) {
+ case 'girasol':
+ return ;
+ case 'trigo':
+ return ;
+ case 'sorgo':
+ return ;
+ case 'maiz':
+ return ;
+ default:
+ return ;
+ }
+};
+
+// Subcomponente para una única tarjeta de grano
+const GranoCard = ({ grano }: { grano: CotizacionGrano }) => {
+ const isPositive = grano.variacionPrecio > 0;
+ const isNegative = grano.variacionPrecio < 0;
+ const color = isPositive ? 'success.main' : isNegative ? 'error.main' : 'text.secondary';
+ const Icon = isPositive ? ArrowUpwardIcon : isNegative ? ArrowDownwardIcon : RemoveIcon;
+
+ return (
+
+
+ {getGrainIcon(grano.nombre)}
+
+ {grano.nombre}
+
+
+
+
+
+ ${formatCurrency(grano.precio)}
+
+
+ por Tonelada
+
+
+
+
+
+
+ {formatCurrency(grano.variacionPrecio)}
+
+
+
+ Operación: {formatDateOnly(grano.fechaOperacion)}
+
+
+ );
+};
+
+export const GranosCardWidget = () => {
+ const { data, loading, error } = useApiData('/mercados/granos');
+
+ if (loading) {
+ return ;
+ }
+
+ if (error) {
+ return {error};
+ }
+
+ if (!data || data.length === 0) {
+ return No hay datos de granos disponibles.;
+ }
+
+ return (
+
+ {data.map((grano) => (
+
+ ))}
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/HistoricalChartWidget.tsx b/frontend/src/components/HistoricalChartWidget.tsx
new file mode 100644
index 0000000..b59db4d
--- /dev/null
+++ b/frontend/src/components/HistoricalChartWidget.tsx
@@ -0,0 +1,45 @@
+import { Box, CircularProgress, Alert } from '@mui/material';
+import type { CotizacionBolsa } from '../models/mercadoModels';
+import { useApiData } from '../hooks/useApiData';
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
+
+interface HistoricalChartWidgetProps {
+ ticker: string;
+ mercado: 'Local' | 'EEUU';
+}
+
+// Formateador para el eje X (muestra DD/MM)
+const formatXAxis = (tickItem: string) => {
+ const date = new Date(tickItem);
+ return date.toLocaleDateString('es-AR', { day: '2-digit', month: '2-digit' });
+};
+
+export const HistoricalChartWidget = ({ ticker, mercado }: HistoricalChartWidgetProps) => {
+ // Usamos el hook para obtener los datos del historial de los últimos 30 días
+ const { data, loading, error } = useApiData(`/mercados/bolsa/history/${ticker}?mercado=${mercado}&dias=30`);
+
+ if (loading) {
+ return ;
+ }
+
+ if (error) {
+ return {error};
+ }
+
+ if (!data || data.length < 2) {
+ return No hay suficientes datos históricos para graficar.;
+ }
+
+ return (
+
+
+
+
+ `$${tick.toLocaleString('es-AR')}`} />
+ [`$${value.toFixed(2)}`, 'Precio']} />
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/MercadoAgroCardWidget.tsx b/frontend/src/components/MercadoAgroCardWidget.tsx
new file mode 100644
index 0000000..9ed9402
--- /dev/null
+++ b/frontend/src/components/MercadoAgroCardWidget.tsx
@@ -0,0 +1,79 @@
+import { Box, CircularProgress, Alert, Paper, Typography } from '@mui/material';
+import { PiCow } from "react-icons/pi"; // Un icono divertido para "cabezas"
+import ScaleIcon from '@mui/icons-material/Scale'; // Para kilos
+
+import type { CotizacionGanado } from '../models/mercadoModels';
+import { useApiData } from '../hooks/useApiData';
+import { formatCurrency, formatInteger } from '../utils/formatters';
+
+const AgroCard = ({ categoria }: { categoria: CotizacionGanado }) => {
+ return (
+
+
+ {categoria.categoria}
+
+
+ Precio Máximo:
+ ${formatCurrency(categoria.maximo)}
+
+
+ Precio Mínimo:
+ ${formatCurrency(categoria.minimo)}
+
+
+ Precio Mediano:
+ ${formatCurrency(categoria.mediano)}
+
+
+
+
+
+ {formatInteger(categoria.cabezas)}
+ Cabezas
+
+
+
+ {formatInteger(categoria.kilosTotales)}
+ Kilos
+
+
+
+ );
+};
+
+// Este widget agrupa los datos por categoría para un resumen más limpio.
+export const MercadoAgroCardWidget = () => {
+ const { data, loading, error } = useApiData('/mercados/agroganadero');
+
+ if (loading) {
+ return ;
+ }
+ if (error) {
+ return {error};
+ }
+ if (!data || data.length === 0) {
+ return No hay datos del mercado agroganadero disponibles.;
+ }
+
+ // Agrupamos y sumamos los datos por categoría principal
+ const resumenPorCategoria = data.reduce((acc, item) => {
+ if (!acc[item.categoria]) {
+ acc[item.categoria] = { ...item };
+ } else {
+ acc[item.categoria].cabezas += item.cabezas;
+ acc[item.categoria].kilosTotales += item.kilosTotales;
+ acc[item.categoria].importeTotal += item.importeTotal;
+ acc[item.categoria].maximo = Math.max(acc[item.categoria].maximo, item.maximo);
+ acc[item.categoria].minimo = Math.min(acc[item.categoria].minimo, item.minimo);
+ }
+ return acc;
+ }, {} as Record);
+
+ return (
+
+ {Object.values(resumenPorCategoria).map(categoria => (
+
+ ))}
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/MercadoAgroWidget.tsx b/frontend/src/components/MercadoAgroWidget.tsx
index 34d886b..e63ea65 100644
--- a/frontend/src/components/MercadoAgroWidget.tsx
+++ b/frontend/src/components/MercadoAgroWidget.tsx
@@ -53,7 +53,7 @@ export const MercadoAgroWidget = () => {
${formatNumber(row.minimo)}
${formatNumber(row.mediano)}
{formatNumber(row.cabezas, 0)}
- {formatNumber(row.kilosTotales, 0)} Kg
+ {formatNumber(row.kilosTotales, 0)}
${formatNumber(row.importeTotal)}
))}
diff --git a/frontend/src/utils/formatters.ts b/frontend/src/utils/formatters.ts
new file mode 100644
index 0000000..288000c
--- /dev/null
+++ b/frontend/src/utils/formatters.ts
@@ -0,0 +1,38 @@
+// Formateadores numéricos
+export const formatCurrency = (num: number, currency = 'ARS') => {
+ const style = currency === 'USD' ? 'currency' : 'decimal';
+ const locale = currency === 'USD' ? 'en-US' : 'es-AR';
+
+ return new Intl.NumberFormat(locale, {
+ style: style,
+ currency: currency,
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }).format(num);
+};
+
+export const formatInteger = (num: number) => {
+ return new Intl.NumberFormat('es-AR').format(num);
+};
+
+// Formateadores de fecha y hora
+export const formatFullDateTime = (dateString: string) => {
+ return new Date(dateString).toLocaleString('es-AR', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ hourCycle: 'h23', // <--- LA CLAVE PARA EL FORMATO 24HS
+ timeZone: 'America/Argentina/Buenos_Aires',
+ });
+};
+
+export const formatDateOnly = (dateString: string) => {
+ return new Date(dateString).toLocaleDateString('es-AR', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ timeZone: 'America/Argentina/Buenos_Aires',
+ });
+};
\ No newline at end of file
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 8b0f57b..f77e6e9 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -4,4 +4,8 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
+ server: {
+ host: true, // o "0.0.0.0"
+ port: 5173 // el puerto que uses, opcional
+ }
})
diff --git a/src/Mercados.Api/Controllers/MercadosController.cs b/src/Mercados.Api/Controllers/MercadosController.cs
index eedd326..a50519a 100644
--- a/src/Mercados.Api/Controllers/MercadosController.cs
+++ b/src/Mercados.Api/Controllers/MercadosController.cs
@@ -12,12 +12,12 @@ namespace Mercados.Api.Controllers
private readonly ICotizacionGranoRepository _granoRepo;
private readonly ICotizacionGanadoRepository _ganadoRepo;
private readonly ILogger _logger;
-
+
// Inyectamos TODOS los repositorios que necesita el controlador.
public MercadosController(
- ICotizacionBolsaRepository bolsaRepo,
- ICotizacionGranoRepository granoRepo,
- ICotizacionGanadoRepository ganadoRepo,
+ ICotizacionBolsaRepository bolsaRepo,
+ ICotizacionGranoRepository granoRepo,
+ ICotizacionGanadoRepository ganadoRepo,
ILogger logger)
{
_bolsaRepo = bolsaRepo;
@@ -61,7 +61,7 @@ namespace Mercados.Api.Controllers
return StatusCode(500, "Ocurrió un error interno en el servidor.");
}
}
-
+
// --- Endpoints de Bolsa ---
[HttpGet("bolsa/eeuu")]
[ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
@@ -96,5 +96,22 @@ namespace Mercados.Api.Controllers
return StatusCode(500, "Ocurrió un error interno en el servidor.");
}
}
+
+ [HttpGet("bolsa/history/{ticker}")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task GetBolsaHistory(string ticker, [FromQuery] string mercado = "Local", [FromQuery] int dias = 30)
+ {
+ try
+ {
+ var data = await _bolsaRepo.ObtenerHistorialPorTickerAsync(ticker, mercado, dias);
+ return Ok(data);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error al obtener historial para el ticker {Ticker}.", ticker);
+ return StatusCode(500, "Ocurrió un error interno en el servidor.");
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/Mercados.Api/Mercados.Api.http b/src/Mercados.Api/Mercados.Api.http
index f049508..130e5e1 100644
--- a/src/Mercados.Api/Mercados.Api.http
+++ b/src/Mercados.Api/Mercados.Api.http
@@ -1,6 +1,5 @@
-@Mercados.Api_HostAddress = http://localhost:5045
+@Mercados.Api_HostAddress = http://192.168.10.78:5045
-GET {{Mercados.Api_HostAddress}}/weatherforecast/
Accept: application/json
-###
+###
\ No newline at end of file
diff --git a/src/Mercados.Api/Program.cs b/src/Mercados.Api/Program.cs
index a00ef41..1813740 100644
--- a/src/Mercados.Api/Program.cs
+++ b/src/Mercados.Api/Program.cs
@@ -16,7 +16,7 @@ builder.Services.AddCors(options =>
options.AddPolicy(name: MyAllowSpecificOrigins,
policy =>
{
- policy.WithOrigins("http://localhost:5173")
+ policy.WithOrigins("http://localhost:5173", "http://192.168.10.78:5173")
.AllowAnyHeader()
.AllowAnyMethod();
});
@@ -47,7 +47,6 @@ builder.Services
// Add services to the container.
builder.Services.AddControllers();
-// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
diff --git a/src/Mercados.Api/Properties/launchSettings.json b/src/Mercados.Api/Properties/launchSettings.json
index d9e0359..dda8c1a 100644
--- a/src/Mercados.Api/Properties/launchSettings.json
+++ b/src/Mercados.Api/Properties/launchSettings.json
@@ -5,7 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
- "applicationUrl": "http://localhost:5045",
+ "applicationUrl": "http://0.0.0.0:5045",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -14,7 +14,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
- "applicationUrl": "https://localhost:7256;http://localhost:5045",
+ "applicationUrl": "https://0.0.0.0:7256;http://0.0.0.0:5045",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionBolsaRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionBolsaRepository.cs
index b34f5b4..a42e8e1 100644
--- a/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionBolsaRepository.cs
+++ b/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionBolsaRepository.cs
@@ -23,12 +23,12 @@ namespace Mercados.Infrastructure.Persistence.Repositories
await connection.ExecuteAsync(sql, cotizaciones);
}
-
+
public async Task> ObtenerUltimasPorMercadoAsync(string mercado)
{
using IDbConnection connection = _connectionFactory.CreateConnection();
- // Esta consulta SQL es un poco más avanzada. Usa una "Common Table Expression" (CTE)
+ // Esta consulta usa una "Common Table Expression" (CTE)
// y la función ROW_NUMBER() para obtener el registro más reciente para cada Ticker
// dentro del mercado especificado. Es extremadamente eficiente.
const string sql = @"
@@ -50,5 +50,24 @@ namespace Mercados.Infrastructure.Persistence.Repositories
return await connection.QueryAsync(sql, new { Mercado = mercado });
}
+
+ public async Task> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias)
+ {
+ using IDbConnection connection = _connectionFactory.CreateConnection();
+
+ const string sql = @"
+ SELECT
+ Id, Ticker, Mercado, PrecioActual, Apertura, CierreAnterior, PorcentajeCambio, FechaRegistro
+ FROM
+ CotizacionesBolsa
+ WHERE
+ Ticker = @Ticker
+ AND Mercado = @Mercado
+ AND FechaRegistro >= DATEADD(day, -@Dias, GETUTCDATE())
+ ORDER BY
+ FechaRegistro ASC;"; // ASC es importante para dibujar la línea del gráfico
+
+ return await connection.QueryAsync(sql, new { Ticker = ticker, Mercado = mercado, Dias = dias });
+ }
}
}
\ No newline at end of file
diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionBolsaRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionBolsaRepository.cs
index ee22b16..b225b75 100644
--- a/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionBolsaRepository.cs
+++ b/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionBolsaRepository.cs
@@ -6,5 +6,6 @@ namespace Mercados.Infrastructure.Persistence.Repositories
{
Task GuardarMuchosAsync(IEnumerable cotizaciones);
Task> ObtenerUltimasPorMercadoAsync(string mercado);
+ Task> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias);
}
}
\ No newline at end of file
diff --git a/src/Mercados.Worker/Program.cs b/src/Mercados.Worker/Program.cs
index 34c828f..4e5af0a 100644
--- a/src/Mercados.Worker/Program.cs
+++ b/src/Mercados.Worker/Program.cs
@@ -35,7 +35,7 @@ IHost host = Host.CreateDefaultBuilder(args)
// que todos implementan la interfaz IDataFetcher.
services.AddScoped();
services.AddScoped();
- //services.AddScoped();
+ services.AddScoped();
services.AddScoped();
// El cliente HTTP es fundamental para hacer llamadas a APIs externas.