feat: Implement BolsaLocalWidget and configure API CORS
This commit is contained in:
		
							
								
								
									
										59
									
								
								frontend/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								frontend/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| import { Box, Container, Typography, AppBar, Toolbar, CssBaseline } from '@mui/material'; | ||||
| import { BolsaLocalWidget } from './components/BolsaLocalWidget'; | ||||
|  | ||||
| function App() { | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <CssBaseline /> | ||||
|       <AppBar position="static" sx={{ backgroundColor: '#028fbe' }}> | ||||
|         <Toolbar> | ||||
|           <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> | ||||
|             Mercados Modernos - Demo | ||||
|           </Typography> | ||||
|         </Toolbar> | ||||
|       </AppBar> | ||||
|  | ||||
|       <Container maxWidth="xl" sx={{ mt: 4, mb: 4 }}> | ||||
|         <Typography variant="h4" gutterBottom> | ||||
|           Datos del Mercado | ||||
|         </Typography> | ||||
|  | ||||
|         {/* --- Sección 1: Mercado Agroganadero de Cañuelas --- */} | ||||
|         <Box component="section" sx={{ mb: 5 }}> | ||||
|           <Typography variant="h5" gutterBottom>Mercado Agroganadero de Cañuelas</Typography> | ||||
|           {/* Aquí irá el <MercadoAgroWidget /> */} | ||||
|           <Typography color="text.secondary">Widget en construcción...</Typography> | ||||
|         </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> | ||||
|            {/* Aquí irá el <GranosWidget /> */} | ||||
|            <Typography color="text.secondary">Widget en construcción...</Typography> | ||||
|         </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> | ||||
|            {/* Aquí irá el <BolsaUsaWidget /> */} | ||||
|            <Typography color="text.secondary">Widget en construcción...</Typography> | ||||
|         </Box> | ||||
|  | ||||
|         {/* --- Sección 4: Mercado de Valores Local --- */} | ||||
|         <Box component="section" sx={{ mb: 5 }}> | ||||
|           <Typography variant="h5" gutterBottom>Mercado de Valores Argentina</Typography> | ||||
|           <BolsaLocalWidget /> | ||||
|         </Box> | ||||
|  | ||||
|       </Container> | ||||
|       <Box component="footer" sx={{ p: 2, mt: 'auto', backgroundColor: '#f5f5f5', textAlign: 'center' }}> | ||||
|           <Typography variant="body2" color="text.secondary"> | ||||
|               Desarrollado con Arquitectura Moderna - {new Date().getFullYear()} | ||||
|           </Typography> | ||||
|       </Box> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default App; | ||||
							
								
								
									
										14
									
								
								frontend/src/api/apiClient.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/api/apiClient.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| 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 apiClient = axios.create({ | ||||
|   baseURL: API_BASE_URL, | ||||
|   headers: { | ||||
|     'Content-Type': 'application/json', | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| export default apiClient; | ||||
							
								
								
									
										94
									
								
								frontend/src/components/BolsaLocalWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								frontend/src/components/BolsaLocalWidget.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| import { | ||||
|   Box, | ||||
|   CircularProgress, | ||||
|   Alert, | ||||
|   Table, | ||||
|   TableBody, | ||||
|   TableCell, | ||||
|   TableContainer, | ||||
|   TableHead, | ||||
|   TableRow, | ||||
|   Paper, | ||||
|   Typography, | ||||
|   Tooltip | ||||
| } from '@mui/material'; | ||||
| import type { CotizacionBolsa } from '../models/mercadoModels'; | ||||
| import { useApiData } from '../hooks/useApiData'; | ||||
| import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; | ||||
| import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; | ||||
| import RemoveIcon from '@mui/icons-material/Remove'; // Para cambios neutros | ||||
|  | ||||
| // Función para formatear números | ||||
| const formatNumber = (num: number) => { | ||||
|   return 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> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export const BolsaLocalWidget = () => { | ||||
|   const { data, loading, error } = useApiData<CotizacionBolsa[]>('/mercados/bolsa/local'); | ||||
|  | ||||
|   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 disponibles para el mercado local en este momento.</Alert>; | ||||
|   } | ||||
|  | ||||
|   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> | ||||
|             </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> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										0
									
								
								frontend/src/components/BolsaUsaWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								frontend/src/components/BolsaUsaWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								frontend/src/components/GranosWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								frontend/src/components/GranosWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								frontend/src/components/MercadoAgroWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								frontend/src/components/MercadoAgroWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										34
									
								
								frontend/src/hooks/useApiData.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								frontend/src/hooks/useApiData.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import { useState, useEffect, useCallback } from 'react'; | ||||
| import apiClient from '../api/apiClient'; | ||||
| import { AxiosError } from 'axios'; | ||||
|  | ||||
| // T es el tipo de dato que esperamos de la API (ej. CotizacionBolsa[]) | ||||
| export function useApiData<T>(url: string) { | ||||
|   const [data, setData] = useState<T | null>(null); | ||||
|   const [loading, setLoading] = useState<boolean>(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|   const fetchData = useCallback(async () => { | ||||
|     setLoading(true); | ||||
|     setError(null); | ||||
|     try { | ||||
|       const response = await apiClient.get<T>(url); | ||||
|       setData(response.data); | ||||
|     } catch (err) { | ||||
|       if (err instanceof AxiosError) { | ||||
|         setError(`Error al cargar datos: ${err.message}`); | ||||
|       } else { | ||||
|         setError('Ocurrió un error inesperado.'); | ||||
|       } | ||||
|       console.error(err); | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }, [url]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     fetchData(); | ||||
|   }, [fetchData]); | ||||
|  | ||||
|   return { data, loading, error, refetch: fetchData }; | ||||
| } | ||||
							
								
								
									
										9
									
								
								frontend/src/main.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/main.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import { StrictMode } from 'react' | ||||
| import { createRoot } from 'react-dom/client' | ||||
| import App from './App.tsx' | ||||
|  | ||||
| createRoot(document.getElementById('root')!).render( | ||||
|   <StrictMode> | ||||
|     <App /> | ||||
|   </StrictMode>, | ||||
| ) | ||||
							
								
								
									
										37
									
								
								frontend/src/models/mercadoModels.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								frontend/src/models/mercadoModels.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| // Nombres de propiedad en minúscula (camelCase) para seguir la convención de JavaScript/JSON | ||||
| // La deserialización de JSON se encargará de mapearlos automáticamente. | ||||
|  | ||||
| export interface CotizacionGanado { | ||||
|   id: number; | ||||
|   categoria: string; | ||||
|   especificaciones: string; | ||||
|   maximo: number; | ||||
|   minimo: number; | ||||
|   promedio: number; | ||||
|   mediano: number; | ||||
|   cabezas: number; | ||||
|   kilosTotales: number; | ||||
|   kilosPorCabeza: number; | ||||
|   importeTotal: number; | ||||
|   fechaRegistro: string; | ||||
| } | ||||
|  | ||||
| export interface CotizacionGrano { | ||||
|   id: number; | ||||
|   nombre: string; | ||||
|   precio: number; | ||||
|   variacionPrecio: number; | ||||
|   fechaOperacion: string; | ||||
|   fechaRegistro: string; | ||||
| } | ||||
|  | ||||
| export interface CotizacionBolsa { | ||||
|   id: number; | ||||
|   ticker: string; | ||||
|   mercado: string; | ||||
|   precioActual: number; | ||||
|   apertura: number; | ||||
|   cierreAnterior: number; | ||||
|   porcentajeCambio: number; | ||||
|   fechaRegistro: string; | ||||
| } | ||||
							
								
								
									
										1
									
								
								frontend/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /// <reference types="vite/client" /> | ||||
		Reference in New Issue
	
	Block a user