﻿unit uDBGridHelper;

interface

uses
  Vcl.DBGrids, Vcl.Grids, Vcl.Controls, Vcl.Forms, System.Classes, System.SysUtils, Winapi.Windows, Data.DB;

const
  // Máximos anchos de columnas configurables (en píxeles) para diseño
  MAX_WIDTH_DATE = 95;       // Ancho suficiente para dd/mm/yyyy hh:nn:ss
  MAX_WIDTH_CODE = 100;      // Códigos de barras o códigos de ~10 caracteres
  MAX_WIDTH_QTY = 75;        // Cantidades de ~5 caracteres
  MAX_WIDTH_UNIT = 60;       // Unidades de medida (ud, kg, etc.)
  MAX_WIDTH_DECIMAL = 120;    // Importes, bases, precios, tipos de IVA

  // Configuración de anchos para columnas de tipo checkbox
  // El tamaño mínimo para una columna de checkbox es el ancho del nombre de la columna (título) + margen
  CHECKBOX_TITLE_PADDING = 20; // Margen adicional en píxeles para el título (bordes de celda y flecha de ordenación)
  MIN_WIDTH_CHECKBOX = 40;     // Ancho mínimo base cuando no hay título

type
  TDBGrid = class(Vcl.DBGrids.TDBGrid)
  private
    FPrevWidths: TArray<Integer>;
    FIsAutoFitting: Boolean;      // Bloquea ColWidthsChanged durante AutoFitColumns
    FHasFittedOnce: Boolean;
    FOriginalFormWidth: Integer;  // Ancho original del formulario al primer Paint
  protected
    procedure ColWidthsChanged; override;
    procedure Paint; override;
    procedure Resize; override;
  public
    function IsCheckBoxColumn(AColumn: TColumn): Boolean;
    function GetCheckBoxColumnMinWidth(AColumn: TColumn): Integer;
    procedure AutoFitColumns;
  end;

function IsCheckBoxColumn(AColumn: TColumn): Boolean;
function GetCheckBoxColumnMinWidth(AGrid: Vcl.DBGrids.TDBGrid; AColumn: TColumn): Integer;

implementation

function GetColumnTitleText(AColumn: TColumn): string;
begin
  Result := '';
  if Assigned(AColumn) then
  begin
    if (AColumn.Title <> nil) and (Trim(AColumn.Title.Caption) <> '') then
      Result := Trim(AColumn.Title.Caption)
    else if Assigned(AColumn.Field) and (Trim(AColumn.Field.DisplayName) <> '') then
      Result := Trim(AColumn.Field.DisplayName)
    else if Assigned(AColumn.Field) and (Trim(AColumn.Field.FieldName) <> '') then
      Result := Trim(AColumn.Field.FieldName)
    else
      Result := Trim(AColumn.FieldName);
  end;
end;

function IsCheckBoxColumn(AColumn: TColumn): Boolean;
var
  LName: string;
begin
  Result := False;
  if not Assigned(AColumn) then Exit;

  // 1. Campo explícito de tipo booleano
  if Assigned(AColumn.Field) and (AColumn.Field is TBooleanField) then
    Exit(True);

  // 2. Comprobar nombre de campo
  if Assigned(AColumn.Field) and (AColumn.Field.FieldName <> '') then
    LName := LowerCase(AColumn.Field.FieldName)
  else
    LName := LowerCase(AColumn.FieldName);

  Result := (Pos('activo', LName) > 0) or
            (Pos('active', LName) > 0) or
            (Pos('compartido', LName) > 0) or
            (Pos('compartida', LName) > 0) or
            (Pos('bloquear', LName) > 0) or
            (Pos('bloqueado', LName) > 0) or
            (Pos('asegurado', LName) > 0) or
            (Pos('comprobado', LName) > 0) or
            (Pos('consolidado', LName) > 0) or
            (Pos('anulado', LName) > 0) or
            (Pos('anulada', LName) > 0) or
            (Pos('validado', LName) > 0);

  // 3. Si el nombre del campo no coincide, comprobar también el título de la cabecera
  if not Result and (AColumn.Title <> nil) and (AColumn.Title.Caption <> '') then
  begin
    LName := LowerCase(AColumn.Title.Caption);
    Result := (Pos('activo', LName) > 0) or
              (Pos('active', LName) > 0) or
              (Pos('compartido', LName) > 0) or
              (Pos('compartida', LName) > 0) or
              (Pos('bloquear', LName) > 0) or
              (Pos('bloqueado', LName) > 0) or
              (Pos('asegurado', LName) > 0) or
              (Pos('comprobado', LName) > 0) or
              (Pos('consolidado', LName) > 0) or
              (Pos('anulado', LName) > 0) or
              (Pos('anulada', LName) > 0) or
              (Pos('validado', LName) > 0);
  end;
