Finalización de Reportes y arreglos varios de controles y comportamientos...
This commit is contained in:
		| @@ -83,7 +83,10 @@ namespace GestionIntegral.Api.Controllers.Distribucion | |||||||
|         public async Task<IActionResult> UpdateMovimiento(int idParte, [FromBody] UpdateEntradaSalidaCanillaDto updateDto) |         public async Task<IActionResult> UpdateMovimiento(int idParte, [FromBody] UpdateEntradaSalidaCanillaDto updateDto) | ||||||
|         { |         { | ||||||
|             if (!TienePermiso(PermisoModificarMovimiento)) return Forbid(); |             if (!TienePermiso(PermisoModificarMovimiento)) return Forbid(); | ||||||
|  |              | ||||||
|  |             // Esta línea es la que dispara la validación del modelo 'updateDto' | ||||||
|             if (!ModelState.IsValid) return BadRequest(ModelState);  |             if (!ModelState.IsValid) return BadRequest(ModelState);  | ||||||
|  |              | ||||||
|             var userId = GetCurrentUserId(); |             var userId = GetCurrentUserId(); | ||||||
|             if (userId == null) return Unauthorized(); |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
| @@ -91,7 +94,7 @@ namespace GestionIntegral.Api.Controllers.Distribucion | |||||||
|             if (!exito) |             if (!exito) | ||||||
|             { |             { | ||||||
|                 if (error == "Movimiento no encontrado." || error == "No se puede modificar un movimiento ya liquidado.") |                 if (error == "Movimiento no encontrado." || error == "No se puede modificar un movimiento ya liquidado.") | ||||||
|                     return NotFound(new { message = error }); // Podría ser 404 o 400 dependiendo del error |                     return NotFound(new { message = error });  | ||||||
|                 return BadRequest(new { message = error }); |                 return BadRequest(new { message = error }); | ||||||
|             } |             } | ||||||
|             return NoContent(); |             return NoContent(); | ||||||
|   | |||||||
| @@ -117,5 +117,65 @@ namespace GestionIntegral.Api.Controllers.Distribucion | |||||||
|             } |             } | ||||||
|             return NoContent(); |             return NoContent(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // Endpoint para obtener las configuraciones de días para una publicación | ||||||
|  |         [HttpGet("{idPublicacion:int}/dias-semana")] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<PublicacionDiaSemanaDto>), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |         public async Task<IActionResult> GetConfiguracionDiasPublicacion(int idPublicacion) | ||||||
|  |         { | ||||||
|  |             // Podrías usar el mismo permiso de ver publicaciones o uno específico | ||||||
|  |             if (!TienePermiso(PermisoVer)) return Forbid(); | ||||||
|  |  | ||||||
|  |             var publicacion = await _publicacionService.ObtenerPorIdAsync(idPublicacion); | ||||||
|  |             if (publicacion == null) return NotFound(new { message = "Publicación no encontrada." }); | ||||||
|  |  | ||||||
|  |             var configs = await _publicacionService.ObtenerConfiguracionDiasAsync(idPublicacion); | ||||||
|  |             return Ok(configs); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Endpoint para actualizar las configuraciones de días para una publicación | ||||||
|  |         [HttpPut("{idPublicacion:int}/dias-semana")] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |         public async Task<IActionResult> UpdateConfiguracionDiasPublicacion(int idPublicacion, [FromBody] UpdatePublicacionDiasSemanaRequestDto requestDto) | ||||||
|  |         { | ||||||
|  |             // Podrías usar el mismo permiso de modificar publicaciones o uno específico | ||||||
|  |             if (!TienePermiso(PermisoModificar)) return Forbid(); | ||||||
|  |             if (!ModelState.IsValid) return BadRequest(ModelState); | ||||||
|  |  | ||||||
|  |             var userId = GetCurrentUserId(); | ||||||
|  |             if (userId == null) return Unauthorized(); | ||||||
|  |  | ||||||
|  |             var (exito, error) = await _publicacionService.ActualizarConfiguracionDiasAsync(idPublicacion, requestDto, userId.Value); | ||||||
|  |             if (!exito) | ||||||
|  |             { | ||||||
|  |                 if (error == "Publicación no encontrada.") return NotFound(new { message = error }); | ||||||
|  |                 return BadRequest(new { message = error }); | ||||||
|  |             } | ||||||
|  |             return NoContent(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Endpoint para obtener publicaciones por día de la semana (para el modal de canillitas) | ||||||
|  |         [HttpGet("por-dia-semana")] | ||||||
|  |         [ProducesResponseType(typeof(IEnumerable<PublicacionDto>), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         public async Task<IActionResult> GetPublicacionesPorDia([FromQuery] byte dia) // dia: 0=Domingo, 1=Lunes... | ||||||
|  |         { | ||||||
|  |             // Generalmente, este endpoint no necesitaría un permiso estricto si solo devuelve datos públicos | ||||||
|  |             // pero puedes añadirlo si es necesario. | ||||||
|  |             // if (!TienePermiso(PermisoVer)) return Forbid();  | ||||||
|  |  | ||||||
|  |             if (dia > 6) // byte no puede ser negativo | ||||||
|  |             { | ||||||
|  |                 return BadRequest(new { message = "El día de la semana debe estar entre 0 (Domingo) y 6 (Sábado)." }); | ||||||
|  |             } | ||||||
|  |             var publicaciones = await _publicacionService.ObtenerPublicacionesPorDiaSemanaAsync(dia); | ||||||
|  |             return Ok(publicaciones); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,992 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <Report xmlns="http://schemas.microsoft.com/sqlserver/reporting/2016/01/reportdefinition" xmlns:rd="http://schemas.microsoft.com/SQLServer/reporting/reportdesigner"> | ||||||
|  |   <AutoRefresh>0</AutoRefresh> | ||||||
|  |   <DataSources> | ||||||
|  |     <DataSource Name="DSLiquidacionCanillas"> | ||||||
|  |       <ConnectionProperties> | ||||||
|  |         <DataProvider>System.Data.DataSet</DataProvider> | ||||||
|  |         <ConnectString>/* Local Connection */</ConnectString> | ||||||
|  |       </ConnectionProperties> | ||||||
|  |       <rd:DataSourceID>ee328c50-edc3-4726-92dc-19ec72ac5a56</rd:DataSourceID> | ||||||
|  |     </DataSource> | ||||||
|  |   </DataSources> | ||||||
|  |   <DataSets> | ||||||
|  |     <DataSet Name="DSLiquidacionCanillas"> | ||||||
|  |       <Query> | ||||||
|  |         <DataSourceName>DSLiquidacionCanillas</DataSourceName> | ||||||
|  |         <CommandText>/* Local Query */</CommandText> | ||||||
|  |       </Query> | ||||||
|  |       <Fields> | ||||||
|  |         <Field Name="Publicacion"> | ||||||
|  |           <DataField>Publicacion</DataField> | ||||||
|  |           <rd:TypeName>System.String</rd:TypeName> | ||||||
|  |         </Field> | ||||||
|  |         <Field Name="Canilla"> | ||||||
|  |           <DataField>Canilla</DataField> | ||||||
|  |           <rd:TypeName>System.String</rd:TypeName> | ||||||
|  |         </Field> | ||||||
|  |         <Field Name="TotalCantSalida"> | ||||||
|  |           <DataField>TotalCantSalida</DataField> | ||||||
|  |           <rd:TypeName>System.Int32</rd:TypeName> | ||||||
|  |         </Field> | ||||||
|  |         <Field Name="TotalCantEntrada"> | ||||||
|  |           <DataField>TotalCantEntrada</DataField> | ||||||
|  |           <rd:TypeName>System.Int32</rd:TypeName> | ||||||
|  |         </Field> | ||||||
|  |         <Field Name="TotalRendir"> | ||||||
|  |           <DataField>TotalRendir</DataField> | ||||||
|  |           <rd:TypeName>System.Decimal</rd:TypeName> | ||||||
|  |         </Field> | ||||||
|  |         <Field Name="PrecioEjemplar"> | ||||||
|  |           <DataField>PrecioEjemplar</DataField> | ||||||
|  |           <rd:TypeName>System.Decimal</rd:TypeName> | ||||||
|  |         </Field> | ||||||
|  |       </Fields> | ||||||
|  |       <rd:DataSetInfo> | ||||||
|  |         <rd:DataSetName>DSLiquidacionCanillas</rd:DataSetName> | ||||||
|  |         <rd:SchemaPath>C:\Users\dmolinari\source\repos\Cobol-VBNet\Reportes\DSLiquidacionCanillas.xsd</rd:SchemaPath> | ||||||
|  |         <rd:TableName>SP_DistCanillasLiquidacion</rd:TableName> | ||||||
|  |         <rd:TableAdapterFillMethod>Fill</rd:TableAdapterFillMethod> | ||||||
|  |         <rd:TableAdapterGetDataMethod>GetData</rd:TableAdapterGetDataMethod> | ||||||
|  |         <rd:TableAdapterName>SP_DistCanillasLiquidacionTableAdapter</rd:TableAdapterName> | ||||||
|  |       </rd:DataSetInfo> | ||||||
|  |     </DataSet> | ||||||
|  |   </DataSets> | ||||||
|  |   <ReportSections> | ||||||
|  |     <ReportSection> | ||||||
|  |       <Body> | ||||||
|  |         <ReportItems> | ||||||
|  |           <Tablix Name="Tablix1"> | ||||||
|  |             <TablixBody> | ||||||
|  |               <TablixColumns> | ||||||
|  |                 <TablixColumn> | ||||||
|  |                   <Width>3.53124cm</Width> | ||||||
|  |                 </TablixColumn> | ||||||
|  |                 <TablixColumn> | ||||||
|  |                   <Width>3.10854cm</Width> | ||||||
|  |                 </TablixColumn> | ||||||
|  |                 <TablixColumn> | ||||||
|  |                   <Width>3.55834cm</Width> | ||||||
|  |                 </TablixColumn> | ||||||
|  |               </TablixColumns> | ||||||
|  |               <TablixRows> | ||||||
|  |                 <TablixRow> | ||||||
|  |                   <Height>0.5cm</Height> | ||||||
|  |                   <TablixCells> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox38"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>="Vendedor: " & First(Fields!Canilla.Value, "DSLiquidacionCanillas")</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                     <FontWeight>Bold</FontWeight> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox38</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                         <ColSpan>3</ColSpan> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell /> | ||||||
|  |                     <TablixCell /> | ||||||
|  |                   </TablixCells> | ||||||
|  |                 </TablixRow> | ||||||
|  |                 <TablixRow> | ||||||
|  |                   <Height>0.5cm</Height> | ||||||
|  |                   <TablixCells> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Publicacion"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>=Fields!Publicacion.Value</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Publicacion</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox13"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>Retirados</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox13</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="TotalCantSalida"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>=Fields!TotalCantSalida.Value</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>TotalCantSalida</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                   </TablixCells> | ||||||
|  |                 </TablixRow> | ||||||
|  |                 <TablixRow> | ||||||
|  |                   <Height>0.5cm</Height> | ||||||
|  |                   <TablixCells> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox7"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value /> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox7</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <TopBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </TopBorder> | ||||||
|  |                             <BottomBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </BottomBorder> | ||||||
|  |                             <LeftBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </LeftBorder> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox14"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>Devueltos</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox14</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox12"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>=Fields!TotalCantEntrada.Value</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox8</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                   </TablixCells> | ||||||
|  |                 </TablixRow> | ||||||
|  |                 <TablixRow> | ||||||
|  |                   <Height>0.5cm</Height> | ||||||
|  |                   <TablixCells> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox27"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value /> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox27</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <TopBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </TopBorder> | ||||||
|  |                             <BottomBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </BottomBorder> | ||||||
|  |                             <LeftBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </LeftBorder> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox28"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>Vendidos</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox28</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox29"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>=Fields!TotalCantSalida.Value-Fields!TotalCantEntrada.Value</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox29</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                   </TablixCells> | ||||||
|  |                 </TablixRow> | ||||||
|  |                 <TablixRow> | ||||||
|  |                   <Height>0.5cm</Height> | ||||||
|  |                   <TablixCells> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox33"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value /> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox33</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <TopBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </TopBorder> | ||||||
|  |                             <BottomBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </BottomBorder> | ||||||
|  |                             <LeftBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </LeftBorder> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox30"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>Precio Unitario</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox30</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox32"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>=Fields!PrecioEjemplar.Value</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                     <Format>'$'#,0.00;'$'-#,0.00</Format> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox32</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                             <rd:FormatSymbolCulture>es-AR</rd:FormatSymbolCulture> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                   </TablixCells> | ||||||
|  |                 </TablixRow> | ||||||
|  |                 <TablixRow> | ||||||
|  |                   <Height>0.5cm</Height> | ||||||
|  |                   <TablixCells> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox35"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value /> | ||||||
|  |                                   <Style /> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox35</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <TopBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </TopBorder> | ||||||
|  |                             <BottomBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </BottomBorder> | ||||||
|  |                             <LeftBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </LeftBorder> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox36"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>Importe Vendido</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                     <FontWeight>Bold</FontWeight> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox36</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <TopBorder> | ||||||
|  |                               <Color>Black</Color> | ||||||
|  |                               <Width>2pt</Width> | ||||||
|  |                             </TopBorder> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox37"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>=Fields!TotalRendir.Value</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                     <FontWeight>Bold</FontWeight> | ||||||
|  |                                     <Format>'$'#,0.00;'$'-#,0.00</Format> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox37</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <TopBorder> | ||||||
|  |                               <Color>Black</Color> | ||||||
|  |                               <Width>2pt</Width> | ||||||
|  |                             </TopBorder> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                             <rd:FormatSymbolCulture>es-AR</rd:FormatSymbolCulture> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                   </TablixCells> | ||||||
|  |                 </TablixRow> | ||||||
|  |                 <TablixRow> | ||||||
|  |                   <Height>0.5cm</Height> | ||||||
|  |                   <TablixCells> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox1"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value /> | ||||||
|  |                                   <Style /> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox1</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <TopBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </TopBorder> | ||||||
|  |                             <BottomBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </BottomBorder> | ||||||
|  |                             <LeftBorder> | ||||||
|  |                               <Style>None</Style> | ||||||
|  |                             </LeftBorder> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox2"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>Total A Rendir</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                     <FontWeight>Bold</FontWeight> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox2</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <TopBorder> | ||||||
|  |                               <Color>Black</Color> | ||||||
|  |                               <Width>2pt</Width> | ||||||
|  |                             </TopBorder> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                     <TablixCell> | ||||||
|  |                       <CellContents> | ||||||
|  |                         <Textbox Name="Textbox4"> | ||||||
|  |                           <CanGrow>true</CanGrow> | ||||||
|  |                           <KeepTogether>true</KeepTogether> | ||||||
|  |                           <Paragraphs> | ||||||
|  |                             <Paragraph> | ||||||
|  |                               <TextRuns> | ||||||
|  |                                 <TextRun> | ||||||
|  |                                   <Value>=Sum(Fields!TotalRendir.Value, "DSLiquidacionCanillas")</Value> | ||||||
|  |                                   <Style> | ||||||
|  |                                     <FontFamily>Roboto</FontFamily> | ||||||
|  |                                     <FontSize>8pt</FontSize> | ||||||
|  |                                     <FontWeight>Bold</FontWeight> | ||||||
|  |                                     <Format>'$'#,0.00;'$'-#,0.00</Format> | ||||||
|  |                                   </Style> | ||||||
|  |                                 </TextRun> | ||||||
|  |                               </TextRuns> | ||||||
|  |                               <Style /> | ||||||
|  |                             </Paragraph> | ||||||
|  |                           </Paragraphs> | ||||||
|  |                           <rd:DefaultName>Textbox4</rd:DefaultName> | ||||||
|  |                           <Style> | ||||||
|  |                             <Border> | ||||||
|  |                               <Color>LightGrey</Color> | ||||||
|  |                               <Style>Solid</Style> | ||||||
|  |                             </Border> | ||||||
|  |                             <TopBorder> | ||||||
|  |                               <Color>Black</Color> | ||||||
|  |                               <Width>2pt</Width> | ||||||
|  |                             </TopBorder> | ||||||
|  |                             <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |                             <PaddingRight>2pt</PaddingRight> | ||||||
|  |                             <PaddingTop>2pt</PaddingTop> | ||||||
|  |                             <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |                             <rd:FormatSymbolCulture>es-AR</rd:FormatSymbolCulture> | ||||||
|  |                           </Style> | ||||||
|  |                         </Textbox> | ||||||
|  |                       </CellContents> | ||||||
|  |                     </TablixCell> | ||||||
|  |                   </TablixCells> | ||||||
|  |                 </TablixRow> | ||||||
|  |               </TablixRows> | ||||||
|  |             </TablixBody> | ||||||
|  |             <TablixColumnHierarchy> | ||||||
|  |               <TablixMembers> | ||||||
|  |                 <TablixMember /> | ||||||
|  |                 <TablixMember /> | ||||||
|  |                 <TablixMember /> | ||||||
|  |               </TablixMembers> | ||||||
|  |             </TablixColumnHierarchy> | ||||||
|  |             <TablixRowHierarchy> | ||||||
|  |               <TablixMembers> | ||||||
|  |                 <TablixMember> | ||||||
|  |                   <KeepWithGroup>After</KeepWithGroup> | ||||||
|  |                 </TablixMember> | ||||||
|  |                 <TablixMember> | ||||||
|  |                   <Group Name="Publicacion"> | ||||||
|  |                     <GroupExpressions> | ||||||
|  |                       <GroupExpression>=Fields!Publicacion.Value</GroupExpression> | ||||||
|  |                     </GroupExpressions> | ||||||
|  |                   </Group> | ||||||
|  |                   <SortExpressions> | ||||||
|  |                     <SortExpression> | ||||||
|  |                       <Value>=Fields!Publicacion.Value</Value> | ||||||
|  |                     </SortExpression> | ||||||
|  |                   </SortExpressions> | ||||||
|  |                   <TablixMembers> | ||||||
|  |                     <TablixMember /> | ||||||
|  |                     <TablixMember /> | ||||||
|  |                     <TablixMember /> | ||||||
|  |                     <TablixMember /> | ||||||
|  |                     <TablixMember /> | ||||||
|  |                   </TablixMembers> | ||||||
|  |                 </TablixMember> | ||||||
|  |                 <TablixMember> | ||||||
|  |                   <KeepWithGroup>Before</KeepWithGroup> | ||||||
|  |                 </TablixMember> | ||||||
|  |               </TablixMembers> | ||||||
|  |             </TablixRowHierarchy> | ||||||
|  |             <DataSetName>DSLiquidacionCanillas</DataSetName> | ||||||
|  |             <Top>1.07592cm</Top> | ||||||
|  |             <Left>2.09225cm</Left> | ||||||
|  |             <Height>3.5cm</Height> | ||||||
|  |             <Width>10.19813cm</Width> | ||||||
|  |             <Style> | ||||||
|  |               <Border> | ||||||
|  |                 <Style>None</Style> | ||||||
|  |               </Border> | ||||||
|  |               <FontSize>8pt</FontSize> | ||||||
|  |             </Style> | ||||||
|  |           </Tablix> | ||||||
|  |           <Textbox Name="Textbox39"> | ||||||
|  |             <CanGrow>true</CanGrow> | ||||||
|  |             <KeepTogether>true</KeepTogether> | ||||||
|  |             <Paragraphs> | ||||||
|  |               <Paragraph> | ||||||
|  |                 <TextRuns> | ||||||
|  |                   <TextRun> | ||||||
|  |                     <Value>EL DIA S.A.I.C. y F.</Value> | ||||||
|  |                     <Style> | ||||||
|  |                       <FontFamily>Roboto</FontFamily> | ||||||
|  |                       <FontSize>8pt</FontSize> | ||||||
|  |                     </Style> | ||||||
|  |                   </TextRun> | ||||||
|  |                 </TextRuns> | ||||||
|  |                 <Style /> | ||||||
|  |               </Paragraph> | ||||||
|  |             </Paragraphs> | ||||||
|  |             <rd:DefaultName>Textbox39</rd:DefaultName> | ||||||
|  |             <Top>0.01025cm</Top> | ||||||
|  |             <Left>2.09225cm</Left> | ||||||
|  |             <Height>0.5cm</Height> | ||||||
|  |             <Width>10.19813cm</Width> | ||||||
|  |             <ZIndex>1</ZIndex> | ||||||
|  |             <Style> | ||||||
|  |               <Border> | ||||||
|  |                 <Style>None</Style> | ||||||
|  |               </Border> | ||||||
|  |               <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |               <PaddingRight>2pt</PaddingRight> | ||||||
|  |               <PaddingTop>2pt</PaddingTop> | ||||||
|  |               <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |             </Style> | ||||||
|  |           </Textbox> | ||||||
|  |           <Textbox Name="Textbox40"> | ||||||
|  |             <CanGrow>true</CanGrow> | ||||||
|  |             <KeepTogether>true</KeepTogether> | ||||||
|  |             <Paragraphs> | ||||||
|  |               <Paragraph> | ||||||
|  |                 <TextRuns> | ||||||
|  |                   <TextRun> | ||||||
|  |                     <Value>Liquidación venta de diarios del:</Value> | ||||||
|  |                     <Style> | ||||||
|  |                       <FontFamily>Roboto</FontFamily> | ||||||
|  |                       <FontSize>8pt</FontSize> | ||||||
|  |                     </Style> | ||||||
|  |                   </TextRun> | ||||||
|  |                 </TextRuns> | ||||||
|  |                 <Style> | ||||||
|  |                   <TextAlign>Left</TextAlign> | ||||||
|  |                 </Style> | ||||||
|  |               </Paragraph> | ||||||
|  |             </Paragraphs> | ||||||
|  |             <rd:DefaultName>Textbox39</rd:DefaultName> | ||||||
|  |             <Top>0.53236cm</Top> | ||||||
|  |             <Left>2.09225cm</Left> | ||||||
|  |             <Height>0.5cm</Height> | ||||||
|  |             <Width>4.43198cm</Width> | ||||||
|  |             <ZIndex>2</ZIndex> | ||||||
|  |             <Style> | ||||||
|  |               <Border> | ||||||
|  |                 <Style>None</Style> | ||||||
|  |               </Border> | ||||||
|  |               <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |               <PaddingRight>2pt</PaddingRight> | ||||||
|  |               <PaddingTop>2pt</PaddingTop> | ||||||
|  |               <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |             </Style> | ||||||
|  |           </Textbox> | ||||||
|  |           <Textbox Name="Textbox41"> | ||||||
|  |             <CanGrow>true</CanGrow> | ||||||
|  |             <KeepTogether>true</KeepTogether> | ||||||
|  |             <Paragraphs> | ||||||
|  |               <Paragraph> | ||||||
|  |                 <TextRuns> | ||||||
|  |                   <TextRun> | ||||||
|  |                     <Value>=Parameters!FechaLiqui.Value</Value> | ||||||
|  |                     <Style> | ||||||
|  |                       <FontFamily>Roboto</FontFamily> | ||||||
|  |                       <FontSize>8pt</FontSize> | ||||||
|  |                       <Format>dd/MM/yyy</Format> | ||||||
|  |                     </Style> | ||||||
|  |                   </TextRun> | ||||||
|  |                 </TextRuns> | ||||||
|  |                 <Style> | ||||||
|  |                   <TextAlign>Left</TextAlign> | ||||||
|  |                 </Style> | ||||||
|  |               </Paragraph> | ||||||
|  |             </Paragraphs> | ||||||
|  |             <rd:DefaultName>Textbox41</rd:DefaultName> | ||||||
|  |             <Top>0.53236cm</Top> | ||||||
|  |             <Left>6.44662cm</Left> | ||||||
|  |             <Height>0.5cm</Height> | ||||||
|  |             <Width>3.86292cm</Width> | ||||||
|  |             <ZIndex>3</ZIndex> | ||||||
|  |             <Style> | ||||||
|  |               <Border> | ||||||
|  |                 <Style>None</Style> | ||||||
|  |               </Border> | ||||||
|  |               <PaddingLeft>2pt</PaddingLeft> | ||||||
|  |               <PaddingRight>2pt</PaddingRight> | ||||||
|  |               <PaddingTop>2pt</PaddingTop> | ||||||
|  |               <PaddingBottom>2pt</PaddingBottom> | ||||||
|  |             </Style> | ||||||
|  |           </Textbox> | ||||||
|  |         </ReportItems> | ||||||
|  |         <Height>4.66667cm</Height> | ||||||
|  |         <Style /> | ||||||
|  |       </Body> | ||||||
|  |       <Width>14.8cm</Width> | ||||||
|  |       <Page> | ||||||
|  |         <PageHeight>21cm</PageHeight> | ||||||
|  |         <PageWidth>14.8cm</PageWidth> | ||||||
|  |         <LeftMargin>0cm</LeftMargin> | ||||||
|  |         <RightMargin>0cm</RightMargin> | ||||||
|  |         <TopMargin>0.5cm</TopMargin> | ||||||
|  |         <BottomMargin>0.5cm</BottomMargin> | ||||||
|  |         <ColumnSpacing>0.13cm</ColumnSpacing> | ||||||
|  |         <Style /> | ||||||
|  |       </Page> | ||||||
|  |     </ReportSection> | ||||||
|  |   </ReportSections> | ||||||
|  |   <ReportParameters> | ||||||
|  |     <ReportParameter Name="FechaLiqui"> | ||||||
|  |       <DataType>DateTime</DataType> | ||||||
|  |       <Prompt>ReportParameter1</Prompt> | ||||||
|  |     </ReportParameter> | ||||||
|  |   </ReportParameters> | ||||||
|  |   <ReportParametersLayout> | ||||||
|  |     <GridLayoutDefinition> | ||||||
|  |       <NumberOfColumns>4</NumberOfColumns> | ||||||
|  |       <NumberOfRows>2</NumberOfRows> | ||||||
|  |       <CellDefinitions> | ||||||
|  |         <CellDefinition> | ||||||
|  |           <ColumnIndex>0</ColumnIndex> | ||||||
|  |           <RowIndex>0</RowIndex> | ||||||
|  |           <ParameterName>FechaLiqui</ParameterName> | ||||||
|  |         </CellDefinition> | ||||||
|  |       </CellDefinitions> | ||||||
|  |     </GridLayoutDefinition> | ||||||
|  |   </ReportParametersLayout> | ||||||
|  |   <rd:ReportUnitType>Cm</rd:ReportUnitType> | ||||||
|  |   <rd:ReportID>3bfdc2c9-c7dc-47b8-b2b1-3512fa773a78</rd:ReportID> | ||||||
|  | </Report> | ||||||
| @@ -1384,5 +1384,78 @@ namespace GestionIntegral.Api.Controllers | |||||||
|                 return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al generar el PDF del reporte."); |                 return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al generar el PDF del reporte."); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         [HttpGet("ticket-liquidacion-canilla/pdf")] | ||||||
|  |         [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||||
|  |         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |         public async Task<IActionResult> GetTicketLiquidacionCanillaPdf( | ||||||
|  |         [FromQuery] DateTime fecha, | ||||||
|  |         [FromQuery] int idCanilla, | ||||||
|  |         [FromQuery] bool esAccionista = false) // Añadir esAccionista | ||||||
|  |         { | ||||||
|  |             // Usar PermisoVerComprobanteLiquidacionCanilla o uno específico si lo creas | ||||||
|  |             if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid(); | ||||||
|  |  | ||||||
|  |             var (detalles, ganancias, error) = await _reportesService.ObtenerDatosTicketLiquidacionAsync(fecha, idCanilla); | ||||||
|  |  | ||||||
|  |             if (error != null) return BadRequest(new { message = error }); | ||||||
|  |  | ||||||
|  |             // El PDF podría funcionar incluso si solo uno de los datasets tiene datos,  | ||||||
|  |             // pero es bueno verificar si al menos hay detalles. | ||||||
|  |             if (detalles == null || !detalles.Any()) | ||||||
|  |             { | ||||||
|  |                 return NotFound(new { message = "No hay detalles de liquidación para generar el PDF." }); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 LocalReport report = new LocalReport(); | ||||||
|  |                 string rdlcPath = esAccionista ? | ||||||
|  |                     "Controllers/Reportes/RDLC/ReporteLiquidacionCanillasAcc.rdlc" : | ||||||
|  |                     "Controllers/Reportes/RDLC/ReporteLiquidacionCanillas.rdlc"; | ||||||
|  |  | ||||||
|  |                 if (!System.IO.File.Exists(rdlcPath)) | ||||||
|  |                 { | ||||||
|  |                     _logger.LogError("Archivo RDLC no encontrado: {Path}", rdlcPath); | ||||||
|  |                     return StatusCode(StatusCodes.Status500InternalServerError, $"Archivo de reporte no encontrado: {System.IO.Path.GetFileName(rdlcPath)}"); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) | ||||||
|  |                 { | ||||||
|  |                     report.LoadReportDefinition(fs); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 report.DataSources.Add(new ReportDataSource("DSLiquidacionCanillas", detalles)); | ||||||
|  |                 if (!esAccionista) // El reporte de accionistas podría no usar el dataset de ganancias, o usar uno diferente | ||||||
|  |                 { | ||||||
|  |                     report.DataSources.Add(new ReportDataSource("DSLiquidacionCanillasGanancias", ganancias ?? new List<LiquidacionCanillaGananciaDto>())); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |                 var parameters = new List<ReportParameter> | ||||||
|  |         { | ||||||
|  |             // El RDLC espera "FechaLiqui" | ||||||
|  |             new ReportParameter("FechaLiqui", fecha.ToString("dd/MM/yyyy")) | ||||||
|  |         }; | ||||||
|  |                 // El nombre del canilla ya está en el DataSet "DSLiquidacionCanillas" (campo "Canilla") | ||||||
|  |                 // Si el RDLC lo espera como parámetro, lo añadiríamos aquí. | ||||||
|  |                 // var canilla = await _canillaRepository.GetByIdAsync(idCanilla); | ||||||
|  |                 // parameters.Add(new ReportParameter("NombreCanillaParam", canilla?.NomApe ?? "N/A")); | ||||||
|  |  | ||||||
|  |                 report.SetParameters(parameters); | ||||||
|  |  | ||||||
|  |                 byte[] pdfBytes = report.Render("PDF"); | ||||||
|  |                 string tipo = esAccionista ? "Accionista" : "Canillita"; | ||||||
|  |                 string fileName = $"TicketLiquidacion_{tipo}_{idCanilla}_{fecha:yyyyMMdd}.pdf"; | ||||||
|  |                 return File(pdfBytes, "application/pdf", fileName); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al generar PDF para Ticket Liquidación Canilla. Fecha: {Fecha}, Canilla: {IdCanilla}", fecha, idCanilla); | ||||||
|  |                 return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al generar el PDF del ticket."); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -263,16 +263,14 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|  |  | ||||||
|         public async Task<bool> DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction) |         public async Task<bool> DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction) | ||||||
|         { |         { | ||||||
|             var actual = await GetByIdAsync(idParte); // No necesita TX, solo para el historial |             var actual = await GetByIdAsync(idParte); // Sigue siendo útil para el historial | ||||||
|             if (actual == null) throw new KeyNotFoundException("Registro E/S Canilla no encontrado para eliminar."); |             if (actual == null) throw new KeyNotFoundException("Registro E/S Canilla no encontrado para eliminar."); | ||||||
|             if (actual.Liquidado) throw new InvalidOperationException("No se puede eliminar un movimiento liquidado."); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|             const string sqlDelete = "DELETE FROM dbo.dist_EntradasSalidasCanillas WHERE Id_Parte = @IdParteParam"; |             const string sqlDelete = "DELETE FROM dbo.dist_EntradasSalidasCanillas WHERE Id_Parte = @IdParteParam"; | ||||||
|             const string sqlHistorico = @" |             const string sqlHistorico = @" | ||||||
|                  INSERT INTO dbo.dist_EntradasSalidasCanillas_H  |          INSERT INTO dbo.dist_EntradasSalidasCanillas_H  | ||||||
|                     (Id_Parte, Id_Publicacion, Id_Canilla, Fecha, CantSalida, CantEntrada, Id_Precio, Id_Recargo, Id_PorcMon, Observacion, Id_Usuario, FechaMod, TipoMod) |             (Id_Parte, Id_Publicacion, Id_Canilla, Fecha, CantSalida, CantEntrada, Id_Precio, Id_Recargo, Id_PorcMon, Observacion, Id_Usuario, FechaMod, TipoMod) | ||||||
|                 VALUES (@IdParteHist, @IdPubHist, @IdCanillaHist, @FechaHist, @CantSalidaHist, @CantEntradaHist, @IdPrecioHist, @IdRecargoHist, @IdPorcMonHist, @ObsHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; |         VALUES (@IdParteHist, @IdPubHist, @IdCanillaHist, @FechaHist, @CantSalidaHist, @CantEntradaHist, @IdPrecioHist, @IdRecargoHist, @IdPorcMonHist, @ObsHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; | ||||||
|  |  | ||||||
|             await transaction.Connection!.ExecuteAsync(sqlHistorico, new |             await transaction.Connection!.ExecuteAsync(sqlHistorico, new | ||||||
|             { |             { | ||||||
|   | |||||||
| @@ -15,5 +15,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|         Task<bool> DeleteAsync(int id, int idUsuario, IDbTransaction transaction); // Borrado físico con historial |         Task<bool> DeleteAsync(int id, int idUsuario, IDbTransaction transaction); // Borrado físico con historial | ||||||
|         Task<bool> ExistsByNameAndEmpresaAsync(string nombre, int idEmpresa, int? excludeIdPublicacion = null); |         Task<bool> ExistsByNameAndEmpresaAsync(string nombre, int idEmpresa, int? excludeIdPublicacion = null); | ||||||
|         Task<bool> IsInUseAsync(int id); |         Task<bool> IsInUseAsync(int id); | ||||||
|  |         Task<IEnumerable<PublicacionDiaSemana>> GetConfiguracionDiasAsync(int idPublicacion); | ||||||
|  |         Task<IEnumerable<int>> GetPublicacionesIdsPorDiaSemanaAsync(byte diaSemana); // Devuelve solo IDs | ||||||
|  |         Task UpdateConfiguracionDiasAsync(int idPublicacion, IEnumerable<byte> diasActivos, IDbTransaction transaction); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -106,8 +106,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|                 WHERE Id_Publicacion = @IdParam"; |                 WHERE Id_Publicacion = @IdParam"; | ||||||
|             try |             try | ||||||
|             { |             { | ||||||
|                  using var connection = _connectionFactory.CreateConnection(); |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|                  return await connection.QuerySingleOrDefaultAsync<Publicacion>(sql, new { IdParam = id }); |                 return await connection.QuerySingleOrDefaultAsync<Publicacion>(sql, new { IdParam = id }); | ||||||
|             } |             } | ||||||
|             catch (Exception ex) |             catch (Exception ex) | ||||||
|             { |             { | ||||||
| @@ -134,7 +134,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|             } |             } | ||||||
|             catch (Exception ex) |             catch (Exception ex) | ||||||
|             { |             { | ||||||
|                  _logger.LogError(ex, "Error en ExistsByNameAndEmpresaAsync. Nombre: {Nombre}, IdEmpresa: {IdEmpresa}", nombre, idEmpresa); |                 _logger.LogError(ex, "Error en ExistsByNameAndEmpresaAsync. Nombre: {Nombre}, IdEmpresa: {IdEmpresa}", nombre, idEmpresa); | ||||||
|                 return true; // Asumir que existe en caso de error para prevenir duplicados |                 return true; // Asumir que existe en caso de error para prevenir duplicados | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -267,5 +267,65 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion | |||||||
|             var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { IdParam = id }, transaction); |             var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { IdParam = id }, transaction); | ||||||
|             return rowsAffected == 1; |             return rowsAffected == 1; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<PublicacionDiaSemana>> GetConfiguracionDiasAsync(int idPublicacion) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |         SELECT IdPublicacionDia, Id_Publicacion AS IdPublicacion, DiaSemana, Activo | ||||||
|  |         FROM dbo.dist_PublicacionDiaSemana | ||||||
|  |         WHERE Id_Publicacion = @IdPublicacion AND Activo = 1;"; // Solo los activos | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<PublicacionDiaSemana>(sql, new { IdPublicacion = idPublicacion }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener configuración de días para Publicacion ID: {IdPublicacion}", idPublicacion); | ||||||
|  |                 return Enumerable.Empty<PublicacionDiaSemana>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<int>> GetPublicacionesIdsPorDiaSemanaAsync(byte diaSemana) | ||||||
|  |         { | ||||||
|  |             const string sql = @" | ||||||
|  |         SELECT pds.Id_Publicacion | ||||||
|  |         FROM dbo.dist_PublicacionDiaSemana pds | ||||||
|  |         INNER JOIN dbo.dist_dtPublicaciones p ON pds.Id_Publicacion = p.Id_Publicacion | ||||||
|  |         WHERE pds.DiaSemana = @DiaSemana AND pds.Activo = 1 AND (p.Habilitada = 1 OR p.Habilitada IS NULL);"; // Solo publicaciones habilitadas | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |                 return await connection.QueryAsync<int>(sql, new { DiaSemana = diaSemana }); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error al obtener IDs de publicaciones por día de semana: {DiaSemana}", diaSemana); | ||||||
|  |                 return Enumerable.Empty<int>(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task UpdateConfiguracionDiasAsync(int idPublicacion, IEnumerable<byte> diasActivos, IDbTransaction transaction) | ||||||
|  |         { | ||||||
|  |             var connection = transaction.Connection!; | ||||||
|  |  | ||||||
|  |             // 1. Eliminar configuraciones existentes para esta publicación (más simple que hacer upserts complejos) | ||||||
|  |             const string sqlDelete = "DELETE FROM dbo.dist_PublicacionDiaSemana WHERE Id_Publicacion = @IdPublicacion;"; | ||||||
|  |             await connection.ExecuteAsync(sqlDelete, new { IdPublicacion = idPublicacion }, transaction); | ||||||
|  |  | ||||||
|  |             // 2. Insertar las nuevas configuraciones activas | ||||||
|  |             if (diasActivos != null && diasActivos.Any()) | ||||||
|  |             { | ||||||
|  |                 const string sqlInsert = @" | ||||||
|  |             INSERT INTO dbo.dist_PublicacionDiaSemana (Id_Publicacion, DiaSemana, Activo) | ||||||
|  |             VALUES (@IdPublicacion, @DiaSemana, 1);"; // Siempre activo al insertar | ||||||
|  |  | ||||||
|  |                 var insertTasks = diasActivos.Select(dia => | ||||||
|  |                     connection.ExecuteAsync(sqlInsert, new { IdPublicacion = idPublicacion, DiaSemana = dia }, transaction) | ||||||
|  |                 ); | ||||||
|  |                 await Task.WhenAll(insertTasks); | ||||||
|  |             } | ||||||
|  |             // No se necesita historial para esta tabla de configuración por ahora. | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -41,5 +41,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes | |||||||
|         Task<IEnumerable<ListadoDistribucionDistSimpleDto>> GetListadoDistribucionDistSimpleAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta); |         Task<IEnumerable<ListadoDistribucionDistSimpleDto>> GetListadoDistribucionDistSimpleAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta); | ||||||
|         Task<IEnumerable<ListadoDistribucionDistPromedioDiaDto>> GetListadoDistribucionDistPromedioDiaAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta); |         Task<IEnumerable<ListadoDistribucionDistPromedioDiaDto>> GetListadoDistribucionDistPromedioDiaAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta); | ||||||
|         Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta); |         Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta); | ||||||
|  |         Task<IEnumerable<LiquidacionCanillaDetalleDto>> GetLiquidacionCanillaDetalleAsync(DateTime fecha, int idCanilla); | ||||||
|  |         Task<IEnumerable<LiquidacionCanillaGananciaDto>> GetLiquidacionCanillaGananciasAsync(DateTime fecha, int idCanilla); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -479,5 +479,41 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes | |||||||
|                 return (Enumerable.Empty<ListadoDistribucionDistSimpleDto>(), Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>(), "Error interno al generar el reporte."); |                 return (Enumerable.Empty<ListadoDistribucionDistSimpleDto>(), Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>(), "Error interno al generar el reporte."); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<LiquidacionCanillaDetalleDto>> GetLiquidacionCanillaDetalleAsync(DateTime fecha, int idCanilla) | ||||||
|  |     { | ||||||
|  |         const string spName = "dbo.SP_DistCanillasLiquidacion"; | ||||||
|  |         var parameters = new DynamicParameters(); | ||||||
|  |         parameters.Add("@fecha", fecha, DbType.DateTime); | ||||||
|  |         parameters.Add("@idCanilla", idCanilla, DbType.Int32); | ||||||
|  |         try  | ||||||
|  |         {  | ||||||
|  |             using var connection = _dbConnectionFactory.CreateConnection();  | ||||||
|  |             return await connection.QueryAsync<LiquidacionCanillaDetalleDto>(spName, parameters, commandType: CommandType.StoredProcedure);  | ||||||
|  |         } | ||||||
|  |         catch (Exception ex)  | ||||||
|  |         {  | ||||||
|  |             _logger.LogError(ex, "Error SP {SPName} para Liquidacion Canilla Detalle. Fecha: {Fecha}, Canilla: {IdCanilla}", spName, fecha, idCanilla);  | ||||||
|  |             return Enumerable.Empty<LiquidacionCanillaDetalleDto>();  | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<IEnumerable<LiquidacionCanillaGananciaDto>> GetLiquidacionCanillaGananciasAsync(DateTime fecha, int idCanilla) | ||||||
|  |     { | ||||||
|  |         const string spName = "dbo.SP_DistCanillasLiquidacionGanancias"; | ||||||
|  |         var parameters = new DynamicParameters(); | ||||||
|  |         parameters.Add("@fecha", fecha, DbType.DateTime); | ||||||
|  |         parameters.Add("@idCanilla", idCanilla, DbType.Int32); | ||||||
|  |         try  | ||||||
|  |         {  | ||||||
|  |             using var connection = _dbConnectionFactory.CreateConnection();  | ||||||
|  |             return await connection.QueryAsync<LiquidacionCanillaGananciaDto>(spName, parameters, commandType: CommandType.StoredProcedure);  | ||||||
|  |         } | ||||||
|  |         catch (Exception ex)  | ||||||
|  |         {  | ||||||
|  |             _logger.LogError(ex, "Error SP {SPName} para Liquidacion Canilla Ganancias. Fecha: {Fecha}, Canilla: {IdCanilla}", spName, fecha, idCanilla);  | ||||||
|  |             return Enumerable.Empty<LiquidacionCanillaGananciaDto>();  | ||||||
|  |         } | ||||||
|  |     } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | namespace GestionIntegral.Api.Models.Distribucion | ||||||
|  | { | ||||||
|  |     public class PublicacionDiaSemana | ||||||
|  |     { | ||||||
|  |         public int IdPublicacionDia { get; set; } | ||||||
|  |         public int IdPublicacion { get; set; } // FK a dist_dtPublicaciones | ||||||
|  |         public byte DiaSemana { get; set; } // 0 (Domingo) a 6 (Sábado) | ||||||
|  |         public bool Activo { get; set; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,38 +1,56 @@ | |||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
|  | using System.Linq; | ||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Dtos.Distribucion | namespace GestionIntegral.Api.Dtos.Distribucion | ||||||
| { | { | ||||||
|  |     [CustomValidation(typeof(CreateBulkEntradaSalidaCanillaDto), nameof(ValidateNoDuplicatePublicationsInItems))] | ||||||
|     public class CreateBulkEntradaSalidaCanillaDto |     public class CreateBulkEntradaSalidaCanillaDto | ||||||
|     { |     { | ||||||
|         [Required(ErrorMessage = "El ID del canillita es obligatorio.")] |         [Required(ErrorMessage = "El ID del canillita es obligatorio.")] | ||||||
|         public int IdCanilla { get; set; } |         public int IdCanilla { get; set; } | ||||||
|  |  | ||||||
|         [Required(ErrorMessage = "La fecha del movimiento es obligatoria.")] |         [Required(ErrorMessage = "La fecha del movimiento es obligatoria.")] | ||||||
|         public DateTime Fecha { get; set; } // Fecha común para todos los ítems |         public DateTime Fecha { get; set; } | ||||||
|  |  | ||||||
|         [Required(ErrorMessage = "Debe haber al menos un ítem de movimiento.")] |         [Required(ErrorMessage = "Debe haber al menos un ítem de movimiento.")] | ||||||
|         [MinLength(1, ErrorMessage = "Debe agregar al menos una publicación.")] |         [MinLength(1, ErrorMessage = "Debe agregar al menos una publicación.")] | ||||||
|  |         // La validación de cada item se hará por los atributos en EntradaSalidaCanillaItemDto | ||||||
|         public List<EntradaSalidaCanillaItemDto> Items { get; set; } = new List<EntradaSalidaCanillaItemDto>(); |         public List<EntradaSalidaCanillaItemDto> Items { get; set; } = new List<EntradaSalidaCanillaItemDto>(); | ||||||
|          |          | ||||||
|         // Validar que no haya publicaciones duplicadas en la lista de items |         public static ValidationResult? ValidateNoDuplicatePublicationsInItems(CreateBulkEntradaSalidaCanillaDto instanceToValidate, ValidationContext context) | ||||||
|         [CustomValidation(typeof(CreateBulkEntradaSalidaCanillaDto), nameof(ValidateNoDuplicatePublications))] |  | ||||||
|         public string? DuplicateError { get; set; } |  | ||||||
|  |  | ||||||
|         public static ValidationResult? ValidateNoDuplicatePublications(CreateBulkEntradaSalidaCanillaDto dto, ValidationContext context) |  | ||||||
|         { |         { | ||||||
|             if (dto.Items != null) |             if (instanceToValidate == null)  | ||||||
|             { |             { | ||||||
|                 var duplicatePublications = dto.Items |                 return new ValidationResult("El objeto principal de la solicitud es nulo."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (instanceToValidate.Items == null)  | ||||||
|  |             { | ||||||
|  |                  return ValidationResult.Success; // O error si una lista nula de items es inválida | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (instanceToValidate.Items.Any())  | ||||||
|  |             { | ||||||
|  |                 if (instanceToValidate.Items.Any(item => item == null)) | ||||||
|  |                 { | ||||||
|  |                     return new ValidationResult("La lista de ítems contiene entradas nulas.", new[] { nameof(Items) }); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 var duplicateGroups = instanceToValidate.Items | ||||||
|  |                     .Where(item => item.IdPublicacion != 0) // Considerar solo IDs válidos si 0 no lo es | ||||||
|                     .GroupBy(item => item.IdPublicacion)  |                     .GroupBy(item => item.IdPublicacion)  | ||||||
|                     .Where(group => group.Count() > 1) |                     .Where(group => group.Count() > 1) | ||||||
|                     .Select(group => group.Key) |  | ||||||
|                     .ToList(); |                     .ToList(); | ||||||
|  |  | ||||||
|                 if (duplicatePublications.Any()) |                 if (duplicateGroups.Any()) | ||||||
|                 { |                 { | ||||||
|                     return new ValidationResult($"No puede agregar la misma publicación varias veces. Publicaciones duplicadas: {string.Join(", ", duplicatePublications)}"); |                     var duplicatedIds = string.Join(", ", duplicateGroups.Select(g => g.Key)); | ||||||
|  |                     return new ValidationResult( | ||||||
|  |                         $"No puede agregar la misma publicación varias veces. Publicaciones duplicadas IDs: {duplicatedIds}", | ||||||
|  |                         new[] { nameof(Items) }  | ||||||
|  |                     ); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             return ValidationResult.Success; |             return ValidationResult.Success; | ||||||
|   | |||||||
| @@ -3,9 +3,11 @@ using System.ComponentModel.DataAnnotations; | |||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Dtos.Distribucion | namespace GestionIntegral.Api.Dtos.Distribucion | ||||||
| { | { | ||||||
|  |     [CustomValidation(typeof(EntradaSalidaCanillaItemDto), nameof(ValidateCantidadesItem))] // Aplicar a nivel de clase | ||||||
|     public class EntradaSalidaCanillaItemDto |     public class EntradaSalidaCanillaItemDto | ||||||
|     { |     { | ||||||
|         [Required(ErrorMessage = "El ID de la publicación es obligatorio.")] |         [Required(ErrorMessage = "El ID de la publicación es obligatorio.")] | ||||||
|  |         [Range(1, int.MaxValue, ErrorMessage = "El ID de la publicación debe ser válido.")] // Asegurar que no sea 0 | ||||||
|         public int IdPublicacion { get; set; } |         public int IdPublicacion { get; set; } | ||||||
|  |  | ||||||
|         [Required(ErrorMessage = "La cantidad de salida es obligatoria.")] |         [Required(ErrorMessage = "La cantidad de salida es obligatoria.")] | ||||||
| @@ -17,17 +19,26 @@ namespace GestionIntegral.Api.Dtos.Distribucion | |||||||
|         public int CantEntrada { get; set; } |         public int CantEntrada { get; set; } | ||||||
|          |          | ||||||
|         [StringLength(150)] |         [StringLength(150)] | ||||||
|         public string? Observacion { get; set; } // Observación por línea |         public string? Observacion { get; set; } | ||||||
|  |  | ||||||
|         // Validar que CantEntrada no sea mayor que CantSalida |         // Ya no necesitamos la propiedad CantidadesError para esta validación | ||||||
|         [CustomValidation(typeof(EntradaSalidaCanillaItemDto), nameof(ValidateCantidades))] |         // public string? CantidadesError { get; set; }  | ||||||
|         public string? CantidadesError { get; set; }  |  | ||||||
|  |  | ||||||
|         public static ValidationResult? ValidateCantidades(EntradaSalidaCanillaItemDto item, ValidationContext context) |         public static ValidationResult? ValidateCantidadesItem(EntradaSalidaCanillaItemDto instanceToValidate, ValidationContext context) | ||||||
|         { |         { | ||||||
|             if (item.CantEntrada > item.CantSalida) |             if (instanceToValidate == null) | ||||||
|             { |             { | ||||||
|                 return new ValidationResult("La cantidad de entrada (devolución) no puede ser mayor a la cantidad de salida (retiro) para esta publicación.", new[] { nameof(CantEntrada) }); |                 // Aunque el model binder debería crear la instancia, verificamos | ||||||
|  |                 return ValidationResult.Success; // O un error si una instancia nula es inválida por sí misma | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (instanceToValidate.CantEntrada > instanceToValidate.CantSalida) | ||||||
|  |             { | ||||||
|  |                 // Asociar el error a ambas propiedades podría ser útil en la UI | ||||||
|  |                 return new ValidationResult( | ||||||
|  |                     "La cantidad de entrada (devolución) no puede ser mayor a la cantidad de salida (retiro).",  | ||||||
|  |                     new[] { nameof(CantEntrada), nameof(CantSalida) } | ||||||
|  |                 ); | ||||||
|             } |             } | ||||||
|             return ValidationResult.Success; |             return ValidationResult.Success; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | namespace GestionIntegral.Api.Dtos.Distribucion | ||||||
|  | { | ||||||
|  |     public class PublicacionDiaSemanaDto | ||||||
|  |     { | ||||||
|  |         public int IdPublicacionDia { get; set; } // Útil si se editan individualmente | ||||||
|  |         public int IdPublicacion { get; set; } | ||||||
|  |         public byte DiaSemana { get; set; } // 0 (Domingo) a 6 (Sábado) | ||||||
|  |         public bool Activo { get; set; } = true; // Por defecto activo al crear/mostrar | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,25 +1,36 @@ | |||||||
| // Similar a E/S Distribuidores, la edición es limitada para no afectar cálculos complejos ya hechos. |  | ||||||
| // Principalmente para corregir cantidades si aún no está liquidado. |  | ||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
|  |  | ||||||
| namespace GestionIntegral.Api.Dtos.Distribucion | namespace GestionIntegral.Api.Dtos.Distribucion | ||||||
| { | { | ||||||
|  |     // Aplicar el CustomValidation a nivel de clase | ||||||
|  |     [CustomValidation(typeof(UpdateEntradaSalidaCanillaDto), nameof(ValidateCantidades))] | ||||||
|     public class UpdateEntradaSalidaCanillaDto |     public class UpdateEntradaSalidaCanillaDto | ||||||
|     { |     { | ||||||
|         [Required, Range(0, int.MaxValue)] |         [Required, Range(0, int.MaxValue)] | ||||||
|         public int CantSalida { get; set; } |         public int CantSalida { get; set; } | ||||||
|  |  | ||||||
|         [Required, Range(0, int.MaxValue)] |         [Required, Range(0, int.MaxValue)] | ||||||
|         public int CantEntrada { get; set; } |         public int CantEntrada { get; set; } | ||||||
|  |  | ||||||
|         [StringLength(150)] |         [StringLength(150)] | ||||||
|         public string? Observacion { get; set; } |         public string? Observacion { get; set; } | ||||||
|  |  | ||||||
|         [CustomValidation(typeof(UpdateEntradaSalidaCanillaDto), nameof(ValidateCantidades))] |         // El método de validación ahora recibe la instancia completa del DTO | ||||||
|         public string? CantidadesError { get; set; } // Dummy para validación |         public static ValidationResult? ValidateCantidades(UpdateEntradaSalidaCanillaDto instanceToValidate, ValidationContext context) | ||||||
|  |  | ||||||
|         public static ValidationResult? ValidateCantidades(UpdateEntradaSalidaCanillaDto dto, ValidationContext context) |  | ||||||
|         { |         { | ||||||
|             if (dto.CantEntrada > dto.CantSalida) |             if (instanceToValidate == null) | ||||||
|             { |             { | ||||||
|                 return new ValidationResult("La cantidad de entrada no puede ser mayor a la de salida."); |                 // No debería ocurrir si el model binding funcionó, pero es una buena práctica. | ||||||
|  |                 return ValidationResult.Success; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (instanceToValidate.CantEntrada > instanceToValidate.CantSalida) | ||||||
|  |             { | ||||||
|  |                 // Asociar el error a las propiedades relevantes si es posible y útil | ||||||
|  |                 return new ValidationResult( | ||||||
|  |                     "La cantidad de entrada no puede ser mayor a la de salida.", | ||||||
|  |                     new[] { nameof(CantEntrada), nameof(CantSalida) } // Opcional: nombres de miembros | ||||||
|  |                 ); | ||||||
|             } |             } | ||||||
|             return ValidationResult.Success; |             return ValidationResult.Success; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | // Este DTO se usará para enviar la lista completa de días activos para una publicación. | ||||||
|  | namespace GestionIntegral.Api.Dtos.Distribucion | ||||||
|  | { | ||||||
|  |     public class UpdatePublicacionDiasSemanaRequestDto | ||||||
|  |     { | ||||||
|  |         // Lista de los días de la semana (0-6) en los que la publicación estará activa. | ||||||
|  |         // Si un día no está en esta lista, se considerará inactivo o se eliminará la configuración para ese día. | ||||||
|  |         public List<byte> DiasActivos { get; set; } = new List<byte>(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | public class LiquidacionCanillaDetalleDto | ||||||
|  | { | ||||||
|  |     public string Publicacion { get; set; } = string.Empty; | ||||||
|  |     public string Canilla { get; set; } = string.Empty; // Para el nombre del canilla en el reporte | ||||||
|  |     public int TotalCantSalida { get; set; } | ||||||
|  |     public int TotalCantEntrada { get; set; } | ||||||
|  |     public decimal TotalRendir { get; set; } | ||||||
|  |     public decimal PrecioEjemplar { get; set; } | ||||||
|  | } | ||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | public class LiquidacionCanillaGananciaDto | ||||||
|  | { | ||||||
|  |     public string Publicacion { get; set; } = string.Empty; | ||||||
|  |     public decimal TotalRendir { get; set; } // Asumo que este es el 'monto de comisión' | ||||||
|  | } | ||||||
| @@ -225,7 +225,7 @@ namespace GestionIntegral.Api.Services.Distribucion | |||||||
|  |  | ||||||
|                 transaction = connection.BeginTransaction(); |                 transaction = connection.BeginTransaction(); | ||||||
|  |  | ||||||
|                 var esExistente = await _esCanillaRepository.GetByIdAsync(idParte); |                 var esExistente = await _esCanillaRepository.GetByIdAsync(idParte); // Obtener el estado actual | ||||||
|                 if (esExistente == null) |                 if (esExistente == null) | ||||||
|                 { |                 { | ||||||
|                     if (transaction?.Connection != null) transaction.Rollback(); |                     if (transaction?.Connection != null) transaction.Rollback(); | ||||||
| @@ -234,17 +234,16 @@ namespace GestionIntegral.Api.Services.Distribucion | |||||||
|  |  | ||||||
|                 if (esExistente.Liquidado) |                 if (esExistente.Liquidado) | ||||||
|                 { |                 { | ||||||
|                     // Permiso MC006 es para "Eliminar Movimientos de Canillita Liquidados" |                     if (!TienePermisoEspecifico("MC006")) // <--- AQUÍ ESTÁ LA VERIFICACIÓN | ||||||
|                     if (!TienePermisoEspecifico("MC006")) |  | ||||||
|                     { |                     { | ||||||
|                         if (transaction?.Connection != null) transaction.Rollback(); |                         if (transaction?.Connection != null) transaction.Rollback(); | ||||||
|                         return (false, "No tiene permiso para eliminar movimientos ya liquidados. Se requiere permiso especial (MC006) o ser SuperAdmin."); |                         return (false, "No tiene permiso para eliminar movimientos ya liquidados. Se requiere permiso especial (MC006) o ser SuperAdmin."); | ||||||
|                     } |                     } | ||||||
|                     _logger.LogWarning("Usuario ID {IdUsuario} está eliminando un movimiento LIQUIDADO (IDParte: {IdParte}). Permiso MC006 verificado.", idUsuario, idParte); |                     _logger.LogWarning("Usuario ID {IdUsuario} está eliminando un movimiento LIQUIDADO (IDParte: {IdParte}). Permiso MC006 verificado.", idUsuario, idParte); | ||||||
|                 } |                 } | ||||||
|                 // Si no está liquidado, el permiso MC004 ya fue verificado en el controlador. |                 // Si no está liquidado, el permiso MC004 ya fue verificado en el controlador (o debería serlo). | ||||||
|  |  | ||||||
|                 var eliminado = await _esCanillaRepository.DeleteAsync(idParte, idUsuario, transaction); |                 var eliminado = await _esCanillaRepository.DeleteAsync(idParte, idUsuario, transaction); // Ahora esto no lanzará la excepción por liquidado | ||||||
|                 if (!eliminado) |                 if (!eliminado) | ||||||
|                 { |                 { | ||||||
|                     // No es necesario hacer rollback aquí si DeleteAsync lanza una excepción, |                     // No es necesario hacer rollback aquí si DeleteAsync lanza una excepción, | ||||||
|   | |||||||
| @@ -11,5 +11,8 @@ namespace GestionIntegral.Api.Services.Distribucion | |||||||
|         Task<(PublicacionDto? Publicacion, string? Error)> CrearAsync(CreatePublicacionDto createDto, int idUsuario); |         Task<(PublicacionDto? Publicacion, string? Error)> CrearAsync(CreatePublicacionDto createDto, int idUsuario); | ||||||
|         Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdatePublicacionDto updateDto, int idUsuario); |         Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdatePublicacionDto updateDto, int idUsuario); | ||||||
|         Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); |         Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); | ||||||
|  |         Task<IEnumerable<PublicacionDiaSemanaDto>> ObtenerConfiguracionDiasAsync(int idPublicacion); | ||||||
|  |         Task<IEnumerable<PublicacionDto>> ObtenerPublicacionesPorDiaSemanaAsync(byte diaSemana); // Devolvemos el DTO completo | ||||||
|  |         Task<(bool Exito, string? Error)> ActualizarConfiguracionDiasAsync(int idPublicacion, UpdatePublicacionDiasSemanaRequestDto requestDto, int idUsuario); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -202,5 +202,71 @@ namespace GestionIntegral.Api.Services.Distribucion | |||||||
|                 return (false, $"Error interno al eliminar la publicación y sus dependencias: {ex.Message}"); |                 return (false, $"Error interno al eliminar la publicación y sus dependencias: {ex.Message}"); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<PublicacionDiaSemanaDto>> ObtenerConfiguracionDiasAsync(int idPublicacion) | ||||||
|  |         { | ||||||
|  |             var configs = await _publicacionRepository.GetConfiguracionDiasAsync(idPublicacion); | ||||||
|  |             return configs.Select(c => new PublicacionDiaSemanaDto | ||||||
|  |             { | ||||||
|  |                 IdPublicacionDia = c.IdPublicacionDia, | ||||||
|  |                 IdPublicacion = c.IdPublicacion, | ||||||
|  |                 DiaSemana = c.DiaSemana, | ||||||
|  |                 Activo = c.Activo | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public async Task<IEnumerable<PublicacionDto>> ObtenerPublicacionesPorDiaSemanaAsync(byte diaSemana) | ||||||
|  |         { | ||||||
|  |             // Obtener IDs de las publicaciones configuradas para ese día | ||||||
|  |             var idsPublicaciones = await _publicacionRepository.GetPublicacionesIdsPorDiaSemanaAsync(diaSemana); | ||||||
|  |  | ||||||
|  |             if (!idsPublicaciones.Any()) | ||||||
|  |             { | ||||||
|  |                 return Enumerable.Empty<PublicacionDto>(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Obtener los detalles completos de esas publicaciones | ||||||
|  |             // Podríamos optimizar esto si GetByIdAsync es eficiente o crear un GetByIdsAsync | ||||||
|  |             var publicacionesTasks = idsPublicaciones.Select(id => ObtenerPorIdAsync(id)); | ||||||
|  |             var publicacionesResult = await Task.WhenAll(publicacionesTasks); | ||||||
|  |  | ||||||
|  |             return publicacionesResult.Where(p => p != null).Select(p => p!); // Filtrar nulos y asegurar no nulabilidad | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         public async Task<(bool Exito, string? Error)> ActualizarConfiguracionDiasAsync(int idPublicacion, UpdatePublicacionDiasSemanaRequestDto requestDto, int idUsuario) | ||||||
|  |         { | ||||||
|  |             var publicacionExistente = await _publicacionRepository.GetByIdSimpleAsync(idPublicacion); | ||||||
|  |             if (publicacionExistente == null) | ||||||
|  |             { | ||||||
|  |                 return (false, "Publicación no encontrada."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Validar que los días de la semana estén en el rango correcto (0-6) | ||||||
|  |             if (requestDto.DiasActivos.Any(d => d > 6)) // byte no puede ser < 0 | ||||||
|  |             { | ||||||
|  |                 return (false, "Día de la semana inválido. Debe estar entre 0 (Domingo) y 6 (Sábado)."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             using var connection = _connectionFactory.CreateConnection(); | ||||||
|  |             if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); | ||||||
|  |             using var transaction = connection.BeginTransaction(); | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 await _publicacionRepository.UpdateConfiguracionDiasAsync(idPublicacion, requestDto.DiasActivos.Distinct(), transaction); | ||||||
|  |                 // No se está implementando historial para PublicacionDiaSemana por ahora. | ||||||
|  |                 // Si se necesitara, se añadiría aquí una llamada al repositorio para insertar en la tabla _H. | ||||||
|  |  | ||||||
|  |                 transaction.Commit(); | ||||||
|  |                 _logger.LogInformation("Configuración de días actualizada para Publicación ID {IdPublicacion} por Usuario ID {UserId}.", idPublicacion, idUsuario); | ||||||
|  |                 return (true, null); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 try { transaction.Rollback(); } catch { } | ||||||
|  |                 _logger.LogError(ex, "Error al actualizar configuración de días para Publicacion ID: {IdPublicacion}", idPublicacion); | ||||||
|  |                 return (false, $"Error interno al actualizar la configuración de días: {ex.Message}"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -63,5 +63,11 @@ namespace GestionIntegral.Api.Services.Reportes | |||||||
|         )> ObtenerReporteCuentasDistribuidorAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta); |         )> ObtenerReporteCuentasDistribuidorAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta); | ||||||
|  |  | ||||||
|         Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta); |         Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta); | ||||||
|  |  | ||||||
|  |         Task<( | ||||||
|  |             IEnumerable<LiquidacionCanillaDetalleDto> Detalles, | ||||||
|  |             IEnumerable<LiquidacionCanillaGananciaDto> Ganancias, | ||||||
|  |             string? Error | ||||||
|  |         )> ObtenerDatosTicketLiquidacionAsync(DateTime fecha, int idCanilla); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -449,5 +449,46 @@ namespace GestionIntegral.Api.Services.Reportes | |||||||
|                 return (Enumerable.Empty<ListadoDistribucionDistSimpleDto>(), Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>(), "Error interno al generar el reporte."); |                 return (Enumerable.Empty<ListadoDistribucionDistSimpleDto>(), Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>(), "Error interno al generar el reporte."); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public async Task<( | ||||||
|  |         IEnumerable<LiquidacionCanillaDetalleDto> Detalles, | ||||||
|  |         IEnumerable<LiquidacionCanillaGananciaDto> Ganancias, | ||||||
|  |         string? Error | ||||||
|  |     )> ObtenerDatosTicketLiquidacionAsync(DateTime fecha, int idCanilla) | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 var detallesTask = _reportesRepository.GetLiquidacionCanillaDetalleAsync(fecha, idCanilla); | ||||||
|  |                 var gananciasTask = _reportesRepository.GetLiquidacionCanillaGananciasAsync(fecha, idCanilla); | ||||||
|  |  | ||||||
|  |                 await Task.WhenAll(detallesTask, gananciasTask); | ||||||
|  |  | ||||||
|  |                 var detalles = await detallesTask; | ||||||
|  |                 var ganancias = await gananciasTask; | ||||||
|  |  | ||||||
|  |                 if ((detalles == null || !detalles.Any()) && (ganancias == null || !ganancias.Any())) | ||||||
|  |                 { | ||||||
|  |                     // Podrías optar por no devolver error aquí si es válido que uno de los dos esté vacío | ||||||
|  |                     // y manejarlo en el controlador o el RDLC. | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // Convertir fechas a UTC si es necesario para el RDLC (aunque estos DTOs no tienen fechas) | ||||||
|  |  | ||||||
|  |                 return ( | ||||||
|  |                     detalles ?? Enumerable.Empty<LiquidacionCanillaDetalleDto>(), | ||||||
|  |                     ganancias ?? Enumerable.Empty<LiquidacionCanillaGananciaDto>(), | ||||||
|  |                     null | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 _logger.LogError(ex, "Error en ReportesService al obtener datos para Ticket Liquidación Canilla. Fecha: {Fecha}, Canilla: {IdCanilla}", fecha, idCanilla); | ||||||
|  |                 return ( | ||||||
|  |                     Enumerable.Empty<LiquidacionCanillaDetalleDto>(), | ||||||
|  |                     Enumerable.Empty<LiquidacionCanillaGananciaDto>(), | ||||||
|  |                     "Error interno al obtener los datos para el ticket de liquidación." | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -6,7 +6,7 @@ | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "ConnectionStrings": { |   "ConnectionStrings": { | ||||||
|     "DefaultConnection": "Server=TECNICA3;Database=gestionvbnet;User ID=apigestion;Password=1351;Encrypt=False;TrustServerCertificate=True;" |     "DefaultConnection": "Server=TECNICA3;Database=gestionvbnet;User ID=apigestion;Password=1351;Encrypt=False;MultipleActiveResultSets=True;TrustServerCertificate=True;" | ||||||
|   }, |   }, | ||||||
|   "Jwt": { |   "Jwt": { | ||||||
|     "Key": "badb1a38d221c9e23bcf70958840ca7f5a5dc54f2047dadf7ce45b578b5bc3e2", |     "Key": "badb1a38d221c9e23bcf70958840ca7f5a5dc54f2047dadf7ce45b578b5bc3e2", | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "ConnectionStrings": { |   "ConnectionStrings": { | ||||||
|     "DefaultConnection": "Server=TECNICA3;Database=gestionvbnet;User ID=apigestion;Password=1351;Encrypt=False;TrustServerCertificate=True;" |     "DefaultConnection": "Server=TECNICA3;Database=gestionvbnet;User ID=apigestion;Password=1351;Encrypt=False;MultipleActiveResultSets=True;TrustServerCertificate=True;" | ||||||
|   }, |   }, | ||||||
|   "Jwt": { |   "Jwt": { | ||||||
|     "Key": "badb1a38d221c9e23bcf70958840ca7f5a5dc54f2047dadf7ce45b578b5bc3e2", |     "Key": "badb1a38d221c9e23bcf70958840ca7f5a5dc54f2047dadf7ce45b578b5bc3e2", | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ using System.Reflection; | |||||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("GestionIntegral.Api")] | [assembly: System.Reflection.AssemblyCompanyAttribute("GestionIntegral.Api")] | ||||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1182a4cdee4fcdb55dc3f2dbfeeb2ec2187f2bea")] | [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+99532b03f191d55f42e738a90b97f9f1e0dc1a9c")] | ||||||
| [assembly: System.Reflection.AssemblyProductAttribute("GestionIntegral.Api")] | [assembly: System.Reflection.AssemblyProductAttribute("GestionIntegral.Api")] | ||||||
| [assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")] | [assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")] | ||||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| {"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","fdI2RZZ9M9QOVHCYU5cE\u002BgVVuT7ssRbMzdXvX8rHofc=","8ePFhqKT0OT9nEg3b5T7COC81U\u002BQBcf\u002BindBGyMy6z0=","/ghcduGmSd1I25YtYli\u002BqxF0xuscxc4cTDkbEC6XYVA=","/a3YEu0oBUeA5Qr2VMdppqLuz4CQPWJt2JfBl2dtUwA=","jEO/q4IO3UFTWxlyFwRr7kbGWcTIiS\u002BClxx3kahX/Fk=","4iYOCKYvhsROdGkA1hINVBejb6r8IkwFj9SNMKub3DM=","CeDswsZIn5a7t\u002BKeHJA222yhFvDVVEW1ky98Xxnxebc=","50j34YXOc950QSqaQBMtgezD3tV5mWWR9c5qZcYQoz4=","W/aX9jIKpjNEVoGrU6RXFOY8SDJVT6XB4Rg4QCaeQkQ=","16IbB\u002B3zYHZvsWbCQK6hBFmKJ6Z28SecBn2jm8R3w8I=","COJtHNQqycTJqXkFv2hhpLUT\u002B/AD4IWyQlmxkUVQPNk=","cp6a5bdvkLnUn3x47KQODzPycnx57RmWO\u002B9q8MuoGQo=","oKZRNhIQRaZrETEa3L6JiwIp0\u002BmjzJo193EWBoCuVUg=","sjwbCAEQX51sEWhYVGBihWUNBxniUKZALVJIGK\u002BYgsk=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","OZUau2FUwouOUoP6Eot2qiZlqRHSBBkSPL6vHtWUfGI="],"CachedAssets":{},"CachedCopyCandidates":{}} | {"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["V/5slELlkFDzZ8iiVKV8Jt0Ia8AL5AZxPCWo9apx5lQ=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","k3qzLxTWHeeJhAuWKMdta6j24bmJ9BMRMjuFEEVCRu0=","x/sHyso3gy4zVCu3ljpnTYCqu8IGZNRok1JoXiabIP8=","fdI2RZZ9M9QOVHCYU5cE\u002BgVVuT7ssRbMzdXvX8rHofc=","8ePFhqKT0OT9nEg3b5T7COC81U\u002BQBcf\u002BindBGyMy6z0=","/ghcduGmSd1I25YtYli\u002BqxF0xuscxc4cTDkbEC6XYVA=","/a3YEu0oBUeA5Qr2VMdppqLuz4CQPWJt2JfBl2dtUwA=","jEO/q4IO3UFTWxlyFwRr7kbGWcTIiS\u002BClxx3kahX/Fk=","4iYOCKYvhsROdGkA1hINVBejb6r8IkwFj9SNMKub3DM=","CeDswsZIn5a7t\u002BKeHJA222yhFvDVVEW1ky98Xxnxebc=","50j34YXOc950QSqaQBMtgezD3tV5mWWR9c5qZcYQoz4=","W/aX9jIKpjNEVoGrU6RXFOY8SDJVT6XB4Rg4QCaeQkQ=","16IbB\u002B3zYHZvsWbCQK6hBFmKJ6Z28SecBn2jm8R3w8I=","COJtHNQqycTJqXkFv2hhpLUT\u002B/AD4IWyQlmxkUVQPNk=","cp6a5bdvkLnUn3x47KQODzPycnx57RmWO\u002B9q8MuoGQo=","oKZRNhIQRaZrETEa3L6JiwIp0\u002BmjzJo193EWBoCuVUg=","sjwbCAEQX51sEWhYVGBihWUNBxniUKZALVJIGK\u002BYgsk=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","hUWSz0FL2Jxxt3J4cPvysP9naCA2/Cxeo8sJx8s2hvs="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||||
| @@ -1 +1 @@ | |||||||
| {"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","fdI2RZZ9M9QOVHCYU5cE\u002BgVVuT7ssRbMzdXvX8rHofc=","8ePFhqKT0OT9nEg3b5T7COC81U\u002BQBcf\u002BindBGyMy6z0=","/ghcduGmSd1I25YtYli\u002BqxF0xuscxc4cTDkbEC6XYVA=","/a3YEu0oBUeA5Qr2VMdppqLuz4CQPWJt2JfBl2dtUwA=","jEO/q4IO3UFTWxlyFwRr7kbGWcTIiS\u002BClxx3kahX/Fk=","4iYOCKYvhsROdGkA1hINVBejb6r8IkwFj9SNMKub3DM=","CeDswsZIn5a7t\u002BKeHJA222yhFvDVVEW1ky98Xxnxebc=","50j34YXOc950QSqaQBMtgezD3tV5mWWR9c5qZcYQoz4=","W/aX9jIKpjNEVoGrU6RXFOY8SDJVT6XB4Rg4QCaeQkQ=","16IbB\u002B3zYHZvsWbCQK6hBFmKJ6Z28SecBn2jm8R3w8I=","COJtHNQqycTJqXkFv2hhpLUT\u002B/AD4IWyQlmxkUVQPNk=","cp6a5bdvkLnUn3x47KQODzPycnx57RmWO\u002B9q8MuoGQo=","oKZRNhIQRaZrETEa3L6JiwIp0\u002BmjzJo193EWBoCuVUg=","sjwbCAEQX51sEWhYVGBihWUNBxniUKZALVJIGK\u002BYgsk=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","OZUau2FUwouOUoP6Eot2qiZlqRHSBBkSPL6vHtWUfGI="],"CachedAssets":{},"CachedCopyCandidates":{}} | {"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["V/5slELlkFDzZ8iiVKV8Jt0Ia8AL5AZxPCWo9apx5lQ=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","k3qzLxTWHeeJhAuWKMdta6j24bmJ9BMRMjuFEEVCRu0=","x/sHyso3gy4zVCu3ljpnTYCqu8IGZNRok1JoXiabIP8=","fdI2RZZ9M9QOVHCYU5cE\u002BgVVuT7ssRbMzdXvX8rHofc=","8ePFhqKT0OT9nEg3b5T7COC81U\u002BQBcf\u002BindBGyMy6z0=","/ghcduGmSd1I25YtYli\u002BqxF0xuscxc4cTDkbEC6XYVA=","/a3YEu0oBUeA5Qr2VMdppqLuz4CQPWJt2JfBl2dtUwA=","jEO/q4IO3UFTWxlyFwRr7kbGWcTIiS\u002BClxx3kahX/Fk=","4iYOCKYvhsROdGkA1hINVBejb6r8IkwFj9SNMKub3DM=","CeDswsZIn5a7t\u002BKeHJA222yhFvDVVEW1ky98Xxnxebc=","50j34YXOc950QSqaQBMtgezD3tV5mWWR9c5qZcYQoz4=","W/aX9jIKpjNEVoGrU6RXFOY8SDJVT6XB4Rg4QCaeQkQ=","16IbB\u002B3zYHZvsWbCQK6hBFmKJ6Z28SecBn2jm8R3w8I=","COJtHNQqycTJqXkFv2hhpLUT\u002B/AD4IWyQlmxkUVQPNk=","cp6a5bdvkLnUn3x47KQODzPycnx57RmWO\u002B9q8MuoGQo=","oKZRNhIQRaZrETEa3L6JiwIp0\u002BmjzJo193EWBoCuVUg=","sjwbCAEQX51sEWhYVGBihWUNBxniUKZALVJIGK\u002BYgsk=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","hUWSz0FL2Jxxt3J4cPvysP9naCA2/Cxeo8sJx8s2hvs="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||||
| @@ -1 +1 @@ | |||||||
| {"GlobalPropertiesHash":"nueagD6vos1qa5Z6EdwL+uix/UGN3umfwM2JskZDeIQ=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y="],"CachedAssets":{},"CachedCopyCandidates":{}} | {"GlobalPropertiesHash":"nueagD6vos1qa5Z6EdwL+uix/UGN3umfwM2JskZDeIQ=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["V/5slELlkFDzZ8iiVKV8Jt0Ia8AL5AZxPCWo9apx5lQ=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||||
| @@ -162,7 +162,7 @@ const ControlDevolucionesFormModal: React.FC<ControlDevolucionesFormModalProps> | |||||||
|                     margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} |                     margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} | ||||||
|                     disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing} |                     disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing} | ||||||
|                 /> |                 /> | ||||||
|                 <TextField label="Entrada (Devolución Total)" type="number" value={entrada} required |                 <TextField label="Entrada (Por Remito)" type="number" value={entrada} required | ||||||
|                     onChange={(e) => {setEntrada(e.target.value); handleInputChange('entrada');}} |                     onChange={(e) => {setEntrada(e.target.value); handleInputChange('entrada');}} | ||||||
|                     margin="dense" fullWidth error={!!localErrors.entrada} helperText={localErrors.entrada || ''} |                     margin="dense" fullWidth error={!!localErrors.entrada} helperText={localErrors.entrada || ''} | ||||||
|                     disabled={loading} inputProps={{min:0}} |                     disabled={loading} inputProps={{min:0}} | ||||||
| @@ -172,7 +172,7 @@ const ControlDevolucionesFormModal: React.FC<ControlDevolucionesFormModalProps> | |||||||
|                     margin="dense" fullWidth error={!!localErrors.sobrantes} helperText={localErrors.sobrantes || ''} |                     margin="dense" fullWidth error={!!localErrors.sobrantes} helperText={localErrors.sobrantes || ''} | ||||||
|                     disabled={loading} inputProps={{min:0}} |                     disabled={loading} inputProps={{min:0}} | ||||||
|                 /> |                 /> | ||||||
|                  <TextField label="Sin Cargo" type="number" value={sinCargo} required |                  <TextField label="Ejemplares Sin Cargo" type="number" value={sinCargo} required | ||||||
|                     onChange={(e) => {setSinCargo(e.target.value); handleInputChange('sinCargo');}} |                     onChange={(e) => {setSinCargo(e.target.value); handleInputChange('sinCargo');}} | ||||||
|                     margin="dense" fullWidth error={!!localErrors.sinCargo} helperText={localErrors.sinCargo || ''} |                     margin="dense" fullWidth error={!!localErrors.sinCargo} helperText={localErrors.sinCargo || ''} | ||||||
|                     disabled={loading} inputProps={{min:0}} |                     disabled={loading} inputProps={{min:0}} | ||||||
|   | |||||||
| @@ -1,12 +1,11 @@ | |||||||
| // src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx | // src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx | ||||||
| import React, { useState, useEffect } from 'react'; | import React, { useState, useEffect } from 'react'; | ||||||
| import { | import { | ||||||
|     Modal, Box, Typography, TextField, Button, CircularProgress, Alert, |   Modal, Box, Typography, TextField, Button, CircularProgress, Alert, | ||||||
|     FormControl, InputLabel, Select, MenuItem, Paper, IconButton, FormHelperText |   FormControl, InputLabel, Select, MenuItem, Paper, IconButton, FormHelperText | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||||
| import DeleteIcon from '@mui/icons-material/Delete'; | import DeleteIcon from '@mui/icons-material/Delete'; | ||||||
|  |  | ||||||
| import type { EntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaDto'; | import type { EntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaDto'; | ||||||
| import type { UpdateEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto'; | import type { UpdateEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto'; | ||||||
| import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; | import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; | ||||||
| @@ -19,17 +18,17 @@ import type { EntradaSalidaCanillaItemDto } from '../../../models/dtos/Distribuc | |||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
|  |  | ||||||
| const modalStyle = { | const modalStyle = { | ||||||
|     position: 'absolute' as 'absolute', |   position: 'absolute' as 'absolute', | ||||||
|     top: '50%', |   top: '50%', | ||||||
|     left: '50%', |   left: '50%', | ||||||
|     transform: 'translate(-50%, -50%)', |   transform: 'translate(-50%, -50%)', | ||||||
|     width: { xs: '95%', sm: '80%', md: '750px' }, |   width: { xs: '95%', sm: '80%', md: '750px' }, | ||||||
|     bgcolor: 'background.paper', |   bgcolor: 'background.paper', | ||||||
|     border: '2px solid #000', |   border: '2px solid #000', | ||||||
|     boxShadow: 24, |   boxShadow: 24, | ||||||
|     p: 2.5, |   p: 2.5, | ||||||
|     maxHeight: '90vh', |   maxHeight: '90vh', | ||||||
|     overflowY: 'auto' |   overflowY: 'auto' | ||||||
| }; | }; | ||||||
|  |  | ||||||
| interface EntradaSalidaCanillaFormModalProps { | interface EntradaSalidaCanillaFormModalProps { | ||||||
| @@ -51,8 +50,8 @@ interface FormRowItem { | |||||||
|  |  | ||||||
| const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps> = ({ | const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps> = ({ | ||||||
|   open, |   open, | ||||||
|   onClose, |   onClose, // Este onClose es el que se pasa desde GestionarEntradasSalidasCanillaPage | ||||||
|   onSubmit, |   onSubmit, // Este onSubmit es el que se pasa para la lógica de EDICIÓN | ||||||
|   initialData, |   initialData, | ||||||
|   errorMessage: parentErrorMessage, |   errorMessage: parentErrorMessage, | ||||||
|   clearErrorMessage |   clearErrorMessage | ||||||
| @@ -63,16 +62,18 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | |||||||
|   const [editCantSalida, setEditCantSalida] = useState<string>('0'); |   const [editCantSalida, setEditCantSalida] = useState<string>('0'); | ||||||
|   const [editCantEntrada, setEditCantEntrada] = useState<string>('0'); |   const [editCantEntrada, setEditCantEntrada] = useState<string>('0'); | ||||||
|   const [editObservacion, setEditObservacion] = useState(''); |   const [editObservacion, setEditObservacion] = useState(''); | ||||||
|   const [items, setItems] = useState<FormRowItem[]>([]); |   const [items, setItems] = useState<FormRowItem[]>([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); // Iniciar con una fila | ||||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); |   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); | ||||||
|   const [canillitas, setCanillitas] = useState<CanillaDto[]>([]); |   const [canillitas, setCanillitas] = useState<CanillaDto[]>([]); | ||||||
|   const [loading, setLoading] = useState(false); |   const [loading, setLoading] = useState(false); // Loading para submit | ||||||
|   const [loadingDropdowns, setLoadingDropdowns] = useState(false); |   const [loadingDropdowns, setLoadingDropdowns] = useState(false); // Loading para canillas/pubs | ||||||
|  |   const [loadingItems, setLoadingItems] = useState(false); // Loading para pre-carga de items | ||||||
|   const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); |   const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); | ||||||
|   const [modalSpecificApiError, setModalSpecificApiError] = useState<string | null>(null); |   const [modalSpecificApiError, setModalSpecificApiError] = useState<string | null>(null); | ||||||
|  |  | ||||||
|   const isEditing = Boolean(initialData); |   const isEditing = Boolean(initialData); | ||||||
|  |  | ||||||
|  |   // Efecto para cargar datos de dropdowns (Publicaciones, Canillitas) SOLO UNA VEZ o cuando open cambia a true | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const fetchDropdownData = async () => { |     const fetchDropdownData = async () => { | ||||||
|       setLoadingDropdowns(true); |       setLoadingDropdowns(true); | ||||||
| @@ -94,6 +95,13 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | |||||||
|  |  | ||||||
|     if (open) { |     if (open) { | ||||||
|       fetchDropdownData(); |       fetchDropdownData(); | ||||||
|  |     } | ||||||
|  |   }, [open]); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   // Efecto para inicializar el formulario cuando se abre o cambia initialData | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (open) { | ||||||
|       clearErrorMessage(); |       clearErrorMessage(); | ||||||
|       setModalSpecificApiError(null); |       setModalSpecificApiError(null); | ||||||
|       setLocalErrors({}); |       setLocalErrors({}); | ||||||
| @@ -105,19 +113,53 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | |||||||
|         setEditCantSalida(initialData.cantSalida?.toString() || '0'); |         setEditCantSalida(initialData.cantSalida?.toString() || '0'); | ||||||
|         setEditCantEntrada(initialData.cantEntrada?.toString() || '0'); |         setEditCantEntrada(initialData.cantEntrada?.toString() || '0'); | ||||||
|         setEditObservacion(initialData.observacion || ''); |         setEditObservacion(initialData.observacion || ''); | ||||||
|         setItems([]); |         setItems([]); // En modo edición, no pre-cargamos items de la lista | ||||||
|       } else { |       } else { | ||||||
|         setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); |         // Modo NUEVO: resetear campos principales y dejar que el efecto de 'fecha' cargue los items | ||||||
|         setIdCanilla(''); |         setIdCanilla(''); | ||||||
|         setFecha(new Date().toISOString().split('T')[0]); |         setFecha(new Date().toISOString().split('T')[0]); // Fecha actual por defecto | ||||||
|         setEditCantSalida('0'); |         // Los items se cargarán por el siguiente useEffect basado en la fecha | ||||||
|         setEditCantEntrada('0'); |  | ||||||
|         setEditObservacion(''); |  | ||||||
|         setEditIdPublicacion(''); |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }, [open, initialData, isEditing, clearErrorMessage]); |   }, [open, initialData, isEditing, clearErrorMessage]); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   // Efecto para pre-cargar/re-cargar items cuando cambia la FECHA (en modo NUEVO) | ||||||
|  |   // y cuando las publicaciones están disponibles. | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (open && !isEditing && publicaciones.length > 0 && fecha) { // Asegurarse que 'fecha' tiene un valor | ||||||
|  |       const diaSemana = new Date(fecha + 'T00:00:00Z').getUTCDay(); // Usar UTC para getDay consistente | ||||||
|  |       setLoadingItems(true); // Indicador de carga para los items | ||||||
|  |       setLocalErrors(prev => ({ ...prev, general: null })); | ||||||
|  |  | ||||||
|  |       publicacionService.getPublicacionesPorDiaSemana(diaSemana) | ||||||
|  |         .then(pubsPorDefecto => { | ||||||
|  |           if (pubsPorDefecto.length > 0) { | ||||||
|  |             const itemsPorDefecto = pubsPorDefecto.map(pub => ({ | ||||||
|  |               id: `${Date.now().toString()}-${pub.idPublicacion}`, | ||||||
|  |               idPublicacion: pub.idPublicacion, | ||||||
|  |               cantSalida: '0', | ||||||
|  |               cantEntrada: '0', | ||||||
|  |               observacion: '' | ||||||
|  |             })); | ||||||
|  |             setItems(itemsPorDefecto); | ||||||
|  |           } else { | ||||||
|  |             // Si no hay configuraciones para el día, iniciar con una fila vacía | ||||||
|  |             setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |         .catch(err => { | ||||||
|  |           console.error("Error al cargar/recargar publicaciones por defecto para el día:", err); | ||||||
|  |           setLocalErrors(prev => ({ ...prev, general: 'Error al pre-cargar publicaciones del día.' })); | ||||||
|  |           setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||||
|  |         }) | ||||||
|  |         .finally(() => setLoadingItems(false)); | ||||||
|  |     } else if (open && !isEditing && publicaciones.length === 0 && !loadingDropdowns) { | ||||||
|  |       // Si las publicaciones aún no se cargaron pero los dropdowns terminaron de cargar, iniciar con 1 item vacío. | ||||||
|  |       setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||||
|  |     } | ||||||
|  |   }, [open, isEditing, fecha, publicaciones, loadingDropdowns]); // Dependencias clave | ||||||
|  |  | ||||||
|   const validate = (): boolean => { |   const validate = (): boolean => { | ||||||
|     const currentErrors: { [key: string]: string | null } = {}; |     const currentErrors: { [key: string]: string | null } = {}; | ||||||
|     if (!idCanilla) currentErrors.idCanilla = 'Seleccione un canillita.'; |     if (!idCanilla) currentErrors.idCanilla = 'Seleccione un canillita.'; | ||||||
| @@ -125,65 +167,65 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | |||||||
|     else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) currentErrors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).'; |     else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) currentErrors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).'; | ||||||
|  |  | ||||||
|     if (isEditing) { |     if (isEditing) { | ||||||
|         const salidaNum = parseInt(editCantSalida, 10); |       const salidaNum = parseInt(editCantSalida, 10); | ||||||
|         const entradaNum = parseInt(editCantEntrada, 10); |       const entradaNum = parseInt(editCantEntrada, 10); | ||||||
|         if (editCantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) { |       if (editCantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) { | ||||||
|             currentErrors.editCantSalida = 'Cant. Salida debe ser un número >= 0.'; |         currentErrors.editCantSalida = 'Cant. Salida debe ser un número >= 0.'; | ||||||
|         } |       } | ||||||
|         if (editCantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) { |       if (editCantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) { | ||||||
|             currentErrors.editCantEntrada = 'Cant. Entrada debe ser un número >= 0.'; |         currentErrors.editCantEntrada = 'Cant. Entrada debe ser un número >= 0.'; | ||||||
|         } else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) { |       } else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) { | ||||||
|             currentErrors.editCantEntrada = 'Cant. Entrada no puede ser mayor a Cant. Salida.'; |         currentErrors.editCantEntrada = 'Cant. Entrada no puede ser mayor a Cant. Salida.'; | ||||||
|         } |       } | ||||||
|     } else { |     } else { | ||||||
|         let hasValidItemWithQuantityOrPub = false; |       let hasValidItemWithQuantityOrPub = false; | ||||||
|         const publicacionIdsEnLote = new Set<number>(); |       const publicacionIdsEnLote = new Set<number>(); | ||||||
|  |  | ||||||
|         if (items.length === 0) { |       if (items.length === 0) { | ||||||
|             currentErrors.general = "Debe agregar al menos una publicación."; |         currentErrors.general = "Debe agregar al menos una publicación."; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       items.forEach((item, index) => { | ||||||
|  |         const salidaNum = parseInt(item.cantSalida, 10); | ||||||
|  |         const entradaNum = parseInt(item.cantEntrada, 10); | ||||||
|  |         const hasQuantity = !isNaN(salidaNum) && salidaNum >= 0 && !isNaN(entradaNum) && entradaNum >= 0 && (salidaNum > 0 || entradaNum > 0); | ||||||
|  |         const hasObservation = item.observacion.trim() !== ''; | ||||||
|  |  | ||||||
|  |         if (item.idPublicacion === '') { | ||||||
|  |           if (hasQuantity || hasObservation) { | ||||||
|  |             currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} obligatoria si hay datos.`; | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           const pubIdNum = Number(item.idPublicacion); | ||||||
|  |           if (publicacionIdsEnLote.has(pubIdNum)) { | ||||||
|  |             currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} duplicada.`; | ||||||
|  |           } else { | ||||||
|  |             publicacionIdsEnLote.add(pubIdNum); | ||||||
|  |           } | ||||||
|  |           if (item.cantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) { | ||||||
|  |             currentErrors[`item_${item.id}_cantSalida`] = `Salida Pub. ${index + 1} inválida.`; | ||||||
|  |           } | ||||||
|  |           if (item.cantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) { | ||||||
|  |             currentErrors[`item_${item.id}_cantEntrada`] = `Entrada Pub. ${index + 1} inválida.`; | ||||||
|  |           } else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) { | ||||||
|  |             currentErrors[`item_${item.id}_cantEntrada`] = `Dev. Pub. ${index + 1} > Salida.`; | ||||||
|  |           } | ||||||
|  |           if (item.idPublicacion !== '') hasValidItemWithQuantityOrPub = true; | ||||||
|         } |         } | ||||||
|  |       }); | ||||||
|  |  | ||||||
|         items.forEach((item, index) => { |       const allItemsAreEmptyAndNoPubSelected = items.every( | ||||||
|             const salidaNum = parseInt(item.cantSalida, 10); |         itm => itm.idPublicacion === '' && | ||||||
|             const entradaNum = parseInt(item.cantEntrada, 10); |           (itm.cantSalida.trim() === '' || parseInt(itm.cantSalida, 10) === 0) && | ||||||
|             const hasQuantity = !isNaN(salidaNum) && salidaNum >=0 && !isNaN(entradaNum) && entradaNum >=0 && (salidaNum > 0 || entradaNum > 0); |           (itm.cantEntrada.trim() === '' || parseInt(itm.cantEntrada, 10) === 0) && | ||||||
|             const hasObservation = item.observacion.trim() !== ''; |           itm.observacion.trim() === '' | ||||||
|  |       ); | ||||||
|  |  | ||||||
|             if (item.idPublicacion === '') { |       if (!isEditing && items.length > 0 && !hasValidItemWithQuantityOrPub && !allItemsAreEmptyAndNoPubSelected) { | ||||||
|                 if (hasQuantity || hasObservation) { |         currentErrors.general = "Debe seleccionar una publicación para los ítems con cantidades y/o observaciones."; | ||||||
|                     currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} obligatoria si hay datos.`; |       } else if (!isEditing && items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida, 10) > 0 || parseInt(i.cantEntrada, 10) > 0)) && !allItemsAreEmptyAndNoPubSelected) { | ||||||
|                 } |         currentErrors.general = "Debe ingresar cantidades para al menos una publicación con datos significativos."; | ||||||
|             } else { |       } | ||||||
|                 const pubIdNum = Number(item.idPublicacion); |  | ||||||
|                 if (publicacionIdsEnLote.has(pubIdNum)) { |  | ||||||
|                     currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} duplicada.`; |  | ||||||
|                 } else { |  | ||||||
|                     publicacionIdsEnLote.add(pubIdNum); |  | ||||||
|                 } |  | ||||||
|                  if (item.cantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) { |  | ||||||
|                     currentErrors[`item_${item.id}_cantSalida`] = `Salida Pub. ${index + 1} inválida.`; |  | ||||||
|                 } |  | ||||||
|                 if (item.cantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) { |  | ||||||
|                     currentErrors[`item_${item.id}_cantEntrada`] = `Entrada Pub. ${index + 1} inválida.`; |  | ||||||
|                 } else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) { |  | ||||||
|                     currentErrors[`item_${item.id}_cantEntrada`] = `Dev. Pub. ${index + 1} > Salida.`; |  | ||||||
|                 } |  | ||||||
|                 if (item.idPublicacion !== '') hasValidItemWithQuantityOrPub = true; |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|          |  | ||||||
|         const allItemsAreEmptyAndNoPubSelected = items.every( |  | ||||||
|           itm => itm.idPublicacion === '' &&  |  | ||||||
|                  (itm.cantSalida.trim() === '' || parseInt(itm.cantSalida, 10) === 0) && |  | ||||||
|                  (itm.cantEntrada.trim() === '' || parseInt(itm.cantEntrada, 10) === 0) && |  | ||||||
|                  itm.observacion.trim() === '' |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         if (!isEditing && items.length > 0 && !hasValidItemWithQuantityOrPub && !allItemsAreEmptyAndNoPubSelected) { |  | ||||||
|              currentErrors.general = "Debe seleccionar una publicación para los ítems con cantidades y/o observaciones."; |  | ||||||
|         } else if (!isEditing && items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida,10) > 0 || parseInt(i.cantEntrada,10) > 0)) && !allItemsAreEmptyAndNoPubSelected) { |  | ||||||
|             currentErrors.general = "Debe ingresar cantidades para al menos una publicación con datos significativos."; |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|     setLocalErrors(currentErrors); |     setLocalErrors(currentErrors); | ||||||
|     return Object.keys(currentErrors).length === 0; |     return Object.keys(currentErrors).length === 0; | ||||||
| @@ -200,7 +242,6 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | |||||||
|     clearErrorMessage(); |     clearErrorMessage(); | ||||||
|     setModalSpecificApiError(null); |     setModalSpecificApiError(null); | ||||||
|     if (!validate()) return; |     if (!validate()) return; | ||||||
|  |  | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     try { |     try { | ||||||
|       if (isEditing && initialData) { |       if (isEditing && initialData) { | ||||||
| @@ -211,12 +252,16 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | |||||||
|           cantEntrada: entradaNum, |           cantEntrada: entradaNum, | ||||||
|           observacion: editObservacion.trim() || undefined, |           observacion: editObservacion.trim() || undefined, | ||||||
|         }; |         }; | ||||||
|  |         // Aquí se llama al onSubmit que viene de la página padre (GestionarEntradasSalidasCanillaPage) | ||||||
|  |         // para la lógica de actualización. | ||||||
|         await onSubmit(dataToSubmitSingle, initialData.idParte); |         await onSubmit(dataToSubmitSingle, initialData.idParte); | ||||||
|  |         onClose(); // Cerrar el modal DESPUÉS de un submit de edición exitoso | ||||||
|       } else { |       } else { | ||||||
|  |         // Lógica de creación BULK (se maneja internamente en el modal) | ||||||
|         const itemsToSubmit: EntradaSalidaCanillaItemDto[] = items |         const itemsToSubmit: EntradaSalidaCanillaItemDto[] = items | ||||||
|           .filter(item => |           .filter(item => | ||||||
|             item.idPublicacion !== '' && |             item.idPublicacion && Number(item.idPublicacion) > 0 && | ||||||
|             ( (parseInt(item.cantSalida, 10) >= 0 && parseInt(item.cantEntrada, 10) >= 0) && (parseInt(item.cantSalida,10) > 0 || parseInt(item.cantEntrada,10) > 0 ) || item.observacion.trim() !== '') |             ((parseInt(item.cantSalida, 10) >= 0 && parseInt(item.cantEntrada, 10) >= 0) && (parseInt(item.cantSalida, 10) > 0 || parseInt(item.cantEntrada, 10) > 0) || item.observacion.trim() !== '') | ||||||
|           ) |           ) | ||||||
|           .map(item => ({ |           .map(item => ({ | ||||||
|             idPublicacion: Number(item.idPublicacion), |             idPublicacion: Number(item.idPublicacion), | ||||||
| @@ -226,7 +271,7 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | |||||||
|           })); |           })); | ||||||
|  |  | ||||||
|         if (itemsToSubmit.length === 0) { |         if (itemsToSubmit.length === 0) { | ||||||
|           setLocalErrors(prev => ({...prev, general: "No hay movimientos válidos para registrar. Asegúrese de seleccionar una publicación y/o ingresar cantidades."})); |           setLocalErrors(prev => ({ ...prev, general: "No hay movimientos válidos para registrar..." })); | ||||||
|           setLoading(false); |           setLoading(false); | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
| @@ -237,8 +282,9 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | |||||||
|           items: itemsToSubmit, |           items: itemsToSubmit, | ||||||
|         }; |         }; | ||||||
|         await entradaSalidaCanillaService.createBulkEntradasSalidasCanilla(bulkData); |         await entradaSalidaCanillaService.createBulkEntradasSalidasCanilla(bulkData); | ||||||
|  |         onClose(); // Cerrar el modal DESPUÉS de un submit de creación bulk exitoso | ||||||
|       } |       } | ||||||
|       onClose(); |       // onClose(); // Movido dentro de los bloques if/else para asegurar que solo se llama tras éxito | ||||||
|     } catch (error: any) { |     } catch (error: any) { | ||||||
|       console.error("Error en submit de EntradaSalidaCanillaFormModal:", error); |       console.error("Error en submit de EntradaSalidaCanillaFormModal:", error); | ||||||
|       if (axios.isAxiosError(error) && error.response) { |       if (axios.isAxiosError(error) && error.response) { | ||||||
| @@ -246,7 +292,10 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | |||||||
|       } else { |       } else { | ||||||
|         setModalSpecificApiError('Ocurrió un error inesperado.'); |         setModalSpecificApiError('Ocurrió un error inesperado.'); | ||||||
|       } |       } | ||||||
|       if (isEditing) throw error; |       // NO llamar a onClose() aquí si hubo un error, para que el modal permanezca abierto | ||||||
|  |       // y muestre el modalSpecificApiError. | ||||||
|  |       // Si la edición (que usa el 'onSubmit' del padre) lanza un error, ese error se propagará | ||||||
|  |       // al padre y el padre decidirá si el modal se cierra o no (actualmente no lo cierra). | ||||||
|     } finally { |     } finally { | ||||||
|       setLoading(false); |       setLoading(false); | ||||||
|     } |     } | ||||||
| @@ -254,8 +303,8 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | |||||||
|  |  | ||||||
|   const handleAddRow = () => { |   const handleAddRow = () => { | ||||||
|     if (items.length >= publicaciones.length) { |     if (items.length >= publicaciones.length) { | ||||||
|         setLocalErrors(prev => ({ ...prev, general: "No se pueden agregar más filas que publicaciones disponibles." })); |       setLocalErrors(prev => ({ ...prev, general: "No se pueden agregar más filas que publicaciones disponibles." })); | ||||||
|         return; |       return; | ||||||
|     } |     } | ||||||
|     setItems([...items, { id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); |     setItems([...items, { id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); | ||||||
|     setLocalErrors(prev => ({ ...prev, general: null })); |     setLocalErrors(prev => ({ ...prev, general: null })); | ||||||
| @@ -300,27 +349,27 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | |||||||
|               onChange={(e) => { setFecha(e.target.value); handleInputChange('fecha'); }} |               onChange={(e) => { setFecha(e.target.value); handleInputChange('fecha'); }} | ||||||
|               margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} |               margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} | ||||||
|               disabled={loading || isEditing} InputLabelProps={{ shrink: true }} |               disabled={loading || isEditing} InputLabelProps={{ shrink: true }} | ||||||
|               autoFocus={!isEditing} |               autoFocus={!isEditing && !idCanilla} // AutoFocus si es nuevo y no hay canillita seleccionado | ||||||
|             /> |             /> | ||||||
|  |  | ||||||
|             {isEditing && initialData && ( |             {isEditing && initialData && ( | ||||||
|               <Paper elevation={1} sx={{ p: 1.5, mt: 1 }}> |               <Paper elevation={1} sx={{ p: 1.5, mt: 1 }}> | ||||||
|                  <Typography variant="body2" gutterBottom color="text.secondary">Editando para Publicación: {publicaciones.find(p=>p.idPublicacion === editIdPublicacion)?.nombre || `ID ${editIdPublicacion}`}</Typography> |                 <Typography variant="body2" gutterBottom color="text.secondary">Editando para Publicación: {publicaciones.find(p => p.idPublicacion === editIdPublicacion)?.nombre || `ID ${editIdPublicacion}`}</Typography> | ||||||
|                  <Box sx={{ display: 'flex', gap: 2, mt:0.5}}> |                 <Box sx={{ display: 'flex', gap: 2, mt: 0.5 }}> | ||||||
|                     <TextField label="Cant. Salida" type="number" value={editCantSalida} |                   <TextField label="Cant. Salida" type="number" value={editCantSalida} | ||||||
|                         onChange={(e) => {setEditCantSalida(e.target.value); handleInputChange('editCantSalida');}} |                     onChange={(e) => { setEditCantSalida(e.target.value); handleInputChange('editCantSalida'); }} | ||||||
|                         margin="dense" fullWidth error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''} |                     margin="dense" fullWidth error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''} | ||||||
|                         disabled={loading} inputProps={{ min: 0 }} sx={{flex:1}} |                     disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1 }} | ||||||
|                     /> |                   /> | ||||||
|                     <TextField label="Cant. Entrada" type="number" value={editCantEntrada} |                   <TextField label="Cant. Entrada" type="number" value={editCantEntrada} | ||||||
|                         onChange={(e) => {setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada');}} |                     onChange={(e) => { setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada'); }} | ||||||
|                         margin="dense" fullWidth error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''} |                     margin="dense" fullWidth error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''} | ||||||
|                         disabled={loading} inputProps={{ min: 0 }} sx={{flex:1}} |                     disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1 }} | ||||||
|                     /> |                   /> | ||||||
|                 </Box> |                 </Box> | ||||||
|                 <TextField label="Observación (General)" value={editObservacion} |                 <TextField label="Observación (General)" value={editObservacion} | ||||||
|                     onChange={(e) => setEditObservacion(e.target.value)} |                   onChange={(e) => setEditObservacion(e.target.value)} | ||||||
|                     margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{mt:1}} |                   margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{ mt: 1 }} | ||||||
|                 /> |                 /> | ||||||
|               </Paper> |               </Paper> | ||||||
|             )} |             )} | ||||||
| @@ -328,47 +377,141 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps | |||||||
|             {!isEditing && ( |             {!isEditing && ( | ||||||
|               <Box> |               <Box> | ||||||
|                 <Typography variant="subtitle2" sx={{ mt: 1.5, mb: 0.5 }}>Movimientos por Publicación:</Typography> |                 <Typography variant="subtitle2" sx={{ mt: 1.5, mb: 0.5 }}>Movimientos por Publicación:</Typography> | ||||||
|                 {items.map((itemRow, index) => ( // item renombrado a itemRow |                 {/* Indicador de carga para los items */} | ||||||
|                   <Paper key={itemRow.id} elevation={1} sx={{ p: 1.5, mb: 1, position: 'relative' }}> |                 {loadingItems && <Box sx={{ display: 'flex', justifyContent: 'center', my: 1 }}><CircularProgress size={20} /></Box>} | ||||||
|                      {items.length > 1 && ( |                 {!loadingItems && items.map((itemRow, index) => ( | ||||||
|                         <IconButton onClick={() => handleRemoveRow(itemRow.id)} color="error" size="small" |                   <Paper | ||||||
|                             sx={{ position: 'absolute', top: 4, right: 4, zIndex:1 }} |                     key={itemRow.id} | ||||||
|                             aria-label="Quitar fila" |                     elevation={1} | ||||||
|  |                     sx={{ | ||||||
|  |                       p: 1.5, | ||||||
|  |                       mb: 1, | ||||||
|  |                     }} | ||||||
|  |                   > | ||||||
|  |                     {/* Nivel 1: contenedor “padre” sin wrap */} | ||||||
|  |                     <Box | ||||||
|  |                       sx={{ | ||||||
|  |                         display: 'flex', | ||||||
|  |                         alignItems: 'center', // centra ícono + campos | ||||||
|  |                         gap: 1, | ||||||
|  |                         // NOTA: aquí NO ponemos flexWrap, por defecto es 'nowrap' | ||||||
|  |                       }} | ||||||
|  |                     > | ||||||
|  |                       {/* Nivel 2: contenedor que agrupa solo los campos y sí puede hacer wrap */} | ||||||
|  |                       <Box | ||||||
|  |                         sx={{ | ||||||
|  |                           display: 'flex', | ||||||
|  |                           alignItems: 'center', | ||||||
|  |                           gap: 1, | ||||||
|  |                           flexWrap: 'wrap',        // los campos sí hacen wrap si no caben | ||||||
|  |                           flexGrow: 1,             // ocupa todo el espacio disponible antes del ícono | ||||||
|  |                         }} | ||||||
|  |                       > | ||||||
|  |                         <FormControl | ||||||
|  |                           sx={{ minWidth: 160, flexBasis: 'calc(40% - 8px)', flexGrow: 1, minHeight: 0 }} | ||||||
|  |                           size="small" | ||||||
|  |                           error={!!localErrors[`item_${itemRow.id}_idPublicacion`]} | ||||||
|                         > |                         > | ||||||
|                             <DeleteIcon fontSize="inherit" /> |                           <InputLabel | ||||||
|  |                             required={ | ||||||
|  |                               parseInt(itemRow.cantSalida) > 0 || | ||||||
|  |                               parseInt(itemRow.cantEntrada) > 0 || | ||||||
|  |                               itemRow.observacion.trim() !== '' | ||||||
|  |                             } | ||||||
|  |                           > | ||||||
|  |                             Pub. {index + 1} | ||||||
|  |                           </InputLabel> | ||||||
|  |                           <Select | ||||||
|  |                             value={itemRow.idPublicacion} | ||||||
|  |                             label={`Publicación ${index + 1}`} | ||||||
|  |                             onChange={(e) => | ||||||
|  |                               handleItemChange(itemRow.id, 'idPublicacion', e.target.value as number) | ||||||
|  |                             } | ||||||
|  |                             disabled={loading || loadingDropdowns} | ||||||
|  |                             sx={{ minWidth: 0 }} // permite que shrink si hace falta | ||||||
|  |                           > | ||||||
|  |                             <MenuItem value="" disabled> | ||||||
|  |                               <em>Seleccione</em> | ||||||
|  |                             </MenuItem> | ||||||
|  |                             {publicaciones.map((p) => ( | ||||||
|  |                               <MenuItem key={p.idPublicacion} value={p.idPublicacion}> | ||||||
|  |                                 {p.nombre} | ||||||
|  |                               </MenuItem> | ||||||
|  |                             ))} | ||||||
|  |                           </Select> | ||||||
|  |                           {localErrors[`item_${itemRow.id}_idPublicacion`] && ( | ||||||
|  |                             <FormHelperText> | ||||||
|  |                               {localErrors[`item_${itemRow.id}_idPublicacion`]} | ||||||
|  |                             </FormHelperText> | ||||||
|  |                           )} | ||||||
|  |                         </FormControl> | ||||||
|  |  | ||||||
|  |                         <TextField | ||||||
|  |                           label="Llevados" | ||||||
|  |                           type="number" | ||||||
|  |                           size="small" | ||||||
|  |                           value={itemRow.cantSalida} | ||||||
|  |                           onChange={(e) => handleItemChange(itemRow.id, 'cantSalida', e.target.value)} | ||||||
|  |                           error={!!localErrors[`item_${itemRow.id}_cantSalida`]} | ||||||
|  |                           helperText={localErrors[`item_${itemRow.id}_cantSalida`]} | ||||||
|  |                           inputProps={{ min: 0 }} | ||||||
|  |                           sx={{ | ||||||
|  |                             flexBasis: 'calc(15% - 8px)', | ||||||
|  |                             minWidth: '80px', | ||||||
|  |                             minHeight: 0, | ||||||
|  |                           }} | ||||||
|  |                         /> | ||||||
|  |  | ||||||
|  |                         <TextField | ||||||
|  |                           label="Devueltos" | ||||||
|  |                           type="number" | ||||||
|  |                           size="small" | ||||||
|  |                           value={itemRow.cantEntrada} | ||||||
|  |                           onChange={(e) => handleItemChange(itemRow.id, 'cantEntrada', e.target.value)} | ||||||
|  |                           error={!!localErrors[`item_${itemRow.id}_cantEntrada`]} | ||||||
|  |                           helperText={localErrors[`item_${itemRow.id}_cantEntrada`]} | ||||||
|  |                           inputProps={{ min: 0 }} | ||||||
|  |                           sx={{ | ||||||
|  |                             flexBasis: 'calc(15% - 8px)', | ||||||
|  |                             minWidth: '80px', | ||||||
|  |                             minHeight: 0, | ||||||
|  |                           }} | ||||||
|  |                         /> | ||||||
|  |  | ||||||
|  |                         <TextField | ||||||
|  |                           label="Obs." | ||||||
|  |                           value={itemRow.observacion} | ||||||
|  |                           onChange={(e) => handleItemChange(itemRow.id, 'observacion', e.target.value)} | ||||||
|  |                           size="small" | ||||||
|  |                           sx={{ | ||||||
|  |                             flexGrow: 1, | ||||||
|  |                             flexBasis: 'calc(25% - 8px)', | ||||||
|  |                             minWidth: '120px', | ||||||
|  |                             minHeight: 0, | ||||||
|  |                           }} | ||||||
|  |                           multiline | ||||||
|  |                           maxRows={1} | ||||||
|  |                         /> | ||||||
|  |                       </Box> | ||||||
|  |  | ||||||
|  |                       {/* Ícono de eliminar: siempre en la misma línea */} | ||||||
|  |                       {items.length > 1 && ( | ||||||
|  |                         <IconButton | ||||||
|  |                           onClick={() => handleRemoveRow(itemRow.id)} | ||||||
|  |                           color="error" | ||||||
|  |                           aria-label="Quitar fila" | ||||||
|  |                           sx={{ | ||||||
|  |                             alignSelf: 'center', // mantén centrado verticalmente | ||||||
|  |                             // No necesita flexShrink, porque el padre no hace wrap | ||||||
|  |                           }} | ||||||
|  |                         > | ||||||
|  |                           <DeleteIcon fontSize="medium" /> | ||||||
|                         </IconButton> |                         </IconButton> | ||||||
|                     )} |                       )} | ||||||
|                     <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, flexWrap: 'wrap' }}> |  | ||||||
|                       <FormControl sx={{ minWidth: 160, flexBasis: 'calc(40% - 8px)', flexGrow:1 }} size="small" error={!!localErrors[`item_${itemRow.id}_idPublicacion`]}> |  | ||||||
|                         <InputLabel required={parseInt(itemRow.cantSalida) > 0 || parseInt(itemRow.cantEntrada) > 0 || itemRow.observacion.trim() !== ''}>Pub. {index + 1}</InputLabel> |  | ||||||
|                         <Select value={itemRow.idPublicacion} label={`Publicación ${index + 1}`} |  | ||||||
|                           onChange={(e) => handleItemChange(itemRow.id, 'idPublicacion', e.target.value as number)} |  | ||||||
|                           disabled={loading || loadingDropdowns} |  | ||||||
|                         > |  | ||||||
|                           <MenuItem value="" disabled><em>Seleccione</em></MenuItem> |  | ||||||
|                           {publicaciones.map((p) => ( |  | ||||||
|                             <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem> |  | ||||||
|                           ))} |  | ||||||
|                         </Select> |  | ||||||
|                         {localErrors[`item_${itemRow.id}_idPublicacion`] && <FormHelperText>{localErrors[`item_${itemRow.id}_idPublicacion`]}</FormHelperText>} |  | ||||||
|                       </FormControl> |  | ||||||
|                       <TextField label="Llevados" type="number" size="small" value={itemRow.cantSalida} |  | ||||||
|                         onChange={(e) => handleItemChange(itemRow.id, 'cantSalida', e.target.value)} |  | ||||||
|                         error={!!localErrors[`item_${itemRow.id}_cantSalida`]} helperText={localErrors[`item_${itemRow.id}_cantSalida`]} |  | ||||||
|                         inputProps={{ min: 0 }} sx={{ width: 'auto', flexBasis: 'calc(15% - 8px)', minWidth: '80px' }} |  | ||||||
|                       /> |  | ||||||
|                       <TextField label="Devueltos" type="number" size="small" value={itemRow.cantEntrada} |  | ||||||
|                         onChange={(e) => handleItemChange(itemRow.id, 'cantEntrada', e.target.value)} |  | ||||||
|                         error={!!localErrors[`item_${itemRow.id}_cantEntrada`]} helperText={localErrors[`item_${itemRow.id}_cantEntrada`]} |  | ||||||
|                         inputProps={{ min: 0 }} sx={{ width: 'auto', flexBasis: 'calc(15% - 8px)', minWidth: '80px' }} |  | ||||||
|                       /> |  | ||||||
|                        <TextField label="Obs." value={itemRow.observacion} |  | ||||||
|                         onChange={(e) => handleItemChange(itemRow.id, 'observacion', e.target.value)} |  | ||||||
|                         size="small" sx={{ flexGrow: 1, flexBasis: 'calc(25% - 8px)', minWidth: '120px' }} multiline maxRows={1} |  | ||||||
|                       /> |  | ||||||
|                     </Box> |                     </Box> | ||||||
|                   </Paper> |                   </Paper> | ||||||
|                 ))} |                 ))} | ||||||
|  |  | ||||||
|                 {localErrors.general && <Alert severity="error" sx={{ mt: 1 }}>{localErrors.general}</Alert>} |                 {localErrors.general && <Alert severity="error" sx={{ mt: 1 }}>{localErrors.general}</Alert>} | ||||||
|                 <Button onClick={handleAddRow} startIcon={<AddIcon />} sx={{ mt: 1, alignSelf: 'flex-start' }} disabled={loading || loadingDropdowns || items.length >= publicaciones.length}> |                 <Button onClick={handleAddRow} startIcon={<AddIcon />} sx={{ mt: 1, alignSelf: 'flex-start' }} disabled={loading || loadingDropdowns || items.length >= publicaciones.length}> | ||||||
|                   Agregar Publicación |                   Agregar Publicación | ||||||
|   | |||||||
| @@ -0,0 +1,141 @@ | |||||||
|  | import React, { useState, useEffect } from 'react'; | ||||||
|  | import { | ||||||
|  |     Modal, Box, Typography, Button, CircularProgress, Alert, FormGroup, FormControlLabel, Checkbox, Paper | ||||||
|  | } from '@mui/material'; | ||||||
|  | import publicacionService from '../../../services/Distribucion/publicacionService'; | ||||||
|  | import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; | ||||||
|  | //import type { PublicacionDiaSemanaDto } from '../../../models/dtos/Distribucion/PublicacionDiaSemanaDto'; | ||||||
|  | import type { UpdatePublicacionDiasSemanaRequestDto } from '../../../models/dtos/Distribucion/UpdatePublicacionDiasSemanaRequestDto'; | ||||||
|  | import axios from 'axios'; | ||||||
|  |  | ||||||
|  | const modalStyle = { | ||||||
|  |     position: 'absolute' as 'absolute', | ||||||
|  |     top: '50%', | ||||||
|  |     left: '50%', | ||||||
|  |     transform: 'translate(-50%, -50%)', | ||||||
|  |     width: { xs: '90%', sm: '70%', md: '500px' }, | ||||||
|  |     bgcolor: 'background.paper', | ||||||
|  |     border: '2px solid #000', | ||||||
|  |     boxShadow: 24, | ||||||
|  |     p: 3, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const diasSemanaNombres = ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"]; | ||||||
|  |  | ||||||
|  | interface PublicacionDiasSemanaModalProps { | ||||||
|  |   open: boolean; | ||||||
|  |   onClose: () => void; | ||||||
|  |   publicacion: PublicacionDto | null; | ||||||
|  |   onConfigSaved: () => void; // Para recargar la lista de publicaciones si es necesario | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const PublicacionDiasSemanaModal: React.FC<PublicacionDiasSemanaModalProps> = ({ | ||||||
|  |   open, | ||||||
|  |   onClose, | ||||||
|  |   publicacion, | ||||||
|  |   onConfigSaved | ||||||
|  | }) => { | ||||||
|  |   const [selectedDays, setSelectedDays] = useState<Set<number>>(new Set()); | ||||||
|  |   const [loading, setLoading] = useState(false); | ||||||
|  |   const [error, setError] = useState<string | null>(null); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (open && publicacion) { | ||||||
|  |       setLoading(true); | ||||||
|  |       setError(null); | ||||||
|  |       publicacionService.getConfiguracionDiasPublicacion(publicacion.idPublicacion) | ||||||
|  |         .then(configs => { | ||||||
|  |           const activeDays = new Set(configs.filter(c => c.activo).map(c => c.diaSemana)); | ||||||
|  |           setSelectedDays(activeDays); | ||||||
|  |         }) | ||||||
|  |         .catch(err => { | ||||||
|  |           console.error("Error al cargar configuración de días:", err); | ||||||
|  |           setError("Error al cargar la configuración actual de días."); | ||||||
|  |         }) | ||||||
|  |         .finally(() => setLoading(false)); | ||||||
|  |     } else { | ||||||
|  |       setSelectedDays(new Set()); // Resetear al cerrar o si no hay publicación | ||||||
|  |     } | ||||||
|  |   }, [open, publicacion]); | ||||||
|  |  | ||||||
|  |   const handleCheckboxChange = (dayIndex: number) => { | ||||||
|  |     setSelectedDays(prev => { | ||||||
|  |       const newSelection = new Set(prev); | ||||||
|  |       if (newSelection.has(dayIndex)) { | ||||||
|  |         newSelection.delete(dayIndex); | ||||||
|  |       } else { | ||||||
|  |         newSelection.add(dayIndex); | ||||||
|  |       } | ||||||
|  |       return newSelection; | ||||||
|  |     }); | ||||||
|  |     setError(null); // Limpiar error al cambiar | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const handleSubmit = async () => { | ||||||
|  |     if (!publicacion) return; | ||||||
|  |     setLoading(true); | ||||||
|  |     setError(null); | ||||||
|  |     const requestDto: UpdatePublicacionDiasSemanaRequestDto = { | ||||||
|  |       diasActivos: Array.from(selectedDays) | ||||||
|  |     }; | ||||||
|  |     try { | ||||||
|  |       await publicacionService.updateConfiguracionDiasPublicacion(publicacion.idPublicacion, requestDto); | ||||||
|  |       onConfigSaved(); // Notificar al padre que se guardó | ||||||
|  |       onClose(); | ||||||
|  |     } catch (err: any) { | ||||||
|  |       console.error("Error al guardar configuración de días:", err); | ||||||
|  |       const message = axios.isAxiosError(err) && err.response?.data?.message  | ||||||
|  |         ? err.response.data.message  | ||||||
|  |         : 'Error al guardar la configuración.'; | ||||||
|  |       setError(message); | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   if (!publicacion) return null; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Modal open={open} onClose={onClose}> | ||||||
|  |       <Box sx={modalStyle}> | ||||||
|  |         <Typography variant="h6" gutterBottom> | ||||||
|  |           Configurar Días de Salida para: {publicacion.nombre} | ||||||
|  |         </Typography> | ||||||
|  |         <Typography variant="body2" color="text.secondary" sx={{mb:2}}> | ||||||
|  |             Marque los días en que esta publicación debe aparecer por defecto en los movimientos de canillitas. | ||||||
|  |         </Typography> | ||||||
|  |          | ||||||
|  |         {loading && <Box sx={{display:'flex', justifyContent:'center', my:2}}><CircularProgress /></Box>} | ||||||
|  |         {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>} | ||||||
|  |  | ||||||
|  |         {!loading && ( | ||||||
|  |             <Paper variant="outlined" sx={{p:2}}> | ||||||
|  |                 <FormGroup> | ||||||
|  |                 {diasSemanaNombres.map((nombreDia, index) => ( | ||||||
|  |                     <FormControlLabel | ||||||
|  |                     key={index} | ||||||
|  |                     control={ | ||||||
|  |                         <Checkbox | ||||||
|  |                         checked={selectedDays.has(index)} | ||||||
|  |                         onChange={() => handleCheckboxChange(index)} | ||||||
|  |                         /> | ||||||
|  |                     } | ||||||
|  |                     label={nombreDia} | ||||||
|  |                     /> | ||||||
|  |                 ))} | ||||||
|  |                 </FormGroup> | ||||||
|  |             </Paper> | ||||||
|  |         )} | ||||||
|  |  | ||||||
|  |         <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}> | ||||||
|  |           <Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button> | ||||||
|  |           <Button onClick={handleSubmit} variant="contained" disabled={loading}> | ||||||
|  |             {loading ? <CircularProgress size={24} /> : 'Guardar Configuración'} | ||||||
|  |           </Button> | ||||||
|  |         </Box> | ||||||
|  |       </Box> | ||||||
|  |     </Modal> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default PublicacionDiasSemanaModal; | ||||||
| @@ -9,6 +9,7 @@ export interface UserContextData { | |||||||
|   nombreCompleto: string; |   nombreCompleto: string; | ||||||
|   esSuperAdmin: boolean; |   esSuperAdmin: boolean; | ||||||
|   debeCambiarClave: boolean; |   debeCambiarClave: boolean; | ||||||
|  |   perfil: string; | ||||||
|   idPerfil: number; |   idPerfil: number; | ||||||
|   permissions: string[]; // Guardamos los codAcc |   permissions: string[]; // Guardamos los codAcc | ||||||
| } | } | ||||||
| @@ -20,6 +21,7 @@ interface DecodedJwtPayload { | |||||||
|   given_name?: string;  // Nombre (estándar, pero verifica tu token) |   given_name?: string;  // Nombre (estándar, pero verifica tu token) | ||||||
|   family_name?: string; // Apellido (estándar, pero verifica tu token) |   family_name?: string; // Apellido (estándar, pero verifica tu token) | ||||||
|   role: string | string[]; // Puede ser uno o varios roles |   role: string | string[]; // Puede ser uno o varios roles | ||||||
|  |   perfil: string; | ||||||
|   idPerfil: string;     // (viene como string) |   idPerfil: string;     // (viene como string) | ||||||
|   debeCambiarClave: string; // (viene como string "True" o "False") |   debeCambiarClave: string; // (viene como string "True" o "False") | ||||||
|   permission?: string | string[]; // Nuestros claims de permiso (codAcc) |   permission?: string | string[]; // Nuestros claims de permiso (codAcc) | ||||||
| @@ -74,6 +76,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => | |||||||
|         debeCambiarClave: decodedToken.debeCambiarClave?.toLowerCase() === 'true', |         debeCambiarClave: decodedToken.debeCambiarClave?.toLowerCase() === 'true', | ||||||
|         idPerfil: decodedToken.idPerfil ? parseInt(decodedToken.idPerfil, 10) : 0, |         idPerfil: decodedToken.idPerfil ? parseInt(decodedToken.idPerfil, 10) : 0, | ||||||
|         permissions: permissions, |         permissions: permissions, | ||||||
|  |         perfil: decodedToken.perfil || 'Usuario' // Asignar un valor por defecto si no existe | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       setToken(jwtToken); |       setToken(jwtToken); | ||||||
|   | |||||||
| @@ -1,8 +1,14 @@ | |||||||
| import React, { type ReactNode, useState, useEffect } from 'react'; | import React, { type ReactNode, useState, useEffect } from 'react'; | ||||||
| import { Box, AppBar, Toolbar, Typography, Button, Tabs, Tab, Paper } from '@mui/material'; | import { | ||||||
|  |     Box, AppBar, Toolbar, Typography, Tabs, Tab, Paper, | ||||||
|  |     IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider // Nuevas importaciones | ||||||
|  | } from '@mui/material'; | ||||||
|  | import AccountCircle from '@mui/icons-material/AccountCircle'; // Icono de usuario | ||||||
|  | import LockResetIcon from '@mui/icons-material/LockReset'; // Icono para cambiar contraseña | ||||||
|  | import LogoutIcon from '@mui/icons-material/Logout'; // Icono para cerrar sesión | ||||||
| import { useAuth } from '../contexts/AuthContext'; | import { useAuth } from '../contexts/AuthContext'; | ||||||
| import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal'; | import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal'; | ||||||
| import { useNavigate, useLocation } from 'react-router-dom'; // Para manejar la navegación y la ruta actual | import { useNavigate, useLocation } from 'react-router-dom'; | ||||||
|  |  | ||||||
| interface MainLayoutProps { | interface MainLayoutProps { | ||||||
|     children: ReactNode; |     children: ReactNode; | ||||||
| @@ -18,12 +24,10 @@ const modules = [ | |||||||
|     { label: 'Usuarios', path: '/usuarios' }, |     { label: 'Usuarios', path: '/usuarios' }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
|  |  | ||||||
| const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||||
|     const { |     const { | ||||||
|         user, |         user, | ||||||
|         logout, |         logout, | ||||||
|         // ... (resto de las props de useAuth) ... |  | ||||||
|         isAuthenticated, |         isAuthenticated, | ||||||
|         isPasswordChangeForced, |         isPasswordChangeForced, | ||||||
|         showForcedPasswordChangeModal, |         showForcedPasswordChangeModal, | ||||||
| @@ -32,9 +36,10 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | |||||||
|     } = useAuth(); |     } = useAuth(); | ||||||
|  |  | ||||||
|     const navigate = useNavigate(); |     const navigate = useNavigate(); | ||||||
|     const location = useLocation(); // Para obtener la ruta actual |     const location = useLocation(); | ||||||
|  |  | ||||||
|     const [selectedTab, setSelectedTab] = useState<number | false>(false); |     const [selectedTab, setSelectedTab] = useState<number | false>(false); | ||||||
|  |     const [anchorElUserMenu, setAnchorElUserMenu] = useState<null | HTMLElement>(null); // Estado para el menú de usuario | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         const currentModulePath = modules.findIndex(module => |         const currentModulePath = modules.findIndex(module => | ||||||
| @@ -43,12 +48,30 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | |||||||
|         if (currentModulePath !== -1) { |         if (currentModulePath !== -1) { | ||||||
|             setSelectedTab(currentModulePath); |             setSelectedTab(currentModulePath); | ||||||
|         } else if (location.pathname === '/') { |         } else if (location.pathname === '/') { | ||||||
|             setSelectedTab(0); |             setSelectedTab(0); // Asegurar que la pestaña de Inicio se seleccione para la ruta raíz | ||||||
|         } else { |         } else { | ||||||
|             setSelectedTab(false); |             setSelectedTab(false); // Ninguna pestaña seleccionada si no coincide | ||||||
|         } |         } | ||||||
|     }, [location.pathname]); |     }, [location.pathname]); | ||||||
|  |  | ||||||
|  |     const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => { | ||||||
|  |         setAnchorElUserMenu(event.currentTarget); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const handleCloseUserMenu = () => { | ||||||
|  |         setAnchorElUserMenu(null); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const handleChangePasswordClick = () => { | ||||||
|  |         setShowForcedPasswordChangeModal(true); | ||||||
|  |         handleCloseUserMenu(); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const handleLogoutClick = () => { | ||||||
|  |         logout(); | ||||||
|  |         handleCloseUserMenu(); // Cierra el menú antes de desloguear completamente | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     const handleModalClose = (passwordChangedSuccessfully: boolean) => { |     const handleModalClose = (passwordChangedSuccessfully: boolean) => { | ||||||
|         if (passwordChangedSuccessfully) { |         if (passwordChangedSuccessfully) { | ||||||
|             passwordChangeCompleted(); |             passwordChangeCompleted(); | ||||||
| @@ -70,7 +93,6 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | |||||||
|     const isReportesModule = location.pathname.startsWith('/reportes'); |     const isReportesModule = location.pathname.startsWith('/reportes'); | ||||||
|  |  | ||||||
|     if (showForcedPasswordChangeModal && isPasswordChangeForced) { |     if (showForcedPasswordChangeModal && isPasswordChangeForced) { | ||||||
|         // ... (lógica del modal forzado sin cambios) ... |  | ||||||
|         return ( |         return ( | ||||||
|             <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}> |             <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}> | ||||||
|                 <ChangePasswordModal |                 <ChangePasswordModal | ||||||
| @@ -84,33 +106,94 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | |||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}> |         <Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}> | ||||||
|             <AppBar position="static"> |             <AppBar position="sticky" elevation={1} /* Elevation sutil para AppBar */> | ||||||
|                 {/* ... (Toolbar y Tabs sin cambios) ... */} |                 <Toolbar sx={{ display: 'flex', justifyContent: 'space-between' }}> | ||||||
|                 <Toolbar> |                     <Typography variant="h6" component="div" noWrap sx={{ cursor: 'pointer' }} onClick={() => navigate('/')}> | ||||||
|                     <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> |  | ||||||
|                         Sistema de Gestión - El Día |                         Sistema de Gestión - El Día | ||||||
|                     </Typography> |                     </Typography> | ||||||
|                     {user && <Typography sx={{ mr: 2 }}>Hola, {user.nombreCompleto}</Typography>} |  | ||||||
|                     {isAuthenticated && !isPasswordChangeForced && ( |                     <Box sx={{ display: 'flex', alignItems: 'center' }}> | ||||||
|                         <Button |                         {user && ( | ||||||
|                             color="inherit" |                             <Typography sx={{ mr: 2, display: { xs: 'none', sm: 'block' } }} /* Ocultar en pantallas muy pequeñas */> | ||||||
|                             onClick={() => setShowForcedPasswordChangeModal(true)} |                                 Hola, {user.nombreCompleto} | ||||||
|                         > |                             </Typography> | ||||||
|                             Cambiar Contraseña |                         )} | ||||||
|                         </Button> |                         {isAuthenticated && ( | ||||||
|                     )} |                             <> | ||||||
|                     <Button color="inherit" onClick={logout}>Cerrar Sesión</Button> |                                 <IconButton | ||||||
|  |                                     size="large" | ||||||
|  |                                     aria-label="Cuenta del usuario" | ||||||
|  |                                     aria-controls="menu-appbar" | ||||||
|  |                                     aria-haspopup="true" | ||||||
|  |                                     sx={{ | ||||||
|  |                                         padding: '15px', | ||||||
|  |                                     }} | ||||||
|  |                                     onClick={handleOpenUserMenu} | ||||||
|  |                                     color="inherit" | ||||||
|  |                                 > | ||||||
|  |                                      <AccountCircle sx={{ fontSize: 36 }} /> | ||||||
|  |                                 </IconButton> | ||||||
|  |                                 <Menu | ||||||
|  |                                     id="menu-appbar" | ||||||
|  |                                     anchorEl={anchorElUserMenu} | ||||||
|  |                                     anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} | ||||||
|  |                                     keepMounted | ||||||
|  |                                     transformOrigin={{ vertical: 'top', horizontal: 'right' }} | ||||||
|  |                                     open={Boolean(anchorElUserMenu)} | ||||||
|  |                                     onClose={handleCloseUserMenu} | ||||||
|  |                                     sx={{ '& .MuiPaper-root': { minWidth: 220, marginTop: '8px' } }} | ||||||
|  |                                 > | ||||||
|  |                                     {user && ( // Mostrar info del usuario en el menú | ||||||
|  |                                         <Box sx={{ px: 2, py: 1.5, pointerEvents: 'none' /* Para que no sea clickeable */ }}> | ||||||
|  |                                             <Typography variant="subtitle1" sx={{ fontWeight: 'medium' }}>{user.nombreCompleto}</Typography> | ||||||
|  |                                             <Typography variant="body2" color="text.secondary">{user.username}</Typography> | ||||||
|  |                                         </Box> | ||||||
|  |                                     )} | ||||||
|  |                                     {user && <Divider sx={{ mb: 1 }} />} | ||||||
|  |  | ||||||
|  |                                     {!isPasswordChangeForced && ( // No mostrar si ya está forzado a cambiarla | ||||||
|  |                                         <MenuItem onClick={handleChangePasswordClick}> | ||||||
|  |                                             <ListItemIcon><LockResetIcon fontSize="small" /></ListItemIcon> | ||||||
|  |                                             <ListItemText>Cambiar Contraseña</ListItemText> | ||||||
|  |                                         </MenuItem> | ||||||
|  |                                     )} | ||||||
|  |                                     <MenuItem onClick={handleLogoutClick}> | ||||||
|  |                                         <ListItemIcon><LogoutIcon fontSize="small" /></ListItemIcon> | ||||||
|  |                                         <ListItemText>Cerrar Sesión</ListItemText> | ||||||
|  |                                     </MenuItem> | ||||||
|  |                                 </Menu> | ||||||
|  |                             </> | ||||||
|  |                         )} | ||||||
|  |                     </Box> | ||||||
|                 </Toolbar> |                 </Toolbar> | ||||||
|                 <Paper square elevation={0} > |                 <Paper square elevation={0} > | ||||||
|                     <Tabs |                     <Tabs | ||||||
|                         value={selectedTab} |                         value={selectedTab} | ||||||
|                         onChange={handleTabChange} |                         onChange={handleTabChange} | ||||||
|                         indicatorColor="secondary" |                         indicatorColor="secondary" // O 'primary' si prefieres el mismo color que el fondo | ||||||
|                         textColor="inherit" |                         textColor="inherit" // El texto de la pestaña hereda el color (blanco sobre fondo oscuro) | ||||||
|                         variant="scrollable" |                         variant="scrollable" | ||||||
|                         scrollButtons="auto" |                         scrollButtons="auto" | ||||||
|  |                         allowScrollButtonsMobile | ||||||
|                         aria-label="módulos principales" |                         aria-label="módulos principales" | ||||||
|                         sx={{ backgroundColor: 'primary.main', color: 'white' }} |                         sx={{ | ||||||
|  |                             backgroundColor: 'primary.main', // Color de fondo de las pestañas | ||||||
|  |                             color: 'white', // Color del texto de las pestañas | ||||||
|  |                             '& .MuiTabs-indicator': { | ||||||
|  |                                 height: 3, // Un indicador un poco más grueso | ||||||
|  |                             }, | ||||||
|  |                             '& .MuiTab-root': { // Estilo para cada pestaña | ||||||
|  |                                 minWidth: 100, // Ancho mínimo para cada pestaña | ||||||
|  |                                 textTransform: 'none', // Evitar MAYÚSCULAS por defecto | ||||||
|  |                                 fontWeight: 'normal', | ||||||
|  |                                 opacity: 0.85, // Ligeramente transparentes si no están seleccionadas | ||||||
|  |                                 '&.Mui-selected': { | ||||||
|  |                                     fontWeight: 'bold', | ||||||
|  |                                     opacity: 1, | ||||||
|  |                                     // color: 'secondary.main' // Opcional: color diferente para la pestaña seleccionada | ||||||
|  |                                 }, | ||||||
|  |                             } | ||||||
|  |                         }} | ||||||
|                     > |                     > | ||||||
|                         {modules.map((module) => ( |                         {modules.map((module) => ( | ||||||
|                             <Tab key={module.path} label={module.label} /> |                             <Tab key={module.path} label={module.label} /> | ||||||
| @@ -119,30 +202,30 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | |||||||
|                 </Paper> |                 </Paper> | ||||||
|             </AppBar> |             </AppBar> | ||||||
|  |  | ||||||
|             {/* Contenido del Módulo */} |  | ||||||
|             <Box |             <Box | ||||||
|                 component="main" |                 component="main" | ||||||
|                 sx={{ |                 sx={{ | ||||||
|                     flexGrow: 1, |                     flexGrow: 1, | ||||||
|                     py: isReportesModule ? 0 : 3, // Padding vertical condicional. Si es el módulo de Reportes, px es 0 si no 3 |                     py: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 }, // Padding vertical responsivo | ||||||
|                     px: isReportesModule ? 0 : 3, // Padding horizontal condicional. Si es el módulo de Reportes, px es 0 si no 3 |                     px: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 }, // Padding horizontal responsivo | ||||||
|                     display: 'flex', // IMPORTANTE: Para que el hijo (ReportesIndexPage) pueda usar height: '100%' |                     display: 'flex', | ||||||
|                     flexDirection: 'column' // IMPORTANTE |                     flexDirection: 'column' | ||||||
|                 }} |                 }} | ||||||
|             > |             > | ||||||
|                 {children} |                 {children} | ||||||
|             </Box> |             </Box> | ||||||
|  |  | ||||||
|             <Box component="footer" sx={{ p: 1, mt: 'auto', backgroundColor: 'primary.dark', color: 'white', textAlign: 'center' }}> |             <Box component="footer" sx={{ p: 1, backgroundColor: 'grey.200' /* Un gris más claro */, color: 'text.secondary', textAlign: 'left', borderTop: (theme) => `1px solid ${theme.palette.divider}` }}> | ||||||
|                 <Typography variant="body2"> |                 <Typography variant="caption"> | ||||||
|                     Usuario: {user?.username} | Acceso: {user?.esSuperAdmin ? 'Super Admin' : `Perfil ID ${user?.userId}`} | Versión: {/* TODO: Obtener versión */} |                     {/* Puedes usar caption para un texto más pequeño en el footer */} | ||||||
|  |                     Usuario: {user?.username} | Acceso: {user?.esSuperAdmin ? 'Super Administrador' : (user?.perfil || `ID ${user?.idPerfil}`)} | ||||||
|                 </Typography> |                 </Typography> | ||||||
|             </Box> |             </Box> | ||||||
|  |  | ||||||
|             <ChangePasswordModal |             <ChangePasswordModal | ||||||
|                 open={showForcedPasswordChangeModal} |                 open={showForcedPasswordChangeModal && !isPasswordChangeForced} // Solo mostrar si no es el forzado inicial | ||||||
|                 onClose={handleModalClose} |                 onClose={() => handleModalClose(false)} // Asumir que si se cierra sin cambiar, no fue exitoso | ||||||
|                 isFirstLogin={isPasswordChangeForced} |                 isFirstLogin={false} // Este modal no es para el primer login forzado | ||||||
|             /> |             /> | ||||||
|         </Box> |         </Box> | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | export interface PublicacionDiaSemanaDto { | ||||||
|  |   idPublicacionDia: number; | ||||||
|  |   idPublicacion: number; | ||||||
|  |   diaSemana: number; // 0 (Domingo) a 6 (Sábado) | ||||||
|  |   activo: boolean; | ||||||
|  | } | ||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | export interface UpdatePublicacionDiasSemanaRequestDto { | ||||||
|  |   diasActivos: number[]; // Array de números de día (0-6) | ||||||
|  | } | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | export interface LiquidacionCanillaDetalleDto { | ||||||
|  |   publicacion: string; | ||||||
|  |   canilla: string; // Nombre del canilla | ||||||
|  |   totalCantSalida: number; | ||||||
|  |   totalCantEntrada: number; | ||||||
|  |   totalRendir: number; | ||||||
|  |   precioEjemplar: number; | ||||||
|  | } | ||||||
| @@ -0,0 +1,4 @@ | |||||||
|  | export interface LiquidacionCanillaGananciaDto { | ||||||
|  |   publicacion: string; | ||||||
|  |   totalRendir: number; // Este es el monto de la comisión/ganancia | ||||||
|  | } | ||||||
| @@ -16,26 +16,40 @@ const ContablesIndexPage: React.FC = () => { | |||||||
|   const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false); |   const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const currentBasePath = '/contables'; |      const currentBasePath = '/contables'; | ||||||
|     const subPath = location.pathname.startsWith(currentBasePath + '/') |      const defaultSubPath = 'pagos-distribuidores'; // Define tu sub-ruta por defecto aquí | ||||||
|                       ? location.pathname.substring(currentBasePath.length + 1) |  | ||||||
|                       : (location.pathname === currentBasePath ? contablesSubModules[0]?.path : undefined); |  | ||||||
|  |  | ||||||
|     const activeTabIndex = contablesSubModules.findIndex( |      const pathParts = location.pathname.split('/'); | ||||||
|       (subModule) => subModule.path === subPath |      const currentSubPathSegment = pathParts[2]; // /contables -> pathParts[1] es 'contables', pathParts[2] sería la sub-ruta | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     if (activeTabIndex !== -1) { |      let activeTabIndex = -1; | ||||||
|       setSelectedSubTab(activeTabIndex); |  | ||||||
|     } else { |      if (currentSubPathSegment) { | ||||||
|       if (location.pathname === currentBasePath && contablesSubModules.length > 0) { |          activeTabIndex = contablesSubModules.findIndex( | ||||||
|          navigate(contablesSubModules[0].path, { replace: true }); |              (subModule) => subModule.path === currentSubPathSegment | ||||||
|          setSelectedSubTab(0); |          ); | ||||||
|       } else { |      } | ||||||
|          setSelectedSubTab(false); |       | ||||||
|       } |      if (activeTabIndex !== -1) { | ||||||
|     } |          setSelectedSubTab(activeTabIndex); | ||||||
|   }, [location.pathname, navigate]); |      } else { | ||||||
|  |          // Si estamos en la ruta base /contables o una subruta no reconocida | ||||||
|  |          if (location.pathname === currentBasePath || (location.pathname.startsWith(currentBasePath) && activeTabIndex === -1) ) { | ||||||
|  |              const defaultTabIndex = contablesSubModules.findIndex(sm => sm.path === defaultSubPath); | ||||||
|  |              if (defaultTabIndex !== -1) { | ||||||
|  |                  navigate(`${currentBasePath}/${defaultSubPath}`, { replace: true }); | ||||||
|  |                  setSelectedSubTab(defaultTabIndex); | ||||||
|  |              } else if (contablesSubModules.length > 0) { // Fallback al primero si el default no existe | ||||||
|  |                  navigate(`${currentBasePath}/${contablesSubModules[0].path}`, { replace: true }); | ||||||
|  |                  setSelectedSubTab(0); | ||||||
|  |              } else { | ||||||
|  |                  setSelectedSubTab(false); // No hay sub-módulos | ||||||
|  |              } | ||||||
|  |          } else { | ||||||
|  |              setSelectedSubTab(false); // No es una ruta del módulo contable | ||||||
|  |          } | ||||||
|  |      } | ||||||
|  |  }, [location.pathname, navigate]); | ||||||
|  |  | ||||||
|   const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => { |   const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => { | ||||||
|     setSelectedSubTab(newValue); |     setSelectedSubTab(newValue); | ||||||
|   | |||||||
| @@ -154,8 +154,8 @@ const GestionarNotasCDPage: React.FC = () => { | |||||||
|   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; |   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Notas de Crédito/Débito</Typography> |       <Typography variant="h5" gutterBottom>Notas de Crédito/Débito</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|          <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography> |          <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography> | ||||||
|          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> |          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> | ||||||
|   | |||||||
| @@ -133,8 +133,8 @@ const GestionarPagosDistribuidorPage: React.FC = () => { | |||||||
|   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; |   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Pagos de Distribuidores</Typography> |       <Typography variant="h5" gutterBottom>Pagos de Distribuidores</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|          <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography> |          <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography> | ||||||
|          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> |          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> | ||||||
|   | |||||||
| @@ -3,10 +3,14 @@ import React, { useState, useEffect, useCallback } from 'react'; | |||||||
| import { | import { | ||||||
|   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||||
|   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|   CircularProgress, Alert |   CircularProgress, Alert, | ||||||
|  |   ListItemIcon, | ||||||
|  |   ListItemText | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; // Icono para agregar | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar | ||||||
| import tipoPagoService from '../../services/Contables/tipoPagoService'; | import tipoPagoService from '../../services/Contables/tipoPagoService'; | ||||||
| import type { TipoPago } from '../../models/Entities/TipoPago'; | import type { TipoPago } from '../../models/Entities/TipoPago'; | ||||||
| import type { CreateTipoPagoDto } from '../../models/dtos/Contables/CreateTipoPagoDto'; | import type { CreateTipoPagoDto } from '../../models/dtos/Contables/CreateTipoPagoDto'; | ||||||
| @@ -129,8 +133,8 @@ const GestionarTiposPagoPage: React.FC = () => { | |||||||
|   const displayData = tiposPago.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); |   const displayData = tiposPago.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom> |       <Typography variant="h5" gutterBottom> | ||||||
|         Gestionar Tipos de Pago |         Gestionar Tipos de Pago | ||||||
|       </Typography> |       </Typography> | ||||||
|  |  | ||||||
| @@ -148,7 +152,6 @@ const GestionarTiposPagoPage: React.FC = () => { | |||||||
|           {/* <Button variant="contained" onClick={cargarTiposPago}>Buscar</Button> */} |           {/* <Button variant="contained" onClick={cargarTiposPago}>Buscar</Button> */} | ||||||
|         </Box> |         </Box> | ||||||
|         {puedeCrear && ( |         {puedeCrear && ( | ||||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> |  | ||||||
|             <Button |             <Button | ||||||
|               variant="contained" |               variant="contained" | ||||||
|               startIcon={<AddIcon />} |               startIcon={<AddIcon />} | ||||||
| @@ -157,7 +160,6 @@ const GestionarTiposPagoPage: React.FC = () => { | |||||||
|             > |             > | ||||||
|               Agregar Nuevo Tipo |               Agregar Nuevo Tipo | ||||||
|             </Button> |             </Button> | ||||||
|           </Box> |  | ||||||
|         )} |         )} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
| @@ -217,12 +219,14 @@ const GestionarTiposPagoPage: React.FC = () => { | |||||||
|       > |       > | ||||||
|         {puedeModificar && ( |         {puedeModificar && ( | ||||||
|           <MenuItem onClick={() => { handleOpenModal(selectedTipoPagoRow!); handleMenuClose(); }}> |           <MenuItem onClick={() => { handleOpenModal(selectedTipoPagoRow!); handleMenuClose(); }}> | ||||||
|             Modificar |             <ListItemIcon><EditIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Modificar</ListItemText> | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {puedeEliminar && ( |         {puedeEliminar && ( | ||||||
|           <MenuItem onClick={() => handleDelete(selectedTipoPagoRow!.idTipoPago)}> |           <MenuItem onClick={() => handleDelete(selectedTipoPagoRow!.idTipoPago)}> | ||||||
|             Eliminar |             <ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Eliminar</ListItemText> | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {/* Si no tiene ningún permiso, el menú podría estar vacío o no mostrarse */} |         {/* Si no tiene ningún permiso, el menú podría estar vacío o no mostrarse */} | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import AddIcon from '@mui/icons-material/Add'; | |||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||||
| import ToggleOnIcon from '@mui/icons-material/ToggleOn'; | import ToggleOnIcon from '@mui/icons-material/ToggleOn'; | ||||||
| import ToggleOffIcon from '@mui/icons-material/ToggleOff'; | import ToggleOffIcon from '@mui/icons-material/ToggleOff'; | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; | ||||||
| import canillaService from '../../services/Distribucion/canillaService'; | import canillaService from '../../services/Distribucion/canillaService'; | ||||||
| import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; | import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; | ||||||
| import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto'; | import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto'; | ||||||
| @@ -121,8 +122,8 @@ const GestionarCanillitasPage: React.FC = () => { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Gestionar Canillitas</Typography> |       <Typography variant="h5" gutterBottom>Gestionar Canillitas</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|          <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}> |          <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}> | ||||||
|             <TextField |             <TextField | ||||||
| @@ -156,9 +157,7 @@ const GestionarCanillitasPage: React.FC = () => { | |||||||
|             {/* <Button variant="contained" onClick={cargarCanillitas} size="small">Buscar</Button> */} |             {/* <Button variant="contained" onClick={cargarCanillitas} size="small">Buscar</Button> */} | ||||||
|          </Box> |          </Box> | ||||||
|          {puedeCrear && ( |          {puedeCrear && ( | ||||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> |  | ||||||
|           <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Canillita</Button> |           <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Canillita</Button> | ||||||
|           </Box> |  | ||||||
|           )} |           )} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
| @@ -203,7 +202,7 @@ const GestionarCanillitasPage: React.FC = () => { | |||||||
|        )} |        )} | ||||||
|  |  | ||||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> |       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||||
|         {puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow!); handleMenuClose(); }}>Modificar</MenuItem>)} |         {puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow!); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)} | ||||||
|         {puedeDarBaja && selectedCanillitaRow && ( |         {puedeDarBaja && selectedCanillitaRow && ( | ||||||
|             <MenuItem onClick={() => handleToggleBaja(selectedCanillitaRow)}> |             <MenuItem onClick={() => handleToggleBaja(selectedCanillitaRow)}> | ||||||
|                 {selectedCanillitaRow.baja ? <ToggleOnIcon sx={{mr:1}}/> : <ToggleOffIcon sx={{mr:1}}/>} |                 {selectedCanillitaRow.baja ? <ToggleOnIcon sx={{mr:1}}/> : <ToggleOffIcon sx={{mr:1}}/>} | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import React, { useState, useEffect, useCallback } from 'react'; | import React, { useState, useEffect, useCallback } from 'react'; | ||||||
| import { | import { | ||||||
|     Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||||
|     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|     CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip |   CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||||
| @@ -11,7 +11,7 @@ import DeleteIcon from '@mui/icons-material/Delete'; | |||||||
| import FilterListIcon from '@mui/icons-material/FilterList'; | import FilterListIcon from '@mui/icons-material/FilterList'; | ||||||
|  |  | ||||||
| import controlDevolucionesService from '../../services/Distribucion/controlDevolucionesService'; | import controlDevolucionesService from '../../services/Distribucion/controlDevolucionesService'; | ||||||
| import empresaService from '../../services/Distribucion/empresaService'; // Para el filtro de empresa | import empresaService from '../../services/Distribucion/empresaService'; | ||||||
|  |  | ||||||
| import type { ControlDevolucionesDto } from '../../models/dtos/Distribucion/ControlDevolucionesDto'; | import type { ControlDevolucionesDto } from '../../models/dtos/Distribucion/ControlDevolucionesDto'; | ||||||
| import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto'; | import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto'; | ||||||
| @@ -28,9 +28,8 @@ const GestionarControlDevolucionesPage: React.FC = () => { | |||||||
|   const [error, setError] = useState<string | null>(null); |   const [error, setError] = useState<string | null>(null); | ||||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); |   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||||
|  |  | ||||||
|   // Filtros |   const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); | ||||||
|   const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState(''); |   const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); | ||||||
|   const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState(''); |  | ||||||
|   const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>(''); |   const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>(''); | ||||||
|  |  | ||||||
|   const [empresas, setEmpresas] = useState<EmpresaDto[]>([]); |   const [empresas, setEmpresas] = useState<EmpresaDto[]>([]); | ||||||
| @@ -45,22 +44,35 @@ const GestionarControlDevolucionesPage: React.FC = () => { | |||||||
|   const [selectedRow, setSelectedRow] = useState<ControlDevolucionesDto | null>(null); |   const [selectedRow, setSelectedRow] = useState<ControlDevolucionesDto | null>(null); | ||||||
|  |  | ||||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); |   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||||
|   // Permisos CD001 (Ver), CD002 (Crear), CD003 (Modificar) |  | ||||||
|   const puedeVer = isSuperAdmin || tienePermiso("CD001"); |   const puedeVer = isSuperAdmin || tienePermiso("CD001"); | ||||||
|   const puedeCrear = isSuperAdmin || tienePermiso("CD002"); |   const puedeCrear = isSuperAdmin || tienePermiso("CD002"); | ||||||
|   const puedeModificar = isSuperAdmin || tienePermiso("CD003"); |   const puedeModificar = isSuperAdmin || tienePermiso("CD003"); | ||||||
|   const puedeEliminar = isSuperAdmin || tienePermiso("CD003"); // Asumiendo que modificar incluye eliminar |   const puedeEliminar = isSuperAdmin || tienePermiso("CD003"); | ||||||
|  |  | ||||||
|  |   // CORREGIDO: Función para formatear la fecha | ||||||
|  |   const formatDate = (dateString?: string | null): string => { | ||||||
|  |     if (!dateString) return '-'; | ||||||
|  |     // Asumimos que dateString viene del backend como "YYYY-MM-DD" o "YYYY-MM-DDTHH:mm:ss..." | ||||||
|  |     const datePart = dateString.split('T')[0]; // Tomar solo la parte YYYY-MM-DD | ||||||
|  |     const parts = datePart.split('-'); | ||||||
|  |     if (parts.length === 3) { | ||||||
|  |       // parts[0] = YYYY, parts[1] = MM, parts[2] = DD | ||||||
|  |       return `${parts[2]}/${parts[1]}/${parts[0]}`; // Formato DD/MM/YYYY | ||||||
|  |     } | ||||||
|  |     return datePart; // Fallback si el formato no es el esperado | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |  | ||||||
|   const fetchFiltersDropdownData = useCallback(async () => { |   const fetchFiltersDropdownData = useCallback(async () => { | ||||||
|     setLoadingFiltersDropdown(true); |     setLoadingFiltersDropdown(true); | ||||||
|     try { |     try { | ||||||
|         const empresasData = await empresaService.getAllEmpresas(); |       const empresasData = await empresaService.getAllEmpresas(); | ||||||
|         setEmpresas(empresasData); |       setEmpresas(empresasData); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|         console.error("Error cargando empresas para filtro:", err); |       console.error("Error cargando empresas para filtro:", err); | ||||||
|         setError("Error al cargar opciones de filtro."); |       setError("Error al cargar opciones de filtro."); | ||||||
|     } finally { |     } finally { | ||||||
|         setLoadingFiltersDropdown(false); |       setLoadingFiltersDropdown(false); | ||||||
|     } |     } | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
| @@ -110,13 +122,13 @@ const GestionarControlDevolucionesPage: React.FC = () => { | |||||||
|  |  | ||||||
|   const handleDelete = async (idControl: number) => { |   const handleDelete = async (idControl: number) => { | ||||||
|     if (window.confirm(`¿Seguro de eliminar este control de devoluciones (ID: ${idControl})?`)) { |     if (window.confirm(`¿Seguro de eliminar este control de devoluciones (ID: ${idControl})?`)) { | ||||||
|        setApiErrorMessage(null); |       setApiErrorMessage(null); | ||||||
|        try { |       try { | ||||||
|         await controlDevolucionesService.deleteControlDevoluciones(idControl); |         await controlDevolucionesService.deleteControlDevoluciones(idControl); | ||||||
|         cargarControles(); |         cargarControles(); | ||||||
|       } catch (err: any) { |       } catch (err: any) { | ||||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; |         const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; | ||||||
|          setApiErrorMessage(message); |         setApiErrorMessage(message); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     handleMenuClose(); |     handleMenuClose(); | ||||||
| @@ -131,81 +143,81 @@ const GestionarControlDevolucionesPage: React.FC = () => { | |||||||
|  |  | ||||||
|   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); |   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||||
|   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { |   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|     setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); |     setRowsPerPage(parseInt(event.target.value, 25)); setPage(0); | ||||||
|   }; |   }; | ||||||
|  |   // displayData ahora usará la 'controles' directamente, el formato se aplica en el renderizado | ||||||
|   const displayData = controles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); |   const displayData = controles.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>; |   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Control de Devoluciones a Empresa</Typography> |       <Typography variant="h5" gutterBottom>Control de Devoluciones a Empresa</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|          <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography> |         <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography> | ||||||
|          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> |         <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 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}}/> |           <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}> |           <FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}> | ||||||
|                 <InputLabel>Empresa</InputLabel> |             <InputLabel>Empresa</InputLabel> | ||||||
|                 <Select value={filtroIdEmpresa} label="Empresa" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}> |             <Select value={filtroIdEmpresa} label="Empresa" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}> | ||||||
|                     <MenuItem value=""><em>Todas</em></MenuItem> |               <MenuItem value=""><em>Todas</em></MenuItem> | ||||||
|                     {empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)} |               {empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)} | ||||||
|                 </Select> |             </Select> | ||||||
|             </FormControl> |           </FormControl> | ||||||
|          </Box> |         </Box> | ||||||
|          {puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Control</Button>)} |         {puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Control</Button>)} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
|       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} |       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} | ||||||
|       {error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>} |       {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>} | ||||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} |       {apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>} | ||||||
|  |  | ||||||
|       {!loading && !error && puedeVer && ( |       {!loading && !error && puedeVer && ( | ||||||
|          <TableContainer component={Paper}> |         <TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> {/* Ajusta maxHeight según sea necesario */} | ||||||
|            <Table size="small"> |           <Table stickyHeader size="small"> | ||||||
|              <TableHead><TableRow> |             <TableHead><TableRow> | ||||||
|                  <TableCell>Fecha</TableCell><TableCell>Empresa</TableCell> |               <TableCell>Fecha</TableCell><TableCell>Empresa</TableCell> | ||||||
|                  <TableCell align="right">Entrada (Total Dev.)</TableCell> |               <TableCell align="right">Entrada (Por Remito)</TableCell> | ||||||
|                  <TableCell align="right">Sobrantes</TableCell> |               <TableCell align="right">Sobrantes</TableCell> | ||||||
|                  <TableCell align="right">Sin Cargo</TableCell> |               <TableCell align="right">Ejemplares Sin Cargo</TableCell> | ||||||
|                  <TableCell>Detalle</TableCell> |               <TableCell>Detalle</TableCell> | ||||||
|                  {(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} |               {(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} | ||||||
|              </TableRow></TableHead> |             </TableRow></TableHead> | ||||||
|              <TableBody> |             <TableBody> | ||||||
|                {displayData.length === 0 ? ( |               {displayData.length === 0 ? ( | ||||||
|                   <TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles.</TableCell></TableRow> |                 <TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles.</TableCell></TableRow> | ||||||
|                ) : ( |               ) : ( | ||||||
|                  displayData.map((c) => ( |                 displayData.map((c) => ( | ||||||
|                      <TableRow key={c.idControl} hover> |                   <TableRow key={c.idControl} hover> | ||||||
|                      <TableCell>{formatDate(c.fecha)}</TableCell> |                     <TableCell>{formatDate(c.fecha)}</TableCell> | ||||||
|                      <TableCell>{c.nombreEmpresa}</TableCell> |                     <TableCell>{c.nombreEmpresa}</TableCell> | ||||||
|                      <TableCell align="right">{c.entrada}</TableCell> |                     <TableCell align="right">{c.entrada}</TableCell> | ||||||
|                      <TableCell align="right">{c.sobrantes}</TableCell> |                     <TableCell align="right">{c.sobrantes}</TableCell> | ||||||
|                      <TableCell align="right">{c.sinCargo}</TableCell> |                     <TableCell align="right">{c.sinCargo}</TableCell> | ||||||
|                      <TableCell><Tooltip title={c.detalle || ''}><Box sx={{maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{c.detalle || '-'}</Box></Tooltip></TableCell> |                     <TableCell><Tooltip title={c.detalle || ''}><Box sx={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.detalle || '-'}</Box></Tooltip></TableCell> | ||||||
|                      {(puedeModificar || puedeEliminar) && ( |                     {(puedeModificar || puedeEliminar) && ( | ||||||
|                         <TableCell align="right"> |                       <TableCell align="right"> | ||||||
|                             <IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton> |                         <IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton> | ||||||
|                         </TableCell> |                       </TableCell> | ||||||
|                      )} |                     )} | ||||||
|                      </TableRow> |                   </TableRow> | ||||||
|                  )))} |                 )))} | ||||||
|              </TableBody> |             </TableBody> | ||||||
|            </Table> |           </Table> | ||||||
|            <TablePagination |           <TablePagination | ||||||
|              rowsPerPageOptions={[10, 25, 50]} component="div" count={controles.length} |             rowsPerPageOptions={[10, 25, 50]} component="div" count={controles.length} | ||||||
|              rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} |             rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} | ||||||
|              onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" |             onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" | ||||||
|            /> |           /> | ||||||
|          </TableContainer> |         </TableContainer> | ||||||
|        )} |       )} | ||||||
|  |  | ||||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> |       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||||
|         {puedeModificar && selectedRow && ( |         {puedeModificar && selectedRow && ( | ||||||
|             <MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)} |           <MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)} | ||||||
|         {puedeEliminar && selectedRow && ( |         {puedeEliminar && selectedRow && ( | ||||||
|             <MenuItem onClick={() => handleDelete(selectedRow.idControl)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)} |           <MenuItem onClick={() => handleDelete(selectedRow.idControl)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)} | ||||||
|       </Menu> |       </Menu> | ||||||
|  |  | ||||||
|       <ControlDevolucionesFormModal |       <ControlDevolucionesFormModal | ||||||
|   | |||||||
| @@ -6,6 +6,8 @@ import { | |||||||
|     CircularProgress, Alert |     CircularProgress, Alert | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; | ||||||
|  | import TrashIcon from '@mui/icons-material/Delete'; | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||||
| import distribuidorService from '../../services/Distribucion/distribuidorService'; | import distribuidorService from '../../services/Distribucion/distribuidorService'; | ||||||
| import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto'; | import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto'; | ||||||
| @@ -110,8 +112,8 @@ const GestionarDistribuidoresPage: React.FC = () => { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Gestionar Distribuidores</Typography> |       <Typography variant="h5" gutterBottom>Gestionar Distribuidores</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|          <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}> |          <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}> | ||||||
|             <TextField |             <TextField | ||||||
| @@ -133,9 +135,7 @@ const GestionarDistribuidoresPage: React.FC = () => { | |||||||
|             {/* <Button variant="contained" onClick={cargarDistribuidores} size="small">Buscar</Button> */} |             {/* <Button variant="contained" onClick={cargarDistribuidores} size="small">Buscar</Button> */} | ||||||
|          </Box> |          </Box> | ||||||
|          {puedeCrear && ( |          {puedeCrear && ( | ||||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> |  | ||||||
|           <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Distribuidor</Button> |           <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Distribuidor</Button> | ||||||
|           </Box> |  | ||||||
|           )} |           )} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
| @@ -179,8 +179,8 @@ const GestionarDistribuidoresPage: React.FC = () => { | |||||||
|        )} |        )} | ||||||
|  |  | ||||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> |       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||||
|         {puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}>Modificar</MenuItem>)} |         {puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} />Modificar</MenuItem>)} | ||||||
|         {puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedDistribuidorRow!.idDistribuidor)}>Eliminar</MenuItem>)} |         {puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedDistribuidorRow!.idDistribuidor)}><TrashIcon fontSize="small" sx={{ mr: 1 }} />Eliminar</MenuItem>)} | ||||||
|         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} |         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} | ||||||
|       </Menu> |       </Menu> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,11 +1,15 @@ | |||||||
| import React, { useState, useEffect, useCallback } from 'react'; | import React, { useState, useEffect, useCallback } from 'react'; | ||||||
| import { | import { | ||||||
|     Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||||
|     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|     CircularProgress, Alert |   CircularProgress, Alert, | ||||||
|  |   ListItemIcon, | ||||||
|  |   ListItemText | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar | ||||||
| import empresaService from '../../services/Distribucion/empresaService'; // Importar el servicio de Empresas | import empresaService from '../../services/Distribucion/empresaService'; // Importar el servicio de Empresas | ||||||
| import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; | import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; | ||||||
| import type { CreateEmpresaDto } from '../../models/dtos/Distribucion/CreateEmpresaDto'; | import type { CreateEmpresaDto } from '../../models/dtos/Distribucion/CreateEmpresaDto'; | ||||||
| @@ -42,9 +46,9 @@ const GestionarEmpresasPage: React.FC = () => { | |||||||
|  |  | ||||||
|   const cargarEmpresas = useCallback(async () => { |   const cargarEmpresas = useCallback(async () => { | ||||||
|     if (!puedeVer) { // Si no tiene permiso de ver, no cargar nada |     if (!puedeVer) { // Si no tiene permiso de ver, no cargar nada | ||||||
|         setError("No tiene permiso para ver esta sección."); |       setError("No tiene permiso para ver esta sección."); | ||||||
|         setLoading(false); |       setLoading(false); | ||||||
|         return; |       return; | ||||||
|     } |     } | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     setError(null); |     setError(null); | ||||||
| @@ -90,8 +94,8 @@ const GestionarEmpresasPage: React.FC = () => { | |||||||
|     } catch (err: any) { |     } catch (err: any) { | ||||||
|       console.error("Error en submit modal (padre):", err); |       console.error("Error en submit modal (padre):", err); | ||||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message |       const message = axios.isAxiosError(err) && err.response?.data?.message | ||||||
|                         ? err.response.data.message |         ? err.response.data.message | ||||||
|                         : 'Ocurrió un error inesperado al guardar la empresa.'; |         : 'Ocurrió un error inesperado al guardar la empresa.'; | ||||||
|       setApiErrorMessage(message); |       setApiErrorMessage(message); | ||||||
|       throw err; // Re-lanzar para que el modal sepa que hubo error y no se cierre |       throw err; // Re-lanzar para que el modal sepa que hubo error y no se cierre | ||||||
|     } |     } | ||||||
| @@ -101,16 +105,16 @@ const GestionarEmpresasPage: React.FC = () => { | |||||||
|   const handleDelete = async (id: number) => { |   const handleDelete = async (id: number) => { | ||||||
|     // Opcional: mostrar un mensaje de confirmación más detallado |     // Opcional: mostrar un mensaje de confirmación más detallado | ||||||
|     if (window.confirm(`¿Está seguro de que desea eliminar esta empresa (ID: ${id})? Esta acción también eliminará los saldos asociados.`)) { |     if (window.confirm(`¿Está seguro de que desea eliminar esta empresa (ID: ${id})? Esta acción también eliminará los saldos asociados.`)) { | ||||||
|        setApiErrorMessage(null); // Limpiar errores previos |       setApiErrorMessage(null); // Limpiar errores previos | ||||||
|        try { |       try { | ||||||
|         await empresaService.deleteEmpresa(id); |         await empresaService.deleteEmpresa(id); | ||||||
|         cargarEmpresas(); // Recargar la lista para reflejar la eliminación |         cargarEmpresas(); // Recargar la lista para reflejar la eliminación | ||||||
|       } catch (err: any) { |       } catch (err: any) { | ||||||
|          console.error("Error al eliminar empresa:", err); |         console.error("Error al eliminar empresa:", err); | ||||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message |         const message = axios.isAxiosError(err) && err.response?.data?.message | ||||||
|                          ? err.response.data.message |           ? err.response.data.message | ||||||
|                          : 'Ocurrió un error inesperado al eliminar la empresa.'; |           : 'Ocurrió un error inesperado al eliminar la empresa.'; | ||||||
|          setApiErrorMessage(message); // Mostrar error de API |         setApiErrorMessage(message); // Mostrar error de API | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     handleMenuClose(); // Cerrar el menú de acciones |     handleMenuClose(); // Cerrar el menú de acciones | ||||||
| @@ -127,115 +131,113 @@ const GestionarEmpresasPage: React.FC = () => { | |||||||
|     setSelectedEmpresaRow(null); |     setSelectedEmpresaRow(null); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|    const handleChangePage = (_event: unknown, newPage: number) => { |   const handleChangePage = (_event: unknown, newPage: number) => { | ||||||
|      setPage(newPage); |     setPage(newPage); | ||||||
|    }; |   }; | ||||||
|  |  | ||||||
|    const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { |   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|      setRowsPerPage(parseInt(event.target.value, 10)); |     setRowsPerPage(parseInt(event.target.value, 10)); | ||||||
|      setPage(0); |     setPage(0); | ||||||
|    }; |   }; | ||||||
|  |  | ||||||
|    // Datos a mostrar en la tabla actual según paginación |   // Datos a mostrar en la tabla actual según paginación | ||||||
|    const displayData = empresas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); |   const displayData = empresas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||||
|  |  | ||||||
|   // Si no tiene permiso para ver, mostrar mensaje y salir |   // Si no tiene permiso para ver, mostrar mensaje y salir | ||||||
|   if (!loading && !puedeVer) { |   if (!loading && !puedeVer) { | ||||||
|       return ( |     return ( | ||||||
|            <Box sx={{ p: 2 }}> |       <Box sx={{ p: 2 }}> | ||||||
|               <Typography variant="h4" gutterBottom>Gestionar Empresas</Typography> |         <Typography variant="h4" gutterBottom>Gestionar Empresas</Typography> | ||||||
|               <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> |         <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> | ||||||
|            </Box> |       </Box> | ||||||
|       ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom> |       <Typography variant="h5" gutterBottom> | ||||||
|         Gestionar Empresas |         Gestionar Empresas | ||||||
|       </Typography> |       </Typography> | ||||||
|  |  | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|          <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}> |         <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}> | ||||||
|             <TextField |           <TextField | ||||||
|                 label="Filtrar por Nombre" |             label="Filtrar por Nombre" | ||||||
|                 variant="outlined" |             variant="outlined" | ||||||
|                 size="small" |             size="small" | ||||||
|                 value={filtroNombre} |             value={filtroNombre} | ||||||
|                 onChange={(e) => setFiltroNombre(e.target.value)} |             onChange={(e) => setFiltroNombre(e.target.value)} | ||||||
|             /> |           /> | ||||||
|          </Box> |         </Box> | ||||||
|          {/* Mostrar botón de agregar solo si tiene permiso */} |         {/* Mostrar botón de agregar solo si tiene permiso */} | ||||||
|          {puedeCrear && ( |         {puedeCrear && ( | ||||||
|              <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> |             <Button | ||||||
|              <Button |               variant="contained" | ||||||
|                variant="contained" |               startIcon={<AddIcon />} | ||||||
|                startIcon={<AddIcon />} |               onClick={() => handleOpenModal()} | ||||||
|                onClick={() => handleOpenModal()} |             > | ||||||
|              > |               Agregar Nueva Empresa | ||||||
|                Agregar Nueva Empresa |             </Button> | ||||||
|              </Button> |  | ||||||
|            </Box> |  | ||||||
|         )} |         )} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
|       {/* Indicador de carga */} |       {/* Indicador de carga */} | ||||||
|       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} |       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} | ||||||
|       {/* Mensaje de error al cargar datos */} |       {/* Mensaje de error al cargar datos */} | ||||||
|       {error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>} |       {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>} | ||||||
|       {/* Mensaje de error de la API (modal/delete) */} |       {/* Mensaje de error de la API (modal/delete) */} | ||||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} |       {apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>} | ||||||
|  |  | ||||||
|       {/* Tabla de datos (solo si no está cargando y no hubo error de carga inicial) */} |       {/* Tabla de datos (solo si no está cargando y no hubo error de carga inicial) */} | ||||||
|       {!loading && !error && ( |       {!loading && !error && ( | ||||||
|          <TableContainer component={Paper}> |         <TableContainer component={Paper}> | ||||||
|            <Table> |           <Table> | ||||||
|              <TableHead> |             <TableHead> | ||||||
|                <TableRow> |               <TableRow> | ||||||
|                  <TableCell>Nombre</TableCell> |                 <TableCell>Nombre</TableCell> | ||||||
|                  <TableCell>Detalle</TableCell> |                 <TableCell>Detalle</TableCell> | ||||||
|                  {/* Mostrar columna de acciones solo si tiene permiso de modificar o eliminar */} |                 {/* Mostrar columna de acciones solo si tiene permiso de modificar o eliminar */} | ||||||
|                  {(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} |                 {(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} | ||||||
|                </TableRow> |               </TableRow> | ||||||
|              </TableHead> |             </TableHead> | ||||||
|              <TableBody> |             <TableBody> | ||||||
|                {displayData.length === 0 && !loading ? ( |               {displayData.length === 0 && !loading ? ( | ||||||
|                   <TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron empresas.</TableCell></TableRow> |                 <TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron empresas.</TableCell></TableRow> | ||||||
|                ) : ( |               ) : ( | ||||||
|                  displayData.map((emp) => ( |                 displayData.map((emp) => ( | ||||||
|                      <TableRow key={emp.idEmpresa}> |                   <TableRow key={emp.idEmpresa}> | ||||||
|                      <TableCell>{emp.nombre}</TableCell> |                     <TableCell>{emp.nombre}</TableCell> | ||||||
|                      <TableCell>{emp.detalle || '-'}</TableCell> |                     <TableCell>{emp.detalle || '-'}</TableCell> | ||||||
|                      {/* Mostrar botón de acciones solo si tiene permiso */} |                     {/* Mostrar botón de acciones solo si tiene permiso */} | ||||||
|                      {(puedeModificar || puedeEliminar) && ( |                     {(puedeModificar || puedeEliminar) && ( | ||||||
|                         <TableCell align="right"> |                       <TableCell align="right"> | ||||||
|                             <IconButton |                         <IconButton | ||||||
|                                 onClick={(e) => handleMenuOpen(e, emp)} |                           onClick={(e) => handleMenuOpen(e, emp)} | ||||||
|                                 // Deshabilitar si no tiene ningún permiso específico (redundante por la condición de la celda, pero seguro) |                           // Deshabilitar si no tiene ningún permiso específico (redundante por la condición de la celda, pero seguro) | ||||||
|                                 disabled={!puedeModificar && !puedeEliminar} |                           disabled={!puedeModificar && !puedeEliminar} | ||||||
|                             > |                         > | ||||||
|                                 <MoreVertIcon /> |                           <MoreVertIcon /> | ||||||
|                             </IconButton> |                         </IconButton> | ||||||
|                         </TableCell> |                       </TableCell> | ||||||
|                      )} |                     )} | ||||||
|                      </TableRow> |                   </TableRow> | ||||||
|                  )) |                 )) | ||||||
|                )} |               )} | ||||||
|              </TableBody> |             </TableBody> | ||||||
|            </Table> |           </Table> | ||||||
|            {/* Paginación */} |           {/* Paginación */} | ||||||
|            <TablePagination |           <TablePagination | ||||||
|              rowsPerPageOptions={[5, 10, 25]} |             rowsPerPageOptions={[5, 10, 25]} | ||||||
|              component="div" |             component="div" | ||||||
|              count={empresas.length} |             count={empresas.length} | ||||||
|              rowsPerPage={rowsPerPage} |             rowsPerPage={rowsPerPage} | ||||||
|              page={page} |             page={page} | ||||||
|              onPageChange={handleChangePage} |             onPageChange={handleChangePage} | ||||||
|              onRowsPerPageChange={handleChangeRowsPerPage} |             onRowsPerPageChange={handleChangeRowsPerPage} | ||||||
|              labelRowsPerPage="Filas por página:" |             labelRowsPerPage="Filas por página:" | ||||||
|            /> |           /> | ||||||
|          </TableContainer> |         </TableContainer> | ||||||
|        )} |       )} | ||||||
|  |  | ||||||
|       {/* Menú contextual para acciones de fila */} |       {/* Menú contextual para acciones de fila */} | ||||||
|       <Menu |       <Menu | ||||||
| @@ -245,15 +247,17 @@ const GestionarEmpresasPage: React.FC = () => { | |||||||
|       > |       > | ||||||
|         {/* Mostrar opción Modificar solo si tiene permiso */} |         {/* Mostrar opción Modificar solo si tiene permiso */} | ||||||
|         {puedeModificar && ( |         {puedeModificar && ( | ||||||
|             <MenuItem onClick={() => { handleOpenModal(selectedEmpresaRow!); handleMenuClose(); }}> |           <MenuItem onClick={() => { handleOpenModal(selectedEmpresaRow!); handleMenuClose(); }}> | ||||||
|                 Modificar |             <ListItemIcon><EditIcon fontSize="small" /></ListItemIcon> | ||||||
|             </MenuItem> |             <ListItemText>Modificar</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {/* Mostrar opción Eliminar solo si tiene permiso */} |         {/* Mostrar opción Eliminar solo si tiene permiso */} | ||||||
|         {puedeEliminar && ( |         {puedeEliminar && ( | ||||||
|             <MenuItem onClick={() => handleDelete(selectedEmpresaRow!.idEmpresa)}> |           <MenuItem onClick={() => handleDelete(selectedEmpresaRow!.idEmpresa)}> | ||||||
|                 Eliminar |             <ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon> | ||||||
|             </MenuItem> |             <ListItemText>Eliminar</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {/* Mensaje si no hay acciones disponibles (por si acaso) */} |         {/* Mensaje si no hay acciones disponibles (por si acaso) */} | ||||||
|         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} |         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| // src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx |  | ||||||
| import React, { useState, useEffect, useCallback } from 'react'; | import React, { useState, useEffect, useCallback } from 'react'; | ||||||
| import { | import { | ||||||
|   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, | ||||||
| @@ -7,27 +6,27 @@ import { | |||||||
|   Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle |   Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||||
|  | import PrintIcon from '@mui/icons-material/Print'; | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||||
| import EditIcon from '@mui/icons-material/Edit'; | import EditIcon from '@mui/icons-material/Edit'; | ||||||
| import DeleteIcon from '@mui/icons-material/Delete'; | import DeleteIcon from '@mui/icons-material/Delete'; | ||||||
| import FilterListIcon from '@mui/icons-material/FilterList'; | import FilterListIcon from '@mui/icons-material/FilterList'; | ||||||
| import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck'; // Para Liquidar | import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck'; | ||||||
|  |  | ||||||
| import entradaSalidaCanillaService from '../../services/Distribucion/entradaSalidaCanillaService'; | import entradaSalidaCanillaService from '../../services/Distribucion/entradaSalidaCanillaService'; | ||||||
| import publicacionService from '../../services/Distribucion/publicacionService'; | import publicacionService from '../../services/Distribucion/publicacionService'; | ||||||
| import canillaService from '../../services/Distribucion/canillaService'; | import canillaService from '../../services/Distribucion/canillaService'; | ||||||
|  |  | ||||||
| import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto'; | import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto'; | ||||||
| import type { CreateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaCanillaDto'; |  | ||||||
| import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto'; | import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto'; | ||||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||||
| import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; | import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; | ||||||
| import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto'; | import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto'; | ||||||
|  |  | ||||||
|  |  | ||||||
| import EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal'; | import EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal'; | ||||||
| import { usePermissions } from '../../hooks/usePermissions'; | import { usePermissions } from '../../hooks/usePermissions'; | ||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
|  | import reportesService from '../../services/Reportes/reportesService'; | ||||||
|  |  | ||||||
| const GestionarEntradasSalidasCanillaPage: React.FC = () => { | const GestionarEntradasSalidasCanillaPage: React.FC = () => { | ||||||
|   const [movimientos, setMovimientos] = useState<EntradaSalidaCanillaDto[]>([]); |   const [movimientos, setMovimientos] = useState<EntradaSalidaCanillaDto[]>([]); | ||||||
| @@ -35,13 +34,12 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | |||||||
|   const [error, setError] = useState<string | null>(null); |   const [error, setError] = useState<string | null>(null); | ||||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); |   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||||
|  |  | ||||||
|   // Filtros |   const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); | ||||||
|   const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState(''); |   const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); | ||||||
|   const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState(''); |  | ||||||
|   const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); |   const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); | ||||||
|   const [filtroIdCanilla, setFiltroIdCanilla] = useState<number | string>(''); |   const [filtroIdCanilla, setFiltroIdCanilla] = useState<number | string>(''); | ||||||
|   const [filtroEstadoLiquidacion, setFiltroEstadoLiquidacion] = useState<'todos' | 'liquidados' | 'noLiquidados'>('noLiquidados'); |   const [filtroEstadoLiquidacion, setFiltroEstadoLiquidacion] = useState<'todos' | 'liquidados' | 'noLiquidados'>('noLiquidados'); | ||||||
|  |   const [loadingTicketPdf, setLoadingTicketPdf] = useState(false); | ||||||
|  |  | ||||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); |   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); | ||||||
|   const [canillitas, setCanillitas] = useState<CanillaDto[]>([]); |   const [canillitas, setCanillitas] = useState<CanillaDto[]>([]); | ||||||
| @@ -58,9 +56,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | |||||||
|   const [fechaLiquidacionDialog, setFechaLiquidacionDialog] = useState<string>(new Date().toISOString().split('T')[0]); |   const [fechaLiquidacionDialog, setFechaLiquidacionDialog] = useState<string>(new Date().toISOString().split('T')[0]); | ||||||
|   const [openLiquidarDialog, setOpenLiquidarDialog] = useState(false); |   const [openLiquidarDialog, setOpenLiquidarDialog] = useState(false); | ||||||
|  |  | ||||||
|  |  | ||||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); |   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||||
|   // MC001 (Ver), MC002 (Crear), MC003 (Modificar), MC004 (Eliminar), MC005 (Liquidar) |  | ||||||
|   const puedeVer = isSuperAdmin || tienePermiso("MC001"); |   const puedeVer = isSuperAdmin || tienePermiso("MC001"); | ||||||
|   const puedeCrear = isSuperAdmin || tienePermiso("MC002"); |   const puedeCrear = isSuperAdmin || tienePermiso("MC002"); | ||||||
|   const puedeModificar = isSuperAdmin || tienePermiso("MC003"); |   const puedeModificar = isSuperAdmin || tienePermiso("MC003"); | ||||||
| @@ -68,6 +64,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | |||||||
|   const puedeLiquidar = isSuperAdmin || tienePermiso("MC005"); |   const puedeLiquidar = isSuperAdmin || tienePermiso("MC005"); | ||||||
|   const puedeEliminarLiquidados = isSuperAdmin || tienePermiso("MC006"); |   const puedeEliminarLiquidados = isSuperAdmin || tienePermiso("MC006"); | ||||||
|  |  | ||||||
|  |   // Función para formatear fechas YYYY-MM-DD a DD/MM/YYYY | ||||||
|  |   const formatDate = (dateString?: string | null): string => { | ||||||
|  |     if (!dateString) return '-'; | ||||||
|  |     const datePart = dateString.split('T')[0]; | ||||||
|  |     const parts = datePart.split('-'); | ||||||
|  |     if (parts.length === 3) { | ||||||
|  |       return `${parts[2]}/${parts[1]}/${parts[0]}`; | ||||||
|  |     } | ||||||
|  |     return datePart; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const fetchFiltersDropdownData = useCallback(async () => { |   const fetchFiltersDropdownData = useCallback(async () => { | ||||||
|     setLoadingFiltersDropdown(true); |     setLoadingFiltersDropdown(true); | ||||||
|     try { |     try { | ||||||
| @@ -120,22 +127,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | |||||||
|   const handleOpenModal = (item?: EntradaSalidaCanillaDto) => { |   const handleOpenModal = (item?: EntradaSalidaCanillaDto) => { | ||||||
|     setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true); |     setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true); | ||||||
|   }; |   }; | ||||||
|   const handleCloseModal = () => { setModalOpen(false); setEditingMovimiento(null); }; |  | ||||||
|  |  | ||||||
|   const handleSubmitModal = async (data: CreateEntradaSalidaCanillaDto | UpdateEntradaSalidaCanillaDto, idParte?: number) => { |  | ||||||
|     setApiErrorMessage(null); |  | ||||||
|     try { |  | ||||||
|       if (idParte && editingMovimiento) { |  | ||||||
|         await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data as UpdateEntradaSalidaCanillaDto); |  | ||||||
|       } else { |  | ||||||
|         await entradaSalidaCanillaService.createEntradaSalidaCanilla(data as CreateEntradaSalidaCanillaDto); |  | ||||||
|       } |  | ||||||
|       cargarMovimientos(); |  | ||||||
|     } 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 (idParte: number) => { |   const handleDelete = async (idParte: number) => { | ||||||
|     if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) { |     if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) { | ||||||
| @@ -147,7 +138,10 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => { |   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => { | ||||||
|     setAnchorEl(event.currentTarget); setSelectedRow(item); |     // Almacenar el idParte en el propio elemento del menú para referencia | ||||||
|  |     event.currentTarget.setAttribute('data-rowid', item.idParte.toString()); | ||||||
|  |     setAnchorEl(event.currentTarget); | ||||||
|  |     setSelectedRow(item); | ||||||
|   }; |   }; | ||||||
|   const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); }; |   const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); }; | ||||||
|  |  | ||||||
| @@ -177,15 +171,63 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | |||||||
|   }; |   }; | ||||||
|   const handleCloseLiquidarDialog = () => setOpenLiquidarDialog(false); |   const handleCloseLiquidarDialog = () => setOpenLiquidarDialog(false); | ||||||
|   const handleConfirmLiquidar = async () => { |   const handleConfirmLiquidar = async () => { | ||||||
|     setApiErrorMessage(null); setLoading(true); |     if (selectedIdsParaLiquidar.size === 0) { | ||||||
|  |         setApiErrorMessage("No hay movimientos seleccionados para liquidar."); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     if (!fechaLiquidacionDialog) { | ||||||
|  |         setApiErrorMessage("Debe seleccionar una fecha de liquidación."); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // --- VALIDACIÓN DE FECHA --- | ||||||
|  |     const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z'); // Usar Z para consistencia con formatDate si es necesario, o T00:00:00 para local | ||||||
|  |  | ||||||
|  |     let fechaMovimientoMasReciente: Date | null = null; | ||||||
|  |  | ||||||
|  |     selectedIdsParaLiquidar.forEach(idParte => { | ||||||
|  |         const movimiento = movimientos.find(m => m.idParte === idParte); | ||||||
|  |         if (movimiento && movimiento.fecha) { // Asegurarse que movimiento.fecha existe | ||||||
|  |             const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z'); // Consistencia con Z | ||||||
|  |             if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando getTime() | ||||||
|  |                 fechaMovimientoMasReciente = movFecha; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando getTime() | ||||||
|  |         setApiErrorMessage(`La fecha de liquidación (${fechaLiquidacionDate.toLocaleDateString('es-AR', {timeZone: 'UTC'})}) no puede ser inferior a la fecha del movimiento más reciente a liquidar (${(fechaMovimientoMasReciente as Date).toLocaleDateString('es-AR', {timeZone: 'UTC'})}).`); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     setApiErrorMessage(null); | ||||||
|  |     setLoading(true); // Usar el loading general para la operación de liquidar | ||||||
|  |  | ||||||
|     const liquidarDto: LiquidarMovimientosCanillaRequestDto = { |     const liquidarDto: LiquidarMovimientosCanillaRequestDto = { | ||||||
|       idsPartesALiquidar: Array.from(selectedIdsParaLiquidar), |       idsPartesALiquidar: Array.from(selectedIdsParaLiquidar), | ||||||
|       fechaLiquidacion: fechaLiquidacionDialog |       fechaLiquidacion: fechaLiquidacionDialog // El backend espera YYYY-MM-DD | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto); |       await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto); | ||||||
|       cargarMovimientos(); // Recargar para ver los cambios |  | ||||||
|       setOpenLiquidarDialog(false);  |       setOpenLiquidarDialog(false);  | ||||||
|  |        | ||||||
|  |       const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0]; | ||||||
|  |       const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado); | ||||||
|  |  | ||||||
|  |       await cargarMovimientos();  | ||||||
|  |  | ||||||
|  |       if (movimientoParaTicket) { | ||||||
|  |         console.log("Liquidación exitosa, intentando generar ticket para canillita:", movimientoParaTicket.idCanilla); | ||||||
|  |         await handleImprimirTicketLiquidacion( | ||||||
|  |             movimientoParaTicket.idCanilla, | ||||||
|  |             fechaLiquidacionDialog,  | ||||||
|  |             movimientoParaTicket.canillaEsAccionista | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         console.warn("No se pudo encontrar información del movimiento para generar el ticket post-liquidación."); | ||||||
|  |       } | ||||||
|  |  | ||||||
|     } catch (err: any) { |     } catch (err: any) { | ||||||
|       const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al liquidar.'; |       const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al liquidar.'; | ||||||
|       setApiErrorMessage(msg);  |       setApiErrorMessage(msg);  | ||||||
| @@ -194,23 +236,85 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   // Esta función se pasa al modal para que la invoque al hacer submit en MODO EDICIÓN | ||||||
|  |   const handleModalEditSubmit = async (data: UpdateEntradaSalidaCanillaDto, idParte: number) => { | ||||||
|  |     setApiErrorMessage(null); | ||||||
|  |     try { | ||||||
|  |       await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data); | ||||||
|  |     } catch (err: any) { | ||||||
|  |       const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar los cambios.'; | ||||||
|  |       setApiErrorMessage(message); | ||||||
|  |       throw err; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const handleCloseModal = () => { | ||||||
|  |     setModalOpen(false); | ||||||
|  |     setEditingMovimiento(null); | ||||||
|  |     // Recargar siempre que se cierre el modal y no haya un error pendiente a nivel de página | ||||||
|  |     // Opcionalmente, podrías tener una bandera ' cambiosGuardados' que el modal active | ||||||
|  |     // para ser más selectivo con la recarga. | ||||||
|  |     if (!apiErrorMessage) { | ||||||
|  |       cargarMovimientos(); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const handleImprimirTicketLiquidacion = useCallback(async ( | ||||||
|  |     // Parámetros necesarios para el ticket | ||||||
|  |     idCanilla: number, | ||||||
|  |     fecha: string, // Fecha para la que se genera el ticket (probablemente fechaLiquidacionDialog) | ||||||
|  |     esAccionista: boolean | ||||||
|  |   ) => { | ||||||
|  |     setLoadingTicketPdf(true); | ||||||
|  |     setApiErrorMessage(null); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       const params = { | ||||||
|  |         fecha: fecha.split('T')[0], // Asegurar formato YYYY-MM-DD | ||||||
|  |         idCanilla: idCanilla, | ||||||
|  |         esAccionista: esAccionista, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const blob = await reportesService.getTicketLiquidacionCanillaPdf(params); | ||||||
|  |  | ||||||
|  |       if (blob.type === "application/json") { | ||||||
|  |         const text = await blob.text(); | ||||||
|  |         const msg = JSON.parse(text).message ?? "Error inesperado al generar el ticket PDF."; | ||||||
|  |         setApiErrorMessage(msg); | ||||||
|  |       } else { | ||||||
|  |         const url = URL.createObjectURL(blob); | ||||||
|  |         const w = window.open(url, '_blank'); | ||||||
|  |         if (!w) alert("Permita popups para ver el PDF del ticket."); | ||||||
|  |       } | ||||||
|  |     } catch (error: any) { | ||||||
|  |       console.error("Error al generar ticket de liquidación:", error); | ||||||
|  |       const message = axios.isAxiosError(error) && error.response?.data?.message | ||||||
|  |         ? error.response.data.message | ||||||
|  |         : 'Ocurrió un error al generar el ticket.'; | ||||||
|  |       setApiErrorMessage(message); | ||||||
|  |     } finally { | ||||||
|  |       setLoadingTicketPdf(false); | ||||||
|  |       // No cerramos el menú aquí si se llama desde handleConfirmLiquidar | ||||||
|  |     } | ||||||
|  |   }, []); // Dependencias vacías si no usa nada del scope exterior que cambie, o añadir si es necesario | ||||||
|  |  | ||||||
|  |  | ||||||
|   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); |   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||||
|   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { |   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|     setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); |     setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); | ||||||
|   }; |   }; | ||||||
|   const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); |   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>; |   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||||
|  |  | ||||||
|   const numSelectedToLiquidate = selectedIdsParaLiquidar.size; |   const numSelectedToLiquidate = selectedIdsParaLiquidar.size; | ||||||
|  |   // Corregido: numNotLiquidatedOnPage debe calcularse sobre 'movimientos' filtrados, no solo 'displayData' | ||||||
|  |   // O, si la selección es solo por página, displayData está bien. Asumamos selección por página por ahora. | ||||||
|   const numNotLiquidatedOnPage = displayData.filter(m => !m.liquidado).length; |   const numNotLiquidatedOnPage = displayData.filter(m => !m.liquidado).length; | ||||||
|  |  | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Entradas/Salidas Canillitas</Typography> |       <Typography variant="h5" gutterBottom>Entradas/Salidas Canillitas</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|         <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography> |         <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography> | ||||||
|         <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}> |         <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}> | ||||||
| @@ -250,36 +354,62 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | |||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
|       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} |       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} | ||||||
|       {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>} |       {error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>} | ||||||
|       {apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>} |       {apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>} | ||||||
|  |       {loadingTicketPdf && | ||||||
|  |         <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}> | ||||||
|  |           <CircularProgress size={20} sx={{ mr: 1 }} /> | ||||||
|  |           <Typography variant="body2">Cargando ticket...</Typography> | ||||||
|  |         </Box> | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |  | ||||||
|       {!loading && !error && puedeVer && ( |       {!loading && !error && puedeVer && ( | ||||||
|         <TableContainer component={Paper}> |         <TableContainer component={Paper}> | ||||||
|           <Table size="small"> |           <Table size="small"> | ||||||
|             <TableHead><TableRow> |             <TableHead> | ||||||
|               {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && |               <TableRow> | ||||||
|                 <TableCell padding="checkbox"> |                 {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && ( | ||||||
|                   <Checkbox |                   <TableCell padding="checkbox"> | ||||||
|                     indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage} |                     <Checkbox | ||||||
|                     checked={numNotLiquidatedOnPage > 0 && numSelectedToLiquidate === numNotLiquidatedOnPage} |                       indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage && numNotLiquidatedOnPage > 0} | ||||||
|                     onChange={handleSelectAllForLiquidar} |                       checked={numNotLiquidatedOnPage > 0 && numSelectedToLiquidate === numNotLiquidatedOnPage} | ||||||
|                     disabled={numNotLiquidatedOnPage === 0} |                       onChange={handleSelectAllForLiquidar} | ||||||
|                   /> |                       disabled={numNotLiquidatedOnPage === 0} | ||||||
|                 </TableCell> |                     /> | ||||||
|               } |                   </TableCell> | ||||||
|               <TableCell>Fecha</TableCell><TableCell>Publicación</TableCell><TableCell>Canillita</TableCell> |                 )} | ||||||
|               <TableCell align="right">Salida</TableCell><TableCell align="right">Entrada</TableCell> |                 <TableCell>Fecha</TableCell> | ||||||
|               <TableCell align="right">Vendidos</TableCell><TableCell align="right">A Rendir</TableCell> |                 <TableCell>Publicación</TableCell> | ||||||
|               <TableCell>Liquidado</TableCell><TableCell>F. Liq.</TableCell><TableCell>Obs.</TableCell> |                 <TableCell>Canillita</TableCell> | ||||||
|               {(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} |                 <TableCell align="right">Salida</TableCell> | ||||||
|             </TableRow></TableHead> |                 <TableCell align="right">Entrada</TableCell> | ||||||
|  |                 <TableCell align="right">Vendidos</TableCell> | ||||||
|  |                 <TableCell align="right">A Rendir</TableCell> | ||||||
|  |                 <TableCell>Liquidado</TableCell> | ||||||
|  |                 <TableCell>F. Liq.</TableCell> | ||||||
|  |                 <TableCell>Obs.</TableCell> | ||||||
|  |                 {(puedeModificar || puedeEliminar || puedeLiquidar) && <TableCell align="right">Acciones</TableCell>} | ||||||
|  |               </TableRow> | ||||||
|  |             </TableHead> | ||||||
|             <TableBody> |             <TableBody> | ||||||
|               {displayData.length === 0 ? ( |               {displayData.length === 0 ? ( | ||||||
|                 <TableRow><TableCell colSpan={puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' ? 12 : 11} align="center">No se encontraron movimientos.</TableCell></TableRow> |                 <TableRow> | ||||||
|  |                   <TableCell | ||||||
|  |                     colSpan={ | ||||||
|  |                       (puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' ? 1 : 0) + | ||||||
|  |                       9 + | ||||||
|  |                       ((puedeModificar || puedeEliminar || puedeLiquidar) ? 1 : 0) | ||||||
|  |                     } | ||||||
|  |                     align="center" | ||||||
|  |                   > | ||||||
|  |                     No se encontraron movimientos. | ||||||
|  |                   </TableCell> | ||||||
|  |                 </TableRow> | ||||||
|               ) : ( |               ) : ( | ||||||
|                 displayData.map((m) => ( |                 displayData.map((m) => ( | ||||||
|                   <TableRow key={m.idParte} hover selected={selectedIdsParaLiquidar.has(m.idParte)}> |                   <TableRow key={m.idParte} hover selected={selectedIdsParaLiquidar.has(m.idParte)}> | ||||||
|                     {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && |                     {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && ( | ||||||
|                       <TableCell padding="checkbox"> |                       <TableCell padding="checkbox"> | ||||||
|                         <Checkbox |                         <Checkbox | ||||||
|                           checked={selectedIdsParaLiquidar.has(m.idParte)} |                           checked={selectedIdsParaLiquidar.has(m.idParte)} | ||||||
| @@ -287,29 +417,34 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | |||||||
|                           disabled={m.liquidado} |                           disabled={m.liquidado} | ||||||
|                         /> |                         /> | ||||||
|                       </TableCell> |                       </TableCell> | ||||||
|                     } |                     )} | ||||||
|                     <TableCell>{formatDate(m.fecha)}</TableCell> |                     <TableCell>{formatDate(m.fecha)}</TableCell> | ||||||
|                     <TableCell>{m.nombrePublicacion}</TableCell> |                     <TableCell>{m.nombrePublicacion}</TableCell> | ||||||
|                     <TableCell>{m.nomApeCanilla}</TableCell> |                     <TableCell>{m.nomApeCanilla}</TableCell> | ||||||
|                     <TableCell align="right">{m.cantSalida}</TableCell> |                     <TableCell align="right">{m.cantSalida}</TableCell> | ||||||
|                     <TableCell align="right">{m.cantEntrada}</TableCell> |                     <TableCell align="right">{m.cantEntrada}</TableCell> | ||||||
|                     <TableCell align="right" sx={{ fontWeight: 'bold' }}>{m.vendidos}</TableCell> |                     <TableCell align="right" sx={{ fontWeight: 'bold' }}>{m.vendidos}</TableCell> | ||||||
|                     <TableCell align="right" sx={{ fontWeight: 'bold' }}>${m.montoARendir.toFixed(2)}</TableCell> |                     <TableCell align="right" sx={{ fontWeight: 'bold' }}> | ||||||
|  |                       {m.montoARendir.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })} | ||||||
|  |                     </TableCell> | ||||||
|                     <TableCell align="center">{m.liquidado ? <Chip label="Sí" color="success" size="small" /> : <Chip label="No" size="small" />}</TableCell> |                     <TableCell align="center">{m.liquidado ? <Chip label="Sí" color="success" size="small" /> : <Chip label="No" size="small" />}</TableCell> | ||||||
|                     <TableCell>{m.fechaLiquidado ? formatDate(m.fechaLiquidado) : '-'}</TableCell> |                     <TableCell>{m.fechaLiquidado ? formatDate(m.fechaLiquidado) : '-'}</TableCell> | ||||||
|                     <TableCell><Tooltip title={m.observacion || ''}><Box sx={{ maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.observacion || '-'}</Box></Tooltip></TableCell> |                     <TableCell> | ||||||
|                     {(puedeModificar || puedeEliminar) && ( |                       <Tooltip title={m.observacion || ''}> | ||||||
|  |                         <Box sx={{ maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> | ||||||
|  |                           {m.observacion || '-'} | ||||||
|  |                         </Box> | ||||||
|  |                       </Tooltip> | ||||||
|  |                     </TableCell> | ||||||
|  |                     {(puedeModificar || puedeEliminar || puedeLiquidar) && ( | ||||||
|                       <TableCell align="right"> |                       <TableCell align="right"> | ||||||
|                         <IconButton |                         <IconButton | ||||||
|         onClick={(e) => handleMenuOpen(e, m)}  |                           onClick={(e) => handleMenuOpen(e, m)} | ||||||
|         disabled={ |                           data-rowid={m.idParte.toString()} // Guardar el id de la fila aquí | ||||||
|             // Deshabilitar si no tiene ningún permiso de eliminación O |                           disabled={m.liquidado && !puedeEliminarLiquidados && !puedeLiquidar} // Lógica simplificada, refinar si es necesario | ||||||
|             // si está liquidado y no tiene permiso para eliminar liquidados |                         > | ||||||
|             !((!m.liquidado && puedeEliminar) || (m.liquidado && puedeEliminarLiquidados)) |                           <MoreVertIcon /> | ||||||
|         } |                         </IconButton> | ||||||
|     > |  | ||||||
|         <MoreVertIcon /> |  | ||||||
|     </IconButton> |  | ||||||
|                       </TableCell> |                       </TableCell> | ||||||
|                     )} |                     )} | ||||||
|                   </TableRow> |                   </TableRow> | ||||||
| @@ -327,18 +462,45 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | |||||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> |       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||||
|         {puedeModificar && selectedRow && !selectedRow.liquidado && ( |         {puedeModificar && selectedRow && !selectedRow.liquidado && ( | ||||||
|           <MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)} |           <MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)} | ||||||
|         {selectedRow && ( |  | ||||||
|           (!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados) |         {/* Opción de Imprimir Ticket Liq. */} | ||||||
|  |         {selectedRow && selectedRow.liquidado && ( // Solo mostrar si ya está liquidado (para reimprimir) | ||||||
|  |           <MenuItem | ||||||
|  |             onClick={() => { | ||||||
|  |               if (selectedRow) { // selectedRow no será null aquí debido a la condición anterior | ||||||
|  |                 handleImprimirTicketLiquidacion( | ||||||
|  |                   selectedRow.idCanilla, | ||||||
|  |                   selectedRow.fechaLiquidado || selectedRow.fecha, // Usar fechaLiquidado si existe, sino la fecha del movimiento | ||||||
|  |                   selectedRow.canillaEsAccionista | ||||||
|  |                 ); | ||||||
|  |               } | ||||||
|  |               // handleMenuClose() es llamado por handleImprimirTicketLiquidacion | ||||||
|  |             }} | ||||||
|  |             disabled={loadingTicketPdf} | ||||||
|  |           > | ||||||
|  |             <PrintIcon fontSize="small" sx={{ mr: 1 }} /> | ||||||
|  |             {loadingTicketPdf && <CircularProgress size={16} sx={{ mr: 1 }} />} | ||||||
|  |             Reimprimir Ticket Liq. | ||||||
|  |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |  | ||||||
|  |         {selectedRow && ( // Opción de Eliminar | ||||||
|  |           ((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados)) | ||||||
|         ) && ( |         ) && ( | ||||||
|             <MenuItem onClick={() => handleDelete(selectedRow.idParte)}> |             <MenuItem onClick={() => { | ||||||
|  |               if (selectedRow) handleDelete(selectedRow.idParte); | ||||||
|  |             }}> | ||||||
|               <DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar |               <DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar | ||||||
|             </MenuItem> |             </MenuItem> | ||||||
|           )} |           )} | ||||||
|       </Menu> |       </Menu> | ||||||
|  |  | ||||||
|       <EntradaSalidaCanillaFormModal |       <EntradaSalidaCanillaFormModal | ||||||
|         open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal} |         open={modalOpen} | ||||||
|         initialData={editingMovimiento} errorMessage={apiErrorMessage} |         onClose={handleCloseModal} | ||||||
|  |         onSubmit={handleModalEditSubmit} | ||||||
|  |         initialData={editingMovimiento} | ||||||
|  |         errorMessage={apiErrorMessage} | ||||||
|         clearErrorMessage={() => setApiErrorMessage(null)} |         clearErrorMessage={() => setApiErrorMessage(null)} | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| // src/pages/Distribucion/GestionarEntradasSalidasDistPage.tsx |  | ||||||
| import React, { useState, useEffect, useCallback } from 'react'; | import React, { useState, useEffect, useCallback } from 'react'; | ||||||
| import { | import { | ||||||
|   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, | ||||||
| @@ -31,14 +30,12 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { | |||||||
|   const [error, setError] = useState<string | null>(null); |   const [error, setError] = useState<string | null>(null); | ||||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); |   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||||
|  |  | ||||||
|   // Filtros |   const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); | ||||||
|   const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState(''); |   const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); | ||||||
|   const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState(''); |  | ||||||
|   const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); |   const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); | ||||||
|   const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>(''); |   const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>(''); | ||||||
|   const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>(''); |   const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>(''); | ||||||
|  |  | ||||||
|  |  | ||||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); |   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); | ||||||
|   const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]); |   const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]); | ||||||
|   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); |   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); | ||||||
| @@ -55,6 +52,19 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { | |||||||
|   const puedeVer = isSuperAdmin || tienePermiso("MD001"); |   const puedeVer = isSuperAdmin || tienePermiso("MD001"); | ||||||
|   const puedeGestionar = isSuperAdmin || tienePermiso("MD002"); |   const puedeGestionar = isSuperAdmin || tienePermiso("MD002"); | ||||||
|  |  | ||||||
|  |   // CORREGIDO: Función para formatear la fecha | ||||||
|  |   const formatDate = (dateString?: string | null): string => { | ||||||
|  |     if (!dateString) return '-'; | ||||||
|  |     // Asumimos que dateString viene del backend como "YYYY-MM-DD" o "YYYY-MM-DDTHH:mm:ss..." | ||||||
|  |     const datePart = dateString.split('T')[0]; // Tomar solo la parte YYYY-MM-DD | ||||||
|  |     const parts = datePart.split('-'); | ||||||
|  |     if (parts.length === 3) { | ||||||
|  |       // parts[0] = YYYY, parts[1] = MM, parts[2] = DD | ||||||
|  |       return `${parts[2]}/${parts[1]}/${parts[0]}`; // Formato DD/MM/YYYY | ||||||
|  |     } | ||||||
|  |     return datePart; // Fallback si el formato no es el esperado | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const fetchFiltersDropdownData = useCallback(async () => { |   const fetchFiltersDropdownData = useCallback(async () => { | ||||||
|     setLoadingFiltersDropdown(true); |     setLoadingFiltersDropdown(true); | ||||||
|     try { |     try { | ||||||
| @@ -126,16 +136,16 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { | |||||||
|  |  | ||||||
|   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); |   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||||
|   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { |   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|     setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); |     setRowsPerPage(parseInt(event.target.value, 25)); setPage(0); | ||||||
|   }; |   }; | ||||||
|   const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); |   const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||||
|   const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; |   // La función formatDate ya está definida arriba. | ||||||
|  |  | ||||||
|   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; |   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Entradas/Salidas a Distribuidores</Typography> |       <Typography variant="h5" gutterBottom>Entradas/Salidas a Distribuidores</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|         <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography> |         <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography> | ||||||
|         <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}> |         <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}> | ||||||
| @@ -187,6 +197,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { | |||||||
|               ) : ( |               ) : ( | ||||||
|                 displayData.map((m) => ( |                 displayData.map((m) => ( | ||||||
|                   <TableRow key={m.idParte} hover> |                   <TableRow key={m.idParte} hover> | ||||||
|  |                     {/* Usar la función formatDate aquí */} | ||||||
|                     <TableCell>{formatDate(m.fecha)}</TableCell> |                     <TableCell>{formatDate(m.fecha)}</TableCell> | ||||||
|                     <TableCell>{m.nombrePublicacion} <Chip label={m.nombreEmpresaPublicacion} size="small" variant="outlined" sx={{ ml: 0.5 }} /></TableCell> |                     <TableCell>{m.nombrePublicacion} <Chip label={m.nombreEmpresaPublicacion} size="small" variant="outlined" sx={{ ml: 0.5 }} /></TableCell> | ||||||
|                     <TableCell>{m.nombreDistribuidor}</TableCell> |                     <TableCell>{m.nombreDistribuidor}</TableCell> | ||||||
| @@ -196,7 +207,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { | |||||||
|                     <TableCell align="right">{m.cantidad}</TableCell> |                     <TableCell align="right">{m.cantidad}</TableCell> | ||||||
|                     <TableCell>{m.remito}</TableCell> |                     <TableCell>{m.remito}</TableCell> | ||||||
|                     <TableCell align="right" sx={{ fontWeight: 'bold', color: m.tipoMovimiento === 'Salida' ? 'success.main' : (m.montoCalculado === 0 ? 'inherit' : 'error.main') }}> |                     <TableCell align="right" sx={{ fontWeight: 'bold', color: m.tipoMovimiento === 'Salida' ? 'success.main' : (m.montoCalculado === 0 ? 'inherit' : 'error.main') }}> | ||||||
|                       {m.tipoMovimiento === 'Salida' ? '$'+m.montoCalculado.toFixed(2) : '$-'+m.montoCalculado.toFixed(2) }            |                       {(m.tipoMovimiento === 'Salida' ? '$' : '$-') + m.montoCalculado.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} | ||||||
|                     </TableCell> |                     </TableCell> | ||||||
|                     <TableCell><Tooltip title={m.observacion || ''}><Box sx={{ maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.observacion || '-'}</Box></Tooltip></TableCell> |                     <TableCell><Tooltip title={m.observacion || ''}><Box sx={{ maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.observacion || '-'}</Box></Tooltip></TableCell> | ||||||
|                     {puedeGestionar && ( |                     {puedeGestionar && ( | ||||||
|   | |||||||
| @@ -1,11 +1,15 @@ | |||||||
| import React, { useState, useEffect, useCallback } from 'react'; | import React, { useState, useEffect, useCallback } from 'react'; | ||||||
| import { | import { | ||||||
|     Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||||
|     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|     CircularProgress, Alert |   CircularProgress, Alert, | ||||||
|  |   ListItemIcon, | ||||||
|  |   ListItemText | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar | ||||||
| import otroDestinoService from '../../services/Distribucion/otroDestinoService'; | import otroDestinoService from '../../services/Distribucion/otroDestinoService'; | ||||||
| import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto'; | import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto'; | ||||||
| import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto'; | import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto'; | ||||||
| @@ -40,9 +44,9 @@ const GestionarOtrosDestinosPage: React.FC = () => { | |||||||
|  |  | ||||||
|   const cargarOtrosDestinos = useCallback(async () => { |   const cargarOtrosDestinos = useCallback(async () => { | ||||||
|     if (!puedeVer) { |     if (!puedeVer) { | ||||||
|         setError("No tiene permiso para ver esta sección."); |       setError("No tiene permiso para ver esta sección."); | ||||||
|         setLoading(false); |       setLoading(false); | ||||||
|         return; |       return; | ||||||
|     } |     } | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     setError(null); |     setError(null); | ||||||
| @@ -84,8 +88,8 @@ const GestionarOtrosDestinosPage: React.FC = () => { | |||||||
|       cargarOtrosDestinos(); |       cargarOtrosDestinos(); | ||||||
|     } catch (err: any) { |     } catch (err: any) { | ||||||
|       const message = axios.isAxiosError(err) && err.response?.data?.message |       const message = axios.isAxiosError(err) && err.response?.data?.message | ||||||
|                         ? err.response.data.message |         ? err.response.data.message | ||||||
|                         : 'Ocurrió un error inesperado al guardar el destino.'; |         : 'Ocurrió un error inesperado al guardar el destino.'; | ||||||
|       setApiErrorMessage(message); |       setApiErrorMessage(message); | ||||||
|       throw err; |       throw err; | ||||||
|     } |     } | ||||||
| @@ -93,15 +97,15 @@ const GestionarOtrosDestinosPage: React.FC = () => { | |||||||
|  |  | ||||||
|   const handleDelete = async (id: number) => { |   const handleDelete = async (id: number) => { | ||||||
|     if (window.confirm(`¿Está seguro de que desea eliminar este destino (ID: ${id})?`)) { |     if (window.confirm(`¿Está seguro de que desea eliminar este destino (ID: ${id})?`)) { | ||||||
|        setApiErrorMessage(null); |       setApiErrorMessage(null); | ||||||
|        try { |       try { | ||||||
|         await otroDestinoService.deleteOtroDestino(id); |         await otroDestinoService.deleteOtroDestino(id); | ||||||
|         cargarOtrosDestinos(); |         cargarOtrosDestinos(); | ||||||
|       } catch (err: any) { |       } catch (err: any) { | ||||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message |         const message = axios.isAxiosError(err) && err.response?.data?.message | ||||||
|                          ? err.response.data.message |           ? err.response.data.message | ||||||
|                          : 'Ocurrió un error inesperado al eliminar el destino.'; |           : 'Ocurrió un error inesperado al eliminar el destino.'; | ||||||
|          setApiErrorMessage(message); |         setApiErrorMessage(message); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     handleMenuClose(); |     handleMenuClose(); | ||||||
| @@ -117,98 +121,102 @@ const GestionarOtrosDestinosPage: React.FC = () => { | |||||||
|     setSelectedDestinoRow(null); |     setSelectedDestinoRow(null); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|    const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); |   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||||
|    const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { |   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|      setRowsPerPage(parseInt(event.target.value, 10)); |     setRowsPerPage(parseInt(event.target.value, 10)); | ||||||
|      setPage(0); |     setPage(0); | ||||||
|    }; |   }; | ||||||
|  |  | ||||||
|    const displayData = otrosDestinos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); |   const displayData = otrosDestinos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||||
|  |  | ||||||
|   if (!loading && !puedeVer) { |   if (!loading && !puedeVer) { | ||||||
|       return ( |     return ( | ||||||
|            <Box sx={{ p: 2 }}> |       <Box sx={{ p: 2 }}> | ||||||
|               <Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography> |         <Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography> | ||||||
|               <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> |         <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> | ||||||
|            </Box> |       </Box> | ||||||
|       ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography> |       <Typography variant="h5" gutterBottom>Gestionar Otros Destinos</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|          <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}> |         <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}> | ||||||
|             <TextField |           <TextField | ||||||
|                 label="Filtrar por Nombre" |             label="Filtrar por Nombre" | ||||||
|                 variant="outlined" |             variant="outlined" | ||||||
|                 size="small" |             size="small" | ||||||
|                 value={filtroNombre} |             value={filtroNombre} | ||||||
|                 onChange={(e) => setFiltroNombre(e.target.value)} |             onChange={(e) => setFiltroNombre(e.target.value)} | ||||||
|             /> |           /> | ||||||
|          </Box> |         </Box> | ||||||
|          {puedeCrear && ( |         {puedeCrear && ( | ||||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> |             <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}> | ||||||
|              <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}> |               Agregar Nuevo Destino | ||||||
|                 Agregar Nuevo Destino |             </Button> | ||||||
|              </Button> |  | ||||||
|           </Box> |  | ||||||
|         )} |         )} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
|       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} |       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} | ||||||
|       {error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>} |       {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>} | ||||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} |       {apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>} | ||||||
|  |  | ||||||
|       {!loading && !error && ( |       {!loading && !error && ( | ||||||
|          <TableContainer component={Paper}> |         <TableContainer component={Paper}> | ||||||
|            <Table> |           <Table> | ||||||
|              <TableHead> |             <TableHead> | ||||||
|                <TableRow> |               <TableRow> | ||||||
|                  <TableCell>Nombre</TableCell> |                 <TableCell>Nombre</TableCell> | ||||||
|                  <TableCell>Observación</TableCell> |                 <TableCell>Observación</TableCell> | ||||||
|                  {(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} |                 {(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} | ||||||
|                </TableRow> |               </TableRow> | ||||||
|              </TableHead> |             </TableHead> | ||||||
|              <TableBody> |             <TableBody> | ||||||
|                {displayData.length === 0 && !loading ? ( |               {displayData.length === 0 && !loading ? ( | ||||||
|                   <TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron otros destinos.</TableCell></TableRow> |                 <TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">No se encontraron otros destinos.</TableCell></TableRow> | ||||||
|                ) : ( |               ) : ( | ||||||
|                  displayData.map((destino) => ( |                 displayData.map((destino) => ( | ||||||
|                      <TableRow key={destino.idDestino}> |                   <TableRow key={destino.idDestino}> | ||||||
|                      <TableCell>{destino.nombre}</TableCell> |                     <TableCell>{destino.nombre}</TableCell> | ||||||
|                      <TableCell>{destino.obs || '-'}</TableCell> |                     <TableCell>{destino.obs || '-'}</TableCell> | ||||||
|                      {(puedeModificar || puedeEliminar) && ( |                     {(puedeModificar || puedeEliminar) && ( | ||||||
|                         <TableCell align="right"> |                       <TableCell align="right"> | ||||||
|                             <IconButton onClick={(e) => handleMenuOpen(e, destino)} disabled={!puedeModificar && !puedeEliminar}> |                         <IconButton onClick={(e) => handleMenuOpen(e, destino)} disabled={!puedeModificar && !puedeEliminar}> | ||||||
|                                 <MoreVertIcon /> |                           <MoreVertIcon /> | ||||||
|                             </IconButton> |                         </IconButton> | ||||||
|                         </TableCell> |                       </TableCell> | ||||||
|                      )} |                     )} | ||||||
|                      </TableRow> |                   </TableRow> | ||||||
|                  )) |                 )) | ||||||
|                )} |               )} | ||||||
|              </TableBody> |             </TableBody> | ||||||
|            </Table> |           </Table> | ||||||
|            <TablePagination |           <TablePagination | ||||||
|              rowsPerPageOptions={[5, 10, 25]} |             rowsPerPageOptions={[5, 10, 25]} | ||||||
|              component="div" |             component="div" | ||||||
|              count={otrosDestinos.length} |             count={otrosDestinos.length} | ||||||
|              rowsPerPage={rowsPerPage} |             rowsPerPage={rowsPerPage} | ||||||
|              page={page} |             page={page} | ||||||
|              onPageChange={handleChangePage} |             onPageChange={handleChangePage} | ||||||
|              onRowsPerPageChange={handleChangeRowsPerPage} |             onRowsPerPageChange={handleChangeRowsPerPage} | ||||||
|              labelRowsPerPage="Filas por página:" |             labelRowsPerPage="Filas por página:" | ||||||
|            /> |           /> | ||||||
|          </TableContainer> |         </TableContainer> | ||||||
|        )} |       )} | ||||||
|  |  | ||||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> |       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||||
|         {puedeModificar && ( |         {puedeModificar && ( | ||||||
|             <MenuItem onClick={() => { handleOpenModal(selectedDestinoRow!); handleMenuClose(); }}>Modificar</MenuItem> |           <MenuItem onClick={() => { handleOpenModal(selectedDestinoRow!); handleMenuClose(); }}> | ||||||
|  |             <ListItemIcon><EditIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Modificar</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {puedeEliminar && ( |         {puedeEliminar && ( | ||||||
|             <MenuItem onClick={() => handleDelete(selectedDestinoRow!.idDestino)}>Eliminar</MenuItem> |           <MenuItem onClick={() => handleDelete(selectedDestinoRow!.idDestino)}> | ||||||
|  |             <ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Eliminar</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} |         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} | ||||||
|       </Menu> |       </Menu> | ||||||
|   | |||||||
| @@ -3,15 +3,26 @@ import { | |||||||
|   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch, | ||||||
|   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|   CircularProgress, Alert, Chip, FormControl, InputLabel, Select, Tooltip, |   CircularProgress, Alert, Chip, FormControl, InputLabel, Select, Tooltip, | ||||||
|   FormControlLabel |   FormControlLabel, | ||||||
|  |   ListItemText, | ||||||
|  |   ListItemIcon | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para el menú de acciones | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar | ||||||
|  | import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; // Icono para días de semana | ||||||
|  | import LocalOfferIcon from '@mui/icons-material/LocalOffer'; // Para Precios | ||||||
|  | import AddCardIcon from '@mui/icons-material/AddCard';         // Para Recargos | ||||||
|  | import PercentIcon from '@mui/icons-material/Percent';       // Para Porcentajes | ||||||
|  | import RequestQuoteIcon from '@mui/icons-material/RequestQuote'; // Para Porc./Monto Canillita | ||||||
|  | import ViewQuiltIcon from '@mui/icons-material/ViewQuilt';     // Para Secciones | ||||||
| import publicacionService from '../../services/Distribucion/publicacionService'; | import publicacionService from '../../services/Distribucion/publicacionService'; | ||||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||||
| import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto'; | import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto'; | ||||||
| import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto'; | import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto'; | ||||||
| import PublicacionFormModal from '../../components/Modals/Distribucion/PublicacionFormModal'; | import PublicacionFormModal from '../../components/Modals/Distribucion/PublicacionFormModal'; | ||||||
|  | import PublicacionDiasSemanaModal from '../../components/Modals/Distribucion/PublicacionDiasSemanaModal'; | ||||||
| import { usePermissions } from '../../hooks/usePermissions'; | import { usePermissions } from '../../hooks/usePermissions'; | ||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
| import { useNavigate } from 'react-router-dom'; | import { useNavigate } from 'react-router-dom'; | ||||||
| @@ -40,9 +51,11 @@ const GestionarPublicacionesPage: React.FC = () => { | |||||||
|   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); |   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||||
|   const [selectedPublicacionRow, setSelectedPublicacionRow] = useState<PublicacionDto | null>(null); |   const [selectedPublicacionRow, setSelectedPublicacionRow] = useState<PublicacionDto | null>(null); | ||||||
|  |  | ||||||
|  |   const [diasSemanaModalOpen, setDiasSemanaModalOpen] = useState(false); | ||||||
|  |   const [selectedPublicacionParaDias, setSelectedPublicacionParaDias] = useState<PublicacionDto | null>(null); | ||||||
|  |  | ||||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); |   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|  |  | ||||||
|   const puedeVer = isSuperAdmin || tienePermiso("DP001"); |   const puedeVer = isSuperAdmin || tienePermiso("DP001"); | ||||||
|   const puedeCrear = isSuperAdmin || tienePermiso("DP002"); |   const puedeCrear = isSuperAdmin || tienePermiso("DP002"); | ||||||
|   const puedeModificar = isSuperAdmin || tienePermiso("DP003"); |   const puedeModificar = isSuperAdmin || tienePermiso("DP003"); | ||||||
| @@ -179,6 +192,23 @@ const GestionarPublicacionesPage: React.FC = () => { | |||||||
|     handleMenuClose(); |     handleMenuClose(); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const handleOpenDiasSemanaModal = (publicacion: PublicacionDto) => { | ||||||
|  |     setSelectedPublicacionParaDias(publicacion); | ||||||
|  |     setDiasSemanaModalOpen(true); | ||||||
|  |     handleMenuClose(); // Cerrar el menú de acciones si estaba abierto | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const handleCloseDiasSemanaModal = () => { | ||||||
|  |     setDiasSemanaModalOpen(false); | ||||||
|  |     setSelectedPublicacionParaDias(null); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const handleConfigDiasSaved = () => { | ||||||
|  |     // Opcional: Recargar publicaciones o simplemente mostrar un mensaje de éxito. | ||||||
|  |     // Por ahora, solo cerramos el modal. | ||||||
|  |     console.log("Configuración de días guardada."); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |  | ||||||
|   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); |   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||||
|   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { |   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||
| @@ -191,8 +221,8 @@ const GestionarPublicacionesPage: React.FC = () => { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Gestionar Publicaciones</Typography> |       <Typography variant="h5" gutterBottom>Gestionar Publicaciones</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|         <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}> |         <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}> | ||||||
|           <TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} /> |           <TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} /> | ||||||
| @@ -261,17 +291,75 @@ const GestionarPublicacionesPage: React.FC = () => { | |||||||
|         </TableContainer> |         </TableContainer> | ||||||
|       )} |       )} | ||||||
|  |  | ||||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> |       <Menu | ||||||
|         {puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedPublicacionRow!); handleMenuClose(); }}>Modificar</MenuItem>)} |         anchorEl={anchorEl} | ||||||
|         {puedeGestionarPrecios && (<MenuItem onClick={() => handleNavigateToPrecios(selectedPublicacionRow!.idPublicacion)}>Gestionar Precios</MenuItem>)} |         open={Boolean(anchorEl)} | ||||||
|         {puedeGestionarRecargos && (<MenuItem onClick={() => handleNavigateToRecargos(selectedPublicacionRow!.idPublicacion)}>Gestionar Recargos</MenuItem>)} |         onClose={handleMenuClose} | ||||||
|         {puedeGestionarPorcDist && (<MenuItem onClick={() => handleNavigateToPorcentajesPagoDist(selectedPublicacionRow!.idPublicacion)}>Porcentajes Pago (Dist.)</MenuItem>)} |         PaperProps={{ | ||||||
|         {puedeGestionarPorcCan && (<MenuItem onClick={() => handleNavigateToPorcMonCanilla(selectedPublicacionRow!.idPublicacion)}>Porc./Monto Canillita</MenuItem>)} |           style: { | ||||||
|         {puedeGestionarSecciones && (<MenuItem onClick={() => handleNavigateToSecciones(selectedPublicacionRow!.idPublicacion)}>Gestionar Secciones</MenuItem>)} |             minWidth: 250, // Un ancho mínimo para que los textos no se corten tanto | ||||||
|         {puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedPublicacionRow!.idPublicacion)}>Eliminar</MenuItem>)} |           }, | ||||||
|         {/* Si no hay permisos para ninguna acción */} |         }} | ||||||
|         {(!puedeModificar && !puedeEliminar && !puedeGestionarPrecios && !puedeGestionarRecargos && !puedeGestionarSecciones) && |       > | ||||||
|           <MenuItem disabled>Sin acciones</MenuItem>} |         {puedeModificar && selectedPublicacionRow && ( | ||||||
|  |           <MenuItem onClick={() => { handleOpenModal(selectedPublicacionRow); handleMenuClose(); }}> | ||||||
|  |             <ListItemIcon><EditIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Modificar</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |         {puedeGestionarPrecios && selectedPublicacionRow && ( | ||||||
|  |           <MenuItem onClick={() => { handleNavigateToPrecios(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}> | ||||||
|  |             <ListItemIcon><LocalOfferIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Gestionar Precios</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |         {puedeGestionarRecargos && selectedPublicacionRow && ( | ||||||
|  |           <MenuItem onClick={() => { handleNavigateToRecargos(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}> | ||||||
|  |             <ListItemIcon><AddCardIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Gestionar Recargos</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |         {puedeGestionarPorcDist && selectedPublicacionRow && ( | ||||||
|  |           <MenuItem onClick={() => { handleNavigateToPorcentajesPagoDist(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}> | ||||||
|  |             <ListItemIcon><PercentIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Porcentajes Pago (Dist.)</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |         {puedeGestionarPorcCan && selectedPublicacionRow && ( | ||||||
|  |           <MenuItem onClick={() => { handleNavigateToPorcMonCanilla(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}> | ||||||
|  |             <ListItemIcon><RequestQuoteIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Porc./Monto Canillita</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |         {puedeGestionarSecciones && selectedPublicacionRow && ( | ||||||
|  |           <MenuItem onClick={() => { handleNavigateToSecciones(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}> | ||||||
|  |             <ListItemIcon><ViewQuiltIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Gestionar Secciones</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |         {puedeModificar && selectedPublicacionRow && ( | ||||||
|  |           <MenuItem onClick={() => { handleOpenDiasSemanaModal(selectedPublicacionRow as PublicacionDto); handleMenuClose(); }}> | ||||||
|  |             <ListItemIcon><CalendarMonthIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Días de Salida</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |         {puedeEliminar && selectedPublicacionRow && ( | ||||||
|  |           <MenuItem onClick={() => { handleDelete(selectedPublicacionRow.idPublicacion); handleMenuClose(); }}> | ||||||
|  |             <ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Eliminar</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |  | ||||||
|  |         {/* Si no hay permisos para ninguna acción y hay una fila seleccionada */} | ||||||
|  |         {selectedPublicacionRow && | ||||||
|  |           !puedeModificar && !puedeEliminar && | ||||||
|  |           !puedeGestionarPrecios && !puedeGestionarRecargos && | ||||||
|  |           !puedeGestionarPorcDist && !puedeGestionarPorcCan && | ||||||
|  |           !puedeGestionarSecciones && ( | ||||||
|  |             <MenuItem disabled> | ||||||
|  |               <ListItemText>Sin acciones disponibles</ListItemText> | ||||||
|  |             </MenuItem> | ||||||
|  |           )} | ||||||
|       </Menu> |       </Menu> | ||||||
|  |  | ||||||
|       <PublicacionFormModal |       <PublicacionFormModal | ||||||
| @@ -279,6 +367,12 @@ const GestionarPublicacionesPage: React.FC = () => { | |||||||
|         initialData={editingPublicacion} errorMessage={apiErrorMessage} |         initialData={editingPublicacion} errorMessage={apiErrorMessage} | ||||||
|         clearErrorMessage={() => setApiErrorMessage(null)} |         clearErrorMessage={() => setApiErrorMessage(null)} | ||||||
|       /> |       /> | ||||||
|  |       <PublicacionDiasSemanaModal | ||||||
|  |         open={diasSemanaModalOpen} | ||||||
|  |         onClose={handleCloseDiasSemanaModal} | ||||||
|  |         publicacion={selectedPublicacionParaDias} | ||||||
|  |         onConfigSaved={handleConfigDiasSaved} | ||||||
|  |       /> | ||||||
|     </Box> |     </Box> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import React, { useState, useEffect, useCallback } from 'react'; | import React, { useState, useEffect, useCallback } from 'react'; | ||||||
| import { | import { | ||||||
|     Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||||
|     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|     CircularProgress, Alert, FormControl, InputLabel, Select |   CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||||
| @@ -29,9 +29,8 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => { | |||||||
|   const [error, setError] = useState<string | null>(null); |   const [error, setError] = useState<string | null>(null); | ||||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); |   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||||
|  |  | ||||||
|   // Filtros |   const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); | ||||||
|   const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState(''); |   const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); | ||||||
|   const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState(''); |  | ||||||
|   const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); |   const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); | ||||||
|   const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>(''); |   const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>(''); | ||||||
|  |  | ||||||
| @@ -48,25 +47,37 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => { | |||||||
|   const [selectedRow, setSelectedRow] = useState<SalidaOtroDestinoDto | null>(null); |   const [selectedRow, setSelectedRow] = useState<SalidaOtroDestinoDto | null>(null); | ||||||
|  |  | ||||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); |   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||||
|   // SO001, SO002 (crear/modificar), SO003 (eliminar) |  | ||||||
|   const puedeVer = isSuperAdmin || tienePermiso("SO001"); |   const puedeVer = isSuperAdmin || tienePermiso("SO001"); | ||||||
|   const puedeCrearModificar = isSuperAdmin || tienePermiso("SO002"); |   const puedeCrearModificar = isSuperAdmin || tienePermiso("SO002"); | ||||||
|   const puedeEliminar = isSuperAdmin || tienePermiso("SO003"); |   const puedeEliminar = isSuperAdmin || tienePermiso("SO003"); | ||||||
|  |  | ||||||
|  |   // CORREGIDO: Función para formatear la fecha | ||||||
|  |   const formatDate = (dateString?: string | null): string => { | ||||||
|  |     if (!dateString) return '-'; | ||||||
|  |     // Asumimos que dateString viene del backend como "YYYY-MM-DD" o "YYYY-MM-DDTHH:mm:ss..." | ||||||
|  |     const datePart = dateString.split('T')[0]; // Tomar solo la parte YYYY-MM-DD | ||||||
|  |     const parts = datePart.split('-'); | ||||||
|  |     if (parts.length === 3) { | ||||||
|  |       // parts[0] = YYYY, parts[1] = MM, parts[2] = DD | ||||||
|  |       return `${parts[2]}/${parts[1]}/${parts[0]}`; // Formato DD/MM/YYYY | ||||||
|  |     } | ||||||
|  |     return datePart; // Fallback si el formato no es el esperado | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const fetchFiltersDropdownData = useCallback(async () => { |   const fetchFiltersDropdownData = useCallback(async () => { | ||||||
|     setLoadingFiltersDropdown(true); |     setLoadingFiltersDropdown(true); | ||||||
|     try { |     try { | ||||||
|         const [pubsData, destinosData] = await Promise.all([ |       const [pubsData, destinosData] = await Promise.all([ | ||||||
|             publicacionService.getAllPublicaciones(undefined, undefined, true), |         publicacionService.getAllPublicaciones(undefined, undefined, true), | ||||||
|             otroDestinoService.getAllOtrosDestinos() |         otroDestinoService.getAllOtrosDestinos() | ||||||
|         ]); |       ]); | ||||||
|         setPublicaciones(pubsData); |       setPublicaciones(pubsData); | ||||||
|         setOtrosDestinos(destinosData); |       setOtrosDestinos(destinosData); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|         console.error("Error cargando datos para filtros:", err); |       console.error("Error cargando datos para filtros:", err); | ||||||
|         setError("Error al cargar opciones de filtro."); |       setError("Error al cargar opciones de filtro."); | ||||||
|     } finally { |     } finally { | ||||||
|         setLoadingFiltersDropdown(false); |       setLoadingFiltersDropdown(false); | ||||||
|     } |     } | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
| @@ -117,13 +128,13 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => { | |||||||
|  |  | ||||||
|   const handleDelete = async (idParte: number) => { |   const handleDelete = async (idParte: number) => { | ||||||
|     if (window.confirm(`¿Seguro de eliminar este registro de salida (ID: ${idParte})?`)) { |     if (window.confirm(`¿Seguro de eliminar este registro de salida (ID: ${idParte})?`)) { | ||||||
|        setApiErrorMessage(null); |       setApiErrorMessage(null); | ||||||
|        try { |       try { | ||||||
|         await salidaOtroDestinoService.deleteSalidaOtroDestino(idParte); |         await salidaOtroDestinoService.deleteSalidaOtroDestino(idParte); | ||||||
|         cargarSalidas(); |         cargarSalidas(); | ||||||
|       } catch (err: any) { |       } catch (err: any) { | ||||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; |         const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; | ||||||
|          setApiErrorMessage(message); |         setApiErrorMessage(message); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     handleMenuClose(); |     handleMenuClose(); | ||||||
| @@ -140,81 +151,91 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => { | |||||||
|   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { |   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|     setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); |     setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const displayData = salidas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); |   const displayData = salidas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||||
|   const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; |   // La función formatDate ya está definida arriba. | ||||||
|  |  | ||||||
|   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; |   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1}}> | ||||||
|       <Typography variant="h4" gutterBottom>Salidas a Otros Destinos</Typography> |       <Typography variant="h5" gutterBottom>Salidas a Otros Destinos</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|          <Typography variant="h6" gutterBottom>Filtros</Typography> |         <Typography variant="h6" gutterBottom>Filtros</Typography> | ||||||
|          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> |         <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 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}}/> |           <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}> |           <FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}> | ||||||
|                 <InputLabel>Publicación</InputLabel> |             <InputLabel>Publicación</InputLabel> | ||||||
|                 <Select value={filtroIdPublicacion} label="Publicación" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}> |             <Select value={filtroIdPublicacion} label="Publicación" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}> | ||||||
|                     <MenuItem value=""><em>Todas</em></MenuItem> |               <MenuItem value=""><em>Todas</em></MenuItem> | ||||||
|                     {publicaciones.map(p => <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>)} |               {publicaciones.map(p => <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>)} | ||||||
|                 </Select> |             </Select> | ||||||
|             </FormControl> |           </FormControl> | ||||||
|             <FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}> |           <FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}> | ||||||
|                 <InputLabel>Destino</InputLabel> |             <InputLabel>Destino</InputLabel> | ||||||
|                 <Select value={filtroIdDestino} label="Destino" onChange={(e) => setFiltroIdDestino(e.target.value as number | string)}> |             <Select value={filtroIdDestino} label="Destino" onChange={(e) => setFiltroIdDestino(e.target.value as number | string)}> | ||||||
|                     <MenuItem value=""><em>Todos</em></MenuItem> |               <MenuItem value=""><em>Todos</em></MenuItem> | ||||||
|                     {otrosDestinos.map(d => <MenuItem key={d.idDestino} value={d.idDestino}>{d.nombre}</MenuItem>)} |               {otrosDestinos.map(d => <MenuItem key={d.idDestino} value={d.idDestino}>{d.nombre}</MenuItem>)} | ||||||
|                 </Select> |             </Select> | ||||||
|             </FormControl> |           </FormControl> | ||||||
|          </Box> |         </Box> | ||||||
|          {puedeCrearModificar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Salida</Button>)} |         {puedeCrearModificar && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Salida</Button>)} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
|       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} |       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} | ||||||
|       {error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>} |       {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>} | ||||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} |       {apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>} | ||||||
|  |  | ||||||
|       {!loading && !error && puedeVer && ( |       {!loading && !error && puedeVer && ( | ||||||
|          <TableContainer component={Paper}> |         <TableContainer component={Paper}> | ||||||
|            <Table size="small"> |           <Table size="small"> | ||||||
|              <TableHead><TableRow> |             <TableHead><TableRow> | ||||||
|                  <TableCell>Fecha</TableCell><TableCell>Publicación</TableCell> |               <TableCell>Fecha</TableCell><TableCell>Publicación</TableCell> | ||||||
|                  <TableCell>Destino</TableCell><TableCell align="right">Cantidad</TableCell> |               <TableCell>Destino</TableCell><TableCell align="right">Cantidad</TableCell> | ||||||
|                  <TableCell>Observación</TableCell> |               <TableCell>Observación</TableCell> | ||||||
|                  {(puedeCrearModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} |               {(puedeCrearModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} | ||||||
|              </TableRow></TableHead> |             </TableRow></TableHead> | ||||||
|              <TableBody> |             <TableBody> | ||||||
|                {displayData.length === 0 ? ( |               {displayData.length === 0 ? ( | ||||||
|                   <TableRow><TableCell colSpan={6} align="center">No se encontraron salidas.</TableCell></TableRow> |                 <TableRow><TableCell colSpan={puedeCrearModificar || puedeEliminar ? 6 : 5} align="center">No se encontraron salidas.</TableCell></TableRow> | ||||||
|                ) : ( |               ) : ( | ||||||
|                  displayData.map((s) => ( |                 displayData.map((s) => ( | ||||||
|                      <TableRow key={s.idParte} hover> |                   <TableRow key={s.idParte} hover> | ||||||
|                      <TableCell>{formatDate(s.fecha)}</TableCell><TableCell>{s.nombrePublicacion}</TableCell> |                     {/* Usar la función formatDate aquí */} | ||||||
|                      <TableCell>{s.nombreDestino}</TableCell><TableCell align="right">{s.cantidad}</TableCell> |                     <TableCell>{formatDate(s.fecha)}</TableCell> | ||||||
|                      <TableCell>{s.observacion || '-'}</TableCell> |                     <TableCell>{s.nombrePublicacion}</TableCell> | ||||||
|                      {(puedeCrearModificar || puedeEliminar) && ( |                     <TableCell>{s.nombreDestino}</TableCell> | ||||||
|                         <TableCell align="right"> |                     <TableCell align="right">{s.cantidad}</TableCell> | ||||||
|                             <IconButton onClick={(e) => handleMenuOpen(e, s)} disabled={!puedeCrearModificar && !puedeEliminar}><MoreVertIcon /></IconButton> |                     <TableCell> | ||||||
|                         </TableCell> |                       <Tooltip title={s.observacion || ''}> | ||||||
|                      )} |                         <Box sx={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> | ||||||
|                      </TableRow> |                           {s.observacion || '-'} | ||||||
|                  )))} |                         </Box> | ||||||
|              </TableBody> |                       </Tooltip> | ||||||
|            </Table> |                     </TableCell> | ||||||
|            <TablePagination |                     {(puedeCrearModificar || puedeEliminar) && ( | ||||||
|              rowsPerPageOptions={[5, 10, 25]} component="div" count={salidas.length} |                       <TableCell align="right"> | ||||||
|              rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} |                         <IconButton onClick={(e) => handleMenuOpen(e, s)} disabled={!puedeCrearModificar && !puedeEliminar}><MoreVertIcon /></IconButton> | ||||||
|              onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" |                       </TableCell> | ||||||
|            /> |                     )} | ||||||
|          </TableContainer> |                   </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}> |       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||||
|         {puedeCrearModificar && selectedRow && ( |         {puedeCrearModificar && selectedRow && ( | ||||||
|             <MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)} |           <MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)} | ||||||
|         {puedeEliminar && selectedRow && ( |         {puedeEliminar && selectedRow && ( | ||||||
|             <MenuItem onClick={() => handleDelete(selectedRow.idParte)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)} |           <MenuItem onClick={() => handleDelete(selectedRow.idParte)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)} | ||||||
|       </Menu> |       </Menu> | ||||||
|  |  | ||||||
|       <SalidaOtroDestinoFormModal |       <SalidaOtroDestinoFormModal | ||||||
|   | |||||||
| @@ -2,10 +2,14 @@ import React, { useState, useEffect, useCallback } from 'react'; | |||||||
| import { | import { | ||||||
|   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||||
|   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|   CircularProgress, Alert |   CircularProgress, Alert, | ||||||
|  |   ListItemIcon, | ||||||
|  |   ListItemText | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar | ||||||
| import zonaService from '../../services/Distribucion/zonaService'; // Servicio de Zonas | import zonaService from '../../services/Distribucion/zonaService'; // Servicio de Zonas | ||||||
| import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // DTO de Zonas | import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // DTO de Zonas | ||||||
| import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto'; // DTOs Create | import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto'; // DTOs Create | ||||||
| @@ -132,8 +136,8 @@ const GestionarZonasPage: React.FC = () => { | |||||||
|  |  | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom> |       <Typography variant="h5" gutterBottom> | ||||||
|         Gestionar Zonas |         Gestionar Zonas | ||||||
|       </Typography> |       </Typography> | ||||||
|  |  | ||||||
| @@ -149,7 +153,6 @@ const GestionarZonasPage: React.FC = () => { | |||||||
|           {/* <TextField label="Filtrar por Descripción" ... /> */} |           {/* <TextField label="Filtrar por Descripción" ... /> */} | ||||||
|         </Box> |         </Box> | ||||||
|         {puedeCrear && ( |         {puedeCrear && ( | ||||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> |  | ||||||
|             <Button |             <Button | ||||||
|               variant="contained" |               variant="contained" | ||||||
|               startIcon={<AddIcon />} |               startIcon={<AddIcon />} | ||||||
| @@ -158,7 +161,6 @@ const GestionarZonasPage: React.FC = () => { | |||||||
|             > |             > | ||||||
|               Agregar Nueva Zona |               Agregar Nueva Zona | ||||||
|             </Button> |             </Button> | ||||||
|           </Box> |  | ||||||
|         )} |         )} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
| @@ -218,12 +220,14 @@ const GestionarZonasPage: React.FC = () => { | |||||||
|       > |       > | ||||||
|         {puedeModificar && ( |         {puedeModificar && ( | ||||||
|           <MenuItem onClick={() => { handleOpenModal(selectedZonaRow!); handleMenuClose(); }}> |           <MenuItem onClick={() => { handleOpenModal(selectedZonaRow!); handleMenuClose(); }}> | ||||||
|             Modificar |             <ListItemIcon><EditIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Modificar</ListItemText> | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {puedeEliminar && ( |         {puedeEliminar && ( | ||||||
|           <MenuItem onClick={() => handleDelete(selectedZonaRow!.idZona)}> |           <MenuItem onClick={() => handleDelete(selectedZonaRow!.idZona)}> | ||||||
|             Eliminar |             <ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Eliminar</ListItemText> | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} |         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} | ||||||
|   | |||||||
| @@ -2,10 +2,14 @@ import React, { useState, useEffect, useCallback } from 'react'; | |||||||
| import { | import { | ||||||
|   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||||
|   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|   CircularProgress, Alert |   CircularProgress, Alert, | ||||||
|  |   ListItemIcon, | ||||||
|  |   ListItemText | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; // Icono para agregar | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar | ||||||
| import estadoBobinaService from '../../services/Impresion/estadoBobinaService'; | import estadoBobinaService from '../../services/Impresion/estadoBobinaService'; | ||||||
| import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; | import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; | ||||||
| import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto'; | import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto'; | ||||||
| @@ -132,16 +136,16 @@ const GestionarEstadosBobinaPage: React.FC = () => { | |||||||
|  |  | ||||||
|   if (!loading && !puedeVer) { |   if (!loading && !puedeVer) { | ||||||
|     return ( |     return ( | ||||||
|       <Box sx={{ p: 2 }}> |       <Box sx={{ p: 1 }}> | ||||||
|         <Typography variant="h4" gutterBottom>Gestionar Estados de Bobina</Typography> |         <Typography variant="h5" gutterBottom>Gestionar Estados de Bobina</Typography> | ||||||
|         <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> |         <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> | ||||||
|       </Box> |       </Box> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom> |       <Typography variant="h5" gutterBottom> | ||||||
|         Gestionar Estados de Bobina |         Gestionar Estados de Bobina | ||||||
|       </Typography> |       </Typography> | ||||||
|  |  | ||||||
| @@ -156,7 +160,6 @@ const GestionarEstadosBobinaPage: React.FC = () => { | |||||||
|           /> |           /> | ||||||
|         </Box> |         </Box> | ||||||
|         {puedeCrear && ( |         {puedeCrear && ( | ||||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> |  | ||||||
|             <Button |             <Button | ||||||
|               variant="contained" |               variant="contained" | ||||||
|               startIcon={<AddIcon />} |               startIcon={<AddIcon />} | ||||||
| @@ -165,7 +168,6 @@ const GestionarEstadosBobinaPage: React.FC = () => { | |||||||
|             > |             > | ||||||
|               Agregar Nuevo Estado |               Agregar Nuevo Estado | ||||||
|             </Button> |             </Button> | ||||||
|           </Box> |  | ||||||
|         )} |         )} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
| @@ -226,12 +228,14 @@ const GestionarEstadosBobinaPage: React.FC = () => { | |||||||
|       > |       > | ||||||
|         {puedeModificar && ( |         {puedeModificar && ( | ||||||
|           <MenuItem onClick={() => { handleOpenModal(selectedEstadoRow!); handleMenuClose(); }}> |           <MenuItem onClick={() => { handleOpenModal(selectedEstadoRow!); handleMenuClose(); }}> | ||||||
|             Modificar |             <ListItemIcon><EditIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Modificar</ListItemText> | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {puedeEliminar && ( |         {puedeEliminar && ( | ||||||
|           <MenuItem onClick={() => handleDelete(selectedEstadoRow!.idEstadoBobina)}> |           <MenuItem onClick={() => handleDelete(selectedEstadoRow!.idEstadoBobina)}> | ||||||
|             Eliminar |             <ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Eliminar</ListItemText> | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} |         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} | ||||||
|   | |||||||
| @@ -2,10 +2,14 @@ import React, { useState, useEffect, useCallback } from 'react'; | |||||||
| import { | import { | ||||||
|   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||||
|   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|   CircularProgress, Alert |   CircularProgress, Alert, | ||||||
|  |   ListItemIcon, | ||||||
|  |   ListItemText | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; // Icono para agregar | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar | ||||||
| import plantaService from '../../services/Impresion/plantaService'; // Servicio de Plantas | import plantaService from '../../services/Impresion/plantaService'; // Servicio de Plantas | ||||||
| import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; | import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; | ||||||
| import type { CreatePlantaDto } from '../../models/dtos/Impresion/CreatePlantaDto'; | import type { CreatePlantaDto } from '../../models/dtos/Impresion/CreatePlantaDto'; | ||||||
| @@ -135,16 +139,16 @@ const GestionarPlantasPage: React.FC = () => { | |||||||
|  |  | ||||||
|   if (!loading && !puedeVer) { |   if (!loading && !puedeVer) { | ||||||
|     return ( |     return ( | ||||||
|       <Box sx={{ p: 2 }}> |       <Box sx={{ p: 1 }}> | ||||||
|         <Typography variant="h4" gutterBottom>Gestionar Plantas de Impresión</Typography> |         <Typography variant="h5" gutterBottom>Gestionar Plantas de Impresión</Typography> | ||||||
|         <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> |         <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> | ||||||
|       </Box> |       </Box> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom> |       <Typography variant="h5" gutterBottom> | ||||||
|         Gestionar Plantas de Impresión |         Gestionar Plantas de Impresión | ||||||
|       </Typography> |       </Typography> | ||||||
|  |  | ||||||
| @@ -167,7 +171,6 @@ const GestionarPlantasPage: React.FC = () => { | |||||||
|           {/* <Button variant="contained" onClick={cargarPlantas}>Buscar</Button> */} |           {/* <Button variant="contained" onClick={cargarPlantas}>Buscar</Button> */} | ||||||
|         </Box> |         </Box> | ||||||
|         {puedeCrear && ( |         {puedeCrear && ( | ||||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> |  | ||||||
|             <Button |             <Button | ||||||
|               variant="contained" |               variant="contained" | ||||||
|               startIcon={<AddIcon />} |               startIcon={<AddIcon />} | ||||||
| @@ -176,7 +179,6 @@ const GestionarPlantasPage: React.FC = () => { | |||||||
|             > |             > | ||||||
|               Agregar Nueva Planta |               Agregar Nueva Planta | ||||||
|             </Button> |             </Button> | ||||||
|           </Box> |  | ||||||
|         )} |         )} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
| @@ -237,12 +239,14 @@ const GestionarPlantasPage: React.FC = () => { | |||||||
|       > |       > | ||||||
|         {puedeModificar && ( |         {puedeModificar && ( | ||||||
|           <MenuItem onClick={() => { handleOpenModal(selectedPlantaRow!); handleMenuClose(); }}> |           <MenuItem onClick={() => { handleOpenModal(selectedPlantaRow!); handleMenuClose(); }}> | ||||||
|             Modificar |             <ListItemIcon><EditIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Modificar</ListItemText> | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {puedeEliminar && ( |         {puedeEliminar && ( | ||||||
|           <MenuItem onClick={() => handleDelete(selectedPlantaRow!.idPlanta)}> |           <MenuItem onClick={() => handleDelete(selectedPlantaRow!.idPlanta)}> | ||||||
|             Eliminar |             <ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Eliminar</ListItemText> | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} |         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} | ||||||
|   | |||||||
| @@ -42,8 +42,8 @@ const GestionarStockBobinasPage: React.FC = () => { | |||||||
|   const [filtroPlanta, setFiltroPlanta] = useState<number | string>(''); |   const [filtroPlanta, setFiltroPlanta] = useState<number | string>(''); | ||||||
|   const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>(''); |   const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>(''); | ||||||
|   const [filtroRemito, setFiltroRemito] = useState(''); |   const [filtroRemito, setFiltroRemito] = useState(''); | ||||||
|   const [filtroFechaDesde, setFiltroFechaDesde] = useState(''); |   const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); | ||||||
|   const [filtroFechaHasta, setFiltroFechaHasta] = useState(''); |   const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); | ||||||
|  |  | ||||||
|   // Datos para dropdowns de filtros |   // Datos para dropdowns de filtros | ||||||
|   const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]); |   const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]); | ||||||
| @@ -178,8 +178,8 @@ const GestionarStockBobinasPage: React.FC = () => { | |||||||
|   if (!loading && !puedeVer) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; |   if (!loading && !puedeVer) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Stock de Bobinas</Typography> |       <Typography variant="h5" gutterBottom>Stock de Bobinas</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|         <Typography variant="h6" gutterBottom>Filtros</Typography> |         <Typography variant="h6" gutterBottom>Filtros</Typography> | ||||||
|         <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2}}> |         <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2}}> | ||||||
|   | |||||||
| @@ -2,10 +2,14 @@ import React, { useState, useEffect, useCallback } from 'react'; | |||||||
| import { | import { | ||||||
|   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||||
|   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|   CircularProgress, Alert |   CircularProgress, Alert, | ||||||
|  |   ListItemIcon, | ||||||
|  |   ListItemText | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; // Icono para agregar | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar | ||||||
| import tipoBobinaService from '../../services/Impresion/tipoBobinaService'; // Servicio específico | import tipoBobinaService from '../../services/Impresion/tipoBobinaService'; // Servicio específico | ||||||
| import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto'; | import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto'; | ||||||
| import type { CreateTipoBobinaDto } from '../../models/dtos/Impresion/CreateTipoBobinaDto'; | import type { CreateTipoBobinaDto } from '../../models/dtos/Impresion/CreateTipoBobinaDto'; | ||||||
| @@ -132,16 +136,16 @@ const GestionarTiposBobinaPage: React.FC = () => { | |||||||
|  |  | ||||||
|   if (!loading && !puedeVer) { |   if (!loading && !puedeVer) { | ||||||
|     return ( |     return ( | ||||||
|       <Box sx={{ p: 2 }}> |       <Box sx={{ p: 1 }}> | ||||||
|         <Typography variant="h4" gutterBottom>Gestionar Tipos de Bobina</Typography> |         <Typography variant="h5" gutterBottom>Gestionar Tipos de Bobina</Typography> | ||||||
|         <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> |         <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> | ||||||
|       </Box> |       </Box> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom> |       <Typography variant="h5" gutterBottom> | ||||||
|         Gestionar Tipos de Bobina |         Gestionar Tipos de Bobina | ||||||
|       </Typography> |       </Typography> | ||||||
|  |  | ||||||
| @@ -157,7 +161,6 @@ const GestionarTiposBobinaPage: React.FC = () => { | |||||||
|           {/* <Button variant="contained" onClick={cargarTiposBobina}>Buscar</Button> */} |           {/* <Button variant="contained" onClick={cargarTiposBobina}>Buscar</Button> */} | ||||||
|         </Box> |         </Box> | ||||||
|         {puedeCrear && ( |         {puedeCrear && ( | ||||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> |  | ||||||
|             <Button |             <Button | ||||||
|               variant="contained" |               variant="contained" | ||||||
|               startIcon={<AddIcon />} |               startIcon={<AddIcon />} | ||||||
| @@ -166,7 +169,6 @@ const GestionarTiposBobinaPage: React.FC = () => { | |||||||
|             > |             > | ||||||
|               Agregar Nuevo Tipo |               Agregar Nuevo Tipo | ||||||
|             </Button> |             </Button> | ||||||
|           </Box> |  | ||||||
|         )} |         )} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
| @@ -225,12 +227,14 @@ const GestionarTiposBobinaPage: React.FC = () => { | |||||||
|       > |       > | ||||||
|         {puedeModificar && ( |         {puedeModificar && ( | ||||||
|           <MenuItem onClick={() => { handleOpenModal(selectedTipoBobinaRow!); handleMenuClose(); }}> |           <MenuItem onClick={() => { handleOpenModal(selectedTipoBobinaRow!); handleMenuClose(); }}> | ||||||
|             Modificar |             <ListItemIcon><EditIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Modificar</ListItemText> | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {puedeEliminar && ( |         {puedeEliminar && ( | ||||||
|           <MenuItem onClick={() => handleDelete(selectedTipoBobinaRow!.idTipoBobina)}> |           <MenuItem onClick={() => handleDelete(selectedTipoBobinaRow!.idTipoBobina)}> | ||||||
|             Eliminar |             <ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Eliminar</ListItemText> | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} |         {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>} | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ const GestionarTiradasPage: React.FC = () => { | |||||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); |   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||||
|  |  | ||||||
|   // Filtros |   // Filtros | ||||||
|   const [filtroFecha, setFiltroFecha] = useState<string>(''); |   const [filtroFecha, setFiltroFecha] = useState<string>(new Date().toISOString().split('T')[0]); | ||||||
|   const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); |   const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); | ||||||
|   const [filtroIdPlanta, setFiltroIdPlanta] = useState<number | string>(''); |   const [filtroIdPlanta, setFiltroIdPlanta] = useState<number | string>(''); | ||||||
|  |  | ||||||
| @@ -124,8 +124,8 @@ const GestionarTiradasPage: React.FC = () => { | |||||||
|   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; |   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Gestión de Tiradas</Typography> |       <Typography variant="h5" gutterBottom>Gestión de Tiradas</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|         <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography> |         <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography> | ||||||
|         <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> |         <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> | ||||||
| @@ -178,8 +178,8 @@ const GestionarTiradasPage: React.FC = () => { | |||||||
|                             <TableContainer component={Paper} variant="outlined"> |                             <TableContainer component={Paper} variant="outlined"> | ||||||
|                                 <Table size="small"> |                                 <Table size="small"> | ||||||
|                                     <TableHead><TableRow> |                                     <TableHead><TableRow> | ||||||
|                                         <TableCell>Sección</TableCell> |                                         <TableCell><strong>Sección</strong></TableCell> | ||||||
|                                         <TableCell align="right">Páginas</TableCell> |                                         <TableCell align="right"><strong>Páginas</strong></TableCell> | ||||||
|                                     </TableRow></TableHead> |                                     </TableRow></TableHead> | ||||||
|                                     <TableBody> |                                     <TableBody> | ||||||
|                                         {tirada.seccionesImpresas.map(sec => ( |                                         {tirada.seccionesImpresas.map(sec => ( | ||||||
|   | |||||||
| @@ -125,8 +125,8 @@ const GestionarListasRadioPage: React.FC = () => { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Generar Listas de Radio</Typography> |       <Typography variant="h5" gutterBottom>Generar Listas de Radio</Typography> | ||||||
|       <Paper sx={{ p: 3, mb: 2 }}> |       <Paper sx={{ p: 3, mb: 2 }}> | ||||||
|         <Typography variant="h6" gutterBottom>Criterios de Generación</Typography> |         <Typography variant="h6" gutterBottom>Criterios de Generación</Typography> | ||||||
|         <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}> {/* Aumentado el gap */} |         <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}> {/* Aumentado el gap */} | ||||||
|   | |||||||
| @@ -118,8 +118,8 @@ const GestionarCancionesPage: React.FC = () => { | |||||||
|   if (!loading && !puedeGestionar && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; |   if (!loading && !puedeGestionar && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Gestionar Canciones</Typography> |       <Typography variant="h5" gutterBottom>Gestionar Canciones</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|          <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography> |          <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography> | ||||||
|          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> |          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> | ||||||
|   | |||||||
| @@ -101,8 +101,8 @@ const GestionarRitmosPage: React.FC = () => { | |||||||
|   if (!loading && !puedeGestionar) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; |   if (!loading && !puedeGestionar) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Gestionar Ritmos</Typography> |       <Typography variant="h5" gutterBottom>Gestionar Ritmos</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|          <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography> |          <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography> | ||||||
|          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> |          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}> | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| import React, { useState, useEffect, useCallback } from 'react'; | import React, { useState, useEffect, useCallback } from 'react'; | ||||||
| import { useParams, useNavigate } from 'react-router-dom'; | import { useParams, useNavigate } from 'react-router-dom'; | ||||||
| import { | import { | ||||||
|     Box, Typography, Button, Paper, CircularProgress, Alert, |     Box, Typography, Button, Paper, CircularProgress, Alert | ||||||
|     Checkbox, FormControlLabel, FormGroup // Para el caso sin componente checklist |  | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | ||||||
| import SaveIcon from '@mui/icons-material/Save'; | import SaveIcon from '@mui/icons-material/Save'; | ||||||
| @@ -119,11 +118,11 @@ const AsignarPermisosAPerfilPage: React.FC = () => { | |||||||
|  |  | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}> |       <Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}> | ||||||
|         Volver a Perfiles |         Volver a Perfiles | ||||||
|       </Button> |       </Button> | ||||||
|       <Typography variant="h4" gutterBottom> |       <Typography variant="h5" gutterBottom> | ||||||
|         Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'} |         Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'} | ||||||
|       </Typography> |       </Typography> | ||||||
|       <Typography variant="body2" color="textSecondary" gutterBottom> |       <Typography variant="body2" color="textSecondary" gutterBottom> | ||||||
|   | |||||||
| @@ -42,12 +42,7 @@ const GestionarAuditoriaUsuariosPage: React.FC = () => { | |||||||
|         try { |         try { | ||||||
|             const usuariosData = await usuarioService.getAllUsuarios(); // Asumiendo que tienes este método |             const usuariosData = await usuarioService.getAllUsuarios(); // Asumiendo que tienes este método | ||||||
|             setUsuariosParaDropdown(usuariosData); |             setUsuariosParaDropdown(usuariosData); | ||||||
|  |             // Filtrar usuarios para dropdown, excluyendo los que no tienen historial | ||||||
|             // Opción B para Tipos de Modificación (desde backend) |  | ||||||
|             // const tiposModData = await apiClient.get<string[]>('/auditoria/tipos-modificacion'); // Ajusta el endpoint si lo creas |  | ||||||
|             // setTiposModificacionParaDropdown(tiposModData.data); |  | ||||||
|  |  | ||||||
|             // Opción A (Hardcodeado en Frontend - más simple para empezar) |  | ||||||
|             setTiposModificacionParaDropdown([ |             setTiposModificacionParaDropdown([ | ||||||
|                 "Creado", "Insertada", |                 "Creado", "Insertada", | ||||||
|                 "Actualizado", "Modificada", |                 "Actualizado", "Modificada", | ||||||
| @@ -136,8 +131,8 @@ const GestionarAuditoriaUsuariosPage: React.FC = () => { | |||||||
|  |  | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <Box sx={{ p: 2 }}> |         <Box sx={{ p: 1 }}> | ||||||
|             <Typography variant="h4" gutterBottom>Auditoría de Usuarios</Typography> |             <Typography variant="h5" gutterBottom>Auditoría de Usuarios</Typography> | ||||||
|             <Paper sx={{ p: 2, mb: 2 }}> |             <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|                 <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography> |                 <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography> | ||||||
|                 <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}> |                 <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}> | ||||||
|   | |||||||
| @@ -1,18 +1,21 @@ | |||||||
| import React, { useState, useEffect, useCallback } from 'react'; | import React, { useState, useEffect, useCallback } from 'react'; | ||||||
| import { | import { | ||||||
|     Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||||
|     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|     CircularProgress, Alert, Tooltip // Añadir Tooltip |   CircularProgress, Alert, | ||||||
|  |   ListItemIcon, | ||||||
|  |   ListItemText | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||||
| import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; // Para asignar permisos | import EditIcon from '@mui/icons-material/Edit'; | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; | ||||||
|  | import LockOpenIcon from '@mui/icons-material/LockOpen'; | ||||||
| import perfilService from '../../services/Usuarios/perfilService'; | import perfilService from '../../services/Usuarios/perfilService'; | ||||||
| import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto'; | import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto'; | ||||||
| import type { CreatePerfilDto } from '../../models/dtos/Usuarios/CreatePerfilDto'; | import type { CreatePerfilDto } from '../../models/dtos/Usuarios/CreatePerfilDto'; | ||||||
| import type { UpdatePerfilDto } from '../../models/dtos/Usuarios/UpdatePerfilDto'; | import type { UpdatePerfilDto } from '../../models/dtos/Usuarios/UpdatePerfilDto'; | ||||||
| import PerfilFormModal from '../../components/Modals/Usuarios/PerfilFormModal'; | import PerfilFormModal from '../../components/Modals/Usuarios/PerfilFormModal'; | ||||||
| // import PermisosPorPerfilModal from '../../components/Modals/PermisosPorPerfilModal'; // Lo crearemos después |  | ||||||
| import { usePermissions } from '../../hooks/usePermissions'; | import { usePermissions } from '../../hooks/usePermissions'; | ||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
| import { useNavigate } from 'react-router-dom'; // Para navegar | import { useNavigate } from 'react-router-dom'; // Para navegar | ||||||
| @@ -27,9 +30,6 @@ const GestionarPerfilesPage: React.FC = () => { | |||||||
|   const [editingPerfil, setEditingPerfil] = useState<PerfilDto | null>(null); |   const [editingPerfil, setEditingPerfil] = useState<PerfilDto | null>(null); | ||||||
|   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); |   const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); | ||||||
|  |  | ||||||
|   // const [permisosModalOpen, setPermisosModalOpen] = useState(false); // Para modal de permisos |  | ||||||
|   // const [selectedPerfilForPermisos, setSelectedPerfilForPermisos] = useState<PerfilDto | null>(null); |  | ||||||
|  |  | ||||||
|   const [page, setPage] = useState(0); |   const [page, setPage] = useState(0); | ||||||
|   const [rowsPerPage, setRowsPerPage] = useState(5); |   const [rowsPerPage, setRowsPerPage] = useState(5); | ||||||
|  |  | ||||||
| @@ -48,9 +48,9 @@ const GestionarPerfilesPage: React.FC = () => { | |||||||
|  |  | ||||||
|   const cargarPerfiles = useCallback(async () => { |   const cargarPerfiles = useCallback(async () => { | ||||||
|     if (!puedeVer) { |     if (!puedeVer) { | ||||||
|         setError("No tiene permiso para ver esta sección."); |       setError("No tiene permiso para ver esta sección."); | ||||||
|         setLoading(false); |       setLoading(false); | ||||||
|         return; |       return; | ||||||
|     } |     } | ||||||
|     setLoading(true); setError(null); setApiErrorMessage(null); |     setLoading(true); setError(null); setApiErrorMessage(null); | ||||||
|     try { |     try { | ||||||
| @@ -87,13 +87,13 @@ const GestionarPerfilesPage: React.FC = () => { | |||||||
|  |  | ||||||
|   const handleDelete = async (id: number) => { |   const handleDelete = async (id: number) => { | ||||||
|     if (window.confirm(`¿Está seguro? ID: ${id}`)) { |     if (window.confirm(`¿Está seguro? ID: ${id}`)) { | ||||||
|        setApiErrorMessage(null); |       setApiErrorMessage(null); | ||||||
|        try { |       try { | ||||||
|         await perfilService.deletePerfil(id); |         await perfilService.deletePerfil(id); | ||||||
|         cargarPerfiles(); |         cargarPerfiles(); | ||||||
|       } catch (err: any) { |       } catch (err: any) { | ||||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el perfil.'; |         const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el perfil.'; | ||||||
|          setApiErrorMessage(message); |         setApiErrorMessage(message); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     handleMenuClose(); |     handleMenuClose(); | ||||||
| @@ -107,112 +107,120 @@ const GestionarPerfilesPage: React.FC = () => { | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const handleOpenPermisosModal = (perfil: PerfilDto) => { |   const handleOpenPermisosModal = (perfil: PerfilDto) => { | ||||||
|     // setSelectedPerfilForPermisos(perfil); |  | ||||||
|     // setPermisosModalOpen(true); |  | ||||||
|     handleMenuClose(); |     handleMenuClose(); | ||||||
|     // Navegar a la página de asignación de permisos |     // Navegar a la página de asignación de permisos | ||||||
|     navigate(`/usuarios/perfiles/${perfil.id}/permisos`); |     navigate(`/usuarios/perfiles/${perfil.id}/permisos`); | ||||||
|   }; |   }; | ||||||
|   // const handleClosePermisosModal = () => { |  | ||||||
|   //   setPermisosModalOpen(false); setSelectedPerfilForPermisos(null); |  | ||||||
|   // }; |  | ||||||
|   // const handleSubmitPermisos = async (idPerfil: number, permisosIds: number[]) => { |  | ||||||
|   //   try { |  | ||||||
|   //     // await perfilService.updatePermisosPorPerfil(idPerfil, permisosIds); |  | ||||||
|   //     // console.log("Permisos actualizados para perfil:", idPerfil); |  | ||||||
|   //     // Quizás un snackbar de éxito |  | ||||||
|   //   } catch (error) { |  | ||||||
|   //     console.error("Error al actualizar permisos:", error); |  | ||||||
|   //     setApiErrorMessage("Error al actualizar permisos."); |  | ||||||
|   //   } |  | ||||||
|   //   handleClosePermisosModal(); |  | ||||||
|   // }; |  | ||||||
|  |  | ||||||
|  |   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||||
|    const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); |   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|    const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { |     setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); | ||||||
|      setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); |   }; | ||||||
|    }; |   const displayData = perfiles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||||
|    const displayData = perfiles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); |  | ||||||
|  |  | ||||||
|   if (!loading && !puedeVer) { |   if (!loading && !puedeVer) { | ||||||
|       return ( |     return ( | ||||||
|            <Box sx={{ p: 2 }}> |       <Box sx={{ p: 1 }}> | ||||||
|               <Typography variant="h4" gutterBottom>Gestionar Perfiles</Typography> |         <Typography variant="h5" gutterBottom>Gestionar Perfiles</Typography> | ||||||
|               <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> |         <Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert> | ||||||
|            </Box> |       </Box> | ||||||
|       ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Gestionar Perfiles</Typography> |       <Typography variant="h5" gutterBottom>Gestionar Perfiles</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|          <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}> |         <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}> | ||||||
|             <TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} /> |           <TextField label="Filtrar por Nombre" variant="outlined" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} /> | ||||||
|          </Box> |         </Box> | ||||||
|          {puedeCrear && ( |         {puedeCrear && ( | ||||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> |           <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}> | ||||||
|              <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}> |             Agregar Nuevo Perfil | ||||||
|                 Agregar Nuevo Perfil |           </Button> | ||||||
|              </Button> |  | ||||||
|           </Box> |  | ||||||
|         )} |         )} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
|       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} |       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} | ||||||
|       {error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>} |       {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>} | ||||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} |       {apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>} | ||||||
|  |  | ||||||
|       {!loading && !error && ( |       {!loading && !error && ( | ||||||
|          <TableContainer component={Paper}> |         <TableContainer component={Paper}> | ||||||
|            <Table> |           <Table> | ||||||
|              <TableHead> |             <TableHead> | ||||||
|                <TableRow> |               <TableRow> | ||||||
|                  <TableCell>Nombre del Perfil</TableCell> |                 <TableCell>Nombre del Perfil</TableCell> | ||||||
|                  <TableCell>Descripción</TableCell> |                 <TableCell>Descripción</TableCell> | ||||||
|                  {(puedeModificar || puedeEliminar || puedeAsignarPermisos) && <TableCell align="right">Acciones</TableCell>} |                 {(puedeModificar || puedeEliminar || puedeAsignarPermisos) && <TableCell align="right">Acciones</TableCell>} | ||||||
|                </TableRow> |               </TableRow> | ||||||
|              </TableHead> |             </TableHead> | ||||||
|              <TableBody> |             <TableBody> | ||||||
|                {displayData.length === 0 && !loading ? ( |               {displayData.length === 0 && !loading ? ( | ||||||
|                   <TableRow><TableCell colSpan={(puedeModificar || puedeEliminar || puedeAsignarPermisos) ? 3 : 2} align="center">No se encontraron perfiles.</TableCell></TableRow> |                 <TableRow><TableCell colSpan={(puedeModificar || puedeEliminar || puedeAsignarPermisos) ? 3 : 2} align="center">No se encontraron perfiles.</TableCell></TableRow> | ||||||
|                ) : ( |               ) : ( | ||||||
|                  displayData.map((perfil) => ( |                 displayData.map((perfil) => ( | ||||||
|                      <TableRow key={perfil.id}> |                   <TableRow key={perfil.id}> | ||||||
|                      <TableCell>{perfil.nombrePerfil}</TableCell> |                     <TableCell>{perfil.nombrePerfil}</TableCell> | ||||||
|                      <TableCell>{perfil.descripcion || '-'}</TableCell> |                     <TableCell>{perfil.descripcion || '-'}</TableCell> | ||||||
|                      {(puedeModificar || puedeEliminar || puedeAsignarPermisos) && ( |                     {(puedeModificar || puedeEliminar || puedeAsignarPermisos) && ( | ||||||
|                         <TableCell align="right"> |                       <TableCell align="right"> | ||||||
|                             <IconButton onClick={(e) => handleMenuOpen(e, perfil)} disabled={!puedeModificar && !puedeEliminar && !puedeAsignarPermisos}> |                         <IconButton onClick={(e) => handleMenuOpen(e, perfil)} disabled={!puedeModificar && !puedeEliminar && !puedeAsignarPermisos}> | ||||||
|                                 <MoreVertIcon /> |                           <MoreVertIcon /> | ||||||
|                             </IconButton> |                         </IconButton> | ||||||
|                         </TableCell> |                       </TableCell> | ||||||
|                      )} |                     )} | ||||||
|                      </TableRow> |                   </TableRow> | ||||||
|                  )) |                 )) | ||||||
|                )} |               )} | ||||||
|              </TableBody> |             </TableBody> | ||||||
|            </Table> |           </Table> | ||||||
|            <TablePagination |           <TablePagination | ||||||
|              rowsPerPageOptions={[5, 10, 25]} component="div" count={perfiles.length} |             rowsPerPageOptions={[5, 10, 25]} component="div" count={perfiles.length} | ||||||
|              rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} |             rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} | ||||||
|              onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" |             onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" | ||||||
|            /> |           /> | ||||||
|          </TableContainer> |         </TableContainer> | ||||||
|        )} |       )} | ||||||
|  |  | ||||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> |       <Menu | ||||||
|         {puedeModificar && ( |         anchorEl={anchorEl} | ||||||
|             <MenuItem onClick={() => { handleOpenModal(selectedPerfilRow!); handleMenuClose(); }}>Modificar</MenuItem> |         open={Boolean(anchorEl)} | ||||||
|  |         onClose={handleMenuClose} | ||||||
|  |         PaperProps={{ | ||||||
|  |           style: { | ||||||
|  |             minWidth: 230, // Ajusta el ancho según el texto más largo | ||||||
|  |           }, | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|  |         {puedeModificar && selectedPerfilRow && ( | ||||||
|  |           <MenuItem onClick={() => { handleOpenModal(selectedPerfilRow); handleMenuClose(); }}> | ||||||
|  |             <ListItemIcon><EditIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Modificar</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {puedeEliminar && ( |         {puedeEliminar && selectedPerfilRow && ( | ||||||
|             <MenuItem onClick={() => handleDelete(selectedPerfilRow!.id)}>Eliminar</MenuItem> |           <MenuItem onClick={() => { handleDelete(selectedPerfilRow.id); handleMenuClose(); }}> | ||||||
|  |             <ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Eliminar</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {puedeAsignarPermisos && ( |         {puedeAsignarPermisos && selectedPerfilRow && ( | ||||||
|             <MenuItem onClick={() => handleOpenPermisosModal(selectedPerfilRow!)}>Asignar Permisos</MenuItem> |           <MenuItem onClick={() => { handleOpenPermisosModal(selectedPerfilRow); handleMenuClose(); }}> | ||||||
|  |             <ListItemIcon><LockOpenIcon fontSize="small" /></ListItemIcon> | ||||||
|  |             <ListItemText>Asignar Permisos</ListItemText> | ||||||
|  |           </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {(!puedeModificar && !puedeEliminar && !puedeAsignarPermisos) && <MenuItem disabled>Sin acciones</MenuItem>} |  | ||||||
|  |         {/* Si no hay permisos para ninguna acción y hay una fila seleccionada */} | ||||||
|  |         {selectedPerfilRow && | ||||||
|  |           !puedeModificar && | ||||||
|  |           !puedeEliminar && | ||||||
|  |           !puedeAsignarPermisos && ( | ||||||
|  |             <MenuItem disabled> | ||||||
|  |               <ListItemText>Sin acciones disponibles</ListItemText> | ||||||
|  |             </MenuItem> | ||||||
|  |           )} | ||||||
|       </Menu> |       </Menu> | ||||||
|  |  | ||||||
|       <PerfilFormModal |       <PerfilFormModal | ||||||
|   | |||||||
| @@ -1,11 +1,15 @@ | |||||||
| import React, { useState, useEffect, useCallback } from 'react'; | import React, { useState, useEffect, useCallback } from 'react'; | ||||||
| import { | import { | ||||||
|     Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, |   Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, | ||||||
|     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |   Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|     CircularProgress, Alert |   CircularProgress, Alert, | ||||||
|  |   ListItemIcon, | ||||||
|  |   ListItemText | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; | ||||||
|  | import DeleteIcon from '@mui/icons-material/Delete'; | ||||||
| import permisoService from '../../services/Usuarios/permisoService'; | import permisoService from '../../services/Usuarios/permisoService'; | ||||||
| import type { PermisoDto } from '../../models/dtos/Usuarios/PermisoDto'; | import type { PermisoDto } from '../../models/dtos/Usuarios/PermisoDto'; | ||||||
| import type { CreatePermisoDto } from '../../models/dtos/Usuarios/CreatePermisoDto'; | import type { CreatePermisoDto } from '../../models/dtos/Usuarios/CreatePermisoDto'; | ||||||
| @@ -35,9 +39,9 @@ const GestionarPermisosPage: React.FC = () => { | |||||||
|  |  | ||||||
|   const cargarPermisos = useCallback(async () => { |   const cargarPermisos = useCallback(async () => { | ||||||
|     if (!isSuperAdmin) { |     if (!isSuperAdmin) { | ||||||
|         setError("Acceso denegado. Solo SuperAdmin puede gestionar permisos."); |       setError("Acceso denegado. Solo SuperAdmin puede gestionar permisos."); | ||||||
|         setLoading(false); |       setLoading(false); | ||||||
|         return; |       return; | ||||||
|     } |     } | ||||||
|     setLoading(true); setError(null); setApiErrorMessage(null); |     setLoading(true); setError(null); setApiErrorMessage(null); | ||||||
|     try { |     try { | ||||||
| @@ -74,13 +78,13 @@ const GestionarPermisosPage: React.FC = () => { | |||||||
|  |  | ||||||
|   const handleDelete = async (id: number) => { |   const handleDelete = async (id: number) => { | ||||||
|     if (window.confirm(`¿Está seguro de eliminar este permiso (ID: ${id})?`)) { |     if (window.confirm(`¿Está seguro de eliminar este permiso (ID: ${id})?`)) { | ||||||
|        setApiErrorMessage(null); |       setApiErrorMessage(null); | ||||||
|        try { |       try { | ||||||
|         await permisoService.deletePermiso(id); |         await permisoService.deletePermiso(id); | ||||||
|         cargarPermisos(); |         cargarPermisos(); | ||||||
|       } catch (err: any) { |       } catch (err: any) { | ||||||
|          const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el permiso.'; |         const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el permiso.'; | ||||||
|          setApiErrorMessage(message); |         setApiErrorMessage(message); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     handleMenuClose(); |     handleMenuClose(); | ||||||
| @@ -93,99 +97,103 @@ const GestionarPermisosPage: React.FC = () => { | |||||||
|     setAnchorEl(null); setSelectedPermisoRow(null); |     setAnchorEl(null); setSelectedPermisoRow(null); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|    const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); |   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||||
|    const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { |   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|      setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); |     setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); | ||||||
|    }; |   }; | ||||||
|    const displayData = permisos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); |   const displayData = permisos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||||
|  |  | ||||||
|   if (!loading && !isSuperAdmin) { |   if (!loading && !isSuperAdmin) { | ||||||
|       return ( |     return ( | ||||||
|            <Box sx={{ p: 2 }}> |       <Box sx={{ p: 1 }}> | ||||||
|               <Typography variant="h4" gutterBottom>Definición de Permisos</Typography> |         <Typography variant="h5" gutterBottom>Definición de Permisos</Typography> | ||||||
|               <Alert severity="error">{error || "Acceso denegado."}</Alert> |         <Alert severity="error">{error || "Acceso denegado."}</Alert> | ||||||
|            </Box> |       </Box> | ||||||
|       ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Definición de Permisos (SuperAdmin)</Typography> |       <Typography variant="h5" gutterBottom>Definición de Permisos (SuperAdmin)</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|         <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}> |         <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}> | ||||||
|             <TextField |           <TextField | ||||||
|                 label="Filtrar por Módulo" |             label="Filtrar por Módulo" | ||||||
|                 variant="outlined" |             variant="outlined" | ||||||
|                 size="small" |             size="small" | ||||||
|                 value={filtroModulo} |             value={filtroModulo} | ||||||
|                 onChange={(e) => setFiltroModulo(e.target.value)} |             onChange={(e) => setFiltroModulo(e.target.value)} | ||||||
|                 sx={{ flexGrow: 1, minWidth: '200px' }} // Para que se adapte mejor |             sx={{ flexGrow: 1, minWidth: '200px' }} // Para que se adapte mejor | ||||||
|             /> |           /> | ||||||
|             <TextField |           <TextField | ||||||
|                 label="Filtrar por CodAcc" |             label="Filtrar por CodAcc" | ||||||
|                 variant="outlined" |             variant="outlined" | ||||||
|                 size="small" |             size="small" | ||||||
|                 value={filtroCodAcc} |             value={filtroCodAcc} | ||||||
|                 onChange={(e) => setFiltroCodAcc(e.target.value)} |             onChange={(e) => setFiltroCodAcc(e.target.value)} | ||||||
|                 sx={{ flexGrow: 1, minWidth: '200px' }} |             sx={{ flexGrow: 1, minWidth: '200px' }} | ||||||
|             /> |           /> | ||||||
|             {/* El botón de búsqueda es opcional si el filtro es en tiempo real */} |           {/* El botón de búsqueda es opcional si el filtro es en tiempo real */} | ||||||
|             {/* <Button variant="contained" onClick={cargarPermisos}>Buscar</Button> */} |           {/* <Button variant="contained" onClick={cargarPermisos}>Buscar</Button> */} | ||||||
|         </Box> |         </Box> | ||||||
|          {isSuperAdmin && ( |         {isSuperAdmin && ( | ||||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> |           <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}> | ||||||
|              <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}> |             Agregar Nuevo Permiso | ||||||
|                 Agregar Nuevo Permiso |           </Button> | ||||||
|              </Button> |  | ||||||
|           </Box> |  | ||||||
|         )} |         )} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
|       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} |       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} | ||||||
|       {error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>} |       {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>} | ||||||
|       {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} |       {apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>} | ||||||
|  |  | ||||||
|       {!loading && !error && isSuperAdmin && ( |       {!loading && !error && isSuperAdmin && ( | ||||||
|          <TableContainer component={Paper}> |         <TableContainer component={Paper}> | ||||||
|            <Table size="small"> |           <Table size="small"> | ||||||
|              <TableHead> |             <TableHead> | ||||||
|                <TableRow> |               <TableRow> | ||||||
|                  <TableCell>Módulo</TableCell> |                 <TableCell>Módulo</TableCell> | ||||||
|                  <TableCell>Descripción</TableCell> |                 <TableCell>Descripción</TableCell> | ||||||
|                  <TableCell>CodAcc</TableCell> |                 <TableCell>CodAcc</TableCell> | ||||||
|                  <TableCell align="right">Acciones</TableCell> |                 <TableCell align="right">Acciones</TableCell> | ||||||
|                </TableRow> |               </TableRow> | ||||||
|              </TableHead> |             </TableHead> | ||||||
|              <TableBody> |             <TableBody> | ||||||
|                {displayData.length === 0 && !loading ? ( |               {displayData.length === 0 && !loading ? ( | ||||||
|                   <TableRow><TableCell colSpan={4} align="center">No se encontraron permisos.</TableCell></TableRow> |                 <TableRow><TableCell colSpan={4} align="center">No se encontraron permisos.</TableCell></TableRow> | ||||||
|                ) : ( |               ) : ( | ||||||
|                  displayData.map((permiso) => ( |                 displayData.map((permiso) => ( | ||||||
|                      <TableRow key={permiso.id}> |                   <TableRow key={permiso.id}> | ||||||
|                      <TableCell>{permiso.modulo}</TableCell> |                     <TableCell>{permiso.modulo}</TableCell> | ||||||
|                      <TableCell>{permiso.descPermiso}</TableCell> |                     <TableCell>{permiso.descPermiso}</TableCell> | ||||||
|                      <TableCell>{permiso.codAcc}</TableCell> |                     <TableCell>{permiso.codAcc}</TableCell> | ||||||
|                      <TableCell align="right"> |                     <TableCell align="right"> | ||||||
|                         <IconButton onClick={(e) => handleMenuOpen(e, permiso)}> |                       <IconButton onClick={(e) => handleMenuOpen(e, permiso)}> | ||||||
|                             <MoreVertIcon /> |                         <MoreVertIcon /> | ||||||
|                         </IconButton> |                       </IconButton> | ||||||
|                      </TableCell> |                     </TableCell> | ||||||
|                      </TableRow> |                   </TableRow> | ||||||
|                  )) |                 )) | ||||||
|                )} |               )} | ||||||
|              </TableBody> |             </TableBody> | ||||||
|            </Table> |           </Table> | ||||||
|            <TablePagination |           <TablePagination | ||||||
|              rowsPerPageOptions={[5, 10, 25, 50]} component="div" count={permisos.length} |             rowsPerPageOptions={[5, 10, 25, 50]} component="div" count={permisos.length} | ||||||
|              rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} |             rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} | ||||||
|              onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" |             onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" | ||||||
|            /> |           /> | ||||||
|          </TableContainer> |         </TableContainer> | ||||||
|        )} |       )} | ||||||
|  |  | ||||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> |       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||||
|         <MenuItem onClick={() => { handleOpenModal(selectedPermisoRow!); handleMenuClose(); }}>Modificar</MenuItem> |         <MenuItem onClick={() => { handleOpenModal(selectedPermisoRow!); handleMenuClose(); }}> | ||||||
|         <MenuItem onClick={() => handleDelete(selectedPermisoRow!.id)}>Eliminar</MenuItem> |           <ListItemIcon><EditIcon fontSize="small" /></ListItemIcon> | ||||||
|  |           <ListItemText>Modificar</ListItemText> | ||||||
|  |         </MenuItem> | ||||||
|  |         <MenuItem onClick={() => handleDelete(selectedPermisoRow!.id)}> | ||||||
|  |           <ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon> | ||||||
|  |           <ListItemText>Eliminar</ListItemText> | ||||||
|  |         </MenuItem> | ||||||
|       </Menu> |       </Menu> | ||||||
|  |  | ||||||
|       <PermisoFormModal |       <PermisoFormModal | ||||||
|   | |||||||
| @@ -2,10 +2,13 @@ import React, { useState, useEffect, useCallback } from 'react'; | |||||||
| import { | import { | ||||||
|     Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch, |     Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch, | ||||||
|     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, |     Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, | ||||||
|     CircularProgress, Alert, Tooltip |     CircularProgress, Alert, Tooltip, | ||||||
|  |     ListItemIcon, | ||||||
|  |     ListItemText | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||||
|  | import EditIcon from '@mui/icons-material/Edit'; | ||||||
| import VpnKeyIcon from '@mui/icons-material/VpnKey'; // Para resetear clave | import VpnKeyIcon from '@mui/icons-material/VpnKey'; // Para resetear clave | ||||||
| import usuarioService from '../../services/Usuarios/usuarioService'; | import usuarioService from '../../services/Usuarios/usuarioService'; | ||||||
| import type { UsuarioDto } from '../../models/dtos/Usuarios/UsuarioDto'; | import type { UsuarioDto } from '../../models/dtos/Usuarios/UsuarioDto'; | ||||||
| @@ -141,10 +144,9 @@ const GestionarUsuariosPage: React.FC = () => { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ p: 2 }}> |     <Box sx={{ p: 1 }}> | ||||||
|       <Typography variant="h4" gutterBottom>Gestionar Usuarios</Typography> |       <Typography variant="h5" gutterBottom>Gestionar Usuarios</Typography> | ||||||
|       <Paper sx={{ p: 2, mb: 2 }}> |       <Paper sx={{ p: 2, mb: 2 }}> | ||||||
|          {/* SECCIÓN DE FILTROS CORREGIDA */} |  | ||||||
|          <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}> |          <Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}> | ||||||
|             <TextField |             <TextField | ||||||
|                 label="Filtrar por Usuario" |                 label="Filtrar por Usuario" | ||||||
| @@ -164,11 +166,9 @@ const GestionarUsuariosPage: React.FC = () => { | |||||||
|             /> |             /> | ||||||
|          </Box> |          </Box> | ||||||
|          {puedeCrear && ( |          {puedeCrear && ( | ||||||
|           <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> |  | ||||||
|              <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenUsuarioModal()} sx={{ mb: 2 }}> |              <Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenUsuarioModal()} sx={{ mb: 2 }}> | ||||||
|                 Agregar Nuevo Usuario |                 Agregar Nuevo Usuario | ||||||
|              </Button> |              </Button> | ||||||
|           </Box> |  | ||||||
|         )} |         )} | ||||||
|       </Paper> |       </Paper> | ||||||
|  |  | ||||||
| @@ -231,7 +231,10 @@ const GestionarUsuariosPage: React.FC = () => { | |||||||
|  |  | ||||||
|       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> |       <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> | ||||||
|         {(puedeModificar || puedeAsignarPerfil) && ( |         {(puedeModificar || puedeAsignarPerfil) && ( | ||||||
|             <MenuItem onClick={() => { handleOpenUsuarioModal(selectedUsuarioRow!); handleMenuClose(); }}>Modificar</MenuItem> |             <MenuItem onClick={() => { handleOpenUsuarioModal(selectedUsuarioRow!); handleMenuClose(); }}> | ||||||
|  |               <ListItemIcon><EditIcon fontSize="small" /></ListItemIcon> | ||||||
|  |           <ListItemText>Modificar</ListItemText> | ||||||
|  |             </MenuItem> | ||||||
|         )} |         )} | ||||||
|         {puedeResetearClave && selectedUsuarioRow && currentUser?.userId !== selectedUsuarioRow.id && ( |         {puedeResetearClave && selectedUsuarioRow && currentUser?.userId !== selectedUsuarioRow.id && ( | ||||||
|             <MenuItem onClick={() => handleOpenSetPasswordModal(selectedUsuarioRow!)}> |             <MenuItem onClick={() => handleOpenSetPasswordModal(selectedUsuarioRow!)}> | ||||||
|   | |||||||
| @@ -2,6 +2,8 @@ import apiClient from '../apiClient'; | |||||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||||
| import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto'; | import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto'; | ||||||
| import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto'; | import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto'; | ||||||
|  | import type { PublicacionDiaSemanaDto } from '../../models/dtos/Distribucion/PublicacionDiaSemanaDto'; | ||||||
|  | import type { UpdatePublicacionDiasSemanaRequestDto } from '../../models/dtos/Distribucion/UpdatePublicacionDiasSemanaRequestDto'; | ||||||
|  |  | ||||||
| const getAllPublicaciones = async ( | const getAllPublicaciones = async ( | ||||||
|     nombreFilter?: string, |     nombreFilter?: string, | ||||||
| @@ -35,12 +37,29 @@ const deletePublicacion = async (id: number): Promise<void> => { | |||||||
|     await apiClient.delete(`/publicaciones/${id}`); |     await apiClient.delete(`/publicaciones/${id}`); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const getConfiguracionDiasPublicacion = async (idPublicacion: number): Promise<PublicacionDiaSemanaDto[]> => { | ||||||
|  |     const response = await apiClient.get<PublicacionDiaSemanaDto[]>(`/publicaciones/${idPublicacion}/dias-semana`); | ||||||
|  |     return response.data; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const updateConfiguracionDiasPublicacion = async (idPublicacion: number, data: UpdatePublicacionDiasSemanaRequestDto): Promise<void> => { | ||||||
|  |     await apiClient.put(`/publicaciones/${idPublicacion}/dias-semana`, data); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getPublicacionesPorDiaSemana = async (diaSemana: number): Promise<PublicacionDto[]> => { | ||||||
|  |     const response = await apiClient.get<PublicacionDto[]>('/publicaciones/por-dia-semana', { params: { dia: diaSemana } }); | ||||||
|  |     return response.data; | ||||||
|  | }; | ||||||
|  |  | ||||||
| const publicacionService = { | const publicacionService = { | ||||||
|     getAllPublicaciones, |     getAllPublicaciones, | ||||||
|     getPublicacionById, |     getPublicacionById, | ||||||
|     createPublicacion, |     createPublicacion, | ||||||
|     updatePublicacion, |     updatePublicacion, | ||||||
|     deletePublicacion, |     deletePublicacion, | ||||||
|  |     getConfiguracionDiasPublicacion, | ||||||
|  |     updateConfiguracionDiasPublicacion, | ||||||
|  |     getPublicacionesPorDiaSemana | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default publicacionService; | export default publicacionService; | ||||||
| @@ -366,6 +366,25 @@ const getControlDevolucionesPdf = async (params: { | |||||||
|   return response.data; |   return response.data; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const getTicketLiquidacionCanillaPdf = async (params: { | ||||||
|  |   fecha: string; // YYYY-MM-DD | ||||||
|  |   idCanilla: number; | ||||||
|  |   esAccionista?: boolean; // Hacerlo opcional, el backend podría tener un default | ||||||
|  | }): Promise<Blob> => { | ||||||
|  |   const queryParams: Record<string, string | number | boolean> = { | ||||||
|  |     fecha: params.fecha, | ||||||
|  |     idCanilla: params.idCanilla, | ||||||
|  |   }; | ||||||
|  |   if (params.esAccionista !== undefined) { | ||||||
|  |     queryParams.esAccionista = params.esAccionista; | ||||||
|  |   } | ||||||
|  |   const response = await apiClient.get('/reportes/ticket-liquidacion-canilla/pdf', { | ||||||
|  |     params: queryParams, | ||||||
|  |     responseType: 'blob', | ||||||
|  |   }); | ||||||
|  |   return response.data; | ||||||
|  | }; | ||||||
|  |  | ||||||
| const reportesService = { | const reportesService = { | ||||||
|   getExistenciaPapel, |   getExistenciaPapel, | ||||||
|   getExistenciaPapelPdf, |   getExistenciaPapelPdf, | ||||||
| @@ -401,6 +420,7 @@ const reportesService = { | |||||||
|   getListadoDistribucionDistribuidoresPdf, |   getListadoDistribucionDistribuidoresPdf, | ||||||
|   getControlDevolucionesData, |   getControlDevolucionesData, | ||||||
|   getControlDevolucionesPdf, |   getControlDevolucionesPdf, | ||||||
|  |   getTicketLiquidacionCanillaPdf, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default reportesService; | export default reportesService; | ||||||
		Reference in New Issue
	
	Block a user