unit search_orm;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Types, System.TypInfo,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, pixel_classes, System.StrUtils,
  System.Generics.Collections, Vcl.StdCtrls, Vcl.ExtCtrls, scGPExtControls,
  scControls, scGPFontControls, scGPControls, sPanel, AdvUtil, AdvGrid,
  AdvCGrid, Vcl.ComCtrls, Vcl.WinXPickers, orm_utils, ORM_CustomAttributes,
  System.Rtti, System.DateUtils, Data.DB, Uni, Vcl.Grids, Vcl.Mask,
  pixel_utils, AdvObj, BaseGrid, interposer_class;

const
  FILTER_PANEL_HEIGHT = 35;
  FILTER_PANEL_SPACING = 5;
  PANEL_MARGIN = 5;
  GRID_MIN_HEIGHT = 200;

type
  TQryRecInfoClass = class of TQryRecInfo;

type
  TSearchColumn = class
  private
    FFieldName: string;       // Database column name
    FPropertyName: string;    // ORM property name
    FDisplayName: string;
    FWidth: Integer;
    FVisible: Boolean;
    FUsableAsFilter: Boolean;
    FDataType: TORMFormatType;
    FCustomFormatType: TNullable<TORMFormatType>;
  public
  constructor Create(const AFieldName, ADisplayName: string; AWidth: Integer;
    AVisible: Boolean = True; AUsableAsFilter: Boolean = True;
    ACustomFormatType: TORMFormatType = tiNone);
    property FieldName: string read FFieldName;
    property PropertyName: string read FPropertyName write FPropertyName;
    property DisplayName: string read FDisplayName;
    property Width: Integer read FWidth;
    property Visible: Boolean read FVisible;
    property UsableAsFilter: Boolean read FUsableAsFilter;
    property DataType: TORMFormatType read FDataType write FDataType;
    property CustomFormatType: TNullable<TORMFormatType> read FCustomFormatType;
    procedure SetFormatType(const Value: TORMFormatType);
  end;

  TSelectedSearch = class
  private
    FValues: TDictionary<string, Variant>;
    FDisplayValues: TDictionary<string, string>;
  public
    constructor Create;
    destructor Destroy; override;
    procedure SetValue(const AFieldName: string; const AValue: Variant; const ADisplayValue: string);
    function GetValue(const AFieldName: string): Variant;
    function GetDisplayValue(const AFieldName: string): string;
    property Values[const AFieldName: string]: Variant read GetValue; default;
  end;

  TFilterPanel = class
  private
    FPanel: TscGPPanel;
    FLabel: TLabel;
    FDeleteButton: TscGPCharGlyphButton;
    FFieldName: string;
    FDisplayText: string;
    FFilterID: TGUID;
  public
    constructor Create(AOwner: TComponent; const AFieldName, ADisplayText: string; const AFilterID: TGUID);
    procedure UpdatePosition(const Top: Integer);
    property Panel: TscGPPanel read FPanel;
    property FieldName: string read FFieldName;
    property DisplayText: string read FDisplayText;
    property FilterID: TGUID read FFilterID;
  end;

  TORMSearchForm = class(TForm)
    pnlTop: TPanel;
    pnlFilters: TscGPPanel;
    pnlActiveFilters: TscGPPanel;
    pnlGrid: TsPanel;
    pnlBottom: TPanel;
    cboField: TscComboBox;
    cboOperator: TscComboBox;
    edtValue1: TscGPEdit;
    edtValue2: TscGPEdit;
    dateValue1: TscGPDateEdit;
    dateValue2: TscGPDateEdit;
    btnAddFilter: TscGPCharGlyphButton;
    SearchGrid: TAdvColumnGrid;
    btnSearch: TscGPCharGlyphButton;
    btnClose: TscGPCharGlyphButton;
    btnClearFilters: TscGPCharGlyphButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure cboFieldChange(Sender: TObject);
    procedure cboOperatorChange(Sender: TObject);
    procedure btnAddFilterClick(Sender: TObject);
    procedure btnSearchClick(Sender: TObject);
    procedure btnCloseClick(Sender: TObject);
    procedure btnClearFiltersClick(Sender: TObject);
    procedure SearchGridDblClick(Sender: TObject);
    procedure FilterDeleteButtonClick(Sender: TObject);
  private
    FFilterInterface: IORMFilter;
    FFilterPanels: TObjectList<TFilterPanel>;
    FSearchColumns: TObjectList<TSearchColumn>;
    FSelectedSearch: TSelectedSearch;
    FBaseQuery: string;
    FConnection: TUniConnection;
    FQuery: TUniQuery;
    FRecordClass: TClass;
    FFieldTypes: TDictionary<string, TORMFormatType>;
    FCurrentFieldType: TORMFormatType;
    cboEnumValue: TscComboBox;
    FCustomFilters: TStringList;  // Store SQL WHERE conditions
    FCustomFilterDisplays: TStringList; // Store readable filter descriptions

    procedure CreateEnumCombo;
    procedure SetupGrid;
    procedure UpdateValueControls;
    procedure RemoveFilterPanel(const AFieldName: string);
    procedure ArrangeFilterPanels;
    procedure ClearFilters;
    procedure CreateFilterPanel(const AFieldName, ADisplayText: string; const AFilterID: TGUID);
    procedure LoadData;
    procedure SetupFieldTypes;
    procedure PopulateOperators(ADataType: TORMFormatType);
    function GetFilteredSQL: string;
    procedure UpdateControlStates;
    procedure UpdateSelectedSearch;
    function GetSelectedRow: Integer;
    procedure SetupColumns;
    procedure UpdateGridData;
    function ValidateFilterValues(DataType: TORMFormatType): Boolean;
    function GetFilterValue1: Variant;
    function GetFilterValue2: Variant;
    function GetFilterDisplayText: string;
    procedure ClearFilterInputs;
    procedure SetupVisualStyles;
    function GetOperatorFromText(const OpText: string): TFilterOperator;
    procedure ConfigureOperatorCombo;
    procedure AdjustValueEditors;
    procedure InitializePropertyMappings;
    function AdjustDateTimeValue(const ADate: TDateTime; AOperator: TFilterOperator; IsEndValue: Boolean = False): TDateTime;
    function GetBooleanMapAttribute(const FieldName: string): BooleanMapAttribute;
    function IsBooleanProperty(const FieldName: string; out IsNullable: Boolean): Boolean;
    procedure SetupBooleanComboBox(const FieldName: string);
    procedure BuildCustomFilterCondition(const FieldName: string;
      DataType: TORMFormatType; Op: TFilterOperator; const Value1, Value2: Variant;
      var SQLCondition: string; var DisplayText: string);
    function GetSQLOperator(Op: TFilterOperator): string;
    function FormatSQLValue(const Value: Variant; DataType: TORMFormatType): string;
    procedure RebuildCustomFilters;

  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

class function Execute(
  AConnection: TUniConnection;
  const ABaseQuery: string;
  const ASearchColumns: array of TSearchColumn;
  var ASelectedSearch: TSelectedSearch;
  ARecordClass: TClass = nil): Boolean;

procedure InitializeSearch(
  AConnection: TUniConnection;
  const ABaseQuery: string;
  const ASearchColumns: array of TSearchColumn;
  ARecordClass: TClass = nil);
  end;

type
  TORMSearchBuilder = class
  private
    FColumns: TObjectList<TSearchColumn>;
    FConnection: TUniConnection;
    FSelectedInfo: TSelectedSearch;
    FRecordClass: TClass;
    FBaseQuery: string;

    procedure ExtractColumnsFromClass;
    function GetFormatTypeFromProperty(Prop: TRttiProperty): TORMFormatType;
  public
    constructor Create;
    destructor Destroy; override;

    // Fluent interface methods
    function ForClass<T: TQryRecInfo>(AClass: TClass = nil): TORMSearchBuilder;
    function WithConnection(AConnection: TUniConnection): TORMSearchBuilder;
    function WithQuery(const AQuery: string): TORMSearchBuilder;
    function AddColumn(const AFieldName, ADisplayName: string; AWidth: Integer;
      AVisible: Boolean = True; AUsableAsFilter: Boolean = True;
      AFormatType: TORMFormatType = tiNone): TORMSearchBuilder;

    // Execute search
    function Execute: Boolean;

    // Access selected results
    property SelectedInfo: TSelectedSearch read FSelectedInfo;
  end;

implementation

{$R *.dfm}

uses Math;

{ TSearchColumn }

constructor TSearchColumn.Create(const AFieldName, ADisplayName: string;
  AWidth: Integer; AVisible: Boolean = True; AUsableAsFilter: Boolean = True;
  ACustomFormatType: TORMFormatType = tiNone);
begin
  inherited Create;
  FFieldName := AFieldName;
  FDisplayName := ADisplayName;
  FWidth := AWidth;
  FVisible := AVisible;
  FUsableAsFilter := AUsableAsFilter;
  FDataType := tiString; // Default value

  TORMLogger.Log(Format('Creating SearchColumn - Field: %s, Display: %s, CustomFormatType: %s',
    [AFieldName, ADisplayName, GetEnumName(TypeInfo(TORMFormatType), Ord(ACustomFormatType))]));

  // Set custom format type if specified
  if ACustomFormatType <> tiNone then
  begin
    FCustomFormatType := ACustomFormatType;
    FDataType := ACustomFormatType; // Also set the DataType
    TORMLogger.Log(Format('  Setting custom format type - Field: %s, Type: %s',
      [AFieldName, GetEnumName(TypeInfo(TORMFormatType), Ord(ACustomFormatType))]));
  end
  else
  begin
    FCustomFormatType := NullValue;
    TORMLogger.Log(Format('  No custom format type specified for field: %s', [AFieldName]));
  end;

  // Property name will be set later when record class is available
  FPropertyName := '';
end;

{ TSelectedSearch }

constructor TSelectedSearch.Create;
begin
  inherited;
  FValues := TDictionary<string, Variant>.Create;
  FDisplayValues := TDictionary<string, string>.Create;
end;

destructor TSelectedSearch.Destroy;
begin
  FValues.Free;
  FDisplayValues.Free;
  inherited;
end;

procedure TSelectedSearch.SetValue(const AFieldName: string; const AValue: Variant; const ADisplayValue: string);
begin
  FValues.AddOrSetValue(AFieldName, AValue);
  FDisplayValues.AddOrSetValue(AFieldName, ADisplayValue);
end;

function TSelectedSearch.GetValue(const AFieldName: string): Variant;
begin
  if not FValues.TryGetValue(AFieldName, Result) then
    Result := Null;
end;

function TSelectedSearch.GetDisplayValue(const AFieldName: string): string;
begin
  if not FDisplayValues.TryGetValue(AFieldName, Result) then
    Result := '';
end;

{ TFilterPanel }

constructor TFilterPanel.Create(AOwner: TComponent; const AFieldName,
  ADisplayText: string; const AFilterID: TGUID);
