Refinamiento de permisos y ajustes en controles. Añade gestión sobre saldos y visualización. Entre otros..
This commit is contained in:
@@ -7,6 +7,7 @@ import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
const contablesSubModules = [
|
||||
{ label: 'Pagos Distribuidores', path: 'pagos-distribuidores' },
|
||||
{ label: 'Notas Crédito/Débito', path: 'notas-cd' },
|
||||
{ label: 'Gestión de Saldos', path: 'gestion-saldos' },
|
||||
{ label: 'Tipos de Pago', path: 'tipos-pago' },
|
||||
];
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ const GestionarNotasCDPage: React.FC = () => {
|
||||
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");
|
||||
|
||||
257
Frontend/src/pages/Contables/GestionarSaldosPage.tsx
Normal file
257
Frontend/src/pages/Contables/GestionarSaldosPage.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, IconButton, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, FormControl, InputLabel, Select
|
||||
} from '@mui/material';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
import EditNoteIcon from '@mui/icons-material/EditNote'; // Icono para ajustar saldo
|
||||
|
||||
import saldoService from '../../services/Contables/saldoService';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
import canillaService from '../../services/Distribucion/canillaService';
|
||||
|
||||
import type { SaldoGestionDto } from '../../models/dtos/Contables/SaldoGestionDto';
|
||||
import type { AjusteSaldoRequestDto } from '../../models/dtos/Contables/AjusteSaldoRequestDto';
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
|
||||
import AjusteSaldoModal from '../../components/Modals/Contables/AjusteSaldoModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
type TipoDestinoFiltro = 'Distribuidores' | 'Canillas' | '';
|
||||
|
||||
const GestionarSaldosPage: React.FC = () => {
|
||||
const [saldos, setSaldos] = useState<SaldoGestionDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); // Para modal
|
||||
|
||||
// Filtros
|
||||
const [filtroTipoDestino, setFiltroTipoDestino] = useState<TipoDestinoFiltro>('');
|
||||
const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>('');
|
||||
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||
|
||||
const [destinatariosDropdown, setDestinatariosDropdown] = useState<(DistribuidorDto | CanillaDto)[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
|
||||
const [modalAjusteOpen, setModalAjusteOpen] = useState(false);
|
||||
const [saldoParaAjustar, setSaldoParaAjustar] = useState<SaldoGestionDto | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25);
|
||||
// No necesitamos menú de acciones por fila si el ajuste es la única acción
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerSaldos = isSuperAdmin || tienePermiso("CS001"); // Permiso para ver
|
||||
const puedeAjustarSaldos = isSuperAdmin || tienePermiso("CS002"); // Permiso para ajustar
|
||||
|
||||
|
||||
const fetchDropdownData = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
setError(null);
|
||||
try {
|
||||
const empData = await empresaService.getAllEmpresas();
|
||||
setEmpresas(empData);
|
||||
|
||||
if (filtroTipoDestino === 'Distribuidores') {
|
||||
const distData = await distribuidorService.getAllDistribuidores();
|
||||
setDestinatariosDropdown(distData);
|
||||
} else if (filtroTipoDestino === 'Canillas') {
|
||||
const canData = await canillaService.getAllCanillas(undefined, undefined, true); // Solo activos
|
||||
setDestinatariosDropdown(canData);
|
||||
} else {
|
||||
setDestinatariosDropdown([]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error cargando datos para filtros:", err);
|
||||
setError("Error al cargar opciones de filtro.");
|
||||
} finally {
|
||||
setLoadingFiltersDropdown(false);
|
||||
}
|
||||
}, [filtroTipoDestino]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDropdownData();
|
||||
}, [fetchDropdownData]);
|
||||
|
||||
|
||||
const cargarSaldos = useCallback(async () => {
|
||||
if (!puedeVerSaldos) {
|
||||
setError("No tiene permiso para ver saldos."); setLoading(false); return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const params = {
|
||||
destino: filtroTipoDestino || undefined, // Enviar undefined si está vacío
|
||||
idDestino: filtroIdDestino ? Number(filtroIdDestino) : undefined,
|
||||
idEmpresa: filtroIdEmpresa ? Number(filtroIdEmpresa) : undefined,
|
||||
};
|
||||
const data = await saldoService.getAllSaldosGestion(params);
|
||||
setSaldos(data);
|
||||
} catch (err) {
|
||||
console.error("Error al cargar saldos:", err);
|
||||
setError('Error al cargar los saldos.');
|
||||
setSaldos([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [puedeVerSaldos, filtroTipoDestino, filtroIdDestino, filtroIdEmpresa]);
|
||||
|
||||
useEffect(() => {
|
||||
cargarSaldos();
|
||||
}, [cargarSaldos]);
|
||||
|
||||
|
||||
const handleOpenAjusteModal = (saldo: SaldoGestionDto) => {
|
||||
if (!puedeAjustarSaldos) {
|
||||
setApiErrorMessage("No tiene permiso para ajustar saldos.");
|
||||
return;
|
||||
}
|
||||
setSaldoParaAjustar(saldo);
|
||||
setApiErrorMessage(null);
|
||||
setModalAjusteOpen(true);
|
||||
};
|
||||
const handleCloseAjusteModal = () => {
|
||||
setModalAjusteOpen(false);
|
||||
setSaldoParaAjustar(null);
|
||||
};
|
||||
|
||||
const handleSubmitAjusteModal = async (data: AjusteSaldoRequestDto) => {
|
||||
if (!puedeAjustarSaldos) return;
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await saldoService.ajustarSaldo(data);
|
||||
cargarSaldos(); // Recargar lista para ver el saldo actualizado
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Error al aplicar el ajuste de saldo.';
|
||||
setApiErrorMessage(message);
|
||||
throw err; // Para que el modal sepa que hubo error
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
|
||||
const displayData = saldos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString).toLocaleString('es-AR', {timeZone:'UTC'}) : '-';
|
||||
const formatCurrency = (value: number) => value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' });
|
||||
|
||||
|
||||
if (!loading && !puedeVerSaldos) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado a esta sección."}</Alert></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestión de Saldos</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 }}>
|
||||
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
|
||||
<InputLabel>Tipo Destinatario</InputLabel>
|
||||
<Select value={filtroTipoDestino} label="Tipo Destinatario"
|
||||
onChange={(e) => {
|
||||
setFiltroTipoDestino(e.target.value as TipoDestinoFiltro);
|
||||
setFiltroIdDestino(''); // Resetear destinatario al cambiar tipo
|
||||
}}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
<MenuItem value="Distribuidores">Distribuidores</MenuItem>
|
||||
<MenuItem value="Canillas">Canillitas</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 220, flexGrow: 1 }} disabled={loadingFiltersDropdown || !filtroTipoDestino}>
|
||||
<InputLabel>Destinatario Específico</InputLabel>
|
||||
<Select value={filtroIdDestino} label="Destinatario Específico"
|
||||
onChange={(e) => setFiltroIdDestino(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
{destinatariosDropdown.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: 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>
|
||||
{/* No hay botón de "Agregar Saldo", se crean automáticamente */}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeVerSaldos && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell sx={{fontWeight:'bold'}}>Destinatario</TableCell>
|
||||
<TableCell sx={{fontWeight:'bold'}}>Tipo</TableCell>
|
||||
<TableCell sx={{fontWeight:'bold'}}>Empresa</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight:'bold'}}>Monto Saldo</TableCell>
|
||||
<TableCell sx={{fontWeight:'bold'}}>Últ. Modificación</TableCell>
|
||||
{puedeAjustarSaldos && <TableCell align="right" sx={{fontWeight:'bold'}}>Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={puedeAjustarSaldos ? 6 : 5} align="center">No se encontraron saldos.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((s) => (
|
||||
<TableRow key={s.idSaldo} hover
|
||||
sx={{ backgroundColor: s.monto < 0 ? 'rgba(255, 0, 0, 0.05)' : (s.monto > 0 ? 'rgba(0, 255, 0, 0.05)' : 'inherit')}}
|
||||
>
|
||||
<TableCell>{s.nombreDestinatario}</TableCell>
|
||||
<TableCell>{s.destino}</TableCell>
|
||||
<TableCell>{s.nombreEmpresa}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight:500}}>{formatCurrency(s.monto)}</TableCell>
|
||||
<TableCell>{formatDate(s.fechaUltimaModificacion)}</TableCell>
|
||||
{puedeAjustarSaldos && (
|
||||
<TableCell align="right">
|
||||
<IconButton size="small" onClick={() => handleOpenAjusteModal(s)} color="primary">
|
||||
<EditNoteIcon fontSize="small"/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]} component="div" count={saldos.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
{saldoParaAjustar &&
|
||||
<AjusteSaldoModal
|
||||
open={modalAjusteOpen}
|
||||
onClose={handleCloseAjusteModal}
|
||||
onSubmit={handleSubmitAjusteModal}
|
||||
saldoParaAjustar={saldoParaAjustar}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarSaldosPage;
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/pages/configuracion/GestionarTiposPagoPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
@@ -7,10 +6,10 @@ import {
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add'; // Icono para agregar
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones
|
||||
import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar
|
||||
import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar
|
||||
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 tipoPagoService from '../../services/Contables/tipoPagoService';
|
||||
import type { TipoPago } from '../../models/Entities/TipoPago';
|
||||
import type { CreateTipoPagoDto } from '../../models/dtos/Contables/CreateTipoPagoDto';
|
||||
@@ -30,20 +29,26 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25); // Cambiado a un valor más común
|
||||
|
||||
// Para el menú contextual de cada fila
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedTipoPagoRow, setSelectedTipoPagoRow] = useState<TipoPago | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions(); // Obtener también isSuperAdmin
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("CT001"); // << AÑADIR ESTA LÍNEA
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CT002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("CT003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("CT004");
|
||||
|
||||
|
||||
const cargarTiposPago = useCallback(async () => {
|
||||
if (!puedeVer) { // << AÑADIR CHEQUEO DE PERMISO AQUÍ
|
||||
setError("No tiene permiso para ver los tipos de pago.");
|
||||
setLoading(false);
|
||||
setTiposPago([]); // Asegurar que no se muestren datos previos
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -55,7 +60,7 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filtroNombre]);
|
||||
}, [filtroNombre, puedeVer]); // << AÑADIR puedeVer A LAS DEPENDENCIAS
|
||||
|
||||
useEffect(() => {
|
||||
cargarTiposPago();
|
||||
@@ -73,15 +78,15 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateTipoPagoDto | UpdateTipoPagoDto) => {
|
||||
setApiErrorMessage(null); // Limpiar error previo
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (editingTipoPago && 'idTipoPago' in data) { // Es Update
|
||||
if (editingTipoPago && editingTipoPago.idTipoPago) {
|
||||
await tipoPagoService.updateTipoPago(editingTipoPago.idTipoPago, data as UpdateTipoPagoDto);
|
||||
} else { // Es Create
|
||||
} else {
|
||||
await tipoPagoService.createTipoPago(data as CreateTipoPagoDto);
|
||||
}
|
||||
cargarTiposPago(); // Recargar lista
|
||||
// onClose se llama desde el modal en caso de éxito
|
||||
cargarTiposPago();
|
||||
// onClose se llama desde el modal si todo va bien
|
||||
} catch (err: any) {
|
||||
console.error("Error en submit modal (padre):", err);
|
||||
if (axios.isAxiosError(err) && err.response) {
|
||||
@@ -89,11 +94,12 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
} else {
|
||||
setApiErrorMessage('Ocurrió un error inesperado al guardar.');
|
||||
}
|
||||
throw err; // Re-lanzar para que el modal sepa que hubo error y no se cierre
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
// ... (sin cambios)
|
||||
if (window.confirm('¿Está seguro de que desea eliminar este tipo de pago?')) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
@@ -126,12 +132,24 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 25));
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); // << CORREGIDO: base 10, no 25
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const displayData = tiposPago.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
// Renderizado condicional si no tiene permiso para ver
|
||||
if (!loading && !puedeVer) { // << AÑADIR ESTE BLOQUE
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Gestionar Tipos de Pago
|
||||
</Typography>
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
@@ -146,10 +164,8 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
size="small"
|
||||
value={filtroNombre}
|
||||
onChange={(e) => setFiltroNombre(e.target.value)}
|
||||
// sx={{ flexGrow: 1 }} // Opcional, para que ocupe más espacio
|
||||
disabled={!puedeVer || loading} // Deshabilitar si no puede ver o está cargando
|
||||
/>
|
||||
{/* El botón de búsqueda se activa al cambiar el texto, pero puedes añadir uno explícito */}
|
||||
{/* <Button variant="contained" onClick={cargarTiposPago}>Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
@@ -164,43 +180,50 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{/* Mostrar error de carga si no es un error de "sin permiso" y no hay error de API */}
|
||||
{error && !loading && puedeVer && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
|
||||
{!loading && !error && (
|
||||
{!loading && !error && puedeVer && ( // Asegurar que puedeVer sea true para mostrar la tabla
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Nombre</TableCell>
|
||||
<TableCell>Detalle</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
{/* Mostrar columna de acciones solo si tiene algún permiso de acción */}
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={3} align="center">No se encontraron tipos de pago.</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">
|
||||
No se encontraron tipos de pago.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
displayData.map((tipo) => (
|
||||
<TableRow key={tipo.idTipoPago}>
|
||||
<TableCell>{tipo.nombre}</TableCell>
|
||||
<TableCell>{tipo.detalle || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={(e) => handleMenuOpen(e, tipo)}
|
||||
disabled={!puedeModificar && !puedeEliminar}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={(e) => handleMenuOpen(e, tipo)}
|
||||
disabled={!puedeModificar && !puedeEliminar}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]}
|
||||
rowsPerPageOptions={[25, 50, 100]} // Opciones más estándar
|
||||
component="div"
|
||||
count={tiposPago.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
@@ -217,20 +240,19 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
>
|
||||
{puedeModificar && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedTipoPagoRow!); handleMenuClose(); }}>
|
||||
{puedeModificar && selectedTipoPagoRow && ( // Asegurar que selectedTipoPagoRow no sea null
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedTipoPagoRow); handleMenuClose(); }}>
|
||||
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Modificar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && (
|
||||
<MenuItem onClick={() => handleDelete(selectedTipoPagoRow!.idTipoPago)}>
|
||||
{puedeEliminar && selectedTipoPagoRow && ( // Asegurar que selectedTipoPagoRow no sea null
|
||||
<MenuItem onClick={() => handleDelete(selectedTipoPagoRow.idTipoPago)}>
|
||||
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Eliminar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Si no tiene ningún permiso, el menú podría estar vacío o no mostrarse */}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
{selectedTipoPagoRow && (!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<TipoPagoFormModal
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/pages/distribucion/DistribucionIndexPage.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Chip, FormControlLabel
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Chip, FormControlLabel, ListItemIcon, ListItemText // << AÑADIR ListItemIcon, ListItemText
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
|
||||
import ToggleOffIcon from '@mui/icons-material/ToggleOff';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EventNoteIcon from '@mui/icons-material/EventNote'; // << AÑADIR IMPORTACIÓN DEL ICONO
|
||||
import { useNavigate } from 'react-router-dom'; // << AÑADIR IMPORTACIÓN DE useNavigate
|
||||
|
||||
import canillaService from '../../services/Distribucion/canillaService';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto';
|
||||
@@ -31,17 +34,24 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25); // << CAMBIADO DE 5 a 25 (valor más común)
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedCanillitaRow, setSelectedCanillitaRow] = useState<CanillaDto | null>(null);
|
||||
|
||||
const navigate = useNavigate(); // << INICIALIZAR useNavigate
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("CG001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CG002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("CG003");
|
||||
// CG004 para Porcentajes/Montos, se gestionará por separado.
|
||||
const puedeDarBaja = isSuperAdmin || tienePermiso("CG005");
|
||||
// Permisos para Novedades
|
||||
const puedeGestionarNovedades = isSuperAdmin || tienePermiso("CG006"); // << DEFINIR PERMISO
|
||||
// Para la opción "Ver Novedades", podemos usar el permiso de ver canillitas (CG001)
|
||||
// O si solo se quiere mostrar si puede gestionarlas, usar puedeGestionarNovedades
|
||||
const puedeVerNovedadesCanilla = puedeVer || puedeGestionarNovedades; // << LÓGICA PARA MOSTRAR LA OPCIÓN
|
||||
|
||||
|
||||
const cargarCanillitas = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
@@ -51,12 +61,12 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const legajoNum = filtroLegajo ? parseInt(filtroLegajo, 25) : undefined;
|
||||
const legajoNum = filtroLegajo ? parseInt(filtroLegajo, 10) : undefined; // << CORREGIDO: parseInt con base 10
|
||||
if (filtroLegajo && isNaN(legajoNum!)) {
|
||||
setApiErrorMessage("Legajo debe ser un número.");
|
||||
setCanillitas([]); // Limpiar resultados si el filtro es inválido
|
||||
setLoading(false);
|
||||
return;
|
||||
setApiErrorMessage("Legajo debe ser un número.");
|
||||
setCanillitas([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const data = await canillaService.getAllCanillas(filtroNomApe, legajoNum, filtroSoloActivos);
|
||||
setCanillitas(data);
|
||||
@@ -83,6 +93,7 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
await canillaService.createCanilla(data as CreateCanillaDto);
|
||||
}
|
||||
cargarCanillitas();
|
||||
// No es necesario llamar a handleCloseModal aquí si el modal se cierra solo en éxito
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el canillita.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
@@ -93,17 +104,22 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
setApiErrorMessage(null);
|
||||
const accion = canillita.baja ? "reactivar" : "dar de baja";
|
||||
if (window.confirm(`¿Está seguro de que desea ${accion} a ${canillita.nomApe}?`)) {
|
||||
try {
|
||||
await canillaService.toggleBajaCanilla(canillita.idCanilla, { darDeBaja: !canillita.baja });
|
||||
cargarCanillitas();
|
||||
} catch (err:any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${accion} el canillita.`;
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
try {
|
||||
await canillaService.toggleBajaCanilla(canillita.idCanilla, { darDeBaja: !canillita.baja });
|
||||
cargarCanillitas();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${accion} el canillita.`;
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleOpenNovedades = (idCan: number) => {
|
||||
navigate(`/distribucion/canillas/${idCan}/novedades`);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, canillita: CanillaDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedCanillitaRow(canillita);
|
||||
};
|
||||
@@ -118,98 +134,120 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
const displayData = canillitas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso."}</Alert></Box>;
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert></Box>; // Mensaje más genérico
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Canillitas</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Filtrar por Nombre/Apellido"
|
||||
variant="outlined"
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Filtrar por Nombre/Apellido"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroNomApe}
|
||||
onChange={(e) => setFiltroNomApe(e.target.value)}
|
||||
sx={{ flex: 2, minWidth: '250px' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Filtrar por Legajo"
|
||||
type="number" // Mantener como number para el input, la conversión se hace al usarlo
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroLegajo}
|
||||
onChange={(e) => setFiltroLegajo(e.target.value)}
|
||||
sx={{ flex: 1, minWidth: '150px' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={filtroSoloActivos === undefined ? true : filtroSoloActivos} // Default a true
|
||||
onChange={(e) => setFiltroSoloActivos(e.target.checked)}
|
||||
size="small"
|
||||
value={filtroNomApe}
|
||||
onChange={(e) => setFiltroNomApe(e.target.value)}
|
||||
sx={{ flex: 2, minWidth: '250px' }} // Dar más espacio al nombre
|
||||
/>
|
||||
<TextField
|
||||
label="Filtrar por Legajo"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroLegajo}
|
||||
onChange={(e) => setFiltroLegajo(e.target.value)}
|
||||
sx={{ flex: 1, minWidth: '150px' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={filtroSoloActivos === undefined ? true : filtroSoloActivos}
|
||||
onChange={(e) => setFiltroSoloActivos(e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label="Ver Activos"
|
||||
sx={{ flexShrink: 0 }} // Para que el label no se comprima demasiado
|
||||
/>
|
||||
{/* <Button variant="contained" onClick={cargarCanillitas} size="small">Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
/>
|
||||
}
|
||||
label="Ver Activos" // Cambiado el label para más claridad
|
||||
sx={{ flexShrink: 0 }}
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Canillita</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>}
|
||||
{/* Mostrar error general si no hay error de API específico */}
|
||||
{error && !apiErrorMessage && !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>Legajo</TableCell><TableCell>Nombre y Apellido</TableCell>
|
||||
<TableCell>Zona</TableCell><TableCell>Empresa</TableCell>
|
||||
<TableCell>Accionista</TableCell><TableCell>Estado</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={7} align="center">No se encontraron canillitas.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((c) => (
|
||||
<TableRow key={c.idCanilla} hover sx={{ backgroundColor: c.baja ? '#ffebee' : 'inherit' }}>
|
||||
<TableCell>{c.legajo || '-'}</TableCell><TableCell>{c.nomApe}</TableCell>
|
||||
<TableCell>{c.nombreZona}</TableCell><TableCell>{c.empresa === 0 ? '-' : c.nombreEmpresa}</TableCell>
|
||||
<TableCell>{c.accionista ? <Chip label="Sí" color="success" size="small" variant="outlined"/> : <Chip label="No" color="default" size="small" variant="outlined"/>}</TableCell>
|
||||
<TableCell>{c.baja ? <Chip label="Baja" color="error" size="small" /> : <Chip label="Activo" color="success" size="small" />}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeDarBaja}>
|
||||
<MoreVertIcon />
|
||||
{!loading && !error && puedeVer && ( // Asegurar que puedeVer sea true también aquí
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Legajo</TableCell><TableCell>Nombre y Apellido</TableCell>
|
||||
<TableCell>Zona</TableCell><TableCell>Empresa</TableCell>
|
||||
<TableCell>Accionista</TableCell><TableCell>Estado</TableCell>
|
||||
{/* Mostrar acciones solo si tiene algún permiso para el menú */}
|
||||
{(puedeModificar || puedeDarBaja || puedeVerNovedadesCanilla) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={(puedeModificar || puedeDarBaja || puedeVerNovedadesCanilla) ? 7 : 6} align="center">No se encontraron canillitas.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((c) => (
|
||||
<TableRow key={c.idCanilla} hover sx={{ backgroundColor: c.baja ? '#ffebee' : 'inherit' }}>
|
||||
<TableCell>{c.legajo || '-'}</TableCell><TableCell>{c.nomApe}</TableCell>
|
||||
<TableCell>{c.nombreZona}</TableCell><TableCell>{c.empresa === 0 ? '-' : c.nombreEmpresa}</TableCell>
|
||||
<TableCell>{c.accionista ? <Chip label="Sí" color="success" size="small" variant="outlined" /> : <Chip label="No" color="default" size="small" variant="outlined" />}</TableCell>
|
||||
<TableCell>{c.baja ? <Chip label="Baja" color="error" size="small" /> : <Chip label="Activo" color="success" size="small" />}</TableCell>
|
||||
{(puedeModificar || puedeDarBaja || puedeVerNovedadesCanilla) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, c)}
|
||||
// Deshabilitar si NO tiene NINGUNO de los permisos para las acciones del menú
|
||||
disabled={!puedeModificar && !puedeDarBaja && !puedeVerNovedadesCanilla}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]} component="div" count={canillitas.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]} component="div" count={canillitas.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow!); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
||||
{puedeDarBaja && selectedCanillitaRow && (
|
||||
<MenuItem onClick={() => handleToggleBaja(selectedCanillitaRow)}>
|
||||
{selectedCanillitaRow.baja ? <ToggleOnIcon sx={{mr:1}}/> : <ToggleOffIcon sx={{mr:1}}/>}
|
||||
{selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'}
|
||||
</MenuItem>
|
||||
{/* Mostrar opción de Novedades si tiene permiso de ver canillitas o gestionar novedades */}
|
||||
{puedeVerNovedadesCanilla && selectedCanillitaRow && (
|
||||
<MenuItem onClick={() => handleOpenNovedades(selectedCanillitaRow.idCanilla)}>
|
||||
<ListItemIcon><EventNoteIcon /></ListItemIcon>
|
||||
<ListItemText>Novedades</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeModificar && selectedCanillitaRow && ( // Asegurar que selectedCanillitaRow existe
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow); handleMenuClose(); }}>
|
||||
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Modificar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeDarBaja && selectedCanillitaRow && (
|
||||
<MenuItem onClick={() => handleToggleBaja(selectedCanillitaRow)}>
|
||||
<ListItemIcon>{selectedCanillitaRow.baja ? <ToggleOnIcon /> : <ToggleOffIcon />}</ListItemIcon>
|
||||
<ListItemText>{selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'}</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{/* Mostrar "Sin acciones" si no hay ninguna acción permitida para la fila seleccionada */}
|
||||
{selectedCanillitaRow && !puedeModificar && !puedeDarBaja && !puedeVerNovedadesCanilla && (
|
||||
<MenuItem disabled>Sin acciones</MenuItem>
|
||||
)}
|
||||
{(!puedeModificar && !puedeDarBaja) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<CanillaFormModal
|
||||
|
||||
@@ -146,8 +146,8 @@ const GestionarEmpresasPage: React.FC = () => {
|
||||
// Si no tiene permiso para ver, mostrar mensaje y salir
|
||||
if (!loading && !puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Empresas</Typography>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Empresas</Typography>
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,8 @@ 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
|
||||
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle,
|
||||
ToggleButtonGroup, ToggleButton
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import PrintIcon from '@mui/icons-material/Print';
|
||||
@@ -19,7 +20,7 @@ import canillaService from '../../services/Distribucion/canillaService';
|
||||
|
||||
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 { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto';
|
||||
|
||||
@@ -28,25 +29,32 @@ import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
|
||||
type TipoDestinatarioFiltro = 'canillitas' | 'accionistas';
|
||||
|
||||
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);
|
||||
const [loading, setLoading] = useState(true); // Para carga principal de movimientos
|
||||
const [error, setError] = useState<string | null>(null); // Error general o de carga
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); // Para errores de modal/API
|
||||
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFecha, setFiltroFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
||||
const [filtroIdCanilla, setFiltroIdCanilla] = useState<number | string>('');
|
||||
const [filtroEstadoLiquidacion, setFiltroEstadoLiquidacion] = useState<'todos' | 'liquidados' | 'noLiquidados'>('noLiquidados');
|
||||
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
|
||||
const [filtroIdCanillitaSeleccionado, setFiltroIdCanillitaSeleccionado] = useState<number | string>('');
|
||||
const [filtroTipoDestinatario, setFiltroTipoDestinatario] = useState<TipoDestinatarioFiltro>('canillitas');
|
||||
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
|
||||
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [destinatariosDropdown, setDestinatariosDropdown] = useState<CanillaDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaCanillaDto | null>(null);
|
||||
const [prefillModalData, setPrefillModalData] = useState<{
|
||||
fecha?: string;
|
||||
idCanilla?: number | string;
|
||||
nombreCanilla?: string; // << AÑADIDO PARA PASAR AL MODAL
|
||||
idPublicacion?: number | string;
|
||||
} | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25);
|
||||
@@ -64,70 +72,123 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const puedeLiquidar = isSuperAdmin || tienePermiso("MC005");
|
||||
const puedeEliminarLiquidados = isSuperAdmin || tienePermiso("MC006");
|
||||
|
||||
// Función para formatear fechas YYYY-MM-DD a DD/MM/YYYY
|
||||
const formatDate = (dateString?: string | null): string => {
|
||||
if (!dateString) return '-';
|
||||
const datePart = dateString.split('T')[0];
|
||||
const parts = datePart.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
}
|
||||
if (parts.length === 3) { return `${parts[2]}/${parts[1]}/${parts[0]}`; }
|
||||
return datePart;
|
||||
};
|
||||
|
||||
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(() => {
|
||||
const fetchPublicaciones = async () => {
|
||||
setLoadingFiltersDropdown(true); // Mover al inicio de la carga de pubs
|
||||
try {
|
||||
const pubsData = await publicacionService.getPublicacionesForDropdown(true);
|
||||
setPublicaciones(pubsData);
|
||||
} catch (err) {
|
||||
console.error("Error cargando publicaciones para filtro:",err);
|
||||
setError("Error al cargar publicaciones."); // Usar error general
|
||||
} finally {
|
||||
// No poner setLoadingFiltersDropdown(false) aquí, esperar a que ambas cargas terminen
|
||||
}
|
||||
};
|
||||
fetchPublicaciones();
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
|
||||
const fetchDestinatariosParaDropdown = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true); // Poner al inicio de esta carga también
|
||||
setFiltroIdCanillitaSeleccionado('');
|
||||
setDestinatariosDropdown([]);
|
||||
setError(null); // Limpiar errores de carga de dropdowns previos
|
||||
try {
|
||||
const esAccionistaFilter = filtroTipoDestinatario === 'accionistas';
|
||||
const data = await canillaService.getAllCanillas(undefined, undefined, true, esAccionistaFilter);
|
||||
setDestinatariosDropdown(data);
|
||||
} catch (err) {
|
||||
console.error("Error cargando destinatarios para filtro:", err);
|
||||
setError("Error al cargar canillitas/accionistas."); // Usar error general
|
||||
} finally {
|
||||
setLoadingFiltersDropdown(false); // Poner al final de AMBAS cargas de dropdown
|
||||
}
|
||||
}, [filtroTipoDestinatario]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDestinatariosParaDropdown();
|
||||
}, [fetchDestinatariosParaDropdown]);
|
||||
|
||||
|
||||
const cargarMovimientos = useCallback(async () => {
|
||||
if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; }
|
||||
if (!puedeVer) { setError("No tiene permiso para ver esta sección."); setLoading(false); return; }
|
||||
if (!filtroFecha || !filtroIdCanillitaSeleccionado) {
|
||||
if (loading) setLoading(false);
|
||||
setMovimientos([]);
|
||||
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,
|
||||
fechaDesde: filtroFecha,
|
||||
fechaHasta: filtroFecha,
|
||||
idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null,
|
||||
idCanilla: filtroIdCanilla ? Number(filtroIdCanilla) : null,
|
||||
liquidados: liquidadosFilter,
|
||||
incluirNoLiquidados: filtroEstadoLiquidacion === 'todos' ? null : incluirNoLiquidadosFilter,
|
||||
idCanilla: Number(filtroIdCanillitaSeleccionado),
|
||||
liquidados: null,
|
||||
incluirNoLiquidados: null,
|
||||
};
|
||||
const data = await entradaSalidaCanillaService.getAllEntradasSalidasCanilla(params);
|
||||
setMovimientos(data);
|
||||
setSelectedIdsParaLiquidar(new Set()); // Limpiar selección al recargar
|
||||
setSelectedIdsParaLiquidar(new Set());
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar movimientos.');
|
||||
} finally { setLoading(false); }
|
||||
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdCanilla, filtroEstadoLiquidacion]);
|
||||
console.error("Error al cargar movimientos:", err);
|
||||
setError('Error al cargar movimientos.');
|
||||
setMovimientos([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdCanillitaSeleccionado]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filtroFecha && filtroIdCanillitaSeleccionado) {
|
||||
cargarMovimientos();
|
||||
} else {
|
||||
setMovimientos([]);
|
||||
if (loading) setLoading(false); // Asegurar que no se quede en loading si los filtros se limpian
|
||||
}
|
||||
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]); // `cargarMovimientos` ya tiene sus dependencias
|
||||
|
||||
useEffect(() => { cargarMovimientos(); }, [cargarMovimientos]);
|
||||
|
||||
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
|
||||
setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
if (!puedeCrear && !item) {
|
||||
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
|
||||
return;
|
||||
}
|
||||
if (item && !puedeModificar) {
|
||||
setApiErrorMessage("No tiene permiso para modificar movimientos.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (item) {
|
||||
setEditingMovimiento(item);
|
||||
setPrefillModalData(null);
|
||||
} else {
|
||||
// --- CAMBIO: Obtener nombre del canillita seleccionado para prefill ---
|
||||
const canillitaSeleccionado = destinatariosDropdown.find(
|
||||
c => c.idCanilla === Number(filtroIdCanillitaSeleccionado)
|
||||
);
|
||||
setEditingMovimiento(null);
|
||||
setPrefillModalData({
|
||||
fecha: filtroFecha,
|
||||
idCanilla: filtroIdCanillitaSeleccionado,
|
||||
nombreCanilla: canillitaSeleccionado?.nomApe, // << AÑADIR NOMBRE
|
||||
idPublicacion: filtroIdPublicacion
|
||||
});
|
||||
}
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
// ... handleDelete, handleMenuOpen, handleMenuClose, handleSelectRowForLiquidar, handleSelectAllForLiquidar, handleOpenLiquidarDialog, handleCloseLiquidarDialog sin cambios ...
|
||||
const handleDelete = async (idParte: number) => {
|
||||
if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
@@ -138,7 +199,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => {
|
||||
// Almacenar el idParte en el propio elemento del menú para referencia
|
||||
event.currentTarget.setAttribute('data-rowid', item.idParte.toString());
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedRow(item);
|
||||
@@ -154,7 +214,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
});
|
||||
};
|
||||
const handleSelectAllForLiquidar = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.checked) {
|
||||
if (event.target.checked) {
|
||||
const newSelectedIds = new Set(movimientos.filter(m => !m.liquidado).map(m => m.idParte));
|
||||
setSelectedIdsParaLiquidar(newSelectedIds);
|
||||
} else {
|
||||
@@ -170,74 +230,65 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
setOpenLiquidarDialog(true);
|
||||
};
|
||||
const handleCloseLiquidarDialog = () => setOpenLiquidarDialog(false);
|
||||
|
||||
|
||||
const handleConfirmLiquidar = async () => {
|
||||
if (selectedIdsParaLiquidar.size === 0) {
|
||||
setApiErrorMessage("No hay movimientos seleccionados para liquidar.");
|
||||
return;
|
||||
}
|
||||
if (!fechaLiquidacionDialog) {
|
||||
setApiErrorMessage("Debe seleccionar una fecha de liquidación.");
|
||||
return;
|
||||
}
|
||||
|
||||
// --- VALIDACIÓN DE FECHA ---
|
||||
const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z'); // Usar Z para consistencia con formatDate si es necesario, o T00:00:00 para local
|
||||
|
||||
if (selectedIdsParaLiquidar.size === 0) { /* ... */ return; }
|
||||
if (!fechaLiquidacionDialog) { /* ... */ return; }
|
||||
// ... (validación de fecha sin cambios)
|
||||
const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z');
|
||||
let fechaMovimientoMasReciente: Date | null = null;
|
||||
|
||||
selectedIdsParaLiquidar.forEach(idParte => {
|
||||
const movimiento = movimientos.find(m => m.idParte === idParte);
|
||||
if (movimiento && movimiento.fecha) { // Asegurarse que movimiento.fecha existe
|
||||
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z'); // Consistencia con Z
|
||||
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando getTime()
|
||||
if (movimiento && movimiento.fecha) {
|
||||
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z');
|
||||
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) {
|
||||
fechaMovimientoMasReciente = movFecha;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando getTime()
|
||||
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) {
|
||||
setApiErrorMessage(`La fecha de liquidación (${fechaLiquidacionDate.toLocaleDateString('es-AR', {timeZone: 'UTC'})}) no puede ser inferior a la fecha del movimiento más reciente a liquidar (${(fechaMovimientoMasReciente as Date).toLocaleDateString('es-AR', {timeZone: 'UTC'})}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
setApiErrorMessage(null);
|
||||
setLoading(true); // Usar el loading general para la operación de liquidar
|
||||
|
||||
setLoading(true);
|
||||
const liquidarDto: LiquidarMovimientosCanillaRequestDto = {
|
||||
idsPartesALiquidar: Array.from(selectedIdsParaLiquidar),
|
||||
fechaLiquidacion: fechaLiquidacionDialog // El backend espera YYYY-MM-DD
|
||||
fechaLiquidacion: fechaLiquidacionDialog
|
||||
};
|
||||
|
||||
try {
|
||||
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
|
||||
setOpenLiquidarDialog(false);
|
||||
|
||||
setOpenLiquidarDialog(false);
|
||||
const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0];
|
||||
const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado);
|
||||
|
||||
await cargarMovimientos();
|
||||
await cargarMovimientos();
|
||||
|
||||
if (movimientoParaTicket) {
|
||||
console.log("Liquidación exitosa, intentando generar ticket para canillita:", movimientoParaTicket.idCanilla);
|
||||
// --- CAMBIO: NO IMPRIMIR TICKET SI ES ACCIONISTA ---
|
||||
if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) {
|
||||
console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla);
|
||||
await handleImprimirTicketLiquidacion(
|
||||
movimientoParaTicket.idCanilla,
|
||||
fechaLiquidacionDialog,
|
||||
movimientoParaTicket.canillaEsAccionista
|
||||
fechaLiquidacionDialog,
|
||||
false // esAccionista = false
|
||||
);
|
||||
} else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) {
|
||||
console.log("Liquidación exitosa para accionista. No se genera ticket automáticamente.");
|
||||
} else {
|
||||
console.warn("No se pudo encontrar información del movimiento para generar el ticket post-liquidación.");
|
||||
console.warn("No se pudo encontrar información del movimiento para ticket post-liquidación.");
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al liquidar.';
|
||||
setApiErrorMessage(msg);
|
||||
setApiErrorMessage(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Esta función se pasa al modal para que la invoque al hacer submit en MODO EDICIÓN
|
||||
const handleModalEditSubmit = async (data: UpdateEntradaSalidaCanillaDto, idParte: number) => {
|
||||
// ... (sin cambios)
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data);
|
||||
@@ -251,32 +302,21 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditingMovimiento(null);
|
||||
// Recargar siempre que se cierre el modal y no haya un error pendiente a nivel de página
|
||||
// Opcionalmente, podrías tener una bandera ' cambiosGuardados' que el modal active
|
||||
// para ser más selectivo con la recarga.
|
||||
setPrefillModalData(null);
|
||||
if (!apiErrorMessage) {
|
||||
cargarMovimientos();
|
||||
}
|
||||
};
|
||||
|
||||
const handleImprimirTicketLiquidacion = useCallback(async (
|
||||
// Parámetros necesarios para el ticket
|
||||
idCanilla: number,
|
||||
fecha: string, // Fecha para la que se genera el ticket (probablemente fechaLiquidacionDialog)
|
||||
esAccionista: boolean
|
||||
idCanilla: number, fecha: string, esAccionista: boolean
|
||||
) => {
|
||||
// ... (sin cambios)
|
||||
setLoadingTicketPdf(true);
|
||||
setApiErrorMessage(null);
|
||||
|
||||
try {
|
||||
const params = {
|
||||
fecha: fecha.split('T')[0], // Asegurar formato YYYY-MM-DD
|
||||
idCanilla: idCanilla,
|
||||
esAccionista: esAccionista,
|
||||
};
|
||||
|
||||
const params = { fecha: fecha.split('T')[0], idCanilla, esAccionista };
|
||||
const blob = await reportesService.getTicketLiquidacionCanillaPdf(params);
|
||||
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
const msg = JSON.parse(text).message ?? "Error inesperado al generar el ticket PDF.";
|
||||
@@ -287,16 +327,11 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
if (!w) alert("Permita popups para ver el PDF del ticket.");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error al generar ticket de liquidación:", error);
|
||||
const message = axios.isAxiosError(error) && error.response?.data?.message
|
||||
? error.response.data.message
|
||||
: 'Ocurrió un error al generar el ticket.';
|
||||
console.error("Error al generar ticket:", error);
|
||||
const message = axios.isAxiosError(error) && error.response?.data?.message ? error.response.data.message : 'Error al generar ticket.';
|
||||
setApiErrorMessage(message);
|
||||
} finally {
|
||||
setLoadingTicketPdf(false);
|
||||
// No cerramos el menú aquí si se llama desde handleConfirmLiquidar
|
||||
}
|
||||
}, []); // Dependencias vacías si no usa nada del scope exterior que cambie, o añadir si es necesario
|
||||
} finally { setLoadingTicketPdf(false); }
|
||||
}, []);
|
||||
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
@@ -305,47 +340,77 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
};
|
||||
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
if (!loading && !puedeVer && !loadingFiltersDropdown && movimientos.length === 0 && !filtroFecha && !filtroIdCanillitaSeleccionado ) { // Modificado para solo mostrar si no hay filtros y no puede ver
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
}
|
||||
|
||||
|
||||
const numSelectedToLiquidate = selectedIdsParaLiquidar.size;
|
||||
// Corregido: numNotLiquidatedOnPage debe calcularse sobre 'movimientos' filtrados, no solo 'displayData'
|
||||
// O, si la selección es solo por página, displayData está bien. Asumamos selección por página por ahora.
|
||||
const numNotLiquidatedOnPage = displayData.filter(m => !m.liquidado).length;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Entradas/Salidas Canillitas</Typography>
|
||||
<Typography variant="h5" gutterBottom>Entradas/Salidas Canillitas & Accionistas</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 }} />
|
||||
<TextField label="Fecha" type="date" size="small" value={filtroFecha}
|
||||
onChange={(e) => setFiltroFecha(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }}
|
||||
required
|
||||
error={!filtroFecha} // Se marca error si está vacío
|
||||
helperText={!filtroFecha ? "Fecha es obligatoria" : ""}
|
||||
/>
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
value={filtroTipoDestinatario}
|
||||
exclusive
|
||||
onChange={(_, newValue: TipoDestinatarioFiltro | null) => {
|
||||
if (newValue !== null) {
|
||||
setFiltroTipoDestinatario(newValue);
|
||||
}
|
||||
}}
|
||||
aria-label="Tipo de Destinatario"
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton value="canillitas">Canillitas</ToggleButton>
|
||||
<ToggleButton value="accionistas">Accionistas</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 220, flexGrow: 1 }} disabled={loadingFiltersDropdown} required error={!filtroIdCanillitaSeleccionado}>
|
||||
<InputLabel>{filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'}</InputLabel>
|
||||
<Select
|
||||
value={filtroIdCanillitaSeleccionado}
|
||||
label={filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'}
|
||||
onChange={(e) => setFiltroIdCanillitaSeleccionado(e.target.value as number | string)}
|
||||
>
|
||||
<MenuItem value=""><em>Seleccione uno</em></MenuItem>
|
||||
{destinatariosDropdown.map(c => <MenuItem key={c.idCanilla} value={c.idCanilla}>{c.nomApe} {c.legajo ? `(Leg: ${c.legajo})`: ''}</MenuItem>)}
|
||||
</Select>
|
||||
{!filtroIdCanillitaSeleccionado && <Typography component="p" color="error" variant="caption" sx={{ml:1.5, fontSize:'0.65rem'}}>Selección obligatoria</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<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)}>
|
||||
<InputLabel>Publicación (Opcional)</InputLabel>
|
||||
<Select value={filtroIdPublicacion} label="Publicación (Opcional)" 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 && (
|
||||
{/* --- CAMBIO: DESHABILITAR BOTÓN SI FILTROS OBLIGATORIOS NO ESTÁN --- */}
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => handleOpenModal()}
|
||||
disabled={!filtroFecha || !filtroIdCanillitaSeleccionado} // <<-- AÑADIDO
|
||||
>
|
||||
Registrar Movimiento
|
||||
</Button>
|
||||
)}
|
||||
{puedeLiquidar && numSelectedToLiquidate > 0 && movimientos.some(m => selectedIdsParaLiquidar.has(m.idParte) && !m.liquidado) && (
|
||||
<Button variant="contained" color="success" startIcon={<PlaylistAddCheckIcon />} onClick={handleOpenLiquidarDialog}>
|
||||
Liquidar Seleccionados ({numSelectedToLiquidate})
|
||||
</Button>
|
||||
@@ -353,8 +418,12 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{!filtroFecha && <Alert severity="info" sx={{my:1}}>Por favor, seleccione una fecha.</Alert>}
|
||||
{filtroFecha && !filtroIdCanillitaSeleccionado && <Alert severity="info" sx={{my:1}}>Por favor, seleccione un {filtroTipoDestinatario === 'canillitas' ? 'canillita' : 'accionista'}.</Alert>}
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{/* Mostrar error general si no hay error de API específico y no está cargando filtros */}
|
||||
{error && !loading && !apiErrorMessage && !loadingFiltersDropdown && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
{loadingTicketPdf &&
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}>
|
||||
@@ -364,12 +433,13 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
}
|
||||
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
{!loading && !error && puedeVer && filtroFecha && filtroIdCanillitaSeleccionado && (
|
||||
// ... (Tabla y Paginación sin cambios)
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
|
||||
{puedeLiquidar && (
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage && numNotLiquidatedOnPage > 0}
|
||||
@@ -381,7 +451,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
)}
|
||||
<TableCell>Fecha</TableCell>
|
||||
<TableCell>Publicación</TableCell>
|
||||
<TableCell>Canillita</TableCell>
|
||||
<TableCell>{filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'}</TableCell>
|
||||
<TableCell align="right">Salida</TableCell>
|
||||
<TableCell align="right">Entrada</TableCell>
|
||||
<TableCell align="right">Vendidos</TableCell>
|
||||
@@ -397,19 +467,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={
|
||||
(puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' ? 1 : 0) +
|
||||
9 +
|
||||
((puedeModificar || puedeEliminar || puedeLiquidar) ? 1 : 0)
|
||||
(puedeLiquidar ? 1 : 0) + 9 + ((puedeModificar || puedeEliminar || puedeLiquidar) ? 1 : 0)
|
||||
}
|
||||
align="center"
|
||||
>
|
||||
No se encontraron movimientos.
|
||||
No se encontraron movimientos con los filtros aplicados.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
displayData.map((m) => (
|
||||
<TableRow key={m.idParte} hover selected={selectedIdsParaLiquidar.has(m.idParte)}>
|
||||
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
|
||||
{puedeLiquidar && (
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
checked={selectedIdsParaLiquidar.has(m.idParte)}
|
||||
@@ -440,8 +508,8 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={(e) => handleMenuOpen(e, m)}
|
||||
data-rowid={m.idParte.toString()} // Guardar el id de la fila aquí
|
||||
disabled={m.liquidado && !puedeEliminarLiquidados && !puedeLiquidar} // Lógica simplificada, refinar si es necesario
|
||||
data-rowid={m.idParte.toString()}
|
||||
disabled={m.liquidado && !puedeEliminarLiquidados && !puedeLiquidar}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
@@ -462,19 +530,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<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>)}
|
||||
|
||||
{/* Opción de Imprimir Ticket Liq. */}
|
||||
{selectedRow && selectedRow.liquidado && ( // Solo mostrar si ya está liquidado (para reimprimir)
|
||||
{/* --- CAMBIO: MOSTRAR REIMPRIMIR TICKET SIEMPRE SI ESTÁ LIQUIDADO --- */}
|
||||
{selectedRow && selectedRow.liquidado && puedeLiquidar && ( // Usar puedeLiquidar para consistencia
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (selectedRow) { // selectedRow no será null aquí debido a la condición anterior
|
||||
if (selectedRow) {
|
||||
handleImprimirTicketLiquidacion(
|
||||
selectedRow.idCanilla,
|
||||
selectedRow.fechaLiquidado || selectedRow.fecha, // Usar fechaLiquidado si existe, sino la fecha del movimiento
|
||||
selectedRow.canillaEsAccionista
|
||||
selectedRow.fechaLiquidado || selectedRow.fecha,
|
||||
selectedRow.canillaEsAccionista // Pasar si es accionista
|
||||
);
|
||||
}
|
||||
// handleMenuClose() es llamado por handleImprimirTicketLiquidacion
|
||||
}}
|
||||
disabled={loadingTicketPdf}
|
||||
>
|
||||
@@ -483,13 +549,10 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
Reimprimir Ticket Liq.
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{selectedRow && ( // Opción de Eliminar
|
||||
{selectedRow && (
|
||||
((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados))
|
||||
) && (
|
||||
<MenuItem onClick={() => {
|
||||
if (selectedRow) handleDelete(selectedRow.idParte);
|
||||
}}>
|
||||
<MenuItem onClick={() => { if (selectedRow) handleDelete(selectedRow.idParte); }}>
|
||||
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -498,13 +561,15 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<EntradaSalidaCanillaFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleModalEditSubmit}
|
||||
onSubmit={handleModalEditSubmit} // Este onSubmit es solo para edición
|
||||
initialData={editingMovimiento}
|
||||
prefillData={prefillModalData}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
|
||||
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
|
||||
{/* ... (Dialog de Liquidación sin cambios) ... */}
|
||||
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
|
||||
<DialogTitle>Confirmar Liquidación</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
@@ -523,7 +588,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
// src/pages/Distribucion/GestionarNovedadesCanillaPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
|
||||
CircularProgress, Alert, TextField, Tooltip
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
|
||||
import novedadCanillaService from '../../services/Distribucion/novedadCanillaService';
|
||||
import canillaService from '../../services/Distribucion/canillaService';
|
||||
import type { NovedadCanillaDto } from '../../models/dtos/Distribucion/NovedadCanillaDto';
|
||||
import type { CreateNovedadCanillaDto } from '../../models/dtos/Distribucion/CreateNovedadCanillaDto';
|
||||
import type { UpdateNovedadCanillaDto } from '../../models/dtos/Distribucion/UpdateNovedadCanillaDto';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
import NovedadCanillaFormModal from '../../components/Modals/Distribucion/NovedadCanillaFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarNovedadesCanillaPage: React.FC = () => {
|
||||
const { idCanilla: idCanillaStr } = useParams<{ idCanilla: string }>();
|
||||
const navigate = useNavigate();
|
||||
const idCanilla = Number(idCanillaStr);
|
||||
|
||||
const [canillita, setCanillita] = useState<CanillaDto | null>(null);
|
||||
const [novedades, setNovedades] = useState<NovedadCanillaDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [errorPage, setErrorPage] = useState<string | null>(null); // Error general de la página
|
||||
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>('');
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingNovedad, setEditingNovedad] = useState<NovedadCanillaDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); // Para modal/delete
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedNovedadRow, setSelectedNovedadRow] = useState<NovedadCanillaDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeGestionarNovedades = isSuperAdmin || tienePermiso("CG006");
|
||||
const puedeVerCanillitas = isSuperAdmin || tienePermiso("CG001");
|
||||
|
||||
// Cargar datos del canillita (solo una vez o si idCanilla cambia)
|
||||
useEffect(() => {
|
||||
if (isNaN(idCanilla)) {
|
||||
setErrorPage("ID de Canillita inválido.");
|
||||
setLoading(false); // Detener carga principal
|
||||
return;
|
||||
}
|
||||
if (!puedeVerCanillitas && !puedeGestionarNovedades) {
|
||||
setErrorPage("No tiene permiso para acceder a esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); // Iniciar carga para datos del canillita
|
||||
const fetchCanillita = async () => {
|
||||
try {
|
||||
if (puedeVerCanillitas) {
|
||||
const canData = await canillaService.getCanillaById(idCanilla);
|
||||
setCanillita(canData);
|
||||
} else {
|
||||
// Si no puede ver detalles del canillita pero sí novedades, al menos mostrar ID
|
||||
setCanillita({ idCanilla, nomApe: `ID ${idCanilla}` } as CanillaDto);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error cargando datos del canillita:", err);
|
||||
setErrorPage(`Error al cargar datos del canillita (ID: ${idCanilla}).`);
|
||||
}
|
||||
// No ponemos setLoading(false) aquí, porque la carga de novedades sigue.
|
||||
};
|
||||
fetchCanillita();
|
||||
}, [idCanilla, puedeVerCanillitas, puedeGestionarNovedades]);
|
||||
|
||||
|
||||
// Cargar/filtrar novedades
|
||||
const cargarNovedades = useCallback(async () => {
|
||||
if (isNaN(idCanilla) || (!puedeGestionarNovedades && !puedeVerCanillitas)) {
|
||||
// Los permisos ya se validaron en el useEffect anterior, pero es bueno tenerlo
|
||||
return;
|
||||
}
|
||||
// Si ya está cargando los datos del canillita, no iniciar otra carga paralela
|
||||
// Se usará el mismo 'loading' para ambas operaciones iniciales.
|
||||
// if (!loading) setLoading(true); // No es necesario si el useEffect anterior ya lo hizo
|
||||
|
||||
setApiErrorMessage(null); // Limpiar errores de API de acciones previas
|
||||
// setErrorPage(null); // No limpiar error de página aquí, podría ser por el canillita
|
||||
|
||||
try {
|
||||
const params = {
|
||||
fechaDesde: filtroFechaDesde || null,
|
||||
fechaHasta: filtroFechaHasta || null,
|
||||
};
|
||||
const dataNovedades = await novedadCanillaService.getNovedadesPorCanilla(idCanilla, params);
|
||||
setNovedades(dataNovedades);
|
||||
// Si no hay datos con filtros, no es un error de API, simplemente no hay datos.
|
||||
// El mensaje de "no hay novedades" se maneja en la tabla.
|
||||
} catch (err: any) {
|
||||
console.error("Error al cargar/filtrar novedades:", err);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Error al cargar las novedades.';
|
||||
setErrorPage(message); // Usar el error de página para problemas de carga de novedades
|
||||
setNovedades([]); // Limpiar en caso de error
|
||||
} finally {
|
||||
// Solo poner setLoading(false) después de que AMBAS cargas (canillita y novedades) se intenten.
|
||||
// Como se llaman en secuencia implícita por los useEffect, el último setLoading(false) es el de novedades.
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idCanilla, puedeGestionarNovedades, puedeVerCanillitas, filtroFechaDesde, filtroFechaHasta]);
|
||||
|
||||
// useEffect para cargar novedades cuando los filtros o el canillita (o permisos) cambian
|
||||
useEffect(() => {
|
||||
// Solo cargar si tenemos un idCanilla válido y permisos
|
||||
if (!isNaN(idCanilla) && (puedeGestionarNovedades || puedeVerCanillitas)) {
|
||||
cargarNovedades();
|
||||
} else if (isNaN(idCanilla)){
|
||||
setErrorPage("ID de Canillita inválido.");
|
||||
setLoading(false);
|
||||
} else if (!puedeGestionarNovedades && !puedeVerCanillitas) {
|
||||
setErrorPage("No tiene permiso para acceder a esta sección.");
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idCanilla, cargarNovedades, puedeGestionarNovedades, puedeVerCanillitas]); // `cargarNovedades` ya tiene sus dependencias
|
||||
|
||||
|
||||
const handleOpenModal = (item?: NovedadCanillaDto) => {
|
||||
if (!puedeGestionarNovedades) {
|
||||
setApiErrorMessage("No tiene permiso para agregar o editar novedades.");
|
||||
return;
|
||||
}
|
||||
setEditingNovedad(item || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingNovedad(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateNovedadCanillaDto | UpdateNovedadCanillaDto, idNovedad?: number) => {
|
||||
if (!puedeGestionarNovedades) return;
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (editingNovedad && idNovedad) {
|
||||
await novedadCanillaService.updateNovedad(idNovedad, data as UpdateNovedadCanillaDto);
|
||||
} else {
|
||||
await novedadCanillaService.createNovedad(data as CreateNovedadCanillaDto);
|
||||
}
|
||||
cargarNovedades(); // Recargar lista de novedades
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la novedad.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (idNovedadDelRow: number) => {
|
||||
if (!puedeGestionarNovedades) return;
|
||||
if (window.confirm(`¿Seguro de eliminar esta novedad (ID: ${idNovedadDelRow})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await novedadCanillaService.deleteNovedad(idNovedadDelRow);
|
||||
cargarNovedades(); // Recargar lista de novedades
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la novedad.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: NovedadCanillaDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedNovedadRow(item);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedNovedadRow(null);
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString).toLocaleDateString('es-AR', {timeZone: 'UTC'}) : '-';
|
||||
|
||||
|
||||
if (loading && !canillita) { // Muestra cargando solo si aún no tenemos los datos del canillita
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
|
||||
}
|
||||
|
||||
if (errorPage && !canillita) { // Si hay un error al cargar el canillita, no mostrar nada más
|
||||
return <Alert severity="error" sx={{ m: 2 }}>{errorPage}</Alert>;
|
||||
}
|
||||
|
||||
// Si no tiene permiso para la sección en general
|
||||
if (!puedeGestionarNovedades && !puedeVerCanillitas) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate(`/distribucion/canillas`)} sx={{ mb: 2 }}>
|
||||
Volver a Canillitas
|
||||
</Button>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Novedades de: {canillita?.nomApe || `Canillita ID ${idCanilla}`}
|
||||
</Typography>
|
||||
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 2}}>
|
||||
{puedeGestionarNovedades && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: {xs: 2, sm:0} }}>
|
||||
Agregar Novedad
|
||||
</Button>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<FilterListIcon sx={{color: 'action.active', alignSelf:'center'}} />
|
||||
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde}
|
||||
onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }}
|
||||
disabled={loading} // Deshabilitar durante cualquier carga
|
||||
/>
|
||||
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta}
|
||||
onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }}
|
||||
disabled={loading} // Deshabilitar durante cualquier carga
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Mostrar error de API (de submit/delete) o error de carga de novedades */}
|
||||
{(apiErrorMessage || (errorPage && novedades.length === 0 && !loading)) && (
|
||||
<Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage || errorPage}</Alert>
|
||||
)}
|
||||
|
||||
{loading && <Box sx={{display:'flex', justifyContent:'center', my:2}}><CircularProgress size={30} /></Box>}
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Fecha</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold', width: '70%' }}>Detalle de Novedad</TableCell>
|
||||
{puedeGestionarNovedades && <TableCell align="right" sx={{ fontWeight: 'bold' }}>Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{novedades.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={puedeGestionarNovedades ? 3 : 2} align="center">
|
||||
No hay novedades registradas { (filtroFechaDesde || filtroFechaHasta) && "con los filtros aplicados"}.
|
||||
</TableCell></TableRow>
|
||||
) : (
|
||||
novedades.map((nov) => (
|
||||
<TableRow key={nov.idNovedad} hover>
|
||||
<TableCell>{formatDate(nov.fecha)}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={nov.detalle || ''} arrow>
|
||||
<Typography variant="body2" sx={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '500px'
|
||||
}}>
|
||||
{nov.detalle || '-'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
{puedeGestionarNovedades && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, nov)} disabled={!puedeGestionarNovedades}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeGestionarNovedades && selectedNovedadRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedNovedadRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Editar</MenuItem>)}
|
||||
{puedeGestionarNovedades && selectedNovedadRow && (
|
||||
<MenuItem onClick={() => handleDelete(selectedNovedadRow.idNovedad)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
|
||||
</Menu>
|
||||
|
||||
{idCanilla &&
|
||||
<NovedadCanillaFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleSubmitModal}
|
||||
idCanilla={idCanilla}
|
||||
nombreCanilla={canillita?.nomApe}
|
||||
initialData={editingNovedad}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarNovedadesCanillaPage;
|
||||
@@ -37,15 +37,16 @@ const GestionarOtrosDestinosPage: React.FC = () => {
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
// Permisos para Otros Destinos (OD001 a OD004) - Revisa tus códigos de permiso
|
||||
const puedeVer = isSuperAdmin || tienePermiso("OD001"); // Asumiendo OD001 es ver entidad
|
||||
const puedeVer = isSuperAdmin || tienePermiso("OD001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("OD002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("OD003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("OD004");
|
||||
|
||||
const cargarOtrosDestinos = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setError("No tiene permiso para ver los 'Otros Destinos'.");
|
||||
setLoading(false);
|
||||
setOtrosDestinos([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -131,8 +132,8 @@ const GestionarOtrosDestinosPage: React.FC = () => {
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Otros Destinos</Typography> {/* Cambiado h4 a h5 */}
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -241,7 +241,7 @@ const GestionarPublicacionesPage: React.FC = () => {
|
||||
</FormControl>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={filtroSoloHabilitadas === undefined ? true : filtroSoloHabilitadas} onChange={(e) => setFiltroSoloHabilitadas(e.target.checked)} size="small" />}
|
||||
label="Solo Habilitadas"
|
||||
label="Ver Habilitadas"
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Publicación</Button>)}
|
||||
|
||||
@@ -37,13 +37,19 @@ const GestionarZonasPage: React.FC = () => {
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
// Ajustar códigos de permiso para Zonas
|
||||
const puedeVer = isSuperAdmin || tienePermiso("ZD001"); // Permiso para ver Zonas
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("ZD002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("ZD003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("ZD004");
|
||||
|
||||
|
||||
const cargarZonas = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver las zonas.");
|
||||
setLoading(false);
|
||||
setZonas([]); // Asegurar que no se muestren datos previos
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -134,6 +140,17 @@ const GestionarZonasPage: React.FC = () => {
|
||||
// Adaptar para paginación
|
||||
const displayData = zonas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Gestionar Zonas
|
||||
</Typography>
|
||||
{/* El error de "sin permiso" ya fue seteado en cargarZonas */}
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
@@ -150,7 +167,6 @@ const GestionarZonasPage: React.FC = () => {
|
||||
value={filtroNombre}
|
||||
onChange={(e) => setFiltroNombre(e.target.value)}
|
||||
/>
|
||||
{/* <TextField label="Filtrar por Descripción" ... /> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
@@ -165,11 +181,11 @@ const GestionarZonasPage: React.FC = () => {
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{error && !loading && puedeVer && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
|
||||
{!loading && !error && (
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
|
||||
@@ -283,8 +283,8 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
|
||||
@@ -4,12 +4,12 @@ import { Box, Typography, Paper, CircularProgress, Alert, Button, Divider, type
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ControlDevolucionesDataResponseDto } from '../../models/dtos/Reportes/ControlDevolucionesDataResponseDto';
|
||||
import SeleccionaReporteControlDevoluciones from './SeleccionaReporteControlDevoluciones';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
|
||||
const ReporteControlDevolucionesPage: React.FC = () => {
|
||||
// ... (estados y funciones de manejo de datos sin cambios significativos, excepto cómo se renderiza) ...
|
||||
const [reportData, setReportData] = useState<ControlDevolucionesDataResponseDto | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
@@ -17,6 +17,8 @@ const ReporteControlDevolucionesPage: React.FC = () => {
|
||||
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
|
||||
const [showParamSelector, setShowParamSelector] = useState(true);
|
||||
const [currentParams, setCurrentParams] = useState<{ fecha: string; idEmpresa: number; nombreEmpresa?: string } | null>(null);
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerReporte = isSuperAdmin || tienePermiso("RR003");
|
||||
|
||||
const numberLocaleFormatter = (value: number | null | undefined, showSign = false): string => {
|
||||
if (value == null) return '';
|
||||
@@ -26,6 +28,11 @@ const ReporteControlDevolucionesPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: { fecha: string; idEmpresa: number }) => {
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este reporte.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
@@ -159,92 +166,92 @@ const ReporteControlDevolucionesPage: React.FC = () => {
|
||||
}, [reportData]);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (!reportData || !calculatedValues || !currentParams) {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
if (!reportData || !calculatedValues || !currentParams) {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
|
||||
const dataForExcel: any[][] = [];
|
||||
const dataForExcel: any[][] = [];
|
||||
|
||||
// --- Títulos y Cabecera ---
|
||||
dataForExcel.push(["Control de Devoluciones"]); // Título Principal
|
||||
dataForExcel.push(["Canillas / Accionistas"]); // Subtítulo
|
||||
dataForExcel.push([]); // Fila vacía para espaciado
|
||||
// --- Títulos y Cabecera ---
|
||||
dataForExcel.push(["Control de Devoluciones"]); // Título Principal
|
||||
dataForExcel.push(["Canillas / Accionistas"]); // Subtítulo
|
||||
dataForExcel.push([]); // Fila vacía para espaciado
|
||||
|
||||
dataForExcel.push([
|
||||
`Fecha Consultada: ${currentParams.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', {timeZone:'UTC'}) : ''}`,
|
||||
"", // Celda vacía para espaciado o para alinear con la segunda columna si fuera necesario
|
||||
`Cantidad Canillas: ${calculatedValues.cantidadCanillas}`
|
||||
]);
|
||||
dataForExcel.push([]);
|
||||
dataForExcel.push([
|
||||
`Fecha Consultada: ${currentParams.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', { timeZone: 'UTC' }) : ''}`,
|
||||
"", // Celda vacía para espaciado o para alinear con la segunda columna si fuera necesario
|
||||
`Cantidad Canillas: ${calculatedValues.cantidadCanillas}`
|
||||
]);
|
||||
dataForExcel.push([]);
|
||||
|
||||
dataForExcel.push([currentParams.nombreEmpresa || 'EL DIA']); // Nombre de la Empresa/Publicación
|
||||
dataForExcel.push([]);
|
||||
dataForExcel.push([currentParams.nombreEmpresa || 'EL DIA']); // Nombre de la Empresa/Publicación
|
||||
dataForExcel.push([]);
|
||||
|
||||
// --- Cuerpo del Reporte ---
|
||||
dataForExcel.push(["Ingresados por Remito:", calculatedValues.ingresadosPorRemito]);
|
||||
dataForExcel.push(["----------------------------------", "-------------------"]); // Línea divisoria (estilo simple)
|
||||
// --- Cuerpo del Reporte ---
|
||||
dataForExcel.push(["Ingresados por Remito:", calculatedValues.ingresadosPorRemito]);
|
||||
dataForExcel.push(["----------------------------------", "-------------------"]); // Línea divisoria (estilo simple)
|
||||
|
||||
dataForExcel.push(["Accionistas"]);
|
||||
dataForExcel.push(["Llevados", -calculatedValues.llevadosAcc]);
|
||||
dataForExcel.push(["Devueltos", calculatedValues.devueltosAcc]);
|
||||
dataForExcel.push(["Total", -calculatedValues.totalAcc]); // Fila de Total con estilo
|
||||
dataForExcel.push([]);
|
||||
dataForExcel.push(["Accionistas"]);
|
||||
dataForExcel.push(["Llevados", -calculatedValues.llevadosAcc]);
|
||||
dataForExcel.push(["Devueltos", calculatedValues.devueltosAcc]);
|
||||
dataForExcel.push(["Total", -calculatedValues.totalAcc]); // Fila de Total con estilo
|
||||
dataForExcel.push([]);
|
||||
|
||||
dataForExcel.push(["Canillitas"]);
|
||||
dataForExcel.push(["Llevados", -calculatedValues.llevadosCan]);
|
||||
dataForExcel.push(["Devueltos", calculatedValues.devueltosCan]);
|
||||
dataForExcel.push(["Total", -calculatedValues.totalCan]); // Fila de Total con estilo
|
||||
dataForExcel.push(["==================================", "==================="]); // Línea divisoria sólida
|
||||
dataForExcel.push(["Canillitas"]);
|
||||
dataForExcel.push(["Llevados", -calculatedValues.llevadosCan]);
|
||||
dataForExcel.push(["Devueltos", calculatedValues.devueltosCan]);
|
||||
dataForExcel.push(["Total", -calculatedValues.totalCan]); // Fila de Total con estilo
|
||||
dataForExcel.push(["==================================", "==================="]); // Línea divisoria sólida
|
||||
|
||||
dataForExcel.push(["Total Devolución a la Fecha", calculatedValues.totalDevolucionFecha]);
|
||||
dataForExcel.push(["Total Devolución Días Anteriores", calculatedValues.totalDevolucionOtrosDias]);
|
||||
dataForExcel.push(["Total Devolución", calculatedValues.totalDevolucion]); // Fila de Total con estilo
|
||||
dataForExcel.push(["----------------------------------", "-------------------"]);
|
||||
dataForExcel.push(["Total Devolución a la Fecha", calculatedValues.totalDevolucionFecha]);
|
||||
dataForExcel.push(["Total Devolución Días Anteriores", calculatedValues.totalDevolucionOtrosDias]);
|
||||
dataForExcel.push(["Total Devolución", calculatedValues.totalDevolucion]); // Fila de Total con estilo
|
||||
dataForExcel.push(["----------------------------------", "-------------------"]);
|
||||
|
||||
dataForExcel.push(["Sin Cargo", calculatedValues.sinCargo]);
|
||||
dataForExcel.push(["Sobrantes", -calculatedValues.sobrantes]);
|
||||
dataForExcel.push(["Diferencia", calculatedValues.diferencia]); // Fila de Total con estilo
|
||||
dataForExcel.push(["Sin Cargo", calculatedValues.sinCargo]);
|
||||
dataForExcel.push(["Sobrantes", -calculatedValues.sobrantes]);
|
||||
dataForExcel.push(["Diferencia", calculatedValues.diferencia]); // Fila de Total con estilo
|
||||
|
||||
// --- Crear Hoja y Libro ---
|
||||
const ws = XLSX.utils.aoa_to_sheet(dataForExcel);
|
||||
// --- Crear Hoja y Libro ---
|
||||
const ws = XLSX.utils.aoa_to_sheet(dataForExcel);
|
||||
|
||||
// Ajustar anchos de columna (opcional, pero recomendado)
|
||||
// Esto es un cálculo aproximado, puedes ajustarlo
|
||||
const colWidths = [
|
||||
{ wch: 40 }, // Columna A (Etiquetas)
|
||||
{ wch: 15 }, // Columna B (Valores)
|
||||
{ wch: 25 } // Columna C (para Cantidad Canillas)
|
||||
];
|
||||
ws['!cols'] = colWidths;
|
||||
// Ajustar anchos de columna (opcional, pero recomendado)
|
||||
// Esto es un cálculo aproximado, puedes ajustarlo
|
||||
const colWidths = [
|
||||
{ wch: 40 }, // Columna A (Etiquetas)
|
||||
{ wch: 15 }, // Columna B (Valores)
|
||||
{ wch: 25 } // Columna C (para Cantidad Canillas)
|
||||
];
|
||||
ws['!cols'] = colWidths;
|
||||
|
||||
// Fusionar celdas para títulos (opcional, requiere más trabajo con la estructura de 'ws')
|
||||
// Ejemplo para el título principal (ocuparía A1:C1)
|
||||
if (!ws['!merges']) ws['!merges'] = [];
|
||||
ws['!merges'].push({ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }); // Fusionar A1 a C1
|
||||
ws['!merges'].push({ s: { r: 1, c: 0 }, e: { r: 1, c: 2 } }); // Fusionar A2 a C2
|
||||
ws['!merges'].push({ s: { r: 5, c: 0 }, e: { r: 5, c: 2 } }); // Fusionar celda de Nombre Empresa
|
||||
// Fusionar celdas para títulos (opcional, requiere más trabajo con la estructura de 'ws')
|
||||
// Ejemplo para el título principal (ocuparía A1:C1)
|
||||
if (!ws['!merges']) ws['!merges'] = [];
|
||||
ws['!merges'].push({ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }); // Fusionar A1 a C1
|
||||
ws['!merges'].push({ s: { r: 1, c: 0 }, e: { r: 1, c: 2 } }); // Fusionar A2 a C2
|
||||
ws['!merges'].push({ s: { r: 5, c: 0 }, e: { r: 5, c: 2 } }); // Fusionar celda de Nombre Empresa
|
||||
|
||||
|
||||
// Aplicar formato numérico (esto es más avanzado y depende de cómo quieras los números en Excel)
|
||||
// Por ahora, los números se exportarán como números si son de tipo number en dataForExcel.
|
||||
// Para formato de moneda o miles, tendrías que modificar las celdas en el objeto 'ws'
|
||||
// o asegurarte de que los valores en 'dataForExcel' ya estén como strings formateados si Excel no los interpreta bien.
|
||||
// Por simplicidad, los dejamos como números y Excel usará su formato por defecto.
|
||||
// Aplicar formato numérico (esto es más avanzado y depende de cómo quieras los números en Excel)
|
||||
// Por ahora, los números se exportarán como números si son de tipo number en dataForExcel.
|
||||
// Para formato de moneda o miles, tendrías que modificar las celdas en el objeto 'ws'
|
||||
// o asegurarte de que los valores en 'dataForExcel' ya estén como strings formateados si Excel no los interpreta bien.
|
||||
// Por simplicidad, los dejamos como números y Excel usará su formato por defecto.
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "ControlDevoluciones");
|
||||
|
||||
let fileName = "ReporteControlDevoluciones";
|
||||
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
|
||||
fileName += `_${currentParams.fecha}`;
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "ControlDevoluciones");
|
||||
|
||||
let fileName = "ReporteControlDevoluciones";
|
||||
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
|
||||
fileName += `_${currentParams.fecha}`;
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
|
||||
}, [reportData, calculatedValues, currentParams]);
|
||||
|
||||
}, [reportData, calculatedValues, currentParams]);
|
||||
|
||||
|
||||
|
||||
|
||||
// Componente para una fila del reporte usando Box con Flexbox
|
||||
interface ReportRowProps {
|
||||
@@ -306,6 +313,9 @@ const ReporteControlDevolucionesPage: React.FC = () => {
|
||||
);
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Button
|
||||
Box, Typography, Paper, CircularProgress, Button,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
@@ -9,6 +10,7 @@ import type { ReporteCuentasDistribuidorResponseDto } from '../../models/dtos/Re
|
||||
import type { BalanceCuentaDistDto } from '../../models/dtos/Reportes/BalanceCuentaDistDto';
|
||||
import type { BalanceCuentaDebCredDto } from '../../models/dtos/Reportes/BalanceCuentaDebCredDto';
|
||||
import type { BalanceCuentaPagosDto } from '../../models/dtos/Reportes/BalanceCuentaPagosDto';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import SeleccionaReporteCuentasDistribuidores from './SeleccionaReporteCuentasDistribuidores';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
@@ -20,6 +22,7 @@ type PagoConSaldo = BalanceCuentaPagosDto & { id: string; saldoAcumulado: number
|
||||
const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
const [originalReportData, setOriginalReportData] = useState<ReporteCuentasDistribuidorResponseDto | null>(null);
|
||||
const [movimientosConSaldo, setMovimientosConSaldo] = useState<MovimientoConSaldo[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notasConSaldo, setNotasConSaldo] = useState<NotaConSaldo[]>([]);
|
||||
const [pagosConSaldo, setPagosConSaldo] = useState<PagoConSaldo[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
@@ -34,6 +37,8 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
nombreDistribuidor?: string;
|
||||
nombreEmpresa?: string;
|
||||
} | null>(null);
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerReporte = isSuperAdmin || tienePermiso("RR001");
|
||||
|
||||
// Calcula saldos acumulados seccion por seccion
|
||||
const calcularSaldosPorSeccion = (data: ReporteCuentasDistribuidorResponseDto) => {
|
||||
@@ -227,6 +232,12 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
}) => {
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este reporte.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
setApiErrorParams(null);
|
||||
setOriginalReportData(null);
|
||||
@@ -237,8 +248,8 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
const distSvc = (await import('../../services/Distribucion/distribuidorService')).default;
|
||||
const empSvc = (await import('../../services/Distribucion/empresaService')).default;
|
||||
const [distData, empData] = await Promise.all([
|
||||
distSvc.getDistribuidorById(params.idDistribuidor),
|
||||
empSvc.getEmpresaById(params.idEmpresa)
|
||||
distSvc.getDistribuidorLookupById(params.idDistribuidor),
|
||||
empSvc.getEmpresaLookupById(params.idEmpresa)
|
||||
]);
|
||||
setCurrentParams({
|
||||
...params,
|
||||
@@ -273,20 +284,39 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (!originalReportData) return;
|
||||
const wb = XLSX.utils.book_new();
|
||||
if (movimientosConSaldo.length) {
|
||||
if (
|
||||
!originalReportData ||
|
||||
(movimientosConSaldo.length === 0 &&
|
||||
notasConSaldo.length === 0 &&
|
||||
pagosConSaldo.length === 0)
|
||||
) {
|
||||
alert("No hay datos para exportar."); // O un mensaje más amigable
|
||||
return;
|
||||
}
|
||||
|
||||
const wb = XLSX.utils.book_new();// Se crea un nuevo libro
|
||||
|
||||
// Movimientos
|
||||
if (movimientosConSaldo.length) { // <--- CHEQUEO 1
|
||||
// Si movimientosConSaldo está vacío, esta hoja no se añade
|
||||
const ws = XLSX.utils.json_to_sheet(movimientosConSaldo.map(({ id, ...rest }) => rest));
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Movimientos');
|
||||
}
|
||||
if (notasConSaldo.length) {
|
||||
// Notas
|
||||
if (notasConSaldo.length) { // <--- CHEQUEO 2
|
||||
// Si notasConSaldo está vacío, esta hoja no se añade
|
||||
const ws = XLSX.utils.json_to_sheet(notasConSaldo.map(({ id, ...rest }) => rest));
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Notas');
|
||||
}
|
||||
if (pagosConSaldo.length) {
|
||||
// Pagos
|
||||
if (pagosConSaldo.length) { // <--- CHEQUEO 3
|
||||
// Si pagosConSaldo está vacío, esta hoja no se añade
|
||||
const ws = XLSX.utils.json_to_sheet(pagosConSaldo.map(({ id, ...rest }) => rest));
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Pagos');
|
||||
}
|
||||
|
||||
// Si ninguno de los arrays tiene datos, el libro 'wb' quedará vacío.
|
||||
// Y la siguiente línea dará el error:
|
||||
XLSX.writeFile(wb, `Reporte_${Date.now()}.xlsx`);
|
||||
}, [originalReportData, movimientosConSaldo, notasConSaldo, pagosConSaldo]);
|
||||
|
||||
@@ -297,13 +327,16 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
const blob = await reportesService.getReporteCuentasDistribuidorPdf(currentParams);
|
||||
window.open(URL.createObjectURL(blob), '_blank');
|
||||
} catch {
|
||||
/* manejar error */
|
||||
setError('Ocurrió un error al generar el PDF.');
|
||||
} finally {
|
||||
setLoadingPdf(false);
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center' }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
@@ -328,6 +361,11 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
{error && (
|
||||
<Paper sx={{ mb: 2, p: 2, backgroundColor: '#ffeaea' }}>
|
||||
<Typography color="error">{error}</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h5">Cuenta Corriente Distribuidor</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
|
||||
404
Frontend/src/pages/Reportes/ReporteListadoDistMensualPage.tsx
Normal file
404
Frontend/src/pages/Reportes/ReporteListadoDistMensualPage.tsx
Normal file
@@ -0,0 +1,404 @@
|
||||
// src/pages/Reportes/ReporteListadoDistMensualPage.tsx
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ListadoDistCanMensualDiariosDto } from '../../models/dtos/Reportes/ListadoDistCanMensualDiariosDto';
|
||||
import type { ListadoDistCanMensualPubDto } from '../../models/dtos/Reportes/ListadoDistCanMensualPubDto';
|
||||
import SeleccionaReporteListadoDistMensual, { type TipoListadoDistMensual } from './SeleccionaReporteListadoDistMensual';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
// Interfaces para DataGrid (añadiendo 'id')
|
||||
interface GridDiariosItem extends ListadoDistCanMensualDiariosDto { id: string; }
|
||||
interface GridPubItem extends ListadoDistCanMensualPubDto { id: string; }
|
||||
|
||||
|
||||
const ReporteListadoDistMensualPage: React.FC = () => {
|
||||
const [reporteDiariosData, setReporteDiariosData] = useState<GridDiariosItem[]>([]);
|
||||
const [reportePubData, setReportePubData] = useState<GridPubItem[]>([]);
|
||||
const [currentReportVariant, setCurrentReportVariant] = useState<TipoListadoDistMensual | null>(null);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
|
||||
const [showParamSelector, setShowParamSelector] = useState(true);
|
||||
const [currentParams, setCurrentParams] = useState<{
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
esAccionista: boolean;
|
||||
tipoReporte: TipoListadoDistMensual;
|
||||
nombreTipoVendedor?: string;
|
||||
mesAnio?: string;
|
||||
} | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerReporte = isSuperAdmin || tienePermiso("RR009"); // Asumiendo RR009 para este reporte
|
||||
|
||||
const currencyFormatter = (value?: number | null) => value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '-';
|
||||
const numberFormatter = (value?: number | null) => value != null ? Number(value).toLocaleString('es-AR') : '-';
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
esAccionista: boolean;
|
||||
tipoReporte: TipoListadoDistMensual;
|
||||
}) => {
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este reporte.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setReporteDiariosData([]);
|
||||
setReportePubData([]);
|
||||
setCurrentReportVariant(params.tipoReporte);
|
||||
|
||||
const mesAnioStr = new Date(params.fechaDesde + 'T00:00:00').toLocaleDateString('es-AR', { month: 'long', year: 'numeric', timeZone: 'UTC' });
|
||||
setCurrentParams({ ...params, nombreTipoVendedor: params.esAccionista ? "Accionistas" : "Canillitas", mesAnio: mesAnioStr });
|
||||
|
||||
try {
|
||||
if (params.tipoReporte === 'diarios') {
|
||||
const data = await reportesService.getListadoDistMensualDiarios(params);
|
||||
setReporteDiariosData(data.map((item, i) => ({ ...item, id: `diario-${item.canilla}-${i}` })));
|
||||
if (data.length === 0) setError("No se encontraron datos para la variante 'Desglose por Diarios'.");
|
||||
} else { // 'publicaciones'
|
||||
const data = await reportesService.getListadoDistMensualPorPublicacion(params);
|
||||
setReportePubData(data.map((item, i) => ({ ...item, id: `pub-${item.canilla}-${item.publicacion}-${i}` })));
|
||||
if (data.length === 0) setError("No se encontraron datos para la variante 'Desglose por Publicación'.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message : 'Ocurrió un error al generar el reporte.';
|
||||
setApiErrorParams(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [puedeVerReporte]);
|
||||
|
||||
const handleVolverAParametros = useCallback(() => {
|
||||
setShowParamSelector(true);
|
||||
setReporteDiariosData([]);
|
||||
setReportePubData([]);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setCurrentParams(null);
|
||||
setCurrentReportVariant(null);
|
||||
}, []);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (reporteDiariosData.length === 0 && reportePubData.length === 0) {
|
||||
alert("No hay datos para exportar."); return;
|
||||
}
|
||||
const wb = XLSX.utils.book_new();
|
||||
let fileName = "ListadoDistMensual";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.nombreTipoVendedor?.replace(/\s+/g, '')}`;
|
||||
fileName += `_${currentParams.mesAnio?.replace(/\s+/g, '-').replace('/', '-')}`;
|
||||
}
|
||||
|
||||
if (currentReportVariant === 'diarios' && reporteDiariosData.length > 0) {
|
||||
const data = reporteDiariosData.map(({ id, ...r }) => ({
|
||||
"Canillita": r.canilla, "El Día (Cant.)": r.elDia, "El Plata (Cant.)": r.elPlata,
|
||||
"Total Vendidos": r.vendidos, "Imp. El Día": r.importeElDia, "Imp. El Plata": r.importeElPlata,
|
||||
"Importe Total": r.importeTotal
|
||||
}));
|
||||
const totalesDiarios = {
|
||||
"Canillita": "TOTALES", "El Día (Cant.)": reporteDiariosData.reduce((s, i) => s + (i.elDia ?? 0), 0),
|
||||
"El Plata (Cant.)": reporteDiariosData.reduce((s, i) => s + (i.elPlata ?? 0), 0),
|
||||
"Total Vendidos": reporteDiariosData.reduce((s, i) => s + (i.vendidos ?? 0), 0),
|
||||
"Imp. El Día": reporteDiariosData.reduce((s, i) => s + (i.importeElDia ?? 0), 0),
|
||||
"Imp. El Plata": reporteDiariosData.reduce((s, i) => s + (i.importeElPlata ?? 0), 0),
|
||||
"Importe Total": reporteDiariosData.reduce((s, i) => s + (i.importeTotal ?? 0), 0)
|
||||
};
|
||||
data.push(totalesDiarios);
|
||||
const ws = XLSX.utils.json_to_sheet(data);
|
||||
const headers = Object.keys(data[0] || {});
|
||||
ws['!cols'] = headers.map(h => ({ wch: Math.max(...data.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 2 }));
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Desglose Diarios");
|
||||
fileName += "_Diarios.xlsx";
|
||||
|
||||
} else if (currentReportVariant === 'publicaciones' && reportePubData.length > 0) {
|
||||
// --- INICIO DE CAMBIOS PARA TOTALES EN EXCEL DE PUBLICACIONES ---
|
||||
const dataAgrupadaParaExcel: any[] = [];
|
||||
const canillitasUnicos = [...new Set(reportePubData.map(item => item.canilla))];
|
||||
|
||||
canillitasUnicos.sort().forEach(nombreCanillita => {
|
||||
const itemsDelCanillita = reportePubData.filter(item => item.canilla === nombreCanillita);
|
||||
itemsDelCanillita.forEach(item => {
|
||||
dataAgrupadaParaExcel.push({
|
||||
"Canillita": item.canilla,
|
||||
"Publicación": item.publicacion,
|
||||
"Llevados": item.totalCantSalida,
|
||||
"Devueltos": item.totalCantEntrada,
|
||||
"A Rendir": item.totalRendir
|
||||
});
|
||||
});
|
||||
// Fila de Total por Canillita
|
||||
const totalLlevadosCanillita = itemsDelCanillita.reduce((sum, item) => sum + (item.totalCantSalida ?? 0), 0);
|
||||
const totalDevueltosCanillita = itemsDelCanillita.reduce((sum, item) => sum + (item.totalCantEntrada ?? 0), 0);
|
||||
const totalRendirCanillita = itemsDelCanillita.reduce((sum, item) => sum + (item.totalRendir ?? 0), 0);
|
||||
dataAgrupadaParaExcel.push({
|
||||
"Canillita": `Total ${nombreCanillita}`,
|
||||
"Publicación": "",
|
||||
"Llevados": totalLlevadosCanillita,
|
||||
"Devueltos": totalDevueltosCanillita,
|
||||
"A Rendir": totalRendirCanillita,
|
||||
});
|
||||
dataAgrupadaParaExcel.push({}); // Fila vacía para separar
|
||||
});
|
||||
|
||||
// Fila de Total General
|
||||
const totalGeneralLlevados = reportePubData.reduce((sum, item) => sum + (item.totalCantSalida ?? 0), 0);
|
||||
const totalGeneralDevueltos = reportePubData.reduce((sum, item) => sum + (item.totalCantEntrada ?? 0), 0);
|
||||
const totalGeneralRendir = reportePubData.reduce((sum, item) => sum + (item.totalRendir ?? 0), 0);
|
||||
dataAgrupadaParaExcel.push({
|
||||
"Canillita": "TOTAL GENERAL",
|
||||
"Publicación": "",
|
||||
"Llevados": totalGeneralLlevados,
|
||||
"Devueltos": totalGeneralDevueltos,
|
||||
"A Rendir": totalGeneralRendir,
|
||||
});
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(dataAgrupadaParaExcel);
|
||||
const headers = ["Canillita", "Publicación", "Llevados", "Devueltos", "A Rendir"]; // Definir orden
|
||||
ws['!cols'] = headers.map(h => ({ wch: Math.max(...dataAgrupadaParaExcel.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 2 }));
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Desglose Publicaciones");
|
||||
fileName += "_Publicaciones.xlsx";
|
||||
}
|
||||
|
||||
if (wb.SheetNames.length > 0) XLSX.writeFile(wb, fileName);
|
||||
else alert("No hay datos para la variante seleccionada para exportar.");
|
||||
|
||||
}, [reporteDiariosData, reportePubData, currentReportVariant, currentParams]);
|
||||
|
||||
const handleGenerarYAbrirPdf = useCallback(async () => {
|
||||
if (!currentParams) { setError("Seleccione parámetros."); return; }
|
||||
if (!puedeVerReporte) { setError("Sin permiso para PDF."); return; }
|
||||
setLoadingPdf(true); setError(null);
|
||||
try {
|
||||
let blob;
|
||||
if (currentParams.tipoReporte === 'diarios') {
|
||||
blob = await reportesService.getListadoDistMensualDiariosPdf(currentParams);
|
||||
} else {
|
||||
blob = await reportesService.getListadoDistMensualPorPublicacionPdf(currentParams);
|
||||
}
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
|
||||
setError(msg);
|
||||
setLoadingPdf(false); // Asegurar que se detenga el loader
|
||||
return;
|
||||
}
|
||||
const url = URL.createObjectURL(blob);
|
||||
const w = window.open(url, '_blank');
|
||||
if (!w) alert("Permita popups para ver el PDF.");
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error al generar el PDF.';
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoadingPdf(false);
|
||||
}
|
||||
}, [currentParams, puedeVerReporte]);
|
||||
|
||||
const FooterSimple = () => (<GridFooterContainer><GridFooter /></GridFooterContainer>);
|
||||
|
||||
const totalGeneralDiarios = useMemo(() => {
|
||||
if (reporteDiariosData.length === 0) return null;
|
||||
return {
|
||||
elDia: reporteDiariosData.reduce((s, i) => s + (i.elDia ?? 0), 0),
|
||||
elPlata: reporteDiariosData.reduce((s, i) => s + (i.elPlata ?? 0), 0),
|
||||
vendidos: reporteDiariosData.reduce((s, i) => s + (i.vendidos ?? 0), 0),
|
||||
importeElDia: reporteDiariosData.reduce((s, i) => s + (i.importeElDia ?? 0), 0),
|
||||
importeElPlata: reporteDiariosData.reduce((s, i) => s + (i.importeElPlata ?? 0), 0),
|
||||
importeTotal: reporteDiariosData.reduce((s, i) => s + (i.importeTotal ?? 0), 0)
|
||||
};
|
||||
}, [reporteDiariosData]);
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const FooterDiarios = () => {
|
||||
if (!totalGeneralDiarios) return <GridFooterContainer><GridFooter sx={{ borderTop: 'none' }} /></GridFooterContainer>;
|
||||
return (
|
||||
<GridFooterContainer sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between', // Separa la paginación (izquierda) de los totales (derecha)
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
minHeight: '52px', // Altura estándar para el footer
|
||||
// No es necesario p: aquí si los hijos lo manejan o el GridFooterContainer lo aplica por defecto
|
||||
}}>
|
||||
{/* Box para la paginación estándar */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0, // Evita que este box se encoja si los totales son anchos
|
||||
overflow: 'hidden', // Para asegurar que no desborde si el contenido interno es muy ancho
|
||||
px: 1, // Padding horizontal para el contenedor de la paginación
|
||||
// Considera un flexGrow o un width/maxWidth si necesitas más control sobre el espacio de la paginación
|
||||
// Ejemplo: flexGrow: 1, maxWidth: 'calc(100% - 250px)' (para dejar espacio a los totales)
|
||||
}}>
|
||||
<GridFooter
|
||||
sx={{
|
||||
borderTop: 'none', // Quitar el borde superior del GridFooter interno
|
||||
width: 'auto', // Permite que el GridFooter se ajuste a su contenido (paginador)
|
||||
'& .MuiToolbar-root': { // Ajustar padding del toolbar de paginación
|
||||
paddingLeft: 0, // O un valor pequeño si es necesario
|
||||
paddingRight: 0,
|
||||
},
|
||||
// Mantenemos oculto el contador de filas seleccionadas si no lo queremos
|
||||
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/* Box para los totales personalizados */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap', // Evita que los totales hagan salto de línea
|
||||
overflowX: 'auto', // Scroll DENTRO de este Box si los totales son muy anchos
|
||||
px: 2, // Padding horizontal para el contenedor de los totales (ajusta pr:2 de tu ejemplo)
|
||||
flexShrink: 1, // Permitir que este contenedor se encoja si la paginación necesita más espacio
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[0].width, textAlign: 'right', fontWeight: 'bold', pr: 1 }}>TOTALES:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[1].width, textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{numberFormatter(totalGeneralDiarios.elDia)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[2].width, textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{numberFormatter(totalGeneralDiarios.elPlata)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[3].width, textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{numberFormatter(totalGeneralDiarios.vendidos)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[4].width, textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{currencyFormatter(totalGeneralDiarios.importeElDia)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[5].width, textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{currencyFormatter(totalGeneralDiarios.importeElPlata)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[6].width, textAlign: 'right', fontWeight: 'bold' }}>{currencyFormatter(totalGeneralDiarios.importeTotal)}</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const columnsDiarios: GridColDef<GridDiariosItem>[] = [
|
||||
{ field: 'canilla', headerName: 'Nombre', width: 250, flex: 1.5 },
|
||||
{ field: 'elDia', headerName: 'El Día (Cant)', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(value as number) },
|
||||
{ field: 'elPlata', headerName: 'El Plata (Cant)', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(value as number) },
|
||||
{ field: 'vendidos', headerName: 'Total Vendidos', type: 'number', width: 130, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(value as number) },
|
||||
{ field: 'importeElDia', headerName: 'Imp. El Día', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(value as number) },
|
||||
{ field: 'importeElPlata', headerName: 'Imp. El Plata', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(value as number) },
|
||||
{ field: 'importeTotal', headerName: 'Importe Total', type: 'number', width: 160, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(value as number) },
|
||||
];
|
||||
const columnsPublicaciones: GridColDef<GridPubItem>[] = [
|
||||
{ field: 'canilla', headerName: 'Canillita', width: 250, flex: 1.2 },
|
||||
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1 },
|
||||
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(value as number) },
|
||||
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(value as number) },
|
||||
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(value as number) },
|
||||
];
|
||||
|
||||
const rowsDiarios = useMemo(() => reporteDiariosData, [reporteDiariosData]);
|
||||
const rowsPubs = useMemo(() => reportePubData, [reportePubData]);
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
<SeleccionaReporteListadoDistMensual
|
||||
onGenerarReporte={handleGenerarReporte}
|
||||
isLoading={loading}
|
||||
apiErrorMessage={apiErrorParams}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!loading && !puedeVerReporte && !showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Alert severity="error" sx={{ m: 2 }}>No tiene permiso para ver este reporte.</Alert>
|
||||
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
|
||||
Volver
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const renderReportContent = () => {
|
||||
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>;
|
||||
if (error && !loading) return <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>;
|
||||
|
||||
if (currentReportVariant === 'diarios') {
|
||||
if (reporteDiariosData.length === 0) return <Typography sx={{ mt: 2, fontStyle: 'italic' }}>No hay datos para la variante "Desglose por Diarios".</Typography>;
|
||||
return (
|
||||
<Paper sx={{ height: 'calc(100vh - 320px)', width: '100%', mt: 2 }}>
|
||||
<DataGrid
|
||||
rows={rowsDiarios}
|
||||
columns={columnsDiarios}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: FooterDiarios }}
|
||||
density="compact"
|
||||
hideFooterSelectedRowCount
|
||||
disableRowSelectionOnClick
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 100 } },
|
||||
}}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
if (currentReportVariant === 'publicaciones') {
|
||||
if (reportePubData.length === 0) return <Typography sx={{ mt: 2, fontStyle: 'italic' }}>No hay datos para la variante "Desglose por Publicación".</Typography>;
|
||||
return (
|
||||
<Paper sx={{ height: 'calc(100vh - 320px)', width: '100%', mt: 2 }}>
|
||||
<DataGrid
|
||||
rows={rowsPubs}
|
||||
columns={columnsPublicaciones}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: FooterSimple }} // Para esta tabla, un footer simple sin totales complejos por ahora
|
||||
density="compact"
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 100 } },
|
||||
}}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Listado Distribución Mensual ({currentParams?.nombreTipoVendedor})</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || (reporteDiariosData.length === 0 && reportePubData.length === 0) || !!error} size="small">
|
||||
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
|
||||
</Button>
|
||||
<Button onClick={handleExportToExcel} variant="outlined" disabled={(reporteDiariosData.length === 0 && reportePubData.length === 0) || !!error} size="small">
|
||||
Exportar a Excel
|
||||
</Button>
|
||||
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
|
||||
Nuevos Parámetros
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Mes: {currentParams?.mesAnio || '-'}
|
||||
</Typography>
|
||||
{renderReportContent()}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReporteListadoDistMensualPage;
|
||||
@@ -8,6 +8,7 @@ import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ListadoDistribucionDistribuidoresResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionDistribuidoresResponseDto';
|
||||
import SeleccionaReporteListadoDistribucion from './SeleccionaReporteListadoDistribucion';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -26,6 +27,8 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
nombrePublicacion?: string;
|
||||
nombreDistribuidor?: string;
|
||||
} | null>(null);
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerReporte = isSuperAdmin || tienePermiso("RR002");
|
||||
|
||||
// --- ESTADO PARA TOTALES CALCULADOS (PARA EL FOOTER DEL DETALLE) ---
|
||||
const [totalesDetalle, setTotalesDetalle] = useState({
|
||||
@@ -35,7 +38,7 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
promedioGeneralVentaNeta: 0,
|
||||
porcentajeDevolucionGeneral: 0,
|
||||
});
|
||||
|
||||
|
||||
const [totalesPromedios, setTotalesPromedios] = useState({
|
||||
cantDias: 0,
|
||||
promLlevados: 0,
|
||||
@@ -51,12 +54,17 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
}) => {
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este reporte.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setReportData(null);
|
||||
setTotalesDetalle({ llevados:0, devueltos:0, ventaNeta:0, promedioGeneralVentaNeta:0, porcentajeDevolucionGeneral:0 });
|
||||
setTotalesPromedios({ cantDias:0, promLlevados:0, promDevueltos:0, promVentas:0, porcentajeDevolucionGeneral:0});
|
||||
setTotalesDetalle({ llevados: 0, devueltos: 0, ventaNeta: 0, promedioGeneralVentaNeta: 0, porcentajeDevolucionGeneral: 0 });
|
||||
setTotalesPromedios({ cantDias: 0, promLlevados: 0, promDevueltos: 0, promVentas: 0, porcentajeDevolucionGeneral: 0 });
|
||||
|
||||
|
||||
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
|
||||
@@ -74,17 +82,17 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
|
||||
try {
|
||||
const data = await reportesService.getListadoDistribucionDistribuidores(params);
|
||||
|
||||
|
||||
let acumuladoVentaNeta = 0;
|
||||
let diasConVenta = 0;
|
||||
|
||||
|
||||
const detalleConCalculos = data.detalleSimple.map((item, index) => {
|
||||
const llevados = item.llevados || 0;
|
||||
const devueltos = item.devueltos || 0;
|
||||
const ventaNeta = llevados - devueltos;
|
||||
if (llevados > 0) diasConVenta++; // O si ventaNeta > 0, dependiendo de la definición de "día con actividad"
|
||||
acumuladoVentaNeta += ventaNeta;
|
||||
|
||||
|
||||
return {
|
||||
...item,
|
||||
id: `simple-${index}`,
|
||||
@@ -97,13 +105,13 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
const totalLlevadosDetalle = detalleConCalculos.reduce((sum, item) => sum + (item.llevados || 0), 0);
|
||||
const totalDevueltosDetalle = detalleConCalculos.reduce((sum, item) => sum + (item.devueltos || 0), 0);
|
||||
const totalVentaNetaDetalle = totalLlevadosDetalle - totalDevueltosDetalle;
|
||||
|
||||
|
||||
setTotalesDetalle({
|
||||
llevados: totalLlevadosDetalle,
|
||||
devueltos: totalDevueltosDetalle,
|
||||
ventaNeta: totalVentaNetaDetalle,
|
||||
promedioGeneralVentaNeta: diasConVenta > 0 ? totalVentaNetaDetalle / diasConVenta : 0,
|
||||
porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0
|
||||
llevados: totalLlevadosDetalle,
|
||||
devueltos: totalDevueltosDetalle,
|
||||
ventaNeta: totalVentaNetaDetalle,
|
||||
promedioGeneralVentaNeta: diasConVenta > 0 ? totalVentaNetaDetalle / diasConVenta : 0,
|
||||
porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0
|
||||
});
|
||||
|
||||
|
||||
@@ -112,7 +120,7 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
id: `prom-${index}`,
|
||||
porcentajeDevolucion: item.promedio_Llevados > 0 ? (item.promedio_Devueltos / item.promedio_Llevados) * 100 : 0,
|
||||
}));
|
||||
|
||||
|
||||
// Calcular totales para la tabla de promedios (ponderados por Cant. Días)
|
||||
const totalDiasPromedios = promediosConCalculos.reduce((sum, item) => sum + (item.cant || 0), 0);
|
||||
const totalPonderadoLlevados = promediosConCalculos.reduce((sum, item) => sum + ((item.promedio_Llevados || 0) * (item.cant || 0)), 0);
|
||||
@@ -190,7 +198,7 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
"Prom. Ventas": rest.promedio_Ventas,
|
||||
"% Devolución": (rest as any).porcentajeDevolucion, // Ya calculado
|
||||
}));
|
||||
// Fila de totales para promedios
|
||||
// Fila de totales para promedios
|
||||
promediosToExport.push({
|
||||
"Día Semana": "General",
|
||||
"Cant. Días": totalesPromedios.cantDias,
|
||||
@@ -222,7 +230,7 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
setError(null);
|
||||
try {
|
||||
const blob = await reportesService.getListadoDistribucionDistribuidoresPdf(currentParams);
|
||||
if (blob.type === "application/json") {
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
|
||||
setError(msg);
|
||||
@@ -261,15 +269,15 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
|
||||
// --- Custom Footer para Detalle Diario ---
|
||||
const CustomFooterDetalle = () => (
|
||||
<GridFooterContainer
|
||||
sx={{
|
||||
<GridFooterContainer
|
||||
sx={{
|
||||
// Asegurar que el contenedor pueda usar todo el ancho
|
||||
// y que los items internos puedan distribuirse.
|
||||
// justifyContent: 'space-between' // Esto podría ayudar
|
||||
}}
|
||||
>
|
||||
{/* Contenedor para los elementos del footer por defecto (paginación, etc.) */}
|
||||
<Box sx={{
|
||||
<Box sx={{
|
||||
// flexGrow: 1, // Originalmente teníamos esto, puede ser muy agresivo
|
||||
display: 'flex', // Para alinear los items del paginador por defecto
|
||||
alignItems: 'center',
|
||||
@@ -277,66 +285,66 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
minWidth: '400px', // AJUSTA ESTE VALOR según lo que necesites
|
||||
flexShrink: 0, // Evita que se encoja demasiado si los totales son muy anchos
|
||||
}}>
|
||||
<GridFooter sx={{
|
||||
borderTop: 'none',
|
||||
// '& .MuiTablePagination-toolbar': { // Para los elementos dentro del paginador
|
||||
// flexWrap: 'wrap', // Permitir que los elementos internos del paginador se envuelvan
|
||||
// justifyContent: 'flex-start',
|
||||
// },
|
||||
// '& .MuiTablePagination-spacer': { // El espaciador puede ser un problema
|
||||
// display: 'none', // Prueba quitándolo
|
||||
// }
|
||||
}} />
|
||||
<GridFooter sx={{
|
||||
borderTop: 'none',
|
||||
// '& .MuiTablePagination-toolbar': { // Para los elementos dentro del paginador
|
||||
// flexWrap: 'wrap', // Permitir que los elementos internos del paginador se envuelvan
|
||||
// justifyContent: 'flex-start',
|
||||
// },
|
||||
// '& .MuiTablePagination-spacer': { // El espaciador puede ser un problema
|
||||
// display: 'none', // Prueba quitándolo
|
||||
// }
|
||||
}} />
|
||||
</Box>
|
||||
{/* Contenedor para tus totales, alineado a la derecha */}
|
||||
<Box sx={{
|
||||
p: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 'auto', // Empuja a la derecha
|
||||
flexShrink: 1, // Permite que este se encoja si es necesario, pero no demasiado
|
||||
overflowX: 'auto', // Si los totales son muchos, que tengan su propio scroll
|
||||
whiteSpace: 'nowrap', // Evitar que los textos de totales se partan
|
||||
<Box sx={{
|
||||
p: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 'auto', // Empuja a la derecha
|
||||
flexShrink: 1, // Permite que este se encoja si es necesario, pero no demasiado
|
||||
overflowX: 'auto', // Si los totales son muchos, que tengan su propio scroll
|
||||
whiteSpace: 'nowrap', // Evitar que los textos de totales se partan
|
||||
}}>
|
||||
{/* Mantén esta estructura, pero quizás necesitas jugar con los minWidth/flex de los Typography */}
|
||||
<Typography variant="subtitle2" sx={{ flexBasis: columnsPromedios[0].width || 'auto', minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold' }}>Generales:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[1].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.llevados.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.devueltos.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.ventaNeta.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.promedioGeneralVentaNeta.toLocaleString('es-AR', { minimumFractionDigits: 3, maximumFractionDigits: 3 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[1].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr: 1 }}>{totalesDetalle.llevados.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr: 1 }}>{totalesDetalle.devueltos.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr: 1 }}>{totalesDetalle.ventaNeta.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr: 1 }}>{totalesDetalle.promedioGeneralVentaNeta.toLocaleString('es-AR', { minimumFractionDigits: 3, maximumFractionDigits: 3 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesDetalle.porcentajeDevolucionGeneral.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
|
||||
|
||||
// --- Custom Footer para Promedios por Día (Ajustado para flex) ---
|
||||
const CustomFooterPromedios = () => (
|
||||
<GridFooterContainer sx={{ /* justifyContent: 'space-between' */ }}>
|
||||
<Box sx={{
|
||||
<Box sx={{
|
||||
// flexGrow: 1,
|
||||
display: 'flex',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minWidth: '400px', // AJUSTA ESTE VALOR
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<GridFooter sx={{ borderTop: 'none' }} />
|
||||
</Box>
|
||||
<Box sx={{
|
||||
p: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 'auto',
|
||||
flexShrink: 1,
|
||||
overflowX: 'auto',
|
||||
whiteSpace: 'nowrap',
|
||||
<Box sx={{
|
||||
p: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 'auto',
|
||||
flexShrink: 1,
|
||||
overflowX: 'auto',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={{ flexBasis: columnsPromedios[0].width || 'auto', minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold' }}>Generales:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsPromedios[1].width || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.cantDias.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[2].flex, minWidth: columnsPromedios[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[3].flex, minWidth: columnsPromedios[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[4].flex, minWidth: columnsPromedios[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsPromedios[1].width || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.cantDias.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[2].flex, minWidth: columnsPromedios[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[3].flex, minWidth: columnsPromedios[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[4].flex, minWidth: columnsPromedios[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[5].flex, minWidth: columnsPromedios[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.porcentajeDevolucionGeneral.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
@@ -344,6 +352,9 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
|
||||
362
Frontend/src/pages/Reportes/ReporteNovedadesCanillasPage.tsx
Normal file
362
Frontend/src/pages/Reportes/ReporteNovedadesCanillasPage.tsx
Normal file
@@ -0,0 +1,362 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button, Tooltip
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { NovedadesCanillasReporteDto } from '../../models/dtos/Reportes/NovedadesCanillasReporteDto';
|
||||
import type { CanillaGananciaReporteDto } from '../../models/dtos/Reportes/CanillaGananciaReporteDto';
|
||||
import SeleccionaReporteNovedadesCanillas from './SeleccionaReporteNovedadesCanillas';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
interface NovedadesCanillasReporteGridItem extends NovedadesCanillasReporteDto {
|
||||
id: string;
|
||||
}
|
||||
interface CanillaGananciaGridItem extends CanillaGananciaReporteDto {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ReporteNovedadesCanillasPage: React.FC = () => {
|
||||
const [novedadesData, setNovedadesData] = useState<NovedadesCanillasReporteGridItem[]>([]);
|
||||
const [gananciasData, setGananciasData] = useState<CanillaGananciaGridItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
|
||||
const [showParamSelector, setShowParamSelector] = useState(true);
|
||||
const [currentParams, setCurrentParams] = useState<{
|
||||
idEmpresa: number;
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
nombreEmpresa?: string;
|
||||
} | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerReporte = isSuperAdmin || tienePermiso("RR004");
|
||||
|
||||
const currencyFormatter = (value: number | null | undefined) => // Helper para formato moneda
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '';
|
||||
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
idEmpresa: number;
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
}) => {
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este reporte.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setNovedadesData([]);
|
||||
setGananciasData([]); // Limpiar datos de ganancias
|
||||
|
||||
let empresaNombre = `Empresa ${params.idEmpresa}`;
|
||||
try {
|
||||
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
|
||||
const empData = await empresaService.getEmpresaLookupById(params.idEmpresa);
|
||||
if (empData) empresaNombre = empData.nombre;
|
||||
} catch (e) { console.warn("No se pudo obtener nombre de empresa para el reporte", e); }
|
||||
|
||||
setCurrentParams({ ...params, nombreEmpresa: empresaNombre });
|
||||
|
||||
try {
|
||||
// Llamadas concurrentes para ambos conjuntos de datos
|
||||
const [novedadesResult, gananciasResult] = await Promise.all([
|
||||
reportesService.getNovedadesCanillasReporte(params),
|
||||
reportesService.getCanillasGananciasReporte(params) // << LLAMAR AL NUEVO SERVICIO
|
||||
]);
|
||||
|
||||
const novedadesConIds = novedadesResult.map((item, index) => ({
|
||||
...item,
|
||||
id: `nov-${item.nomApe || 'sinnom'}-${item.fecha || 'sinfec'}-${index}`
|
||||
}));
|
||||
setNovedadesData(novedadesConIds);
|
||||
|
||||
const gananciasConIds = gananciasResult.map((item, index) => ({
|
||||
...item,
|
||||
id: `gan-${item.canilla || 'sincan'}-${index}`
|
||||
}));
|
||||
setGananciasData(gananciasConIds);
|
||||
|
||||
if (novedadesConIds.length === 0 && gananciasConIds.length === 0) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error al generar el reporte.';
|
||||
setApiErrorParams(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [puedeVerReporte]);
|
||||
|
||||
const handleVolverAParametros = useCallback(() => {
|
||||
setShowParamSelector(true);
|
||||
setNovedadesData([]);
|
||||
setGananciasData([]); // Limpiar también
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setCurrentParams(null);
|
||||
}, []);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (novedadesData.length === 0 && gananciasData.length === 0) { // Chequear ambos
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// Hoja de Ganancias
|
||||
if (gananciasData.length > 0) {
|
||||
const gananciasToExport = gananciasData.map(({ id, ...rest }) => ({
|
||||
"Canilla": rest.canilla,
|
||||
"Legajo": rest.legajo ?? '-',
|
||||
"Faltas": rest.faltas ?? 0,
|
||||
"Francos": rest.francos ?? 0,
|
||||
"Comisiones": rest.totalRendir ?? 0,
|
||||
}));
|
||||
const totalComisiones = gananciasData.reduce((sum, item) => sum + (item.totalRendir ?? 0), 0);
|
||||
gananciasToExport.push({
|
||||
"Canilla": "Total", "Legajo": "", "Faltas": 0, "Francos": 0,
|
||||
"Comisiones": totalComisiones
|
||||
});
|
||||
const wsGanancias = XLSX.utils.json_to_sheet(gananciasToExport);
|
||||
const headersGan = Object.keys(gananciasToExport[0] || {});
|
||||
wsGanancias['!cols'] = headersGan.map(h => {
|
||||
const maxLen = Math.max(...gananciasToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length);
|
||||
return { wch: maxLen + 2 };
|
||||
});
|
||||
XLSX.utils.book_append_sheet(wb, wsGanancias, "ResumenCanillas");
|
||||
}
|
||||
|
||||
// Hoja de Novedades
|
||||
if (novedadesData.length > 0) {
|
||||
const novedadesToExport = novedadesData.map(({ id, ...rest }) => ({
|
||||
"Canillita": rest.nomApe,
|
||||
"Fecha": rest.fecha ? new Date(rest.fecha).toLocaleDateString('es-AR', {timeZone: 'UTC'}) : '-',
|
||||
"Detalle": rest.detalle,
|
||||
}));
|
||||
const wsNovedades = XLSX.utils.json_to_sheet(novedadesToExport);
|
||||
const headersNov = Object.keys(novedadesToExport[0] || {});
|
||||
wsNovedades['!cols'] = headersNov.map(h => {
|
||||
const maxLen = Math.max(...novedadesToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length);
|
||||
if (h === "Detalle") return { wch: Math.max(maxLen + 2, 50) };
|
||||
return { wch: maxLen + 2 };
|
||||
});
|
||||
XLSX.utils.book_append_sheet(wb, wsNovedades, "DetalleNovedades");
|
||||
}
|
||||
|
||||
let fileName = "ReporteNovedadesCanillas";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') || `Emp${currentParams.idEmpresa}`}`;
|
||||
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
|
||||
}
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
}, [novedadesData, gananciasData, currentParams]);
|
||||
|
||||
const handleGenerarYAbrirPdf = useCallback(async () => {
|
||||
// ... (sin cambios, ya que el PDF del backend ya debería estar manejando ambos DataSets)
|
||||
if (!currentParams) {
|
||||
setError("Primero debe generar el reporte en pantalla.");
|
||||
return;
|
||||
}
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este PDF.");
|
||||
return;
|
||||
}
|
||||
setLoadingPdf(true);
|
||||
setError(null);
|
||||
try {
|
||||
const blob = await reportesService.getNovedadesCanillasReportePdf(currentParams);
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
|
||||
setError(msg);
|
||||
} else {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const w = window.open(url, '_blank');
|
||||
if (!w) alert("Permita popups para ver el PDF.");
|
||||
}
|
||||
} catch (err: any){
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error al generar el PDF.';
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoadingPdf(false);
|
||||
}
|
||||
}, [currentParams, puedeVerReporte]);
|
||||
|
||||
// Columnas para la tabla de Resumen/Ganancias
|
||||
const columnsGanancias: GridColDef<CanillaGananciaGridItem>[] = [
|
||||
{ field: 'canilla', headerName: 'Canilla', width: 250, flex: 1.5 },
|
||||
{ field: 'legajo', headerName: 'Legajo', width: 100, type: 'number' },
|
||||
{ field: 'faltas', headerName: 'Faltas', width: 100, type: 'number', align: 'right', headerAlign: 'right' },
|
||||
{ field: 'francos', headerName: 'Francos', width: 100, type: 'number', align: 'right', headerAlign: 'right' },
|
||||
{ field: 'totalRendir', headerName: 'Comisiones', width: 150, type: 'number', align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
// Columnas para la tabla de Detalles de Novedades (ya existentes)
|
||||
const columnsNovedades: GridColDef<NovedadesCanillasReporteGridItem>[] = [
|
||||
{ field: 'nomApe', headerName: 'Canillita', width: 250, flex: 1.5 },
|
||||
{
|
||||
field: 'fecha',
|
||||
headerName: 'Fecha',
|
||||
width: 120,
|
||||
type: 'date',
|
||||
valueGetter: (value) => value ? new Date(value as string) : null,
|
||||
valueFormatter: (value) => value ? new Date(value as string).toLocaleDateString('es-AR', {timeZone: 'UTC'}) : '-',
|
||||
},
|
||||
{ field: 'detalle', headerName: 'Detalle Novedad', flex: 2, minWidth: 350,
|
||||
renderCell: (params) => (
|
||||
<Tooltip title={params.value || ''} arrow placement="top">
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', width: '100%' }}>
|
||||
{params.value || '-'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const rowsGanancias = useMemo(() => gananciasData, [gananciasData]);
|
||||
const rowsNovedades = useMemo(() => novedadesData, [novedadesData]);
|
||||
|
||||
const totalComisionesGanancias = useMemo(() =>
|
||||
gananciasData.reduce((sum, item) => sum + (item.totalRendir ?? 0), 0),
|
||||
[gananciasData]);
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const FooterGanancias = () => (
|
||||
<GridFooterContainer sx={{ justifyContent: 'flex-end', borderTop: (theme) => `1px solid ${theme.palette.divider}` }}>
|
||||
<GridFooter />
|
||||
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold' }}>
|
||||
<Typography variant="subtitle1" sx={{ mr: 2 }}>Total Comisiones:</Typography>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>{currencyFormatter(totalComisionesGanancias)}</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
const FooterNovedades = () => ( <GridFooterContainer sx={{ justifyContent: 'flex-end', borderTop: (theme) => `1px solid ${theme.palette.divider}` }}><GridFooter /></GridFooterContainer>);
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
<SeleccionaReporteNovedadesCanillas
|
||||
onGenerarReporte={handleGenerarReporte}
|
||||
isLoading={loading}
|
||||
apiErrorMessage={apiErrorParams}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!loading && !puedeVerReporte && !showParamSelector) {
|
||||
// ... (renderizado de "sin permiso" sin cambios)
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Alert severity="error" sx={{ m: 2 }}>No tiene permiso para ver este reporte.</Alert>
|
||||
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
|
||||
Volver
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Reporte: Listado de Novedades Canillitas</Typography> {/* Título más genérico */}
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || (novedadesData.length === 0 && gananciasData.length === 0) || !!error} size="small">
|
||||
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
|
||||
</Button>
|
||||
<Button onClick={handleExportToExcel} variant="outlined" disabled={(novedadesData.length === 0 && gananciasData.length === 0) || !!error} size="small">
|
||||
Exportar a Excel
|
||||
</Button>
|
||||
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
|
||||
Nuevos Parámetros
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Empresa: {currentParams?.nombreEmpresa || '-'} |
|
||||
Período: {currentParams?.fechaDesde ? new Date(currentParams.fechaDesde + 'T00:00:00').toLocaleDateString('es-AR') : ''} al {currentParams?.fechaHasta ? new Date(currentParams.fechaHasta + 'T00:00:00').toLocaleDateString('es-AR') : ''}
|
||||
</Typography>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{(error && !loading) && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{/* Sección de Ganancias/Resumen */}
|
||||
{!loading && !error && currentParams && (
|
||||
<>
|
||||
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>Resumen de Actividad</Typography>
|
||||
{gananciasData.length > 0 ? (
|
||||
<Paper sx={{ height: 250, width: '100%', mb: 3 }}>
|
||||
<DataGrid
|
||||
rows={rowsGanancias}
|
||||
columns={columnsGanancias}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: FooterGanancias }}
|
||||
density="compact"
|
||||
initialState={{ pagination: { paginationModel: { pageSize: 25 } } }}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
disableRowSelectionOnClick
|
||||
rowHeight={48}
|
||||
sx={{
|
||||
'& .MuiDataGrid-cell': { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
'& .MuiDataGrid-columnHeaderTitleContainer': { overflow: 'hidden' },
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography sx={{mt:1, mb:3, fontStyle:'italic'}}>No hay datos de resumen de actividad para mostrar.</Typography>
|
||||
)}
|
||||
|
||||
{/* Sección de Detalle de Novedades */}
|
||||
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>Otras Novedades (Detalle)</Typography>
|
||||
{novedadesData.length > 0 ? (
|
||||
<Paper sx={{ height: 250, width: '100%', mb: 3 }}> {/* Ajustar altura si es necesario */}
|
||||
<DataGrid
|
||||
rows={rowsNovedades}
|
||||
columns={columnsNovedades}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: FooterNovedades }}
|
||||
density="compact"
|
||||
initialState={{ pagination: { paginationModel: { pageSize: 25 } } }}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
disableRowSelectionOnClick
|
||||
rowHeight={48}
|
||||
sx={{
|
||||
'& .MuiDataGrid-cell': { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
'& .MuiDataGrid-columnHeaderTitleContainer': { overflow: 'hidden' },
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography sx={{mt:1, fontStyle:'italic'}}>No hay detalles de otras novedades para mostrar.</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!loading && !error && novedadesData.length === 0 && gananciasData.length === 0 && currentParams && (
|
||||
<Typography sx={{mt:2, fontStyle:'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReporteNovedadesCanillasPage;
|
||||
@@ -21,12 +21,15 @@ const allReportModules: { category: string; label: string; path: string }[] = [
|
||||
{ category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas' },
|
||||
{ category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' },
|
||||
{ category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones' },
|
||||
{ category: 'Novedades de Canillitas', label: 'Novedades de Canillitas', path: 'novedades-canillas' },
|
||||
{ category: 'Listados Distribución', label: 'Dist. Mensual Can/Acc', path: 'listado-distribucion-mensual' },
|
||||
];
|
||||
|
||||
const predefinedCategoryOrder = [
|
||||
'Balance de Cuentas',
|
||||
'Listados Distribución',
|
||||
'Ctrl. Devoluciones',
|
||||
'Novedades de Canillitas',
|
||||
'Existencia Papel',
|
||||
'Movimientos Bobinas',
|
||||
'Consumos Bobinas',
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||
|
||||
interface SeleccionaReporteControlDevolucionesProps {
|
||||
onGenerarReporte: (params: {
|
||||
@@ -24,7 +24,7 @@ const SeleccionaReporteControlDevoluciones: React.FC<SeleccionaReporteControlDev
|
||||
}) => {
|
||||
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||
const [loadingEmpresas, setLoadingEmpresas] = useState(false);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
|
||||
@@ -32,7 +32,7 @@ const SeleccionaReporteControlDevoluciones: React.FC<SeleccionaReporteControlDev
|
||||
const fetchEmpresas = async () => {
|
||||
setLoadingEmpresas(true);
|
||||
try {
|
||||
const data = await empresaService.getAllEmpresas(); // Solo habilitadas
|
||||
const data = await empresaService.getEmpresasDropdown(); // Solo habilitadas
|
||||
setEmpresas(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar empresas:", error);
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
|
||||
interface SeleccionaReporteCuentasDistribuidoresProps {
|
||||
@@ -30,8 +30,8 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
||||
const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
@@ -40,8 +40,8 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [distData, empData] = await Promise.all([
|
||||
distribuidorService.getAllDistribuidores(), // Asume que este servicio existe
|
||||
empresaService.getAllEmpresas() // Asume que este servicio existe
|
||||
distribuidorService.getAllDistribuidoresDropdown(), // Asume que este servicio existe
|
||||
empresaService.getEmpresasDropdown() // Asume que este servicio existe
|
||||
]);
|
||||
setDistribuidores(distData.map(d => d)); // El servicio devuelve tupla
|
||||
setEmpresas(empData);
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl,
|
||||
ToggleButtonGroup, ToggleButton, RadioGroup, FormControlLabel, Radio
|
||||
} from '@mui/material';
|
||||
|
||||
export type TipoListadoDistMensual = 'diarios' | 'publicaciones';
|
||||
|
||||
interface SeleccionaReporteListadoDistMensualProps {
|
||||
onGenerarReporte: (params: {
|
||||
fechaDesde: string; // yyyy-MM-dd (primer día del mes)
|
||||
fechaHasta: string; // yyyy-MM-dd (último día del mes)
|
||||
esAccionista: boolean;
|
||||
tipoReporte: TipoListadoDistMensual;
|
||||
}) => Promise<void>;
|
||||
onCancel?: () => void;
|
||||
isLoading?: boolean;
|
||||
apiErrorMessage?: string | null;
|
||||
}
|
||||
|
||||
const SeleccionaReporteListadoDistMensual: React.FC<SeleccionaReporteListadoDistMensualProps> = ({
|
||||
onGenerarReporte,
|
||||
isLoading,
|
||||
apiErrorMessage
|
||||
}) => {
|
||||
const [mesAnio, setMesAnio] = useState<string>(new Date().toISOString().substring(0, 7)); // Formato "YYYY-MM"
|
||||
const [esAccionista, setEsAccionista] = useState<boolean>(false); // Default a Canillitas
|
||||
const [tipoReporte, setTipoReporte] = useState<TipoListadoDistMensual>('publicaciones'); // Default a Por Publicación
|
||||
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!mesAnio) errors.mesAnio = 'Debe seleccionar un Mes/Año.';
|
||||
// esAccionista y tipoReporte siempre tendrán un valor debido a los defaults y ToggleButton/RadioGroup
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleGenerar = () => {
|
||||
if (!validate()) return;
|
||||
|
||||
const [year, month] = mesAnio.split('-').map(Number);
|
||||
const fechaDesde = new Date(year, month - 1, 1).toISOString().split('T')[0];
|
||||
const fechaHasta = new Date(year, month, 0).toISOString().split('T')[0]; // Último día del mes
|
||||
|
||||
onGenerarReporte({
|
||||
fechaDesde,
|
||||
fechaHasta,
|
||||
esAccionista,
|
||||
tipoReporte
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Parámetros: Listado Distribución Mensual
|
||||
</Typography>
|
||||
<TextField
|
||||
label="Mes y Año"
|
||||
type="month"
|
||||
value={mesAnio}
|
||||
onChange={(e) => { setMesAnio(e.target.value); setLocalErrors(p => ({ ...p, mesAnio: null })); }}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
required
|
||||
error={!!localErrors.mesAnio}
|
||||
helperText={localErrors.mesAnio}
|
||||
disabled={isLoading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
<FormControl component="fieldset" margin="normal" fullWidth disabled={isLoading}>
|
||||
<Typography component="legend" variant="subtitle2" sx={{mb:0.5, color: 'rgba(0, 0, 0, 0.6)'}}>Tipo de Vendedor</Typography>
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
value={esAccionista ? 'accionistas' : 'canillitas'}
|
||||
exclusive
|
||||
onChange={(_, newValue) => {
|
||||
if (newValue !== null) setEsAccionista(newValue === 'accionistas');
|
||||
}}
|
||||
aria-label="Tipo de Vendedor"
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton value="canillitas">Canillitas</ToggleButton>
|
||||
<ToggleButton value="accionistas">Accionistas</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</FormControl>
|
||||
|
||||
<FormControl component="fieldset" margin="normal" fullWidth disabled={isLoading}>
|
||||
<Typography component="legend" variant="subtitle2" sx={{mb:0.5, color: 'rgba(0, 0, 0, 0.6)'}}>Variante del Reporte</Typography>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-label="Variante del Reporte"
|
||||
name="tipoReporte"
|
||||
value={tipoReporte}
|
||||
onChange={(e) => setTipoReporte(e.target.value as TipoListadoDistMensual)}
|
||||
>
|
||||
<FormControlLabel value="publicaciones" control={<Radio size="small" />} label="Por Publicación" />
|
||||
<FormControlLabel value="diarios" control={<Radio size="small" />} label="Por Diarios (El Día/El Plata)" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={handleGenerar} variant="contained" disabled={isLoading}>
|
||||
{isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeleccionaReporteListadoDistMensual;
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
import publicacionService from '../../services/Distribucion/publicacionService';
|
||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
|
||||
interface SeleccionaReporteListadoDistribucionProps {
|
||||
@@ -30,8 +30,8 @@ const SeleccionaReporteListadoDistribucion: React.FC<SeleccionaReporteListadoDis
|
||||
const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
@@ -40,8 +40,8 @@ const SeleccionaReporteListadoDistribucion: React.FC<SeleccionaReporteListadoDis
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [distData, pubData] = await Promise.all([
|
||||
distribuidorService.getAllDistribuidores(),
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true) // Solo habilitadas
|
||||
distribuidorService.getAllDistribuidoresDropdown(),
|
||||
publicacionService.getPublicacionesForDropdown(true) // Solo habilitadas
|
||||
]);
|
||||
setDistribuidores(distData.map(d => d));
|
||||
setPublicaciones(pubData.map(p => p));
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
|
||||
interface SeleccionaReporteNovedadesCanillasProps {
|
||||
onGenerarReporte: (params: {
|
||||
idEmpresa: number;
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
}) => Promise<void>;
|
||||
onCancel?: () => void; // Opcional si se usa dentro de ReportesIndexPage
|
||||
isLoading?: boolean;
|
||||
apiErrorMessage?: string | null;
|
||||
}
|
||||
|
||||
const SeleccionaReporteNovedadesCanillas: React.FC<SeleccionaReporteNovedadesCanillasProps> = ({
|
||||
onGenerarReporte,
|
||||
// onCancel,
|
||||
isLoading,
|
||||
apiErrorMessage
|
||||
}) => {
|
||||
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
||||
const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
|
||||
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEmpresas = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const data = await empresaService.getEmpresasDropdown();
|
||||
setEmpresas(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar empresas:", error);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar empresas.' }));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
fetchEmpresas();
|
||||
}, []);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!idEmpresa) errors.idEmpresa = 'Debe seleccionar una empresa.';
|
||||
if (!fechaDesde) errors.fechaDesde = 'Fecha Desde es obligatoria.';
|
||||
if (!fechaHasta) errors.fechaHasta = 'Fecha Hasta es obligatoria.';
|
||||
if (fechaDesde && fechaHasta && new Date(fechaDesde) > new Date(fechaHasta)) {
|
||||
errors.fechaHasta = 'Fecha Hasta no puede ser anterior a Fecha Desde.';
|
||||
}
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleGenerar = () => {
|
||||
if (!validate()) return;
|
||||
onGenerarReporte({
|
||||
idEmpresa: Number(idEmpresa),
|
||||
fechaDesde,
|
||||
fechaHasta
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Parámetros: Reporte Novedades de Canillitas
|
||||
</Typography>
|
||||
<FormControl fullWidth margin="normal" error={!!localErrors.idEmpresa} disabled={isLoading || loadingDropdowns}>
|
||||
<InputLabel id="empresa-novedades-select-label" required>Empresa</InputLabel>
|
||||
<Select
|
||||
labelId="empresa-novedades-select-label"
|
||||
label="Empresa"
|
||||
value={idEmpresa}
|
||||
onChange={(e) => { setIdEmpresa(e.target.value as number); setLocalErrors(p => ({ ...p, idEmpresa: null })); }}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione una empresa</em></MenuItem>
|
||||
{empresas.map((e) => (
|
||||
<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{localErrors.idEmpresa && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idEmpresa}</Typography>}
|
||||
</FormControl>
|
||||
<TextField
|
||||
label="Fecha Desde"
|
||||
type="date"
|
||||
value={fechaDesde}
|
||||
onChange={(e) => { setFechaDesde(e.target.value); setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null })); }}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
required
|
||||
error={!!localErrors.fechaDesde}
|
||||
helperText={localErrors.fechaDesde}
|
||||
disabled={isLoading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
label="Fecha Hasta"
|
||||
type="date"
|
||||
value={fechaHasta}
|
||||
onChange={(e) => { setFechaHasta(e.target.value); setLocalErrors(p => ({ ...p, fechaHasta: null })); }}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
required
|
||||
error={!!localErrors.fechaHasta}
|
||||
helperText={localErrors.fechaHasta}
|
||||
disabled={isLoading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
|
||||
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
{/* {onCancel && <Button onClick={onCancel} color="secondary" disabled={isLoading}>Cancelar</Button>} */}
|
||||
<Button onClick={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}>
|
||||
{isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeleccionaReporteNovedadesCanillas;
|
||||
@@ -1,27 +1,79 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box, Typography, Button, Paper, CircularProgress, Alert
|
||||
Box, Typography, Button, Paper, CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import perfilService from '../../services/Usuarios/perfilService';
|
||||
import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto';
|
||||
import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto';
|
||||
import { usePermissions } from '../../hooks/usePermissions'; // Para verificar si el usuario actual puede estar aquí
|
||||
import { usePermissions as usePagePermissions } from '../../hooks/usePermissions'; // Renombrar para evitar conflicto
|
||||
import axios from 'axios';
|
||||
import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist'; // Importar el componente
|
||||
import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist';
|
||||
|
||||
const SECCION_PERMISSIONS_PREFIX = "SS";
|
||||
|
||||
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
|
||||
if (codAcc === "SS001") return "Distribución";
|
||||
if (codAcc === "SS002") return "Contables";
|
||||
if (codAcc === "SS003") return "Impresión";
|
||||
if (codAcc === "SS004") return "Reportes";
|
||||
if (codAcc === "SS005") return "Radios";
|
||||
if (codAcc === "SS006") return "Usuarios";
|
||||
return null;
|
||||
};
|
||||
|
||||
const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
const moduloLower = permisoModulo.toLowerCase();
|
||||
if (moduloLower.includes("distribuidores") ||
|
||||
moduloLower.includes("canillas") ||
|
||||
moduloLower.includes("publicaciones distribución") ||
|
||||
moduloLower.includes("zonas distribuidores") ||
|
||||
moduloLower.includes("movimientos distribuidores") ||
|
||||
moduloLower.includes("empresas") ||
|
||||
moduloLower.includes("otros destinos") ||
|
||||
moduloLower.includes("ctrl. devoluciones") ||
|
||||
moduloLower.includes("movimientos canillas") ||
|
||||
moduloLower.includes("salidas otros destinos")) {
|
||||
return "Distribución";
|
||||
}
|
||||
if (moduloLower.includes("cuentas pagos") ||
|
||||
moduloLower.includes("cuentas notas") ||
|
||||
moduloLower.includes("cuentas tipos pagos")) {
|
||||
return "Contables";
|
||||
}
|
||||
if (moduloLower.includes("impresión tiradas") ||
|
||||
moduloLower.includes("impresión bobinas") ||
|
||||
moduloLower.includes("impresión plantas") ||
|
||||
moduloLower.includes("tipos bobinas")) {
|
||||
return "Impresión";
|
||||
}
|
||||
if (moduloLower.includes("radios")) {
|
||||
return "Radios";
|
||||
}
|
||||
if (moduloLower.includes("usuarios") ||
|
||||
moduloLower.includes("perfiles")) {
|
||||
return "Usuarios";
|
||||
}
|
||||
if (moduloLower.includes("reportes")) {
|
||||
return "Reportes";
|
||||
}
|
||||
if (moduloLower.includes("permisos")) {
|
||||
return "Permisos (Definición)";
|
||||
}
|
||||
return permisoModulo;
|
||||
};
|
||||
|
||||
const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
const { idPerfil } = useParams<{ idPerfil: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const { tienePermiso: tienePermisoPagina, isSuperAdmin } = usePagePermissions(); // Renombrado
|
||||
|
||||
const puedeAsignar = isSuperAdmin || tienePermiso("PU004");
|
||||
const puedeAsignar = isSuperAdmin || tienePermisoPagina("PU004");
|
||||
|
||||
const [perfil, setPerfil] = useState<PerfilDto | null>(null);
|
||||
const [permisosDisponibles, setPermisosDisponibles] = useState<PermisoAsignadoDto[]>([]);
|
||||
// Usamos un Set para los IDs de los permisos seleccionados para eficiencia
|
||||
const [permisosSeleccionados, setPermisosSeleccionados] = useState<Set<number>>(new Set());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -32,29 +84,28 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
|
||||
const cargarDatos = useCallback(async () => {
|
||||
if (!puedeAsignar) {
|
||||
setError("Acceso denegado. No tiene permiso para asignar permisos.");
|
||||
setLoading(false);
|
||||
return;
|
||||
setError("Acceso denegado. No tiene permiso para asignar permisos.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (isNaN(idPerfilNum)) {
|
||||
setError("ID de Perfil inválido.");
|
||||
setLoading(false);
|
||||
return;
|
||||
setError("ID de Perfil inválido.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setSuccessMessage(null);
|
||||
try {
|
||||
const [perfilData, permisosData] = await Promise.all([
|
||||
perfilService.getPerfilById(idPerfilNum),
|
||||
perfilService.getPermisosPorPerfil(idPerfilNum)
|
||||
perfilService.getPermisosPorPerfil(idPerfilNum) // Esto devuelve todos los permisos con su estado 'asignado'
|
||||
]);
|
||||
setPerfil(perfilData);
|
||||
setPermisosDisponibles(permisosData);
|
||||
// Inicializar los permisos seleccionados basados en los que vienen 'asignado: true'
|
||||
setPermisosSeleccionados(new Set(permisosData.filter(p => p.asignado).map(p => p.id)));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Error al cargar datos del perfil o permisos.');
|
||||
if (axios.isAxiosError(err) && err.response?.status === 404) {
|
||||
if (axios.isAxiosError(err) && err.response?.status === 404) {
|
||||
setError(`Perfil con ID ${idPerfilNum} no encontrado.`);
|
||||
}
|
||||
} finally {
|
||||
@@ -66,22 +117,83 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
cargarDatos();
|
||||
}, [cargarDatos]);
|
||||
|
||||
const handlePermisoChange = (permisoId: number, asignado: boolean) => {
|
||||
setPermisosSeleccionados(prev => {
|
||||
const next = new Set(prev);
|
||||
if (asignado) {
|
||||
next.add(permisoId);
|
||||
} else {
|
||||
next.delete(permisoId);
|
||||
}
|
||||
return next;
|
||||
const handlePermisoChange = useCallback((
|
||||
permisoId: number,
|
||||
asignadoViaCheckboxHijo: boolean, // Este valor es el 'e.target.checked' si el clic fue en un hijo
|
||||
esPermisoSeccionClick = false,
|
||||
moduloConceptualAsociado?: string // Este es el módulo conceptual del padre SSxxx o del grupo del hijo
|
||||
) => {
|
||||
setPermisosSeleccionados(prevSelected => {
|
||||
const newSelected = new Set(prevSelected);
|
||||
const permisoActual = permisosDisponibles.find(p => p.id === permisoId);
|
||||
if (!permisoActual) return prevSelected;
|
||||
|
||||
const permisosDelModuloHijo = moduloConceptualAsociado
|
||||
? permisosDisponibles.filter(p => {
|
||||
const mc = getModuloConceptualDelPermiso(p.modulo); // Usar la función helper
|
||||
return mc === moduloConceptualAsociado && !p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX);
|
||||
})
|
||||
: [];
|
||||
|
||||
if (esPermisoSeccionClick && moduloConceptualAsociado) {
|
||||
const idPermisoSeccion = permisoActual.id;
|
||||
const estabaSeccionSeleccionada = prevSelected.has(idPermisoSeccion);
|
||||
const todosHijosEstabanSeleccionados = permisosDelModuloHijo.length > 0 && permisosDelModuloHijo.every(p => prevSelected.has(p.id));
|
||||
const ningunHijoEstabaSeleccionado = permisosDelModuloHijo.every(p => !prevSelected.has(p.id));
|
||||
|
||||
|
||||
if (!estabaSeccionSeleccionada) { // Estaba Off, pasa a "Solo Sección" (Indeterminate si hay hijos)
|
||||
newSelected.add(idPermisoSeccion);
|
||||
// NO se marcan los hijos
|
||||
} else if (estabaSeccionSeleccionada && (ningunHijoEstabaSeleccionado || !todosHijosEstabanSeleccionados) && permisosDelModuloHijo.length > 0 ) {
|
||||
// Estaba "Solo Sección" o "Parcial Hijos", pasa a "Sección + Todos los Hijos"
|
||||
newSelected.add(idPermisoSeccion); // Asegurar
|
||||
permisosDelModuloHijo.forEach(p => newSelected.add(p.id));
|
||||
} else { // Estaba "Sección + Todos los Hijos" (o no había hijos), pasa a Off
|
||||
newSelected.delete(idPermisoSeccion);
|
||||
permisosDelModuloHijo.forEach(p => newSelected.delete(p.id));
|
||||
}
|
||||
|
||||
} else if (!esPermisoSeccionClick && moduloConceptualAsociado) { // Clic en un permiso hijo
|
||||
if (asignadoViaCheckboxHijo) {
|
||||
newSelected.add(permisoId);
|
||||
const permisoSeccionPadre = permisosDisponibles.find(
|
||||
ps => ps.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX) && getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptualAsociado
|
||||
);
|
||||
if (permisoSeccionPadre && !newSelected.has(permisoSeccionPadre.id)) {
|
||||
newSelected.add(permisoSeccionPadre.id); // Marcar padre si no estaba
|
||||
}
|
||||
} else { // Desmarcando un hijo
|
||||
newSelected.delete(permisoId);
|
||||
const permisoSeccionPadre = permisosDisponibles.find(
|
||||
ps => ps.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX) && getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptualAsociado
|
||||
);
|
||||
if (permisoSeccionPadre) {
|
||||
const algunOtroHijoSeleccionado = permisosDelModuloHijo.some(p => p.id !== permisoId && newSelected.has(p.id));
|
||||
if (!algunOtroHijoSeleccionado && newSelected.has(permisoSeccionPadre.id)) {
|
||||
// Si era el último hijo y el padre estaba marcado, NO desmarcamos el padre automáticamente.
|
||||
// El estado indeterminate se encargará visualmente.
|
||||
// Si quisiéramos que se desmarque el padre, aquí iría: newSelected.delete(permisoSeccionPadre.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // Permiso sin módulo conceptual asociado (ej: "Permisos (Definición)")
|
||||
if (asignadoViaCheckboxHijo) {
|
||||
newSelected.add(permisoId);
|
||||
} else {
|
||||
newSelected.delete(permisoId);
|
||||
}
|
||||
}
|
||||
|
||||
if (successMessage) setSuccessMessage(null);
|
||||
if (error) setError(null);
|
||||
return newSelected;
|
||||
});
|
||||
// Limpiar mensajes al cambiar selección
|
||||
if (successMessage) setSuccessMessage(null);
|
||||
if (error) setError(null);
|
||||
};
|
||||
}, [permisosDisponibles, successMessage, error]);
|
||||
|
||||
|
||||
const handleGuardarCambios = async () => {
|
||||
// ... (sin cambios) ...
|
||||
if (!puedeAsignar || !perfil) return;
|
||||
setSaving(true); setError(null); setSuccessMessage(null);
|
||||
try {
|
||||
@@ -89,13 +201,12 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
permisosIds: Array.from(permisosSeleccionados)
|
||||
});
|
||||
setSuccessMessage('Permisos actualizados correctamente.');
|
||||
// Opcional: recargar datos, aunque el estado local ya está actualizado
|
||||
// cargarDatos();
|
||||
await cargarDatos();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Error al guardar los permisos.';
|
||||
? err.response.data.message
|
||||
: 'Error al guardar los permisos.';
|
||||
setError(message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
@@ -103,56 +214,54 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
|
||||
}
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
|
||||
}
|
||||
|
||||
if (error && !perfil) { // Si hay un error crítico al cargar el perfil
|
||||
return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
|
||||
}
|
||||
if (!puedeAsignar) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
|
||||
}
|
||||
if (!perfil) { // Si no hay error, pero el perfil es null después de cargar (no debería pasar si no hay error)
|
||||
return <Alert severity="warning" sx={{ m: 2 }}>Perfil no encontrado.</Alert>;
|
||||
}
|
||||
if (error && !perfil) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
|
||||
}
|
||||
if (!puedeAsignar) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
|
||||
}
|
||||
if (!perfil && !loading) {
|
||||
return <Alert severity="warning" sx={{ m: 2 }}>Perfil no encontrado o error al cargar.</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}>
|
||||
Volver a Perfiles
|
||||
</Button>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
ID Perfil: {perfil?.id}
|
||||
</Typography>
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}>
|
||||
Volver a Perfiles
|
||||
</Button>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
ID Perfil: {perfil?.id}
|
||||
</Typography>
|
||||
{error && !successMessage && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
{successMessage && <Alert severity="success" sx={{ mb: 2 }}>{successMessage}</Alert>}
|
||||
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
{successMessage && <Alert severity="success" sx={{ mb: 2 }}>{successMessage}</Alert>}
|
||||
|
||||
<Paper sx={{ p: 2, mt: 2 }}>
|
||||
<PermisosChecklist
|
||||
permisosDisponibles={permisosDisponibles}
|
||||
permisosSeleccionados={permisosSeleccionados}
|
||||
onPermisoChange={handlePermisoChange}
|
||||
disabled={saving}
|
||||
/>
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
|
||||
onClick={handleGuardarCambios}
|
||||
disabled={saving || !puedeAsignar}
|
||||
>
|
||||
Guardar Cambios
|
||||
</Button>
|
||||
<Paper sx={{ p: { xs: 1, sm: 2 }, mt: 2 }}>
|
||||
<PermisosChecklist
|
||||
permisosDisponibles={permisosDisponibles}
|
||||
permisosSeleccionados={permisosSeleccionados}
|
||||
onPermisoChange={handlePermisoChange}
|
||||
disabled={saving}
|
||||
/>
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
|
||||
onClick={handleGuardarCambios}
|
||||
disabled={saving || !puedeAsignar}
|
||||
>
|
||||
Guardar Cambios
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default AsignarPermisosAPerfilPage;
|
||||
Reference in New Issue
Block a user