162 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			162 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import React, { useState, useRef } from 'react';
 | |
| import {
 | |
|   Box, CircularProgress, Alert, Table, TableBody, TableCell, TableContainer,
 | |
|   TableHead, TableRow, Paper, Typography, Dialog, DialogTitle,
 | |
|   DialogContent, IconButton
 | |
| } from '@mui/material';
 | |
| 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 { PiChartLineUpBold } from 'react-icons/pi';
 | |
| 
 | |
| // Importaciones de nuestro proyecto
 | |
| 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;
 | |
|   return (
 | |
|     <Box component="span" sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color }}>
 | |
|       <Icon sx={{ fontSize: '1rem', mr: 0.5 }} />
 | |
|       <Typography variant="body2" component="span" sx={{ fontWeight: 'bold' }}>{value.toFixed(2)}%</Typography>
 | |
|     </Box>
 | |
|   );
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Widget autónomo para la tabla de acciones líderes locales (Panel Merval).
 | |
|  */
 | |
| export const BolsaLocalWidget = () => {
 | |
|   // Este widget obtiene todos los datos del mercado local y luego los filtra.
 | |
|   const { data, loading: dataLoading, error: dataError } = useApiData<CotizacionBolsa[]>('/mercados/bolsa/local');
 | |
|   const isHoliday = useIsHoliday('BA');
 | |
|   
 | |
|   const [selectedTicker, setSelectedTicker] = useState<string | null>(null);
 | |
|   const triggerButtonRef = useRef<HTMLButtonElement | null>(null);
 | |
| 
 | |
|   const handleOpenModal = (ticker: string, event: React.MouseEvent<HTMLButtonElement>) => {
 | |
|     triggerButtonRef.current = event.currentTarget;
 | |
|     setSelectedTicker(ticker);
 | |
|   };
 | |
|   
 | |
|   const handleCloseDialog = () => {
 | |
|     setSelectedTicker(null);
 | |
|     setTimeout(() => {
 | |
|       triggerButtonRef.current?.focus();
 | |
|     }, 0);
 | |
|   };
 | |
|   
 | |
|   // Filtramos para obtener solo las acciones, excluyendo el índice.
 | |
|   const panelPrincipal = data?.filter(d => d.ticker !== '^MERV') || [];
 | |
| 
 | |
|   const isLoading = dataLoading || isHoliday === null;
 | |
| 
 | |
|   if (isLoading) {
 | |
|     return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
 | |
|   }
 | |
| 
 | |
|   if (dataError) {
 | |
|     return <Alert severity="error">{dataError}</Alert>;
 | |
|   }
 | |
|   
 | |
|   // Si después de filtrar no queda ninguna acción, mostramos el mensaje apropiado.
 | |
|   if (panelPrincipal.length === 0) {
 | |
|       // Si sabemos que es feriado, la alerta de feriado es el mensaje más relevante.
 | |
|       if (isHoliday) {
 | |
|           return <HolidayAlert />;
 | |
|       }
 | |
|       return <Alert severity="info">No hay acciones líderes disponibles para mostrar.</Alert>;
 | |
|   }
 | |
| 
 | |
|   return (
 | |
|     <Box>
 | |
|       {/* La alerta de feriado también se aplica a esta tabla. */}
 | |
|       {isHoliday && (
 | |
|         <Box sx={{ mb: 2 }}>
 | |
|           <HolidayAlert />
 | |
|         </Box>
 | |
|       )}
 | |
| 
 | |
|       <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>
 | |
| 
 | |
|       <Dialog
 | |
|         open={Boolean(selectedTicker)}
 | |
|         onClose={handleCloseDialog}
 | |
|         maxWidth="md"
 | |
|         fullWidth
 | |
|         sx={{ '& .MuiDialog-paper': { overflow: 'visible' } }}
 | |
|       >
 | |
|         <IconButton
 | |
|           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' },
 | |
|           }}
 | |
|         >
 | |
|           <CloseIcon />
 | |
|         </IconButton>
 | |
|         <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>
 | |
|       </Dialog>
 | |
|     </Box>
 | |
|   );
 | |
| }; |