feat: Add visual summary cards for Agro/Grains and implement 24h time format
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/eldia.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>Mercadors - El Día</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
410
frontend/package-lock.json
generated
410
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
6
frontend/public/eldia.svg
Normal file
6
frontend/public/eldia.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="89" height="69">
|
||||
<path d="M0 0 C29.37 0 58.74 0 89 0 C89 22.77 89 45.54 89 69 C59.63 69 30.26 69 0 69 C0 46.23 0 23.46 0 0 Z " fill="#008FBD" transform="translate(0,0)"/>
|
||||
<path d="M0 0 C3.3 0 6.6 0 10 0 C13.04822999 3.04822999 12.29337257 6.08805307 12.32226562 10.25390625 C12.31904297 11.32511719 12.31582031 12.39632812 12.3125 13.5 C12.32861328 14.57121094 12.34472656 15.64242187 12.36132812 16.74609375 C12.36197266 17.76832031 12.36261719 18.79054688 12.36328125 19.84375 C12.36775269 21.25430664 12.36775269 21.25430664 12.37231445 22.69335938 C12.1880188 23.83514648 12.1880188 23.83514648 12 25 C9 27 9 27 0 27 C0 18.09 0 9.18 0 0 Z " fill="#000000" transform="translate(0,21)"/>
|
||||
<path d="M0 0 C5.61 0 11.22 0 17 0 C17 3.3 17 6.6 17 10 C25.58 10 34.16 10 43 10 C43 10.99 43 11.98 43 13 C34.42 13 25.84 13 17 13 C17 17.29 17 21.58 17 26 C11.39 26 5.78 26 0 26 C0 24.68 0 23.36 0 22 C4.62 22 9.24 22 14 22 C14 16.06 14 10.12 14 4 C9.38 4 4.76 4 0 4 C0 2.68 0 1.36 0 0 Z " fill="#000000" transform="translate(46,21)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -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() {
|
||||
<AppBar position="static" sx={{ backgroundColor: '#028fbe' }}>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
Mercados Modernos - Demo
|
||||
Mercados - El Día
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
@@ -27,16 +30,36 @@ function App() {
|
||||
<MercadoAgroWidget />
|
||||
</Box>
|
||||
|
||||
{/* --- Sección 1: Mercado Agroganadero de Cañuelas --- */}
|
||||
<Box component="section" sx={{ mb: 5 }}>
|
||||
<Typography variant="h5" gutterBottom>Mercado Agroganadero de Cañuelas</Typography>
|
||||
<MercadoAgroCardWidget />
|
||||
<Divider sx={{ my: 3 }}>
|
||||
<Typography variant="caption" color="text.secondary">Datos Detallados</Typography>
|
||||
</Divider>
|
||||
<MercadoAgroWidget />
|
||||
</Box>
|
||||
|
||||
{/* --- Sección 2: Granos - Bolsa de Comercio de Rosario --- */}
|
||||
<Box component="section" sx={{ mb: 5 }}>
|
||||
<Typography variant="h5" gutterBottom>Granos - Bolsa de Comercio de Rosario</Typography>
|
||||
<GranosWidget />
|
||||
<GranosCardWidget />
|
||||
<Divider sx={{ my: 3 }}>
|
||||
<Typography variant="caption" color="text.secondary">Datos Detallados</Typography>
|
||||
</Divider>
|
||||
<GranosWidget />
|
||||
</Box>
|
||||
|
||||
{/* --- Sección 2: Granos - Bolsa de Comercio de Rosario --- */}
|
||||
<Box component="section" sx={{ mb: 5 }}>
|
||||
<Typography variant="h5" gutterBottom>Granos - Bolsa de Comercio de Rosario</Typography>
|
||||
<GranosWidget />
|
||||
</Box>
|
||||
|
||||
{/* --- Sección 3: Mercado de Valores de Estados Unidos --- */}
|
||||
<Box component="section" sx={{ mb: 5 }}>
|
||||
<Typography variant="h5" gutterBottom>Mercado de Valores de Estados Unidos</Typography>
|
||||
<BolsaUsaWidget />
|
||||
<BolsaUsaWidget />
|
||||
</Box>
|
||||
|
||||
{/* --- Sección 4: Mercado de Valores Local --- */}
|
||||
@@ -47,9 +70,9 @@ function App() {
|
||||
|
||||
</Container>
|
||||
<Box component="footer" sx={{ p: 2, mt: 'auto', backgroundColor: '#f5f5f5', textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Desarrollado por El Día - {new Date().getFullYear()}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Desarrollado por El Día - {new Date().getFullYear()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<Box component="span" sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color }}>
|
||||
<Icon sx={{ fontSize: '1rem', mr: 0.5 }} />
|
||||
<Typography variant="body2" component="span" sx={{ fontWeight: 'bold' }}>
|
||||
{formatNumber(value)}%
|
||||
</Typography>
|
||||
<Typography variant="body2" component="span" sx={{ fontWeight: 'bold' }}>{formatNumber(value)}%</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const BolsaLocalWidget = () => {
|
||||
const { data, loading, error } = useApiData<CotizacionBolsa[]>('/mercados/bolsa/local');
|
||||
const [selectedTicker, setSelectedTicker] = useState<string | null>(null);
|
||||
|
||||
const handleRowClick = (ticker: string) => {
|
||||
setSelectedTicker(ticker);
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setSelectedTicker(null);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
||||
@@ -47,38 +51,52 @@ export const BolsaLocalWidget = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small" aria-label="tabla bolsa local">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Símbolo</TableCell>
|
||||
<TableCell align="right">Precio Actual</TableCell>
|
||||
<TableCell align="right">Apertura</TableCell>
|
||||
<TableCell align="right">Cierre Anterior</TableCell>
|
||||
<TableCell align="center">% Cambio</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row) => (
|
||||
<TableRow key={row.ticker} hover>
|
||||
<TableCell component="th" scope="row">
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>{row.ticker}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">${formatNumber(row.precioActual)}</TableCell>
|
||||
<TableCell align="right">${formatNumber(row.apertura)}</TableCell>
|
||||
<TableCell align="right">${formatNumber(row.cierreAnterior)}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Variacion value={row.porcentajeCambio} />
|
||||
</TableCell>
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Box sx={{ p: 1, m: 0 }}>
|
||||
<Typography variant="caption" sx={{ fontStyle: 'italic', color: 'text.secondary' }}>
|
||||
Última actualización: {formatFullDateTime(data[0].fechaRegistro)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Table size="small" aria-label="tabla bolsa local">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Símbolo</TableCell>
|
||||
<TableCell align="right">Precio Actual</TableCell>
|
||||
<TableCell align="right">Apertura</TableCell>
|
||||
<TableCell align="right">Cierre Anterior</TableCell>
|
||||
<TableCell align="center">% Cambio</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Tooltip title={`Última actualización: ${new Date(data[0].fechaRegistro).toLocaleString('es-AR')}`}>
|
||||
<Typography variant="caption" sx={{ p: 1, display: 'block', textAlign: 'right', color: 'text.secondary' }}>
|
||||
Fuente: Yahoo Finance
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</TableContainer>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row) => (
|
||||
<TableRow key={row.ticker} hover sx={{ cursor: 'pointer' }} onClick={() => handleRowClick(row.ticker)}>
|
||||
<TableCell component="th" scope="row"><Typography variant="body2" sx={{ fontWeight: 'bold' }}>{row.ticker}</Typography></TableCell>
|
||||
<TableCell align="right">${formatNumber(row.precioActual)}</TableCell>
|
||||
<TableCell align="right">${formatNumber(row.apertura)}</TableCell>
|
||||
<TableCell align="right">${formatNumber(row.cierreAnterior)}</TableCell>
|
||||
<TableCell align="center"><Variacion value={row.porcentajeCambio} /></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Dialog open={Boolean(selectedTicker)} onClose={handleCloseDialog} maxWidth="md" fullWidth>
|
||||
<DialogTitle sx={{ m: 0, p: 2 }}>
|
||||
Historial de 30 días para: {selectedTicker}
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={handleCloseDialog}
|
||||
sx={{ position: 'absolute', right: 8, top: 8, color: (theme) => theme.palette.grey[500] }}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{selectedTicker && <HistoricalChartWidget ticker={selectedTicker} mercado="Local" />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<Box component="span" sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color }}>
|
||||
<Icon sx={{ fontSize: '1rem', mr: 0.5 }} />
|
||||
<Typography variant="body2" component="span" sx={{ fontWeight: 'bold' }}>
|
||||
{value.toFixed(2)}%
|
||||
</Typography>
|
||||
<Typography variant="body2" component="span" sx={{ fontWeight: 'bold' }}>{formatPercentage(value)}%</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const BolsaUsaWidget = () => {
|
||||
const { data, loading, error } = useApiData<CotizacionBolsa[]>('/mercados/bolsa/eeuu');
|
||||
const [selectedTicker, setSelectedTicker] = useState<string | null>(null);
|
||||
|
||||
const handleRowClick = (ticker: string) => {
|
||||
setSelectedTicker(ticker);
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setSelectedTicker(null);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
||||
@@ -40,43 +48,58 @@ export const BolsaUsaWidget = () => {
|
||||
return <Alert severity="error">{error}</Alert>;
|
||||
}
|
||||
|
||||
// Recordatorio de que el fetcher puede estar desactivado
|
||||
if (!data || data.length === 0) {
|
||||
return <Alert severity="info">No hay datos disponibles para el mercado de EEUU. (El fetcher puede estar desactivado)</Alert>;
|
||||
return <Alert severity="info">No hay datos disponibles para el mercado de EEUU. (El fetcher podría estar desactivado en el Worker).</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small" aria-label="tabla bolsa eeuu">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Símbolo</TableCell>
|
||||
<TableCell align="right">Precio Actual</TableCell>
|
||||
<TableCell align="right">Apertura</TableCell>
|
||||
<TableCell align="right">Cierre Anterior</TableCell>
|
||||
<TableCell align="center">% Cambio</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row) => (
|
||||
<TableRow key={row.ticker} hover>
|
||||
<TableCell component="th" scope="row">
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>{row.ticker}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">{formatNumber(row.precioActual)}</TableCell>
|
||||
<TableCell align="right">{formatNumber(row.apertura)}</TableCell>
|
||||
<TableCell align="right">{formatNumber(row.cierreAnterior)}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Variacion value={row.porcentajeCambio} />
|
||||
</TableCell>
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Box sx={{ p: 1, pb: 0 }}>
|
||||
<Typography variant="caption" sx={{ fontStyle: 'italic', color: 'text.secondary' }}>
|
||||
Última actualización: {formatFullDateTime(data[0].fechaRegistro)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Table size="small" aria-label="tabla bolsa eeuu">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Símbolo</TableCell>
|
||||
<TableCell align="right">Precio Actual</TableCell>
|
||||
<TableCell align="right">Apertura</TableCell>
|
||||
<TableCell align="right">Cierre Anterior</TableCell>
|
||||
<TableCell align="center">% Cambio</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Tooltip title={`Última actualización: ${new Date(data[0].fechaRegistro).toLocaleString('es-AR')}`}>
|
||||
<Typography variant="caption" sx={{ p: 1, display: 'block', textAlign: 'right', color: 'text.secondary' }}>
|
||||
Fuente: Finnhub
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</TableContainer>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row) => (
|
||||
<TableRow key={row.ticker} hover sx={{ cursor: 'pointer' }} onClick={() => handleRowClick(row.ticker)}>
|
||||
<TableCell component="th" scope="row"><Typography variant="body2" sx={{ fontWeight: 'bold' }}>{row.ticker}</Typography></TableCell>
|
||||
<TableCell align="right">{formatCurrency(row.precioActual)}</TableCell>
|
||||
<TableCell align="right">{formatCurrency(row.apertura)}</TableCell>
|
||||
<TableCell align="right">{formatCurrency(row.cierreAnterior)}</TableCell>
|
||||
<TableCell align="center"><Variacion value={row.porcentajeCambio} /></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Dialog open={Boolean(selectedTicker)} onClose={handleCloseDialog} maxWidth="md" fullWidth>
|
||||
<DialogTitle sx={{ m: 0, p: 2 }}>
|
||||
Historial de 30 días para: {selectedTicker}
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={handleCloseDialog}
|
||||
sx={{ position: 'absolute', right: 8, top: 8, color: (theme) => theme.palette.grey[500] }}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{selectedTicker && <HistoricalChartWidget ticker={selectedTicker} mercado="EEUU" />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
102
frontend/src/components/GranosCardWidget.tsx
Normal file
102
frontend/src/components/GranosCardWidget.tsx
Normal file
@@ -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 <GiSunflower size={28} color="#fbc02d" />;
|
||||
case 'trigo':
|
||||
return <GiWheat size={28} color="#fbc02d" />;
|
||||
case 'sorgo':
|
||||
return <TbGrain size={28} color="#fbc02d" />;
|
||||
case 'maiz':
|
||||
return <GiCorn size={28} color="#fbc02d" />;
|
||||
default:
|
||||
return <GiGrain size={28} color="#fbc02d" />;
|
||||
}
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
flex: '1 1 180px',
|
||||
minWidth: '180px',
|
||||
maxWidth: '220px',
|
||||
height: '160px',
|
||||
borderTop: `4px solid ${isPositive ? '#2e7d32' : isNegative ? '#d32f2f' : '#bdbdbd'}`
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
{getGrainIcon(grano.nombre)}
|
||||
<Typography variant="h6" component="h3" sx={{ fontWeight: 'bold', ml: 1 }}>
|
||||
{grano.nombre}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ textAlign: 'center', my: 1 }}>
|
||||
<Typography variant="h5" component="p" sx={{ fontWeight: 'bold' }}>
|
||||
${formatCurrency(grano.precio)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
por Tonelada
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color }}>
|
||||
<Icon sx={{ fontSize: '1.1rem', mr: 0.5 }} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
|
||||
{formatCurrency(grano.variacionPrecio)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="caption" align="center" sx={{ mt: 1, color: 'text.secondary' }}>
|
||||
Operación: {formatDateOnly(grano.fechaOperacion)}
|
||||
</Typography>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export const GranosCardWidget = () => {
|
||||
const { data, loading, error } = useApiData<CotizacionGrano[]>('/mercados/granos');
|
||||
|
||||
if (loading) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Alert severity="error">{error}</Alert>;
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Alert severity="info">No hay datos de granos disponibles.</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, justifyContent: 'center' }}>
|
||||
{data.map((grano) => (
|
||||
<GranoCard key={grano.nombre} grano={grano} />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
45
frontend/src/components/HistoricalChartWidget.tsx
Normal file
45
frontend/src/components/HistoricalChartWidget.tsx
Normal file
@@ -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<CotizacionBolsa[]>(`/mercados/bolsa/history/${ticker}?mercado=${mercado}&dias=30`);
|
||||
|
||||
if (loading) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4, height: 300 }}><CircularProgress /></Box>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Alert severity="error" sx={{height: 300}}>{error}</Alert>;
|
||||
}
|
||||
|
||||
if (!data || data.length < 2) {
|
||||
return <Alert severity="info" sx={{height: 300}}>No hay suficientes datos históricos para graficar.</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="fechaRegistro" tickFormatter={formatXAxis} />
|
||||
<YAxis domain={['dataMin - 1', 'dataMax + 1']} tickFormatter={(tick) => `$${tick.toLocaleString('es-AR')}`} />
|
||||
<Tooltip formatter={(value: number) => [`$${value.toFixed(2)}`, 'Precio']} />
|
||||
<Legend />
|
||||
<Line type="monotone" dataKey="precioActual" name="Precio de Cierre" stroke="#8884d8" strokeWidth={2} dot={false} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
79
frontend/src/components/MercadoAgroCardWidget.tsx
Normal file
79
frontend/src/components/MercadoAgroCardWidget.tsx
Normal file
@@ -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 (
|
||||
<Paper elevation={2} sx={{ p: 2, flex: '1 1 250px', minWidth: '250px', maxWidth: '300px' }}>
|
||||
<Typography variant="h6" component="h3" sx={{ fontWeight: 'bold', borderBottom: 1, borderColor: 'divider', pb: 1, mb: 2 }}>
|
||||
{categoria.categoria}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">Precio Máximo:</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: 'success.main' }}>${formatCurrency(categoria.maximo)}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">Precio Mínimo:</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: 'error.main' }}>${formatCurrency(categoria.minimo)}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">Precio Mediano:</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>${formatCurrency(categoria.mediano)}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 3, pt: 1, borderTop: 1, borderColor: 'divider' }}>
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<PiCow size={28}/>
|
||||
<Typography variant="body1" sx={{ fontWeight: 'bold' }}>{formatInteger(categoria.cabezas)}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Cabezas</Typography>
|
||||
</Box>
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<ScaleIcon color="action" />
|
||||
<Typography variant="body1" sx={{ fontWeight: 'bold' }}>{formatInteger(categoria.kilosTotales)}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Kilos</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
// Este widget agrupa los datos por categoría para un resumen más limpio.
|
||||
export const MercadoAgroCardWidget = () => {
|
||||
const { data, loading, error } = useApiData<CotizacionGanado[]>('/mercados/agroganadero');
|
||||
|
||||
if (loading) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
||||
}
|
||||
if (error) {
|
||||
return <Alert severity="error">{error}</Alert>;
|
||||
}
|
||||
if (!data || data.length === 0) {
|
||||
return <Alert severity="info">No hay datos del mercado agroganadero disponibles.</Alert>;
|
||||
}
|
||||
|
||||
// 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<string, CotizacionGanado>);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, justifyContent: 'center' }}>
|
||||
{Object.values(resumenPorCategoria).map(categoria => (
|
||||
<AgroCard key={categoria.categoria} categoria={categoria} />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -53,7 +53,7 @@ export const MercadoAgroWidget = () => {
|
||||
<TableCell align="right">${formatNumber(row.minimo)}</TableCell>
|
||||
<TableCell align="right">${formatNumber(row.mediano)}</TableCell>
|
||||
<TableCell align="right">{formatNumber(row.cabezas, 0)}</TableCell>
|
||||
<TableCell align="right">{formatNumber(row.kilosTotales, 0)} Kg</TableCell>
|
||||
<TableCell align="right">{formatNumber(row.kilosTotales, 0)}</TableCell>
|
||||
<TableCell align="right">${formatNumber(row.importeTotal)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
38
frontend/src/utils/formatters.ts
Normal file
38
frontend/src/utils/formatters.ts
Normal file
@@ -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',
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,12 +12,12 @@ namespace Mercados.Api.Controllers
|
||||
private readonly ICotizacionGranoRepository _granoRepo;
|
||||
private readonly ICotizacionGanadoRepository _ganadoRepo;
|
||||
private readonly ILogger<MercadosController> _logger;
|
||||
|
||||
|
||||
// Inyectamos TODOS los repositorios que necesita el controlador.
|
||||
public MercadosController(
|
||||
ICotizacionBolsaRepository bolsaRepo,
|
||||
ICotizacionGranoRepository granoRepo,
|
||||
ICotizacionGanadoRepository ganadoRepo,
|
||||
ICotizacionBolsaRepository bolsaRepo,
|
||||
ICotizacionGranoRepository granoRepo,
|
||||
ICotizacionGanadoRepository ganadoRepo,
|
||||
ILogger<MercadosController> 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<CotizacionBolsa>), 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<CotizacionBolsa>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
###
|
||||
###
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ namespace Mercados.Infrastructure.Persistence.Repositories
|
||||
|
||||
await connection.ExecuteAsync(sql, cotizaciones);
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<CotizacionBolsa>> 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<CotizacionBolsa>(sql, new { Mercado = mercado });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CotizacionBolsa>> 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<CotizacionBolsa>(sql, new { Ticker = ticker, Mercado = mercado, Dias = dias });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,6 @@ namespace Mercados.Infrastructure.Persistence.Repositories
|
||||
{
|
||||
Task GuardarMuchosAsync(IEnumerable<CotizacionBolsa> cotizaciones);
|
||||
Task<IEnumerable<CotizacionBolsa>> ObtenerUltimasPorMercadoAsync(string mercado);
|
||||
Task<IEnumerable<CotizacionBolsa>> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias);
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ IHost host = Host.CreateDefaultBuilder(args)
|
||||
// que todos implementan la interfaz IDataFetcher.
|
||||
services.AddScoped<IDataFetcher, MercadoAgroFetcher>();
|
||||
services.AddScoped<IDataFetcher, BcrDataFetcher>();
|
||||
//services.AddScoped<IDataFetcher, FinnhubDataFetcher>();
|
||||
services.AddScoped<IDataFetcher, FinnhubDataFetcher>();
|
||||
services.AddScoped<IDataFetcher, YahooFinanceDataFetcher>();
|
||||
|
||||
// El cliente HTTP es fundamental para hacer llamadas a APIs externas.
|
||||
|
||||
Reference in New Issue
Block a user