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:
2025-05-21 14:58:52 -03:00
parent b6ba52f074
commit e7e185a9cb
140 changed files with 10465 additions and 394 deletions

View File

@@ -0,0 +1,227 @@
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, RadioGroup, FormControlLabel, Radio
} from '@mui/material';
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 publicacionService from '../../../services/Distribucion/publicacionService';
import distribuidorService from '../../../services/Distribucion/distribuidorService';
const modalStyle = { /* ... (mismo estilo) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 550 },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflowY: 'auto'
};
interface EntradaSalidaDistFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreateEntradaSalidaDistDto | UpdateEntradaSalidaDistDto, idParte?: number) => Promise<void>;
initialData?: EntradaSalidaDistDto | null; // Para editar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
open,
onClose,
onSubmit,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
const [idDistribuidor, setIdDistribuidor] = useState<number | string>('');
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [tipoMovimiento, setTipoMovimiento] = useState<'Salida' | 'Entrada'>('Salida');
const [cantidad, setCantidad] = useState<string>('');
const [remito, setRemito] = useState<string>('');
const [observacion, setObservacion] = useState('');
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const isEditing = Boolean(initialData);
useEffect(() => {
const fetchDropdownData = async () => {
setLoadingDropdowns(true);
try {
const [pubsData, distData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true), // Solo habilitadas
distribuidorService.getAllDistribuidores()
]);
setPublicaciones(pubsData);
setDistribuidores(distData);
} catch (error) {
console.error("Error al cargar datos para dropdowns", error);
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'}));
} finally {
setLoadingDropdowns(false);
}
};
if (open) {
fetchDropdownData();
setIdPublicacion(initialData?.idPublicacion || '');
setIdDistribuidor(initialData?.idDistribuidor || '');
setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]);
setTipoMovimiento(initialData?.tipoMovimiento || 'Salida');
setCantidad(initialData?.cantidad?.toString() || '');
setRemito(initialData?.remito?.toString() || '');
setObservacion(initialData?.observacion || '');
setLocalErrors({});
clearErrorMessage();
}
}, [open, initialData, clearErrorMessage]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.';
if (!idDistribuidor) errors.idDistribuidor = 'Seleccione un distribuidor.';
if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.';
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido.';
if (!tipoMovimiento) errors.tipoMovimiento = 'Seleccione un tipo de movimiento.';
if (!cantidad.trim() || isNaN(parseInt(cantidad)) || parseInt(cantidad) <= 0) {
errors.cantidad = 'La cantidad debe ser un número positivo.';
}
if (!isEditing && (!remito.trim() || isNaN(parseInt(remito)) || parseInt(remito) <= 0)) {
errors.remito = 'El Nro. Remito es obligatorio y debe ser un número positivo.';
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleInputChange = (fieldName: string) => {
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
if (errorMessage) clearErrorMessage();
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate()) return;
setLoading(true);
try {
if (isEditing && initialData) {
const dataToSubmit: UpdateEntradaSalidaDistDto = {
cantidad: parseInt(cantidad, 10),
observacion: observacion || undefined,
};
await onSubmit(dataToSubmit, initialData.idParte);
} else {
const dataToSubmit: CreateEntradaSalidaDistDto = {
idPublicacion: Number(idPublicacion),
idDistribuidor: Number(idDistribuidor),
fecha,
tipoMovimiento,
cantidad: parseInt(cantidad, 10),
remito: parseInt(remito, 10),
observacion: observacion || undefined,
};
await onSubmit(dataToSubmit);
}
onClose();
} catch (error: any) {
console.error("Error en submit de EntradaSalidaDistFormModal:", error);
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Movimiento Distribuidor' : 'Registrar Movimiento Distribuidor'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required>
<InputLabel id="publicacion-esd-select-label">Publicación</InputLabel>
<Select labelId="publicacion-esd-select-label" label="Publicación" value={idPublicacion}
onChange={(e) => {setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion');}}
disabled={loading || loadingDropdowns || isEditing}
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{publicaciones.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre} ({p.nombreEmpresa})</MenuItem>))}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
</FormControl>
<FormControl fullWidth margin="dense" error={!!localErrors.idDistribuidor} required>
<InputLabel id="distribuidor-esd-select-label">Distribuidor</InputLabel>
<Select labelId="distribuidor-esd-select-label" label="Distribuidor" value={idDistribuidor}
onChange={(e) => {setIdDistribuidor(e.target.value as number); handleInputChange('idDistribuidor');}}
disabled={loading || loadingDropdowns || isEditing}
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{distribuidores.map((d) => (<MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>))}
</Select>
{localErrors.idDistribuidor && <Typography color="error" variant="caption">{localErrors.idDistribuidor}</Typography>}
</FormControl>
<TextField label="Fecha Movimiento" type="date" value={fecha} required
onChange={(e) => {setFecha(e.target.value); handleInputChange('fecha');}}
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
/>
<FormControl component="fieldset" margin="dense" error={!!localErrors.tipoMovimiento} required>
<Typography component="legend" variant="body2" sx={{mb:0.5}}>Tipo de Movimiento</Typography>
<RadioGroup row value={tipoMovimiento} onChange={(e) => {setTipoMovimiento(e.target.value as 'Salida' | 'Entrada'); handleInputChange('tipoMovimiento');}} >
<FormControlLabel value="Salida" control={<Radio size="small"/>} label="Salida (a Distribuidor)" disabled={loading || isEditing}/>
<FormControlLabel value="Entrada" control={<Radio size="small"/>} label="Entrada (de Distribuidor)" disabled={loading || isEditing}/>
</RadioGroup>
{localErrors.tipoMovimiento && <Typography color="error" variant="caption">{localErrors.tipoMovimiento}</Typography>}
</FormControl>
<TextField label="Nro. Remito" type="number" value={remito} required={!isEditing}
onChange={(e) => {setRemito(e.target.value); handleInputChange('remito');}}
margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''}
disabled={loading || isEditing} inputProps={{min:1}}
/>
<TextField label="Cantidad" type="number" value={cantidad} required
onChange={(e) => {setCantidad(e.target.value); handleInputChange('cantidad');}}
margin="dense" fullWidth error={!!localErrors.cantidad} helperText={localErrors.cantidad || ''}
disabled={loading} inputProps={{min:1}}
/>
<TextField label="Observación (Opcional)" value={observacion}
onChange={(e) => setObservacion(e.target.value)}
margin="dense" fullWidth multiline rows={2} disabled={loading}
/>
</Box>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Registrar Movimiento')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default EntradaSalidaDistFormModal;

View File

@@ -0,0 +1,209 @@
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox, InputAdornment
} from '@mui/material';
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 { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; // Para el dropdown
import canillaService from '../../../services/Distribucion/canillaService'; // Para cargar canillitas
const modalStyle = { /* ... (mismo estilo) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 550 },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflowY: 'auto'
};
interface PorcMonCanillaFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreatePorcMonCanillaDto | UpdatePorcMonCanillaDto, idPorcMon?: number) => Promise<void>;
idPublicacion: number;
initialData?: PorcMonCanillaDto | null;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const PorcMonCanillaFormModal: React.FC<PorcMonCanillaFormModalProps> = ({
open,
onClose,
onSubmit,
idPublicacion,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idCanilla, setIdCanilla] = useState<number | string>('');
const [vigenciaD, setVigenciaD] = useState('');
const [vigenciaH, setVigenciaH] = useState('');
const [porcMon, setPorcMon] = useState<string>('');
const [esPorcentaje, setEsPorcentaje] = useState(true); // Default a porcentaje
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingCanillitas, setLoadingCanillitas] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const isEditing = Boolean(initialData);
useEffect(() => {
const fetchCanillitas = async () => {
setLoadingCanillitas(true);
try {
// Aquí podríamos querer filtrar solo canillitas accionistas si la regla de negocio lo impone
// o todos los activos. Por ahora, todos los activos.
const data = await canillaService.getAllCanillas(undefined, undefined, true);
setCanillitas(data);
} catch (error) {
console.error("Error al cargar canillitas", error);
setLocalErrors(prev => ({...prev, canillitas: 'Error al cargar canillitas.'}));
} finally {
setLoadingCanillitas(false);
}
};
if (open) {
fetchCanillitas();
setIdCanilla(initialData?.idCanilla || '');
setVigenciaD(initialData?.vigenciaD || '');
setVigenciaH(initialData?.vigenciaH || '');
setPorcMon(initialData?.porcMon?.toString() || '');
setEsPorcentaje(initialData ? initialData.esPorcentaje : true);
setLocalErrors({});
clearErrorMessage();
}
}, [open, initialData, clearErrorMessage]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idCanilla) errors.idCanilla = 'Debe seleccionar un canillita.';
if (!isEditing && !vigenciaD.trim()) {
errors.vigenciaD = 'La Vigencia Desde es obligatoria.';
} else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) {
errors.vigenciaD = 'Formato de Vigencia Desde inválido (YYYY-MM-DD).';
}
if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) {
errors.vigenciaH = 'Formato de Vigencia Hasta inválido (YYYY-MM-DD).';
} else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) {
errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.';
}
if (!porcMon.trim()) errors.porcMon = 'El valor es obligatorio.';
else {
const numVal = parseFloat(porcMon);
if (isNaN(numVal) || numVal < 0) {
errors.porcMon = 'El valor debe ser un número positivo.';
} else if (esPorcentaje && numVal > 100) {
errors.porcMon = 'El porcentaje no puede ser mayor a 100.';
}
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleInputChange = (fieldName: string) => {
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
if (errorMessage) clearErrorMessage();
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate()) return;
setLoading(true);
try {
const valorNum = parseFloat(porcMon);
if (isEditing && initialData) {
const dataToSubmit: UpdatePorcMonCanillaDto = {
porcMon: valorNum,
esPorcentaje,
vigenciaH: vigenciaH.trim() ? vigenciaH : null,
};
await onSubmit(dataToSubmit, initialData.idPorcMon);
} else {
const dataToSubmit: CreatePorcMonCanillaDto = {
idPublicacion,
idCanilla: Number(idCanilla),
vigenciaD,
porcMon: valorNum,
esPorcentaje,
};
await onSubmit(dataToSubmit);
}
onClose();
} catch (error: any) {
console.error("Error en submit de PorcMonCanillaFormModal:", error);
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Porcentaje/Monto Canillita' : 'Agregar Nuevo Porcentaje/Monto Canillita'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idCanilla}>
<InputLabel id="canilla-pmc-select-label" required>Canillita</InputLabel>
<Select labelId="canilla-pmc-select-label" label="Canillita" value={idCanilla}
onChange={(e) => {setIdCanilla(e.target.value as number); handleInputChange('idCanilla');}}
disabled={loading || loadingCanillitas || isEditing}
>
<MenuItem value="" disabled><em>Seleccione un canillita</em></MenuItem>
{canillitas.map((c) => (<MenuItem key={c.idCanilla} value={c.idCanilla}>{`${c.nomApe} (Leg: ${c.legajo || 'S/L'})`}</MenuItem>))}
</Select>
{localErrors.idCanilla && <Typography color="error" variant="caption">{localErrors.idCanilla}</Typography>}
</FormControl>
<TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing}
onChange={(e) => {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}}
margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''}
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
/>
{isEditing && (
<TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH}
onChange={(e) => {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}}
margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''}
disabled={loading} InputLabelProps={{ shrink: true }}
/>
)}
<TextField label="Valor" type="number" value={porcMon} required
onChange={(e) => {setPorcMon(e.target.value); handleInputChange('porcMon');}}
margin="dense" fullWidth error={!!localErrors.porcMon} helperText={localErrors.porcMon || ''}
disabled={loading}
InputProps={{ startAdornment: esPorcentaje ? undefined : <InputAdornment position="start">$</InputAdornment>,
endAdornment: esPorcentaje ? <InputAdornment position="end">%</InputAdornment> : undefined }}
inputProps={{ step: "0.01", lang:"es-AR" }}
/>
<FormControlLabel
control={<Checkbox checked={esPorcentaje} onChange={(e) => setEsPorcentaje(e.target.checked)} disabled={loading}/>}
label="Es Porcentaje (si no, es Monto Fijo)" sx={{mt:1}}
/>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.canillitas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.canillitas}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingCanillitas}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default PorcMonCanillaFormModal;

View File

@@ -0,0 +1,196 @@
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, InputAdornment
} from '@mui/material';
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 { DistribuidorDto } from '../../../models/dtos/Distribucion/DistribuidorDto';
import distribuidorService from '../../../services/Distribucion/distribuidorService'; // Para cargar distribuidores
const modalStyle = { /* ... (mismo estilo) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 500 },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflowY: 'auto'
};
interface PorcPagoFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreatePorcPagoDto | UpdatePorcPagoDto, idPorcentaje?: number) => Promise<void>;
idPublicacion: number;
initialData?: PorcPagoDto | null; // Para editar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const PorcPagoFormModal: React.FC<PorcPagoFormModalProps> = ({
open,
onClose,
onSubmit,
idPublicacion,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idDistribuidor, setIdDistribuidor] = useState<number | string>('');
const [vigenciaD, setVigenciaD] = useState(''); // "yyyy-MM-dd"
const [vigenciaH, setVigenciaH] = useState(''); // "yyyy-MM-dd"
const [porcentaje, setPorcentaje] = useState<string>('');
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingDistribuidores, setLoadingDistribuidores] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const isEditing = Boolean(initialData);
useEffect(() => {
const fetchDistribuidores = async () => {
setLoadingDistribuidores(true);
try {
const data = await distribuidorService.getAllDistribuidores();
setDistribuidores(data);
} catch (error) {
console.error("Error al cargar distribuidores", error);
setLocalErrors(prev => ({...prev, distribuidores: 'Error al cargar distribuidores.'}));
} finally {
setLoadingDistribuidores(false);
}
};
if (open) {
fetchDistribuidores();
setIdDistribuidor(initialData?.idDistribuidor || '');
setVigenciaD(initialData?.vigenciaD || '');
setVigenciaH(initialData?.vigenciaH || '');
setPorcentaje(initialData?.porcentaje?.toString() || '');
setLocalErrors({});
clearErrorMessage();
}
}, [open, initialData, clearErrorMessage]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idDistribuidor) errors.idDistribuidor = 'Debe seleccionar un distribuidor.';
if (!isEditing && !vigenciaD.trim()) {
errors.vigenciaD = 'La Vigencia Desde es obligatoria.';
} else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) {
errors.vigenciaD = 'Formato de Vigencia Desde inválido (YYYY-MM-DD).';
}
if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) {
errors.vigenciaH = 'Formato de Vigencia Hasta inválido (YYYY-MM-DD).';
} else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) {
errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.';
}
if (!porcentaje.trim()) errors.porcentaje = 'El porcentaje es obligatorio.';
else {
const porcNum = parseFloat(porcentaje);
if (isNaN(porcNum) || porcNum < 0 || porcNum > 100) {
errors.porcentaje = 'El porcentaje debe ser un número entre 0 y 100.';
}
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleInputChange = (fieldName: string) => {
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
if (errorMessage) clearErrorMessage();
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate()) return;
setLoading(true);
try {
const porcentajeNum = parseFloat(porcentaje);
if (isEditing && initialData) {
const dataToSubmit: UpdatePorcPagoDto = {
porcentaje: porcentajeNum,
vigenciaH: vigenciaH.trim() ? vigenciaH : null,
};
await onSubmit(dataToSubmit, initialData.idPorcentaje);
} else {
const dataToSubmit: CreatePorcPagoDto = {
idPublicacion,
idDistribuidor: Number(idDistribuidor),
vigenciaD,
porcentaje: porcentajeNum,
};
await onSubmit(dataToSubmit);
}
onClose();
} catch (error: any) {
console.error("Error en submit de PorcPagoFormModal:", error);
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Porcentaje de Pago' : 'Agregar Nuevo Porcentaje de Pago'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idDistribuidor}>
<InputLabel id="distribuidor-porc-select-label" required>Distribuidor</InputLabel>
<Select labelId="distribuidor-porc-select-label" label="Distribuidor" value={idDistribuidor}
onChange={(e) => {setIdDistribuidor(e.target.value as number); handleInputChange('idDistribuidor');}}
disabled={loading || loadingDistribuidores || isEditing} // Distribuidor no se edita
>
<MenuItem value="" disabled><em>Seleccione un distribuidor</em></MenuItem>
{distribuidores.map((d) => (<MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>))}
</Select>
{localErrors.idDistribuidor && <Typography color="error" variant="caption">{localErrors.idDistribuidor}</Typography>}
</FormControl>
<TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing}
onChange={(e) => {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}}
margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''}
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
/>
{isEditing && (
<TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH}
onChange={(e) => {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}}
margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''}
disabled={loading} InputLabelProps={{ shrink: true }}
/>
)}
<TextField label="Porcentaje" type="number" value={porcentaje} required
onChange={(e) => {setPorcentaje(e.target.value); handleInputChange('porcentaje');}}
margin="dense" fullWidth error={!!localErrors.porcentaje} helperText={localErrors.porcentaje || ''}
disabled={loading}
InputProps={{ endAdornment: <InputAdornment position="end">%</InputAdornment> }}
inputProps={{ step: "0.01", lang:"es-AR" }}
/>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.distribuidores && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.distribuidores}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDistribuidores}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Porcentaje')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default PorcPagoFormModal;

View File

@@ -0,0 +1,130 @@
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControlLabel, Checkbox
} from '@mui/material';
import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto';
import type { CreatePubliSeccionDto } from '../../../models/dtos/Distribucion/CreatePubliSeccionDto';
import type { UpdatePubliSeccionDto } from '../../../models/dtos/Distribucion/UpdatePubliSeccionDto';
const modalStyle = { /* ... (mismo estilo) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 450 },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
};
interface PubliSeccionFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreatePubliSeccionDto | UpdatePubliSeccionDto, idSeccion?: number) => Promise<void>;
idPublicacion: number; // Siempre necesario para la creación
initialData?: PubliSeccionDto | null;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const PubliSeccionFormModal: React.FC<PubliSeccionFormModalProps> = ({
open,
onClose,
onSubmit,
idPublicacion,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [nombre, setNombre] = useState('');
const [estado, setEstado] = useState(true); // Default a activa
const [loading, setLoading] = useState(false);
const [localErrorNombre, setLocalErrorNombre] = useState<string | null>(null);
const isEditing = Boolean(initialData);
useEffect(() => {
if (open) {
setNombre(initialData?.nombre || '');
setEstado(initialData ? initialData.estado : true);
setLocalErrorNombre(null);
clearErrorMessage();
}
}, [open, initialData, clearErrorMessage]);
const validate = (): boolean => {
let isValid = true;
if (!nombre.trim()) {
setLocalErrorNombre('El nombre de la sección es obligatorio.');
isValid = false;
}
return isValid;
};
const handleInputChange = () => {
if (localErrorNombre) setLocalErrorNombre(null);
if (errorMessage) clearErrorMessage();
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate()) return;
setLoading(true);
try {
if (isEditing && initialData) {
const dataToSubmit: UpdatePubliSeccionDto = { nombre, estado };
await onSubmit(dataToSubmit, initialData.idSeccion);
} else {
const dataToSubmit: CreatePubliSeccionDto = {
idPublicacion, // Viene de props
nombre,
estado
};
await onSubmit(dataToSubmit);
}
onClose();
} catch (error: any) {
console.error("Error en submit de PubliSeccionFormModal:", error);
// El error de API se maneja en la página
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Sección' : 'Agregar Nueva Sección'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<TextField label="Nombre de la Sección" value={nombre} required
onChange={(e) => {setNombre(e.target.value); handleInputChange();}}
margin="dense" fullWidth error={!!localErrorNombre} helperText={localErrorNombre || ''}
disabled={loading} autoFocus
/>
<FormControlLabel
control={<Checkbox checked={estado} onChange={(e) => setEstado(e.target.checked)} disabled={loading}/>}
label="Activa" sx={{mt:1}}
/>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Sección')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default PubliSeccionFormModal;

View File

@@ -0,0 +1,193 @@
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, InputAdornment
} from '@mui/material';
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 { ZonaDto } from '../../../models/dtos/Zonas/ZonaDto'; // Para el dropdown de zonas
import zonaService from '../../../services/Distribucion/zonaService'; // Para cargar zonas
const modalStyle = { /* ... (mismo estilo) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 500 },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflowY: 'auto'
};
interface RecargoZonaFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreateRecargoZonaDto | UpdateRecargoZonaDto, idRecargo?: number) => Promise<void>;
idPublicacion: number;
initialData?: RecargoZonaDto | null; // Para editar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const RecargoZonaFormModal: React.FC<RecargoZonaFormModalProps> = ({
open,
onClose,
onSubmit,
idPublicacion,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idZona, setIdZona] = useState<number | string>('');
const [vigenciaD, setVigenciaD] = useState(''); // "yyyy-MM-dd"
const [vigenciaH, setVigenciaH] = useState(''); // "yyyy-MM-dd"
const [valor, setValor] = useState<string>('');
const [zonas, setZonas] = useState<ZonaDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingZonas, setLoadingZonas] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const isEditing = Boolean(initialData);
useEffect(() => {
const fetchZonas = async () => {
setLoadingZonas(true);
try {
const data = await zonaService.getAllZonas(); // Asume que devuelve zonas activas
setZonas(data);
} catch (error) {
console.error("Error al cargar zonas", error);
setLocalErrors(prev => ({...prev, zonas: 'Error al cargar zonas.'}));
} finally {
setLoadingZonas(false);
}
};
if (open) {
fetchZonas();
setIdZona(initialData?.idZona || '');
setVigenciaD(initialData?.vigenciaD || '');
setVigenciaH(initialData?.vigenciaH || '');
setValor(initialData?.valor?.toString() || '');
setLocalErrors({});
clearErrorMessage();
}
}, [open, initialData, clearErrorMessage]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idZona) errors.idZona = 'Debe seleccionar una zona.';
if (!isEditing && !vigenciaD.trim()) {
errors.vigenciaD = 'La Vigencia Desde es obligatoria.';
} else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) {
errors.vigenciaD = 'Formato de Vigencia Desde inválido (YYYY-MM-DD).';
}
if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) {
errors.vigenciaH = 'Formato de Vigencia Hasta inválido (YYYY-MM-DD).';
} else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) {
errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.';
}
if (!valor.trim()) errors.valor = 'El valor es obligatorio.';
else if (isNaN(parseFloat(valor)) || parseFloat(valor) < 0) {
errors.valor = 'El valor debe ser un número positivo.';
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleInputChange = (fieldName: string) => {
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
if (errorMessage) clearErrorMessage();
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate()) return;
setLoading(true);
try {
const valorNum = parseFloat(valor);
if (isEditing && initialData) {
const dataToSubmit: UpdateRecargoZonaDto = {
valor: valorNum,
vigenciaH: vigenciaH.trim() ? vigenciaH : null,
};
await onSubmit(dataToSubmit, initialData.idRecargo);
} else {
const dataToSubmit: CreateRecargoZonaDto = {
idPublicacion,
idZona: Number(idZona),
vigenciaD,
valor: valorNum,
};
await onSubmit(dataToSubmit);
}
onClose();
} catch (error: any) {
console.error("Error en submit de RecargoZonaFormModal:", error);
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Recargo por Zona' : 'Agregar Nuevo Recargo por Zona'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idZona}>
<InputLabel id="zona-recargo-select-label" required>Zona</InputLabel>
<Select labelId="zona-recargo-select-label" label="Zona" value={idZona}
onChange={(e) => {setIdZona(e.target.value as number); handleInputChange('idZona');}}
disabled={loading || loadingZonas || isEditing} // Zona no se edita
>
<MenuItem value="" disabled><em>Seleccione una zona</em></MenuItem>
{zonas.map((z) => (<MenuItem key={z.idZona} value={z.idZona}>{z.nombre}</MenuItem>))}
</Select>
{localErrors.idZona && <Typography color="error" variant="caption">{localErrors.idZona}</Typography>}
</FormControl>
<TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing}
onChange={(e) => {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}}
margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''}
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
/>
{isEditing && (
<TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH}
onChange={(e) => {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}}
margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''}
disabled={loading} InputLabelProps={{ shrink: true }}
/>
)}
<TextField label="Valor Recargo" type="number" value={valor} required
onChange={(e) => {setValor(e.target.value); handleInputChange('valor');}}
margin="dense" fullWidth error={!!localErrors.valor} helperText={localErrors.valor || ''}
disabled={loading}
InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }}
inputProps={{ step: "0.01", lang:"es-AR" }}
/>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.zonas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.zonas}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingZonas}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Recargo')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default RecargoZonaFormModal;

View File

@@ -0,0 +1,199 @@
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem
} from '@mui/material';
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 publicacionService from '../../../services/Distribucion/publicacionService';
import otroDestinoService from '../../../services/Distribucion/otroDestinoService';
const modalStyle = { /* ... (mismo estilo) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 500 },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflowY: 'auto'
};
interface SalidaOtroDestinoFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreateSalidaOtroDestinoDto | UpdateSalidaOtroDestinoDto, idParte?: number) => Promise<void>;
initialData?: SalidaOtroDestinoDto | null; // Para editar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const SalidaOtroDestinoFormModal: React.FC<SalidaOtroDestinoFormModalProps> = ({
open,
onClose,
onSubmit,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
const [idDestino, setIdDestino] = useState<number | string>('');
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [cantidad, setCantidad] = useState<string>('');
const [observacion, setObservacion] = useState('');
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const isEditing = Boolean(initialData);
useEffect(() => {
const fetchDropdownData = async () => {
setLoadingDropdowns(true);
try {
const [pubsData, destinosData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true), // Solo habilitadas
otroDestinoService.getAllOtrosDestinos()
]);
setPublicaciones(pubsData);
setOtrosDestinos(destinosData);
} catch (error) {
console.error("Error al cargar datos para dropdowns", error);
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'}));
} finally {
setLoadingDropdowns(false);
}
};
if (open) {
fetchDropdownData();
setIdPublicacion(initialData?.idPublicacion || '');
setIdDestino(initialData?.idDestino || '');
setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]);
setCantidad(initialData?.cantidad?.toString() || '');
setObservacion(initialData?.observacion || '');
setLocalErrors({});
clearErrorMessage();
}
}, [open, initialData, clearErrorMessage]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.';
if (!idDestino) errors.idDestino = 'Seleccione un destino.';
if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.';
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido.';
if (!cantidad.trim() || isNaN(parseInt(cantidad)) || parseInt(cantidad) <= 0) {
errors.cantidad = 'La cantidad debe ser un número positivo.';
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleInputChange = (fieldName: string) => {
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
if (errorMessage) clearErrorMessage();
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate()) return;
setLoading(true);
try {
if (isEditing && initialData) {
const dataToSubmit: UpdateSalidaOtroDestinoDto = {
cantidad: parseInt(cantidad, 10),
observacion: observacion || undefined,
};
await onSubmit(dataToSubmit, initialData.idParte);
} else {
const dataToSubmit: CreateSalidaOtroDestinoDto = {
idPublicacion: Number(idPublicacion),
idDestino: Number(idDestino),
fecha,
cantidad: parseInt(cantidad, 10),
observacion: observacion || undefined,
};
await onSubmit(dataToSubmit);
}
onClose();
} catch (error: any) {
console.error("Error en submit de SalidaOtroDestinoFormModal:", error);
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Salida a Otro Destino' : 'Registrar Salida a Otro Destino'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required>
<InputLabel id="publicacion-sod-select-label">Publicación</InputLabel>
<Select labelId="publicacion-sod-select-label" label="Publicación" value={idPublicacion}
onChange={(e) => {setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion');}}
disabled={loading || loadingDropdowns || isEditing} // No se edita publicación
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{publicaciones.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre} ({p.nombreEmpresa})</MenuItem>))}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
</FormControl>
<FormControl fullWidth margin="dense" error={!!localErrors.idDestino} required>
<InputLabel id="destino-sod-select-label">Otro Destino</InputLabel>
<Select labelId="destino-sod-select-label" label="Otro Destino" value={idDestino}
onChange={(e) => {setIdDestino(e.target.value as number); handleInputChange('idDestino');}}
disabled={loading || loadingDropdowns || isEditing} // No se edita destino
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{otrosDestinos.map((d) => (<MenuItem key={d.idDestino} value={d.idDestino}>{d.nombre}</MenuItem>))}
</Select>
{localErrors.idDestino && <Typography color="error" variant="caption">{localErrors.idDestino}</Typography>}
</FormControl>
<TextField label="Fecha" type="date" value={fecha} required
onChange={(e) => {setFecha(e.target.value); handleInputChange('fecha');}}
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
/>
<TextField label="Cantidad" type="number" value={cantidad} required
onChange={(e) => {setCantidad(e.target.value); handleInputChange('cantidad');}}
margin="dense" fullWidth error={!!localErrors.cantidad} helperText={localErrors.cantidad || ''}
disabled={loading} inputProps={{min:1}}
/>
<TextField label="Observación (Opcional)" value={observacion}
onChange={(e) => setObservacion(e.target.value)}
margin="dense" fullWidth multiline rows={3} disabled={loading}
/>
</Box>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Registrar Salida')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default SalidaOtroDestinoFormModal;

View File

@@ -0,0 +1,261 @@
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem
} from '@mui/material';
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
import type { CambiarEstadoBobinaDto } from '../../../models/dtos/Impresion/CambiarEstadoBobinaDto';
import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto';
import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto';
import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto'; // Asumiendo que tienes este DTO
import estadoBobinaService from '../../../services/Impresion/estadoBobinaService'; // Para cargar estados
import publicacionService from '../../../services/Distribucion/publicacionService'; // Para cargar publicaciones
import publiSeccionService from '../../../services/Distribucion/publiSeccionService'; // Para cargar secciones
const modalStyle = { /* ... (mismo estilo) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 500 },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflowY: 'auto'
};
// IDs de estados conocidos (ajusta según tu BD)
const ID_ESTADO_EN_USO = 2;
const ID_ESTADO_DANADA = 3;
// const ID_ESTADO_DISPONIBLE = 1; // No se cambia a Disponible desde este modal
interface StockBobinaCambioEstadoModalProps {
open: boolean;
onClose: () => void;
onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise<void>;
bobinaActual: StockBobinaDto | null; // La bobina cuyo estado se va a cambiar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> = ({
open,
onClose,
onSubmit,
bobinaActual,
errorMessage,
clearErrorMessage
}) => {
const [nuevoEstadoId, setNuevoEstadoId] = useState<number | string>('');
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
const [idSeccion, setIdSeccion] = useState<number | string>('');
const [obs, setObs] = useState('');
const [fechaCambioEstado, setFechaCambioEstado] = useState('');
const [estadosDisponibles, setEstadosDisponibles] = useState<EstadoBobinaDto[]>([]);
const [publicacionesDisponibles, setPublicacionesDisponibles] = useState<PublicacionDto[]>([]);
const [seccionesDisponibles, setSeccionesDisponibles] = useState<PubliSeccionDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
useEffect(() => {
const fetchDropdownData = async () => {
if (!bobinaActual) return;
setLoadingDropdowns(true);
try {
const estadosData = await estadoBobinaService.getAllEstadosBobina();
// Filtrar estados: no se puede volver a "Disponible" o al mismo estado actual desde aquí.
// Y si está "Dañada", no se puede cambiar.
let estadosFiltrados = estadosData.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina && e.idEstadoBobina !== 1);
if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) { // Si ya está dañada, no hay más cambios
estadosFiltrados = [];
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) { // Si está en uso, solo puede pasar a Dañada
estadosFiltrados = estadosData.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA);
}
setEstadosDisponibles(estadosFiltrados);
if (estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO)) { // Solo cargar publicaciones si "En Uso" es una opción
const publicacionesData = await publicacionService.getAllPublicaciones(undefined, undefined, true); // Solo habilitadas
setPublicacionesDisponibles(publicacionesData);
} else {
setPublicacionesDisponibles([]);
}
} catch (error) {
console.error("Error al cargar datos para dropdowns (Cambio Estado Bobina)", error);
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'}));
} finally {
setLoadingDropdowns(false);
}
};
if (open && bobinaActual) {
fetchDropdownData();
setNuevoEstadoId('');
setIdPublicacion('');
setIdSeccion('');
setObs(bobinaActual.obs || ''); // Pre-cargar obs existente
setFechaCambioEstado(new Date().toISOString().split('T')[0]); // Default a hoy
setLocalErrors({});
clearErrorMessage();
}
}, [open, bobinaActual, clearErrorMessage]);
// Cargar secciones cuando cambia la publicación seleccionada y el estado es "En Uso"
useEffect(() => {
const fetchSecciones = async () => {
if (nuevoEstadoId === ID_ESTADO_EN_USO && idPublicacion) {
setLoadingDropdowns(true); // Podrías tener un loader específico para secciones
try {
const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true); // Solo activas
setSeccionesDisponibles(data);
} catch (error) {
console.error("Error al cargar secciones:", error);
setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.'}));
} finally {
setLoadingDropdowns(false);
}
} else {
setSeccionesDisponibles([]); // Limpiar secciones si no aplica
}
};
fetchSecciones();
}, [nuevoEstadoId, idPublicacion]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.';
if (!fechaCambioEstado.trim()) errors.fechaCambioEstado = 'La fecha de cambio es obligatoria.';
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaCambioEstado)) errors.fechaCambioEstado = 'Formato de fecha inválido.';
if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO) {
if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.';
if (!idSeccion) errors.idSeccion = 'Seleccione una sección.';
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleInputChange = (fieldName: string) => {
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
if (errorMessage) clearErrorMessage();
if (fieldName === 'nuevoEstadoId') { // Si cambia el estado, resetear pub/secc
setIdPublicacion('');
setIdSeccion('');
}
if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion
setIdSeccion('');
}
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate() || !bobinaActual) return;
setLoading(true);
try {
const dataToSubmit: CambiarEstadoBobinaDto = {
nuevoEstadoId: Number(nuevoEstadoId),
idPublicacion: Number(nuevoEstadoId) === ID_ESTADO_EN_USO ? Number(idPublicacion) : null,
idSeccion: Number(nuevoEstadoId) === ID_ESTADO_EN_USO ? Number(idSeccion) : null,
obs: obs || undefined,
fechaCambioEstado,
};
await onSubmit(bobinaActual.idBobina, dataToSubmit);
onClose();
} catch (error: any) {
console.error("Error en submit de StockBobinaCambioEstadoModal:", error);
} finally {
setLoading(false);
}
};
if (!bobinaActual) return null;
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
Cambiar Estado de Bobina: {bobinaActual.nroBobina}
</Typography>
<Typography variant="body2" gutterBottom>
Estado Actual: <strong>{bobinaActual.nombreEstadoBobina}</strong>
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.nuevoEstadoId}>
<InputLabel id="nuevo-estado-select-label" required>Nuevo Estado</InputLabel>
<Select labelId="nuevo-estado-select-label" label="Nuevo Estado" value={nuevoEstadoId}
onChange={(e) => {setNuevoEstadoId(e.target.value as number); handleInputChange('nuevoEstadoId');}}
disabled={loading || loadingDropdowns || estadosDisponibles.length === 0}
>
<MenuItem value="" disabled><em>Seleccione un estado</em></MenuItem>
{estadosDisponibles.map((e) => (<MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>))}
</Select>
{localErrors.nuevoEstadoId && <Typography color="error" variant="caption">{localErrors.nuevoEstadoId}</Typography>}
</FormControl>
{Number(nuevoEstadoId) === ID_ESTADO_EN_USO && (
<>
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion}>
<InputLabel id="publicacion-estado-select-label" required>Publicación</InputLabel>
<Select labelId="publicacion-estado-select-label" label="Publicación" value={idPublicacion}
onChange={(e) => {setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione publicación</em></MenuItem>
{publicacionesDisponibles.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>))}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
</FormControl>
<FormControl fullWidth margin="dense" error={!!localErrors.idSeccion}>
<InputLabel id="seccion-estado-select-label" required>Sección</InputLabel>
<Select labelId="seccion-estado-select-label" label="Sección" value={idSeccion}
onChange={(e) => {setIdSeccion(e.target.value as number); handleInputChange('idSeccion');}}
disabled={loading || loadingDropdowns || !idPublicacion || seccionesDisponibles.length === 0}
>
<MenuItem value="" disabled><em>{idPublicacion ? 'Seleccione sección' : 'Seleccione publicación primero'}</em></MenuItem>
{seccionesDisponibles.map((s) => (<MenuItem key={s.idSeccion} value={s.idSeccion}>{s.nombre}</MenuItem>))}
</Select>
{localErrors.idSeccion && <Typography color="error" variant="caption">{localErrors.idSeccion}</Typography>}
{localErrors.secciones && <Alert severity="warning" sx={{mt:0.5}}>{localErrors.secciones}</Alert>}
</FormControl>
</>
)}
<TextField label="Fecha Cambio de Estado" type="date" value={fechaCambioEstado} required
onChange={(e) => {setFechaCambioEstado(e.target.value); handleInputChange('fechaCambioEstado');}}
margin="dense" fullWidth error={!!localErrors.fechaCambioEstado} helperText={localErrors.fechaCambioEstado || ''}
disabled={loading} InputLabelProps={{ shrink: true }}
/>
<TextField label="Observaciones (Opcional)" value={obs}
onChange={(e) => setObs(e.target.value)}
margin="dense" fullWidth multiline rows={3} disabled={loading}
/>
</Box>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns || estadosDisponibles.length === 0}>
{loading ? <CircularProgress size={24} /> : 'Guardar Cambio de Estado'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default StockBobinaCambioEstadoModal;

View File

@@ -0,0 +1,206 @@
import React, { useState, useEffect } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem
} from '@mui/material';
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
import type { UpdateStockBobinaDto } from '../../../models/dtos/Impresion/UpdateStockBobinaDto';
import type { TipoBobinaDto } from '../../../models/dtos/Impresion/TipoBobinaDto';
import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto';
import tipoBobinaService from '../../../services/Impresion/tipoBobinaService';
import plantaService from '../../../services/Impresion/plantaService';
const modalStyle = { /* ... (mismo estilo que StockBobinaIngresoFormModal) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 550 },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflowY: 'auto'
};
interface StockBobinaEditFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (idBobina: number, data: UpdateStockBobinaDto) => Promise<void>;
initialData: StockBobinaDto | null; // Siempre habrá initialData para editar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const StockBobinaEditFormModal: React.FC<StockBobinaEditFormModalProps> = ({
open,
onClose,
onSubmit,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idTipoBobina, setIdTipoBobina] = useState<number | string>('');
const [nroBobina, setNroBobina] = useState('');
const [peso, setPeso] = useState<string>('');
const [idPlanta, setIdPlanta] = useState<number | string>('');
const [remito, setRemito] = useState('');
const [fechaRemito, setFechaRemito] = useState(''); // yyyy-MM-dd
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
useEffect(() => {
const fetchDropdownData = async () => {
setLoadingDropdowns(true);
try {
const [tiposData, plantasData] = await Promise.all([
tipoBobinaService.getAllTiposBobina(),
plantaService.getAllPlantas()
]);
setTiposBobina(tiposData);
setPlantas(plantasData);
} catch (error) {
console.error("Error al cargar datos para dropdowns (StockBobina Edit)", error);
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar tipos/plantas.'}));
} finally {
setLoadingDropdowns(false);
}
};
if (open && initialData) {
fetchDropdownData();
setIdTipoBobina(initialData.idTipoBobina || '');
setNroBobina(initialData.nroBobina || '');
setPeso(initialData.peso?.toString() || '');
setIdPlanta(initialData.idPlanta || '');
setRemito(initialData.remito || '');
setFechaRemito(initialData.fechaRemito || ''); // Asume yyyy-MM-dd del DTO
setLocalErrors({});
clearErrorMessage();
}
}, [open, initialData, clearErrorMessage]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idTipoBobina) errors.idTipoBobina = 'Seleccione un tipo.';
if (!nroBobina.trim()) errors.nroBobina = 'Nro. Bobina es obligatorio.';
if (!peso.trim() || isNaN(parseInt(peso)) || parseInt(peso) <= 0) errors.peso = 'Peso debe ser un número positivo.';
if (!idPlanta) errors.idPlanta = 'Seleccione una planta.';
if (!remito.trim()) errors.remito = 'Remito es obligatorio.';
if (!fechaRemito.trim()) errors.fechaRemito = 'Fecha de Remito es obligatoria.';
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaRemito)) errors.fechaRemito = 'Formato de fecha inválido.';
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleInputChange = (fieldName: string) => {
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
if (errorMessage) clearErrorMessage();
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate() || !initialData) return; // initialData siempre debería existir aquí
setLoading(true);
try {
const dataToSubmit: UpdateStockBobinaDto = {
idTipoBobina: Number(idTipoBobina),
nroBobina,
peso: parseInt(peso, 10),
idPlanta: Number(idPlanta),
remito,
fechaRemito,
};
await onSubmit(initialData.idBobina, dataToSubmit);
onClose();
} catch (error: any) {
console.error("Error en submit de StockBobinaEditFormModal:", error);
// El error de API lo maneja la página que llama a este modal
} finally {
setLoading(false);
}
};
if (!initialData) return null; // No renderizar si no hay datos iniciales (aunque open lo controla)
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
Editar Datos de Bobina (ID: {initialData.idBobina})
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idTipoBobina} sx={{flex:1, minWidth: '200px'}}>
<InputLabel id="edit-tipo-bobina-select-label" required>Tipo Bobina</InputLabel>
<Select labelId="edit-tipo-bobina-select-label" label="Tipo Bobina" value={idTipoBobina}
onChange={(e) => {setIdTipoBobina(e.target.value as number); handleInputChange('idTipoBobina');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione un tipo</em></MenuItem>
{tiposBobina.map((t) => (<MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>))}
</Select>
{localErrors.idTipoBobina && <Typography color="error" variant="caption">{localErrors.idTipoBobina}</Typography>}
</FormControl>
<TextField label="Nro. Bobina" value={nroBobina} required
onChange={(e) => {setNroBobina(e.target.value); handleInputChange('nroBobina');}}
margin="dense" fullWidth error={!!localErrors.nroBobina} helperText={localErrors.nroBobina || ''}
disabled={loading} sx={{flex:1, minWidth: '200px'}} autoFocus
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<TextField label="Peso (Kg)" type="number" value={peso} required
onChange={(e) => {setPeso(e.target.value); handleInputChange('peso');}}
margin="dense" fullWidth error={!!localErrors.peso} helperText={localErrors.peso || ''}
disabled={loading} sx={{flex:1, minWidth: '150px'}}
/>
<FormControl fullWidth margin="dense" error={!!localErrors.idPlanta} sx={{flex:1, minWidth: '200px'}}>
<InputLabel id="edit-planta-select-label" required>Planta Destino</InputLabel>
<Select labelId="edit-planta-select-label" label="Planta Destino" value={idPlanta}
onChange={(e) => {setIdPlanta(e.target.value as number); handleInputChange('idPlanta');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione una planta</em></MenuItem>
{plantas.map((p) => (<MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>))}
</Select>
{localErrors.idPlanta && <Typography color="error" variant="caption">{localErrors.idPlanta}</Typography>}
</FormControl>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<TextField label="Nro. Remito" value={remito} required
onChange={(e) => {setRemito(e.target.value); handleInputChange('remito');}}
margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''}
disabled={loading} sx={{flex:1, minWidth: '200px'}}
/>
<TextField label="Fecha Remito" type="date" value={fechaRemito} required
onChange={(e) => {setFechaRemito(e.target.value); handleInputChange('fechaRemito');}}
margin="dense" fullWidth error={!!localErrors.fechaRemito} helperText={localErrors.fechaRemito || ''}
disabled={loading} InputLabelProps={{ shrink: true }} sx={{flex:1, minWidth: '200px'}}
/>
</Box>
</Box>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
{loading ? <CircularProgress size={24} /> : 'Guardar Cambios'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default StockBobinaEditFormModal;

View File

@@ -0,0 +1,192 @@
import React, { useState, useEffect } from 'react';
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem } from '@mui/material';
import type { CreateStockBobinaDto } from '../../../models/dtos/Impresion/CreateStockBobinaDto';
import type { TipoBobinaDto } from '../../../models/dtos/Impresion/TipoBobinaDto';
import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto';
import tipoBobinaService from '../../../services/Impresion/tipoBobinaService';
import plantaService from '../../../services/Impresion/plantaService';
const modalStyle = { /* ... (mismo estilo) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 550 },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
maxHeight: '90vh',
overflowY: 'auto'
};
interface StockBobinaIngresoFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreateStockBobinaDto) => Promise<void>; // Solo para crear
errorMessage?: string | null;
clearErrorMessage: () => void;
// initialData no es necesario para un modal de solo creación
}
const StockBobinaIngresoFormModal: React.FC<StockBobinaIngresoFormModalProps> = ({
open,
onClose,
onSubmit,
errorMessage,
clearErrorMessage
}) => {
const [idTipoBobina, setIdTipoBobina] = useState<number | string>('');
const [nroBobina, setNroBobina] = useState('');
const [peso, setPeso] = useState<string>('');
const [idPlanta, setIdPlanta] = useState<number | string>('');
const [remito, setRemito] = useState('');
const [fechaRemito, setFechaRemito] = useState(''); // yyyy-MM-dd
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
useEffect(() => {
const fetchDropdownData = async () => {
setLoadingDropdowns(true);
try {
const [tiposData, plantasData] = await Promise.all([
tipoBobinaService.getAllTiposBobina(),
plantaService.getAllPlantas()
]);
setTiposBobina(tiposData);
setPlantas(plantasData);
} catch (error) {
console.error("Error al cargar datos para dropdowns (StockBobina)", error);
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar tipos/plantas.'}));
} finally {
setLoadingDropdowns(false);
}
};
if (open) {
fetchDropdownData();
// Resetear campos
setIdTipoBobina(''); setNroBobina(''); setPeso(''); setIdPlanta('');
setRemito(''); setFechaRemito(new Date().toISOString().split('T')[0]); // Default a hoy
setLocalErrors({});
clearErrorMessage();
}
}, [open, clearErrorMessage]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idTipoBobina) errors.idTipoBobina = 'Seleccione un tipo.';
if (!nroBobina.trim()) errors.nroBobina = 'Nro. Bobina es obligatorio.';
if (!peso.trim() || isNaN(parseInt(peso)) || parseInt(peso) <= 0) errors.peso = 'Peso debe ser un número positivo.';
if (!idPlanta) errors.idPlanta = 'Seleccione una planta.';
if (!remito.trim()) errors.remito = 'Remito es obligatorio.';
if (!fechaRemito.trim()) errors.fechaRemito = 'Fecha de Remito es obligatoria.';
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaRemito)) errors.fechaRemito = 'Formato de fecha inválido.';
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleInputChange = (fieldName: string) => {
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
if (errorMessage) clearErrorMessage();
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate()) return;
setLoading(true);
try {
const dataToSubmit: CreateStockBobinaDto = {
idTipoBobina: Number(idTipoBobina),
nroBobina,
peso: parseInt(peso, 10),
idPlanta: Number(idPlanta),
remito,
fechaRemito,
};
await onSubmit(dataToSubmit);
onClose();
} catch (error: any) {
console.error("Error en submit de StockBobinaIngresoFormModal:", error);
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>Ingresar Nueva Bobina a Stock</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idTipoBobina} sx={{flex:1, minWidth: '200px'}}>
<InputLabel id="tipo-bobina-select-label" required>Tipo Bobina</InputLabel>
<Select labelId="tipo-bobina-select-label" label="Tipo Bobina" value={idTipoBobina}
onChange={(e) => {setIdTipoBobina(e.target.value as number); handleInputChange('idTipoBobina');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione un tipo</em></MenuItem>
{tiposBobina.map((t) => (<MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>))}
</Select>
{localErrors.idTipoBobina && <Typography color="error" variant="caption">{localErrors.idTipoBobina}</Typography>}
</FormControl>
<TextField label="Nro. Bobina" value={nroBobina} required
onChange={(e) => {setNroBobina(e.target.value); handleInputChange('nroBobina');}}
margin="dense" fullWidth error={!!localErrors.nroBobina} helperText={localErrors.nroBobina || ''}
disabled={loading} sx={{flex:1, minWidth: '200px'}}
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<TextField label="Peso (Kg)" type="number" value={peso} required
onChange={(e) => {setPeso(e.target.value); handleInputChange('peso');}}
margin="dense" fullWidth error={!!localErrors.peso} helperText={localErrors.peso || ''}
disabled={loading} sx={{flex:1, minWidth: '150px'}}
/>
<FormControl fullWidth margin="dense" error={!!localErrors.idPlanta} sx={{flex:1, minWidth: '200px'}}>
<InputLabel id="planta-select-label" required>Planta Destino</InputLabel>
<Select labelId="planta-select-label" label="Planta Destino" value={idPlanta}
onChange={(e) => {setIdPlanta(e.target.value as number); handleInputChange('idPlanta');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione una planta</em></MenuItem>
{plantas.map((p) => (<MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>))}
</Select>
{localErrors.idPlanta && <Typography color="error" variant="caption">{localErrors.idPlanta}</Typography>}
</FormControl>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<TextField label="Nro. Remito" value={remito} required
onChange={(e) => {setRemito(e.target.value); handleInputChange('remito');}}
margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''}
disabled={loading} sx={{flex:1, minWidth: '200px'}}
/>
<TextField label="Fecha Remito" type="date" value={fechaRemito} required
onChange={(e) => {setFechaRemito(e.target.value); handleInputChange('fechaRemito');}}
margin="dense" fullWidth error={!!localErrors.fechaRemito} helperText={localErrors.fechaRemito || ''}
disabled={loading} InputLabelProps={{ shrink: true }} sx={{flex:1, minWidth: '200px'}}
/>
</Box>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
{loading ? <CircularProgress size={24} /> : 'Ingresar Bobina'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default StockBobinaIngresoFormModal;

View File

@@ -0,0 +1,334 @@
// src/components/Modals/TiradaFormModal.tsx
import React, { useState, useEffect, useCallback } from 'react';
import {
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, IconButton, Paper,
Table, TableHead, TableRow, TableCell, TableBody,
TableContainer
} from '@mui/material';
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import type { CreateTiradaRequestDto } from '../../../models/dtos/Impresion/CreateTiradaRequestDto';
import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto';
import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto';
import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto';
import publicacionService from '../../../services/Distribucion/publicacionService';
import plantaService from '../../../services/Impresion/plantaService';
import publiSeccionService from '../../../services/Distribucion/publiSeccionService';
const modalStyle = { /* ... (mismo estilo) ... */
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '95%', sm: '80%', md: '750px' },
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 3,
maxHeight: '90vh',
overflowY: 'auto'
};
// CORREGIDO: Ajustar el tipo para los inputs. Usaremos string para los inputs,
// y convertiremos a number al hacer submit o al validar donde sea necesario.
interface DetalleSeccionFormState {
idSeccion: number | ''; // Permitir string vacío para el Select no seleccionado
nombreSeccion?: string;
cantPag: string; // TextField de cantPag siempre es string
idTemporal: string; // Para la key de React
}
interface TiradaFormModalProps {
open: boolean;
onClose: () => void;
onSubmit: (data: CreateTiradaRequestDto) => Promise<void>;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const TiradaFormModal: React.FC<TiradaFormModalProps> = ({
open,
onClose,
onSubmit,
errorMessage,
clearErrorMessage
}) => {
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [idPlanta, setIdPlanta] = useState<number | string>('');
const [ejemplares, setEjemplares] = useState<string>('');
// CORREGIDO: Usar el nuevo tipo para el estado del formulario de secciones
const [seccionesDeTirada, setSeccionesDeTirada] = useState<DetalleSeccionFormState[]>([]);
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
const [seccionesPublicacion, setSeccionesPublicacion] = useState<PubliSeccionDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const resetForm = () => {
setIdPublicacion('');
setFecha(new Date().toISOString().split('T')[0]);
setIdPlanta('');
setEjemplares('');
setSeccionesDeTirada([]);
setSeccionesPublicacion([]);
setLocalErrors({});
clearErrorMessage();
};
const fetchInitialDropdowns = useCallback(async () => {
setLoadingDropdowns(true);
try {
const [pubsData, plantasData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true),
plantaService.getAllPlantas()
]);
setPublicaciones(pubsData);
setPlantas(plantasData);
} catch (error) {
console.error("Error al cargar publicaciones/plantas", error);
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos iniciales.'}));
} finally {
setLoadingDropdowns(false);
}
}, []);
useEffect(() => {
if (open) {
resetForm(); // Llama a resetForm aquí
fetchInitialDropdowns();
}
}, [open, fetchInitialDropdowns]); // resetForm no necesita estar en las dependencias si su contenido no cambia basado en props/estado que también estén en las dependencias.
const fetchSeccionesDePublicacion = useCallback(async (pubId: number) => {
if (!pubId) {
setSeccionesPublicacion([]);
setSeccionesDeTirada([]);
return;
}
setLoadingDropdowns(true);
try {
const data = await publiSeccionService.getSeccionesPorPublicacion(pubId, true);
setSeccionesPublicacion(data);
setSeccionesDeTirada([]);
} catch (error) {
console.error("Error al cargar secciones de la publicación", error);
setLocalErrors(prev => ({...prev, secciones: 'Error al cargar secciones.'}));
} finally {
setLoadingDropdowns(false);
}
}, []);
useEffect(() => {
if (idPublicacion) {
fetchSeccionesDePublicacion(Number(idPublicacion));
} else {
setSeccionesPublicacion([]);
setSeccionesDeTirada([]);
}
}, [idPublicacion, fetchSeccionesDePublicacion]);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.';
if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.';
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido.';
if (!idPlanta) errors.idPlanta = 'Seleccione una planta.';
if (!ejemplares.trim() || isNaN(parseInt(ejemplares)) || parseInt(ejemplares) <= 0) errors.ejemplares = 'Ejemplares debe ser un número positivo.';
if (seccionesDeTirada.length === 0) {
errors.seccionesArray = 'Debe agregar al menos una sección a la tirada.';
} else {
seccionesDeTirada.forEach((sec, index) => {
if (sec.idSeccion === '') errors[`seccion_${index}_id`] = `Fila ${index + 1}: Debe seleccionar una sección.`;
if (!sec.cantPag.trim() || isNaN(Number(sec.cantPag)) || Number(sec.cantPag) <= 0) {
errors[`seccion_${index}_pag`] = `Fila ${index + 1}: Cant. Páginas debe ser un número positivo.`;
}
});
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleAddSeccion = () => {
setSeccionesDeTirada([...seccionesDeTirada, { idSeccion: '', cantPag: '', nombreSeccion: '', idTemporal: crypto.randomUUID() }]);
if (localErrors.seccionesArray) setLocalErrors(prev => ({ ...prev, seccionesArray: null }));
};
const handleRemoveSeccion = (index: number) => {
setSeccionesDeTirada(seccionesDeTirada.filter((_, i) => i !== index));
};
const handleSeccionChange = (index: number, field: 'idSeccion' | 'cantPag', value: string | number) => {
const nuevasSecciones = [...seccionesDeTirada];
const targetSeccion = nuevasSecciones[index];
if (field === 'idSeccion') {
const numValue = Number(value); // El valor del Select es string, pero lo guardamos como number | ''
targetSeccion.idSeccion = numValue === 0 ? '' : numValue; // Si es 0 (placeholder), guardar ''
const seccionSeleccionada = seccionesPublicacion.find(s => s.idSeccion === numValue);
targetSeccion.nombreSeccion = seccionSeleccionada?.nombre || '';
} else { // cantPag
targetSeccion.cantPag = value as string; // Guardar como string, validar como número después
}
setSeccionesDeTirada(nuevasSecciones);
if (localErrors[`seccion_${index}_id`]) setLocalErrors(prev => ({ ...prev, [`seccion_${index}_id`]: null }));
if (localErrors[`seccion_${index}_pag`]) setLocalErrors(prev => ({ ...prev, [`seccion_${index}_pag`]: null }));
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearErrorMessage();
if (!validate()) return;
setLoading(true);
try {
const dataToSubmit: CreateTiradaRequestDto = {
idPublicacion: Number(idPublicacion),
fecha,
idPlanta: Number(idPlanta),
ejemplares: parseInt(ejemplares, 10),
// CORREGIDO: Asegurar que los datos de secciones sean números
secciones: seccionesDeTirada.map(s => ({
idSeccion: Number(s.idSeccion), // Convertir a número aquí
cantPag: Number(s.cantPag) // Convertir a número aquí
}))
};
await onSubmit(dataToSubmit);
onClose();
} catch (error: any) {
console.error("Error en submit de TiradaFormModal:", error);
} finally {
setLoading(false);
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>Registrar Nueva Tirada</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
{/* ... (campos de Publicacion, Fecha, Planta, Ejemplares sin cambios) ... */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} sx={{flex:1, minWidth: 200}}>
<InputLabel id="publicacion-tirada-select-label" required>Publicación</InputLabel>
<Select labelId="publicacion-tirada-select-label" label="Publicación" value={idPublicacion}
onChange={(e) => { setIdPublicacion(e.target.value as number); setLocalErrors(p => ({...p, idPublicacion: null})); }}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{publicaciones.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre} ({p.nombreEmpresa})</MenuItem>))}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPublicacion}</Typography>}
</FormControl>
<TextField label="Fecha Tirada" type="date" value={fecha} required
onChange={(e) => {setFecha(e.target.value); setLocalErrors(p => ({...p, fecha: null}));}}
margin="dense" error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
disabled={loading} InputLabelProps={{ shrink: true }} sx={{flex:1, minWidth: 160}}
/>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idPlanta} sx={{flex:1, minWidth: 200}}>
<InputLabel id="planta-tirada-select-label" required>Planta</InputLabel>
<Select labelId="planta-tirada-select-label" label="Planta" value={idPlanta}
onChange={(e) => {setIdPlanta(e.target.value as number); setLocalErrors(p => ({...p, idPlanta: null}));}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{plantas.map((p) => (<MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>))}
</Select>
{localErrors.idPlanta && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPlanta}</Typography>}
</FormControl>
<TextField label="Total Ejemplares" type="number" value={ejemplares} required
onChange={(e) => {setEjemplares(e.target.value); setLocalErrors(p => ({...p, ejemplares: null}));}}
margin="dense" error={!!localErrors.ejemplares} helperText={localErrors.ejemplares || ''}
disabled={loading} sx={{flex:1, minWidth: 150}}
inputProps={{min:1}}
/>
</Box>
<Typography variant="subtitle1" sx={{mt: 2, mb:1}}>Detalle de Secciones Impresas:</Typography>
{localErrors.seccionesArray && <Alert severity="error" sx={{mb:1}}>{localErrors.seccionesArray}</Alert>}
<Paper variant="outlined" sx={{p:1, mb:2, maxHeight: '250px', overflowY: 'auto'}}> {/* Permitir scroll en tabla de secciones */}
<TableContainer>
<Table size="small" stickyHeader> {/* stickyHeader para que cabecera quede fija */}
<TableHead>
<TableRow>
<TableCell sx={{fontWeight:'bold', minWidth: 200}}>Sección</TableCell>
<TableCell sx={{fontWeight:'bold', width: '150px'}}>Cant. Páginas</TableCell>
<TableCell align="right" sx={{width: '50px'}}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{seccionesDeTirada.map((sec, index) => (
<TableRow key={sec.idTemporal || index}> {/* Usar idTemporal para key */}
<TableCell sx={{py:0.5}}>
<FormControl fullWidth size="small" error={!!localErrors[`seccion_${index}_id`]}>
<Select value={sec.idSeccion} // Ahora idSeccion es number | ''
onChange={(e) => handleSeccionChange(index, 'idSeccion', e.target.value as number | '')}
disabled={loading || loadingDropdowns || seccionesPublicacion.length === 0}
displayEmpty
>
<MenuItem value="" disabled><em>Seleccionar</em></MenuItem>
{seccionesPublicacion.map(s => <MenuItem key={s.idSeccion} value={s.idSeccion}>{s.nombre}</MenuItem>)}
</Select>
{localErrors[`seccion_${index}_id`] && <Typography color="error" variant="caption">{localErrors[`seccion_${index}_id`]}</Typography>}
</FormControl>
</TableCell>
<TableCell sx={{py:0.5}}>
<TextField type="number" size="small" fullWidth value={sec.cantPag}
onChange={(e) => handleSeccionChange(index, 'cantPag', e.target.value)}
error={!!localErrors[`seccion_${index}_pag`]}
helperText={localErrors[`seccion_${index}_pag`] || ''}
disabled={loading}
inputProps={{min:1}}
/>
</TableCell>
<TableCell align="right" sx={{py:0.5}}>
<IconButton onClick={() => handleRemoveSeccion(index)} size="small" color="error" disabled={loading}>
<DeleteOutlineIcon />
</IconButton>
</TableCell>
</TableRow>
))}
{seccionesDeTirada.length === 0 && (
<TableRow>
<TableCell colSpan={3} align="center">
<Typography variant="caption" color="textSecondary">
Agregue secciones a la tirada.
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<Button startIcon={<AddCircleOutlineIcon />} onClick={handleAddSeccion} sx={{mt:1}} size="small" disabled={loading || !idPublicacion}>
Agregar Sección
</Button>
</Paper>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
{loading ? <CircularProgress size={24} /> : 'Registrar Tirada'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default TiradaFormModal;

View File

@@ -0,0 +1,9 @@
export interface CreateEntradaSalidaDistDto {
idPublicacion: number;
idDistribuidor: number;
fecha: string; // "yyyy-MM-dd"
tipoMovimiento: 'Salida' | 'Entrada';
cantidad: number;
remito: number;
observacion?: string | null;
}

View File

@@ -0,0 +1,7 @@
export interface CreatePorcMonCanillaDto {
idPublicacion: number;
idCanilla: number;
vigenciaD: string; // "yyyy-MM-dd"
porcMon: number;
esPorcentaje: boolean;
}

View File

@@ -0,0 +1,6 @@
export interface CreatePorcPagoDto {
idPublicacion: number;
idDistribuidor: number;
vigenciaD: string; // "yyyy-MM-dd"
porcentaje: number;
}

View File

@@ -0,0 +1,5 @@
export interface CreatePubliSeccionDto {
idPublicacion: number;
nombre: string;
estado?: boolean; // Default true en backend
}

View File

@@ -0,0 +1,6 @@
export interface CreateRecargoZonaDto {
idPublicacion: number;
idZona: number;
vigenciaD: string; // "yyyy-MM-dd"
valor: number;
}

View File

@@ -0,0 +1,7 @@
export interface CreateSalidaOtroDestinoDto {
idPublicacion: number;
idDestino: number;
fecha: string; // "yyyy-MM-dd"
cantidad: number;
observacion?: string | null;
}

View File

@@ -0,0 +1,15 @@
export interface EntradaSalidaDistDto {
idParte: number;
idPublicacion: number;
nombrePublicacion: string;
nombreEmpresaPublicacion: string;
idEmpresaPublicacion: number;
idDistribuidor: number;
nombreDistribuidor: string;
fecha: string; // "yyyy-MM-dd"
tipoMovimiento: 'Salida' | 'Entrada';
cantidad: number;
remito: number;
observacion?: string | null;
montoCalculado: number;
}

View File

@@ -0,0 +1,10 @@
export interface PorcMonCanillaDto {
idPorcMon: number;
idPublicacion: number;
idCanilla: number;
nomApeCanilla: string;
vigenciaD: string; // "yyyy-MM-dd"
vigenciaH?: string | null; // "yyyy-MM-dd"
porcMon: number;
esPorcentaje: boolean;
}

View File

@@ -0,0 +1,9 @@
export interface PorcPagoDto {
idPorcentaje: number;
idPublicacion: number;
idDistribuidor: number;
nombreDistribuidor: string;
vigenciaD: string; // "yyyy-MM-dd"
vigenciaH?: string | null; // "yyyy-MM-dd"
porcentaje: number;
}

View File

@@ -0,0 +1,6 @@
export interface PubliSeccionDto {
idSeccion: number;
idPublicacion: number;
nombre: string;
estado: boolean; // true = activa, false = inactiva
}

View File

@@ -0,0 +1,9 @@
export interface RecargoZonaDto {
idRecargo: number;
idPublicacion: number;
idZona: number;
nombreZona: string;
vigenciaD: string; // "yyyy-MM-dd"
vigenciaH?: string | null; // "yyyy-MM-dd"
valor: number;
}

View File

@@ -0,0 +1,10 @@
export interface SalidaOtroDestinoDto {
idParte: number;
idPublicacion: number;
nombrePublicacion: string;
idDestino: number;
nombreDestino: string;
fecha: string; // "yyyy-MM-dd"
cantidad: number;
observacion?: string | null;
}

View File

@@ -0,0 +1,4 @@
export interface UpdateEntradaSalidaDistDto {
cantidad: number;
observacion?: string | null;
}

View File

@@ -0,0 +1,5 @@
export interface UpdatePorcMonCanillaDto {
porcMon: number;
esPorcentaje: boolean;
vigenciaH?: string | null; // "yyyy-MM-dd"
}

View File

@@ -0,0 +1,4 @@
export interface UpdatePorcPagoDto {
porcentaje: number;
vigenciaH?: string | null; // "yyyy-MM-dd"
}

View File

@@ -0,0 +1,4 @@
export interface UpdatePubliSeccionDto {
nombre: string;
estado: boolean;
}

View File

@@ -0,0 +1,4 @@
export interface UpdateRecargoZonaDto {
valor: number;
vigenciaH?: string | null; // "yyyy-MM-dd"
}

View File

@@ -0,0 +1,4 @@
export interface UpdateSalidaOtroDestinoDto {
cantidad: number;
observacion?: string | null;
}

View File

@@ -0,0 +1,7 @@
export interface CambiarEstadoBobinaDto {
nuevoEstadoId: number;
idPublicacion?: number | null;
idSeccion?: number | null;
obs?: string | null;
fechaCambioEstado: string; // "yyyy-MM-dd"
}

View File

@@ -0,0 +1,8 @@
export interface CreateStockBobinaDto {
idTipoBobina: number;
nroBobina: string;
peso: number;
idPlanta: number;
remito: string;
fechaRemito: string; // "yyyy-MM-dd"
}

View File

@@ -0,0 +1,9 @@
import type { DetalleSeccionTiradaDto } from "./DetalleSeccionTiradaDto";
export interface CreateTiradaRequestDto {
idPublicacion: number;
fecha: string; // "yyyy-MM-dd"
idPlanta: number;
ejemplares: number;
secciones: DetalleSeccionTiradaDto[];
}

View File

@@ -0,0 +1,4 @@
export interface DetalleSeccionTiradaDto {
idSeccion: number;
cantPag: number;
}

View File

@@ -0,0 +1,19 @@
export interface StockBobinaDto {
idBobina: number;
idTipoBobina: number;
nombreTipoBobina: string;
nroBobina: string;
peso: number;
idPlanta: number;
nombrePlanta: string;
idEstadoBobina: number;
nombreEstadoBobina: string;
remito: string;
fechaRemito: string; // "yyyy-MM-dd"
fechaEstado?: string | null; // "yyyy-MM-dd"
idPublicacion?: number | null;
nombrePublicacion?: string | null;
idSeccion?: number | null;
nombreSeccion?: string | null;
obs?: string | null;
}

View File

@@ -0,0 +1,17 @@
export interface DetalleSeccionEnListadoDto {
idSeccion: number;
nombreSeccion: string;
cantPag: number;
idRegPublicacionSeccion: number;
}
export interface TiradaDto {
idRegistroTirada: number;
idPublicacion: number;
nombrePublicacion: string;
fecha: string; // "yyyy-MM-dd"
idPlanta: number;
nombrePlanta: string;
ejemplares: number;
seccionesImpresas: DetalleSeccionEnListadoDto[];
totalPaginasSumadas: number;
}

View File

@@ -0,0 +1,8 @@
export interface UpdateStockBobinaDto {
idTipoBobina: number;
nroBobina: string;
peso: number;
idPlanta: number;
remito: string;
fechaRemito: string; // "yyyy-MM-dd"
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View 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;

View 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;

View File

@@ -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 */}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,302 @@
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 SwapHorizIcon from '@mui/icons-material/SwapHoriz'; // Para cambiar estado
import stockBobinaService from '../../services/Impresion/stockBobinaService';
import tipoBobinaService from '../../services/Impresion/tipoBobinaService';
import plantaService from '../../services/Impresion/plantaService';
import estadoBobinaService from '../../services/Impresion/estadoBobinaService';
import type { StockBobinaDto } from '../../models/dtos/Impresion/StockBobinaDto';
import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateStockBobinaDto';
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto';
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal';
import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal';
import StockBobinaCambioEstadoModal from '../../components/Modals/Impresion/StockBobinaCambioEstadoModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarStockBobinasPage: React.FC = () => {
const [stock, setStock] = useState<StockBobinaDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Estados para filtros
const [filtroTipoBobina, setFiltroTipoBobina] = useState<number | string>('');
const [filtroNroBobina, setFiltroNroBobina] = useState('');
const [filtroPlanta, setFiltroPlanta] = useState<number | string>('');
const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>('');
const [filtroRemito, setFiltroRemito] = useState('');
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
// Datos para dropdowns de filtros
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [ingresoModalOpen, setIngresoModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [cambioEstadoModalOpen, setCambioEstadoModalOpen] = useState(false);
const [selectedBobina, setSelectedBobina] = useState<StockBobinaDto | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVer = isSuperAdmin || tienePermiso("IB001");
const puedeIngresar = isSuperAdmin || tienePermiso("IB002");
const puedeCambiarEstado = isSuperAdmin || tienePermiso("IB003");
const puedeModificarDatos = isSuperAdmin || tienePermiso("IB004");
const puedeEliminar = isSuperAdmin || tienePermiso("IB005");
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const [tiposData, plantasData, estadosData] = await Promise.all([
tipoBobinaService.getAllTiposBobina(),
plantaService.getAllPlantas(),
estadoBobinaService.getAllEstadosBobina()
]);
setTiposBobina(tiposData);
setPlantas(plantasData);
setEstadosBobina(estadosData);
} catch (err) {
console.error("Error cargando datos para filtros:", err);
setError("Error al cargar opciones de filtro.");
} finally {
setLoadingFiltersDropdown(false);
}
}, []);
useEffect(() => {
fetchFiltersDropdownData();
}, [fetchFiltersDropdownData]);
const cargarStock = 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 = {
idTipoBobina: filtroTipoBobina ? Number(filtroTipoBobina) : null,
nroBobinaFilter: filtroNroBobina || null,
idPlanta: filtroPlanta ? Number(filtroPlanta) : null,
idEstadoBobina: filtroEstadoBobina ? Number(filtroEstadoBobina) : null,
remitoFilter: filtroRemito || null,
fechaDesde: filtroFechaDesde || null,
fechaHasta: filtroFechaHasta || null,
};
const data = await stockBobinaService.getAllStockBobinas(params);
setStock(data);
} catch (err) {
console.error(err); setError('Error al cargar el stock de bobinas.');
} finally { setLoading(false); }
}, [puedeVer, filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito, filtroFechaDesde, filtroFechaHasta]);
useEffect(() => {
cargarStock();
}, [cargarStock]);
// Handlers para modales
const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); };
const handleCloseIngresoModal = () => setIngresoModalOpen(false);
const handleSubmitIngresoModal = async (data: CreateStockBobinaDto) => {
setApiErrorMessage(null);
try { await stockBobinaService.ingresarBobina(data); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al ingresar bobina.'; setApiErrorMessage(msg); throw err; }
};
const handleOpenEditModal = (bobina: StockBobinaDto) => {
setSelectedBobina(bobina); setApiErrorMessage(null); setEditModalOpen(true); handleMenuClose();
};
const handleCloseEditModal = () => { setEditModalOpen(false); setSelectedBobina(null); };
const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => {
setApiErrorMessage(null);
try { await stockBobinaService.updateDatosBobinaDisponible(idBobina, data); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al actualizar bobina.'; setApiErrorMessage(msg); throw err; }
};
const handleOpenCambioEstadoModal = (bobina: StockBobinaDto) => {
setSelectedBobina(bobina); setApiErrorMessage(null); setCambioEstadoModalOpen(true); handleMenuClose();
};
const handleCloseCambioEstadoModal = () => { setCambioEstadoModalOpen(false); setSelectedBobina(null); };
const handleSubmitCambioEstadoModal = async (idBobina: number, data: CambiarEstadoBobinaDto) => {
setApiErrorMessage(null);
try { await stockBobinaService.cambiarEstadoBobina(idBobina, data); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar estado.'; setApiErrorMessage(msg); throw err; }
};
const handleDeleteBobina = async (idBobina: number) => {
if (window.confirm(`¿Seguro de eliminar este ingreso de bobina (ID: ${idBobina})? Solo se permite si está 'Disponible'.`)) {
setApiErrorMessage(null);
try { await stockBobinaService.deleteIngresoBobina(idBobina); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
}
handleMenuClose();
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, bobina: StockBobinaDto) => {
setAnchorEl(event.currentTarget); setSelectedBobina(bobina);
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedBobina(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 = stock.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
if (!loading && !puedeVer) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 2 }}>
<Typography variant="h4" gutterBottom>Stock de Bobinas</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2}}>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}}>
<InputLabel>Tipo Bobina</InputLabel>
<Select value={filtroTipoBobina} label="Tipo Bobina" onChange={(e) => setFiltroTipoBobina(e.target.value)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todos</em></MenuItem>
{tiposBobina.map(t => <MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>)}
</Select>
</FormControl>
<TextField label="Nro. Bobina" size="small" value={filtroNroBobina} onChange={(e) => setFiltroNroBobina(e.target.value)} sx={{minWidth: 150, flexGrow: 1}}/>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}}>
<InputLabel>Planta</InputLabel>
<Select value={filtroPlanta} label="Planta" onChange={(e) => setFiltroPlanta(e.target.value)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todas</em></MenuItem>
{plantas.map(p => <MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}}>
<InputLabel>Estado</InputLabel>
<Select value={filtroEstadoBobina} label="Estado" onChange={(e) => setFiltroEstadoBobina(e.target.value)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todos</em></MenuItem>
{estadosBobina.map(e => <MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>)}
</Select>
</FormControl>
<TextField label="Remito" size="small" value={filtroRemito} onChange={(e) => setFiltroRemito(e.target.value)} sx={{minWidth: 150, flexGrow: 1}}/>
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170, flexGrow: 1}}/>
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170, flexGrow: 1}}/>
</Box>
{/* <Button variant="outlined" onClick={cargarStock} sx={{ mr: 1 }}>Aplicar Filtros</Button>
<Button variant="outlined" color="secondary" onClick={() => { // Resetear filtros
setFiltroTipoBobina(''); setFiltroNroBobina(''); setFiltroPlanta('');
setFiltroEstadoBobina(''); setFiltroRemito(''); setFiltroFechaDesde(''); setFiltroFechaHasta('');
// cargarStock(); // Opcional: recargar inmediatamente
}}>Limpiar Filtros</Button> */}
{puedeIngresar && (<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenIngresoModal} sx={{ mt:2 }}>Ingresar Bobina</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>Nro. Bobina</TableCell><TableCell>Tipo</TableCell><TableCell>Peso (Kg)</TableCell>
<TableCell>Planta</TableCell><TableCell>Estado</TableCell><TableCell>Remito</TableCell>
<TableCell>F. Remito</TableCell><TableCell>F. Estado</TableCell>
<TableCell>Publicación</TableCell><TableCell>Sección</TableCell>
<TableCell>Obs.</TableCell><TableCell align="right">Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={12} align="center">No se encontraron bobinas con los filtros aplicados.</TableCell></TableRow>
) : (
displayData.map((b) => (
<TableRow key={b.idBobina} hover>
<TableCell>{b.nroBobina}</TableCell><TableCell>{b.nombreTipoBobina}</TableCell>
<TableCell align="right">{b.peso}</TableCell><TableCell>{b.nombrePlanta}</TableCell>
<TableCell><Chip label={b.nombreEstadoBobina} size="small" color={
b.idEstadoBobina === 1 ? "success" : b.idEstadoBobina === 2 ? "primary" : b.idEstadoBobina === 3 ? "error" : "default"
}/></TableCell>
<TableCell>{b.remito}</TableCell><TableCell>{formatDate(b.fechaRemito)}</TableCell>
<TableCell>{formatDate(b.fechaEstado)}</TableCell>
<TableCell>{b.nombrePublicacion || '-'}</TableCell><TableCell>{b.nombreSeccion || '-'}</TableCell>
<TableCell>{b.obs || '-'}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, b)}
disabled={!puedeModificarDatos && !puedeCambiarEstado && !puedeEliminar}
><MoreVertIcon /></IconButton>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 50]} component="div" count={stock.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{selectedBobina?.idEstadoBobina === 1 && puedeModificarDatos && (
<MenuItem onClick={() => handleOpenEditModal(selectedBobina!)}><EditIcon fontSize="small" sx={{mr:1}}/> Editar Datos</MenuItem>)}
{selectedBobina?.idEstadoBobina !== 3 && puedeCambiarEstado && ( // No se puede cambiar estado si está dañada
<MenuItem onClick={() => handleOpenCambioEstadoModal(selectedBobina!)}><SwapHorizIcon fontSize="small" sx={{mr:1}}/> Cambiar Estado</MenuItem>)}
{selectedBobina?.idEstadoBobina === 1 && puedeEliminar && (
<MenuItem onClick={() => handleDeleteBobina(selectedBobina!.idBobina)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar Ingreso</MenuItem>)}
{selectedBobina && selectedBobina.idEstadoBobina === 3 && (!puedeModificarDatos && !puedeCambiarEstado && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
{selectedBobina && selectedBobina.idEstadoBobina !== 1 && selectedBobina.idEstadoBobina !== 3 && (!puedeCambiarEstado) && <MenuItem disabled>Sin acciones</MenuItem>}
</Menu>
<StockBobinaIngresoFormModal
open={ingresoModalOpen} onClose={handleCloseIngresoModal} onSubmit={handleSubmitIngresoModal}
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
/>
{selectedBobina && editModalOpen &&
<StockBobinaEditFormModal
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
initialData={selectedBobina} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
{selectedBobina && cambioEstadoModalOpen &&
<StockBobinaCambioEstadoModal
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
bobinaActual={selectedBobina} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
</Box>
);
};
export default GestionarStockBobinasPage;

View File

@@ -0,0 +1,213 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
CircularProgress, Alert, Accordion, AccordionSummary, AccordionDetails, Chip,
FormControl,
InputLabel,
Select
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import FilterListIcon from '@mui/icons-material/FilterList';
import tiradaService from '../../services/Impresion/tiradaService';
import publicacionService from '../../services/Distribucion/publicacionService'; // Para filtro
import plantaService from '../../services/Impresion/plantaService'; // Para filtro
import type { TiradaDto } from '../../models/dtos/Impresion/TiradaDto';
import type { CreateTiradaRequestDto } from '../../models/dtos/Impresion/CreateTiradaRequestDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
import TiradaFormModal from '../../components/Modals/Impresion/TiradaFormModal';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const GestionarTiradasPage: React.FC = () => {
const [tiradas, setTiradas] = useState<TiradaDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFecha, setFiltroFecha] = useState<string>('');
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
const [filtroIdPlanta, setFiltroIdPlanta] = useState<number | string>('');
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
// No hay "editing" para tiradas por ahora, solo crear y borrar.
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVer = isSuperAdmin || tienePermiso("IT001");
const puedeRegistrar = isSuperAdmin || tienePermiso("IT002");
const puedeEliminar = isSuperAdmin || tienePermiso("IT003");
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const [pubsData, plantasData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true),
plantaService.getAllPlantas()
]);
setPublicaciones(pubsData);
setPlantas(plantasData);
} catch (err) {
console.error("Error cargando datos para filtros:", err);
setError("Error al cargar opciones de filtro.");
} finally {
setLoadingFiltersDropdown(false);
}
}, []);
useEffect(() => {
fetchFiltersDropdownData();
}, [fetchFiltersDropdownData]);
const cargarTiradas = 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 = {
fecha: filtroFecha || null,
idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null,
idPlanta: filtroIdPlanta ? Number(filtroIdPlanta) : null,
};
const data = await tiradaService.getTiradas(params);
setTiradas(data);
} catch (err) {
console.error(err); setError('Error al cargar las tiradas.');
} finally { setLoading(false); }
}, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdPlanta]);
useEffect(() => {
cargarTiradas();
}, [cargarTiradas]);
const handleOpenModal = () => { setApiErrorMessage(null); setModalOpen(true); };
const handleCloseModal = () => setModalOpen(false);
const handleSubmitModal = async (data: CreateTiradaRequestDto) => {
setApiErrorMessage(null);
try {
await tiradaService.registrarTirada(data);
cargarTiradas();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al registrar la tirada.';
setApiErrorMessage(message); throw err;
}
};
const handleDeleteTirada = async (tirada: TiradaDto) => {
if (window.confirm(`¿Seguro de eliminar la tirada del ${tirada.fecha} para "${tirada.nombrePublicacion}" en planta "${tirada.nombrePlanta}"? Esta acción eliminará el total de ejemplares y todas sus secciones asociadas.`)) {
setApiErrorMessage(null);
try {
await tiradaService.deleteTiradaCompleta(tirada.fecha, tirada.idPublicacion, tirada.idPlanta);
cargarTiradas();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la tirada.';
setApiErrorMessage(message);
}
}
};
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>Gestión de Tiradas</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" type="date" size="small" value={filtroFecha} onChange={(e) => setFiltroFecha(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} ({p.nombreEmpresa})</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}} disabled={loadingFiltersDropdown}>
<InputLabel>Planta</InputLabel>
<Select value={filtroIdPlanta} label="Planta" onChange={(e) => setFiltroIdPlanta(e.target.value as number | string)}>
<MenuItem value=""><em>Todas</em></MenuItem>
{plantas.map(p => <MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>)}
</Select>
</FormControl>
{/* <Button variant="outlined" onClick={cargarTiradas} size="small">Buscar</Button> */}
</Box>
{puedeRegistrar && (<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenModal}>Registrar Nueva Tirada</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 && (
<Box>
{tiradas.length === 0 ? (
<Typography sx={{mt:2, textAlign:'center'}}>No se encontraron tiradas con los filtros aplicados.</Typography>
) : (
tiradas.map((tirada) => (
<Accordion key={tirada.idRegistroTirada} sx={{mb:1}}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{display: 'flex', justifyContent: 'space-between', width: '100%', alignItems: 'center'}}>
<Typography sx={{fontWeight:'bold'}}>{formatDate(tirada.fecha)} - {tirada.nombrePublicacion} ({tirada.nombrePlanta})</Typography>
<Box>
<Chip label={`${tirada.ejemplares} ej.`} color="primary" size="small" sx={{mr:1}}/>
<Chip label={`${tirada.totalPaginasSumadas} pág.`} size="small" />
{puedeEliminar && (
<IconButton size="small" onClick={(e) => { e.stopPropagation(); handleDeleteTirada(tirada);}} sx={{ml:1}}>
<DeleteIcon color="error"/>
</IconButton>
)}
</Box>
</Box>
</AccordionSummary>
<AccordionDetails>
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead><TableRow>
<TableCell>Sección</TableCell>
<TableCell align="right">Páginas</TableCell>
</TableRow></TableHead>
<TableBody>
{tirada.seccionesImpresas.map(sec => (
<TableRow key={sec.idRegPublicacionSeccion}>
<TableCell>{sec.nombreSeccion}</TableCell>
<TableCell align="right">{sec.cantPag}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
))
)}
</Box>
)}
{/* No hay paginación para la lista de Acordeones por ahora */}
<TiradaFormModal
open={modalOpen}
onClose={handleCloseModal}
onSubmit={handleSubmitModal}
errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box>
);
};
export default GestionarTiradasPage;