end;

function GetCheckBoxColumnMinWidth(AGrid: Vcl.DBGrids.TDBGrid; AColumn: TColumn): Integer;
var
  LTitle: string;
begin
  Result := MIN_WIDTH_CHECKBOX;
  if not Assigned(AColumn) then Exit;

  LTitle := GetColumnTitleText(AColumn);
  // Eliminar caracteres aceleradores '&' de la cabecera para medir solo el texto real
  LTitle := StringReplace(LTitle, '&', '', [rfReplaceAll]);

  if (LTitle <> '') and Assigned(AGrid) then
  begin
    AGrid.Canvas.Font.Assign(AGrid.TitleFont);
    Result := AGrid.Canvas.TextWidth(LTitle) + CHECKBOX_TITLE_PADDING;
    if Result < MIN_WIDTH_CHECKBOX then
      Result := MIN_WIDTH_CHECKBOX;
  end;
end;

{ TDBGrid }

function TDBGrid.IsCheckBoxColumn(AColumn: TColumn): Boolean;
begin
  Result := uDBGridHelper.IsCheckBoxColumn(AColumn);
end;

function TDBGrid.GetCheckBoxColumnMinWidth(AColumn: TColumn): Integer;
begin
  Result := uDBGridHelper.GetCheckBoxColumnMinWidth(Self, AColumn);
end;

procedure TDBGrid.ColWidthsChanged;
var
  i: Integer;
  Delta: Integer;
  ParentForm: TCustomForm;
  NewFormWidth, MaxWidth: Integer;
  LMinW: Integer;
begin
  inherited ColWidthsChanged;

  // Bloquear completamente durante AutoFitColumns para evitar bucle de retroalimentación
  if FIsAutoFitting then Exit;
  if Columns.Count = 0 then Exit;

  // Asegurar que ninguna columna checkbox se reduzca por debajo del ancho de su nombre
  for i := 0 to Columns.Count - 1 do
  begin
    if IsCheckBoxColumn(Columns[i]) then
    begin
      LMinW := GetCheckBoxColumnMinWidth(Columns[i]);
      if Columns[i].Width < LMinW then
        Columns[i].Width := LMinW;
    end;
  end;

  // Solo permitir cambiar el ancho del formulario si el usuario está arrastrando
  // manualmente una columna (el control tiene la captura del ratón)
  if Winapi.Windows.GetCapture <> Self.Handle then
  begin
    // Aún así, sincronizar los anchos almacenados para evitar deltas fantasma
    SetLength(FPrevWidths, Columns.Count);
    for i := 0 to Columns.Count - 1 do
      FPrevWidths[i] := Columns[i].Width;
    Exit;
  end;

  // Si el tamaño previo no está inicializado o cambió la cantidad de columnas, reiniciamos
  if Length(FPrevWidths) <> Columns.Count then
  begin
    SetLength(FPrevWidths, Columns.Count);
    for i := 0 to Columns.Count - 1 do
      FPrevWidths[i] := Columns[i].Width;
    Exit;
  end;

  // Buscar qué columna ha cambiado y calcular el delta
  Delta := 0;
  for i := 0 to Columns.Count - 1 do
  begin
    if Columns[i].Width <> FPrevWidths[i] then
    begin
      Delta := Columns[i].Width - FPrevWidths[i];
      Break;
    end;
  end;

  // Solo ampliamos el formulario si el usuario ensancha una columna (Delta > 0)
  if Delta > 0 then
  begin
    ParentForm := GetParentForm(Self);
    if Assigned(ParentForm) then
    begin
      // Limitar al ancho del monitor para no desbordar
      MaxWidth := Screen.MonitorFromWindow(ParentForm.Handle).WorkareaRect.Width;
      NewFormWidth := ParentForm.Width + Delta;
      if NewFormWidth > MaxWidth then
        NewFormWidth := MaxWidth;
      
      if NewFormWidth > ParentForm.Width then
      begin
        FIsAutoFitting := True;
        try
          ParentForm.Width := NewFormWidth;
        finally
          FIsAutoFitting := False;
        end;
      end;
    end;
  end;

  // Actualizar los anchos almacenados
  SetLength(FPrevWidths, Columns.Count);
  for i := 0 to Columns.Count - 1 do
    FPrevWidths[i] := Columns[i].Width;
