Fase 3:
- Backend API: Autenticación y autorización básicas con JWT implementadas. Cambio de contraseña funcional. Módulo "Tipos de Pago" (CRUD completo) implementado en el backend (Controlador, Servicio, Repositorio) usando Dapper, transacciones y con lógica de historial. Se incluyen permisos en el token JWT. - Frontend React: Estructura base con Vite, TypeScript, MUI. Contexto de autenticación (AuthContext) que maneja el estado del usuario y el token. Página de Login. Modal de Cambio de Contraseña (forzado y opcional). Hook usePermissions para verificar permisos. Página GestionarTiposPagoPage con tabla, paginación, filtro, modal para crear/editar, y menú de acciones, respetando permisos. Layout principal (MainLayout) con navegación por Tabs (funcionalidad básica de navegación). Estructura de enrutamiento (AppRoutes) que maneja rutas públicas, protegidas y anidadas para módulos.
This commit is contained in:
640
Frontend/package-lock.json
generated
640
Frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,14 @@
|
||||
"@mui/icons-material": "^7.0.2",
|
||||
"@mui/material": "^7.0.2",
|
||||
"axios": "^1.9.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/jwt-decode": "^2.2.1",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
|
||||
205
Frontend/src/components/ChangePasswordModal.tsx
Normal file
205
Frontend/src/components/ChangePasswordModal.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import authService from '../services/authService';
|
||||
import type { ChangePasswordRequestDto } from '../models/dtos/ChangePasswordRequestDto';
|
||||
import axios from 'axios';
|
||||
|
||||
import { Modal, Box, Typography, TextField, Button, Alert, CircularProgress, Backdrop } from '@mui/material';
|
||||
|
||||
const style = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
interface ChangePasswordModalProps {
|
||||
open: boolean;
|
||||
onClose: (success: boolean) => void;
|
||||
isFirstLogin?: boolean;
|
||||
}
|
||||
|
||||
const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({ open, onClose, isFirstLogin }) => {
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmNewPassword, setConfirmNewPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
// Ya no necesitamos passwordChangeCompleted ni contextIsFirstLogin aquí
|
||||
const { user, logout } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmNewPassword('');
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
setLoading(false); // Asegurarse de resetear loading también
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Esta función se llama al hacer clic en el botón Cancelar
|
||||
const handleCancelClick = () => {
|
||||
onClose(false); // Notifica al padre (MainLayout) que se canceló
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
if (newPassword !== confirmNewPassword) {
|
||||
setError('La nueva contraseña y la confirmación no coinciden.');
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 6) {
|
||||
setError('La nueva contraseña debe tener al menos 6 caracteres.');
|
||||
return;
|
||||
}
|
||||
if (user && user.username === newPassword) {
|
||||
setError('La nueva contraseña no puede ser igual al nombre de usuario.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const changePasswordData: ChangePasswordRequestDto = {
|
||||
currentPassword,
|
||||
newPassword,
|
||||
confirmNewPassword,
|
||||
};
|
||||
|
||||
try {
|
||||
await authService.changePassword(changePasswordData);
|
||||
setSuccess('Contraseña cambiada exitosamente.');
|
||||
setTimeout(() => {
|
||||
onClose(true); // Notifica al padre (MainLayout) que fue exitoso
|
||||
}, 1500);
|
||||
} catch (err: any) {
|
||||
console.error("Change password error:", err);
|
||||
let errorMessage = 'Ocurrió un error inesperado al cambiar la contraseña.';
|
||||
if (axios.isAxiosError(err) && err.response) {
|
||||
errorMessage = err.response.data?.message || errorMessage;
|
||||
if (err.response.status === 401) {
|
||||
logout(); // Desloguear si el token es inválido
|
||||
onClose(false); // Notificar cierre sin éxito
|
||||
}
|
||||
}
|
||||
setError(errorMessage);
|
||||
setLoading(false); // Asegurarse de quitar loading en caso de error
|
||||
}
|
||||
// No poner setLoading(false) en el finally si quieres que el botón siga deshabilitado durante el success
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={(_event, reason) => { // onClose del Modal de MUI (para backdrop y Escape)
|
||||
if (reason === "backdropClick" && isFirstLogin) {
|
||||
return; // No permitir cerrar con backdrop si es el primer login
|
||||
}
|
||||
onClose(false); // Llamar a la prop onClose (que va a handleModalClose en MainLayout)
|
||||
}}
|
||||
disableEscapeKeyDown={isFirstLogin} // Deshabilitar Escape si es primer login
|
||||
aria-labelledby="change-password-modal-title"
|
||||
aria-describedby="change-password-modal-description"
|
||||
closeAfterTransition
|
||||
slots={{ backdrop: Backdrop }}
|
||||
slotProps={{
|
||||
backdrop: {
|
||||
timeout: 500,
|
||||
sx: { backdropFilter: 'blur(3px)' }
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={style}>
|
||||
<Typography id="change-password-modal-title" variant="h6" component="h2">
|
||||
Cambiar Contraseña
|
||||
</Typography>
|
||||
{isFirstLogin && (
|
||||
<Alert severity="warning" sx={{ mt: 2, width: '100%' }}>
|
||||
Por seguridad, debes cambiar tu contraseña inicial.
|
||||
</Alert>
|
||||
)}
|
||||
<Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 1, width: '100%' }}>
|
||||
{/* ... TextFields ... */}
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="currentPassword"
|
||||
label="Contraseña Actual"
|
||||
type="password"
|
||||
id="currentPasswordModal"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
disabled={loading || !!success}
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="newPassword"
|
||||
label="Nueva Contraseña"
|
||||
type="password"
|
||||
id="newPasswordModal"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
disabled={loading || !!success}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="confirmNewPassword"
|
||||
label="Confirmar Nueva Contraseña"
|
||||
type="password"
|
||||
id="confirmNewPasswordModal"
|
||||
value={confirmNewPassword}
|
||||
onChange={(e) => setConfirmNewPassword(e.target.value)}
|
||||
disabled={loading || !!success}
|
||||
error={newPassword !== confirmNewPassword && confirmNewPassword !== ''}
|
||||
helperText={newPassword !== confirmNewPassword && confirmNewPassword !== '' ? 'Las contraseñas no coinciden' : ''}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 2, width: '100%' }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
{success && (
|
||||
<Alert severity="success" sx={{ mt: 2, width: '100%' }}>
|
||||
{success}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Un solo grupo de botones */}
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
{/* El botón de cancelar llama a handleCancelClick */}
|
||||
{/* Se podría ocultar si isFirstLogin es true y no queremos que el usuario cancele */}
|
||||
{/* {!isFirstLogin && ( */}
|
||||
<Button onClick={handleCancelClick} disabled={loading || !!success} color="secondary">
|
||||
{isFirstLogin ? "Cancelar y Salir" : "Cancelar"}
|
||||
</Button>
|
||||
{/* )} */}
|
||||
<Button type="submit" variant="contained" disabled={loading || !!success}>
|
||||
{loading ? <CircularProgress size={24} /> : 'Cambiar Contraseña'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePasswordModal;
|
||||
129
Frontend/src/components/Modals/TipoPagoFormModal.tsx
Normal file
129
Frontend/src/components/Modals/TipoPagoFormModal.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
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';
|
||||
|
||||
const modalStyle = {
|
||||
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 TipoPagoFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateTipoPagoDto | (CreateTipoPagoDto & { idTipoPago: number })) => Promise<void>; // Puede ser para crear o actualizar
|
||||
initialData?: TipoPago | null; // Datos para editar
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const TipoPagoFormModal: React.FC<TipoPagoFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [nombre, setNombre] = useState('');
|
||||
const [detalle, setDetalle] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setNombre(initialData?.nombre || '');
|
||||
setDetalle(initialData?.detalle || '');
|
||||
setLocalError(null); // Limpiar errores locales al abrir
|
||||
clearErrorMessage(); // Limpiar errores del padre
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const handleInputChange = () => {
|
||||
if (localError) setLocalError(null);
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setLocalError(null);
|
||||
clearErrorMessage();
|
||||
|
||||
if (!nombre.trim()) {
|
||||
setLocalError('El nombre es obligatorio.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const dataToSubmit: CreateTipoPagoDto = { nombre, detalle: detalle || undefined };
|
||||
if (isEditing && initialData) {
|
||||
await onSubmit({ ...dataToSubmit, idTipoPago: initialData.idTipoPago });
|
||||
} else {
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
onClose(); // Cerrar modal en éxito
|
||||
} catch (error: any) {
|
||||
// El error de la API ya se debería manejar en el componente padre
|
||||
// y pasarse a través de 'errorMessage', pero podemos loggear aquí si es un error inesperado
|
||||
console.error("Error en submit de TipoPagoFormModal:", error);
|
||||
// No seteamos localError aquí si el padre maneja 'errorMessage'
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2">
|
||||
{isEditing ? 'Editar Tipo de Pago' : 'Agregar Nuevo Tipo de Pago'}
|
||||
</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={!!localError && nombre.trim() === ''}
|
||||
helperText={localError && nombre.trim() === '' ? localError : ''}
|
||||
disabled={loading}
|
||||
/>
|
||||
<TextField
|
||||
label="Detalle (Opcional)"
|
||||
fullWidth
|
||||
value={detalle}
|
||||
onChange={(e) => { setDetalle(e.target.value); handleInputChange();}}
|
||||
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 TipoPagoFormModal;
|
||||
@@ -1,13 +1,41 @@
|
||||
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||
import type { ReactNode } from 'react'; // Importar como tipo
|
||||
import React, { createContext, useState, useContext, type ReactNode, useEffect } from 'react';
|
||||
import type { LoginResponseDto } from '../models/dtos/LoginResponseDto';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
// Interfaz para los datos del usuario que guardaremos en el contexto
|
||||
export interface UserContextData {
|
||||
userId: number;
|
||||
username: string;
|
||||
nombreCompleto: string;
|
||||
esSuperAdmin: boolean;
|
||||
debeCambiarClave: boolean;
|
||||
idPerfil: number;
|
||||
permissions: string[]; // Guardamos los codAcc
|
||||
}
|
||||
|
||||
// Interfaz para el payload decodificado del JWT
|
||||
interface DecodedJwtPayload {
|
||||
sub: string; // User ID (viene como string)
|
||||
name: string; // Username
|
||||
given_name?: string; // Nombre (estándar, pero verifica tu token)
|
||||
family_name?: string; // Apellido (estándar, pero verifica tu token)
|
||||
role: string | string[]; // Puede ser uno o varios roles
|
||||
idPerfil: string; // (viene como string)
|
||||
debeCambiarClave: string; // (viene como string "True" o "False")
|
||||
permission?: string | string[]; // Nuestros claims de permiso (codAcc)
|
||||
[key: string]: any; // Para otros claims
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
isAuthenticated: boolean;
|
||||
user: LoginResponseDto | null;
|
||||
user: UserContextData | null; // Usar el tipo extendido
|
||||
token: string | null;
|
||||
isLoading: boolean; // Para saber si aún está verificando el token inicial
|
||||
login: (userData: LoginResponseDto) => void;
|
||||
isLoading: boolean;
|
||||
showForcedPasswordChangeModal: boolean;
|
||||
isPasswordChangeForced: boolean;
|
||||
setShowForcedPasswordChangeModal: (show: boolean) => void;
|
||||
passwordChangeCompleted: () => void;
|
||||
login: (apiLoginResponse: LoginResponseDto) => void; // Recibe el DTO de la API
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
@@ -15,37 +43,68 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const [user, setUser] = useState<LoginResponseDto | null>(null);
|
||||
const [user, setUser] = useState<UserContextData | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true); // Empieza cargando
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [showForcedPasswordChangeModal, setShowForcedPasswordChangeModal] = useState<boolean>(false);
|
||||
const [isPasswordChangeForced, setIsPasswordChangeForced] = useState<boolean>(false);
|
||||
|
||||
// Efecto para verificar token al cargar la app
|
||||
useEffect(() => {
|
||||
const storedToken = localStorage.getItem('authToken');
|
||||
const storedUser = localStorage.getItem('authUser'); // Guardamos el usuario también
|
||||
const processTokenAndSetUser = (jwtToken: string) => {
|
||||
try {
|
||||
const decodedToken = jwtDecode<DecodedJwtPayload>(jwtToken);
|
||||
|
||||
if (storedToken && storedUser) {
|
||||
try {
|
||||
// Aquí podrías añadir lógica para validar si el token aún es válido (ej: decodificarlo)
|
||||
// Por ahora, simplemente asumimos que si está, es válido.
|
||||
const parsedUser: LoginResponseDto = JSON.parse(storedUser);
|
||||
setToken(storedToken);
|
||||
setUser(parsedUser);
|
||||
setIsAuthenticated(true);
|
||||
} catch (error) {
|
||||
console.error("Error parsing stored user data", error);
|
||||
logout(); // Limpia si hay error al parsear
|
||||
// Verificar expiración (opcional, pero buena práctica aquí también)
|
||||
const currentTime = Date.now() / 1000;
|
||||
if (decodedToken.exp && decodedToken.exp < currentTime) {
|
||||
console.warn("Token expirado al procesar.");
|
||||
logout(); // Llama a logout que limpia todo
|
||||
return;
|
||||
}
|
||||
|
||||
let permissions: string[] = [];
|
||||
if (decodedToken.permission) {
|
||||
permissions = Array.isArray(decodedToken.permission) ? decodedToken.permission : [decodedToken.permission];
|
||||
}
|
||||
|
||||
const userForContext: UserContextData = {
|
||||
userId: parseInt(decodedToken.sub, 10),
|
||||
username: decodedToken.name,
|
||||
nombreCompleto: `${decodedToken.given_name || ''} ${decodedToken.family_name || ''}`.trim(),
|
||||
esSuperAdmin: decodedToken.role === "SuperAdmin" || (Array.isArray(decodedToken.role) && decodedToken.role.includes("SuperAdmin")),
|
||||
debeCambiarClave: decodedToken.debeCambiarClave?.toLowerCase() === 'true',
|
||||
idPerfil: decodedToken.idPerfil ? parseInt(decodedToken.idPerfil, 10) : 0,
|
||||
permissions: permissions,
|
||||
};
|
||||
|
||||
setToken(jwtToken);
|
||||
setUser(userForContext);
|
||||
setIsAuthenticated(true);
|
||||
localStorage.setItem('authToken', jwtToken);
|
||||
localStorage.setItem('authUser', JSON.stringify(userForContext)); // Guardar el usuario procesado
|
||||
|
||||
// Lógica para el modal de cambio de clave
|
||||
if (userForContext.debeCambiarClave) {
|
||||
setShowForcedPasswordChangeModal(true);
|
||||
setIsPasswordChangeForced(true);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error al decodificar o procesar token:", error);
|
||||
logout(); // Limpiar estado si el token es inválido
|
||||
}
|
||||
setIsLoading(false); // Termina la carga inicial
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
const storedToken = localStorage.getItem('authToken');
|
||||
if (storedToken) {
|
||||
processTokenAndSetUser(storedToken);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
const login = (userData: LoginResponseDto) => {
|
||||
localStorage.setItem('authToken', userData.Token);
|
||||
localStorage.setItem('authUser', JSON.stringify(userData)); // Guardar datos de usuario
|
||||
setToken(userData.Token);
|
||||
setUser(userData);
|
||||
setIsAuthenticated(true);
|
||||
const login = (apiLoginResponse: LoginResponseDto) => {
|
||||
processTokenAndSetUser(apiLoginResponse.token); // Procesar el token recibido
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
@@ -54,16 +113,36 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
setIsAuthenticated(false);
|
||||
setShowForcedPasswordChangeModal(false);
|
||||
setIsPasswordChangeForced(false);
|
||||
};
|
||||
|
||||
const passwordChangeCompleted = () => {
|
||||
setShowForcedPasswordChangeModal(false);
|
||||
setIsPasswordChangeForced(false);
|
||||
// Importante: Si el cambio de clave afecta el claim "debeCambiarClave" en el token,
|
||||
// idealmente el backend debería devolver un *nuevo token* después del cambio de clave.
|
||||
// Si no lo hace, el token actual aún dirá que debe cambiar clave.
|
||||
// Una solución simple es actualizar el estado local del usuario:
|
||||
if (user) {
|
||||
const updatedUser = { ...user, debeCambiarClave: false };
|
||||
setUser(updatedUser);
|
||||
localStorage.setItem('authUser', JSON.stringify(updatedUser));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ isAuthenticated, user, token, isLoading, login, logout }}>
|
||||
<AuthContext.Provider value={{
|
||||
isAuthenticated, user, token, isLoading,
|
||||
showForcedPasswordChangeModal, isPasswordChangeForced,
|
||||
setShowForcedPasswordChangeModal, passwordChangeCompleted,
|
||||
login, logout
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook personalizado para usar el contexto fácilmente
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
|
||||
26
Frontend/src/hooks/usePermissions.ts
Normal file
26
Frontend/src/hooks/usePermissions.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// src/hooks/usePermissions.ts
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
export const usePermissions = () => {
|
||||
const { user } = useAuth(); // user aquí es de tipo UserContextData | null
|
||||
|
||||
const tienePermiso = (codigoPermisoRequerido: string): boolean => {
|
||||
if (!user) { // Si no hay usuario logueado
|
||||
return false;
|
||||
}
|
||||
if (user.esSuperAdmin) { // SuperAdmin tiene todos los permisos
|
||||
return true;
|
||||
}
|
||||
// Verificar si la lista de permisos del usuario incluye el código requerido
|
||||
return user.permissions?.includes(codigoPermisoRequerido) ?? false;
|
||||
};
|
||||
|
||||
// También puede exportar el objeto user completo si se necesita en otros lugares
|
||||
// o propiedades específicas como idPerfil, esSuperAdmin.
|
||||
return {
|
||||
tienePermiso,
|
||||
isSuperAdmin: user?.esSuperAdmin ?? false,
|
||||
idPerfil: user?.idPerfil ?? 0,
|
||||
currentUser: user
|
||||
};
|
||||
};
|
||||
@@ -1,46 +1,152 @@
|
||||
import React from 'react';
|
||||
import type { ReactNode } from 'react'; // Importar como tipo
|
||||
import { Box, AppBar, Toolbar, Typography, Button } from '@mui/material';
|
||||
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 { useNavigate, useLocation } from 'react-router-dom'; // Para manejar la navegación y la ruta actual
|
||||
|
||||
interface MainLayoutProps {
|
||||
children: ReactNode; // Para renderizar las páginas hijas
|
||||
children: ReactNode; // Esto será el <Outlet /> que renderiza las páginas del módulo
|
||||
}
|
||||
|
||||
// Definir los módulos y sus rutas base
|
||||
const modules = [
|
||||
{ label: 'Inicio', path: '/' },
|
||||
{ label: 'Distribución', path: '/distribucion' }, // Asumiremos rutas base como /distribucion, /contables, etc.
|
||||
{ label: 'Contables', path: '/contables' },
|
||||
{ label: 'Impresión', path: '/impresion' },
|
||||
{ label: 'Reportes', path: '/reportes' },
|
||||
{ label: 'Radios', path: '/radios' },
|
||||
{ label: 'Usuarios', path: '/usuarios' },
|
||||
];
|
||||
|
||||
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
const { user, logout } = useAuth();
|
||||
const {
|
||||
user,
|
||||
logout,
|
||||
showForcedPasswordChangeModal,
|
||||
isPasswordChangeForced,
|
||||
passwordChangeCompleted,
|
||||
setShowForcedPasswordChangeModal,
|
||||
isAuthenticated
|
||||
} = useAuth();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation(); // Para obtener la ruta actual
|
||||
|
||||
// Estado para el tab seleccionado
|
||||
const [selectedTab, setSelectedTab] = useState<number | false>(false);
|
||||
|
||||
// Efecto para sincronizar el tab seleccionado con la ruta actual
|
||||
useEffect(() => {
|
||||
const currentModulePath = modules.findIndex(module =>
|
||||
location.pathname === module.path || (module.path !== '/' && location.pathname.startsWith(module.path + '/'))
|
||||
);
|
||||
if (currentModulePath !== -1) {
|
||||
setSelectedTab(currentModulePath);
|
||||
} else if (location.pathname === '/') {
|
||||
setSelectedTab(0); // Seleccionar "Inicio" si es la raíz
|
||||
} else {
|
||||
setSelectedTab(false); // Ningún tab coincide (podría ser una sub-ruta no principal)
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
const handleModalClose = (passwordChangedSuccessfully: boolean) => {
|
||||
// ... (lógica de handleModalClose existente) ...
|
||||
if (passwordChangedSuccessfully) {
|
||||
passwordChangeCompleted();
|
||||
} else {
|
||||
if (isPasswordChangeForced) {
|
||||
logout();
|
||||
} else {
|
||||
setShowForcedPasswordChangeModal(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
setSelectedTab(newValue);
|
||||
navigate(modules[newValue].path); // Navegar a la ruta base del módulo
|
||||
};
|
||||
|
||||
// Si el modal de cambio de clave forzado está activo, no mostramos la navegación principal aún.
|
||||
// El modal se superpone.
|
||||
if (showForcedPasswordChangeModal && isPasswordChangeForced) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
|
||||
<ChangePasswordModal
|
||||
open={showForcedPasswordChangeModal}
|
||||
onClose={handleModalClose}
|
||||
isFirstLogin={isPasswordChangeForced}
|
||||
/>
|
||||
{/* Podrías querer un fondo o layout mínimo aquí si el modal no es pantalla completa */}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
Gestión Integral
|
||||
Sistema de Gestión - El Día
|
||||
</Typography>
|
||||
{user && <Typography sx={{ mr: 2 }}>Hola, {user.Username}</Typography> }
|
||||
{user && <Typography sx={{ mr: 2 }}>Hola, {user.nombreCompleto}</Typography>}
|
||||
{isAuthenticated && !isPasswordChangeForced && (
|
||||
<Button
|
||||
color="inherit"
|
||||
onClick={() => setShowForcedPasswordChangeModal(true)} // Ahora abre el modal
|
||||
>
|
||||
Cambiar Contraseña
|
||||
</Button>
|
||||
)}
|
||||
<Button color="inherit" onClick={logout}>Cerrar Sesión</Button>
|
||||
</Toolbar>
|
||||
{/* Aquí iría el MaterialTabControl o similar para la navegación principal */}
|
||||
{/* Navegación Principal por Módulos */}
|
||||
<Paper square elevation={0} > {/* Usamos Paper para un fondo consistente para los Tabs */}
|
||||
<Tabs
|
||||
value={selectedTab}
|
||||
onChange={handleTabChange}
|
||||
indicatorColor="secondary" // O "primary"
|
||||
textColor="inherit" // O "primary" / "secondary"
|
||||
variant="scrollable" // Permite scroll si hay muchos tabs
|
||||
scrollButtons="auto" // Muestra botones de scroll si es necesario
|
||||
aria-label="módulos principales"
|
||||
sx={{ backgroundColor: 'primary.main', color: 'white' }} // Color de fondo para los tabs
|
||||
>
|
||||
{modules.map((module) => (
|
||||
<Tab key={module.path} label={module.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
</AppBar>
|
||||
|
||||
{/* Contenido del Módulo (renderizado por <Outlet /> en AppRoutes) */}
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
p: 3, // Padding
|
||||
// Puedes añadir color de fondo si lo deseas
|
||||
// backgroundColor: (theme) => theme.palette.background.default,
|
||||
// overflowY: 'auto' // Si el contenido del módulo es muy largo
|
||||
}}
|
||||
>
|
||||
{/* El contenido de la página actual se renderizará aquí */}
|
||||
{children}
|
||||
</Box>
|
||||
{/* Aquí podría ir un Footer o StatusStrip */}
|
||||
<Box component="footer" sx={{ p: 1, mt: 'auto', backgroundColor: 'primary.dark', color: 'white', textAlign: 'center' }}>
|
||||
|
||||
<Box component="footer" sx={{ p: 1, mt: 'auto', backgroundColor: 'primary.dark', color: 'white', textAlign: 'center' }}>
|
||||
<Typography variant="body2">
|
||||
{/* Replicar info del StatusStrip original */}
|
||||
Usuario: {user?.Username} | Acceso: {user?.EsSuperAdmin ? 'Super Admin' : 'Perfil...'} | Versión: {/** Obtener versión **/}
|
||||
Usuario: {user?.username} | Acceso: {user?.esSuperAdmin ? 'Super Admin' : `Perfil ID ${user?.userId}`} | Versión: {/* TODO: Obtener versión */}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Modal para cambio de clave opcional (no forzado) */}
|
||||
{/* Si showForcedPasswordChangeModal es true pero isPasswordChangeForced es false,
|
||||
se mostrará aquí también. */}
|
||||
<ChangePasswordModal
|
||||
open={showForcedPasswordChangeModal}
|
||||
onClose={handleModalClose}
|
||||
isFirstLogin={isPasswordChangeForced} // Esto controla el comportamiento del modal
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
5
Frontend/src/models/Entities/TipoPago.ts
Normal file
5
Frontend/src/models/Entities/TipoPago.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface TipoPago {
|
||||
idTipoPago: number;
|
||||
nombre: string;
|
||||
detalle?: string; // El detalle es opcional
|
||||
}
|
||||
6
Frontend/src/models/dtos/ChangePasswordRequestDto.ts
Normal file
6
Frontend/src/models/dtos/ChangePasswordRequestDto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// src/models/dtos/ChangePasswordRequestDto.ts
|
||||
export interface ChangePasswordRequestDto {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
confirmNewPassword: string;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// src/models/dtos/LoginResponseDto.ts
|
||||
export interface LoginResponseDto {
|
||||
Token: string;
|
||||
UserId: number;
|
||||
Username: string;
|
||||
NombreCompleto: string;
|
||||
EsSuperAdmin: boolean;
|
||||
DebeCambiarClave: boolean;
|
||||
token: string;
|
||||
userId: number;
|
||||
username: string;
|
||||
nombreCompleto: string;
|
||||
esSuperAdmin: boolean;
|
||||
debeCambiarClave: boolean;
|
||||
// Añade otros campos si los definiste en el DTO C#
|
||||
}
|
||||
4
Frontend/src/models/dtos/tiposPago/CreateTipoPagoDto.ts
Normal file
4
Frontend/src/models/dtos/tiposPago/CreateTipoPagoDto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface CreateTipoPagoDto {
|
||||
nombre: string;
|
||||
detalle?: string;
|
||||
}
|
||||
4
Frontend/src/models/dtos/tiposPago/UpdateTipoPagoDto.ts
Normal file
4
Frontend/src/models/dtos/tiposPago/UpdateTipoPagoDto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface UpdateTipoPagoDto {
|
||||
nombre: string;
|
||||
detalle?: string;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography, Container } from '@mui/material';
|
||||
// import { useLocation } from 'react-router-dom'; // Para obtener el estado 'firstLogin'
|
||||
|
||||
const ChangePasswordPage: React.FC = () => {
|
||||
// const location = useLocation();
|
||||
// const isFirstLogin = location.state?.firstLogin;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
Cambiar Contraseña
|
||||
</Typography>
|
||||
{/* {isFirstLogin && <Alert severity="warning">Debes cambiar tu contraseña inicial.</Alert>} */}
|
||||
{/* Aquí irá el formulario de cambio de contraseña */}
|
||||
<Typography variant="body1">
|
||||
Formulario de cambio de contraseña irá aquí...
|
||||
</Typography>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePasswordPage;
|
||||
22
Frontend/src/pages/ChangePasswordPagePlaceholder.tsx
Normal file
22
Frontend/src/pages/ChangePasswordPagePlaceholder.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Typography, Container, Button } from '@mui/material';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const ChangePasswordPagePlaceholder: React.FC = () => {
|
||||
const { setShowForcedPasswordChangeModal } = useAuth();
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
Cambiar Contraseña (Página)
|
||||
</Typography>
|
||||
<Typography>
|
||||
La funcionalidad de cambio de contraseña ahora se maneja principalmente a través de un modal.
|
||||
</Typography>
|
||||
<Button onClick={() => setShowForcedPasswordChangeModal(true)}>
|
||||
Abrir Modal de Cambio de Contraseña
|
||||
</Button>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePasswordPagePlaceholder;
|
||||
70
Frontend/src/pages/Contables/ContablesIndexPage.tsx
Normal file
70
Frontend/src/pages/Contables/ContablesIndexPage.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
// src/pages/contables/ContablesIndexPage.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
// Define las sub-pestañas del módulo Contables
|
||||
const contablesSubModules = [
|
||||
{ label: 'Tipos de Pago', path: 'tipos-pago' }, // Se convertirá en /contables/tipos-pago
|
||||
// { label: 'Pagos', path: 'pagos' }, // Ejemplo de otra sub-pestaña futura
|
||||
// { label: 'Créditos/Débitos', path: 'creditos-debitos' },
|
||||
];
|
||||
|
||||
const ContablesIndexPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const currentBasePath = '/contables';
|
||||
const subPath = location.pathname.startsWith(currentBasePath + '/')
|
||||
? location.pathname.substring(currentBasePath.length + 1)
|
||||
: (location.pathname === currentBasePath ? contablesSubModules[0]?.path : undefined);
|
||||
|
||||
const activeTabIndex = contablesSubModules.findIndex(
|
||||
(subModule) => subModule.path === subPath
|
||||
);
|
||||
|
||||
if (activeTabIndex !== -1) {
|
||||
setSelectedSubTab(activeTabIndex);
|
||||
} else {
|
||||
if (location.pathname === currentBasePath && contablesSubModules.length > 0) {
|
||||
navigate(contablesSubModules[0].path, { replace: true });
|
||||
setSelectedSubTab(0);
|
||||
} else {
|
||||
setSelectedSubTab(false);
|
||||
}
|
||||
}
|
||||
}, [location.pathname, navigate]);
|
||||
|
||||
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
setSelectedSubTab(newValue);
|
||||
navigate(contablesSubModules[newValue].path);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" gutterBottom>Módulo Contable</Typography>
|
||||
<Paper square elevation={1}>
|
||||
<Tabs
|
||||
value={selectedSubTab}
|
||||
onChange={handleSubTabChange}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
aria-label="sub-módulos contables"
|
||||
>
|
||||
{contablesSubModules.map((subModule) => (
|
||||
<Tab key={subModule.path} label={subModule.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<Outlet /> {/* Aquí se renderizarán GestionarTiposPagoPage, etc. */}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContablesIndexPage;
|
||||
242
Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx
Normal file
242
Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
// src/pages/configuracion/GestionarTiposPagoPage.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 tipoPagoService from '../../services/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 axios from 'axios';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
|
||||
const GestionarTiposPagoPage: React.FC = () => {
|
||||
const [tiposPago, setTiposPago] = useState<TipoPago[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingTipoPago, setEditingTipoPago] = useState<TipoPago | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
|
||||
// Para el menú contextual de cada fila
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedTipoPagoRow, setSelectedTipoPagoRow] = useState<TipoPago | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions(); // Obtener también isSuperAdmin
|
||||
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CT002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("CT003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("CT004");
|
||||
|
||||
|
||||
const cargarTiposPago = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await tipoPagoService.getAllTiposPago(filtroNombre);
|
||||
setTiposPago(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Error al cargar los tipos de pago.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filtroNombre]);
|
||||
|
||||
useEffect(() => {
|
||||
cargarTiposPago();
|
||||
}, [cargarTiposPago]);
|
||||
|
||||
const handleOpenModal = (tipoPago?: TipoPago) => {
|
||||
setEditingTipoPago(tipoPago || null);
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditingTipoPago(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateTipoPagoDto | UpdateTipoPagoDto) => {
|
||||
setApiErrorMessage(null); // Limpiar error previo
|
||||
try {
|
||||
if (editingTipoPago && 'idTipoPago' in data) { // Es Update
|
||||
await tipoPagoService.updateTipoPago(editingTipoPago.idTipoPago, data as UpdateTipoPagoDto);
|
||||
} else { // Es Create
|
||||
await tipoPagoService.createTipoPago(data as CreateTipoPagoDto);
|
||||
}
|
||||
cargarTiposPago(); // Recargar lista
|
||||
// onClose se llama desde el modal en caso de éxito
|
||||
} catch (err: any) {
|
||||
console.error("Error en submit modal (padre):", err);
|
||||
if (axios.isAxiosError(err) && err.response) {
|
||||
setApiErrorMessage(err.response.data?.message || 'Error al guardar.');
|
||||
} else {
|
||||
setApiErrorMessage('Ocurrió un error inesperado al guardar.');
|
||||
}
|
||||
throw err; // Re-lanzar para que el modal sepa que hubo error y no se cierre
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm('¿Está seguro de que desea eliminar este tipo de pago?')) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await tipoPagoService.deleteTipoPago(id);
|
||||
cargarTiposPago();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
if (axios.isAxiosError(err) && err.response) {
|
||||
setApiErrorMessage(err.response.data?.message || 'Error al eliminar.');
|
||||
} else {
|
||||
setApiErrorMessage('Ocurrió un error inesperado al eliminar.');
|
||||
}
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, tipoPago: TipoPago) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedTipoPagoRow(tipoPago);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedTipoPagoRow(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 = tiposPago.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Gestionar Tipos de Pago
|
||||
</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)}
|
||||
// sx={{ flexGrow: 1 }} // Opcional, para que ocupe más espacio
|
||||
/>
|
||||
{/* El botón de búsqueda se activa al cambiar el texto, pero puedes añadir uno explícito */}
|
||||
{/* <Button variant="contained" onClick={cargarTiposPago}>Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => handleOpenModal()}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
Agregar Nuevo Tipo
|
||||
</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 && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Nombre</TableCell>
|
||||
<TableCell>Detalle</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={3} align="center">No se encontraron tipos de pago.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((tipo) => (
|
||||
<TableRow key={tipo.idTipoPago}>
|
||||
<TableCell>{tipo.nombre}</TableCell>
|
||||
<TableCell>{tipo.detalle || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={(e) => handleMenuOpen(e, tipo)}
|
||||
disabled={!puedeModificar && !puedeEliminar}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
component="div"
|
||||
count={tiposPago.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(selectedTipoPagoRow!); handleMenuClose(); }}>
|
||||
Modificar
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && (
|
||||
<MenuItem onClick={() => handleDelete(selectedTipoPagoRow!.idTipoPago)}>
|
||||
Eliminar
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Si no tiene ningún permiso, el menú podría estar vacío o no mostrarse */}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<TipoPagoFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleSubmitModal}
|
||||
initialData={editingTipoPago}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarTiposPagoPage;
|
||||
7
Frontend/src/pages/Distribucion/CanillasPage.tsx
Normal file
7
Frontend/src/pages/Distribucion/CanillasPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const CtrlDevolucionesPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión del Control de Devoluciones</Typography>;
|
||||
};
|
||||
export default CtrlDevolucionesPage;
|
||||
88
Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx
Normal file
88
Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// src/pages/distribucion/DistribucionIndexPage.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||
import { Outlet, useNavigate, useLocation, Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
// Define las sub-pestañas del módulo Distribución
|
||||
// El path es relativo a la ruta base del módulo (ej: /distribucion)
|
||||
const distribucionSubModules = [
|
||||
{ label: 'E/S Canillas', path: 'es-canillas' }, // Se convertirá en /distribucion/es-canillas
|
||||
{ label: 'Ctrl. Devoluciones', path: 'control-devoluciones' },
|
||||
{ label: 'E/S Distribuidores', path: 'es-distribuidores' },
|
||||
{ label: 'Salidas Otros Dest.', path: 'salidas-otros-destinos' },
|
||||
{ label: 'Canillas', path: 'canillas' },
|
||||
{ label: 'Distribuidores', path: 'distribuidores' },
|
||||
{ label: 'Publicaciones', path: 'publicaciones' },
|
||||
{ label: 'Otros Destinos', path: 'otros-destinos' },
|
||||
{ label: 'Zonas', path: 'zonas' },
|
||||
{ label: 'Empresas', path: 'empresas' },
|
||||
];
|
||||
|
||||
const DistribucionIndexPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
|
||||
|
||||
// Sincronizar el sub-tab con la URL actual
|
||||
useEffect(() => {
|
||||
// location.pathname será algo como /distribucion/canillas
|
||||
// Necesitamos extraer la última parte para compararla con los paths de subSubModules
|
||||
const currentBasePath = '/distribucion'; // Ruta base de este módulo
|
||||
const subPath = location.pathname.startsWith(currentBasePath + '/')
|
||||
? location.pathname.substring(currentBasePath.length + 1)
|
||||
: (location.pathname === currentBasePath ? distribucionSubModules[0]?.path : undefined); // Si es /distribucion, selecciona el primero
|
||||
|
||||
const activeTabIndex = distribucionSubModules.findIndex(
|
||||
(subModule) => subModule.path === subPath
|
||||
);
|
||||
|
||||
if (activeTabIndex !== -1) {
|
||||
setSelectedSubTab(activeTabIndex);
|
||||
} else {
|
||||
// Si no coincide ninguna sub-ruta, pero estamos en /distribucion, ir al primer tab
|
||||
if (location.pathname === currentBasePath && distribucionSubModules.length > 0) {
|
||||
navigate(distribucionSubModules[0].path, { replace: true }); // Navegar a la primera sub-ruta
|
||||
setSelectedSubTab(0);
|
||||
} else {
|
||||
setSelectedSubTab(false); // Ningún sub-tab activo
|
||||
}
|
||||
}
|
||||
}, [location.pathname, navigate]);
|
||||
|
||||
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
setSelectedSubTab(newValue);
|
||||
navigate(distribucionSubModules[newValue].path); // Navega a la sub-ruta (ej: 'canillas')
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" gutterBottom>Módulo de Distribución</Typography>
|
||||
<Paper square elevation={1}>
|
||||
<Tabs
|
||||
value={selectedSubTab}
|
||||
onChange={handleSubTabChange}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
aria-label="sub-módulos de distribución"
|
||||
>
|
||||
{distribucionSubModules.map((subModule) => (
|
||||
// Usar RouterLink para que el tab se comporte como un enlace y actualice la URL
|
||||
// La navegación real la manejamos con navigate en handleSubTabChange
|
||||
// para poder actualizar el estado del tab seleccionado.
|
||||
// Podríamos usar `component={RouterLink} to={subModule.path}` también,
|
||||
// pero manejarlo con navigate da más control sobre el estado.
|
||||
<Tab key={subModule.path} label={subModule.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
<Box sx={{ pt: 2 }}> {/* Padding para el contenido de la sub-pestaña */}
|
||||
{/* Outlet renderizará el componente de la sub-ruta activa (ej: CanillasPage) */}
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistribucionIndexPage;
|
||||
7
Frontend/src/pages/Distribucion/DistribuidoresPage.tsx
Normal file
7
Frontend/src/pages/Distribucion/DistribuidoresPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
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;
|
||||
7
Frontend/src/pages/Distribucion/ESCanillasPage.tsx
Normal file
7
Frontend/src/pages/Distribucion/ESCanillasPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const ESCanillasPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de E/S de Canillas</Typography>;
|
||||
};
|
||||
export default ESCanillasPage;
|
||||
7
Frontend/src/pages/Distribucion/ESDistribuidoresPage.tsx
Normal file
7
Frontend/src/pages/Distribucion/ESDistribuidoresPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const ESDistribuidoresPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de E/S de Distribuidores</Typography>;
|
||||
};
|
||||
export default ESDistribuidoresPage;
|
||||
7
Frontend/src/pages/Distribucion/EmpresasPage.tsx
Normal file
7
Frontend/src/pages/Distribucion/EmpresasPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const EmpresasPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de Empresas</Typography>;
|
||||
};
|
||||
export default EmpresasPage;
|
||||
7
Frontend/src/pages/Distribucion/OtrosDestinosPage.tsx
Normal file
7
Frontend/src/pages/Distribucion/OtrosDestinosPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
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;
|
||||
7
Frontend/src/pages/Distribucion/PublicacionesPage.tsx
Normal file
7
Frontend/src/pages/Distribucion/PublicacionesPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const SalidastrosDestinosPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de Salidas a Otros Destinos</Typography>;
|
||||
};
|
||||
export default SalidastrosDestinosPage;
|
||||
7
Frontend/src/pages/Distribucion/ZonasPage.tsx
Normal file
7
Frontend/src/pages/Distribucion/ZonasPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const ZonasPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de Zonas</Typography>;
|
||||
};
|
||||
export default ZonasPage;
|
||||
@@ -1,13 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios'; // Importar axios
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import apiClient from '../services/apiClient'; // Nuestro cliente axios
|
||||
import type { LoginRequestDto } from '../models/dtos/LoginRequestDto'; // Usar type
|
||||
import type { LoginResponseDto } from '../models/dtos/LoginResponseDto'; // Usar type
|
||||
|
||||
// Importaciones de Material UI
|
||||
import { Container, TextField, Button, Typography, Box, Alert } from '@mui/material';
|
||||
import authService from '../services/authService';
|
||||
|
||||
const LoginPage: React.FC = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
@@ -15,7 +13,6 @@ const LoginPage: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { login } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
@@ -25,25 +22,17 @@ const LoginPage: React.FC = () => {
|
||||
const loginData: LoginRequestDto = { Username: username, Password: password };
|
||||
|
||||
try {
|
||||
const response = await apiClient.post<LoginResponseDto>('/auth/login', loginData);
|
||||
login(response.data); // Guardar token y estado de usuario en el contexto
|
||||
|
||||
// TODO: Verificar si response.data.DebeCambiarClave es true y redirigir
|
||||
// a '/change-password' si es necesario.
|
||||
// if (response.data.DebeCambiarClave) {
|
||||
// navigate('/change-password', { state: { firstLogin: true } }); // Pasar estado si es necesario
|
||||
// } else {
|
||||
navigate('/'); // Redirigir a la página principal
|
||||
// }
|
||||
|
||||
const response = await authService.login(loginData);
|
||||
login(response);
|
||||
} catch (err: any) {
|
||||
console.error("Login error:", err);
|
||||
if (axios.isAxiosError(err) && err.response) {
|
||||
// Intenta obtener el mensaje de error de la API, si no, usa uno genérico
|
||||
setError(err.response.data?.message || 'Error al iniciar sesión. Verifique sus credenciales.');
|
||||
} else {
|
||||
setError('Ocurrió un error inesperado.');
|
||||
}
|
||||
// Importante: NO llamar a navigate('/') aquí en el catch,
|
||||
// porque el estado isAuthenticated no habrá cambiado a true
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -1,63 +1,125 @@
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
// src/routes/AppRoutes.tsx
|
||||
import React, { type JSX } from 'react';
|
||||
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom';
|
||||
import LoginPage from '../pages/LoginPage';
|
||||
import HomePage from '../pages/HomePage'; // Crearemos esta página simple
|
||||
import HomePage from '../pages/HomePage';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import ChangePasswordPage from '../pages/ChangePasswordPage'; // Crearemos esta
|
||||
import MainLayout from '../layouts/MainLayout'; // Crearemos este
|
||||
import MainLayout from '../layouts/MainLayout';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
// Componente para proteger rutas
|
||||
// Distribución
|
||||
import DistribucionIndexPage from '../pages/Distribucion/DistribucionIndexPage';
|
||||
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 ZonasPage from '../pages/Distribucion/ZonasPage';
|
||||
import EmpresasPage from '../pages/Distribucion/EmpresasPage';
|
||||
|
||||
// Contables
|
||||
import ContablesIndexPage from '../pages/Contables/ContablesIndexPage';
|
||||
import GestionarTiposPagoPage from '../pages/Contables/GestionarTiposPagoPage'; // Asumiendo que lo moviste aquí
|
||||
|
||||
// --- ProtectedRoute y PublicRoute SIN CAMBIOS ---
|
||||
const ProtectedRoute: React.FC<{ children: JSX.Element }> = ({ children }) => {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
// Muestra algo mientras verifica el token (ej: un spinner)
|
||||
return <div>Cargando...</div>;
|
||||
}
|
||||
|
||||
return isAuthenticated ? children : <Navigate to="/login" replace />;
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
// console.log("ProtectedRoute Check:", { path: window.location.pathname, isAuthenticated, isLoading });
|
||||
if (isLoading) return null;
|
||||
if (!isAuthenticated) {
|
||||
// console.log("ProtectedRoute: Not authenticated, redirecting to /login");
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
// console.log("ProtectedRoute: Authenticated, rendering children");
|
||||
return children;
|
||||
};
|
||||
|
||||
// Componente para rutas públicas (redirige si ya está logueado)
|
||||
const PublicRoute: React.FC<{ children: JSX.Element }> = ({ children }) => {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Cargando...</div>;
|
||||
// console.log("PublicRoute Check:", { path: window.location.pathname, isAuthenticated, isLoading });
|
||||
if (isLoading) return null;
|
||||
if (isAuthenticated) {
|
||||
// console.log("PublicRoute: Authenticated, redirecting to /");
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return !isAuthenticated ? children : <Navigate to="/" replace />;
|
||||
// console.log("PublicRoute: Not authenticated, rendering children");
|
||||
return children;
|
||||
};
|
||||
// --- Fin Protected/Public ---
|
||||
|
||||
const MainLayoutWrapper: React.FC = () => (
|
||||
<MainLayout>
|
||||
<Outlet />
|
||||
</MainLayout>
|
||||
);
|
||||
|
||||
// Placeholder simple
|
||||
const PlaceholderPage: React.FC<{ moduleName: string }> = ({ moduleName }) => (
|
||||
<Typography variant="h5" sx={{ p:2 }}>Página Principal del Módulo: {moduleName}</Typography>
|
||||
);
|
||||
|
||||
const AppRoutes = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* Rutas Públicas */}
|
||||
<Route path="/login" element={<PublicRoute><LoginPage /></PublicRoute>} />
|
||||
<Route path="/change-password" element={<ProtectedRoute><ChangePasswordPage /></ProtectedRoute>} /> {/* Asumimos que se accede logueado */}
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes> {/* Un solo <Routes> de nivel superior */}
|
||||
<Route path="/login" element={<PublicRoute><LoginPage /></PublicRoute>} />
|
||||
|
||||
{/* Rutas Protegidas dentro del Layout Principal */}
|
||||
<Route
|
||||
path="/*" // Captura todas las demás rutas
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<MainLayout> {/* Layout que tendrá la navegación principal */}
|
||||
{/* Aquí irán las rutas de los módulos */}
|
||||
<Routes>
|
||||
<Route index element={<HomePage />} /> {/* Página por defecto al loguearse */}
|
||||
{/* <Route path="/usuarios" element={<GestionUsuariosPage />} /> */}
|
||||
{/* <Route path="/zonas" element={<GestionZonasPage />} /> */}
|
||||
{/* ... otras rutas de módulos ... */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} /> {/* Redirige rutas no encontradas al home */}
|
||||
</Routes>
|
||||
</MainLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
{/* Rutas Protegidas que usan el MainLayout */}
|
||||
<Route
|
||||
path="/" // La ruta padre para todas las secciones protegidas
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<MainLayoutWrapper/>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
>
|
||||
{/* Rutas hijas que se renderizarán en el Outlet de MainLayoutWrapper */}
|
||||
<Route index element={<HomePage />} /> {/* Para la ruta exacta "/" */}
|
||||
|
||||
{/* Módulo de Distribución (anidado) */}
|
||||
<Route path="distribucion" element={<DistribucionIndexPage />}>
|
||||
<Route index element={<Navigate to="es-canillas" replace />} />
|
||||
<Route path="es-canillas" element={<ESCanillasPage />} />
|
||||
<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="zonas" element={<ZonasPage />} />
|
||||
<Route path="empresas" element={<EmpresasPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Módulo Contable (anidado) */}
|
||||
<Route path="contables" element={<ContablesIndexPage />}>
|
||||
<Route index element={<Navigate to="tipos-pago" replace />} />
|
||||
<Route path="tipos-pago" element={<GestionarTiposPagoPage />} />
|
||||
{/* Futuras sub-rutas de contables aquí */}
|
||||
</Route>
|
||||
|
||||
{/* Otros Módulos Principales (estos son "finales", no tienen más hijos) */}
|
||||
<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" />} /> */}
|
||||
|
||||
{/* Ruta catch-all DENTRO del layout protegido */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route> {/* Cierre de la ruta padre "/" */}
|
||||
|
||||
{/* Podrías tener un catch-all global aquí si una ruta no coincide EN ABSOLUTO,
|
||||
pero el path="*" dentro de la ruta "/" ya debería manejar la mayoría de los casos
|
||||
después de un login exitoso.
|
||||
Si un usuario no autenticado intenta una ruta inválida, ProtectedRoute lo manda a /login.
|
||||
*/}
|
||||
{/* <Route path="*" element={<Navigate to="/login" replace />} /> */}
|
||||
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppRoutes;
|
||||
21
Frontend/src/services/authService.ts
Normal file
21
Frontend/src/services/authService.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
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
|
||||
|
||||
const login = async (credentials: LoginRequestDto): Promise<LoginResponseDto> => {
|
||||
const response = await apiClient.post<LoginResponseDto>('/auth/login', credentials);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const changePassword = async (data: ChangePasswordRequestDto): Promise<void> => {
|
||||
// No esperamos datos de vuelta, solo éxito (204) o error
|
||||
await apiClient.post('/auth/change-password', data);
|
||||
};
|
||||
|
||||
const authService = {
|
||||
login,
|
||||
changePassword, // Exportar la nueva función
|
||||
};
|
||||
|
||||
export default authService;
|
||||
43
Frontend/src/services/tipoPagoService.ts
Normal file
43
Frontend/src/services/tipoPagoService.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
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> = {};
|
||||
if (nombreFilter) {
|
||||
params.nombre = nombreFilter;
|
||||
}
|
||||
const response = await apiClient.get<TipoPago[]>('/tipospago', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getTipoPagoById = async (id: number): Promise<TipoPago> => {
|
||||
const response = await apiClient.get<TipoPago>(`/tipospago/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createTipoPago = async (data: CreateTipoPagoDto): Promise<TipoPago> => {
|
||||
const response = await apiClient.post<TipoPago>('/tipospago', data);
|
||||
return response.data; // La API devuelve el objeto creado (201 Created)
|
||||
};
|
||||
|
||||
const updateTipoPago = async (id: number, data: UpdateTipoPagoDto): Promise<void> => {
|
||||
// PUT no suele devolver contenido en éxito (204 No Content)
|
||||
await apiClient.put(`/tipospago/${id}`, data);
|
||||
};
|
||||
|
||||
const deleteTipoPago = async (id: number): Promise<void> => {
|
||||
// DELETE no suele devolver contenido en éxito (204 No Content)
|
||||
await apiClient.delete(`/tipospago/${id}`);
|
||||
};
|
||||
|
||||
const tipoPagoService = {
|
||||
getAllTiposPago,
|
||||
getTipoPagoById,
|
||||
createTipoPago,
|
||||
updateTipoPago,
|
||||
deleteTipoPago,
|
||||
};
|
||||
|
||||
export default tipoPagoService;
|
||||
Reference in New Issue
Block a user