271 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			271 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | 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; |