begin
  inherited Create;
  FFieldName := AFieldName;
  FDisplayText := ADisplayText;
  FFilterID := AFilterID;

  FPanel := TscGPPanel.Create(AOwner);
  with FPanel do
  begin
    Parent := TWinControl(AOwner);
    Height := FILTER_PANEL_HEIGHT;
    Align := alNone;
    FluentUIOpaque := False;
    BackgroundStyle := gppbsColor;
    FillColor := clWhite;
    FillColorAlpha := 230;
    FrameColor := clGray;
    FrameColorAlpha := 40;
    FrameRadius := 3;
    Padding.Left := 5;
    Padding.Right := 5;
    Padding.Top := 2;
    Padding.Bottom := 2;
  end;

  FLabel := TLabel.Create(FPanel);
  with FLabel do
  begin
    Parent := FPanel;
    Left := 10;
    Top := (FPanel.Height - Height) div 2;
    AutoSize := True;
    Caption := ADisplayText;
  end;

  FDeleteButton := TscGPCharGlyphButton.Create(FPanel);
  with FDeleteButton do
  begin
    Parent := FPanel;
    Width := 24;
    Height := 24;
    Top := (FPanel.Height - Height) div 2;
    Align := alRight;
    Margins.Right := 5;
    Caption := '';
    Tag := NativeInt(Self);
    Options.NormalColor := clBtnFace;
    Options.HotColor := clBtnFace;
    Options.PressedColor := clBtnFace;
    Options.FocusedColor := clBtnFace;
    GlyphOptions.NormalColor := clGray;
    GlyphOptions.HotColor := clRed;
    GlyphOptions.PressedColor := clMaroon;
    GlyphOptions.Size := 10;
    GlyphOptions.Index := 13;
  end;
end;

procedure TFilterPanel.UpdatePosition(const Top: Integer);
begin
  FPanel.Top := Top;
end;

{ TORMSearchForm }

constructor TORMSearchForm.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FCustomFilters := TStringList.Create;
  FCustomFilterDisplays := TStringList.Create;
  FFilterPanels := TObjectList<TFilterPanel>.Create(True);
  FSearchColumns := TObjectList<TSearchColumn>.Create(True);
  FFieldTypes := TDictionary<string, TORMFormatType>.Create;

  pnlActiveFilters.Visible := False;
  pnlActiveFilters.Height := 0;
  pnlActiveFilters.Align := alTop;

  SetupGrid;
  SetupVisualStyles;
end;

destructor TORMSearchForm.Destroy;
begin
  try
    if Assigned(FFilterInterface) then
      FFilterInterface.Clear;

    FCustomFilters.Free;
    FCustomFilterDisplays.Free;
    FFilterPanels.Free;
    FSearchColumns.Free;
    FFieldTypes.Free;
    FQuery.Free;

  except
    on E: Exception do
      ShowMessage('Error during cleanup: ' + E.Message);
  end;
  inherited;
end;

procedure TORMSearchForm.FormCreate(Sender: TObject);
begin
  inherited;
  CreateEnumCombo;
  SetupGrid;
  SetupVisualStyles;
  UpdateControlStates;
end;

procedure TORMSearchForm.FormDestroy(Sender: TObject);
begin
  if Assigned(cboEnumValue) then
    FreeAndNil(cboEnumValue);
  inherited;
end;

procedure TORMSearchForm.cboFieldChange(Sender: TObject);
var
  SelectedColumn: TSearchColumn;
begin
  if cboField.ItemIndex < 0 then
    Exit;

  for SelectedColumn in FSearchColumns do
  begin
    if SelectedColumn.DisplayName = cboField.Text then
    begin
      ConfigureOperatorCombo;  // Will handle operator population
      Break;
    end;
  end;
end;

procedure TORMSearchForm.cboOperatorChange(Sender: TObject);
begin
  AdjustValueEditors;
end;

procedure TORMSearchForm.btnAddFilterClick(Sender: TObject);
var
  Column: TSearchColumn;
  Op: TFilterOperator;
  Value1, Value2: Variant;
  SQLCondition, DisplayText: string;
  FilterID: TGUID;
  IsDateType: Boolean;
  ActualDataType: TORMFormatType;
  Context: TRttiContext;
  RttiType: TRttiType;
  Prop: TRttiProperty;
  Field: TRttiField;
  EnumAttr: EnumValuesAttribute;
  TransAttr: TranslationAttribute;
  PropName: string;
  AdjustedValue1, AdjustedValue2: TDateTime;
begin
  if cboField.ItemIndex < 0 then Exit;

  Op := TFilterOperator(NativeInt(cboOperator.Items.Objects[cboOperator.ItemIndex]));
  FilterID := GUID_NULL;

  for Column in FSearchColumns do
  begin
    if Column.DisplayName = cboField.Text then
    begin
      // Get property name based on whether we're using ORM or custom search
      if Assigned(FFilterInterface) then
        PropName := Column.PropertyName
      else
        PropName := Column.FieldName;

      if Column.CustomFormatType.HasValue then
        ActualDataType := Column.CustomFormatType.Value
      else
        ActualDataType := Column.DataType;

      if not ValidateFilterValues(ActualDataType) then
        Exit;

      IsDateType := ActualDataType in [tiDate, tiDateTime];

      // Get filter values based on control visibility
      if cboEnumValue.Visible then
        Value1 := cboEnumValue.Text
      else if IsDateType then
      begin
        if Op in [foBetween, foNotBetween] then
        begin
          AdjustedValue1 := AdjustDateTimeValue(dateValue1.Date, Op, False);
          AdjustedValue2 := AdjustDateTimeValue(dateValue2.Date, Op, True);
          Value1 := AdjustedValue1;
          Value2 := AdjustedValue2;
        end
        else
        begin
          if Op = foEqual then
          begin
            // For Equal operator on dates, we'll use BETWEEN with start and end of day
            AdjustedValue1 := AdjustDateTimeValue(dateValue1.Date, Op, False);
            AdjustedValue2 := AdjustDateTimeValue(dateValue1.Date, Op, True);
            Value1 := AdjustedValue1;
            Value2 := AdjustedValue2;
          end
          else if Op = foNotEqual then
          begin
            // For NotEqual operator on dates, we'll use NOT BETWEEN with start and end of day
            AdjustedValue1 := AdjustDateTimeValue(dateValue1.Date, Op, False);
            AdjustedValue2 := AdjustDateTimeValue(dateValue1.Date, Op, True);
            Value1 := AdjustedValue1;
            Value2 := AdjustedValue2;
          end
          else if Op = foLessEqual then
          begin
            // For LessOrEqual, adjust to end of day
            Value1 := AdjustDateTimeValue(dateValue1.Date, Op, True);
          end
          else
          begin
            Value1 := AdjustDateTimeValue(dateValue1.Date, Op, False);
          end;
        end;
      end
      else
      begin
        case ActualDataType of
          tiInteger: Value1 := PegaInt(edtValue1.Text);
          tiFloat, tiMoney: Value1 := PegaFloat(edtValue1.Text);
          else Value1 := edtValue1.Text;
        end;

        if Op in [foBetween, foNotBetween] then
        begin
          case ActualDataType of
            tiInteger: Value2 := PegaInt(edtValue2.Text);
            tiFloat, tiMoney: Value2 := PegaFloat(edtValue2.Text);
            else Value2 := edtValue2.Text;
          end;
        end;
      end;

      // Apply filters based on the interface type
      if Assigned(FFilterInterface) then
      begin
        if IsDateType then
        begin
          case Op of
            foEqual:
              begin
                // Convert Equal to Between for dates
                FFilterInterface.Where(PropName).Between(Value1, Value2);
                DisplayText := Format('%s = %s',
                  [Column.DisplayName, FormatDateTime('dd/mm/yyyy', dateValue1.Date)]);
              end;

            foNotEqual:
              begin
                FFilterInterface.Where(PropName).NotBetween(Value1, Value2);
                DisplayText := Format('%s <> %s',
                  [Column.DisplayName, FormatDateTime('dd/mm/yyyy', dateValue1.Date)]);
              end;

            foBetween:
              begin
                FFilterInterface.Where(PropName).Between(Value1, Value2);
                DisplayText := Format('%s Between %s And %s',
                  [Column.DisplayName,
                   FormatDateTime('dd/mm/yyyy', dateValue1.Date),
                   FormatDateTime('dd/mm/yyyy', dateValue2.Date)]);
              end;

            foNotBetween:
              begin
                FFilterInterface.Where(PropName).NotBetween(Value1, Value2);
                DisplayText := Format('%s Not Between %s And %s',
                  [Column.DisplayName,
                   FormatDateTime('dd/mm/yyyy', dateValue1.Date),
                   FormatDateTime('dd/mm/yyyy', dateValue2.Date)]);
              end;

            foLessEqual:
              begin
                FFilterInterface.Where(PropName).LessOrEqual(Value1);
                DisplayText := Format('%s <= %s',
                  [Column.DisplayName, FormatDateTime('dd/mm/yyyy', dateValue1.Date)]);
              end;

            foGreaterEqual:
              begin
                FFilterInterface.Where(PropName).GreaterOrEqual(Value1);
                DisplayText := Format('%s >= %s',
                  [Column.DisplayName, FormatDateTime('dd/mm/yyyy', dateValue1.Date)]);
              end;

            foLessThan:
              begin
                FFilterInterface.Where(PropName).LessThan(Value1);
                DisplayText := Format('%s < %s',
                  [Column.DisplayName, FormatDateTime('dd/mm/yyyy', dateValue1.Date)]);
              end;

            foGreaterThan:
              begin
                FFilterInterface.Where(PropName).GreaterThan(Value1);
                DisplayText := Format('%s > %s',
                  [Column.DisplayName, FormatDateTime('dd/mm/yyyy', dateValue1.Date)]);
              end;

            foIsNull:
              begin
                FFilterInterface.Where(PropName).IsNull;
                DisplayText := Format('%s Is Empty', [Column.DisplayName]);
              end;

            foIsNotNull:
              begin
                FFilterInterface.Where(PropName).IsNotNull;
                DisplayText := Format('%s Is Not Empty', [Column.DisplayName]);
              end;
          end;
        end
        else
        begin
          // Non-date handling
          case Op of
            foEqual:
              begin
                FFilterInterface.Where(PropName).EqualTo(Value1);
                DisplayText := Format('%s = %s', [Column.DisplayName, VarToStr(Value1)]);
              end;
            foNotEqual:
              begin
                FFilterInterface.Where(PropName).NotEqualTo(Value1);
                DisplayText := Format('%s <> %s', [Column.DisplayName, VarToStr(Value1)]);
              end;
            foGreaterThan:
              begin
                FFilterInterface.Where(PropName).GreaterThan(Value1);
                DisplayText := Format('%s > %s', [Column.DisplayName, VarToStr(Value1)]);
              end;
            foLessThan:
              begin
                FFilterInterface.Where(PropName).LessThan(Value1);
                DisplayText := Format('%s < %s', [Column.DisplayName, VarToStr(Value1)]);
              end;
            foGreaterEqual:
              begin
                FFilterInterface.Where(PropName).GreaterOrEqual(Value1);
                DisplayText := Format('%s >= %s', [Column.DisplayName, VarToStr(Value1)]);
              end;
            foLessEqual:
              begin
                FFilterInterface.Where(PropName).LessOrEqual(Value1);
                DisplayText := Format('%s <= %s', [Column.DisplayName, VarToStr(Value1)]);
              end;
            foBetween:
              begin
                FFilterInterface.Where(PropName).Between(Value1, Value2);
                DisplayText := Format('%s Between %s And %s',
                  [Column.DisplayName, VarToStr(Value1), VarToStr(Value2)]);
              end;
            foNotBetween:
              begin
                FFilterInterface.Where(PropName).NotBetween(Value1, Value2);
                DisplayText := Format('%s Not Between %s And %s',
                  [Column.DisplayName, VarToStr(Value1), VarToStr(Value2)]);
              end;
            foLike:
              begin
                FFilterInterface.Where(PropName).Like(Value1);
                DisplayText := Format('%s Like %s', [Column.DisplayName, VarToStr(Value1)]);
              end;
            foNotLike:
              begin
                FFilterInterface.Where(PropName).NotLike(Value1);
                DisplayText := Format('%s Not Like %s', [Column.DisplayName, VarToStr(Value1)]);
              end;
            foContains:
              begin
                FFilterInterface.Where(PropName).Contains(Value1);
                DisplayText := Format('%s Contains "%s"', [Column.DisplayName, VarToStr(Value1)]);
              end;
            foBeginsWith:
              begin
                FFilterInterface.Where(PropName).BeginsWith(Value1);
                DisplayText := Format('%s Starts With "%s"', [Column.DisplayName, VarToStr(Value1)]);
              end;
            foEndsWith:
              begin
                FFilterInterface.Where(PropName).EndsWith(Value1);
                DisplayText := Format('%s Ends With "%s"', [Column.DisplayName, VarToStr(Value1)]);
              end;
            foIsNull:
              begin
                FFilterInterface.Where(PropName).IsNull;
                DisplayText := Format('%s Is Empty', [Column.DisplayName]);
              end;
            foIsNotNull:
              begin
                FFilterInterface.Where(PropName).IsNotNull;
                DisplayText := Format('%s Is Not Empty', [Column.DisplayName]);
              end;
          end;
        end;

        FilterID := FFilterInterface.GetLastConditionID;
      end
      else
      begin
        // Custom SQL filtering remains the same
        BuildCustomFilterCondition(Column.FieldName, ActualDataType, Op,
          Value1, Value2, SQLCondition, DisplayText);

        if FCustomFilters.Count > 0 then
          SQLCondition := ' AND ' + SQLCondition;

        FCustomFilters.Add(SQLCondition);
        FCustomFilterDisplays.Add(DisplayText);
      end;

      CreateFilterPanel(Column.FieldName, DisplayText, FilterID);
      ClearFilterInputs;
      LoadData;
      Break;
    end;
  end;
