Feat(holidays): Implement database-backed holiday detection system
- Adds a new `MercadosFeriados` table to the database to persist market holidays.
- Implements `HolidayDataFetcher` to update holidays weekly from Finnhub API.
- Implements `IHolidayService` with in-memory caching to check for holidays efficiently.
- Worker service now skips fetcher execution on market holidays.
- Adds a new API endpoint `/api/mercados/es-feriado/{mercado}`.
- Integrates a non-blocking holiday alert into the `BolsaLocalWidget`."
			
			
This commit is contained in:
		| @@ -1,4 +1,5 @@ | ||||
| import { useState } from 'react'; | ||||
| // Importaciones de React y Material-UI | ||||
| import React, { useState, useRef } from 'react'; | ||||
| import { | ||||
|   Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer, | ||||
|   TableHead, TableRow, Paper, Typography, Dialog, DialogTitle, | ||||
| @@ -8,12 +9,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 nuestros 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,91 +33,155 @@ const Variacion = ({ value }: { value: number }) => { | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Sub-componente que renderiza la tabla de acciones detalladas. | ||||
|  * Se extrae para mantener el componente principal más limpio. | ||||
|  */ | ||||
| const RenderContent = ({ data, handleOpenModal }: {  | ||||
|     data: CotizacionBolsa[],  | ||||
|     handleOpenModal: (ticker: string, event: React.MouseEvent<HTMLButtonElement>) => void,  | ||||
| }) => { | ||||
|   // Filtramos para obtener solo las acciones, excluyendo el índice. | ||||
|   const panelPrincipal = data.filter(d => d.ticker !== '^MERV'); | ||||
|    | ||||
|   if (panelPrincipal.length === 0) { | ||||
|       return <Alert severity="info">No hay acciones líderes para mostrar en este momento.</Alert>; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <TableContainer component={Paper}> | ||||
|       <Box sx={{ p: 1, m: 0 }}> | ||||
|         <Typography variant="caption" sx={{ fontStyle: 'italic', color: 'text.secondary' }}> | ||||
|           Última actualización de acciones: {formatFullDateTime(panelPrincipal[0].fechaRegistro)} | ||||
|         </Typography> | ||||
|       </Box> | ||||
|       <Table size="small" aria-label="panel principal merval"> | ||||
|         <TableHead> | ||||
|           <TableRow> | ||||
|             <TableCell>Símbolo</TableCell> | ||||
|             <TableCell align="right">Precio Actual</TableCell> | ||||
|             <TableCell align="right" sx={{ display: { xs: 'none', md: 'table-cell' } }}>Apertura</TableCell> | ||||
|             <TableCell align="right" sx={{ display: { xs: 'none', sm: 'table-cell' } }}>Cierre Anterior</TableCell> | ||||
|             <TableCell align="center">% Cambio</TableCell> | ||||
|             <TableCell align="center">Historial</TableCell> | ||||
|           </TableRow> | ||||
|         </TableHead> | ||||
|         <TableBody> | ||||
|           {panelPrincipal.map((row) => ( | ||||
|             <TableRow key={row.ticker} hover> | ||||
|               <TableCell component="th" scope="row"><Typography variant="body2" sx={{ fontWeight: 'bold' }}>{row.ticker}</Typography></TableCell> | ||||
|               <TableCell align="right">${formatCurrency(row.precioActual)}</TableCell> | ||||
|               <TableCell align="right" sx={{ display: { xs: 'none', md: 'table-cell' } }}>${formatCurrency(row.apertura)}</TableCell> | ||||
|               <TableCell align="right" sx={{ display: { xs: 'none', sm: 'table-cell' } }}>${formatCurrency(row.cierreAnterior)}</TableCell> | ||||
|               <TableCell align="center"><Variacion value={row.porcentajeCambio} /></TableCell> | ||||
|               <TableCell align="center"> | ||||
|                 <IconButton | ||||
|                   aria-label={`ver historial de ${row.ticker}`} | ||||
|                   size="small" | ||||
|                   onClick={(event) => 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)' } | ||||
|                   }} | ||||
|                 > | ||||
|                   <PiChartLineUpBold size="18" /> | ||||
|                 </IconButton> | ||||
|               </TableCell> | ||||
|             </TableRow> | ||||
|           ))} | ||||
|         </TableBody> | ||||
|       </Table> | ||||
|     </TableContainer> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Widget principal para la sección "Bolsa Local". | ||||
|  * Muestra una tarjeta de héroe para el MERVAL y una tabla detallada para las acciones líderes. | ||||
|  */ | ||||
| export const BolsaLocalWidget = () => { | ||||
|   const { data, loading, error } = useApiData<CotizacionBolsa[]>('/mercados/bolsa/local'); | ||||
|   // Hooks para obtener los datos y el estado de feriado. Las llamadas se disparan en paralelo. | ||||
|   const { data, loading: dataLoading, error: dataError } = useApiData<CotizacionBolsa[]>('/mercados/bolsa/local'); | ||||
|   const isHoliday = useIsHoliday('BA'); | ||||
|    | ||||
|   // Estado y referencia para manejar el modal del gráfico. | ||||
|   const [selectedTicker, setSelectedTicker] = useState<string | null>(null); | ||||
|   const triggerButtonRef = useRef<HTMLButtonElement | null>(null); | ||||
|  | ||||
|   const handleRowClick = (ticker: string) => setSelectedTicker(ticker); | ||||
|   const handleCloseDialog = () => setSelectedTicker(null); | ||||
|   const handleOpenModal = (ticker: string, event: React.MouseEvent<HTMLButtonElement>) => { | ||||
|     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); | ||||
|   }; | ||||
|    | ||||
|   // 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; | ||||
|  | ||||
|   const panelPrincipal = data?.filter(d => d.ticker !== '^MERV') || []; | ||||
|  | ||||
|   if (loading) { | ||||
|   if (isLoading) { | ||||
|     return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>; | ||||
|   } | ||||
|  | ||||
|   if (error) { | ||||
|     return <Alert severity="error">{error}</Alert>; | ||||
|   if (dataError) { | ||||
|     return <Alert severity="error">{dataError}</Alert>; | ||||
|   } | ||||
|  | ||||
|    | ||||
|   // Si no hay ningún dato en absoluto, mostramos un mensaje final. | ||||
|   if (!data || data.length === 0) { | ||||
|     return <Alert severity="info">No hay datos disponibles para el mercado local.</Alert>; | ||||
|       // Si sabemos que es feriado, la alerta de feriado tiene prioridad. | ||||
|       if (isHoliday) { | ||||
|           return <HolidayAlert />; | ||||
|       } | ||||
|       return <Alert severity="info">No hay datos disponibles para el mercado local.</Alert>; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|  | ||||
|       {panelPrincipal.length > 0 && ( | ||||
|         <TableContainer component={Paper}> | ||||
|           <Box sx={{ p: 1, m: 0 }}> | ||||
|             <Typography variant="caption" sx={{ fontStyle: 'italic', color: 'text.secondary' }}> | ||||
|               Última actualización de acciones: {formatFullDateTime(panelPrincipal[0].fechaRegistro)} | ||||
|             </Typography> | ||||
|           </Box> | ||||
|           <Table size="small" aria-label="panel principal merval"> | ||||
|             <TableHead> | ||||
|               <TableRow> | ||||
|                 <TableCell>Símbolo</TableCell> | ||||
|                 <TableCell align="right">Precio Actual</TableCell> | ||||
|                 <TableCell align="right" sx={{ display: { xs: 'none', md: 'table-cell' } }}>Apertura</TableCell> | ||||
|                 <TableCell align="right" sx={{ display: { xs: 'none', sm: 'table-cell' } }}>Cierre Anterior</TableCell> | ||||
|                 <TableCell align="center">% Cambio</TableCell> | ||||
|                 <TableCell align="center">Historial</TableCell> | ||||
|               </TableRow> | ||||
|             </TableHead> | ||||
|             <TableBody> | ||||
|               {panelPrincipal.map((row) => ( | ||||
|                 <TableRow key={row.ticker} hover> | ||||
|                   <TableCell component="th" scope="row"><Typography variant="body2" sx={{ fontWeight: 'bold' }}>{row.ticker}</Typography></TableCell> | ||||
|                   <TableCell align="right">${formatCurrency(row.precioActual)}</TableCell> | ||||
|                   <TableCell align="right" sx={{ display: { xs: 'none', md: 'table-cell' } }}>${formatCurrency(row.apertura)}</TableCell> | ||||
|                   <TableCell align="right" sx={{ display: { xs: 'none', sm: 'table-cell' } }}>${formatCurrency(row.cierreAnterior)}</TableCell> | ||||
|                   <TableCell align="center"><Variacion value={row.porcentajeCambio} /></TableCell> | ||||
|                   <TableCell align="center"> | ||||
|                     <IconButton | ||||
|                       aria-label={`ver historial de ${row.ticker}`} | ||||
|                       size="small" | ||||
|                       onClick={() => 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)' } | ||||
|                       }} | ||||
|                     > | ||||
|                       <PiChartLineUpBold size="18" /> | ||||
|                     </IconButton> | ||||
|                   </TableCell> | ||||
|                 </TableRow> | ||||
|               ))} | ||||
|             </TableBody> | ||||
|           </Table> | ||||
|         </TableContainer> | ||||
|       {/* Si es feriado, mostramos la alerta informativa en la parte superior. */} | ||||
|       {isHoliday && ( | ||||
|         <Box sx={{ mb: 2 }}> | ||||
|           <HolidayAlert /> | ||||
|         </Box> | ||||
|       )} | ||||
|  | ||||
|       {/* La tabla de acciones detalladas se muestra siempre que haya datos para ella. */} | ||||
|       <RenderContent  | ||||
|         data={data}  | ||||
|         handleOpenModal={handleOpenModal} | ||||
|       /> | ||||
|  | ||||
|       {/* El Dialog para mostrar el gráfico histórico. */} | ||||
|       <Dialog | ||||
|         open={Boolean(selectedTicker)} onClose={handleCloseDialog} maxWidth="md" fullWidth | ||||
|         open={Boolean(selectedTicker)} | ||||
|         onClose={handleCloseDialog} | ||||
|         maxWidth="md" | ||||
|         fullWidth | ||||
|         sx={{ '& .MuiDialog-paper': { overflow: 'visible' } }} | ||||
|       > | ||||
|         <IconButton | ||||
|           aria-label="close" onClick={handleCloseDialog} | ||||
|           aria-label="close" | ||||
|           onClick={handleCloseDialog} | ||||
|           sx={{ | ||||
|             position: 'absolute', top: -15, right: -15, color: (theme) => theme.palette.grey[500], | ||||
|             backgroundColor: 'white', boxShadow: 3, '&:hover': { backgroundColor: 'grey.100' }, | ||||
|             position: 'absolute', top: -15, right: -15, | ||||
|             color: (theme) => theme.palette.grey[500], | ||||
|             backgroundColor: 'white', boxShadow: 3, | ||||
|             '&:hover': { backgroundColor: 'grey.100' }, | ||||
|           }} | ||||
|         > | ||||
|           <CloseIcon /> | ||||
|         </IconButton> | ||||
|         <DialogTitle sx={{ m: 0, p: 2 }}>Historial de 30 días para: {selectedTicker}</DialogTitle> | ||||
|         <DialogTitle sx={{ m: 0, p: 2 }}> | ||||
|           Historial de 30 días para: {selectedTicker} | ||||
|         </DialogTitle> | ||||
|         <DialogContent dividers> | ||||
|           {selectedTicker && <HistoricalChartWidget ticker={selectedTicker} mercado="Local" dias={30} />} | ||||
|         </DialogContent> | ||||
|   | ||||
| @@ -17,6 +17,9 @@ import { formatInteger, formatDateOnly } from '../utils/formatters'; | ||||
| import { GrainsHistoricalChartWidget } from './GrainsHistoricalChartWidget'; | ||||
| import { LuBean } from 'react-icons/lu'; | ||||
|  | ||||
| import { useIsHoliday } from '../hooks/useIsHoliday'; | ||||
| import { HolidayAlert } from './common/HolidayAlert'; | ||||
|  | ||||
| const getGrainIcon = (nombre: string) => { | ||||
|   switch (nombre.toLowerCase()) { | ||||
|     case 'girasol': return <GiSunflower size={28} color="#fbc02d" />; | ||||
| @@ -68,7 +71,7 @@ const GranoCard = ({ grano, onChartClick }: { grano: CotizacionGrano, onChartCli | ||||
|       <Box sx={{ display: 'flex', alignItems: 'center', mb: 1, pr: 5 }}> | ||||
|         {getGrainIcon(grano.nombre)} | ||||
|         <Typography variant="h6" component="h3" sx={{ fontWeight: 'bold', ml: 1 }}> | ||||
|             {grano.nombre} | ||||
|           {grano.nombre} | ||||
|         </Typography> | ||||
|       </Box> | ||||
|  | ||||
| @@ -96,6 +99,7 @@ const GranoCard = ({ grano, onChartClick }: { grano: CotizacionGrano, onChartCli | ||||
|  | ||||
| export const GranosCardWidget = () => { | ||||
|   const { data, loading, error } = useApiData<CotizacionGrano[]>('/mercados/granos'); | ||||
|   const isHoliday = useIsHoliday('BA'); | ||||
|   const [selectedGrano, setSelectedGrano] = useState<string | null>(null); | ||||
|   const triggerButtonRef = useRef<HTMLButtonElement | null>(null); | ||||
|  | ||||
| @@ -107,11 +111,12 @@ export const GranosCardWidget = () => { | ||||
|   const handleCloseDialog = () => { | ||||
|     setSelectedGrano(null); | ||||
|     setTimeout(() => { | ||||
|         triggerButtonRef.current?.focus(); | ||||
|       triggerButtonRef.current?.focus(); | ||||
|     }, 0); | ||||
|   }; | ||||
|  | ||||
|   if (loading) { | ||||
|     // El spinner de carga sigue siendo prioritario | ||||
|     return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>; | ||||
|   } | ||||
|  | ||||
| @@ -125,41 +130,47 @@ export const GranosCardWidget = () => { | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|     <Box | ||||
|       sx={{ | ||||
|         display: 'flex', | ||||
|         flexWrap: 'wrap', | ||||
|         // Usamos el objeto para definir gaps responsivos | ||||
|         gap: { | ||||
|           xs: 4, // 16px de gap en pantallas extra pequeñas (afecta el espaciado vertical) | ||||
|           sm: 4, // 16px en pantallas pequeñas | ||||
|           md: 3, // 24px en pantallas medianas (afecta el espaciado horizontal) | ||||
|         }, | ||||
|         justifyContent: 'center' | ||||
|       }} | ||||
|     > | ||||
|       {data.map((grano) => ( | ||||
|       {/* Si es feriado (y la comprobación ha terminado), mostramos la alerta encima */} | ||||
|       {isHoliday === true && ( | ||||
|         <Box sx={{ mb: 2 }}> {/* Añadimos un margen inferior a la alerta */} | ||||
|           <HolidayAlert /> | ||||
|         </Box> | ||||
|       )} | ||||
|       <Box | ||||
|         sx={{ | ||||
|           display: 'flex', | ||||
|           flexWrap: 'wrap', | ||||
|           // Usamos el objeto para definir gaps responsivos | ||||
|           gap: { | ||||
|             xs: 4, // 16px de gap en pantallas extra pequeñas (afecta el espaciado vertical) | ||||
|             sm: 4, // 16px en pantallas pequeñas | ||||
|             md: 3, // 24px en pantallas medianas (afecta el espaciado horizontal) | ||||
|           }, | ||||
|           justifyContent: 'center' | ||||
|         }} | ||||
|       > | ||||
|         {data.map((grano) => ( | ||||
|           <GranoCard key={grano.nombre} grano={grano} onChartClick={(event) => handleChartClick(grano.nombre, event)} /> | ||||
|         ))} | ||||
|       </Box> | ||||
|       <Dialog open={Boolean(selectedGrano)} onClose={handleCloseDialog} maxWidth="md" fullWidth sx={{ '& .MuiDialog-paper': { overflow: 'visible' } }}> | ||||
|         <IconButton | ||||
|                     aria-label="close" | ||||
|                     onClick={handleCloseDialog} | ||||
|                     sx={{ | ||||
|                         position: 'absolute', | ||||
|                         top: -15, // Mueve el botón hacia arriba, fuera del Dialog | ||||
|                         right: -15, // Mueve el botón hacia la derecha, fuera del Dialog | ||||
|                         color: (theme) => 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 | ||||
|                         }, | ||||
|                     }} | ||||
|                 > | ||||
|                     <CloseIcon /> | ||||
|                 </IconButton> | ||||
|           aria-label="close" | ||||
|           onClick={handleCloseDialog} | ||||
|           sx={{ | ||||
|             position: 'absolute', | ||||
|             top: -15, // Mueve el botón hacia arriba, fuera del Dialog | ||||
|             right: -15, // Mueve el botón hacia la derecha, fuera del Dialog | ||||
|             color: (theme) => 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 | ||||
|             }, | ||||
|           }} | ||||
|         > | ||||
|           <CloseIcon /> | ||||
|         </IconButton> | ||||
|         <DialogTitle sx={{ m: 0, p: 2 }}>Mensual de {selectedGrano}</DialogTitle> | ||||
|         <DialogContent dividers> | ||||
|           {selectedGrano && <GrainsHistoricalChartWidget nombre={selectedGrano} />} | ||||
|   | ||||
| @@ -52,8 +52,8 @@ export const MervalHeroCard = () => { | ||||
|         <Paper elevation={3} sx={{ p: 2, mb: 3 }}> | ||||
|             <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}> | ||||
|                 <Box> | ||||
|                     <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold' }}>Índice S&P MERVAL</Typography> | ||||
|                     <Typography variant="h3" component="p" sx={{ fontWeight: 'bold', mt:1 }}>{formatCurrency2Decimal(mervalData.precioActual)}</Typography> | ||||
|                     <Typography variant="h6" component="h2" sx={{ fontWeight: 'bold' }}>Índice S&P MERVAL</Typography> | ||||
|                     <Typography variant="h4" component="p" sx={{ fontWeight: 'bold', mt:1 }}>{formatCurrency2Decimal(mervalData.precioActual)}</Typography> | ||||
|                 </Box> | ||||
|                 <Box sx={{ pt: 2 }}> | ||||
|                     <VariacionMerval actual={mervalData.precioActual} anterior={mervalData.cierreAnterior} /> | ||||
|   | ||||
| @@ -51,8 +51,8 @@ export const UsaIndexHeroCard = () => { | ||||
|         <Paper elevation={3} sx={{ p: 2, mb: 3 }}> | ||||
|             <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}> | ||||
|                 <Box> | ||||
|                     <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold' }}>S&P 500 Index</Typography> | ||||
|                     <Typography variant="h3" component="p" sx={{ fontWeight: 'bold', mt:1 }}>{formatInteger(indexData.precioActual)}</Typography> | ||||
|                     <Typography variant="h6" component="h2" sx={{ fontWeight: 'bold' }}>S&P 500 Index</Typography> | ||||
|                     <Typography variant="h4" component="p" sx={{ fontWeight: 'bold', mt:1 }}>{formatInteger(indexData.precioActual)}</Typography> | ||||
|                 </Box> | ||||
|                 <Box sx={{ pt: 2 }}> | ||||
|                     <VariacionIndex actual={indexData.precioActual} anterior={indexData.cierreAnterior} /> | ||||
|   | ||||
							
								
								
									
										10
									
								
								frontend/src/components/common/HolidayAlert.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/components/common/HolidayAlert.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import { Alert } from '@mui/material'; | ||||
| import CelebrationIcon from '@mui/icons-material/Celebration'; | ||||
|  | ||||
| export const HolidayAlert = () => { | ||||
|     return ( | ||||
|         <Alert severity="info" icon={<CelebrationIcon fontSize="inherit" />}> | ||||
|             Mercado cerrado por feriado. | ||||
|         </Alert> | ||||
|     ); | ||||
| }; | ||||
							
								
								
									
										24
									
								
								frontend/src/hooks/useIsHoliday.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/src/hooks/useIsHoliday.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import { useState, useEffect } from 'react'; | ||||
| import apiClient from '../api/apiClient'; | ||||
|  | ||||
| export function useIsHoliday(marketCode: 'BA' | 'US') { | ||||
|   const [isHoliday, setIsHoliday] = useState<boolean | null>(null); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const checkHoliday = async () => { | ||||
|       try { | ||||
|         const response = await apiClient.get<boolean>(`/api/mercados/es-feriado/${marketCode}`); | ||||
|         setIsHoliday(response.data); | ||||
|         console.log(`Feriado para ${marketCode}: ${response.data}`); | ||||
|       } catch (error) { | ||||
|         console.error(`Error al verificar feriado para ${marketCode}:`, error); | ||||
|         // Si la API de feriados falla, asumimos que no es feriado para no bloquear la UI. | ||||
|         setIsHoliday(false); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     checkHoliday(); | ||||
|   }, [marketCode]); | ||||
|  | ||||
|   return isHoliday; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user