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:
2025-05-20 12:38:55 -03:00
parent daf84d2708
commit b6ba52f074
228 changed files with 10745 additions and 178 deletions

View File

@@ -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',

View 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;

View File

@@ -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;

View File

@@ -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',

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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';

View 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;

View 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;

View File

@@ -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;

View 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;

View 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;

View File

@@ -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

View File

@@ -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 {

View 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
}

View File

@@ -0,0 +1,9 @@
export interface CreateCanillaDto {
legajo?: number | null;
nomApe: string;
parada?: string | null;
idZona: number;
accionista: boolean;
obs?: string | null;
empresa: number;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
export interface CreateOtroDestinoDto {
nombre: string;
obs?: string;
}

View 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;
}

View File

@@ -0,0 +1,7 @@
export interface CreatePublicacionDto {
nombre: string;
observacion?: string | null;
idEmpresa: number;
ctrlDevoluciones: boolean;
habilitada: boolean;
}

View 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;
}

View File

@@ -0,0 +1,5 @@
export interface OtroDestinoDto {
idDestino: number;
nombre: string;
obs?: string;
}

View 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;
}

View File

@@ -0,0 +1,9 @@
export interface PublicacionDto {
idPublicacion: number;
nombre: string;
observacion?: string | null;
idEmpresa: number;
nombreEmpresa: string;
ctrlDevoluciones: boolean;
habilitada: boolean;
}

View File

@@ -0,0 +1,3 @@
export interface ToggleBajaCanillaDto {
darDeBaja: boolean;
}

View File

@@ -0,0 +1,9 @@
export interface UpdateCanillaDto {
legajo?: number | null;
nomApe: string;
parada?: string | null;
idZona: number;
accionista: boolean;
obs?: string | null;
empresa: number;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
export interface UpdateOtroDestinoDto {
nombre: string;
obs?: string;
}

View 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;
}

View File

@@ -0,0 +1,7 @@
export interface UpdatePublicacionDto {
nombre: string;
observacion?: string | null;
idEmpresa: number;
ctrlDevoluciones: boolean;
habilitada: boolean;
}

View File

@@ -1,5 +0,0 @@
// src/models/dtos/LoginRequestDto.ts
export interface LoginRequestDto {
Username: string; // Coincide con las propiedades C#
Password: string;
}

View File

@@ -0,0 +1,3 @@
export interface ActualizarPermisosPerfilRequestDto {
permisosIds: number[];
}

View File

@@ -1,4 +1,3 @@
// src/models/dtos/ChangePasswordRequestDto.ts
export interface ChangePasswordRequestDto {
currentPassword: string;
newPassword: string;

View File

@@ -0,0 +1,4 @@
export interface CreatePerfilDto {
nombrePerfil: string;
descripcion?: string;
}

View File

@@ -0,0 +1,5 @@
export interface CreatePermisoDto {
modulo: string;
descPermiso: string;
codAcc: string;
}

View 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;
}

View File

@@ -0,0 +1,4 @@
export interface LoginRequestDto {
Username: string;
Password: string;
}

View File

@@ -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#
}

View File

@@ -0,0 +1,5 @@
export interface PerfilDto {
id: number;
nombrePerfil: string;
descripcion?: string;
}

View File

@@ -0,0 +1,7 @@
export interface PermisoAsignadoDto {
id: number;
modulo: string;
descPermiso: string;
codAcc: string;
asignado: boolean;
}

View File

@@ -0,0 +1,7 @@
export interface PermisoDto {
id: number;
modulo: string;
descPermiso: string;
codAcc: string;
asignado?: boolean;
}

View File

@@ -0,0 +1,4 @@
export interface SetPasswordRequestDto {
newPassword: string;
forceChangeOnNextLogin?: boolean;
}

View File

@@ -0,0 +1,4 @@
export interface UpdatePerfilDto {
nombrePerfil: string;
descripcion?: string;
}

View File

@@ -0,0 +1,5 @@
export interface UpdatePermisoDto {
modulo: string;
descPermiso: string;
codAcc: string;
}

View File

@@ -0,0 +1,9 @@
export interface UpdateUsuarioRequestDto {
nombre: string;
apellido: string;
idPerfil: number;
habilitada: boolean;
supAdmin: boolean;
debeCambiarClave: boolean;
verLog?: string;
}

View 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;
}

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View 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;

View 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;

View File

@@ -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 */}

View 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;

View File

@@ -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;

View 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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View 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;

View File

@@ -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();

View 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;

View 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;

View 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;

View 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;

View File

@@ -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 />} />

View File

@@ -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> = {};

View 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;

View 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;

View File

@@ -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> = {};

View 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;

View 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;

View 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;

View File

@@ -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> = {};

View File

@@ -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> = {};

View File

@@ -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> = {};

View File

@@ -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> = {};

View File

@@ -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);

View 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;

View 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;

View 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;