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

@@ -6,8 +6,8 @@ import { Outlet, useNavigate, useLocation } from 'react-router-dom';
// Define las sub-pestañas del módulo Contables
const contablesSubModules = [
{ label: 'Tipos de Pago', path: 'tipos-pago' }, // Se convertirá en /contables/tipos-pago
// { label: 'Pagos', path: 'pagos' }, // Ejemplo de otra sub-pestaña futura
// { label: 'Créditos/Débitos', path: 'creditos-debitos' },
{ label: 'Pagos Distribuidores', path: 'pagos-distribuidores' },
{ label: 'Notas Crédito/Débito', path: 'notas-cd' },
];
const ContablesIndexPage: React.FC = () => {

View File

@@ -0,0 +1,262 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip
} 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 notaCreditoDebitoService from '../../services/Contables/notaCreditoDebitoService';
import distribuidorService from '../../services/Distribucion/distribuidorService';
import canillaService from '../../services/Distribucion/canillaService';
import empresaService from '../../services/Distribucion/empresaService';
import type { NotaCreditoDebitoDto } from '../../models/dtos/Contables/NotaCreditoDebitoDto';
import type { CreateNotaDto } from '../../models/dtos/Contables/CreateNotaDto';
import type { UpdateNotaDto } from '../../models/dtos/Contables/UpdateNotaDto';
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
import NotaCreditoDebitoFormModal from '../../components/Modals/Contables/NotaCreditoDebitoFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
type DestinoFiltroType = 'Distribuidores' | 'Canillas' | '';
type TipoNotaFiltroType = 'Credito' | 'Debito' | '';
const GestionarNotasCDPage: React.FC = () => {
const [notas, setNotas] = useState<NotaCreditoDebitoDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
const [filtroDestino, setFiltroDestino] = useState<DestinoFiltroType>('');
const [filtroIdDestinatario, setFiltroIdDestinatario] = useState<number | string>('');
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
const [filtroTipoNota, setFiltroTipoNota] = useState<TipoNotaFiltroType>('');
const [destinatariosFiltro, setDestinatariosFiltro] = useState<(DistribuidorDto | CanillaDto)[]>([]);
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editingNota, setEditingNota] = useState<NotaCreditoDebitoDto | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedRow, setSelectedRow] = useState<NotaCreditoDebitoDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
// CN001 (Ver), CN002 (Crear), CN003 (Modificar), CN004 (Eliminar)
const puedeVer = isSuperAdmin || tienePermiso("CN001");
const puedeCrear = isSuperAdmin || tienePermiso("CN002");
const puedeModificar = isSuperAdmin || tienePermiso("CN003");
const puedeEliminar = isSuperAdmin || tienePermiso("CN004");
const fetchEmpresasParaFiltro = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const empData = await empresaService.getAllEmpresas();
setEmpresas(empData);
} catch (err) { console.error(err); setError("Error al cargar empresas.");
} finally { setLoadingFiltersDropdown(false); }
}, []);
const fetchDestinatariosParaFiltro = useCallback(async (tipoDestino: DestinoFiltroType) => {
if (!tipoDestino) { setDestinatariosFiltro([]); return; }
setLoadingFiltersDropdown(true);
setFiltroIdDestinatario(''); // Resetear selección de destinatario
try {
if (tipoDestino === 'Distribuidores') {
const data = await distribuidorService.getAllDistribuidores();
setDestinatariosFiltro(data);
} else if (tipoDestino === 'Canillas') {
const data = await canillaService.getAllCanillas(undefined, undefined, true);
setDestinatariosFiltro(data);
}
} catch (err) { console.error(err); setError(`Error al cargar ${tipoDestino}.`);
} finally { setLoadingFiltersDropdown(false); }
}, []);
useEffect(() => { fetchEmpresasParaFiltro(); }, [fetchEmpresasParaFiltro]);
useEffect(() => { fetchDestinatariosParaFiltro(filtroDestino); }, [filtroDestino, fetchDestinatariosParaFiltro]);
const cargarNotas = useCallback(async () => {
if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; }
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const params = {
fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null,
destino: filtroDestino || null,
idDestino: filtroIdDestinatario ? Number(filtroIdDestinatario) : null,
idEmpresa: filtroIdEmpresa ? Number(filtroIdEmpresa) : null,
tipoNota: filtroTipoNota || null,
};
const data = await notaCreditoDebitoService.getAllNotas(params);
setNotas(data);
} catch (err) { console.error(err); setError('Error al cargar las notas.');
} finally { setLoading(false); }
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroDestino, filtroIdDestinatario, filtroIdEmpresa, filtroTipoNota]);
useEffect(() => { cargarNotas(); }, [cargarNotas]);
const handleOpenModal = (item?: NotaCreditoDebitoDto) => {
setEditingNota(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => { setModalOpen(false); setEditingNota(null); };
const handleSubmitModal = async (data: CreateNotaDto | UpdateNotaDto, idNota?: number) => {
setApiErrorMessage(null);
try {
if (idNota && editingNota) {
await notaCreditoDebitoService.updateNota(idNota, data as UpdateNotaDto);
} else {
await notaCreditoDebitoService.createNota(data as CreateNotaDto);
}
cargarNotas();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la nota.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (idNota: number) => {
if (window.confirm(`¿Seguro de eliminar esta nota (ID: ${idNota})? Esta acción revertirá el impacto en el saldo.`)) {
setApiErrorMessage(null);
try { await notaCreditoDebitoService.deleteNota(idNota); cargarNotas(); }
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: NotaCreditoDebitoDto) => {
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 = notas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Notas de Crédito/Débito</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="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/>
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}}>
<InputLabel>Empresa</InputLabel>
<Select value={filtroIdEmpresa} label="Empresa" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todas</em></MenuItem>
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}}>
<InputLabel>Tipo Destino</InputLabel>
<Select value={filtroDestino} label="Tipo Destino" onChange={(e) => setFiltroDestino(e.target.value as DestinoFiltroType)}>
<MenuItem value=""><em>Todos</em></MenuItem>
<MenuItem value="Distribuidores">Distribuidores</MenuItem>
<MenuItem value="Canillas">Canillas</MenuItem>
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown || !filtroDestino}>
<InputLabel>Destinatario</InputLabel>
<Select value={filtroIdDestinatario} label="Destinatario" onChange={(e) => setFiltroIdDestinatario(e.target.value as number | string)}>
<MenuItem value=""><em>Todos</em></MenuItem>
{destinatariosFiltro.map(d => (
<MenuItem key={'idDistribuidor' in d ? d.idDistribuidor : d.idCanilla} value={'idDistribuidor' in d ? d.idDistribuidor : d.idCanilla}>
{'nomApe' in d ? d.nomApe : d.nombre}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 150, flexGrow: 1}}>
<InputLabel>Tipo Nota</InputLabel>
<Select value={filtroTipoNota} label="Tipo Nota" onChange={(e) => setFiltroTipoNota(e.target.value as TipoNotaFiltroType)}>
<MenuItem value=""><em>Ambas</em></MenuItem>
<MenuItem value="Credito">Crédito</MenuItem>
<MenuItem value="Debito">Débito</MenuItem>
</Select>
</FormControl>
</Box>
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Nota</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 && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Empresa</TableCell><TableCell>Destino</TableCell>
<TableCell>Destinatario</TableCell><TableCell>Tipo</TableCell>
<TableCell align="right">Monto</TableCell><TableCell>Referencia</TableCell><TableCell>Obs.</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 9 : 8} align="center">No se encontraron notas.</TableCell></TableRow>
) : (
displayData.map((n) => (
<TableRow key={n.idNota} hover>
<TableCell>{formatDate(n.fecha)}</TableCell><TableCell>{n.nombreEmpresa}</TableCell>
<TableCell>{n.destino}</TableCell><TableCell>{n.nombreDestinatario}</TableCell>
<TableCell>
<Chip label={n.tipo} color={n.tipo === 'Credito' ? 'success' : 'error'} size="small"/>
</TableCell>
<TableCell align="right" sx={{color: n.tipo === 'Credito' ? 'green' : 'red'}}>${n.monto.toFixed(2)}</TableCell>
<TableCell>{n.referencia || '-'}</TableCell>
<TableCell><Tooltip title={n.observaciones || ''}><Box sx={{maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{n.observaciones || '-'}</Box></Tooltip></TableCell>
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, n)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[10, 25, 50]} component="div" count={notas.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && selectedRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
{puedeEliminar && selectedRow && (
<MenuItem onClick={() => handleDelete(selectedRow.idNota)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
</Menu>
<NotaCreditoDebitoFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingNota} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarNotasCDPage;

View File

@@ -0,0 +1,231 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip
} 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 pagoDistribuidorService from '../../services/Contables/pagoDistribuidorService';
import distribuidorService from '../../services/Distribucion/distribuidorService';
import empresaService from '../../services/Distribucion/empresaService';
import type { PagoDistribuidorDto } from '../../models/dtos/Contables/PagoDistribuidorDto';
import type { CreatePagoDistribuidorDto } from '../../models/dtos/Contables/CreatePagoDistribuidorDto';
import type { UpdatePagoDistribuidorDto } from '../../models/dtos/Contables/UpdatePagoDistribuidorDto';
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
import PagoDistribuidorFormModal from '../../components/Modals/Contables/PagoDistribuidorFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarPagosDistribuidorPage: React.FC = () => {
const [pagos, setPagos] = useState<PagoDistribuidorDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
const [filtroTipoMov, setFiltroTipoMov] = useState<'Recibido' | 'Realizado' | ''>('');
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editingPago, setEditingPago] = useState<PagoDistribuidorDto | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedRow, setSelectedRow] = useState<PagoDistribuidorDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
// Permisos CP001 (Ver), CP002 (Crear), CP003 (Modificar), CP004 (Eliminar)
const puedeVer = isSuperAdmin || tienePermiso("CP001");
const puedeCrear = isSuperAdmin || tienePermiso("CP002");
const puedeModificar = isSuperAdmin || tienePermiso("CP003");
const puedeEliminar = isSuperAdmin || tienePermiso("CP004");
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const [distData, empData] = await Promise.all([
distribuidorService.getAllDistribuidores(),
empresaService.getAllEmpresas()
]);
setDistribuidores(distData);
setEmpresas(empData);
} catch (err) { console.error(err); setError("Error al cargar opciones de filtro.");
} finally { setLoadingFiltersDropdown(false); }
}, []);
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
const cargarPagos = useCallback(async () => {
if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; }
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const params = {
fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null,
idDistribuidor: filtroIdDistribuidor ? Number(filtroIdDistribuidor) : null,
idEmpresa: filtroIdEmpresa ? Number(filtroIdEmpresa) : null,
tipoMovimiento: filtroTipoMov || null,
};
const data = await pagoDistribuidorService.getAllPagosDistribuidor(params);
setPagos(data);
} catch (err) { console.error(err); setError('Error al cargar los pagos.');
} finally { setLoading(false); }
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdDistribuidor, filtroIdEmpresa, filtroTipoMov]);
useEffect(() => { cargarPagos(); }, [cargarPagos]);
const handleOpenModal = (item?: PagoDistribuidorDto) => {
setEditingPago(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => { setModalOpen(false); setEditingPago(null); };
const handleSubmitModal = async (data: CreatePagoDistribuidorDto | UpdatePagoDistribuidorDto, idPago?: number) => {
setApiErrorMessage(null);
try {
if (idPago && editingPago) {
await pagoDistribuidorService.updatePagoDistribuidor(idPago, data as UpdatePagoDistribuidorDto);
} else {
await pagoDistribuidorService.createPagoDistribuidor(data as CreatePagoDistribuidorDto);
}
cargarPagos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el pago.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (idPago: number) => {
if (window.confirm(`¿Seguro de eliminar este pago (ID: ${idPago})? Esta acción revertirá el impacto en el saldo.`)) {
setApiErrorMessage(null);
try { await pagoDistribuidorService.deletePagoDistribuidor(idPago); cargarPagos(); }
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: PagoDistribuidorDto) => {
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 = pagos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Pagos de Distribuidores</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="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/>
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/>
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}>
<InputLabel>Distribuidor</InputLabel>
<Select value={filtroIdDistribuidor} label="Distribuidor" onChange={(e) => setFiltroIdDistribuidor(e.target.value as number | string)}>
<MenuItem value=""><em>Todos</em></MenuItem>
{distribuidores.map(d => <MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}>
<InputLabel>Empresa (Saldo)</InputLabel>
<Select value={filtroIdEmpresa} label="Empresa (Saldo)" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}>
<MenuItem value=""><em>Todas</em></MenuItem>
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 150, flexGrow: 1}}>
<InputLabel>Tipo Mov.</InputLabel>
<Select value={filtroTipoMov} label="Tipo Mov." onChange={(e) => setFiltroTipoMov(e.target.value as 'Recibido' | 'Realizado' | '')}>
<MenuItem value=""><em>Todos</em></MenuItem>
<MenuItem value="Recibido">Recibido</MenuItem>
<MenuItem value="Realizado">Realizado</MenuItem>
</Select>
</FormControl>
</Box>
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Pago</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 && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Distribuidor</TableCell><TableCell>Empresa (Saldo)</TableCell>
<TableCell>Tipo Mov.</TableCell><TableCell>Recibo N°</TableCell>
<TableCell align="right">Monto</TableCell><TableCell>Tipo Pago</TableCell>
<TableCell>Detalle</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 9 : 8} align="center">No se encontraron pagos.</TableCell></TableRow>
) : (
displayData.map((p) => (
<TableRow key={p.idPago} hover>
<TableCell>{formatDate(p.fecha)}</TableCell><TableCell>{p.nombreDistribuidor}</TableCell>
<TableCell>{p.nombreEmpresa}</TableCell>
<TableCell>
<Chip label={p.tipoMovimiento} color={p.tipoMovimiento === 'Recibido' ? 'success' : 'warning'} size="small"/>
</TableCell>
<TableCell>{p.recibo}</TableCell>
<TableCell align="right">${p.monto.toFixed(2)}</TableCell>
<TableCell>{p.nombreTipoPago}</TableCell>
<TableCell><Tooltip title={p.detalle || ''}><Box sx={{maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{p.detalle || '-'}</Box></Tooltip></TableCell>
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[10, 25, 50]} component="div" count={pagos.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && selectedRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
{puedeEliminar && selectedRow && (
<MenuItem onClick={() => handleDelete(selectedRow.idPago)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
</Menu>
<PagoDistribuidorFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingPago} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarPagosDistribuidorPage;

View File

@@ -9,8 +9,8 @@ import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import tipoPagoService from '../../services/Contables/tipoPagoService';
import type { TipoPago } from '../../models/Entities/TipoPago';
import type { CreateTipoPagoDto } from '../../models/dtos/tiposPago/CreateTipoPagoDto';
import type { UpdateTipoPagoDto } from '../../models/dtos/tiposPago/UpdateTipoPagoDto';
import type { CreateTipoPagoDto } from '../../models/dtos/Contables/CreateTipoPagoDto';
import type { UpdateTipoPagoDto } from '../../models/dtos/Contables/UpdateTipoPagoDto';
import TipoPagoFormModal from '../../components/Modals/Contables/TipoPagoFormModal';
import axios from 'axios';
import { usePermissions } from '../../hooks/usePermissions';

View File

@@ -1,7 +0,0 @@
import React from 'react';
import { Typography } from '@mui/material';
const ESCanillasPage: React.FC = () => {
return <Typography variant="h6">Página de Gestión de E/S de Canillas</Typography>;
};
export default ESCanillasPage;

View File

@@ -0,0 +1,220 @@
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, Tooltip
} 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 controlDevolucionesService from '../../services/Distribucion/controlDevolucionesService';
import empresaService from '../../services/Distribucion/empresaService'; // Para el filtro de empresa
import type { ControlDevolucionesDto } from '../../models/dtos/Distribucion/ControlDevolucionesDto';
import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto';
import type { UpdateControlDevolucionesDto } from '../../models/dtos/Distribucion/UpdateControlDevolucionesDto';
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
import ControlDevolucionesFormModal from '../../components/Modals/Distribucion/ControlDevolucionesFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarControlDevolucionesPage: React.FC = () => {
const [controles, setControles] = useState<ControlDevolucionesDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editingControl, setEditingControl] = useState<ControlDevolucionesDto | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedRow, setSelectedRow] = useState<ControlDevolucionesDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
// Permisos CD001 (Ver), CD002 (Crear), CD003 (Modificar)
const puedeVer = isSuperAdmin || tienePermiso("CD001");
const puedeCrear = isSuperAdmin || tienePermiso("CD002");
const puedeModificar = isSuperAdmin || tienePermiso("CD003");
const puedeEliminar = isSuperAdmin || tienePermiso("CD003"); // Asumiendo que modificar incluye eliminar
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const empresasData = await empresaService.getAllEmpresas();
setEmpresas(empresasData);
} catch (err) {
console.error("Error cargando empresas para filtro:", err);
setError("Error al cargar opciones de filtro.");
} finally {
setLoadingFiltersDropdown(false);
}
}, []);
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
const cargarControles = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección."); setLoading(false); return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const params = {
fechaDesde: filtroFechaDesde || null,
fechaHasta: filtroFechaHasta || null,
idEmpresa: filtroIdEmpresa ? Number(filtroIdEmpresa) : null,
};
const data = await controlDevolucionesService.getAllControlesDevoluciones(params);
setControles(data);
} catch (err) {
console.error(err); setError('Error al cargar los controles de devoluciones.');
} finally { setLoading(false); }
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdEmpresa]);
useEffect(() => { cargarControles(); }, [cargarControles]);
const handleOpenModal = (item?: ControlDevolucionesDto) => {
setEditingControl(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingControl(null);
};
const handleSubmitModal = async (data: CreateControlDevolucionesDto | UpdateControlDevolucionesDto, idControl?: number) => {
setApiErrorMessage(null);
try {
if (idControl && editingControl) {
await controlDevolucionesService.updateControlDevoluciones(idControl, data as UpdateControlDevolucionesDto);
} else {
await controlDevolucionesService.createControlDevoluciones(data as CreateControlDevolucionesDto);
}
cargarControles();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el control.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (idControl: number) => {
if (window.confirm(`¿Seguro de eliminar este control de devoluciones (ID: ${idControl})?`)) {
setApiErrorMessage(null);
try {
await controlDevolucionesService.deleteControlDevoluciones(idControl);
cargarControles();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
setApiErrorMessage(message);
}
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: ControlDevolucionesDto) => {
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 = controles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Control de Devoluciones a Empresa</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="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/>
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/>
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}>
<InputLabel>Empresa</InputLabel>
<Select value={filtroIdEmpresa} label="Empresa" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}>
<MenuItem value=""><em>Todas</em></MenuItem>
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
</Select>
</FormControl>
</Box>
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Control</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 && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Empresa</TableCell>
<TableCell align="right">Entrada (Total Dev.)</TableCell>
<TableCell align="right">Sobrantes</TableCell>
<TableCell align="right">Sin Cargo</TableCell>
<TableCell>Detalle</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles.</TableCell></TableRow>
) : (
displayData.map((c) => (
<TableRow key={c.idControl} hover>
<TableCell>{formatDate(c.fecha)}</TableCell>
<TableCell>{c.nombreEmpresa}</TableCell>
<TableCell align="right">{c.entrada}</TableCell>
<TableCell align="right">{c.sobrantes}</TableCell>
<TableCell align="right">{c.sinCargo}</TableCell>
<TableCell><Tooltip title={c.detalle || ''}><Box sx={{maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{c.detalle || '-'}</Box></Tooltip></TableCell>
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[10, 25, 50]} component="div" count={controles.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && selectedRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
{puedeEliminar && selectedRow && (
<MenuItem onClick={() => handleDelete(selectedRow.idControl)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
</Menu>
<ControlDevolucionesFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingControl} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarControlDevolucionesPage;

View File

@@ -0,0 +1,369 @@
// src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select, Checkbox, Tooltip,
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle
} 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 PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck'; // Para Liquidar
import entradaSalidaCanillaService from '../../services/Distribucion/entradaSalidaCanillaService';
import publicacionService from '../../services/Distribucion/publicacionService';
import canillaService from '../../services/Distribucion/canillaService';
import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
import type { CreateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaCanillaDto';
import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto';
import EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const [movimientos, setMovimientos] = useState<EntradaSalidaCanillaDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
const [filtroIdCanilla, setFiltroIdCanilla] = useState<number | string>('');
const [filtroEstadoLiquidacion, setFiltroEstadoLiquidacion] = useState<'todos' | 'liquidados' | 'noLiquidados'>('noLiquidados');
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaCanillaDto | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedRow, setSelectedRow] = useState<EntradaSalidaCanillaDto | null>(null);
const [selectedIdsParaLiquidar, setSelectedIdsParaLiquidar] = useState<Set<number>>(new Set());
const [fechaLiquidacionDialog, setFechaLiquidacionDialog] = useState<string>(new Date().toISOString().split('T')[0]);
const [openLiquidarDialog, setOpenLiquidarDialog] = useState(false);
const { tienePermiso, isSuperAdmin } = usePermissions();
// MC001 (Ver), MC002 (Crear), MC003 (Modificar), MC004 (Eliminar), MC005 (Liquidar)
const puedeVer = isSuperAdmin || tienePermiso("MC001");
const puedeCrear = isSuperAdmin || tienePermiso("MC002");
const puedeModificar = isSuperAdmin || tienePermiso("MC003");
const puedeEliminar = isSuperAdmin || tienePermiso("MC004");
const puedeLiquidar = isSuperAdmin || tienePermiso("MC005");
const puedeEliminarLiquidados = isSuperAdmin || tienePermiso("MC006");
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const [pubsData, canData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true),
canillaService.getAllCanillas(undefined, undefined, true)
]);
setPublicaciones(pubsData);
setCanillitas(canData);
} catch (err) {
console.error(err); setError("Error al cargar opciones de filtro.");
} finally { setLoadingFiltersDropdown(false); }
}, []);
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
const cargarMovimientos = useCallback(async () => {
if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; }
setLoading(true); setError(null); setApiErrorMessage(null);
try {
let liquidadosFilter: boolean | null = null;
let incluirNoLiquidadosFilter: boolean | null = true; // Por defecto mostrar no liquidados
if (filtroEstadoLiquidacion === 'liquidados') {
liquidadosFilter = true;
incluirNoLiquidadosFilter = false;
} else if (filtroEstadoLiquidacion === 'noLiquidados') {
liquidadosFilter = false;
incluirNoLiquidadosFilter = true;
} // Si es 'todos', ambos son null o true y false respectivamente (backend debe manejarlo)
const params = {
fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null,
idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null,
idCanilla: filtroIdCanilla ? Number(filtroIdCanilla) : null,
liquidados: liquidadosFilter,
incluirNoLiquidados: filtroEstadoLiquidacion === 'todos' ? null : incluirNoLiquidadosFilter,
};
const data = await entradaSalidaCanillaService.getAllEntradasSalidasCanilla(params);
setMovimientos(data);
setSelectedIdsParaLiquidar(new Set()); // Limpiar selección al recargar
} catch (err) {
console.error(err); setError('Error al cargar movimientos.');
} finally { setLoading(false); }
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdCanilla, filtroEstadoLiquidacion]);
useEffect(() => { cargarMovimientos(); }, [cargarMovimientos]);
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => { setModalOpen(false); setEditingMovimiento(null); };
const handleSubmitModal = async (data: CreateEntradaSalidaCanillaDto | UpdateEntradaSalidaCanillaDto, idParte?: number) => {
setApiErrorMessage(null);
try {
if (idParte && editingMovimiento) {
await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data as UpdateEntradaSalidaCanillaDto);
} else {
await entradaSalidaCanillaService.createEntradaSalidaCanilla(data as CreateEntradaSalidaCanillaDto);
}
cargarMovimientos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (idParte: number) => {
if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) {
setApiErrorMessage(null);
try { await entradaSalidaCanillaService.deleteEntradaSalidaCanilla(idParte); cargarMovimientos(); }
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: EntradaSalidaCanillaDto) => {
setAnchorEl(event.currentTarget); setSelectedRow(item);
};
const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); };
const handleSelectRowForLiquidar = (idParte: number) => {
setSelectedIdsParaLiquidar(prev => {
const newSet = new Set(prev);
if (newSet.has(idParte)) newSet.delete(idParte);
else newSet.add(idParte);
return newSet;
});
};
const handleSelectAllForLiquidar = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
const newSelectedIds = new Set(movimientos.filter(m => !m.liquidado).map(m => m.idParte));
setSelectedIdsParaLiquidar(newSelectedIds);
} else {
setSelectedIdsParaLiquidar(new Set());
}
};
const handleOpenLiquidarDialog = () => {
if (selectedIdsParaLiquidar.size === 0) {
setApiErrorMessage("Seleccione al menos un movimiento para liquidar.");
return;
}
setOpenLiquidarDialog(true);
};
const handleCloseLiquidarDialog = () => setOpenLiquidarDialog(false);
const handleConfirmLiquidar = async () => {
setApiErrorMessage(null); setLoading(true);
const liquidarDto: LiquidarMovimientosCanillaRequestDto = {
idsPartesALiquidar: Array.from(selectedIdsParaLiquidar),
fechaLiquidacion: fechaLiquidacionDialog
};
try {
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
cargarMovimientos(); // Recargar para ver los cambios
setOpenLiquidarDialog(false);
} catch (err: any) {
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al liquidar.';
setApiErrorMessage(msg);
} finally {
setLoading(false);
}
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
const numSelectedToLiquidate = selectedIdsParaLiquidar.size;
const numNotLiquidatedOnPage = displayData.filter(m => !m.liquidado).length;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Entradas/Salidas Canillitas</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="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
<InputLabel>Publicación</InputLabel>
<Select value={filtroIdPublicacion} label="Publicación" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}>
<MenuItem value=""><em>Todas</em></MenuItem>
{publicaciones.map(p => <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
<InputLabel>Canillita</InputLabel>
<Select value={filtroIdCanilla} label="Canillita" onChange={(e) => setFiltroIdCanilla(e.target.value as number | string)}>
<MenuItem value=""><em>Todos</em></MenuItem>
{canillitas.map(c => <MenuItem key={c.idCanilla} value={c.idCanilla}>{c.nomApe}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
<InputLabel>Estado Liquidación</InputLabel>
<Select value={filtroEstadoLiquidacion} label="Estado Liquidación" onChange={(e) => setFiltroEstadoLiquidacion(e.target.value as 'todos' | 'liquidados' | 'noLiquidados')}>
<MenuItem value="noLiquidados">No Liquidados</MenuItem>
<MenuItem value="liquidados">Liquidados</MenuItem>
<MenuItem value="todos">Todos</MenuItem>
</Select>
</FormControl>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Movimiento</Button>)}
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && numSelectedToLiquidate > 0 && (
<Button variant="contained" color="success" startIcon={<PlaylistAddCheckIcon />} onClick={handleOpenLiquidarDialog}>
Liquidar Seleccionados ({numSelectedToLiquidate})
</Button>
)}
</Box>
</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 && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' &&
<TableCell padding="checkbox">
<Checkbox
indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage}
checked={numNotLiquidatedOnPage > 0 && numSelectedToLiquidate === numNotLiquidatedOnPage}
onChange={handleSelectAllForLiquidar}
disabled={numNotLiquidatedOnPage === 0}
/>
</TableCell>
}
<TableCell>Fecha</TableCell><TableCell>Publicación</TableCell><TableCell>Canillita</TableCell>
<TableCell align="right">Salida</TableCell><TableCell align="right">Entrada</TableCell>
<TableCell align="right">Vendidos</TableCell><TableCell align="right">A Rendir</TableCell>
<TableCell>Liquidado</TableCell><TableCell>F. Liq.</TableCell><TableCell>Obs.</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' ? 12 : 11} align="center">No se encontraron movimientos.</TableCell></TableRow>
) : (
displayData.map((m) => (
<TableRow key={m.idParte} hover selected={selectedIdsParaLiquidar.has(m.idParte)}>
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' &&
<TableCell padding="checkbox">
<Checkbox
checked={selectedIdsParaLiquidar.has(m.idParte)}
onChange={() => handleSelectRowForLiquidar(m.idParte)}
disabled={m.liquidado}
/>
</TableCell>
}
<TableCell>{formatDate(m.fecha)}</TableCell>
<TableCell>{m.nombrePublicacion}</TableCell>
<TableCell>{m.nomApeCanilla}</TableCell>
<TableCell align="right">{m.cantSalida}</TableCell>
<TableCell align="right">{m.cantEntrada}</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>{m.vendidos}</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>${m.montoARendir.toFixed(2)}</TableCell>
<TableCell align="center">{m.liquidado ? <Chip label="Sí" color="success" size="small" /> : <Chip label="No" size="small" />}</TableCell>
<TableCell>{m.fechaLiquidado ? formatDate(m.fechaLiquidado) : '-'}</TableCell>
<TableCell><Tooltip title={m.observacion || ''}><Box sx={{ maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.observacion || '-'}</Box></Tooltip></TableCell>
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton
onClick={(e) => handleMenuOpen(e, m)}
disabled={
// Deshabilitar si no tiene ningún permiso de eliminación O
// si está liquidado y no tiene permiso para eliminar liquidados
!((!m.liquidado && puedeEliminar) || (m.liquidado && puedeEliminarLiquidados))
}
>
<MoreVertIcon />
</IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]} component="div" count={movimientos.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && selectedRow && !selectedRow.liquidado && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
{selectedRow && (
(!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados)
) && (
<MenuItem onClick={() => handleDelete(selectedRow.idParte)}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar
</MenuItem>
)}
</Menu>
<EntradaSalidaCanillaFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingMovimiento} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
<DialogTitle>Confirmar Liquidación</DialogTitle>
<DialogContent>
<DialogContentText>
Se marcarán como liquidados {selectedIdsParaLiquidar.size} movimiento(s).
</DialogContentText>
<TextField autoFocus margin="dense" id="fechaLiquidacion" label="Fecha de Liquidación" type="date"
fullWidth variant="standard" value={fechaLiquidacionDialog}
onChange={(e) => setFechaLiquidacionDialog(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseLiquidarDialog} color="secondary" disabled={loading}>Cancelar</Button>
<Button onClick={handleConfirmLiquidar} variant="contained" color="primary" disabled={loading || !fechaLiquidacionDialog}>
{loading ? <CircularProgress size={24} /> : "Liquidar"}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default GestionarEntradasSalidasCanillaPage;

View File

@@ -1,8 +1,9 @@
// src/pages/Distribucion/GestionarEntradasSalidasDistPage.tsx
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
@@ -52,36 +53,30 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVer = isSuperAdmin || tienePermiso("MD001");
const puedeGestionar = isSuperAdmin || tienePermiso("MD002"); // Para Crear, Editar, Eliminar
const puedeGestionar = isSuperAdmin || tienePermiso("MD002");
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const [pubsData, distData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true),
distribuidorService.getAllDistribuidores()
]);
setPublicaciones(pubsData);
setDistribuidores(distData);
const [pubsData, distData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true),
distribuidorService.getAllDistribuidores()
]);
setPublicaciones(pubsData);
setDistribuidores(distData);
} catch (err) {
console.error("Error cargando datos para filtros:", err);
setError("Error al cargar opciones de filtro.");
} finally {
setLoadingFiltersDropdown(false);
}
console.error(err); setError("Error al cargar opciones de filtro.");
} finally { setLoadingFiltersDropdown(false); }
}, []);
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
const cargarMovimientos = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección."); setLoading(false); return;
}
if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; }
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const params = {
fechaDesde: filtroFechaDesde || null,
fechaHasta: filtroFechaHasta || null,
fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null,
idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null,
idDistribuidor: filtroIdDistribuidor ? Number(filtroIdDistribuidor) : null,
tipoMovimiento: filtroTipoMov || null,
@@ -89,7 +84,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
const data = await entradaSalidaDistService.getAllEntradasSalidasDist(params);
setMovimientos(data);
} catch (err) {
console.error(err); setError('Error al cargar los movimientos.');
console.error(err); setError('Error al cargar movimientos.');
} finally { setLoading(false); }
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdDistribuidor, filtroTipoMov]);
@@ -98,9 +93,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
const handleOpenModal = (item?: EntradaSalidaDistDto) => {
setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingMovimiento(null);
};
const handleCloseModal = () => { setModalOpen(false); setEditingMovimiento(null); };
const handleSubmitModal = async (data: CreateEntradaSalidaDistDto | UpdateEntradaSalidaDistDto, idParte?: number) => {
setApiErrorMessage(null);
@@ -112,21 +105,16 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
}
cargarMovimientos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el movimiento.';
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (idParte: number) => {
if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})? Esta acción revertirá el impacto en el saldo del distribuidor.`)) {
setApiErrorMessage(null);
try {
await entradaSalidaDistService.deleteEntradaSalidaDist(idParte);
cargarMovimientos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
setApiErrorMessage(message);
}
if (window.confirm(`¿Seguro (ID: ${idParte})? Esto revertirá el saldo.`)) {
setApiErrorMessage(null);
try { await entradaSalidaDistService.deleteEntradaSalidaDist(idParte); cargarMovimientos(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
}
handleMenuClose();
};
@@ -134,9 +122,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaDistDto) => {
setAnchorEl(event.currentTarget); setSelectedRow(item);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedRow(null);
};
const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); };
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -145,97 +131,96 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Entradas/Salidas Distribuidores</Typography>
<Typography variant="h4" gutterBottom>Entradas/Salidas a Distribuidores</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="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/>
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/>
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}>
<InputLabel>Publicación</InputLabel>
<Select value={filtroIdPublicacion} label="Publicación" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}>
<MenuItem value=""><em>Todas</em></MenuItem>
{publicaciones.map(p => <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}>
<InputLabel>Distribuidor</InputLabel>
<Select value={filtroIdDistribuidor} label="Distribuidor" onChange={(e) => setFiltroIdDistribuidor(e.target.value as number | string)}>
<MenuItem value=""><em>Todos</em></MenuItem>
{distribuidores.map(d => <MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 150, flexGrow: 1}}>
<InputLabel>Tipo</InputLabel>
<Select value={filtroTipoMov} label="Tipo" onChange={(e) => setFiltroTipoMov(e.target.value as 'Salida' | 'Entrada' | '')}>
<MenuItem value=""><em>Todos</em></MenuItem>
<MenuItem value="Salida">Salida</MenuItem>
<MenuItem value="Entrada">Entrada</MenuItem>
</Select>
</FormControl>
</Box>
{puedeGestionar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Movimiento</Button>)}
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
<InputLabel>Publicación</InputLabel>
<Select value={filtroIdPublicacion} label="Publicación" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}>
<MenuItem value=""><em>Todas</em></MenuItem>
{publicaciones.map(p => <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
<InputLabel>Distribuidor</InputLabel>
<Select value={filtroIdDistribuidor} label="Distribuidor" onChange={(e) => setFiltroIdDistribuidor(e.target.value as number | string)}>
<MenuItem value=""><em>Todos</em></MenuItem>
{distribuidores.map(d => <MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 150, flexGrow: 1 }}>
<InputLabel>Tipo</InputLabel>
<Select value={filtroTipoMov} label="Tipo" onChange={(e) => setFiltroTipoMov(e.target.value as 'Salida' | 'Entrada' | '')}>
<MenuItem value=""><em>Todos</em></MenuItem>
<MenuItem value="Salida">Salida</MenuItem>
<MenuItem value="Entrada">Entrada</MenuItem>
</Select>
</FormControl>
</Box>
{puedeGestionar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Movimiento</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>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{!loading && !error && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Publicación (Empresa)</TableCell>
<TableCell>Distribuidor</TableCell><TableCell>Tipo</TableCell>
<TableCell align="right">Cantidad</TableCell><TableCell>Remito</TableCell>
<TableCell align="right">Monto Afectado</TableCell><TableCell>Obs.</TableCell>
{puedeGestionar && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeGestionar ? 9 : 8} align="center">No se encontraron movimientos.</TableCell></TableRow>
) : (
displayData.map((m) => (
<TableRow key={m.idParte} hover>
<TableCell>{formatDate(m.fecha)}</TableCell>
<TableCell>{m.nombrePublicacion} ({m.nombreEmpresaPublicacion})</TableCell>
<TableCell>{m.nombreDistribuidor}</TableCell>
<TableCell>
<Chip label={m.tipoMovimiento} color={m.tipoMovimiento === 'Salida' ? 'primary' : 'secondary'} size="small"/>
</TableCell>
<TableCell align="right">{m.cantidad}</TableCell>
<TableCell>{m.remito}</TableCell>
<TableCell align="right" sx={{color: m.montoCalculado < 0 ? 'green' : (m.montoCalculado > 0 ? 'red' : 'inherit')}}>
${m.montoCalculado.toFixed(2)}
</TableCell>
<TableCell>{m.observacion || '-'}</TableCell>
{puedeGestionar && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, m)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 50]} component="div" count={movimientos.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Publicación (Empresa)</TableCell>
<TableCell>Distribuidor</TableCell><TableCell>Tipo</TableCell>
<TableCell align="right">Cantidad</TableCell><TableCell>Remito</TableCell>
<TableCell align="right">Monto Afectado</TableCell><TableCell>Obs.</TableCell>
{puedeGestionar && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeGestionar ? 9 : 8} align="center">No se encontraron movimientos.</TableCell></TableRow>
) : (
displayData.map((m) => (
<TableRow key={m.idParte} hover>
<TableCell>{formatDate(m.fecha)}</TableCell>
<TableCell>{m.nombrePublicacion} <Chip label={m.nombreEmpresaPublicacion} size="small" variant="outlined" sx={{ ml: 0.5 }} /></TableCell>
<TableCell>{m.nombreDistribuidor}</TableCell>
<TableCell>
<Chip label={m.tipoMovimiento} color={m.tipoMovimiento === 'Salida' ? 'success' : 'error'} size="small" variant="outlined" />
</TableCell>
<TableCell align="right">{m.cantidad}</TableCell>
<TableCell>{m.remito}</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', color: m.tipoMovimiento === 'Salida' ? 'success.main' : (m.montoCalculado === 0 ? 'inherit' : 'error.main') }}>
{m.tipoMovimiento === 'Salida' ? '$'+m.montoCalculado.toFixed(2) : '$-'+m.montoCalculado.toFixed(2) }
</TableCell>
<TableCell><Tooltip title={m.observacion || ''}><Box sx={{ maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.observacion || '-'}</Box></Tooltip></TableCell>
{puedeGestionar && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, m)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[10, 25, 50]} component="div" count={movimientos.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 && ( // O un permiso más específico si "eliminar" es diferente de "modificar"
<MenuItem onClick={() => handleDelete(selectedRow.idParte)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
{puedeGestionar && selectedRow && (
<MenuItem onClick={() => handleDelete(selectedRow.idParte)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)}
</Menu>
<EntradaSalidaDistFormModal

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;

View File

@@ -0,0 +1,256 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
CircularProgress, Alert, TablePagination, Tooltip, Autocomplete,
MenuItem,
FormControl,
InputLabel,
Select
} from '@mui/material';
import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto';
import FilterListIcon from '@mui/icons-material/FilterList';
import usuarioService from '../../../services/Usuarios/usuarioService';
import type { UsuarioHistorialDto } from '../../../models/dtos/Usuarios/Auditoria/UsuarioHistorialDto';
import { usePermissions } from '../../../hooks/usePermissions';
const GestionarAuditoriaUsuariosPage: React.FC = () => {
const [historial, setHistorial] = useState<UsuarioHistorialDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Filtros
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
const [filtroIdUsuarioAfectado, setFiltroIdUsuarioAfectado] = useState<UsuarioDto | null>(null);
const [filtroIdUsuarioModifico, setFiltroIdUsuarioModifico] = useState<UsuarioDto | null>(null);
const [filtroTipoMod, setFiltroTipoMod] = useState('');
const [usuariosParaDropdown, setUsuariosParaDropdown] = useState<UsuarioDto[]>([]);
const [tiposModificacionParaDropdown, setTiposModificacionParaDropdown] = useState<string[]>([]);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVerAuditoria = isSuperAdmin || tienePermiso("AU001"); // O el permiso que definas
const fetchDropdownData = useCallback(async () => {
if (!puedeVerAuditoria) return;
setLoadingDropdowns(true);
try {
const usuariosData = await usuarioService.getAllUsuarios(); // Asumiendo que tienes este método
setUsuariosParaDropdown(usuariosData);
// Opción B para Tipos de Modificación (desde backend)
// const tiposModData = await apiClient.get<string[]>('/auditoria/tipos-modificacion'); // Ajusta el endpoint si lo creas
// setTiposModificacionParaDropdown(tiposModData.data);
// Opción A (Hardcodeado en Frontend - más simple para empezar)
setTiposModificacionParaDropdown([
"Creado", "Insertada",
"Actualizado", "Modificada",
"Eliminado", "Eliminada",
"Baja", "Alta",
"Liquidada",
"Eliminado (Cascada)"
].sort());
} catch (err) {
console.error("Error al cargar datos para dropdowns de auditoría:", err);
setError("Error al cargar opciones de filtro."); // O un error más específico
} finally {
setLoadingDropdowns(false);
}
}, [puedeVerAuditoria]);
const cargarHistorial = useCallback(async () => {
if (!puedeVerAuditoria) {
setError("No tiene permiso para ver el historial de auditoría.");
setLoading(false);
return;
}
setLoading(true); setError(null);
try {
let data;
// Ahora usamos los IDs de los objetos UsuarioDto seleccionados
const idAfectado = filtroIdUsuarioAfectado ? filtroIdUsuarioAfectado.id : null;
const idModifico = filtroIdUsuarioModifico ? filtroIdUsuarioModifico.id : null;
if (idAfectado) { // Si se seleccionó un usuario afectado específico
data = await usuarioService.getHistorialDeUsuario(idAfectado, {
fechaDesde: filtroFechaDesde || undefined,
fechaHasta: filtroFechaHasta || undefined,
});
} else { // Sino, buscar en todo el historial con los otros filtros
data = await usuarioService.getTodoElHistorialDeUsuarios({
fechaDesde: filtroFechaDesde || undefined,
fechaHasta: filtroFechaHasta || undefined,
idUsuarioModifico: idModifico || undefined,
tipoModificacion: filtroTipoMod || undefined,
});
}
setHistorial(data);
} catch (err: any) {
console.error(err);
setError(err.response?.data?.message || 'Error al cargar el historial de usuarios.');
} finally {
setLoading(false);
}
}, [puedeVerAuditoria, filtroFechaDesde, filtroFechaHasta, filtroIdUsuarioAfectado, filtroIdUsuarioModifico, filtroTipoMod]);
useEffect(() => {
fetchDropdownData();
}, [fetchDropdownData]); // Cargar al montar
useEffect(() => {
// Cargar historial cuando los filtros cambian o al inicio si puedeVerAuditoria está listo
if (puedeVerAuditoria) {
cargarHistorial();
}
}, [cargarHistorial, puedeVerAuditoria]); // Quitar dependencias de filtro directo para evitar llamadas múltiples, handleFiltrar se encarga.
const handleFiltrar = () => {
setPage(0);
cargarHistorial(); // cargarHistorial ahora usa los estados de filtro directamente
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString('es-AR', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = historial.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeVerAuditoria) {
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Auditoría de Usuarios</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 }}>
<Autocomplete
options={usuariosParaDropdown}
getOptionLabel={(option) => `${option.nombre} ${option.apellido} (${option.user})`}
value={filtroIdUsuarioAfectado}
onChange={(_event, newValue) => {
setFiltroIdUsuarioAfectado(newValue);
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderInput={(params) => (
<TextField {...params} label="Usuario Afectado (Opcional)" size="small" sx={{ minWidth: 250 }} />
)}
loading={loadingDropdowns}
disabled={loadingDropdowns}
sx={{ flexGrow: 1 }}
/>
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} />
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} />
<Autocomplete
options={usuariosParaDropdown}
getOptionLabel={(option) => `${option.nombre} ${option.apellido} (${option.user})`}
value={filtroIdUsuarioModifico}
onChange={(_event, newValue) => {
setFiltroIdUsuarioModifico(newValue);
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderInput={(params) => (
<TextField {...params} label="Usuario Modificó (Opcional)" size="small" sx={{ minWidth: 250 }} />
)}
loading={loadingDropdowns}
disabled={loadingDropdowns}
sx={{ flexGrow: 1 }}
/>
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingDropdowns}>
<InputLabel>Tipo Modificación</InputLabel>
<Select
value={filtroTipoMod}
label="Tipo Modificación"
onChange={(e) => setFiltroTipoMod(e.target.value as string)}
>
<MenuItem value=""><em>Todos</em></MenuItem>
{tiposModificacionParaDropdown.map((tipo) => (
<MenuItem key={tipo} value={tipo}>{tipo}</MenuItem>
))}
</Select>
</FormControl>
<Button variant="outlined" onClick={handleFiltrar} size="small" disabled={loading || loadingDropdowns}>Filtrar</Button>
</Box>
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow sx={{ backgroundColor: 'action.hover' }}>
<TableCell>Fecha</TableCell>
<TableCell>Usuario Afectado (ID)</TableCell>
<TableCell>Username Afectado</TableCell>
<TableCell>Acción</TableCell>
<TableCell>Modificado Por (ID)</TableCell>
<TableCell>Nombre Modificador</TableCell>
<TableCell>Detalles (Simplificado)</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={7} align="center">No se encontraron registros de historial.</TableCell></TableRow>
) : (
displayData.map((h) => (
<TableRow key={h.idHist} hover>
<TableCell><Tooltip title={h.fechaModificacion}><Box>{formatDate(h.fechaModificacion)}</Box></Tooltip></TableCell>
<TableCell>{h.idUsuarioAfectado}</TableCell>
<TableCell>{h.userAfectado}</TableCell>
<TableCell>{h.tipoModificacion}</TableCell>
<TableCell>{h.idUsuarioModifico}</TableCell>
<TableCell>{h.nombreUsuarioModifico}</TableCell>
<TableCell>
<Tooltip title={
`User: ${h.userAnt || '-'} -> ${h.userNvo}\n` +
`Nombre: ${h.nombreAnt || '-'} -> ${h.nombreNvo}\n` +
`Apellido: ${h.apellidoAnt || '-'} -> ${h.apellidoNvo}\n` +
`Habilitado: ${h.habilitadaAnt ?? '-'} -> ${h.habilitadaNva}\n` +
`SupAdmin: ${h.supAdminAnt ?? '-'} -> ${h.supAdminNvo}\n` +
`Perfil: ${h.nombrePerfilAnt || h.idPerfilAnt || '-'} -> ${h.nombrePerfilNvo} (${h.idPerfilNvo})\n` +
`CambiaClave: ${h.debeCambiarClaveAnt ?? '-'} -> ${h.debeCambiarClaveNva}`
}>
<Box sx={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{`User: ${h.userAnt?.substring(0, 5)}..->${h.userNvo.substring(0, 5)}.., Nom: ${h.nombreAnt?.substring(0, 3)}..->${h.nombreNvo.substring(0, 3)}..`}
</Box>
</Tooltip>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]} component="div" count={historial.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
</Box>
);
};
export default GestionarAuditoriaUsuariosPage;

View File

@@ -6,6 +6,7 @@ const usuariosSubModules = [
{ label: 'Perfiles', path: 'perfiles' },
{ label: 'Permisos (Definición)', path: 'permisos' },
{ label: 'Usuarios', path: 'gestion-usuarios' },
{ label: 'Auditoría Usuarios', path: 'auditoria-usuarios' },
];
const UsuariosIndexPage: React.FC = () => {