All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 5m17s
246 lines
14 KiB
TypeScript
246 lines
14 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
|
|
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
|
CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip
|
|
} from '@mui/material';
|
|
import AddIcon from '@mui/icons-material/Add';
|
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
|
import EditIcon from '@mui/icons-material/Edit';
|
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
import FilterListIcon from '@mui/icons-material/FilterList';
|
|
|
|
import entradaSalidaDistService from '../../services/Distribucion/entradaSalidaDistService';
|
|
import publicacionService from '../../services/Distribucion/publicacionService';
|
|
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
|
|
|
import type { EntradaSalidaDistDto } from '../../models/dtos/Distribucion/EntradaSalidaDistDto';
|
|
import type { CreateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaDistDto';
|
|
import type { UpdateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto';
|
|
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
|
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
|
|
|
import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal';
|
|
import { usePermissions } from '../../hooks/usePermissions';
|
|
import axios from 'axios';
|
|
|
|
const GestionarEntradasSalidasDistPage: React.FC = () => {
|
|
const [movimientos, setMovimientos] = useState<EntradaSalidaDistDto[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
|
|
|
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
|
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
|
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
|
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
|
|
const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>('');
|
|
|
|
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
|
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
|
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
|
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaDistDto | null>(null);
|
|
|
|
const [page, setPage] = useState(0);
|
|
const [rowsPerPage, setRowsPerPage] = useState(25);
|
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
const [selectedRow, setSelectedRow] = useState<EntradaSalidaDistDto | null>(null);
|
|
|
|
const { tienePermiso, isSuperAdmin } = usePermissions();
|
|
const puedeVer = isSuperAdmin || tienePermiso("MD001");
|
|
const puedeGestionar = isSuperAdmin || tienePermiso("MD002");
|
|
|
|
// CORREGIDO: Función para formatear la fecha
|
|
const formatDate = (dateString?: string | null): string => {
|
|
if (!dateString) return '-';
|
|
// Asumimos que dateString viene del backend como "YYYY-MM-DD" o "YYYY-MM-DDTHH:mm:ss..."
|
|
const datePart = dateString.split('T')[0]; // Tomar solo la parte YYYY-MM-DD
|
|
const parts = datePart.split('-');
|
|
if (parts.length === 3) {
|
|
// parts[0] = YYYY, parts[1] = MM, parts[2] = DD
|
|
return `${parts[2]}/${parts[1]}/${parts[0]}`; // Formato DD/MM/YYYY
|
|
}
|
|
return datePart; // Fallback si el formato no es el esperado
|
|
};
|
|
|
|
const fetchFiltersDropdownData = useCallback(async () => {
|
|
setLoadingFiltersDropdown(true);
|
|
try {
|
|
const [pubsData, distData] = await Promise.all([
|
|
publicacionService.getPublicacionesForDropdown(true),
|
|
distribuidorService.getAllDistribuidoresDropdown()
|
|
]);
|
|
setPublicaciones(pubsData);
|
|
setDistribuidores(distData);
|
|
} catch (err) {
|
|
console.error(err); setError("Error al cargar opciones de filtro.");
|
|
} finally { setLoadingFiltersDropdown(false); }
|
|
}, []);
|
|
|
|
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
|
|
|
|
const cargarMovimientos = useCallback(async () => {
|
|
if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; }
|
|
setLoading(true); setError(null); setApiErrorMessage(null);
|
|
try {
|
|
const params = {
|
|
fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null,
|
|
idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null,
|
|
idDistribuidor: filtroIdDistribuidor ? Number(filtroIdDistribuidor) : null,
|
|
tipoMovimiento: filtroTipoMov || null,
|
|
};
|
|
const data = await entradaSalidaDistService.getAllEntradasSalidasDist(params);
|
|
setMovimientos(data);
|
|
} catch (err) {
|
|
console.error(err); setError('Error al cargar movimientos.');
|
|
} finally { setLoading(false); }
|
|
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdDistribuidor, filtroTipoMov]);
|
|
|
|
useEffect(() => { cargarMovimientos(); }, [cargarMovimientos]);
|
|
|
|
const handleOpenModal = (item?: EntradaSalidaDistDto) => {
|
|
setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true);
|
|
};
|
|
const handleCloseModal = () => { setModalOpen(false); setEditingMovimiento(null); };
|
|
|
|
const handleSubmitModal = async (data: CreateEntradaSalidaDistDto | UpdateEntradaSalidaDistDto, idParte?: number) => {
|
|
setApiErrorMessage(null);
|
|
try {
|
|
if (idParte && editingMovimiento) {
|
|
await entradaSalidaDistService.updateEntradaSalidaDist(idParte, data as UpdateEntradaSalidaDistDto);
|
|
} else {
|
|
await entradaSalidaDistService.createEntradaSalidaDist(data as CreateEntradaSalidaDistDto);
|
|
}
|
|
cargarMovimientos();
|
|
} catch (err: any) {
|
|
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.';
|
|
setApiErrorMessage(message); throw err;
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (idParte: number) => {
|
|
if (window.confirm(`¿Seguro (ID: ${idParte})? Esto revertirá el saldo.`)) {
|
|
setApiErrorMessage(null);
|
|
try { await entradaSalidaDistService.deleteEntradaSalidaDist(idParte); cargarMovimientos(); }
|
|
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
|
|
}
|
|
handleMenuClose();
|
|
};
|
|
|
|
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaDistDto) => {
|
|
setAnchorEl(event.currentTarget); setSelectedRow(item);
|
|
};
|
|
const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); };
|
|
|
|
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
|
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
setRowsPerPage(parseInt(event.target.value, 25)); setPage(0);
|
|
};
|
|
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
|
// La función formatDate ya está definida arriba.
|
|
|
|
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
|
|
|
return (
|
|
<Box sx={{ p: 1 }}>
|
|
<Typography variant="h5" gutterBottom>Entradas/Salidas a Distribuidores</Typography>
|
|
<Paper sx={{ p: 2, mb: 2 }}>
|
|
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
|
|
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
|
|
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
|
|
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
|
|
<InputLabel>Publicación</InputLabel>
|
|
<Select value={filtroIdPublicacion} label="Publicación" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}>
|
|
<MenuItem value=""><em>Todas</em></MenuItem>
|
|
{publicaciones.map(p => <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>)}
|
|
</Select>
|
|
</FormControl>
|
|
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
|
|
<InputLabel>Distribuidor</InputLabel>
|
|
<Select value={filtroIdDistribuidor} label="Distribuidor" onChange={(e) => setFiltroIdDistribuidor(e.target.value as number | string)}>
|
|
<MenuItem value=""><em>Todos</em></MenuItem>
|
|
{distribuidores.map(d => <MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>)}
|
|
</Select>
|
|
</FormControl>
|
|
<FormControl size="small" sx={{ minWidth: 150, flexGrow: 1 }}>
|
|
<InputLabel>Tipo</InputLabel>
|
|
<Select value={filtroTipoMov} label="Tipo" onChange={(e) => setFiltroTipoMov(e.target.value as 'Salida' | 'Entrada' | '')}>
|
|
<MenuItem value=""><em>Todos</em></MenuItem>
|
|
<MenuItem value="Salida">Salida</MenuItem>
|
|
<MenuItem value="Entrada">Entrada</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
{puedeGestionar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Movimiento</Button>)}
|
|
</Paper>
|
|
|
|
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
|
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
|
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
|
|
|
{!loading && !error && puedeVer && (
|
|
<TableContainer component={Paper}>
|
|
<Table size="small">
|
|
<TableHead><TableRow>
|
|
<TableCell>Fecha</TableCell><TableCell>Publicación (Empresa)</TableCell>
|
|
<TableCell>Distribuidor</TableCell><TableCell>Tipo</TableCell>
|
|
<TableCell align="right">Cantidad</TableCell><TableCell>Remito</TableCell>
|
|
<TableCell align="right">Monto Afectado</TableCell><TableCell>Obs.</TableCell>
|
|
{puedeGestionar && <TableCell align="right">Acciones</TableCell>}
|
|
</TableRow></TableHead>
|
|
<TableBody>
|
|
{displayData.length === 0 ? (
|
|
<TableRow><TableCell colSpan={puedeGestionar ? 9 : 8} align="center">No se encontraron movimientos.</TableCell></TableRow>
|
|
) : (
|
|
displayData.map((m) => (
|
|
<TableRow key={m.idParte} hover>
|
|
{/* Usar la función formatDate aquí */}
|
|
<TableCell>{formatDate(m.fecha)}</TableCell>
|
|
<TableCell>{m.nombrePublicacion} <Chip label={m.nombreEmpresaPublicacion} size="small" variant="outlined" sx={{ ml: 0.5 }} /></TableCell>
|
|
<TableCell>{m.nombreDistribuidor}</TableCell>
|
|
<TableCell>
|
|
<Chip label={m.tipoMovimiento} color={m.tipoMovimiento === 'Salida' ? 'success' : 'error'} size="small" variant="outlined" />
|
|
</TableCell>
|
|
<TableCell align="right">{m.cantidad}</TableCell>
|
|
<TableCell>{m.remito}</TableCell>
|
|
<TableCell align="right" sx={{ fontWeight: 'bold', color: m.tipoMovimiento === 'Salida' ? 'success.main' : (m.montoCalculado === 0 ? 'inherit' : 'error.main') }}>
|
|
{(m.tipoMovimiento === 'Salida' ? '$' : '$-') + m.montoCalculado.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
</TableCell>
|
|
<TableCell><Tooltip title={m.observacion || ''}><Box sx={{ maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.observacion || '-'}</Box></Tooltip></TableCell>
|
|
{puedeGestionar && (
|
|
<TableCell align="right">
|
|
<IconButton onClick={(e) => handleMenuOpen(e, m)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton>
|
|
</TableCell>
|
|
)}
|
|
</TableRow>
|
|
)))}
|
|
</TableBody>
|
|
</Table>
|
|
<TablePagination
|
|
rowsPerPageOptions={[25, 50, 100]} component="div" count={movimientos.length}
|
|
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
|
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
|
/>
|
|
</TableContainer>
|
|
)}
|
|
|
|
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
|
{puedeGestionar && selectedRow && (
|
|
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
|
{puedeGestionar && selectedRow && (
|
|
<MenuItem onClick={() => handleDelete(selectedRow.idParte)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)}
|
|
</Menu>
|
|
|
|
<EntradaSalidaDistFormModal
|
|
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
|
initialData={editingMovimiento} errorMessage={apiErrorMessage}
|
|
clearErrorMessage={() => setApiErrorMessage(null)}
|
|
/>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default GestionarEntradasSalidasDistPage; |