Feat: RawData Table Format
This commit is contained in:
		
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -178,6 +178,10 @@ DocProject/Help/*.hhk | |||||||
| DocProject/Help/*.hhp | DocProject/Help/*.hhp | ||||||
| DocProject/Help/Html2 | DocProject/Help/Html2 | ||||||
| DocProject/Help/html | DocProject/Help/html | ||||||
|  | # DocFx | ||||||
|  | [Dd]ocs/ | ||||||
|  | docfx.build.json | ||||||
|  | docfx.metadata.json | ||||||
|  |  | ||||||
| # Click-Once directory | # Click-Once directory | ||||||
| publish/ | publish/ | ||||||
|   | |||||||
| @@ -1,33 +1,29 @@ | |||||||
| import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button } from '@mui/material'; | import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button, Typography } from '@mui/material'; | ||||||
| import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | ||||||
|  |  | ||||||
| // Importaciones de nuestro proyecto | // Importaciones de nuestro proyecto | ||||||
| import { useApiData } from '../../hooks/useApiData'; | import { useApiData } from '../../hooks/useApiData'; | ||||||
| import { useIsHoliday } from '../../hooks/useIsHoliday'; | import { useIsHoliday } from '../../hooks/useIsHoliday'; | ||||||
| import type { CotizacionGanado } from '../../models/mercadoModels'; | import type { CotizacionGanado } from '../../models/mercadoModels'; | ||||||
| import { formatInteger, formatCurrency, formatFullDateTime } from '../../utils/formatters'; | import { formatInteger, formatFullDateTime } from '../../utils/formatters'; | ||||||
| import { copyToClipboard } from '../../utils/clipboardUtils'; | import { copyToClipboard } from '../../utils/clipboardUtils'; | ||||||
| import { HolidayAlert } from '../common/HolidayAlert'; | import { HolidayAlert } from '../common/HolidayAlert'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Función para convertir los datos de la tabla a un formato CSV para el portapapeles. |  * Función para convertir los datos a formato TSV (Tab-Separated Values) | ||||||
|  |  * con el formato específico solicitado por redacción. | ||||||
|  */ |  */ | ||||||
| const toCSV = (headers: string[], data: CotizacionGanado[]) => { | const toTSV = (data: CotizacionGanado[]) => { | ||||||
|     const headerRow = headers.join(';'); |     const dataRows = data.map(row => { | ||||||
|     const dataRows = data.map(row =>  |         // Unimos Categoría y Especificaciones en una sola columna para el copiado | ||||||
|         [ |         const categoriaCompleta = `${row.categoria}/${row.especificaciones}`; | ||||||
|             row.categoria, |         const cabezas = formatInteger(row.cabezas); | ||||||
|             row.especificaciones, |         const importeTotal = formatInteger(row.importeTotal); | ||||||
|             formatCurrency(row.maximo), |  | ||||||
|             formatCurrency(row.minimo), |         return [categoriaCompleta, cabezas, importeTotal].join('\t'); | ||||||
|             formatCurrency(row.mediano), |     }); | ||||||
|             formatInteger(row.cabezas), |  | ||||||
|             formatInteger(row.kilosTotales), |     return dataRows.join('\n'); | ||||||
|             formatInteger(row.importeTotal), |  | ||||||
|             formatFullDateTime(row.fechaRegistro) |  | ||||||
|         ].join(';') |  | ||||||
|     ); |  | ||||||
|     return [headerRow, ...dataRows].join('\n'); |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -35,24 +31,21 @@ const toCSV = (headers: string[], data: CotizacionGanado[]) => { | |||||||
|  * diseñado para la página de redacción. |  * diseñado para la página de redacción. | ||||||
|  */ |  */ | ||||||
| export const RawAgroTable = () => { | export const RawAgroTable = () => { | ||||||
|     // Hooks para obtener los datos y el estado de feriado. |  | ||||||
|     const { data, loading: dataLoading, error: dataError } = useApiData<CotizacionGanado[]>('/mercados/agroganadero'); |     const { data, loading: dataLoading, error: dataError } = useApiData<CotizacionGanado[]>('/mercados/agroganadero'); | ||||||
|     const isHoliday = useIsHoliday('BA'); |     const isHoliday = useIsHoliday('BA'); | ||||||
|  |  | ||||||
|     const handleCopy = () => { |     const handleCopy = () => { | ||||||
|         if (!data) return; |         if (!data) return; | ||||||
|         const headers = ["Categoría", "Especificaciones", "Máximo", "Mínimo", "Mediano", "Cabezas", "Kg Total", "Importe Total", "Fecha de Registro"]; |         const tsvData = toTSV(data); | ||||||
|         const csvData = toCSV(headers, data); |  | ||||||
|          |          | ||||||
|         copyToClipboard(csvData) |         copyToClipboard(tsvData) | ||||||
|             .then(() => alert('¡Tabla copiada al portapapeles!')) |             .then(() => alert('Datos del Mercado Agroganadero copiados al portapapeles!')) | ||||||
|             .catch(err => { |             .catch(err => { | ||||||
|                 console.error('Error al copiar:', err); |                 console.error('Error al copiar:', err); | ||||||
|                 alert('Error: No se pudo copiar la tabla.'); |                 alert('Error: No se pudo copiar la tabla.'); | ||||||
|             }); |             }); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     // Estado de carga unificado. |  | ||||||
|     const isLoading = dataLoading || isHoliday === null; |     const isLoading = dataLoading || isHoliday === null; | ||||||
|  |  | ||||||
|     if (isLoading) return <CircularProgress />; |     if (isLoading) return <CircularProgress />; | ||||||
| @@ -67,43 +60,37 @@ export const RawAgroTable = () => { | |||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <Box> |         <Box> | ||||||
|             {/* Si es feriado, mostramos una alerta informativa encima de la tabla. */} |  | ||||||
|             {isHoliday && ( |             {isHoliday && ( | ||||||
|                 <Box sx={{ mb: 2 }}> |                 <Box sx={{ mb: 2 }}> | ||||||
|                     <HolidayAlert /> |                     <HolidayAlert /> | ||||||
|                 </Box> |                 </Box> | ||||||
|             )} |             )} | ||||||
|  |  | ||||||
|             <Button startIcon={<ContentCopyIcon />} onClick={handleCopy} sx={{ mb: 1 }}> |             <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}> | ||||||
|                 Copiar como CSV |                 <Button startIcon={<ContentCopyIcon />} onClick={handleCopy}> | ||||||
|  |                     Copiar Datos para Redacción | ||||||
|                 </Button> |                 </Button> | ||||||
|  |                 <Typography variant="caption" sx={{ fontStyle: 'italic', color: 'text.secondary' }}> | ||||||
|  |                     Última actualización: {formatFullDateTime(data[0].fechaRegistro)} | ||||||
|  |                 </Typography> | ||||||
|  |             </Box> | ||||||
|  |  | ||||||
|  |             {/* La tabla ahora muestra solo las columnas requeridas para facilitar la visualización */} | ||||||
|             <TableContainer component={Paper}> |             <TableContainer component={Paper}> | ||||||
|                 <Table size="small"> |                 <Table size="small"> | ||||||
|                     <TableHead> |                     <TableHead> | ||||||
|                         <TableRow> |                         <TableRow> | ||||||
|                             <TableCell>Categoría</TableCell> |                             <TableCell>Categoría / Especificaciones</TableCell> | ||||||
|                             <TableCell>Especificaciones</TableCell> |  | ||||||
|                             <TableCell align="right">Máximo</TableCell> |  | ||||||
|                             <TableCell align="right">Mínimo</TableCell> |  | ||||||
|                             <TableCell align="right">Mediano</TableCell> |  | ||||||
|                             <TableCell align="right">Cabezas</TableCell> |                             <TableCell align="right">Cabezas</TableCell> | ||||||
|                             <TableCell align="right">Kg Total</TableCell> |  | ||||||
|                             <TableCell align="right">Importe Total</TableCell> |                             <TableCell align="right">Importe Total</TableCell> | ||||||
|                             <TableCell>Última Act.</TableCell> |  | ||||||
|                         </TableRow> |                         </TableRow> | ||||||
|                     </TableHead> |                     </TableHead> | ||||||
|                     <TableBody> |                     <TableBody> | ||||||
|                         {data.map(row => ( |                         {data.map(row => ( | ||||||
|                             <TableRow key={row.id}> |                             <TableRow key={row.id}> | ||||||
|                                 <TableCell>{row.categoria}</TableCell> |                                 <TableCell>{row.categoria} / {row.especificaciones}</TableCell> | ||||||
|                                 <TableCell>{row.especificaciones}</TableCell> |  | ||||||
|                                 <TableCell align="right">${formatCurrency(row.maximo)}</TableCell> |  | ||||||
|                                 <TableCell align="right">${formatCurrency(row.minimo)}</TableCell> |  | ||||||
|                                 <TableCell align="right">${formatCurrency(row.mediano)}</TableCell> |  | ||||||
|                                 <TableCell align="right">{formatInteger(row.cabezas)}</TableCell> |                                 <TableCell align="right">{formatInteger(row.cabezas)}</TableCell> | ||||||
|                                 <TableCell align="right">{formatInteger(row.kilosTotales)}</TableCell> |  | ||||||
|                                 <TableCell align="right">${formatInteger(row.importeTotal)}</TableCell> |                                 <TableCell align="right">${formatInteger(row.importeTotal)}</TableCell> | ||||||
|                                 <TableCell>{formatFullDateTime(row.fechaRegistro)}</TableCell> |  | ||||||
|                             </TableRow> |                             </TableRow> | ||||||
|                         ))} |                         ))} | ||||||
|                     </TableBody> |                     </TableBody> | ||||||
|   | |||||||
| @@ -1,105 +1,121 @@ | |||||||
| import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button } from '@mui/material'; | import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button, Typography, Divider } from '@mui/material'; | ||||||
| import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | ||||||
|  |  | ||||||
| // Importaciones de nuestro proyecto |  | ||||||
| import { useApiData } from '../../hooks/useApiData'; | import { useApiData } from '../../hooks/useApiData'; | ||||||
| import { useIsHoliday } from '../../hooks/useIsHoliday'; |  | ||||||
| import type { CotizacionBolsa } from '../../models/mercadoModels'; | import type { CotizacionBolsa } from '../../models/mercadoModels'; | ||||||
| import { formatCurrency, formatFullDateTime } from '../../utils/formatters'; | import { formatCurrency, formatFullDateTime } from '../../utils/formatters'; | ||||||
| import { copyToClipboard } from '../../utils/clipboardUtils'; | import { copyToClipboard } from '../../utils/clipboardUtils'; | ||||||
| import { HolidayAlert } from '../common/HolidayAlert'; | import { TICKERS_PRIORITARIOS_LOCAL } from '../../config/priorityTickers'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Función para convertir los datos de la tabla a formato CSV. |  * Función para convertir los datos prioritarios a formato TSV (Tab-Separated Values). | ||||||
|  */ |  */ | ||||||
| const toCSV = (headers: string[], data: CotizacionBolsa[]) => { | const toTSV = (data: CotizacionBolsa[]) => { | ||||||
|     const headerRow = headers.join(';'); |     const dataRows = data.map(row => { | ||||||
|     const dataRows = data.map(row =>  |         // Formateamos el nombre para que quede como "GGAL.BA (GRUPO FINANCIERO GALICIA)" | ||||||
|         [ |         const nombreCompleto = `${row.ticker} (${row.nombreEmpresa || ''})`; | ||||||
|             row.ticker, |         const precio = `$${formatCurrency(row.precioActual)}` | ||||||
|             row.nombreEmpresa, |         const cambio = `${row.porcentajeCambio.toFixed(2)}%` | ||||||
|             formatCurrency(row.precioActual), |  | ||||||
|             formatCurrency(row.cierreAnterior), |         // Unimos los campos con un carácter de tabulación '\t' | ||||||
|             `${row.porcentajeCambio.toFixed(2)}%`, |         return [nombreCompleto, precio, cambio].join('\t'); | ||||||
|             formatFullDateTime(row.fechaRegistro) |     }); | ||||||
|         ].join(';') |      | ||||||
|     ); |     // Unimos todas las filas con un salto de línea | ||||||
|     return [headerRow, ...dataRows].join('\n'); |     return dataRows.join('\n'); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Componente de tabla de datos crudos para la Bolsa Local (MERVAL y acciones), |  * Componente de tabla de datos crudos para la Bolsa Local, adaptado para redacción. | ||||||
|  * diseñado para la página de redacción. |  | ||||||
|  */ |  */ | ||||||
| export const RawBolsaLocalTable = () => { | export const RawBolsaLocalTable = () => { | ||||||
|     // Hooks para obtener los datos y el estado de feriado. |     const { data, loading, error } = useApiData<CotizacionBolsa[]>('/mercados/bolsa/local'); | ||||||
|     const { data, loading: dataLoading, error: dataError } = useApiData<CotizacionBolsa[]>('/mercados/bolsa/local'); |  | ||||||
|     const isHoliday = useIsHoliday('BA'); |     // Separamos los datos en prioritarios y el resto | ||||||
|  |     const priorityData = data?.filter(d => TICKERS_PRIORITARIOS_LOCAL.includes(d.ticker)) | ||||||
|  |                              .sort((a, b) => TICKERS_PRIORITARIOS_LOCAL.indexOf(a.ticker) - TICKERS_PRIORITARIOS_LOCAL.indexOf(b.ticker)); // Mantenemos el orden | ||||||
|  |      | ||||||
|  |     const otherData = data?.filter(d => !TICKERS_PRIORITARIOS_LOCAL.includes(d.ticker)); | ||||||
|  |  | ||||||
|     const handleCopy = () => { |     const handleCopy = () => { | ||||||
|         if (!data) return; |         if (!priorityData) return; | ||||||
|         const headers = ["Ticker", "Nombre", "Último Precio", "Cierre Anterior", "Variación %", "Fecha de Registro"]; |         const tsvData = toTSV(priorityData); | ||||||
|         const csvData = toCSV(headers, data); |  | ||||||
|          |          | ||||||
|         copyToClipboard(csvData) |         copyToClipboard(tsvData) | ||||||
|             .then(() => alert('¡Tabla copiada al portapapeles!')) |             .then(() => alert('Datos prioritarios copiados al portapapeles.')) | ||||||
|             .catch(err => { |             .catch(err => { | ||||||
|                 console.error('Error al copiar:', err); |                 console.error('Error al copiar:', err); | ||||||
|                 alert('Error: No se pudo copiar la tabla.'); |                 alert('Error: No se pudo copiar la tabla.'); | ||||||
|             }); |             }); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     // Estado de carga unificado. |     if (loading) return <CircularProgress />; | ||||||
|     const isLoading = dataLoading || isHoliday === null; |     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.</Alert>; | ||||||
|     if (isLoading) return <CircularProgress />; |  | ||||||
|     if (dataError) return <Alert severity="error">{dataError}</Alert>; |  | ||||||
|  |  | ||||||
|     if (!data || data.length === 0) { |  | ||||||
|         if (isHoliday) { |  | ||||||
|             return <HolidayAlert />; |  | ||||||
|         } |  | ||||||
|         return <Alert severity="info">No hay datos disponibles para el mercado local.</Alert>; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <Box> |         <Box> | ||||||
|             {/* Si es feriado, mostramos una alerta informativa encima de la tabla. */} |             <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}> | ||||||
|             {isHoliday && ( |                 <Button startIcon={<ContentCopyIcon />} onClick={handleCopy}> | ||||||
|                 <Box sx={{ mb: 2 }}> |                     Copiar Datos Principales | ||||||
|                     <HolidayAlert /> |  | ||||||
|                 </Box> |  | ||||||
|             )} |  | ||||||
|  |  | ||||||
|             <Button startIcon={<ContentCopyIcon />} onClick={handleCopy} sx={{ mb: 1 }}> |  | ||||||
|                 Copiar como CSV |  | ||||||
|                 </Button> |                 </Button> | ||||||
|  |                 <Typography variant="caption" sx={{ fontStyle: 'italic', color: 'text.secondary' }}> | ||||||
|  |                     Última actualización: {formatFullDateTime(data[0].fechaRegistro)} | ||||||
|  |                 </Typography> | ||||||
|  |             </Box> | ||||||
|  |  | ||||||
|  |             {/* Tabla de Datos Prioritarios */} | ||||||
|  |             <TableContainer component={Paper}> | ||||||
|  |                 <Table size="small"> | ||||||
|  |                     <TableHead> | ||||||
|  |                         <TableRow> | ||||||
|  |                             <TableCell>Símbolo (Nombre)</TableCell> | ||||||
|  |                             <TableCell align="right">Precio Actual</TableCell> | ||||||
|  |                             <TableCell align="right">% Cambio</TableCell> | ||||||
|  |                         </TableRow> | ||||||
|  |                     </TableHead> | ||||||
|  |                     <TableBody> | ||||||
|  |                         {priorityData?.map(row => ( | ||||||
|  |                             <TableRow key={row.id}> | ||||||
|  |                                 <TableCell> | ||||||
|  |                                     <Typography component="span" sx={{ fontWeight: 'bold' }}>{row.ticker}</Typography> | ||||||
|  |                                     <Typography component="span" sx={{ ml: 1, color: 'text.secondary' }}>({row.nombreEmpresa})</Typography> | ||||||
|  |                                 </TableCell> | ||||||
|  |                                 <TableCell align="right">${formatCurrency(row.precioActual)}</TableCell> | ||||||
|  |                                 <TableCell align="right">{row.porcentajeCambio.toFixed(2)}%</TableCell> | ||||||
|  |                             </TableRow> | ||||||
|  |                         ))} | ||||||
|  |                     </TableBody> | ||||||
|  |                 </Table> | ||||||
|  |             </TableContainer> | ||||||
|  |  | ||||||
|  |             {/* Sección para Otros Tickers (solo para consulta) */} | ||||||
|  |             {otherData && otherData.length > 0 && ( | ||||||
|  |                 <Box mt={4}> | ||||||
|  |                     <Divider sx={{ mb: 2 }}> | ||||||
|  |                         <Typography variant="overline">Otros Tickers (Solo Consulta)</Typography> | ||||||
|  |                     </Divider> | ||||||
|                     <TableContainer component={Paper}> |                     <TableContainer component={Paper}> | ||||||
|                         <Table size="small"> |                         <Table size="small"> | ||||||
|                             <TableHead> |                             <TableHead> | ||||||
|                                 <TableRow> |                                 <TableRow> | ||||||
|                                     <TableCell>Ticker</TableCell> |                                     <TableCell>Ticker</TableCell> | ||||||
|                                     <TableCell>Nombre</TableCell> |                                     <TableCell>Nombre</TableCell> | ||||||
|                             <TableCell align="right">Último Precio</TableCell> |                                     <TableCell align="right">Precio</TableCell> | ||||||
|                             <TableCell align="right">Cierre Anterior</TableCell> |  | ||||||
|                             <TableCell align="right">Variación %</TableCell> |  | ||||||
|                             <TableCell>Última Act.</TableCell> |  | ||||||
|                                 </TableRow> |                                 </TableRow> | ||||||
|                             </TableHead> |                             </TableHead> | ||||||
|                             <TableBody> |                             <TableBody> | ||||||
|                         {data.map(row => ( |                                 {otherData.map(row => ( | ||||||
|                                     <TableRow key={row.id}> |                                     <TableRow key={row.id}> | ||||||
|                                         <TableCell>{row.ticker}</TableCell> |                                         <TableCell>{row.ticker}</TableCell> | ||||||
|                                         <TableCell>{row.nombreEmpresa}</TableCell> |                                         <TableCell>{row.nombreEmpresa}</TableCell> | ||||||
|                                         <TableCell align="right">${formatCurrency(row.precioActual)}</TableCell> |                                         <TableCell align="right">${formatCurrency(row.precioActual)}</TableCell> | ||||||
|                                 <TableCell align="right">${formatCurrency(row.cierreAnterior)}</TableCell> |  | ||||||
|                                 <TableCell align="right">{row.porcentajeCambio.toFixed(2)}%</TableCell> |  | ||||||
|                                 <TableCell>{formatFullDateTime(row.fechaRegistro)}</TableCell> |  | ||||||
|                                     </TableRow> |                                     </TableRow> | ||||||
|                                 ))} |                                 ))} | ||||||
|                             </TableBody> |                             </TableBody> | ||||||
|                         </Table> |                         </Table> | ||||||
|                     </TableContainer> |                     </TableContainer> | ||||||
|                 </Box> |                 </Box> | ||||||
|  |             )} | ||||||
|  |         </Box> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button } from '@mui/material'; | import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button, Typography, Divider } from '@mui/material'; | ||||||
| import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | ||||||
|  |  | ||||||
| // Importaciones de nuestro proyecto | // Importaciones de nuestro proyecto | ||||||
| @@ -8,98 +8,125 @@ import type { CotizacionBolsa } from '../../models/mercadoModels'; | |||||||
| import { formatCurrency, formatFullDateTime } from '../../utils/formatters'; | import { formatCurrency, formatFullDateTime } from '../../utils/formatters'; | ||||||
| import { copyToClipboard } from '../../utils/clipboardUtils'; | import { copyToClipboard } from '../../utils/clipboardUtils'; | ||||||
| import { HolidayAlert } from '../common/HolidayAlert'; | import { HolidayAlert } from '../common/HolidayAlert'; | ||||||
|  | import { TICKERS_PRIORITARIOS_USA } from '../../config/priorityTickers'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Función para convertir los datos de la tabla a formato CSV. |  * Función para convertir los datos prioritarios a formato TSV (Tab-Separated Values). | ||||||
|  */ |  */ | ||||||
| const toCSV = (headers: string[], data: CotizacionBolsa[]) => { | const toTSV = (data: CotizacionBolsa[]) => { | ||||||
|     const headerRow = headers.join(';'); |     const dataRows = data.map(row => { | ||||||
|     const dataRows = data.map(row =>  |         // Formateamos los datos según los requisitos de redacción | ||||||
|         [ |         const nombreCompleto = `${row.ticker} (${row.nombreEmpresa || ''})`; | ||||||
|             row.ticker, |         const precio = `$${formatCurrency(row.precioActual)}`; | ||||||
|             row.nombreEmpresa, |         const cambio = `${row.porcentajeCambio.toFixed(2)}%`; | ||||||
|             formatCurrency(row.precioActual, 'USD'), |  | ||||||
|             formatCurrency(row.cierreAnterior, 'USD'), |         return [nombreCompleto, precio, cambio].join('\t'); | ||||||
|             `${row.porcentajeCambio.toFixed(2)}%`, |     }); | ||||||
|             formatFullDateTime(row.fechaRegistro) |     return dataRows.join('\n'); | ||||||
|         ].join(';') |  | ||||||
|     ); |  | ||||||
|     return [headerRow, ...dataRows].join('\n'); |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Componente de tabla de datos crudos para la Bolsa de EEUU y ADRs, |  * Componente de tabla de datos crudos para la Bolsa de EEUU y ADRs, | ||||||
|  * diseñado para la página de redacción. |  * adaptado para las necesidades de redacción. | ||||||
|  */ |  */ | ||||||
| export const RawBolsaUsaTable = () => { | export const RawBolsaUsaTable = () => { | ||||||
|     // Hooks para obtener los datos y el estado de feriado para el mercado de EEUU. |  | ||||||
|     const { data, loading: dataLoading, error: dataError } = useApiData<CotizacionBolsa[]>('/mercados/bolsa/eeuu'); |     const { data, loading: dataLoading, error: dataError } = useApiData<CotizacionBolsa[]>('/mercados/bolsa/eeuu'); | ||||||
|     const isHoliday = useIsHoliday('US'); |     const isHoliday = useIsHoliday('US'); | ||||||
|  |  | ||||||
|     const handleCopy = () => { |     // Separamos los datos en prioritarios y el resto, manteniendo el orden de la lista | ||||||
|         if (!data) return; |     const priorityData = data?.filter(d => TICKERS_PRIORITARIOS_USA.includes(d.ticker)) | ||||||
|         const headers = ["Ticker", "Nombre", "Último Precio (USD)", "Cierre Anterior (USD)", "Variación %", "Fecha de Registro"]; |                              .sort((a, b) => TICKERS_PRIORITARIOS_USA.indexOf(a.ticker) - TICKERS_PRIORITARIOS_USA.indexOf(b.ticker)); | ||||||
|         const csvData = toCSV(headers, data); |  | ||||||
|      |      | ||||||
|         copyToClipboard(csvData) |     const otherData = data?.filter(d => !TICKERS_PRIORITARIOS_USA.includes(d.ticker)); | ||||||
|             .then(() => alert('¡Tabla copiada al portapapeles!')) |  | ||||||
|  |     const handleCopy = () => { | ||||||
|  |         if (!priorityData) return; | ||||||
|  |         const tsvData = toTSV(priorityData); | ||||||
|  |          | ||||||
|  |         copyToClipboard(tsvData) | ||||||
|  |             .then(() => alert('Datos prioritarios copiados al portapapeles!')) | ||||||
|             .catch(err => { |             .catch(err => { | ||||||
|                 console.error('Error al copiar:', err); |                 console.error('Error al copiar:', err); | ||||||
|                 alert('Error: No se pudo copiar la tabla.'); |                 alert('Error: No se pudo copiar la tabla.'); | ||||||
|             }); |             }); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     // Estado de carga unificado. |  | ||||||
|     const isLoading = dataLoading || isHoliday === null; |     const isLoading = dataLoading || isHoliday === null; | ||||||
|  |  | ||||||
|     if (isLoading) return <CircularProgress />; |     if (isLoading) return <CircularProgress />; | ||||||
|     if (dataError) return <Alert severity="error">{dataError}</Alert>; |     if (dataError) return <Alert severity="error">{dataError}</Alert>; | ||||||
|  |  | ||||||
|     if (!data || data.length === 0) { |     if (!data || data.length === 0) { | ||||||
|         if (isHoliday) { |         if (isHoliday) { return <HolidayAlert />; } | ||||||
|             return <HolidayAlert />; |         return <Alert severity="info">No hay datos disponibles para el mercado de EEUU.</Alert>; | ||||||
|         } |  | ||||||
|         return <Alert severity="info">No hay datos disponibles para el mercado de EEUU (el fetcher puede estar desactivado).</Alert>; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <Box> |         <Box> | ||||||
|             {/* Si es feriado, mostramos una alerta informativa encima de la tabla. */} |             {isHoliday && <Box sx={{ mb: 2 }}><HolidayAlert /></Box>} | ||||||
|             {isHoliday && ( |  | ||||||
|                 <Box sx={{ mb: 2 }}> |  | ||||||
|                     <HolidayAlert /> |  | ||||||
|                 </Box> |  | ||||||
|             )} |  | ||||||
|  |  | ||||||
|             <Button startIcon={<ContentCopyIcon />} onClick={handleCopy} sx={{ mb: 1 }}> |             <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}> | ||||||
|                 Copiar como CSV |                 <Button startIcon={<ContentCopyIcon />} onClick={handleCopy}> | ||||||
|  |                     Copiar Datos Principales | ||||||
|                 </Button> |                 </Button> | ||||||
|  |                 <Typography variant="caption" sx={{ fontStyle: 'italic', color: 'text.secondary' }}> | ||||||
|  |                     Última actualización: {formatFullDateTime(data[0].fechaRegistro)} | ||||||
|  |                 </Typography> | ||||||
|  |             </Box> | ||||||
|  |  | ||||||
|  |             {/* Tabla de Datos Prioritarios */} | ||||||
|  |             <TableContainer component={Paper}> | ||||||
|  |                 <Table size="small"> | ||||||
|  |                     <TableHead> | ||||||
|  |                         <TableRow> | ||||||
|  |                             <TableCell>Símbolo (Nombre)</TableCell> | ||||||
|  |                             <TableCell align="right">Precio Actual</TableCell> | ||||||
|  |                             <TableCell align="right">% Cambio</TableCell> | ||||||
|  |                         </TableRow> | ||||||
|  |                     </TableHead> | ||||||
|  |                     <TableBody> | ||||||
|  |                         {priorityData?.map(row => ( | ||||||
|  |                             <TableRow key={row.id}> | ||||||
|  |                                 <TableCell> | ||||||
|  |                                     <Typography component="span" sx={{ fontWeight: 'bold' }}>{row.ticker}</Typography> | ||||||
|  |                                     <Typography component="span" sx={{ ml: 1, color: 'text.secondary' }}>({row.nombreEmpresa})</Typography> | ||||||
|  |                                 </TableCell> | ||||||
|  |                                 <TableCell align="right">{formatCurrency(row.precioActual, 'USD')}</TableCell> | ||||||
|  |                                 <TableCell align="right">{row.porcentajeCambio.toFixed(2)}%</TableCell> | ||||||
|  |                             </TableRow> | ||||||
|  |                         ))} | ||||||
|  |                     </TableBody> | ||||||
|  |                 </Table> | ||||||
|  |             </TableContainer> | ||||||
|  |  | ||||||
|  |             {/* Sección para Otros Tickers (solo para consulta) */} | ||||||
|  |             {otherData && otherData.length > 0 && ( | ||||||
|  |                 <Box mt={4}> | ||||||
|  |                     <Divider sx={{ mb: 2 }}> | ||||||
|  |                         <Typography variant="overline">Otros Tickers (Solo Consulta)</Typography> | ||||||
|  |                     </Divider> | ||||||
|                     <TableContainer component={Paper}> |                     <TableContainer component={Paper}> | ||||||
|                         <Table size="small"> |                         <Table size="small"> | ||||||
|                             <TableHead> |                             <TableHead> | ||||||
|                                 <TableRow> |                                 <TableRow> | ||||||
|                                     <TableCell>Ticker</TableCell> |                                     <TableCell>Ticker</TableCell> | ||||||
|                                     <TableCell>Nombre</TableCell> |                                     <TableCell>Nombre</TableCell> | ||||||
|                             <TableCell align="right">Último Precio</TableCell> |                                     <TableCell align="right">Precio</TableCell> | ||||||
|                             <TableCell align="right">Cierre Anterior</TableCell> |  | ||||||
|                             <TableCell align="right">Variación %</TableCell> |  | ||||||
|                             <TableCell>Última Act.</TableCell> |  | ||||||
|                                 </TableRow> |                                 </TableRow> | ||||||
|                             </TableHead> |                             </TableHead> | ||||||
|                             <TableBody> |                             <TableBody> | ||||||
|                         {data.map(row => ( |                                 {otherData.map(row => ( | ||||||
|                                     <TableRow key={row.id}> |                                     <TableRow key={row.id}> | ||||||
|                                         <TableCell>{row.ticker}</TableCell> |                                         <TableCell>{row.ticker}</TableCell> | ||||||
|                                         <TableCell>{row.nombreEmpresa}</TableCell> |                                         <TableCell>{row.nombreEmpresa}</TableCell> | ||||||
|                                         <TableCell align="right">{formatCurrency(row.precioActual, 'USD')}</TableCell> |                                         <TableCell align="right">{formatCurrency(row.precioActual, 'USD')}</TableCell> | ||||||
|                                 <TableCell align="right">{formatCurrency(row.cierreAnterior, 'USD')}</TableCell> |  | ||||||
|                                 <TableCell align="right">{row.porcentajeCambio.toFixed(2)}%</TableCell> |  | ||||||
|                                 <TableCell>{formatFullDateTime(row.fechaRegistro)}</TableCell> |  | ||||||
|                                     </TableRow> |                                     </TableRow> | ||||||
|                                 ))} |                                 ))} | ||||||
|                             </TableBody> |                             </TableBody> | ||||||
|                         </Table> |                         </Table> | ||||||
|                     </TableContainer> |                     </TableContainer> | ||||||
|                 </Box> |                 </Box> | ||||||
|  |             )} | ||||||
|  |         </Box> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
| @@ -1,29 +1,32 @@ | |||||||
| import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button } from '@mui/material'; | import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button, Typography } from '@mui/material'; | ||||||
| import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | ||||||
|  |  | ||||||
| // Importaciones de nuestro proyecto | // Importaciones de nuestro proyecto | ||||||
| import { useApiData } from '../../hooks/useApiData'; | import { useApiData } from '../../hooks/useApiData'; | ||||||
| import { useIsHoliday } from '../../hooks/useIsHoliday'; | import { useIsHoliday } from '../../hooks/useIsHoliday'; | ||||||
| import type { CotizacionGrano } from '../../models/mercadoModels'; | import type { CotizacionGrano } from '../../models/mercadoModels'; | ||||||
| import { formatInteger, formatDateOnly, formatFullDateTime } from '../../utils/formatters'; | import { formatInteger, formatFullDateTime } from '../../utils/formatters'; | ||||||
| import { copyToClipboard } from '../../utils/clipboardUtils'; | import { copyToClipboard } from '../../utils/clipboardUtils'; | ||||||
| import { HolidayAlert } from '../common/HolidayAlert'; | import { HolidayAlert } from '../common/HolidayAlert'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Función para convertir los datos de la tabla a formato CSV. |  * Función para convertir los datos a formato TSV (Tab-Separated Values) | ||||||
|  |  * con el formato específico solicitado por redacción. | ||||||
|  */ |  */ | ||||||
| const toCSV = (headers: string[], data: CotizacionGrano[]) => { | const toTSV = (data: CotizacionGrano[]) => { | ||||||
|     const headerRow = headers.join(';'); |     const dataRows = data.map(row => { | ||||||
|     const dataRows = data.map(row =>  |         // Formateamos la variación para que muestre "=" si es cero. | ||||||
|         [ |         const variacion = row.variacionPrecio === 0  | ||||||
|             row.nombre, |             ? '= 0'  | ||||||
|             formatInteger(row.precio), |             : formatInteger(row.variacionPrecio); | ||||||
|             formatInteger(row.variacionPrecio), |  | ||||||
|             formatDateOnly(row.fechaOperacion), |         const precio = formatInteger(row.precio); | ||||||
|             formatFullDateTime(row.fechaRegistro) |  | ||||||
|         ].join(';') |         // Unimos los campos con un carácter de tabulación '\t' | ||||||
|     ); |         return [row.nombre, precio, variacion].join('\t'); | ||||||
|     return [headerRow, ...dataRows].join('\n'); |     }); | ||||||
|  |      | ||||||
|  |     return dataRows.join('\n'); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -31,24 +34,21 @@ const toCSV = (headers: string[], data: CotizacionGrano[]) => { | |||||||
|  * diseñado para la página de redacción. |  * diseñado para la página de redacción. | ||||||
|  */ |  */ | ||||||
| export const RawGranosTable = () => { | export const RawGranosTable = () => { | ||||||
|     // Hooks para obtener los datos y el estado de feriado para el mercado argentino. |  | ||||||
|     const { data, loading: dataLoading, error: dataError } = useApiData<CotizacionGrano[]>('/mercados/granos'); |     const { data, loading: dataLoading, error: dataError } = useApiData<CotizacionGrano[]>('/mercados/granos'); | ||||||
|     const isHoliday = useIsHoliday('BA'); |     const isHoliday = useIsHoliday('BA'); | ||||||
|  |  | ||||||
|     const handleCopy = () => { |     const handleCopy = () => { | ||||||
|         if (!data) return; |         if (!data) return; | ||||||
|         const headers = ["Grano", "Precio ($/Tn)", "Variación", "Fecha Op.", "Fecha de Registro"]; |         const tsvData = toTSV(data); | ||||||
|         const csvData = toCSV(headers, data); |  | ||||||
|          |          | ||||||
|         copyToClipboard(csvData) |         copyToClipboard(tsvData) | ||||||
|             .then(() => alert('¡Tabla copiada al portapapeles!')) |             .then(() => alert('Datos de Granos copiados al portapapeles!')) | ||||||
|             .catch(err => { |             .catch(err => { | ||||||
|                 console.error('Error al copiar:', err); |                 console.error('Error al copiar:', err); | ||||||
|                 alert('Error: No se pudo copiar la tabla.'); |                 alert('Error: No se pudo copiar la tabla.'); | ||||||
|             }); |             }); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     // Estado de carga unificado. |  | ||||||
|     const isLoading = dataLoading || isHoliday === null; |     const isLoading = dataLoading || isHoliday === null; | ||||||
|  |  | ||||||
|     if (isLoading) return <CircularProgress />; |     if (isLoading) return <CircularProgress />; | ||||||
| @@ -63,16 +63,21 @@ export const RawGranosTable = () => { | |||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <Box> |         <Box> | ||||||
|             {/* Si es feriado, mostramos una alerta informativa encima de la tabla. */} |  | ||||||
|             {isHoliday && ( |             {isHoliday && ( | ||||||
|                 <Box sx={{ mb: 2 }}> |                 <Box sx={{ mb: 2 }}> | ||||||
|                     <HolidayAlert /> |                     <HolidayAlert /> | ||||||
|                 </Box> |                 </Box> | ||||||
|             )} |             )} | ||||||
|  |  | ||||||
|             <Button startIcon={<ContentCopyIcon />} onClick={handleCopy} sx={{ mb: 1 }}> |             <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}> | ||||||
|                 Copiar como CSV |                 <Button startIcon={<ContentCopyIcon />} onClick={handleCopy}> | ||||||
|  |                     Copiar Datos para Redacción | ||||||
|                 </Button> |                 </Button> | ||||||
|  |                 <Typography variant="caption" sx={{ fontStyle: 'italic', color: 'text.secondary' }}> | ||||||
|  |                     Última actualización: {formatFullDateTime(data[0].fechaRegistro)} | ||||||
|  |                 </Typography> | ||||||
|  |             </Box> | ||||||
|  |              | ||||||
|             <TableContainer component={Paper}> |             <TableContainer component={Paper}> | ||||||
|                 <Table size="small"> |                 <Table size="small"> | ||||||
|                     <TableHead> |                     <TableHead> | ||||||
| @@ -80,8 +85,6 @@ export const RawGranosTable = () => { | |||||||
|                             <TableCell>Grano</TableCell> |                             <TableCell>Grano</TableCell> | ||||||
|                             <TableCell align="right">Precio ($/Tn)</TableCell> |                             <TableCell align="right">Precio ($/Tn)</TableCell> | ||||||
|                             <TableCell align="right">Variación</TableCell> |                             <TableCell align="right">Variación</TableCell> | ||||||
|                             <TableCell>Fecha Op.</TableCell> |  | ||||||
|                             <TableCell>Última Act.</TableCell> |  | ||||||
|                         </TableRow> |                         </TableRow> | ||||||
|                     </TableHead> |                     </TableHead> | ||||||
|                     <TableBody> |                     <TableBody> | ||||||
| @@ -89,9 +92,7 @@ export const RawGranosTable = () => { | |||||||
|                             <TableRow key={row.id}> |                             <TableRow key={row.id}> | ||||||
|                                 <TableCell>{row.nombre}</TableCell> |                                 <TableCell>{row.nombre}</TableCell> | ||||||
|                                 <TableCell align="right">${formatInteger(row.precio)}</TableCell> |                                 <TableCell align="right">${formatInteger(row.precio)}</TableCell> | ||||||
|                                 <TableCell align="right">{formatInteger(row.variacionPrecio)}</TableCell> |                                 <TableCell align="right">{row.variacionPrecio === 0 ? '= 0' : formatInteger(row.variacionPrecio)}</TableCell> | ||||||
|                                 <TableCell>{formatDateOnly(row.fechaOperacion)}</TableCell> |  | ||||||
|                                 <TableCell>{formatFullDateTime(row.fechaRegistro)}</TableCell> |  | ||||||
|                             </TableRow> |                             </TableRow> | ||||||
|                         ))} |                         ))} | ||||||
|                     </TableBody> |                     </TableBody> | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								frontend/src/config/priorityTickers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/config/priorityTickers.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | export const TICKERS_PRIORITARIOS_LOCAL = [ | ||||||
|  |   '^MERV', 'GGAL.BA', 'YPFD.BA', 'PAMP.BA', 'BMA.BA',  | ||||||
|  |   'COME.BA', 'TECO2.BA', 'EDN.BA', 'CRES.BA', 'TXAR.BA',  | ||||||
|  |   'MIRG.BA', 'CEPU.BA', 'LOMA.BA', 'VALO.BA' | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | // Dejaremos las otras listas aquí para los siguientes componentes | ||||||
|  | export const TICKERS_PRIORITARIOS_USA = [ | ||||||
|  |   'AAPL', 'AMD', 'AMZN', 'BRK-B', 'KO', 'MSFT', 'NVDA', | ||||||
|  |   'GLD', 'XLF', 'XLI', 'XLE', 'XLK', 'MELI' | ||||||
|  | ]; | ||||||
		Reference in New Issue
	
	Block a user