diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Impresion/StockBobinaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/StockBobinaRepository.cs index 2787fd1..c872b4f 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Impresion/StockBobinaRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/StockBobinaRepository.cs @@ -61,13 +61,13 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion } if (fechaDesde.HasValue) { - sqlBuilder.Append(" AND sb.FechaRemito >= @FechaDesdeParam"); // O FechaEstado según el contexto del filtro + sqlBuilder.Append(" AND sb.FechaRemito >= @FechaDesdeParam"); parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); } if (fechaHasta.HasValue) { - sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam"); // O FechaEstado - parameters.Add("FechaHastaParam", fechaHasta.Value.Date.AddDays(1).AddTicks(-1)); // Hasta el final del día + sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam"); + parameters.Add("FechaHastaParam", fechaHasta.Value.Date); } sqlBuilder.Append(" ORDER BY sb.FechaRemito DESC, sb.NroBobina;"); @@ -224,14 +224,12 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion if (actual == null) throw new KeyNotFoundException("Bobina no encontrada para eliminar."); - // --- INICIO DE CAMBIO EN VALIDACIÓN --- // Permitir eliminar si está Disponible (1) o Dañada (3) if (actual.IdEstadoBobina != 1 && actual.IdEstadoBobina != 3) { _logger.LogWarning("Intento de eliminar bobina {IdBobina} que no está en estado 'Disponible' o 'Dañada'. Estado actual: {EstadoActual}", idBobina, actual.IdEstadoBobina); return false; // Devolver false si no cumple la condición para ser eliminada } - // --- FIN DE CAMBIO EN VALIDACIÓN --- const string sqlDelete = "DELETE FROM dbo.bob_StockBobinas WHERE Id_Bobina = @IdBobinaParam"; const string sqlInsertHistorico = @" diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PublicacionDropdownDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PublicacionDropdownDto.cs index 6ac6ec1..8f865ed 100644 --- a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PublicacionDropdownDto.cs +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PublicacionDropdownDto.cs @@ -5,6 +5,6 @@ namespace GestionIntegral.Api.Dtos.Distribucion public int IdPublicacion { get; set; } public string Nombre { get; set; } = string.Empty; public string NombreEmpresa { get; set; } = string.Empty; - public bool Habilitada { get; set; } + public bool? Habilitada { get; set; } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PublicacionDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PublicacionDto.cs index f1ca7b8..5ddade7 100644 --- a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PublicacionDto.cs +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PublicacionDto.cs @@ -8,6 +8,6 @@ namespace GestionIntegral.Api.Dtos.Distribucion public int IdEmpresa { get; set; } public string NombreEmpresa { get; set; } = string.Empty; // Para mostrar en UI public bool CtrlDevoluciones { get; set; } - public bool Habilitada { get; set; } // Simplificamos a bool, el backend manejará el default si es null + public bool? Habilitada { get; set; } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IPublicacionService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IPublicacionService.cs index 2cf5aa4..a913250 100644 --- a/Backend/GestionIntegral.Api/Services/Distribucion/IPublicacionService.cs +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IPublicacionService.cs @@ -15,7 +15,7 @@ namespace GestionIntegral.Api.Services.Distribucion Task> ObtenerConfiguracionDiasAsync(int idPublicacion); Task> ObtenerPublicacionesPorDiaSemanaAsync(byte diaSemana); // Devolvemos el DTO completo Task<(bool Exito, string? Error)> ActualizarConfiguracionDiasAsync(int idPublicacion, UpdatePublicacionDiasSemanaRequestDto requestDto, int idUsuario); - Task> ObtenerParaDropdownAsync(bool soloHabilitadas = true); + Task> ObtenerParaDropdownAsync(bool soloHabilitadas); Task> ObtenerHistorialAsync( DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModifico, string? tipoModificacion, diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/PublicacionService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/PublicacionService.cs index e5e6877..8a6bc5e 100644 --- a/Backend/GestionIntegral.Api/Services/Distribucion/PublicacionService.cs +++ b/Backend/GestionIntegral.Api/Services/Distribucion/PublicacionService.cs @@ -58,7 +58,7 @@ namespace GestionIntegral.Api.Services.Distribucion IdEmpresa = data.Publicacion.IdEmpresa, NombreEmpresa = data.NombreEmpresa ?? "Empresa Desconocida", // Manejar null para NombreEmpresa CtrlDevoluciones = data.Publicacion.CtrlDevoluciones, - Habilitada = data.Publicacion.Habilitada ?? true // Asumir true si es null desde BD + Habilitada = data.Publicacion.Habilitada }; } @@ -76,9 +76,9 @@ namespace GestionIntegral.Api.Services.Distribucion return MapToDto(data); } - public async Task> ObtenerParaDropdownAsync(bool soloHabilitadas = true) + public async Task> ObtenerParaDropdownAsync(bool soloHabilitadas) { - var data = await _publicacionRepository.GetAllAsync(null, null, soloHabilitadas ? (bool?)true : null); + var data = await _publicacionRepository.GetAllAsync(null, null, soloHabilitadas); return data .Where(p => p.Publicacion != null) // Asegurar que la publicación no sea null @@ -87,7 +87,7 @@ namespace GestionIntegral.Api.Services.Distribucion IdPublicacion = d.Publicacion!.IdPublicacion, // Usar ! si estás seguro que no es null después del Where Nombre = d.Publicacion!.Nombre, NombreEmpresa = d.NombreEmpresa ?? "Empresa Desconocida", - Habilitada = d.Publicacion!.Habilitada ?? true // Si necesitas filtrar por esto + Habilitada = d.Publicacion!.Habilitada }) .OrderBy(p => p.Nombre) .ToList(); // O ToListAsync si el método del repo es async y devuelve IQueryable diff --git a/Frontend/src/pages/Impresion/GestionarStockBobinasPage.tsx b/Frontend/src/pages/Impresion/GestionarStockBobinasPage.tsx index 55d1780..effa1a7 100644 --- a/Frontend/src/pages/Impresion/GestionarStockBobinasPage.tsx +++ b/Frontend/src/pages/Impresion/GestionarStockBobinasPage.tsx @@ -1,15 +1,16 @@ -// src/pages/Impresion/GestionarStockBobinasPage.tsx -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, - CircularProgress, Alert, FormControl, InputLabel, Select + CircularProgress, Alert, FormControl, InputLabel, Select, FormControlLabel, Checkbox } 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 SwapHorizIcon from '@mui/icons-material/SwapHoriz'; +import SearchIcon from '@mui/icons-material/Search'; +import ClearIcon from '@mui/icons-material/Clear'; import stockBobinaService from '../../services/Impresion/stockBobinaService'; import tipoBobinaService from '../../services/Impresion/tipoBobinaService'; @@ -21,8 +22,8 @@ import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateSto import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto'; import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto'; import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto'; -import type { PlantaDropdownDto } from '../../models/dtos/Impresion/PlantaDropdownDto'; -import type { EstadoBobinaDropdownDto } from '../../models/dtos/Impresion/EstadoBobinaDropdownDto'; +import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; +import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal'; import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal'; @@ -37,33 +38,39 @@ const ID_ESTADO_DANADA = 3; const GestionarStockBobinasPage: React.FC = () => { const [stock, setStock] = useState([]); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); // No carga al inicio const [error, setError] = useState(null); const [apiErrorMessage, setApiErrorMessage] = useState(null); + // Estados de los filtros const [filtroTipoBobina, setFiltroTipoBobina] = useState(''); const [filtroNroBobina, setFiltroNroBobina] = useState(''); const [filtroPlanta, setFiltroPlanta] = useState(''); const [filtroEstadoBobina, setFiltroEstadoBobina] = useState(''); const [filtroRemito, setFiltroRemito] = useState(''); + const [filtroFechaHabilitado, setFiltroFechaHabilitado] = useState(false); // <-- NUEVO const [filtroFechaDesde, setFiltroFechaDesde] = useState(new Date().toISOString().split('T')[0]); const [filtroFechaHasta, setFiltroFechaHasta] = useState(new Date().toISOString().split('T')[0]); + // Estados para datos de dropdowns const [tiposBobina, setTiposBobina] = useState([]); - const [plantas, setPlantas] = useState([]); - const [estadosBobina, setEstadosBobina] = useState([]); + const [plantas, setPlantas] = useState([]); + const [estadosBobina, setEstadosBobina] = useState([]); const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); + // Estados de los modales const [ingresoModalOpen, setIngresoModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); const [cambioEstadoModalOpen, setCambioEstadoModalOpen] = useState(false); - const [selectedBobina, setSelectedBobina] = useState(null); // Para los modales + // Estado para la bobina seleccionada en un modal o menú + const [selectedBobina, setSelectedBobina] = useState(null); + // Estados para la paginación y el menú de acciones const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(25); const [anchorEl, setAnchorEl] = useState(null); - const [selectedBobinaForRowMenu, setSelectedBobinaForRowMenu] = useState(null); // Para el menú contextual + const [selectedBobinaForRowMenu, setSelectedBobinaForRowMenu] = useState(null); const { tienePermiso, isSuperAdmin } = usePermissions(); const puedeVer = isSuperAdmin || tienePermiso("IB001"); @@ -72,13 +79,16 @@ const GestionarStockBobinasPage: React.FC = () => { const puedeModificarDatos = isSuperAdmin || tienePermiso("IB004"); const puedeEliminar = isSuperAdmin || tienePermiso("IB005"); + const lastOpenedMenuButtonRef = useRef(null); + const fetchFiltersDropdownData = useCallback(async () => { setLoadingFiltersDropdown(true); try { + // Asumiendo que estos servicios existen y devuelven los DTOs correctos const [tiposData, plantasData, estadosData] = await Promise.all([ - tipoBobinaService.getAllDropdownTiposBobina(), - plantaService.getPlantasForDropdown(), - estadoBobinaService.getAllDropdownEstadosBobina() + tipoBobinaService.getAllTiposBobina(), + plantaService.getAllPlantas(), + estadoBobinaService.getAllEstadosBobina() ]); setTiposBobina(tiposData); setPlantas(plantasData); @@ -95,12 +105,15 @@ const GestionarStockBobinasPage: React.FC = () => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); - const cargarStock = useCallback(async () => { if (!puedeVer) { - setError("No tiene permiso para ver esta sección."); setLoading(false); return; + setError("No tiene permiso para ver esta sección."); + setLoading(false); + return; } - setLoading(true); setError(null); setApiErrorMessage(null); + setLoading(true); + setError(null); + setApiErrorMessage(null); try { const params = { idTipoBobina: filtroTipoBobina ? Number(filtroTipoBobina) : null, @@ -108,19 +121,39 @@ const GestionarStockBobinasPage: React.FC = () => { idPlanta: filtroPlanta ? Number(filtroPlanta) : null, idEstadoBobina: filtroEstadoBobina ? Number(filtroEstadoBobina) : null, remitoFilter: filtroRemito || null, - fechaDesde: filtroFechaDesde || null, - fechaHasta: filtroFechaHasta || null, + fechaDesde: filtroFechaHabilitado ? filtroFechaDesde : null, + fechaHasta: filtroFechaHabilitado ? filtroFechaHasta : null, }; const data = await stockBobinaService.getAllStockBobinas(params); setStock(data); + if (data.length === 0) { + setError("No se encontraron resultados con los filtros aplicados."); + } } catch (err) { - console.error(err); setError('Error al cargar el stock de bobinas.'); - } finally { setLoading(false); } - }, [puedeVer, filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito, filtroFechaDesde, filtroFechaHasta]); + console.error(err); + setError('Error al cargar el stock de bobinas.'); + } finally { + setLoading(false); + } + }, [puedeVer, filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito, filtroFechaHabilitado, filtroFechaDesde, filtroFechaHasta]); - useEffect(() => { + const handleBuscarClick = () => { + setPage(0); // Resetear la paginación al buscar cargarStock(); - }, [cargarStock]); + }; + + const handleLimpiarFiltros = () => { + setFiltroTipoBobina(''); + setFiltroNroBobina(''); + setFiltroPlanta(''); + setFiltroEstadoBobina(''); + setFiltroRemito(''); + setFiltroFechaHabilitado(false); + setFiltroFechaDesde(new Date().toISOString().split('T')[0]); + setFiltroFechaHasta(new Date().toISOString().split('T')[0]); + setStock([]); // Limpiar los resultados actuales + setError(null); + }; const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); }; const handleCloseIngresoModal = () => setIngresoModalOpen(false); @@ -139,13 +172,10 @@ const GestionarStockBobinasPage: React.FC = () => { const handleCloseEditModal = () => { setEditModalOpen(false); setSelectedBobina(null); - // Devolver el foco al botón que abrió el menú (si el modal se abrió desde el menú) if (lastOpenedMenuButtonRef.current) { - setTimeout(() => { // setTimeout puede ayudar - lastOpenedMenuButtonRef.current?.focus(); - }, 0); + setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0); } -}; + }; const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => { setApiErrorMessage(null); try { await stockBobinaService.updateDatosBobinaDisponible(idBobina, data); cargarStock(); } @@ -158,7 +188,7 @@ const GestionarStockBobinasPage: React.FC = () => { setApiErrorMessage(null); setCambioEstadoModalOpen(true); }; - const handleCloseCambioEstadoModal = () => { setCambioEstadoModalOpen(false); setSelectedBobina(null); }; + const handleCloseCambioEstadoModal = () => setCambioEstadoModalOpen(false); const handleSubmitCambioEstadoModal = async (idBobina: number, data: CambiarEstadoBobinaDto) => { setApiErrorMessage(null); try { await stockBobinaService.cambiarEstadoBobina(idBobina, data); cargarStock(); } @@ -167,7 +197,6 @@ const GestionarStockBobinasPage: React.FC = () => { const handleDeleteBobina = async (bobina: StockBobinaDto | null) => { if (!bobina) return; - // Permitir eliminar si está Disponible (1) o Dañada (3) if (bobina.idEstadoBobina !== ID_ESTADO_DISPONIBLE && bobina.idEstadoBobina !== ID_ESTADO_DANADA) { alert("Solo se pueden eliminar bobinas en estado 'Disponible' o 'Dañada'."); handleMenuClose(); @@ -181,26 +210,16 @@ const GestionarStockBobinasPage: React.FC = () => { handleMenuClose(); }; - const lastOpenedMenuButtonRef = React.useRef(null); - const handleMenuOpen = (event: React.MouseEvent, bobina: StockBobinaDto) => { setAnchorEl(event.currentTarget); setSelectedBobinaForRowMenu(bobina); - lastOpenedMenuButtonRef.current = event.currentTarget; // Guardar el botón que abrió el menú + lastOpenedMenuButtonRef.current = event.currentTarget; }; - const handleMenuClose = () => { setAnchorEl(null); - // No es estrictamente necesario limpiar selectedBobinaForRowMenu aquí, - // ya que se actualiza en el siguiente handleMenuOpen. - // Pero se puede ser explícito: setSelectedBobinaForRowMenu(null); - - // Devolver el foco al botón que abrió el menú si existe if (lastOpenedMenuButtonRef.current) { - setTimeout(() => { // Pequeño retraso para asegurar que el menú se haya cerrado visualmente - lastOpenedMenuButtonRef.current?.focus(); - }, 0); + setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0); } }; @@ -209,15 +228,26 @@ const GestionarStockBobinasPage: React.FC = () => { setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); }; const displayData = stock.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); - const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; + const formatDate = (dateString?: string | null) => { + if (!dateString) return '-'; + const date = new Date(dateString); + if (isNaN(date.getTime())) return '-'; - if (!loading && !puedeVer) return {error || "Acceso denegado."}; + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: '2-digit', + day: '2-digit', + timeZone: 'UTC' + }; + return new Intl.DateTimeFormat('es-AR', options).format(date); + }; + + if (!puedeVer) return {error || "Acceso denegado."}; return ( Stock de Bobinas - {/* ... (Filtros sin cambios) ... */} Filtros @@ -243,17 +273,37 @@ const GestionarStockBobinasPage: React.FC = () => { setFiltroRemito(e.target.value)} sx={{ minWidth: 150, flexGrow: 1 }} /> - setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170, flexGrow: 1 }} /> - setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170, flexGrow: 1 }} /> - {puedeIngresar && ()} + + setFiltroFechaHabilitado(e.target.checked)} />} + label="Filtrar por Fechas de Remitos" + /> + setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} disabled={!filtroFechaHabilitado} /> + setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} disabled={!filtroFechaHabilitado} /> + + + + + + {puedeIngresar && ()} + {loading && } - {error && !loading && {error}} + {error && !loading && {error}} {apiErrorMessage && {apiErrorMessage}} - {!loading && !error && puedeVer && ( + {!loading && !error && ( @@ -262,12 +312,11 @@ const GestionarStockBobinasPage: React.FC = () => { F. RemitoF. Estado PublicaciónSección Obs. - {/* Mostrar columna de acciones si tiene algún permiso de acción */} {(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) && Acciones} {displayData.length === 0 ? ( - No se encontraron bobinas con los filtros aplicados. + No se encontraron bobinas con los filtros aplicados. Haga clic en "Buscar" para iniciar una consulta. ) : ( displayData.map((b) => ( @@ -283,10 +332,9 @@ const GestionarStockBobinasPage: React.FC = () => { {(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) && ( handleMenuOpen(e, b)} - // El botón de menú se deshabilita si no hay NINGUNA acción posible para esa fila disabled={ !(b.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos) && - !(puedeCambiarEstado) && // Siempre se puede intentar cambiar estado (el modal lo validará) + !(puedeCambiarEstado) && !((b.idEstadoBobina === ID_ESTADO_DISPONIBLE || b.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar) } > @@ -310,21 +358,17 @@ const GestionarStockBobinasPage: React.FC = () => { Editar Datos )} - {/* --- CAMBIO: Permitir cambiar estado incluso si está Dañada --- */} {selectedBobinaForRowMenu && puedeCambiarEstado && ( { handleOpenCambioEstadoModal(selectedBobinaForRowMenu); handleMenuClose(); }}> Cambiar Estado )} - {/* --- CAMBIO: Permitir eliminar si está Disponible o Dañada --- */} {selectedBobinaForRowMenu && puedeEliminar && (selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && ( handleDeleteBobina(selectedBobinaForRowMenu)}> Eliminar Ingreso )} - - {/* Lógica para el MenuItem "Sin acciones" */} {selectedBobinaForRowMenu && !((selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos)) && !(puedeCambiarEstado) && @@ -333,6 +377,7 @@ const GestionarStockBobinasPage: React.FC = () => { } + {/* Modales sin cambios */} setApiErrorMessage(null)} diff --git a/Frontend/src/pages/Reportes/ReporteDetalleDistribucionCanillasPage.tsx b/Frontend/src/pages/Reportes/ReporteDetalleDistribucionCanillasPage.tsx index f0f8c33..41d9b5a 100644 --- a/Frontend/src/pages/Reportes/ReporteDetalleDistribucionCanillasPage.tsx +++ b/Frontend/src/pages/Reportes/ReporteDetalleDistribucionCanillasPage.tsx @@ -1,8 +1,8 @@ -import React, { useState, useCallback, useMemo, type JSXElementConstructor, type HTMLAttributes } from 'react'; // Añadido JSXElementConstructor, HTMLAttributes +import React, { useState, useCallback, useMemo, type JSXElementConstructor, type HTMLAttributes } from 'react'; import { - Box, Typography, Paper, CircularProgress, Alert, Button, type SxProps, type Theme // Añadido SxProps, Theme + Box, Typography, Paper, CircularProgress, Alert, Button, type SxProps, type Theme } from '@mui/material'; -import { DataGrid, type GridColDef, GridFooterContainer, GridFooter, type GridSlotsComponent } from '@mui/x-data-grid'; // Añadido GridSlotsComponent +import { DataGrid, type GridColDef, GridFooterContainer, GridFooter, type GridSlotsComponent } from '@mui/x-data-grid'; import { esES } from '@mui/x-data-grid/locales'; import reportesService from '../../services/Reportes/reportesService'; import type { ReporteDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ReporteDistribucionCanillasResponseDto'; @@ -11,7 +11,7 @@ import * as XLSX from 'xlsx'; import axios from 'axios'; // Para el tipo del footer en DataGridSectionProps -type FooterPropsOverrides = {}; // Puedes extender esto si tus footers tienen props específicos +type FooterPropsOverrides = {}; type CustomFooterType = JSXElementConstructor & { sx?: SxProps } & FooterPropsOverrides>; @@ -39,7 +39,7 @@ const DataGridSection: React.FC = ({ title, data, columns, } if (!rows || rows.length === 0) { - return No hay datos para {title.toLowerCase()}.; + return No hay datos para {title.toLowerCase()}.; } const slotsProp: Partial = {}; @@ -50,7 +50,7 @@ const DataGridSection: React.FC = ({ title, data, columns, return ( <> {title} - + = ({ title, data, columns, slots={slotsProp} // Usar el objeto slotsProp hideFooterSelectedRowCount={!!footerComponent} autoHeight={!!footerComponent} - sx={!footerComponent ? {} : { + sx={!footerComponent ? {} : { '& .MuiTablePagination-root': { display: 'none' }, '& .MuiDataGrid-selectedRowCount': { display: 'none' }, }} @@ -90,7 +90,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { const [totalesAccionistas, setTotalesAccionistas] = useState(initialTotals); const [totalesCanillasOtraFecha, setTotalesCanillasOtraFecha] = useState(initialTotals); const [totalesAccionistasOtraFecha, setTotalesAccionistasOtraFecha] = useState(initialTotals); - + const [totalesResumen, setTotalesResumen] = useState(initialTotals); const currencyFormatter = (value: number | null | undefined) => value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''; @@ -121,7 +121,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { setApiErrorParams(null); const empresaService = (await import('../../services/Distribucion/empresaService')).default; const empData = await empresaService.getEmpresaById(params.idEmpresa); - setCurrentParams({...params, nombreEmpresa: empData?.nombre}); + setCurrentParams({ ...params, nombreEmpresa: empData?.nombre }); setReportData(null); // Resetear totales @@ -129,11 +129,12 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { setTotalesAccionistas(initialTotals); setTotalesCanillasOtraFecha(initialTotals); setTotalesAccionistasOtraFecha(initialTotals); + setTotalesResumen(initialTotals); try { const data = await reportesService.getReporteDistribucionCanillas(params); - - const addIds = >(arr: T[] | undefined, prefix: string): Array => + + const addIds = >(arr: T[] | undefined, prefix: string): Array => (arr || []).map((item, index) => ({ ...item, id: `${prefix}-${item.publicacion || item.tipoVendedor || item.remito || item.devueltos || 'item'}-${index}-${Math.random().toString(36).substring(7)}` })); const processedData = { @@ -142,7 +143,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { canillasTodos: addIds(data.canillasTodos, 'all'), // Aún necesita IDs para DataGridSection canillasLiquidadasOtraFecha: addIds(data.canillasLiquidadasOtraFecha, 'canliq'), canillasAccionistasLiquidadasOtraFecha: addIds(data.canillasAccionistasLiquidadasOtraFecha, 'accliq'), - controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'), + controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'), controlDevolucionesRemitos: addIds(data.controlDevolucionesRemitos, 'cdr'), controlDevolucionesOtrosDias: addIds(data.controlDevolucionesOtrosDias, 'cdo') }; @@ -152,7 +153,8 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { calculateAndSetTotals(processedData.canillasAccionistas, setTotalesAccionistas); calculateAndSetTotals(processedData.canillasLiquidadasOtraFecha, setTotalesCanillasOtraFecha); calculateAndSetTotals(processedData.canillasAccionistasLiquidadasOtraFecha, setTotalesAccionistasOtraFecha); - + calculateAndSetTotals(processedData.canillasTodos, setTotalesResumen); + const noDataFound = Object.values(processedData).every(arr => !arr || arr.length === 0); if (noDataFound) { setError("No se encontraron datos para los parámetros seleccionados."); @@ -183,41 +185,41 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { const wb = XLSX.utils.book_new(); const formatAndSheet = (data: any[], sheetName: string, fields: Record) => { - if (data && data.length > 0) { - const exportedData = data.map(item => { - const row: Record = {}; - // Excluir el 'id' generado para DataGrid si existe - const { id, ...itemData } = item; - Object.keys(fields).forEach(key => { - row[fields[key]] = (itemData as any)[key]; // Usar itemData - if (key === 'fecha' && (itemData as any)[key]) { - row[fields[key]] = new Date((itemData as any)[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' }); - } - if ((key === 'totalRendir') && (itemData as any)[key] != null) { - row[fields[key]] = parseFloat((itemData as any)[key]).toFixed(2); - } - if (key === 'vendidos' && itemData.totalCantSalida != null && itemData.totalCantEntrada != null) { - row[fields[key]] = itemData.totalCantSalida - itemData.totalCantEntrada; - } - }); - return row; - }); - const ws = XLSX.utils.json_to_sheet(exportedData); - const headers = Object.values(fields); - ws['!cols'] = headers.map(h => { - const maxLen = Math.max(...exportedData.map(row => (row[h]?.toString() ?? '').length), h.length); - return { wch: maxLen + 2 }; - }); - ws['!freeze'] = { xSplit: 0, ySplit: 1 }; - XLSX.utils.book_append_sheet(wb, ws, sheetName); - } + if (data && data.length > 0) { + const exportedData = data.map(item => { + const row: Record = {}; + // Excluir el 'id' generado para DataGrid si existe + const { id, ...itemData } = item; + Object.keys(fields).forEach(key => { + row[fields[key]] = (itemData as any)[key]; // Usar itemData + if (key === 'fecha' && (itemData as any)[key]) { + row[fields[key]] = new Date((itemData as any)[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' }); + } + if ((key === 'totalRendir') && (itemData as any)[key] != null) { + row[fields[key]] = parseFloat((itemData as any)[key]).toFixed(2); + } + if (key === 'vendidos' && itemData.totalCantSalida != null && itemData.totalCantEntrada != null) { + row[fields[key]] = itemData.totalCantSalida - itemData.totalCantEntrada; + } + }); + return row; + }); + const ws = XLSX.utils.json_to_sheet(exportedData); + const headers = Object.values(fields); + ws['!cols'] = headers.map(h => { + const maxLen = Math.max(...exportedData.map(row => (row[h]?.toString() ?? '').length), h.length); + return { wch: maxLen + 2 }; + }); + ws['!freeze'] = { xSplit: 0, ySplit: 1 }; + XLSX.utils.book_append_sheet(wb, ws, sheetName); + } }; - + // Definición de campos para la exportación const fieldsCanillaAccionista = { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" }; - const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" }; + const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha: "Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" }; const fieldsTodos = { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" }; - + formatAndSheet(reportData.canillas, "Canillitas_Dia", fieldsCanillaAccionista); formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", fieldsCanillaAccionista); formatAndSheet(reportData.canillasTodos, "Resumen_Dia", fieldsTodos); @@ -273,13 +275,13 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { { field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) }, { field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) }, ]; - + const commonColumnsWithFecha: GridColDef[] = [ { field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1 }, { field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.1 }, { field: 'fecha', headerName: 'Fecha Mov.', width: 120, flex: 0.7, valueFormatter: (value) => value ? new Date(value as string).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-' }, { field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) }, - { field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))}, + { field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) }, { field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) }, { field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) }, ]; @@ -288,11 +290,11 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { { field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 }, { field: 'tipoVendedor', headerName: 'Tipo Vendedor', width: 150, flex: 0.8 }, { field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) }, - { field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))}, + { field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) }, { field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) }, { field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) }, ]; - + // --- Custom Footers --- const createCustomFooterComponent = (totals: TotalesComunes, columnsDef: GridColDef[]): CustomFooterType => { // Especificar el tipo de retorno const getCellStyle = (colConfig: GridColDef | undefined, isPlaceholder: boolean = false) => { @@ -303,11 +305,11 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { flex: colConfig.flex || undefined, minWidth: colConfig.minWidth || colConfig.width || defaultWidth, textAlign: (colConfig.align || 'right') as 'right' | 'left' | 'center', - pr: isPlaceholder || colConfig.field === columnsDef[columnsDef.length-1].field ? 0 : 1, + pr: isPlaceholder || colConfig.field === columnsDef[columnsDef.length - 1].field ? 0 : 1, fontWeight: 'bold', }; }; - + // eslint-disable-next-line react/display-name const FooterComponent: CustomFooterType = (props) => ( // El componente debe aceptar props { borderTop: (theme) => `1px solid ${theme.palette.divider}`, minHeight: '52px', }}> - + - theme.spacing(0, 1), display: 'flex', alignItems: 'center', + theme.spacing(0, 1), display: 'flex', alignItems: 'center', fontWeight: 'bold', marginLeft: 'auto', whiteSpace: 'nowrap', overflowX: 'auto', }}> - c.field === 'publicacion' || c.field === columnsDef[0].field)), textAlign:'right' }}>TOTALES: + c.field === 'publicacion' || c.field === columnsDef[0].field)), textAlign: 'right' }}>TOTALES: c.field === 'canilla' || c.field === 'tipoVendedor' || c.field === columnsDef[1].field), true) }}> - {columnsDef.some(c => c.field === 'fecha') && + {columnsDef.some(c => c.field === 'fecha') && c.field === 'fecha'), true) }}> } c.field === 'totalCantSalida'))}>{numberFormatter(totals.totalCantSalida)} c.field === 'totalCantEntrada'))}>{numberFormatter(totals.totalCantEntrada)} c.field === 'vendidos'))}>{numberFormatter(totals.vendidos)} - c.field === 'totalRendir')), pr:0 }}>{currencyFormatter(totals.totalRendir)} + c.field === 'totalRendir')), pr: 0 }}>{currencyFormatter(totals.totalRendir)} ); @@ -341,7 +343,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { const FooterAccionistas = useMemo(() => createCustomFooterComponent(totalesAccionistas, commonColumns), [totalesAccionistas]); const FooterCanillasOtraFecha = useMemo(() => createCustomFooterComponent(totalesCanillasOtraFecha, commonColumnsWithFecha), [totalesCanillasOtraFecha]); const FooterAccionistasOtraFecha = useMemo(() => createCustomFooterComponent(totalesAccionistasOtraFecha, commonColumnsWithFecha), [totalesAccionistasOtraFecha]); - + const FooterResumen = useMemo(() => createCustomFooterComponent(totalesResumen, columnsTodos), [totalesResumen, columnsTodos]); if (showParamSelector) { return ( @@ -357,16 +359,16 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { ); } - + return ( - Reporte: Detalle Distribución Canillitas ({currentParams?.nombreEmpresa}) - {currentParams?.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', {timeZone:'UTC'}) : ''} + Reporte: Detalle Distribución Canillitas ({currentParams?.nombreEmpresa}) - {currentParams?.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', { timeZone: 'UTC' }) : ''} -