feat: Implementación CRUD Canillitas, Distribuidores y Precios de Publicación
Backend API:
- Canillitas (`dist_dtCanillas`):
- Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Lógica para manejo de `Accionista`, `Baja`, `FechaBaja`.
- Auditoría en `dist_dtCanillas_H`.
- Validación de legajo único y lógica de empresa vs accionista.
- Distribuidores (`dist_dtDistribuidores`):
- Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Auditoría en `dist_dtDistribuidores_H`.
- Creación de saldos iniciales para el nuevo distribuidor en todas las empresas.
- Verificación de NroDoc único y Nombre opcionalmente único.
- Precios de Publicación (`dist_Precios`):
- Implementado CRUD básico (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Endpoints anidados bajo `/publicaciones/{idPublicacion}/precios`.
- Lógica de negocio para cerrar período de precio anterior al crear uno nuevo.
- Lógica de negocio para reabrir período de precio anterior al eliminar el último.
- Auditoría en `dist_Precios_H`.
- Auditoría en Eliminación de Publicaciones:
- Extendido `PublicacionService.EliminarAsync` para eliminar en cascada registros de precios, recargos, porcentajes de pago (distribuidores y canillitas) y secciones de publicación.
- Repositorios correspondientes (`PrecioRepository`, `RecargoZonaRepository`, `PorcPagoRepository`, `PorcMonCanillaRepository`, `PubliSeccionRepository`) actualizados con métodos `DeleteByPublicacionIdAsync` que registran en sus respectivas tablas `_H` (si existen y se implementó la lógica).
- Asegurada la correcta propagación del `idUsuario` para la auditoría en cascada.
- Correcciones de Nulabilidad:
- Ajustados los métodos `MapToDto` y su uso en `CanillaService` y `PublicacionService` para manejar correctamente tipos anulables.
Frontend React:
- Canillitas:
- `canillaService.ts`.
- `CanillaFormModal.tsx` con selectores para Zona y Empresa, y lógica de Accionista.
- `GestionarCanillitasPage.tsx` con filtros, paginación, y acciones (editar, toggle baja).
- Distribuidores:
- `distribuidorService.ts`.
- `DistribuidorFormModal.tsx` con múltiples campos y selector de Zona.
- `GestionarDistribuidoresPage.tsx` con filtros, paginación, y acciones (editar, eliminar).
- Precios de Publicación:
- `precioService.ts`.
- `PrecioFormModal.tsx` para crear/editar períodos de precios (VigenciaD, VigenciaH opcional, precios por día).
- `GestionarPreciosPublicacionPage.tsx` accesible desde la gestión de publicaciones, para listar y gestionar los períodos de precios de una publicación específica.
- Layout:
- Reemplazado el uso de `Grid` por `Box` con Flexbox en `CanillaFormModal`, `GestionarCanillitasPage` (filtros), `DistribuidorFormModal` y `PrecioFormModal` para resolver problemas de tipos y mejorar la consistencia del layout de formularios.
- Navegación:
- Actualizadas las rutas y pestañas para los nuevos módulos y sub-módulos.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||
import type { TipoPago } from '../../models/Entities/TipoPago';
|
||||
import type { CreateTipoPagoDto } from '../../models/dtos/tiposPago/CreateTipoPagoDto';
|
||||
import type { TipoPago } from '../../../models/Entities/TipoPago';
|
||||
import type { CreateTipoPagoDto } from '../../../models/dtos/tiposPago/CreateTipoPagoDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
239
Frontend/src/components/Modals/Distribucion/CanillaFormModal.tsx
Normal file
239
Frontend/src/components/Modals/Distribucion/CanillaFormModal.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox
|
||||
} from '@mui/material';
|
||||
import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto';
|
||||
import type { CreateCanillaDto } from '../../../models/dtos/Distribucion/CreateCanillaDto';
|
||||
import type { UpdateCanillaDto } from '../../../models/dtos/Distribucion/UpdateCanillaDto';
|
||||
import type { ZonaDto } from '../../../models/dtos/Zonas/ZonaDto'; // Para el dropdown de zonas
|
||||
import type { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto'; // Para el dropdown de empresas
|
||||
import zonaService from '../../../services/Distribucion/zonaService';
|
||||
import empresaService from '../../../services/Distribucion/empresaService';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 600 }, // Un poco más ancho
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
interface CanillaFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateCanillaDto | UpdateCanillaDto, id?: number) => Promise<void>;
|
||||
initialData?: CanillaDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const CanillaFormModal: React.FC<CanillaFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [legajo, setLegajo] = useState<string>(''); // Manejar como string para el TextField
|
||||
const [nomApe, setNomApe] = useState('');
|
||||
const [parada, setParada] = useState('');
|
||||
const [idZona, setIdZona] = useState<number | string>('');
|
||||
const [accionista, setAccionista] = useState(false);
|
||||
const [obs, setObs] = useState('');
|
||||
const [empresa, setEmpresa] = useState<number | string>(0); // 0 para N/A (accionista)
|
||||
|
||||
const [zonas, setZonas] = useState<ZonaDto[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDropdownData = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [zonasData, empresasData] = await Promise.all([
|
||||
zonaService.getAllZonas(), // Asume que este servicio devuelve zonas activas
|
||||
empresaService.getAllEmpresas()
|
||||
]);
|
||||
setZonas(zonasData);
|
||||
setEmpresas(empresasData);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar datos para dropdowns", error);
|
||||
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar zonas/empresas.'}));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchDropdownData();
|
||||
setLegajo(initialData?.legajo?.toString() || '');
|
||||
setNomApe(initialData?.nomApe || '');
|
||||
setParada(initialData?.parada || '');
|
||||
setIdZona(initialData?.idZona || '');
|
||||
setAccionista(initialData ? initialData.accionista : false);
|
||||
setObs(initialData?.obs || '');
|
||||
setEmpresa(initialData ? initialData.empresa : 0); // Si es accionista, empresa es 0
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!nomApe.trim()) errors.nomApe = 'Nombre y Apellido son obligatorios.';
|
||||
if (!idZona) errors.idZona = 'Debe seleccionar una zona.';
|
||||
|
||||
const legajoNum = legajo ? parseInt(legajo, 10) : null;
|
||||
if (legajo.trim() && (isNaN(legajoNum!) || legajoNum! < 0)) {
|
||||
errors.legajo = 'Legajo debe ser un número positivo o vacío.';
|
||||
}
|
||||
|
||||
// Lógica de empresa y accionista
|
||||
if (accionista && empresa !== 0 && empresa !== '') {
|
||||
errors.empresa = 'Si es Accionista, la Empresa debe ser N/A (0).';
|
||||
}
|
||||
if (!accionista && (empresa === 0 || empresa === '')) {
|
||||
errors.empresa = 'Si no es Accionista, debe seleccionar una Empresa.';
|
||||
}
|
||||
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
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 {
|
||||
const legajoParsed = legajo.trim() ? parseInt(legajo, 10) : null;
|
||||
|
||||
const dataToSubmit = {
|
||||
legajo: legajoParsed,
|
||||
nomApe,
|
||||
parada: parada || undefined,
|
||||
idZona: Number(idZona),
|
||||
accionista,
|
||||
obs: obs || undefined,
|
||||
empresa: Number(empresa)
|
||||
};
|
||||
|
||||
if (isEditing && initialData) {
|
||||
await onSubmit(dataToSubmit as UpdateCanillaDto, initialData.idCanilla);
|
||||
} else {
|
||||
await onSubmit(dataToSubmit as CreateCanillaDto);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de CanillaFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Canillita' : 'Agregar Nuevo Canillita'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
{/* SECCIÓN DE CAMPOS CON BOX Y FLEXBOX */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'flex-start' }}>
|
||||
<TextField label="Legajo" value={legajo} type="number"
|
||||
onChange={(e) => {setLegajo(e.target.value); handleInputChange('legajo');}}
|
||||
margin="dense" fullWidth error={!!localErrors.legajo} helperText={localErrors.legajo || ''}
|
||||
disabled={loading}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
|
||||
/>
|
||||
<TextField label="Nombre y Apellido" value={nomApe} required
|
||||
onChange={(e) => {setNomApe(e.target.value); handleInputChange('nomApe');}}
|
||||
margin="dense" fullWidth error={!!localErrors.nomApe} helperText={localErrors.nomApe || ''}
|
||||
disabled={loading} autoFocus={!isEditing}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
|
||||
/>
|
||||
</Box>
|
||||
<TextField label="Parada (Dirección)" value={parada}
|
||||
onChange={(e) => setParada(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={2} disabled={loading}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'flex-start', mt: 1 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idZona} sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}>
|
||||
<InputLabel id="zona-select-label" required>Zona</InputLabel>
|
||||
<Select labelId="zona-select-label" label="Zona" value={idZona}
|
||||
onChange={(e) => {setIdZona(e.target.value as number); handleInputChange('idZona');}}
|
||||
disabled={loading || loadingDropdowns}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione una zona</em></MenuItem>
|
||||
{zonas.map((z) => (<MenuItem key={z.idZona} value={z.idZona}>{z.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idZona && <Typography color="error" variant="caption">{localErrors.idZona}</Typography>}
|
||||
</FormControl>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.empresa} sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}>
|
||||
<InputLabel id="empresa-select-label">Empresa</InputLabel>
|
||||
<Select labelId="empresa-select-label" label="Empresa" value={empresa}
|
||||
onChange={(e) => {setEmpresa(e.target.value as number); handleInputChange('empresa');}}
|
||||
disabled={loading || loadingDropdowns || accionista}
|
||||
>
|
||||
<MenuItem value={0}><em>N/A (Accionista)</em></MenuItem>
|
||||
{empresas.map((e) => (<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.empresa && <Typography color="error" variant="caption">{localErrors.empresa}</Typography>}
|
||||
</FormControl>
|
||||
</Box>
|
||||
<TextField label="Observaciones" value={obs}
|
||||
onChange={(e) => setObs(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{mt:1}}
|
||||
/>
|
||||
<Box sx={{mt:1}}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={accionista} onChange={(e) => {
|
||||
setAccionista(e.target.checked);
|
||||
if (e.target.checked) setEmpresa(0);
|
||||
handleInputChange('accionista');
|
||||
handleInputChange('empresa');
|
||||
}} disabled={loading}/>}
|
||||
label="Es Accionista"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</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 || loadingDropdowns}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Crear Canillita')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CanillaFormModal;
|
||||
@@ -0,0 +1,239 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import type { DistribuidorDto } from '../../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { CreateDistribuidorDto } from '../../../models/dtos/Distribucion/CreateDistribuidorDto';
|
||||
import type { UpdateDistribuidorDto } from '../../../models/dtos/Distribucion/UpdateDistribuidorDto';
|
||||
import type { ZonaDto } from '../../../models/dtos/Zonas/ZonaDto';
|
||||
import zonaService from '../../../services/Distribucion/zonaService';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '95%', sm: '80%', md: '600px' },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
interface DistribuidorFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateDistribuidorDto | UpdateDistribuidorDto, id?: number) => Promise<void>;
|
||||
initialData?: DistribuidorDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const DistribuidorFormModal: React.FC<DistribuidorFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [nombre, setNombre] = useState('');
|
||||
const [contacto, setContacto] = useState('');
|
||||
const [nroDoc, setNroDoc] = useState('');
|
||||
const [idZona, setIdZona] = useState<number | string>(''); // Puede ser string vacío
|
||||
const [calle, setCalle] = useState('');
|
||||
const [numero, setNumero] = useState('');
|
||||
const [piso, setPiso] = useState('');
|
||||
const [depto, setDepto] = useState('');
|
||||
const [telefono, setTelefono] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [localidad, setLocalidad] = useState('');
|
||||
|
||||
const [zonas, setZonas] = useState<ZonaDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingZonas, setLoadingZonas] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchZonas = async () => {
|
||||
setLoadingZonas(true);
|
||||
try {
|
||||
const data = await zonaService.getAllZonas(); // Solo activas por defecto
|
||||
setZonas(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar zonas", error);
|
||||
setLocalErrors(prev => ({...prev, zonas: 'Error al cargar zonas.'}));
|
||||
} finally {
|
||||
setLoadingZonas(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchZonas();
|
||||
setNombre(initialData?.nombre || '');
|
||||
setContacto(initialData?.contacto || '');
|
||||
setNroDoc(initialData?.nroDoc || '');
|
||||
setIdZona(initialData?.idZona || '');
|
||||
setCalle(initialData?.calle || '');
|
||||
setNumero(initialData?.numero || '');
|
||||
setPiso(initialData?.piso || '');
|
||||
setDepto(initialData?.depto || '');
|
||||
setTelefono(initialData?.telefono || '');
|
||||
setEmail(initialData?.email || '');
|
||||
setLocalidad(initialData?.localidad || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!nombre.trim()) errors.nombre = 'El nombre es obligatorio.';
|
||||
if (!nroDoc.trim()) errors.nroDoc = 'El Nro. Documento es obligatorio.';
|
||||
else if (nroDoc.trim().length > 11) errors.nroDoc = 'El Nro. Documento no debe exceder los 11 caracteres.';
|
||||
|
||||
if (email.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
errors.email = 'Formato de email inválido.';
|
||||
}
|
||||
// No es obligatorio que IdZona tenga valor
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
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 {
|
||||
const dataToSubmit = {
|
||||
nombre,
|
||||
contacto: contacto || undefined,
|
||||
nroDoc,
|
||||
idZona: idZona ? Number(idZona) : null, // Enviar null si está vacío
|
||||
calle: calle || undefined,
|
||||
numero: numero || undefined,
|
||||
piso: piso || undefined,
|
||||
depto: depto || undefined,
|
||||
telefono: telefono || undefined,
|
||||
email: email || undefined,
|
||||
localidad: localidad || undefined,
|
||||
};
|
||||
|
||||
if (isEditing && initialData) {
|
||||
await onSubmit(dataToSubmit as UpdateDistribuidorDto, initialData.idDistribuidor);
|
||||
} else {
|
||||
await onSubmit(dataToSubmit as CreateDistribuidorDto);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de DistribuidorFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Distribuidor' : 'Agregar Nuevo Distribuidor'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
{/* Usando Box con Flexbox para layout de dos columnas responsivo */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
<TextField label="Nombre Distribuidor" value={nombre} required
|
||||
onChange={(e) => {setNombre(e.target.value); handleInputChange('nombre');}}
|
||||
margin="dense" fullWidth error={!!localErrors.nombre} helperText={localErrors.nombre || ''}
|
||||
disabled={loading} autoFocus={!isEditing}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} // -8px es la mitad del gap
|
||||
/>
|
||||
<TextField label="Nro. Documento (CUIT/CUIL)" value={nroDoc} required
|
||||
onChange={(e) => {setNroDoc(e.target.value); handleInputChange('nroDoc');}}
|
||||
margin="dense" fullWidth error={!!localErrors.nroDoc} helperText={localErrors.nroDoc || ''}
|
||||
disabled={loading}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt:1 }}>
|
||||
<TextField label="Contacto" value={contacto}
|
||||
onChange={(e) => setContacto(e.target.value)}
|
||||
margin="dense" fullWidth disabled={loading}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
|
||||
/>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idZona} sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}>
|
||||
<InputLabel id="zona-dist-select-label">Zona (Opcional)</InputLabel>
|
||||
<Select labelId="zona-dist-select-label" label="Zona (Opcional)" value={idZona}
|
||||
onChange={(e) => {setIdZona(e.target.value as number); handleInputChange('idZona');}}
|
||||
disabled={loading || loadingZonas}
|
||||
>
|
||||
<MenuItem value=""><em>Ninguna</em></MenuItem>
|
||||
{zonas.map((z) => (<MenuItem key={z.idZona} value={z.idZona}>{z.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt:1 }}>
|
||||
<TextField label="Calle" value={calle} onChange={(e) => setCalle(e.target.value)} margin="dense"
|
||||
sx={{ flex: 3, minWidth: 'calc(60% - 8px)'}} // Más ancho para la calle
|
||||
disabled={loading}
|
||||
/>
|
||||
<TextField label="Número" value={numero} onChange={(e) => setNumero(e.target.value)} margin="dense"
|
||||
sx={{ flex: 1, minWidth: 'calc(40% - 8px)'}}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt:1 }}>
|
||||
<TextField label="Piso" value={piso} onChange={(e) => setPiso(e.target.value)} margin="dense"
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}
|
||||
disabled={loading}
|
||||
/>
|
||||
<TextField label="Depto." value={depto} onChange={(e) => setDepto(e.target.value)} margin="dense"
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt:1 }}>
|
||||
<TextField label="Teléfono" value={telefono} onChange={(e) => setTelefono(e.target.value)} margin="dense"
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}
|
||||
disabled={loading}
|
||||
/>
|
||||
<TextField label="Email" value={email} type="email"
|
||||
onChange={(e) => {setEmail(e.target.value); handleInputChange('email');}}
|
||||
margin="dense" fullWidth error={!!localErrors.email} helperText={localErrors.email || ''}
|
||||
disabled={loading}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
|
||||
/>
|
||||
</Box>
|
||||
<TextField label="Localidad" value={localidad} onChange={(e) => setLocalidad(e.target.value)} margin="dense" fullWidth disabled={loading} sx={{mt:1}}/>
|
||||
</Box>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.zonas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.zonas}</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 || loadingZonas}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Crear Distribuidor')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistribuidorFormModal;
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||
import type { EmpresaDto } from '../../models/dtos/Empresas/EmpresaDto';
|
||||
import type { CreateEmpresaDto } from '../../models/dtos/Empresas/CreateEmpresaDto';
|
||||
import type { UpdateEmpresaDto } from '../../models/dtos/Empresas/UpdateEmpresaDto'; // Necesitamos Update DTO también
|
||||
import type { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { CreateEmpresaDto } from '../../../models/dtos/Distribucion/CreateEmpresaDto';
|
||||
import type { UpdateEmpresaDto } from '../../../models/dtos/Distribucion/UpdateEmpresaDto'; // Necesitamos Update DTO también
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
@@ -0,0 +1,128 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
|
||||
import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto';
|
||||
import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto';
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo) ... */
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
interface OtroDestinoFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateOtroDestinoDto | (UpdateOtroDestinoDto & { idDestino: number })) => Promise<void>;
|
||||
initialData?: OtroDestinoDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const OtroDestinoFormModal: React.FC<OtroDestinoFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [nombre, setNombre] = useState('');
|
||||
const [obs, setObs] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrorNombre, setLocalErrorNombre] = useState<string | null>(null);
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setNombre(initialData?.nombre || '');
|
||||
setObs(initialData?.obs || '');
|
||||
setLocalErrorNombre(null);
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const handleInputChange = () => {
|
||||
if (localErrorNombre) setLocalErrorNombre(null);
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setLocalErrorNombre(null);
|
||||
clearErrorMessage();
|
||||
|
||||
if (!nombre.trim()) {
|
||||
setLocalErrorNombre('El nombre es obligatorio.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const dataToSubmit = { nombre, obs: obs || undefined };
|
||||
|
||||
if (isEditing && initialData) {
|
||||
await onSubmit({ ...dataToSubmit, idDestino: initialData.idDestino });
|
||||
} else {
|
||||
await onSubmit(dataToSubmit as CreateOtroDestinoDto);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de OtroDestinoFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2">
|
||||
{isEditing ? 'Editar Otro Destino' : 'Agregar Nuevo Destino'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<TextField
|
||||
label="Nombre"
|
||||
fullWidth
|
||||
required
|
||||
value={nombre}
|
||||
onChange={(e) => { setNombre(e.target.value); handleInputChange(); }}
|
||||
margin="normal"
|
||||
error={!!localErrorNombre}
|
||||
helperText={localErrorNombre || ''}
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
label="Observación (Opcional)"
|
||||
fullWidth
|
||||
value={obs}
|
||||
onChange={(e) => setObs(e.target.value)}
|
||||
margin="normal"
|
||||
multiline
|
||||
rows={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
{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} /> : (isEditing ? 'Guardar Cambios' : 'Agregar')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default OtroDestinoFormModal;
|
||||
225
Frontend/src/components/Modals/Distribucion/PrecioFormModal.tsx
Normal file
225
Frontend/src/components/Modals/Distribucion/PrecioFormModal.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
InputAdornment
|
||||
// Quitar Grid si no se usa
|
||||
} from '@mui/material';
|
||||
import type { PrecioDto } from '../../../models/dtos/Distribucion/PrecioDto';
|
||||
import type { CreatePrecioDto } from '../../../models/dtos/Distribucion/CreatePrecioDto';
|
||||
import type { UpdatePrecioDto } from '../../../models/dtos/Distribucion/UpdatePrecioDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '95%', sm: '80%', md: '700px' },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
const diasSemana = ["Lunes", "Martes", "Miercoles", "Jueves", "Viernes", "Sabado", "Domingo"] as const;
|
||||
type DiaSemana = typeof diasSemana[number];
|
||||
|
||||
interface PrecioFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreatePrecioDto | UpdatePrecioDto, idPrecio?: number) => Promise<void>;
|
||||
idPublicacion: number;
|
||||
initialData?: PrecioDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const PrecioFormModal: React.FC<PrecioFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
idPublicacion,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [vigenciaD, setVigenciaD] = useState('');
|
||||
const [vigenciaH, setVigenciaH] = useState('');
|
||||
const [preciosDia, setPreciosDia] = useState<Record<DiaSemana, string>>({
|
||||
Lunes: '', Martes: '', Miercoles: '', Jueves: '', Viernes: '', Sabado: '', Domingo: ''
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setVigenciaD(initialData?.vigenciaD || '');
|
||||
setVigenciaH(initialData?.vigenciaH || '');
|
||||
const initialPrecios: Record<DiaSemana, string> = { Lunes: '', Martes: '', Miercoles: '', Jueves: '', Viernes: '', Sabado: '', Domingo: '' };
|
||||
if (initialData) {
|
||||
diasSemana.forEach(dia => {
|
||||
const key = dia.toLowerCase() as keyof Omit<PrecioDto, 'idPrecio' | 'idPublicacion' | 'vigenciaD' | 'vigenciaH'>;
|
||||
initialPrecios[dia] = initialData[key]?.toString() || '';
|
||||
});
|
||||
}
|
||||
setPreciosDia(initialPrecios);
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!isEditing && !vigenciaD.trim()) {
|
||||
errors.vigenciaD = 'La Vigencia Desde es obligatoria.';
|
||||
} else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) {
|
||||
errors.vigenciaD = 'Formato de fecha inválido (YYYY-MM-DD).';
|
||||
}
|
||||
|
||||
if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) {
|
||||
errors.vigenciaH = 'Formato de fecha inválido (YYYY-MM-DD).';
|
||||
} else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) {
|
||||
errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.';
|
||||
}
|
||||
|
||||
let alMenosUnPrecio = false;
|
||||
diasSemana.forEach(dia => {
|
||||
const valor = preciosDia[dia];
|
||||
if (valor.trim()) {
|
||||
alMenosUnPrecio = true;
|
||||
if (isNaN(parseFloat(valor)) || parseFloat(valor) < 0) {
|
||||
errors[dia.toLowerCase()] = `Precio de ${dia} inválido.`;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!isEditing && !alMenosUnPrecio) {
|
||||
errors.dias = 'Debe ingresar al menos un precio para un día de la semana.';
|
||||
}
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handlePrecioDiaChange = (dia: DiaSemana, value: string) => {
|
||||
setPreciosDia(prev => ({ ...prev, [dia]: value }));
|
||||
if (localErrors[dia.toLowerCase()] || localErrors.dias) {
|
||||
setLocalErrors(prev => ({ ...prev, [dia.toLowerCase()]: null, dias: null }));
|
||||
}
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
const handleDateChange = (setter: React.Dispatch<React.SetStateAction<string>>, fieldName: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setter(e.target.value);
|
||||
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 {
|
||||
const preciosNumericos: Partial<Record<keyof Omit<PrecioDto, 'idPrecio'|'idPublicacion'|'vigenciaD'|'vigenciaH'>, number | null>> = {};
|
||||
diasSemana.forEach(dia => {
|
||||
const valor = preciosDia[dia].trim();
|
||||
// Convertir el nombre del día a la clave correcta del DTO (ej. "Lunes" -> "lunes")
|
||||
const key = dia.toLowerCase() as keyof typeof preciosNumericos;
|
||||
preciosNumericos[key] = valor ? parseFloat(valor) : null;
|
||||
});
|
||||
|
||||
|
||||
if (isEditing && initialData) {
|
||||
const dataToSubmit: UpdatePrecioDto = {
|
||||
vigenciaH: vigenciaH.trim() ? vigenciaH : null,
|
||||
...preciosNumericos // Spread de los precios de los días
|
||||
};
|
||||
await onSubmit(dataToSubmit, initialData.idPrecio);
|
||||
} else {
|
||||
const dataToSubmit: CreatePrecioDto = {
|
||||
idPublicacion,
|
||||
vigenciaD,
|
||||
...preciosNumericos
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de PrecioFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Período de Precio' : 'Agregar Nuevo Período de Precio'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
{/* Sección de Vigencias */}
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
|
||||
<TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing}
|
||||
onChange={handleDateChange(setVigenciaD, 'vigenciaD')} margin="dense"
|
||||
error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''}
|
||||
disabled={loading || isEditing}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ flex: 1, minWidth: isEditing ? 'calc(50% - 8px)' : '100%' }}
|
||||
autoFocus={!isEditing}
|
||||
/>
|
||||
{isEditing && (
|
||||
<TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH}
|
||||
onChange={handleDateChange(setVigenciaH, 'vigenciaH')} margin="dense"
|
||||
error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''}
|
||||
disabled={loading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Sección de Precios por Día con Flexbox */}
|
||||
<Typography variant="subtitle1" sx={{mt: 2, mb: 1}}>Precios por Día:</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
|
||||
{diasSemana.map(dia => (
|
||||
<Box key={dia} sx={{ flexBasis: { xs: 'calc(50% - 8px)', sm: 'calc(33.33% - 11px)', md: 'calc(25% - 12px)' }, minWidth: '120px' }}>
|
||||
{/* El ajuste de -Xpx en flexBasis es aproximado para compensar el gap.
|
||||
Para 3 columnas (33.33%): gap es 16px, se distribuye entre 2 espacios -> 16/2 * 2/3 = ~11px
|
||||
Para 4 columnas (25%): gap es 16px, se distribuye entre 3 espacios -> 16/3 * 3/4 = 12px
|
||||
*/}
|
||||
<TextField label={dia} type="number"
|
||||
value={preciosDia[dia]}
|
||||
onChange={(e) => handlePrecioDiaChange(dia, e.target.value)}
|
||||
margin="dense" fullWidth
|
||||
error={!!localErrors[dia.toLowerCase()]} helperText={localErrors[dia.toLowerCase()] || ''}
|
||||
disabled={loading}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }}
|
||||
inputProps={{ step: "0.01", lang:"es-AR" }}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
{localErrors.dias && <Alert severity="error" sx={{width: '100%', mt:1}}>{localErrors.dias}</Alert>}
|
||||
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{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} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Período')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrecioFormModal;
|
||||
@@ -0,0 +1,180 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox
|
||||
} from '@mui/material';
|
||||
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 { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto';
|
||||
import empresaService from '../../../services/Distribucion/empresaService'; // Para cargar empresas
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo) ... */
|
||||
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 PublicacionFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreatePublicacionDto | UpdatePublicacionDto, id?: number) => Promise<void>;
|
||||
initialData?: PublicacionDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const PublicacionFormModal: React.FC<PublicacionFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [nombre, setNombre] = useState('');
|
||||
const [observacion, setObservacion] = useState('');
|
||||
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
||||
const [ctrlDevoluciones, setCtrlDevoluciones] = useState(false);
|
||||
const [habilitada, setHabilitada] = useState(true);
|
||||
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingEmpresas, setLoadingEmpresas] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEmpresas = async () => {
|
||||
setLoadingEmpresas(true);
|
||||
try {
|
||||
const data = await empresaService.getAllEmpresas();
|
||||
setEmpresas(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar empresas", error);
|
||||
setLocalErrors(prev => ({...prev, empresas: 'Error al cargar empresas.'}));
|
||||
} finally {
|
||||
setLoadingEmpresas(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchEmpresas();
|
||||
setNombre(initialData?.nombre || '');
|
||||
setObservacion(initialData?.observacion || '');
|
||||
setIdEmpresa(initialData?.idEmpresa || '');
|
||||
setCtrlDevoluciones(initialData ? initialData.ctrlDevoluciones : false);
|
||||
setHabilitada(initialData ? initialData.habilitada : true);
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!nombre.trim()) errors.nombre = 'El nombre es obligatorio.';
|
||||
if (!idEmpresa) errors.idEmpresa = 'Debe seleccionar una empresa.';
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
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 {
|
||||
const dataToSubmit = {
|
||||
nombre,
|
||||
observacion: observacion || undefined,
|
||||
idEmpresa: Number(idEmpresa),
|
||||
ctrlDevoluciones,
|
||||
habilitada
|
||||
};
|
||||
|
||||
if (isEditing && initialData) {
|
||||
await onSubmit(dataToSubmit as UpdatePublicacionDto, initialData.idPublicacion);
|
||||
} else {
|
||||
await onSubmit(dataToSubmit as CreatePublicacionDto);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de PublicacionFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Publicación' : 'Agregar Nueva Publicación'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
<TextField label="Nombre Publicación" value={nombre} required
|
||||
onChange={(e) => {setNombre(e.target.value); handleInputChange('nombre');}}
|
||||
margin="dense" fullWidth error={!!localErrors.nombre} helperText={localErrors.nombre || ''}
|
||||
disabled={loading} autoFocus={!isEditing}
|
||||
/>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idEmpresa}>
|
||||
<InputLabel id="empresa-pub-select-label" required>Empresa</InputLabel>
|
||||
<Select labelId="empresa-pub-select-label" label="Empresa" value={idEmpresa}
|
||||
onChange={(e) => {setIdEmpresa(e.target.value as number); handleInputChange('idEmpresa');}}
|
||||
disabled={loading || loadingEmpresas}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione una empresa</em></MenuItem>
|
||||
{empresas.map((e) => (<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idEmpresa && <Typography color="error" variant="caption">{localErrors.idEmpresa}</Typography>}
|
||||
</FormControl>
|
||||
<TextField label="Observación (Opcional)" value={observacion}
|
||||
onChange={(e) => setObservacion(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={3} disabled={loading}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-around', mt: 1, flexWrap: 'wrap' }}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={ctrlDevoluciones} onChange={(e) => setCtrlDevoluciones(e.target.checked)} disabled={loading}/>}
|
||||
label="Controla Devoluciones"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={habilitada} onChange={(e) => setHabilitada(e.target.checked)} disabled={loading}/>}
|
||||
label="Habilitada"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.empresas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.empresas}</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 || loadingEmpresas}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Crear Publicación')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicacionFormModal;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||
import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // Usamos el DTO de la API para listar
|
||||
import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto';
|
||||
import type { ZonaDto } from '../../../models/dtos/Zonas/ZonaDto'; // Usamos el DTO de la API para listar
|
||||
import type { CreateZonaDto } from '../../../models/dtos/Zonas/CreateZonaDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
|
||||
import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto';
|
||||
import type { UpdateEstadoBobinaDto } from '../../models/dtos/Impresion/UpdateEstadoBobinaDto';
|
||||
import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto';
|
||||
import type { CreateEstadoBobinaDto } from '../../../models/dtos/Impresion/CreateEstadoBobinaDto';
|
||||
import type { UpdateEstadoBobinaDto } from '../../../models/dtos/Impresion/UpdateEstadoBobinaDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
|
||||
import type { CreatePlantaDto } from '../../models/dtos/Impresion/CreatePlantaDto';
|
||||
import type { UpdatePlantaDto } from '../../models/dtos/Impresion/UpdatePlantaDto';
|
||||
import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto';
|
||||
import type { CreatePlantaDto } from '../../../models/dtos/Impresion/CreatePlantaDto';
|
||||
import type { UpdatePlantaDto } from '../../../models/dtos/Impresion/UpdatePlantaDto';
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo que otros modales) ... */
|
||||
position: 'absolute' as 'absolute',
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
|
||||
import type { CreateTipoBobinaDto } from '../../models/dtos/Impresion/CreateTipoBobinaDto';
|
||||
import type { UpdateTipoBobinaDto } from '../../models/dtos/Impresion/UpdateTipoBobinaDto';
|
||||
import type { TipoBobinaDto } from '../../../models/dtos/Impresion/TipoBobinaDto';
|
||||
import type { CreateTipoBobinaDto } from '../../../models/dtos/Impresion/CreateTipoBobinaDto';
|
||||
import type { UpdateTipoBobinaDto } from '../../../models/dtos/Impresion/UpdateTipoBobinaDto';
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo que otros modales) ... */
|
||||
position: 'absolute' as 'absolute',
|
||||
205
Frontend/src/components/Modals/Usuarios/ChangePasswordModal.tsx
Normal file
205
Frontend/src/components/Modals/Usuarios/ChangePasswordModal.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../../../contexts/AuthContext';
|
||||
import authService from '../../../services/Usuarios/authService';
|
||||
import type { ChangePasswordRequestDto } from '../../../models/dtos/Usuarios/ChangePasswordRequestDto';
|
||||
import axios from 'axios';
|
||||
|
||||
import { Modal, Box, Typography, TextField, Button, Alert, CircularProgress, Backdrop } from '@mui/material';
|
||||
|
||||
const style = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
interface ChangePasswordModalProps {
|
||||
open: boolean;
|
||||
onClose: (success: boolean) => void;
|
||||
isFirstLogin?: boolean;
|
||||
}
|
||||
|
||||
const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({ open, onClose, isFirstLogin }) => {
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmNewPassword, setConfirmNewPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
// Ya no necesitamos passwordChangeCompleted ni contextIsFirstLogin aquí
|
||||
const { user, logout } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmNewPassword('');
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
setLoading(false); // Asegurarse de resetear loading también
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Esta función se llama al hacer clic en el botón Cancelar
|
||||
const handleCancelClick = () => {
|
||||
onClose(false); // Notifica al padre (MainLayout) que se canceló
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
if (newPassword !== confirmNewPassword) {
|
||||
setError('La nueva contraseña y la confirmación no coinciden.');
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 6) {
|
||||
setError('La nueva contraseña debe tener al menos 6 caracteres.');
|
||||
return;
|
||||
}
|
||||
if (user && user.username === newPassword) {
|
||||
setError('La nueva contraseña no puede ser igual al nombre de usuario.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const changePasswordData: ChangePasswordRequestDto = {
|
||||
currentPassword,
|
||||
newPassword,
|
||||
confirmNewPassword,
|
||||
};
|
||||
|
||||
try {
|
||||
await authService.changePassword(changePasswordData);
|
||||
setSuccess('Contraseña cambiada exitosamente.');
|
||||
setTimeout(() => {
|
||||
onClose(true); // Notifica al padre (MainLayout) que fue exitoso
|
||||
}, 1500);
|
||||
} catch (err: any) {
|
||||
console.error("Change password error:", err);
|
||||
let errorMessage = 'Ocurrió un error inesperado al cambiar la contraseña.';
|
||||
if (axios.isAxiosError(err) && err.response) {
|
||||
errorMessage = err.response.data?.message || errorMessage;
|
||||
if (err.response.status === 401) {
|
||||
logout(); // Desloguear si el token es inválido
|
||||
onClose(false); // Notificar cierre sin éxito
|
||||
}
|
||||
}
|
||||
setError(errorMessage);
|
||||
setLoading(false); // Asegurarse de quitar loading en caso de error
|
||||
}
|
||||
// No poner setLoading(false) en el finally si quieres que el botón siga deshabilitado durante el success
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={(_event, reason) => { // onClose del Modal de MUI (para backdrop y Escape)
|
||||
if (reason === "backdropClick" && isFirstLogin) {
|
||||
return; // No permitir cerrar con backdrop si es el primer login
|
||||
}
|
||||
onClose(false); // Llamar a la prop onClose (que va a handleModalClose en MainLayout)
|
||||
}}
|
||||
disableEscapeKeyDown={isFirstLogin} // Deshabilitar Escape si es primer login
|
||||
aria-labelledby="change-password-modal-title"
|
||||
aria-describedby="change-password-modal-description"
|
||||
closeAfterTransition
|
||||
slots={{ backdrop: Backdrop }}
|
||||
slotProps={{
|
||||
backdrop: {
|
||||
timeout: 500,
|
||||
sx: { backdropFilter: 'blur(3px)' }
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={style}>
|
||||
<Typography id="change-password-modal-title" variant="h6" component="h2">
|
||||
Cambiar Contraseña
|
||||
</Typography>
|
||||
{isFirstLogin && (
|
||||
<Alert severity="warning" sx={{ mt: 2, width: '100%' }}>
|
||||
Por seguridad, debes cambiar tu contraseña inicial.
|
||||
</Alert>
|
||||
)}
|
||||
<Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 1, width: '100%' }}>
|
||||
{/* ... TextFields ... */}
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="currentPassword"
|
||||
label="Contraseña Actual"
|
||||
type="password"
|
||||
id="currentPasswordModal"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
disabled={loading || !!success}
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="newPassword"
|
||||
label="Nueva Contraseña"
|
||||
type="password"
|
||||
id="newPasswordModal"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
disabled={loading || !!success}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="confirmNewPassword"
|
||||
label="Confirmar Nueva Contraseña"
|
||||
type="password"
|
||||
id="confirmNewPasswordModal"
|
||||
value={confirmNewPassword}
|
||||
onChange={(e) => setConfirmNewPassword(e.target.value)}
|
||||
disabled={loading || !!success}
|
||||
error={newPassword !== confirmNewPassword && confirmNewPassword !== ''}
|
||||
helperText={newPassword !== confirmNewPassword && confirmNewPassword !== '' ? 'Las contraseñas no coinciden' : ''}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 2, width: '100%' }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
{success && (
|
||||
<Alert severity="success" sx={{ mt: 2, width: '100%' }}>
|
||||
{success}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Un solo grupo de botones */}
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
{/* El botón de cancelar llama a handleCancelClick */}
|
||||
{/* Se podría ocultar si isFirstLogin es true y no queremos que el usuario cancele */}
|
||||
{/* {!isFirstLogin && ( */}
|
||||
<Button onClick={handleCancelClick} disabled={loading || !!success} color="secondary">
|
||||
{isFirstLogin ? "Cancelar y Salir" : "Cancelar"}
|
||||
</Button>
|
||||
{/* )} */}
|
||||
<Button type="submit" variant="contained" disabled={loading || !!success}>
|
||||
{loading ? <CircularProgress size={24} /> : 'Cambiar Contraseña'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePasswordModal;
|
||||
128
Frontend/src/components/Modals/Usuarios/PerfilFormModal.tsx
Normal file
128
Frontend/src/components/Modals/Usuarios/PerfilFormModal.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||
import type { PerfilDto } from '../../../models/dtos/Usuarios/PerfilDto';
|
||||
import type { CreatePerfilDto } from '../../../models/dtos/Usuarios/CreatePerfilDto';
|
||||
import type { UpdatePerfilDto } from '../../../models/dtos/Usuarios/UpdatePerfilDto';
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo) ... */
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
interface PerfilFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreatePerfilDto | (UpdatePerfilDto & { id: number })) => Promise<void>;
|
||||
initialData?: PerfilDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const PerfilFormModal: React.FC<PerfilFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [nombrePerfil, setNombrePerfil] = useState('');
|
||||
const [descripcion, setDescripcion] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrorNombre, setLocalErrorNombre] = useState<string | null>(null);
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setNombrePerfil(initialData?.nombrePerfil || '');
|
||||
setDescripcion(initialData?.descripcion || '');
|
||||
setLocalErrorNombre(null);
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const handleInputChange = () => {
|
||||
if (localErrorNombre) setLocalErrorNombre(null);
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setLocalErrorNombre(null);
|
||||
clearErrorMessage();
|
||||
|
||||
if (!nombrePerfil.trim()) {
|
||||
setLocalErrorNombre('El nombre del perfil es obligatorio.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const dataToSubmit = { nombrePerfil, descripcion: descripcion || undefined };
|
||||
|
||||
if (isEditing && initialData) {
|
||||
await onSubmit({ ...dataToSubmit, id: initialData.id });
|
||||
} else {
|
||||
await onSubmit(dataToSubmit as CreatePerfilDto);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de PerfilFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2">
|
||||
{isEditing ? 'Editar Perfil' : 'Agregar Nuevo Perfil'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<TextField
|
||||
label="Nombre del Perfil"
|
||||
fullWidth
|
||||
required
|
||||
value={nombrePerfil}
|
||||
onChange={(e) => { setNombrePerfil(e.target.value); handleInputChange(); }}
|
||||
margin="normal"
|
||||
error={!!localErrorNombre}
|
||||
helperText={localErrorNombre || ''}
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
label="Descripción (Opcional)"
|
||||
fullWidth
|
||||
value={descripcion}
|
||||
onChange={(e) => setDescripcion(e.target.value)}
|
||||
margin="normal"
|
||||
multiline
|
||||
rows={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
{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} /> : (isEditing ? 'Guardar Cambios' : 'Agregar')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerfilFormModal;
|
||||
135
Frontend/src/components/Modals/Usuarios/PermisoFormModal.tsx
Normal file
135
Frontend/src/components/Modals/Usuarios/PermisoFormModal.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||
import type { PermisoDto } from '../../../models/dtos/Usuarios/PermisoDto'; // Usamos el DTO de la API para listar
|
||||
import type { CreatePermisoDto } from '../../../models/dtos/Usuarios/CreatePermisoDto';
|
||||
import type { UpdatePermisoDto } from '../../../models/dtos/Usuarios/UpdatePermisoDto';
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo) ... */
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 450, // Un poco más ancho para los campos
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
interface PermisoFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreatePermisoDto | (UpdatePermisoDto & { id: number })) => Promise<void>;
|
||||
initialData?: PermisoDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const PermisoFormModal: React.FC<PermisoFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [modulo, setModulo] = useState('');
|
||||
const [descPermiso, setDescPermiso] = useState('');
|
||||
const [codAcc, setCodAcc] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setModulo(initialData?.modulo || '');
|
||||
setDescPermiso(initialData?.descPermiso || '');
|
||||
setCodAcc(initialData?.codAcc || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!modulo.trim()) errors.modulo = 'El módulo es obligatorio.';
|
||||
if (!descPermiso.trim()) errors.descPermiso = 'La descripción es obligatoria.';
|
||||
if (!codAcc.trim()) errors.codAcc = 'El código de acceso es obligatorio.';
|
||||
else if (codAcc.length > 10) errors.codAcc = 'El código de acceso no debe exceder los 10 caracteres.';
|
||||
// Aquí podrías añadir validación de formato para codAcc si es necesario.
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (setter: React.Dispatch<React.SetStateAction<string>>, fieldName: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setter(e.target.value);
|
||||
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 {
|
||||
const dataToSubmit = { modulo, descPermiso, codAcc };
|
||||
|
||||
if (isEditing && initialData) {
|
||||
await onSubmit({ ...dataToSubmit, id: initialData.id });
|
||||
} else {
|
||||
await onSubmit(dataToSubmit as CreatePermisoDto);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de PermisoFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2">
|
||||
{isEditing ? 'Editar Permiso' : 'Agregar Nuevo Permiso'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<TextField
|
||||
label="Módulo" fullWidth required value={modulo}
|
||||
onChange={handleInputChange(setModulo, 'modulo')} margin="normal"
|
||||
error={!!localErrors.modulo} helperText={localErrors.modulo || ''}
|
||||
disabled={loading} autoFocus
|
||||
/>
|
||||
<TextField
|
||||
label="Descripción del Permiso" fullWidth required value={descPermiso}
|
||||
onChange={handleInputChange(setDescPermiso, 'descPermiso')} margin="normal"
|
||||
error={!!localErrors.descPermiso} helperText={localErrors.descPermiso || ''}
|
||||
disabled={loading}
|
||||
/>
|
||||
<TextField
|
||||
label="Código de Acceso (CodAcc)" fullWidth required value={codAcc}
|
||||
onChange={handleInputChange(setCodAcc, 'codAcc')} margin="normal"
|
||||
error={!!localErrors.codAcc} helperText={localErrors.codAcc || ''}
|
||||
disabled={loading} inputProps={{ maxLength: 10 }}
|
||||
/>
|
||||
{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} /> : (isEditing ? 'Guardar Cambios' : 'Agregar')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermisoFormModal;
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { Box, Checkbox, FormControlLabel, FormGroup, Typography, Paper } from '@mui/material'; // Quitar Grid
|
||||
import type { PermisoAsignadoDto } from '../../../models/dtos/Usuarios/PermisoAsignadoDto';
|
||||
|
||||
interface PermisosChecklistProps {
|
||||
permisosDisponibles: PermisoAsignadoDto[];
|
||||
permisosSeleccionados: Set<number>;
|
||||
onPermisoChange: (permisoId: number, asignado: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
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] = [];
|
||||
}
|
||||
acc[modulo].push(permiso);
|
||||
return acc;
|
||||
}, {} as Record<string, PermisoAsignadoDto[]>);
|
||||
|
||||
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>}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
</Paper>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermisosChecklist;
|
||||
122
Frontend/src/components/Modals/Usuarios/SetPasswordModal.tsx
Normal file
122
Frontend/src/components/Modals/Usuarios/SetPasswordModal.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControlLabel, Checkbox } from '@mui/material';
|
||||
import type { SetPasswordRequestDto } from '../../../models/dtos/Usuarios/SetPasswordRequestDto';
|
||||
import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto';
|
||||
|
||||
const modalStyle = { /* ... (mismo estilo) ... */
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
interface SetPasswordModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (userId: number, data: SetPasswordRequestDto) => Promise<void>;
|
||||
usuario: UsuarioDto | null; // Usuario para el cual se setea la clave
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const SetPasswordModal: React.FC<SetPasswordModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
usuario,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmNewPassword, setConfirmNewPassword] = useState('');
|
||||
const [forceChange, setForceChange] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setNewPassword('');
|
||||
setConfirmNewPassword('');
|
||||
setForceChange(true);
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!newPassword) errors.newPassword = 'La nueva contraseña es obligatoria.';
|
||||
else if (newPassword.length < 6) errors.newPassword = 'La contraseña debe tener al menos 6 caracteres.';
|
||||
if (newPassword !== confirmNewPassword) errors.confirmNewPassword = 'Las contraseñas no coinciden.';
|
||||
if (usuario && usuario.user.toLowerCase() === newPassword.toLowerCase()) errors.newPassword = 'La contraseña no puede ser igual al nombre de usuario.'
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
if (localErrors[fieldName]) {
|
||||
setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
}
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate() || !usuario) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const dataToSubmit: SetPasswordRequestDto = { newPassword, forceChangeOnNextLogin: forceChange };
|
||||
await onSubmit(usuario.id, dataToSubmit);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Error en submit de SetPasswordModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
Establecer Contraseña para {usuario?.user || ''}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<TextField label="Nueva Contraseña" type="password" fullWidth required value={newPassword}
|
||||
onChange={(e)=>{setNewPassword(e.target.value); handleInputChange('newPassword');}} margin="dense"
|
||||
error={!!localErrors.newPassword} helperText={localErrors.newPassword || ''}
|
||||
disabled={loading} autoFocus
|
||||
/>
|
||||
<TextField label="Confirmar Nueva Contraseña" type="password" fullWidth required value={confirmNewPassword}
|
||||
onChange={(e)=>{setConfirmNewPassword(e.target.value); handleInputChange('confirmNewPassword');}} margin="dense"
|
||||
error={!!localErrors.confirmNewPassword} helperText={localErrors.confirmNewPassword || ''}
|
||||
disabled={loading}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={forceChange} onChange={(e) => setForceChange(e.target.checked)} disabled={loading}/>}
|
||||
label="Forzar cambio en próximo inicio de sesión"
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{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} /> : 'Establecer Contraseña'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetPasswordModal;
|
||||
255
Frontend/src/components/Modals/Usuarios/UsuarioFormModal.tsx
Normal file
255
Frontend/src/components/Modals/Usuarios/UsuarioFormModal.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox
|
||||
} from '@mui/material';
|
||||
import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto';
|
||||
import type { CreateUsuarioRequestDto } from '../../../models/dtos/Usuarios/CreateUsuarioRequestDto';
|
||||
import type { UpdateUsuarioRequestDto } from '../../../models/dtos/Usuarios/UpdateUsuarioRequestDto';
|
||||
import type { PerfilDto } from '../../../models/dtos/Usuarios/PerfilDto'; // Para el dropdown de perfiles
|
||||
import perfilService from '../../../services/Usuarios/perfilService'; // Para obtener la lista de perfiles
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 500 }, // Responsive width
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
maxHeight: '90vh', // Para permitir scroll si el contenido es mucho
|
||||
overflowY: 'auto' // Habilitar scroll vertical
|
||||
};
|
||||
|
||||
interface UsuarioFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateUsuarioRequestDto | UpdateUsuarioRequestDto, id?: number) => Promise<void>;
|
||||
initialData?: UsuarioDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const UsuarioFormModal: React.FC<UsuarioFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [user, setUser] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [nombre, setNombre] = useState('');
|
||||
const [apellido, setApellido] = useState('');
|
||||
const [idPerfil, setIdPerfil] = useState<number | string>(''); // Puede ser string vacío inicialmente
|
||||
const [habilitada, setHabilitada] = useState(true);
|
||||
const [supAdmin, setSupAdmin] = useState(false);
|
||||
const [debeCambiarClave, setDebeCambiarClave] = useState(true);
|
||||
const [verLog, setVerLog] = useState('1.0.0.0');
|
||||
|
||||
const [perfiles, setPerfiles] = useState<PerfilDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPerfiles, setLoadingPerfiles] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPerfiles = async () => {
|
||||
setLoadingPerfiles(true);
|
||||
try {
|
||||
const data = await perfilService.getAllPerfiles();
|
||||
setPerfiles(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar perfiles", error);
|
||||
setLocalErrors(prev => ({...prev, perfiles: 'Error al cargar perfiles.'}));
|
||||
} finally {
|
||||
setLoadingPerfiles(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchPerfiles();
|
||||
setUser(initialData?.user || '');
|
||||
// No pre-rellenar contraseña en edición
|
||||
setPassword('');
|
||||
setConfirmPassword('');
|
||||
setNombre(initialData?.nombre || '');
|
||||
setApellido(initialData?.apellido || '');
|
||||
setIdPerfil(initialData?.idPerfil || '');
|
||||
setHabilitada(initialData ? initialData.habilitada : true);
|
||||
setSupAdmin(initialData ? initialData.supAdmin : false);
|
||||
setDebeCambiarClave(initialData ? initialData.debeCambiarClave : !isEditing); // true para creación
|
||||
setVerLog(initialData?.verLog || '1.0.0.0');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage, isEditing]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!user.trim()) errors.user = 'El nombre de usuario es obligatorio.';
|
||||
else if (user.length < 3) errors.user = 'El usuario debe tener al menos 3 caracteres.';
|
||||
|
||||
if (!isEditing || (isEditing && password)) { // Validar contraseña solo si se está creando o si se ingresó algo en edición
|
||||
if (!password) errors.password = 'La contraseña es obligatoria.';
|
||||
else if (password.length < 6) errors.password = 'La contraseña debe tener al menos 6 caracteres.';
|
||||
if (password !== confirmPassword) errors.confirmPassword = 'Las contraseñas no coinciden.';
|
||||
if (user.trim().toLowerCase() === password.toLowerCase()) errors.password = 'La contraseña no puede ser igual al nombre de usuario.'
|
||||
}
|
||||
|
||||
if (!nombre.trim()) errors.nombre = 'El nombre es obligatorio.';
|
||||
if (!apellido.trim()) errors.apellido = 'El apellido es obligatorio.';
|
||||
if (!idPerfil) errors.idPerfil = 'Debe seleccionar un perfil.';
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
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: UpdateUsuarioRequestDto = {
|
||||
nombre, apellido, idPerfil: Number(idPerfil), habilitada, supAdmin, debeCambiarClave, verLog
|
||||
};
|
||||
await onSubmit(dataToSubmit, initialData.id);
|
||||
// Si se ingresó una nueva contraseña, llamar a un endpoint separado para cambiarla
|
||||
if (password) {
|
||||
// Esto requeriría un endpoint adicional en el backend para que un admin cambie la clave de otro user
|
||||
// o adaptar el flujo de `AuthService.ChangePasswordAsync` si es posible,
|
||||
// o un nuevo endpoint en UsuarioController para setear clave.
|
||||
// Por ahora, lo dejamos así, la clave se cambia por el propio usuario o por un reset del admin.
|
||||
console.warn("El cambio de contraseña en edición de usuario no está implementado en este modal directamente. Usar la opción de 'Resetear Contraseña'.");
|
||||
}
|
||||
|
||||
} else {
|
||||
const dataToSubmit: CreateUsuarioRequestDto = {
|
||||
user, password, nombre, apellido, idPerfil: Number(idPerfil), habilitada, supAdmin, debeCambiarClave, verLog
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de UsuarioFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Usuario' : 'Agregar Nuevo Usuario'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
{/* SECCIÓN DE CAMPOS CON BOX Y FLEXBOX */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> {/* Contenedor principal de campos */}
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}> {/* Fila 1 */}
|
||||
<TextField label="Nombre de Usuario" fullWidth required value={user}
|
||||
onChange={(e) => {setUser(e.target.value); handleInputChange('user');}} margin="dense"
|
||||
error={!!localErrors.user} helperText={localErrors.user || ''}
|
||||
disabled={loading || isEditing} autoFocus={!isEditing}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} // 8px es la mitad del gap
|
||||
/>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idPerfil} sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}>
|
||||
<InputLabel id="perfil-select-label" required>Perfil</InputLabel>
|
||||
<Select
|
||||
labelId="perfil-select-label"
|
||||
label="Perfil"
|
||||
value={idPerfil}
|
||||
onChange={(e) => { setIdPerfil(e.target.value as number); handleInputChange('idPerfil');}}
|
||||
disabled={loading || loadingPerfiles}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione un perfil</em></MenuItem>
|
||||
{perfiles.map((p) => ( <MenuItem key={p.id} value={p.id}>{p.nombrePerfil}</MenuItem> ))}
|
||||
</Select>
|
||||
{localErrors.idPerfil && <Typography color="error" variant="caption">{localErrors.idPerfil}</Typography>}
|
||||
{loadingPerfiles && <CircularProgress size={20} sx={{position: 'absolute', right: 30, top: '50%', marginTop: '-10px'}}/>}
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
{!isEditing && (
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 1 }}> {/* Fila 2 (Contraseñas) */}
|
||||
<TextField label="Contraseña" type="password" fullWidth required={!isEditing} value={password}
|
||||
onChange={(e) => {setPassword(e.target.value); handleInputChange('password');}} margin="dense"
|
||||
error={!!localErrors.password} helperText={localErrors.password || ''}
|
||||
disabled={loading}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
|
||||
/>
|
||||
<TextField label="Confirmar Contraseña" type="password" fullWidth required={!isEditing} value={confirmPassword}
|
||||
onChange={(e) => {setConfirmPassword(e.target.value); handleInputChange('confirmPassword');}} margin="dense"
|
||||
error={!!localErrors.confirmPassword} helperText={localErrors.confirmPassword || ''}
|
||||
disabled={loading}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 1 }}> {/* Fila 3 (Nombre y Apellido) */}
|
||||
<TextField label="Nombre" fullWidth required value={nombre}
|
||||
onChange={(e) => {setNombre(e.target.value); handleInputChange('nombre');}} margin="dense"
|
||||
error={!!localErrors.nombre} helperText={localErrors.nombre || ''}
|
||||
disabled={loading}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
|
||||
/>
|
||||
<TextField label="Apellido" fullWidth required value={apellido}
|
||||
onChange={(e) => {setApellido(e.target.value); handleInputChange('apellido');}} margin="dense"
|
||||
error={!!localErrors.apellido} helperText={localErrors.apellido || ''}
|
||||
disabled={loading}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 1, alignItems: 'center' }}> {/* Fila 4 (VerLog y Habilitado) */}
|
||||
<TextField label="Versión Log" fullWidth value={verLog}
|
||||
onChange={(e) => setVerLog(e.target.value)} margin="dense"
|
||||
disabled={loading}
|
||||
sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }}
|
||||
/>
|
||||
<Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)', display: 'flex', justifyContent: 'flex-start', pl:1 /* o pt si es vertical */ }}>
|
||||
<FormControlLabel control={<Checkbox checked={habilitada} onChange={(e) => setHabilitada(e.target.checked)} disabled={loading}/>} label="Habilitado" />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 0.5 }}> {/* Fila 5 (Checkboxes) */}
|
||||
<Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}>
|
||||
<FormControlLabel control={<Checkbox checked={supAdmin} onChange={(e) => setSupAdmin(e.target.checked)} disabled={loading}/>} label="Super Administrador" />
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}>
|
||||
<FormControlLabel control={<Checkbox checked={debeCambiarClave} onChange={(e) => setDebeCambiarClave(e.target.checked)} disabled={loading}/>} label="Debe Cambiar Clave" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box> {/* Fin contenedor principal de campos */}
|
||||
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{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} /> : (isEditing ? 'Guardar Cambios' : 'Crear Usuario')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsuarioFormModal;
|
||||
Reference in New Issue
Block a user