feat: Implementación CRUD Canillitas, Distribuidores y Precios de Publicación
Backend API:
- Canillitas (`dist_dtCanillas`):
- Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Lógica para manejo de `Accionista`, `Baja`, `FechaBaja`.
- Auditoría en `dist_dtCanillas_H`.
- Validación de legajo único y lógica de empresa vs accionista.
- Distribuidores (`dist_dtDistribuidores`):
- Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Auditoría en `dist_dtDistribuidores_H`.
- Creación de saldos iniciales para el nuevo distribuidor en todas las empresas.
- Verificación de NroDoc único y Nombre opcionalmente único.
- Precios de Publicación (`dist_Precios`):
- Implementado CRUD básico (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Endpoints anidados bajo `/publicaciones/{idPublicacion}/precios`.
- Lógica de negocio para cerrar período de precio anterior al crear uno nuevo.
- Lógica de negocio para reabrir período de precio anterior al eliminar el último.
- Auditoría en `dist_Precios_H`.
- Auditoría en Eliminación de Publicaciones:
- Extendido `PublicacionService.EliminarAsync` para eliminar en cascada registros de precios, recargos, porcentajes de pago (distribuidores y canillitas) y secciones de publicación.
- Repositorios correspondientes (`PrecioRepository`, `RecargoZonaRepository`, `PorcPagoRepository`, `PorcMonCanillaRepository`, `PubliSeccionRepository`) actualizados con métodos `DeleteByPublicacionIdAsync` que registran en sus respectivas tablas `_H` (si existen y se implementó la lógica).
- Asegurada la correcta propagación del `idUsuario` para la auditoría en cascada.
- Correcciones de Nulabilidad:
- Ajustados los métodos `MapToDto` y su uso en `CanillaService` y `PublicacionService` para manejar correctamente tipos anulables.
Frontend React:
- Canillitas:
- `canillaService.ts`.
- `CanillaFormModal.tsx` con selectores para Zona y Empresa, y lógica de Accionista.
- `GestionarCanillitasPage.tsx` con filtros, paginación, y acciones (editar, toggle baja).
- Distribuidores:
- `distribuidorService.ts`.
- `DistribuidorFormModal.tsx` con múltiples campos y selector de Zona.
- `GestionarDistribuidoresPage.tsx` con filtros, paginación, y acciones (editar, eliminar).
- Precios de Publicación:
- `precioService.ts`.
- `PrecioFormModal.tsx` para crear/editar períodos de precios (VigenciaD, VigenciaH opcional, precios por día).
- `GestionarPreciosPublicacionPage.tsx` accesible desde la gestión de publicaciones, para listar y gestionar los períodos de precios de una publicación específica.
- Layout:
- Reemplazado el uso de `Grid` por `Box` con Flexbox en `CanillaFormModal`, `GestionarCanillitasPage` (filtros), `DistribuidorFormModal` y `PrecioFormModal` para resolver problemas de tipos y mejorar la consistencia del layout de formularios.
- Navegación:
- Actualizadas las rutas y pestañas para los nuevos módulos y sub-módulos.
2025-05-20 12:38:55 -03:00
import React , { useState , useEffect } from 'react' ;
import {
Modal , Box , Typography , TextField , Button , CircularProgress , Alert ,
FormControl , InputLabel , Select , MenuItem , FormControlLabel , Checkbox
} from '@mui/material' ;
import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto' ;
import type { CreateUsuarioRequestDto } from '../../../models/dtos/Usuarios/CreateUsuarioRequestDto' ;
import type { UpdateUsuarioRequestDto } from '../../../models/dtos/Usuarios/UpdateUsuarioRequestDto' ;
import type { PerfilDto } from '../../../models/dtos/Usuarios/PerfilDto' ; // Para el dropdown de perfiles
import perfilService from '../../../services/Usuarios/perfilService' ; // Para obtener la lista de perfiles
const modalStyle = {
position : 'absolute' as 'absolute' ,
top : '50%' ,
left : '50%' ,
transform : 'translate(-50%, -50%)' ,
width : { xs : '90%' , sm : 500 } , // Responsive width
bgcolor : 'background.paper' ,
border : '2px solid #000' ,
boxShadow : 24 ,
p : 4 ,
maxHeight : '90vh' , // Para permitir scroll si el contenido es mucho
overflowY : 'auto' // Habilitar scroll vertical
} ;
interface UsuarioFormModalProps {
open : boolean ;
onClose : ( ) = > void ;
onSubmit : ( data : CreateUsuarioRequestDto | UpdateUsuarioRequestDto , id? : number ) = > Promise < void > ;
initialData? : UsuarioDto | null ;
errorMessage? : string | null ;
clearErrorMessage : ( ) = > void ;
}
const UsuarioFormModal : React.FC < UsuarioFormModalProps > = ( {
open ,
onClose ,
onSubmit ,
initialData ,
errorMessage ,
clearErrorMessage
} ) = > {
const [ user , setUser ] = useState ( '' ) ;
const [ password , setPassword ] = useState ( '' ) ;
const [ confirmPassword , setConfirmPassword ] = useState ( '' ) ;
const [ nombre , setNombre ] = useState ( '' ) ;
const [ apellido , setApellido ] = useState ( '' ) ;
const [ idPerfil , setIdPerfil ] = useState < number | string > ( '' ) ; // Puede ser string vacío inicialmente
const [ habilitada , setHabilitada ] = useState ( true ) ;
const [ supAdmin , setSupAdmin ] = useState ( false ) ;
const [ debeCambiarClave , setDebeCambiarClave ] = useState ( true ) ;
const [ verLog , setVerLog ] = useState ( '1.0.0.0' ) ;
const [ perfiles , setPerfiles ] = useState < PerfilDto [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ loadingPerfiles , setLoadingPerfiles ] = useState ( false ) ;
const [ localErrors , setLocalErrors ] = useState < { [ key : string ] : string | null } > ( { } ) ;
const isEditing = Boolean ( initialData ) ;
useEffect ( ( ) = > {
const fetchPerfiles = async ( ) = > {
setLoadingPerfiles ( true ) ;
try {
const data = await perfilService . getAllPerfiles ( ) ;
setPerfiles ( data ) ;
} catch ( error ) {
console . error ( "Error al cargar perfiles" , error ) ;
setLocalErrors ( prev = > ( { . . . prev , perfiles : 'Error al cargar perfiles.' } ) ) ;
} finally {
setLoadingPerfiles ( false ) ;
}
} ;
if ( open ) {
fetchPerfiles ( ) ;
setUser ( initialData ? . user || '' ) ;
// No pre-rellenar contraseña en edición
setPassword ( '' ) ;
setConfirmPassword ( '' ) ;
setNombre ( initialData ? . nombre || '' ) ;
setApellido ( initialData ? . apellido || '' ) ;
setIdPerfil ( initialData ? . idPerfil || '' ) ;
setHabilitada ( initialData ? initialData.habilitada : true ) ;
setSupAdmin ( initialData ? initialData.supAdmin : false ) ;
setDebeCambiarClave ( initialData ? initialData . debeCambiarClave : ! isEditing ) ; // true para creación
setVerLog ( initialData ? . verLog || '1.0.0.0' ) ;
setLocalErrors ( { } ) ;
clearErrorMessage ( ) ;
}
} , [ open , initialData , clearErrorMessage , isEditing ] ) ;
const validate = ( ) : boolean = > {
const errors : { [ key : string ] : string | null } = { } ;
if ( ! user . trim ( ) ) errors . user = 'El nombre de usuario es obligatorio.' ;
else if ( user . length < 3 ) errors . user = 'El usuario debe tener al menos 3 caracteres.' ;
if ( ! isEditing || ( isEditing && password ) ) { // Validar contraseña solo si se está creando o si se ingresó algo en edición
if ( ! password ) errors . password = 'La contraseña es obligatoria.' ;
else if ( password . length < 6 ) errors . password = 'La contraseña debe tener al menos 6 caracteres.' ;
if ( password !== confirmPassword ) errors . confirmPassword = 'Las contraseñas no coinciden.' ;
if ( user . trim ( ) . toLowerCase ( ) === password . toLowerCase ( ) ) errors . password = 'La contraseña no puede ser igual al nombre de usuario.'
}
if ( ! nombre . trim ( ) ) errors . nombre = 'El nombre es obligatorio.' ;
if ( ! apellido . trim ( ) ) errors . apellido = 'El apellido es obligatorio.' ;
if ( ! idPerfil ) errors . idPerfil = 'Debe seleccionar un perfil.' ;
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 : UpdateUsuarioRequestDto = {
nombre , apellido , idPerfil : Number ( idPerfil ) , habilitada , supAdmin , debeCambiarClave , verLog
} ;
await onSubmit ( dataToSubmit , initialData . id ) ;
// Si se ingresó una nueva contraseña, llamar a un endpoint separado para cambiarla
if ( password ) {
// Esto requeriría un endpoint adicional en el backend para que un admin cambie la clave de otro user
// o adaptar el flujo de `AuthService.ChangePasswordAsync` si es posible,
// o un nuevo endpoint en UsuarioController para setear clave.
// Por ahora, lo dejamos así, la clave se cambia por el propio usuario o por un reset del admin.
console . warn ( "El cambio de contraseña en edición de usuario no está implementado en este modal directamente. Usar la opción de 'Resetear Contraseña'." ) ;
}
} else {
const dataToSubmit : CreateUsuarioRequestDto = {
user , password , nombre , apellido , idPerfil : Number ( idPerfil ) , habilitada , supAdmin , debeCambiarClave , verLog
} ;
await onSubmit ( dataToSubmit ) ;
}
onClose ( ) ;
} catch ( error : any ) {
console . error ( "Error en submit de UsuarioFormModal:" , error ) ;
} finally {
setLoading ( false ) ;
}
} ;
return (
< Modal open = { open } onClose = { onClose } >
< Box sx = { modalStyle } >
< Typography variant = "h6" component = "h2" gutterBottom >
{ isEditing ? 'Editar Usuario' : 'Agregar Nuevo Usuario' }
< / Typography >
< Box component = "form" onSubmit = { handleSubmit } sx = { { mt : 1 } } >
{ /* SECCIÓN DE CAMPOS CON BOX Y FLEXBOX */ }
< Box sx = { { display : 'flex' , flexDirection : 'column' , gap : 0.5 } } > { /* Contenedor principal de campos */ }
< Box sx = { { display : 'flex' , gap : 2 , flexWrap : 'wrap' } } > { /* Fila 1 */ }
< TextField label = "Nombre de Usuario" fullWidth required value = { user }
onChange = { ( e ) = > { setUser ( e . target . value ) ; handleInputChange ( 'user' ) ; } } margin = "dense"
error = { ! ! localErrors . user } helperText = { localErrors . user || '' }
disabled = { loading || isEditing } autoFocus = { ! isEditing }
sx = { { flex : 1 , minWidth : 'calc(50% - 8px)' } } // 8px es la mitad del gap
/ >
< FormControl fullWidth margin = "dense" error = { ! ! localErrors . idPerfil } sx = { { flex : 1 , minWidth : 'calc(50% - 8px)' } } >
< InputLabel id = "perfil-select-label" required > Perfil < / InputLabel >
< Select
labelId = "perfil-select-label"
label = "Perfil"
value = { idPerfil }
onChange = { ( e ) = > { setIdPerfil ( e . target . value as number ) ; handleInputChange ( 'idPerfil' ) ; } }
disabled = { loading || loadingPerfiles }
>
< MenuItem value = "" disabled > < em > Seleccione un perfil < / em > < / MenuItem >
{ perfiles . map ( ( p ) = > ( < MenuItem key = { p . id } value = { p . id } > { p . nombrePerfil } < / MenuItem > ) ) }
< / Select >
{ localErrors . idPerfil && < Typography color = "error" variant = "caption" > { localErrors . idPerfil } < / Typography > }
{ loadingPerfiles && < CircularProgress size = { 20 } sx = { { position : 'absolute' , right : 30 , top : '50%' , marginTop : '-10px' } } / > }
< / FormControl >
< / Box >
{ ! isEditing && (
< Box sx = { { display : 'flex' , gap : 2 , flexWrap : 'wrap' , mt : 1 } } > { /* Fila 2 (Contraseñas) */ }
< TextField label = "Contraseña" type = "password" fullWidth required = { ! isEditing } value = { password }
onChange = { ( e ) = > { setPassword ( e . target . value ) ; handleInputChange ( 'password' ) ; } } margin = "dense"
error = { ! ! localErrors . password } helperText = { localErrors . password || '' }
disabled = { loading }
sx = { { flex : 1 , minWidth : 'calc(50% - 8px)' } }
/ >
< TextField label = "Confirmar Contraseña" type = "password" fullWidth required = { ! isEditing } value = { confirmPassword }
onChange = { ( e ) = > { setConfirmPassword ( e . target . value ) ; handleInputChange ( 'confirmPassword' ) ; } } margin = "dense"
error = { ! ! localErrors . confirmPassword } helperText = { localErrors . confirmPassword || '' }
disabled = { loading }
sx = { { flex : 1 , minWidth : 'calc(50% - 8px)' } }
/ >
< / Box >
) }
< Box sx = { { display : 'flex' , gap : 2 , flexWrap : 'wrap' , mt : 1 } } > { /* Fila 3 (Nombre y Apellido) */ }
< TextField label = "Nombre" fullWidth required value = { nombre }
onChange = { ( e ) = > { setNombre ( e . target . value ) ; handleInputChange ( 'nombre' ) ; } } margin = "dense"
error = { ! ! localErrors . nombre } helperText = { localErrors . nombre || '' }
disabled = { loading }
sx = { { flex : 1 , minWidth : 'calc(50% - 8px)' } }
/ >
< TextField label = "Apellido" fullWidth required value = { apellido }
onChange = { ( e ) = > { setApellido ( e . target . value ) ; handleInputChange ( 'apellido' ) ; } } margin = "dense"
error = { ! ! localErrors . apellido } helperText = { localErrors . apellido || '' }
disabled = { loading }
sx = { { flex : 1 , minWidth : 'calc(50% - 8px)' } }
/ >
< / Box >
< Box sx = { { display : 'flex' , gap : 2 , flexWrap : 'wrap' , mt : 1 , alignItems : 'center' } } > { /* Fila 4 (VerLog y Habilitado) */ }
< TextField label = "Versión Log" fullWidth value = { verLog }
onChange = { ( e ) = > setVerLog ( e . target . value ) } margin = "dense"
disabled = { loading }
sx = { { flex : 1 , minWidth : 'calc(50% - 8px)' } }
/ >
< Box sx = { { flex : 1 , minWidth : 'calc(50% - 8px)' , display : 'flex' , justifyContent : 'flex-start' , pl :1 /* o pt si es vertical */ } } >
< FormControlLabel control = { < Checkbox checked = { habilitada } onChange = { ( e ) = > setHabilitada ( e . target . checked ) } disabled = { loading } / > } label = "Habilitado" / >
< / Box >
< / Box >
2025-06-30 15:26:14 -03:00
< Box sx = { { display : 'flex' , gap : 2 , flexWrap : 'wrap' , mt : 0.5 } } >
feat: Implementación CRUD Canillitas, Distribuidores y Precios de Publicación
Backend API:
- Canillitas (`dist_dtCanillas`):
- Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Lógica para manejo de `Accionista`, `Baja`, `FechaBaja`.
- Auditoría en `dist_dtCanillas_H`.
- Validación de legajo único y lógica de empresa vs accionista.
- Distribuidores (`dist_dtDistribuidores`):
- Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Auditoría en `dist_dtDistribuidores_H`.
- Creación de saldos iniciales para el nuevo distribuidor en todas las empresas.
- Verificación de NroDoc único y Nombre opcionalmente único.
- Precios de Publicación (`dist_Precios`):
- Implementado CRUD básico (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Endpoints anidados bajo `/publicaciones/{idPublicacion}/precios`.
- Lógica de negocio para cerrar período de precio anterior al crear uno nuevo.
- Lógica de negocio para reabrir período de precio anterior al eliminar el último.
- Auditoría en `dist_Precios_H`.
- Auditoría en Eliminación de Publicaciones:
- Extendido `PublicacionService.EliminarAsync` para eliminar en cascada registros de precios, recargos, porcentajes de pago (distribuidores y canillitas) y secciones de publicación.
- Repositorios correspondientes (`PrecioRepository`, `RecargoZonaRepository`, `PorcPagoRepository`, `PorcMonCanillaRepository`, `PubliSeccionRepository`) actualizados con métodos `DeleteByPublicacionIdAsync` que registran en sus respectivas tablas `_H` (si existen y se implementó la lógica).
- Asegurada la correcta propagación del `idUsuario` para la auditoría en cascada.
- Correcciones de Nulabilidad:
- Ajustados los métodos `MapToDto` y su uso en `CanillaService` y `PublicacionService` para manejar correctamente tipos anulables.
Frontend React:
- Canillitas:
- `canillaService.ts`.
- `CanillaFormModal.tsx` con selectores para Zona y Empresa, y lógica de Accionista.
- `GestionarCanillitasPage.tsx` con filtros, paginación, y acciones (editar, toggle baja).
- Distribuidores:
- `distribuidorService.ts`.
- `DistribuidorFormModal.tsx` con múltiples campos y selector de Zona.
- `GestionarDistribuidoresPage.tsx` con filtros, paginación, y acciones (editar, eliminar).
- Precios de Publicación:
- `precioService.ts`.
- `PrecioFormModal.tsx` para crear/editar períodos de precios (VigenciaD, VigenciaH opcional, precios por día).
- `GestionarPreciosPublicacionPage.tsx` accesible desde la gestión de publicaciones, para listar y gestionar los períodos de precios de una publicación específica.
- Layout:
- Reemplazado el uso de `Grid` por `Box` con Flexbox en `CanillaFormModal`, `GestionarCanillitasPage` (filtros), `DistribuidorFormModal` y `PrecioFormModal` para resolver problemas de tipos y mejorar la consistencia del layout de formularios.
- Navegación:
- Actualizadas las rutas y pestañas para los nuevos módulos y sub-módulos.
2025-05-20 12:38:55 -03:00
< Box sx = { { flex : 1 , minWidth : 'calc(50% - 8px)' } } >
< FormControlLabel control = { < Checkbox checked = { debeCambiarClave } onChange = { ( e ) = > setDebeCambiarClave ( e . target . checked ) } disabled = { loading } / > } label = "Debe Cambiar Clave" / >
< / Box >
< / Box >
2025-06-30 15:26:14 -03:00
< / Box >
feat: Implementación CRUD Canillitas, Distribuidores y Precios de Publicación
Backend API:
- Canillitas (`dist_dtCanillas`):
- Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Lógica para manejo de `Accionista`, `Baja`, `FechaBaja`.
- Auditoría en `dist_dtCanillas_H`.
- Validación de legajo único y lógica de empresa vs accionista.
- Distribuidores (`dist_dtDistribuidores`):
- Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Auditoría en `dist_dtDistribuidores_H`.
- Creación de saldos iniciales para el nuevo distribuidor en todas las empresas.
- Verificación de NroDoc único y Nombre opcionalmente único.
- Precios de Publicación (`dist_Precios`):
- Implementado CRUD básico (Modelos, DTOs, Repositorio, Servicio, Controlador).
- Endpoints anidados bajo `/publicaciones/{idPublicacion}/precios`.
- Lógica de negocio para cerrar período de precio anterior al crear uno nuevo.
- Lógica de negocio para reabrir período de precio anterior al eliminar el último.
- Auditoría en `dist_Precios_H`.
- Auditoría en Eliminación de Publicaciones:
- Extendido `PublicacionService.EliminarAsync` para eliminar en cascada registros de precios, recargos, porcentajes de pago (distribuidores y canillitas) y secciones de publicación.
- Repositorios correspondientes (`PrecioRepository`, `RecargoZonaRepository`, `PorcPagoRepository`, `PorcMonCanillaRepository`, `PubliSeccionRepository`) actualizados con métodos `DeleteByPublicacionIdAsync` que registran en sus respectivas tablas `_H` (si existen y se implementó la lógica).
- Asegurada la correcta propagación del `idUsuario` para la auditoría en cascada.
- Correcciones de Nulabilidad:
- Ajustados los métodos `MapToDto` y su uso en `CanillaService` y `PublicacionService` para manejar correctamente tipos anulables.
Frontend React:
- Canillitas:
- `canillaService.ts`.
- `CanillaFormModal.tsx` con selectores para Zona y Empresa, y lógica de Accionista.
- `GestionarCanillitasPage.tsx` con filtros, paginación, y acciones (editar, toggle baja).
- Distribuidores:
- `distribuidorService.ts`.
- `DistribuidorFormModal.tsx` con múltiples campos y selector de Zona.
- `GestionarDistribuidoresPage.tsx` con filtros, paginación, y acciones (editar, eliminar).
- Precios de Publicación:
- `precioService.ts`.
- `PrecioFormModal.tsx` para crear/editar períodos de precios (VigenciaD, VigenciaH opcional, precios por día).
- `GestionarPreciosPublicacionPage.tsx` accesible desde la gestión de publicaciones, para listar y gestionar los períodos de precios de una publicación específica.
- Layout:
- Reemplazado el uso de `Grid` por `Box` con Flexbox en `CanillaFormModal`, `GestionarCanillitasPage` (filtros), `DistribuidorFormModal` y `PrecioFormModal` para resolver problemas de tipos y mejorar la consistencia del layout de formularios.
- Navegación:
- Actualizadas las rutas y pestañas para los nuevos módulos y sub-módulos.
2025-05-20 12:38:55 -03:00
{ 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' : 'Crear Usuario' ) }
< / Button >
< / Box >
< / Box >
< / Box >
< / Modal >
) ;
} ;
export default UsuarioFormModal ;