feat: Implementación CRUD Canillitas, Distribuidores y Precios de Publicación

Backend API:
- Canillitas (`dist_dtCanillas`):
  - Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Lógica para manejo de `Accionista`, `Baja`, `FechaBaja`.
  - Auditoría en `dist_dtCanillas_H`.
  - Validación de legajo único y lógica de empresa vs accionista.
- Distribuidores (`dist_dtDistribuidores`):
  - Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Auditoría en `dist_dtDistribuidores_H`.
  - Creación de saldos iniciales para el nuevo distribuidor en todas las empresas.
  - Verificación de NroDoc único y Nombre opcionalmente único.
- Precios de Publicación (`dist_Precios`):
  - Implementado CRUD básico (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/precios`.
  - Lógica de negocio para cerrar período de precio anterior al crear uno nuevo.
  - Lógica de negocio para reabrir período de precio anterior al eliminar el último.
  - Auditoría en `dist_Precios_H`.
- Auditoría en Eliminación de Publicaciones:
  - Extendido `PublicacionService.EliminarAsync` para eliminar en cascada registros de precios, recargos, porcentajes de pago (distribuidores y canillitas) y secciones de publicación.
  - Repositorios correspondientes (`PrecioRepository`, `RecargoZonaRepository`, `PorcPagoRepository`, `PorcMonCanillaRepository`, `PubliSeccionRepository`) actualizados con métodos `DeleteByPublicacionIdAsync` que registran en sus respectivas tablas `_H` (si existen y se implementó la lógica).
  - Asegurada la correcta propagación del `idUsuario` para la auditoría en cascada.
- Correcciones de Nulabilidad:
  - Ajustados los métodos `MapToDto` y su uso en `CanillaService` y `PublicacionService` para manejar correctamente tipos anulables.

Frontend React:
- Canillitas:
  - `canillaService.ts`.
  - `CanillaFormModal.tsx` con selectores para Zona y Empresa, y lógica de Accionista.
  - `GestionarCanillitasPage.tsx` con filtros, paginación, y acciones (editar, toggle baja).
- Distribuidores:
  - `distribuidorService.ts`.
  - `DistribuidorFormModal.tsx` con múltiples campos y selector de Zona.
  - `GestionarDistribuidoresPage.tsx` con filtros, paginación, y acciones (editar, eliminar).
- Precios de Publicación:
  - `precioService.ts`.
  - `PrecioFormModal.tsx` para crear/editar períodos de precios (VigenciaD, VigenciaH opcional, precios por día).
  - `GestionarPreciosPublicacionPage.tsx` accesible desde la gestión de publicaciones, para listar y gestionar los períodos de precios de una publicación específica.
- Layout:
  - Reemplazado el uso de `Grid` por `Box` con Flexbox en `CanillaFormModal`, `GestionarCanillitasPage` (filtros), `DistribuidorFormModal` y `PrecioFormModal` para resolver problemas de tipos y mejorar la consistencia del layout de formularios.
- Navegación:
  - Actualizadas las rutas y pestañas para los nuevos módulos y sub-módulos.
This commit is contained in:
2025-05-20 12:38:55 -03:00
parent daf84d2708
commit b6ba52f074
228 changed files with 10745 additions and 178 deletions

View File

@@ -1,7 +0,0 @@
import React from 'react';
import { Typography } from '@mui/material';
const CanillasPage: React.FC = () => {
return <Typography variant="h6">Página de Gestión de Canillas</Typography>;
};
export default CanillasPage;

View File

@@ -1,7 +0,0 @@
import React from 'react';
import { Typography } from '@mui/material';
const DistribuidoresPage: React.FC = () => {
return <Typography variant="h6">Página de Gestión de Distribuidores</Typography>;
};
export default DistribuidoresPage;

View File

@@ -0,0 +1,225 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, Chip, FormControlLabel
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
import ToggleOffIcon from '@mui/icons-material/ToggleOff';
import canillaService from '../../services/Distribucion/canillaService';
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto';
import type { UpdateCanillaDto } from '../../models/dtos/Distribucion/UpdateCanillaDto';
import CanillaFormModal from '../../components/Modals/Distribucion/CanillaFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarCanillitasPage: React.FC = () => {
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filtroNomApe, setFiltroNomApe] = useState('');
const [filtroLegajo, setFiltroLegajo] = useState<string>('');
const [filtroSoloActivos, setFiltroSoloActivos] = useState<boolean | undefined>(true);
const [modalOpen, setModalOpen] = useState(false);
const [editingCanillita, setEditingCanillita] = useState<CanillaDto | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedCanillitaRow, setSelectedCanillitaRow] = useState<CanillaDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVer = isSuperAdmin || tienePermiso("CG001");
const puedeCrear = isSuperAdmin || tienePermiso("CG002");
const puedeModificar = isSuperAdmin || tienePermiso("CG003");
// CG004 para Porcentajes/Montos, se gestionará por separado.
const puedeDarBaja = isSuperAdmin || tienePermiso("CG005");
const cargarCanillitas = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const legajoNum = filtroLegajo ? parseInt(filtroLegajo, 10) : undefined;
if (filtroLegajo && isNaN(legajoNum!)) {
setApiErrorMessage("Legajo debe ser un número.");
setCanillitas([]); // Limpiar resultados si el filtro es inválido
setLoading(false);
return;
}
const data = await canillaService.getAllCanillas(filtroNomApe, legajoNum, filtroSoloActivos);
setCanillitas(data);
} catch (err) {
console.error(err); setError('Error al cargar los canillitas.');
} finally { setLoading(false); }
}, [filtroNomApe, filtroLegajo, filtroSoloActivos, puedeVer]);
useEffect(() => { cargarCanillitas(); }, [cargarCanillitas]);
const handleOpenModal = (canillita?: CanillaDto) => {
setEditingCanillita(canillita || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingCanillita(null);
};
const handleSubmitModal = async (data: CreateCanillaDto | UpdateCanillaDto, id?: number) => {
setApiErrorMessage(null);
try {
if (id && editingCanillita) {
await canillaService.updateCanilla(id, data as UpdateCanillaDto);
} else {
await canillaService.createCanilla(data as CreateCanillaDto);
}
cargarCanillitas();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el canillita.';
setApiErrorMessage(message); throw err;
}
};
const handleToggleBaja = async (canillita: CanillaDto) => {
setApiErrorMessage(null);
const accion = canillita.baja ? "reactivar" : "dar de baja";
if (window.confirm(`¿Está seguro de que desea ${accion} a ${canillita.nomApe}?`)) {
try {
await canillaService.toggleBajaCanilla(canillita.idCanilla, { darDeBaja: !canillita.baja });
cargarCanillitas();
} catch (err:any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${accion} el canillita.`;
setApiErrorMessage(message);
}
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, canillita: CanillaDto) => {
setAnchorEl(event.currentTarget); setSelectedCanillitaRow(canillita);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedCanillitaRow(null);
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = canillitas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeVer) {
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso."}</Alert></Box>;
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Canillitas</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
<TextField
label="Filtrar por Nombre/Apellido"
variant="outlined"
size="small"
value={filtroNomApe}
onChange={(e) => setFiltroNomApe(e.target.value)}
sx={{ flex: 2, minWidth: '250px' }} // Dar más espacio al nombre
/>
<TextField
label="Filtrar por Legajo"
type="number"
variant="outlined"
size="small"
value={filtroLegajo}
onChange={(e) => setFiltroLegajo(e.target.value)}
sx={{ flex: 1, minWidth: '150px' }}
/>
<FormControlLabel
control={
<Switch
checked={filtroSoloActivos === undefined ? true : filtroSoloActivos}
onChange={(e) => setFiltroSoloActivos(e.target.checked)}
size="small"
/>
}
label="Solo Activos"
sx={{ flexShrink: 0 }} // Para que el label no se comprima demasiado
/>
{/* <Button variant="contained" onClick={cargarCanillitas} size="small">Buscar</Button> */}
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Canillita</Button>
</Box>
)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{!loading && !error && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Legajo</TableCell><TableCell>Nombre y Apellido</TableCell>
<TableCell>Zona</TableCell><TableCell>Empresa</TableCell>
<TableCell>Accionista</TableCell><TableCell>Estado</TableCell>
<TableCell align="right">Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={7} align="center">No se encontraron canillitas.</TableCell></TableRow>
) : (
displayData.map((c) => (
<TableRow key={c.idCanilla} hover sx={{ backgroundColor: c.baja ? '#ffebee' : 'inherit' }}>
<TableCell>{c.legajo || '-'}</TableCell><TableCell>{c.nomApe}</TableCell>
<TableCell>{c.nombreZona}</TableCell><TableCell>{c.empresa === 0 ? '-' : c.nombreEmpresa}</TableCell>
<TableCell>{c.accionista ? <Chip label="Sí" color="success" size="small" variant="outlined"/> : <Chip label="No" color="default" size="small" variant="outlined"/>}</TableCell>
<TableCell>{c.baja ? <Chip label="Baja" color="error" size="small" /> : <Chip label="Activo" color="success" size="small" />}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeDarBaja}>
<MoreVertIcon />
</IconButton>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25]} component="div" count={canillitas.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
{puedeDarBaja && selectedCanillitaRow && (
<MenuItem onClick={() => handleToggleBaja(selectedCanillitaRow)}>
{selectedCanillitaRow.baja ? <ToggleOnIcon sx={{mr:1}}/> : <ToggleOffIcon sx={{mr:1}}/>}
{selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'}
</MenuItem>
)}
{(!puedeModificar && !puedeDarBaja) && <MenuItem disabled>Sin acciones</MenuItem>}
</Menu>
<CanillaFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingCanillita} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarCanillitasPage;

View File

@@ -0,0 +1,196 @@
// src/pages/Distribucion/GestionarDistribuidoresPage.tsx
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import distribuidorService from '../../services/Distribucion/distribuidorService';
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
import type { CreateDistribuidorDto } from '../../models/dtos/Distribucion/CreateDistribuidorDto';
import type { UpdateDistribuidorDto } from '../../models/dtos/Distribucion/UpdateDistribuidorDto';
import DistribuidorFormModal from '../../components/Modals/Distribucion/DistribuidorFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarDistribuidoresPage: React.FC = () => {
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filtroNombre, setFiltroNombre] = useState('');
const [filtroNroDoc, setFiltroNroDoc] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const [editingDistribuidor, setEditingDistribuidor] = useState<DistribuidorDto | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedDistribuidorRow, setSelectedDistribuidorRow] = useState<DistribuidorDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVer = isSuperAdmin || tienePermiso("DG001");
const puedeCrear = isSuperAdmin || tienePermiso("DG002");
const puedeModificar = isSuperAdmin || tienePermiso("DG003");
const puedeEliminar = isSuperAdmin || tienePermiso("DG005");
const cargarDistribuidores = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const data = await distribuidorService.getAllDistribuidores(filtroNombre, filtroNroDoc);
setDistribuidores(data);
} catch (err) {
console.error(err); setError('Error al cargar los distribuidores.');
} finally { setLoading(false); }
}, [filtroNombre, filtroNroDoc, puedeVer]);
useEffect(() => { cargarDistribuidores(); }, [cargarDistribuidores]);
const handleOpenModal = (distribuidor?: DistribuidorDto) => {
setEditingDistribuidor(distribuidor || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingDistribuidor(null);
};
const handleSubmitModal = async (data: CreateDistribuidorDto | UpdateDistribuidorDto, id?: number) => {
setApiErrorMessage(null);
try {
if (id && editingDistribuidor) {
await distribuidorService.updateDistribuidor(id, data as UpdateDistribuidorDto);
} else {
await distribuidorService.createDistribuidor(data as CreateDistribuidorDto);
}
cargarDistribuidores();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el distribuidor.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (id: number) => {
if (window.confirm(`¿Está seguro de eliminar este distribuidor (ID: ${id})?`)) {
setApiErrorMessage(null);
try {
await distribuidorService.deleteDistribuidor(id);
cargarDistribuidores();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el distribuidor.';
setApiErrorMessage(message);
}
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, distribuidor: DistribuidorDto) => {
setAnchorEl(event.currentTarget); setSelectedDistribuidorRow(distribuidor);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedDistribuidorRow(null);
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = distribuidores.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeVer) {
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso."}</Alert></Box>;
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Distribuidores</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
<TextField
label="Filtrar por Nombre"
variant="outlined"
size="small"
value={filtroNombre}
onChange={(e) => setFiltroNombre(e.target.value)}
sx={{ flexGrow: 1, minWidth: '200px' }}
/>
<TextField
label="Filtrar por Nro. Doc."
variant="outlined"
size="small"
value={filtroNroDoc}
onChange={(e) => setFiltroNroDoc(e.target.value)}
sx={{ flexGrow: 1, minWidth: '200px' }}
/>
{/* <Button variant="contained" onClick={cargarDistribuidores} size="small">Buscar</Button> */}
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Distribuidor</Button>
</Box>
)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{!loading && !error && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Nombre</TableCell><TableCell>Nro. Doc.</TableCell>
<TableCell>Contacto</TableCell><TableCell>Zona</TableCell>
<TableCell>Teléfono</TableCell><TableCell>Localidad</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={7} align="center">No se encontraron distribuidores.</TableCell></TableRow>
) : (
displayData.map((d) => (
<TableRow key={d.idDistribuidor} hover>
<TableCell>{d.nombre}</TableCell><TableCell>{d.nroDoc}</TableCell>
<TableCell>{d.contacto || '-'}</TableCell><TableCell>{d.nombreZona || '-'}</TableCell>
<TableCell>{d.telefono || '-'}</TableCell><TableCell>{d.localidad || '-'}</TableCell>
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, d)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25]} component="div" count={distribuidores.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedDistribuidorRow!.idDistribuidor)}>Eliminar</MenuItem>)}
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
</Menu>
<DistribuidorFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingDistribuidor} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarDistribuidoresPage;

