Refinamiento de permisos y ajustes en controles. Añade gestión sobre saldos y visualización. Entre otros..
This commit is contained in:
162
Frontend/src/components/Modals/Contables/AjusteSaldoModal.tsx
Normal file
162
Frontend/src/components/Modals/Contables/AjusteSaldoModal.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert, InputAdornment
|
||||
} from '@mui/material';
|
||||
import type { SaldoGestionDto } from '../../../models/dtos/Contables/SaldoGestionDto';
|
||||
import type { AjusteSaldoRequestDto } from '../../../models/dtos/Contables/AjusteSaldoRequestDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 450 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 3,
|
||||
};
|
||||
|
||||
interface AjusteSaldoModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: AjusteSaldoRequestDto) => Promise<void>; // El padre maneja la recarga
|
||||
saldoParaAjustar: SaldoGestionDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const AjusteSaldoModal: React.FC<AjusteSaldoModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
saldoParaAjustar,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [montoAjuste, setMontoAjuste] = useState<string>('');
|
||||
const [justificacion, setJustificacion] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setMontoAjuste('');
|
||||
setJustificacion('');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
const numMontoAjuste = parseFloat(montoAjuste);
|
||||
|
||||
if (!montoAjuste.trim()) {
|
||||
errors.montoAjuste = 'El monto de ajuste es obligatorio.';
|
||||
} else if (isNaN(numMontoAjuste)) {
|
||||
errors.montoAjuste = 'El monto debe ser un número.';
|
||||
} else if (numMontoAjuste === 0) {
|
||||
errors.montoAjuste = 'El monto de ajuste no puede ser cero.';
|
||||
}
|
||||
|
||||
if (!justificacion.trim()) {
|
||||
errors.justificacion = 'La justificación es obligatoria.';
|
||||
} else if (justificacion.trim().length < 5 || justificacion.trim().length > 250) {
|
||||
errors.justificacion = 'La justificación debe tener entre 5 y 250 caracteres.';
|
||||
}
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: 'montoAjuste' | 'justificacion') => {
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate() || !saldoParaAjustar) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const dataToSubmit: AjusteSaldoRequestDto = {
|
||||
destino: saldoParaAjustar.destino as 'Distribuidores' | 'Canillas',
|
||||
idDestino: saldoParaAjustar.idDestino,
|
||||
idEmpresa: saldoParaAjustar.idEmpresa,
|
||||
montoAjuste: parseFloat(montoAjuste),
|
||||
justificacion,
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
onClose(); // Cerrar en éxito (el padre recargará)
|
||||
} catch (error: any) {
|
||||
// El error de API es manejado por la página padre
|
||||
console.error("Error en submit de AjusteSaldoModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!saldoParaAjustar) return null;
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
Ajustar Saldo Manualmente
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Destinatario: <strong>{saldoParaAjustar.nombreDestinatario}</strong> ({saldoParaAjustar.destino})
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Empresa: <strong>{saldoParaAjustar.nombreEmpresa}</strong>
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Saldo Actual: <strong>{saldoParaAjustar.monto.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
|
||||
</Typography>
|
||||
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<TextField
|
||||
label="Monto de Ajuste (+/-)"
|
||||
type="number"
|
||||
fullWidth
|
||||
required
|
||||
value={montoAjuste}
|
||||
onChange={(e) => { setMontoAjuste(e.target.value); handleInputChange('montoAjuste'); }}
|
||||
margin="normal"
|
||||
error={!!localErrors.montoAjuste}
|
||||
helperText={localErrors.montoAjuste || 'Ingrese un valor positivo para aumentar deuda o negativo para disminuirla.'}
|
||||
disabled={loading}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }}
|
||||
inputProps={{ step: "0.01" }}
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
label="Justificación del Ajuste"
|
||||
fullWidth
|
||||
required
|
||||
value={justificacion}
|
||||
onChange={(e) => { setJustificacion(e.target.value); handleInputChange('justificacion'); }}
|
||||
margin="normal"
|
||||
multiline
|
||||
rows={3}
|
||||
error={!!localErrors.justificacion}
|
||||
helperText={localErrors.justificacion || ''}
|
||||
disabled={loading}
|
||||
inputProps={{ maxLength: 250 }}
|
||||
/>
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 1 }}>{errorMessage}</Alert>}
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained" disabled={loading}>
|
||||
{loading ? <CircularProgress size={24} /> : 'Aplicar Ajuste'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AjusteSaldoModal;
|
||||
@@ -8,14 +8,14 @@ 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';
|
||||
import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto';
|
||||
// import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; // Ya no es necesario cargar todos los canillitas aquí
|
||||
import publicacionService from '../../../services/Distribucion/publicacionService';
|
||||
import canillaService from '../../../services/Distribucion/canillaService';
|
||||
// import canillaService from '../../../services/Distribucion/canillaService'; // Ya no es necesario
|
||||
import entradaSalidaCanillaService from '../../../services/Distribucion/entradaSalidaCanillaService';
|
||||
import type { CreateBulkEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto';
|
||||
import type { EntradaSalidaCanillaItemDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaItemDto';
|
||||
import axios from 'axios';
|
||||
import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
@@ -34,14 +34,21 @@ const modalStyle = {
|
||||
interface EntradaSalidaCanillaFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
// El onSubmit de la página padre se usa solo para edición. La creación se maneja internamente.
|
||||
onSubmit: (data: UpdateEntradaSalidaCanillaDto, idParte: number) => Promise<void>;
|
||||
initialData?: EntradaSalidaCanillaDto | null;
|
||||
initialData?: EntradaSalidaCanillaDto | null; // Para edición
|
||||
prefillData?: { // Para creación, prellenar desde la página padre
|
||||
fecha?: string; // YYYY-MM-DD
|
||||
idCanilla?: number | string;
|
||||
nombreCanilla?: string; // << AÑADIR NOMBRE PARA MOSTRAR
|
||||
idPublicacion?: number | string; // Para pre-seleccionar la primera publicación en la lista de items
|
||||
} | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
interface FormRowItem {
|
||||
id: string;
|
||||
id: string; // ID temporal para el frontend
|
||||
idPublicacion: number | string;
|
||||
cantSalida: string;
|
||||
cantEntrada: string;
|
||||
@@ -50,56 +57,62 @@ interface FormRowItem {
|
||||
|
||||
const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps> = ({
|
||||
open,
|
||||
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
|
||||
onClose,
|
||||
onSubmit: onSubmitEdit, // Renombrar para claridad, ya que solo se usa para editar
|
||||
initialData,
|
||||
prefillData,
|
||||
errorMessage: parentErrorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [idCanilla, setIdCanilla] = useState<number | string>('');
|
||||
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [editIdPublicacion, setEditIdPublicacion] = useState<number | string>('');
|
||||
// Estados para los campos que SÍ son editables o parte del formulario de items
|
||||
const [editIdPublicacion, setEditIdPublicacion] = useState<number | string>(''); // Solo para modo edición
|
||||
const [editCantSalida, setEditCantSalida] = useState<string>('0');
|
||||
const [editCantEntrada, setEditCantEntrada] = useState<string>('0');
|
||||
const [editObservacion, setEditObservacion] = useState('');
|
||||
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); // Loading para submit
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false); // Loading para canillas/pubs
|
||||
const [loadingItems, setLoadingItems] = useState(false); // Loading para pre-carga de items
|
||||
const [items, setItems] = useState<FormRowItem[]>([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
|
||||
// Estados para datos de dropdowns
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]); // Sigue siendo necesario para la lista de items
|
||||
|
||||
// Estados de carga y error
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false); // Solo para publicaciones
|
||||
const [loadingItems, setLoadingItems] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
const [modalSpecificApiError, setModalSpecificApiError] = useState<string | null>(null);
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
const isEditing = Boolean(initialData && initialData.idParte);
|
||||
|
||||
// Efecto para cargar datos de dropdowns (Publicaciones, Canillitas) SOLO UNA VEZ o cuando open cambia a true
|
||||
// Datos que vienen prellenados y no son editables en el modal (Fecha y Canillita)
|
||||
const displayFecha = isEditing ? (initialData?.fecha ? initialData.fecha.split('T')[0] : '') : (prefillData?.fecha || '');
|
||||
const displayIdCanilla = isEditing ? initialData?.idCanilla : prefillData?.idCanilla;
|
||||
const displayNombreCanilla = isEditing ? initialData?.nomApeCanilla : prefillData?.nombreCanilla;
|
||||
|
||||
|
||||
// Cargar publicaciones para el dropdown de items
|
||||
useEffect(() => {
|
||||
const fetchDropdownData = async () => {
|
||||
const fetchPublicacionesDropdown = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: null }));
|
||||
try {
|
||||
const [pubsData, canillitasData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
||||
canillaService.getAllCanillas(undefined, undefined, true)
|
||||
]);
|
||||
// Usar getPublicacionesForDropdown si lo tienes, sino getAllPublicaciones
|
||||
const pubsData = await publicacionService.getPublicacionesForDropdown(true);
|
||||
setPublicaciones(pubsData);
|
||||
setCanillitas(canillitasData);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar datos para dropdowns", error);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar datos necesarios (publicaciones/canillitas).' }));
|
||||
console.error("Error al cargar publicaciones para dropdown", error);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar publicaciones.' }));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchDropdownData();
|
||||
fetchPublicacionesDropdown();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
|
||||
// Efecto para inicializar el formulario cuando se abre o cambia initialData
|
||||
// Inicializar formulario y/o pre-cargar items
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
clearErrorMessage();
|
||||
@@ -107,65 +120,90 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
setLocalErrors({});
|
||||
|
||||
if (isEditing && initialData) {
|
||||
setIdCanilla(initialData.idCanilla || '');
|
||||
setFecha(initialData.fecha ? initialData.fecha.split('T')[0] : new Date().toISOString().split('T')[0]);
|
||||
setEditIdPublicacion(initialData.idPublicacion || '');
|
||||
setEditCantSalida(initialData.cantSalida?.toString() || '0');
|
||||
setEditCantEntrada(initialData.cantEntrada?.toString() || '0');
|
||||
setEditObservacion(initialData.observacion || '');
|
||||
setItems([]); // En modo edición, no pre-cargamos items de la lista
|
||||
} else {
|
||||
// Modo NUEVO: resetear campos principales y dejar que el efecto de 'fecha' cargue los items
|
||||
setIdCanilla('');
|
||||
setFecha(new Date().toISOString().split('T')[0]); // Fecha actual por defecto
|
||||
// Los items se cargarán por el siguiente useEffect basado en la fecha
|
||||
setItems([]); // No hay lista de items en modo edición de un solo movimiento
|
||||
} else { // Modo Creación
|
||||
// Limpiar campos de edición
|
||||
setEditIdPublicacion('');
|
||||
setEditCantSalida('0');
|
||||
setEditCantEntrada('0');
|
||||
setEditObservacion('');
|
||||
|
||||
// Lógica para pre-cargar items basada en displayFecha y prefillData.idPublicacion
|
||||
// (Esta lógica se mueve al siguiente useEffect que depende de displayFecha y publicaciones)
|
||||
// Por ahora, solo aseguramos que `items` se resetee si es necesario.
|
||||
const idPubPrefill = prefillData?.idPublicacion;
|
||||
if (idPubPrefill && publicaciones.length > 0) {
|
||||
// Si ya tenemos publicaciones, y un prefill de publicación, intentamos setearlo
|
||||
const diaSemana = new Date(displayFecha + 'T00:00:00Z').getUTCDay();
|
||||
setLoadingItems(true);
|
||||
publicacionService.getPublicacionesPorDiaSemana(diaSemana)
|
||||
.then(pubsPorDefecto => {
|
||||
let itemsIniciales: FormRowItem[];
|
||||
if (pubsPorDefecto.find(p => p.idPublicacion === Number(idPubPrefill))) {
|
||||
// Si la publicación prellenada está en las de por defecto, la usamos
|
||||
itemsIniciales = [{ id: Date.now().toString(), idPublicacion: Number(idPubPrefill), cantSalida: '0', cantEntrada: '0', observacion: '' }];
|
||||
} else if (pubsPorDefecto.length > 0) {
|
||||
// Si no, pero hay otras por defecto, usamos la primera de ellas
|
||||
itemsIniciales = pubsPorDefecto.map(pub => ({
|
||||
id: `${Date.now().toString()}-${pub.idPublicacion}`,
|
||||
idPublicacion: pub.idPublicacion,
|
||||
cantSalida: '0', cantEntrada: '0', observacion: ''
|
||||
}));
|
||||
} else {
|
||||
// Si no hay ninguna por defecto, y la prellenada no aplica, usamos la prellenada sola o vacía
|
||||
itemsIniciales = [{ id: Date.now().toString(), idPublicacion: idPubPrefill || '', cantSalida: '0', cantEntrada: '0', observacion: '' }];
|
||||
}
|
||||
setItems(itemsIniciales.length > 0 ? itemsIniciales : [{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
})
|
||||
.catch(() => setItems([{ id: Date.now().toString(), idPublicacion: idPubPrefill || '', cantSalida: '0', cantEntrada: '0', observacion: '' }])) // Fallback
|
||||
.finally(() => setLoadingItems(false));
|
||||
} else if (publicaciones.length === 0 && !loadingDropdowns) { // Si no hay prefill de pub o no hay pubs aún
|
||||
setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [open, initialData, isEditing, clearErrorMessage]);
|
||||
}, [open, initialData, isEditing, prefillData, clearErrorMessage, publicaciones, loadingDropdowns, displayFecha]); // Añadir displayFecha
|
||||
|
||||
|
||||
// Efecto para pre-cargar/re-cargar items cuando cambia la FECHA (en modo NUEVO)
|
||||
// y cuando las publicaciones están disponibles.
|
||||
// Efecto para pre-cargar items por defecto cuando cambia la FECHA (displayFecha) en modo NUEVO
|
||||
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 }));
|
||||
|
||||
if (open && !isEditing && publicaciones.length > 0 && displayFecha) {
|
||||
const diaSemana = new Date(displayFecha + 'T00:00:00Z').getUTCDay();
|
||||
setLoadingItems(true);
|
||||
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: '' }]);
|
||||
}
|
||||
const itemsPorDefecto = pubsPorDefecto.map(pub => ({
|
||||
id: `${Date.now().toString()}-${pub.idPublicacion}`,
|
||||
idPublicacion: pub.idPublicacion,
|
||||
cantSalida: '0',
|
||||
cantEntrada: '0',
|
||||
observacion: ''
|
||||
}));
|
||||
setItems(itemsPorDefecto.length > 0 ? itemsPorDefecto : [{ 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);
|
||||
console.error("Error al cargar 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
|
||||
}, [open, isEditing, displayFecha, publicaciones]); // Dependencia de displayFecha y publicaciones
|
||||
|
||||
|
||||
const validate = (): boolean => {
|
||||
// ... (lógica de validación sin cambios, pero 'idCanilla' y 'fecha' ya no son estados del modal)
|
||||
const currentErrors: { [key: string]: string | null } = {};
|
||||
if (!idCanilla) currentErrors.idCanilla = 'Seleccione un canillita.';
|
||||
if (!fecha.trim()) currentErrors.fecha = 'La fecha es obligatoria.';
|
||||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) currentErrors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).';
|
||||
|
||||
// Validar displayIdCanilla y displayFecha si es modo creación
|
||||
if (!isEditing) {
|
||||
if (!displayIdCanilla) currentErrors.idCanilla = 'El canillita es obligatorio (provisto por la página).';
|
||||
if (!displayFecha || !displayFecha.trim()) currentErrors.fecha = 'La fecha es obligatoria (provista por la página).';
|
||||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(displayFecha)) currentErrors.fecha = 'Formato de fecha inválido.';
|
||||
}
|
||||
// ... resto de la validación para items (modo creación) o campos edit (modo edición) ...
|
||||
if (isEditing) {
|
||||
const salidaNum = parseInt(editCantSalida, 10);
|
||||
const entradaNum = parseInt(editCantEntrada, 10);
|
||||
@@ -177,14 +215,15 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
} else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) {
|
||||
currentErrors.editCantEntrada = 'Cant. Entrada no puede ser mayor a Cant. Salida.';
|
||||
}
|
||||
} else {
|
||||
if (!editIdPublicacion) { // En edición, la publicación es fija, pero debe existir
|
||||
currentErrors.editIdPublicacion = 'Error: Publicación no especificada para edición.';
|
||||
}
|
||||
} else { // Modo Creación (Bulk)
|
||||
let hasValidItemWithQuantityOrPub = false;
|
||||
const publicacionIdsEnLote = new Set<number>();
|
||||
|
||||
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);
|
||||
@@ -199,9 +238,8 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
const pubIdNum = Number(item.idPublicacion);
|
||||
if (publicacionIdsEnLote.has(pubIdNum)) {
|
||||
currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} duplicada.`;
|
||||
} else {
|
||||
publicacionIdsEnLote.add(pubIdNum);
|
||||
}
|
||||
} else { publicacionIdsEnLote.add(pubIdNum); }
|
||||
|
||||
if (item.cantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) {
|
||||
currentErrors[`item_${item.id}_cantSalida`] = `Salida Pub. ${index + 1} inválida.`;
|
||||
}
|
||||
@@ -213,18 +251,11 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
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) {
|
||||
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 (!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.";
|
||||
} else if (items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida, 10) > 0 || parseInt(i.cantEntrada, 10) > 0 || i.observacion.trim() !== '')) && !allItemsAreEmptyAndNoPubSelected) {
|
||||
currentErrors.general = "Debe ingresar datos significativos (cantidades u observación) para al menos una publicación seleccionada.";
|
||||
}
|
||||
}
|
||||
setLocalErrors(currentErrors);
|
||||
@@ -232,6 +263,7 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
// ... (sin cambios)
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (parentErrorMessage) clearErrorMessage();
|
||||
if (modalSpecificApiError) setModalSpecificApiError(null);
|
||||
@@ -252,16 +284,17 @@ 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)
|
||||
await onSubmitEdit(dataToSubmitSingle, initialData.idParte);
|
||||
} else { // Modo Creación
|
||||
if (!displayIdCanilla || !displayFecha) {
|
||||
setModalSpecificApiError("Faltan datos del canillita o la fecha para crear los movimientos.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const itemsToSubmit: EntradaSalidaCanillaItemDto[] = items
|
||||
.filter(item =>
|
||||
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() !== '')
|
||||
( (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),
|
||||
@@ -271,37 +304,30 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
}));
|
||||
|
||||
if (itemsToSubmit.length === 0) {
|
||||
setLocalErrors(prev => ({ ...prev, general: "No hay movimientos válidos para registrar..." }));
|
||||
setLocalErrors(prev => ({ ...prev, general: "No hay movimientos válidos para registrar." }));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const bulkData: CreateBulkEntradaSalidaCanillaDto = {
|
||||
idCanilla: Number(idCanilla),
|
||||
fecha,
|
||||
idCanilla: Number(displayIdCanilla), // Usar el displayIdCanilla
|
||||
fecha: displayFecha, // Usar el displayFecha
|
||||
items: itemsToSubmit,
|
||||
};
|
||||
await entradaSalidaCanillaService.createBulkEntradasSalidasCanilla(bulkData);
|
||||
onClose(); // Cerrar el modal DESPUÉS de un submit de creación bulk exitoso
|
||||
}
|
||||
// onClose(); // Movido dentro de los bloques if/else para asegurar que solo se llama tras éxito
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de EntradaSalidaCanillaFormModal:", error);
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
setModalSpecificApiError(error.response.data?.message || 'Error al procesar la solicitud.');
|
||||
} else {
|
||||
setModalSpecificApiError('Ocurrió un error inesperado.');
|
||||
}
|
||||
// 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).
|
||||
const message = axios.isAxiosError(error) && error.response?.data?.message
|
||||
? error.response.data.message
|
||||
: 'Ocurrió un error inesperado al procesar la solicitud.';
|
||||
setModalSpecificApiError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddRow = () => {
|
||||
const handleAddRow = () => { /* ... (sin cambios) ... */
|
||||
if (items.length >= publicaciones.length) {
|
||||
setLocalErrors(prev => ({ ...prev, general: "No se pueden agregar más filas que publicaciones disponibles." }));
|
||||
return;
|
||||
@@ -309,15 +335,13 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
setItems([...items, { id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
setLocalErrors(prev => ({ ...prev, general: null }));
|
||||
};
|
||||
|
||||
const handleRemoveRow = (idToRemove: string) => {
|
||||
const handleRemoveRow = (idToRemove: string) => { /* ... (sin cambios) ... */
|
||||
if (items.length <= 1 && !isEditing) return;
|
||||
setItems(items.filter(item => item.id !== idToRemove));
|
||||
};
|
||||
|
||||
const handleItemChange = (id: string, field: keyof Omit<FormRowItem, 'id'>, value: string | number) => {
|
||||
setItems(items.map(itemRow => itemRow.id === id ? { ...itemRow, [field]: value } : itemRow)); // CORREGIDO: item a itemRow para evitar conflicto de nombres de variable con el `item` del map en el JSX
|
||||
if (localErrors[`item_${id}_${field}`]) { // Aquí item se refiere al id del item.
|
||||
const handleItemChange = (id: string, field: keyof Omit<FormRowItem, 'id'>, value: string | number) => { /* ... (sin cambios) ... */
|
||||
setItems(items.map(itemRow => itemRow.id === id ? { ...itemRow, [field]: value } : itemRow));
|
||||
if (localErrors[`item_${id}_${field}`]) {
|
||||
setLocalErrors(prev => ({ ...prev, [`item_${id}_${field}`]: null }));
|
||||
}
|
||||
if (localErrors.general) setLocalErrors(prev => ({ ...prev, general: null }));
|
||||
@@ -325,200 +349,102 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
if (modalSpecificApiError) setModalSpecificApiError(null);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Movimiento Canillita' : 'Registrar Movimientos Canillita'}
|
||||
{isEditing ? `Editar Movimiento (ID: ${initialData?.idParte})` : 'Registrar Nuevos Movimientos'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idCanilla} required>
|
||||
<InputLabel id="canilla-esc-select-label">Canillita</InputLabel>
|
||||
<Select labelId="canilla-esc-select-label" label="Canillita" value={idCanilla}
|
||||
onChange={(e) => { setIdCanilla(e.target.value as number); handleInputChange('idCanilla'); }}
|
||||
disabled={loading || loadingDropdowns || isEditing}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione un canillita</em></MenuItem>
|
||||
{canillitas.map((c) => (<MenuItem key={c.idCanilla} value={c.idCanilla}>{`${c.nomApe} (Leg: ${c.legajo || 'S/L'})`}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idCanilla && <FormHelperText>{localErrors.idCanilla}</FormHelperText>}
|
||||
</FormControl>
|
||||
{/* --- MOSTRAR DATOS PRELLENADOS/FIJOS --- */}
|
||||
<Paper variant="outlined" sx={{ p: 1.5, mb: 2, backgroundColor: 'grey.100' }}>
|
||||
<Typography variant="body1" component="div" sx={{ display: 'flex', justifyContent: 'space-between', flexWrap:'wrap' }}>
|
||||
<Box><strong>{isEditing ? "Canillita:" : "Para Canillita:"}</strong> {displayNombreCanilla || 'N/A'}</Box>
|
||||
<Box><strong>{isEditing ? "Fecha Movimiento:" : "Para Fecha:"}</strong> {displayFecha ? new Date(displayFecha + 'T00:00:00Z').toLocaleDateString('es-AR', {timeZone: 'UTC'}) : 'N/A'}</Box>
|
||||
</Typography>
|
||||
{localErrors.idCanilla && <Typography color="error" variant="caption" display="block">{localErrors.idCanilla}</Typography>}
|
||||
{localErrors.fecha && <Typography color="error" variant="caption" display="block">{localErrors.fecha}</Typography>}
|
||||
</Paper>
|
||||
{/* --- FIN DATOS PRELLENADOS --- */}
|
||||
|
||||
<TextField label="Fecha Movimientos" type="date" value={fecha} required
|
||||
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 && !idCanilla} // AutoFocus si es nuevo y no hay canillita seleccionado
|
||||
/>
|
||||
{/* El Select de Canillita y TextField de Fecha se eliminan de aquí si son fijos */}
|
||||
|
||||
{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 }}
|
||||
/>
|
||||
</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 }}
|
||||
{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, flexWrap: 'wrap' }}>
|
||||
<TextField label="Cant. Salida" type="number" value={editCantSalida}
|
||||
onChange={(e) => { setEditCantSalida(e.target.value); handleInputChange('editCantSalida'); }}
|
||||
margin="dense" error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''}
|
||||
disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1, minWidth:'120px' }}
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
<TextField label="Cant. Entrada" type="number" value={editCantEntrada}
|
||||
onChange={(e) => { setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada'); }}
|
||||
margin="dense" error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''}
|
||||
disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1, minWidth:'120px' }}
|
||||
/>
|
||||
</Box>
|
||||
<TextField label="Observación (Movimiento)" value={editObservacion} // Label cambiado
|
||||
onChange={(e) => setEditObservacion(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{ mt: 1 }}
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{!isEditing && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mt: 1.5, mb: 0.5 }}>Movimientos por Publicación:</Typography>
|
||||
{/* 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`]}
|
||||
>
|
||||
<InputLabel
|
||||
required={
|
||||
parseInt(itemRow.cantSalida) > 0 ||
|
||||
parseInt(itemRow.cantEntrada) > 0 ||
|
||||
itemRow.observacion.trim() !== ''
|
||||
}
|
||||
>
|
||||
{!isEditing && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mt: 1.5, mb: 0.5 }}>Movimientos por Publicación:</Typography>
|
||||
{loadingItems && <Box sx={{ display: 'flex', justifyContent: 'center', my: 1 }}><CircularProgress size={20} /></Box>}
|
||||
{!loadingItems && items.map((itemRow, index) => (
|
||||
// ... (Renderizado de la fila de items sin cambios significativos,
|
||||
// solo asegúrate que el Select de Publicación use `publicaciones` y no `canillitas`)
|
||||
<Paper key={itemRow.id} elevation={1} sx={{ p: 1.5, mb: 1, }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap', flexGrow: 1, }}>
|
||||
<FormControl sx={{ minWidth: 160, flexBasis: 'calc(40% - 8px)', flexGrow: 1, minHeight: 0 }} 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}
|
||||
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 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 }} >
|
||||
<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>
|
||||
)}
|
||||
{localErrors[`item_${itemRow.id}_idPublicacion`] && ( <FormHelperText> {localErrors[`item_${itemRow.id}_idPublicacion`]} </FormHelperText> )}
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
label="Llevados"
|
||||
type="number"
|
||||
size="small"
|
||||
value={itemRow.cantSalida}
|
||||
<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,
|
||||
}}
|
||||
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}
|
||||
<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,
|
||||
}}
|
||||
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}
|
||||
<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
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={() => handleRemoveRow(itemRow.id)} color="error" aria-label="Quitar fila" sx={{ alignSelf: 'center', }} >
|
||||
<DeleteIcon fontSize="medium" />
|
||||
</IconButton>
|
||||
)}
|
||||
</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
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{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
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{parentErrorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{parentErrorMessage}</Alert>}
|
||||
{modalSpecificApiError && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{modalSpecificApiError}</Alert>}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import type { NovedadCanillaDto } from '../../../models/dtos/Distribucion/NovedadCanillaDto';
|
||||
import type { CreateNovedadCanillaDto } from '../../../models/dtos/Distribucion/CreateNovedadCanillaDto';
|
||||
import type { UpdateNovedadCanillaDto } from '../../../models/dtos/Distribucion/UpdateNovedadCanillaDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 500 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
interface NovedadCanillaFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateNovedadCanillaDto | UpdateNovedadCanillaDto, idNovedad?: number) => Promise<void>;
|
||||
// Props para pasar datos necesarios:
|
||||
idCanilla: number | null; // Necesario para crear una nueva novedad
|
||||
nombreCanilla?: string; // Para mostrar en el título
|
||||
initialData?: NovedadCanillaDto | null; // Para editar
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const NovedadCanillaFormModal: React.FC<NovedadCanillaFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
idCanilla,
|
||||
nombreCanilla,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [fecha, setFecha] = useState<string>('');
|
||||
const [detalle, setDetalle] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData && initialData.idNovedad);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFecha(initialData?.fecha ? initialData.fecha.split('T')[0] : new Date().toISOString().split('T')[0]);
|
||||
setDetalle(initialData?.detalle || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.';
|
||||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).';
|
||||
|
||||
if (!detalle.trim()) errors.detalle = 'El detalle es obligatorio.';
|
||||
else if (detalle.trim().length > 250) errors.detalle = 'El detalle no puede exceder los 250 caracteres.';
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: 'fecha' | 'detalle') => {
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
if (isEditing && initialData) {
|
||||
const dataToSubmit: UpdateNovedadCanillaDto = { detalle };
|
||||
await onSubmit(dataToSubmit, initialData.idNovedad);
|
||||
} else if (idCanilla) { // Asegurarse que idCanilla esté disponible para creación
|
||||
const dataToSubmit: CreateNovedadCanillaDto = {
|
||||
idCanilla: idCanilla, // Tomado de props
|
||||
fecha,
|
||||
detalle,
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
} else {
|
||||
// Esto no debería pasar si la lógica de la página que llama al modal es correcta
|
||||
setLocalErrors(prev => ({...prev, general: "No se proporcionó ID de Canillita para crear la novedad."}))
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
// El error de API es manejado por la página padre a través de 'errorMessage'
|
||||
console.error("Error en submit de NovedadCanillaFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Novedad' : `Agregar Novedad para ${nombreCanilla || 'Canillita'}`}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{isEditing && initialData ? `Editando Novedad ID: ${initialData.idNovedad}` : `Canillita ID: ${idCanilla || 'N/A'}`}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<TextField
|
||||
label="Fecha Novedad"
|
||||
type="date"
|
||||
value={fecha}
|
||||
required
|
||||
onChange={(e) => {setFecha(e.target.value); handleInputChange('fecha');}}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
error={!!localErrors.fecha}
|
||||
helperText={localErrors.fecha || ''}
|
||||
disabled={loading || isEditing} // Fecha no se edita
|
||||
InputLabelProps={{ shrink: true }}
|
||||
autoFocus={!isEditing}
|
||||
/>
|
||||
<TextField
|
||||
label="Detalle Novedad"
|
||||
value={detalle}
|
||||
required
|
||||
onChange={(e) => {setDetalle(e.target.value); handleInputChange('detalle');}}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
error={!!localErrors.detalle}
|
||||
helperText={localErrors.detalle || (detalle ? `${250 - detalle.length} caracteres restantes` : '')}
|
||||
disabled={loading}
|
||||
inputProps={{ maxLength: 250 }}
|
||||
/>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.general && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.general}</Alert>}
|
||||
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained" disabled={loading}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Novedad')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovedadCanillaFormModal;
|
||||
@@ -1,67 +1,225 @@
|
||||
import React from 'react';
|
||||
import { Box, Checkbox, FormControlLabel, FormGroup, Typography, Paper } from '@mui/material'; // Quitar Grid
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Checkbox, FormControlLabel, FormGroup, Typography, Paper, Divider, TextField } from '@mui/material';
|
||||
import type { PermisoAsignadoDto } from '../../../models/dtos/Usuarios/PermisoAsignadoDto';
|
||||
|
||||
interface PermisosChecklistProps {
|
||||
permisosDisponibles: PermisoAsignadoDto[];
|
||||
permisosSeleccionados: Set<number>;
|
||||
onPermisoChange: (permisoId: number, asignado: boolean) => void;
|
||||
onPermisoChange: (permisoId: number, asignado: boolean, esPermisoSeccion?: boolean, moduloHijo?: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SECCION_PERMISSIONS_PREFIX = "SS";
|
||||
|
||||
// Mapeo de codAcc de sección a su módulo conceptual
|
||||
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
|
||||
if (codAcc === "SS001") return "Distribución";
|
||||
if (codAcc === "SS002") return "Contables";
|
||||
if (codAcc === "SS003") return "Impresión";
|
||||
if (codAcc === "SS004") return "Reportes";
|
||||
if (codAcc === "SS005") return "Radios";
|
||||
if (codAcc === "SS006") return "Usuarios";
|
||||
return null;
|
||||
};
|
||||
|
||||
// Función para determinar el módulo conceptual de un permiso individual
|
||||
const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
const moduloLower = permisoModulo.toLowerCase();
|
||||
|
||||
if (moduloLower.includes("distribuidores") ||
|
||||
moduloLower.includes("canillas") || // Cubre "Canillas" y "Movimientos Canillas"
|
||||
moduloLower.includes("publicaciones distribución") ||
|
||||
moduloLower.includes("zonas distribuidores") ||
|
||||
moduloLower.includes("movimientos distribuidores") ||
|
||||
moduloLower.includes("empresas") || // Módulo "Empresas"
|
||||
moduloLower.includes("otros destinos") || // Cubre "Otros Destinos" y "Salidas Otros Destinos"
|
||||
moduloLower.includes("ctrl. devoluciones")) {
|
||||
return "Distribución";
|
||||
}
|
||||
if (moduloLower.includes("cuentas pagos") ||
|
||||
moduloLower.includes("cuentas notas") ||
|
||||
moduloLower.includes("cuentas tipos pagos")) {
|
||||
return "Contables";
|
||||
}
|
||||
if (moduloLower.includes("impresión tiradas") ||
|
||||
moduloLower.includes("impresión bobinas") || // Cubre "Impresión Bobinas" y "Tipos Bobinas"
|
||||
moduloLower.includes("impresión plantas") ||
|
||||
moduloLower.includes("tipos bobinas")) { // Añadido explícitamente
|
||||
return "Impresión";
|
||||
}
|
||||
if (moduloLower.includes("radios")) { // Asumiendo que los permisos de radios tendrán "Radios" en su módulo
|
||||
return "Radios";
|
||||
}
|
||||
if (moduloLower.includes("usuarios") || // Cubre "Usuarios" y "Perfiles"
|
||||
moduloLower.includes("perfiles")) {
|
||||
return "Usuarios";
|
||||
}
|
||||
if (moduloLower.includes("reportes")) { // Para los permisos RRxxx
|
||||
return "Reportes";
|
||||
}
|
||||
if (moduloLower.includes("permisos")) { // Para "Permisos (Definición)"
|
||||
return "Permisos (Definición)";
|
||||
}
|
||||
return permisoModulo; // Fallback al nombre original si no coincide
|
||||
};
|
||||
|
||||
|
||||
const PermisosChecklist: React.FC<PermisosChecklistProps> = ({
|
||||
permisosDisponibles,
|
||||
permisosSeleccionados,
|
||||
onPermisoChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const permisosAgrupados = permisosDisponibles.reduce((acc, permiso) => {
|
||||
const modulo = permiso.modulo || 'Otros';
|
||||
if (!acc[modulo]) {
|
||||
acc[modulo] = [];
|
||||
const [filtrosModulo, setFiltrosModulo] = useState<Record<string, string>>({});
|
||||
|
||||
const handleFiltroChange = (moduloConceptual: string, texto: string) => {
|
||||
setFiltrosModulo(prev => ({ ...prev, [moduloConceptual]: texto.toLowerCase() }));
|
||||
};
|
||||
|
||||
const permisosDeSeccion = permisosDisponibles.filter(p => p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX));
|
||||
const permisosNormales = permisosDisponibles.filter(p => !p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX));
|
||||
|
||||
const permisosAgrupadosConceptualmente = permisosNormales.reduce((acc, permiso) => {
|
||||
const moduloConceptual = getModuloConceptualDelPermiso(permiso.modulo);
|
||||
if (!acc[moduloConceptual]) {
|
||||
acc[moduloConceptual] = [];
|
||||
}
|
||||
acc[modulo].push(permiso);
|
||||
acc[moduloConceptual].push(permiso);
|
||||
return acc;
|
||||
}, {} as Record<string, PermisoAsignadoDto[]>);
|
||||
|
||||
const ordenModulosPrincipales = ["Distribución", "Contables", "Impresión", "Radios", "Usuarios", "Reportes", "Permisos (Definición)"];
|
||||
// Añadir módulos que solo tienen permiso de sección (como Radios) pero no hijos (aún)
|
||||
permisosDeSeccion.forEach(ps => {
|
||||
const moduloConceptual = getModuloFromSeccionCodAcc(ps.codAcc);
|
||||
if (moduloConceptual && !ordenModulosPrincipales.includes(moduloConceptual)) {
|
||||
// Insertar después de un módulo conocido o al final si no hay un orden específico para él
|
||||
const indexReportes = ordenModulosPrincipales.indexOf("Reportes");
|
||||
if (indexReportes !== -1) {
|
||||
ordenModulosPrincipales.splice(indexReportes, 0, moduloConceptual);
|
||||
} else {
|
||||
ordenModulosPrincipales.push(moduloConceptual);
|
||||
}
|
||||
}
|
||||
if (moduloConceptual && !permisosAgrupadosConceptualmente[moduloConceptual]) {
|
||||
permisosAgrupadosConceptualmente[moduloConceptual] = []; // Asegurar que el grupo exista para Radios
|
||||
}
|
||||
});
|
||||
// Eliminar duplicados del orden por si acaso
|
||||
const ordenFinalModulos = [...new Set(ordenModulosPrincipales)];
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}> {/* Contenedor Flexbox */}
|
||||
{Object.entries(permisosAgrupados).map(([modulo, permisosDelModulo]) => (
|
||||
<Box
|
||||
key={modulo}
|
||||
sx={{
|
||||
flexGrow: 1, // Para que las columnas crezcan
|
||||
flexBasis: { xs: '100%', sm: 'calc(50% - 16px)', md: 'calc(33.333% - 16px)' }, // Simula xs, sm, md
|
||||
// El '-16px' es por el gap (si el gap es 2 = 16px). Ajustar si el gap es diferente.
|
||||
// Alternativamente, usar porcentajes más simples y dejar que el flexWrap maneje el layout.
|
||||
// flexBasis: '300px', // Un ancho base y dejar que flexWrap haga el resto
|
||||
minWidth: '280px', // Ancho mínimo para cada columna
|
||||
maxWidth: { xs: '100%', sm: '50%', md: '33.333%' }, // Máximo ancho
|
||||
}}
|
||||
>
|
||||
<Paper elevation={2} sx={{ p: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="subtitle1" gutterBottom component="div" sx={{ fontWeight: 'bold', borderBottom: 1, borderColor: 'divider', pb: 1, mb:1 }}>
|
||||
{modulo}
|
||||
</Typography>
|
||||
<FormGroup sx={{ flexGrow: 1}}> {/* Para que ocupe el espacio vertical */}
|
||||
{permisosDelModulo.map((permiso) => (
|
||||
<FormControlLabel
|
||||
key={permiso.id}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={permisosSeleccionados.has(permiso.id)}
|
||||
onChange={(e) => onPermisoChange(permiso.id, e.target.checked)}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">{`${permiso.descPermiso} (${permiso.codAcc})`}</Typography>}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2.5, justifyContent: 'center' }}>
|
||||
{ordenFinalModulos.map(moduloConceptual => { // Usar ordenFinalModulos
|
||||
const permisoSeccionAsociado = permisosDeSeccion.find(
|
||||
ps => getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptual
|
||||
);
|
||||
const permisosDelModuloHijosOriginales = permisosAgrupadosConceptualmente[moduloConceptual] || [];
|
||||
|
||||
// Condición para renderizar la sección
|
||||
if (!permisoSeccionAsociado && permisosDelModuloHijosOriginales.length === 0 && moduloConceptual !== "Permisos (Definición)") {
|
||||
// No renderizar si no hay permiso de sección Y no hay hijos, EXCEPTO para "Permisos (Definición)" que es especial
|
||||
return null;
|
||||
}
|
||||
if (moduloConceptual === "Permisos (Definición)" && permisosDelModuloHijosOriginales.length === 0) {
|
||||
// No renderizar "Permisos (Definición)" si no tiene hijos
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const esSeccionSeleccionada = permisoSeccionAsociado ? permisosSeleccionados.has(permisoSeccionAsociado.id) : false;
|
||||
const todosHijosSeleccionados = permisosDelModuloHijosOriginales.length > 0 && permisosDelModuloHijosOriginales.every(p => permisosSeleccionados.has(p.id));
|
||||
const ningunHijoSeleccionado = permisosDelModuloHijosOriginales.every(p => !permisosSeleccionados.has(p.id));
|
||||
const algunosHijosSeleccionados = !todosHijosSeleccionados && !ningunHijoSeleccionado && permisosDelModuloHijosOriginales.length > 0;
|
||||
|
||||
const textoFiltro = filtrosModulo[moduloConceptual] || '';
|
||||
const permisosDelModuloHijosFiltrados = textoFiltro
|
||||
? permisosDelModuloHijosOriginales.filter(permiso =>
|
||||
permiso.descPermiso.toLowerCase().includes(textoFiltro) ||
|
||||
permiso.codAcc.toLowerCase().includes(textoFiltro)
|
||||
)
|
||||
: permisosDelModuloHijosOriginales;
|
||||
|
||||
return (
|
||||
<Box key={moduloConceptual} sx={{ /* ... estilos del Box ... */
|
||||
flexGrow: 1,
|
||||
flexBasis: { xs: '100%', sm: 'calc(50% - 20px)', md: 'calc(33.333% - 20px)' },
|
||||
minWidth: '320px', // Aumentar un poco para el filtro
|
||||
maxWidth: { xs: '100%', sm: 'calc(50% - 10px)', md: 'calc(33.333% - 10px)'},
|
||||
}}>
|
||||
<Paper elevation={2} sx={{ p: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="subtitle1" gutterBottom component="div" sx={{ fontWeight: 'bold', borderBottom: 1, borderColor: 'divider', pb: 1, mb: 1 }}>
|
||||
{moduloConceptual}
|
||||
</Typography>
|
||||
|
||||
{permisosDelModuloHijosOriginales.length > 3 && ( // Mostrar filtro si hay más de 3 permisos
|
||||
<TextField
|
||||
label={`Buscar en ${moduloConceptual}...`}
|
||||
variant="standard"
|
||||
size="small"
|
||||
fullWidth
|
||||
value={filtrosModulo[moduloConceptual] || ''}
|
||||
onChange={(e) => handleFiltroChange(moduloConceptual, e.target.value)}
|
||||
sx={{ mb: 1, mt: 0.5 }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
</Paper>
|
||||
</Box>
|
||||
))}
|
||||
)}
|
||||
|
||||
{permisoSeccionAsociado && (
|
||||
<>
|
||||
<FormControlLabel
|
||||
label={`Acceso a Sección ${moduloConceptual}`} // Cambiado el Label
|
||||
labelPlacement="end"
|
||||
sx={{mb:1, '& .MuiFormControlLabel-label': { fontWeight: 'medium'}}}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={esSeccionSeleccionada && (permisosDelModuloHijosOriginales.length === 0 || todosHijosSeleccionados)}
|
||||
indeterminate={esSeccionSeleccionada && (algunosHijosSeleccionados || (ningunHijoSeleccionado && permisosDelModuloHijosOriginales.length > 0))}
|
||||
onChange={() => onPermisoChange(permisoSeccionAsociado.id, false, true, moduloConceptual)}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* Mostrar Divider solo si hay hijos o es la sección Radios (que queremos que aparezca aunque esté vacía) */}
|
||||
{(permisosDelModuloHijosOriginales.length > 0 || moduloConceptual === "Radios") && <Divider sx={{mb:1.5}}/> }
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box sx={{ maxHeight: '280px', overflowY: 'auto', flexGrow: 1 }}> {/* Aumentar un poco maxHeight */}
|
||||
<FormGroup sx={{ pl: permisoSeccionAsociado ? 2 : 0 }}>
|
||||
{permisosDelModuloHijosFiltrados.map((permiso) => (
|
||||
<FormControlLabel
|
||||
key={permiso.id}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={permisosSeleccionados.has(permiso.id)}
|
||||
onChange={(e) => onPermisoChange(permiso.id, e.target.checked, false, moduloConceptual)}
|
||||
disabled={disabled || (permisoSeccionAsociado && !esSeccionSeleccionada)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">{`${permiso.descPermiso} (${permiso.codAcc})`}</Typography>}
|
||||
/>
|
||||
))}
|
||||
{textoFiltro && permisosDelModuloHijosFiltrados.length === 0 && permisosDelModuloHijosOriginales.length > 0 && (
|
||||
<Typography variant="caption" sx={{p:1, fontStyle: 'italic', textAlign: 'center'}}>
|
||||
No hay permisos que coincidan con "{textoFiltro}".
|
||||
</Typography>
|
||||
)}
|
||||
{/* Mensaje si no hay hijos en general (y no es por filtro) */}
|
||||
{permisosDelModuloHijosOriginales.length === 0 && !textoFiltro && moduloConceptual !== "Permisos (Definición)" && (
|
||||
<Typography variant="caption" sx={{p:1, fontStyle: 'italic', textAlign: 'center'}}>
|
||||
No hay permisos específicos en esta sección.
|
||||
</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user