end;

procedure TORMSearchForm.btnSearchClick(Sender: TObject);
begin
  showmessage(GetFilteredSQL);
  LoadData;
end;

procedure TORMSearchForm.btnClearFiltersClick(Sender: TObject);
begin
  ClearFilters;
  LoadData;
end;

procedure TORMSearchForm.btnCloseClick(Sender: TObject);
begin
  ModalResult := mrCancel;
end;

procedure TORMSearchForm.SearchGridDblClick(Sender: TObject);
begin
  if GetSelectedRow > 0 then
    ModalResult := mrOk;
end;

procedure TORMSearchForm.RebuildCustomFilters;
var
  TempFilters: TStringList;
  i: Integer;
begin
  if FCustomFilters.Count = 0 then Exit;

  TempFilters := TStringList.Create;
  try
    // Copy existing filters, removing any leading AND/OR
    for i := 0 to FCustomFilters.Count - 1 do
    begin
      var Filter := FCustomFilters[i];
      if Pos(' AND ', Filter) = 1 then
        Filter := TrimLeft(Copy(Filter, 6, Length(Filter)))
      else if Pos(' OR ', Filter) = 1 then
        Filter := TrimLeft(Copy(Filter, 5, Length(Filter)));

      TempFilters.Add(Filter);
    end;

    // Clear and rebuild with proper conjunctions
    FCustomFilters.Clear;
    for i := 0 to TempFilters.Count - 1 do
    begin
      if i > 0 then
        FCustomFilters.Add(' AND ' + TempFilters[i])
      else
        FCustomFilters.Add(TempFilters[i]);
    end;
  finally
    TempFilters.Free;
  end;
end;

procedure TORMSearchForm.FilterDeleteButtonClick(Sender: TObject);
var
  Button: TscGPCharGlyphButton;
  Panel: TscGPPanel;
  FilterPanel: TFilterPanel;
  i: Integer;
  NeedRebuild: Boolean;
begin
  if not (Sender is TscGPCharGlyphButton) then Exit;
  NeedRebuild := False;

  Button := TscGPCharGlyphButton(Sender);
  Panel := TscGPPanel(Button.Parent);

  FilterPanel := nil;
  for i := 0 to FFilterPanels.Count - 1 do
  begin
    if FFilterPanels[i].Panel = Panel then
    begin
      FilterPanel := FFilterPanels[i];
      Break;
    end;
  end;

  if not Assigned(FilterPanel) then Exit;

  try
    if Assigned(FFilterInterface) then
      FFilterInterface.RemoveFieldConditions(FilterPanel.FilterID)
    else if (i >= 0) and (i < FCustomFilters.Count) then
    begin
      FCustomFilters.Delete(i);
      FCustomFilterDisplays.Delete(i);
      NeedRebuild := True;
    end;

    FilterPanel.Panel.Parent := nil;
    FilterPanel.Panel.Free;

    i := FFilterPanels.IndexOf(FilterPanel);
    if i >= 0 then
      FFilterPanels.Delete(i);

    pnlActiveFilters.DisableAlign;
    pnlTop.DisableAlign;
    try
      var newHeight := PANEL_MARGIN * 2 +
                      (FFilterPanels.Count * FILTER_PANEL_HEIGHT) +
                      (Max(0, FFilterPanels.Count - 1) * FILTER_PANEL_SPACING);

      pnlActiveFilters.Height := newHeight;
      pnlTop.Height := pnlFilters.Height + newHeight;
    finally
      pnlTop.EnableAlign;
      pnlActiveFilters.EnableAlign;
    end;

    pnlActiveFilters.Visible := FFilterPanels.Count > 0;
    ArrangeFilterPanels;

    if NeedRebuild then
      RebuildCustomFilters;

    LoadData;
    UpdateControlStates;

  except
    on E: Exception do
      ArrangeFilterPanels;
  end;
end;

class function TORMSearchForm.Execute(AConnection: TUniConnection;
  const ABaseQuery: string; const ASearchColumns: array of TSearchColumn;
  var ASelectedSearch: TSelectedSearch; ARecordClass: TClass): Boolean;
var
  SearchForm: TORMSearchForm;
begin
  Result := False;
  SearchForm := TORMSearchForm.Create(nil);
  try
    SearchForm.InitializeSearch(AConnection, ABaseQuery, ASearchColumns,
      ARecordClass);
    if SearchForm.ShowModal = mrOk then
    begin
      SearchForm.UpdateSelectedSearch;
      ASelectedSearch := SearchForm.FSelectedSearch;
      Result := True;
    end;
  finally
    SearchForm.Free;
  end;
end;

procedure TORMSearchForm.InitializeSearch(AConnection: TUniConnection;
  const ABaseQuery: string; const ASearchColumns: array of TSearchColumn;
  ARecordClass: TClass);
var
  FormatType: TORMFormatType;
begin
  FConnection := AConnection;
  FBaseQuery := ABaseQuery;
  FRecordClass := ARecordClass;

  // Clear existing state
  FSearchColumns.Clear;
  FFilterPanels.Clear;
  FFilterInterface := nil;

  // Copy search columns
  for var Column in ASearchColumns do
  begin
    if Column.CustomFormatType.HasValue then
      FormatType := Column.CustomFormatType.Value
    else
      FormatType := tiNone;

    var NewColumn := TSearchColumn.Create(
      Column.FieldName,
      Column.DisplayName,
      Column.Width,
      Column.Visible,
      Column.UsableAsFilter,
      FormatType
    );
    FSearchColumns.Add(NewColumn);
  end;

  // Initialize ORM if record class is provided
  if Assigned(FRecordClass) then
  begin
    InitializePropertyMappings;
    FFilterInterface := TORMFilterFactory.GetInstance.CreateFilterInterfaceForClass(FRecordClass);
  end
  else raise EInvalidOpException.Create('Classe de Busca não informada');

  SetupColumns;
  SetupFieldTypes;
  LoadData;
end;

procedure TORMSearchForm.CreateEnumCombo;
begin
  if not Assigned(cboEnumValue) then
  begin
    cboEnumValue := TscComboBox.Create(Self);
    with cboEnumValue do
    begin
      Parent := pnlFilters;
      Left := edtValue1.Left;
      Top := edtValue1.Top;
      Width := edtValue1.Width;
      Height := edtValue1.Height;
      Style := csDropDownList;
      Visible := False;

      // Match style with other scComboBox components
      FluentUIOpaque := False;
      SelectionStyle := scstStyled;
      SelectionColor := clNone;
      SelectionTextColor := clHighlightText;
      ItemHeight := 16;
      DropDownCount := 25;
      StyleKind := scscbDefault;
      ShowFocusRect := True;
    end;
  end;
end;

procedure TORMSearchForm.SetupGrid;
begin
  with SearchGrid do
  begin
    Options := [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine,
                goRangeSelect, goRowSelect];
    FixedRows := 1;
    RowCount := 2;
    ColCount := 2;

    Font.Name := 'Segoe UI';
    Font.Size := 9;
    DefaultRowHeight := 25;
    FixedFont.Name := 'Segoe UI';
    FixedFont.Style := [fsBold];

    FixedColor := clBtnFace;
    Color := clWhite;
  end;
end;

procedure TORMSearchForm.UpdateValueControls;
var
  IsBetweenType: Boolean;
  Column: TSearchColumn;
  ActualDataType: TORMFormatType;
begin
  if cboField.ItemIndex < 0 then Exit;

  // Check if operator is either Between or NotBetween using Pos()
  IsBetweenType := Pos('Between', cboOperator.Text) > 0;

  for Column in FSearchColumns do
  begin
    if Column.DisplayName = cboField.Text then
    begin
      // Use custom format type if specified
      if Column.CustomFormatType.HasValue then
        ActualDataType := Column.CustomFormatType.Value
      else
        ActualDataType := Column.DataType;

      case ActualDataType of
        tiDate, tiDateTime:
          begin
            dateValue1.Visible := True;
            dateValue2.Visible := IsBetweenType;
            edtValue1.Visible := False;
            edtValue2.Visible := False;
          end;
        else
          begin
            dateValue1.Visible := False;
            dateValue2.Visible := False;
            edtValue1.Visible := True;
            edtValue2.Visible := IsBetweenType;
          end;
      end;
      Break;
    end;
  end;
end;

procedure TORMSearchForm.RemoveFilterPanel(const AFieldName: string);
var
  i: Integer;
  PanelToRemove: TFilterPanel;
begin
  PanelToRemove := nil;

  for i := 0 to FFilterPanels.Count - 1 do
  begin
    if SameText(FFilterPanels[i].FieldName, AFieldName) then
    begin
      PanelToRemove := FFilterPanels[i];
      Break;
    end;
  end;

  if Assigned(PanelToRemove) then
  begin
    try
      FFilterPanels.Extract(PanelToRemove);

      if Assigned(PanelToRemove.Panel) then
      begin
        if Assigned(PanelToRemove.Panel.Parent) then
        begin
          PanelToRemove.Panel.Parent := nil;
          PanelToRemove.Panel.Free;
        end;
      end;

      PanelToRemove.Free;

    except
      on E: Exception do
        ShowMessage('Error removing panel: ' + E.Message);
    end;
  end;

  pnlActiveFilters.Visible := FFilterPanels.Count > 0;
  ArrangeFilterPanels;
end;

procedure TORMSearchForm.ArrangeFilterPanels;
var
  YPos: Integer;
  RequiredHeight: Integer;
  i: Integer;
