Finalización de Reportes y arreglos varios de controles y comportamientos...

This commit is contained in:
2025-06-03 13:45:20 -03:00
parent 99532b03f1
commit 062cc05fd0
67 changed files with 4523 additions and 993 deletions

View File

@@ -162,7 +162,7 @@ const ControlDevolucionesFormModal: React.FC<ControlDevolucionesFormModalProps>
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
/>
<TextField label="Entrada (Devolución Total)" type="number" value={entrada} required
<TextField label="Entrada (Por Remito)" type="number" value={entrada} required
onChange={(e) => {setEntrada(e.target.value); handleInputChange('entrada');}}
margin="dense" fullWidth error={!!localErrors.entrada} helperText={localErrors.entrada || ''}
disabled={loading} inputProps={{min:0}}
@@ -172,7 +172,7 @@ const ControlDevolucionesFormModal: React.FC<ControlDevolucionesFormModalProps>
margin="dense" fullWidth error={!!localErrors.sobrantes} helperText={localErrors.sobrantes || ''}
disabled={loading} inputProps={{min:0}}
/>
<TextField label="Sin Cargo" type="number" value={sinCargo} required
<TextField label="Ejemplares Sin Cargo" type="number" value={sinCargo} required
onChange={(e) => {setSinCargo(e.target.value); handleInputChange('sinCargo');}}
margin="dense" fullWidth error={!!localErrors.sinCargo} helperText={localErrors.sinCargo || ''}
disabled={loading} inputProps={{min:0}}

View File

