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)} + + + ))} + + + + + + + Historial de 30 días para: {selectedTicker} + theme.palette.grey[500] }} + > + + + + + {selectedTicker && } + + + ); }; \ 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)} + + + ))} + + + + + + + Historial de 30 días para: {selectedTicker} + theme.palette.grey[500] }} + > + + + + + {selectedTicker && } + + + ); }; \ 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.