begin
  if not Assigned(FFilterPanels) then Exit;

  pnlActiveFilters.Visible := FFilterPanels.Count > 0;

  if FFilterPanels.Count = 0 then
  begin
    pnlTop.DisableAlign;
    try
      pnlActiveFilters.Height := 0;
      pnlTop.Height := pnlFilters.Height;
    finally
      pnlTop.EnableAlign;
    end;
    Exit;
  end;

  try
    RequiredHeight := PANEL_MARGIN * 2;
    for i := 0 to FFilterPanels.Count - 1 do
    begin
      Inc(RequiredHeight, FILTER_PANEL_HEIGHT);
      if i < FFilterPanels.Count - 1 then
        Inc(RequiredHeight, FILTER_PANEL_SPACING);
    end;


    pnlActiveFilters.DisableAlign;
    pnlTop.DisableAlign;
    try
      pnlActiveFilters.Height := RequiredHeight;

      YPos := PANEL_MARGIN;
      for i := 0 to FFilterPanels.Count - 1 do
      begin
        if not Assigned(FFilterPanels[i]) then Continue;

        with FFilterPanels[i] do
        begin
          if not Assigned(Panel) then Continue;

          if Panel.Parent <> pnlActiveFilters then
            Panel.Parent := pnlActiveFilters;

          Panel.DisableAlign;
          try
            Panel.BoundsRect := Rect(
              PANEL_MARGIN,
              YPos,
              pnlActiveFilters.ClientWidth - PANEL_MARGIN,
              YPos + FILTER_PANEL_HEIGHT
            );
            Panel.Visible := True;
          finally
            Panel.EnableAlign;
          end;

          Inc(YPos, FILTER_PANEL_HEIGHT + FILTER_PANEL_SPACING);
        end;
      end;

      pnlTop.Height := pnlFilters.Height + pnlActiveFilters.Height;

    finally
      pnlActiveFilters.EnableAlign;
      pnlTop.EnableAlign;
    end;


    pnlActiveFilters.Realign;
    pnlActiveFilters.Invalidate;
    pnlTop.Realign;
    pnlTop.Invalidate;
    Application.ProcessMessages;

  except
    on E: Exception do
    begin
      pnlActiveFilters.Visible := FFilterPanels.Count > 0;
      if pnlActiveFilters.Visible then
      begin
        if pnlActiveFilters.Height < FILTER_PANEL_HEIGHT then
          pnlActiveFilters.Height := FILTER_PANEL_HEIGHT;
        pnlTop.Height := pnlFilters.Height + pnlActiveFilters.Height;
      end else
        pnlTop.Height := pnlFilters.Height;
    end;
  end;
end;

procedure TORMSearchForm.ClearFilters;
begin
  // Clear ORM filters if available
  if Assigned(FFilterInterface) then
    FFilterInterface.Clear;

  // Clear custom filters
  FCustomFilters.Clear;
  FCustomFilterDisplays.Clear;

  // Clear filter panels
  while FFilterPanels.Count > 0 do
  begin
    if Assigned(FFilterPanels[0]) and Assigned(FFilterPanels[0].Panel) then
      FFilterPanels[0].Panel.Free;
    FFilterPanels.Delete(0);
  end;

  // Update UI
  pnlActiveFilters.Height := 0;
  pnlActiveFilters.Visible := False;
  pnlTop.Height := pnlFilters.Height;

  UpdateControlStates;
  LoadData; // Reload with no filters
end;

procedure TORMSearchForm.LoadData;
var
  WhereClause: string;
begin
  if not Assigned(FQuery) then
    FQuery := TUniQuery.Create(nil);

  try
    FQuery.Close;

    // Build the complete SQL query
    WhereClause := GetFilteredSQL;

    FQuery.SQL.Text := WhereClause;
    FQuery.Connection := FConnection;
    FQuery.Open;

    UpdateGridData;
    UpdateControlStates;
  except
    on E: Exception do
    begin
      ShowMessage('Error loading data: ' + E.Message);
      FQuery.Close;
    end;
  end;
end;

function TORMSearchForm.GetFilteredSQL: string;
var
  WhereClause: string;
  i: Integer;
begin
  Result := FBaseQuery;

  // Handle ORM-based filtering
  if Assigned(FFilterInterface) and (FFilterInterface.GetSQL <> '') then
    Result := Result + ' WHERE ' + FFilterInterface.GetSQL
  else if FCustomFilters.Count > 0 then
  begin
    WhereClause := ' WHERE ';

    // Add each filter condition
    for i := 0 to FCustomFilters.Count - 1 do
    begin
      if (i = 0) and (Pos(' AND ', FCustomFilters[i]) = 1) then
        WhereClause := WhereClause + Copy(FCustomFilters[i], 6, MaxInt)
      else
        WhereClause := WhereClause + FCustomFilters[i];
    end;

    // Check for empty WHERE clause
    if WhereClause <> ' WHERE ' then
      Result := Result + WhereClause;
  end;
end;

procedure TORMSearchForm.UpdateControlStates;
begin
  btnClearFilters.Enabled := FFilterPanels.Count > 0;

  // Update search button state based on filtering mode
  if Assigned(FFilterInterface) then
    btnSearch.Enabled := FFilterInterface.ConditionCount > 0
  else
    btnSearch.Enabled := FFilterPanels.Count > 0;
end;

function TORMSearchForm.ValidateFilterValues(DataType: TORMFormatType): Boolean;
var
  DummyInt: Int64;
  DummyFloat: Extended;
  Op: TFilterOperator;
  CurrentColumn: TSearchColumn;
  ActualDataType: TORMFormatType;
begin
  Result := True;
  if cboOperator.ItemIndex < 0 then
    Exit(False);

  // Get the current column and its actual data type
  for var Column in FSearchColumns do
  begin
    if Column.DisplayName = cboField.Text then
    begin
      CurrentColumn := Column;
      Break;
    end;
  end;

  // Determine actual data type to use
  if Assigned(CurrentColumn) and CurrentColumn.CustomFormatType.HasValue then
    ActualDataType := CurrentColumn.CustomFormatType.Value
  else
    ActualDataType := DataType;

  Op := TFilterOperator(NativeInt(cboOperator.Items.Objects[cboOperator.ItemIndex]));

  // Skip validation for operators that don't need values
  if Op in [foIsNull, foIsNotNull] then
    Exit(True);

  case ActualDataType of
    tiInteger:
      begin
        if edtValue1.Visible and (edtValue1.Text <> '') then
          Result := TryStrToInt64(edtValue1.Text, DummyInt);

        if Result and (Op in [foBetween, foNotBetween]) and edtValue2.Visible and (edtValue2.Text <> '') then
        begin
          Result := TryStrToInt64(edtValue2.Text, DummyInt);
          if Result then
          begin
            var Value1 := StrToInt64(edtValue1.Text);
            var Value2 := StrToInt64(edtValue2.Text);
            if Value1 > Value2 then
            begin
              ShowMessage('First value must be less than or equal to second value');
              Result := False;
            end;
          end;
        end;
      end;

    tiFloat, tiMoney:
      begin
        if edtValue1.Visible and (edtValue1.Text <> '') then
          Result := TryStrToFloat(StringReplace(edtValue1.Text, ',', '', [rfReplaceAll]), DummyFloat);

        if Result and (Op in [foBetween, foNotBetween]) and edtValue2.Visible and (edtValue2.Text <> '') then
        begin
          Result := TryStrToFloat(StringReplace(edtValue2.Text, ',', '', [rfReplaceAll]), DummyFloat);
          if Result then
          begin
            var Value1 := StrToFloat(StringReplace(edtValue1.Text, ',', '', [rfReplaceAll]));
            var Value2 := StrToFloat(StringReplace(edtValue2.Text, ',', '', [rfReplaceAll]));
            if Value1 > Value2 then
            begin
              ShowMessage('First value must be less than or equal to second value');
              Result := False;
            end;
          end;
        end;
      end;

    tiDate, tiDateTime:
      begin
        if dateValue1.Visible then
          Result := dateValue1.Date > 0;

if Result and dateValue2.Visible then
begin
  Result := dateValue2.Date > 0;
  if Result and (Op in [foBetween, foNotBetween]) then
  begin
    // When comparing dates for validation, compare without time component
    if DateOf(dateValue1.Date) > DateOf(dateValue2.Date) then
    begin
      ShowMessage('Start date must be less than or equal to end date');
      Result := False;
    end;
  end;
end;
      end;
  end;

  if not Result then
    ShowMessage('Please enter valid values for the selected data type');
end;

procedure TORMSearchForm.SetupFieldTypes;
begin
  TORMLogger.Log('Starting SetupFieldTypes');

  if not Assigned(FQuery) then
  begin
    FQuery := TUniQuery.Create(nil);
    TORMLogger.Log('Created new TUniQuery instance');
  end;

  try
    FQuery.Connection := FConnection;
    FQuery.SQL.Text := FBaseQuery + ' WHERE 1=0';
    TORMLogger.Log('Opening query: ' + FQuery.SQL.Text);
    FQuery.Open;

    FFieldTypes.Clear;
    for var Column in FSearchColumns do
    begin
      TORMLogger.Log(Format('Processing column %s - Current DataType: %s, Has CustomFormatType: %s',
        [Column.FieldName,
         GetEnumName(TypeInfo(TORMFormatType), Ord(Column.DataType)),
         BoolToStr(Column.CustomFormatType.HasValue, True)]));

      // Use custom format type if specified
      if Column.CustomFormatType.HasValue then
      begin
        Column.DataType := Column.CustomFormatType.Value;
        FFieldTypes.Add(Column.FieldName, Column.DataType);
        TORMLogger.Log(Format('Using custom format type for %s: %s',
          [Column.FieldName,
           GetEnumName(TypeInfo(TORMFormatType), Ord(Column.DataType))]));
        Continue;
      end;

      // If no custom format, infer from database field
      var Field := FQuery.FindField(Column.FieldName);
      if Assigned(Field) then
      begin
        var OldType := Column.DataType;
        case Field.DataType of
          ftString, ftWideString, ftMemo, ftWideMemo:
            Column.DataType := tiString;
          ftSmallint, ftInteger, ftWord, ftLongWord, ftShortint:
            Column.DataType := tiInteger;
          ftFloat, ftCurrency, ftBCD:
            Column.DataType := tiFloat;
          ftDate:
            Column.DataType := tiDate;
          ftDateTime, ftTimeStamp:
            Column.DataType := tiDateTime;
          ftBoolean:
            Column.DataType := tiBoolean;
          else
            Column.DataType := tiString;
        end;

        TORMLogger.Log(Format('Inferred type from DB field %s: %s -> %s (Field type: %s)',
          [Column.FieldName,
           GetEnumName(TypeInfo(TORMFormatType), Ord(OldType)),
           GetEnumName(TypeInfo(TORMFormatType), Ord(Column.DataType)),
           GetEnumName(TypeInfo(TFieldType), Ord(Field.DataType))]));

        FFieldTypes.Add(Column.FieldName, Column.DataType);
      end
      else
        TORMLogger.Log(Format('Warning: Field %s not found in query', [Column.FieldName]));
    end;

  finally
    FQuery.Close;
    TORMLogger.Log('Completed SetupFieldTypes');
  end;
end;

procedure TORMSearchForm.UpdateGridData;
var
  Row: Integer;