@@ -1,12 +1,11 @@
// src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, Paper, IconButton, FormHelperText
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, Paper, IconButton, FormHelperText
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import type { EntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
import type { UpdateEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto';
@@ -19,17 +18,17 @@ import type { EntradaSalidaCanillaItemDto } from '../../../models/dtos/Distribuc
import axios from 'axios';
const modalStyle = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '95%', sm: '80%', md: '750px' },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 2.5,
maxHeight: '90vh',
overflowY: 'auto'
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '95%', sm: '80%', md: '750px' },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 2.5,
maxHeight: '90vh',
overflowY: 'auto'
};
interface EntradaSalidaCanillaFormModalProps {
@@ -51,8 +50,8 @@ interface FormRowItem {
const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps> = ({
open,
onClose,
onSubmit,
onClose, // Este onClose es el que se pasa desde GestionarEntradasSalidasCanillaPage
onSubmit, // Este onSubmit es el que se pasa para la lógica de EDICIÓN
initialData,
errorMessage: parentErrorMessage,
clearErrorMessage
@@ -63,16 +62,18 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
const [editCantSalida, setEditCantSalida] = useState<string>('0');
const [editCantEntrada, setEditCantEntrada] = useState<string>('0');
const [editObservacion, setEditObservacion] = useState('');
const [items, setItems] = useState<FormRowItem[]>([]);
const [items, setItems] = useState<FormRowItem[]>([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); // Iniciar con una fila
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [loading, setLoading] = useState(false); // Loading para submit
const [loadingDropdowns, setLoadingDropdowns] = useState(false); // Loading para canillas/pubs
const [loadingItems, setLoadingItems] = useState(false); // Loading para pre-carga de items
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const [modalSpecificApiError, setModalSpecificApiError] = useState<string | null>(null);
const isEditing = Boolean(initialData);
// Efecto para cargar datos de dropdowns (Publicaciones, Canillitas) SOLO UNA VEZ o cuando open cambia a true
useEffect(() => {
const fetchDropdownData = async () => {
setLoadingDropdowns(true);
@@ -94,6 +95,13 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
if (open) {
fetchDropdownData();
}
}, [open]);
// Efecto para inicializar el formulario cuando se abre o cambia initialData
useEffect(() => {
if (open) {
clearErrorMessage();
setModalSpecificApiError(null);
setLocalErrors({});
@@ -105,19 +113,53 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
setEditCantSalida(initialData.cantSalida?.toString() || '0');
setEditCantEntrada(initialData.cantEntrada?.toString() || '0');
setEditObservacion(initialData.observacion || '');
setItems([]);
setItems([]); // En modo edición, no pre-cargamos items de la lista
} else {
setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
// Modo NUEVO: resetear campos principales y dejar que el efecto de 'fecha' cargue los items
setIdCanilla('');
setFecha(new Date().toISOString().split('T')[0]);
setEditCantSalida('0');
setEditCantEntrada('0');
setEditObservacion('');
setEditIdPublicacion('');
setFecha(new Date().toISOString().split('T')[0]); // Fecha actual por defecto
// Los items se cargarán por el siguiente useEffect basado en la fecha
}
}
}, [open, initialData, isEditing, clearErrorMessage]);
// Efecto para pre-cargar/re-cargar items cuando cambia la FECHA (en modo NUEVO)
// y cuando las publicaciones están disponibles.
useEffect(() => {
if (open && !isEditing && publicaciones.length > 0 && fecha) { // Asegurarse que 'fecha' tiene un valor
const diaSemana = new Date(fecha + 'T00:00:00Z').getUTCDay(); // Usar UTC para getDay consistente
setLoadingItems(true); // Indicador de carga para los items
setLocalErrors(prev => ({ ...prev, general: null }));
publicacionService.getPublicacionesPorDiaSemana(diaSemana)
.then(pubsPorDefecto => {
if (pubsPorDefecto.length > 0) {
const itemsPorDefecto = pubsPorDefecto.map(pub => ({
id: `${Date.now().toString()}-${pub.idPublicacion}`,
idPublicacion: pub.idPublicacion,
cantSalida: '0',
cantEntrada: '0',
observacion: ''
}));
setItems(itemsPorDefecto);
} else {
// Si no hay configuraciones para el día, iniciar con una fila vacía
setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
}
})
.catch(err => {
console.error("Error al cargar/recargar publicaciones por defecto para el día:", err);
setLocalErrors(prev => ({ ...prev, general: 'Error al pre-cargar publicaciones del día.' }));
setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
})
.finally(() => setLoadingItems(false));
} else if (open && !isEditing && publicaciones.length === 0 && !loadingDropdowns) {
// Si las publicaciones aún no se cargaron pero los dropdowns terminaron de cargar, iniciar con 1 item vacío.
setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
}
}, [open, isEditing, fecha, publicaciones, loadingDropdowns]); // Dependencias clave
const validate = (): boolean => {
const currentErrors: { [key: string]: string | null } = {};
if (!idCanilla) currentErrors.idCanilla = 'Seleccione un canillita.';
@@ -125,65 +167,65 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) currentErrors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).';
if (isEditing) {
const salidaNum = parseInt(editCantSalida, 10);
const entradaNum = parseInt(editCantEntrada, 10);
if (editCantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) {
currentErrors.editCantSalida = 'Cant. Salida debe ser un número >= 0.';
}
if (editCantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) {
currentErrors.editCantEntrada = 'Cant. Entrada debe ser un número >= 0.';
} else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) {
currentErrors.editCantEntrada = 'Cant. Entrada no puede ser mayor a Cant. Salida.';
}
const salidaNum = parseInt(editCantSalida, 10);
const entradaNum = parseInt(editCantEntrada, 10);
if (editCantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) {
currentErrors.editCantSalida = 'Cant. Salida debe ser un número >= 0.';
}
if (editCantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) {
currentErrors.editCantEntrada = 'Cant. Entrada debe ser un número >= 0.';
} else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) {
currentErrors.editCantEntrada = 'Cant. Entrada no puede ser mayor a Cant. Salida.';
}
} else {
let hasValidItemWithQuantityOrPub = false;
const publicacionIdsEnLote = new Set<number>();
let hasValidItemWithQuantityOrPub = false;
const publicacionIdsEnLote = new Set<number>();
if (items.length === 0) {
currentErrors.general = "Debe agregar al menos una publicación.";
if (items.length === 0) {
currentErrors.general = "Debe agregar al menos una publicación.";
}
items.forEach((item, index) => {
const salidaNum = parseInt(item.cantSalida, 10);
const entradaNum = parseInt(item.cantEntrada, 10);
const hasQuantity = !isNaN(salidaNum) && salidaNum >= 0 && !isNaN(entradaNum) && entradaNum >= 0 && (salidaNum > 0 || entradaNum > 0);
const hasObservation = item.observacion.trim() !== '';
if (item.idPublicacion === '') {
if (hasQuantity || hasObservation) {
currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} obligatoria si hay datos.`;
}
} else {
const pubIdNum = Number(item.idPublicacion);
if (publicacionIdsEnLote.has(pubIdNum)) {
currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} duplicada.`;
} else {
publicacionIdsEnLote.add(pubIdNum);
}
if (item.cantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) {
currentErrors[`item_${item.id}_cantSalida`] = `Salida Pub. ${index + 1} inválida.`;
}
if (item.cantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) {
currentErrors[`item_${item.id}_cantEntrada`] = `Entrada Pub. ${index + 1} inválida.`;
} else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) {
currentErrors[`item_${item.id}_cantEntrada`] = `Dev. Pub. ${index + 1} > Salida.`;
}
if (item.idPublicacion !== '') hasValidItemWithQuantityOrPub = true;
}
});
items.forEach((item, index) => {
const salidaNum = parseInt(item.cantSalida, 10);
const entradaNum = parseInt(item.cantEntrada, 10);
const hasQuantity = !isNaN(salidaNum) && salidaNum >=0 && !isNaN(entradaNum) && entradaNum >=0 && (salidaNum > 0 || entradaNum > 0);
const hasObservation = item.observacion.trim() !== '';
const allItemsAreEmptyAndNoPubSelected = items.every(
itm => itm.idPublicacion === '' &&
(itm.cantSalida.trim() === '' || parseInt(itm.cantSalida, 10) === 0) &&
(itm.cantEntrada.trim() === '' || parseInt(itm.cantEntrada, 10) === 0) &&
itm.observacion.trim() === ''
);
if (item.idPublicacion === '') {
if (hasQuantity || hasObservation) {
currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} obligatoria si hay datos.`;
}
} else {
const pubIdNum = Number(item.idPublicacion);
if (publicacionIdsEnLote.has(pubIdNum)) {
currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} duplicada.`;
} else {
publicacionIdsEnLote.add(pubIdNum);
}
if (item.cantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) {
currentErrors[`item_${item.id}_cantSalida`] = `Salida Pub. ${index + 1} inválida.`;
}
if (item.cantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) {
currentErrors[`item_${item.id}_cantEntrada`] = `Entrada Pub. ${index + 1} inválida.`;
} else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) {
currentErrors[`item_${item.id}_cantEntrada`] = `Dev. Pub. ${index + 1} > Salida.`;
}
if (item.idPublicacion !== '') hasValidItemWithQuantityOrPub = true;
}
});
const allItemsAreEmptyAndNoPubSelected = items.every(
itm => itm.idPublicacion === '' &&
(itm.cantSalida.trim() === '' || parseInt(itm.cantSalida, 10) === 0) &&
(itm.cantEntrada.trim() === '' || parseInt(itm.cantEntrada, 10) === 0) &&
itm.observacion.trim() === ''
);
if (!isEditing && items.length > 0 && !hasValidItemWithQuantityOrPub && !allItemsAreEmptyAndNoPubSelected) {
currentErrors.general = "Debe seleccionar una publicación para los ítems con cantidades y/o observaciones.";
} else if (!isEditing && items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida,10) > 0 || parseInt(i.cantEntrada,10) > 0)) && !allItemsAreEmptyAndNoPubSelected) {
currentErrors.general = "Debe ingresar cantidades para al menos una publicación con datos significativos.";
}
if (!isEditing && items.length > 0 && !hasValidItemWithQuantityOrPub && !allItemsAreEmptyAndNoPubSelected) {
currentErrors.general = "Debe seleccionar una publicación para los ítems con cantidades y/o observaciones.";
} else if (!isEditing && items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida, 10) > 0 || parseInt(i.cantEntrada, 10) > 0)) && !allItemsAreEmptyAndNoPubSelected) {
currentErrors.general = "Debe ingresar cantidades para al menos una publicación con datos significativos.";
}
}
setLocalErrors(currentErrors);
return Object.keys(currentErrors).length === 0;
@@ -200,7 +242,6 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
clearErrorMessage();
setModalSpecificApiError(null);
if (!validate()) return;
setLoading(true);
try {
if (isEditing && initialData) {
@@ -211,12 +252,16 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
cantEntrada: entradaNum,
observacion: editObservacion.trim() || undefined,
};
// Aquí se llama al onSubmit que viene de la página padre (GestionarEntradasSalidasCanillaPage)
// para la lógica de actualización.
await onSubmit(dataToSubmitSingle, initialData.idParte);
onClose(); // Cerrar el modal DESPUÉS de un submit de edición exitoso
} else {
// Lógica de creación BULK (se maneja internamente en el modal)
const itemsToSubmit: EntradaSalidaCanillaItemDto[] = items
.filter(item =>
item.idPublicacion !== '' &&
( (parseInt(item.cantSalida, 10) >= 0 && parseInt(item.cantEntrada, 10) >= 0) && (parseInt(item.cantSalida,10) > 0 || parseInt(item.cantEntrada,10) > 0 ) || item.observacion.trim() !== '')
item.idPublicacion && Number(item.idPublicacion) > 0 &&
((parseInt(item.cantSalida, 10) >= 0 && parseInt(item.cantEntrada, 10) >= 0) && (parseInt(item.cantSalida, 10) > 0 || parseInt(item.cantEntrada, 10) > 0) || item.observacion.trim() !== '')
)
.map(item => ({
idPublicacion: Number(item.idPublicacion),
@@ -226,7 +271,7 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
}));
if (itemsToSubmit.length === 0) {
setLocalErrors(prev => ({...prev, general: "No hay movimientos válidos para registrar. Asegúrese de seleccionar una publicación y/o ingresar cantidades."}));
setLocalErrors(prev => ({ ...prev, general: "No hay movimientos válidos para registrar..." }));
setLoading(false);
return;
}
@@ -237,8 +282,9 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
items: itemsToSubmit,
};
await entradaSalidaCanillaService.createBulkEntradasSalidasCanilla(bulkData);
onClose(); // Cerrar el modal DESPUÉS de un submit de creación bulk exitoso
}
onClose();
// onClose(); // Movido dentro de los bloques if/else para asegurar que solo se llama tras éxito
} catch (error: any) {
console.error("Error en submit de EntradaSalidaCanillaFormModal:", error);
if (axios.isAxiosError(error) && error.response) {
@@ -246,7 +292,10 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
} else {
setModalSpecificApiError('Ocurrió un error inesperado.');
}
if (isEditing) throw error;
// NO llamar a onClose() aquí si hubo un error, para que el modal permanezca abierto
// y muestre el modalSpecificApiError.
// Si la edición (que usa el 'onSubmit' del padre) lanza un error, ese error se propagará
// al padre y el padre decidirá si el modal se cierra o no (actualmente no lo cierra).
} finally {
setLoading(false);
}
@@ -254,8 +303,8 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
const handleAddRow = () => {
if (items.length >= publicaciones.length) {
setLocalErrors(prev => ({ ...prev, general: "No se pueden agregar más filas que publicaciones disponibles." }));
return;
setLocalErrors(prev => ({ ...prev, general: "No se pueden agregar más filas que publicaciones disponibles." }));
return;
}
setItems([...items, { id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
setLocalErrors(prev => ({ ...prev, general: null }));
@@ -300,27 +349,27 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
onChange={(e) => { setFecha(e.target.value); handleInputChange('fecha'); }}
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
disabled={loading || isEditing} InputLabelProps={{ shrink: true }}
autoFocus={!isEditing}
autoFocus={!isEditing && !idCanilla} // AutoFocus si es nuevo y no hay canillita seleccionado
/>
{isEditing && initialData && (
<Paper elevation={1} sx={{ p: 1.5, mt: 1 }}>
<Typography variant="body2" gutterBottom color="text.secondary">Editando para Publicación: {publicaciones.find(p=>p.idPublicacion === editIdPublicacion)?.nombre || `ID ${editIdPublicacion}`}</Typography>
<Box sx={{ display: 'flex', gap: 2, mt:0.5}}>
<TextField label="Cant. Salida" type="number" value={editCantSalida}
onChange={(e) => {setEditCantSalida(e.target.value); handleInputChange('editCantSalida');}}
margin="dense" fullWidth error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''}
disabled={loading} inputProps={{ min: 0 }} sx={{flex:1}}
/>
<TextField label="Cant. Entrada" type="number" value={editCantEntrada}
onChange={(e) => {setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada');}}
margin="dense" fullWidth error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''}
disabled={loading} inputProps={{ min: 0 }} sx={{flex:1}}
/>
<Typography variant="body2" gutterBottom color="text.secondary">Editando para Publicación: {publicaciones.find(p => p.idPublicacion === editIdPublicacion)?.nombre || `ID ${editIdPublicacion}`}</Typography>
<Box sx={{ display: 'flex', gap: 2, mt: 0.5 }}>
<TextField label="Cant. Salida" type="number" value={editCantSalida}
onChange={(e) => { setEditCantSalida(e.target.value); handleInputChange('editCantSalida'); }}
margin="dense" fullWidth error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''}
disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1 }}
/>
<TextField label="Cant. Entrada" type="number" value={editCantEntrada}
onChange={(e) => { setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada'); }}
margin="dense" fullWidth error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''}
disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1 }}
/>
</Box>
<TextField label="Observación (General)" value={editObservacion}
onChange={(e) => setEditObservacion(e.target.value)}
margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{mt:1}}
onChange={(e) => setEditObservacion(e.target.value)}
margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{ mt: 1 }}
/>
</Paper>
)}
@@ -328,47 +377,141 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
{!isEditing && (
<Box>
<Typography variant="subtitle2" sx={{ mt: 1.5, mb: 0.5 }}>Movimientos por Publicación:</Typography>
{items.map((itemRow, index) => ( // item renombrado a itemRow
<Paper key={itemRow.id} elevation={1} sx={{ p: 1.5, mb: 1, position: 'relative' }}>
{items.length > 1 && (
<IconButton onClick={() => handleRemoveRow(itemRow.id)} color="error" size="small"
sx={{ position: 'absolute', top: 4, right: 4, zIndex:1 }}
aria-label="Quitar fila"
{/* Indicador de carga para los items */}
{loadingItems && <Box sx={{ display: 'flex', justifyContent: 'center', my: 1 }}><CircularProgress size={20} /></Box>}
{!loadingItems && items.map((itemRow, index) => (
<Paper
key={itemRow.id}
elevation={1}
sx={{
p: 1.5,
mb: 1,
}}
>
{/* Nivel 1: contenedor “padre” sin wrap */}
<Box
sx={{
display: 'flex',
alignItems: 'center', // centra ícono + campos
gap: 1,
// NOTA: aquí NO ponemos flexWrap, por defecto es 'nowrap'
}}
>
{/* Nivel 2: contenedor que agrupa solo los campos y sí puede hacer wrap */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: 'wrap', // los campos sí hacen wrap si no caben
flexGrow: 1, // ocupa todo el espacio disponible antes del ícono
}}
>
<FormControl
sx={{ minWidth: 160, flexBasis: 'calc(40% - 8px)', flexGrow: 1, minHeight: 0 }}
size="small"
error={!!localErrors[`item_${itemRow.id}_idPublicacion`]}
>
<DeleteIcon fontSize="inherit" />
<InputLabel
required={
parseInt(itemRow.cantSalida) > 0 ||
parseInt(itemRow.cantEntrada) > 0 ||
itemRow.observacion.trim() !== ''
}
>
Pub. {index + 1}
</InputLabel>
<Select
value={itemRow.idPublicacion}
label={`Publicación ${index + 1}`}
onChange={(e) =>
handleItemChange(itemRow.id, 'idPublicacion', e.target.value as number)
}
disabled={loading || loadingDropdowns}
sx={{ minWidth: 0 }} // permite que shrink si hace falta
>
<MenuItem value="" disabled>
<em>Seleccione</em>
</MenuItem>
{publicaciones.map((p) => (
<MenuItem key={p.idPublicacion} value={p.idPublicacion}>
{p.nombre}
</MenuItem>
))}
</Select>
{localErrors[`item_${itemRow.id}_idPublicacion`] && (
<FormHelperText>
{localErrors[`item_${itemRow.id}_idPublicacion`]}
</FormHelperText>
)}
</FormControl>
<TextField
label="Llevados"
type="number"
size="small"
value={itemRow.cantSalida}
onChange={(e) => handleItemChange(itemRow.id, 'cantSalida', e.target.value)}
error={!!localErrors[`item_${itemRow.id}_cantSalida`]}
helperText={localErrors[`item_${itemRow.id}_cantSalida`]}
inputProps={{ min: 0 }}
sx={{
flexBasis: 'calc(15% - 8px)',
minWidth: '80px',
minHeight: 0,
}}
/>
<TextField
label="Devueltos"
type="number"
size="small"
value={itemRow.cantEntrada}
onChange={(e) => handleItemChange(itemRow.id, 'cantEntrada', e.target.value)}
error={!!localErrors[`item_${itemRow.id}_cantEntrada`]}
helperText={localErrors[`item_${itemRow.id}_cantEntrada`]}
inputProps={{ min: 0 }}
sx={{
flexBasis: 'calc(15% - 8px)',
minWidth: '80px',
minHeight: 0,
}}
/>
<TextField
label="Obs."
value={itemRow.observacion}
onChange={(e) => handleItemChange(itemRow.id, 'observacion', e.target.value)}
size="small"
sx={{
flexGrow: 1,
flexBasis: 'calc(25% - 8px)',
minWidth: '120px',
minHeight: 0,
}}
multiline
maxRows={1}
/>
</Box>
{/* Ícono de eliminar: siempre en la misma línea */}
{items.length > 1 && (
<IconButton
onClick={() => handleRemoveRow(itemRow.id)}
color="error"
aria-label="Quitar fila"
sx={{
alignSelf: 'center', // mantén centrado verticalmente
// No necesita flexShrink, porque el padre no hace wrap
}}
>
<DeleteIcon fontSize="medium" />
</IconButton>
)}
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, flexWrap: 'wrap' }}>
<FormControl sx={{ minWidth: 160, flexBasis: 'calc(40% - 8px)', flexGrow:1 }} size="small" error={!!localErrors[`item_${itemRow.id}_idPublicacion`]}>
<InputLabel required={parseInt(itemRow.cantSalida) > 0 || parseInt(itemRow.cantEntrada) > 0 || itemRow.observacion.trim() !== ''}>Pub. {index + 1}</InputLabel>
<Select value={itemRow.idPublicacion} label={`Publicación ${index + 1}`}
onChange={(e) => handleItemChange(itemRow.id, 'idPublicacion', e.target.value as number)}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{publicaciones.map((p) => (
<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>
))}
</Select>
{localErrors[`item_${itemRow.id}_idPublicacion`] && <FormHelperText>{localErrors[`item_${itemRow.id}_idPublicacion`]}</FormHelperText>}
</FormControl>
<TextField label="Llevados" type="number" size="small" value={itemRow.cantSalida}
onChange={(e) => handleItemChange(itemRow.id, 'cantSalida', e.target.value)}
error={!!localErrors[`item_${itemRow.id}_cantSalida`]} helperText={localErrors[`item_${itemRow.id}_cantSalida`]}
inputProps={{ min: 0 }} sx={{ width: 'auto', flexBasis: 'calc(15% - 8px)', minWidth: '80px' }}
/>
<TextField label="Devueltos" type="number" size="small" value={itemRow.cantEntrada}
onChange={(e) => handleItemChange(itemRow.id, 'cantEntrada', e.target.value)}
error={!!localErrors[`item_${itemRow.id}_cantEntrada`]} helperText={localErrors[`item_${itemRow.id}_cantEntrada`]}
inputProps={{ min: 0 }} sx={{ width: 'auto', flexBasis: 'calc(15% - 8px)', minWidth: '80px' }}
/>
<TextField label="Obs." value={itemRow.observacion}
onChange={(e) => handleItemChange(itemRow.id, 'observacion', e.target.value)}
size="small" sx={{ flexGrow: 1, flexBasis: 'calc(25% - 8px)', minWidth: '120px' }} multiline maxRows={1}
/>
)}
</Box>
</Paper>
))}
{localErrors.general && <Alert severity="error" sx={{ mt: 1 }}>{localErrors.general}</Alert>}
<Button onClick={handleAddRow} startIcon={<AddIcon />} sx={{ mt: 1, alignSelf: 'flex-start' }} disabled={loading || loadingDropdowns || items.length >= publicaciones.length}>
Agregar Publicación

View File

@@ -0,0 +1,141 @@
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, Button, CircularProgress, Alert, FormGroup, FormControlLabel, Checkbox, Paper
} from '@mui/material';
import publicacionService from '../../../services/Distribucion/publicacionService';
import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto';
//import type { PublicacionDiaSemanaDto } from '../../../models/dtos/Distribucion/PublicacionDiaSemanaDto';
import type { UpdatePublicacionDiasSemanaRequestDto } from '../../../models/dtos/Distribucion/UpdatePublicacionDiasSemanaRequestDto';
import axios from 'axios';
const modalStyle = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: '70%', md: '500px' },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 3,
};
const diasSemanaNombres = ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"];
interface PublicacionDiasSemanaModalProps {
open: boolean;
onClose: () => void;
publicacion: PublicacionDto | null;
onConfigSaved: () => void; // Para recargar la lista de publicaciones si es necesario
}
const PublicacionDiasSemanaModal: React.FC<PublicacionDiasSemanaModalProps> = ({
open,
onClose,
publicacion,
onConfigSaved
}) => {
const [selectedDays, setSelectedDays] = useState<Set<number>>(new Set());
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (open && publicacion) {
setLoading(true);
setError(null);
publicacionService.getConfiguracionDiasPublicacion(publicacion.idPublicacion)
.then(configs => {
const activeDays = new Set(configs.filter(c => c.activo).map(c => c.diaSemana));
setSelectedDays(activeDays);
})
.catch(err => {
console.error("Error al cargar configuración de días:", err);
setError("Error al cargar la configuración actual de días.");
})
.finally(() => setLoading(false));
} else {
setSelectedDays(new Set()); // Resetear al cerrar o si no hay publicación
}
}, [open, publicacion]);
const handleCheckboxChange = (dayIndex: number) => {
setSelectedDays(prev => {
const newSelection = new Set(prev);
if (newSelection.has(dayIndex)) {
newSelection.delete(dayIndex);
} else {
newSelection.add(dayIndex);
}
return newSelection;
});
setError(null); // Limpiar error al cambiar
};
const handleSubmit = async () => {
if (!publicacion) return;
setLoading(true);
setError(null);
const requestDto: UpdatePublicacionDiasSemanaRequestDto = {
diasActivos: Array.from(selectedDays)
};
try {
await publicacionService.updateConfiguracionDiasPublicacion(publicacion.idPublicacion, requestDto);
onConfigSaved(); // Notificar al padre que se guardó
onClose();
} catch (err: any) {
console.error("Error al guardar configuración de días:", err);
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Error al guardar la configuración.';
setError(message);
} finally {
setLoading(false);
}
};
if (!publicacion) return null;
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" gutterBottom>
Configurar Días de Salida para: {publicacion.nombre}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{mb:2}}>
Marque los días en que esta publicación debe aparecer por defecto en los movimientos de canillitas.
</Typography>
{loading && <Box sx={{display:'flex', justifyContent:'center', my:2}}><CircularProgress /></Box>}
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
{!loading && (
<Paper variant="outlined" sx={{p:2}}>
<FormGroup>
{diasSemanaNombres.map((nombreDia, index) => (
<FormControlLabel
key={index}
control={
<Checkbox
checked={selectedDays.has(index)}
onChange={() => handleCheckboxChange(index)}
/>
}
label={nombreDia}
/>
))}
</FormGroup>
</Paper>
)}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button onClick={handleSubmit} variant="contained" disabled={loading}>
{loading ? <CircularProgress size={24} /> : 'Guardar Configuración'}
</Button>
</Box>
</Box>
</Modal>
);
};
export default PublicacionDiasSemanaModal;