end;

procedure TDBGrid.Paint;
var
  i: Integer;
  LMinW: Integer;
begin
  if Columns.Count > 0 then
  begin
    for i := 0 to Columns.Count - 1 do
    begin
      if Assigned(Columns[i].Field) and (Columns[i].Field is TDateTimeField) then
      begin
        TDateTimeField(Columns[i].Field).DisplayFormat := 'dd/mm/yyyy';
      end;

      // Garantizar que toda columna checkbox mida como mínimo el ancho de su nombre
      if IsCheckBoxColumn(Columns[i]) then
      begin
        LMinW := GetCheckBoxColumnMinWidth(Columns[i]);
        if Columns[i].Width < LMinW then
          Columns[i].Width := LMinW;
      end;
    end;
  end;

  inherited Paint;

  { COMENTADO: Deshabilitado para mantener siempre los anchos de columnas configurados estrictamente en el código
  if not FHasFittedOnce and (Columns.Count > 0) and (ClientWidth > 50) then
  begin
    FHasFittedOnce := True;
    // Recordar el ancho original del formulario padre la primera vez
    if FOriginalFormWidth = 0 then
    begin
      if GetParentForm(Self) <> nil then
        FOriginalFormWidth := GetParentForm(Self).Width;
    end;
    AutoFitColumns;
  end;
  }
end;

procedure TDBGrid.Resize;
begin
  inherited Resize;
  // Solo reajustar si ya se hizo el ajuste inicial y no estamos en medio de otro ajuste
  { COMENTADO: Deshabilitado para mantener siempre los anchos de columnas configurados estrictamente en el código
  if FHasFittedOnce and not FIsAutoFitting and (Columns.Count > 0) and (ClientWidth > 50) then
    AutoFitColumns;
  }
end;

procedure TDBGrid.AutoFitColumns;
var
  i: Integer;
  TotalWidth, ClientW: Integer;
  Ratio: Double;
  ColMax: TArray<Integer>;
  IsConstrained: TArray<Boolean>;
  RemainingWidth, AllocWidth: Integer;
  HasConstraints: Boolean;
  LField: TField;
  LFieldName: string;
  LMinW: Integer;