begin
  SearchGrid.RowCount := FQuery.RecordCount + 1;

  Row := 1;
  FQuery.First;
  while not FQuery.Eof do
  begin
    for var i := 0 to FSearchColumns.Count - 1 do
    begin
      var Field := FQuery.FieldByName(FSearchColumns[i].FieldName);
      if Assigned(Field) then
      begin
        if Field.IsNull then
          SearchGrid.Cells[i, Row] := ''
        else
          case FSearchColumns[i].DataType of
            tiDate:
              SearchGrid.Cells[i, Row] := DateToStr(Field.AsDateTime);
            tiDateTime:
              SearchGrid.Cells[i, Row] := DateTimeToStr(Field.AsDateTime);
            tiFloat, tiMoney:
              SearchGrid.Cells[i, Row] := FormatFloat('#,##0.00', Field.AsFloat);
            else
              SearchGrid.Cells[i, Row] := Field.AsString;
          end;
      end;
    end;
    Inc(Row);
    FQuery.Next;
  end;
end;

procedure TORMSearchForm.UpdateSelectedSearch;
var
  Row: Integer;
  RecNo: Integer;
  FieldValue: Variant;
  DisplayValue: string;
begin
  Row := GetSelectedRow;
  if Row < 1 then Exit;

  FSelectedSearch.Free;
  FSelectedSearch := TSelectedSearch.Create;

  try
    // Calculate record position
    RecNo := Row - SearchGrid.FixedRows;

    // Position the query
    FQuery.First;
    for var i := 0 to RecNo - 1 do
      FQuery.Next;

    // Process each column
    for var i := 0 to FSearchColumns.Count - 1 do
    begin
      var Field := FQuery.FieldByName(FSearchColumns[i].FieldName);
      if not Assigned(Field) then Continue;

      // Get the field value
      if Field.IsNull then
        FieldValue := Null
      else
        case Field.DataType of
          ftString, ftWideString, ftMemo, ftWideMemo:
            FieldValue := Field.AsString;
          ftSmallint, ftInteger, ftWord, ftLongWord:
            FieldValue := Field.AsInteger;
          ftFloat, ftCurrency, ftBCD:
            FieldValue := Field.AsFloat;
          ftDateTime, ftTimeStamp:
            FieldValue := Field.AsDateTime;
          ftDate:
            FieldValue := DateOf(Field.AsDateTime);
          ftBoolean:
            FieldValue := Field.AsBoolean;
          else
            FieldValue := Field.AsVariant;
        end;

      // Get display value from grid
      DisplayValue := SearchGrid.Cells[i, Row];

      // Store both the actual value and display value
      FSelectedSearch.SetValue(
        FSearchColumns[i].FieldName,
        FieldValue,
        DisplayValue
      );
    end;
  except
    on E: Exception do
    begin
      ShowMessage('Error updating selection: ' + E.Message);
      FreeAndNil(FSelectedSearch);
    end;
  end;
end;

function TORMSearchForm.GetSelectedRow: Integer;
begin
  Result := SearchGrid.Row;
  if Result >= SearchGrid.RowCount then
    Result := -1;
end;

function TORMSearchForm.GetFilterValue1: Variant;
begin
  if dateValue1.Visible then
    Result := dateValue1.Date
  else
    Result := edtValue1.Text;
end;

function TORMSearchForm.GetFilterValue2: Variant;
begin
  if dateValue2.Visible then
    Result := dateValue2.Date
  else
    Result := edtValue2.Text;
end;

function TORMSearchForm.GetFilterDisplayText: string;
var
  Column: TSearchColumn;
  DataType: TORMFormatType;
  Value1Str, Value2Str: string;
  Op: TFilterOperator;
  Context: TRttiContext;
  RttiType: TRttiType;
  Prop: TRttiProperty;
  Field: TRttiField;
  EnumAttr: EnumValuesAttribute;
  TransAttr: TranslationAttribute;
  BoolMapAttr: BooleanMapAttribute;
  DateValue: TDateTime;
  PropName: string;
  IsNullableBoolean: Boolean;
begin
  if (cboField.ItemIndex < 0) or (cboOperator.ItemIndex < 0) then
    Exit('');

  Op := TFilterOperator(NativeInt(cboOperator.Items.Objects[cboOperator.ItemIndex]));
  DataType := tiString;
  Value1Str := '';
  Value2Str := '';

  for Column in FSearchColumns do
  begin
    if Column.DisplayName = cboField.Text then
    begin
      Context := TRttiContext.Create;
      try
          RttiType := Context.GetType(FRecordClass);
          PropName := FindPropertyByColumnName(Column.FieldName, RttiType);

          // Handle Boolean with BooleanMap
          if IsBooleanProperty(Column.FieldName, IsNullableBoolean) then
          begin
            if cboEnumValue.Visible and (cboEnumValue.ItemIndex >= 0) then
            begin
              if IsNullableBoolean and (cboEnumValue.ItemIndex = 0) then
                Value1Str := '(Empty)'
              else
                Value1Str := cboEnumValue.Text;
            end;
            DataType := tiBoolean;
            Break;
          end;

          DataType := Column.DataType;
          if Column.CustomFormatType.HasValue then
            DataType := Column.CustomFormatType.Value;

          // Handle Enums & Translations
          EnumAttr := nil;
          TransAttr := nil;

          Prop := RttiType.GetProperty(PropName);
          if Assigned(Prop) then
          begin
            for var Attr in Prop.GetAttributes do
            begin
              if Attr is EnumValuesAttribute then
                EnumAttr := EnumValuesAttribute(Attr)
              else if Attr is TranslationAttribute then
                TransAttr := TranslationAttribute(Attr);
            end;
          end;

          if not (Assigned(EnumAttr) or Assigned(TransAttr)) then
          begin
            Field := RttiType.GetField('_' + PropName);
            if Assigned(Field) then
              for var Attr in Field.GetAttributes do
              begin
                if Attr is EnumValuesAttribute then
                  EnumAttr := EnumValuesAttribute(Attr)
                else if Attr is TranslationAttribute then
                  TransAttr := TranslationAttribute(Attr);
              end;
          end;

          // Handle different data types
          if cboEnumValue.Visible and (not (DataType = tiBoolean)) then
          begin
            Value1Str := cboEnumValue.Text;
          end
          else if DataType in [tiDate, tiDateTime] then
          begin
            if dateValue1.Visible then
            begin
              DateValue := dateValue1.Date;
              if DataType = tiDate then
                Value1Str := FormatDateTime('dd/mm/yyyy', DateValue)
              else
              begin
                if Frac(DateValue) = 0 then
                  Value1Str := FormatDateTime('dd/mm/yyyy', DateValue)
                else
                  Value1Str := FormatDateTime('dd/mm/yyyy hh:nn:ss', DateValue);
              end;
            end;

            if dateValue2.Visible then
            begin
              DateValue := dateValue2.Date;
              if DataType = tiDate then
                Value2Str := FormatDateTime('dd/mm/yyyy', DateValue)
              else
              begin
                if Frac(DateValue) = 0 then
                  Value2Str := FormatDateTime('dd/mm/yyyy', DateValue)
                else
                  Value2Str := FormatDateTime('dd/mm/yyyy hh:nn:ss', DateValue);
              end;
            end;
          end
          else
          begin
            if edtValue1.Visible then
              Value1Str := edtValue1.Text;
            if edtValue2.Visible then
              Value2Str := edtValue2.Text;
          end;
          Break;
      finally
        // Context is a record, no need to free
      end;
    end;
  end;

  // Build the display text
  case Op of
    foIsNull:
      Result := Format('%s Is Empty', [cboField.Text]);

    foIsNotNull:
      Result := Format('%s Is Not Empty', [cboField.Text]);

    foBetween, foNotBetween:
      begin
        if Op = foBetween then
          Result := Format('%s Between %s AND %s', [cboField.Text, Value1Str, Value2Str])
        else
          Result := Format('%s Not Between %s AND %s', [cboField.Text, Value1Str, Value2Str]);
      end;

    foIn, foNotIn:
      begin
        if Op = foIn then
          Result := Format('%s In (%s)', [cboField.Text, Value1Str])
        else
          Result := Format('%s Not In (%s)', [cboField.Text, Value1Str]);
      end
    else
      begin
        if Value1Str <> '' then
          Result := Format('%s %s %s', [cboField.Text, cboOperator.Text, Value1Str])
        else
          Result := Format('%s %s', [cboField.Text, cboOperator.Text]);
      end;
  end;

  TORMLogger.Log(Format('Filter display text generated: %s', [Result]));
end;

procedure TORMSearchForm.ClearFilterInputs;
begin
  edtValue1.Clear;
  edtValue2.Clear;
  dateValue1.Date := Date;
  dateValue2.Date := Date;
end;

procedure TORMSearchForm.CreateFilterPanel(const AFieldName,
  ADisplayText: string; const AFilterID: TGUID);
var
  FilterPanel: TFilterPanel;
begin
  FilterPanel := TFilterPanel.Create(pnlActiveFilters, AFieldName, ADisplayText, AFilterID);
  FilterPanel.Panel.Width := pnlActiveFilters.ClientWidth - (2 * PANEL_MARGIN);
  FilterPanel.FDeleteButton.OnClick := FilterDeleteButtonClick;
  FilterPanel.FDeleteButton.Tag := NativeInt(FilterPanel);

  FFilterPanels.Add(FilterPanel);

  if FFilterPanels.Count = 1 then
    pnlActiveFilters.Visible := True;

  ArrangeFilterPanels;
end;


procedure TORMSearchForm.PopulateOperators(ADataType: TORMFormatType);
begin
  FCurrentFieldType := ADataType;
  cboOperator.Items.Clear;


  cboOperator.Items.Add('Is Empty');
  cboOperator.Items.Add('Is Not Empty');

  case ADataType of
    tiString:
      begin
        cboOperator.Items.AddStrings(['Equals', 'Not Equals',
          'Contains', 'Starts With', 'Ends With']);
      end;

    tiInteger, tiFloat, tiMoney:
      begin
        cboOperator.Items.AddStrings(['Equals', 'Not Equals',
          'Greater Than', 'Less Than', 'Greater or Equal',
          'Less or Equal', 'Between', 'Not Between']);
      end;

    tiDate, tiDateTime:
      begin
        cboOperator.Items.AddStrings(['Equals', 'Not Equals',
          'Greater Than', 'Less Than', 'Greater or Equal',
          'Less or Equal', 'Between', 'Not Between']);
      end;

    tiBoolean:
      begin
        cboOperator.Items.AddStrings(['Is True', 'Is False']);
      end;
  end;

  if cboOperator.Items.Count > 0 then
    cboOperator.ItemIndex := 0;

  UpdateValueControls;
end;

procedure TORMSearchForm.SetupColumns;
begin

  SearchGrid.ColCount := FSearchColumns.Count;
  SearchGrid.FixedRows := 1;

  for var i := 0 to FSearchColumns.Count - 1 do
  begin
    SearchGrid.Cells[i, 0] := FSearchColumns[i].DisplayName;
    SearchGrid.ColWidths[i] := FSearchColumns[i].Width;
    if not FSearchColumns[i].Visible then
      SearchGrid.ColWidths[i] := 0;
  end;


  cboField.Items.Clear;
  for var Column in FSearchColumns do
    if Column.UsableAsFilter then
      cboField.Items.Add(Column.DisplayName);

  if cboField.Items.Count > 0 then
    cboField.ItemIndex := 0;
end;

