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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user