feat: Implementación CRUD Canillitas, Distribuidores y Precios de Publicación
Backend API:
- Canillitas (`dist_dtCanillas`):
- Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Lógica para manejo de `Accionista`, `Baja`, `FechaBaja`.
- Auditoría en `dist_dtCanillas_H`.
- Validación de legajo único y lógica de empresa vs accionista.
- Distribuidores (`dist_dtDistribuidores`):
- Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Auditoría en `dist_dtDistribuidores_H`.
- Creación de saldos iniciales para el nuevo distribuidor en todas las empresas.
- Verificación de NroDoc único y Nombre opcionalmente único.
- Precios de Publicación (`dist_Precios`):
- Implementado CRUD básico (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Endpoints anidados bajo `/publicaciones/{idPublicacion}/precios`.
- Lógica de negocio para cerrar período de precio anterior al crear uno nuevo.
- Lógica de negocio para reabrir período de precio anterior al eliminar el último.
- Auditoría en `dist_Precios_H`.
- Auditoría en Eliminación de Publicaciones:
- Extendido `PublicacionService.EliminarAsync` para eliminar en cascada registros de precios, recargos, porcentajes de pago (distribuidores y canillitas) y secciones de publicación.
- Repositorios correspondientes (`PrecioRepository`, `RecargoZonaRepository`, `PorcPagoRepository`, `PorcMonCanillaRepository`, `PubliSeccionRepository`) actualizados con métodos `DeleteByPublicacionIdAsync` que registran en sus respectivas tablas `_H` (si existen y se implementó la lógica).
- Asegurada la correcta propagación del `idUsuario` para la auditoría en cascada.
- Correcciones de Nulabilidad:
- Ajustados los métodos `MapToDto` y su uso en `CanillaService` y `PublicacionService` para manejar correctamente tipos anulables.
Frontend React:
- Canillitas:
- `canillaService.ts`.
- `CanillaFormModal.tsx` con selectores para Zona y Empresa, y lógica de Accionista.
- `GestionarCanillitasPage.tsx` con filtros, paginación, y acciones (editar, toggle baja).
- Distribuidores:
- `distribuidorService.ts`.
- `DistribuidorFormModal.tsx` con múltiples campos y selector de Zona.
- `GestionarDistribuidoresPage.tsx` con filtros, paginación, y acciones (editar, eliminar).
- Precios de Publicación:
- `precioService.ts`.
- `PrecioFormModal.tsx` para crear/editar períodos de precios (VigenciaD, VigenciaH opcional, precios por día).
- `GestionarPreciosPublicacionPage.tsx` accesible desde la gestión de publicaciones, para listar y gestionar los períodos de precios de una publicación específica.
- Layout:
- Reemplazado el uso de `Grid` por `Box` con Flexbox en `CanillaFormModal`, `GestionarCanillitasPage` (filtros), `DistribuidorFormModal` y `PrecioFormModal` para resolver problemas de tipos y mejorar la consistencia del layout de formularios.
- Navegación:
- Actualizadas las rutas y pestañas para los nuevos módulos y sub-módulos.
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const CanillasPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de Canillas</Typography>;
|
||||
};
|
||||
export default CanillasPage;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const DistribuidoresPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de Distribuidores</Typography>;
|
||||
};
|
||||
export default DistribuidoresPage;
|
||||
225
Frontend/src/pages/Distribucion/GestionarCanillitasPage.tsx
Normal file
225
Frontend/src/pages/Distribucion/GestionarCanillitasPage.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Chip, FormControlLabel
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
|
||||
import ToggleOffIcon from '@mui/icons-material/ToggleOff';
|
||||
import canillaService from '../../services/Distribucion/canillaService';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto';
|
||||
import type { UpdateCanillaDto } from '../../models/dtos/Distribucion/UpdateCanillaDto';
|
||||
import CanillaFormModal from '../../components/Modals/Distribucion/CanillaFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarCanillitasPage: React.FC = () => {
|
||||
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroNomApe, setFiltroNomApe] = useState('');
|
||||
const [filtroLegajo, setFiltroLegajo] = useState<string>('');
|
||||
const [filtroSoloActivos, setFiltroSoloActivos] = useState<boolean | undefined>(true);
|
||||
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingCanillita, setEditingCanillita] = useState<CanillaDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedCanillitaRow, setSelectedCanillitaRow] = useState<CanillaDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("CG001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CG002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("CG003");
|
||||
// CG004 para Porcentajes/Montos, se gestionará por separado.
|
||||
const puedeDarBaja = isSuperAdmin || tienePermiso("CG005");
|
||||
|
||||
const cargarCanillitas = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const legajoNum = filtroLegajo ? parseInt(filtroLegajo, 10) : undefined;
|
||||
if (filtroLegajo && isNaN(legajoNum!)) {
|
||||
setApiErrorMessage("Legajo debe ser un número.");
|
||||
setCanillitas([]); // Limpiar resultados si el filtro es inválido
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const data = await canillaService.getAllCanillas(filtroNomApe, legajoNum, filtroSoloActivos);
|
||||
setCanillitas(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar los canillitas.');
|
||||
} finally { setLoading(false); }
|
||||
}, [filtroNomApe, filtroLegajo, filtroSoloActivos, puedeVer]);
|
||||
|
||||
useEffect(() => { cargarCanillitas(); }, [cargarCanillitas]);
|
||||
|
||||
const handleOpenModal = (canillita?: CanillaDto) => {
|
||||
setEditingCanillita(canillita || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingCanillita(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateCanillaDto | UpdateCanillaDto, id?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (id && editingCanillita) {
|
||||
await canillaService.updateCanilla(id, data as UpdateCanillaDto);
|
||||
} else {
|
||||
await canillaService.createCanilla(data as CreateCanillaDto);
|
||||
}
|
||||
cargarCanillitas();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el canillita.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleBaja = async (canillita: CanillaDto) => {
|
||||
setApiErrorMessage(null);
|
||||
const accion = canillita.baja ? "reactivar" : "dar de baja";
|
||||
if (window.confirm(`¿Está seguro de que desea ${accion} a ${canillita.nomApe}?`)) {
|
||||
try {
|
||||
await canillaService.toggleBajaCanilla(canillita.idCanilla, { darDeBaja: !canillita.baja });
|
||||
cargarCanillitas();
|
||||
} catch (err:any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${accion} el canillita.`;
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, canillita: CanillaDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedCanillitaRow(canillita);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedCanillitaRow(null);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = canillitas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso."}</Alert></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Canillitas</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Filtrar por Nombre/Apellido"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroNomApe}
|
||||
onChange={(e) => setFiltroNomApe(e.target.value)}
|
||||
sx={{ flex: 2, minWidth: '250px' }} // Dar más espacio al nombre
|
||||
/>
|
||||
<TextField
|
||||
label="Filtrar por Legajo"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroLegajo}
|
||||
onChange={(e) => setFiltroLegajo(e.target.value)}
|
||||
sx={{ flex: 1, minWidth: '150px' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={filtroSoloActivos === undefined ? true : filtroSoloActivos}
|
||||
onChange={(e) => setFiltroSoloActivos(e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label="Solo Activos"
|
||||
sx={{ flexShrink: 0 }} // Para que el label no se comprima demasiado
|
||||
/>
|
||||
{/* <Button variant="contained" onClick={cargarCanillitas} size="small">Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Canillita</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Legajo</TableCell><TableCell>Nombre y Apellido</TableCell>
|
||||
<TableCell>Zona</TableCell><TableCell>Empresa</TableCell>
|
||||
<TableCell>Accionista</TableCell><TableCell>Estado</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={7} align="center">No se encontraron canillitas.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((c) => (
|
||||
<TableRow key={c.idCanilla} hover sx={{ backgroundColor: c.baja ? '#ffebee' : 'inherit' }}>
|
||||
<TableCell>{c.legajo || '-'}</TableCell><TableCell>{c.nomApe}</TableCell>
|
||||
<TableCell>{c.nombreZona}</TableCell><TableCell>{c.empresa === 0 ? '-' : c.nombreEmpresa}</TableCell>
|
||||
<TableCell>{c.accionista ? <Chip label="Sí" color="success" size="small" variant="outlined"/> : <Chip label="No" color="default" size="small" variant="outlined"/>}</TableCell>
|
||||
<TableCell>{c.baja ? <Chip label="Baja" color="error" size="small" /> : <Chip label="Activo" color="success" size="small" />}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeDarBaja}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]} component="div" count={canillitas.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
|
||||
{puedeDarBaja && selectedCanillitaRow && (
|
||||
<MenuItem onClick={() => handleToggleBaja(selectedCanillitaRow)}>
|
||||
{selectedCanillitaRow.baja ? <ToggleOnIcon sx={{mr:1}}/> : <ToggleOffIcon sx={{mr:1}}/>}
|
||||
{selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'}
|
||||
</MenuItem>
|
||||
)}
|
||||
{(!puedeModificar && !puedeDarBaja) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<CanillaFormModal
|
||||
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
||||
initialData={editingCanillita} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarCanillitasPage;
|
||||
196
Frontend/src/pages/Distribucion/GestionarDistribuidoresPage.tsx
Normal file
196
Frontend/src/pages/Distribucion/GestionarDistribuidoresPage.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
// src/pages/Distribucion/GestionarDistribuidoresPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { CreateDistribuidorDto } from '../../models/dtos/Distribucion/CreateDistribuidorDto';
|
||||
import type { UpdateDistribuidorDto } from '../../models/dtos/Distribucion/UpdateDistribuidorDto';
|
||||
import DistribuidorFormModal from '../../components/Modals/Distribucion/DistribuidorFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
|
||||
const GestionarDistribuidoresPage: React.FC = () => {
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
const [filtroNroDoc, setFiltroNroDoc] = useState('');
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingDistribuidor, setEditingDistribuidor] = useState<DistribuidorDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedDistribuidorRow, setSelectedDistribuidorRow] = useState<DistribuidorDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("DG001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("DG002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("DG003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("DG005");
|
||||
|
||||
const cargarDistribuidores = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const data = await distribuidorService.getAllDistribuidores(filtroNombre, filtroNroDoc);
|
||||
setDistribuidores(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar los distribuidores.');
|
||||
} finally { setLoading(false); }
|
||||
}, [filtroNombre, filtroNroDoc, puedeVer]);
|
||||
|
||||
useEffect(() => { cargarDistribuidores(); }, [cargarDistribuidores]);
|
||||
|
||||
const handleOpenModal = (distribuidor?: DistribuidorDto) => {
|
||||
setEditingDistribuidor(distribuidor || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingDistribuidor(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateDistribuidorDto | UpdateDistribuidorDto, id?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (id && editingDistribuidor) {
|
||||
await distribuidorService.updateDistribuidor(id, data as UpdateDistribuidorDto);
|
||||
} else {
|
||||
await distribuidorService.createDistribuidor(data as CreateDistribuidorDto);
|
||||
}
|
||||
cargarDistribuidores();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el distribuidor.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm(`¿Está seguro de eliminar este distribuidor (ID: ${id})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await distribuidorService.deleteDistribuidor(id);
|
||||
cargarDistribuidores();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el distribuidor.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, distribuidor: DistribuidorDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedDistribuidorRow(distribuidor);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedDistribuidorRow(null);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = distribuidores.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso."}</Alert></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Distribuidores</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
|
||||
<TextField
|
||||
label="Filtrar por Nombre"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroNombre}
|
||||
onChange={(e) => setFiltroNombre(e.target.value)}
|
||||
sx={{ flexGrow: 1, minWidth: '200px' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Filtrar por Nro. Doc."
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroNroDoc}
|
||||
onChange={(e) => setFiltroNroDoc(e.target.value)}
|
||||
sx={{ flexGrow: 1, minWidth: '200px' }}
|
||||
/>
|
||||
{/* <Button variant="contained" onClick={cargarDistribuidores} size="small">Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Distribuidor</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Nombre</TableCell><TableCell>Nro. Doc.</TableCell>
|
||||
<TableCell>Contacto</TableCell><TableCell>Zona</TableCell>
|
||||
<TableCell>Teléfono</TableCell><TableCell>Localidad</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={7} align="center">No se encontraron distribuidores.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((d) => (
|
||||
<TableRow key={d.idDistribuidor} hover>
|
||||
<TableCell>{d.nombre}</TableCell><TableCell>{d.nroDoc}</TableCell>
|
||||
<TableCell>{d.contacto || '-'}</TableCell><TableCell>{d.nombreZona || '-'}</TableCell>
|
||||
<TableCell>{d.telefono || '-'}</TableCell><TableCell>{d.localidad || '-'}</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, d)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]} component="div" count={distribuidores.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
|
||||
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedDistribuidorRow!.idDistribuidor)}>Eliminar</MenuItem>)}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<DistribuidorFormModal
|
||||
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
||||
initialData={editingDistribuidor} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarDistribuidoresPage;
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import empresaService from '../../services/empresaService'; // Importar el servicio de Empresas
|
||||
import type { EmpresaDto } from '../../models/dtos/Empresas/EmpresaDto';
|
||||
import type { CreateEmpresaDto } from '../../models/dtos/Empresas/CreateEmpresaDto';
|
||||
import type { UpdateEmpresaDto } from '../../models/dtos/Empresas/UpdateEmpresaDto';
|
||||
import EmpresaFormModal from '../../components/Modals/EmpresaFormModal'; // Importar el modal de Empresas
|
||||
import empresaService from '../../services/Distribucion/empresaService'; // Importar el servicio de Empresas
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { CreateEmpresaDto } from '../../models/dtos/Distribucion/CreateEmpresaDto';
|
||||
import type { UpdateEmpresaDto } from '../../models/dtos/Distribucion/UpdateEmpresaDto';
|
||||
import EmpresaFormModal from '../../components/Modals/Distribucion/EmpresaFormModal'; // Importar el modal de Empresas
|
||||
import { usePermissions } from '../../hooks/usePermissions'; // Importar hook de permisos
|
||||
import axios from 'axios'; // Para manejo de errores de API
|
||||
|
||||
@@ -163,7 +163,6 @@ const GestionarEmpresasPage: React.FC = () => {
|
||||
size="small"
|
||||
value={filtroNombre}
|
||||
onChange={(e) => setFiltroNombre(e.target.value)}
|
||||
// Puedes añadir un botón de buscar explícito o dejar que filtre al escribir
|
||||
/>
|
||||
</Box>
|
||||
{/* Mostrar botón de agregar solo si tiene permiso */}
|
||||
|
||||
228
Frontend/src/pages/Distribucion/GestionarOtrosDestinosPage.tsx
Normal file
228
Frontend/src/pages/Distribucion/GestionarOtrosDestinosPage.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import otroDestinoService from '../../services/Distribucion/otroDestinoService';
|
||||
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
|
||||
import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto';
|
||||
import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto';
|
||||
import OtroDestinoFormModal from '../../components/Modals/Distribucion/OtroDestinoFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarOtrosDestinosPage: React.FC = () => {
|
||||
const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingDestino, setEditingDestino] = useState<OtroDestinoDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedDestinoRow, setSelectedDestinoRow] = useState<OtroDestinoDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
// Permisos para Otros Destinos (OD001 a OD004) - Revisa tus códigos de permiso
|
||||
const puedeVer = isSuperAdmin || tienePermiso("OD001"); // Asumiendo OD001 es ver entidad
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("OD002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("OD003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("OD004");
|
||||
|
||||
const cargarOtrosDestinos = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
const data = await otroDestinoService.getAllOtrosDestinos(filtroNombre);
|
||||
setOtrosDestinos(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Error al cargar los otros destinos.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filtroNombre, puedeVer]);
|
||||
|
||||
useEffect(() => {
|
||||
cargarOtrosDestinos();
|
||||
}, [cargarOtrosDestinos]);
|
||||
|
||||
const handleOpenModal = (destino?: OtroDestinoDto) => {
|
||||
setEditingDestino(destino || null);
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditingDestino(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateOtroDestinoDto | (UpdateOtroDestinoDto & { idDestino: number })) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (editingDestino && 'idDestino' in data) {
|
||||
await otroDestinoService.updateOtroDestino(editingDestino.idDestino, data);
|
||||
} else {
|
||||
await otroDestinoService.createOtroDestino(data as CreateOtroDestinoDto);
|
||||
}
|
||||
cargarOtrosDestinos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error inesperado al guardar el destino.';
|
||||
setApiErrorMessage(message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm(`¿Está seguro de que desea eliminar este destino (ID: ${id})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await otroDestinoService.deleteOtroDestino(id);
|
||||
cargarOtrosDestinos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error inesperado al eliminar el destino.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, destino: OtroDestinoDto) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedDestinoRow(destino);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedDestinoRow(null);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const displayData = otrosDestinos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<TextField
|
||||
label="Filtrar por Nombre"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroNombre}
|
||||
onChange={(e) => setFiltroNombre(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
|
||||
Agregar Nuevo Destino
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Nombre</TableCell>
|
||||
<TableCell>Observación</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron otros destinos.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((destino) => (
|
||||
<TableRow key={destino.idDestino}>
|
||||
<TableCell>{destino.nombre}</TableCell>
|
||||
<TableCell>{destino.obs || '-'}</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, destino)} disabled={!puedeModificar && !puedeEliminar}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
component="div"
|
||||
count={otrosDestinos.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedDestinoRow!); handleMenuClose(); }}>Modificar</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && (
|
||||
<MenuItem onClick={() => handleDelete(selectedDestinoRow!.idDestino)}>Eliminar</MenuItem>
|
||||
)}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<OtroDestinoFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleSubmitModal}
|
||||
initialData={editingDestino}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarOtrosDestinosPage;
|
||||
@@ -0,0 +1,240 @@
|
||||
// src/pages/Distribucion/Publicaciones/GestionarPreciosPublicacionPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
|
||||
CircularProgress, Alert, Chip
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
import precioService from '../../services/Distribucion/precioService';
|
||||
import publicacionService from '../../services/Distribucion/publicacionService';
|
||||
import type { PrecioDto } from '../../models/dtos/Distribucion/PrecioDto';
|
||||
import type { CreatePrecioDto } from '../../models/dtos/Distribucion/CreatePrecioDto';
|
||||
import type { UpdatePrecioDto } from '../../models/dtos/Distribucion/UpdatePrecioDto';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import PrecioFormModal from '../../components/Modals/Distribucion/PrecioFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarPreciosPublicacionPage: React.FC = () => {
|
||||
const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>();
|
||||
const navigate = useNavigate();
|
||||
const idPublicacion = Number(idPublicacionStr);
|
||||
|
||||
const [publicacion, setPublicacion] = useState<PublicacionDto | null>(null);
|
||||
const [precios, setPrecios] = useState<PrecioDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingPrecio, setEditingPrecio] = useState<PrecioDto | null>(null); // Este estado determina si el modal edita
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedPrecioRow, setSelectedPrecioRow] = useState<PrecioDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeGestionarPrecios = isSuperAdmin || tienePermiso("DP004");
|
||||
|
||||
const cargarDatos = useCallback(async () => {
|
||||
if (isNaN(idPublicacion)) {
|
||||
setError("ID de Publicación inválido.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!puedeGestionarPrecios) {
|
||||
setError("No tiene permiso para gestionar precios.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const [pubData, preciosData] = await Promise.all([
|
||||
publicacionService.getPublicacionById(idPublicacion),
|
||||
precioService.getPreciosPorPublicacion(idPublicacion)
|
||||
]);
|
||||
setPublicacion(pubData);
|
||||
setPrecios(preciosData);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
if (axios.isAxiosError(err) && err.response?.status === 404) {
|
||||
setError(`Publicación con ID ${idPublicacion} no encontrada o sin acceso a sus precios.`);
|
||||
} else {
|
||||
setError('Error al cargar los datos de precios.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idPublicacion, puedeGestionarPrecios]);
|
||||
|
||||
useEffect(() => {
|
||||
cargarDatos();
|
||||
}, [cargarDatos]);
|
||||
|
||||
const handleOpenModal = (precio?: PrecioDto) => {
|
||||
setEditingPrecio(precio || null); // Si hay 'precio', el modal estará en modo edición
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditingPrecio(null);
|
||||
};
|
||||
|
||||
// CORREGIDO: El segundo parámetro 'idPrecio' determina si es edición
|
||||
const handleSubmitModal = async (data: CreatePrecioDto | UpdatePrecioDto, idPrecio?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
// Si idPrecio tiene valor, Y editingPrecio (initialData del modal) también lo tenía, es una actualización
|
||||
if (idPrecio && editingPrecio) {
|
||||
await precioService.updatePrecio(idPublicacion, idPrecio, data as UpdatePrecioDto);
|
||||
} else {
|
||||
await precioService.createPrecio(idPublicacion, data as CreatePrecioDto);
|
||||
}
|
||||
cargarDatos(); // Recargar lista
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el período de precio.';
|
||||
setApiErrorMessage(message); throw err; // Re-lanzar para que el modal maneje el estado de error
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (idPrecio: number) => {
|
||||
if (window.confirm(`¿Está seguro de eliminar este período de precio (ID: ${idPrecio})? Esta acción puede afectar la vigencia de períodos anteriores.`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await precioService.deletePrecio(idPublicacion, idPrecio);
|
||||
cargarDatos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el período de precio.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, precio: PrecioDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedPrecioRow(precio);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedPrecioRow(null);
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string | null) => {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString); // Asegurar que se parsee correctamente si viene con hora
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0'); // Meses son 0-indexados
|
||||
const year = date.getUTCFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
|
||||
if (loading) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
|
||||
}
|
||||
if (error) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
|
||||
}
|
||||
if (!puedeGestionarPrecios) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/distribucion/publicaciones')} sx={{ mb: 2 }}>
|
||||
Volver a Publicaciones
|
||||
</Button>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Gestionar Precios para: {publicacion?.nombre || 'Cargando...'}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
|
||||
Empresa: {publicacion?.nombreEmpresa || '-'}
|
||||
</Typography>
|
||||
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
{puedeGestionarPrecios && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
|
||||
Agregar Nuevo Período de Precio
|
||||
</Button>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Vigencia Desde</TableCell><TableCell>Vigencia Hasta</TableCell>
|
||||
<TableCell align="right">Lunes</TableCell><TableCell align="right">Martes</TableCell>
|
||||
<TableCell align="right">Miércoles</TableCell><TableCell align="right">Jueves</TableCell>
|
||||
<TableCell align="right">Viernes</TableCell><TableCell align="right">Sábado</TableCell>
|
||||
<TableCell align="right">Domingo</TableCell>
|
||||
<TableCell align="center">Estado</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{precios.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={11} align="center">No hay períodos de precios definidos para esta publicación.</TableCell></TableRow>
|
||||
) : (
|
||||
precios.map((p) => (
|
||||
<TableRow key={p.idPrecio} hover>
|
||||
<TableCell>{formatDate(p.vigenciaD)}</TableCell>
|
||||
<TableCell>{formatDate(p.vigenciaH)}</TableCell>
|
||||
<TableCell align="right">{p.lunes?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="right">{p.martes?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="right">{p.miercoles?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="right">{p.jueves?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="right">{p.viernes?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="right">{p.sabado?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="right">{p.domingo?.toFixed(2) || '-'}</TableCell>
|
||||
<TableCell align="center">
|
||||
{!p.vigenciaH ? <Chip label="Activo" color="success" size="small" /> : <Chip label="Cerrado" size="small" />}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeGestionarPrecios}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeGestionarPrecios && selectedPrecioRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedPrecioRow); handleMenuClose(); }}>
|
||||
<EditIcon fontSize="small" sx={{mr:1}}/> Editar Precios/Cerrar Período
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeGestionarPrecios && selectedPrecioRow && (
|
||||
<MenuItem onClick={() => handleDelete(selectedPrecioRow.idPrecio)}>
|
||||
<DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar Período
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
{idPublicacion &&
|
||||
<PrecioFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleSubmitModal}
|
||||
idPublicacion={idPublicacion}
|
||||
initialData={editingPrecio} // Esto le dice al modal si está editando
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarPreciosPublicacionPage;
|
||||
260
Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx
Normal file
260
Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Chip, FormControl, InputLabel, Select, Tooltip,
|
||||
FormControlLabel
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import publicacionService from '../../services/Distribucion/publicacionService';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto';
|
||||
import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto';
|
||||
import PublicacionFormModal from '../../components/Modals/Distribucion/PublicacionFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
|
||||
|
||||
const GestionarPublicacionesPage: React.FC = () => {
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||
const [filtroSoloHabilitadas, setFiltroSoloHabilitadas] = useState<boolean | undefined>(true);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [loadingEmpresas, setLoadingEmpresas] = useState(false);
|
||||
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingPublicacion, setEditingPublicacion] = useState<PublicacionDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedPublicacionRow, setSelectedPublicacionRow] = useState<PublicacionDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("DP001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("DP002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("DP003");
|
||||
const puedeGestionarPrecios = isSuperAdmin || tienePermiso("DP004");
|
||||
const puedeGestionarRecargos = isSuperAdmin || tienePermiso("DP005");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("DP006");
|
||||
const puedeGestionarSecciones = isSuperAdmin || tienePermiso("DP007");
|
||||
|
||||
const fetchEmpresas = useCallback(async () => {
|
||||
setLoadingEmpresas(true);
|
||||
try {
|
||||
const data = await empresaService.getAllEmpresas();
|
||||
setEmpresas(data);
|
||||
} catch (err) {
|
||||
console.error("Error cargando empresas para filtro:", err);
|
||||
// Manejar error si es necesario, ej. mostrando un mensaje
|
||||
} finally {
|
||||
setLoadingEmpresas(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchEmpresas();
|
||||
}, [fetchEmpresas]);
|
||||
|
||||
|
||||
const cargarPublicaciones = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección."); setLoading(false); return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const idEmpresa = filtroIdEmpresa ? Number(filtroIdEmpresa) : undefined;
|
||||
const data = await publicacionService.getAllPublicaciones(filtroNombre, idEmpresa, filtroSoloHabilitadas);
|
||||
setPublicaciones(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar las publicaciones.');
|
||||
} finally { setLoading(false); }
|
||||
}, [filtroNombre, filtroIdEmpresa, filtroSoloHabilitadas, puedeVer]);
|
||||
|
||||
useEffect(() => { cargarPublicaciones(); }, [cargarPublicaciones]);
|
||||
|
||||
const handleOpenModal = (publicacion?: PublicacionDto) => {
|
||||
setEditingPublicacion(publicacion || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingPublicacion(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreatePublicacionDto | UpdatePublicacionDto, id?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (id && editingPublicacion) {
|
||||
await publicacionService.updatePublicacion(id, data as UpdatePublicacionDto);
|
||||
} else {
|
||||
await publicacionService.createPublicacion(data as CreatePublicacionDto);
|
||||
}
|
||||
cargarPublicaciones();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la publicación.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm(`¿Está seguro? Esta acción eliminará la publicación (ID: ${id}) y todas sus configuraciones asociadas (precios, recargos, secciones, etc.). ESTA ACCIÓN NO SE PUEDE DESHACER.`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await publicacionService.deletePublicacion(id);
|
||||
cargarPublicaciones();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la publicación.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleToggleHabilitada = async (publicacion: PublicacionDto) => {
|
||||
setApiErrorMessage(null);
|
||||
const datosActualizados: UpdatePublicacionDto = {
|
||||
nombre: publicacion.nombre,
|
||||
observacion: publicacion.observacion,
|
||||
idEmpresa: publicacion.idEmpresa,
|
||||
ctrlDevoluciones: publicacion.ctrlDevoluciones,
|
||||
habilitada: !publicacion.habilitada // Invertir estado
|
||||
};
|
||||
try {
|
||||
await publicacionService.updatePublicacion(publicacion.idPublicacion, datosActualizados);
|
||||
cargarPublicaciones(); // Recargar para ver el cambio
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar estado de habilitación.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, publicacion: PublicacionDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedPublicacionRow(publicacion);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedPublicacionRow(null);
|
||||
};
|
||||
|
||||
// TODO: Implementar navegación a páginas de gestión de Precios, Recargos, Secciones
|
||||
const handleNavigateToPrecios = (idPub: number) => {
|
||||
navigate(`/distribucion/publicaciones/${idPub}/precios`); // Ruta anidada
|
||||
handleMenuClose();
|
||||
};
|
||||
const handleNavigateToRecargos = (idPub: number) => { navigate(`/distribucion/publicaciones/${idPub}/recargos`); handleMenuClose(); };
|
||||
const handleNavigateToSecciones = (idPub: number) => { navigate(`/distribucion/publicaciones/${idPub}/secciones`); handleMenuClose(); };
|
||||
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = publicaciones.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso."}</Alert></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Publicaciones</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} />
|
||||
<FormControl size="small" sx={{ flexGrow: 1, minWidth: '200px' }}>
|
||||
<InputLabel id="empresa-filter-label">Empresa</InputLabel>
|
||||
<Select
|
||||
labelId="empresa-filter-label"
|
||||
label="Empresa"
|
||||
value={filtroIdEmpresa}
|
||||
onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}
|
||||
disabled={loadingEmpresas}
|
||||
>
|
||||
<MenuItem value=""><em>Todas</em></MenuItem>
|
||||
{empresas.map((e) => (<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={filtroSoloHabilitadas === undefined ? true : filtroSoloHabilitadas} onChange={(e) => setFiltroSoloHabilitadas(e.target.checked)} size="small" />}
|
||||
label="Solo Habilitadas"
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Publicación</Button>)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Nombre</TableCell><TableCell>Empresa</TableCell>
|
||||
<TableCell>Ctrl. Devol.</TableCell><TableCell>Habilitada</TableCell>
|
||||
<TableCell>Observación</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={6} align="center">No se encontraron publicaciones.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((p) => (
|
||||
<TableRow key={p.idPublicacion} hover sx={{ backgroundColor: !p.habilitada ? '#fff59d' : 'inherit' }}>
|
||||
<TableCell>{p.nombre}</TableCell><TableCell>{p.nombreEmpresa}</TableCell>
|
||||
<TableCell align="center">{p.ctrlDevoluciones ? <Chip label="Sí" size="small" color="info" /> : <Chip label="No" size="small" />}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Tooltip title={p.habilitada ? "Deshabilitar" : "Habilitar"}>
|
||||
<Switch checked={p.habilitada} onChange={() => handleToggleHabilitada(p)} size="small" disabled={!puedeModificar} />
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>{p.observacion || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeModificar && !puedeEliminar && !puedeGestionarPrecios && !puedeGestionarRecargos && !puedeGestionarSecciones}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]} component="div" count={publicaciones.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedPublicacionRow!); handleMenuClose(); }}>Modificar</MenuItem>)}
|
||||
{puedeGestionarPrecios && (<MenuItem onClick={() => handleNavigateToPrecios(selectedPublicacionRow!.idPublicacion)}>Gestionar Precios</MenuItem>)}
|
||||
{puedeGestionarRecargos && (<MenuItem onClick={() => handleNavigateToRecargos(selectedPublicacionRow!.idPublicacion)}>Gestionar Recargos</MenuItem>)}
|
||||
{puedeGestionarSecciones && (<MenuItem onClick={() => handleNavigateToSecciones(selectedPublicacionRow!.idPublicacion)}>Gestionar Secciones</MenuItem>)}
|
||||
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedPublicacionRow!.idPublicacion)}>Eliminar</MenuItem>)}
|
||||
{/* Si no hay permisos para ninguna acción */}
|
||||
{(!puedeModificar && !puedeEliminar && !puedeGestionarPrecios && !puedeGestionarRecargos && !puedeGestionarSecciones) &&
|
||||
<MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<PublicacionFormModal
|
||||
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
||||
initialData={editingPublicacion} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarPublicacionesPage;
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import zonaService from '../../services/zonaService'; // Servicio de Zonas
|
||||
import zonaService from '../../services/Distribucion/zonaService'; // Servicio de Zonas
|
||||
import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // DTO de Zonas
|
||||
import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto'; // DTOs Create
|
||||
import type { UpdateZonaDto } from '../../models/dtos/Zonas/UpdateZonaDto'; // DTOs Update
|
||||
import ZonaFormModal from '../../components/Modals/ZonaFormModal'; // Modal de Zonas
|
||||
import ZonaFormModal from '../../components/Modals/Distribucion/ZonaFormModal'; // Modal de Zonas
|
||||
import { usePermissions } from '../../hooks/usePermissions'; // Hook de permisos
|
||||
import axios from 'axios'; // Para manejo de errores
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const OtrosDestinosPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de Otros Destinos</Typography>;
|
||||
};
|
||||
export default OtrosDestinosPage;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const PublicacionesPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión de Publicaciones</Typography>;
|
||||
};
|
||||
export default PublicacionesPage;
|
||||
Reference in New Issue
Block a user