procedure TORMSearchForm.SetupVisualStyles;
begin

  pnlActiveFilters.Padding.Left := PANEL_MARGIN;
  pnlActiveFilters.Padding.Right := PANEL_MARGIN;
  pnlActiveFilters.Padding.Top := PANEL_MARGIN;
  pnlActiveFilters.Padding.Bottom := PANEL_MARGIN;


  SearchGrid.Constraints.MinHeight := GRID_MIN_HEIGHT;


  edtValue1.Options.ShapeCornerRadius := 3;
  edtValue2.Options.ShapeCornerRadius := 3;
  dateValue1.Options.ShapeCornerRadius := 3;
  dateValue2.Options.ShapeCornerRadius := 3;


  btnAddFilter.Options.ShapeCornerRadius := 3;
  btnSearch.Options.ShapeCornerRadius := 3;
  btnClearFilters.Options.ShapeCornerRadius := 3;
  btnClose.Options.ShapeCornerRadius := 3;
end;



function TORMSearchForm.GetOperatorFromText(
  const OpText: string): TFilterOperator;
begin
  if OpText = 'Equals' then
    Result := foEqual
  else if OpText = 'Not Equals' then
    Result := foNotEqual
  else if OpText = 'Greater Than' then
    Result := foGreaterThan
  else if OpText = 'Less Than' then
    Result := foLessThan
  else if OpText = 'Greater or Equal' then
    Result := foGreaterEqual
  else if OpText = 'Less or Equal' then
    Result := foLessEqual
  else if OpText = 'Between' then
    Result := foBetween
  else if OpText = 'Not Between' then
    Result := foNotBetween
  else if OpText = 'Contains' then
    Result := foContains
  else if OpText = 'Starts With' then
    Result := foBeginsWith
  else if OpText = 'Ends With' then
    Result := foEndsWith
  else if OpText = 'Is Empty' then
    Result := foIsNull
  else if OpText = 'Is Not Empty' then
    Result := foIsNotNull
  else if OpText = 'Is True' then
    Result := foEqual
  else if OpText = 'Is False' then
    Result := foEqual
  else
    Result := foEqual;
end;

procedure TORMSearchForm.ConfigureOperatorCombo;
type
  TOperatorInfo = record
    Op: TFilterOperator;
    Display: string;
  end;
var
  Operators: TArray<TOperatorInfo>;
  DataType: TORMFormatType;
  IsNullableBoolean: Boolean;
  procedure AddOperator(Op: TFilterOperator; const Display: string);
  begin
    SetLength(Operators, Length(Operators) + 1);
    Operators[High(Operators)].Op := Op;
    Operators[High(Operators)].Display := Display;
  end;
begin
  SetLength(Operators, 0);
  if cboField.ItemIndex < 0 then Exit;
  for var Column in FSearchColumns do
  begin
    if Column.DisplayName = cboField.Text then
    begin
      if IsBooleanProperty(Column.PropertyName, IsNullableBoolean) then
      begin
        AddOperator(foEqual, 'Equals');
        AddOperator(foNotEqual, 'Not Equals');
        if IsNullableBoolean then
        begin
          AddOperator(foIsNull, 'Is Empty');
          AddOperator(foIsNotNull, 'Is Not Empty');
        end;
        Break;
      end;
      if Column.CustomFormatType.HasValue then
        DataType := Column.CustomFormatType.Value
      else
        DataType := Column.DataType;
      AddOperator(foIsNull, 'Is Empty');
      AddOperator(foIsNotNull, 'Is Not Empty');
      AddOperator(foEqual, 'Equals');
      AddOperator(foNotEqual, 'Not Equals');
      case DataType of
        tiString:
          begin
            AddOperator(foContains, 'Contains');
            AddOperator(foBeginsWith, 'Starts With');
            AddOperator(foEndsWith, 'Ends With');
            AddOperator(foLike, 'Like');
            AddOperator(foNotLike, 'Not Like');
            AddOperator(foIn, 'In List');
            AddOperator(foNotIn, 'Not In List');
          end;
        tiInteger, tiFloat, tiMoney:
          begin
            AddOperator(foGreaterThan, 'Greater Than');
            AddOperator(foLessThan, 'Less Than');
            AddOperator(foGreaterEqual, 'Greater or Equal');
            AddOperator(foLessEqual, 'Less or Equal');
            AddOperator(foBetween, 'Between');
            AddOperator(foNotBetween, 'Not Between');
            AddOperator(foIn, 'In List');
            AddOperator(foNotIn, 'Not In List');
          end;
        tiDate, tiDateTime:
          begin
            AddOperator(foGreaterThan, 'Greater Than');
            AddOperator(foLessThan, 'Less Than');
            AddOperator(foGreaterEqual, 'Greater or Equal');
            AddOperator(foLessEqual, 'Less or Equal');
            AddOperator(foBetween, 'Between');
            AddOperator(foNotBetween, 'Not Between');
          end;
      end;
      Break;
    end;
  end;
  cboOperator.Clear;
  cboOperator.Items.BeginUpdate;
  try
    for var Op in Operators do
      cboOperator.Items.AddObject(Op.Display, TObject(NativeInt(Op.Op)));
  finally
    cboOperator.Items.EndUpdate;
  end;
  if cboOperator.Items.Count > 0 then
    cboOperator.ItemIndex := 0;
  AdjustValueEditors;
end;

procedure TORMSearchForm.AdjustValueEditors;
var
  Op: TFilterOperator;
  Column: TSearchColumn;
  ShowValue1, ShowValue2: Boolean;
  DataType: TORMFormatType;
  IsDateType, IsBoolType: Boolean;
  IsNullableBoolean: Boolean;
  Context: TRttiContext;
  RttiType: TRttiType;
  Prop: TRttiProperty;
  Field: TRttiField;
  EnumAttr: EnumValuesAttribute;
  TransAttr: TranslationAttribute;
  FmtAttr: FmtPropAttribute;
  UseEnumCombo: Boolean;
begin
  if not Assigned(cboEnumValue) then
    CreateEnumCombo;
  ShowValue1 := True;
  ShowValue2 := False;
  IsDateType := False;
  UseEnumCombo := False;
  EnumAttr := nil;
  TransAttr := nil;
  FmtAttr := nil;
  if (cboOperator.ItemIndex < 0) or (cboField.ItemIndex < 0) then
  begin
    edtValue1.Visible := False;
    edtValue2.Visible := False;
    dateValue1.Visible := False;
    dateValue2.Visible := False;
    cboEnumValue.Visible := False;
    Exit;
  end;
  Op := TFilterOperator(NativeInt(cboOperator.Items.Objects[cboOperator.ItemIndex]));
  for Column in FSearchColumns do
  begin
    if Column.DisplayName = cboField.Text then
    begin
      IsBoolType := IsBooleanProperty(Column.PropertyName, IsNullableBoolean);
      if IsBoolType then
      begin
        DataType := tiBoolean;
        if Op in [foEqual, foNotEqual] then
        begin
          SetupBooleanComboBox(Column.PropertyName);
          ShowValue1 := True;
          ShowValue2 := False;
          UseEnumCombo := True;
        end;
      end;
      DataType := Column.DataType;
      Context := TRttiContext.Create;
      try
          RttiType := Context.GetType(FRecordClass);
          Prop := RttiType.GetProperty(Column.FieldName);
          if Assigned(Prop) then
          begin
            for var Attr in Prop.GetAttributes do
            begin
              if Attr is EnumValuesAttribute then
                EnumAttr := EnumValuesAttribute(Attr)
              else if Attr is TranslationAttribute then
                TransAttr := TranslationAttribute(Attr)
              else if Attr is FmtPropAttribute then
              begin
                FmtAttr := FmtPropAttribute(Attr);
                DataType := FmtAttr.FormatType;
              end;
            end;
          end;
          if not (Assigned(EnumAttr) or Assigned(TransAttr) or Assigned(FmtAttr)) then
          begin
            Field := RttiType.GetField('_' + Column.FieldName);
            if Assigned(Field) then
              for var Attr in Field.GetAttributes do
              begin
                if Attr is EnumValuesAttribute then
                  EnumAttr := EnumValuesAttribute(Attr)
                else if Attr is TranslationAttribute then
                  TransAttr := TranslationAttribute(Attr)
                else if Attr is FmtPropAttribute then
                begin
                  FmtAttr := FmtPropAttribute(Attr);
                  DataType := FmtAttr.FormatType;
                end;
              end;
          end;
          if Assigned(EnumAttr) and (Op in [foEqual, foNotEqual]) then
          begin
            UseEnumCombo := True;
            cboEnumValue.Items.Clear;
            if Assigned(TransAttr) then
            begin
              for var Value in EnumAttr.GetValues do
                cboEnumValue.Items.Add(TransAttr.GetDisplayValue(Value));
            end
            else
            begin
              for var Value in EnumAttr.GetValues do
                cboEnumValue.Items.Add(Value);
            end;
            if cboEnumValue.Items.Count > 0 then
              cboEnumValue.ItemIndex := 0;
          end;
      finally
      end;
      Break;
    end;
  end;
  IsDateType := (DataType in [tiDate, tiDateTime]);
  case Op of
    foIsNull, foIsNotNull:
      begin
        ShowValue1 := False;
        ShowValue2 := False;
      end;
    foBetween, foNotBetween:
      begin
        ShowValue1 := True;
        ShowValue2 := True;
        UseEnumCombo := False;
      end;
    else
      begin
        ShowValue1 := True;
        ShowValue2 := False;
      end;
  end;
  if IsDateType then
  begin
    dateValue1.Visible := ShowValue1;
    dateValue2.Visible := ShowValue2;
    edtValue1.Visible := False;
    edtValue2.Visible := False;
    cboEnumValue.Visible := False;
  end
  else if UseEnumCombo then
  begin
    dateValue1.Visible := False;
    dateValue2.Visible := False;
    edtValue1.Visible := False;
    edtValue2.Visible := False;
    cboEnumValue.Visible := ShowValue1;
  end
  else
  begin
    dateValue1.Visible := False;
    dateValue2.Visible := False;
    edtValue1.Visible := ShowValue1;
    edtValue2.Visible := ShowValue2;
    cboEnumValue.Visible := False;
  end;
end;

procedure TORMSearchForm.InitializePropertyMappings;
var
  Context: TRttiContext;
  RttiType: TRttiType;
  Field: TRttiField;
  Attr: TCustomAttribute;
  FmtAttr: FmtPropAttribute;
  IsNullableBoolean: Boolean;
begin
  Context := TRttiContext.Create;
  try
    RttiType := Context.GetType(FRecordClass);
    for var Column in FSearchColumns do
    begin
      Column.PropertyName := FindPropertyByColumnName(Column.FieldName, RttiType);
      if Column.PropertyName = '' then
      begin
        TORMLogger.Log(Format('Warning: No property mapping found for column %s', [Column.FieldName]));
        Continue;
      end;
      TORMLogger.Log(Format('Mapped column %s to property %s', [Column.FieldName, Column.PropertyName]));
      if IsBooleanProperty(Column.PropertyName, IsNullableBoolean) then
      begin
        Column.DataType := tiBoolean;
        if not Column.CustomFormatType.HasValue then
          Column.SetFormatType(tiBoolean);
        Continue;
      end;
      if not Column.CustomFormatType.HasValue then
      begin
        Field := RttiType.GetField('_' + Column.PropertyName);
        if Assigned(Field) then
        begin
          for Attr in Field.GetAttributes do
          begin
            if Attr is FmtPropAttribute then
            begin
              FmtAttr := FmtPropAttribute(Attr);
              Column.SetFormatType(FmtAttr.FormatType);
              TORMLogger.Log(Format('Found format type %s for property %s from field attributes',
                [GetEnumName(TypeInfo(TORMFormatType), Ord(FmtAttr.FormatType)), Column.PropertyName]));
              Break;
            end;
          end;
        end;
      end;
    end;
  finally
  end;
