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',
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import authService from '../services/authService';
|
||||
import type { ChangePasswordRequestDto } from '../models/dtos/ChangePasswordRequestDto';
|
||||
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';
|
||||
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;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { createContext, useState, useContext, type ReactNode, useEffect } from 'react';
|
||||
import type { LoginResponseDto } from '../models/dtos/LoginResponseDto';
|
||||
import type { LoginResponseDto } from '../models/dtos/Usuarios/LoginResponseDto';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
// Interfaz para los datos del usuario que guardaremos en el contexto
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { type ReactNode, useState, useEffect } from 'react';
|
||||
import { Box, AppBar, Toolbar, Typography, Button, Tabs, Tab, Paper } from '@mui/material';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import ChangePasswordModal from '../components/ChangePasswordModal';
|
||||
import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal';
|
||||
import { useNavigate, useLocation } from 'react-router-dom'; // Para manejar la navegación y la ruta actual
|
||||
|
||||
interface MainLayoutProps {
|
||||
|
||||
14
Frontend/src/models/dtos/Distribucion/CanillaDto.ts
Normal file
14
Frontend/src/models/dtos/Distribucion/CanillaDto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface CanillaDto {
|
||||
idCanilla: number;
|
||||
legajo?: number | null;
|
||||
nomApe: string;
|
||||
parada?: string | null;
|
||||
idZona: number;
|
||||
nombreZona: string;
|
||||
accionista: boolean;
|
||||
obs?: string | null;
|
||||
empresa: number;
|
||||
nombreEmpresa: string;
|
||||
baja: boolean;
|
||||
fechaBaja?: string | null; // string dd/MM/yyyy
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface CreateCanillaDto {
|
||||
legajo?: number | null;
|
||||
nomApe: string;
|
||||
parada?: string | null;
|
||||
idZona: number;
|
||||
accionista: boolean;
|
||||
obs?: string | null;
|
||||
empresa: number;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface CreateDistribuidorDto {
|
||||
nombre: string;
|
||||
contacto?: string | null;
|
||||
nroDoc: string;
|
||||
idZona?: number | null;
|
||||
calle?: string | null;
|
||||
numero?: string | null;
|
||||
piso?: string | null;
|
||||
depto?: string | null;
|
||||
telefono?: string | null;
|
||||
email?: string | null;
|
||||
localidad?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface CreateOtroDestinoDto {
|
||||
nombre: string;
|
||||
obs?: string;
|
||||
}
|
||||
12
Frontend/src/models/dtos/Distribucion/CreatePrecioDto.ts
Normal file
12
Frontend/src/models/dtos/Distribucion/CreatePrecioDto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface CreatePrecioDto {
|
||||
idPublicacion: number; // Importante para la ruta y para el backend
|
||||
vigenciaD: string; // "yyyy-MM-dd"
|
||||
// VigenciaH no se envía al crear, se calcula en backend o se deja null
|
||||
lunes?: number | null;
|
||||
martes?: number | null;
|
||||
miercoles?: number | null;
|
||||
jueves?: number | null;
|
||||
viernes?: number | null;
|
||||
sabado?: number | null;
|
||||
domingo?: number | null;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface CreatePublicacionDto {
|
||||
nombre: string;
|
||||
observacion?: string | null;
|
||||
idEmpresa: number;
|
||||
ctrlDevoluciones: boolean;
|
||||
habilitada: boolean;
|
||||
}
|
||||
15
Frontend/src/models/dtos/Distribucion/DistribuidorDto.ts
Normal file
15
Frontend/src/models/dtos/Distribucion/DistribuidorDto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface DistribuidorDto {
|
||||
idDistribuidor: number;
|
||||
nombre: string;
|
||||
contacto?: string | null;
|
||||
nroDoc: string;
|
||||
idZona?: number | null;
|
||||
nombreZona?: string | null;
|
||||
calle?: string | null;
|
||||
numero?: string | null;
|
||||
piso?: string | null;
|
||||
depto?: string | null;
|
||||
telefono?: string | null;
|
||||
email?: string | null;
|
||||
localidad?: string | null;
|
||||
}
|
||||
5
Frontend/src/models/dtos/Distribucion/OtroDestinoDto.ts
Normal file
5
Frontend/src/models/dtos/Distribucion/OtroDestinoDto.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface OtroDestinoDto {
|
||||
idDestino: number;
|
||||
nombre: string;
|
||||
obs?: string;
|
||||
}
|
||||
13
Frontend/src/models/dtos/Distribucion/PrecioDto.ts
Normal file
13
Frontend/src/models/dtos/Distribucion/PrecioDto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface PrecioDto {
|
||||
idPrecio: number;
|
||||
idPublicacion: number;
|
||||
vigenciaD: string; // "yyyy-MM-dd"
|
||||
vigenciaH?: string | null; // "yyyy-MM-dd"
|
||||
lunes?: number | null;
|
||||
martes?: number | null;
|
||||
miercoles?: number | null;
|
||||
jueves?: number | null;
|
||||
viernes?: number | null;
|
||||
sabado?: number | null;
|
||||
domingo?: number | null;
|
||||
}
|
||||
9
Frontend/src/models/dtos/Distribucion/PublicacionDto.ts
Normal file
9
Frontend/src/models/dtos/Distribucion/PublicacionDto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface PublicacionDto {
|
||||
idPublicacion: number;
|
||||
nombre: string;
|
||||
observacion?: string | null;
|
||||
idEmpresa: number;
|
||||
nombreEmpresa: string;
|
||||
ctrlDevoluciones: boolean;
|
||||
habilitada: boolean;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface ToggleBajaCanillaDto {
|
||||
darDeBaja: boolean;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface UpdateCanillaDto {
|
||||
legajo?: number | null;
|
||||
nomApe: string;
|
||||
parada?: string | null;
|
||||
idZona: number;
|
||||
accionista: boolean;
|
||||
obs?: string | null;
|
||||
empresa: number;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface UpdateDistribuidorDto {
|
||||
nombre: string;
|
||||
contacto?: string | null;
|
||||
nroDoc: string;
|
||||
idZona?: number | null;
|
||||
calle?: string | null;
|
||||
numero?: string | null;
|
||||
piso?: string | null;
|
||||
depto?: string | null;
|
||||
telefono?: string | null;
|
||||
email?: string | null;
|
||||
localidad?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface UpdateOtroDestinoDto {
|
||||
nombre: string;
|
||||
obs?: string;
|
||||
}
|
||||
11
Frontend/src/models/dtos/Distribucion/UpdatePrecioDto.ts
Normal file
11
Frontend/src/models/dtos/Distribucion/UpdatePrecioDto.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// Para actualizar, principalmente se modifican los montos o se cierra un periodo con VigenciaH
|
||||
export interface UpdatePrecioDto {
|
||||
vigenciaH?: string | null; // "yyyy-MM-dd", para cerrar un periodo
|
||||
lunes?: number | null;
|
||||
martes?: number | null;
|
||||
miercoles?: number | null;
|
||||
jueves?: number | null;
|
||||
viernes?: number | null;
|
||||
sabado?: number | null;
|
||||
domingo?: number | null;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface UpdatePublicacionDto {
|
||||
nombre: string;
|
||||
observacion?: string | null;
|
||||
idEmpresa: number;
|
||||
ctrlDevoluciones: boolean;
|
||||
habilitada: boolean;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// src/models/dtos/LoginRequestDto.ts
|
||||
export interface LoginRequestDto {
|
||||
Username: string; // Coincide con las propiedades C#
|
||||
Password: string;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface ActualizarPermisosPerfilRequestDto {
|
||||
permisosIds: number[];
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/models/dtos/ChangePasswordRequestDto.ts
|
||||
export interface ChangePasswordRequestDto {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
4
Frontend/src/models/dtos/Usuarios/CreatePerfilDto.ts
Normal file
4
Frontend/src/models/dtos/Usuarios/CreatePerfilDto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface CreatePerfilDto {
|
||||
nombrePerfil: string;
|
||||
descripcion?: string;
|
||||
}
|
||||
5
Frontend/src/models/dtos/Usuarios/CreatePermisoDto.ts
Normal file
5
Frontend/src/models/dtos/Usuarios/CreatePermisoDto.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface CreatePermisoDto {
|
||||
modulo: string;
|
||||
descPermiso: string;
|
||||
codAcc: string;
|
||||
}
|
||||
11
Frontend/src/models/dtos/Usuarios/CreateUsuarioRequestDto.ts
Normal file
11
Frontend/src/models/dtos/Usuarios/CreateUsuarioRequestDto.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface CreateUsuarioRequestDto {
|
||||
user: string;
|
||||
password?: string; // Puede ser opcional si la clave se genera o se fuerza cambio
|
||||
nombre: string;
|
||||
apellido: string;
|
||||
idPerfil: number;
|
||||
habilitada?: boolean;
|
||||
supAdmin?: boolean;
|
||||
debeCambiarClave?: boolean;
|
||||
verLog?: string;
|
||||
}
|
||||
4
Frontend/src/models/dtos/Usuarios/LoginRequestDto.ts
Normal file
4
Frontend/src/models/dtos/Usuarios/LoginRequestDto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface LoginRequestDto {
|
||||
Username: string;
|
||||
Password: string;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/models/dtos/LoginResponseDto.ts
|
||||
export interface LoginResponseDto {
|
||||
token: string;
|
||||
userId: number;
|
||||
@@ -6,5 +5,4 @@ export interface LoginResponseDto {
|
||||
nombreCompleto: string;
|
||||
esSuperAdmin: boolean;
|
||||
debeCambiarClave: boolean;
|
||||
// Añade otros campos si los definiste en el DTO C#
|
||||
}
|
||||
5
Frontend/src/models/dtos/Usuarios/PerfilDto.ts
Normal file
5
Frontend/src/models/dtos/Usuarios/PerfilDto.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface PerfilDto {
|
||||
id: number;
|
||||
nombrePerfil: string;
|
||||
descripcion?: string;
|
||||
}
|
||||
7
Frontend/src/models/dtos/Usuarios/PermisoAsignadoDto.ts
Normal file
7
Frontend/src/models/dtos/Usuarios/PermisoAsignadoDto.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface PermisoAsignadoDto {
|
||||
id: number;
|
||||
modulo: string;
|
||||
descPermiso: string;
|
||||
codAcc: string;
|
||||
asignado: boolean;
|
||||
}
|
||||
7
Frontend/src/models/dtos/Usuarios/PermisoDto.ts
Normal file
7
Frontend/src/models/dtos/Usuarios/PermisoDto.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface PermisoDto {
|
||||
id: number;
|
||||
modulo: string;
|
||||
descPermiso: string;
|
||||
codAcc: string;
|
||||
asignado?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface SetPasswordRequestDto {
|
||||
newPassword: string;
|
||||
forceChangeOnNextLogin?: boolean;
|
||||
}
|
||||
4
Frontend/src/models/dtos/Usuarios/UpdatePerfilDto.ts
Normal file
4
Frontend/src/models/dtos/Usuarios/UpdatePerfilDto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface UpdatePerfilDto {
|
||||
nombrePerfil: string;
|
||||
descripcion?: string;
|
||||
}
|
||||
5
Frontend/src/models/dtos/Usuarios/UpdatePermisoDto.ts
Normal file
5
Frontend/src/models/dtos/Usuarios/UpdatePermisoDto.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface UpdatePermisoDto {
|
||||
modulo: string;
|
||||
descPermiso: string;
|
||||
codAcc: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface UpdateUsuarioRequestDto {
|
||||
nombre: string;
|
||||
apellido: string;
|
||||
idPerfil: number;
|
||||
habilitada: boolean;
|
||||
supAdmin: boolean;
|
||||
debeCambiarClave: boolean;
|
||||
verLog?: string;
|
||||
}
|
||||
12
Frontend/src/models/dtos/Usuarios/UsuarioDto.ts
Normal file
12
Frontend/src/models/dtos/Usuarios/UsuarioDto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface UsuarioDto {
|
||||
id: number;
|
||||
user: string;
|
||||
habilitada: boolean;
|
||||
supAdmin: boolean;
|
||||
nombre: string;
|
||||
apellido: string;
|
||||
idPerfil: number;
|
||||
nombrePerfil: string;
|
||||
debeCambiarClave: boolean;
|
||||
verLog: string;
|
||||
}
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import tipoPagoService from '../../services/tipoPagoService';
|
||||
import tipoPagoService from '../../services/Contables/tipoPagoService';
|
||||
import type { TipoPago } from '../../models/Entities/TipoPago';
|
||||
import type { CreateTipoPagoDto } from '../../models/dtos/tiposPago/CreateTipoPagoDto';
|
||||
import type { UpdateTipoPagoDto } from '../../models/dtos/tiposPago/UpdateTipoPagoDto';
|
||||
import TipoPagoFormModal from '../../components/Modals/TipoPagoFormModal';
|
||||
import TipoPagoFormModal from '../../components/Modals/Contables/TipoPagoFormModal';
|
||||
import axios from 'axios';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const CanillasPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de Canillas</Typography>;
|
||||
};
|
||||
export default CanillasPage;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const DistribuidoresPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de Distribuidores</Typography>;
|
||||
};
|
||||
export default DistribuidoresPage;
|
||||
225
Frontend/src/pages/Distribucion/GestionarCanillitasPage.tsx
Normal file
225
Frontend/src/pages/Distribucion/GestionarCanillitasPage.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Chip, FormControlLabel
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
|
||||
import ToggleOffIcon from '@mui/icons-material/ToggleOff';
|
||||
import canillaService from '../../services/Distribucion/canillaService';
|
||||
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 CanillaFormModal from '../../components/Modals/Distribucion/CanillaFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarCanillitasPage: React.FC = () => {
|
||||
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroNomApe, setFiltroNomApe] = useState('');
|
||||
const [filtroLegajo, setFiltroLegajo] = useState<string>('');
|
||||
const [filtroSoloActivos, setFiltroSoloActivos] = useState<boolean | undefined>(true);
|
||||
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingCanillita, setEditingCanillita] = useState<CanillaDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedCanillitaRow, setSelectedCanillitaRow] = useState<CanillaDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("CG001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CG002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("CG003");
|
||||
// CG004 para Porcentajes/Montos, se gestionará por separado.
|
||||
const puedeDarBaja = isSuperAdmin || tienePermiso("CG005");
|
||||
|
||||
const cargarCanillitas = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const legajoNum = filtroLegajo ? parseInt(filtroLegajo, 10) : undefined;
|
||||
if (filtroLegajo && isNaN(legajoNum!)) {
|
||||
setApiErrorMessage("Legajo debe ser un número.");
|
||||
setCanillitas([]); // Limpiar resultados si el filtro es inválido
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const data = await canillaService.getAllCanillas(filtroNomApe, legajoNum, filtroSoloActivos);
|
||||
setCanillitas(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar los canillitas.');
|
||||
} finally { setLoading(false); }
|
||||
}, [filtroNomApe, filtroLegajo, filtroSoloActivos, puedeVer]);
|
||||
|
||||
useEffect(() => { cargarCanillitas(); }, [cargarCanillitas]);
|
||||
|
||||
const handleOpenModal = (canillita?: CanillaDto) => {
|
||||
setEditingCanillita(canillita || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingCanillita(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateCanillaDto | UpdateCanillaDto, id?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (id && editingCanillita) {
|
||||
await canillaService.updateCanilla(id, data as UpdateCanillaDto);
|
||||
} else {
|
||||
await canillaService.createCanilla(data as CreateCanillaDto);
|
||||
}
|
||||
cargarCanillitas();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el canillita.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleBaja = async (canillita: CanillaDto) => {
|
||||
setApiErrorMessage(null);
|
||||
const accion = canillita.baja ? "reactivar" : "dar de baja";
|
||||
if (window.confirm(`¿Está seguro de que desea ${accion} a ${canillita.nomApe}?`)) {
|
||||
try {
|
||||
await canillaService.toggleBajaCanilla(canillita.idCanilla, { darDeBaja: !canillita.baja });
|
||||
cargarCanillitas();
|
||||
} catch (err:any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${accion} el canillita.`;
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, canillita: CanillaDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedCanillitaRow(canillita);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedCanillitaRow(null);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = canillitas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso."}</Alert></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Canillitas</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Filtrar por Nombre/Apellido"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroNomApe}
|
||||
onChange={(e) => setFiltroNomApe(e.target.value)}
|
||||
sx={{ flex: 2, minWidth: '250px' }} // Dar más espacio al nombre
|
||||
/>
|
||||
<TextField
|
||||
label="Filtrar por Legajo"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroLegajo}
|
||||
onChange={(e) => setFiltroLegajo(e.target.value)}
|
||||
sx={{ flex: 1, minWidth: '150px' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={filtroSoloActivos === undefined ? true : filtroSoloActivos}
|
||||
onChange={(e) => setFiltroSoloActivos(e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label="Solo Activos"
|
||||
sx={{ flexShrink: 0 }} // Para que el label no se comprima demasiado
|
||||
/>
|
||||
{/* <Button variant="contained" onClick={cargarCanillitas} size="small">Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Canillita</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Legajo</TableCell><TableCell>Nombre y Apellido</TableCell>
|
||||
<TableCell>Zona</TableCell><TableCell>Empresa</TableCell>
|
||||
<TableCell>Accionista</TableCell><TableCell>Estado</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={7} align="center">No se encontraron canillitas.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((c) => (
|
||||
<TableRow key={c.idCanilla} hover sx={{ backgroundColor: c.baja ? '#ffebee' : 'inherit' }}>
|
||||
<TableCell>{c.legajo || '-'}</TableCell><TableCell>{c.nomApe}</TableCell>
|
||||
<TableCell>{c.nombreZona}</TableCell><TableCell>{c.empresa === 0 ? '-' : c.nombreEmpresa}</TableCell>
|
||||
<TableCell>{c.accionista ? <Chip label="Sí" color="success" size="small" variant="outlined"/> : <Chip label="No" color="default" size="small" variant="outlined"/>}</TableCell>
|
||||
<TableCell>{c.baja ? <Chip label="Baja" color="error" size="small" /> : <Chip label="Activo" color="success" size="small" />}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeDarBaja}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]} component="div" count={canillitas.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
|
||||
{puedeDarBaja && selectedCanillitaRow && (
|
||||
<MenuItem onClick={() => handleToggleBaja(selectedCanillitaRow)}>
|
||||
{selectedCanillitaRow.baja ? <ToggleOnIcon sx={{mr:1}}/> : <ToggleOffIcon sx={{mr:1}}/>}
|
||||
{selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'}
|
||||
</MenuItem>
|
||||
)}
|
||||
{(!puedeModificar && !puedeDarBaja) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<CanillaFormModal
|
||||
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
||||
initialData={editingCanillita} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarCanillitasPage;
|
||||
196
Frontend/src/pages/Distribucion/GestionarDistribuidoresPage.tsx
Normal file
196
Frontend/src/pages/Distribucion/GestionarDistribuidoresPage.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
// src/pages/Distribucion/GestionarDistribuidoresPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
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 DistribuidorFormModal from '../../components/Modals/Distribucion/DistribuidorFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
|
||||
const GestionarDistribuidoresPage: React.FC = () => {
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
const [filtroNroDoc, setFiltroNroDoc] = useState('');
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingDistribuidor, setEditingDistribuidor] = useState<DistribuidorDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedDistribuidorRow, setSelectedDistribuidorRow] = useState<DistribuidorDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("DG001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("DG002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("DG003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("DG005");
|
||||
|
||||
const cargarDistribuidores = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const data = await distribuidorService.getAllDistribuidores(filtroNombre, filtroNroDoc);
|
||||
setDistribuidores(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar los distribuidores.');
|
||||
} finally { setLoading(false); }
|
||||
}, [filtroNombre, filtroNroDoc, puedeVer]);
|
||||
|
||||
useEffect(() => { cargarDistribuidores(); }, [cargarDistribuidores]);
|
||||
|
||||
const handleOpenModal = (distribuidor?: DistribuidorDto) => {
|
||||
setEditingDistribuidor(distribuidor || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingDistribuidor(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateDistribuidorDto | UpdateDistribuidorDto, id?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (id && editingDistribuidor) {
|
||||
await distribuidorService.updateDistribuidor(id, data as UpdateDistribuidorDto);
|
||||
} else {
|
||||
await distribuidorService.createDistribuidor(data as CreateDistribuidorDto);
|
||||
}
|
||||
cargarDistribuidores();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el distribuidor.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm(`¿Está seguro de eliminar este distribuidor (ID: ${id})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await distribuidorService.deleteDistribuidor(id);
|
||||
cargarDistribuidores();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el distribuidor.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, distribuidor: DistribuidorDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedDistribuidorRow(distribuidor);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedDistribuidorRow(null);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = distribuidores.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso."}</Alert></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Distribuidores</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
|
||||
<TextField
|
||||
label="Filtrar por Nombre"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroNombre}
|
||||
onChange={(e) => setFiltroNombre(e.target.value)}
|
||||
sx={{ flexGrow: 1, minWidth: '200px' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Filtrar por Nro. Doc."
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroNroDoc}
|
||||
onChange={(e) => setFiltroNroDoc(e.target.value)}
|
||||
sx={{ flexGrow: 1, minWidth: '200px' }}
|
||||
/>
|
||||
{/* <Button variant="contained" onClick={cargarDistribuidores} size="small">Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Distribuidor</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Nombre</TableCell><TableCell>Nro. Doc.</TableCell>
|
||||
<TableCell>Contacto</TableCell><TableCell>Zona</TableCell>
|
||||
<TableCell>Teléfono</TableCell><TableCell>Localidad</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={7} align="center">No se encontraron distribuidores.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((d) => (
|
||||
<TableRow key={d.idDistribuidor} hover>
|
||||
<TableCell>{d.nombre}</TableCell><TableCell>{d.nroDoc}</TableCell>
|
||||
<TableCell>{d.contacto || '-'}</TableCell><TableCell>{d.nombreZona || '-'}</TableCell>
|
||||
<TableCell>{d.telefono || '-'}</TableCell><TableCell>{d.localidad || '-'}</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, d)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]} component="div" count={distribuidores.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
|
||||
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedDistribuidorRow!.idDistribuidor)}>Eliminar</MenuItem>)}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<DistribuidorFormModal
|
||||
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
||||
initialData={editingDistribuidor} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarDistribuidoresPage;
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import empresaService from '../../services/empresaService'; // Importar el servicio de Empresas
|
||||
import type { EmpresaDto } from '../../models/dtos/Empresas/EmpresaDto';
|
||||
import type { CreateEmpresaDto } from '../../models/dtos/Empresas/CreateEmpresaDto';
|
||||
import type { UpdateEmpresaDto } from '../../models/dtos/Empresas/UpdateEmpresaDto';
|
||||
import EmpresaFormModal from '../../components/Modals/EmpresaFormModal'; // Importar el modal de Empresas
|
||||
import empresaService from '../../services/Distribucion/empresaService'; // Importar el servicio de Empresas
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { CreateEmpresaDto } from '../../models/dtos/Distribucion/CreateEmpresaDto';
|
||||
import type { UpdateEmpresaDto } from '../../models/dtos/Distribucion/UpdateEmpresaDto';
|
||||
import EmpresaFormModal from '../../components/Modals/Distribucion/EmpresaFormModal'; // Importar el modal de Empresas
|
||||
import { usePermissions } from '../../hooks/usePermissions'; // Importar hook de permisos
|
||||
import axios from 'axios'; // Para manejo de errores de API
|
||||
|
||||
@@ -163,7 +163,6 @@ const GestionarEmpresasPage: React.FC = () => {
|
||||
size="small"
|
||||
value={filtroNombre}
|
||||
onChange={(e) => setFiltroNombre(e.target.value)}
|
||||
// Puedes añadir un botón de buscar explícito o dejar que filtre al escribir
|
||||
/>
|
||||
</Box>
|
||||
{/* Mostrar botón de agregar solo si tiene permiso */}
|
||||
|
||||
228
Frontend/src/pages/Distribucion/GestionarOtrosDestinosPage.tsx
Normal file
228
Frontend/src/pages/Distribucion/GestionarOtrosDestinosPage.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import otroDestinoService from '../../services/Distribucion/otroDestinoService';
|
||||
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
|
||||
import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto';
|
||||
import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto';
|
||||
import OtroDestinoFormModal from '../../components/Modals/Distribucion/OtroDestinoFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarOtrosDestinosPage: React.FC = () => {
|
||||
const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingDestino, setEditingDestino] = useState<OtroDestinoDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedDestinoRow, setSelectedDestinoRow] = useState<OtroDestinoDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
// Permisos para Otros Destinos (OD001 a OD004) - Revisa tus códigos de permiso
|
||||
const puedeVer = isSuperAdmin || tienePermiso("OD001"); // Asumiendo OD001 es ver entidad
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("OD002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("OD003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("OD004");
|
||||
|
||||
const cargarOtrosDestinos = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
const data = await otroDestinoService.getAllOtrosDestinos(filtroNombre);
|
||||
setOtrosDestinos(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Error al cargar los otros destinos.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filtroNombre, puedeVer]);
|
||||
|
||||
useEffect(() => {
|
||||
cargarOtrosDestinos();
|
||||
}, [cargarOtrosDestinos]);
|
||||
|
||||
const handleOpenModal = (destino?: OtroDestinoDto) => {
|
||||
setEditingDestino(destino || null);
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditingDestino(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateOtroDestinoDto | (UpdateOtroDestinoDto & { idDestino: number })) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (editingDestino && 'idDestino' in data) {
|
||||
await otroDestinoService.updateOtroDestino(editingDestino.idDestino, data);
|
||||
} else {
|
||||
await otroDestinoService.createOtroDestino(data as CreateOtroDestinoDto);
|
||||
}
|
||||
cargarOtrosDestinos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error inesperado al guardar el destino.';
|
||||
setApiErrorMessage(message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm(`¿Está seguro de que desea eliminar este destino (ID: ${id})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await otroDestinoService.deleteOtroDestino(id);
|
||||
cargarOtrosDestinos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error inesperado al eliminar el destino.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, destino: OtroDestinoDto) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedDestinoRow(destino);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedDestinoRow(null);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const displayData = otrosDestinos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<TextField
|
||||
label="Filtrar por Nombre"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroNombre}
|
||||
onChange={(e) => setFiltroNombre(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
|
||||
Agregar Nuevo Destino
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Nombre</TableCell>
|
||||
<TableCell>Observación</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron otros destinos.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((destino) => (
|
||||
<TableRow key={destino.idDestino}>
|
||||
<TableCell>{destino.nombre}</TableCell>
|
||||
<TableCell>{destino.obs || '-'}</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, destino)} disabled={!puedeModificar && !puedeEliminar}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
component="div"
|
||||
count={otrosDestinos.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedDestinoRow!); handleMenuClose(); }}>Modificar</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && (
|
||||
<MenuItem onClick={() => handleDelete(selectedDestinoRow!.idDestino)}>Eliminar</MenuItem>
|
||||
)}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<OtroDestinoFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleSubmitModal}
|
||||
initialData={editingDestino}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarOtrosDestinosPage;
|
||||
@@ -0,0 +1,240 @@
|
||||
// src/pages/Distribucion/Publicaciones/GestionarPreciosPublicacionPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
|
||||
CircularProgress, Alert, Chip
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
import precioService from '../../services/Distribucion/precioService';
|
||||
import publicacionService from '../../services/Distribucion/publicacionService';
|
||||
import type { PrecioDto } from '../../models/dtos/Distribucion/PrecioDto';
|
||||
import type { CreatePrecioDto } from '../../models/dtos/Distribucion/CreatePrecioDto';
|
||||
import type { UpdatePrecioDto } from '../../models/dtos/Distribucion/UpdatePrecioDto';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import PrecioFormModal from '../../components/Modals/Distribucion/PrecioFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarPreciosPublicacionPage: React.FC = () => {
|
||||
const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>();
|
||||
const navigate = useNavigate();
|
||||
const idPublicacion = Number(idPublicacionStr);
|
||||
|
||||
const [publicacion, setPublicacion] = useState<PublicacionDto | null>(null);
|
||||
const [precios, setPrecios] = useState<PrecioDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingPrecio, setEditingPrecio] = useState<PrecioDto | null>(null); // Este estado determina si el modal edita
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedPrecioRow, setSelectedPrecioRow] = useState<PrecioDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeGestionarPrecios = isSuperAdmin || tienePermiso("DP004");
|
||||
|
||||
const cargarDatos = useCallback(async () => {
|
||||
if (isNaN(idPublicacion)) {
|
||||
setError("ID de Publicación inválido.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!puedeGestionarPrecios) {
|
||||
setError("No tiene permiso para gestionar precios.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const [pubData, preciosData] = await Promise.all([
|
||||
publicacionService.getPublicacionById(idPublicacion),
|
||||
precioService.getPreciosPorPublicacion(idPublicacion)
|
||||
]);
|
||||
setPublicacion(pubData);
|
||||
setPrecios(preciosData);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
if (axios.isAxiosError(err) && err.response?.status === 404) {
|
||||
setError(`Publicación con ID ${idPublicacion} no encontrada o sin acceso a sus precios.`);
|
||||
} else {
|
||||
setError('Error al cargar los datos de precios.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idPublicacion, puedeGestionarPrecios]);
|
||||
|
||||
useEffect(() => {
|
||||
cargarDatos();
|
||||
}, [cargarDatos]);
|
||||
|
||||
const handleOpenModal = (precio?: PrecioDto) => {
|
||||
setEditingPrecio(precio || null); // Si hay 'precio', el modal estará en modo edición
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditingPrecio(null);
|
||||
};
|
||||
|
||||
// CORREGIDO: El segundo parámetro 'idPrecio' determina si es edición
|
||||
const handleSubmitModal = async (data: CreatePrecioDto | UpdatePrecioDto, idPrecio?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
// Si idPrecio tiene valor, Y editingPrecio (initialData del modal) también lo tenía, es una actualización
|
||||
if (idPrecio && editingPrecio) {
|
||||
await precioService.updatePrecio(idPublicacion, idPrecio, data as UpdatePrecioDto);
|
||||
} else {
|
||||
await precioService.createPrecio(idPublicacion, data as CreatePrecioDto);
|
||||
}
|
||||
cargarDatos(); // Recargar lista
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el período de precio.';
|
||||
setApiErrorMessage(message); throw err; // Re-lanzar para que el modal maneje el estado de error
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (idPrecio: number) => {
|
||||
if (window.confirm(`¿Está seguro de eliminar este período de precio (ID: ${idPrecio})? Esta acción puede afectar la vigencia de períodos anteriores.`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await precioService.deletePrecio(idPublicacion, idPrecio);
|
||||
cargarDatos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el período de precio.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, precio: PrecioDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedPrecioRow(precio);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedPrecioRow(null);
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string | null) => {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString); // Asegurar que se parsee correctamente si viene con hora
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0'); // Meses son 0-indexados
|
||||
const year = date.getUTCFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
|
||||
if (loading) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
|
||||
}
|
||||
if (error) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
|
||||
}
|
||||
if (!puedeGestionarPrecios) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/distribucion/publicaciones')} sx={{ mb: 2 }}>
|
||||
Volver a Publicaciones
|
||||
</Button>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Gestionar Precios para: {publicacion?.nombre || 'Cargando...'}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
|
||||
Empresa: {publicacion?.nombreEmpresa || '-'}
|
||||
</Typography>
|
||||
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
{puedeGestionarPrecios && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
|
||||
Agregar Nuevo Período de Precio
|
||||
</Button>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Vigencia Desde</TableCell><TableCell>Vigencia Hasta</TableCell>
|
||||
<TableCell align="right">Lunes</TableCell><TableCell align="right">Martes</TableCell>
|
||||
<TableCell align="right">Miércoles</TableCell><TableCell align="right">Jueves</TableCell>
|
||||
<TableCell align="right">Viernes</TableCell><TableCell align="right">Sábado</TableCell>
|
||||
<TableCell align="right">Domingo</TableCell>
|
||||
<TableCell align="center">Estado</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{precios.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={11} align="center">No hay períodos de precios definidos para esta publicación.</TableCell></TableRow>
|
||||
) : (
|
||||
precios.map((p) => (
|
||||
<TableRow key={p.idPrecio} hover>
|
||||
<TableCell>{formatDate(p.vigenciaD)}</TableCell>
|
||||
<TableCell>{formatDate(p.vigenciaH)}</TableCell>
|
||||
<TableCell align="right">{p.lunes?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="right">{p.martes?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="right">{p.miercoles?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="right">{p.jueves?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="right">{p.viernes?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="right">{p.sabado?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="right">{p.domingo?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="center">
|
||||
{!p.vigenciaH ? <Chip label="Activo" color="success" size="small" /> : <Chip label="Cerrado" size="small" />}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeGestionarPrecios}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeGestionarPrecios && selectedPrecioRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedPrecioRow); handleMenuClose(); }}>
|
||||
<EditIcon fontSize="small" sx={{mr:1}}/> Editar Precios/Cerrar Período
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeGestionarPrecios && selectedPrecioRow && (
|
||||
<MenuItem onClick={() => handleDelete(selectedPrecioRow.idPrecio)}>
|
||||
<DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar Período
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
{idPublicacion &&
|
||||
<PrecioFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleSubmitModal}
|
||||
idPublicacion={idPublicacion}
|
||||
initialData={editingPrecio} // Esto le dice al modal si está editando
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarPreciosPublicacionPage;
|
||||
260
Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx
Normal file
260
Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Chip, FormControl, InputLabel, Select, Tooltip,
|
||||
FormControlLabel
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import publicacionService from '../../services/Distribucion/publicacionService';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto';
|
||||
import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto';
|
||||
import PublicacionFormModal from '../../components/Modals/Distribucion/PublicacionFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
|
||||
|
||||
const GestionarPublicacionesPage: React.FC = () => {
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||
const [filtroSoloHabilitadas, setFiltroSoloHabilitadas] = useState<boolean | undefined>(true);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [loadingEmpresas, setLoadingEmpresas] = useState(false);
|
||||
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingPublicacion, setEditingPublicacion] = useState<PublicacionDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedPublicacionRow, setSelectedPublicacionRow] = useState<PublicacionDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("DP001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("DP002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("DP003");
|
||||
const puedeGestionarPrecios = isSuperAdmin || tienePermiso("DP004");
|
||||
const puedeGestionarRecargos = isSuperAdmin || tienePermiso("DP005");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("DP006");
|
||||
const puedeGestionarSecciones = isSuperAdmin || tienePermiso("DP007");
|
||||
|
||||
const fetchEmpresas = useCallback(async () => {
|
||||
setLoadingEmpresas(true);
|
||||
try {
|
||||
const data = await empresaService.getAllEmpresas();
|
||||
setEmpresas(data);
|
||||
} catch (err) {
|
||||
console.error("Error cargando empresas para filtro:", err);
|
||||
// Manejar error si es necesario, ej. mostrando un mensaje
|
||||
} finally {
|
||||
setLoadingEmpresas(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchEmpresas();
|
||||
}, [fetchEmpresas]);
|
||||
|
||||
|
||||
const cargarPublicaciones = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección."); setLoading(false); return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const idEmpresa = filtroIdEmpresa ? Number(filtroIdEmpresa) : undefined;
|
||||
const data = await publicacionService.getAllPublicaciones(filtroNombre, idEmpresa, filtroSoloHabilitadas);
|
||||
setPublicaciones(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar las publicaciones.');
|
||||
} finally { setLoading(false); }
|
||||
}, [filtroNombre, filtroIdEmpresa, filtroSoloHabilitadas, puedeVer]);
|
||||
|
||||
useEffect(() => { cargarPublicaciones(); }, [cargarPublicaciones]);
|
||||
|
||||
const handleOpenModal = (publicacion?: PublicacionDto) => {
|
||||
setEditingPublicacion(publicacion || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingPublicacion(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreatePublicacionDto | UpdatePublicacionDto, id?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (id && editingPublicacion) {
|
||||
await publicacionService.updatePublicacion(id, data as UpdatePublicacionDto);
|
||||
} else {
|
||||
await publicacionService.createPublicacion(data as CreatePublicacionDto);
|
||||
}
|
||||
cargarPublicaciones();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la publicación.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm(`¿Está seguro? Esta acción eliminará la publicación (ID: ${id}) y todas sus configuraciones asociadas (precios, recargos, secciones, etc.). ESTA ACCIÓN NO SE PUEDE DESHACER.`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await publicacionService.deletePublicacion(id);
|
||||
cargarPublicaciones();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la publicación.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleToggleHabilitada = async (publicacion: PublicacionDto) => {
|
||||
setApiErrorMessage(null);
|
||||
const datosActualizados: UpdatePublicacionDto = {
|
||||
nombre: publicacion.nombre,
|
||||
observacion: publicacion.observacion,
|
||||
idEmpresa: publicacion.idEmpresa,
|
||||
ctrlDevoluciones: publicacion.ctrlDevoluciones,
|
||||
habilitada: !publicacion.habilitada // Invertir estado
|
||||
};
|
||||
try {
|
||||
await publicacionService.updatePublicacion(publicacion.idPublicacion, datosActualizados);
|
||||
cargarPublicaciones(); // Recargar para ver el cambio
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar estado de habilitación.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, publicacion: PublicacionDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedPublicacionRow(publicacion);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedPublicacionRow(null);
|
||||
};
|
||||
|
||||
// TODO: Implementar navegación a páginas de gestión de Precios, Recargos, Secciones
|
||||
const handleNavigateToPrecios = (idPub: number) => {
|
||||
navigate(`/distribucion/publicaciones/${idPub}/precios`); // Ruta anidada
|
||||
handleMenuClose();
|
||||
};
|
||||
const handleNavigateToRecargos = (idPub: number) => { navigate(`/distribucion/publicaciones/${idPub}/recargos`); handleMenuClose(); };
|
||||
const handleNavigateToSecciones = (idPub: number) => { navigate(`/distribucion/publicaciones/${idPub}/secciones`); handleMenuClose(); };
|
||||
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = publicaciones.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso."}</Alert></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Publicaciones</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} />
|
||||
<FormControl size="small" sx={{ flexGrow: 1, minWidth: '200px' }}>
|
||||
<InputLabel id="empresa-filter-label">Empresa</InputLabel>
|
||||
<Select
|
||||
labelId="empresa-filter-label"
|
||||
label="Empresa"
|
||||
value={filtroIdEmpresa}
|
||||
onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}
|
||||
disabled={loadingEmpresas}
|
||||
>
|
||||
<MenuItem value=""><em>Todas</em></MenuItem>
|
||||
{empresas.map((e) => (<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={filtroSoloHabilitadas === undefined ? true : filtroSoloHabilitadas} onChange={(e) => setFiltroSoloHabilitadas(e.target.checked)} size="small" />}
|
||||
label="Solo Habilitadas"
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Publicación</Button>)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Nombre</TableCell><TableCell>Empresa</TableCell>
|
||||
<TableCell>Ctrl. Devol.</TableCell><TableCell>Habilitada</TableCell>
|
||||
<TableCell>Observación</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={6} align="center">No se encontraron publicaciones.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((p) => (
|
||||
<TableRow key={p.idPublicacion} hover sx={{ backgroundColor: !p.habilitada ? '#fff59d' : 'inherit' }}>
|
||||
<TableCell>{p.nombre}</TableCell><TableCell>{p.nombreEmpresa}</TableCell>
|
||||
<TableCell align="center">{p.ctrlDevoluciones ? <Chip label="Sí" size="small" color="info" /> : <Chip label="No" size="small" />}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Tooltip title={p.habilitada ? "Deshabilitar" : "Habilitar"}>
|
||||
<Switch checked={p.habilitada} onChange={() => handleToggleHabilitada(p)} size="small" disabled={!puedeModificar} />
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>{p.observacion || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeModificar && !puedeEliminar && !puedeGestionarPrecios && !puedeGestionarRecargos && !puedeGestionarSecciones}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]} component="div" count={publicaciones.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedPublicacionRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
|
||||
{puedeGestionarPrecios && (<MenuItem onClick={() => handleNavigateToPrecios(selectedPublicacionRow!.idPublicacion)}>Gestionar Precios</MenuItem>)}
|
||||
{puedeGestionarRecargos && (<MenuItem onClick={() => handleNavigateToRecargos(selectedPublicacionRow!.idPublicacion)}>Gestionar Recargos</MenuItem>)}
|
||||
{puedeGestionarSecciones && (<MenuItem onClick={() => handleNavigateToSecciones(selectedPublicacionRow!.idPublicacion)}>Gestionar Secciones</MenuItem>)}
|
||||
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedPublicacionRow!.idPublicacion)}>Eliminar</MenuItem>)}
|
||||
{/* Si no hay permisos para ninguna acción */}
|
||||
{(!puedeModificar && !puedeEliminar && !puedeGestionarPrecios && !puedeGestionarRecargos && !puedeGestionarSecciones) &&
|
||||
<MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<PublicacionFormModal
|
||||
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
||||
initialData={editingPublicacion} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarPublicacionesPage;
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import zonaService from '../../services/zonaService'; // Servicio de Zonas
|
||||
import zonaService from '../../services/Distribucion/zonaService'; // Servicio de Zonas
|
||||
import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // DTO de Zonas
|
||||
import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto'; // DTOs Create
|
||||
import type { UpdateZonaDto } from '../../models/dtos/Zonas/UpdateZonaDto'; // DTOs Update
|
||||
import ZonaFormModal from '../../components/Modals/ZonaFormModal'; // Modal de Zonas
|
||||
import ZonaFormModal from '../../components/Modals/Distribucion/ZonaFormModal'; // Modal de Zonas
|
||||
import { usePermissions } from '../../hooks/usePermissions'; // Hook de permisos
|
||||
import axios from 'axios'; // Para manejo de errores
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const OtrosDestinosPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de Otros Destinos</Typography>;
|
||||
};
|
||||
export default OtrosDestinosPage;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const PublicacionesPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de Publicaciones</Typography>;
|
||||
};
|
||||
export default PublicacionesPage;
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import estadoBobinaService from '../../services/estadoBobinaService';
|
||||
import estadoBobinaService from '../../services/Impresion/estadoBobinaService';
|
||||
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 EstadoBobinaFormModal from '../../components/Modals/EstadoBobinaFormModal';
|
||||
import EstadoBobinaFormModal from '../../components/Modals/Impresion/EstadoBobinaFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import plantaService from '../../services/plantaService'; // Servicio de Plantas
|
||||
import plantaService from '../../services/Impresion/plantaService'; // Servicio de Plantas
|
||||
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
|
||||
import type { CreatePlantaDto } from '../../models/dtos/Impresion/CreatePlantaDto';
|
||||
import type { UpdatePlantaDto } from '../../models/dtos/Impresion/UpdatePlantaDto';
|
||||
import PlantaFormModal from '../../components/Modals/PlantaFormModal'; // Modal de Plantas
|
||||
import PlantaFormModal from '../../components/Modals/Impresion/PlantaFormModal'; // Modal de Plantas
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import tipoBobinaService from '../../services/tipoBobinaService'; // Servicio específico
|
||||
import tipoBobinaService from '../../services/Impresion/tipoBobinaService'; // Servicio específico
|
||||
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
|
||||
import type { CreateTipoBobinaDto } from '../../models/dtos/Impresion/CreateTipoBobinaDto';
|
||||
import type { UpdateTipoBobinaDto } from '../../models/dtos/Impresion/UpdateTipoBobinaDto';
|
||||
import TipoBobinaFormModal from '../../components/Modals/TipoBobinaFormModal'; // Modal específico
|
||||
import TipoBobinaFormModal from '../../components/Modals/Impresion/TipoBobinaFormModal'; // Modal específico
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios'; // Importar axios
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import type { LoginRequestDto } from '../models/dtos/LoginRequestDto'; // Usar type
|
||||
import type { LoginRequestDto } from '../models/dtos/Usuarios/LoginRequestDto'; // Usar type
|
||||
|
||||
// Importaciones de Material UI
|
||||
import { Container, TextField, Button, Typography, Box, Alert } from '@mui/material';
|
||||
import authService from '../services/authService';
|
||||
import authService from '../services/Usuarios/authService';
|
||||
|
||||
import logo from '../assets/eldia.png';
|
||||
|
||||
|
||||
159
Frontend/src/pages/Usuarios/AsignarPermisosAPerfilPage.tsx
Normal file
159
Frontend/src/pages/Usuarios/AsignarPermisosAPerfilPage.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box, Typography, Button, Paper, CircularProgress, Alert,
|
||||
Checkbox, FormControlLabel, FormGroup // Para el caso sin componente checklist
|
||||
} from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import perfilService from '../../services/Usuarios/perfilService';
|
||||
import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto';
|
||||
import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto';
|
||||
import { usePermissions } from '../../hooks/usePermissions'; // Para verificar si el usuario actual puede estar aquí
|
||||
import axios from 'axios';
|
||||
import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist'; // Importar el componente
|
||||
|
||||
const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
const { idPerfil } = useParams<{ idPerfil: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
const puedeAsignar = isSuperAdmin || tienePermiso("PU004");
|
||||
|
||||
const [perfil, setPerfil] = useState<PerfilDto | null>(null);
|
||||
const [permisosDisponibles, setPermisosDisponibles] = useState<PermisoAsignadoDto[]>([]);
|
||||
// Usamos un Set para los IDs de los permisos seleccionados para eficiencia
|
||||
const [permisosSeleccionados, setPermisosSeleccionados] = useState<Set<number>>(new Set());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
const idPerfilNum = Number(idPerfil);
|
||||
|
||||
const cargarDatos = useCallback(async () => {
|
||||
if (!puedeAsignar) {
|
||||
setError("Acceso denegado. No tiene permiso para asignar permisos.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (isNaN(idPerfilNum)) {
|
||||
setError("ID de Perfil inválido.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setSuccessMessage(null);
|
||||
try {
|
||||
const [perfilData, permisosData] = await Promise.all([
|
||||
perfilService.getPerfilById(idPerfilNum),
|
||||
perfilService.getPermisosPorPerfil(idPerfilNum)
|
||||
]);
|
||||
setPerfil(perfilData);
|
||||
setPermisosDisponibles(permisosData);
|
||||
// Inicializar los permisos seleccionados basados en los que vienen 'asignado: true'
|
||||
setPermisosSeleccionados(new Set(permisosData.filter(p => p.asignado).map(p => p.id)));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Error al cargar datos del perfil o permisos.');
|
||||
if (axios.isAxiosError(err) && err.response?.status === 404) {
|
||||
setError(`Perfil con ID ${idPerfilNum} no encontrado.`);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idPerfilNum, puedeAsignar]);
|
||||
|
||||
useEffect(() => {
|
||||
cargarDatos();
|
||||
}, [cargarDatos]);
|
||||
|
||||
const handlePermisoChange = (permisoId: number, asignado: boolean) => {
|
||||
setPermisosSeleccionados(prev => {
|
||||
const next = new Set(prev);
|
||||
if (asignado) {
|
||||
next.add(permisoId);
|
||||
} else {
|
||||
next.delete(permisoId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
// Limpiar mensajes al cambiar selección
|
||||
if (successMessage) setSuccessMessage(null);
|
||||
if (error) setError(null);
|
||||
};
|
||||
|
||||
const handleGuardarCambios = async () => {
|
||||
if (!puedeAsignar || !perfil) return;
|
||||
setSaving(true); setError(null); setSuccessMessage(null);
|
||||
try {
|
||||
await perfilService.updatePermisosPorPerfil(perfil.id, {
|
||||
permisosIds: Array.from(permisosSeleccionados)
|
||||
});
|
||||
setSuccessMessage('Permisos actualizados correctamente.');
|
||||
// Opcional: recargar datos, aunque el estado local ya está actualizado
|
||||
// cargarDatos();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Error al guardar los permisos.';
|
||||
setError(message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
|
||||
}
|
||||
|
||||
if (error && !perfil) { // Si hay un error crítico al cargar el perfil
|
||||
return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
|
||||
}
|
||||
if (!puedeAsignar) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
|
||||
}
|
||||
if (!perfil) { // Si no hay error, pero el perfil es null después de cargar (no debería pasar si no hay error)
|
||||
return <Alert severity="warning" sx={{ m: 2 }}>Perfil no encontrado.</Alert>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}>
|
||||
Volver a Perfiles
|
||||
</Button>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
ID Perfil: {perfil?.id}
|
||||
</Typography>
|
||||
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
{successMessage && <Alert severity="success" sx={{ mb: 2 }}>{successMessage}</Alert>}
|
||||
|
||||
<Paper sx={{ p: 2, mt: 2 }}>
|
||||
<PermisosChecklist
|
||||
permisosDisponibles={permisosDisponibles}
|
||||
permisosSeleccionados={permisosSeleccionados}
|
||||
onPermisoChange={handlePermisoChange}
|
||||
disabled={saving}
|
||||
/>
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
|
||||
onClick={handleGuardarCambios}
|
||||
disabled={saving || !puedeAsignar}
|
||||
>
|
||||
Guardar Cambios
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AsignarPermisosAPerfilPage;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Typography, Container, Button } from '@mui/material';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const ChangePasswordPagePlaceholder: React.FC = () => {
|
||||
const { setShowForcedPasswordChangeModal } = useAuth();
|
||||
237
Frontend/src/pages/Usuarios/GestionarPerfilesPage.tsx
Normal file
237
Frontend/src/pages/Usuarios/GestionarPerfilesPage.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Tooltip // Añadir Tooltip
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; // Para asignar permisos
|
||||
import perfilService from '../../services/Usuarios/perfilService';
|
||||
import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto';
|
||||
import type { CreatePerfilDto } from '../../models/dtos/Usuarios/CreatePerfilDto';
|
||||
import type { UpdatePerfilDto } from '../../models/dtos/Usuarios/UpdatePerfilDto';
|
||||
import PerfilFormModal from '../../components/Modals/Usuarios/PerfilFormModal';
|
||||
// import PermisosPorPerfilModal from '../../components/Modals/PermisosPorPerfilModal'; // Lo crearemos después
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom'; // Para navegar
|
||||
|
||||
const GestionarPerfilesPage: React.FC = () => {
|
||||
const [perfiles, setPerfiles] = useState<PerfilDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingPerfil, setEditingPerfil] = useState<PerfilDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// const [permisosModalOpen, setPermisosModalOpen] = useState(false); // Para modal de permisos
|
||||
// const [selectedPerfilForPermisos, setSelectedPerfilForPermisos] = useState<PerfilDto | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedPerfilRow, setSelectedPerfilRow] = useState<PerfilDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const navigate = useNavigate(); // Hook para navegación
|
||||
|
||||
// Permisos para Perfiles (PU001 a PU004)
|
||||
const puedeVer = isSuperAdmin || tienePermiso("PU001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("PU002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("PU003"); // Modificar nombre/desc
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("PU003"); // Excel dice PU003 para eliminar
|
||||
const puedeAsignarPermisos = isSuperAdmin || tienePermiso("PU004");
|
||||
|
||||
const cargarPerfiles = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const data = await perfilService.getAllPerfiles(filtroNombre);
|
||||
setPerfiles(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar los perfiles.');
|
||||
} finally { setLoading(false); }
|
||||
}, [filtroNombre, puedeVer]);
|
||||
|
||||
useEffect(() => { cargarPerfiles(); }, [cargarPerfiles]);
|
||||
|
||||
const handleOpenModal = (perfil?: PerfilDto) => {
|
||||
setEditingPerfil(perfil || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingPerfil(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreatePerfilDto | (UpdatePerfilDto & { id: number })) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (editingPerfil && 'id' in data) {
|
||||
await perfilService.updatePerfil(editingPerfil.id, data);
|
||||
} else {
|
||||
await perfilService.createPerfil(data as CreatePerfilDto);
|
||||
}
|
||||
cargarPerfiles();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el perfil.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm(`¿Está seguro? ID: ${id}`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await perfilService.deletePerfil(id);
|
||||
cargarPerfiles();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el perfil.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, perfil: PerfilDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedPerfilRow(perfil);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedPerfilRow(null);
|
||||
};
|
||||
|
||||
const handleOpenPermisosModal = (perfil: PerfilDto) => {
|
||||
// setSelectedPerfilForPermisos(perfil);
|
||||
// setPermisosModalOpen(true);
|
||||
handleMenuClose();
|
||||
// Navegar a la página de asignación de permisos
|
||||
navigate(`/usuarios/perfiles/${perfil.id}/permisos`);
|
||||
};
|
||||
// const handleClosePermisosModal = () => {
|
||||
// setPermisosModalOpen(false); setSelectedPerfilForPermisos(null);
|
||||
// };
|
||||
// const handleSubmitPermisos = async (idPerfil: number, permisosIds: number[]) => {
|
||||
// try {
|
||||
// // await perfilService.updatePermisosPorPerfil(idPerfil, permisosIds);
|
||||
// // console.log("Permisos actualizados para perfil:", idPerfil);
|
||||
// // Quizás un snackbar de éxito
|
||||
// } catch (error) {
|
||||
// console.error("Error al actualizar permisos:", error);
|
||||
// setApiErrorMessage("Error al actualizar permisos.");
|
||||
// }
|
||||
// handleClosePermisosModal();
|
||||
// };
|
||||
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = perfiles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Perfiles</Typography>
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Perfiles</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} />
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
|
||||
Agregar Nuevo Perfil
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Nombre del Perfil</TableCell>
|
||||
<TableCell>Descripción</TableCell>
|
||||
{(puedeModificar || puedeEliminar || puedeAsignarPermisos) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar || puedeAsignarPermisos) ? 3 : 2} align="center">No se encontraron perfiles.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((perfil) => (
|
||||
<TableRow key={perfil.id}>
|
||||
<TableCell>{perfil.nombrePerfil}</TableCell>
|
||||
<TableCell>{perfil.descripcion || '-'}</TableCell>
|
||||
{(puedeModificar || puedeEliminar || puedeAsignarPermisos) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, perfil)} disabled={!puedeModificar && !puedeEliminar && !puedeAsignarPermisos}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]} component="div" count={perfiles.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedPerfilRow!); handleMenuClose(); }}>Modificar</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && (
|
||||
<MenuItem onClick={() => handleDelete(selectedPerfilRow!.id)}>Eliminar</MenuItem>
|
||||
)}
|
||||
{puedeAsignarPermisos && (
|
||||
<MenuItem onClick={() => handleOpenPermisosModal(selectedPerfilRow!)}>Asignar Permisos</MenuItem>
|
||||
)}
|
||||
{(!puedeModificar && !puedeEliminar && !puedeAsignarPermisos) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<PerfilFormModal
|
||||
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
||||
initialData={editingPerfil} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
{/* {selectedPerfilForPermisos && (
|
||||
<PermisosPorPerfilModal
|
||||
open={permisosModalOpen}
|
||||
onClose={handleClosePermisosModal}
|
||||
perfil={selectedPerfilForPermisos}
|
||||
onSubmit={handleSubmitPermisos}
|
||||
// Asume que tienes un servicio para obtener todos los permisos disponibles
|
||||
// getAllPermisosDisponibles={async () => []} // Implementar esto
|
||||
/>
|
||||
)} */}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarPerfilesPage;
|
||||
200
Frontend/src/pages/Usuarios/GestionarPermisosPage.tsx
Normal file
200
Frontend/src/pages/Usuarios/GestionarPermisosPage.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import permisoService from '../../services/Usuarios/permisoService';
|
||||
import type { PermisoDto } from '../../models/dtos/Usuarios/PermisoDto';
|
||||
import type { CreatePermisoDto } from '../../models/dtos/Usuarios/CreatePermisoDto';
|
||||
import type { UpdatePermisoDto } from '../../models/dtos/Usuarios/UpdatePermisoDto';
|
||||
import PermisoFormModal from '../../components/Modals/Usuarios/PermisoFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarPermisosPage: React.FC = () => {
|
||||
const [permisos, setPermisos] = useState<PermisoDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroModulo, setFiltroModulo] = useState('');
|
||||
const [filtroCodAcc, setFiltroCodAcc] = useState('');
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingPermiso, setEditingPermiso] = useState<PermisoDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10); // Un poco más para esta tabla
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedPermisoRow, setSelectedPermisoRow] = useState<PermisoDto | null>(null);
|
||||
|
||||
const { isSuperAdmin } = usePermissions(); // Solo SuperAdmin puede acceder
|
||||
|
||||
const cargarPermisos = useCallback(async () => {
|
||||
if (!isSuperAdmin) {
|
||||
setError("Acceso denegado. Solo SuperAdmin puede gestionar permisos.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const data = await permisoService.getAllPermisos(filtroModulo, filtroCodAcc);
|
||||
setPermisos(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar los permisos.');
|
||||
} finally { setLoading(false); }
|
||||
}, [filtroModulo, filtroCodAcc, isSuperAdmin]);
|
||||
|
||||
useEffect(() => { cargarPermisos(); }, [cargarPermisos]);
|
||||
|
||||
const handleOpenModal = (permiso?: PermisoDto) => {
|
||||
setEditingPermiso(permiso || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingPermiso(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreatePermisoDto | (UpdatePermisoDto & { id: number })) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (editingPermiso && 'id' in data) {
|
||||
await permisoService.updatePermiso(editingPermiso.id, data);
|
||||
} else {
|
||||
await permisoService.createPermiso(data as CreatePermisoDto);
|
||||
}
|
||||
cargarPermisos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el permiso.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm(`¿Está seguro de eliminar este permiso (ID: ${id})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await permisoService.deletePermiso(id);
|
||||
cargarPermisos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el permiso.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, permiso: PermisoDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedPermisoRow(permiso);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedPermisoRow(null);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = permisos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !isSuperAdmin) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Definición de Permisos</Typography>
|
||||
<Alert severity="error">{error || "Acceso denegado."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Definición de Permisos (SuperAdmin)</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
|
||||
<TextField
|
||||
label="Filtrar por Módulo"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroModulo}
|
||||
onChange={(e) => setFiltroModulo(e.target.value)}
|
||||
sx={{ flexGrow: 1, minWidth: '200px' }} // Para que se adapte mejor
|
||||
/>
|
||||
<TextField
|
||||
label="Filtrar por CodAcc"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroCodAcc}
|
||||
onChange={(e) => setFiltroCodAcc(e.target.value)}
|
||||
sx={{ flexGrow: 1, minWidth: '200px' }}
|
||||
/>
|
||||
{/* El botón de búsqueda es opcional si el filtro es en tiempo real */}
|
||||
{/* <Button variant="contained" onClick={cargarPermisos}>Buscar</Button> */}
|
||||
</Box>
|
||||
{isSuperAdmin && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
|
||||
Agregar Nuevo Permiso
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && isSuperAdmin && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Módulo</TableCell>
|
||||
<TableCell>Descripción</TableCell>
|
||||
<TableCell>CodAcc</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={4} align="center">No se encontraron permisos.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((permiso) => (
|
||||
<TableRow key={permiso.id}>
|
||||
<TableCell>{permiso.modulo}</TableCell>
|
||||
<TableCell>{permiso.descPermiso}</TableCell>
|
||||
<TableCell>{permiso.codAcc}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, permiso)}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25, 50]} component="div" count={permisos.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedPermisoRow!); handleMenuClose(); }}>Modificar</MenuItem>
|
||||
<MenuItem onClick={() => handleDelete(selectedPermisoRow!.id)}>Eliminar</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<PermisoFormModal
|
||||
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
||||
initialData={editingPermiso} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarPermisosPage;
|
||||
264
Frontend/src/pages/Usuarios/GestionarUsuariosPage.tsx
Normal file
264
Frontend/src/pages/Usuarios/GestionarUsuariosPage.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Tooltip
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import VpnKeyIcon from '@mui/icons-material/VpnKey'; // Para resetear clave
|
||||
import usuarioService from '../../services/Usuarios/usuarioService';
|
||||
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 { SetPasswordRequestDto } from '../../models/dtos/Usuarios/SetPasswordRequestDto';
|
||||
import UsuarioFormModal from '../../components/Modals/Usuarios/UsuarioFormModal';
|
||||
import SetPasswordModal from '../../components/Modals/Usuarios/SetPasswordModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarUsuariosPage: React.FC = () => {
|
||||
const [usuarios, setUsuarios] = useState<UsuarioDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroUser, setFiltroUser] = useState('');
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
|
||||
const [usuarioModalOpen, setUsuarioModalOpen] = useState(false);
|
||||
const [editingUsuario, setEditingUsuario] = useState<UsuarioDto | null>(null);
|
||||
|
||||
const [setPasswordModalOpen, setSetPasswordModalOpen] = useState(false);
|
||||
const [selectedUsuarioForPassword, setSelectedUsuarioForPassword] = useState<UsuarioDto | null>(null);
|
||||
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedUsuarioRow, setSelectedUsuarioRow] = useState<UsuarioDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin, currentUser } = usePermissions();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("CU001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CU002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("CU003"); // Modificar datos básicos
|
||||
const puedeAsignarPerfil = isSuperAdmin || tienePermiso("CU004"); // Modificar perfil
|
||||
// Resetear clave es típicamente SuperAdmin
|
||||
const puedeResetearClave = isSuperAdmin;
|
||||
|
||||
const cargarUsuarios = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const data = await usuarioService.getAllUsuarios(filtroUser, filtroNombre);
|
||||
setUsuarios(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar los usuarios.');
|
||||
} finally { setLoading(false); }
|
||||
}, [filtroUser, filtroNombre, puedeVer]);
|
||||
|
||||
useEffect(() => { cargarUsuarios(); }, [cargarUsuarios]);
|
||||
|
||||
const handleOpenUsuarioModal = (usuario?: UsuarioDto) => {
|
||||
setEditingUsuario(usuario || null); setApiErrorMessage(null); setUsuarioModalOpen(true);
|
||||
};
|
||||
const handleCloseUsuarioModal = () => {
|
||||
setUsuarioModalOpen(false); setEditingUsuario(null);
|
||||
};
|
||||
|
||||
const handleSubmitUsuarioModal = async (data: CreateUsuarioRequestDto | UpdateUsuarioRequestDto, id?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (id && editingUsuario) { // Es Update
|
||||
await usuarioService.updateUsuario(id, data as UpdateUsuarioRequestDto);
|
||||
} else { // Es Create
|
||||
await usuarioService.createUsuario(data as CreateUsuarioRequestDto);
|
||||
}
|
||||
cargarUsuarios();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el usuario.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenSetPasswordModal = (usuario: UsuarioDto) => {
|
||||
setSelectedUsuarioForPassword(usuario);
|
||||
setApiErrorMessage(null);
|
||||
setSetPasswordModalOpen(true);
|
||||
handleMenuClose();
|
||||
};
|
||||
const handleCloseSetPasswordModal = () => {
|
||||
setSetPasswordModalOpen(false); setSelectedUsuarioForPassword(null);
|
||||
};
|
||||
const handleSubmitSetPassword = async (userId: number, data: SetPasswordRequestDto) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await usuarioService.setPassword(userId, data);
|
||||
cargarUsuarios(); // Para reflejar el cambio en 'DebeCambiarClave'
|
||||
} catch (err:any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al establecer la contraseña.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleHabilitado = async (usuario: UsuarioDto) => {
|
||||
setApiErrorMessage(null);
|
||||
// Un usuario no puede deshabilitarse a sí mismo
|
||||
if (currentUser?.userId === usuario.id) {
|
||||
setApiErrorMessage("No puede cambiar el estado de habilitación de su propio usuario.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await usuarioService.toggleHabilitado(usuario.id, !usuario.habilitada);
|
||||
cargarUsuarios();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar el estado del usuario.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, usuario: UsuarioDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedUsuarioRow(usuario);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedUsuarioRow(null);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = usuarios.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Usuarios</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
{/* SECCIÓN DE FILTROS CORREGIDA */}
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
|
||||
<TextField
|
||||
label="Filtrar por Usuario"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroUser}
|
||||
onChange={(e) => setFiltroUser(e.target.value)}
|
||||
sx={{ flexGrow: 1, minWidth: '200px' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Filtrar por Nombre/Apellido"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroNombre}
|
||||
onChange={(e) => setFiltroNombre(e.target.value)}
|
||||
sx={{ flexGrow: 1, minWidth: '200px' }}
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenUsuarioModal()} sx={{ mb: 2 }}>
|
||||
Agregar Nuevo Usuario
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Usuario</TableCell>
|
||||
<TableCell>Nombre Completo</TableCell>
|
||||
<TableCell>Perfil</TableCell>
|
||||
<TableCell>Habilitado</TableCell>
|
||||
<TableCell>Cambiar Clave</TableCell>
|
||||
<TableCell>SuperAdmin</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={7} align="center">No se encontraron usuarios.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((usr) => (
|
||||
<TableRow key={usr.id}>
|
||||
<TableCell>{usr.user}</TableCell>
|
||||
<TableCell>{`${usr.nombre} ${usr.apellido}`}</TableCell>
|
||||
<TableCell>{usr.nombrePerfil}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={usr.habilitada ? "Deshabilitar" : "Habilitar"}>
|
||||
<Switch
|
||||
checked={usr.habilitada}
|
||||
onChange={() => handleToggleHabilitado(usr)}
|
||||
disabled={!puedeModificar || currentUser?.userId === usr.id}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>{usr.debeCambiarClave ? 'Sí' : 'No'}</TableCell>
|
||||
<TableCell>{usr.supAdmin ? 'Sí' : 'No'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, usr)} disabled={!puedeModificar && !puedeAsignarPerfil && !puedeResetearClave}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]} component="div" count={usuarios.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{(puedeModificar || puedeAsignarPerfil) && (
|
||||
<MenuItem onClick={() => { handleOpenUsuarioModal(selectedUsuarioRow!); handleMenuClose(); }}>Modificar</MenuItem>
|
||||
)}
|
||||
{puedeResetearClave && selectedUsuarioRow && currentUser?.userId !== selectedUsuarioRow.id && (
|
||||
<MenuItem onClick={() => handleOpenSetPasswordModal(selectedUsuarioRow!)}>
|
||||
<VpnKeyIcon fontSize="small" sx={{ mr: 1 }} /> Resetear Contraseña
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* No hay "Eliminar" directo, se usa el switch de Habilitado */}
|
||||
{(!puedeModificar && !puedeAsignarPerfil && !puedeResetearClave) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<UsuarioFormModal
|
||||
open={usuarioModalOpen} onClose={handleCloseUsuarioModal} onSubmit={handleSubmitUsuarioModal}
|
||||
initialData={editingUsuario} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
{selectedUsuarioForPassword && (
|
||||
<SetPasswordModal
|
||||
open={setPasswordModalOpen}
|
||||
onClose={handleCloseSetPasswordModal}
|
||||
onSubmit={handleSubmitSetPassword}
|
||||
usuario={selectedUsuarioForPassword}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarUsuariosPage;
|
||||
68
Frontend/src/pages/Usuarios/UsuariosIndexPage.tsx
Normal file
68
Frontend/src/pages/Usuarios/UsuariosIndexPage.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
const usuariosSubModules = [
|
||||
{ label: 'Perfiles', path: 'perfiles' },
|
||||
{ label: 'Permisos (Definición)', path: 'permisos' },
|
||||
{ label: 'Usuarios', path: 'gestion-usuarios' },
|
||||
];
|
||||
|
||||
const UsuariosIndexPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const currentBasePath = '/usuarios';
|
||||
const subPath = location.pathname.startsWith(currentBasePath + '/')
|
||||
? location.pathname.substring(currentBasePath.length + 1).split('/')[0] // Tomar solo la primera parte de la subruta
|
||||
: (location.pathname === currentBasePath ? usuariosSubModules[0]?.path : undefined);
|
||||
|
||||
const activeTabIndex = usuariosSubModules.findIndex(
|
||||
(subModule) => subModule.path === subPath
|
||||
);
|
||||
|
||||
if (activeTabIndex !== -1) {
|
||||
setSelectedSubTab(activeTabIndex);
|
||||
} else {
|
||||
if (location.pathname === currentBasePath && usuariosSubModules.length > 0) {
|
||||
navigate(usuariosSubModules[0].path, { replace: true });
|
||||
setSelectedSubTab(0);
|
||||
} else {
|
||||
setSelectedSubTab(false);
|
||||
}
|
||||
}
|
||||
}, [location.pathname, navigate]);
|
||||
|
||||
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
setSelectedSubTab(newValue);
|
||||
navigate(usuariosSubModules[newValue].path);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" gutterBottom>Módulo de Usuarios y Seguridad</Typography>
|
||||
<Paper square elevation={1}>
|
||||
<Tabs
|
||||
value={selectedSubTab}
|
||||
onChange={handleSubTabChange}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
aria-label="sub-módulos de usuarios"
|
||||
>
|
||||
{usuariosSubModules.map((subModule) => (
|
||||
<Tab key={subModule.path} label={subModule.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsuariosIndexPage;
|
||||
@@ -13,10 +13,11 @@ import ESCanillasPage from '../pages/Distribucion/ESCanillasPage';
|
||||
import ControlDevolucionesPage from '../pages/Distribucion/ControlDevolucionesPage';
|
||||
import ESDistribuidoresPage from '../pages/Distribucion/ESDistribuidoresPage';
|
||||
import SalidasOtrosDestinosPage from '../pages/Distribucion/SalidasOtrosDestinosPage';
|
||||
import CanillasPage from '../pages/Distribucion/CanillasPage';
|
||||
import DistribuidoresPage from '../pages/Distribucion/DistribuidoresPage';
|
||||
import PublicacionesPage from '../pages/Distribucion/PublicacionesPage';
|
||||
import OtrosDestinosPage from '../pages/Distribucion/OtrosDestinosPage';
|
||||
import GestionarCanillitasPage from '../pages/Distribucion/GestionarCanillitasPage';
|
||||
import GestionarDistribuidoresPage from '../pages/Distribucion/GestionarDistribuidoresPage';
|
||||
import GestionarPublicacionesPage from '../pages/Distribucion/GestionarPublicacionesPage'; // Ajusta la ruta si la moviste
|
||||
import GestionarPreciosPublicacionPage from '../pages/Distribucion/GestionarPreciosPublicacionPage';
|
||||
import GestionarOtrosDestinosPage from '../pages/Distribucion/GestionarOtrosDestinosPage';
|
||||
import GestionarZonasPage from '../pages/Distribucion/GestionarZonasPage';
|
||||
import GestionarEmpresasPage from '../pages/Distribucion/GestionarEmpresasPage';
|
||||
|
||||
@@ -30,6 +31,13 @@ import GestionarEstadosBobinaPage from '../pages/Impresion/GestionarEstadosBobin
|
||||
import ContablesIndexPage from '../pages/Contables/ContablesIndexPage';
|
||||
import GestionarTiposPagoPage from '../pages/Contables/GestionarTiposPagoPage';
|
||||
|
||||
// Usuarios
|
||||
import UsuariosIndexPage from '../pages/Usuarios/UsuariosIndexPage'; // Crear este componente
|
||||
import GestionarPerfilesPage from '../pages/Usuarios/GestionarPerfilesPage';
|
||||
import GestionarPermisosPage from '../pages/Usuarios/GestionarPermisosPage';
|
||||
import AsignarPermisosAPerfilPage from '../pages/Usuarios/AsignarPermisosAPerfilPage';
|
||||
import GestionarUsuariosPage from '../pages/Usuarios/GestionarUsuariosPage';
|
||||
|
||||
// --- ProtectedRoute y PublicRoute SIN CAMBIOS ---
|
||||
const ProtectedRoute: React.FC<{ children: JSX.Element }> = ({ children }) => {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
@@ -91,12 +99,13 @@ const AppRoutes = () => {
|
||||
<Route path="control-devoluciones" element={<ControlDevolucionesPage />} />
|
||||
<Route path="es-distribuidores" element={<ESDistribuidoresPage />} />
|
||||
<Route path="salidas-otros-destinos" element={<SalidasOtrosDestinosPage />} />
|
||||
<Route path="canillas" element={<CanillasPage />} />
|
||||
<Route path="distribuidores" element={<DistribuidoresPage />} />
|
||||
<Route path="publicaciones" element={<PublicacionesPage />} />
|
||||
<Route path="otros-destinos" element={<OtrosDestinosPage />} />
|
||||
<Route path="canillas" element={<GestionarCanillitasPage />} />
|
||||
<Route path="distribuidores" element={<GestionarDistribuidoresPage />} />
|
||||
<Route index element={<GestionarPublicacionesPage />} />
|
||||
<Route path=":idPublicacion/precios" element={<GestionarPreciosPublicacionPage />} />
|
||||
<Route path="otros-destinos" element={<GestionarOtrosDestinosPage />} />
|
||||
<Route path="zonas" element={<GestionarZonasPage />} />
|
||||
<Route path="empresas" element={<GestionarEmpresasPage />} />
|
||||
<Route path="empresas" element={<GestionarEmpresasPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Módulo Contable (anidado) */}
|
||||
@@ -118,7 +127,15 @@ const AppRoutes = () => {
|
||||
<Route path="impresion" element={<PlaceholderPage moduleName="Impresión" />} />
|
||||
<Route path="reportes" element={<PlaceholderPage moduleName="Reportes" />} />
|
||||
<Route path="radios" element={<PlaceholderPage moduleName="Radios" />} />
|
||||
{/* <Route path="usuarios" element={<PlaceholderPage moduleName="Usuarios" />} /> */}
|
||||
|
||||
{/* Módulo de Usuarios (anidado) */}
|
||||
<Route path="usuarios" element={<UsuariosIndexPage />}>
|
||||
<Route index element={<Navigate to="perfiles" replace />} /> {/* Redirigir a la primera subpestaña */}
|
||||
<Route path="perfiles" element={<GestionarPerfilesPage />} />
|
||||
<Route path="permisos" element={<GestionarPermisosPage />} />
|
||||
<Route path="perfiles/:idPerfil/permisos" element={<AsignarPermisosAPerfilPage />} />
|
||||
<Route path="gestion-usuarios" element={<GestionarUsuariosPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Ruta catch-all DENTRO del layout protegido */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { TipoPago } from '../models/Entities/TipoPago';
|
||||
import type { CreateTipoPagoDto } from '../models/dtos/tiposPago/CreateTipoPagoDto';
|
||||
import type { UpdateTipoPagoDto } from '../models/dtos/tiposPago/UpdateTipoPagoDto';
|
||||
import apiClient from '../apiClient';
|
||||
import type { TipoPago } from '../../models/Entities/TipoPago';
|
||||
import type { CreateTipoPagoDto } from '../../models/dtos/tiposPago/CreateTipoPagoDto';
|
||||
import type { UpdateTipoPagoDto } from '../../models/dtos/tiposPago/UpdateTipoPagoDto';
|
||||
|
||||
const getAllTiposPago = async (nombreFilter?: string): Promise<TipoPago[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
46
Frontend/src/services/Distribucion/canillaService.ts
Normal file
46
Frontend/src/services/Distribucion/canillaService.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import apiClient from '../apiClient';
|
||||
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 { ToggleBajaCanillaDto } from '../../models/dtos/Distribucion/ToggleBajaCanillaDto';
|
||||
|
||||
|
||||
const getAllCanillas = async (nomApeFilter?: string, legajoFilter?: number, soloActivos?: boolean): Promise<CanillaDto[]> => {
|
||||
const params: Record<string, string | number | boolean> = {};
|
||||
if (nomApeFilter) params.nomApe = nomApeFilter;
|
||||
if (legajoFilter !== undefined && legajoFilter !== null) params.legajo = legajoFilter;
|
||||
if (soloActivos !== undefined) params.soloActivos = soloActivos;
|
||||
|
||||
const response = await apiClient.get<CanillaDto[]>('/canillas', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getCanillaById = async (id: number): Promise<CanillaDto> => {
|
||||
const response = await apiClient.get<CanillaDto>(`/canillas/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createCanilla = async (data: CreateCanillaDto): Promise<CanillaDto> => {
|
||||
const response = await apiClient.post<CanillaDto>('/canillas', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateCanilla = async (id: number, data: UpdateCanillaDto): Promise<void> => {
|
||||
await apiClient.put(`/canillas/${id}`, data);
|
||||
};
|
||||
|
||||
const toggleBajaCanilla = async (id: number, data: ToggleBajaCanillaDto): Promise<void> => {
|
||||
// El backend espera el DTO en el cuerpo para este endpoint específico.
|
||||
await apiClient.post(`/canillas/${id}/toggle-baja`, data);
|
||||
};
|
||||
|
||||
|
||||
const canillaService = {
|
||||
getAllCanillas,
|
||||
getCanillaById,
|
||||
createCanilla,
|
||||
updateCanilla,
|
||||
toggleBajaCanilla,
|
||||
};
|
||||
|
||||
export default canillaService;
|
||||
41
Frontend/src/services/Distribucion/distribuidorService.ts
Normal file
41
Frontend/src/services/Distribucion/distribuidorService.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { CreateDistribuidorDto } from '../../models/dtos/Distribucion/CreateDistribuidorDto';
|
||||
import type { UpdateDistribuidorDto } from '../../models/dtos/Distribucion/UpdateDistribuidorDto';
|
||||
|
||||
const getAllDistribuidores = async (nombreFilter?: string, nroDocFilter?: string): Promise<DistribuidorDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
if (nombreFilter) params.nombre = nombreFilter;
|
||||
if (nroDocFilter) params.nroDoc = nroDocFilter;
|
||||
|
||||
const response = await apiClient.get<DistribuidorDto[]>('/distribuidores', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getDistribuidorById = async (id: number): Promise<DistribuidorDto> => {
|
||||
const response = await apiClient.get<DistribuidorDto>(`/distribuidores/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createDistribuidor = async (data: CreateDistribuidorDto): Promise<DistribuidorDto> => {
|
||||
const response = await apiClient.post<DistribuidorDto>('/distribuidores', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateDistribuidor = async (id: number, data: UpdateDistribuidorDto): Promise<void> => {
|
||||
await apiClient.put(`/distribuidores/${id}`, data);
|
||||
};
|
||||
|
||||
const deleteDistribuidor = async (id: number): Promise<void> => {
|
||||
await apiClient.delete(`/distribuidores/${id}`);
|
||||
};
|
||||
|
||||
const distribuidorService = {
|
||||
getAllDistribuidores,
|
||||
getDistribuidorById,
|
||||
createDistribuidor,
|
||||
updateDistribuidor,
|
||||
deleteDistribuidor,
|
||||
};
|
||||
|
||||
export default distribuidorService;
|
||||
@@ -1,7 +1,7 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { EmpresaDto } from '../models/dtos/Empresas/EmpresaDto';
|
||||
import type { CreateEmpresaDto } from '../models/dtos/Empresas/CreateEmpresaDto';
|
||||
import type { UpdateEmpresaDto } from '../models/dtos/Empresas/UpdateEmpresaDto';
|
||||
import apiClient from '../apiClient';
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { CreateEmpresaDto } from '../../models/dtos/Distribucion/CreateEmpresaDto';
|
||||
import type { UpdateEmpresaDto } from '../../models/dtos/Distribucion/UpdateEmpresaDto';
|
||||
|
||||
const getAllEmpresas = async (nombreFilter?: string, detalleFilter?: string): Promise<EmpresaDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
45
Frontend/src/services/Distribucion/otroDestinoService.ts
Normal file
45
Frontend/src/services/Distribucion/otroDestinoService.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import apiClient from '../apiClient';
|
||||
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 getAllOtrosDestinos = async (nombreFilter?: string): Promise<OtroDestinoDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
if (nombreFilter) params.nombre = nombreFilter;
|
||||
|
||||
// Llama a GET /api/otrosdestinos
|
||||
const response = await apiClient.get<OtroDestinoDto[]>('/otrosdestinos', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getOtroDestinoById = async (id: number): Promise<OtroDestinoDto> => {
|
||||
// Llama a GET /api/otrosdestinos/{id}
|
||||
const response = await apiClient.get<OtroDestinoDto>(`/otrosdestinos/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createOtroDestino = async (data: CreateOtroDestinoDto): Promise<OtroDestinoDto> => {
|
||||
// Llama a POST /api/otrosdestinos
|
||||
const response = await apiClient.post<OtroDestinoDto>('/otrosdestinos', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateOtroDestino = async (id: number, data: UpdateOtroDestinoDto): Promise<void> => {
|
||||
// Llama a PUT /api/otrosdestinos/{id}
|
||||
await apiClient.put(`/otrosdestinos/${id}`, data);
|
||||
};
|
||||
|
||||
const deleteOtroDestino = async (id: number): Promise<void> => {
|
||||
// Llama a DELETE /api/otrosdestinos/{id}
|
||||
await apiClient.delete(`/otrosdestinos/${id}`);
|
||||
};
|
||||
|
||||
const otroDestinoService = {
|
||||
getAllOtrosDestinos,
|
||||
getOtroDestinoById,
|
||||
createOtroDestino,
|
||||
updateOtroDestino,
|
||||
deleteOtroDestino,
|
||||
};
|
||||
|
||||
export default otroDestinoService;
|
||||
40
Frontend/src/services/Distribucion/precioService.ts
Normal file
40
Frontend/src/services/Distribucion/precioService.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { PrecioDto } from '../../models/dtos/Distribucion/PrecioDto';
|
||||
import type { CreatePrecioDto } from '../../models/dtos/Distribucion/CreatePrecioDto';
|
||||
import type { UpdatePrecioDto } from '../../models/dtos/Distribucion/UpdatePrecioDto';
|
||||
|
||||
// El idPublicacion se pasa en la URL para estos endpoints
|
||||
const getPreciosPorPublicacion = async (idPublicacion: number): Promise<PrecioDto[]> => {
|
||||
const response = await apiClient.get<PrecioDto[]>(`/publicaciones/${idPublicacion}/precios`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getPrecioById = async (idPublicacion: number, idPrecio: number): Promise<PrecioDto> => {
|
||||
const response = await apiClient.get<PrecioDto>(`/publicaciones/${idPublicacion}/precios/${idPrecio}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createPrecio = async (idPublicacion: number, data: CreatePrecioDto): Promise<PrecioDto> => {
|
||||
// Asegurarse que el DTO también contenga el idPublicacion si el backend lo espera en el cuerpo.
|
||||
// En nuestro caso, el CreatePrecioDto ya tiene IdPublicacion.
|
||||
const response = await apiClient.post<PrecioDto>(`/publicaciones/${idPublicacion}/precios`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updatePrecio = async (idPublicacion: number, idPrecio: number, data: UpdatePrecioDto): Promise<void> => {
|
||||
await apiClient.put(`/publicaciones/${idPublicacion}/precios/${idPrecio}`, data);
|
||||
};
|
||||
|
||||
const deletePrecio = async (idPublicacion: number, idPrecio: number): Promise<void> => {
|
||||
await apiClient.delete(`/publicaciones/${idPublicacion}/precios/${idPrecio}`);
|
||||
};
|
||||
|
||||
const precioService = {
|
||||
getPreciosPorPublicacion,
|
||||
getPrecioById,
|
||||
createPrecio,
|
||||
updatePrecio,
|
||||
deletePrecio,
|
||||
};
|
||||
|
||||
export default precioService;
|
||||
46
Frontend/src/services/Distribucion/publicacionService.ts
Normal file
46
Frontend/src/services/Distribucion/publicacionService.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto';
|
||||
import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto';
|
||||
|
||||
const getAllPublicaciones = async (
|
||||
nombreFilter?: string,
|
||||
idEmpresaFilter?: number,
|
||||
soloHabilitadas?: boolean
|
||||
): Promise<PublicacionDto[]> => {
|
||||
const params: Record<string, string | number | boolean> = {};
|
||||
if (nombreFilter) params.nombre = nombreFilter;
|
||||
if (idEmpresaFilter) params.idEmpresa = idEmpresaFilter;
|
||||
if (soloHabilitadas !== undefined) params.soloHabilitadas = soloHabilitadas;
|
||||
|
||||
const response = await apiClient.get<PublicacionDto[]>('/publicaciones', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getPublicacionById = async (id: number): Promise<PublicacionDto> => {
|
||||
const response = await apiClient.get<PublicacionDto>(`/publicaciones/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createPublicacion = async (data: CreatePublicacionDto): Promise<PublicacionDto> => {
|
||||
const response = await apiClient.post<PublicacionDto>('/publicaciones', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updatePublicacion = async (id: number, data: UpdatePublicacionDto): Promise<void> => {
|
||||
await apiClient.put(`/publicaciones/${id}`, data);
|
||||
};
|
||||
|
||||
const deletePublicacion = async (id: number): Promise<void> => {
|
||||
await apiClient.delete(`/publicaciones/${id}`);
|
||||
};
|
||||
|
||||
const publicacionService = {
|
||||
getAllPublicaciones,
|
||||
getPublicacionById,
|
||||
createPublicacion,
|
||||
updatePublicacion,
|
||||
deletePublicacion,
|
||||
};
|
||||
|
||||
export default publicacionService;
|
||||
@@ -1,7 +1,7 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { ZonaDto } from '../models/dtos/Zonas/ZonaDto'; // DTO para recibir listas
|
||||
import type { CreateZonaDto } from '../models/dtos/Zonas/CreateZonaDto';
|
||||
import type { UpdateZonaDto } from '../models/dtos/Zonas/UpdateZonaDto';
|
||||
import apiClient from '../apiClient';
|
||||
import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // DTO para recibir listas
|
||||
import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto';
|
||||
import type { UpdateZonaDto } from '../../models/dtos/Zonas/UpdateZonaDto';
|
||||
|
||||
const getAllZonas = async (nombreFilter?: string, descripcionFilter?: string): Promise<ZonaDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
@@ -1,7 +1,7 @@
|
||||
import apiClient from './apiClient';
|
||||
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 apiClient from '../apiClient';
|
||||
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 getAllEstadosBobina = async (denominacionFilter?: string): Promise<EstadoBobinaDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
@@ -1,7 +1,7 @@
|
||||
import apiClient from './apiClient';
|
||||
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 apiClient from '../apiClient';
|
||||
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 getAllPlantas = async (nombreFilter?: string, detalleFilter?: string): Promise<PlantaDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
@@ -1,7 +1,7 @@
|
||||
import apiClient from './apiClient';
|
||||
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 apiClient from '../apiClient';
|
||||
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 getAllTiposBobina = async (denominacionFilter?: string): Promise<TipoBobinaDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
@@ -1,7 +1,7 @@
|
||||
import apiClient from './apiClient';
|
||||
import type { LoginRequestDto } from '../models/dtos/LoginRequestDto';
|
||||
import type { LoginResponseDto } from '../models/dtos/LoginResponseDto';
|
||||
import type { ChangePasswordRequestDto } from '../models/dtos/ChangePasswordRequestDto'; // Importar DTO
|
||||
import apiClient from '../apiClient';
|
||||
import type { LoginRequestDto } from '../../models/dtos/Usuarios/LoginRequestDto';
|
||||
import type { LoginResponseDto } from '../../models/dtos/Usuarios/LoginResponseDto';
|
||||
import type { ChangePasswordRequestDto } from '../../models/dtos/Usuarios/ChangePasswordRequestDto'; // Importar DTO
|
||||
|
||||
const login = async (credentials: LoginRequestDto): Promise<LoginResponseDto> => {
|
||||
const response = await apiClient.post<LoginResponseDto>('/auth/login', credentials);
|
||||
54
Frontend/src/services/Usuarios/perfilService.ts
Normal file
54
Frontend/src/services/Usuarios/perfilService.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto';
|
||||
import type { CreatePerfilDto } from '../../models/dtos/Usuarios/CreatePerfilDto';
|
||||
import type { UpdatePerfilDto } from '../../models/dtos/Usuarios/UpdatePerfilDto';
|
||||
import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto';
|
||||
import type { ActualizarPermisosPerfilRequestDto } from '../../models/dtos/Usuarios/ActualizarPermisosPerfilRequestDto';
|
||||
|
||||
const getAllPerfiles = async (nombreFilter?: string): Promise<PerfilDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
if (nombreFilter) params.nombre = nombreFilter; // El backend espera 'nombre'
|
||||
|
||||
const response = await apiClient.get<PerfilDto[]>('/perfiles', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getPerfilById = async (id: number): Promise<PerfilDto> => {
|
||||
const response = await apiClient.get<PerfilDto>(`/perfiles/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createPerfil = async (data: CreatePerfilDto): Promise<PerfilDto> => {
|
||||
const response = await apiClient.post<PerfilDto>('/perfiles', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updatePerfil = async (id: number, data: UpdatePerfilDto): Promise<void> => {
|
||||
await apiClient.put(`/perfiles/${id}`, data);
|
||||
};
|
||||
|
||||
const deletePerfil = async (id: number): Promise<void> => {
|
||||
await apiClient.delete(`/perfiles/${id}`);
|
||||
};
|
||||
|
||||
const getPermisosPorPerfil = async (idPerfil: number): Promise<PermisoAsignadoDto[]> => {
|
||||
const response = await apiClient.get<PermisoAsignadoDto[]>(`/perfiles/${idPerfil}/permisos`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updatePermisosPorPerfil = async (idPerfil: number, data: ActualizarPermisosPerfilRequestDto): Promise<void> => {
|
||||
await apiClient.put(`/perfiles/${idPerfil}/permisos`, data);
|
||||
};
|
||||
|
||||
|
||||
const perfilService = {
|
||||
getAllPerfiles,
|
||||
getPerfilById,
|
||||
createPerfil,
|
||||
updatePerfil,
|
||||
deletePerfil,
|
||||
getPermisosPorPerfil,
|
||||
updatePermisosPorPerfil,
|
||||
};
|
||||
|
||||
export default perfilService;
|
||||
42
Frontend/src/services/Usuarios/permisoService.ts
Normal file
42
Frontend/src/services/Usuarios/permisoService.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import apiClient from '../apiClient';
|
||||
// Asegúrate que las rutas a los DTOs sean correctas
|
||||
import type { PermisoDto } from '../../models/dtos/Usuarios/PermisoDto';
|
||||
import type { CreatePermisoDto } from '../../models/dtos/Usuarios/CreatePermisoDto.ts';
|
||||
import type { UpdatePermisoDto } from '../../models/dtos/Usuarios/UpdatePermisoDto';
|
||||
|
||||
const getAllPermisos = async (moduloFilter?: string, codAccFilter?: string): Promise<PermisoDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
if (moduloFilter) params.modulo = moduloFilter;
|
||||
if (codAccFilter) params.codAcc = codAccFilter;
|
||||
|
||||
const response = await apiClient.get<PermisoDto[]>('/permisos', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getPermisoById = async (id: number): Promise<PermisoDto> => {
|
||||
const response = await apiClient.get<PermisoDto>(`/permisos/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createPermiso = async (data: CreatePermisoDto): Promise<PermisoDto> => {
|
||||
const response = await apiClient.post<PermisoDto>('/permisos', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updatePermiso = async (id: number, data: UpdatePermisoDto): Promise<void> => {
|
||||
await apiClient.put(`/permisos/${id}`, data);
|
||||
};
|
||||
|
||||
const deletePermiso = async (id: number): Promise<void> => {
|
||||
await apiClient.delete(`/permisos/${id}`);
|
||||
};
|
||||
|
||||
const permisoService = {
|
||||
getAllPermisos,
|
||||
getPermisoById,
|
||||
createPermiso,
|
||||
updatePermiso,
|
||||
deletePermiso,
|
||||
};
|
||||
|
||||
export default permisoService;
|
||||
51
Frontend/src/services/Usuarios/usuarioService.ts
Normal file
51
Frontend/src/services/Usuarios/usuarioService.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import apiClient from '../apiClient';
|
||||
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 { SetPasswordRequestDto } from '../../models/dtos/Usuarios/SetPasswordRequestDto';
|
||||
|
||||
const getAllUsuarios = async (userFilter?: string, nombreFilter?: string): Promise<UsuarioDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
if (userFilter) params.user = userFilter;
|
||||
if (nombreFilter) params.nombre = nombreFilter;
|
||||
|
||||
const response = await apiClient.get<UsuarioDto[]>('/usuarios', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getUsuarioById = async (id: number): Promise<UsuarioDto> => {
|
||||
const response = await apiClient.get<UsuarioDto>(`/usuarios/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createUsuario = async (data: CreateUsuarioRequestDto): Promise<UsuarioDto> => {
|
||||
const response = await apiClient.post<UsuarioDto>('/usuarios', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateUsuario = async (id: number, data: UpdateUsuarioRequestDto): Promise<void> => {
|
||||
await apiClient.put(`/usuarios/${id}`, data);
|
||||
};
|
||||
|
||||
const setPassword = async (id: number, data: SetPasswordRequestDto): Promise<void> => {
|
||||
await apiClient.post(`/usuarios/${id}/set-password`, data);
|
||||
};
|
||||
|
||||
const toggleHabilitado = async (id: number, habilitar: boolean): Promise<void> => {
|
||||
// El backend espera un booleano simple en el cuerpo para este endpoint específico.
|
||||
await apiClient.post(`/usuarios/${id}/toggle-habilitado`, habilitar, {
|
||||
headers: { 'Content-Type': 'application/json' } // Asegurarse de que se envíe como JSON
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const usuarioService = {
|
||||
getAllUsuarios,
|
||||
getUsuarioById,
|
||||
createUsuario,
|
||||
updateUsuario,
|
||||
setPassword,
|
||||
toggleHabilitado,
|
||||
};
|
||||
|
||||
export default usuarioService;
|
||||
Reference in New Issue
Block a user