Feat: RawData Table Format

This commit is contained in:
2025-07-17 11:52:00 -03:00
parent 8878ec632e
commit 23f0d02fe3
6 changed files with 227 additions and 181 deletions

4
.gitignore vendored
View File

@@ -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/

View File

@@ -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>

View File

@@ -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>
); );
}; };

View File

@@ -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>
); );
}; };

View File

@@ -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>

View 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'
];