end;

function TORMSearchForm.AdjustDateTimeValue(const ADate: TDateTime;
  AOperator: TFilterOperator; IsEndValue: Boolean = False): TDateTime;
begin
  Result := ADate;

  // Handle cases where we need to set end-of-day time
  case AOperator of
    foEqual, foNotEqual:
      begin
        if IsEndValue then
          Result := RecodeTime(Result, 23, 59, 59, 999)
        else
          Result := RecodeTime(Result, 0, 0, 0, 0);
      end;

    foBetween, foNotBetween:
      begin
        if IsEndValue then
          Result := RecodeTime(Result, 23, 59, 59, 999)
        else
          Result := RecodeTime(Result, 0, 0, 0, 0);
      end;

    foLessEqual:
      Result := RecodeTime(Result, 23, 59, 59, 999);

    foGreaterEqual:
      Result := RecodeTime(Result, 0, 0, 0, 0);

    else
      // For other operators, keep the time as is
      if IsEndValue then
        Result := RecodeTime(Result, 23, 59, 59, 999)
      else
        Result := RecodeTime(Result, 0, 0, 0, 0);
  end;
end;

function TORMSearchForm.GetBooleanMapAttribute(const FieldName: string): BooleanMapAttribute;
var
  Context: TRttiContext;
  RttiType: TRttiType;
  Prop: TRttiProperty;
  Field: TRttiField;
  PropName: string;
begin
  Result := nil;

  Context := TRttiContext.Create;
  try
    RttiType := Context.GetType(FRecordClass);

    // Get the correct property name from the field name
    PropName := FindPropertyByColumnName(FieldName, RttiType);
    if PropName = '' then Exit;

    TORMLogger.Log(Format('GetBooleanMapAttribute - Field: %s, Mapped Property: %s',
      [FieldName, PropName]));

    Prop := RttiType.GetProperty(PropName);

    // Check property attributes first
    if Assigned(Prop) then
      for var Attr in Prop.GetAttributes do
        if Attr is BooleanMapAttribute then
        begin
          Result := BooleanMapAttribute(Attr);
          TORMLogger.Log(Format('Found BooleanMap attribute on property with values: %s, %s',
            [Result.FalseValue, Result.TrueValue]));
          Exit;
        end;

    // Check field attributes if not found in property
    Field := RttiType.GetField('_' + PropName);
    if Assigned(Field) then
      for var Attr in Field.GetAttributes do
        if Attr is BooleanMapAttribute then
        begin
          Result := BooleanMapAttribute(Attr);
          TORMLogger.Log(Format('Found BooleanMap attribute on field with values: %s, %s',
            [Result.FalseValue, Result.TrueValue]));
          Exit;
        end;
  finally
    // Context is a record, no need to free
  end;
end;

function TORMSearchForm.IsBooleanProperty(const FieldName: string;
  out IsNullable: Boolean): Boolean;
var
  Context: TRttiContext;
  RttiType: TRttiType;
  Prop: TRttiProperty;
  Field: TRttiField;
  PropName: string;
begin
  Result := False;
  IsNullable := False;

  Context := TRttiContext.Create;
  try
    RttiType := Context.GetType(FRecordClass);

    // Get the correct property name from the field name
    PropName := FindPropertyByColumnName(FieldName, RttiType);
    if PropName = '' then Exit;

    TORMLogger.Log(Format('IsBooleanProperty - Field: %s, Mapped Property: %s', [FieldName, PropName]));

    Prop := RttiType.GetProperty(PropName);
    if not Assigned(Prop) then Exit;

    // First check if it's a TNullable<Boolean>
    if Prop.PropertyType.TypeKind = tkRecord then
    begin
      if Prop.PropertyType.Name.StartsWith('TNullable<') then
      begin
        var ValueField := Prop.PropertyType.GetField('Value');
        if Assigned(ValueField) and (ValueField.FieldType.Handle = TypeInfo(Boolean)) then
        begin
          Result := True;
          IsNullable := True;
          TORMLogger.Log('Found nullable boolean property');
        end;
      end;
    end
    // Then check if it's a direct Boolean
    else if Prop.PropertyType.Handle = TypeInfo(Boolean) then
    begin
      Result := True;
      IsNullable := False;
      TORMLogger.Log('Found regular boolean property');
    end;

    // Check for BooleanMap attribute
    if Result then
    begin
      // Check property attributes
      for var Attr in Prop.GetAttributes do
        if Attr is BooleanMapAttribute then
        begin
          TORMLogger.Log('Found BooleanMap attribute on property');
          Exit;
        end;

      // Check field attributes
      Field := RttiType.GetField('_' + PropName);
      if Assigned(Field) then
        for var Attr in Field.GetAttributes do
          if Attr is BooleanMapAttribute then
          begin
            TORMLogger.Log('Found BooleanMap attribute on field');
            Exit;
          end;
    end;
  finally
    // Context is a record, no need to free
  end;
end;

procedure TORMSearchForm.BuildCustomFilterCondition(const FieldName: string;
  DataType: TORMFormatType; Op: TFilterOperator; const Value1, Value2: Variant;
  var SQLCondition: string; var DisplayText: string);
var
  OpStr: string;
  FormattedValue1, FormattedValue2: string;
  DateValue1, DateValue2: TDateTime;
begin
  OpStr := GetSQLOperator(Op);

  case Op of
    foIsNull:
      begin
        SQLCondition := Format('%s IS NULL', [FieldName]);
        DisplayText := Format('%s Is Empty', [cboField.Text]);
      end;

    foIsNotNull:
      begin
        SQLCondition := Format('%s IS NOT NULL', [FieldName]);
        DisplayText := Format('%s Is Not Empty', [cboField.Text]);
      end;

    foEqual:
      begin
        if DataType = tiDateTime then
        begin
          DateValue1 := AdjustDateTimeValue(Value1, foEqual, False);
          DateValue2 := AdjustDateTimeValue(Value1, foEqual, True);

          FormattedValue1 := FormatSQLValue(DateValue1, DataType);
          FormattedValue2 := FormatSQLValue(DateValue2, DataType);

          SQLCondition := Format('%s BETWEEN %s AND %s',
            [FieldName, FormattedValue1, FormattedValue2]);

          DisplayText := Format('%s = %s',
            [cboField.Text, FormatDateTime('dd/mm/yyyy', Value1)]);
        end
        else
        begin
          FormattedValue1 := FormatSQLValue(Value1, DataType);
          SQLCondition := Format('%s = %s', [FieldName, FormattedValue1]);
          DisplayText := Format('%s = %s', [cboField.Text, Value1]);
        end;
      end;

    foNotEqual:
      begin
        if DataType = tiDateTime then
        begin
          DateValue1 := AdjustDateTimeValue(Value1, foNotEqual, False);
          DateValue2 := AdjustDateTimeValue(Value1, foNotEqual, True);

          FormattedValue1 := FormatSQLValue(DateValue1, DataType);
          FormattedValue2 := FormatSQLValue(DateValue2, DataType);

          SQLCondition := Format('(%s NOT BETWEEN %s AND %s)',
            [FieldName, FormattedValue1, FormattedValue2]);

          DisplayText := Format('%s <> %s',
            [cboField.Text, FormatDateTime('dd/mm/yyyy', Value1)]);
        end
        else
        begin
          FormattedValue1 := FormatSQLValue(Value1, DataType);
          SQLCondition := Format('%s <> %s', [FieldName, FormattedValue1]);
          DisplayText := Format('%s <> %s', [cboField.Text, Value1]);
        end;
      end;

    foBetween, foNotBetween:
      begin
        if DataType = tiDateTime then
        begin
          DateValue1 := AdjustDateTimeValue(Value1, Op, False);
          DateValue2 := AdjustDateTimeValue(Value2, Op, True);

          FormattedValue1 := FormatSQLValue(DateValue1, DataType);
          FormattedValue2 := FormatSQLValue(DateValue2, DataType);
        end
        else
        begin
          FormattedValue1 := FormatSQLValue(Value1, DataType);
          FormattedValue2 := FormatSQLValue(Value2, DataType);
        end;

        if Op = foBetween then
        begin
          SQLCondition := Format('%s BETWEEN %s AND %s',
            [FieldName, FormattedValue1, FormattedValue2]);
        end
        else
        begin
          SQLCondition := Format('%s NOT BETWEEN %s AND %s',
            [FieldName, FormattedValue1, FormattedValue2]);
        end;

        DisplayText := Format('%s %s %s AND %s',
          [cboField.Text, OpStr, VarToStr(Value1), VarToStr(Value2)]);
      end;

    foLessEqual:
      begin
        if DataType = tiDateTime then
        begin
          DateValue1 := AdjustDateTimeValue(Value1, foLessEqual);
          FormattedValue1 := FormatSQLValue(DateValue1, DataType);
        end
        else
          FormattedValue1 := FormatSQLValue(Value1, DataType);

        SQLCondition := Format('%s <= %s', [FieldName, FormattedValue1]);
        DisplayText := Format('%s <= %s', [cboField.Text, VarToStr(Value1)]);
      end;

    else
      begin
        if DataType = tiDateTime then
        begin
          DateValue1 := AdjustDateTimeValue(Value1, Op);
          FormattedValue1 := FormatSQLValue(DateValue1, DataType);
        end
        else
          FormattedValue1 := FormatSQLValue(Value1, DataType);

        SQLCondition := Format('%s %s %s', [FieldName, OpStr, FormattedValue1]);
        DisplayText := Format('%s %s %s', [cboField.Text, OpStr, VarToStr(Value1)]);
      end;
  end;
end;

function TORMSearchForm.GetSQLOperator(Op: TFilterOperator): string;
begin
  case Op of
    foEqual: Result := '=';
    foNotEqual: Result := '<>';
    foGreaterThan: Result := '>';
    foLessThan: Result := '<';
    foGreaterEqual: Result := '>=';
    foLessEqual: Result := '<=';
    foLike: Result := 'LIKE';
    foNotLike: Result := 'NOT LIKE';
    else Result := '=';
  end;
end;

function TORMSearchForm.FormatSQLValue(const Value: Variant; DataType: TORMFormatType): string;
begin
  if VarIsNull(Value) or VarIsEmpty(Value) then
    Exit('NULL');

  case DataType of
    tiString:
      Result := QuoteEsc(VarToStr(Value));

    tiInteger:
      Result := VarToStr(Value);

    tiFloat, tiMoney:
      Result := StringReplace(FormatFloat('0.########', Value), ',', '.', [rfReplaceAll]);

    tiDate:
      Result := QuoteDate_SQL(VarToStr(Value));

    tiDateTime:
      begin
        if VarType(Value) = varDate then
          Result := QuotedStr(FormatDateTime('yyyy-mm-dd hh:nn:ss', Value))
        else
          Result := QuotedStr(FormatDateTime('yyyy-mm-dd hh:nn:ss', VarToDateTime(Value)));
      end;

    tiBoolean:
      begin
        if Value then
          Result := 'TRUE'
        else
          Result := 'FALSE';
      end;

    else
      Result := QuoteEsc(VarToStr(Value));
  end;
end;

procedure TORMSearchForm.SetupBooleanComboBox(const FieldName: string);
var
  IsNullable: Boolean;
  BoolMapAttr: BooleanMapAttribute;
  Context: TRttiContext;
  RttiType: TRttiType;
  PropName: string;
