Ya perdí el hilo de los cambios pero ahi van.
This commit is contained in:
210
Frontend/src/pages/Radios/GenerarListasRadioPage.tsx
Normal file
210
Frontend/src/pages/Radios/GenerarListasRadioPage.tsx
Normal 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;
|
||||
195
Frontend/src/pages/Radios/GestionarCancionesPage.tsx
Normal file
195
Frontend/src/pages/Radios/GestionarCancionesPage.tsx
Normal 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;
|
||||
164
Frontend/src/pages/Radios/GestionarRitmosPage.tsx
Normal file
164
Frontend/src/pages/Radios/GestionarRitmosPage.tsx
Normal 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;
|
||||
56
Frontend/src/pages/Radios/RadiosIndexPage.tsx
Normal file
56
Frontend/src/pages/Radios/RadiosIndexPage.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user