Ya perdí el hilo de los cambios pero ahi van.

This commit is contained in:
2025-05-23 15:47:39 -03:00
parent e7e185a9cb
commit 3c1fe15b1f
141 changed files with 9764 additions and 190 deletions

View File

@@ -0,0 +1,210 @@
import React, { useState } from 'react';
import {
Box, Typography, TextField, Button, Paper, CircularProgress, Alert,
FormControl,
InputLabel,
Select,
MenuItem,
FormHelperText
} from '@mui/material';
import DownloadIcon from '@mui/icons-material/Download';
import radioListaService from '../../services/Radios/radioListaService';
import type { GenerarListaRadioRequestDto } from '../../models/dtos/Radios/GenerarListaRadioRequestDto';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios'; // Para el manejo de errores de Axios
const GestionarListasRadioPage: React.FC = () => {
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth() + 1; // Meses son 0-indexados
const [mes, setMes] = useState<string>(currentMonth.toString());
const [anio, setAnio] = useState<string>(currentYear.toString());
const [institucion, setInstitucion] = useState<"AADI" | "SADAIC">("AADI");
const [radio, setRadio] = useState<"FM 99.1" | "FM 100.3">("FM 99.1");
const [loading, setLoading] = useState(false);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [apiSuccessMessage, setApiSuccessMessage] = useState<string | null>(null);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const { tienePermiso, isSuperAdmin } = usePermissions();
// Usar el permiso general de la sección Radios (SS005) o uno específico
const puedeGenerar = isSuperAdmin || tienePermiso("SS005");
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
const numMes = parseInt(mes, 10);
const numAnio = parseInt(anio, 10);
if (!mes.trim() || isNaN(numMes) || numMes < 1 || numMes > 12) {
errors.mes = 'Mes debe ser un número entre 1 y 12.';
}
if (!anio.trim() || isNaN(numAnio) || numAnio < 2000 || numAnio > 2999) {
errors.anio = 'Año debe ser válido (ej: 2024).';
}
if (!institucion) errors.institucion = 'Institución es obligatoria.'; // Aunque el estado tiene tipo, la validación es buena
if (!radio) errors.radio = 'Radio es obligatoria.';
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleGenerarLista = async () => {
clearMessages();
if (!validate()) return;
setLoading(true);
try {
const params: GenerarListaRadioRequestDto = {
mes: parseInt(mes, 10),
anio: parseInt(anio, 10),
institucion,
radio,
};
const blob = await radioListaService.generarListaRadio(params);
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
// Construir el nombre de archivo como en VB.NET para el zip
// Ejemplo AADI-FM99.1-FM-0524.xlsx.zip
const mesTexto = params.mes.toString().padStart(2, '0');
const anioCortoTexto = (params.anio % 100).toString().padStart(2, '0');
let baseFileName = "";
if (params.institucion === "AADI") {
baseFileName = params.radio === "FM 99.1" ? `AADI-FM99.1-FM-${mesTexto}${anioCortoTexto}` : `AADI-FM100.3-FM-${mesTexto}${anioCortoTexto}`;
} else { // SADAIC
baseFileName = params.radio === "FM 99.1" ? `FM99.1-FM-${mesTexto}${anioCortoTexto}` : `FM100.3-FM-${mesTexto}${anioCortoTexto}`;
}
const defaultFileName = `${baseFileName}.zip`; // Nombre final del ZIP
link.setAttribute('download', defaultFileName);
document.body.appendChild(link);
link.click();
link.parentNode?.removeChild(link);
window.URL.revokeObjectURL(url); // Liberar el objeto URL
setApiSuccessMessage('Lista generada y descarga iniciada.');
} catch (err: any) {
console.error("Error al generar lista:", err);
if (axios.isAxiosError(err) && err.response) {
if (err.response.data instanceof Blob && err.response.data.type === "application/json") {
// Intentar leer el error JSON del Blob
const errorJson = await err.response.data.text();
try {
const parsedError = JSON.parse(errorJson);
setApiErrorMessage(parsedError.message || 'Error al generar la lista.');
} catch (parseError) {
setApiErrorMessage('Error al generar la lista. Respuesta de error no válida.');
}
} else {
setApiErrorMessage(err.response.data?.message || err.message || 'Error desconocido al generar la lista.');
}
} else {
setApiErrorMessage('Error al generar la lista.');
}
} finally {
setLoading(false);
}
};
const clearMessages = () => {
setApiErrorMessage(null);
setApiSuccessMessage(null);
};
const handleInputChange = (fieldName: string) => {
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
clearMessages();
};
if (!puedeGenerar) {
return <Box sx={{ p: 2 }}><Alert severity="error">Acceso denegado.</Alert></Box>;
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Generar Listas de Radio</Typography>
<Paper sx={{ p: 3, mb: 2 }}>
<Typography variant="h6" gutterBottom>Criterios de Generación</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}> {/* Aumentado el gap */}
<TextField
label="Mes (1-12)"
type="number"
value={mes}
onChange={(e) => { setMes(e.target.value); handleInputChange('mes'); }}
error={!!localErrors.mes}
helperText={localErrors.mes || ''}
InputLabelProps={{ shrink: true }}
inputProps={{ min: 1, max: 12 }}
required
fullWidth
/>
<TextField
label="Año (ej: 2024)"
type="number"
value={anio}
onChange={(e) => { setAnio(e.target.value); handleInputChange('anio'); }}
error={!!localErrors.anio}
helperText={localErrors.anio || ''}
InputLabelProps={{ shrink: true }}
inputProps={{ min: 2000, max: 2099 }}
required
fullWidth
/>
<FormControl fullWidth required error={!!localErrors.institucion}>
<InputLabel id="institucion-select-label">Institución</InputLabel>
<Select
labelId="institucion-select-label"
id="institucion-select"
name="institucion"
value={institucion}
label="Institución"
onChange={(e) => { setInstitucion(e.target.value as "AADI" | "SADAIC"); handleInputChange('institucion'); }}
>
<MenuItem value="AADI">AADI</MenuItem>
<MenuItem value="SADAIC">SADAIC</MenuItem>
</Select>
{localErrors.institucion && <FormHelperText>{localErrors.institucion}</FormHelperText>}
</FormControl>
<FormControl fullWidth required error={!!localErrors.radio}>
<InputLabel id="radio-select-label">Radio</InputLabel>
<Select
labelId="radio-select-label"
id="radio-select"
name="radio"
value={radio}
label="Radio"
onChange={(e) => { setRadio(e.target.value as "FM 99.1" | "FM 100.3"); handleInputChange('radio'); }}
>
<MenuItem value="FM 99.1">FM 99.1</MenuItem>
<MenuItem value="FM 100.3">FM 100.3</MenuItem>
</Select>
{localErrors.radio && <FormHelperText>{localErrors.radio}</FormHelperText>}
</FormControl>
<Button
variant="contained"
color="primary"
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <DownloadIcon />}
onClick={handleGenerarLista}
disabled={loading}
sx={{ mt: 1, alignSelf: 'flex-start' }} // Un pequeño margen superior
>
Generar y Descargar Lista
</Button>
</Box>
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{apiSuccessMessage && <Alert severity="success" sx={{ my: 2 }}>{apiSuccessMessage}</Alert>}
</Box>
);
};
export default GestionarListasRadioPage;