begin
  if Columns.Count = 0 then Exit;
  if FIsAutoFitting then Exit; // Prevenir reentrada

  FIsAutoFitting := True;
  try
    // 1. Determinar el ancho máximo configurado para cada columna
    SetLength(ColMax, Columns.Count);
    SetLength(IsConstrained, Columns.Count);
    HasConstraints := False;

    for i := 0 to Columns.Count - 1 do
    begin
      ColMax[i] := 0; // 0 significa sin restricción
      IsConstrained[i] := False;

      // Columna de tipo checkbox: el tamaño mínimo es el ancho del nombre de la columna
      if IsCheckBoxColumn(Columns[i]) then
      begin
        LMinW := GetCheckBoxColumnMinWidth(Columns[i]);
        if Columns[i].Width < LMinW then
          Columns[i].Width := LMinW;

        // Limitar también el máximo para no sobredimensionar la celda del checkbox
        ColMax[i] := Columns[i].Width;
        HasConstraints := True;
        Continue;
      end;

      LField := Columns[i].Field;
      if Assigned(LField) then
      begin
        LFieldName := LowerCase(LField.FieldName);
        
        // Tipo Fecha
        if (LField is TDateField) or (LField is TDateTimeField) or 
           (Pos('fecha', LFieldName) > 0) or (Pos('date', LFieldName) > 0) then
        begin
          ColMax[i] := MAX_WIDTH_DATE;
        end
        // Códigos
        else if (Pos('codigo', LFieldName) > 0) or (Pos('code', LFieldName) > 0) or (Pos('cod_', LFieldName) > 0) then
        begin
          ColMax[i] := MAX_WIDTH_CODE;
        end
        // Cantidades
        else if (Pos('cantidad', LFieldName) > 0) or (Pos('cant', LFieldName) > 0) or (Pos('qty', LFieldName) > 0) then
        begin
          ColMax[i] := MAX_WIDTH_QTY;
        end
        // Unidades
        else if (Pos('unidad', LFieldName) > 0) or (Pos('unit', LFieldName) > 0) then
        begin
          ColMax[i] := MAX_WIDTH_UNIT;
        end
        // Importes / Decimales / Precios / IVA
        else if (LField is TBCDField) or (LField is TFMTBCDField) or (LField is TFloatField) or 
                (Pos('precio', LFieldName) > 0) or (Pos('pvp', LFieldName) > 0) or (Pos('total', LFieldName) > 0) or
                (Pos('iva', LFieldName) > 0) or (Pos('base', LFieldName) > 0) or (Pos('subtotal', LFieldName) > 0) then
        begin
          ColMax[i] := MAX_WIDTH_DECIMAL;
        end;

        if ColMax[i] > 0 then
          HasConstraints := True;
      end;
    end;

    // 2. Calcular el ancho disponible real del grid
    ClientW := ClientWidth;
    if dgIndicator in Options then
      Dec(ClientW, 15);
    Dec(ClientW, 6); // Margen extra para bordes y líneas

    if ClientW <= 0 then Exit;

    // 3. Distribuir columnas DENTRO del espacio disponible (nunca expandir)
    if HasConstraints then
    begin
      TotalWidth := 0;
      for i := 0 to Columns.Count - 1 do
        TotalWidth := TotalWidth + Columns[i].Width;

      if TotalWidth > 0 then
      begin
        Ratio := ClientW / TotalWidth;
        RemainingWidth := ClientW;
        AllocWidth := 0;

        // Paso 1: Fijar columnas de checkbox y las que exceden su máximo tras la escala
        for i := 0 to Columns.Count - 1 do
        begin
          if IsCheckBoxColumn(Columns[i]) then
          begin
            // El checkbox queda fijado con su ancho mínimo (ancho del nombre de columna)
            IsConstrained[i] := True;
            Dec(RemainingWidth, Columns[i].Width);
          end
          else if (ColMax[i] > 0) and (Round(Columns[i].Width * Ratio) > ColMax[i]) then
          begin
            IsConstrained[i] := True;
            Columns[i].Width := ColMax[i];
            Dec(RemainingWidth, ColMax[i]);
          end
          else
            AllocWidth := AllocWidth + Columns[i].Width;
        end;

        // Paso 2: Distribuir el ancho restante entre las columnas no restringidas
        if (AllocWidth > 0) and (RemainingWidth > 0) then
        begin
          Ratio := RemainingWidth / AllocWidth;
          for i := 0 to Columns.Count - 1 do
          begin
            if not IsConstrained[i] then
              Columns[i].Width := Round(Columns[i].Width * Ratio);
          end;
        end;
      end;
    end
    else
    begin
      // Comportamiento proporcional clásico si no hay campos restringidos
      TotalWidth := 0;
      for i := 0 to Columns.Count - 1 do
        TotalWidth := TotalWidth + Columns[i].Width;

      if TotalWidth > 0 then
      begin
        Ratio := ClientW / TotalWidth;
        for i := 0 to Columns.Count - 1 do
          Columns[i].Width := Round(Columns[i].Width * Ratio);
      end;
    end;

    // Verificación final de seguridad: asegurar tamaño mínimo en columnas checkbox
    for i := 0 to Columns.Count - 1 do
    begin
      if IsCheckBoxColumn(Columns[i]) then
      begin
        LMinW := GetCheckBoxColumnMinWidth(Columns[i]);
        if Columns[i].Width < LMinW then
          Columns[i].Width := LMinW;
      end;
    end;

    // 4. Sincronizar el array con los nuevos anchos
    SetLength(FPrevWidths, Columns.Count);
    for i := 0 to Columns.Count - 1 do
      FPrevWidths[i] := Columns[i].Width;

  finally
    FIsAutoFitting := False;
  end;
end;

end.