View File

@@ -6,11 +6,11 @@ import {
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import empresaService from '../../services/empresaService'; // Importar el servicio de Empresas
import type { EmpresaDto } from '../../models/dtos/Empresas/EmpresaDto';
import type { CreateEmpresaDto } from '../../models/dtos/Empresas/CreateEmpresaDto';
import type { UpdateEmpresaDto } from '../../models/dtos/Empresas/UpdateEmpresaDto';
import EmpresaFormModal from '../../components/Modals/EmpresaFormModal'; // Importar el modal de Empresas
import empresaService from '../../services/Distribucion/empresaService'; // Importar el servicio de Empresas
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
import type { CreateEmpresaDto } from '../../models/dtos/Distribucion/CreateEmpresaDto';
import type { UpdateEmpresaDto } from '../../models/dtos/Distribucion/UpdateEmpresaDto';
import EmpresaFormModal from '../../components/Modals/Distribucion/EmpresaFormModal'; // Importar el modal de Empresas
import { usePermissions } from '../../hooks/usePermissions'; // Importar hook de permisos
import axios from 'axios'; // Para manejo de errores de API
@@ -163,7 +163,6 @@ const GestionarEmpresasPage: React.FC = () => {
size="small"
value={filtroNombre}
onChange={(e) => setFiltroNombre(e.target.value)}
// Puedes añadir un botón de buscar explícito o dejar que filtre al escribir
/>
</Box>
{/* Mostrar botón de agregar solo si tiene permiso */}

View File

@@ -0,0 +1,228 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import otroDestinoService from '../../services/Distribucion/otroDestinoService';
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto';
import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto';
import OtroDestinoFormModal from '../../components/Modals/Distribucion/OtroDestinoFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarOtrosDestinosPage: React.FC = () => {
const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filtroNombre, setFiltroNombre] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const [editingDestino, setEditingDestino] = useState<OtroDestinoDto | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedDestinoRow, setSelectedDestinoRow] = useState<OtroDestinoDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
// Permisos para Otros Destinos (OD001 a OD004) - Revisa tus códigos de permiso
const puedeVer = isSuperAdmin || tienePermiso("OD001"); // Asumiendo OD001 es ver entidad
const puedeCrear = isSuperAdmin || tienePermiso("OD002");
const puedeModificar = isSuperAdmin || tienePermiso("OD003");
const puedeEliminar = isSuperAdmin || tienePermiso("OD004");
const cargarOtrosDestinos = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
}
setLoading(true);
setError(null);
setApiErrorMessage(null);
try {
const data = await otroDestinoService.getAllOtrosDestinos(filtroNombre);
setOtrosDestinos(data);
} catch (err) {
console.error(err);
setError('Error al cargar los otros destinos.');
} finally {
setLoading(false);
}
}, [filtroNombre, puedeVer]);
useEffect(() => {
cargarOtrosDestinos();
}, [cargarOtrosDestinos]);
const handleOpenModal = (destino?: OtroDestinoDto) => {
setEditingDestino(destino || null);
setApiErrorMessage(null);
setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false);
setEditingDestino(null);
};
const handleSubmitModal = async (data: CreateOtroDestinoDto | (UpdateOtroDestinoDto & { idDestino: number })) => {
setApiErrorMessage(null);
try {
if (editingDestino && 'idDestino' in data) {
await otroDestinoService.updateOtroDestino(editingDestino.idDestino, data);
} else {
await otroDestinoService.createOtroDestino(data as CreateOtroDestinoDto);
}
cargarOtrosDestinos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Ocurrió un error inesperado al guardar el destino.';
setApiErrorMessage(message);
throw err;
}
};
const handleDelete = async (id: number) => {
if (window.confirm(`¿Está seguro de que desea eliminar este destino (ID: ${id})?`)) {
setApiErrorMessage(null);
try {
await otroDestinoService.deleteOtroDestino(id);
cargarOtrosDestinos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Ocurrió un error inesperado al eliminar el destino.';
setApiErrorMessage(message);
}
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, destino: OtroDestinoDto) => {
setAnchorEl(event.currentTarget);
setSelectedDestinoRow(destino);
};
const handleMenuClose = () => {
setAnchorEl(null);
setSelectedDestinoRow(null);
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const displayData = otrosDestinos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeVer) {
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
</Box>
);
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<TextField
label="Filtrar por Nombre"
variant="outlined"
size="small"
value={filtroNombre}
onChange={(e) => setFiltroNombre(e.target.value)}
/>
</Box>
{puedeCrear && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Nuevo Destino
</Button>
</Box>
)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{!loading && !error && (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Nombre</TableCell>
<TableCell>Observación</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{displayData.length === 0 && !loading ? (
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron otros destinos.</TableCell></TableRow>
) : (
displayData.map((destino) => (
<TableRow key={destino.idDestino}>
<TableCell>{destino.nombre}</TableCell>
<TableCell>{destino.obs || '-'}</TableCell>
{(puedeModificar || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, destino)} disabled={!puedeModificar && !puedeEliminar}>
<MoreVertIcon />
</IconButton>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={otrosDestinos.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && (
<MenuItem onClick={() => { handleOpenModal(selectedDestinoRow!); handleMenuClose(); }}>Modificar</MenuItem>
)}
{puedeEliminar && (
<MenuItem onClick={() => handleDelete(selectedDestinoRow!.idDestino)}>Eliminar</MenuItem>
)}
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
</Menu>
<OtroDestinoFormModal
open={modalOpen}
onClose={handleCloseModal}
onSubmit={handleSubmitModal}
initialData={editingDestino}
errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarOtrosDestinosPage;

View File

@@ -0,0 +1,240 @@
// src/pages/Distribucion/Publicaciones/GestionarPreciosPublicacionPage.tsx
import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
CircularProgress, Alert, Chip
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import precioService from '../../services/Distribucion/precioService';
import publicacionService from '../../services/Distribucion/publicacionService';
import type { PrecioDto } from '../../models/dtos/Distribucion/PrecioDto';
import type { CreatePrecioDto } from '../../models/dtos/Distribucion/CreatePrecioDto';
import type { UpdatePrecioDto } from '../../models/dtos/Distribucion/UpdatePrecioDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import PrecioFormModal from '../../components/Modals/Distribucion/PrecioFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarPreciosPublicacionPage: React.FC = () => {
const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>();
const navigate = useNavigate();
const idPublicacion = Number(idPublicacionStr);
const [publicacion, setPublicacion] = useState<PublicacionDto | null>(null);
const [precios, setPrecios] = useState<PrecioDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const [editingPrecio, setEditingPrecio] = useState<PrecioDto | null>(null); // Este estado determina si el modal edita
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedPrecioRow, setSelectedPrecioRow] = useState<PrecioDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeGestionarPrecios = isSuperAdmin || tienePermiso("DP004");
const cargarDatos = useCallback(async () => {
if (isNaN(idPublicacion)) {
setError("ID de Publicación inválido.");
setLoading(false);
return;
}
if (!puedeGestionarPrecios) {
setError("No tiene permiso para gestionar precios.");
setLoading(false);
return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const [pubData, preciosData] = await Promise.all([
publicacionService.getPublicacionById(idPublicacion),
precioService.getPreciosPorPublicacion(idPublicacion)
]);
setPublicacion(pubData);
setPrecios(preciosData);
} catch (err: any) {
console.error(err);
if (axios.isAxiosError(err) && err.response?.status === 404) {
setError(`Publicación con ID ${idPublicacion} no encontrada o sin acceso a sus precios.`);
} else {
setError('Error al cargar los datos de precios.');
}
} finally {
setLoading(false);
}
}, [idPublicacion, puedeGestionarPrecios]);
useEffect(() => {
cargarDatos();
}, [cargarDatos]);
const handleOpenModal = (precio?: PrecioDto) => {
setEditingPrecio(precio || null); // Si hay 'precio', el modal estará en modo edición
setApiErrorMessage(null);
setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false);
setEditingPrecio(null);
};
// CORREGIDO: El segundo parámetro 'idPrecio' determina si es edición
const handleSubmitModal = async (data: CreatePrecioDto | UpdatePrecioDto, idPrecio?: number) => {
setApiErrorMessage(null);
try {
// Si idPrecio tiene valor, Y editingPrecio (initialData del modal) también lo tenía, es una actualización
if (idPrecio && editingPrecio) {
await precioService.updatePrecio(idPublicacion, idPrecio, data as UpdatePrecioDto);
} else {
await precioService.createPrecio(idPublicacion, data as CreatePrecioDto);
}
cargarDatos(); // Recargar lista
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el período de precio.';
setApiErrorMessage(message); throw err; // Re-lanzar para que el modal maneje el estado de error
}
};
const handleDelete = async (idPrecio: number) => {
if (window.confirm(`¿Está seguro de eliminar este período de precio (ID: ${idPrecio})? Esta acción puede afectar la vigencia de períodos anteriores.`)) {
setApiErrorMessage(null);
try {
await precioService.deletePrecio(idPublicacion, idPrecio);
cargarDatos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el período de precio.';
setApiErrorMessage(message);
}
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, precio: PrecioDto) => {
setAnchorEl(event.currentTarget); setSelectedPrecioRow(precio);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedPrecioRow(null);
};
const formatDate = (dateString?: string | null) => {
if (!dateString) return '-';
const date = new Date(dateString); // Asegurar que se parsee correctamente si viene con hora
const day = String(date.getUTCDate()).padStart(2, '0');
const month = String(date.getUTCMonth() + 1).padStart(2, '0'); // Meses son 0-indexados
const year = date.getUTCFullYear();
return `${day}/${month}/${year}`;
};
if (loading) {
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
}
if (error) {
return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
}
if (!puedeGestionarPrecios) {
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
}
return (
<Box sx={{ p: 2 }}>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/distribucion/publicaciones')} sx={{ mb: 2 }}>
Volver a Publicaciones
</Button>
<Typography variant="h4" gutterBottom>
Gestionar Precios para: {publicacion?.nombre || 'Cargando...'}
</Typography>
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
Empresa: {publicacion?.nombreEmpresa || '-'}
</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
{puedeGestionarPrecios && (
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Nuevo Período de Precio
</Button>
)}
</Paper>
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Vigencia Desde</TableCell><TableCell>Vigencia Hasta</TableCell>
<TableCell align="right">Lunes</TableCell><TableCell align="right">Martes</TableCell>
<TableCell align="right">Miércoles</TableCell><TableCell align="right">Jueves</TableCell>
<TableCell align="right">Viernes</TableCell><TableCell align="right">Sábado</TableCell>
<TableCell align="right">Domingo</TableCell>
<TableCell align="center">Estado</TableCell>
<TableCell align="right">Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{precios.length === 0 ? (
<TableRow><TableCell colSpan={11} align="center">No hay períodos de precios definidos para esta publicación.</TableCell></TableRow>
) : (
precios.map((p) => (
<TableRow key={p.idPrecio} hover>
<TableCell>{formatDate(p.vigenciaD)}</TableCell>
<TableCell>{formatDate(p.vigenciaH)}</TableCell>
<TableCell align="right">{p.lunes?.toFixed(2) || '-'}</TableCell>
<TableCell align="right">{p.martes?.toFixed(2) || '-'}</TableCell>
<TableCell align="right">{p.miercoles?.toFixed(2) || '-'}</TableCell>
<TableCell align="right">{p.jueves?.toFixed(2) || '-'}</TableCell>
<TableCell align="right">{p.viernes?.toFixed(2) || '-'}</TableCell>
<TableCell align="right">{p.sabado?.toFixed(2) || '-'}</TableCell>
<TableCell align="right">{p.domingo?.toFixed(2) || '-'}</TableCell>
<TableCell align="center">
{!p.vigenciaH ? <Chip label="Activo" color="success" size="small" /> : <Chip label="Cerrado" size="small" />}
</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeGestionarPrecios}>
<MoreVertIcon />
</IconButton>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
</TableContainer>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeGestionarPrecios && selectedPrecioRow && (
<MenuItem onClick={() => { handleOpenModal(selectedPrecioRow); handleMenuClose(); }}>
<EditIcon fontSize="small" sx={{mr:1}}/> Editar Precios/Cerrar Período
</MenuItem>
)}
{puedeGestionarPrecios && selectedPrecioRow && (
<MenuItem onClick={() => handleDelete(selectedPrecioRow.idPrecio)}>
<DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar Período
</MenuItem>
)}
</Menu>
{idPublicacion &&
<PrecioFormModal
open={modalOpen}
onClose={handleCloseModal}
onSubmit={handleSubmitModal}
idPublicacion={idPublicacion}
initialData={editingPrecio} // Esto le dice al modal si está editando
errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
</Box>
);
};
export default GestionarPreciosPublicacionPage;

View File

@@ -0,0 +1,260 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, Chip, FormControl, InputLabel, Select, Tooltip,
FormControlLabel
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import publicacionService from '../../services/Distribucion/publicacionService';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto';
import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto';
import PublicacionFormModal from '../../components/Modals/Distribucion/PublicacionFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
import empresaService from '../../services/Distribucion/empresaService';
const GestionarPublicacionesPage: React.FC = () => {
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filtroNombre, setFiltroNombre] = useState('');
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
const [filtroSoloHabilitadas, setFiltroSoloHabilitadas] = useState<boolean | undefined>(true);
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
const [loadingEmpresas, setLoadingEmpresas] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editingPublicacion, setEditingPublicacion] = useState<PublicacionDto | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedPublicacionRow, setSelectedPublicacionRow] = useState<PublicacionDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const navigate = useNavigate();
const puedeVer = isSuperAdmin || tienePermiso("DP001");
const puedeCrear = isSuperAdmin || tienePermiso("DP002");
const puedeModificar = isSuperAdmin || tienePermiso("DP003");
const puedeGestionarPrecios = isSuperAdmin || tienePermiso("DP004");
const puedeGestionarRecargos = isSuperAdmin || tienePermiso("DP005");
const puedeEliminar = isSuperAdmin || tienePermiso("DP006");
const puedeGestionarSecciones = isSuperAdmin || tienePermiso("DP007");
const fetchEmpresas = useCallback(async () => {
setLoadingEmpresas(true);
try {
const data = await empresaService.getAllEmpresas();
setEmpresas(data);
} catch (err) {
console.error("Error cargando empresas para filtro:", err);
// Manejar error si es necesario, ej. mostrando un mensaje
} finally {
setLoadingEmpresas(false);
}
}, []);
useEffect(() => {
fetchEmpresas();
}, [fetchEmpresas]);
const cargarPublicaciones = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección."); setLoading(false); return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
const idEmpresa = filtroIdEmpresa ? Number(filtroIdEmpresa) : undefined;
const data = await publicacionService.getAllPublicaciones(filtroNombre, idEmpresa, filtroSoloHabilitadas);
setPublicaciones(data);
} catch (err) {
console.error(err); setError('Error al cargar las publicaciones.');
} finally { setLoading(false); }
}, [filtroNombre, filtroIdEmpresa, filtroSoloHabilitadas, puedeVer]);
useEffect(() => { cargarPublicaciones(); }, [cargarPublicaciones]);
const handleOpenModal = (publicacion?: PublicacionDto) => {
setEditingPublicacion(publicacion || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingPublicacion(null);
};
const handleSubmitModal = async (data: CreatePublicacionDto | UpdatePublicacionDto, id?: number) => {
setApiErrorMessage(null);
try {
if (id && editingPublicacion) {
await publicacionService.updatePublicacion(id, data as UpdatePublicacionDto);
} else {
await publicacionService.createPublicacion(data as CreatePublicacionDto);
}
cargarPublicaciones();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la publicación.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (id: number) => {
if (window.confirm(`¿Está seguro? Esta acción eliminará la publicación (ID: ${id}) y todas sus configuraciones asociadas (precios, recargos, secciones, etc.). ESTA ACCIÓN NO SE PUEDE DESHACER.`)) {
setApiErrorMessage(null);
try {
await publicacionService.deletePublicacion(id);
cargarPublicaciones();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la publicación.';
setApiErrorMessage(message);
}
}
handleMenuClose();
};
const handleToggleHabilitada = async (publicacion: PublicacionDto) => {
setApiErrorMessage(null);
const datosActualizados: UpdatePublicacionDto = {
nombre: publicacion.nombre,
observacion: publicacion.observacion,
idEmpresa: publicacion.idEmpresa,
ctrlDevoluciones: publicacion.ctrlDevoluciones,
habilitada: !publicacion.habilitada // Invertir estado
};
try {
await publicacionService.updatePublicacion(publicacion.idPublicacion, datosActualizados);
cargarPublicaciones(); // Recargar para ver el cambio
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar estado de habilitación.';
setApiErrorMessage(message);
}
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, publicacion: PublicacionDto) => {
setAnchorEl(event.currentTarget); setSelectedPublicacionRow(publicacion);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedPublicacionRow(null);
};
// TODO: Implementar navegación a páginas de gestión de Precios, Recargos, Secciones
const handleNavigateToPrecios = (idPub: number) => {
navigate(`/distribucion/publicaciones/${idPub}/precios`); // Ruta anidada
handleMenuClose();
};
const handleNavigateToRecargos = (idPub: number) => { navigate(`/distribucion/publicaciones/${idPub}/recargos`); handleMenuClose(); };
const handleNavigateToSecciones = (idPub: number) => { navigate(`/distribucion/publicaciones/${idPub}/secciones`); handleMenuClose(); };
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = publicaciones.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeVer) {
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso."}</Alert></Box>;
}
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Gestionar Publicaciones</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
<TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} />
<FormControl size="small" sx={{ flexGrow: 1, minWidth: '200px' }}>
<InputLabel id="empresa-filter-label">Empresa</InputLabel>
<Select
labelId="empresa-filter-label"
label="Empresa"
value={filtroIdEmpresa}
onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}
disabled={loadingEmpresas}
>
<MenuItem value=""><em>Todas</em></MenuItem>
{empresas.map((e) => (<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>))}
</Select>
</FormControl>
<FormControlLabel
control={<Switch checked={filtroSoloHabilitadas === undefined ? true : filtroSoloHabilitadas} onChange={(e) => setFiltroSoloHabilitadas(e.target.checked)} size="small" />}
label="Solo Habilitadas"
/>
</Box>
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Publicación</Button>)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{!loading && !error && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Nombre</TableCell><TableCell>Empresa</TableCell>
<TableCell>Ctrl. Devol.</TableCell><TableCell>Habilitada</TableCell>
<TableCell>Observación</TableCell>
<TableCell align="right">Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={6} align="center">No se encontraron publicaciones.</TableCell></TableRow>
) : (
displayData.map((p) => (
<TableRow key={p.idPublicacion} hover sx={{ backgroundColor: !p.habilitada ? '#fff59d' : 'inherit' }}>
<TableCell>{p.nombre}</TableCell><TableCell>{p.nombreEmpresa}</TableCell>
<TableCell align="center">{p.ctrlDevoluciones ? <Chip label="Sí" size="small" color="info" /> : <Chip label="No" size="small" />}</TableCell>
<TableCell align="center">
<Tooltip title={p.habilitada ? "Deshabilitar" : "Habilitar"}>
<Switch checked={p.habilitada} onChange={() => handleToggleHabilitada(p)} size="small" disabled={!puedeModificar} />
</Tooltip>
</TableCell>
<TableCell>{p.observacion || '-'}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeModificar && !puedeEliminar && !puedeGestionarPrecios && !puedeGestionarRecargos && !puedeGestionarSecciones}>
<MoreVertIcon />
</IconButton>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25]} component="div" count={publicaciones.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedPublicacionRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
{puedeGestionarPrecios && (<MenuItem onClick={() => handleNavigateToPrecios(selectedPublicacionRow!.idPublicacion)}>Gestionar Precios</MenuItem>)}
{puedeGestionarRecargos && (<MenuItem onClick={() => handleNavigateToRecargos(selectedPublicacionRow!.idPublicacion)}>Gestionar Recargos</MenuItem>)}
{puedeGestionarSecciones && (<MenuItem onClick={() => handleNavigateToSecciones(selectedPublicacionRow!.idPublicacion)}>Gestionar Secciones</MenuItem>)}
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedPublicacionRow!.idPublicacion)}>Eliminar</MenuItem>)}
{/* Si no hay permisos para ninguna acción */}
{(!puedeModificar && !puedeEliminar && !puedeGestionarPrecios && !puedeGestionarRecargos && !puedeGestionarSecciones) &&
<MenuItem disabled>Sin acciones</MenuItem>}
</Menu>
<PublicacionFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingPublicacion} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarPublicacionesPage;

View File

@@ -6,11 +6,11 @@ import {
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import zonaService from '../../services/zonaService'; // Servicio de Zonas
import zonaService from '../../services/Distribucion/zonaService'; // Servicio de Zonas
import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // DTO de Zonas
import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto'; // DTOs Create
import type { UpdateZonaDto } from '../../models/dtos/Zonas/UpdateZonaDto'; // DTOs Update
import ZonaFormModal from '../../components/Modals/ZonaFormModal'; // Modal de Zonas
import ZonaFormModal from '../../components/Modals/Distribucion/ZonaFormModal'; // Modal de Zonas
import { usePermissions } from '../../hooks/usePermissions'; // Hook de permisos
import axios from 'axios'; // Para manejo de errores

View File

@@ -1,7 +0,0 @@
import React from 'react';
import { Typography } from '@mui/material';
const OtrosDestinosPage: React.FC = () => {
return <Typography variant="h6">Página de Gestión de Otros Destinos</Typography>;
};
export default OtrosDestinosPage;

View File

@@ -1,7 +0,0 @@
import React from 'react';
import { Typography } from '@mui/material';
const PublicacionesPage: React.FC = () => {
return <Typography variant="h6">Página de Gestión de Publicaciones</Typography>;
};
export default PublicacionesPage;