View File

@@ -9,6 +9,7 @@ export interface UserContextData {
nombreCompleto: string;
esSuperAdmin: boolean;
debeCambiarClave: boolean;
perfil: string;
idPerfil: number;
permissions: string[]; // Guardamos los codAcc
}
@@ -20,6 +21,7 @@ interface DecodedJwtPayload {
given_name?: string; // Nombre (estándar, pero verifica tu token)
family_name?: string; // Apellido (estándar, pero verifica tu token)
role: string | string[]; // Puede ser uno o varios roles
perfil: string;
idPerfil: string; // (viene como string)
debeCambiarClave: string; // (viene como string "True" o "False")
permission?: string | string[]; // Nuestros claims de permiso (codAcc)
@@ -74,6 +76,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
debeCambiarClave: decodedToken.debeCambiarClave?.toLowerCase() === 'true',
idPerfil: decodedToken.idPerfil ? parseInt(decodedToken.idPerfil, 10) : 0,
permissions: permissions,
perfil: decodedToken.perfil || 'Usuario' // Asignar un valor por defecto si no existe
};
setToken(jwtToken);

View File

@@ -1,8 +1,14 @@
import React, { type ReactNode, useState, useEffect } from 'react';
import { Box, AppBar, Toolbar, Typography, Button, Tabs, Tab, Paper } from '@mui/material';
import {
Box, AppBar, Toolbar, Typography, Tabs, Tab, Paper,
IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider // Nuevas importaciones
} from '@mui/material';
import AccountCircle from '@mui/icons-material/AccountCircle'; // Icono de usuario
import LockResetIcon from '@mui/icons-material/LockReset'; // Icono para cambiar contraseña
import LogoutIcon from '@mui/icons-material/Logout'; // Icono para cerrar sesión
import { useAuth } from '../contexts/AuthContext';
import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal';
import { useNavigate, useLocation } from 'react-router-dom'; // Para manejar la navegación y la ruta actual
import { useNavigate, useLocation } from 'react-router-dom';
interface MainLayoutProps {
children: ReactNode;
@@ -18,12 +24,10 @@ const modules = [
{ label: 'Usuarios', path: '/usuarios' },
];
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const {
user,
logout,
// ... (resto de las props de useAuth) ...
isAuthenticated,
isPasswordChangeForced,
showForcedPasswordChangeModal,
@@ -32,9 +36,10 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
} = useAuth();
const navigate = useNavigate();
const location = useLocation(); // Para obtener la ruta actual
const location = useLocation();
const [selectedTab, setSelectedTab] = useState<number | false>(false);
const [anchorElUserMenu, setAnchorElUserMenu] = useState<null | HTMLElement>(null); // Estado para el menú de usuario
useEffect(() => {
const currentModulePath = modules.findIndex(module =>
@@ -43,12 +48,30 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
if (currentModulePath !== -1) {
setSelectedTab(currentModulePath);
} else if (location.pathname === '/') {
setSelectedTab(0);
setSelectedTab(0); // Asegurar que la pestaña de Inicio se seleccione para la ruta raíz
} else {
setSelectedTab(false);
setSelectedTab(false); // Ninguna pestaña seleccionada si no coincide
}
}, [location.pathname]);
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUserMenu(event.currentTarget);
};
const handleCloseUserMenu = () => {
setAnchorElUserMenu(null);
};
const handleChangePasswordClick = () => {
setShowForcedPasswordChangeModal(true);
handleCloseUserMenu();
};
const handleLogoutClick = () => {
logout();
handleCloseUserMenu(); // Cierra el menú antes de desloguear completamente
};
const handleModalClose = (passwordChangedSuccessfully: boolean) => {
if (passwordChangedSuccessfully) {
passwordChangeCompleted();
@@ -70,7 +93,6 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const isReportesModule = location.pathname.startsWith('/reportes');
if (showForcedPasswordChangeModal && isPasswordChangeForced) {
// ... (lógica del modal forzado sin cambios) ...
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<ChangePasswordModal
@@ -84,33 +106,94 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<AppBar position="static">
{/* ... (Toolbar y Tabs sin cambios) ... */}
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
<AppBar position="sticky" elevation={1} /* Elevation sutil para AppBar */>
<Toolbar sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="h6" component="div" noWrap sx={{ cursor: 'pointer' }} onClick={() => navigate('/')}>
Sistema de Gestión - El Día
</Typography>
{user && <Typography sx={{ mr: 2 }}>Hola, {user.nombreCompleto}</Typography>}
{isAuthenticated && !isPasswordChangeForced && (
<Button
color="inherit"
onClick={() => setShowForcedPasswordChangeModal(true)}
>
Cambiar Contraseña
</Button>
)}
<Button color="inherit" onClick={logout}>Cerrar Sesión</Button>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{user && (
<Typography sx={{ mr: 2, display: { xs: 'none', sm: 'block' } }} /* Ocultar en pantallas muy pequeñas */>
Hola, {user.nombreCompleto}
</Typography>
)}
{isAuthenticated && (
<>
<IconButton
size="large"
aria-label="Cuenta del usuario"
aria-controls="menu-appbar"
aria-haspopup="true"
sx={{
padding: '15px',
}}
onClick={handleOpenUserMenu}
color="inherit"
>
<AccountCircle sx={{ fontSize: 36 }} />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorElUserMenu}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
keepMounted
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
open={Boolean(anchorElUserMenu)}
onClose={handleCloseUserMenu}
sx={{ '& .MuiPaper-root': { minWidth: 220, marginTop: '8px' } }}
>
{user && ( // Mostrar info del usuario en el menú
<Box sx={{ px: 2, py: 1.5, pointerEvents: 'none' /* Para que no sea clickeable */ }}>
<Typography variant="subtitle1" sx={{ fontWeight: 'medium' }}>{user.nombreCompleto}</Typography>
<Typography variant="body2" color="text.secondary">{user.username}</Typography>
</Box>
)}
{user && <Divider sx={{ mb: 1 }} />}
{!isPasswordChangeForced && ( // No mostrar si ya está forzado a cambiarla
<MenuItem onClick={handleChangePasswordClick}>
<ListItemIcon><LockResetIcon fontSize="small" /></ListItemIcon>
<ListItemText>Cambiar Contraseña</ListItemText>
</MenuItem>
)}
<MenuItem onClick={handleLogoutClick}>
<ListItemIcon><LogoutIcon fontSize="small" /></ListItemIcon>
<ListItemText>Cerrar Sesión</ListItemText>
</MenuItem>
</Menu>
</>
)}
</Box>
</Toolbar>
<Paper square elevation={0} >
<Tabs
value={selectedTab}
onChange={handleTabChange}
indicatorColor="secondary"
textColor="inherit"
indicatorColor="secondary" // O 'primary' si prefieres el mismo color que el fondo
textColor="inherit" // El texto de la pestaña hereda el color (blanco sobre fondo oscuro)
variant="scrollable"
scrollButtons="auto"
allowScrollButtonsMobile
aria-label="módulos principales"
sx={{ backgroundColor: 'primary.main', color: 'white' }}
sx={{
backgroundColor: 'primary.main', // Color de fondo de las pestañas
color: 'white', // Color del texto de las pestañas
'& .MuiTabs-indicator': {
height: 3, // Un indicador un poco más grueso
},
'& .MuiTab-root': { // Estilo para cada pestaña
minWidth: 100, // Ancho mínimo para cada pestaña
textTransform: 'none', // Evitar MAYÚSCULAS por defecto
fontWeight: 'normal',
opacity: 0.85, // Ligeramente transparentes si no están seleccionadas
'&.Mui-selected': {
fontWeight: 'bold',
opacity: 1,
// color: 'secondary.main' // Opcional: color diferente para la pestaña seleccionada
},
}
}}
>
{modules.map((module) => (
<Tab key={module.path} label={module.label} />
@@ -119,30 +202,30 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
</Paper>
</AppBar>
{/* Contenido del Módulo */}
<Box
component="main"
sx={{
flexGrow: 1,
py: isReportesModule ? 0 : 3, // Padding vertical condicional. Si es el módulo de Reportes, px es 0 si no 3
px: isReportesModule ? 0 : 3, // Padding horizontal condicional. Si es el módulo de Reportes, px es 0 si no 3
display: 'flex', // IMPORTANTE: Para que el hijo (ReportesIndexPage) pueda usar height: '100%'
flexDirection: 'column' // IMPORTANTE
py: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 }, // Padding vertical responsivo
px: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 }, // Padding horizontal responsivo
display: 'flex',
flexDirection: 'column'
}}
>
{children}
</Box>
<Box component="footer" sx={{ p: 1, mt: 'auto', backgroundColor: 'primary.dark', color: 'white', textAlign: 'center' }}>
<Typography variant="body2">
Usuario: {user?.username} | Acceso: {user?.esSuperAdmin ? 'Super Admin' : `Perfil ID ${user?.userId}`} | Versión: {/* TODO: Obtener versión */}
<Box component="footer" sx={{ p: 1, backgroundColor: 'grey.200' /* Un gris más claro */, color: 'text.secondary', textAlign: 'left', borderTop: (theme) => `1px solid ${theme.palette.divider}` }}>
<Typography variant="caption">
{/* Puedes usar caption para un texto más pequeño en el footer */}
Usuario: {user?.username} | Acceso: {user?.esSuperAdmin ? 'Super Administrador' : (user?.perfil || `ID ${user?.idPerfil}`)}
</Typography>
</Box>
<ChangePasswordModal
open={showForcedPasswordChangeModal}
onClose={handleModalClose}
isFirstLogin={isPasswordChangeForced}
open={showForcedPasswordChangeModal && !isPasswordChangeForced} // Solo mostrar si no es el forzado inicial
onClose={() => handleModalClose(false)} // Asumir que si se cierra sin cambiar, no fue exitoso
isFirstLogin={false} // Este modal no es para el primer login forzado
/>
</Box>
);

View File

@@ -0,0 +1,6 @@
export interface PublicacionDiaSemanaDto {
idPublicacionDia: number;
idPublicacion: number;
diaSemana: number; // 0 (Domingo) a 6 (Sábado)
activo: boolean;
}

View File

@@ -0,0 +1,3 @@
export interface UpdatePublicacionDiasSemanaRequestDto {
diasActivos: number[]; // Array de números de día (0-6)
}

View File

@@ -0,0 +1,8 @@
export interface LiquidacionCanillaDetalleDto {
publicacion: string;
canilla: string; // Nombre del canilla
totalCantSalida: number;
totalCantEntrada: number;
totalRendir: number;
precioEjemplar: number;
}

View File

@@ -0,0 +1,4 @@
export interface LiquidacionCanillaGananciaDto {
publicacion: string;
totalRendir: number; // Este es el monto de la comisión/ganancia
}

View File

@@ -7,7 +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: 'Tipos de Pago', path: 'tipos-pago' },
{ label: 'Tipos de Pago', path: 'tipos-pago' },
];
const ContablesIndexPage: React.FC = () => {
@@ -16,26 +16,40 @@ const ContablesIndexPage: React.FC = () => {
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
useEffect(() => {
const currentBasePath = '/contables';
const subPath = location.pathname.startsWith(currentBasePath + '/')
? location.pathname.substring(currentBasePath.length + 1)
: (location.pathname === currentBasePath ? contablesSubModules[0]?.path : undefined);
const currentBasePath = '/contables';
const defaultSubPath = 'pagos-distribuidores'; // Define tu sub-ruta por defecto aquí
const activeTabIndex = contablesSubModules.findIndex(
(subModule) => subModule.path === subPath
);
const pathParts = location.pathname.split('/');
const currentSubPathSegment = pathParts[2]; // /contables -> pathParts[1] es 'contables', pathParts[2] sería la sub-ruta
if (activeTabIndex !== -1) {
setSelectedSubTab(activeTabIndex);
} else {
if (location.pathname === currentBasePath && contablesSubModules.length > 0) {
navigate(contablesSubModules[0].path, { replace: true });
setSelectedSubTab(0);
} else {
setSelectedSubTab(false);
}
}
}, [location.pathname, navigate]);
let activeTabIndex = -1;
if (currentSubPathSegment) {
activeTabIndex = contablesSubModules.findIndex(
(subModule) => subModule.path === currentSubPathSegment
);
}
if (activeTabIndex !== -1) {
setSelectedSubTab(activeTabIndex);
} else {
// Si estamos en la ruta base /contables o una subruta no reconocida
if (location.pathname === currentBasePath || (location.pathname.startsWith(currentBasePath) && activeTabIndex === -1) ) {
const defaultTabIndex = contablesSubModules.findIndex(sm => sm.path === defaultSubPath);
if (defaultTabIndex !== -1) {
navigate(`${currentBasePath}/${defaultSubPath}`, { replace: true });
setSelectedSubTab(defaultTabIndex);
} else if (contablesSubModules.length > 0) { // Fallback al primero si el default no existe
navigate(`${currentBasePath}/${contablesSubModules[0].path}`, { replace: true });
setSelectedSubTab(0);
} else {
setSelectedSubTab(false); // No hay sub-módulos
}
} else {
setSelectedSubTab(false); // No es una ruta del módulo contable
}
}
}, [location.pathname, navigate]);
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setSelectedSubTab(newValue);

View File

@@ -154,8 +154,8 @@ const GestionarNotasCDPage: React.FC = () => {
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Notas de Crédito/Débito</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Notas de Crédito/Débito</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}>

View File

@@ -133,8 +133,8 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Pagos de Distribuidores</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Pagos de Distribuidores</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}>