begin
  Context := TRttiContext.Create;
  try
    RttiType := Context.GetType(FRecordClass);
    PropName := FindPropertyByColumnName(FieldName, RttiType);

    TORMLogger.Log(Format('SetupBooleanComboBox - Field: %s, Mapped Property: %s',
      [FieldName, PropName]));

    if PropName = '' then Exit;

    if not IsBooleanProperty(FieldName, IsNullable) then Exit;

    cboEnumValue.Items.Clear;
    BoolMapAttr := GetBooleanMapAttribute(PropName);  // Use the property name here

    if IsNullable then
    begin
      cboEnumValue.Items.Add('(Empty)');
      TORMLogger.Log('Added (Empty) option for nullable boolean');
    end;

    if Assigned(BoolMapAttr) then
    begin
      // Use mapped values from attribute
      cboEnumValue.Items.Add(BoolMapAttr.FalseValue);
      cboEnumValue.Items.Add(BoolMapAttr.TrueValue);
      TORMLogger.Log(Format('Added mapped values: %s, %s',
        [BoolMapAttr.FalseValue, BoolMapAttr.TrueValue]));
    end
    else
    begin
      // Use default values
      cboEnumValue.Items.Add('Não');
      cboEnumValue.Items.Add('Sim');
      TORMLogger.Log('Added default boolean values');
    end;

    if cboEnumValue.Items.Count > 0 then
      cboEnumValue.ItemIndex := 0;
  finally
    // Context is a record, no need to free
  end;
end;

procedure TSearchColumn.SetFormatType(const Value: TORMFormatType);
begin
  TORMLogger.Log(Format('SetFormatType called for field %s - Old DataType: %s, New Value: %s',
    [FFieldName, GetEnumName(TypeInfo(TORMFormatType), Ord(FDataType)),
     GetEnumName(TypeInfo(TORMFormatType), Ord(Value))]));

  if Value = tiNone then
  begin
    FCustomFormatType := NullValue;
    TORMLogger.Log('  Setting CustomFormatType to NullValue');
  end
  else
  begin
    FCustomFormatType := Value;
    FDataType := Value;
    TORMLogger.Log(Format('  Updated both CustomFormatType and DataType to: %s',
      [GetEnumName(TypeInfo(TORMFormatType), Ord(Value))]));
  end;
end;

{ TORMSearchBuilder }

constructor TORMSearchBuilder.Create;
begin
  inherited;
  FColumns := TObjectList<TSearchColumn>.Create(True);
end;

destructor TORMSearchBuilder.Destroy;
begin
  FColumns.Free;
  FSelectedInfo.Free;
  inherited;
end;

function TORMSearchBuilder.ForClass<T>(AClass: TClass = nil): TORMSearchBuilder;
begin
  if AClass = nil then
    FRecordClass := TClass(T)
  else
    FRecordClass := AClass;

  // Clear any existing columns
  FColumns.Clear;

  // Extract columns from class
  ExtractColumnsFromClass;

  // Log the number of columns found
  TORMLogger.Log(Format('Extracted %d columns from class %s',
    [FColumns.Count, FRecordClass.ClassName]));

  Result := Self;
end;

procedure TORMSearchBuilder.ExtractColumnsFromClass;
var
  Context: TRttiContext;
  RttiType: TRttiType;
  Prop: TRttiProperty;
  Field: TRttiField;
  FieldName, DisplayName: string;
  Width: Integer;
  FormatType: TORMFormatType;
  IsVisible, IsUsableFilter: Boolean;
  SearchAttr: SearchColumnAttribute;

  // Helper function to find SearchColumn attribute
  function FindSearchColumnAttribute(AProp: TRttiProperty; AField: TRttiField): SearchColumnAttribute;
  var
    Attr: TCustomAttribute;
  begin
    Result := nil;

    // First check property attributes
    if Assigned(AProp) then
    begin
      TORMLogger.Log(Format('Checking property %s for SearchColumn attribute', [AProp.Name]));
      for Attr in AProp.GetAttributes do
      begin
        if Attr is SearchColumnAttribute then
        begin
          Result := SearchColumnAttribute(Attr);
          TORMLogger.Log(Format('Found SearchColumn on property: DisplayName="%s", Width=%d',
            [Result.DisplayName, Result.Width]));
          Exit;
        end;
      end;
    end;

    // Then check field attributes
    if Assigned(AField) then
    begin
      TORMLogger.Log(Format('Checking field %s for SearchColumn attribute', [AField.Name]));
      for Attr in AField.GetAttributes do
      begin
        if Attr is SearchColumnAttribute then
        begin
          Result := SearchColumnAttribute(Attr);
          TORMLogger.Log(Format('Found SearchColumn on field: DisplayName="%s", Width=%d',
            [Result.DisplayName, Result.Width]));
          Exit;
        end;
      end;
    end;
  end;

begin
  Context := TRttiContext.Create;
  try
    RttiType := Context.GetType(FRecordClass);

    TORMLogger.Log(Format('Starting column extraction for class: %s', [FRecordClass.ClassName]));

    // Process published properties
    for Prop in RttiType.GetProperties do
    begin
      if Prop.Visibility <> mvPublished then
        Continue;

      TORMLogger.Log(Format('Processing property: %s', [Prop.Name]));

      // Get backing field
      Field := RttiType.GetField('_' + Prop.Name);
      if not Assigned(Field) then
      begin
        TORMLogger.Log(Format('No backing field found for property: %s', [Prop.Name]));
        Continue;
      end;

      // Initialize defaults
      DisplayName := Prop.Name;
      Width := 100;
      IsVisible := True;
      IsUsableFilter := True;
      FormatType := tiNone;
      FieldName := '';

      // Get column name from RefColumn attribute
      for var Attr in Field.GetAttributes do
      begin
        if Attr is RefColumnAttribute then
        begin
          FieldName := RefColumnAttribute(Attr).ColumnName;
          Break;
        end;
      end;

      if FieldName = '' then
      begin
        TORMLogger.Log(Format('No RefColumn attribute found for field: %s', [Field.Name]));
        Continue;
      end;

      // Find SearchColumn attribute
      SearchAttr := FindSearchColumnAttribute(Prop, Field);
      if Assigned(SearchAttr) then
      begin
        DisplayName := SearchAttr.DisplayName;
        Width := SearchAttr.Width;
        IsVisible := SearchAttr.Visible;
        IsUsableFilter := SearchAttr.UsableAsFilter;

        TORMLogger.Log(Format('Applied SearchColumn settings: Display="%s", Width=%d',
          [DisplayName, Width]));
      end;

      // Get format type
      for var Attr in Field.GetAttributes do
      begin
        if Attr is FmtPropAttribute then
        begin
          FormatType := FmtPropAttribute(Attr).FormatType;
          Break;
        end;
      end;

      if FormatType = tiNone then
        FormatType := GetFormatTypeFromProperty(Prop);

      TORMLogger.Log(Format('Creating column: Field=%s, Display="%s", Type=%s',
        [FieldName, DisplayName, GetEnumName(TypeInfo(TORMFormatType), Ord(FormatType))]));

      // Add column
      AddColumn(
        FieldName,
        DisplayName,
        Width,
        IsVisible,
        IsUsableFilter,
        FormatType
      );
    end;

    TORMLogger.Log(Format('Total columns extracted: %d', [FColumns.Count]));
  finally
    // Context is a record, no need to free
  end;
end;

function TORMSearchBuilder.Execute: Boolean;
begin
  Result := False;

  // Validate required parameters
  if not Assigned(FConnection) then
  begin
    ShowMessage('Database connection not assigned');
    Exit;
  end;

  if FBaseQuery = '' then
  begin
    // Use base query from class if available
    FBaseQuery := TQryRecInfoClass(FRecordClass).GetBaseQuery;
  end;

  // Check if we have any columns defined
  if FColumns.Count = 0 then
  begin
      // Extract columns from class if no columns were manually added
      ExtractColumnsFromClass;
  end;

  try
    FSelectedInfo.Free;
    FSelectedInfo := nil;

    TORMLogger.Log('Executing search with:');
    TORMLogger.Log('  Query: ' + FBaseQuery);
    TORMLogger.Log(Format('  Columns: %d', [FColumns.Count]));
    for var i := 0 to FColumns.Count - 1 do
    begin
      TORMLogger.Log(Format('    %d: %s ("%s") Width=%d',
        [i, FColumns[i].FieldName, FColumns[i].DisplayName, FColumns[i].Width]));
    end;

    Result := TORMSearchForm.Execute(
      FConnection,
      FBaseQuery,
      FColumns.ToArray,
      FSelectedInfo,
      FRecordClass  // Can be nil for custom searches
    );

  except
    on E: Exception do
    begin
      ShowMessage('Error executing search: ' + E.Message);
      Result := False;
    end;
  end;
end;

function TORMSearchBuilder.GetFormatTypeFromProperty(Prop: TRttiProperty): TORMFormatType;
var
  Attr: TCustomAttribute;
  PropType: TRttiType;
begin
  Result := tiNone;

  // First check for explicit format type from FmtProp attribute
  for Attr in Prop.GetAttributes do
  begin
    if Attr is FmtPropAttribute then
    begin
      Result := FmtPropAttribute(Attr).FormatType;
      Exit;
    end;
  end;

  // If no explicit format, infer from property type
  PropType := Prop.PropertyType;

  // Handle TNullable<T>
  if (PropType.TypeKind = tkRecord) and PropType.Name.StartsWith('TNullable<') then
  begin
    var ValueField := PropType.GetField('Value');
    if Assigned(ValueField) then
      PropType := ValueField.FieldType;
  end;

  case PropType.TypeKind of
    tkInteger, tkInt64:
      Result := tiInteger;

    tkFloat:
      begin
        if PropType.Handle = TypeInfo(Currency) then
          Result := tiMoney
        else if PropType.Handle = TypeInfo(TDateTime) then
          Result := tiDateTime
        else
          Result := tiFloat;
      end;

    tkEnumeration:
      if PropType.Handle = TypeInfo(Boolean) then
        Result := tiBoolean
      else
        Result := tiString;

    tkString, tkLString, tkWString, tkUString:
      Result := tiString;

    tkVariant:
      Result := tiString;
  end;
end;

function TORMSearchBuilder.WithConnection(AConnection: TUniConnection): TORMSearchBuilder;
begin
  FConnection := AConnection;
  Result := Self;
end;

function TORMSearchBuilder.WithQuery(const AQuery: string): TORMSearchBuilder;
begin
  FBaseQuery := AQuery;
  Result := Self;
end;

function TORMSearchBuilder.AddColumn(const AFieldName, ADisplayName: string;
  AWidth: Integer; AVisible: Boolean = True; AUsableAsFilter: Boolean = True;
  AFormatType: TORMFormatType = tiNone): TORMSearchBuilder;
var
  Column: TSearchColumn;
begin
  TORMLogger.Log(Format('Adding custom column: Field=%s, Display=%s, Width=%d, Type=%s',
    [AFieldName, ADisplayName, AWidth, GetEnumName(TypeInfo(TORMFormatType), Ord(AFormatType))]));

  Column := TSearchColumn.Create(AFieldName, ADisplayName, AWidth, AVisible,
    AUsableAsFilter, AFormatType);
  FColumns.Add(Column);
  Result := Self;
end;

end.

-----------

Fix the code so, when the column datatype is tiBoolean, then apply the appropriate filter instead of the direct contents of the cboEnumValue.
code snippet:
      cboEnumValue.Items.Add('Não');
      cboEnumValue.Items.Add('Sim');