View File

@@ -0,0 +1,195 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FilterListIcon from '@mui/icons-material/FilterList';
import cancionService from '../../services/Radios/cancionService';
import ritmoService from '../../services/Radios/ritmoService'; // Para el filtro de ritmo
import type { CancionDto } from '../../models/dtos/Radios/CancionDto';
import type { CreateCancionDto } from '../../models/dtos/Radios/CreateCancionDto';
import type { UpdateCancionDto } from '../../models/dtos/Radios/UpdateCancionDto';
import type { RitmoDto } from '../../models/dtos/Radios/RitmoDto';
import CancionFormModal from '../../components/Modals/Radios/CancionFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarCancionesPage: React.FC = () => {
const [canciones, setCanciones] = useState<CancionDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroTema, setFiltroTema] = useState('');
const [filtroInterprete, setFiltroInterprete] = useState('');
const [filtroIdRitmo, setFiltroIdRitmo] = useState<number | string>('');
const [ritmos, setRitmos] = useState<RitmoDto[]>([]); // Para el dropdown de filtro
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editingCancion, setEditingCancion] = useState<CancionDto | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedRow, setSelectedRow] = useState<CancionDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeGestionar = isSuperAdmin || tienePermiso("SS005"); // Usar permiso general de la sección
const fetchRitmosParaFiltro = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const data = await ritmoService.getAllRitmos();
setRitmos(data);
} catch (err) { console.error(err); setError("Error al cargar ritmos para filtro.");
} finally { setLoadingFiltersDropdown(false); }
}, []);
useEffect(() => { fetchRitmosParaFiltro(); }, [fetchRitmosParaFiltro]);
const cargarCanciones = useCallback(async () => {
if (!puedeGestionar) { setError("No tiene permiso."); setLoading(false); return; }
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const params = {
temaFilter: filtroTema || null,
interpreteFilter: filtroInterprete || null,
idRitmoFilter: filtroIdRitmo ? Number(filtroIdRitmo) : null,
};
const data = await cancionService.getAllCanciones(params);
setCanciones(data);
} catch (err) { console.error(err); setError('Error al cargar las canciones.');
} finally { setLoading(false); }
}, [puedeGestionar, filtroTema, filtroInterprete, filtroIdRitmo]);
useEffect(() => { cargarCanciones(); }, [cargarCanciones]);
const handleOpenModal = (item?: CancionDto) => {
setEditingCancion(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => { setModalOpen(false); setEditingCancion(null); };
const handleSubmitModal = async (data: CreateCancionDto | UpdateCancionDto, id?: number) => {
setApiErrorMessage(null);
try {
if (id && editingCancion) {
await cancionService.updateCancion(id, data as UpdateCancionDto);
} else {
await cancionService.createCancion(data as CreateCancionDto);
}
cargarCanciones();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la canción.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (id: number) => {
if (window.confirm(`¿Seguro de eliminar esta canción (ID: ${id})?`)) {
setApiErrorMessage(null);
try { await cancionService.deleteCancion(id); cargarCanciones(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: CancionDto) => {
setAnchorEl(event.currentTarget); setSelectedRow(item);
};
const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); };
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = canciones.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeGestionar && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Canciones</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}>
<TextField label="Filtrar por Tema" size="small" value={filtroTema} onChange={(e) => setFiltroTema(e.target.value)} sx={{minWidth: 200, flexGrow: 1}}/>
<TextField label="Filtrar por Intérprete" size="small" value={filtroInterprete} onChange={(e) => setFiltroInterprete(e.target.value)} sx={{minWidth: 200, flexGrow: 1}}/>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}} disabled={loadingFiltersDropdown}>
<InputLabel>Ritmo</InputLabel>
<Select value={filtroIdRitmo} label="Ritmo" onChange={(e) => setFiltroIdRitmo(e.target.value as number | string)}>
<MenuItem value=""><em>Todos</em></MenuItem>
{ritmos.map(r => <MenuItem key={r.id} value={r.id}>{r.nombreRitmo}</MenuItem>)}
</Select>
</FormControl>
</Box>
{puedeGestionar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Agregar Canción</Button>)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{!loading && !error && puedeGestionar && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow sx={{backgroundColor: 'action.hover'}}>
<TableCell>Tema</TableCell><TableCell>Intérprete</TableCell>
<TableCell>Álbum</TableCell><TableCell>Ritmo</TableCell>
<TableCell>Formato</TableCell><TableCell>Pista</TableCell>
<TableCell align="right">Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={7} align="center">No se encontraron canciones.</TableCell></TableRow>
) : (
displayData.map((c) => (
<TableRow key={c.id} hover>
<TableCell sx={{fontWeight:500}}>{c.tema || '-'}</TableCell>
<TableCell>{c.interprete || '-'}</TableCell>
<TableCell>{c.album || '-'}</TableCell>
<TableCell>{c.nombreRitmo || '-'}</TableCell>
<TableCell>{c.formato || '-'}</TableCell>
<TableCell align="center">{c.pista ?? '-'}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]} component="div" count={canciones.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeGestionar && selectedRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
{puedeGestionar && selectedRow && (
<MenuItem onClick={() => handleDelete(selectedRow.id)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
</Menu>
<CancionFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingCancion} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarCancionesPage;

View File

@@ -0,0 +1,164 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FilterListIcon from '@mui/icons-material/FilterList';
import ritmoService from '../../services/Radios/ritmoService';
import type { RitmoDto } from '../../models/dtos/Radios/RitmoDto';
import type { CreateRitmoDto } from '../../models/dtos/Radios/CreateRitmoDto';
import type { UpdateRitmoDto } from '../../models/dtos/Radios/UpdateRitmoDto';
import RitmoFormModal from '../../components/Modals/Radios/RitmoFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarRitmosPage: React.FC = () => {
const [ritmos, setRitmos] = useState<RitmoDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [filtroNombre, setFiltroNombre] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const [editingRitmo, setEditingRitmo] = useState<RitmoDto | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedRow, setSelectedRow] = useState<RitmoDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
// Usar el permiso general de la sección Radios (SS005) o crear permisos específicos si es necesario.
const puedeGestionar = isSuperAdmin || tienePermiso("SS005");
const cargarRitmos = useCallback(async () => {
if (!puedeGestionar) { // Asumimos que el mismo permiso es para ver y gestionar
setError("No tiene permiso para acceder a esta sección.");
setLoading(false);
return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const data = await ritmoService.getAllRitmos(filtroNombre);
setRitmos(data);
} catch (err) {
console.error(err); setError('Error al cargar los ritmos.');
} finally { setLoading(false); }
}, [puedeGestionar, filtroNombre]);
useEffect(() => { cargarRitmos(); }, [cargarRitmos]);
const handleOpenModal = (item?: RitmoDto) => {
setEditingRitmo(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingRitmo(null);
};
const handleSubmitModal = async (data: CreateRitmoDto | UpdateRitmoDto, id?: number) => {
setApiErrorMessage(null);
try {
if (id && editingRitmo) {
await ritmoService.updateRitmo(id, data as UpdateRitmoDto);
} else {
await ritmoService.createRitmo(data as CreateRitmoDto);
}
cargarRitmos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el ritmo.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (id: number) => {
if (window.confirm(`¿Seguro de eliminar este ritmo (ID: ${id})?`)) {
setApiErrorMessage(null);
try { await ritmoService.deleteRitmo(id); cargarRitmos(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: RitmoDto) => {
setAnchorEl(event.currentTarget); setSelectedRow(item);
};
const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); };
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = ritmos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeGestionar) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Ritmos</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}>
<TextField label="Filtrar por Nombre" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{minWidth: 200, flexGrow: 1}}/>
{/* <Button variant="outlined" onClick={cargarRitmos} size="small">Buscar</Button> */}
</Box>
{puedeGestionar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Agregar Ritmo</Button>)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{!loading && !error && puedeGestionar && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell sx={{fontWeight: 'bold'}}>Nombre del Ritmo</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={2} align="center">No se encontraron ritmos.</TableCell></TableRow>
) : (
displayData.map((r) => (
<TableRow key={r.id} hover>
<TableCell>{r.nombreRitmo || '-'}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, r)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[10, 25, 50]} component="div" count={ritmos.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeGestionar && selectedRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
{puedeGestionar && selectedRow && (
<MenuItem onClick={() => handleDelete(selectedRow.id)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
</Menu>
<RitmoFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingRitmo} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarRitmosPage;

View File

@@ -0,0 +1,56 @@
import React, { useState, useEffect } from 'react';
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
const radiosSubModules = [
{ label: 'Ritmos', path: 'ritmos' },
{ label: 'Canciones', path: 'canciones' },
{ label: 'Generar Listas', path: 'generar-listas' },
];
const RadiosIndexPage: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
useEffect(() => {
const currentBasePath = '/radios';
const subPath = location.pathname.startsWith(currentBasePath + '/')
? location.pathname.substring(currentBasePath.length + 1).split('/')[0]
: (location.pathname === currentBasePath ? radiosSubModules[0]?.path : undefined);
const activeTabIndex = radiosSubModules.findIndex(sm => sm.path === subPath);
if (activeTabIndex !== -1) {
setSelectedSubTab(activeTabIndex);
} else {
if (location.pathname === currentBasePath && radiosSubModules.length > 0) {
navigate(radiosSubModules[0].path, { replace: true });
setSelectedSubTab(0);
} else {
setSelectedSubTab(false);
}
}
}, [location.pathname, navigate]);
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setSelectedSubTab(newValue);
navigate(radiosSubModules[newValue].path);
};
return (
<Box>
<Typography variant="h5" gutterBottom>Módulo de Radios</Typography>
<Paper square elevation={1}>
<Tabs value={selectedSubTab} onChange={handleSubTabChange} indicatorColor="primary" textColor="primary" variant="scrollable" scrollButtons="auto">
{radiosSubModules.map((subModule) => (
<Tab key={subModule.path} label={subModule.label} />
))}
</Tabs>
</Paper>
<Box sx={{ pt: 2 }}>
<Outlet />
</Box>
</Box>
);
};
export default RadiosIndexPage;