diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Contables/IPagoDistribuidorRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Contables/IPagoDistribuidorRepository.cs index e40b4df..1917422 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Contables/IPagoDistribuidorRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Contables/IPagoDistribuidorRepository.cs @@ -16,7 +16,7 @@ namespace GestionIntegral.Api.Data.Repositories.Contables Task CreateAsync(PagoDistribuidor nuevoPago, int idUsuario, IDbTransaction transaction); Task UpdateAsync(PagoDistribuidor pagoAActualizar, int idUsuario, IDbTransaction transaction); Task DeleteAsync(int idPago, int idUsuario, IDbTransaction transaction); - Task ExistsByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null); + Task GetByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null); Task> GetHistorialAsync( DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModifico, string? tipoModificacion, diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Contables/PagoDistribuidorRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Contables/PagoDistribuidorRepository.cs index 87fe0e4..1f7be57 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Contables/PagoDistribuidorRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Contables/PagoDistribuidorRepository.cs @@ -70,9 +70,10 @@ namespace GestionIntegral.Api.Data.Repositories.Contables } } - public async Task ExistsByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null) + public async Task GetByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null) { - var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.cue_PagosDistribuidor WHERE Recibo = @ReciboParam AND TipoMovimiento = @TipoMovParam"); + var sqlBuilder = new StringBuilder(SelectQueryBase()); // Reutiliza la consulta base + sqlBuilder.Append(" WHERE Recibo = @ReciboParam AND TipoMovimiento = @TipoMovParam"); var parameters = new DynamicParameters(); parameters.Add("ReciboParam", recibo); parameters.Add("TipoMovParam", tipoMovimiento); @@ -85,12 +86,12 @@ namespace GestionIntegral.Api.Data.Repositories.Contables try { using var connection = _cf.CreateConnection(); - return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + return await connection.QuerySingleOrDefaultAsync(sqlBuilder.ToString(), parameters); } catch (Exception ex) { - _log.LogError(ex, "Error en ExistsByReciboAndTipoMovimientoAsync. Recibo: {Recibo}, Tipo: {Tipo}", recibo, tipoMovimiento); - return true; // Asumir que existe en caso de error para prevenir duplicados + _log.LogError(ex, "Error en GetByReciboAndTipoMovimientoAsync. Recibo: {Recibo}, Tipo: {Tipo}", recibo, tipoMovimiento); + throw; // Relanzar para que el servicio lo maneje } } diff --git a/Backend/GestionIntegral.Api/Services/Contables/PagoDistribuidorService.cs b/Backend/GestionIntegral.Api/Services/Contables/PagoDistribuidorService.cs index 403b166..9e5f3b1 100644 --- a/Backend/GestionIntegral.Api/Services/Contables/PagoDistribuidorService.cs +++ b/Backend/GestionIntegral.Api/Services/Contables/PagoDistribuidorService.cs @@ -93,8 +93,18 @@ namespace GestionIntegral.Api.Services.Contables return (null, "Tipo de pago no válido."); if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null) return (null, "Empresa no válida."); - if (await _pagoRepo.ExistsByReciboAndTipoMovimientoAsync(createDto.Recibo, createDto.TipoMovimiento)) - return (null, $"Ya existe un pago '{createDto.TipoMovimiento}' con el número de recibo '{createDto.Recibo}'."); + var pagoExistente = await _pagoRepo.GetByReciboAndTipoMovimientoAsync(createDto.Recibo, createDto.TipoMovimiento); + if (pagoExistente != null) + { + // Si encontramos un duplicado, obtenemos los detalles para el mensaje de error + var distribuidor = await _distribuidorRepo.GetByIdSimpleAsync(pagoExistente.IdDistribuidor); + var empresa = await _empresaRepo.GetByIdAsync(pagoExistente.IdEmpresa); + + string mensajeError = $"El recibo N° {createDto.Recibo} ya fue registrado como '{pagoExistente.TipoMovimiento}' el {pagoExistente.Fecha:dd/MM/yyyy} " + + $"para el distribuidor '{distribuidor?.Nombre ?? "Desconocido"}' en la empresa '{empresa?.Nombre ?? "Desconocida"}'."; + + return (null, mensajeError); + } var nuevoPago = new PagoDistribuidor { @@ -270,30 +280,30 @@ namespace GestionIntegral.Api.Services.Contables } } } - + public async Task> ObtenerHistorialAsync( DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModifico, string? tipoModificacion, int? idPagoAfectado) - { - var historialData = await _pagoRepo.GetHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idPagoAfectado); - - return historialData.Select(h => new PagoDistribuidorHistorialDto { - Id_Pago = h.Historial.Id_Pago, - Id_Distribuidor = h.Historial.Id_Distribuidor, - Fecha = h.Historial.Fecha, - TipoMovimiento = h.Historial.TipoMovimiento, - Recibo = h.Historial.Recibo, - Monto = h.Historial.Monto, - Id_TipoPago = h.Historial.Id_TipoPago, - Detalle = h.Historial.Detalle, - Id_Empresa = h.Historial.Id_Empresa, - Id_Usuario = h.Historial.Id_Usuario, - NombreUsuarioModifico = h.NombreUsuarioModifico, - FechaMod = h.Historial.FechaMod, - TipoMod = h.Historial.TipoMod - }).ToList(); - } + var historialData = await _pagoRepo.GetHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idPagoAfectado); + + return historialData.Select(h => new PagoDistribuidorHistorialDto + { + Id_Pago = h.Historial.Id_Pago, + Id_Distribuidor = h.Historial.Id_Distribuidor, + Fecha = h.Historial.Fecha, + TipoMovimiento = h.Historial.TipoMovimiento, + Recibo = h.Historial.Recibo, + Monto = h.Historial.Monto, + Id_TipoPago = h.Historial.Id_TipoPago, + Detalle = h.Historial.Detalle, + Id_Empresa = h.Historial.Id_Empresa, + Id_Usuario = h.Historial.Id_Usuario, + NombreUsuarioModifico = h.NombreUsuarioModifico, + FechaMod = h.Historial.FechaMod, + TipoMod = h.Historial.TipoMod + }).ToList(); + } } } \ No newline at end of file diff --git a/Frontend/src/components/Modals/Contables/PagoDistribuidorFormModal.tsx b/Frontend/src/components/Modals/Contables/PagoDistribuidorFormModal.tsx index 20ac026..2e65389 100644 --- a/Frontend/src/components/Modals/Contables/PagoDistribuidorFormModal.tsx +++ b/Frontend/src/components/Modals/Contables/PagoDistribuidorFormModal.tsx @@ -64,6 +64,7 @@ const PagoDistribuidorFormModal: React.FC = ({ const isEditing = Boolean(initialData); useEffect(() => { + // Esta función se encarga de cargar los datos de los dropdowns. const fetchDropdownData = async () => { setLoadingDropdowns(true); try { @@ -126,14 +127,16 @@ const PagoDistribuidorFormModal: React.FC = ({ setLoading(true); try { const montoNum = parseFloat(monto); - + if (isEditing && initialData) { const dataToSubmit: UpdatePagoDistribuidorDto = { monto: montoNum, idTipoPago: Number(idTipoPago), detalle: detalle || undefined, }; + // << INICIO DE LA CORRECCIÓN >> await onSubmit(dataToSubmit, initialData.idPago); + // << FIN DE LA CORRECCIÓN >> } else { const dataToSubmit: CreatePagoDistribuidorDto = { idDistribuidor: Number(idDistribuidor), @@ -147,7 +150,9 @@ const PagoDistribuidorFormModal: React.FC = ({ }; await onSubmit(dataToSubmit); } + onClose(); + } catch (error: any) { console.error("Error en submit de PagoDistribuidorFormModal:", error); } finally { diff --git a/Frontend/src/pages/Contables/GestionarPagosDistribuidorPage.tsx b/Frontend/src/pages/Contables/GestionarPagosDistribuidorPage.tsx index 06936ef..05c95f3 100644 --- a/Frontend/src/pages/Contables/GestionarPagosDistribuidorPage.tsx +++ b/Frontend/src/pages/Contables/GestionarPagosDistribuidorPage.tsx @@ -1,8 +1,8 @@ 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 + 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'; @@ -28,11 +28,11 @@ const GestionarPagosDistribuidorPage: React.FC = () => { const [pagos, setPagos] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [apiErrorMessage, setApiErrorMessage] = useState(null); + const [pageApiErrorMessage, setPageApiErrorMessage] = useState(null); + const [modalApiErrorMessage, setModalApiErrorMessage] = useState(null); - // Filtros - const [filtroFechaDesde, setFiltroFechaDesde] = useState(new Date().toISOString().split('T')[0]); //useState(''); - const [filtroFechaHasta, setFiltroFechaHasta] = useState(new Date().toISOString().split('T')[0]);//useState(''); + const [filtroFechaDesde, setFiltroFechaDesde] = useState(new Date().toISOString().split('T')[0]); + const [filtroFechaHasta, setFiltroFechaHasta] = useState(new Date().toISOString().split('T')[0]); const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState(''); const [filtroIdEmpresa, setFiltroIdEmpresa] = useState(''); const [filtroTipoMov, setFiltroTipoMov] = useState<'Recibido' | 'Realizado' | ''>(''); @@ -50,7 +50,6 @@ const GestionarPagosDistribuidorPage: React.FC = () => { const [selectedRow, setSelectedRow] = useState(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"); @@ -59,21 +58,27 @@ const GestionarPagosDistribuidorPage: React.FC = () => { 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."); + 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 clearModalApiErrorMessage = useCallback(() => { + setModalApiErrorMessage(null); + }, []); + + const cargarPagos = useCallback(async () => { if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; } - setLoading(true); setError(null); setApiErrorMessage(null); + setLoading(true); setError(null); setPageApiErrorMessage(null); try { const params = { fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null, @@ -83,19 +88,20 @@ const GestionarPagosDistribuidorPage: React.FC = () => { }; const data = await pagoDistribuidorService.getAllPagosDistribuidor(params); setPagos(data); - } catch (err) { console.error(err); setError('Error al cargar los pagos.'); + } 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); + setEditingPago(item || null); setModalApiErrorMessage(null); setModalOpen(true); }; const handleCloseModal = () => { setModalOpen(false); setEditingPago(null); }; const handleSubmitModal = async (data: CreatePagoDistribuidorDto | UpdatePagoDistribuidorDto, idPago?: number) => { - setApiErrorMessage(null); + setModalApiErrorMessage(null); try { if (idPago && editingPago) { await pagoDistribuidorService.updatePagoDistribuidor(idPago, data as UpdatePagoDistribuidorDto); @@ -105,15 +111,19 @@ const GestionarPagosDistribuidorPage: React.FC = () => { 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; + setModalApiErrorMessage(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); } + setPageApiErrorMessage(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.'; + setPageApiErrorMessage(msg); + } } handleMenuClose(); }; @@ -130,18 +140,11 @@ const GestionarPagosDistribuidorPage: React.FC = () => { const displayData = pagos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); const formatDate = (dateString?: string | null): string => { if (!dateString) return '-'; - // La fecha llega como "YYYY-MM-DD" o "YYYY-MM-DDTHH:mm:ss" - // Tomamos solo la parte de la fecha para evitar problemas de zona horaria. const datePart = dateString.split('T')[0]; const parts = datePart.split('-'); - - // Aseguramos que el formato sea el esperado antes de reordenar if (parts.length === 3) { - // parts[0] = YYYY, parts[1] = MM, parts[2] = DD - return `${parts[2]}/${parts[1]}/${parts[0]}`; // Formato DD/MM/YYYY + return `${parts[2]}/${parts[1]}/${parts[0]}`; } - - // Si el formato no es el esperado, devolvemos la parte de la fecha tal como vino. return datePart; }; @@ -151,93 +154,96 @@ const GestionarPagosDistribuidorPage: React.FC = () => { Pagos de Distribuidores - Filtros - - setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> - setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> - - Distribuidor - - - - Empresa (Saldo) - - - - Tipo Mov. - - - - {puedeCrear && ()} + Filtros + + setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} /> + setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} /> + + Distribuidor + + + + Empresa (Saldo) + + + + Tipo Mov. + + + + {puedeCrear && ()} {loading && } - {error && !loading && {error}} - {apiErrorMessage && {apiErrorMessage}} + {error && !loading && {error}} + {pageApiErrorMessage && {pageApiErrorMessage}} {!loading && !error && puedeVer && ( - - - - FechaDistribuidorEmpresa (Saldo) - Tipo Mov.Recibo N° - MontoTipo Pago - Detalle - {(puedeModificar || puedeEliminar) && Acciones} - - - {displayData.length === 0 ? ( - No se encontraron pagos. - ) : ( - displayData.map((p) => ( - - {formatDate(p.fecha)}{p.nombreDistribuidor} - {p.nombreEmpresa} - - - - {p.recibo} - ${p.monto.toFixed(2)} - {p.nombreTipoPago} - {p.detalle || '-'} - {(puedeModificar || puedeEliminar) && ( - - handleMenuOpen(e, p)} disabled={!puedeModificar && !puedeEliminar}> - - )} - - )))} - -
- -
- )} + + + + FechaDistribuidorEmpresa (Saldo) + Tipo Mov.Recibo N° + MontoTipo Pago + Detalle + {(puedeModificar || puedeEliminar) && Acciones} + + + {displayData.length === 0 ? ( + No se encontraron pagos. + ) : ( + displayData.map((p) => ( + + {formatDate(p.fecha)}{p.nombreDistribuidor} + {p.nombreEmpresa} + + + + {p.recibo} + ${p.monto.toFixed(2)} + {p.nombreTipoPago} + {p.detalle || '-'} + {(puedeModificar || puedeEliminar) && ( + + handleMenuOpen(e, p)} disabled={!puedeModificar && !puedeEliminar}> + + )} + + )))} + +
+ +
+ )} {puedeModificar && selectedRow && ( - { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} + { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} {puedeEliminar && selectedRow && ( - handleDelete(selectedRow.idPago)}> Eliminar)} + handleDelete(selectedRow.idPago)}> Eliminar)} setApiErrorMessage(null)} + open={modalOpen} + onClose={handleCloseModal} + onSubmit={handleSubmitModal} + initialData={editingPago} + errorMessage={modalApiErrorMessage} + clearErrorMessage={clearModalApiErrorMessage} />
);