View File

@@ -3,10 +3,14 @@ import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert
CircularProgress, Alert,
ListItemIcon,
ListItemText
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
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 tipoPagoService from '../../services/Contables/tipoPagoService';
import type { TipoPago } from '../../models/Entities/TipoPago';
import type { CreateTipoPagoDto } from '../../models/dtos/Contables/CreateTipoPagoDto';
@@ -129,8 +133,8 @@ const GestionarTiposPagoPage: React.FC = () => {
const displayData = tiposPago.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>
Gestionar Tipos de Pago
</Typography>
@@ -148,7 +152,6 @@ const GestionarTiposPagoPage: React.FC = () => {
{/* <Button variant="contained" onClick={cargarTiposPago}>Buscar</Button> */}
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button
variant="contained"
startIcon={<AddIcon />}
@@ -157,7 +160,6 @@ const GestionarTiposPagoPage: React.FC = () => {
>
Agregar Nuevo Tipo
</Button>
</Box>
)}
</Paper>
@@ -217,12 +219,14 @@ const GestionarTiposPagoPage: React.FC = () => {
>
{puedeModificar && (
<MenuItem onClick={() => { handleOpenModal(selectedTipoPagoRow!); handleMenuClose(); }}>
Modificar
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
<ListItemText>Modificar</ListItemText>
</MenuItem>
)}
{puedeEliminar && (
<MenuItem onClick={() => handleDelete(selectedTipoPagoRow!.idTipoPago)}>
Eliminar
<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 */}

View File

@@ -8,6 +8,7 @@ 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 canillaService from '../../services/Distribucion/canillaService';
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto';
@@ -121,8 +122,8 @@ const GestionarCanillitasPage: React.FC = () => {
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Canillitas</Typography>
<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
@@ -156,9 +157,7 @@ const GestionarCanillitasPage: React.FC = () => {
{/* <Button variant="contained" onClick={cargarCanillitas} size="small">Buscar</Button> */}
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Canillita</Button>
</Box>
)}
</Paper>
@@ -203,7 +202,7 @@ const GestionarCanillitasPage: React.FC = () => {
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
{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}}/>}

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
@@ -11,7 +11,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
import FilterListIcon from '@mui/icons-material/FilterList';
import controlDevolucionesService from '../../services/Distribucion/controlDevolucionesService';
import empresaService from '../../services/Distribucion/empresaService'; // Para el filtro de empresa
import empresaService from '../../services/Distribucion/empresaService';
import type { ControlDevolucionesDto } from '../../models/dtos/Distribucion/ControlDevolucionesDto';
import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto';
@@ -28,9 +28,8 @@ const GestionarControlDevolucionesPage: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
@@ -45,22 +44,35 @@ const GestionarControlDevolucionesPage: React.FC = () => {
const [selectedRow, setSelectedRow] = useState<ControlDevolucionesDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
// Permisos CD001 (Ver), CD002 (Crear), CD003 (Modificar)
const puedeVer = isSuperAdmin || tienePermiso("CD001");
const puedeCrear = isSuperAdmin || tienePermiso("CD002");
const puedeModificar = isSuperAdmin || tienePermiso("CD003");
const puedeEliminar = isSuperAdmin || tienePermiso("CD003"); // Asumiendo que modificar incluye eliminar
const puedeEliminar = isSuperAdmin || tienePermiso("CD003");
// 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 empresasData = await empresaService.getAllEmpresas();
setEmpresas(empresasData);
const empresasData = await empresaService.getAllEmpresas();
setEmpresas(empresasData);
} catch (err) {
console.error("Error cargando empresas para filtro:", err);
setError("Error al cargar opciones de filtro.");
console.error("Error cargando empresas para filtro:", err);
setError("Error al cargar opciones de filtro.");
} finally {
setLoadingFiltersDropdown(false);
setLoadingFiltersDropdown(false);
}
}, []);
@@ -110,13 +122,13 @@ const GestionarControlDevolucionesPage: React.FC = () => {
const handleDelete = async (idControl: number) => {
if (window.confirm(`¿Seguro de eliminar este control de devoluciones (ID: ${idControl})?`)) {
setApiErrorMessage(null);
try {
setApiErrorMessage(null);
try {
await controlDevolucionesService.deleteControlDevoluciones(idControl);
cargarControles();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
setApiErrorMessage(message);
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
setApiErrorMessage(message);
}
}
handleMenuClose();
@@ -131,81 +143,81 @@ const GestionarControlDevolucionesPage: React.FC = () => {
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
setRowsPerPage(parseInt(event.target.value, 25)); setPage(0);
};
// displayData ahora usará la 'controles' directamente, el formato se aplica en el renderizado
const displayData = controles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Control de Devoluciones a Empresa</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Control de Devoluciones a Empresa</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}>
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/>
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/>
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}>
<InputLabel>Empresa</InputLabel>
<Select value={filtroIdEmpresa} label="Empresa" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}>
<MenuItem value=""><em>Todas</em></MenuItem>
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
</Select>
</FormControl>
</Box>
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Control</Button>)}
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
<InputLabel>Empresa</InputLabel>
<Select value={filtroIdEmpresa} label="Empresa" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}>
<MenuItem value=""><em>Todas</em></MenuItem>
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
</Select>
</FormControl>
</Box>
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Control</Button>)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{!loading && !error && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Empresa</TableCell>
<TableCell align="right">Entrada (Total Dev.)</TableCell>
<TableCell align="right">Sobrantes</TableCell>
<TableCell align="right">Sin Cargo</TableCell>
<TableCell>Detalle</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles.</TableCell></TableRow>
) : (
displayData.map((c) => (
<TableRow key={c.idControl} hover>
<TableCell>{formatDate(c.fecha)}</TableCell>
<TableCell>{c.nombreEmpresa}</TableCell>
<TableCell align="right">{c.entrada}</TableCell>
<TableCell align="right">{c.sobrantes}</TableCell>
<TableCell align="right">{c.sinCargo}</TableCell>
<TableCell><Tooltip title={c.detalle || ''}><Box sx={{maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{c.detalle || '-'}</Box></Tooltip></TableCell>
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[10, 25, 50]} component="div" count={controles.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> {/* Ajusta maxHeight según sea necesario */}
<Table stickyHeader size="small">
<TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Empresa</TableCell>
<TableCell align="right">Entrada (Por Remito)</TableCell>
<TableCell align="right">Sobrantes</TableCell>
<TableCell align="right">Ejemplares Sin Cargo</TableCell>
<TableCell>Detalle</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles.</TableCell></TableRow>
) : (
displayData.map((c) => (
<TableRow key={c.idControl} hover>
<TableCell>{formatDate(c.fecha)}</TableCell>
<TableCell>{c.nombreEmpresa}</TableCell>
<TableCell align="right">{c.entrada}</TableCell>
<TableCell align="right">{c.sobrantes}</TableCell>
<TableCell align="right">{c.sinCargo}</TableCell>
<TableCell><Tooltip title={c.detalle || ''}><Box sx={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.detalle || '-'}</Box></Tooltip></TableCell>
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[10, 25, 50]} component="div" count={controles.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && selectedRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
{puedeEliminar && selectedRow && (
<MenuItem onClick={() => handleDelete(selectedRow.idControl)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
<MenuItem onClick={() => handleDelete(selectedRow.idControl)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)}
</Menu>
<ControlDevolucionesFormModal

View File

@@ -6,6 +6,8 @@ import {
CircularProgress, Alert
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import TrashIcon from '@mui/icons-material/Delete';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import distribuidorService from '../../services/Distribucion/distribuidorService';
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
@@ -110,8 +112,8 @@ const GestionarDistribuidoresPage: React.FC = () => {
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Distribuidores</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Gestionar Distribuidores</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
<TextField
@@ -133,9 +135,7 @@ const GestionarDistribuidoresPage: React.FC = () => {
{/* <Button variant="contained" onClick={cargarDistribuidores} size="small">Buscar</Button> */}
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Distribuidor</Button>
</Box>
)}
</Paper>
@@ -179,8 +179,8 @@ const GestionarDistribuidoresPage: React.FC = () => {
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedDistribuidorRow!.idDistribuidor)}>Eliminar</MenuItem>)}
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} />Modificar</MenuItem>)}
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedDistribuidorRow!.idDistribuidor)}><TrashIcon fontSize="small" sx={{ mr: 1 }} />Eliminar</MenuItem>)}
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
</Menu>

View File

