diff --git a/frontend/src/components/BolsaUsaWidget.tsx b/frontend/src/components/BolsaUsaWidget.tsx index 38b1e36..140a15b 100644 --- a/frontend/src/components/BolsaUsaWidget.tsx +++ b/frontend/src/components/BolsaUsaWidget.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Typography, Dialog, DialogTitle, @@ -8,12 +8,19 @@ 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, formatCurrency } from '../utils/formatters'; -import type { CotizacionBolsa } from '../models/mercadoModels'; -import { useApiData } from '../hooks/useApiData'; -import { HistoricalChartWidget } from './HistoricalChartWidget'; import { PiChartLineUpBold } from 'react-icons/pi'; +// Importaciones de modelos, hooks y utilidades +import type { CotizacionBolsa } from '../models/mercadoModels'; +import { useApiData } from '../hooks/useApiData'; +import { useIsHoliday } from '../hooks/useIsHoliday'; +import { formatFullDateTime, formatCurrency } from '../utils/formatters'; +import { HistoricalChartWidget } from './HistoricalChartWidget'; +import { HolidayAlert } from './common/HolidayAlert'; + +/** + * Sub-componente para mostrar la variación porcentual con un icono y color apropiado. + */ 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; @@ -25,83 +32,135 @@ const Variacion = ({ value }: { value: number }) => { ); }; +/** + * Widget autónomo para la tabla de acciones de EEUU y ADRs Argentinos. + */ export const BolsaUsaWidget = () => { - const { data, loading, error } = useApiData('/mercados/bolsa/eeuu'); + // Hooks para obtener los datos y el estado de feriado para el mercado de EEUU. + const { data, loading: dataLoading, error: dataError } = useApiData('/mercados/bolsa/eeuu'); + const isHoliday = useIsHoliday('US'); // <-- Usamos el código de mercado 'US' + + // Estado y referencia para manejar el modal del gráfico. const [selectedTicker, setSelectedTicker] = useState(null); + const triggerButtonRef = useRef(null); - const handleRowClick = (ticker: string) => setSelectedTicker(ticker); - const handleCloseDialog = () => setSelectedTicker(null); - + const handleOpenModal = (ticker: string, event: React.MouseEvent) => { + triggerButtonRef.current = event.currentTarget; + setSelectedTicker(ticker); + }; + + const handleCloseDialog = () => { + setSelectedTicker(null); + // Devuelve el foco al botón que abrió el modal para mejorar la accesibilidad. + setTimeout(() => { + triggerButtonRef.current?.focus(); + }, 0); + }; + + // Filtramos para obtener solo las acciones, excluyendo el índice S&P 500. const otherStocks = data?.filter(d => d.ticker !== '^GSPC') || []; - if (loading) { + // Estado de carga unificado: el componente está "cargando" si los datos principales + // o la información del feriado todavía no han llegado. + const isLoading = dataLoading || isHoliday === null; + + if (isLoading) { return ; } - if (error) { - return {error}; + if (dataError) { + return {dataError}; } - - if (!data || data.length === 0) { - return No hay datos disponibles para el mercado de EEUU.; + + // Si después de filtrar no queda ninguna acción, mostramos el mensaje apropiado. + if (otherStocks.length === 0) { + // Si sabemos que es feriado, la alerta de feriado es el mensaje más relevante. + if (isHoliday) { + return ; + } + return No hay acciones de EEUU disponibles para mostrar.; } return ( - <> - {/* Renderizamos la tabla solo si hay otras acciones */} - {otherStocks.length > 0 && ( - - - - Última actualización de acciones: {formatFullDateTime(otherStocks[0].fechaRegistro)} - - - - - - Símbolo - Precio Actual - - Apertura - Cierre Anterior - - % Cambio - Historial - - - - {otherStocks.map((row) => ( - - {row.ticker} - {formatCurrency(row.precioActual, 'USD')} - - {formatCurrency(row.apertura, 'USD')} - {formatCurrency(row.cierreAnterior, 'USD')} - - - - handleRowClick(row.ticker)} - sx={{ boxShadow: '0 1px 3px rgba(0,0,0,0.1)', transition: 'all 0.2s ease-in-out', '&:hover': { transform: 'scale(1.1)', boxShadow: '0 2px 6px rgba(0,0,0,0.2)' } }} - > - - - ))} - -
-
+ + {/* Si es feriado, mostramos la alerta informativa en la parte superior. */} + {isHoliday && ( + + + )} - - theme.palette.grey[500], backgroundColor: 'white', boxShadow: 3, '&:hover': { backgroundColor: 'grey.100' }, }}> + + + + Última actualización de acciones: {formatFullDateTime(otherStocks[0].fechaRegistro)} + + + + + + Símbolo + Precio Actual + Apertura + Cierre Anterior + % Cambio + Historial + + + + {otherStocks.map((row) => ( + + {row.ticker} + {formatCurrency(row.precioActual, 'USD')} + {formatCurrency(row.apertura, 'USD')} + {formatCurrency(row.cierreAnterior, 'USD')} + + + handleOpenModal(row.ticker, event)} + sx={{ + boxShadow: '0 1px 3px rgba(0,0,0,0.1)', + transition: 'all 0.2s ease-in-out', + '&:hover': { transform: 'scale(1.1)', boxShadow: '0 2px 6px rgba(0,0,0,0.2)' } + }} + > + + + + + ))} + +
+
+ + + theme.palette.grey[500], + backgroundColor: 'white', boxShadow: 3, + '&:hover': { backgroundColor: 'grey.100' }, + }} + > - Historial de 30 días para: {selectedTicker} + + Historial de 30 días para: {selectedTicker} + {selectedTicker && } - +
); }; \ No newline at end of file diff --git a/frontend/src/components/GranosWidget.tsx b/frontend/src/components/GranosWidget.tsx index f163cf8..d52a599 100644 --- a/frontend/src/components/GranosWidget.tsx +++ b/frontend/src/components/GranosWidget.tsx @@ -2,19 +2,20 @@ import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Typography, Tooltip } from '@mui/material'; -import type { CotizacionGrano } 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'; -const formatNumber = (num: number) => { - return new Intl.NumberFormat('es-AR', { - minimumFractionDigits: 0, - maximumFractionDigits: 2, - }).format(num); -}; +// Importaciones de nuestro proyecto +import type { CotizacionGrano } from '../models/mercadoModels'; +import { useApiData } from '../hooks/useApiData'; +import { useIsHoliday } from '../hooks/useIsHoliday'; +import { formatInteger, formatDateOnly, formatFullDateTime } from '../utils/formatters'; +import { HolidayAlert } from './common/HolidayAlert'; +/** + * Sub-componente para mostrar la variación con icono y color. + */ 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; @@ -23,58 +24,79 @@ const Variacion = ({ value }: { value: number }) => { - {formatNumber(value)} + {formatInteger(value)} ); }; +/** + * Widget autónomo para la tabla detallada del mercado de granos. + */ export const GranosWidget = () => { - const { data, loading, error } = useApiData('/mercados/granos'); + // Hooks para obtener los datos y el estado de feriado para el mercado argentino. + const { data, loading: dataLoading, error: dataError } = useApiData('/mercados/granos'); + const isHoliday = useIsHoliday('BA'); - if (loading) { + // Estado de carga unificado. + const isLoading = dataLoading || isHoliday === null; + + if (isLoading) { return ; } - if (error) { - return {error}; + if (dataError) { + return {dataError}; } + // Si no hay ningún dato que mostrar. if (!data || data.length === 0) { + if (isHoliday) { + return ; + } return No hay datos de granos disponibles en este momento.; } return ( - - - - - Grano - Precio ($/Tn) - Variación - Fecha Operación - - - - {data.map((row) => ( - - - {row.nombre} - - ${formatNumber(row.precio)} - - - - {new Date(row.fechaOperacion).toLocaleDateString('es-AR')} + + {/* Si es feriado, mostramos la alerta como un aviso encima del contenido. */} + {isHoliday && ( + + + + )} + + +
+ + + Grano + Precio ($/Tn) + Variación + Fecha Operación - ))} - -
- - - Fuente: Bolsa de Comercio de Rosario - - -
+ + + {data.map((row) => ( + + + {row.nombre} + + ${formatInteger(row.precio)} + + + + {formatDateOnly(row.fechaOperacion)} + + ))} + + + + + Fuente: Bolsa de Comercio de Rosario + + + + ); }; \ No newline at end of file diff --git a/frontend/src/components/MercadoAgroCardWidget.tsx b/frontend/src/components/MercadoAgroCardWidget.tsx index 3455e7c..6bc329a 100644 --- a/frontend/src/components/MercadoAgroCardWidget.tsx +++ b/frontend/src/components/MercadoAgroCardWidget.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { Box, CircularProgress, Alert, Paper, Typography, Dialog, DialogTitle, DialogContent, IconButton @@ -8,61 +8,63 @@ import ScaleIcon from '@mui/icons-material/Scale'; import { PiChartLineUpBold } from "react-icons/pi"; import CloseIcon from '@mui/icons-material/Close'; +// Importaciones de nuestro proyecto import type { CotizacionGanado } from '../models/mercadoModels'; import { useApiData } from '../hooks/useApiData'; +import { useIsHoliday } from '../hooks/useIsHoliday'; import { formatCurrency, formatInteger, formatDateOnly } from '../utils/formatters'; import { AgroHistoricalChartWidget } from './AgroHistoricalChartWidget'; +import { HolidayAlert } from './common/HolidayAlert'; -// El subcomponente ahora tendrá un botón para el gráfico. -const AgroCard = ({ registro, onChartClick }: { registro: CotizacionGanado, onChartClick: () => void }) => { +/** + * Sub-componente para una única tarjeta de categoría de ganado. + */ +const AgroCard = ({ registro, onChartClick }: { registro: CotizacionGanado, onChartClick: (event: React.MouseEvent) => void }) => { return ( - // Añadimos posición relativa para poder posicionar el botón del gráfico. - - { - e.stopPropagation(); - onChartClick(); - }} - sx={{ - position: 'absolute', - top: 8, - right: 8, - backgroundColor: 'rgba(255, 255, 255, 0.7)', // Fondo semitransparente - backdropFilter: 'blur(2px)', // Efecto "frosty glass" para el fondo - border: '1px solid rgba(0, 0, 0, 0.1)', - boxShadow: '0 2px 5px rgba(0,0,0,0.1)', - transition: 'all 0.2s ease-in-out', // Transición suave para todos los cambios - '&:hover': { - transform: 'translateY(-2px)', // Se eleva un poco - boxShadow: '0 4px 10px rgba(0,0,0,0.2)', // La sombra se hace más grande - backgroundColor: 'rgba(255, 255, 255, 0.9)', - } - }} - > - - + + {/* Contenido principal de la tarjeta */} + + + + - - {registro.categoria} - {registro.especificaciones} - + + {registro.categoria} + {registro.especificaciones} + - - Precio Máximo: - ${formatCurrency(registro.maximo)} - - - Precio Mínimo: - ${formatCurrency(registro.minimo)} - - - Precio Mediano: - ${formatCurrency(registro.mediano)} + + Precio Máximo: + ${formatCurrency(registro.maximo)} + + + Precio Mínimo: + ${formatCurrency(registro.minimo)} + + + Precio Mediano: + ${formatCurrency(registro.mediano)} + {/* Pie de la tarjeta */} - + @@ -77,69 +79,88 @@ const AgroCard = ({ registro, onChartClick }: { registro: CotizacionGanado, onCh - Dato Registrado el {formatDateOnly(registro.fechaRegistro)} + {formatDateOnly(registro.fechaRegistro)} ); }; +/** + * Widget autónomo para las tarjetas de resumen del Mercado Agroganadero. + */ export const MercadoAgroCardWidget = () => { - const { data, loading, error } = useApiData('/mercados/agroganadero'); - const [selectedCategory, setSelectedCategory] = useState(null); + const { data, loading: dataLoading, error: dataError } = useApiData('/mercados/agroganadero'); + const isHoliday = useIsHoliday('BA'); - const handleChartClick = (registro: CotizacionGanado) => { + const [selectedCategory, setSelectedCategory] = useState(null); + const triggerButtonRef = useRef(null); + + const handleChartClick = (registro: CotizacionGanado, event: React.MouseEvent) => { + triggerButtonRef.current = event.currentTarget; setSelectedCategory(registro); }; const handleCloseDialog = () => { setSelectedCategory(null); + setTimeout(() => { + triggerButtonRef.current?.focus(); + }, 0); }; - if (loading) { + const isLoading = dataLoading || isHoliday === null; + + if (isLoading) { return ; } - if (error) { - return {error}; + + if (dataError) { + return {dataError}; } + if (!data || data.length === 0) { + if (isHoliday) { + return ; + } return No hay datos del mercado agroganadero disponibles.; } return ( <> - + {isHoliday && ( + + + + )} + + {data.map(registro => ( - handleChartClick(registro)} /> + handleChartClick(registro, event)} /> ))} + theme.palette.grey[500], - backgroundColor: 'white', - boxShadow: 3, // Añade una sombra para que destaque - '&:hover': { - backgroundColor: 'grey.100', // Un leve cambio de color al pasar el mouse - }, + backgroundColor: 'white', boxShadow: 3, + '&:hover': { backgroundColor: 'grey.100' }, }} > - Mensual de {selectedCategory?.categoria} ({selectedCategory?.especificaciones}) + Historial de 30 días para {selectedCategory?.categoria} ({selectedCategory?.especificaciones}) {selectedCategory && ( diff --git a/frontend/src/components/MercadoAgroWidget.tsx b/frontend/src/components/MercadoAgroWidget.tsx index 4770da3..76d4ca6 100644 --- a/frontend/src/components/MercadoAgroWidget.tsx +++ b/frontend/src/components/MercadoAgroWidget.tsx @@ -2,12 +2,17 @@ import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Typography, Tooltip } from '@mui/material'; + +// Importaciones de nuestro proyecto import type { CotizacionGanado } from '../models/mercadoModels'; import { useApiData } from '../hooks/useApiData'; -import { formatCurrency, formatInteger, formatFullDateTime } from '../utils/formatters'; +import { useIsHoliday } from '../hooks/useIsHoliday'; +import { formatCurrency, formatInteger, formatDateOnly } from '../utils/formatters'; +import { HolidayAlert } from './common/HolidayAlert'; -// El sub-componente ahora solo necesita renderizar la tarjeta de móvil. -// La fila de la tabla la haremos directamente en el componente principal. +/** + * Sub-componente para renderizar cada registro como una tarjeta en la vista móvil. + */ const AgroDataCard = ({ row }: { row: CotizacionGanado }) => { const commonStyles = { cell: { @@ -56,16 +61,41 @@ const AgroDataCard = ({ row }: { row: CotizacionGanado }) => { ); }; - +/** + * Widget autónomo para la tabla/lista responsiva del Mercado Agroganadero. + */ export const MercadoAgroWidget = () => { - const { data, loading, error } = useApiData('/mercados/agroganadero'); + // Hooks para obtener los datos y el estado de feriado. + const { data, loading: dataLoading, error: dataError } = useApiData('/mercados/agroganadero'); + const isHoliday = useIsHoliday('BA'); - if (loading) { return ; } - if (error) { return {error}; } - if (!data || data.length === 0) { return No hay datos del mercado agroganadero disponibles.; } + // Estado de carga unificado. + const isLoading = dataLoading || isHoliday === null; + + if (isLoading) { + return ; + } + + if (dataError) { + return {dataError}; + } + + if (!data || data.length === 0) { + if (isHoliday) { + return ; + } + return No hay datos del mercado agroganadero disponibles.; + } return ( - + + {/* Si es feriado, mostramos la alerta como un aviso encima del contenido. */} + {isHoliday && ( + + + + )} + {/* VISTA DE ESCRITORIO (se oculta en móvil) */} @@ -105,9 +135,10 @@ export const MercadoAgroWidget = () => { ))} - + {/* La información de la fuente se muestra siempre, usando la fecha del primer registro */} + - Fuente: Mercado Agroganadero S.A. + {formatDateOnly(data[0].fechaRegistro)} - Fuente: Mercado Agroganadero S.A. diff --git a/frontend/src/components/MervalHeroCard.tsx b/frontend/src/components/MervalHeroCard.tsx index be3fb0d..2e891c8 100644 --- a/frontend/src/components/MervalHeroCard.tsx +++ b/frontend/src/components/MervalHeroCard.tsx @@ -90,7 +90,7 @@ export const MervalHeroCard = () => { )} {/* El contenido principal del widget siempre se muestra si hay datos. */} - + Índice S&P MERVAL diff --git a/frontend/src/components/UsaIndexHeroCard.tsx b/frontend/src/components/UsaIndexHeroCard.tsx index 3904efe..9c5405b 100644 --- a/frontend/src/components/UsaIndexHeroCard.tsx +++ b/frontend/src/components/UsaIndexHeroCard.tsx @@ -1,13 +1,20 @@ -import { useState } from 'react'; +import React, { useState } from 'react'; import { Box, Paper, Typography, ToggleButton, ToggleButtonGroup, CircularProgress, Alert } from '@mui/material'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import RemoveIcon from '@mui/icons-material/Remove'; + +// Importaciones de nuestro proyecto import type { CotizacionBolsa } from '../models/mercadoModels'; import { useApiData } from '../hooks/useApiData'; +import { useIsHoliday } from '../hooks/useIsHoliday'; import { formatInteger } from '../utils/formatters'; import { HistoricalChartWidget } from './HistoricalChartWidget'; +import { HolidayAlert } from './common/HolidayAlert'; +/** + * Sub-componente interno para mostrar la variación del índice. + */ const VariacionIndex = ({ actual, anterior }: { actual: number, anterior: number }) => { if (anterior === 0) return null; const variacionPuntos = actual - anterior; @@ -33,41 +40,74 @@ const VariacionIndex = ({ actual, anterior }: { actual: number, anterior: number ); }; +/** + * Widget autónomo para la tarjeta de héroe del S&P 500. + */ export const UsaIndexHeroCard = () => { - const { data: allUsaData, loading, error } = useApiData('/mercados/bolsa/eeuu'); + // Hooks para obtener los datos y el estado de feriado para el mercado de EEUU. + const { data: allUsaData, loading: dataLoading, error: dataError } = useApiData('/mercados/bolsa/eeuu'); + const isHoliday = useIsHoliday('US'); + + // Estado interno para el gráfico. const [dias, setDias] = useState(30); const handleRangoChange = ( _event: React.MouseEvent, nuevoRango: number | null ) => { if (nuevoRango !== null) { setDias(nuevoRango); } }; + // Filtramos el dato específico que este widget necesita. const indexData = allUsaData?.find(d => d.ticker === '^GSPC'); - if (loading) { return ; } - if (error) { return {error}; } - if (!indexData) { return No se encontraron datos para el índice S&P 500.; } + // Estado de carga unificado: esperamos a que AMBAS llamadas a la API terminen. + const isLoading = dataLoading || isHoliday === null; + if (isLoading) { + return ; + } + + if (dataError) { + return {dataError}; + } + + // Si no hay datos del S&P 500, mostramos el mensaje apropiado. + if (!indexData) { + if (isHoliday) { + return ; + } + return No se encontraron datos para el índice S&P 500.; + } + + // Si hay datos, renderizamos el contenido completo. return ( - - - - S&P 500 Index - {formatInteger(indexData.precioActual)} + + {/* Si es feriado, mostramos la alerta como un aviso encima del contenido. */} + {isHoliday && ( + + - - + )} + + + + + S&P 500 Index + {formatInteger(indexData.precioActual)} + + + + - - - - - Semanal - Mensual - Anual - + + + + Semanal + Mensual + Anual + + + - - - + + ); }; \ No newline at end of file diff --git a/frontend/src/components/common/HolidayAlert.tsx b/frontend/src/components/common/HolidayAlert.tsx index 3730fa6..63381f3 100644 --- a/frontend/src/components/common/HolidayAlert.tsx +++ b/frontend/src/components/common/HolidayAlert.tsx @@ -1,10 +1,16 @@ import { Alert } from '@mui/material'; import CelebrationIcon from '@mui/icons-material/Celebration'; +import { formatDateOnly } from '../../utils/formatters'; export const HolidayAlert = () => { + // Obtener la fecha actual en la zona horaria de Buenos Aires + const nowInBuenosAires = new Date( + new Date().toLocaleString('en-US', { timeZone: 'America/Argentina/Buenos_Aires' }) + ); + return ( }> - Mercado cerrado por feriado. + {formatDateOnly(nowInBuenosAires.toISOString())} Mercado cerrado por feriado. ); }; \ No newline at end of file diff --git a/frontend/src/components/raw-data/RawAgroTable.tsx b/frontend/src/components/raw-data/RawAgroTable.tsx index 7cfd63d..fd89764 100644 --- a/frontend/src/components/raw-data/RawAgroTable.tsx +++ b/frontend/src/components/raw-data/RawAgroTable.tsx @@ -1,11 +1,17 @@ import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button } from '@mui/material'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; + +// Importaciones de nuestro proyecto import { useApiData } from '../../hooks/useApiData'; +import { useIsHoliday } from '../../hooks/useIsHoliday'; import type { CotizacionGanado } from '../../models/mercadoModels'; import { formatInteger, formatCurrency, formatFullDateTime } from '../../utils/formatters'; import { copyToClipboard } from '../../utils/clipboardUtils'; +import { HolidayAlert } from '../common/HolidayAlert'; -// Función para convertir datos a formato CSV +/** + * Función para convertir los datos de la tabla a un formato CSV para el portapapeles. + */ const toCSV = (headers: string[], data: CotizacionGanado[]) => { const headerRow = headers.join(';'); const dataRows = data.map(row => @@ -17,18 +23,25 @@ const toCSV = (headers: string[], data: CotizacionGanado[]) => { formatCurrency(row.mediano), formatInteger(row.cabezas), formatInteger(row.kilosTotales), - formatInteger(row.importeTotal) + formatInteger(row.importeTotal), + formatFullDateTime(row.fechaRegistro) ].join(';') ); return [headerRow, ...dataRows].join('\n'); }; +/** + * Componente de tabla de datos crudos para el Mercado Agroganadero, + * diseñado para la página de redacción. + */ export const RawAgroTable = () => { - const { data, loading, error } = useApiData('/mercados/agroganadero'); + // Hooks para obtener los datos y el estado de feriado. + const { data, loading: dataLoading, error: dataError } = useApiData('/mercados/agroganadero'); + const isHoliday = useIsHoliday('BA'); const handleCopy = () => { if (!data) return; - const headers = ["Categoría", "Especificaciones", "Máximo", "Mínimo", "Mediano", "Cabezas", "Kg Total", "Importe Total"]; + const headers = ["Categoría", "Especificaciones", "Máximo", "Mínimo", "Mediano", "Cabezas", "Kg Total", "Importe Total", "Fecha de Registro"]; const csvData = toCSV(headers, data); copyToClipboard(csvData) @@ -39,12 +52,28 @@ export const RawAgroTable = () => { }); }; - if (loading) return ; - if (error) return {error}; - if (!data) return null; + // Estado de carga unificado. + const isLoading = dataLoading || isHoliday === null; + + if (isLoading) return ; + if (dataError) return {dataError}; + + if (!data || data.length === 0) { + if (isHoliday) { + return ; + } + return No hay datos del mercado agroganadero disponibles.; + } return ( + {/* Si es feriado, mostramos una alerta informativa encima de la tabla. */} + {isHoliday && ( + + + + )} + diff --git a/frontend/src/components/raw-data/RawBolsaLocalTable.tsx b/frontend/src/components/raw-data/RawBolsaLocalTable.tsx index 8d4ec32..cf1b65c 100644 --- a/frontend/src/components/raw-data/RawBolsaLocalTable.tsx +++ b/frontend/src/components/raw-data/RawBolsaLocalTable.tsx @@ -1,11 +1,17 @@ import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button } from '@mui/material'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; + +// Importaciones de nuestro proyecto import { useApiData } from '../../hooks/useApiData'; +import { useIsHoliday } from '../../hooks/useIsHoliday'; import type { CotizacionBolsa } from '../../models/mercadoModels'; import { formatCurrency, formatFullDateTime } from '../../utils/formatters'; import { copyToClipboard } from '../../utils/clipboardUtils'; +import { HolidayAlert } from '../common/HolidayAlert'; -// Función para convertir datos a formato CSV +/** + * Función para convertir los datos de la tabla a formato CSV. + */ const toCSV = (headers: string[], data: CotizacionBolsa[]) => { const headerRow = headers.join(';'); const dataRows = data.map(row => @@ -14,36 +20,57 @@ const toCSV = (headers: string[], data: CotizacionBolsa[]) => { row.nombreEmpresa, formatCurrency(row.precioActual), formatCurrency(row.cierreAnterior), - `${row.porcentajeCambio.toFixed(2)}%` + `${row.porcentajeCambio.toFixed(2)}%`, + formatFullDateTime(row.fechaRegistro) ].join(';') ); return [headerRow, ...dataRows].join('\n'); }; +/** + * Componente de tabla de datos crudos para la Bolsa Local (MERVAL y acciones), + * diseñado para la página de redacción. + */ export const RawBolsaLocalTable = () => { - const { data, loading, error } = useApiData('/mercados/bolsa/local'); + // Hooks para obtener los datos y el estado de feriado. + const { data, loading: dataLoading, error: dataError } = useApiData('/mercados/bolsa/local'); + const isHoliday = useIsHoliday('BA'); const handleCopy = () => { if (!data) return; - const headers = ["Ticker", "Nombre", "Último Precio", "Cierre Anterior", "Variación %"]; + const headers = ["Ticker", "Nombre", "Último Precio", "Cierre Anterior", "Variación %", "Fecha de Registro"]; const csvData = toCSV(headers, data); copyToClipboard(csvData) - .then(() => { - alert('¡Tabla copiada al portapapeles!'); - }) + .then(() => alert('¡Tabla copiada al portapapeles!')) .catch(err => { console.error('Error al copiar:', err); alert('Error: No se pudo copiar la tabla.'); }); }; - if (loading) return ; - if (error) return {error}; - if (!data) return null; + // Estado de carga unificado. + const isLoading = dataLoading || isHoliday === null; + + if (isLoading) return ; + if (dataError) return {dataError}; + + if (!data || data.length === 0) { + if (isHoliday) { + return ; + } + return No hay datos disponibles para el mercado local.; + } return ( + {/* Si es feriado, mostramos una alerta informativa encima de la tabla. */} + {isHoliday && ( + + + + )} + diff --git a/frontend/src/components/raw-data/RawBolsaUsaTable.tsx b/frontend/src/components/raw-data/RawBolsaUsaTable.tsx index 43d828c..9be22a6 100644 --- a/frontend/src/components/raw-data/RawBolsaUsaTable.tsx +++ b/frontend/src/components/raw-data/RawBolsaUsaTable.tsx @@ -1,11 +1,17 @@ import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button } from '@mui/material'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; + +// Importaciones de nuestro proyecto import { useApiData } from '../../hooks/useApiData'; +import { useIsHoliday } from '../../hooks/useIsHoliday'; import type { CotizacionBolsa } from '../../models/mercadoModels'; import { formatCurrency, formatFullDateTime } from '../../utils/formatters'; import { copyToClipboard } from '../../utils/clipboardUtils'; +import { HolidayAlert } from '../common/HolidayAlert'; -// Función para convertir datos a formato CSV +/** + * Función para convertir los datos de la tabla a formato CSV. + */ const toCSV = (headers: string[], data: CotizacionBolsa[]) => { const headerRow = headers.join(';'); const dataRows = data.map(row => @@ -14,18 +20,25 @@ const toCSV = (headers: string[], data: CotizacionBolsa[]) => { row.nombreEmpresa, formatCurrency(row.precioActual, 'USD'), formatCurrency(row.cierreAnterior, 'USD'), - `${row.porcentajeCambio.toFixed(2)}%` + `${row.porcentajeCambio.toFixed(2)}%`, + formatFullDateTime(row.fechaRegistro) ].join(';') ); return [headerRow, ...dataRows].join('\n'); }; +/** + * Componente de tabla de datos crudos para la Bolsa de EEUU y ADRs, + * diseñado para la página de redacción. + */ export const RawBolsaUsaTable = () => { - const { data, loading, error } = useApiData('/mercados/bolsa/eeuu'); + // Hooks para obtener los datos y el estado de feriado para el mercado de EEUU. + const { data, loading: dataLoading, error: dataError } = useApiData('/mercados/bolsa/eeuu'); + const isHoliday = useIsHoliday('US'); const handleCopy = () => { if (!data) return; - const headers = ["Ticker", "Nombre", "Último Precio (USD)", "Cierre Anterior (USD)", "Variación %"]; + const headers = ["Ticker", "Nombre", "Último Precio (USD)", "Cierre Anterior (USD)", "Variación %", "Fecha de Registro"]; const csvData = toCSV(headers, data); copyToClipboard(csvData) @@ -36,12 +49,28 @@ export const RawBolsaUsaTable = () => { }); }; - if (loading) return ; - if (error) return {error}; - if (!data || data.length === 0) return No hay datos disponibles (el fetcher puede estar desactivado).; + // Estado de carga unificado. + const isLoading = dataLoading || isHoliday === null; + + if (isLoading) return ; + if (dataError) return {dataError}; + + if (!data || data.length === 0) { + if (isHoliday) { + return ; + } + return No hay datos disponibles para el mercado de EEUU (el fetcher puede estar desactivado).; + } return ( + {/* Si es feriado, mostramos una alerta informativa encima de la tabla. */} + {isHoliday && ( + + + + )} + diff --git a/frontend/src/components/raw-data/RawGranosTable.tsx b/frontend/src/components/raw-data/RawGranosTable.tsx index 84abc67..da17bce 100644 --- a/frontend/src/components/raw-data/RawGranosTable.tsx +++ b/frontend/src/components/raw-data/RawGranosTable.tsx @@ -1,11 +1,17 @@ import { Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button } from '@mui/material'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; + +// Importaciones de nuestro proyecto import { useApiData } from '../../hooks/useApiData'; +import { useIsHoliday } from '../../hooks/useIsHoliday'; import type { CotizacionGrano } from '../../models/mercadoModels'; import { formatInteger, formatDateOnly, formatFullDateTime } from '../../utils/formatters'; import { copyToClipboard } from '../../utils/clipboardUtils'; +import { HolidayAlert } from '../common/HolidayAlert'; -// Función para convertir datos a formato CSV +/** + * Función para convertir los datos de la tabla a formato CSV. + */ const toCSV = (headers: string[], data: CotizacionGrano[]) => { const headerRow = headers.join(';'); const dataRows = data.map(row => @@ -13,18 +19,25 @@ const toCSV = (headers: string[], data: CotizacionGrano[]) => { row.nombre, formatInteger(row.precio), formatInteger(row.variacionPrecio), - formatDateOnly(row.fechaOperacion) + formatDateOnly(row.fechaOperacion), + formatFullDateTime(row.fechaRegistro) ].join(';') ); return [headerRow, ...dataRows].join('\n'); }; +/** + * Componente de tabla de datos crudos para el mercado de Granos, + * diseñado para la página de redacción. + */ export const RawGranosTable = () => { - const { data, loading, error } = useApiData('/mercados/granos'); + // Hooks para obtener los datos y el estado de feriado para el mercado argentino. + const { data, loading: dataLoading, error: dataError } = useApiData('/mercados/granos'); + const isHoliday = useIsHoliday('BA'); const handleCopy = () => { if (!data) return; - const headers = ["Grano", "Precio ($/Tn)", "Variación", "Fecha Op."]; + const headers = ["Grano", "Precio ($/Tn)", "Variación", "Fecha Op.", "Fecha de Registro"]; const csvData = toCSV(headers, data); copyToClipboard(csvData) @@ -35,12 +48,28 @@ export const RawGranosTable = () => { }); }; - if (loading) return ; - if (error) return {error}; - if (!data) return null; + // Estado de carga unificado. + const isLoading = dataLoading || isHoliday === null; + + if (isLoading) return ; + if (dataError) return {dataError}; + + if (!data || data.length === 0) { + if (isHoliday) { + return ; + } + return No hay datos de granos disponibles.; + } return ( + {/* Si es feriado, mostramos una alerta informativa encima de la tabla. */} + {isHoliday && ( + + + + )} + diff --git a/frontend/src/utils/formatters.ts b/frontend/src/utils/formatters.ts index 0dbeaa8..e4ce2f3 100644 --- a/frontend/src/utils/formatters.ts +++ b/frontend/src/utils/formatters.ts @@ -11,12 +11,9 @@ export const formatCurrency = (num: number, currency = 'ARS') => { }).format(num); }; -export const formatCurrency2Decimal = (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, +export const formatCurrency2Decimal = (num: number, currency = 'ARS') => { + return new Intl.NumberFormat('es-AR', { + style: 'decimal', currency: currency, minimumFractionDigits: 2, maximumFractionDigits: 2,