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;
|
||||
Reference in New Issue
Block a user