Ya perdí el hilo de los cambios pero ahi van.
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, RadioGroup, FormControlLabel, Radio, InputAdornment
|
||||
} from '@mui/material';
|
||||
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 { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { DistribuidorDto } from '../../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; // Para el dropdown
|
||||
import empresaService from '../../../services/Distribucion/empresaService';
|
||||
import distribuidorService from '../../../services/Distribucion/distribuidorService';
|
||||
import canillaService from '../../../services/Distribucion/canillaService'; // Para cargar canillitas
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo) ... */
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 550 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
type DestinoType = 'Distribuidores' | 'Canillas';
|
||||
type TipoNotaType = 'Credito' | 'Debito';
|
||||
|
||||
interface NotaCreditoDebitoFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateNotaDto | UpdateNotaDto, idNota?: number) => Promise<void>;
|
||||
initialData?: NotaCreditoDebitoDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const NotaCreditoDebitoFormModal: React.FC<NotaCreditoDebitoFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [destino, setDestino] = useState<DestinoType>('Distribuidores');
|
||||
const [idDestino, setIdDestino] = useState<number | string>('');
|
||||
const [referencia, setReferencia] = useState('');
|
||||
const [tipo, setTipo] = useState<TipoNotaType>('Credito');
|
||||
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [monto, setMonto] = useState<string>('');
|
||||
const [observaciones, setObservaciones] = useState('');
|
||||
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
||||
|
||||
const [destinatarios, setDestinatarios] = useState<(DistribuidorDto | CanillaDto)[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
const fetchEmpresas = useCallback(async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const data = await empresaService.getAllEmpresas();
|
||||
setEmpresas(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar empresas", error);
|
||||
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar empresas.'}));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchDestinatarios = useCallback(async (tipoDestino: DestinoType) => {
|
||||
setLoadingDropdowns(true);
|
||||
setIdDestino(''); // Resetear selección de destinatario al cambiar tipo
|
||||
setDestinatarios([]);
|
||||
try {
|
||||
if (tipoDestino === 'Distribuidores') {
|
||||
const data = await distribuidorService.getAllDistribuidores();
|
||||
setDestinatarios(data);
|
||||
} else if (tipoDestino === 'Canillas') {
|
||||
const data = await canillaService.getAllCanillas(undefined, undefined, true); // Solo activos
|
||||
setDestinatarios(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error al cargar ${tipoDestino}`, error);
|
||||
setLocalErrors(prev => ({...prev, dropdowns: `Error al cargar ${tipoDestino}.`}));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
fetchEmpresas();
|
||||
// Cargar destinatarios basados en el initialData o el default
|
||||
const initialDestinoType = initialData?.destino || 'Distribuidores';
|
||||
setDestino(initialDestinoType as DestinoType);
|
||||
fetchDestinatarios(initialDestinoType as DestinoType);
|
||||
|
||||
setIdDestino(initialData?.idDestino || '');
|
||||
setReferencia(initialData?.referencia || '');
|
||||
setTipo(initialData?.tipo as TipoNotaType || 'Credito');
|
||||
setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]);
|
||||
setMonto(initialData?.monto?.toString() || '');
|
||||
setObservaciones(initialData?.observaciones || '');
|
||||
setIdEmpresa(initialData?.idEmpresa || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage, fetchEmpresas, fetchDestinatarios]);
|
||||
|
||||
useEffect(() => {
|
||||
if(open && !isEditing) { // Solo cambiar destinatarios si es creación y cambia el tipo de Destino
|
||||
fetchDestinatarios(destino);
|
||||
}
|
||||
}, [destino, open, isEditing, fetchDestinatarios]);
|
||||
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!destino) errors.destino = 'Seleccione un tipo de destino.';
|
||||
if (!idDestino) errors.idDestino = 'Seleccione un destinatario.';
|
||||
if (!tipo) errors.tipo = 'Seleccione el tipo de nota.';
|
||||
if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.';
|
||||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido.';
|
||||
if (!monto.trim() || isNaN(parseFloat(monto)) || parseFloat(monto) <= 0) errors.monto = 'Monto debe ser un número positivo.';
|
||||
if (!idEmpresa) errors.idEmpresa = 'Seleccione una empresa (para el saldo).';
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const montoNum = parseFloat(monto);
|
||||
|
||||
if (isEditing && initialData) {
|
||||
const dataToSubmit: UpdateNotaDto = {
|
||||
monto: montoNum,
|
||||
observaciones: observaciones || undefined,
|
||||
};
|
||||
await onSubmit(dataToSubmit, initialData.idNota);
|
||||
} else {
|
||||
const dataToSubmit: CreateNotaDto = {
|
||||
destino,
|
||||
idDestino: Number(idDestino),
|
||||
referencia: referencia || undefined,
|
||||
tipo,
|
||||
fecha,
|
||||
monto: montoNum,
|
||||
observaciones: observaciones || undefined,
|
||||
idEmpresa: Number(idEmpresa),
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de NotaCreditoDebitoFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Nota de Crédito/Débito' : 'Registrar Nota de Crédito/Débito'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<FormControl component="fieldset" margin="dense" required disabled={isEditing}>
|
||||
<Typography component="legend" variant="body2" sx={{mb:0.5}}>Destino</Typography>
|
||||
<RadioGroup row value={destino} onChange={(e) => {setDestino(e.target.value as DestinoType); handleInputChange('destino'); }}>
|
||||
<FormControlLabel value="Distribuidores" control={<Radio size="small"/>} label="Distribuidor" disabled={loading || isEditing}/>
|
||||
<FormControlLabel value="Canillas" control={<Radio size="small"/>} label="Canillita" disabled={loading || isEditing}/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idDestino} required disabled={isEditing}>
|
||||
<InputLabel id="destinatario-nota-select-label">Destinatario</InputLabel>
|
||||
<Select labelId="destinatario-nota-select-label" label="Destinatario" value={idDestino}
|
||||
onChange={(e) => {setIdDestino(e.target.value as number); handleInputChange('idDestino');}}
|
||||
disabled={loading || loadingDropdowns || isEditing || destinatarios.length === 0}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
||||
{destinatarios.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} {/* Muestra nomApe para Canilla, nombre para Distribuidor */}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{localErrors.idDestino && <Typography color="error" variant="caption">{localErrors.idDestino}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idEmpresa} required disabled={isEditing}>
|
||||
<InputLabel id="empresa-nota-select-label">Empresa (del Saldo)</InputLabel>
|
||||
<Select labelId="empresa-nota-select-label" label="Empresa (del Saldo)" value={idEmpresa}
|
||||
onChange={(e) => {setIdEmpresa(e.target.value as number); handleInputChange('idEmpresa');}}
|
||||
disabled={loading || loadingDropdowns || isEditing}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
||||
{empresas.map((e) => (<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idEmpresa && <Typography color="error" variant="caption">{localErrors.idEmpresa}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<TextField label="Fecha Nota" type="date" value={fecha} required
|
||||
onChange={(e) => {setFecha(e.target.value); handleInputChange('fecha');}}
|
||||
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
|
||||
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
|
||||
/>
|
||||
<TextField label="Referencia (Opcional)" value={referencia}
|
||||
onChange={(e) => setReferencia(e.target.value)}
|
||||
margin="dense" fullWidth disabled={loading || isEditing}
|
||||
/>
|
||||
<FormControl component="fieldset" margin="dense" error={!!localErrors.tipo} required>
|
||||
<Typography component="legend" variant="body2" sx={{mb:0.5}}>Tipo de Nota</Typography>
|
||||
<RadioGroup row value={tipo} onChange={(e) => {setTipo(e.target.value as TipoNotaType); handleInputChange('tipo');}} >
|
||||
<FormControlLabel value="Credito" control={<Radio size="small"/>} label="Crédito" disabled={loading || isEditing}/>
|
||||
<FormControlLabel value="Debito" control={<Radio size="small"/>} label="Débito" disabled={loading || isEditing}/>
|
||||
</RadioGroup>
|
||||
{localErrors.tipo && <Typography color="error" variant="caption">{localErrors.tipo}</Typography>}
|
||||
</FormControl>
|
||||
<TextField label="Monto" type="number" value={monto} required
|
||||
onChange={(e) => {setMonto(e.target.value); handleInputChange('monto');}}
|
||||
margin="dense" fullWidth error={!!localErrors.monto} helperText={localErrors.monto || ''}
|
||||
disabled={loading} inputProps={{step: "0.01", min:0.01, lang:"es-AR" }}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }}
|
||||
/>
|
||||
<TextField label="Observaciones (Opcional)" value={observaciones}
|
||||
onChange={(e) => setObservaciones(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={2} disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Registrar Nota')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotaCreditoDebitoFormModal;
|
||||
@@ -0,0 +1,248 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, RadioGroup, FormControlLabel, Radio, InputAdornment
|
||||
} from '@mui/material';
|
||||
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 { TipoPago } from '../../../models/Entities/TipoPago';
|
||||
import type { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto';
|
||||
import distribuidorService from '../../../services/Distribucion/distribuidorService';
|
||||
import tipoPagoService from '../../../services/Contables/tipoPagoService';
|
||||
import empresaService from '../../../services/Distribucion/empresaService';
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo) ... */
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 600 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
interface PagoDistribuidorFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreatePagoDistribuidorDto | UpdatePagoDistribuidorDto, idPago?: number) => Promise<void>;
|
||||
initialData?: PagoDistribuidorDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const PagoDistribuidorFormModal: React.FC<PagoDistribuidorFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [idDistribuidor, setIdDistribuidor] = useState<number | string>('');
|
||||
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [tipoMovimiento, setTipoMovimiento] = useState<'Recibido' | 'Realizado'>('Recibido');
|
||||
const [recibo, setRecibo] = useState<string>('');
|
||||
const [monto, setMonto] = useState<string>('');
|
||||
const [idTipoPago, setIdTipoPago] = useState<number | string>('');
|
||||
const [detalle, setDetalle] = useState('');
|
||||
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
||||
|
||||
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
|
||||
const [tiposPago, setTiposPago] = useState<TipoPago[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDropdownData = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [distData, tiposPagoData, empresasData] = await Promise.all([
|
||||
distribuidorService.getAllDistribuidores(),
|
||||
tipoPagoService.getAllTiposPago(),
|
||||
empresaService.getAllEmpresas()
|
||||
]);
|
||||
setDistribuidores(distData);
|
||||
setTiposPago(tiposPagoData);
|
||||
setEmpresas(empresasData);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar datos para dropdowns", error);
|
||||
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'}));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchDropdownData();
|
||||
setIdDistribuidor(initialData?.idDistribuidor || '');
|
||||
setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]);
|
||||
setTipoMovimiento(initialData?.tipoMovimiento || 'Recibido');
|
||||
setRecibo(initialData?.recibo?.toString() || '');
|
||||
setMonto(initialData?.monto?.toString() || '');
|
||||
setIdTipoPago(initialData?.idTipoPago || '');
|
||||
setDetalle(initialData?.detalle || '');
|
||||
setIdEmpresa(initialData?.idEmpresa || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!idDistribuidor) errors.idDistribuidor = 'Seleccione un distribuidor.';
|
||||
if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.';
|
||||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido.';
|
||||
if (!tipoMovimiento) errors.tipoMovimiento = 'Seleccione un tipo de movimiento.';
|
||||
if (!recibo.trim() || isNaN(parseInt(recibo)) || parseInt(recibo) <= 0) errors.recibo = 'Nro. Recibo es obligatorio y numérico.';
|
||||
if (!monto.trim() || isNaN(parseFloat(monto)) || parseFloat(monto) <= 0) errors.monto = 'Monto debe ser un número positivo.';
|
||||
if (!idTipoPago) errors.idTipoPago = 'Seleccione un tipo de pago.';
|
||||
if (!idEmpresa) errors.idEmpresa = 'Seleccione una empresa (para el saldo).';
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const montoNum = parseFloat(monto);
|
||||
|
||||
if (isEditing && initialData) {
|
||||
const dataToSubmit: UpdatePagoDistribuidorDto = {
|
||||
monto: montoNum,
|
||||
idTipoPago: Number(idTipoPago),
|
||||
detalle: detalle || undefined,
|
||||
};
|
||||
await onSubmit(dataToSubmit, initialData.idPago);
|
||||
} else {
|
||||
const dataToSubmit: CreatePagoDistribuidorDto = {
|
||||
idDistribuidor: Number(idDistribuidor),
|
||||
fecha,
|
||||
tipoMovimiento,
|
||||
recibo: parseInt(recibo, 10),
|
||||
monto: montoNum,
|
||||
idTipoPago: Number(idTipoPago),
|
||||
detalle: detalle || undefined,
|
||||
idEmpresa: Number(idEmpresa),
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de PagoDistribuidorFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Pago de Distribuidor' : 'Registrar Nuevo Pago de Distribuidor'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idDistribuidor} required disabled={isEditing}>
|
||||
<InputLabel id="distribuidor-pago-select-label">Distribuidor</InputLabel>
|
||||
<Select labelId="distribuidor-pago-select-label" label="Distribuidor" value={idDistribuidor}
|
||||
onChange={(e) => {setIdDistribuidor(e.target.value as number); handleInputChange('idDistribuidor');}}
|
||||
disabled={loading || loadingDropdowns || isEditing}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
||||
{distribuidores.map((d) => (<MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idDistribuidor && <Typography color="error" variant="caption">{localErrors.idDistribuidor}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idEmpresa} required disabled={isEditing}>
|
||||
<InputLabel id="empresa-pago-select-label">Empresa (del Saldo)</InputLabel>
|
||||
<Select labelId="empresa-pago-select-label" label="Empresa (del Saldo)" value={idEmpresa}
|
||||
onChange={(e) => {setIdEmpresa(e.target.value as number); handleInputChange('idEmpresa');}}
|
||||
disabled={loading || loadingDropdowns || isEditing}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
||||
{empresas.map((e) => (<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idEmpresa && <Typography color="error" variant="caption">{localErrors.idEmpresa}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<TextField label="Fecha Pago" type="date" value={fecha} required
|
||||
onChange={(e) => {setFecha(e.target.value); handleInputChange('fecha');}}
|
||||
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
|
||||
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
|
||||
/>
|
||||
|
||||
<FormControl component="fieldset" margin="dense" error={!!localErrors.tipoMovimiento} required>
|
||||
<Typography component="legend" variant="body2" sx={{mb:0.5}}>Tipo de Movimiento</Typography>
|
||||
<RadioGroup row value={tipoMovimiento} onChange={(e) => {setTipoMovimiento(e.target.value as 'Recibido' | 'Realizado'); handleInputChange('tipoMovimiento');}} >
|
||||
<FormControlLabel value="Recibido" control={<Radio size="small"/>} label="Recibido de Distribuidor" disabled={loading || isEditing}/>
|
||||
<FormControlLabel value="Realizado" control={<Radio size="small"/>} label="Realizado a Distribuidor" disabled={loading || isEditing}/>
|
||||
</RadioGroup>
|
||||
{localErrors.tipoMovimiento && <Typography color="error" variant="caption">{localErrors.tipoMovimiento}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<TextField label="Nro. Recibo" type="number" value={recibo} required
|
||||
onChange={(e) => {setRecibo(e.target.value); handleInputChange('recibo');}}
|
||||
margin="dense" fullWidth error={!!localErrors.recibo} helperText={localErrors.recibo || ''}
|
||||
disabled={loading || isEditing} inputProps={{min:1}}
|
||||
/>
|
||||
<TextField label="Monto" type="number" value={monto} required
|
||||
onChange={(e) => {setMonto(e.target.value); handleInputChange('monto');}}
|
||||
margin="dense" fullWidth error={!!localErrors.monto} helperText={localErrors.monto || ''}
|
||||
disabled={loading} inputProps={{step: "0.01", min:0.01, lang:"es-AR" }}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }}
|
||||
/>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idTipoPago} required>
|
||||
<InputLabel id="tipopago-pago-select-label">Tipo de Pago</InputLabel>
|
||||
<Select labelId="tipopago-pago-select-label" label="Tipo de Pago" value={idTipoPago}
|
||||
onChange={(e) => {setIdTipoPago(e.target.value as number); handleInputChange('idTipoPago');}}
|
||||
disabled={loading || loadingDropdowns}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
||||
{tiposPago.map((tp) => (<MenuItem key={tp.idTipoPago} value={tp.idTipoPago}>{tp.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idTipoPago && <Typography color="error" variant="caption">{localErrors.idTipoPago}</Typography>}
|
||||
</FormControl>
|
||||
<TextField label="Detalle (Opcional)" value={detalle}
|
||||
onChange={(e) => setDetalle(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={2} disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Registrar Pago')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PagoDistribuidorFormModal;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||
import type { TipoPago } from '../../../models/Entities/TipoPago';
|
||||
import type { CreateTipoPagoDto } from '../../../models/dtos/tiposPago/CreateTipoPagoDto';
|
||||
import type { CreateTipoPagoDto } from '../../../models/dtos/Contables/CreateTipoPagoDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
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'; // DTO de Empresa
|
||||
import empresaService from '../../../services//Distribucion/empresaService'; // Servicio de Empresa
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo) ... */
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 500 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
interface ControlDevolucionesFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateControlDevolucionesDto | UpdateControlDevolucionesDto, idControl?: number) => Promise<void>;
|
||||
initialData?: ControlDevolucionesDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const ControlDevolucionesFormModal: React.FC<ControlDevolucionesFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
||||
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [entrada, setEntrada] = useState<string>('');
|
||||
const [sobrantes, setSobrantes] = useState<string>('');
|
||||
const [detalle, setDetalle] = useState('');
|
||||
const [sinCargo, setSinCargo] = useState<string>('0'); // Default 0
|
||||
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingEmpresas, setLoadingEmpresas] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEmpresas = async () => {
|
||||
setLoadingEmpresas(true);
|
||||
try {
|
||||
const data = await empresaService.getAllEmpresas();
|
||||
setEmpresas(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar empresas", error);
|
||||
setLocalErrors(prev => ({...prev, empresas: 'Error al cargar empresas.'}));
|
||||
} finally {
|
||||
setLoadingEmpresas(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchEmpresas();
|
||||
setIdEmpresa(initialData?.idEmpresa || '');
|
||||
setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]);
|
||||
setEntrada(initialData?.entrada?.toString() || '0');
|
||||
setSobrantes(initialData?.sobrantes?.toString() || '0');
|
||||
setDetalle(initialData?.detalle || '');
|
||||
setSinCargo(initialData?.sinCargo?.toString() || '0');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!idEmpresa) errors.idEmpresa = 'Seleccione una empresa.';
|
||||
if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.';
|
||||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido.';
|
||||
|
||||
const entradaNum = parseInt(entrada, 10);
|
||||
const sobrantesNum = parseInt(sobrantes, 10);
|
||||
const sinCargoNum = parseInt(sinCargo, 10);
|
||||
|
||||
if (entrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) errors.entrada = 'Entrada debe ser un número >= 0.';
|
||||
if (sobrantes.trim() === '' || isNaN(sobrantesNum) || sobrantesNum < 0) errors.sobrantes = 'Sobrantes debe ser un número >= 0.';
|
||||
if (sinCargo.trim() === '' || isNaN(sinCargoNum) || sinCargoNum < 0) errors.sinCargo = 'Sin Cargo debe ser un número >= 0.';
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const commonData = {
|
||||
entrada: parseInt(entrada, 10),
|
||||
sobrantes: parseInt(sobrantes, 10),
|
||||
detalle: detalle || undefined,
|
||||
sinCargo: parseInt(sinCargo, 10),
|
||||
};
|
||||
|
||||
if (isEditing && initialData) {
|
||||
const dataToSubmit: UpdateControlDevolucionesDto = { ...commonData };
|
||||
await onSubmit(dataToSubmit, initialData.idControl);
|
||||
} else {
|
||||
const dataToSubmit: CreateControlDevolucionesDto = {
|
||||
...commonData,
|
||||
idEmpresa: Number(idEmpresa),
|
||||
fecha,
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de ControlDevolucionesFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Control de Devoluciones' : 'Registrar Control de Devoluciones'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idEmpresa} required>
|
||||
<InputLabel id="empresa-cd-select-label">Empresa</InputLabel>
|
||||
<Select labelId="empresa-cd-select-label" label="Empresa" value={idEmpresa}
|
||||
onChange={(e) => {setIdEmpresa(e.target.value as number); handleInputChange('idEmpresa');}}
|
||||
disabled={loading || loadingEmpresas || isEditing}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
||||
{empresas.map((e) => (<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idEmpresa && <Typography color="error" variant="caption">{localErrors.idEmpresa}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<TextField label="Fecha" type="date" value={fecha} required
|
||||
onChange={(e) => {setFecha(e.target.value); handleInputChange('fecha');}}
|
||||
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
|
||||
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
|
||||
/>
|
||||
<TextField label="Entrada (Devolución Total)" type="number" value={entrada} required
|
||||
onChange={(e) => {setEntrada(e.target.value); handleInputChange('entrada');}}
|
||||
margin="dense" fullWidth error={!!localErrors.entrada} helperText={localErrors.entrada || ''}
|
||||
disabled={loading} inputProps={{min:0}}
|
||||
/>
|
||||
<TextField label="Sobrantes" type="number" value={sobrantes} required
|
||||
onChange={(e) => {setSobrantes(e.target.value); handleInputChange('sobrantes');}}
|
||||
margin="dense" fullWidth error={!!localErrors.sobrantes} helperText={localErrors.sobrantes || ''}
|
||||
disabled={loading} inputProps={{min:0}}
|
||||
/>
|
||||
<TextField label="Sin Cargo" type="number" value={sinCargo} required
|
||||
onChange={(e) => {setSinCargo(e.target.value); handleInputChange('sinCargo');}}
|
||||
margin="dense" fullWidth error={!!localErrors.sinCargo} helperText={localErrors.sinCargo || ''}
|
||||
disabled={loading} inputProps={{min:0}}
|
||||
/>
|
||||
<TextField label="Detalle (Opcional)" value={detalle}
|
||||
onChange={(e) => setDetalle(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={2} disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.empresas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.empresas}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained" disabled={loading || loadingEmpresas}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Registrar Control')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ControlDevolucionesFormModal;
|
||||
@@ -0,0 +1,396 @@
|
||||
// src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, Paper, IconButton, FormHelperText
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
import type { EntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
|
||||
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 publicacionService from '../../../services/Distribucion/publicacionService';
|
||||
import canillaService from '../../../services/Distribucion/canillaService';
|
||||
import entradaSalidaCanillaService from '../../../services/Distribucion/entradaSalidaCanillaService';
|
||||
import type { CreateBulkEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto';
|
||||
import type { EntradaSalidaCanillaItemDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaItemDto';
|
||||
import axios from 'axios';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '95%', sm: '80%', md: '750px' },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 2.5,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
interface EntradaSalidaCanillaFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: UpdateEntradaSalidaCanillaDto, idParte: number) => Promise<void>;
|
||||
initialData?: EntradaSalidaCanillaDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
interface FormRowItem {
|
||||
id: string;
|
||||
idPublicacion: number | string;
|
||||
cantSalida: string;
|
||||
cantEntrada: string;
|
||||
observacion: string;
|
||||
}
|
||||
|
||||
const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage: parentErrorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [idCanilla, setIdCanilla] = useState<number | string>('');
|
||||
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [editIdPublicacion, setEditIdPublicacion] = useState<number | string>('');
|
||||
const [editCantSalida, setEditCantSalida] = useState<string>('0');
|
||||
const [editCantEntrada, setEditCantEntrada] = useState<string>('0');
|
||||
const [editObservacion, setEditObservacion] = useState('');
|
||||
const [items, setItems] = useState<FormRowItem[]>([]);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
const [modalSpecificApiError, setModalSpecificApiError] = useState<string | null>(null);
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDropdownData = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: null }));
|
||||
try {
|
||||
const [pubsData, canillitasData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
||||
canillaService.getAllCanillas(undefined, undefined, true)
|
||||
]);
|
||||
setPublicaciones(pubsData);
|
||||
setCanillitas(canillitasData);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar datos para dropdowns", error);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar datos necesarios (publicaciones/canillitas).' }));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchDropdownData();
|
||||
clearErrorMessage();
|
||||
setModalSpecificApiError(null);
|
||||
setLocalErrors({});
|
||||
|
||||
if (isEditing && initialData) {
|
||||
setIdCanilla(initialData.idCanilla || '');
|
||||
setFecha(initialData.fecha ? initialData.fecha.split('T')[0] : new Date().toISOString().split('T')[0]);
|
||||
setEditIdPublicacion(initialData.idPublicacion || '');
|
||||
setEditCantSalida(initialData.cantSalida?.toString() || '0');
|
||||
setEditCantEntrada(initialData.cantEntrada?.toString() || '0');
|
||||
setEditObservacion(initialData.observacion || '');
|
||||
setItems([]);
|
||||
} else {
|
||||
setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
setIdCanilla('');
|
||||
setFecha(new Date().toISOString().split('T')[0]);
|
||||
setEditCantSalida('0');
|
||||
setEditCantEntrada('0');
|
||||
setEditObservacion('');
|
||||
setEditIdPublicacion('');
|
||||
}
|
||||
}
|
||||
}, [open, initialData, isEditing, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const currentErrors: { [key: string]: string | null } = {};
|
||||
if (!idCanilla) currentErrors.idCanilla = 'Seleccione un canillita.';
|
||||
if (!fecha.trim()) currentErrors.fecha = 'La fecha es obligatoria.';
|
||||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) currentErrors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).';
|
||||
|
||||
if (isEditing) {
|
||||
const salidaNum = parseInt(editCantSalida, 10);
|
||||
const entradaNum = parseInt(editCantEntrada, 10);
|
||||
if (editCantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) {
|
||||
currentErrors.editCantSalida = 'Cant. Salida debe ser un número >= 0.';
|
||||
}
|
||||
if (editCantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) {
|
||||
currentErrors.editCantEntrada = 'Cant. Entrada debe ser un número >= 0.';
|
||||
} else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) {
|
||||
currentErrors.editCantEntrada = 'Cant. Entrada no puede ser mayor a Cant. Salida.';
|
||||
}
|
||||
} else {
|
||||
let hasValidItemWithQuantityOrPub = false;
|
||||
const publicacionIdsEnLote = new Set<number>();
|
||||
|
||||
if (items.length === 0) {
|
||||
currentErrors.general = "Debe agregar al menos una publicación.";
|
||||
}
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const salidaNum = parseInt(item.cantSalida, 10);
|
||||
const entradaNum = parseInt(item.cantEntrada, 10);
|
||||
const hasQuantity = !isNaN(salidaNum) && salidaNum >=0 && !isNaN(entradaNum) && entradaNum >=0 && (salidaNum > 0 || entradaNum > 0);
|
||||
const hasObservation = item.observacion.trim() !== '';
|
||||
|
||||
if (item.idPublicacion === '') {
|
||||
if (hasQuantity || hasObservation) {
|
||||
currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} obligatoria si hay datos.`;
|
||||
}
|
||||
} else {
|
||||
const pubIdNum = Number(item.idPublicacion);
|
||||
if (publicacionIdsEnLote.has(pubIdNum)) {
|
||||
currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} duplicada.`;
|
||||
} else {
|
||||
publicacionIdsEnLote.add(pubIdNum);
|
||||
}
|
||||
if (item.cantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) {
|
||||
currentErrors[`item_${item.id}_cantSalida`] = `Salida Pub. ${index + 1} inválida.`;
|
||||
}
|
||||
if (item.cantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) {
|
||||
currentErrors[`item_${item.id}_cantEntrada`] = `Entrada Pub. ${index + 1} inválida.`;
|
||||
} else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) {
|
||||
currentErrors[`item_${item.id}_cantEntrada`] = `Dev. Pub. ${index + 1} > Salida.`;
|
||||
}
|
||||
if (item.idPublicacion !== '') hasValidItemWithQuantityOrPub = true;
|
||||
}
|
||||
});
|
||||
|
||||
const allItemsAreEmptyAndNoPubSelected = items.every(
|
||||
itm => itm.idPublicacion === '' &&
|
||||
(itm.cantSalida.trim() === '' || parseInt(itm.cantSalida, 10) === 0) &&
|
||||
(itm.cantEntrada.trim() === '' || parseInt(itm.cantEntrada, 10) === 0) &&
|
||||
itm.observacion.trim() === ''
|
||||
);
|
||||
|
||||
if (!isEditing && items.length > 0 && !hasValidItemWithQuantityOrPub && !allItemsAreEmptyAndNoPubSelected) {
|
||||
currentErrors.general = "Debe seleccionar una publicación para los ítems con cantidades y/o observaciones.";
|
||||
} else if (!isEditing && items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida,10) > 0 || parseInt(i.cantEntrada,10) > 0)) && !allItemsAreEmptyAndNoPubSelected) {
|
||||
currentErrors.general = "Debe ingresar cantidades para al menos una publicación con datos significativos.";
|
||||
}
|
||||
}
|
||||
setLocalErrors(currentErrors);
|
||||
return Object.keys(currentErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (parentErrorMessage) clearErrorMessage();
|
||||
if (modalSpecificApiError) setModalSpecificApiError(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
setModalSpecificApiError(null);
|
||||
if (!validate()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
if (isEditing && initialData) {
|
||||
const salidaNum = parseInt(editCantSalida, 10);
|
||||
const entradaNum = parseInt(editCantEntrada, 10);
|
||||
const dataToSubmitSingle: UpdateEntradaSalidaCanillaDto = {
|
||||
cantSalida: salidaNum,
|
||||
cantEntrada: entradaNum,
|
||||
observacion: editObservacion.trim() || undefined,
|
||||
};
|
||||
await onSubmit(dataToSubmitSingle, initialData.idParte);
|
||||
} else {
|
||||
const itemsToSubmit: EntradaSalidaCanillaItemDto[] = items
|
||||
.filter(item =>
|
||||
item.idPublicacion !== '' &&
|
||||
( (parseInt(item.cantSalida, 10) >= 0 && parseInt(item.cantEntrada, 10) >= 0) && (parseInt(item.cantSalida,10) > 0 || parseInt(item.cantEntrada,10) > 0 ) || item.observacion.trim() !== '')
|
||||
)
|
||||
.map(item => ({
|
||||
idPublicacion: Number(item.idPublicacion),
|
||||
cantSalida: parseInt(item.cantSalida, 10) || 0,
|
||||
cantEntrada: parseInt(item.cantEntrada, 10) || 0,
|
||||
observacion: item.observacion.trim() || undefined,
|
||||
}));
|
||||
|
||||
if (itemsToSubmit.length === 0) {
|
||||
setLocalErrors(prev => ({...prev, general: "No hay movimientos válidos para registrar. Asegúrese de seleccionar una publicación y/o ingresar cantidades."}));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const bulkData: CreateBulkEntradaSalidaCanillaDto = {
|
||||
idCanilla: Number(idCanilla),
|
||||
fecha,
|
||||
items: itemsToSubmit,
|
||||
};
|
||||
await entradaSalidaCanillaService.createBulkEntradasSalidasCanilla(bulkData);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de EntradaSalidaCanillaFormModal:", error);
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
setModalSpecificApiError(error.response.data?.message || 'Error al procesar la solicitud.');
|
||||
} else {
|
||||
setModalSpecificApiError('Ocurrió un error inesperado.');
|
||||
}
|
||||
if (isEditing) throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddRow = () => {
|
||||
if (items.length >= publicaciones.length) {
|
||||
setLocalErrors(prev => ({ ...prev, general: "No se pueden agregar más filas que publicaciones disponibles." }));
|
||||
return;
|
||||
}
|
||||
setItems([...items, { id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
setLocalErrors(prev => ({ ...prev, general: null }));
|
||||
};
|
||||
|
||||
const handleRemoveRow = (idToRemove: string) => {
|
||||
if (items.length <= 1 && !isEditing) return;
|
||||
setItems(items.filter(item => item.id !== idToRemove));
|
||||
};
|
||||
|
||||
const handleItemChange = (id: string, field: keyof Omit<FormRowItem, 'id'>, value: string | number) => {
|
||||
setItems(items.map(itemRow => itemRow.id === id ? { ...itemRow, [field]: value } : itemRow)); // CORREGIDO: item a itemRow para evitar conflicto de nombres de variable con el `item` del map en el JSX
|
||||
if (localErrors[`item_${id}_${field}`]) { // Aquí item se refiere al id del item.
|
||||
setLocalErrors(prev => ({ ...prev, [`item_${id}_${field}`]: null }));
|
||||
}
|
||||
if (localErrors.general) setLocalErrors(prev => ({ ...prev, general: null }));
|
||||
if (parentErrorMessage) clearErrorMessage();
|
||||
if (modalSpecificApiError) setModalSpecificApiError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Movimiento Canillita' : 'Registrar Movimientos Canillita'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idCanilla} required>
|
||||
<InputLabel id="canilla-esc-select-label">Canillita</InputLabel>
|
||||
<Select labelId="canilla-esc-select-label" label="Canillita" value={idCanilla}
|
||||
onChange={(e) => { setIdCanilla(e.target.value as number); handleInputChange('idCanilla'); }}
|
||||
disabled={loading || loadingDropdowns || isEditing}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione un canillita</em></MenuItem>
|
||||
{canillitas.map((c) => (<MenuItem key={c.idCanilla} value={c.idCanilla}>{`${c.nomApe} (Leg: ${c.legajo || 'S/L'})`}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idCanilla && <FormHelperText>{localErrors.idCanilla}</FormHelperText>}
|
||||
</FormControl>
|
||||
|
||||
<TextField label="Fecha Movimientos" type="date" value={fecha} required
|
||||
onChange={(e) => { setFecha(e.target.value); handleInputChange('fecha'); }}
|
||||
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
|
||||
disabled={loading || isEditing} InputLabelProps={{ shrink: true }}
|
||||
autoFocus={!isEditing}
|
||||
/>
|
||||
|
||||
{isEditing && initialData && (
|
||||
<Paper elevation={1} sx={{ p: 1.5, mt: 1 }}>
|
||||
<Typography variant="body2" gutterBottom color="text.secondary">Editando para Publicación: {publicaciones.find(p=>p.idPublicacion === editIdPublicacion)?.nombre || `ID ${editIdPublicacion}`}</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, mt:0.5}}>
|
||||
<TextField label="Cant. Salida" type="number" value={editCantSalida}
|
||||
onChange={(e) => {setEditCantSalida(e.target.value); handleInputChange('editCantSalida');}}
|
||||
margin="dense" fullWidth error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''}
|
||||
disabled={loading} inputProps={{ min: 0 }} sx={{flex:1}}
|
||||
/>
|
||||
<TextField label="Cant. Entrada" type="number" value={editCantEntrada}
|
||||
onChange={(e) => {setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada');}}
|
||||
margin="dense" fullWidth error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''}
|
||||
disabled={loading} inputProps={{ min: 0 }} sx={{flex:1}}
|
||||
/>
|
||||
</Box>
|
||||
<TextField label="Observación (General)" value={editObservacion}
|
||||
onChange={(e) => setEditObservacion(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{mt:1}}
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{!isEditing && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mt: 1.5, mb: 0.5 }}>Movimientos por Publicación:</Typography>
|
||||
{items.map((itemRow, index) => ( // item renombrado a itemRow
|
||||
<Paper key={itemRow.id} elevation={1} sx={{ p: 1.5, mb: 1, position: 'relative' }}>
|
||||
{items.length > 1 && (
|
||||
<IconButton onClick={() => handleRemoveRow(itemRow.id)} color="error" size="small"
|
||||
sx={{ position: 'absolute', top: 4, right: 4, zIndex:1 }}
|
||||
aria-label="Quitar fila"
|
||||
>
|
||||
<DeleteIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, flexWrap: 'wrap' }}>
|
||||
<FormControl sx={{ minWidth: 160, flexBasis: 'calc(40% - 8px)', flexGrow:1 }} size="small" error={!!localErrors[`item_${itemRow.id}_idPublicacion`]}>
|
||||
<InputLabel required={parseInt(itemRow.cantSalida) > 0 || parseInt(itemRow.cantEntrada) > 0 || itemRow.observacion.trim() !== ''}>Pub. {index + 1}</InputLabel>
|
||||
<Select value={itemRow.idPublicacion} label={`Publicación ${index + 1}`}
|
||||
onChange={(e) => handleItemChange(itemRow.id, 'idPublicacion', e.target.value as number)}
|
||||
disabled={loading || loadingDropdowns}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
||||
{publicaciones.map((p) => (
|
||||
<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{localErrors[`item_${itemRow.id}_idPublicacion`] && <FormHelperText>{localErrors[`item_${itemRow.id}_idPublicacion`]}</FormHelperText>}
|
||||
</FormControl>
|
||||
<TextField label="Llevados" type="number" size="small" value={itemRow.cantSalida}
|
||||
onChange={(e) => handleItemChange(itemRow.id, 'cantSalida', e.target.value)}
|
||||
error={!!localErrors[`item_${itemRow.id}_cantSalida`]} helperText={localErrors[`item_${itemRow.id}_cantSalida`]}
|
||||
inputProps={{ min: 0 }} sx={{ width: 'auto', flexBasis: 'calc(15% - 8px)', minWidth: '80px' }}
|
||||
/>
|
||||
<TextField label="Devueltos" type="number" size="small" value={itemRow.cantEntrada}
|
||||
onChange={(e) => handleItemChange(itemRow.id, 'cantEntrada', e.target.value)}
|
||||
error={!!localErrors[`item_${itemRow.id}_cantEntrada`]} helperText={localErrors[`item_${itemRow.id}_cantEntrada`]}
|
||||
inputProps={{ min: 0 }} sx={{ width: 'auto', flexBasis: 'calc(15% - 8px)', minWidth: '80px' }}
|
||||
/>
|
||||
<TextField label="Obs." value={itemRow.observacion}
|
||||
onChange={(e) => handleItemChange(itemRow.id, 'observacion', e.target.value)}
|
||||
size="small" sx={{ flexGrow: 1, flexBasis: 'calc(25% - 8px)', minWidth: '120px' }} multiline maxRows={1}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
))}
|
||||
{localErrors.general && <Alert severity="error" sx={{ mt: 1 }}>{localErrors.general}</Alert>}
|
||||
<Button onClick={handleAddRow} startIcon={<AddIcon />} sx={{ mt: 1, alignSelf: 'flex-start' }} disabled={loading || loadingDropdowns || items.length >= publicaciones.length}>
|
||||
Agregar Publicación
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{parentErrorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{parentErrorMessage}</Alert>}
|
||||
{modalSpecificApiError && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{modalSpecificApiError}</Alert>}
|
||||
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Registrar Movimientos')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntradaSalidaCanillaFormModal;
|
||||
@@ -29,7 +29,7 @@ interface EntradaSalidaDistFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateEntradaSalidaDistDto | UpdateEntradaSalidaDistDto, idParte?: number) => Promise<void>;
|
||||
initialData?: EntradaSalidaDistDto | null; // Para editar
|
||||
initialData?: EntradaSalidaDistDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
@@ -63,8 +63,8 @@ const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [pubsData, distData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true), // Solo habilitadas
|
||||
distribuidorService.getAllDistribuidores()
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
||||
distribuidorService.getAllDistribuidores() // Asume que este servicio existe y funciona
|
||||
]);
|
||||
setPublicaciones(pubsData);
|
||||
setDistribuidores(distData);
|
||||
@@ -100,7 +100,7 @@ const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
|
||||
if (!cantidad.trim() || isNaN(parseInt(cantidad)) || parseInt(cantidad) <= 0) {
|
||||
errors.cantidad = 'La cantidad debe ser un número positivo.';
|
||||
}
|
||||
if (!isEditing && (!remito.trim() || isNaN(parseInt(remito)) || parseInt(remito) <= 0)) {
|
||||
if (!remito.trim() || isNaN(parseInt(remito)) || parseInt(remito) <= 0) { // Remito obligatorio en creación y edición
|
||||
errors.remito = 'El Nro. Remito es obligatorio y debe ser un número positivo.';
|
||||
}
|
||||
setLocalErrors(errors);
|
||||
@@ -123,6 +123,7 @@ const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
|
||||
const dataToSubmit: UpdateEntradaSalidaDistDto = {
|
||||
cantidad: parseInt(cantidad, 10),
|
||||
observacion: observacion || undefined,
|
||||
// Remito no se edita según el DTO de Update
|
||||
};
|
||||
await onSubmit(dataToSubmit, initialData.idParte);
|
||||
} else {
|
||||
@@ -184,15 +185,15 @@ const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
|
||||
/>
|
||||
|
||||
<FormControl component="fieldset" margin="dense" error={!!localErrors.tipoMovimiento} required>
|
||||
<Typography component="legend" variant="body2" sx={{mb:0.5}}>Tipo de Movimiento</Typography>
|
||||
<Typography component="legend" variant="body2" sx={{mb:0.5, fontSize:'0.8rem'}}>Tipo Movimiento</Typography>
|
||||
<RadioGroup row value={tipoMovimiento} onChange={(e) => {setTipoMovimiento(e.target.value as 'Salida' | 'Entrada'); handleInputChange('tipoMovimiento');}} >
|
||||
<FormControlLabel value="Salida" control={<Radio size="small"/>} label="Salida (a Distribuidor)" disabled={loading || isEditing}/>
|
||||
<FormControlLabel value="Entrada" control={<Radio size="small"/>} label="Entrada (de Distribuidor)" disabled={loading || isEditing}/>
|
||||
<FormControlLabel value="Salida" control={<Radio size="small"/>} label="Salida" disabled={loading || isEditing}/>
|
||||
<FormControlLabel value="Entrada" control={<Radio size="small"/>} label="Entrada" disabled={loading || isEditing}/>
|
||||
</RadioGroup>
|
||||
{localErrors.tipoMovimiento && <Typography color="error" variant="caption">{localErrors.tipoMovimiento}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<TextField label="Nro. Remito" type="number" value={remito} required={!isEditing}
|
||||
<TextField label="Nro. Remito" type="number" value={remito} required
|
||||
onChange={(e) => {setRemito(e.target.value); handleInputChange('remito');}}
|
||||
margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''}
|
||||
disabled={loading || isEditing} inputProps={{min:1}}
|
||||
|
||||
212
Frontend/src/components/Modals/Radios/CancionFormModal.tsx
Normal file
212
Frontend/src/components/Modals/Radios/CancionFormModal.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
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'; // Para el dropdown de ritmos
|
||||
import ritmoService from '../../../services/Radios/ritmoService'; // Para cargar ritmos
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo, pero más ancho y alto) ... */
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '95%', sm: '80%', md: '700px' }, // Más ancho
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 3,
|
||||
maxHeight: '90vh', // Permitir scroll
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
interface CancionFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateCancionDto | UpdateCancionDto, id?: number) => Promise<void>;
|
||||
initialData?: CancionDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
type CancionFormState = Omit<CreateCancionDto, 'pista' | 'idRitmo'> & {
|
||||
pista: string; // TextField siempre es string
|
||||
idRitmo: number | string; // Select puede ser string vacío
|
||||
};
|
||||
|
||||
|
||||
const CancionFormModal: React.FC<CancionFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const initialFormState: CancionFormState = {
|
||||
tema: '', compositorAutor: '', interprete: '', sello: '', placa: '',
|
||||
pista: '', introduccion: '', idRitmo: '', formato: '', album: ''
|
||||
};
|
||||
const [formState, setFormState] = useState<CancionFormState>(initialFormState);
|
||||
const [ritmos, setRitmos] = useState<RitmoDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingRitmos, setLoadingRitmos] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRitmos = async () => {
|
||||
setLoadingRitmos(true);
|
||||
try {
|
||||
const data = await ritmoService.getAllRitmos();
|
||||
setRitmos(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar ritmos", error);
|
||||
setLocalErrors(prev => ({...prev, ritmos: 'Error al cargar ritmos.'}));
|
||||
} finally {
|
||||
setLoadingRitmos(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchRitmos();
|
||||
if (initialData) {
|
||||
setFormState({
|
||||
tema: initialData.tema || '',
|
||||
compositorAutor: initialData.compositorAutor || '',
|
||||
interprete: initialData.interprete || '',
|
||||
sello: initialData.sello || '',
|
||||
placa: initialData.placa || '',
|
||||
pista: initialData.pista?.toString() || '',
|
||||
introduccion: initialData.introduccion || '',
|
||||
idRitmo: initialData.idRitmo || '',
|
||||
formato: initialData.formato || '',
|
||||
album: initialData.album || '',
|
||||
});
|
||||
} else {
|
||||
setFormState(initialFormState);
|
||||
}
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]); // No incluir initialFormState aquí directamente
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!formState.tema?.trim() && !formState.interprete?.trim()) { // Al menos tema o intérprete
|
||||
errors.tema = 'Se requiere al menos el Tema o el Intérprete.';
|
||||
errors.interprete = 'Se requiere al menos el Tema o el Intérprete.';
|
||||
}
|
||||
if (formState.pista.trim() && isNaN(parseInt(formState.pista))) {
|
||||
errors.pista = 'Pista debe ser un número.';
|
||||
}
|
||||
// Otras validaciones específicas si son necesarias
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | (Event & { target: { name: string; value: unknown } })) => {
|
||||
const { name, value } = event.target as { name: keyof CancionFormState, value: string };
|
||||
setFormState(prev => ({ ...prev, [name]: value }));
|
||||
if (localErrors[name]) {
|
||||
setLocalErrors(prev => ({ ...prev, [name]: null }));
|
||||
}
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSelectChange = (event: React.ChangeEvent<{ name?: string; value: unknown }>) => {
|
||||
const name = event.target.name as keyof CancionFormState;
|
||||
setFormState(prev => ({ ...prev, [name]: event.target.value as string | number }));
|
||||
if (localErrors[name]) {
|
||||
setLocalErrors(prev => ({ ...prev, [name]: null }));
|
||||
}
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const dataToSubmit: CreateCancionDto | UpdateCancionDto = {
|
||||
...formState,
|
||||
pista: formState.pista.trim() ? parseInt(formState.pista, 10) : null,
|
||||
idRitmo: formState.idRitmo ? Number(formState.idRitmo) : null,
|
||||
// Convertir strings vacíos a undefined para que no se envíen si son opcionales en el backend
|
||||
tema: formState.tema?.trim() || undefined,
|
||||
compositorAutor: formState.compositorAutor?.trim() || undefined,
|
||||
interprete: formState.interprete?.trim() || undefined,
|
||||
sello: formState.sello?.trim() || undefined,
|
||||
placa: formState.placa?.trim() || undefined,
|
||||
introduccion: formState.introduccion?.trim() || undefined,
|
||||
formato: formState.formato?.trim() || undefined,
|
||||
album: formState.album?.trim() || undefined,
|
||||
};
|
||||
|
||||
if (isEditing && initialData) {
|
||||
await onSubmit(dataToSubmit as UpdateCancionDto, initialData.id);
|
||||
} else {
|
||||
await onSubmit(dataToSubmit as CreateCancionDto);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de CancionFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Canción' : 'Agregar Nueva Canción'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
<TextField name="tema" label="Tema" value={formState.tema} onChange={handleChange} margin="dense" fullWidth error={!!localErrors.tema} helperText={localErrors.tema || ''} autoFocus />
|
||||
<TextField name="interprete" label="Intérprete" value={formState.interprete} onChange={handleChange} margin="dense" fullWidth error={!!localErrors.interprete} helperText={localErrors.interprete || ''} />
|
||||
<TextField name="compositorAutor" label="Compositor/Autor" value={formState.compositorAutor} onChange={handleChange} margin="dense" fullWidth />
|
||||
<TextField name="album" label="Álbum" value={formState.album} onChange={handleChange} margin="dense" fullWidth />
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt:1 }}>
|
||||
<TextField name="sello" label="Sello" value={formState.sello} onChange={handleChange} margin="dense" sx={{flex:1, minWidth: 'calc(50% - 8px)'}} />
|
||||
<TextField name="placa" label="Placa" value={formState.placa} onChange={handleChange} margin="dense" sx={{flex:1, minWidth: 'calc(50% - 8px)'}} />
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt:1}}>
|
||||
<TextField name="pista" label="N° Pista" type="number" value={formState.pista} onChange={handleChange} margin="dense" error={!!localErrors.pista} helperText={localErrors.pista || ''} sx={{flex:1, minWidth: 'calc(33% - 8px)'}}/>
|
||||
<FormControl fullWidth margin="dense" sx={{flex:2, minWidth: 'calc(67% - 8px)'}} disabled={loadingRitmos}>
|
||||
<InputLabel id="ritmo-cancion-select-label">Ritmo</InputLabel>
|
||||
<Select name="idRitmo" labelId="ritmo-cancion-select-label" label="Ritmo" value={formState.idRitmo} onChange={handleSelectChange as any}>
|
||||
<MenuItem value=""><em>Ninguno</em></MenuItem>
|
||||
{ritmos.map((r) => (<MenuItem key={r.id} value={r.id}>{r.nombreRitmo}</MenuItem>))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<TextField name="introduccion" label="Introducción (ej: (:10)/(3:30)/(F))" value={formState.introduccion} onChange={handleChange} margin="dense" fullWidth />
|
||||
<TextField name="formato" label="Formato (ej: REC, ANG)" value={formState.formato} onChange={handleChange} margin="dense" fullWidth />
|
||||
|
||||
</Box>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.ritmos && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.ritmos}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained" disabled={loading || loadingRitmos}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Canción')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CancionFormModal;
|
||||
111
Frontend/src/components/Modals/Radios/RitmoFormModal.tsx
Normal file
111
Frontend/src/components/Modals/Radios/RitmoFormModal.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import type { RitmoDto } from '../../../models/dtos/Radios/RitmoDto';
|
||||
import type { CreateRitmoDto } from '../../../models/dtos/Radios/CreateRitmoDto';
|
||||
import type { UpdateRitmoDto } from '../../../models/dtos/Radios/UpdateRitmoDto';
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo) ... */
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 400 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
interface RitmoFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateRitmoDto | UpdateRitmoDto, id?: number) => Promise<void>;
|
||||
initialData?: RitmoDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const RitmoFormModal: React.FC<RitmoFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [nombreRitmo, setNombreRitmo] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrorNombre, setLocalErrorNombre] = useState<string | null>(null);
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setNombreRitmo(initialData?.nombreRitmo || '');
|
||||
setLocalErrorNombre(null);
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
if (!nombreRitmo.trim()) {
|
||||
setLocalErrorNombre('El nombre del ritmo es obligatorio.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleInputChange = () => {
|
||||
if (localErrorNombre) setLocalErrorNombre(null);
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const dataToSubmit = { nombreRitmo };
|
||||
if (isEditing && initialData) {
|
||||
await onSubmit(dataToSubmit as UpdateRitmoDto, initialData.id);
|
||||
} else {
|
||||
await onSubmit(dataToSubmit as CreateRitmoDto);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de RitmoFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Ritmo' : 'Agregar Nuevo Ritmo'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<TextField label="Nombre del Ritmo" value={nombreRitmo} required
|
||||
onChange={(e) => {setNombreRitmo(e.target.value); handleInputChange();}}
|
||||
margin="dense" fullWidth error={!!localErrorNombre} helperText={localErrorNombre || ''}
|
||||
disabled={loading} autoFocus
|
||||
/>
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained" disabled={loading}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Ritmo')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RitmoFormModal;
|
||||
10
Frontend/src/models/dtos/Contables/CreateNotaDto.ts
Normal file
10
Frontend/src/models/dtos/Contables/CreateNotaDto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface CreateNotaDto {
|
||||
destino: 'Distribuidores' | 'Canillas';
|
||||
idDestino: number;
|
||||
referencia?: string | null;
|
||||
tipo: 'Debito' | 'Credito';
|
||||
fecha: string; // "yyyy-MM-dd"
|
||||
monto: number;
|
||||
observaciones?: string | null;
|
||||
idEmpresa: number;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface CreatePagoDistribuidorDto {
|
||||
idDistribuidor: number;
|
||||
fecha: string; // "yyyy-MM-dd"
|
||||
tipoMovimiento: 'Recibido' | 'Realizado';
|
||||
recibo: number;
|
||||
monto: number;
|
||||
idTipoPago: number;
|
||||
detalle?: string | null;
|
||||
idEmpresa: number;
|
||||
}
|
||||
13
Frontend/src/models/dtos/Contables/NotaCreditoDebitoDto.ts
Normal file
13
Frontend/src/models/dtos/Contables/NotaCreditoDebitoDto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface NotaCreditoDebitoDto {
|
||||
idNota: number;
|
||||
destino: 'Distribuidores' | 'Canillas';
|
||||
idDestino: number;
|
||||
nombreDestinatario: string;
|
||||
referencia?: string | null;
|
||||
tipo: 'Debito' | 'Credito';
|
||||
fecha: string; // "yyyy-MM-dd"
|
||||
monto: number;
|
||||
observaciones?: string | null;
|
||||
idEmpresa: number;
|
||||
nombreEmpresa: string;
|
||||
}
|
||||
14
Frontend/src/models/dtos/Contables/PagoDistribuidorDto.ts
Normal file
14
Frontend/src/models/dtos/Contables/PagoDistribuidorDto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface PagoDistribuidorDto {
|
||||
idPago: number;
|
||||
idDistribuidor: number;
|
||||
nombreDistribuidor: string;
|
||||
fecha: string; // "yyyy-MM-dd"
|
||||
tipoMovimiento: 'Recibido' | 'Realizado';
|
||||
recibo: number;
|
||||
monto: number;
|
||||
idTipoPago: number;
|
||||
nombreTipoPago: string;
|
||||
detalle?: string | null;
|
||||
idEmpresa: number;
|
||||
nombreEmpresa: string;
|
||||
}
|
||||
4
Frontend/src/models/dtos/Contables/UpdateNotaDto.ts
Normal file
4
Frontend/src/models/dtos/Contables/UpdateNotaDto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface UpdateNotaDto {
|
||||
monto: number;
|
||||
observaciones?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface UpdatePagoDistribuidorDto {
|
||||
monto: number;
|
||||
idTipoPago: number;
|
||||
detalle?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface ControlDevolucionesDto {
|
||||
idControl: number;
|
||||
idEmpresa: number;
|
||||
nombreEmpresa: string;
|
||||
fecha: string; // "yyyy-MM-dd"
|
||||
entrada: number;
|
||||
sobrantes: number;
|
||||
detalle?: string | null;
|
||||
sinCargo: number;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { EntradaSalidaCanillaItemDto } from './EntradaSalidaCanillaItemDto';
|
||||
|
||||
export interface CreateBulkEntradaSalidaCanillaDto {
|
||||
idCanilla: number;
|
||||
fecha: string; // "yyyy-MM-dd"
|
||||
items: EntradaSalidaCanillaItemDto[];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface CreateControlDevolucionesDto {
|
||||
idEmpresa: number;
|
||||
fecha: string; // "yyyy-MM-dd"
|
||||
entrada: number;
|
||||
sobrantes: number;
|
||||
detalle?: string | null;
|
||||
sinCargo: number;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface CreateEntradaSalidaCanillaDto {
|
||||
idPublicacion: number;
|
||||
idCanilla: number;
|
||||
fecha: string; // "yyyy-MM-dd"
|
||||
cantSalida: number;
|
||||
cantEntrada: number;
|
||||
observacion?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export interface EntradaSalidaCanillaDto {
|
||||
idParte: number;
|
||||
idPublicacion: number;
|
||||
nombrePublicacion: string;
|
||||
idCanilla: number;
|
||||
nomApeCanilla: string;
|
||||
canillaEsAccionista: boolean;
|
||||
fecha: string; // "yyyy-MM-dd"
|
||||
cantSalida: number;
|
||||
cantEntrada: number;
|
||||
vendidos: number;
|
||||
observacion?: string | null;
|
||||
liquidado: boolean;
|
||||
fechaLiquidado?: string | null; // "yyyy-MM-dd"
|
||||
userLiq?: number | null;
|
||||
nombreUserLiq?: string | null;
|
||||
montoARendir: number;
|
||||
precioUnitarioAplicado: number;
|
||||
recargoAplicado: number;
|
||||
porcentajeOMontoCanillaAplicado: number;
|
||||
esPorcentajeCanilla: boolean;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface EntradaSalidaCanillaItemDto {
|
||||
idPublicacion: number;
|
||||
cantSalida: number;
|
||||
cantEntrada: number;
|
||||
observacion?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface LiquidarMovimientosCanillaRequestDto {
|
||||
idsPartesALiquidar: number[];
|
||||
fechaLiquidacion: string; // "yyyy-MM-dd"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface UpdateControlDevolucionesDto {
|
||||
entrada: number;
|
||||
sobrantes: number;
|
||||
detalle?: string | null;
|
||||
sinCargo: number;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface UpdateEntradaSalidaCanillaDto {
|
||||
cantSalida: number;
|
||||
cantEntrada: number;
|
||||
observacion?: string | null;
|
||||
}
|
||||
14
Frontend/src/models/dtos/Radios/CancionDto.ts
Normal file
14
Frontend/src/models/dtos/Radios/CancionDto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface CancionDto {
|
||||
id: number;
|
||||
tema?: string | null;
|
||||
compositorAutor?: string | null;
|
||||
interprete?: string | null;
|
||||
sello?: string | null;
|
||||
placa?: string | null;
|
||||
pista?: number | null;
|
||||
introduccion?: string | null;
|
||||
idRitmo?: number | null;
|
||||
nombreRitmo?: string | null;
|
||||
formato?: string | null;
|
||||
album?: string | null;
|
||||
}
|
||||
12
Frontend/src/models/dtos/Radios/CreateCancionDto.ts
Normal file
12
Frontend/src/models/dtos/Radios/CreateCancionDto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface CreateCancionDto {
|
||||
tema?: string | null;
|
||||
compositorAutor?: string | null;
|
||||
interprete?: string | null;
|
||||
sello?: string | null;
|
||||
placa?: string | null;
|
||||
pista?: number | null;
|
||||
introduccion?: string | null;
|
||||
idRitmo?: number | null;
|
||||
formato?: string | null;
|
||||
album?: string | null;
|
||||
}
|
||||
3
Frontend/src/models/dtos/Radios/CreateRitmoDto.ts
Normal file
3
Frontend/src/models/dtos/Radios/CreateRitmoDto.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface CreateRitmoDto {
|
||||
nombreRitmo: string; // Al crear, usualmente es requerido
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface GenerarListaRadioRequestDto {
|
||||
mes: number;
|
||||
anio: number;
|
||||
institucion: "AADI" | "SADAIC"; // Tipos literales para restricción
|
||||
radio: "FM 99.1" | "FM 100.3"; // Tipos literales para restricción
|
||||
}
|
||||
4
Frontend/src/models/dtos/Radios/RitmoDto.ts
Normal file
4
Frontend/src/models/dtos/Radios/RitmoDto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface RitmoDto {
|
||||
id: number;
|
||||
nombreRitmo?: string | null; // La BD permite NULL para la columna Ritmo
|
||||
}
|
||||
12
Frontend/src/models/dtos/Radios/UpdateCancionDto.ts
Normal file
12
Frontend/src/models/dtos/Radios/UpdateCancionDto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface UpdateCancionDto {
|
||||
tema?: string | null;
|
||||
compositorAutor?: string | null;
|
||||
interprete?: string | null;
|
||||
sello?: string | null;
|
||||
placa?: string | null;
|
||||
pista?: number | null;
|
||||
introduccion?: string | null;
|
||||
idRitmo?: number | null;
|
||||
formato?: string | null;
|
||||
album?: string | null;
|
||||
}
|
||||
3
Frontend/src/models/dtos/Radios/UpdateRitmoDto.ts
Normal file
3
Frontend/src/models/dtos/Radios/UpdateRitmoDto.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface UpdateRitmoDto {
|
||||
nombreRitmo: string;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export interface UsuarioHistorialDto {
|
||||
idHist: number;
|
||||
idUsuarioAfectado: number;
|
||||
userAfectado: string;
|
||||
|
||||
userAnt?: string | null;
|
||||
userNvo: string;
|
||||
habilitadaAnt?: boolean | null;
|
||||
habilitadaNva: boolean;
|
||||
supAdminAnt?: boolean | null;
|
||||
supAdminNvo: boolean;
|
||||
nombreAnt?: string | null;
|
||||
nombreNvo: string;
|
||||
apellidoAnt?: string | null;
|
||||
apellidoNvo: string;
|
||||
idPerfilAnt?: number | null;
|
||||
idPerfilNvo: number;
|
||||
nombrePerfilAnt?: string | null;
|
||||
nombrePerfilNvo: string;
|
||||
debeCambiarClaveAnt?: boolean | null;
|
||||
debeCambiarClaveNva: boolean;
|
||||
|
||||
idUsuarioModifico: number;
|
||||
nombreUsuarioModifico: string;
|
||||
fechaModificacion: string; // vendrá como string ISO "2023-10-27T10:30:00"
|
||||
tipoModificacion: string;
|
||||
}
|
||||
@@ -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 = () => {
|
||||
|
||||
262
Frontend/src/pages/Contables/GestionarNotasCDPage.tsx
Normal file
262
Frontend/src/pages/Contables/GestionarNotasCDPage.tsx
Normal 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;
|
||||
231
Frontend/src/pages/Contables/GestionarPagosDistribuidorPage.tsx
Normal file
231
Frontend/src/pages/Contables/GestionarPagosDistribuidorPage.tsx
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
210
Frontend/src/pages/Radios/GenerarListasRadioPage.tsx
Normal file
210
Frontend/src/pages/Radios/GenerarListasRadioPage.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, CircularProgress, Alert,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormHelperText
|
||||
} from '@mui/material';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import radioListaService from '../../services/Radios/radioListaService';
|
||||
import type { GenerarListaRadioRequestDto } from '../../models/dtos/Radios/GenerarListaRadioRequestDto';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios'; // Para el manejo de errores de Axios
|
||||
|
||||
const GestionarListasRadioPage: React.FC = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentMonth = new Date().getMonth() + 1; // Meses son 0-indexados
|
||||
|
||||
const [mes, setMes] = useState<string>(currentMonth.toString());
|
||||
const [anio, setAnio] = useState<string>(currentYear.toString());
|
||||
const [institucion, setInstitucion] = useState<"AADI" | "SADAIC">("AADI");
|
||||
const [radio, setRadio] = useState<"FM 99.1" | "FM 100.3">("FM 99.1");
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
const [apiSuccessMessage, setApiSuccessMessage] = useState<string | null>(null);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
// Usar el permiso general de la sección Radios (SS005) o uno específico
|
||||
const puedeGenerar = isSuperAdmin || tienePermiso("SS005");
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
const numMes = parseInt(mes, 10);
|
||||
const numAnio = parseInt(anio, 10);
|
||||
|
||||
if (!mes.trim() || isNaN(numMes) || numMes < 1 || numMes > 12) {
|
||||
errors.mes = 'Mes debe ser un número entre 1 y 12.';
|
||||
}
|
||||
if (!anio.trim() || isNaN(numAnio) || numAnio < 2000 || numAnio > 2999) {
|
||||
errors.anio = 'Año debe ser válido (ej: 2024).';
|
||||
}
|
||||
if (!institucion) errors.institucion = 'Institución es obligatoria.'; // Aunque el estado tiene tipo, la validación es buena
|
||||
if (!radio) errors.radio = 'Radio es obligatoria.';
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleGenerarLista = async () => {
|
||||
clearMessages();
|
||||
if (!validate()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: GenerarListaRadioRequestDto = {
|
||||
mes: parseInt(mes, 10),
|
||||
anio: parseInt(anio, 10),
|
||||
institucion,
|
||||
radio,
|
||||
};
|
||||
|
||||
const blob = await radioListaService.generarListaRadio(params);
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
|
||||
// Construir el nombre de archivo como en VB.NET para el zip
|
||||
// Ejemplo AADI-FM99.1-FM-0524.xlsx.zip
|
||||
const mesTexto = params.mes.toString().padStart(2, '0');
|
||||
const anioCortoTexto = (params.anio % 100).toString().padStart(2, '0');
|
||||
let baseFileName = "";
|
||||
if (params.institucion === "AADI") {
|
||||
baseFileName = params.radio === "FM 99.1" ? `AADI-FM99.1-FM-${mesTexto}${anioCortoTexto}` : `AADI-FM100.3-FM-${mesTexto}${anioCortoTexto}`;
|
||||
} else { // SADAIC
|
||||
baseFileName = params.radio === "FM 99.1" ? `FM99.1-FM-${mesTexto}${anioCortoTexto}` : `FM100.3-FM-${mesTexto}${anioCortoTexto}`;
|
||||
}
|
||||
const defaultFileName = `${baseFileName}.zip`; // Nombre final del ZIP
|
||||
|
||||
link.setAttribute('download', defaultFileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode?.removeChild(link);
|
||||
window.URL.revokeObjectURL(url); // Liberar el objeto URL
|
||||
setApiSuccessMessage('Lista generada y descarga iniciada.');
|
||||
|
||||
} catch (err: any) {
|
||||
console.error("Error al generar lista:", err);
|
||||
if (axios.isAxiosError(err) && err.response) {
|
||||
if (err.response.data instanceof Blob && err.response.data.type === "application/json") {
|
||||
// Intentar leer el error JSON del Blob
|
||||
const errorJson = await err.response.data.text();
|
||||
try {
|
||||
const parsedError = JSON.parse(errorJson);
|
||||
setApiErrorMessage(parsedError.message || 'Error al generar la lista.');
|
||||
} catch (parseError) {
|
||||
setApiErrorMessage('Error al generar la lista. Respuesta de error no válida.');
|
||||
}
|
||||
} else {
|
||||
setApiErrorMessage(err.response.data?.message || err.message || 'Error desconocido al generar la lista.');
|
||||
}
|
||||
} else {
|
||||
setApiErrorMessage('Error al generar la lista.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearMessages = () => {
|
||||
setApiErrorMessage(null);
|
||||
setApiSuccessMessage(null);
|
||||
};
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
clearMessages();
|
||||
};
|
||||
|
||||
|
||||
if (!puedeGenerar) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">Acceso denegado.</Alert></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Generar Listas de Radio</Typography>
|
||||
<Paper sx={{ p: 3, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Criterios de Generación</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}> {/* Aumentado el gap */}
|
||||
<TextField
|
||||
label="Mes (1-12)"
|
||||
type="number"
|
||||
value={mes}
|
||||
onChange={(e) => { setMes(e.target.value); handleInputChange('mes'); }}
|
||||
error={!!localErrors.mes}
|
||||
helperText={localErrors.mes || ''}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
inputProps={{ min: 1, max: 12 }}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Año (ej: 2024)"
|
||||
type="number"
|
||||
value={anio}
|
||||
onChange={(e) => { setAnio(e.target.value); handleInputChange('anio'); }}
|
||||
error={!!localErrors.anio}
|
||||
helperText={localErrors.anio || ''}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
inputProps={{ min: 2000, max: 2099 }}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<FormControl fullWidth required error={!!localErrors.institucion}>
|
||||
<InputLabel id="institucion-select-label">Institución</InputLabel>
|
||||
<Select
|
||||
labelId="institucion-select-label"
|
||||
id="institucion-select"
|
||||
name="institucion"
|
||||
value={institucion}
|
||||
label="Institución"
|
||||
onChange={(e) => { setInstitucion(e.target.value as "AADI" | "SADAIC"); handleInputChange('institucion'); }}
|
||||
>
|
||||
<MenuItem value="AADI">AADI</MenuItem>
|
||||
<MenuItem value="SADAIC">SADAIC</MenuItem>
|
||||
</Select>
|
||||
{localErrors.institucion && <FormHelperText>{localErrors.institucion}</FormHelperText>}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth required error={!!localErrors.radio}>
|
||||
<InputLabel id="radio-select-label">Radio</InputLabel>
|
||||
<Select
|
||||
labelId="radio-select-label"
|
||||
id="radio-select"
|
||||
name="radio"
|
||||
value={radio}
|
||||
label="Radio"
|
||||
onChange={(e) => { setRadio(e.target.value as "FM 99.1" | "FM 100.3"); handleInputChange('radio'); }}
|
||||
>
|
||||
<MenuItem value="FM 99.1">FM 99.1</MenuItem>
|
||||
<MenuItem value="FM 100.3">FM 100.3</MenuItem>
|
||||
</Select>
|
||||
{localErrors.radio && <FormHelperText>{localErrors.radio}</FormHelperText>}
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <DownloadIcon />}
|
||||
onClick={handleGenerarLista}
|
||||
disabled={loading}
|
||||
sx={{ mt: 1, alignSelf: 'flex-start' }} // Un pequeño margen superior
|
||||
>
|
||||
Generar y Descargar Lista
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
{apiSuccessMessage && <Alert severity="success" sx={{ my: 2 }}>{apiSuccessMessage}</Alert>}
|
||||
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarListasRadioPage;
|
||||
195
Frontend/src/pages/Radios/GestionarCancionesPage.tsx
Normal file
195
Frontend/src/pages/Radios/GestionarCancionesPage.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, FormControl, InputLabel, Select
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
|
||||
import cancionService from '../../services/Radios/cancionService';
|
||||
import ritmoService from '../../services/Radios/ritmoService'; // Para el filtro de ritmo
|
||||
|
||||
import type { CancionDto } from '../../models/dtos/Radios/CancionDto';
|
||||
import type { CreateCancionDto } from '../../models/dtos/Radios/CreateCancionDto';
|
||||
import type { UpdateCancionDto } from '../../models/dtos/Radios/UpdateCancionDto';
|
||||
import type { RitmoDto } from '../../models/dtos/Radios/RitmoDto';
|
||||
|
||||
import CancionFormModal from '../../components/Modals/Radios/CancionFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarCancionesPage: React.FC = () => {
|
||||
const [canciones, setCanciones] = useState<CancionDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroTema, setFiltroTema] = useState('');
|
||||
const [filtroInterprete, setFiltroInterprete] = useState('');
|
||||
const [filtroIdRitmo, setFiltroIdRitmo] = useState<number | string>('');
|
||||
|
||||
const [ritmos, setRitmos] = useState<RitmoDto[]>([]); // Para el dropdown de filtro
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingCancion, setEditingCancion] = useState<CancionDto | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedRow, setSelectedRow] = useState<CancionDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeGestionar = isSuperAdmin || tienePermiso("SS005"); // Usar permiso general de la sección
|
||||
|
||||
const fetchRitmosParaFiltro = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
const data = await ritmoService.getAllRitmos();
|
||||
setRitmos(data);
|
||||
} catch (err) { console.error(err); setError("Error al cargar ritmos para filtro.");
|
||||
} finally { setLoadingFiltersDropdown(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchRitmosParaFiltro(); }, [fetchRitmosParaFiltro]);
|
||||
|
||||
const cargarCanciones = useCallback(async () => {
|
||||
if (!puedeGestionar) { setError("No tiene permiso."); setLoading(false); return; }
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const params = {
|
||||
temaFilter: filtroTema || null,
|
||||
interpreteFilter: filtroInterprete || null,
|
||||
idRitmoFilter: filtroIdRitmo ? Number(filtroIdRitmo) : null,
|
||||
};
|
||||
const data = await cancionService.getAllCanciones(params);
|
||||
setCanciones(data);
|
||||
} catch (err) { console.error(err); setError('Error al cargar las canciones.');
|
||||
} finally { setLoading(false); }
|
||||
}, [puedeGestionar, filtroTema, filtroInterprete, filtroIdRitmo]);
|
||||
|
||||
useEffect(() => { cargarCanciones(); }, [cargarCanciones]);
|
||||
|
||||
const handleOpenModal = (item?: CancionDto) => {
|
||||
setEditingCancion(item || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => { setModalOpen(false); setEditingCancion(null); };
|
||||
|
||||
const handleSubmitModal = async (data: CreateCancionDto | UpdateCancionDto, id?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (id && editingCancion) {
|
||||
await cancionService.updateCancion(id, data as UpdateCancionDto);
|
||||
} else {
|
||||
await cancionService.createCancion(data as CreateCancionDto);
|
||||
}
|
||||
cargarCanciones();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la canción.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm(`¿Seguro de eliminar esta canción (ID: ${id})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try { await cancionService.deleteCancion(id); cargarCanciones(); }
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: CancionDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedRow(item);
|
||||
};
|
||||
const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); };
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = canciones.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeGestionar && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Canciones</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}>
|
||||
<TextField label="Filtrar por Tema" size="small" value={filtroTema} onChange={(e) => setFiltroTema(e.target.value)} sx={{minWidth: 200, flexGrow: 1}}/>
|
||||
<TextField label="Filtrar por Intérprete" size="small" value={filtroInterprete} onChange={(e) => setFiltroInterprete(e.target.value)} sx={{minWidth: 200, flexGrow: 1}}/>
|
||||
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Ritmo</InputLabel>
|
||||
<Select value={filtroIdRitmo} label="Ritmo" onChange={(e) => setFiltroIdRitmo(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
{ritmos.map(r => <MenuItem key={r.id} value={r.id}>{r.nombreRitmo}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
{puedeGestionar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Agregar Canción</Button>)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeGestionar && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow sx={{backgroundColor: 'action.hover'}}>
|
||||
<TableCell>Tema</TableCell><TableCell>Intérprete</TableCell>
|
||||
<TableCell>Álbum</TableCell><TableCell>Ritmo</TableCell>
|
||||
<TableCell>Formato</TableCell><TableCell>Pista</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={7} align="center">No se encontraron canciones.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((c) => (
|
||||
<TableRow key={c.id} hover>
|
||||
<TableCell sx={{fontWeight:500}}>{c.tema || '-'}</TableCell>
|
||||
<TableCell>{c.interprete || '-'}</TableCell>
|
||||
<TableCell>{c.album || '-'}</TableCell>
|
||||
<TableCell>{c.nombreRitmo || '-'}</TableCell>
|
||||
<TableCell>{c.formato || '-'}</TableCell>
|
||||
<TableCell align="center">{c.pista ?? '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[10, 25, 50, 100]} component="div" count={canciones.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeGestionar && selectedRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
|
||||
{puedeGestionar && selectedRow && (
|
||||
<MenuItem onClick={() => handleDelete(selectedRow.id)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
|
||||
</Menu>
|
||||
|
||||
<CancionFormModal
|
||||
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
||||
initialData={editingCancion} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarCancionesPage;
|
||||
164
Frontend/src/pages/Radios/GestionarRitmosPage.tsx
Normal file
164
Frontend/src/pages/Radios/GestionarRitmosPage.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
|
||||
import ritmoService from '../../services/Radios/ritmoService';
|
||||
import type { RitmoDto } from '../../models/dtos/Radios/RitmoDto';
|
||||
import type { CreateRitmoDto } from '../../models/dtos/Radios/CreateRitmoDto';
|
||||
import type { UpdateRitmoDto } from '../../models/dtos/Radios/UpdateRitmoDto';
|
||||
import RitmoFormModal from '../../components/Modals/Radios/RitmoFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarRitmosPage: React.FC = () => {
|
||||
const [ritmos, setRitmos] = useState<RitmoDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingRitmo, setEditingRitmo] = useState<RitmoDto | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedRow, setSelectedRow] = useState<RitmoDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
// Usar el permiso general de la sección Radios (SS005) o crear permisos específicos si es necesario.
|
||||
const puedeGestionar = isSuperAdmin || tienePermiso("SS005");
|
||||
|
||||
|
||||
const cargarRitmos = useCallback(async () => {
|
||||
if (!puedeGestionar) { // Asumimos que el mismo permiso es para ver y gestionar
|
||||
setError("No tiene permiso para acceder a esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const data = await ritmoService.getAllRitmos(filtroNombre);
|
||||
setRitmos(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar los ritmos.');
|
||||
} finally { setLoading(false); }
|
||||
}, [puedeGestionar, filtroNombre]);
|
||||
|
||||
useEffect(() => { cargarRitmos(); }, [cargarRitmos]);
|
||||
|
||||
const handleOpenModal = (item?: RitmoDto) => {
|
||||
setEditingRitmo(item || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingRitmo(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateRitmoDto | UpdateRitmoDto, id?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (id && editingRitmo) {
|
||||
await ritmoService.updateRitmo(id, data as UpdateRitmoDto);
|
||||
} else {
|
||||
await ritmoService.createRitmo(data as CreateRitmoDto);
|
||||
}
|
||||
cargarRitmos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el ritmo.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm(`¿Seguro de eliminar este ritmo (ID: ${id})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try { await ritmoService.deleteRitmo(id); cargarRitmos(); }
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: RitmoDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedRow(item);
|
||||
};
|
||||
const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); };
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = ritmos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeGestionar) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Ritmos</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}>
|
||||
<TextField label="Filtrar por Nombre" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{minWidth: 200, flexGrow: 1}}/>
|
||||
{/* <Button variant="outlined" onClick={cargarRitmos} size="small">Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeGestionar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Agregar Ritmo</Button>)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeGestionar && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell sx={{fontWeight: 'bold'}}>Nombre del Ritmo</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell>
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={2} align="center">No se encontraron ritmos.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((r) => (
|
||||
<TableRow key={r.id} hover>
|
||||
<TableCell>{r.nombreRitmo || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, r)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[10, 25, 50]} component="div" count={ritmos.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeGestionar && selectedRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
|
||||
{puedeGestionar && selectedRow && (
|
||||
<MenuItem onClick={() => handleDelete(selectedRow.id)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
|
||||
</Menu>
|
||||
|
||||
<RitmoFormModal
|
||||
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
||||
initialData={editingRitmo} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarRitmosPage;
|
||||
56
Frontend/src/pages/Radios/RadiosIndexPage.tsx
Normal file
56
Frontend/src/pages/Radios/RadiosIndexPage.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
const radiosSubModules = [
|
||||
{ label: 'Ritmos', path: 'ritmos' },
|
||||
{ label: 'Canciones', path: 'canciones' },
|
||||
{ label: 'Generar Listas', path: 'generar-listas' },
|
||||
];
|
||||
|
||||
const RadiosIndexPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const currentBasePath = '/radios';
|
||||
const subPath = location.pathname.startsWith(currentBasePath + '/')
|
||||
? location.pathname.substring(currentBasePath.length + 1).split('/')[0]
|
||||
: (location.pathname === currentBasePath ? radiosSubModules[0]?.path : undefined);
|
||||
const activeTabIndex = radiosSubModules.findIndex(sm => sm.path === subPath);
|
||||
|
||||
if (activeTabIndex !== -1) {
|
||||
setSelectedSubTab(activeTabIndex);
|
||||
} else {
|
||||
if (location.pathname === currentBasePath && radiosSubModules.length > 0) {
|
||||
navigate(radiosSubModules[0].path, { replace: true });
|
||||
setSelectedSubTab(0);
|
||||
} else {
|
||||
setSelectedSubTab(false);
|
||||
}
|
||||
}
|
||||
}, [location.pathname, navigate]);
|
||||
|
||||
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
setSelectedSubTab(newValue);
|
||||
navigate(radiosSubModules[newValue].path);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" gutterBottom>Módulo de Radios</Typography>
|
||||
<Paper square elevation={1}>
|
||||
<Tabs value={selectedSubTab} onChange={handleSubTabChange} indicatorColor="primary" textColor="primary" variant="scrollable" scrollButtons="auto">
|
||||
{radiosSubModules.map((subModule) => (
|
||||
<Tab key={subModule.path} label={subModule.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default RadiosIndexPage;
|
||||
@@ -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;
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -9,8 +9,6 @@ import { Typography } from '@mui/material';
|
||||
|
||||
// Distribución
|
||||
import DistribucionIndexPage from '../pages/Distribucion/DistribucionIndexPage';
|
||||
import ESCanillasPage from '../pages/Distribucion/ESCanillasPage';
|
||||
import ControlDevolucionesPage from '../pages/Distribucion/ControlDevolucionesPage';
|
||||
import GestionarCanillitasPage from '../pages/Distribucion/GestionarCanillitasPage';
|
||||
import GestionarDistribuidoresPage from '../pages/Distribucion/GestionarDistribuidoresPage';
|
||||
import GestionarPublicacionesPage from '../pages/Distribucion/GestionarPublicacionesPage';
|
||||
@@ -24,6 +22,8 @@ import GestionarZonasPage from '../pages/Distribucion/GestionarZonasPage';
|
||||
import GestionarEmpresasPage from '../pages/Distribucion/GestionarEmpresasPage';
|
||||
import GestionarSalidasOtrosDestinosPage from '../pages/Distribucion/GestionarSalidasOtrosDestinosPage';
|
||||
import GestionarEntradasSalidasDistPage from '../pages/Distribucion/GestionarEntradasSalidasDistPage';
|
||||
import GestionarEntradasSalidasCanillaPage from '../pages/Distribucion/GestionarEntradasSalidasCanillaPage';
|
||||
import GestionarControlDevolucionesPage from '../pages/Distribucion/GestionarControlDevolucionesPage';
|
||||
|
||||
// Impresión
|
||||
import ImpresionIndexPage from '../pages/Impresion/ImpresionIndexPage';
|
||||
@@ -36,6 +36,8 @@ import GestionarTiradasPage from '../pages/Impresion/GestionarTiradasPage';
|
||||
// Contables
|
||||
import ContablesIndexPage from '../pages/Contables/ContablesIndexPage';
|
||||
import GestionarTiposPagoPage from '../pages/Contables/GestionarTiposPagoPage';
|
||||
import GestionarPagosDistribuidorPage from '../pages/Contables/GestionarPagosDistribuidorPage';
|
||||
import GestionarNotasCDPage from '../pages/Contables/GestionarNotasCDPage';
|
||||
|
||||
// Usuarios
|
||||
import UsuariosIndexPage from '../pages/Usuarios/UsuariosIndexPage'; // Crear este componente
|
||||
@@ -44,6 +46,15 @@ import GestionarPermisosPage from '../pages/Usuarios/GestionarPermisosPage';
|
||||
import AsignarPermisosAPerfilPage from '../pages/Usuarios/AsignarPermisosAPerfilPage';
|
||||
import GestionarUsuariosPage from '../pages/Usuarios/GestionarUsuariosPage';
|
||||
|
||||
// Radios
|
||||
import RadiosIndexPage from '../pages/Radios/RadiosIndexPage';
|
||||
import GestionarRitmosPage from '../pages/Radios/GestionarRitmosPage';
|
||||
import GestionarCancionesPage from '../pages/Radios/GestionarCancionesPage';
|
||||
import GenerarListasRadioPage from '../pages/Radios/GenerarListasRadioPage';
|
||||
|
||||
// Auditorias
|
||||
import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage';
|
||||
|
||||
// --- ProtectedRoute y PublicRoute SIN CAMBIOS ---
|
||||
const ProtectedRoute: React.FC<{ children: JSX.Element }> = ({ children }) => {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
@@ -101,8 +112,8 @@ const AppRoutes = () => {
|
||||
{/* Módulo de Distribución (anidado) */}
|
||||
<Route path="distribucion" element={<DistribucionIndexPage />}>
|
||||
<Route index element={<Navigate to="es-canillas" replace />} />
|
||||
<Route path="es-canillas" element={<ESCanillasPage />} />
|
||||
<Route path="control-devoluciones" element={<ControlDevolucionesPage />} />
|
||||
<Route path="es-canillas" element={<GestionarEntradasSalidasCanillaPage />} />
|
||||
<Route path="control-devoluciones" element={<GestionarControlDevolucionesPage />} />
|
||||
<Route path="es-distribuidores" element={<GestionarEntradasSalidasDistPage />} />
|
||||
<Route path="salidas-otros-destinos" element={<GestionarSalidasOtrosDestinosPage />} />
|
||||
<Route path="canillas" element={<GestionarCanillitasPage />} />
|
||||
@@ -125,7 +136,8 @@ const AppRoutes = () => {
|
||||
<Route path="contables" element={<ContablesIndexPage />}>
|
||||
<Route index element={<Navigate to="tipos-pago" replace />} />
|
||||
<Route path="tipos-pago" element={<GestionarTiposPagoPage />} />
|
||||
{/* Futuras sub-rutas de contables aquí */}
|
||||
<Route path="pagos-distribuidores" element={<GestionarPagosDistribuidorPage />} />
|
||||
<Route path="notas-cd" element={<GestionarNotasCDPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Módulo de Impresión (anidado) */}
|
||||
@@ -140,7 +152,14 @@ const AppRoutes = () => {
|
||||
|
||||
{/* Otros Módulos Principales (estos son "finales", no tienen más hijos) */}
|
||||
<Route path="reportes" element={<PlaceholderPage moduleName="Reportes" />} />
|
||||
<Route path="radios" element={<PlaceholderPage moduleName="Radios" />} />
|
||||
|
||||
{/* Módulo de Radios (anidado) */}
|
||||
<Route path="radios" element={<RadiosIndexPage />}>
|
||||
<Route index element={<Navigate to="ritmos" replace />} />
|
||||
<Route path="ritmos" element={<GestionarRitmosPage />} />
|
||||
<Route path="canciones" element={<GestionarCancionesPage />} />
|
||||
<Route path="generar-listas" element={<GenerarListasRadioPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Módulo de Usuarios (anidado) */}
|
||||
<Route path="usuarios" element={<UsuariosIndexPage />}>
|
||||
@@ -149,6 +168,7 @@ const AppRoutes = () => {
|
||||
<Route path="permisos" element={<GestionarPermisosPage />} />
|
||||
<Route path="perfiles/:idPerfil/permisos" element={<AsignarPermisosAPerfilPage />} />
|
||||
<Route path="gestion-usuarios" element={<GestionarUsuariosPage />} />
|
||||
<Route path="auditoria-usuarios" element={<GestionarAuditoriaUsuariosPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Ruta catch-all DENTRO del layout protegido */}
|
||||
|
||||
54
Frontend/src/services/Contables/notaCreditoDebitoService.ts
Normal file
54
Frontend/src/services/Contables/notaCreditoDebitoService.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { NotaCreditoDebitoDto } from '../../models/dtos/Contables/NotaCreditoDebitoDto';
|
||||
import type { CreateNotaDto } from '../../models/dtos/Contables/CreateNotaDto';
|
||||
import type { UpdateNotaDto } from '../../models/dtos/Contables/UpdateNotaDto';
|
||||
|
||||
interface GetAllNotasParams {
|
||||
fechaDesde?: string | null; // yyyy-MM-dd
|
||||
fechaHasta?: string | null; // yyyy-MM-dd
|
||||
destino?: 'Distribuidores' | 'Canillas' | '' | null;
|
||||
idDestino?: number | null;
|
||||
idEmpresa?: number | null;
|
||||
tipoNota?: 'Debito' | 'Credito' | '' | null;
|
||||
}
|
||||
|
||||
const getAllNotas = async (filters: GetAllNotasParams): Promise<NotaCreditoDebitoDto[]> => {
|
||||
const params: Record<string, string | number> = {};
|
||||
if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde;
|
||||
if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta;
|
||||
if (filters.destino) params.destino = filters.destino;
|
||||
if (filters.idDestino) params.idDestino = filters.idDestino;
|
||||
if (filters.idEmpresa) params.idEmpresa = filters.idEmpresa;
|
||||
if (filters.tipoNota) params.tipo = filters.tipoNota; // El backend espera 'tipo'
|
||||
|
||||
const response = await apiClient.get<NotaCreditoDebitoDto[]>('/notascreditodebito', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getNotaById = async (idNota: number): Promise<NotaCreditoDebitoDto> => {
|
||||
const response = await apiClient.get<NotaCreditoDebitoDto>(`/notascreditodebito/${idNota}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createNota = async (data: CreateNotaDto): Promise<NotaCreditoDebitoDto> => {
|
||||
const response = await apiClient.post<NotaCreditoDebitoDto>('/notascreditodebito', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateNota = async (idNota: number, data: UpdateNotaDto): Promise<void> => {
|
||||
await apiClient.put(`/notascreditodebito/${idNota}`, data);
|
||||
};
|
||||
|
||||
const deleteNota = async (idNota: number): Promise<void> => {
|
||||
await apiClient.delete(`/notascreditodebito/${idNota}`);
|
||||
};
|
||||
|
||||
const notaCreditoDebitoService = {
|
||||
getAllNotas,
|
||||
getNotaById,
|
||||
createNota,
|
||||
updateNota,
|
||||
deleteNota,
|
||||
};
|
||||
|
||||
export default notaCreditoDebitoService;
|
||||
52
Frontend/src/services/Contables/pagoDistribuidorService.ts
Normal file
52
Frontend/src/services/Contables/pagoDistribuidorService.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { PagoDistribuidorDto } from '../../models/dtos/Contables/PagoDistribuidorDto';
|
||||
import type { CreatePagoDistribuidorDto } from '../../models/dtos/Contables/CreatePagoDistribuidorDto';
|
||||
import type { UpdatePagoDistribuidorDto } from '../../models/dtos/Contables/UpdatePagoDistribuidorDto';
|
||||
|
||||
interface GetAllPagosDistParams {
|
||||
fechaDesde?: string | null; // yyyy-MM-dd
|
||||
fechaHasta?: string | null; // yyyy-MM-dd
|
||||
idDistribuidor?: number | null;
|
||||
idEmpresa?: number | null;
|
||||
tipoMovimiento?: 'Recibido' | 'Realizado' | '' | null;
|
||||
}
|
||||
|
||||
const getAllPagosDistribuidor = async (filters: GetAllPagosDistParams): Promise<PagoDistribuidorDto[]> => {
|
||||
const params: Record<string, string | number> = {};
|
||||
if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde;
|
||||
if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta;
|
||||
if (filters.idDistribuidor) params.idDistribuidor = filters.idDistribuidor;
|
||||
if (filters.idEmpresa) params.idEmpresa = filters.idEmpresa;
|
||||
if (filters.tipoMovimiento) params.tipoMovimiento = filters.tipoMovimiento;
|
||||
|
||||
const response = await apiClient.get<PagoDistribuidorDto[]>('/pagosdistribuidor', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getPagoDistribuidorById = async (idPago: number): Promise<PagoDistribuidorDto> => {
|
||||
const response = await apiClient.get<PagoDistribuidorDto>(`/pagosdistribuidor/${idPago}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createPagoDistribuidor = async (data: CreatePagoDistribuidorDto): Promise<PagoDistribuidorDto> => {
|
||||
const response = await apiClient.post<PagoDistribuidorDto>('/pagosdistribuidor', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updatePagoDistribuidor = async (idPago: number, data: UpdatePagoDistribuidorDto): Promise<void> => {
|
||||
await apiClient.put(`/pagosdistribuidor/${idPago}`, data);
|
||||
};
|
||||
|
||||
const deletePagoDistribuidor = async (idPago: number): Promise<void> => {
|
||||
await apiClient.delete(`/pagosdistribuidor/${idPago}`);
|
||||
};
|
||||
|
||||
const pagoDistribuidorService = {
|
||||
getAllPagosDistribuidor,
|
||||
getPagoDistribuidorById,
|
||||
createPagoDistribuidor,
|
||||
updatePagoDistribuidor,
|
||||
deletePagoDistribuidor,
|
||||
};
|
||||
|
||||
export default pagoDistribuidorService;
|
||||
@@ -1,7 +1,7 @@
|
||||
import apiClient from '../apiClient';
|
||||
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';
|
||||
|
||||
const getAllTiposPago = async (nombreFilter?: string): Promise<TipoPago[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { ControlDevolucionesDto } from '../../models/dtos/Distribucion/ControlDevolucionesDto';
|
||||
import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto';
|
||||
import type { UpdateControlDevolucionesDto } from '../../models/dtos/Distribucion/UpdateControlDevolucionesDto';
|
||||
|
||||
interface GetAllControlesParams {
|
||||
fechaDesde?: string | null; // yyyy-MM-dd
|
||||
fechaHasta?: string | null; // yyyy-MM-dd
|
||||
idEmpresa?: number | null;
|
||||
}
|
||||
|
||||
const getAllControlesDevoluciones = async (filters: GetAllControlesParams): Promise<ControlDevolucionesDto[]> => {
|
||||
const params: Record<string, string | number> = {};
|
||||
if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde;
|
||||
if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta;
|
||||
if (filters.idEmpresa) params.idEmpresa = filters.idEmpresa;
|
||||
|
||||
const response = await apiClient.get<ControlDevolucionesDto[]>('/controldevoluciones', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getControlDevolucionesById = async (idControl: number): Promise<ControlDevolucionesDto> => {
|
||||
const response = await apiClient.get<ControlDevolucionesDto>(`/controldevoluciones/${idControl}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createControlDevoluciones = async (data: CreateControlDevolucionesDto): Promise<ControlDevolucionesDto> => {
|
||||
const response = await apiClient.post<ControlDevolucionesDto>('/controldevoluciones', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateControlDevoluciones = async (idControl: number, data: UpdateControlDevolucionesDto): Promise<void> => {
|
||||
await apiClient.put(`/controldevoluciones/${idControl}`, data);
|
||||
};
|
||||
|
||||
const deleteControlDevoluciones = async (idControl: number): Promise<void> => {
|
||||
await apiClient.delete(`/controldevoluciones/${idControl}`);
|
||||
};
|
||||
|
||||
const controlDevolucionesService = {
|
||||
getAllControlesDevoluciones,
|
||||
getControlDevolucionesById,
|
||||
createControlDevoluciones,
|
||||
updateControlDevoluciones,
|
||||
deleteControlDevoluciones,
|
||||
};
|
||||
|
||||
export default controlDevolucionesService;
|
||||
@@ -0,0 +1,72 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
|
||||
import type { CreateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaCanillaDto'; // Para creación individual si se mantiene
|
||||
import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
|
||||
import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto';
|
||||
import type { CreateBulkEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto';
|
||||
|
||||
interface GetAllESCanillaParams {
|
||||
fechaDesde?: string | null; // yyyy-MM-dd
|
||||
fechaHasta?: string | null; // yyyy-MM-dd
|
||||
idPublicacion?: number | null;
|
||||
idCanilla?: number | null;
|
||||
liquidados?: boolean | null; // true para solo liquidados, false para solo no liquidados
|
||||
incluirNoLiquidados?: boolean | null; // Si liquidados es null, este determina si se muestran no liquidados
|
||||
}
|
||||
|
||||
const getAllEntradasSalidasCanilla = async (filters: GetAllESCanillaParams): Promise<EntradaSalidaCanillaDto[]> => {
|
||||
const params: Record<string, string | number | boolean> = {};
|
||||
if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde;
|
||||
if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta;
|
||||
if (filters.idPublicacion) params.idPublicacion = filters.idPublicacion;
|
||||
if (filters.idCanilla) params.idCanilla = filters.idCanilla;
|
||||
if (filters.liquidados !== undefined && filters.liquidados !== null) params.liquidados = filters.liquidados;
|
||||
if (filters.incluirNoLiquidados !== undefined && filters.incluirNoLiquidados !== null) params.incluirNoLiquidados = filters.incluirNoLiquidados;
|
||||
|
||||
|
||||
const response = await apiClient.get<EntradaSalidaCanillaDto[]>('/entradassalidascanilla', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getEntradaSalidaCanillaById = async (idParte: number): Promise<EntradaSalidaCanillaDto> => {
|
||||
const response = await apiClient.get<EntradaSalidaCanillaDto>(`/entradassalidascanilla/${idParte}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Método para creación individual (si decides mantenerlo y diferenciar en el backend o aquí)
|
||||
const createEntradaSalidaCanilla = async (data: CreateEntradaSalidaCanillaDto): Promise<EntradaSalidaCanillaDto> => {
|
||||
console.warn("Llamando a createEntradaSalidaCanilla (single), considera usar createBulk si es para el modal de múltiples items.")
|
||||
const response = await apiClient.post<EntradaSalidaCanillaDto>('/entradassalidascanilla', data); // Asume que el endpoint /entradassalidascanilla acepta el DTO individual
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Nuevo método para creación en lote
|
||||
const createBulkEntradasSalidasCanilla = async (data: CreateBulkEntradaSalidaCanillaDto): Promise<EntradaSalidaCanillaDto[]> => {
|
||||
const response = await apiClient.post<EntradaSalidaCanillaDto[]>('/entradassalidascanilla/bulk', data); // Endpoint para el lote
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
const updateEntradaSalidaCanilla = async (idParte: number, data: UpdateEntradaSalidaCanillaDto): Promise<void> => {
|
||||
await apiClient.put(`/entradassalidascanilla/${idParte}`, data);
|
||||
};
|
||||
|
||||
const deleteEntradaSalidaCanilla = async (idParte: number): Promise<void> => {
|
||||
await apiClient.delete(`/entradassalidascanilla/${idParte}`);
|
||||
};
|
||||
|
||||
const liquidarMovimientos = async (data: LiquidarMovimientosCanillaRequestDto): Promise<void> => {
|
||||
await apiClient.post('/entradassalidascanilla/liquidar', data);
|
||||
};
|
||||
|
||||
const entradaSalidaCanillaService = {
|
||||
getAllEntradasSalidasCanilla,
|
||||
getEntradaSalidaCanillaById,
|
||||
createEntradaSalidaCanilla, // Mantener si se usa
|
||||
createBulkEntradasSalidasCanilla, // Nuevo
|
||||
updateEntradaSalidaCanilla,
|
||||
deleteEntradaSalidaCanilla,
|
||||
liquidarMovimientos,
|
||||
};
|
||||
|
||||
export default entradaSalidaCanillaService;
|
||||
@@ -12,7 +12,7 @@ interface GetAllESDistParams {
|
||||
}
|
||||
|
||||
const getAllEntradasSalidasDist = async (filters: GetAllESDistParams): Promise<EntradaSalidaDistDto[]> => {
|
||||
const params: Record<string, string | number> = {};
|
||||
const params: Record<string, string | number | boolean> = {}; // Permitir boolean para que coincida con la interfaz
|
||||
if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde;
|
||||
if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta;
|
||||
if (filters.idPublicacion) params.idPublicacion = filters.idPublicacion;
|
||||
|
||||
48
Frontend/src/services/Radios/cancionService.ts
Normal file
48
Frontend/src/services/Radios/cancionService.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { CancionDto } from '../../models/dtos/Radios/CancionDto';
|
||||
import type { CreateCancionDto } from '../../models/dtos/Radios/CreateCancionDto';
|
||||
import type { UpdateCancionDto } from '../../models/dtos/Radios/UpdateCancionDto';
|
||||
|
||||
interface GetAllCancionesParams {
|
||||
temaFilter?: string | null;
|
||||
interpreteFilter?: string | null;
|
||||
idRitmoFilter?: number | null;
|
||||
}
|
||||
|
||||
const getAllCanciones = async (filters: GetAllCancionesParams): Promise<CancionDto[]> => {
|
||||
const params: Record<string, string | number> = {};
|
||||
if (filters.temaFilter) params.tema = filters.temaFilter; // Backend espera 'tema'
|
||||
if (filters.interpreteFilter) params.interprete = filters.interpreteFilter; // Backend espera 'interprete'
|
||||
if (filters.idRitmoFilter) params.idRitmo = filters.idRitmoFilter; // Backend espera 'idRitmo'
|
||||
|
||||
const response = await apiClient.get<CancionDto[]>('/canciones', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getCancionById = async (id: number): Promise<CancionDto> => {
|
||||
const response = await apiClient.get<CancionDto>(`/canciones/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createCancion = async (data: CreateCancionDto): Promise<CancionDto> => {
|
||||
const response = await apiClient.post<CancionDto>('/canciones', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateCancion = async (id: number, data: UpdateCancionDto): Promise<void> => {
|
||||
await apiClient.put(`/canciones/${id}`, data);
|
||||
};
|
||||
|
||||
const deleteCancion = async (id: number): Promise<void> => {
|
||||
await apiClient.delete(`/canciones/${id}`);
|
||||
};
|
||||
|
||||
const cancionService = {
|
||||
getAllCanciones,
|
||||
getCancionById,
|
||||
createCancion,
|
||||
updateCancion,
|
||||
deleteCancion,
|
||||
};
|
||||
|
||||
export default cancionService;
|
||||
20
Frontend/src/services/Radios/radioListaService.ts
Normal file
20
Frontend/src/services/Radios/radioListaService.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { GenerarListaRadioRequestDto } from '../../models/dtos/Radios/GenerarListaRadioRequestDto';
|
||||
// No esperamos un DTO de respuesta complejo, sino un archivo.
|
||||
|
||||
interface GenerarListaRadioParams extends GenerarListaRadioRequestDto {}
|
||||
|
||||
const generarListaRadio = async (params: GenerarListaRadioParams): Promise<Blob> => {
|
||||
// El backend devuelve un archivo (FileContentResult con "application/zip")
|
||||
// Axios necesita responseType: 'blob' para manejar descargas de archivos.
|
||||
const response = await apiClient.post('/radios/listas/generar', params, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data; // Esto será un Blob
|
||||
};
|
||||
|
||||
const radioListaService = {
|
||||
generarListaRadio,
|
||||
};
|
||||
|
||||
export default radioListaService;
|
||||
40
Frontend/src/services/Radios/ritmoService.ts
Normal file
40
Frontend/src/services/Radios/ritmoService.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { RitmoDto } from '../../models/dtos/Radios/RitmoDto';
|
||||
import type { CreateRitmoDto } from '../../models/dtos/Radios/CreateRitmoDto';
|
||||
import type { UpdateRitmoDto } from '../../models/dtos/Radios/UpdateRitmoDto';
|
||||
|
||||
const getAllRitmos = async (nombreFilter?: string): Promise<RitmoDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
if (nombreFilter) params.nombre = nombreFilter; // El backend espera 'nombre'
|
||||
|
||||
const response = await apiClient.get<RitmoDto[]>('/ritmos', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getRitmoById = async (id: number): Promise<RitmoDto> => {
|
||||
const response = await apiClient.get<RitmoDto>(`/ritmos/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createRitmo = async (data: CreateRitmoDto): Promise<RitmoDto> => {
|
||||
const response = await apiClient.post<RitmoDto>('/ritmos', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateRitmo = async (id: number, data: UpdateRitmoDto): Promise<void> => {
|
||||
await apiClient.put(`/ritmos/${id}`, data);
|
||||
};
|
||||
|
||||
const deleteRitmo = async (id: number): Promise<void> => {
|
||||
await apiClient.delete(`/ritmos/${id}`);
|
||||
};
|
||||
|
||||
const ritmoService = {
|
||||
getAllRitmos,
|
||||
getRitmoById,
|
||||
createRitmo,
|
||||
updateRitmo,
|
||||
deleteRitmo,
|
||||
};
|
||||
|
||||
export default ritmoService;
|
||||
@@ -1,9 +1,17 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { UsuarioHistorialDto } from '../../models/dtos/Usuarios/Auditoria/UsuarioHistorialDto';
|
||||
import type { UsuarioDto } from '../../models/dtos/Usuarios/UsuarioDto';
|
||||
import type { CreateUsuarioRequestDto } from '../../models/dtos/Usuarios/CreateUsuarioRequestDto';
|
||||
import type { UpdateUsuarioRequestDto } from '../../models/dtos/Usuarios/UpdateUsuarioRequestDto';
|
||||
import type { SetPasswordRequestDto } from '../../models/dtos/Usuarios/SetPasswordRequestDto';
|
||||
|
||||
interface HistorialParams {
|
||||
fechaDesde?: string | null; // "yyyy-MM-dd"
|
||||
fechaHasta?: string | null; // "yyyy-MM-dd"
|
||||
idUsuarioModifico?: number | null;
|
||||
tipoModificacion?: string | null;
|
||||
}
|
||||
|
||||
const getAllUsuarios = async (userFilter?: string, nombreFilter?: string): Promise<UsuarioDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
if (userFilter) params.user = userFilter;
|
||||
@@ -38,6 +46,25 @@ const toggleHabilitado = async (id: number, habilitar: boolean): Promise<void> =
|
||||
});
|
||||
};
|
||||
|
||||
const getHistorialDeUsuario = async (idUsuarioAfectado: number, params?: Omit<HistorialParams, 'idUsuarioModifico' | 'tipoModificacion'>): Promise<UsuarioHistorialDto[]> => {
|
||||
const queryParams: Record<string, string> = {};
|
||||
if (params?.fechaDesde) queryParams.fechaDesde = params.fechaDesde;
|
||||
if (params?.fechaHasta) queryParams.fechaHasta = params.fechaHasta;
|
||||
|
||||
const response = await apiClient.get<UsuarioHistorialDto[]>(`/usuarios/${idUsuarioAfectado}/historial`, { params: queryParams });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getTodoElHistorialDeUsuarios = async (params?: HistorialParams): Promise<UsuarioHistorialDto[]> => {
|
||||
const queryParams: Record<string, string | number> = {};
|
||||
if (params?.fechaDesde) queryParams.fechaDesde = params.fechaDesde;
|
||||
if (params?.fechaHasta) queryParams.fechaHasta = params.fechaHasta;
|
||||
if (params?.idUsuarioModifico) queryParams.idUsuarioModifico = params.idUsuarioModifico;
|
||||
if (params?.tipoModificacion) queryParams.tipoModificacion = params.tipoModificacion;
|
||||
|
||||
const response = await apiClient.get<UsuarioHistorialDto[]>('/usuarios/historial', { params: queryParams });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const usuarioService = {
|
||||
getAllUsuarios,
|
||||
@@ -46,6 +73,8 @@ const usuarioService = {
|
||||
updateUsuario,
|
||||
setPassword,
|
||||
toggleHabilitado,
|
||||
getHistorialDeUsuario,
|
||||
getTodoElHistorialDeUsuarios,
|
||||
};
|
||||
|
||||
export default usuarioService;
|
||||
Reference in New Issue
Block a user