feat: Implementación de Secciones, Recargos, Porc. Pago Dist. y backend E/S Dist.
Backend API:
- Recargos por Zona (`dist_RecargoZona`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/recargos`.
  - Lógica de negocio para vigencias (cierre/reapertura de períodos).
  - Auditoría en `dist_RecargoZona_H`.
- Porcentajes de Pago Distribuidores (`dist_PorcPago`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/porcentajespago`.
  - Lógica de negocio para vigencias.
  - Auditoría en `dist_PorcPago_H`.
- Porcentajes/Montos Pago Canillitas (`dist_PorcMonPagoCanilla`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/porcentajesmoncanilla`.
  - Lógica de negocio para vigencias.
  - Auditoría en `dist_PorcMonPagoCanilla_H`.
- Secciones de Publicación (`dist_dtPubliSecciones`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/secciones`.
  - Auditoría en `dist_dtPubliSecciones_H`.
- Entradas/Salidas Distribuidores (`dist_EntradasSalidas`):
  - Implementado backend (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Lógica para determinar precios/recargos/porcentajes aplicables.
  - Cálculo de monto y afectación de saldos de distribuidores en `cue_Saldos`.
  - Auditoría en `dist_EntradasSalidas_H`.
- Correcciones de Mapeo Dapper:
  - Aplicados alias explícitos en repositorios de RecargoZona, PorcPago, PorcMonCanilla, PubliSeccion,
    Canilla, Distribuidor y Precio para asegurar mapeo correcto de IDs y columnas.
Frontend React:
- Recargos por Zona:
  - `recargoZonaService.ts`.
  - `RecargoZonaFormModal.tsx` para crear/editar períodos de recargos.
  - `GestionarRecargosPublicacionPage.tsx` para listar y gestionar recargos por publicación.
- Porcentajes de Pago Distribuidores:
  - `porcPagoService.ts`.
  - `PorcPagoFormModal.tsx`.
  - `GestionarPorcentajesPagoPage.tsx`.
- Porcentajes/Montos Pago Canillitas:
  - `porcMonCanillaService.ts`.
  - `PorcMonCanillaFormModal.tsx`.
  - `GestionarPorcMonCanillaPage.tsx`.
- Secciones de Publicación:
  - `publiSeccionService.ts`.
  - `PubliSeccionFormModal.tsx`.
  - `GestionarSeccionesPublicacionPage.tsx`.
- Navegación:
  - Actualizadas rutas y menús para acceder a la gestión de recargos, porcentajes (dist. y canillita) y secciones desde la vista de una publicación.
- Layout:
  - Uso consistente de `Box` con Flexbox en lugar de `Grid` en nuevos modales y páginas para evitar errores de tipo.
			
			
This commit is contained in:
		| @@ -68,11 +68,6 @@ const DistribucionIndexPage: React.FC = () => { | ||||
|           aria-label="sub-módulos de distribución" | ||||
|         > | ||||
|           {distribucionSubModules.map((subModule) => ( | ||||
|             // Usar RouterLink para que el tab se comporte como un enlace y actualice la URL | ||||
|             // La navegación real la manejamos con navigate en handleSubTabChange | ||||
|             // para poder actualizar el estado del tab seleccionado. | ||||
|             // Podríamos usar `component={RouterLink} to={subModule.path}` también, | ||||
|             // pero manejarlo con navigate da más control sobre el estado. | ||||
|             <Tab key={subModule.path} label={subModule.label} /> | ||||
|           ))} | ||||
|         </Tabs> | ||||
|   | ||||
| @@ -1,7 +0,0 @@ | ||||
| import React from 'react'; | ||||
| import { Typography } from '@mui/material'; | ||||
|  | ||||
| const ESDistribuidoresPage: React.FC = () => { | ||||
|   return <Typography variant="h6">Página de Gestión de E/S de Distribuidores</Typography>; | ||||
| }; | ||||
| export default ESDistribuidoresPage; | ||||
| @@ -0,0 +1,250 @@ | ||||
| import React, { useState, useEffect, useCallback } from 'react'; | ||||
| import { | ||||
|     Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, | ||||
|     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||
|     CircularProgress, Alert, FormControl, InputLabel, Select | ||||
| } from '@mui/material'; | ||||
| import AddIcon from '@mui/icons-material/Add'; | ||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||
| import EditIcon from '@mui/icons-material/Edit'; | ||||
| import DeleteIcon from '@mui/icons-material/Delete'; | ||||
| import FilterListIcon from '@mui/icons-material/FilterList'; | ||||
|  | ||||
| import entradaSalidaDistService from '../../services/Distribucion/entradaSalidaDistService'; | ||||
| import publicacionService from '../../services/Distribucion/publicacionService'; | ||||
| import distribuidorService from '../../services/Distribucion/distribuidorService'; | ||||
|  | ||||
| import type { EntradaSalidaDistDto } from '../../models/dtos/Distribucion/EntradaSalidaDistDto'; | ||||
| import type { CreateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaDistDto'; | ||||
| import type { UpdateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto'; | ||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||
| import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto'; | ||||
|  | ||||
| import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const GestionarEntradasSalidasDistPage: React.FC = () => { | ||||
|   const [movimientos, setMovimientos] = useState<EntradaSalidaDistDto[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||
|  | ||||
|   // Filtros | ||||
|   const [filtroFechaDesde, setFiltroFechaDesde] = useState(''); | ||||
|   const [filtroFechaHasta, setFiltroFechaHasta] = useState(''); | ||||
|   const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); | ||||
|   const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>(''); | ||||
|   const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>(''); | ||||
|  | ||||
|  | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); | ||||
|   const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]); | ||||
|   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaDistDto | null>(null); | ||||
|  | ||||
|   const [page, setPage] = useState(0); | ||||
|   const [rowsPerPage, setRowsPerPage] = useState(10); | ||||
|   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
|   const [selectedRow, setSelectedRow] = useState<EntradaSalidaDistDto | null>(null); | ||||
|  | ||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||
|   const puedeVer = isSuperAdmin || tienePermiso("MD001"); | ||||
|   const puedeGestionar = isSuperAdmin || tienePermiso("MD002"); // Para Crear, Editar, Eliminar | ||||
|  | ||||
|   const fetchFiltersDropdownData = useCallback(async () => { | ||||
|     setLoadingFiltersDropdown(true); | ||||
|     try { | ||||
|         const [pubsData, distData] = await Promise.all([ | ||||
|             publicacionService.getAllPublicaciones(undefined, undefined, true), | ||||
|             distribuidorService.getAllDistribuidores() | ||||
|         ]); | ||||
|         setPublicaciones(pubsData); | ||||
|         setDistribuidores(distData); | ||||
|     } catch (err) { | ||||
|         console.error("Error cargando datos para filtros:", err); | ||||
|         setError("Error al cargar opciones de filtro."); | ||||
|     } finally { | ||||
|         setLoadingFiltersDropdown(false); | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); | ||||
|  | ||||
|   const cargarMovimientos = useCallback(async () => { | ||||
|     if (!puedeVer) { | ||||
|       setError("No tiene permiso para ver esta sección."); setLoading(false); return; | ||||
|     } | ||||
|     setLoading(true); setError(null); setApiErrorMessage(null); | ||||
|     try { | ||||
|       const params = { | ||||
|         fechaDesde: filtroFechaDesde || null, | ||||
|         fechaHasta: filtroFechaHasta || null, | ||||
|         idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null, | ||||
|         idDistribuidor: filtroIdDistribuidor ? Number(filtroIdDistribuidor) : null, | ||||
|         tipoMovimiento: filtroTipoMov || null, | ||||
|       }; | ||||
|       const data = await entradaSalidaDistService.getAllEntradasSalidasDist(params); | ||||
|       setMovimientos(data); | ||||
|     } catch (err) { | ||||
|       console.error(err); setError('Error al cargar los movimientos.'); | ||||
|     } finally { setLoading(false); } | ||||
|   }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdDistribuidor, filtroTipoMov]); | ||||
|  | ||||
|   useEffect(() => { cargarMovimientos(); }, [cargarMovimientos]); | ||||
|  | ||||
|   const handleOpenModal = (item?: EntradaSalidaDistDto) => { | ||||
|     setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true); | ||||
|   }; | ||||
|   const handleCloseModal = () => { | ||||
|     setModalOpen(false); setEditingMovimiento(null); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmitModal = async (data: CreateEntradaSalidaDistDto | UpdateEntradaSalidaDistDto, idParte?: number) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|       if (idParte && editingMovimiento) { | ||||
|         await entradaSalidaDistService.updateEntradaSalidaDist(idParte, data as UpdateEntradaSalidaDistDto); | ||||
|       } else { | ||||
|         await entradaSalidaDistService.createEntradaSalidaDist(data as CreateEntradaSalidaDistDto); | ||||
|       } | ||||
|       cargarMovimientos(); | ||||
|     } catch (err: any) { | ||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el movimiento.'; | ||||
|       setApiErrorMessage(message); throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = async (idParte: number) => { | ||||
|     if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})? Esta acción revertirá el impacto en el saldo del distribuidor.`)) { | ||||
|        setApiErrorMessage(null); | ||||
|        try { | ||||
|         await entradaSalidaDistService.deleteEntradaSalidaDist(idParte); | ||||
|         cargarMovimientos(); | ||||
|       } catch (err: any) { | ||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; | ||||
|          setApiErrorMessage(message); | ||||
|       } | ||||
|     } | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaDistDto) => { | ||||
|     setAnchorEl(event.currentTarget); setSelectedRow(item); | ||||
|   }; | ||||
|   const handleMenuClose = () => { | ||||
|     setAnchorEl(null); setSelectedRow(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 = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||
|   const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; | ||||
|  | ||||
|  | ||||
|   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 2 }}> | ||||
|       <Typography variant="h4" gutterBottom>Entradas/Salidas Distribuidores</Typography> | ||||
|       <Paper sx={{ p: 2, mb: 2 }}> | ||||
|          <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography> | ||||
|          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> | ||||
|             <TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> | ||||
|             <TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> | ||||
|             <FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}> | ||||
|                 <InputLabel>Publicación</InputLabel> | ||||
|                 <Select value={filtroIdPublicacion} label="Publicación" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}> | ||||
|                     <MenuItem value=""><em>Todas</em></MenuItem> | ||||
|                     {publicaciones.map(p => <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>)} | ||||
|                 </Select> | ||||
|             </FormControl> | ||||
|             <FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}> | ||||
|                 <InputLabel>Distribuidor</InputLabel> | ||||
|                 <Select value={filtroIdDistribuidor} label="Distribuidor" onChange={(e) => setFiltroIdDistribuidor(e.target.value as number | string)}> | ||||
|                     <MenuItem value=""><em>Todos</em></MenuItem> | ||||
|                     {distribuidores.map(d => <MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>)} | ||||
|                 </Select> | ||||
|             </FormControl> | ||||
|              <FormControl size="small" sx={{minWidth: 150, flexGrow: 1}}> | ||||
|                 <InputLabel>Tipo</InputLabel> | ||||
|                 <Select value={filtroTipoMov} label="Tipo" onChange={(e) => setFiltroTipoMov(e.target.value as 'Salida' | 'Entrada' | '')}> | ||||
|                     <MenuItem value=""><em>Todos</em></MenuItem> | ||||
|                     <MenuItem value="Salida">Salida</MenuItem> | ||||
|                     <MenuItem value="Entrada">Entrada</MenuItem> | ||||
|                 </Select> | ||||
|             </FormControl> | ||||
|          </Box> | ||||
|          {puedeGestionar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Movimiento</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>Fecha</TableCell><TableCell>Publicación (Empresa)</TableCell> | ||||
|                  <TableCell>Distribuidor</TableCell><TableCell>Tipo</TableCell> | ||||
|                  <TableCell align="right">Cantidad</TableCell><TableCell>Remito</TableCell> | ||||
|                  <TableCell align="right">Monto Afectado</TableCell><TableCell>Obs.</TableCell> | ||||
|                  {puedeGestionar && <TableCell align="right">Acciones</TableCell>} | ||||
|              </TableRow></TableHead> | ||||
|              <TableBody> | ||||
|                {displayData.length === 0 ? ( | ||||
|                   <TableRow><TableCell colSpan={puedeGestionar ? 9 : 8} align="center">No se encontraron movimientos.</TableCell></TableRow> | ||||
|                ) : ( | ||||
|                  displayData.map((m) => ( | ||||
|                      <TableRow key={m.idParte} hover> | ||||
|                      <TableCell>{formatDate(m.fecha)}</TableCell> | ||||
|                      <TableCell>{m.nombrePublicacion} ({m.nombreEmpresaPublicacion})</TableCell> | ||||
|                      <TableCell>{m.nombreDistribuidor}</TableCell> | ||||
|                      <TableCell> | ||||
|                         <Chip label={m.tipoMovimiento} color={m.tipoMovimiento === 'Salida' ? 'primary' : 'secondary'} size="small"/> | ||||
|                      </TableCell> | ||||
|                      <TableCell align="right">{m.cantidad}</TableCell> | ||||
|                      <TableCell>{m.remito}</TableCell> | ||||
|                      <TableCell align="right" sx={{color: m.montoCalculado < 0 ? 'green' : (m.montoCalculado > 0 ? 'red' : 'inherit')}}> | ||||
|                         ${m.montoCalculado.toFixed(2)} | ||||
|                      </TableCell> | ||||
|                      <TableCell>{m.observacion || '-'}</TableCell> | ||||
|                      {puedeGestionar && ( | ||||
|                         <TableCell align="right"> | ||||
|                             <IconButton onClick={(e) => handleMenuOpen(e, m)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton> | ||||
|                         </TableCell> | ||||
|                      )} | ||||
|                      </TableRow> | ||||
|                  )))} | ||||
|              </TableBody> | ||||
|            </Table> | ||||
|            <TablePagination | ||||
|              rowsPerPageOptions={[5, 10, 25, 50]} component="div" count={movimientos.length} | ||||
|              rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} | ||||
|              onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" | ||||
|            /> | ||||
|          </TableContainer> | ||||
|        )} | ||||
|  | ||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||
|         {puedeGestionar && selectedRow && ( | ||||
|             <MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)} | ||||
|         {puedeGestionar && selectedRow && ( // O un permiso más específico si "eliminar" es diferente de "modificar" | ||||
|             <MenuItem onClick={() => handleDelete(selectedRow.idParte)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)} | ||||
|       </Menu> | ||||
|  | ||||
|       <EntradaSalidaDistFormModal | ||||
|         open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal} | ||||
|         initialData={editingMovimiento} errorMessage={apiErrorMessage} | ||||
|         clearErrorMessage={() => setApiErrorMessage(null)} | ||||
|       /> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default GestionarEntradasSalidasDistPage; | ||||
							
								
								
									
										189
									
								
								Frontend/src/pages/Distribucion/GestionarPorcMonCanillaPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								Frontend/src/pages/Distribucion/GestionarPorcMonCanillaPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| 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 porcMonCanillaService from '../../services/Distribucion/porcMonCanillaService'; | ||||
| import publicacionService from '../../services/Distribucion/publicacionService'; | ||||
| import type { PorcMonCanillaDto } from '../../models/dtos/Distribucion/PorcMonCanillaDto'; | ||||
| import type { CreatePorcMonCanillaDto } from '../../models/dtos/Distribucion/CreatePorcMonCanillaDto'; | ||||
| import type { UpdatePorcMonCanillaDto } from '../../models/dtos/Distribucion/UpdatePorcMonCanillaDto'; | ||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||
| import PorcMonCanillaFormModal from '../../components/Modals/Distribucion/PorcMonCanillaFormModal'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const GestionarPorcMonCanillaPage: React.FC = () => { | ||||
|   const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>(); | ||||
|   const navigate = useNavigate(); | ||||
|   const idPublicacion = Number(idPublicacionStr); | ||||
|  | ||||
|   const [publicacion, setPublicacion] = useState<PublicacionDto | null>(null); | ||||
|   const [items, setItems] = useState<PorcMonCanillaDto[]>([]); // Renombrado de 'porcentajes' a 'items' | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [editingItem, setEditingItem] = useState<PorcMonCanillaDto | null>(null); | ||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||
|  | ||||
|   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
|   const [selectedRow, setSelectedRow] = useState<PorcMonCanillaDto | null>(null); | ||||
|  | ||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||
|   // Permiso CG004 para porcentajes/montos de pago de canillitas | ||||
|   const puedeGestionar = isSuperAdmin || tienePermiso("CG004"); | ||||
|  | ||||
|   const cargarDatos = useCallback(async () => { | ||||
|     if (isNaN(idPublicacion)) { | ||||
|       setError("ID de Publicación inválido."); setLoading(false); return; | ||||
|     } | ||||
|     if (!puedeGestionar) { | ||||
|         setError("No tiene permiso para gestionar esta configuración."); setLoading(false); return; | ||||
|     } | ||||
|     setLoading(true); setError(null); setApiErrorMessage(null); | ||||
|     try { | ||||
|       const [pubData, data] = await Promise.all([ | ||||
|         publicacionService.getPublicacionById(idPublicacion), | ||||
|         porcMonCanillaService.getPorcMonCanillaPorPublicacion(idPublicacion) | ||||
|       ]); | ||||
|       setPublicacion(pubData); | ||||
|       setItems(data); | ||||
|     } catch (err: any) { | ||||
|       console.error(err); | ||||
|       if (axios.isAxiosError(err) && err.response?.status === 404) { | ||||
|         setError(`Publicación ID ${idPublicacion} no encontrada.`); | ||||
|       } else { | ||||
|         setError('Error al cargar los datos.'); | ||||
|       } | ||||
|     } finally { setLoading(false); } | ||||
|   }, [idPublicacion, puedeGestionar]); | ||||
|  | ||||
|   useEffect(() => { cargarDatos(); }, [cargarDatos]); | ||||
|  | ||||
|   const handleOpenModal = (item?: PorcMonCanillaDto) => { | ||||
|     setEditingItem(item || null); setApiErrorMessage(null); setModalOpen(true); | ||||
|   }; | ||||
|   const handleCloseModal = () => { | ||||
|     setModalOpen(false); setEditingItem(null); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmitModal = async (data: CreatePorcMonCanillaDto | UpdatePorcMonCanillaDto, idPorcMon?: number) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|       if (editingItem && idPorcMon) { | ||||
|         await porcMonCanillaService.updatePorcMonCanilla(idPublicacion, idPorcMon, data as UpdatePorcMonCanillaDto); | ||||
|       } else { | ||||
|         await porcMonCanillaService.createPorcMonCanilla(idPublicacion, data as CreatePorcMonCanillaDto); | ||||
|       } | ||||
|       cargarDatos(); | ||||
|     } catch (err: any) { | ||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.'; | ||||
|       setApiErrorMessage(message); throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = async (idPorcMonDelRow: number) => { | ||||
|     if (window.confirm(`¿Seguro de eliminar este registro (ID: ${idPorcMonDelRow})?`)) { | ||||
|        setApiErrorMessage(null); | ||||
|        try { | ||||
|         await porcMonCanillaService.deletePorcMonCanilla(idPublicacion, idPorcMonDelRow); | ||||
|         cargarDatos(); | ||||
|       } catch (err: any) { | ||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; | ||||
|          setApiErrorMessage(message); | ||||
|       } | ||||
|     } | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: PorcMonCanillaDto) => { | ||||
|     setAnchorEl(event.currentTarget); setSelectedRow(item); | ||||
|   }; | ||||
|   const handleMenuClose = () => { | ||||
|     setAnchorEl(null); setSelectedRow(null); | ||||
|   }; | ||||
|  | ||||
|   const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00').toLocaleDateString('es-AR') : '-'; | ||||
|  | ||||
|   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 (!puedeGestionar) 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>Porcentajes/Montos Pago Canillita: {publicacion?.nombre || 'Cargando...'}</Typography> | ||||
|       <Typography variant="subtitle1" color="text.secondary" gutterBottom>Empresa: {publicacion?.nombreEmpresa || '-'}</Typography> | ||||
|  | ||||
|       <Paper sx={{ p: 2, mb: 2 }}> | ||||
|          {puedeGestionar && ( | ||||
|             <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}> | ||||
|                 Agregar Configuración | ||||
|             </Button> | ||||
|         )} | ||||
|       </Paper> | ||||
|  | ||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} | ||||
|  | ||||
|       <TableContainer component={Paper}> | ||||
|         <Table size="small"> | ||||
|           <TableHead><TableRow> | ||||
|               <TableCell sx={{fontWeight: 'bold'}}>Canillita</TableCell> | ||||
|               <TableCell sx={{fontWeight: 'bold'}}>Vig. Desde</TableCell> | ||||
|               <TableCell sx={{fontWeight: 'bold'}}>Vig. Hasta</TableCell> | ||||
|               <TableCell align="right" sx={{fontWeight: 'bold'}}>Valor</TableCell> | ||||
|               <TableCell align="center" sx={{fontWeight: 'bold'}}>Tipo</TableCell> | ||||
|               <TableCell align="center" sx={{fontWeight: 'bold'}}>Estado</TableCell> | ||||
|               <TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell> | ||||
|           </TableRow></TableHead> | ||||
|           <TableBody> | ||||
|             {items.length === 0 ? ( | ||||
|               <TableRow><TableCell colSpan={7} align="center">No hay configuraciones definidas.</TableCell></TableRow> | ||||
|             ) : ( | ||||
|               items.sort((a,b) => new Date(b.vigenciaD).getTime() - new Date(a.vigenciaD).getTime() || a.nomApeCanilla.localeCompare(b.nomApeCanilla)) | ||||
|               .map((item) => ( | ||||
|                   <TableRow key={item.idPorcMon} hover> | ||||
|                   <TableCell>{item.nomApeCanilla}</TableCell><TableCell>{formatDate(item.vigenciaD)}</TableCell> | ||||
|                   <TableCell>{formatDate(item.vigenciaH)}</TableCell> | ||||
|                   <TableCell align="right">{item.esPorcentaje ? `${item.porcMon.toFixed(2)}%` : `$${item.porcMon.toFixed(2)}`}</TableCell> | ||||
|                   <TableCell align="center">{item.esPorcentaje ? <Chip label="%" color="primary" size="small" variant="outlined"/> : <Chip label="Monto" color="secondary" size="small" variant="outlined"/>}</TableCell> | ||||
|                   <TableCell align="center">{!item.vigenciaH ? <Chip label="Activo" color="success" size="small" /> : <Chip label="Cerrado" size="small" />}</TableCell> | ||||
|                   <TableCell align="right"> | ||||
|                     <IconButton onClick={(e) => handleMenuOpen(e, item)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton> | ||||
|                   </TableCell> | ||||
|                   </TableRow> | ||||
|               )))} | ||||
|           </TableBody> | ||||
|         </Table> | ||||
|       </TableContainer> | ||||
|  | ||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||
|         {puedeGestionar && selectedRow && ( | ||||
|             <MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Editar/Cerrar</MenuItem>)} | ||||
|         {puedeGestionar && selectedRow && ( | ||||
|             <MenuItem onClick={() => handleDelete(selectedRow.idPorcMon)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)} | ||||
|       </Menu> | ||||
|  | ||||
|       {idPublicacion && | ||||
|         <PorcMonCanillaFormModal | ||||
|             open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal} | ||||
|             idPublicacion={idPublicacion} initialData={editingItem} | ||||
|             errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)} | ||||
|         /> | ||||
|       } | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default GestionarPorcMonCanillaPage; | ||||
							
								
								
									
										187
									
								
								Frontend/src/pages/Distribucion/GestionarPorcentajesPagoPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								Frontend/src/pages/Distribucion/GestionarPorcentajesPagoPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | ||||
| 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 porcPagoService from '../../services/Distribucion/porcPagoService'; | ||||
| import publicacionService from '../../services/Distribucion/publicacionService'; | ||||
| import type { PorcPagoDto } from '../../models/dtos/Distribucion/PorcPagoDto'; | ||||
| import type { CreatePorcPagoDto } from '../../models/dtos/Distribucion/CreatePorcPagoDto'; | ||||
| import type { UpdatePorcPagoDto } from '../../models/dtos/Distribucion/UpdatePorcPagoDto'; | ||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||
| import PorcPagoFormModal from '../../components/Modals/Distribucion/PorcPagoFormModal'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const GestionarPorcentajesPagoPage: React.FC = () => { | ||||
|   const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>(); | ||||
|   const navigate = useNavigate(); | ||||
|   const idPublicacion = Number(idPublicacionStr); | ||||
|  | ||||
|   const [publicacion, setPublicacion] = useState<PublicacionDto | null>(null); | ||||
|   const [porcentajes, setPorcentajes] = useState<PorcPagoDto[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [editingPorcentaje, setEditingPorcentaje] = useState<PorcPagoDto | null>(null); | ||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||
|  | ||||
|   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
|   const [selectedPorcentajeRow, setSelectedPorcentajeRow] = useState<PorcPagoDto | null>(null); | ||||
|  | ||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||
|   // Permiso DG004 para porcentajes de pago de distribuidores | ||||
|   const puedeGestionar = isSuperAdmin || tienePermiso("DG004"); | ||||
|  | ||||
|   const cargarDatos = useCallback(async () => { | ||||
|     if (isNaN(idPublicacion)) { | ||||
|       setError("ID de Publicación inválido."); setLoading(false); return; | ||||
|     } | ||||
|     if (!puedeGestionar) { // Permiso para ver precios de una publicacion (DP004) o el específico de porcentajes (DG004) | ||||
|         setError("No tiene permiso para gestionar porcentajes de pago."); setLoading(false); return; | ||||
|     } | ||||
|     setLoading(true); setError(null); setApiErrorMessage(null); | ||||
|     try { | ||||
|       const [pubData, data] = await Promise.all([ | ||||
|         publicacionService.getPublicacionById(idPublicacion), | ||||
|         porcPagoService.getPorcentajesPorPublicacion(idPublicacion) | ||||
|       ]); | ||||
|       setPublicacion(pubData); | ||||
|       setPorcentajes(data); | ||||
|     } catch (err: any) { | ||||
|       console.error(err); | ||||
|       if (axios.isAxiosError(err) && err.response?.status === 404) { | ||||
|         setError(`Publicación ID ${idPublicacion} no encontrada.`); | ||||
|       } else { | ||||
|         setError('Error al cargar los datos.'); | ||||
|       } | ||||
|     } finally { setLoading(false); } | ||||
|   }, [idPublicacion, puedeGestionar]); | ||||
|  | ||||
|   useEffect(() => { cargarDatos(); }, [cargarDatos]); | ||||
|  | ||||
|   const handleOpenModal = (item?: PorcPagoDto) => { | ||||
|     setEditingPorcentaje(item || null); setApiErrorMessage(null); setModalOpen(true); | ||||
|   }; | ||||
|   const handleCloseModal = () => { | ||||
|     setModalOpen(false); setEditingPorcentaje(null); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmitModal = async (data: CreatePorcPagoDto | UpdatePorcPagoDto, idPorcentaje?: number) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|       if (editingPorcentaje && idPorcentaje) { | ||||
|         await porcPagoService.updatePorcPago(idPublicacion, idPorcentaje, data as UpdatePorcPagoDto); | ||||
|       } else { | ||||
|         await porcPagoService.createPorcPago(idPublicacion, data as CreatePorcPagoDto); | ||||
|       } | ||||
|       cargarDatos(); | ||||
|     } catch (err: any) { | ||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.'; | ||||
|       setApiErrorMessage(message); throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = async (idPorcentajeDelRow: number) => { | ||||
|     if (window.confirm(`¿Seguro de eliminar este porcentaje de pago (ID: ${idPorcentajeDelRow})?`)) { | ||||
|        setApiErrorMessage(null); | ||||
|        try { | ||||
|         await porcPagoService.deletePorcPago(idPublicacion, idPorcentajeDelRow); | ||||
|         cargarDatos(); | ||||
|       } catch (err: any) { | ||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; | ||||
|          setApiErrorMessage(message); | ||||
|       } | ||||
|     } | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: PorcPagoDto) => { | ||||
|     setAnchorEl(event.currentTarget); setSelectedPorcentajeRow(item); | ||||
|   }; | ||||
|   const handleMenuClose = () => { | ||||
|     setAnchorEl(null); setSelectedPorcentajeRow(null); | ||||
|   }; | ||||
|  | ||||
|   const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00').toLocaleDateString('es-AR') : '-'; | ||||
|  | ||||
|   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 (!puedeGestionar) 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>Porcentajes Pago Distribuidor: {publicacion?.nombre || 'Cargando...'}</Typography> | ||||
|       <Typography variant="subtitle1" color="text.secondary" gutterBottom>Empresa: {publicacion?.nombreEmpresa || '-'}</Typography> | ||||
|  | ||||
|       <Paper sx={{ p: 2, mb: 2 }}> | ||||
|          {puedeGestionar && ( | ||||
|             <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}> | ||||
|                 Agregar Nuevo Porcentaje | ||||
|             </Button> | ||||
|         )} | ||||
|       </Paper> | ||||
|  | ||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} | ||||
|  | ||||
|       <TableContainer component={Paper}> | ||||
|         <Table size="small"> | ||||
|           <TableHead><TableRow> | ||||
|               <TableCell sx={{fontWeight: 'bold'}}>Distribuidor</TableCell> | ||||
|               <TableCell sx={{fontWeight: 'bold'}}>Vigencia Desde</TableCell> | ||||
|               <TableCell sx={{fontWeight: 'bold'}}>Vigencia Hasta</TableCell> | ||||
|               <TableCell align="right" sx={{fontWeight: 'bold'}}>Porcentaje (%)</TableCell> | ||||
|               <TableCell align="center" sx={{fontWeight: 'bold'}}>Estado</TableCell> | ||||
|               <TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell> | ||||
|           </TableRow></TableHead> | ||||
|           <TableBody> | ||||
|             {porcentajes.length === 0 ? ( | ||||
|               <TableRow><TableCell colSpan={6} align="center">No hay porcentajes definidos.</TableCell></TableRow> | ||||
|             ) : ( | ||||
|               porcentajes.sort((a,b) => new Date(b.vigenciaD).getTime() - new Date(a.vigenciaD).getTime() || a.nombreDistribuidor.localeCompare(b.nombreDistribuidor)) | ||||
|               .map((p) => ( | ||||
|                   <TableRow key={p.idPorcentaje} hover> | ||||
|                   <TableCell>{p.nombreDistribuidor}</TableCell><TableCell>{formatDate(p.vigenciaD)}</TableCell> | ||||
|                   <TableCell>{formatDate(p.vigenciaH)}</TableCell> | ||||
|                   <TableCell align="right">{p.porcentaje.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={!puedeGestionar}><MoreVertIcon /></IconButton> | ||||
|                   </TableCell> | ||||
|                   </TableRow> | ||||
|               )))} | ||||
|           </TableBody> | ||||
|         </Table> | ||||
|       </TableContainer> | ||||
|  | ||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||
|         {puedeGestionar && selectedPorcentajeRow && ( | ||||
|             <MenuItem onClick={() => { handleOpenModal(selectedPorcentajeRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Editar/Cerrar</MenuItem>)} | ||||
|         {puedeGestionar && selectedPorcentajeRow && ( | ||||
|             <MenuItem onClick={() => handleDelete(selectedPorcentajeRow.idPorcentaje)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)} | ||||
|       </Menu> | ||||
|  | ||||
|       {idPublicacion && | ||||
|         <PorcPagoFormModal | ||||
|             open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal} | ||||
|             idPublicacion={idPublicacion} initialData={editingPorcentaje} | ||||
|             errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)} | ||||
|         /> | ||||
|       } | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default GestionarPorcentajesPagoPage; | ||||
| @@ -48,8 +48,13 @@ const GestionarPublicacionesPage: React.FC = () => { | ||||
|   const puedeModificar = isSuperAdmin || tienePermiso("DP003"); | ||||
|   const puedeGestionarPrecios = isSuperAdmin || tienePermiso("DP004"); | ||||
|   const puedeGestionarRecargos = isSuperAdmin || tienePermiso("DP005"); | ||||
|   const puedeGestionarPorcDist = isSuperAdmin || tienePermiso("DG004"); | ||||
|   const puedeEliminar = isSuperAdmin || tienePermiso("DP006"); | ||||
|   // Permiso DP007 para secciones | ||||
|   const puedeGestionarSecciones = isSuperAdmin || tienePermiso("DP007"); | ||||
|   // Permiso CG004 para porcentajes/montos de pago de canillitas | ||||
|   const puedeGestionarPorcCan = isSuperAdmin || tienePermiso("CG004"); | ||||
|  | ||||
|  | ||||
|   const fetchEmpresas = useCallback(async () => { | ||||
|     setLoadingEmpresas(true); | ||||
| @@ -149,11 +154,30 @@ const GestionarPublicacionesPage: React.FC = () => { | ||||
|  | ||||
|   // TODO: Implementar navegación a páginas de gestión de Precios, Recargos, Secciones | ||||
|   const handleNavigateToPrecios = (idPub: number) => { | ||||
|     console.log("Navegando a precios para ID:", idPub); | ||||
|     console.log("Fila seleccionada:", selectedPublicacionRow); | ||||
|     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 handleNavigateToRecargos = (idPub: number) => { | ||||
|     navigate(`/distribucion/publicaciones/${idPub}/recargos`); | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleNavigateToPorcentajesPagoDist = (idPub: number) => { | ||||
|     navigate(`/distribucion/publicaciones/${idPub}/porcentajes-pago-dist`); | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleNavigateToPorcMonCanilla = (idPub: number) => { | ||||
|     navigate(`/distribucion/publicaciones/${idPub}/porcentajes-mon-canilla`); | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleNavigateToSecciones = (idPub: number) => { | ||||
|     navigate(`/distribucion/publicaciones/${idPub}/secciones`); | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|  | ||||
|   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||
| @@ -241,6 +265,8 @@ const GestionarPublicacionesPage: React.FC = () => { | ||||
|         {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>)} | ||||
|         {puedeGestionarPorcDist && (<MenuItem onClick={() => handleNavigateToPorcentajesPagoDist(selectedPublicacionRow!.idPublicacion)}>Porcentajes Pago (Dist.)</MenuItem>)} | ||||
|         {puedeGestionarPorcCan && (<MenuItem onClick={() => handleNavigateToPorcMonCanilla(selectedPublicacionRow!.idPublicacion)}>Porc./Monto Canillita</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 */} | ||||
|   | ||||
| @@ -0,0 +1,197 @@ | ||||
| // src/pages/Distribucion/Publicaciones/GestionarRecargosPublicacionPage.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 recargoZonaService from '../../services/Distribucion/recargoZonaService'; | ||||
| import publicacionService from '../../services/Distribucion/publicacionService'; | ||||
| import type { RecargoZonaDto } from '../../models/dtos/Distribucion/RecargoZonaDto'; | ||||
| import type { CreateRecargoZonaDto } from '../../models/dtos/Distribucion/CreateRecargoZonaDto'; | ||||
| import type { UpdateRecargoZonaDto } from '../../models/dtos/Distribucion/UpdateRecargoZonaDto'; | ||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||
| import RecargoZonaFormModal from '../../components/Modals/Distribucion/RecargoZonaFormModal'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const GestionarRecargosPublicacionPage: React.FC = () => { | ||||
|   const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>(); | ||||
|   const navigate = useNavigate(); | ||||
|   const idPublicacion = Number(idPublicacionStr); | ||||
|  | ||||
|   const [publicacion, setPublicacion] = useState<PublicacionDto | null>(null); | ||||
|   const [recargos, setRecargos] = useState<RecargoZonaDto[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [editingRecargo, setEditingRecargo] = useState<RecargoZonaDto | null>(null); | ||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||
|  | ||||
|   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
|   const [selectedRecargoRow, setSelectedRecargoRow] = useState<RecargoZonaDto | null>(null); | ||||
|  | ||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||
|   const puedeGestionarRecargos = isSuperAdmin || tienePermiso("DP005"); | ||||
|  | ||||
|   const cargarDatos = useCallback(async () => { | ||||
|     if (isNaN(idPublicacion)) { | ||||
|       setError("ID de Publicación inválido."); setLoading(false); return; | ||||
|     } | ||||
|     if (!puedeGestionarRecargos) { | ||||
|         setError("No tiene permiso para gestionar recargos."); setLoading(false); return; | ||||
|     } | ||||
|     setLoading(true); setError(null); setApiErrorMessage(null); | ||||
|     try { | ||||
|       const [pubData, recargosData] = await Promise.all([ | ||||
|         publicacionService.getPublicacionById(idPublicacion), | ||||
|         recargoZonaService.getRecargosPorPublicacion(idPublicacion) | ||||
|       ]); | ||||
|       setPublicacion(pubData); | ||||
|       setRecargos(recargosData); | ||||
|     } catch (err: any) { | ||||
|       console.error(err); | ||||
|       if (axios.isAxiosError(err) && err.response?.status === 404) { | ||||
|         setError(`Publicación ID ${idPublicacion} no encontrada o sin acceso a sus recargos.`); | ||||
|       } else { | ||||
|         setError('Error al cargar los datos de recargos.'); | ||||
|       } | ||||
|     } finally { setLoading(false); } | ||||
|   }, [idPublicacion, puedeGestionarRecargos]); | ||||
|  | ||||
|   useEffect(() => { cargarDatos(); }, [cargarDatos]); | ||||
|  | ||||
|   const handleOpenModal = (recargo?: RecargoZonaDto) => { | ||||
|     setEditingRecargo(recargo || null); setApiErrorMessage(null); setModalOpen(true); | ||||
|   }; | ||||
|   const handleCloseModal = () => { | ||||
|     setModalOpen(false); setEditingRecargo(null); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmitModal = async (data: CreateRecargoZonaDto | UpdateRecargoZonaDto, idRecargo?: number) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|       if (editingRecargo && idRecargo) { | ||||
|         await recargoZonaService.updateRecargoZona(idPublicacion, idRecargo, data as UpdateRecargoZonaDto); | ||||
|       } else { | ||||
|         await recargoZonaService.createRecargoZona(idPublicacion, data as CreateRecargoZonaDto); | ||||
|       } | ||||
|       cargarDatos(); | ||||
|     } catch (err: any) { | ||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el recargo.'; | ||||
|       setApiErrorMessage(message); throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = async (idRecargoDelRow: number) => { | ||||
|     if (window.confirm(`¿Seguro de eliminar este recargo (ID: ${idRecargoDelRow})? Puede afectar vigencias.`)) { | ||||
|        setApiErrorMessage(null); | ||||
|        try { | ||||
|         await recargoZonaService.deleteRecargoZona(idPublicacion, idRecargoDelRow); | ||||
|         cargarDatos(); | ||||
|       } catch (err: any) { | ||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el recargo.'; | ||||
|          setApiErrorMessage(message); | ||||
|       } | ||||
|     } | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, recargo: RecargoZonaDto) => { | ||||
|     setAnchorEl(event.currentTarget); setSelectedRecargoRow(recargo); | ||||
|   }; | ||||
|   const handleMenuClose = () => { | ||||
|     setAnchorEl(null); setSelectedRecargoRow(null); | ||||
|   }; | ||||
|  | ||||
|   const formatDate = (dateString?: string | null) => { | ||||
|     if (!dateString) return '-'; | ||||
|     // Asume que dateString es "yyyy-MM-dd" del backend, ya formateado por el DTO. | ||||
|     // Si viniera como DateTime completo, necesitarías parsearlo y formatearlo. | ||||
|     const parts = dateString.split('-'); | ||||
|     if (parts.length === 3) { | ||||
|         return `${parts[2]}/${parts[1]}/${parts[0]}`; // dd/MM/yyyy | ||||
|     } | ||||
|     return dateString; // Devolver como está si no es el formato esperado | ||||
|   }; | ||||
|  | ||||
|  | ||||
|   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 (!puedeGestionarRecargos) 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>Recargos por Zona para: {publicacion?.nombre || 'Cargando...'}</Typography> | ||||
|       <Typography variant="subtitle1" color="text.secondary" gutterBottom>Empresa: {publicacion?.nombreEmpresa || '-'}</Typography> | ||||
|  | ||||
|       <Paper sx={{ p: 2, mb: 2 }}> | ||||
|          {puedeGestionarRecargos && ( | ||||
|             <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}> | ||||
|                 Agregar Nuevo Recargo | ||||
|             </Button> | ||||
|         )} | ||||
|       </Paper> | ||||
|  | ||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} | ||||
|  | ||||
|       <TableContainer component={Paper}> | ||||
|         <Table size="small"> | ||||
|           <TableHead><TableRow> | ||||
|               <TableCell sx={{fontWeight: 'bold'}}>Zona</TableCell> | ||||
|               <TableCell sx={{fontWeight: 'bold'}}>Vigencia Desde</TableCell> | ||||
|               <TableCell sx={{fontWeight: 'bold'}}>Vigencia Hasta</TableCell> | ||||
|               <TableCell align="right" sx={{fontWeight: 'bold'}}>Valor</TableCell> | ||||
|               <TableCell align="center" sx={{fontWeight: 'bold'}}>Estado</TableCell> | ||||
|               <TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell> | ||||
|           </TableRow></TableHead> | ||||
|           <TableBody> | ||||
|             {recargos.length === 0 ? ( | ||||
|               <TableRow><TableCell colSpan={6} align="center">No hay recargos definidos para esta publicación.</TableCell></TableRow> | ||||
|             ) : ( | ||||
|               recargos.sort((a, b) => new Date(b.vigenciaD).getTime() - new Date(a.vigenciaD).getTime() || a.nombreZona.localeCompare(b.nombreZona)) // Ordenar por fecha desc, luego zona asc | ||||
|               .map((r) => ( | ||||
|                   <TableRow key={r.idRecargo} hover> | ||||
|                   <TableCell>{r.nombreZona}</TableCell><TableCell>{formatDate(r.vigenciaD)}</TableCell> | ||||
|                   <TableCell>{formatDate(r.vigenciaH)}</TableCell> | ||||
|                   <TableCell align="right">${r.valor.toFixed(2)}</TableCell> | ||||
|                   <TableCell align="center">{!r.vigenciaH ? <Chip label="Activo" color="success" size="small" /> : <Chip label="Cerrado" size="small" />}</TableCell> | ||||
|                   <TableCell align="right"> | ||||
|                     <IconButton onClick={(e) => handleMenuOpen(e, r)} disabled={!puedeGestionarRecargos}><MoreVertIcon /></IconButton> | ||||
|                   </TableCell> | ||||
|                   </TableRow> | ||||
|               )))} | ||||
|           </TableBody> | ||||
|         </Table> | ||||
|       </TableContainer> | ||||
|  | ||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||
|         {puedeGestionarRecargos && selectedRecargoRow && ( | ||||
|             <MenuItem onClick={() => { handleOpenModal(selectedRecargoRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Editar/Cerrar</MenuItem>)} | ||||
|         {puedeGestionarRecargos && selectedRecargoRow && ( | ||||
|             <MenuItem onClick={() => handleDelete(selectedRecargoRow.idRecargo)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)} | ||||
|       </Menu> | ||||
|  | ||||
|       {idPublicacion && | ||||
|         <RecargoZonaFormModal | ||||
|             open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal} | ||||
|             idPublicacion={idPublicacion} initialData={editingRecargo} | ||||
|             errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)} | ||||
|         /> | ||||
|       } | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default GestionarRecargosPublicacionPage; | ||||
| @@ -0,0 +1,229 @@ | ||||
| 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, FormControl, InputLabel, Select | ||||
| } from '@mui/material'; | ||||
| import AddIcon from '@mui/icons-material/Add'; | ||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||
| import EditIcon from '@mui/icons-material/Edit'; | ||||
| import DeleteIcon from '@mui/icons-material/Delete'; | ||||
|  | ||||
| import salidaOtroDestinoService from '../../services/Distribucion/salidaOtroDestinoService'; | ||||
| import publicacionService from '../../services/Distribucion/publicacionService'; | ||||
| import otroDestinoService from '../../services/Distribucion/otroDestinoService'; | ||||
|  | ||||
| import type { SalidaOtroDestinoDto } from '../../models/dtos/Distribucion/SalidaOtroDestinoDto'; | ||||
| import type { CreateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto'; | ||||
| import type { UpdateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto'; | ||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||
| import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto'; | ||||
|  | ||||
| import SalidaOtroDestinoFormModal from '../../components/Modals/Distribucion/SalidaOtroDestinoFormModal'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const GestionarSalidasOtrosDestinosPage: React.FC = () => { | ||||
|   const [salidas, setSalidas] = useState<SalidaOtroDestinoDto[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||
|  | ||||
|   // Filtros | ||||
|   const [filtroFechaDesde, setFiltroFechaDesde] = useState(''); | ||||
|   const [filtroFechaHasta, setFiltroFechaHasta] = useState(''); | ||||
|   const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); | ||||
|   const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>(''); | ||||
|  | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); | ||||
|   const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDto[]>([]); | ||||
|   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [editingSalida, setEditingSalida] = useState<SalidaOtroDestinoDto | null>(null); | ||||
|  | ||||
|   const [page, setPage] = useState(0); | ||||
|   const [rowsPerPage, setRowsPerPage] = useState(10); | ||||
|   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
|   const [selectedRow, setSelectedRow] = useState<SalidaOtroDestinoDto | null>(null); | ||||
|  | ||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||
|   // SO001, SO002 (crear/modificar), SO003 (eliminar) | ||||
|   const puedeVer = isSuperAdmin || tienePermiso("SO001"); | ||||
|   const puedeCrearModificar = isSuperAdmin || tienePermiso("SO002"); | ||||
|   const puedeEliminar = isSuperAdmin || tienePermiso("SO003"); | ||||
|  | ||||
|   const fetchFiltersDropdownData = useCallback(async () => { | ||||
|     setLoadingFiltersDropdown(true); | ||||
|     try { | ||||
|         const [pubsData, destinosData] = await Promise.all([ | ||||
|             publicacionService.getAllPublicaciones(undefined, undefined, true), | ||||
|             otroDestinoService.getAllOtrosDestinos() | ||||
|         ]); | ||||
|         setPublicaciones(pubsData); | ||||
|         setOtrosDestinos(destinosData); | ||||
|     } catch (err) { | ||||
|         console.error("Error cargando datos para filtros:", err); | ||||
|         setError("Error al cargar opciones de filtro."); | ||||
|     } finally { | ||||
|         setLoadingFiltersDropdown(false); | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); | ||||
|  | ||||
|   const cargarSalidas = useCallback(async () => { | ||||
|     if (!puedeVer) { | ||||
|       setError("No tiene permiso para ver esta sección."); setLoading(false); return; | ||||
|     } | ||||
|     setLoading(true); setError(null); setApiErrorMessage(null); | ||||
|     try { | ||||
|       const params = { | ||||
|         fechaDesde: filtroFechaDesde || null, | ||||
|         fechaHasta: filtroFechaHasta || null, | ||||
|         idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null, | ||||
|         idDestino: filtroIdDestino ? Number(filtroIdDestino) : null, | ||||
|       }; | ||||
|       const data = await salidaOtroDestinoService.getAllSalidasOtrosDestinos(params); | ||||
|       setSalidas(data); | ||||
|     } catch (err) { | ||||
|       console.error(err); setError('Error al cargar las salidas.'); | ||||
|     } finally { setLoading(false); } | ||||
|   }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdDestino]); | ||||
|  | ||||
|   useEffect(() => { cargarSalidas(); }, [cargarSalidas]); | ||||
|  | ||||
|   const handleOpenModal = (item?: SalidaOtroDestinoDto) => { | ||||
|     setEditingSalida(item || null); setApiErrorMessage(null); setModalOpen(true); | ||||
|   }; | ||||
|   const handleCloseModal = () => { | ||||
|     setModalOpen(false); setEditingSalida(null); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmitModal = async (data: CreateSalidaOtroDestinoDto | UpdateSalidaOtroDestinoDto, idParte?: number) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|       if (idParte && editingSalida) { | ||||
|         await salidaOtroDestinoService.updateSalidaOtroDestino(idParte, data as UpdateSalidaOtroDestinoDto); | ||||
|       } else { | ||||
|         await salidaOtroDestinoService.createSalidaOtroDestino(data as CreateSalidaOtroDestinoDto); | ||||
|       } | ||||
|       cargarSalidas(); | ||||
|     } catch (err: any) { | ||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la salida.'; | ||||
|       setApiErrorMessage(message); throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = async (idParte: number) => { | ||||
|     if (window.confirm(`¿Seguro de eliminar este registro de salida (ID: ${idParte})?`)) { | ||||
|        setApiErrorMessage(null); | ||||
|        try { | ||||
|         await salidaOtroDestinoService.deleteSalidaOtroDestino(idParte); | ||||
|         cargarSalidas(); | ||||
|       } catch (err: any) { | ||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; | ||||
|          setApiErrorMessage(message); | ||||
|       } | ||||
|     } | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: SalidaOtroDestinoDto) => { | ||||
|     setAnchorEl(event.currentTarget); setSelectedRow(item); | ||||
|   }; | ||||
|   const handleMenuClose = () => { | ||||
|     setAnchorEl(null); setSelectedRow(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 = salidas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||
|   const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; | ||||
|  | ||||
|   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 2 }}> | ||||
|       <Typography variant="h4" gutterBottom>Salidas a Otros Destinos</Typography> | ||||
|       <Paper sx={{ p: 2, mb: 2 }}> | ||||
|          <Typography variant="h6" gutterBottom>Filtros</Typography> | ||||
|          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> | ||||
|             <TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> | ||||
|             <TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> | ||||
|             <FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}> | ||||
|                 <InputLabel>Publicación</InputLabel> | ||||
|                 <Select value={filtroIdPublicacion} label="Publicación" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}> | ||||
|                     <MenuItem value=""><em>Todas</em></MenuItem> | ||||
|                     {publicaciones.map(p => <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>)} | ||||
|                 </Select> | ||||
|             </FormControl> | ||||
|             <FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}> | ||||
|                 <InputLabel>Destino</InputLabel> | ||||
|                 <Select value={filtroIdDestino} label="Destino" onChange={(e) => setFiltroIdDestino(e.target.value as number | string)}> | ||||
|                     <MenuItem value=""><em>Todos</em></MenuItem> | ||||
|                     {otrosDestinos.map(d => <MenuItem key={d.idDestino} value={d.idDestino}>{d.nombre}</MenuItem>)} | ||||
|                 </Select> | ||||
|             </FormControl> | ||||
|          </Box> | ||||
|          {puedeCrearModificar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Salida</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>Fecha</TableCell><TableCell>Publicación</TableCell> | ||||
|                  <TableCell>Destino</TableCell><TableCell align="right">Cantidad</TableCell> | ||||
|                  <TableCell>Observación</TableCell> | ||||
|                  {(puedeCrearModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} | ||||
|              </TableRow></TableHead> | ||||
|              <TableBody> | ||||
|                {displayData.length === 0 ? ( | ||||
|                   <TableRow><TableCell colSpan={6} align="center">No se encontraron salidas.</TableCell></TableRow> | ||||
|                ) : ( | ||||
|                  displayData.map((s) => ( | ||||
|                      <TableRow key={s.idParte} hover> | ||||
|                      <TableCell>{formatDate(s.fecha)}</TableCell><TableCell>{s.nombrePublicacion}</TableCell> | ||||
|                      <TableCell>{s.nombreDestino}</TableCell><TableCell align="right">{s.cantidad}</TableCell> | ||||
|                      <TableCell>{s.observacion || '-'}</TableCell> | ||||
|                      {(puedeCrearModificar || puedeEliminar) && ( | ||||
|                         <TableCell align="right"> | ||||
|                             <IconButton onClick={(e) => handleMenuOpen(e, s)} disabled={!puedeCrearModificar && !puedeEliminar}><MoreVertIcon /></IconButton> | ||||
|                         </TableCell> | ||||
|                      )} | ||||
|                      </TableRow> | ||||
|                  )))} | ||||
|              </TableBody> | ||||
|            </Table> | ||||
|            <TablePagination | ||||
|              rowsPerPageOptions={[5, 10, 25]} component="div" count={salidas.length} | ||||
|              rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} | ||||
|              onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" | ||||
|            /> | ||||
|          </TableContainer> | ||||
|        )} | ||||
|  | ||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||
|         {puedeCrearModificar && selectedRow && ( | ||||
|             <MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)} | ||||
|         {puedeEliminar && selectedRow && ( | ||||
|             <MenuItem onClick={() => handleDelete(selectedRow.idParte)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)} | ||||
|       </Menu> | ||||
|  | ||||
|       <SalidaOtroDestinoFormModal | ||||
|         open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal} | ||||
|         initialData={editingSalida} errorMessage={apiErrorMessage} | ||||
|         clearErrorMessage={() => setApiErrorMessage(null)} | ||||
|       /> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default GestionarSalidasOtrosDestinosPage; | ||||
| @@ -0,0 +1,186 @@ | ||||
| import React, { useState, useEffect, useCallback } from 'react'; | ||||
| import { useParams, useNavigate } from 'react-router-dom'; | ||||
| import { | ||||
|     Box, Typography, Button, Paper, IconButton, Menu, MenuItem, Switch, | ||||
|     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, | ||||
|     CircularProgress, Alert, Chip, FormControlLabel | ||||
| } 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 publiSeccionService from '../../services/Distribucion/publiSeccionService'; | ||||
| import publicacionService from '../../services/Distribucion/publicacionService'; | ||||
| import type { PubliSeccionDto } from '../../models/dtos/Distribucion/PubliSeccionDto'; | ||||
| import type { CreatePubliSeccionDto } from '../../models/dtos/Distribucion/CreatePubliSeccionDto'; | ||||
| import type { UpdatePubliSeccionDto } from '../../models/dtos/Distribucion/UpdatePubliSeccionDto'; | ||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||
| import PubliSeccionFormModal from '../../components/Modals/Distribucion/PubliSeccionFormModal'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const GestionarSeccionesPublicacionPage: React.FC = () => { | ||||
|   const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>(); | ||||
|   const navigate = useNavigate(); | ||||
|   const idPublicacion = Number(idPublicacionStr); | ||||
|  | ||||
|   const [publicacion, setPublicacion] = useState<PublicacionDto | null>(null); | ||||
|   const [secciones, setSecciones] = useState<PubliSeccionDto[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [filtroSoloActivas, setFiltroSoloActivas] = useState<boolean | undefined>(undefined); // undefined para mostrar todas | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [editingSeccion, setEditingSeccion] = useState<PubliSeccionDto | null>(null); | ||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||
|  | ||||
|   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
|   const [selectedSeccionRow, setSelectedSeccionRow] = useState<PubliSeccionDto | null>(null); | ||||
|  | ||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||
|   // Permiso DP007 para gestionar secciones | ||||
|   const puedeGestionar = isSuperAdmin || tienePermiso("DP007"); | ||||
|  | ||||
|   const cargarDatos = useCallback(async () => { | ||||
|     if (isNaN(idPublicacion)) { | ||||
|       setError("ID de Publicación inválido."); setLoading(false); return; | ||||
|     } | ||||
|     if (!puedeGestionar) { // O también DP001 si solo quiere ver | ||||
|         setError("No tiene permiso para gestionar secciones."); setLoading(false); return; | ||||
|     } | ||||
|     setLoading(true); setError(null); setApiErrorMessage(null); | ||||
|     try { | ||||
|       const [pubData, data] = await Promise.all([ | ||||
|         publicacionService.getPublicacionById(idPublicacion), | ||||
|         publiSeccionService.getSeccionesPorPublicacion(idPublicacion, filtroSoloActivas) | ||||
|       ]); | ||||
|       setPublicacion(pubData); | ||||
|       setSecciones(data); | ||||
|     } catch (err: any) { | ||||
|       console.error(err); | ||||
|       if (axios.isAxiosError(err) && err.response?.status === 404) { | ||||
|         setError(`Publicación ID ${idPublicacion} no encontrada.`); | ||||
|       } else { | ||||
|         setError('Error al cargar los datos.'); | ||||
|       } | ||||
|     } finally { setLoading(false); } | ||||
|   }, [idPublicacion, puedeGestionar, filtroSoloActivas]); | ||||
|  | ||||
|   useEffect(() => { cargarDatos(); }, [cargarDatos]); | ||||
|  | ||||
|   const handleOpenModal = (item?: PubliSeccionDto) => { | ||||
|     setEditingSeccion(item || null); setApiErrorMessage(null); setModalOpen(true); | ||||
|   }; | ||||
|   const handleCloseModal = () => { | ||||
|     setModalOpen(false); setEditingSeccion(null); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmitModal = async (data: CreatePubliSeccionDto | UpdatePubliSeccionDto, idSeccion?: number) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|       if (editingSeccion && idSeccion) { | ||||
|         await publiSeccionService.updatePubliSeccion(idPublicacion, idSeccion, data as UpdatePubliSeccionDto); | ||||
|       } else { | ||||
|         await publiSeccionService.createPubliSeccion(idPublicacion, data as CreatePubliSeccionDto); | ||||
|       } | ||||
|       cargarDatos(); | ||||
|     } catch (err: any) { | ||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la sección.'; | ||||
|       setApiErrorMessage(message); throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = async (idSeccionDelRow: number) => { | ||||
|     if (window.confirm(`¿Seguro de eliminar esta sección (ID: ${idSeccionDelRow})?`)) { | ||||
|        setApiErrorMessage(null); | ||||
|        try { | ||||
|         await publiSeccionService.deletePubliSeccion(idPublicacion, idSeccionDelRow); | ||||
|         cargarDatos(); | ||||
|       } catch (err: any) { | ||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la sección.'; | ||||
|          setApiErrorMessage(message); | ||||
|       } | ||||
|     } | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: PubliSeccionDto) => { | ||||
|     setAnchorEl(event.currentTarget); setSelectedSeccionRow(item); | ||||
|   }; | ||||
|   const handleMenuClose = () => { | ||||
|     setAnchorEl(null); setSelectedSeccionRow(null); | ||||
|   }; | ||||
|  | ||||
|   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 (!puedeGestionar) 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>Secciones de: {publicacion?.nombre || 'Cargando...'}</Typography> | ||||
|       <Typography variant="subtitle1" color="text.secondary" gutterBottom>Empresa: {publicacion?.nombreEmpresa || '-'}</Typography> | ||||
|  | ||||
|       <Paper sx={{ p: 2, mb: 2 }}> | ||||
|         <Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap'}}> | ||||
|             {puedeGestionar && ( | ||||
|                 <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: {xs: 2, sm:0} }}> | ||||
|                     Agregar Sección | ||||
|                 </Button> | ||||
|             )} | ||||
|             <FormControlLabel | ||||
|                 control={<Switch checked={filtroSoloActivas === undefined ? false : filtroSoloActivas} onChange={(e) => setFiltroSoloActivas(e.target.checked ? true : undefined)} />} | ||||
|                 label="Mostrar solo activas" | ||||
|             /> | ||||
|         </Box> | ||||
|       </Paper> | ||||
|  | ||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} | ||||
|  | ||||
|       <TableContainer component={Paper}> | ||||
|         <Table size="small"> | ||||
|           <TableHead><TableRow> | ||||
|               <TableCell sx={{fontWeight: 'bold'}}>Nombre Sección</TableCell> | ||||
|               <TableCell align="center" sx={{fontWeight: 'bold'}}>Estado</TableCell> | ||||
|               <TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell> | ||||
|           </TableRow></TableHead> | ||||
|           <TableBody> | ||||
|             {secciones.length === 0 ? ( | ||||
|               <TableRow><TableCell colSpan={3} align="center">No hay secciones definidas.</TableCell></TableRow> | ||||
|             ) : ( | ||||
|               secciones.map((s) => ( | ||||
|                   <TableRow key={s.idSeccion} hover> | ||||
|                   <TableCell>{s.nombre}</TableCell> | ||||
|                   <TableCell align="center">{s.estado ? <Chip label="Activa" color="success" size="small" /> : <Chip label="Inactiva" size="small" />}</TableCell> | ||||
|                   <TableCell align="right"> | ||||
|                     <IconButton onClick={(e) => handleMenuOpen(e, s)} disabled={!puedeGestionar}><MoreVertIcon /></IconButton> | ||||
|                   </TableCell> | ||||
|                   </TableRow> | ||||
|               )))} | ||||
|           </TableBody> | ||||
|         </Table> | ||||
|       </TableContainer> | ||||
|  | ||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||
|         {puedeGestionar && selectedSeccionRow && ( | ||||
|             <MenuItem onClick={() => { handleOpenModal(selectedSeccionRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Editar</MenuItem>)} | ||||
|         {puedeGestionar && selectedSeccionRow && ( | ||||
|             <MenuItem onClick={() => handleDelete(selectedSeccionRow.idSeccion)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)} | ||||
|       </Menu> | ||||
|  | ||||
|       {idPublicacion && | ||||
|         <PubliSeccionFormModal | ||||
|             open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal} | ||||
|             idPublicacion={idPublicacion} initialData={editingSeccion} | ||||
|             errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)} | ||||
|         /> | ||||
|       } | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default GestionarSeccionesPublicacionPage; | ||||
| @@ -1,7 +0,0 @@ | ||||
| import React from 'react'; | ||||
| import { Typography } from '@mui/material'; | ||||
|  | ||||
| const SalidastrosDestinosPage: React.FC = () => { | ||||
|   return <Typography variant="h6">Página de Gestión de Salidas a Otros Destinos</Typography>; | ||||
| }; | ||||
| export default SalidastrosDestinosPage; | ||||
		Reference in New Issue
	
	Block a user