View File

@@ -7,8 +7,8 @@ const impresionSubModules = [
{ label: 'Plantas', path: 'plantas' },
{ label: 'Tipos Bobina', path: 'tipos-bobina' },
{ label: 'Estados Bobina', path: 'estados-bobina' },
// { label: 'Stock Bobinas', path: 'stock-bobinas' },
// { label: 'Tiradas', path: 'tiradas' },
{ label: 'Stock Bobinas', path: 'stock-bobinas' },
{ label: 'Tiradas', path: 'tiradas' },
];
const ImpresionIndexPage: React.FC = () => {

View File

@@ -11,21 +11,27 @@ import { Typography } from '@mui/material';
import DistribucionIndexPage from '../pages/Distribucion/DistribucionIndexPage';
import ESCanillasPage from '../pages/Distribucion/ESCanillasPage';
import ControlDevolucionesPage from '../pages/Distribucion/ControlDevolucionesPage';
import ESDistribuidoresPage from '../pages/Distribucion/ESDistribuidoresPage';
import SalidasOtrosDestinosPage from '../pages/Distribucion/SalidasOtrosDestinosPage';
import GestionarCanillitasPage from '../pages/Distribucion/GestionarCanillitasPage';
import GestionarDistribuidoresPage from '../pages/Distribucion/GestionarDistribuidoresPage';
import GestionarPublicacionesPage from '../pages/Distribucion/GestionarPublicacionesPage'; // Ajusta la ruta si la moviste
import GestionarPublicacionesPage from '../pages/Distribucion/GestionarPublicacionesPage';
import GestionarSeccionesPublicacionPage from '../pages/Distribucion/GestionarSeccionesPublicacionPage';
import GestionarPreciosPublicacionPage from '../pages/Distribucion/GestionarPreciosPublicacionPage';
import GestionarRecargosPublicacionPage from '../pages/Distribucion/GestionarRecargosPublicacionPage';
import GestionarPorcentajesPagoPage from '../pages/Distribucion/GestionarPorcentajesPagoPage';
import GestionarPorcMonCanillaPage from '../pages/Distribucion/GestionarPorcMonCanillaPage';
import GestionarOtrosDestinosPage from '../pages/Distribucion/GestionarOtrosDestinosPage';
import GestionarZonasPage from '../pages/Distribucion/GestionarZonasPage';
import GestionarEmpresasPage from '../pages/Distribucion/GestionarEmpresasPage';
import GestionarSalidasOtrosDestinosPage from '../pages/Distribucion/GestionarSalidasOtrosDestinosPage';
import GestionarEntradasSalidasDistPage from '../pages/Distribucion/GestionarEntradasSalidasDistPage';
// Impresión
import ImpresionIndexPage from '../pages/Impresion/ImpresionIndexPage';
import GestionarPlantasPage from '../pages/Impresion/GestionarPlantasPage';
import GestionarTiposBobinaPage from '../pages/Impresion/GestionarTiposBobinaPage';
import GestionarEstadosBobinaPage from '../pages/Impresion/GestionarEstadosBobinaPage';
import GestionarStockBobinasPage from '../pages/Impresion/GestionarStockBobinasPage';
import GestionarTiradasPage from '../pages/Impresion/GestionarTiradasPage';
// Contables
import ContablesIndexPage from '../pages/Contables/ContablesIndexPage';
@@ -97,15 +103,22 @@ const AppRoutes = () => {
<Route index element={<Navigate to="es-canillas" replace />} />
<Route path="es-canillas" element={<ESCanillasPage />} />
<Route path="control-devoluciones" element={<ControlDevolucionesPage />} />
<Route path="es-distribuidores" element={<ESDistribuidoresPage />} />
<Route path="salidas-otros-destinos" element={<SalidasOtrosDestinosPage />} />
<Route path="es-distribuidores" element={<GestionarEntradasSalidasDistPage />} />
<Route path="salidas-otros-destinos" element={<GestionarSalidasOtrosDestinosPage />} />
<Route path="canillas" element={<GestionarCanillitasPage />} />
<Route path="distribuidores" element={<GestionarDistribuidoresPage />} />
<Route index element={<GestionarPublicacionesPage />} />
<Route path=":idPublicacion/precios" element={<GestionarPreciosPublicacionPage />} />
<Route path="otros-destinos" element={<GestionarOtrosDestinosPage />} />
<Route path="zonas" element={<GestionarZonasPage />} />
<Route path="empresas" element={<GestionarEmpresasPage />} />
<Route path="empresas" element={<GestionarEmpresasPage />} />
{/* Rutas para Publicaciones y sus detalles */}
<Route path="publicaciones" element={<Outlet />}> {/* Contenedor para sub-rutas de publicaciones */}
<Route index element={<GestionarPublicacionesPage />} /> {/* Lista de publicaciones */}
<Route path=":idPublicacion/precios" element={<GestionarPreciosPublicacionPage />} />
<Route path=":idPublicacion/recargos" element={<GestionarRecargosPublicacionPage />} />
<Route path=":idPublicacion/porcentajes-pago-dist" element={<GestionarPorcentajesPagoPage />} />
<Route path=":idPublicacion/porcentajes-mon-canilla" element={<GestionarPorcMonCanillaPage />} />
<Route path=":idPublicacion/secciones" element={<GestionarSeccionesPublicacionPage />} />
</Route>
</Route>
{/* Módulo Contable (anidado) */}
@@ -121,10 +134,11 @@ const AppRoutes = () => {
<Route path="plantas" element={<GestionarPlantasPage />} />
<Route path="tipos-bobina" element={<GestionarTiposBobinaPage />} />
<Route path="estados-bobina" element={<GestionarEstadosBobinaPage />} />
<Route path="stock-bobinas" element={<GestionarStockBobinasPage />} />
<Route path="tiradas" element={<GestionarTiradasPage />} />
</Route>
{/* Otros Módulos Principales (estos son "finales", no tienen más hijos) */}
<Route path="impresion" element={<PlaceholderPage moduleName="Impresión" />} />
<Route path="reportes" element={<PlaceholderPage moduleName="Reportes" />} />
<Route path="radios" element={<PlaceholderPage moduleName="Radios" />} />

View File

@@ -0,0 +1,52 @@
import apiClient from '../apiClient';
import type { EntradaSalidaDistDto } from '../../models/dtos/Distribucion/EntradaSalidaDistDto';
import type { CreateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaDistDto';
import type { UpdateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto';
interface GetAllESDistParams {
fechaDesde?: string | null; // yyyy-MM-dd
fechaHasta?: string | null; // yyyy-MM-dd
idPublicacion?: number | null;
idDistribuidor?: number | null;
tipoMovimiento?: 'Salida' | 'Entrada' | '' | null;
}
const getAllEntradasSalidasDist = async (filters: GetAllESDistParams): Promise<EntradaSalidaDistDto[]> => {
const params: Record<string, string | number> = {};
if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde;
if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta;
if (filters.idPublicacion) params.idPublicacion = filters.idPublicacion;
if (filters.idDistribuidor) params.idDistribuidor = filters.idDistribuidor;
if (filters.tipoMovimiento) params.tipoMovimiento = filters.tipoMovimiento;
const response = await apiClient.get<EntradaSalidaDistDto[]>('/entradassalidasdist', { params });
return response.data;
};
const getEntradaSalidaDistById = async (idParte: number): Promise<EntradaSalidaDistDto> => {
const response = await apiClient.get<EntradaSalidaDistDto>(`/entradassalidasdist/${idParte}`);
return response.data;
};
const createEntradaSalidaDist = async (data: CreateEntradaSalidaDistDto): Promise<EntradaSalidaDistDto> => {
const response = await apiClient.post<EntradaSalidaDistDto>('/entradassalidasdist', data);
return response.data;
};
const updateEntradaSalidaDist = async (idParte: number, data: UpdateEntradaSalidaDistDto): Promise<void> => {
await apiClient.put(`/entradassalidasdist/${idParte}`, data);
};
const deleteEntradaSalidaDist = async (idParte: number): Promise<void> => {
await apiClient.delete(`/entradassalidasdist/${idParte}`);
};
const entradaSalidaDistService = {
getAllEntradasSalidasDist,
getEntradaSalidaDistById,
createEntradaSalidaDist,
updateEntradaSalidaDist,
deleteEntradaSalidaDist,
};
export default entradaSalidaDistService;

View File

@@ -0,0 +1,37 @@
import apiClient from '../apiClient';
import type { PorcMonCanillaDto } from '../../models/dtos/Distribucion/PorcMonCanillaDto';
import type { CreatePorcMonCanillaDto } from '../../models/dtos/Distribucion/CreatePorcMonCanillaDto';
import type { UpdatePorcMonCanillaDto } from '../../models/dtos/Distribucion/UpdatePorcMonCanillaDto';
const getPorcMonCanillaPorPublicacion = async (idPublicacion: number): Promise<PorcMonCanillaDto[]> => {
const response = await apiClient.get<PorcMonCanillaDto[]>(`/publicaciones/${idPublicacion}/porcentajesmoncanilla`);
return response.data;
};
const getPorcMonCanillaById = async (idPublicacion: number, idPorcMon: number): Promise<PorcMonCanillaDto> => {
const response = await apiClient.get<PorcMonCanillaDto>(`/publicaciones/${idPublicacion}/porcentajesmoncanilla/${idPorcMon}`);
return response.data;
};
const createPorcMonCanilla = async (idPublicacion: number, data: CreatePorcMonCanillaDto): Promise<PorcMonCanillaDto> => {
const response = await apiClient.post<PorcMonCanillaDto>(`/publicaciones/${idPublicacion}/porcentajesmoncanilla`, data);
return response.data;
};
const updatePorcMonCanilla = async (idPublicacion: number, idPorcMon: number, data: UpdatePorcMonCanillaDto): Promise<void> => {
await apiClient.put(`/publicaciones/${idPublicacion}/porcentajesmoncanilla/${idPorcMon}`, data);
};
const deletePorcMonCanilla = async (idPublicacion: number, idPorcMon: number): Promise<void> => {
await apiClient.delete(`/publicaciones/${idPublicacion}/porcentajesmoncanilla/${idPorcMon}`);
};
const porcMonCanillaService = {
getPorcMonCanillaPorPublicacion,
getPorcMonCanillaById,
createPorcMonCanilla,
updatePorcMonCanilla,
deletePorcMonCanilla,
};
export default porcMonCanillaService;

View File

@@ -0,0 +1,39 @@
import apiClient from '../apiClient';
import type { PorcPagoDto } from '../../models/dtos/Distribucion/PorcPagoDto';
import type { CreatePorcPagoDto } from '../../models/dtos/Distribucion/CreatePorcPagoDto';
import type { UpdatePorcPagoDto } from '../../models/dtos/Distribucion/UpdatePorcPagoDto';
const getPorcentajesPorPublicacion = async (idPublicacion: number): Promise<PorcPagoDto[]> => {
const response = await apiClient.get<PorcPagoDto[]>(`/publicaciones/${idPublicacion}/porcentajespago`);
return response.data;
};
// getPorcPagoById no es estrictamente necesario para el CRUD dentro de la página de una publicación,
// pero podría ser útil para una edición muy específica o si se accede directamente.
const getPorcPagoById = async (idPublicacion: number, idPorcentaje: number): Promise<PorcPagoDto> => {
const response = await apiClient.get<PorcPagoDto>(`/publicaciones/${idPublicacion}/porcentajespago/${idPorcentaje}`);
return response.data;
};
const createPorcPago = async (idPublicacion: number, data: CreatePorcPagoDto): Promise<PorcPagoDto> => {
const response = await apiClient.post<PorcPagoDto>(`/publicaciones/${idPublicacion}/porcentajespago`, data);
return response.data;
};
const updatePorcPago = async (idPublicacion: number, idPorcentaje: number, data: UpdatePorcPagoDto): Promise<void> => {
await apiClient.put(`/publicaciones/${idPublicacion}/porcentajespago/${idPorcentaje}`, data);
};
const deletePorcPago = async (idPublicacion: number, idPorcentaje: number): Promise<void> => {
await apiClient.delete(`/publicaciones/${idPublicacion}/porcentajespago/${idPorcentaje}`);
};
const porcPagoService = {
getPorcentajesPorPublicacion,
getPorcPagoById,
createPorcPago,
updatePorcPago,
deletePorcPago,
};
export default porcPagoService;

View File

@@ -0,0 +1,41 @@
import apiClient from '../apiClient';
import type { PubliSeccionDto } from '../../models/dtos/Distribucion/PubliSeccionDto';
import type { CreatePubliSeccionDto } from '../../models/dtos/Distribucion/CreatePubliSeccionDto';
import type { UpdatePubliSeccionDto } from '../../models/dtos/Distribucion/UpdatePubliSeccionDto';
const getSeccionesPorPublicacion = async (idPublicacion: number, soloActivas?: boolean): Promise<PubliSeccionDto[]> => {
const params: Record<string, boolean> = {};
if (soloActivas !== undefined) {
params.soloActivas = soloActivas;
}
const response = await apiClient.get<PubliSeccionDto[]>(`/publicaciones/${idPublicacion}/secciones`, { params });
return response.data;
};
const getPubliSeccionById = async (idPublicacion: number, idSeccion: number): Promise<PubliSeccionDto> => {
const response = await apiClient.get<PubliSeccionDto>(`/publicaciones/${idPublicacion}/secciones/${idSeccion}`);
return response.data;
};
const createPubliSeccion = async (idPublicacion: number, data: CreatePubliSeccionDto): Promise<PubliSeccionDto> => {
const response = await apiClient.post<PubliSeccionDto>(`/publicaciones/${idPublicacion}/secciones`, data);
return response.data;
};
const updatePubliSeccion = async (idPublicacion: number, idSeccion: number, data: UpdatePubliSeccionDto): Promise<void> => {
await apiClient.put(`/publicaciones/${idPublicacion}/secciones/${idSeccion}`, data);
};
const deletePubliSeccion = async (idPublicacion: number, idSeccion: number): Promise<void> => {
await apiClient.delete(`/publicaciones/${idPublicacion}/secciones/${idSeccion}`);
};
const publiSeccionService = {
getSeccionesPorPublicacion,
getPubliSeccionById,
createPubliSeccion,
updatePubliSeccion,
deletePubliSeccion,
};
export default publiSeccionService;

View File

@@ -0,0 +1,38 @@
// src/services/recargoZonaService.ts
import apiClient from '../apiClient';
import type { RecargoZonaDto } from '../../models/dtos/Distribucion/RecargoZonaDto';
import type { CreateRecargoZonaDto } from '../../models/dtos/Distribucion/CreateRecargoZonaDto';
import type { UpdateRecargoZonaDto } from '../../models/dtos/Distribucion/UpdateRecargoZonaDto';
const getRecargosPorPublicacion = async (idPublicacion: number): Promise<RecargoZonaDto[]> => {
const response = await apiClient.get<RecargoZonaDto[]>(`/publicaciones/${idPublicacion}/recargos`);
return response.data;
};
const getRecargoZonaById = async (idPublicacion: number, idRecargo: number): Promise<RecargoZonaDto> => {
const response = await apiClient.get<RecargoZonaDto>(`/publicaciones/${idPublicacion}/recargos/${idRecargo}`);
return response.data;
};
const createRecargoZona = async (idPublicacion: number, data: CreateRecargoZonaDto): Promise<RecargoZonaDto> => {
const response = await apiClient.post<RecargoZonaDto>(`/publicaciones/${idPublicacion}/recargos`, data);
return response.data;
};
const updateRecargoZona = async (idPublicacion: number, idRecargo: number, data: UpdateRecargoZonaDto): Promise<void> => {
await apiClient.put(`/publicaciones/${idPublicacion}/recargos/${idRecargo}`, data);
};
const deleteRecargoZona = async (idPublicacion: number, idRecargo: number): Promise<void> => {
await apiClient.delete(`/publicaciones/${idPublicacion}/recargos/${idRecargo}`);
};
const recargoZonaService = {
getRecargosPorPublicacion,
getRecargoZonaById,
createRecargoZona,
updateRecargoZona,
deleteRecargoZona,
};
export default recargoZonaService;

View File

@@ -0,0 +1,50 @@
import apiClient from '../apiClient';
import type { SalidaOtroDestinoDto } from '../../models/dtos/Distribucion/SalidaOtroDestinoDto';
import type { CreateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto';
import type { UpdateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto';
interface GetAllSalidasParams {
fechaDesde?: string | null; // yyyy-MM-dd
fechaHasta?: string | null; // yyyy-MM-dd
idPublicacion?: number | null;
idDestino?: number | null;
}
const getAllSalidasOtrosDestinos = async (filters: GetAllSalidasParams): Promise<SalidaOtroDestinoDto[]> => {
const params: Record<string, string | number> = {};
if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde;
if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta;
if (filters.idPublicacion) params.idPublicacion = filters.idPublicacion;
if (filters.idDestino) params.idDestino = filters.idDestino;
const response = await apiClient.get<SalidaOtroDestinoDto[]>('/salidasotrosdestinos', { params });
return response.data;
};
const getSalidaOtroDestinoById = async (idParte: number): Promise<SalidaOtroDestinoDto> => {
const response = await apiClient.get<SalidaOtroDestinoDto>(`/salidasotrosdestinos/${idParte}`);
return response.data;
};
const createSalidaOtroDestino = async (data: CreateSalidaOtroDestinoDto): Promise<SalidaOtroDestinoDto> => {
const response = await apiClient.post<SalidaOtroDestinoDto>('/salidasotrosdestinos', data);
return response.data;
};
const updateSalidaOtroDestino = async (idParte: number, data: UpdateSalidaOtroDestinoDto): Promise<void> => {
await apiClient.put(`/salidasotrosdestinos/${idParte}`, data);
};
const deleteSalidaOtroDestino = async (idParte: number): Promise<void> => {
await apiClient.delete(`/salidasotrosdestinos/${idParte}`);
};
const salidaOtroDestinoService = {
getAllSalidasOtrosDestinos,
getSalidaOtroDestinoById,
createSalidaOtroDestino,
updateSalidaOtroDestino,
deleteSalidaOtroDestino,
};
export default salidaOtroDestinoService;

View File

@@ -0,0 +1,62 @@
import apiClient from '../apiClient';
import type { StockBobinaDto } from '../../models/dtos/Impresion/StockBobinaDto';
import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateStockBobinaDto';
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto';
interface GetAllStockBobinasParams {
idTipoBobina?: number | null;
nroBobinaFilter?: string | null;
idPlanta?: number | null;
idEstadoBobina?: number | null;
remitoFilter?: string | null;
fechaDesde?: string | null; // "yyyy-MM-dd"
fechaHasta?: string | null; // "yyyy-MM-dd"
}
const getAllStockBobinas = async (filters: GetAllStockBobinasParams): Promise<StockBobinaDto[]> => {
const params: Record<string, string | number | boolean> = {};
if (filters.idTipoBobina) params.idTipoBobina = filters.idTipoBobina;
if (filters.nroBobinaFilter) params.nroBobina = filters.nroBobinaFilter; // El backend espera nroBobina
if (filters.idPlanta) params.idPlanta = filters.idPlanta;
if (filters.idEstadoBobina) params.idEstadoBobina = filters.idEstadoBobina;
if (filters.remitoFilter) params.remito = filters.remitoFilter; // El backend espera remito
if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde;
if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta;
const response = await apiClient.get<StockBobinaDto[]>('/stockbobinas', { params });
return response.data;
};
const getStockBobinaById = async (idBobina: number): Promise<StockBobinaDto> => {
const response = await apiClient.get<StockBobinaDto>(`/stockbobinas/${idBobina}`);
return response.data;
};
const ingresarBobina = async (data: CreateStockBobinaDto): Promise<StockBobinaDto> => {
const response = await apiClient.post<StockBobinaDto>('/stockbobinas', data);
return response.data;
};
const updateDatosBobinaDisponible = async (idBobina: number, data: UpdateStockBobinaDto): Promise<void> => {
await apiClient.put(`/stockbobinas/${idBobina}/datos`, data);
};
const cambiarEstadoBobina = async (idBobina: number, data: CambiarEstadoBobinaDto): Promise<void> => {
await apiClient.put(`/stockbobinas/${idBobina}/cambiar-estado`, data);
};
const deleteIngresoBobina = async (idBobina: number): Promise<void> => {
await apiClient.delete(`/stockbobinas/${idBobina}`);
};
const stockBobinaService = {
getAllStockBobinas,
getStockBobinaById,
ingresarBobina,
updateDatosBobinaDisponible,
cambiarEstadoBobina,
deleteIngresoBobina,
};
export default stockBobinaService;

View File

@@ -0,0 +1,43 @@
import apiClient from '../apiClient';
import type { TiradaDto } from '../../models/dtos/Impresion/TiradaDto';
import type { CreateTiradaRequestDto } from '../../models/dtos/Impresion/CreateTiradaRequestDto';
interface GetTiradasParams {
fecha?: string | null; // "yyyy-MM-dd"
idPublicacion?: number | null;
idPlanta?: number | null;
}
const getTiradas = async (filters: GetTiradasParams): Promise<TiradaDto[]> => {
const params: Record<string, string | number> = {};
if (filters.fecha) params.fecha = filters.fecha;
if (filters.idPublicacion) params.idPublicacion = filters.idPublicacion;
if (filters.idPlanta) params.idPlanta = filters.idPlanta;
const response = await apiClient.get<TiradaDto[]>('/tiradas', { params });
return response.data;
};
const registrarTirada = async (data: CreateTiradaRequestDto): Promise<TiradaDto> => {
const response = await apiClient.post<TiradaDto>('/tiradas', data);
return response.data; // El backend devuelve la tirada creada
};
const deleteTiradaCompleta = async (fecha: string, idPublicacion: number, idPlanta: number): Promise<void> => {
// Los parámetros van en la query string para este DELETE
await apiClient.delete('/tiradas', {
params: {
fecha,
idPublicacion,
idPlanta
}
});
};
const tiradaService = {
getTiradas,
registrarTirada,
deleteTiradaCompleta,
};
export default tiradaService;