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