@@ -1,11 +1,15 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert,
ListItemIcon,
ListItemText
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
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 empresaService from '../../services/Distribucion/empresaService'; // Importar el servicio de Empresas
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
import type { CreateEmpresaDto } from '../../models/dtos/Distribucion/CreateEmpresaDto';
@@ -42,9 +46,9 @@ const GestionarEmpresasPage: React.FC = () => {
const cargarEmpresas = useCallback(async () => {
if (!puedeVer) { // Si no tiene permiso de ver, no cargar nada
setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
}
setLoading(true);
setError(null);
@@ -90,8 +94,8 @@ const GestionarEmpresasPage: React.FC = () => {
} catch (err: any) {
console.error("Error en submit modal (padre):", err);
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Ocurrió un error inesperado al guardar la empresa.';
? err.response.data.message
: 'Ocurrió un error inesperado al guardar la empresa.';
setApiErrorMessage(message);
throw err; // Re-lanzar para que el modal sepa que hubo error y no se cierre
}
@@ -101,16 +105,16 @@ const GestionarEmpresasPage: React.FC = () => {
const handleDelete = async (id: number) => {
// Opcional: mostrar un mensaje de confirmación más detallado
if (window.confirm(`¿Está seguro de que desea eliminar esta empresa (ID: ${id})? Esta acción también eliminará los saldos asociados.`)) {
setApiErrorMessage(null); // Limpiar errores previos
try {
setApiErrorMessage(null); // Limpiar errores previos
try {
await empresaService.deleteEmpresa(id);
cargarEmpresas(); // Recargar la lista para reflejar la eliminación
} catch (err: any) {
console.error("Error al eliminar empresa:", err);
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Ocurrió un error inesperado al eliminar la empresa.';
setApiErrorMessage(message); // Mostrar error de API
console.error("Error al eliminar empresa:", err);
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Ocurrió un error inesperado al eliminar la empresa.';
setApiErrorMessage(message); // Mostrar error de API
}
}
handleMenuClose(); // Cerrar el menú de acciones
@@ -127,115 +131,113 @@ const GestionarEmpresasPage: React.FC = () => {
setSelectedEmpresaRow(null);
};
const handleChangePage = (_event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangePage = (_event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
// Datos a mostrar en la tabla actual según paginación
const displayData = empresas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
// Datos a mostrar en la tabla actual según paginación
const displayData = empresas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
// 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>
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
</Box>
);
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Empresas</Typography>
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
</Box>
);
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>
Gestionar Empresas
</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<TextField
label="Filtrar por Nombre"
variant="outlined"
size="small"
value={filtroNombre}
onChange={(e) => setFiltroNombre(e.target.value)}
/>
</Box>
{/* Mostrar botón de agregar solo si tiene permiso */}
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => handleOpenModal()}
>
Agregar Nueva Empresa
</Button>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<TextField
label="Filtrar por Nombre"
variant="outlined"
size="small"
value={filtroNombre}
onChange={(e) => setFiltroNombre(e.target.value)}
/>
</Box>
{/* Mostrar botón de agregar solo si tiene permiso */}
{puedeCrear && (
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => handleOpenModal()}
>
Agregar Nueva Empresa
</Button>
)}
</Paper>
{/* Indicador de carga */}
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{/* Mensaje de error al cargar datos */}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{/* Mensaje de error de la API (modal/delete) */}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{/* Tabla de datos (solo si no está cargando y no hubo error de carga inicial) */}
{!loading && !error && (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Nombre</TableCell>
<TableCell>Detalle</TableCell>
{/* Mostrar columna de acciones solo si tiene permiso de modificar o eliminar */}
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{displayData.length === 0 && !loading ? (
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron empresas.</TableCell></TableRow>
) : (
displayData.map((emp) => (
<TableRow key={emp.idEmpresa}>
<TableCell>{emp.nombre}</TableCell>
<TableCell>{emp.detalle || '-'}</TableCell>
{/* Mostrar botón de acciones solo si tiene permiso */}
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton
onClick={(e) => handleMenuOpen(e, emp)}
// Deshabilitar si no tiene ningún permiso específico (redundante por la condición de la celda, pero seguro)
disabled={!puedeModificar && !puedeEliminar}
>
<MoreVertIcon />
</IconButton>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
{/* Paginación */}
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={empresas.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Nombre</TableCell>
<TableCell>Detalle</TableCell>
{/* Mostrar columna de acciones solo si tiene permiso de modificar o eliminar */}
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{displayData.length === 0 && !loading ? (
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron empresas.</TableCell></TableRow>
) : (
displayData.map((emp) => (
<TableRow key={emp.idEmpresa}>
<TableCell>{emp.nombre}</TableCell>
<TableCell>{emp.detalle || '-'}</TableCell>
{/* Mostrar botón de acciones solo si tiene permiso */}
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton
onClick={(e) => handleMenuOpen(e, emp)}
// Deshabilitar si no tiene ningún permiso específico (redundante por la condición de la celda, pero seguro)
disabled={!puedeModificar && !puedeEliminar}
>
<MoreVertIcon />
</IconButton>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
{/* Paginación */}
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={empresas.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
{/* Menú contextual para acciones de fila */}
<Menu
@@ -245,15 +247,17 @@ const GestionarEmpresasPage: React.FC = () => {
>
{/* Mostrar opción Modificar solo si tiene permiso */}
{puedeModificar && (
<MenuItem onClick={() => { handleOpenModal(selectedEmpresaRow!); handleMenuClose(); }}>
Modificar
</MenuItem>
<MenuItem onClick={() => { handleOpenModal(selectedEmpresaRow!); handleMenuClose(); }}>
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
<ListItemText>Modificar</ListItemText>
</MenuItem>
)}
{/* Mostrar opción Eliminar solo si tiene permiso */}
{puedeEliminar && (
<MenuItem onClick={() => handleDelete(selectedEmpresaRow!.idEmpresa)}>
Eliminar
</MenuItem>
<MenuItem onClick={() => handleDelete(selectedEmpresaRow!.idEmpresa)}>
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
<ListItemText>Eliminar</ListItemText>
</MenuItem>
)}
{/* Mensaje si no hay acciones disponibles (por si acaso) */}
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}

View File

@@ -1,4 +1,3 @@
// src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
@@ -7,27 +6,27 @@ import {
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import PrintIcon from '@mui/icons-material/Print';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import FilterListIcon from '@mui/icons-material/FilterList';
import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck'; // Para Liquidar
import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck';
import entradaSalidaCanillaService from '../../services/Distribucion/entradaSalidaCanillaService';
import publicacionService from '../../services/Distribucion/publicacionService';
import canillaService from '../../services/Distribucion/canillaService';
import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
import type { CreateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaCanillaDto';
import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto';
import EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
import reportesService from '../../services/Reportes/reportesService';
const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const [movimientos, setMovimientos] = useState<EntradaSalidaCanillaDto[]>([]);
@@ -35,13 +34,12 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
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 [filtroIdCanilla, setFiltroIdCanilla] = useState<number | string>('');
const [filtroEstadoLiquidacion, setFiltroEstadoLiquidacion] = useState<'todos' | 'liquidados' | 'noLiquidados'>('noLiquidados');
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
@@ -58,9 +56,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const [fechaLiquidacionDialog, setFechaLiquidacionDialog] = useState<string>(new Date().toISOString().split('T')[0]);
const [openLiquidarDialog, setOpenLiquidarDialog] = useState(false);
const { tienePermiso, isSuperAdmin } = usePermissions();
// MC001 (Ver), MC002 (Crear), MC003 (Modificar), MC004 (Eliminar), MC005 (Liquidar)
const puedeVer = isSuperAdmin || tienePermiso("MC001");
const puedeCrear = isSuperAdmin || tienePermiso("MC002");
const puedeModificar = isSuperAdmin || tienePermiso("MC003");
@@ -68,6 +64,17 @@ 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]}`;
}
return datePart;
};
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
@@ -120,22 +127,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => { setModalOpen(false); setEditingMovimiento(null); };
const handleSubmitModal = async (data: CreateEntradaSalidaCanillaDto | UpdateEntradaSalidaCanillaDto, idParte?: number) => {
setApiErrorMessage(null);
try {
if (idParte && editingMovimiento) {
await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data as UpdateEntradaSalidaCanillaDto);
} else {
await entradaSalidaCanillaService.createEntradaSalidaCanilla(data as CreateEntradaSalidaCanillaDto);
}
cargarMovimientos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (idParte: number) => {
if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) {
@@ -147,7 +138,10 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => {
setAnchorEl(event.currentTarget); setSelectedRow(item);
// Almacenar el idParte en el propio elemento del menú para referencia
event.currentTarget.setAttribute('data-rowid', item.idParte.toString());
setAnchorEl(event.currentTarget);
setSelectedRow(item);
};
const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); };
@@ -177,40 +171,150 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
};
const handleCloseLiquidarDialog = () => setOpenLiquidarDialog(false);
const handleConfirmLiquidar = async () => {
setApiErrorMessage(null); setLoading(true);
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
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()
fechaMovimientoMasReciente = movFecha;
}
}
});
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando 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
const liquidarDto: LiquidarMovimientosCanillaRequestDto = {
idsPartesALiquidar: Array.from(selectedIdsParaLiquidar),
fechaLiquidacion: fechaLiquidacionDialog
fechaLiquidacion: fechaLiquidacionDialog // El backend espera YYYY-MM-DD
};
try {
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
cargarMovimientos(); // Recargar para ver los cambios
setOpenLiquidarDialog(false);
setOpenLiquidarDialog(false);
const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0];
const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado);
await cargarMovimientos();
if (movimientoParaTicket) {
console.log("Liquidación exitosa, intentando generar ticket para canillita:", movimientoParaTicket.idCanilla);
await handleImprimirTicketLiquidacion(
movimientoParaTicket.idCanilla,
fechaLiquidacionDialog,
movimientoParaTicket.canillaEsAccionista
);
} else {
console.warn("No se pudo encontrar información del movimiento para generar el 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) => {
setApiErrorMessage(null);
try {
await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data);
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar los cambios.';
setApiErrorMessage(message);
throw err;
}
};
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.
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
) => {
setLoadingTicketPdf(true);
setApiErrorMessage(null);
try {
const params = {
fecha: fecha.split('T')[0], // Asegurar formato YYYY-MM-DD
idCanilla: idCanilla,
esAccionista: 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.";
setApiErrorMessage(msg);
} else {
const url = URL.createObjectURL(blob);
const w = window.open(url, '_blank');
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.';
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
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
const numSelectedToLiquidate = selectedIdsParaLiquidar.size;
// 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: 2 }}>
<Typography variant="h4" gutterBottom>Entradas/Salidas Canillitas</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Entradas/Salidas Canillitas</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
@@ -250,36 +354,62 @@ const GestionarEntradasSalidasCanillaPage: 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 && !apiErrorMessage && <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 }}>
<CircularProgress size={20} sx={{ mr: 1 }} />
<Typography variant="body2">Cargando ticket...</Typography>
</Box>
}
{!loading && !error && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' &&
<TableCell padding="checkbox">
<Checkbox
indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage}
checked={numNotLiquidatedOnPage > 0 && numSelectedToLiquidate === numNotLiquidatedOnPage}
onChange={handleSelectAllForLiquidar}
disabled={numNotLiquidatedOnPage === 0}
/>
</TableCell>
}
<TableCell>Fecha</TableCell><TableCell>Publicación</TableCell><TableCell>Canillita</TableCell>
<TableCell align="right">Salida</TableCell><TableCell align="right">Entrada</TableCell>
<TableCell align="right">Vendidos</TableCell><TableCell align="right">A Rendir</TableCell>
<TableCell>Liquidado</TableCell><TableCell>F. Liq.</TableCell><TableCell>Obs.</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableHead>
<TableRow>
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
<TableCell padding="checkbox">
<Checkbox
indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage && numNotLiquidatedOnPage > 0}
checked={numNotLiquidatedOnPage > 0 && numSelectedToLiquidate === numNotLiquidatedOnPage}
onChange={handleSelectAllForLiquidar}
disabled={numNotLiquidatedOnPage === 0}
/>
</TableCell>
)}
<TableCell>Fecha</TableCell>
<TableCell>Publicación</TableCell>
<TableCell>Canillita</TableCell>
<TableCell align="right">Salida</TableCell>
<TableCell align="right">Entrada</TableCell>
<TableCell align="right">Vendidos</TableCell>
<TableCell align="right">A Rendir</TableCell>
<TableCell>Liquidado</TableCell>
<TableCell>F. Liq.</TableCell>
<TableCell>Obs.</TableCell>
{(puedeModificar || puedeEliminar || puedeLiquidar) && <TableCell align="right">Acciones</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' ? 12 : 11} align="center">No se encontraron movimientos.</TableCell></TableRow>
<TableRow>
<TableCell
colSpan={
(puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' ? 1 : 0) +
9 +
((puedeModificar || puedeEliminar || puedeLiquidar) ? 1 : 0)
}
align="center"
>
No se encontraron movimientos.
</TableCell>
</TableRow>
) : (
displayData.map((m) => (
<TableRow key={m.idParte} hover selected={selectedIdsParaLiquidar.has(m.idParte)}>
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' &&
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
<TableCell padding="checkbox">
<Checkbox
checked={selectedIdsParaLiquidar.has(m.idParte)}
@@ -287,29 +417,34 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
disabled={m.liquidado}
/>
</TableCell>
}
)}
<TableCell>{formatDate(m.fecha)}</TableCell>
<TableCell>{m.nombrePublicacion}</TableCell>
<TableCell>{m.nomApeCanilla}</TableCell>
<TableCell align="right">{m.cantSalida}</TableCell>
<TableCell align="right">{m.cantEntrada}</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>{m.vendidos}</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>${m.montoARendir.toFixed(2)}</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
{m.montoARendir.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
</TableCell>
<TableCell align="center">{m.liquidado ? <Chip label="Sí" color="success" size="small" /> : <Chip label="No" size="small" />}</TableCell>
<TableCell>{m.fechaLiquidado ? formatDate(m.fechaLiquidado) : '-'}</TableCell>
<TableCell><Tooltip title={m.observacion || ''}><Box sx={{ maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.observacion || '-'}</Box></Tooltip></TableCell>
{(puedeModificar || puedeEliminar) && (
<TableCell>
<Tooltip title={m.observacion || ''}>
<Box sx={{ maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{m.observacion || '-'}
</Box>
</Tooltip>
</TableCell>
{(puedeModificar || puedeEliminar || puedeLiquidar) && (
<TableCell align="right">
<IconButton
onClick={(e) => handleMenuOpen(e, m)}
disabled={
// Deshabilitar si no tiene ningún permiso de eliminación O
// si está liquidado y no tiene permiso para eliminar liquidados
!((!m.liquidado && puedeEliminar) || (m.liquidado && puedeEliminarLiquidados))
}
>
<MoreVertIcon />
</IconButton>
<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
>
<MoreVertIcon />
</IconButton>
</TableCell>
)}
</TableRow>
@@ -327,18 +462,45 @@ 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>)}
{selectedRow && (
(!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados)
{/* Opción de Imprimir Ticket Liq. */}
{selectedRow && selectedRow.liquidado && ( // Solo mostrar si ya está liquidado (para reimprimir)
<MenuItem
onClick={() => {
if (selectedRow) { // selectedRow no será null aquí debido a la condición anterior
handleImprimirTicketLiquidacion(
selectedRow.idCanilla,
selectedRow.fechaLiquidado || selectedRow.fecha, // Usar fechaLiquidado si existe, sino la fecha del movimiento
selectedRow.canillaEsAccionista
);
}
// handleMenuClose() es llamado por handleImprimirTicketLiquidacion
}}
disabled={loadingTicketPdf}
>
<PrintIcon fontSize="small" sx={{ mr: 1 }} />
{loadingTicketPdf && <CircularProgress size={16} sx={{ mr: 1 }} />}
Reimprimir Ticket Liq.
</MenuItem>
)}
{selectedRow && ( // Opción de Eliminar
((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados))
) && (
<MenuItem onClick={() => handleDelete(selectedRow.idParte)}>
<MenuItem onClick={() => {
if (selectedRow) handleDelete(selectedRow.idParte);
}}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar
</MenuItem>
)}
</Menu>
<EntradaSalidaCanillaFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingMovimiento} errorMessage={apiErrorMessage}
open={modalOpen}
onClose={handleCloseModal}
onSubmit={handleModalEditSubmit}
initialData={editingMovimiento}
errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>

View File

@@ -1,4 +1,3 @@
// src/pages/Distribucion/GestionarEntradasSalidasDistPage.tsx
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
@@ -31,14 +30,12 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
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<PublicacionDto[]>([]);
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
@@ -55,6 +52,19 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
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 {
@@ -126,16 +136,16 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
setRowsPerPage(parseInt(event.target.value, 25)); setPage(0);
};
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
// 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: 2 }}>
<Typography variant="h4" gutterBottom>Entradas/Salidas a Distribuidores</Typography>
<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 }}>
@@ -187,6 +197,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
) : (
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>
@@ -196,7 +207,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
<TableCell align="right">{m.cantidad}</TableCell>
<TableCell>{m.remito}</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', color: m.tipoMovimiento === 'Salida' ? 'success.main' : (m.montoCalculado === 0 ? 'inherit' : 'error.main') }}>
{m.tipoMovimiento === 'Salida' ? '$'+m.montoCalculado.toFixed(2) : '$-'+m.montoCalculado.toFixed(2) }
{(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 && (

View File

@@ -1,11 +1,15 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert,
ListItemIcon,
ListItemText
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
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 otroDestinoService from '../../services/Distribucion/otroDestinoService';
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto';
@@ -40,9 +44,9 @@ const GestionarOtrosDestinosPage: React.FC = () => {
const cargarOtrosDestinos = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
}
setLoading(true);
setError(null);
@@ -84,8 +88,8 @@ const GestionarOtrosDestinosPage: React.FC = () => {
cargarOtrosDestinos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Ocurrió un error inesperado al guardar el destino.';
? err.response.data.message
: 'Ocurrió un error inesperado al guardar el destino.';
setApiErrorMessage(message);
throw err;
}
@@ -93,15 +97,15 @@ const GestionarOtrosDestinosPage: React.FC = () => {
const handleDelete = async (id: number) => {
if (window.confirm(`¿Está seguro de que desea eliminar este destino (ID: ${id})?`)) {
setApiErrorMessage(null);
try {
setApiErrorMessage(null);
try {
await otroDestinoService.deleteOtroDestino(id);
cargarOtrosDestinos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Ocurrió un error inesperado al eliminar el destino.';
setApiErrorMessage(message);
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Ocurrió un error inesperado al eliminar el destino.';
setApiErrorMessage(message);
}
}
handleMenuClose();
@@ -117,98 +121,102 @@ const GestionarOtrosDestinosPage: React.FC = () => {
setSelectedDestinoRow(null);
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const displayData = otrosDestinos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const displayData = otrosDestinos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeVer) {
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
</Box>
);
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
</Box>
);
}
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>
<Paper sx={{ p: 2, mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<TextField
label="Filtrar por Nombre"
variant="outlined"
size="small"
value={filtroNombre}
onChange={(e) => setFiltroNombre(e.target.value)}
/>
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Nuevo Destino
</Button>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<TextField
label="Filtrar por Nombre"
variant="outlined"
size="small"
value={filtroNombre}
onChange={(e) => setFiltroNombre(e.target.value)}
/>
</Box>
{puedeCrear && (
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Nuevo Destino
</Button>
)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{!loading && !error && (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Nombre</TableCell>
<TableCell>Observación</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{displayData.length === 0 && !loading ? (
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron otros destinos.</TableCell></TableRow>
) : (
displayData.map((destino) => (
<TableRow key={destino.idDestino}>
<TableCell>{destino.nombre}</TableCell>
<TableCell>{destino.obs || '-'}</TableCell>
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, destino)} disabled={!puedeModificar && !puedeEliminar}>
<MoreVertIcon />
</IconButton>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={otrosDestinos.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Nombre</TableCell>
<TableCell>Observación</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{displayData.length === 0 && !loading ? (
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron otros destinos.</TableCell></TableRow>
) : (
displayData.map((destino) => (
<TableRow key={destino.idDestino}>
<TableCell>{destino.nombre}</TableCell>
<TableCell>{destino.obs || '-'}</TableCell>
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, destino)} disabled={!puedeModificar && !puedeEliminar}>
<MoreVertIcon />
</IconButton>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={otrosDestinos.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(selectedDestinoRow!); handleMenuClose(); }}>Modificar</MenuItem>
<MenuItem onClick={() => { handleOpenModal(selectedDestinoRow!); handleMenuClose(); }}>
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
<ListItemText>Modificar</ListItemText>
</MenuItem>
)}
{puedeEliminar && (
<MenuItem onClick={() => handleDelete(selectedDestinoRow!.idDestino)}>Eliminar</MenuItem>
<MenuItem onClick={() => handleDelete(selectedDestinoRow!.idDestino)}>
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
<ListItemText>Eliminar</ListItemText>
</MenuItem>
)}
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
</Menu>

View File

@@ -3,15 +3,26 @@ import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, Chip, FormControl, InputLabel, Select, Tooltip,
FormControlLabel
FormControlLabel,
ListItemText,
ListItemIcon
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para el menú de acciones
import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar
import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; // Icono para días de semana
import LocalOfferIcon from '@mui/icons-material/LocalOffer'; // Para Precios
import AddCardIcon from '@mui/icons-material/AddCard'; // Para Recargos
import PercentIcon from '@mui/icons-material/Percent'; // Para Porcentajes
import RequestQuoteIcon from '@mui/icons-material/RequestQuote'; // Para Porc./Monto Canillita
import ViewQuiltIcon from '@mui/icons-material/ViewQuilt'; // Para Secciones
import publicacionService from '../../services/Distribucion/publicacionService';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto';
import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto';
import PublicacionFormModal from '../../components/Modals/Distribucion/PublicacionFormModal';
import PublicacionDiasSemanaModal from '../../components/Modals/Distribucion/PublicacionDiasSemanaModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
@@ -40,9 +51,11 @@ const GestionarPublicacionesPage: React.FC = () => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedPublicacionRow, setSelectedPublicacionRow] = useState<PublicacionDto | null>(null);
const [diasSemanaModalOpen, setDiasSemanaModalOpen] = useState(false);
const [selectedPublicacionParaDias, setSelectedPublicacionParaDias] = useState<PublicacionDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const navigate = useNavigate();
const puedeVer = isSuperAdmin || tienePermiso("DP001");
const puedeCrear = isSuperAdmin || tienePermiso("DP002");
const puedeModificar = isSuperAdmin || tienePermiso("DP003");
@@ -179,6 +192,23 @@ const GestionarPublicacionesPage: React.FC = () => {
handleMenuClose();
};
const handleOpenDiasSemanaModal = (publicacion: PublicacionDto) => {
setSelectedPublicacionParaDias(publicacion);
setDiasSemanaModalOpen(true);
handleMenuClose(); // Cerrar el menú de acciones si estaba abierto
};
const handleCloseDiasSemanaModal = () => {
setDiasSemanaModalOpen(false);
setSelectedPublicacionParaDias(null);
};
const handleConfigDiasSaved = () => {
// Opcional: Recargar publicaciones o simplemente mostrar un mensaje de éxito.
// Por ahora, solo cerramos el modal.
console.log("Configuración de días guardada.");
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -191,8 +221,8 @@ const GestionarPublicacionesPage: React.FC = () => {
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Publicaciones</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Gestionar Publicaciones</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
<TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} />
@@ -261,17 +291,75 @@ const GestionarPublicacionesPage: React.FC = () => {
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedPublicacionRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
{puedeGestionarPrecios && (<MenuItem onClick={() => handleNavigateToPrecios(selectedPublicacionRow!.idPublicacion)}>Gestionar Precios</MenuItem>)}
{puedeGestionarRecargos && (<MenuItem onClick={() => handleNavigateToRecargos(selectedPublicacionRow!.idPublicacion)}>Gestionar Recargos</MenuItem>)}
{puedeGestionarPorcDist && (<MenuItem onClick={() => handleNavigateToPorcentajesPagoDist(selectedPublicacionRow!.idPublicacion)}>Porcentajes Pago (Dist.)</MenuItem>)}
{puedeGestionarPorcCan && (<MenuItem onClick={() => handleNavigateToPorcMonCanilla(selectedPublicacionRow!.idPublicacion)}>Porc./Monto Canillita</MenuItem>)}
{puedeGestionarSecciones && (<MenuItem onClick={() => handleNavigateToSecciones(selectedPublicacionRow!.idPublicacion)}>Gestionar Secciones</MenuItem>)}
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedPublicacionRow!.idPublicacion)}>Eliminar</MenuItem>)}
{/* Si no hay permisos para ninguna acción */}
{(!puedeModificar && !puedeEliminar && !puedeGestionarPrecios && !puedeGestionarRecargos && !puedeGestionarSecciones) &&
<MenuItem disabled>Sin acciones</MenuItem>}
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
PaperProps={{
style: {
minWidth: 250, // Un ancho mínimo para que los textos no se corten tanto
},
}}
>
{puedeModificar && selectedPublicacionRow && (
<MenuItem onClick={() => { handleOpenModal(selectedPublicacionRow); handleMenuClose(); }}>
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
<ListItemText>Modificar</ListItemText>
</MenuItem>
)}
{puedeGestionarPrecios && selectedPublicacionRow && (
<MenuItem onClick={() => { handleNavigateToPrecios(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}>
<ListItemIcon><LocalOfferIcon fontSize="small" /></ListItemIcon>
<ListItemText>Gestionar Precios</ListItemText>
</MenuItem>
)}
{puedeGestionarRecargos && selectedPublicacionRow && (
<MenuItem onClick={() => { handleNavigateToRecargos(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}>
<ListItemIcon><AddCardIcon fontSize="small" /></ListItemIcon>
<ListItemText>Gestionar Recargos</ListItemText>
</MenuItem>
)}
{puedeGestionarPorcDist && selectedPublicacionRow && (
<MenuItem onClick={() => { handleNavigateToPorcentajesPagoDist(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}>
<ListItemIcon><PercentIcon fontSize="small" /></ListItemIcon>
<ListItemText>Porcentajes Pago (Dist.)</ListItemText>
</MenuItem>
)}
{puedeGestionarPorcCan && selectedPublicacionRow && (
<MenuItem onClick={() => { handleNavigateToPorcMonCanilla(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}>
<ListItemIcon><RequestQuoteIcon fontSize="small" /></ListItemIcon>
<ListItemText>Porc./Monto Canillita</ListItemText>
</MenuItem>
)}
{puedeGestionarSecciones && selectedPublicacionRow && (
<MenuItem onClick={() => { handleNavigateToSecciones(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}>
<ListItemIcon><ViewQuiltIcon fontSize="small" /></ListItemIcon>
<ListItemText>Gestionar Secciones</ListItemText>
</MenuItem>
)}
{puedeModificar && selectedPublicacionRow && (
<MenuItem onClick={() => { handleOpenDiasSemanaModal(selectedPublicacionRow as PublicacionDto); handleMenuClose(); }}>
<ListItemIcon><CalendarMonthIcon fontSize="small" /></ListItemIcon>
<ListItemText>Días de Salida</ListItemText>
</MenuItem>
)}
{puedeEliminar && selectedPublicacionRow && (
<MenuItem onClick={() => { handleDelete(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}>
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
<ListItemText>Eliminar</ListItemText>
</MenuItem>
)}
{/* Si no hay permisos para ninguna acción y hay una fila seleccionada */}
{selectedPublicacionRow &&
!puedeModificar && !puedeEliminar &&
!puedeGestionarPrecios && !puedeGestionarRecargos &&
!puedeGestionarPorcDist && !puedeGestionarPorcCan &&
!puedeGestionarSecciones && (
<MenuItem disabled>
<ListItemText>Sin acciones disponibles</ListItemText>
</MenuItem>
)}
</Menu>
<PublicacionFormModal
@@ -279,6 +367,12 @@ const GestionarPublicacionesPage: React.FC = () => {
initialData={editingPublicacion} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
<PublicacionDiasSemanaModal
open={diasSemanaModalOpen}
onClose={handleCloseDiasSemanaModal}
publicacion={selectedPublicacionParaDias}
onConfigSaved={handleConfigDiasSaved}
/>
</Box>
);
};

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
@@ -29,9 +29,8 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
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 [filtroIdDestino, setFiltroIdDestino] = useState<number | string>('');
@@ -48,25 +47,37 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
const [selectedRow, setSelectedRow] = useState<SalidaOtroDestinoDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
// SO001, SO002 (crear/modificar), SO003 (eliminar)
const puedeVer = isSuperAdmin || tienePermiso("SO001");
const puedeCrearModificar = isSuperAdmin || tienePermiso("SO002");
const puedeEliminar = isSuperAdmin || tienePermiso("SO003");
// 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, destinosData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true),
otroDestinoService.getAllOtrosDestinos()
]);
setPublicaciones(pubsData);
setOtrosDestinos(destinosData);
const [pubsData, destinosData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true),
otroDestinoService.getAllOtrosDestinos()
]);
setPublicaciones(pubsData);
setOtrosDestinos(destinosData);
} catch (err) {
console.error("Error cargando datos para filtros:", err);
setError("Error al cargar opciones de filtro.");
console.error("Error cargando datos para filtros:", err);
setError("Error al cargar opciones de filtro.");
} finally {
setLoadingFiltersDropdown(false);
setLoadingFiltersDropdown(false);
}
}, []);
@@ -117,13 +128,13 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
const handleDelete = async (idParte: number) => {
if (window.confirm(`¿Seguro de eliminar este registro de salida (ID: ${idParte})?`)) {
setApiErrorMessage(null);
try {
setApiErrorMessage(null);
try {
await salidaOtroDestinoService.deleteSalidaOtroDestino(idParte);
cargarSalidas();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
setApiErrorMessage(message);
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
setApiErrorMessage(message);
}
}
handleMenuClose();
@@ -140,81 +151,91 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = salidas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
// 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: 2 }}>
<Typography variant="h4" gutterBottom>Salidas a Otros Destinos</Typography>
<Box sx={{ p: 1}}>
<Typography variant="h5" gutterBottom>Salidas a Otros Destinos</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros</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>Destino</InputLabel>
<Select value={filtroIdDestino} label="Destino" onChange={(e) => setFiltroIdDestino(e.target.value as number | string)}>
<MenuItem value=""><em>Todos</em></MenuItem>
{otrosDestinos.map(d => <MenuItem key={d.idDestino} value={d.idDestino}>{d.nombre}</MenuItem>)}
</Select>
</FormControl>
</Box>
{puedeCrearModificar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Salida</Button>)}
<Typography variant="h6" gutterBottom>Filtros</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>Destino</InputLabel>
<Select value={filtroIdDestino} label="Destino" onChange={(e) => setFiltroIdDestino(e.target.value as number | string)}>
<MenuItem value=""><em>Todos</em></MenuItem>
{otrosDestinos.map(d => <MenuItem key={d.idDestino} value={d.idDestino}>{d.nombre}</MenuItem>)}
</Select>
</FormControl>
</Box>
{puedeCrearModificar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Salida</Button>)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{!loading && !error && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Publicación</TableCell>
<TableCell>Destino</TableCell><TableCell align="right">Cantidad</TableCell>
<TableCell>Observación</TableCell>
{(puedeCrearModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={6} align="center">No se encontraron salidas.</TableCell></TableRow>
) : (
displayData.map((s) => (
<TableRow key={s.idParte} hover>
<TableCell>{formatDate(s.fecha)}</TableCell><TableCell>{s.nombrePublicacion}</TableCell>
<TableCell>{s.nombreDestino}</TableCell><TableCell align="right">{s.cantidad}</TableCell>
<TableCell>{s.observacion || '-'}</TableCell>
{(puedeCrearModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, s)} disabled={!puedeCrearModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25]} component="div" count={salidas.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Publicación</TableCell>
<TableCell>Destino</TableCell><TableCell align="right">Cantidad</TableCell>
<TableCell>Observación</TableCell>
{(puedeCrearModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeCrearModificar || puedeEliminar ? 6 : 5} align="center">No se encontraron salidas.</TableCell></TableRow>
) : (
displayData.map((s) => (
<TableRow key={s.idParte} hover>
{/* Usar la función formatDate aquí */}
<TableCell>{formatDate(s.fecha)}</TableCell>
<TableCell>{s.nombrePublicacion}</TableCell>
<TableCell>{s.nombreDestino}</TableCell>
<TableCell align="right">{s.cantidad}</TableCell>
<TableCell>
<Tooltip title={s.observacion || ''}>
<Box sx={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{s.observacion || '-'}
</Box>
</Tooltip>
</TableCell>
{(puedeCrearModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, s)} disabled={!puedeCrearModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25]} component="div" count={salidas.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeCrearModificar && selectedRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
{puedeEliminar && selectedRow && (
<MenuItem onClick={() => handleDelete(selectedRow.idParte)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
<MenuItem onClick={() => handleDelete(selectedRow.idParte)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)}
</Menu>
<SalidaOtroDestinoFormModal

View File

@@ -2,10 +2,14 @@ import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert
CircularProgress, Alert,
ListItemIcon,
ListItemText
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
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 zonaService from '../../services/Distribucion/zonaService'; // Servicio de Zonas
import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // DTO de Zonas
import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto'; // DTOs Create
@@ -132,8 +136,8 @@ const GestionarZonasPage: React.FC = () => {
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>
Gestionar Zonas
</Typography>
@@ -149,7 +153,6 @@ const GestionarZonasPage: React.FC = () => {
{/* <TextField label="Filtrar por Descripción" ... /> */}
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button
variant="contained"
startIcon={<AddIcon />}
@@ -158,7 +161,6 @@ const GestionarZonasPage: React.FC = () => {
>
Agregar Nueva Zona
</Button>
</Box>
)}
</Paper>
@@ -218,12 +220,14 @@ const GestionarZonasPage: React.FC = () => {
>
{puedeModificar && (
<MenuItem onClick={() => { handleOpenModal(selectedZonaRow!); handleMenuClose(); }}>
Modificar
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
<ListItemText>Modificar</ListItemText>
</MenuItem>
)}
{puedeEliminar && (
<MenuItem onClick={() => handleDelete(selectedZonaRow!.idZona)}>
Eliminar
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
<ListItemText>Eliminar</ListItemText>
</MenuItem>
)}
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}

View File

@@ -2,10 +2,14 @@ import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert
CircularProgress, Alert,
ListItemIcon,
ListItemText
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
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 estadoBobinaService from '../../services/Impresion/estadoBobinaService';
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto';
@@ -132,16 +136,16 @@ const GestionarEstadosBobinaPage: React.FC = () => {
if (!loading && !puedeVer) {
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Estados de Bobina</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Gestionar Estados de Bobina</Typography>
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
</Box>
);
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>
Gestionar Estados de Bobina
</Typography>
@@ -156,7 +160,6 @@ const GestionarEstadosBobinaPage: React.FC = () => {
/>
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button
variant="contained"
startIcon={<AddIcon />}
@@ -165,7 +168,6 @@ const GestionarEstadosBobinaPage: React.FC = () => {
>
Agregar Nuevo Estado
</Button>
</Box>
)}
</Paper>
@@ -226,12 +228,14 @@ const GestionarEstadosBobinaPage: React.FC = () => {
>
{puedeModificar && (
<MenuItem onClick={() => { handleOpenModal(selectedEstadoRow!); handleMenuClose(); }}>
Modificar
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
<ListItemText>Modificar</ListItemText>
</MenuItem>
)}
{puedeEliminar && (
<MenuItem onClick={() => handleDelete(selectedEstadoRow!.idEstadoBobina)}>
Eliminar
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
<ListItemText>Eliminar</ListItemText>
</MenuItem>
)}
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}

View File

@@ -2,10 +2,14 @@ import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert
CircularProgress, Alert,
ListItemIcon,
ListItemText
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
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 plantaService from '../../services/Impresion/plantaService'; // Servicio de Plantas
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
import type { CreatePlantaDto } from '../../models/dtos/Impresion/CreatePlantaDto';
@@ -135,16 +139,16 @@ const GestionarPlantasPage: React.FC = () => {
if (!loading && !puedeVer) {
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Plantas de Impresión</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Gestionar Plantas de Impresión</Typography>
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
</Box>
);
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>
Gestionar Plantas de Impresión
</Typography>
@@ -167,7 +171,6 @@ const GestionarPlantasPage: React.FC = () => {
{/* <Button variant="contained" onClick={cargarPlantas}>Buscar</Button> */}
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button
variant="contained"
startIcon={<AddIcon />}
@@ -176,7 +179,6 @@ const GestionarPlantasPage: React.FC = () => {
>
Agregar Nueva Planta
</Button>
</Box>
)}
</Paper>
@@ -237,12 +239,14 @@ const GestionarPlantasPage: React.FC = () => {
>
{puedeModificar && (
<MenuItem onClick={() => { handleOpenModal(selectedPlantaRow!); handleMenuClose(); }}>
Modificar
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
<ListItemText>Modificar</ListItemText>
</MenuItem>
)}
{puedeEliminar && (
<MenuItem onClick={() => handleDelete(selectedPlantaRow!.idPlanta)}>
Eliminar
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
<ListItemText>Eliminar</ListItemText>
</MenuItem>
)}
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}

View File

@@ -42,8 +42,8 @@ const GestionarStockBobinasPage: React.FC = () => {
const [filtroPlanta, setFiltroPlanta] = useState<number | string>('');
const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>('');
const [filtroRemito, setFiltroRemito] = useState('');
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
// Datos para dropdowns de filtros
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
@@ -178,8 +178,8 @@ const GestionarStockBobinasPage: React.FC = () => {
if (!loading && !puedeVer) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Stock de Bobinas</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Stock de Bobinas</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2}}>

View File

@@ -2,10 +2,14 @@ import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert
CircularProgress, Alert,
ListItemIcon,
ListItemText
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
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 tipoBobinaService from '../../services/Impresion/tipoBobinaService'; // Servicio específico
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
import type { CreateTipoBobinaDto } from '../../models/dtos/Impresion/CreateTipoBobinaDto';
@@ -132,16 +136,16 @@ const GestionarTiposBobinaPage: React.FC = () => {
if (!loading && !puedeVer) {
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Tipos de Bobina</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Gestionar Tipos de Bobina</Typography>
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
</Box>
);
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>
Gestionar Tipos de Bobina
</Typography>
@@ -157,7 +161,6 @@ const GestionarTiposBobinaPage: React.FC = () => {
{/* <Button variant="contained" onClick={cargarTiposBobina}>Buscar</Button> */}
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button
variant="contained"
startIcon={<AddIcon />}
@@ -166,7 +169,6 @@ const GestionarTiposBobinaPage: React.FC = () => {
>
Agregar Nuevo Tipo
</Button>
</Box>
)}
</Paper>
@@ -225,12 +227,14 @@ const GestionarTiposBobinaPage: React.FC = () => {
>
{puedeModificar && (
<MenuItem onClick={() => { handleOpenModal(selectedTipoBobinaRow!); handleMenuClose(); }}>
Modificar
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
<ListItemText>Modificar</ListItemText>
</MenuItem>
)}
{puedeEliminar && (
<MenuItem onClick={() => handleDelete(selectedTipoBobinaRow!.idTipoBobina)}>
Eliminar
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
<ListItemText>Eliminar</ListItemText>
</MenuItem>
)}
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}

View File

@@ -32,7 +32,7 @@ const GestionarTiradasPage: React.FC = () => {
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFecha, setFiltroFecha] = useState<string>('');
const [filtroFecha, setFiltroFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
const [filtroIdPlanta, setFiltroIdPlanta] = useState<number | string>('');
@@ -124,8 +124,8 @@ const GestionarTiradasPage: React.FC = () => {
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestión de Tiradas</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Gestión de Tiradas</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}}>
@@ -178,8 +178,8 @@ const GestionarTiradasPage: React.FC = () => {
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead><TableRow>
<TableCell>Sección</TableCell>
<TableCell align="right">Páginas</TableCell>
<TableCell><strong>Sección</strong></TableCell>
<TableCell align="right"><strong>Páginas</strong></TableCell>
</TableRow></TableHead>
<TableBody>
{tirada.seccionesImpresas.map(sec => (

View File

@@ -125,8 +125,8 @@ const GestionarListasRadioPage: React.FC = () => {
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Generar Listas de Radio</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Generar Listas de Radio</Typography>
<Paper sx={{ p: 3, mb: 2 }}>
<Typography variant="h6" gutterBottom>Criterios de Generación</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}> {/* Aumentado el gap */}

View File

@@ -118,8 +118,8 @@ const GestionarCancionesPage: React.FC = () => {
if (!loading && !puedeGestionar && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Canciones</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Gestionar Canciones</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}>

View File

@@ -101,8 +101,8 @@ const GestionarRitmosPage: React.FC = () => {
if (!loading && !puedeGestionar) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Ritmos</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Gestionar Ritmos</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}>

View File

@@ -1,8 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box, Typography, Button, Paper, CircularProgress, Alert,
Checkbox, FormControlLabel, FormGroup // Para el caso sin componente checklist
Box, Typography, Button, Paper, CircularProgress, Alert
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import SaveIcon from '@mui/icons-material/Save';
@@ -119,11 +118,11 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
return (
<Box sx={{ p: 2 }}>
<Box sx={{ p: 1 }}>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}>
Volver a Perfiles
</Button>
<Typography variant="h4" gutterBottom>
<Typography variant="h5" gutterBottom>
Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'}
</Typography>
<Typography variant="body2" color="textSecondary" gutterBottom>

View File

@@ -42,12 +42,7 @@ const GestionarAuditoriaUsuariosPage: React.FC = () => {
try {
const usuariosData = await usuarioService.getAllUsuarios(); // Asumiendo que tienes este método
setUsuariosParaDropdown(usuariosData);
// Opción B para Tipos de Modificación (desde backend)
// const tiposModData = await apiClient.get<string[]>('/auditoria/tipos-modificacion'); // Ajusta el endpoint si lo creas
// setTiposModificacionParaDropdown(tiposModData.data);
// Opción A (Hardcodeado en Frontend - más simple para empezar)
// Filtrar usuarios para dropdown, excluyendo los que no tienen historial
setTiposModificacionParaDropdown([
"Creado", "Insertada",
"Actualizado", "Modificada",
@@ -136,8 +131,8 @@ const GestionarAuditoriaUsuariosPage: React.FC = () => {
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Auditoría de Usuarios</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Auditoría de Usuarios</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>

View File

@@ -1,18 +1,21 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, Tooltip // Añadir Tooltip
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert,
ListItemIcon,
ListItemText
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; // Para asignar permisos
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import perfilService from '../../services/Usuarios/perfilService';
import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto';
import type { CreatePerfilDto } from '../../models/dtos/Usuarios/CreatePerfilDto';
import type { UpdatePerfilDto } from '../../models/dtos/Usuarios/UpdatePerfilDto';
import PerfilFormModal from '../../components/Modals/Usuarios/PerfilFormModal';
// import PermisosPorPerfilModal from '../../components/Modals/PermisosPorPerfilModal'; // Lo crearemos después
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
import { useNavigate } from 'react-router-dom'; // Para navegar
@@ -27,9 +30,6 @@ const GestionarPerfilesPage: React.FC = () => {
const [editingPerfil, setEditingPerfil] = useState<PerfilDto | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// const [permisosModalOpen, setPermisosModalOpen] = useState(false); // Para modal de permisos
// const [selectedPerfilForPermisos, setSelectedPerfilForPermisos] = useState<PerfilDto | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
@@ -48,9 +48,9 @@ const GestionarPerfilesPage: React.FC = () => {
const cargarPerfiles = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
@@ -87,13 +87,13 @@ const GestionarPerfilesPage: React.FC = () => {
const handleDelete = async (id: number) => {
if (window.confirm(`¿Está seguro? ID: ${id}`)) {
setApiErrorMessage(null);
try {
setApiErrorMessage(null);
try {
await perfilService.deletePerfil(id);
cargarPerfiles();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el perfil.';
setApiErrorMessage(message);
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el perfil.';
setApiErrorMessage(message);
}
}
handleMenuClose();
@@ -107,112 +107,120 @@ const GestionarPerfilesPage: React.FC = () => {
};
const handleOpenPermisosModal = (perfil: PerfilDto) => {
// setSelectedPerfilForPermisos(perfil);
// setPermisosModalOpen(true);
handleMenuClose();
// Navegar a la página de asignación de permisos
navigate(`/usuarios/perfiles/${perfil.id}/permisos`);
};
// const handleClosePermisosModal = () => {
// setPermisosModalOpen(false); setSelectedPerfilForPermisos(null);
// };
// const handleSubmitPermisos = async (idPerfil: number, permisosIds: number[]) => {
// try {
// // await perfilService.updatePermisosPorPerfil(idPerfil, permisosIds);
// // console.log("Permisos actualizados para perfil:", idPerfil);
// // Quizás un snackbar de éxito
// } catch (error) {
// console.error("Error al actualizar permisos:", error);
// setApiErrorMessage("Error al actualizar permisos.");
// }
// handleClosePermisosModal();
// };
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = perfiles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = perfiles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeVer) {
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Perfiles</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>Gestionar Perfiles</Typography>
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
</Box>
);
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Perfiles</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Gestionar Perfiles</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} />
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Nuevo Perfil
</Button>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} />
</Box>
{puedeCrear && (
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Nuevo Perfil
</Button>
)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{!loading && !error && (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Nombre del Perfil</TableCell>
<TableCell>Descripción</TableCell>
{(puedeModificar || puedeEliminar || puedeAsignarPermisos) && <TableCell align="right">Acciones</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{displayData.length === 0 && !loading ? (
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar || puedeAsignarPermisos) ? 3 : 2} align="center">No se encontraron perfiles.</TableCell></TableRow>
) : (
displayData.map((perfil) => (
<TableRow key={perfil.id}>
<TableCell>{perfil.nombrePerfil}</TableCell>
<TableCell>{perfil.descripcion || '-'}</TableCell>
{(puedeModificar || puedeEliminar || puedeAsignarPermisos) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, perfil)} disabled={!puedeModificar && !puedeEliminar && !puedeAsignarPermisos}>
<MoreVertIcon />
</IconButton>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25]} component="div" count={perfiles.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Nombre del Perfil</TableCell>
<TableCell>Descripción</TableCell>
{(puedeModificar || puedeEliminar || puedeAsignarPermisos) && <TableCell align="right">Acciones</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{displayData.length === 0 && !loading ? (
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar || puedeAsignarPermisos) ? 3 : 2} align="center">No se encontraron perfiles.</TableCell></TableRow>
) : (
displayData.map((perfil) => (
<TableRow key={perfil.id}>
<TableCell>{perfil.nombrePerfil}</TableCell>
<TableCell>{perfil.descripcion || '-'}</TableCell>
{(puedeModificar || puedeEliminar || puedeAsignarPermisos) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, perfil)} disabled={!puedeModificar && !puedeEliminar && !puedeAsignarPermisos}>
<MoreVertIcon />
</IconButton>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25]} component="div" count={perfiles.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(selectedPerfilRow!); handleMenuClose(); }}>Modificar</MenuItem>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
PaperProps={{
style: {
minWidth: 230, // Ajusta el ancho según el texto más largo
},
}}
>
{puedeModificar && selectedPerfilRow && (
<MenuItem onClick={() => { handleOpenModal(selectedPerfilRow); handleMenuClose(); }}>
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
<ListItemText>Modificar</ListItemText>
</MenuItem>
)}
{puedeEliminar && (
<MenuItem onClick={() => handleDelete(selectedPerfilRow!.id)}>Eliminar</MenuItem>
{puedeEliminar && selectedPerfilRow && (
<MenuItem onClick={() => { handleDelete(selectedPerfilRow.id); handleMenuClose(); }}>
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
<ListItemText>Eliminar</ListItemText>
</MenuItem>
)}
{puedeAsignarPermisos && (
<MenuItem onClick={() => handleOpenPermisosModal(selectedPerfilRow!)}>Asignar Permisos</MenuItem>
{puedeAsignarPermisos && selectedPerfilRow && (
<MenuItem onClick={() => { handleOpenPermisosModal(selectedPerfilRow); handleMenuClose(); }}>
<ListItemIcon><LockOpenIcon fontSize="small" /></ListItemIcon>
<ListItemText>Asignar Permisos</ListItemText>
</MenuItem>
)}
{(!puedeModificar && !puedeEliminar && !puedeAsignarPermisos) && <MenuItem disabled>Sin acciones</MenuItem>}
{/* Si no hay permisos para ninguna acción y hay una fila seleccionada */}
{selectedPerfilRow &&
!puedeModificar &&
!puedeEliminar &&
!puedeAsignarPermisos && (
<MenuItem disabled>
<ListItemText>Sin acciones disponibles</ListItemText>
</MenuItem>
)}
</Menu>
<PerfilFormModal

View File

@@ -1,11 +1,15 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert,
ListItemIcon,
ListItemText
} 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 permisoService from '../../services/Usuarios/permisoService';
import type { PermisoDto } from '../../models/dtos/Usuarios/PermisoDto';
import type { CreatePermisoDto } from '../../models/dtos/Usuarios/CreatePermisoDto';
@@ -35,9 +39,9 @@ const GestionarPermisosPage: React.FC = () => {
const cargarPermisos = useCallback(async () => {
if (!isSuperAdmin) {
setError("Acceso denegado. Solo SuperAdmin puede gestionar permisos.");
setLoading(false);
return;
setError("Acceso denegado. Solo SuperAdmin puede gestionar permisos.");
setLoading(false);
return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
@@ -74,13 +78,13 @@ const GestionarPermisosPage: React.FC = () => {
const handleDelete = async (id: number) => {
if (window.confirm(`¿Está seguro de eliminar este permiso (ID: ${id})?`)) {
setApiErrorMessage(null);
try {
setApiErrorMessage(null);
try {
await permisoService.deletePermiso(id);
cargarPermisos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el permiso.';
setApiErrorMessage(message);
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el permiso.';
setApiErrorMessage(message);
}
}
handleMenuClose();
@@ -93,99 +97,103 @@ const GestionarPermisosPage: React.FC = () => {
setAnchorEl(null); setSelectedPermisoRow(null);
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = permisos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = permisos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !isSuperAdmin) {
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Definición de Permisos</Typography>
<Alert severity="error">{error || "Acceso denegado."}</Alert>
</Box>
);
return (
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Definición de Permisos</Typography>
<Alert severity="error">{error || "Acceso denegado."}</Alert>
</Box>
);
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Definición de Permisos (SuperAdmin)</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Definición de Permisos (SuperAdmin)</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
<TextField
label="Filtrar por Módulo"
variant="outlined"
size="small"
value={filtroModulo}
onChange={(e) => setFiltroModulo(e.target.value)}
sx={{ flexGrow: 1, minWidth: '200px' }} // Para que se adapte mejor
/>
<TextField
label="Filtrar por CodAcc"
variant="outlined"
size="small"
value={filtroCodAcc}
onChange={(e) => setFiltroCodAcc(e.target.value)}
sx={{ flexGrow: 1, minWidth: '200px' }}
/>
{/* El botón de búsqueda es opcional si el filtro es en tiempo real */}
{/* <Button variant="contained" onClick={cargarPermisos}>Buscar</Button> */}
<TextField
label="Filtrar por Módulo"
variant="outlined"
size="small"
value={filtroModulo}
onChange={(e) => setFiltroModulo(e.target.value)}
sx={{ flexGrow: 1, minWidth: '200px' }} // Para que se adapte mejor
/>
<TextField
label="Filtrar por CodAcc"
variant="outlined"
size="small"
value={filtroCodAcc}
onChange={(e) => setFiltroCodAcc(e.target.value)}
sx={{ flexGrow: 1, minWidth: '200px' }}
/>
{/* El botón de búsqueda es opcional si el filtro es en tiempo real */}
{/* <Button variant="contained" onClick={cargarPermisos}>Buscar</Button> */}
</Box>
{isSuperAdmin && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Nuevo Permiso
</Button>
</Box>
{isSuperAdmin && (
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Nuevo Permiso
</Button>
)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{!loading && !error && isSuperAdmin && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Módulo</TableCell>
<TableCell>Descripción</TableCell>
<TableCell>CodAcc</TableCell>
<TableCell align="right">Acciones</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayData.length === 0 && !loading ? (
<TableRow><TableCell colSpan={4} align="center">No se encontraron permisos.</TableCell></TableRow>
) : (
displayData.map((permiso) => (
<TableRow key={permiso.id}>
<TableCell>{permiso.modulo}</TableCell>
<TableCell>{permiso.descPermiso}</TableCell>
<TableCell>{permiso.codAcc}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, permiso)}>
<MoreVertIcon />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 50]} component="div" count={permisos.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Módulo</TableCell>
<TableCell>Descripción</TableCell>
<TableCell>CodAcc</TableCell>
<TableCell align="right">Acciones</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayData.length === 0 && !loading ? (
<TableRow><TableCell colSpan={4} align="center">No se encontraron permisos.</TableCell></TableRow>
) : (
displayData.map((permiso) => (
<TableRow key={permiso.id}>
<TableCell>{permiso.modulo}</TableCell>
<TableCell>{permiso.descPermiso}</TableCell>
<TableCell>{permiso.codAcc}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, permiso)}>
<MoreVertIcon />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 50]} component="div" count={permisos.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
<MenuItem onClick={() => { handleOpenModal(selectedPermisoRow!); handleMenuClose(); }}>Modificar</MenuItem>
<MenuItem onClick={() => handleDelete(selectedPermisoRow!.id)}>Eliminar</MenuItem>
<MenuItem onClick={() => { handleOpenModal(selectedPermisoRow!); handleMenuClose(); }}>
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
<ListItemText>Modificar</ListItemText>
</MenuItem>
<MenuItem onClick={() => handleDelete(selectedPermisoRow!.id)}>
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
<ListItemText>Eliminar</ListItemText>
</MenuItem>
</Menu>
<PermisoFormModal

View File

@@ -2,10 +2,13 @@ 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, Tooltip
CircularProgress, Alert, Tooltip,
ListItemIcon,
ListItemText
} 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 VpnKeyIcon from '@mui/icons-material/VpnKey'; // Para resetear clave
import usuarioService from '../../services/Usuarios/usuarioService';
import type { UsuarioDto } from '../../models/dtos/Usuarios/UsuarioDto';
@@ -141,10 +144,9 @@ const GestionarUsuariosPage: React.FC = () => {
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Usuarios</Typography>
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Gestionar Usuarios</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
{/* SECCIÓN DE FILTROS CORREGIDA */}
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
<TextField
label="Filtrar por Usuario"
@@ -164,11 +166,9 @@ const GestionarUsuariosPage: React.FC = () => {
/>
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenUsuarioModal()} sx={{ mb: 2 }}>
Agregar Nuevo Usuario
</Button>
</Box>
)}
</Paper>
@@ -231,7 +231,10 @@ const GestionarUsuariosPage: React.FC = () => {
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{(puedeModificar || puedeAsignarPerfil) && (
<MenuItem onClick={() => { handleOpenUsuarioModal(selectedUsuarioRow!); handleMenuClose(); }}>Modificar</MenuItem>
<MenuItem onClick={() => { handleOpenUsuarioModal(selectedUsuarioRow!); handleMenuClose(); }}>
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
<ListItemText>Modificar</ListItemText>
</MenuItem>
)}
{puedeResetearClave && selectedUsuarioRow && currentUser?.userId !== selectedUsuarioRow.id && (
<MenuItem onClick={() => handleOpenSetPasswordModal(selectedUsuarioRow!)}>

View File

@@ -2,6 +2,8 @@ import apiClient from '../apiClient';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto';
import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto';
import type { PublicacionDiaSemanaDto } from '../../models/dtos/Distribucion/PublicacionDiaSemanaDto';
import type { UpdatePublicacionDiasSemanaRequestDto } from '../../models/dtos/Distribucion/UpdatePublicacionDiasSemanaRequestDto';
const getAllPublicaciones = async (
nombreFilter?: string,
@@ -35,12 +37,29 @@ const deletePublicacion = async (id: number): Promise<void> => {
await apiClient.delete(`/publicaciones/${id}`);
};
const getConfiguracionDiasPublicacion = async (idPublicacion: number): Promise<PublicacionDiaSemanaDto[]> => {
const response = await apiClient.get<PublicacionDiaSemanaDto[]>(`/publicaciones/${idPublicacion}/dias-semana`);
return response.data;
};
const updateConfiguracionDiasPublicacion = async (idPublicacion: number, data: UpdatePublicacionDiasSemanaRequestDto): Promise<void> => {
await apiClient.put(`/publicaciones/${idPublicacion}/dias-semana`, data);
};
const getPublicacionesPorDiaSemana = async (diaSemana: number): Promise<PublicacionDto[]> => {
const response = await apiClient.get<PublicacionDto[]>('/publicaciones/por-dia-semana', { params: { dia: diaSemana } });
return response.data;
};
const publicacionService = {
getAllPublicaciones,
getPublicacionById,
createPublicacion,
updatePublicacion,
deletePublicacion,
getConfiguracionDiasPublicacion,
updateConfiguracionDiasPublicacion,
getPublicacionesPorDiaSemana
};
export default publicacionService;

View File

@@ -366,6 +366,25 @@ const getControlDevolucionesPdf = async (params: {
return response.data;
};
const getTicketLiquidacionCanillaPdf = async (params: {
fecha: string; // YYYY-MM-DD
idCanilla: number;
esAccionista?: boolean; // Hacerlo opcional, el backend podría tener un default
}): Promise<Blob> => {
const queryParams: Record<string, string | number | boolean> = {
fecha: params.fecha,
idCanilla: params.idCanilla,
};
if (params.esAccionista !== undefined) {
queryParams.esAccionista = params.esAccionista;
}
const response = await apiClient.get('/reportes/ticket-liquidacion-canilla/pdf', {
params: queryParams,
responseType: 'blob',
});
return response.data;
};
const reportesService = {
getExistenciaPapel,
getExistenciaPapelPdf,
@@ -401,6 +420,7 @@ const reportesService = {
getListadoDistribucionDistribuidoresPdf,
getControlDevolucionesData,
getControlDevolucionesPdf,
getTicketLiquidacionCanillaPdf,
};
export default reportesService;