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