unit frm_ClientesGruposEditor;
interface
uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.Buttons,
  Vcl.ComCtrls, Data.DB, FireDAC.Comp.Client, uAppTheme, FireDAC.Stan.Param, uBaseForm,
  Vcl.Grids, Vcl.DBGrids, FireDAC.Comp.DataSet, FireDAC.Stan.Option, FireDAC.Stan.Error, FireDAC.Stan.Intf,
  FireDAC.DatS, FireDAC.Phys.Intf, FireDAC.DApt.Intf, FireDAC.Stan.Async,
  FireDAC.DApt;
type
  TfrmClientesGruposEditor = class(TfrmBase)
    lblCodigo: TLabel;
    edtCodigo: TEdit;
    lblNombre: TLabel;
    edtNombre: TEdit;
    chkActivo: TCheckBox;
    qryPreciosGroup: TFDQuery;
    dsPreciosGroup: TDataSource;

    // Pestaña Clientes del Grupo
    tsClientesGroup: TTabSheet;
    dbgClientesGroup: TDBGrid;
    qryClientesGroup: TFDQuery;
    dsClientesGroup: TDataSource;

    procedure FormShow(Sender: TObject);
    procedure btnSaveClick(Sender: TObject);
    procedure btnSalirClick(Sender: TObject);
    procedure btnEliminarClick(Sender: TObject);
    procedure btnAnadirPrecioClick(Sender: TObject);
    procedure btnEliminarPrecioClick(Sender: TObject);
    procedure qryPreciosGroupCalcFields(DataSet: TDataSet);
    procedure DateFieldSetText(Sender: TField; const Text: string);
    procedure PrecioBaseChange(Sender: TField);
    procedure PrecioConIvaChange(Sender: TField);
    procedure DescuentoPorcentajeChange(Sender: TField);
    procedure dbgPreciosGroupKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure qryPreciosGroupAfterPost(DataSet: TDataSet);
    procedure qryPreciosGroupUpdateRecord(ASender: TDataSet; ARequest: TFDUpdateRequest;
      var AAction: TFDErrorAction; AOptions: TFDUpdateRowOptions);
    procedure dbgClientesGroupDblClick(Sender: TObject);
  protected
    procedure DoShow; override;
    procedure DoAnadir; override;
    procedure DoModificar; override;
    procedure DoPrimero; override;
    procedure DoAnterior; override;
    procedure DoSiguiente; override;
    procedure DoUltimo; override;
    procedure DoDuplicar; override;
  private
    FGrupoId: string;
    FParentForm: TForm;
    FUpdatingCalcField: Boolean;

    procedure ClearEditsModified;
    procedure LoadPreciosGroup;
    procedure LoadClientesGroup;
  published
    procedure LoadData;
  public
    property ClienteGrupoId: string read FGrupoId write FGrupoId;
    property GrupoId: string read FGrupoId write FGrupoId;
    property ParentForm: TForm read FParentForm write FParentForm;
  end;
var
  frmClientesGruposEditor: TfrmClientesGruposEditor;
implementation
uses
  dmg_Main, uDbErrorHandler, frm_ClientesGrupos, frm_ClientesEditor, frm_SelectProduct, frm_BaseSelect, System.Math, System.UITypes, dlg_ClonarTarifa;
{$R *.dfm}
procedure TfrmClientesGruposEditor.FormShow(Sender: TObject);
begin
  // Mantener evento vacío para compatibilidad DFM.
  // Toda la lógica de inicialización está en DoShow.
end;
procedure TfrmClientesGruposEditor.DoShow;
begin
  inherited DoShow;

  TAppTheme.ApplyToForm(Self);

  btnGuardar.OnClick := btnSaveClick;
  btnSalir.OnClick := btnSalirClick;
  btnEliminar.OnClick := btnEliminarClick;

  // Enlazar botones laterales de la plantilla heredada (pnlSideToolbar)
  btnNuevoArticulo.OnClick := btnAnadirPrecioClick;
  btnEliminaArticulo.OnClick := btnEliminarPrecioClick;
  btnBuscarArticulo.OnClick := btnAnadirPrecioClick;

  pgcDetails.Visible := True;
  TsListado.TabVisible := False;
  tsDatosEnvio.Caption := 'Precios Específicos del Grupo';

  pgcDetails.ActivePage := tsDatosEnvio;
  pnlSideToolbar.Visible := True;
  dbgItems.Visible := True;
  dbgItems.Options := [dgEditing, dgTitles, dgIndicator, dgColumnResize, dgColLines, dgRowLines, dgTabs, dgConfirmDelete, dgCancelOnExit, dgTitleClick, dgTitleHotTrack];
  dbgItems.ReadOnly := False;
  pnlFooter.Visible := False;

  // Configurar qryPreciosGroup y grilla de precios heredada (dbgItems)
  qryPreciosGroup.Close;
  qryPreciosGroup.Connection := dmgMain.dbConn;
  qryPreciosGroup.CachedUpdates := True;
  qryPreciosGroup.UpdateOptions.ReadOnly := False;
  qryPreciosGroup.UpdateOptions.EnableUpdate := True;
  qryPreciosGroup.UpdateOptions.EnableDelete := True;
  qryPreciosGroup.OnUpdateRecord := qryPreciosGroupUpdateRecord;
  qryPreciosGroup.AfterPost := qryPreciosGroupAfterPost;
  qryPreciosGroup.OnCalcFields := qryPreciosGroupCalcFields;

  qryPreciosGroup.SQL.Text :=
    'SELECT cgp.id, cgp.id_cliente_grupo, cgp.id_articulo, cgp.precio, cgp.valido_desde, cgp.valido_hasta, cgp.und_min, cgp.und_max, ' +
    '       a.id as codigo, a.descripcion as articulo_desc, a.pvp1 as tarifa_pvp1, COALESCE(t.tipo, 10.0) as tipo_iva, ' +
    '       ROUND(cgp.precio * (1 + (COALESCE(t.tipo, 10.00) / 100)), 2) as precio_con_iva, ' +
    '       CASE WHEN a.pvp1 IS NOT NULL AND a.pvp1 > 0 ' +
    '            THEN ROUND(((a.pvp1 - cgp.precio) / a.pvp1) * 100, 2) ' +
    '            ELSE 0.00 END as descuento_pct ' +
    'FROM ge_clientes_grupos_precios cgp ' +
    'INNER JOIN ge_articulos a ON cgp.id_articulo = a.id ' +
    'LEFT JOIN ge_tipos_iva t ON a.id_tipo_iva = t.id ' +
    'WHERE cgp.id_cliente_grupo = :id_grupo ' +
    'ORDER BY a.descripcion, cgp.und_min, cgp.valido_desde';

  if FGrupoId <> '' then
  begin
    LoadData;
    LoadPreciosGroup;
    LoadClientesGroup;
  end
  else
    DoAnadir;

  ResetChangeTracking;
end;

procedure TfrmClientesGruposEditor.dbgClientesGroupDblClick(Sender: TObject);
begin
  if qryClientesGroup.Active and not qryClientesGroup.IsEmpty then
  begin
    if not Assigned(frmClientesEditor) then
      Application.CreateForm(TfrmClientesEditor, frmClientesEditor);
    frmClientesEditor.ClienteId := qryClientesGroup.FieldByName('id').AsString;
    frmClientesEditor.ShowModal;
    LoadClientesGroup;
  end;
end;

procedure TfrmClientesGruposEditor.LoadClientesGroup;
var
  LIdInt: Integer;
begin
  if not Assigned(qryClientesGroup) then Exit;

  qryClientesGroup.Close;
  qryClientesGroup.SQL.Text :=
    'SELECT c.id, c.nombre_fiscal, c.nombre_comercial, c.nif, c.telefono, c.poblacion, c.email, c.activo ' +
    'FROM ge_clientes c ' +
    'WHERE c.clientesgrupo_id = :id_grupo ' +
    'ORDER BY c.nombre_fiscal';

  if TryStrToInt(FGrupoId, LIdInt) then
    qryClientesGroup.ParamByName('id_grupo').AsInteger := LIdInt
  else
    qryClientesGroup.ParamByName('id_grupo').AsString := FGrupoId;

  qryClientesGroup.Open;

  tsClientesGroup.Caption := Format('Clientes Pertenecientes (%d)', [qryClientesGroup.RecordCount]);

  dbgClientesGroup.Columns.Clear;
  with dbgClientesGroup.Columns.Add do
  begin
    FieldName := 'id';
    Title.Caption := 'Código';
    Width := 60;
  end;
  with dbgClientesGroup.Columns.Add do
  begin
    FieldName := 'nombre_fiscal';
    Title.Caption := 'Nombre Fiscal / Razón Social';
    Width := 220;
  end;
  with dbgClientesGroup.Columns.Add do
  begin
    FieldName := 'nombre_comercial';
    Title.Caption := 'Nombre Comercial';
    Width := 180;
  end;
  with dbgClientesGroup.Columns.Add do
  begin
    FieldName := 'nif';
    Title.Caption := 'NIF / CIF';
    Width := 95;
  end;
  with dbgClientesGroup.Columns.Add do
  begin
    FieldName := 'telefono';
    Title.Caption := 'Teléfono';
    Width := 95;
  end;
  with dbgClientesGroup.Columns.Add do
  begin
    FieldName := 'poblacion';
    Title.Caption := 'Población';
    Width := 120;
  end;
  with dbgClientesGroup.Columns.Add do
  begin
    FieldName := 'email';
    Title.Caption := 'Email';
    Width := 180;
  end;
  with dbgClientesGroup.Columns.Add do
  begin
    FieldName := 'activo';
    Title.Caption := 'Activo';
    Width := 60;
  end;
end;

procedure TfrmClientesGruposEditor.LoadPreciosGroup;
var
  LIdInt: Integer;
begin
  qryPreciosGroup.Close;
  if TryStrToInt(FGrupoId, LIdInt) then
    qryPreciosGroup.ParamByName('id_grupo').AsInteger := LIdInt
  else
    qryPreciosGroup.ParamByName('id_grupo').AsString := FGrupoId;
  qryPreciosGroup.Open;
  qryPreciosGroup.CommitUpdates;

  if qryPreciosGroup.FindField('tarifa_pvp1') is TNumericField then
    TNumericField(qryPreciosGroup.FieldByName('tarifa_pvp1')).DisplayFormat := '#,##0.00';

  if qryPreciosGroup.FindField('precio') is TNumericField then
  begin
    TNumericField(qryPreciosGroup.FieldByName('precio')).DisplayFormat := '#,##0.00';
    qryPreciosGroup.FieldByName('precio').ReadOnly := False;
    qryPreciosGroup.FieldByName('precio').OnChange := PrecioBaseChange;
  end;

  if qryPreciosGroup.FindField('und_min') is TNumericField then
  begin
    TNumericField(qryPreciosGroup.FieldByName('und_min')).DisplayFormat := '#,##0';
    qryPreciosGroup.FieldByName('und_min').ReadOnly := False;
  end;

  if qryPreciosGroup.FindField('und_max') is TNumericField then
  begin
    TNumericField(qryPreciosGroup.FieldByName('und_max')).DisplayFormat := '#,##0';
    qryPreciosGroup.FieldByName('und_max').ReadOnly := False;
  end;

  if qryPreciosGroup.FindField('descuento_pct') is TNumericField then
  begin
    TNumericField(qryPreciosGroup.FieldByName('descuento_pct')).DisplayFormat := '0.00 %';
    qryPreciosGroup.FieldByName('descuento_pct').ProviderFlags := [];
    qryPreciosGroup.FieldByName('descuento_pct').ReadOnly := False;
    qryPreciosGroup.FieldByName('descuento_pct').OnChange := DescuentoPorcentajeChange;
  end;

  if qryPreciosGroup.FindField('precio_con_iva') is TNumericField then
  begin
    TNumericField(qryPreciosGroup.FieldByName('precio_con_iva')).DisplayFormat := '#,##0.00';
    qryPreciosGroup.FieldByName('precio_con_iva').ProviderFlags := [];
    qryPreciosGroup.FieldByName('precio_con_iva').ReadOnly := False;
    qryPreciosGroup.FieldByName('precio_con_iva').OnChange := PrecioConIvaChange;
  end;

  if qryPreciosGroup.FindField('valido_desde') is TDateTimeField then
  begin
    TDateTimeField(qryPreciosGroup.FieldByName('valido_desde')).DisplayFormat := 'dd/mm/yyyy';
    TDateTimeField(qryPreciosGroup.FieldByName('valido_desde')).EditMask := '!99/99/9999;1; ';
    qryPreciosGroup.FieldByName('valido_desde').ReadOnly := False;
    TDateTimeField(qryPreciosGroup.FieldByName('valido_desde')).OnSetText := DateFieldSetText;
  end;
  if qryPreciosGroup.FindField('valido_hasta') is TDateTimeField then
  begin
    TDateTimeField(qryPreciosGroup.FieldByName('valido_hasta')).DisplayFormat := 'dd/mm/yyyy';
    TDateTimeField(qryPreciosGroup.FieldByName('valido_hasta')).EditMask := '!99/99/9999;1; ';
    qryPreciosGroup.FieldByName('valido_hasta').ReadOnly := False;
    TDateTimeField(qryPreciosGroup.FieldByName('valido_hasta')).OnSetText := DateFieldSetText;
  end;

  dbgItems.OnKeyDown := dbgPreciosGroupKeyDown;
  dbgItems.Font.Size := 9;
  dbgItems.TitleFont.Size := 9;
  dbgItems.ReadOnly := False;

  dbgItems.Columns.Clear;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'codigo';
    Title.Caption := 'Ref. Art.';
    Width := 55;
    ReadOnly := True;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'articulo_desc';
    Title.Caption := 'Artículo';
    Width := 150;
    ReadOnly := True;
  end;

  with dbgItems.Columns.Add do
  begin
    FieldName := 'tarifa_pvp1';
    Title.Caption := 'Tarifa Base';
    Width := 65;
    ReadOnly := True;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'precio';
    Title.Caption := 'Precio Sin IVA';
    Width := 75;
    ReadOnly := False;
  end;

  with dbgItems.Columns.Add do
  begin
    FieldName := 'precio_con_iva';
    Title.Caption := 'Precio Con IVA';
    Width := 80;
    ReadOnly := False;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'descuento_pct';
    Title.Caption := '% Dto.';
    Width := 55;
    ReadOnly := False;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'und_min';
    Title.Caption := 'Ud. Mín.';
    Width := 55;
    ReadOnly := False;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'und_max';
    Title.Caption := 'Ud. Máx.';
    Width := 55;
    ReadOnly := False;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'valido_desde';
    Title.Caption := 'Válido Desde';
    Width := 75;
    ReadOnly := False;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'valido_hasta';
    Title.Caption := 'Válido Hasta';
    Width := 75;
    ReadOnly := False;
  end;
end;
procedure TfrmClientesGruposEditor.qryPreciosGroupCalcFields(DataSet: TDataSet);
var
  LPrecioBase, LTarifa, LTipoIva, LDtoPct, LPrecioConIva: Double;
  FldPrecio, FldTarifa, FldTipoIva, FldPrecioConIva, FldDtoPct: TField;
begin
  if FUpdatingCalcField then Exit;
  FldPrecio := DataSet.FindField('precio');
  FldTarifa := DataSet.FindField('tarifa_pvp1');
  FldTipoIva := DataSet.FindField('tipo_iva');
  FldPrecioConIva := DataSet.FindField('precio_con_iva');
  FldDtoPct := DataSet.FindField('descuento_pct');
  if Assigned(FldPrecio) and Assigned(FldTipoIva) and Assigned(FldPrecioConIva) then
  begin
    LPrecioBase := FldPrecio.AsFloat;
    LTipoIva := FldTipoIva.AsFloat;
    LPrecioConIva := SimpleRoundTo(LPrecioBase * (1 + (LTipoIva / 100)), -2);
    FldPrecioConIva.AsFloat := LPrecioConIva;
    if Assigned(FldTarifa) and Assigned(FldDtoPct) then
    begin
      LTarifa := FldTarifa.AsFloat;
      if LTarifa > 0 then
        LDtoPct := SimpleRoundTo(((LTarifa - LPrecioBase) / LTarifa) * 100, -2)
      else
        LDtoPct := 0;
      FldDtoPct.AsFloat := LDtoPct;
    end;
  end;
end;
procedure TfrmClientesGruposEditor.DateFieldSetText(Sender: TField; const Text: string);
var
  LCleanText, LDateStr: string;
  LDate: TDateTime;
  LParts: TArray<string>;
  LDay, LMonth, LYear: Integer;
begin
  LCleanText := StringReplace(StringReplace(Text, '/', '', [rfReplaceAll]), ' ', '', [rfReplaceAll]);
  if LCleanText = '' then
  begin
    Sender.Clear;
    Exit;
  end;
  LDateStr := Trim(Text);
  if Pos('/', LDateStr) > 0 then
  begin
    LParts := LDateStr.Split(['/']);
    if Length(LParts) = 3 then
    begin
      LDay := StrToIntDef(Trim(LParts[0]), 0);
      LMonth := StrToIntDef(Trim(LParts[1]), 0);
      LYear := StrToIntDef(Trim(LParts[2]), 0);
      if (LYear > 0) and (LYear < 100) then
        LYear := 2000 + LYear;
      if (LDay >= 1) and (LDay <= 31) and (LMonth >= 1) and (LMonth <= 12) and (LYear >= 1900) then
      begin
        try
          Sender.AsDateTime := EncodeDate(LYear, LMonth, LDay);
          Exit;
        except
        end;
      end;
    end;
  end
  else
  begin
    if Length(LCleanText) = 6 then
    begin
      LDay := StrToIntDef(Copy(LCleanText, 1, 2), 0);
      LMonth := StrToIntDef(Copy(LCleanText, 3, 2), 0);
      LYear := 2000 + StrToIntDef(Copy(LCleanText, 5, 2), 0);
      if (LDay >= 1) and (LDay <= 31) and (LMonth >= 1) and (LMonth <= 12) and (LYear >= 1900) then
      begin
        try
          Sender.AsDateTime := EncodeDate(LYear, LMonth, LDay);
          Exit;
        except
        end;
      end;
    end;
    if Length(LCleanText) = 8 then
    begin
      LDay := StrToIntDef(Copy(LCleanText, 1, 2), 0);
      LMonth := StrToIntDef(Copy(LCleanText, 3, 2), 0);
      LYear := StrToIntDef(Copy(LCleanText, 5, 4), 0);
      if (LDay >= 1) and (LDay <= 31) and (LMonth >= 1) and (LMonth <= 12) and (LYear >= 1900) then
      begin
        try
          Sender.AsDateTime := EncodeDate(LYear, LMonth, LDay);
          Exit;
        except
        end;
      end;
    end;
  end;
  if TryStrToDate(LDateStr, LDate) then
    Sender.AsDateTime := LDate
  else
    Sender.Clear;
end;
procedure TfrmClientesGruposEditor.PrecioBaseChange(Sender: TField);
var
  LPrecioBase, LTarifa, LTipoIva, LPrecioConIvaCalc, LDtoPctCalc: Double;
  FldPrecioConIva, FldTarifa, FldTipoIva, FldDtoPct: TField;
begin
  if FUpdatingCalcField or not (qryPreciosGroup.State in [dsEdit, dsInsert]) then Exit;
  FldPrecioConIva := qryPreciosGroup.FindField('precio_con_iva');
  FldTarifa := qryPreciosGroup.FindField('tarifa_pvp1');
  FldTipoIva := qryPreciosGroup.FindField('tipo_iva');
  FldDtoPct := qryPreciosGroup.FindField('descuento_pct');
  if Assigned(FldPrecioConIva) and Assigned(FldTipoIva) then
  begin
    FUpdatingCalcField := True;
    try
      LPrecioBase := Sender.AsFloat;
      LTipoIva := FldTipoIva.AsFloat;
      LPrecioConIvaCalc := SimpleRoundTo(LPrecioBase * (1 + (LTipoIva / 100)), -2);
      FldPrecioConIva.AsFloat := LPrecioConIvaCalc;
      if Assigned(FldTarifa) and Assigned(FldDtoPct) then
      begin
        LTarifa := FldTarifa.AsFloat;
        if LTarifa > 0 then
          LDtoPctCalc := SimpleRoundTo(((LTarifa - LPrecioBase) / LTarifa) * 100, -2)
        else
          LDtoPctCalc := 0;
        FldDtoPct.AsFloat := LDtoPctCalc;
      end;
    finally
      FUpdatingCalcField := False;
    end;
  end;
end;
procedure TfrmClientesGruposEditor.DescuentoPorcentajeChange(Sender: TField);
var
  LDtoPct, LTarifa, LTipoIva, LPrecioBaseCalc, LPrecioConIvaCalc: Double;
  FldPrecio, FldPrecioConIva, FldTarifa, FldTipoIva: TField;
begin
  if FUpdatingCalcField or not (qryPreciosGroup.State in [dsEdit, dsInsert]) then Exit;
  FldPrecio := qryPreciosGroup.FindField('precio');
  FldPrecioConIva := qryPreciosGroup.FindField('precio_con_iva');
  FldTarifa := qryPreciosGroup.FindField('tarifa_pvp1');
  FldTipoIva := qryPreciosGroup.FindField('tipo_iva');
  if Assigned(FldPrecio) and Assigned(FldTarifa) and Assigned(FldTipoIva) then
  begin
    FUpdatingCalcField := True;
    try
      LDtoPct := Sender.AsFloat;
      LTarifa := FldTarifa.AsFloat;
      LTipoIva := FldTipoIva.AsFloat;
      LPrecioBaseCalc := SimpleRoundTo(LTarifa * (1 - (LDtoPct / 100)), -2);
      FldPrecio.AsFloat := LPrecioBaseCalc;
      if Assigned(FldPrecioConIva) then
      begin
        LPrecioConIvaCalc := SimpleRoundTo(LPrecioBaseCalc * (1 + (LTipoIva / 100)), -2);
        FldPrecioConIva.AsFloat := LPrecioConIvaCalc;
      end;
    finally
      FUpdatingCalcField := False;
    end;
  end;
end;
procedure TfrmClientesGruposEditor.PrecioConIvaChange(Sender: TField);
var
  LPrecioConIva, LTarifa, LTipoIva, LPrecioBaseCalc, LDtoPctCalc: Double;
  FldPrecio, FldTarifa, FldTipoIva, FldDtoPct: TField;
begin
  if FUpdatingCalcField or not (qryPreciosGroup.State in [dsEdit, dsInsert]) then Exit;
  FldPrecio := qryPreciosGroup.FindField('precio');
  FldTarifa := qryPreciosGroup.FindField('tarifa_pvp1');
  FldTipoIva := qryPreciosGroup.FindField('tipo_iva');
  FldDtoPct := qryPreciosGroup.FindField('descuento_pct');
  if Assigned(FldPrecio) and Assigned(FldTipoIva) then
  begin
    FUpdatingCalcField := True;
    try
      LPrecioConIva := Sender.AsFloat;
      LTipoIva := FldTipoIva.AsFloat;
      if (1 + (LTipoIva / 100)) <> 0 then
      begin
        LPrecioBaseCalc := SimpleRoundTo(LPrecioConIva / (1 + (LTipoIva / 100)), -2);
        FldPrecio.AsFloat := LPrecioBaseCalc;
        if Assigned(FldTarifa) and Assigned(FldDtoPct) then
        begin
          LTarifa := FldTarifa.AsFloat;
          if LTarifa > 0 then
            LDtoPctCalc := SimpleRoundTo(((LTarifa - LPrecioBaseCalc) / LTarifa) * 100, -2)
          else
            LDtoPctCalc := 0;
          FldDtoPct.AsFloat := LDtoPctCalc;
        end;
      end;
    finally
      FUpdatingCalcField := False;
    end;
  end;
end;
procedure TfrmClientesGruposEditor.dbgPreciosGroupKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  if Key = VK_RETURN then
  begin
    Key := 0;
    if dbgItems.SelectedIndex < dbgItems.Columns.Count - 1 then
    begin
      dbgItems.SelectedIndex := dbgItems.SelectedIndex + 1;
    end
    else
    begin
      if qryPreciosGroup.State in [dsEdit, dsInsert] then
        qryPreciosGroup.Post;
      qryPreciosGroup.Next;
      dbgItems.SelectedIndex := 3;
    end;
  end;
end;
procedure TfrmClientesGruposEditor.btnAnadirPrecioClick(Sender: TObject);
var
  i: Integer;
  LArtId: string;
  LPrice: Double;
begin
  if FGrupoId = '' then
  begin
    ShowMessage('Debe guardar el grupo de clientes antes de asignar precios específicos.');
    Exit;
  end;
  if not Assigned(frmSelectProduct) then
    Application.CreateForm(TfrmSelectProduct, frmSelectProduct);
  frmSelectProduct.MultiSelect := True;
  try
    if frmSelectProduct.ShowModal = mrOk then
    begin
      for i := 0 to frmSelectProduct.SelectedIds.Count - 1 do
      begin
        LArtId := frmSelectProduct.SelectedIds[i];
        LPrice := StrToFloatDef(frmSelectProduct.SelectedPrices[i], 0);
        dmgMain.qryExec.Close;
        dmgMain.qryExec.SQL.Text :=
          'INSERT INTO ge_clientes_grupos_precios (id_cliente_grupo, id_articulo, precio, id_user_creator, id_user_update) ' +
          'VALUES (:id_grupo, :id_articulo, :precio, :user, :user)';
        dmgMain.qryExec.ParamByName('id_grupo').AsInteger := StrToIntDef(FGrupoId, 0);
        dmgMain.qryExec.ParamByName('id_articulo').AsInteger := StrToIntDef(LArtId, 0);
        dmgMain.qryExec.ParamByName('precio').AsFloat := LPrice;
        dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
        dmgMain.qryExec.ExecSQL;
        dmgMain.qryExec.Close;
      end;
      LoadPreciosGroup;
      ResetChangeTracking;
      ResetModifiedState;
    end;
  finally
    frmSelectProduct.MultiSelect := False;
  end;
end;

procedure TfrmClientesGruposEditor.btnEliminarPrecioClick(Sender: TObject);
begin
  if qryPreciosGroup.IsEmpty then Exit;
  if MessageDlg('¿Desea eliminar el precio específico seleccionado?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'DELETE FROM ge_clientes_grupos_precios WHERE id = :id';
    dmgMain.qryExec.ParamByName('id').AsInteger := qryPreciosGroup.FieldByName('id').AsInteger;
    dmgMain.qryExec.ExecSQL;
    dmgMain.qryExec.Close;
    LoadPreciosGroup;
    ResetChangeTracking;
    ResetModifiedState;
  end;
end;

procedure TfrmClientesGruposEditor.qryPreciosGroupAfterPost(DataSet: TDataSet);
begin
  qryPreciosGroup.ApplyUpdates;
  qryPreciosGroup.CommitUpdates;
  ResetChangeTracking;
  ResetModifiedState;
end;

procedure TfrmClientesGruposEditor.qryPreciosGroupUpdateRecord(ASender: TDataSet; ARequest: TFDUpdateRequest;
  var AAction: TFDErrorAction; AOptions: TFDUpdateRowOptions);
var
  LUndMin, LUndMax: Integer;
  LValidoDesde, LValidoHasta: TDateTime;
  LHasDesde, LHasHasta: Boolean;
  LArticuloId, LRecordId: Integer;
  LQryCheck: TFDQuery;
  EMin, EMax: Integer;
  EHasDesde, EHasHasta: Boolean;
  EDesde, EHasta: TDateTime;
  DatesOverlap: Boolean;
begin
  if ARequest = arUpdate then
  begin
    // Validar rango de unidades
    if not ASender.FieldByName('und_min').IsNull and not ASender.FieldByName('und_max').IsNull then
    begin
      if ASender.FieldByName('und_min').AsInteger > ASender.FieldByName('und_max').AsInteger then
      begin
        AAction := eaFail;
        ShowMessage('Error: La unidad mínima (' + ASender.FieldByName('und_min').AsString +
                    ') no puede ser mayor que la unidad máxima (' + ASender.FieldByName('und_max').AsString + ').');
        Exit;
      end;
    end;

    // Comprobar solapamiento de rangos en base de datos
    LRecordId := ASender.FieldByName('id').AsInteger;
    LArticuloId := ASender.FieldByName('id_articulo').AsInteger;
    
    if ASender.FieldByName('und_min').IsNull then LUndMin := 0 else LUndMin := ASender.FieldByName('und_min').AsInteger;
    if ASender.FieldByName('und_max').IsNull then LUndMax := 2147483647 else LUndMax := ASender.FieldByName('und_max').AsInteger;

    LHasDesde := not ASender.FieldByName('valido_desde').IsNull;
    LHasHasta := not ASender.FieldByName('valido_hasta').IsNull;
    if LHasDesde then LValidoDesde := ASender.FieldByName('valido_desde').AsDateTime else LValidoDesde := 0;
    if LHasHasta then LValidoHasta := ASender.FieldByName('valido_hasta').AsDateTime else LValidoHasta := 0;

    LQryCheck := TFDQuery.Create(nil);
    try
      LQryCheck.Connection := dmgMain.dbConn;
      LQryCheck.SQL.Text :=
        'SELECT id, und_min, und_max, valido_desde, valido_hasta FROM ge_clientes_grupos_precios ' +
        'WHERE id_cliente_grupo = :id_grupo AND id_articulo = :id_art AND id <> :id';
      LQryCheck.ParamByName('id_grupo').AsInteger := StrToIntDef(FGrupoId, 0);
      LQryCheck.ParamByName('id_art').AsInteger := LArticuloId;
      LQryCheck.ParamByName('id').AsInteger := LRecordId;
      LQryCheck.Open;

      while not LQryCheck.Eof do
      begin
        if LQryCheck.FieldByName('und_min').IsNull then EMin := 0 else EMin := LQryCheck.FieldByName('und_min').AsInteger;
        if LQryCheck.FieldByName('und_max').IsNull then EMax := 2147483647 else EMax := LQryCheck.FieldByName('und_max').AsInteger;

        if (LUndMin <= EMax) and (LUndMax >= EMin) then
        begin
          EHasDesde := not LQryCheck.FieldByName('valido_desde').IsNull;
          EHasHasta := not LQryCheck.FieldByName('valido_hasta').IsNull;
          if EHasDesde then EDesde := LQryCheck.FieldByName('valido_desde').AsDateTime else EDesde := 0;
          if EHasHasta then EHasta := LQryCheck.FieldByName('valido_hasta').AsDateTime else EHasta := 0;

          DatesOverlap := True;
          if LHasDesde and EHasHasta and (LValidoDesde > EHasta) then DatesOverlap := False;
          if LHasHasta and EHasDesde and (LValidoHasta < EDesde) then DatesOverlap := False;

          if DatesOverlap then
          begin
            ShowMessage('Aviso: El rango de unidades/fechas introducido coincide o se solapa con otra regla de este artículo.');
            Break;
          end;
        end;
        LQryCheck.Next;
      end;
    finally
      LQryCheck.Free;
    end;

    try
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text :=
        'UPDATE ge_clientes_grupos_precios SET precio = :precio, und_min = :und_min, und_max = :und_max, ' +
        'valido_desde = :valido_desde, valido_hasta = :valido_hasta, ' +
        'id_user_update = :user, update_at = CURRENT_TIMESTAMP ' +
        'WHERE id = :id';
      dmgMain.qryExec.ParamByName('id').AsInteger := LRecordId;
      dmgMain.qryExec.ParamByName('precio').AsFloat := ASender.FieldByName('precio').AsFloat;

      dmgMain.qryExec.ParamByName('und_min').DataType := ftInteger;
      if ASender.FieldByName('und_min').IsNull then
        dmgMain.qryExec.ParamByName('und_min').Clear
      else
        dmgMain.qryExec.ParamByName('und_min').AsInteger := ASender.FieldByName('und_min').AsInteger;

      dmgMain.qryExec.ParamByName('und_max').DataType := ftInteger;
      if ASender.FieldByName('und_max').IsNull then
        dmgMain.qryExec.ParamByName('und_max').Clear
      else
        dmgMain.qryExec.ParamByName('und_max').AsInteger := ASender.FieldByName('und_max').AsInteger;

      dmgMain.qryExec.ParamByName('valido_desde').DataType := ftDate;
      if ASender.FieldByName('valido_desde').IsNull then
        dmgMain.qryExec.ParamByName('valido_desde').Clear
      else
        dmgMain.qryExec.ParamByName('valido_desde').AsDate := ASender.FieldByName('valido_desde').AsDateTime;

      dmgMain.qryExec.ParamByName('valido_hasta').DataType := ftDate;
      if ASender.FieldByName('valido_hasta').IsNull then
        dmgMain.qryExec.ParamByName('valido_hasta').Clear
      else
        dmgMain.qryExec.ParamByName('valido_hasta').AsDate := ASender.FieldByName('valido_hasta').AsDateTime;

      dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
      dmgMain.qryExec.ExecSQL;
      AAction := eaApplied;
    except
      on E: Exception do
        begin
          AAction := eaFail;
          TDbErrorHandler.HandleException(E, 'Error al guardar los cambios del precio específico de grupo');
        end;
    end;
  end
  else if ARequest = arDelete then
  begin
    try
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 'DELETE FROM ge_clientes_grupos_precios WHERE id = :id';
      dmgMain.qryExec.ParamByName('id').AsInteger := ASender.FieldByName('id').AsInteger;
      dmgMain.qryExec.ExecSQL;
      AAction := eaApplied;
    except
      on E: Exception do
        begin
          AAction := eaFail;
          TDbErrorHandler.HandleException(E, 'Error al eliminar el precio específico de grupo');
        end;
    end;
  end
  else
    AAction := eaApplied;
end;
procedure TfrmClientesGruposEditor.ClearEditsModified;
var
  I: Integer;
begin
  for I := 0 to ComponentCount - 1 do
  begin
    if Components[I] is TCustomEdit then
      TCustomEdit(Components[I]).Modified := False;
  end;
end;
procedure TfrmClientesGruposEditor.LoadData;
begin
  if FGrupoId = '' then Exit;
  dmgMain.qryExec.Close;
  dmgMain.qryExec.SQL.Text := 'SELECT * FROM ge_clientes_grupos WHERE id = :id';
  dmgMain.qryExec.ParamByName('id').AsString := FGrupoId;
  dmgMain.qryExec.Open;
  
  if not dmgMain.qryExec.IsEmpty then
  begin
    edtCodigo.Text := dmgMain.qryExec.FieldByName('id').AsString;
    edtNombre.Text := dmgMain.qryExec.FieldByName('nombre').AsString;
    chkActivo.Checked := dmgMain.qryExec.FieldByName('activo').AsBoolean;
  end;
  dmgMain.qryExec.Close;

  // Cargar/actualizar precios específicos y clientes asignados al grupo
  LoadPreciosGroup;
  LoadClientesGroup;

  ClearEditsModified;
  ResetChangeTracking;
end;
procedure TfrmClientesGruposEditor.btnSaveClick(Sender: TObject);
var
  LIsNew: Boolean;
begin
  if Trim(edtNombre.Text) = '' then
  begin
    ShowMessage('El nombre del grupo de clientes es obligatorio.');
    edtNombre.SetFocus;
    Exit;
  end;
  LIsNew := (FGrupoId = '');
  dmgMain.qryExec.Close;
  if LIsNew then
  begin
    dmgMain.qryExec.SQL.Text :=
      'INSERT INTO ge_clientes_grupos (nombre, activo, id_user_creator, id_user_update) ' +
      'VALUES (:nom, :act, :user, :user)';
  end
  else
  begin
    dmgMain.qryExec.SQL.Text :=
      'UPDATE ge_clientes_grupos SET nombre = :nom, activo = :act, id_user_update = :user, updated_at = CURRENT_TIMESTAMP ' +
      'WHERE id = :id';
    dmgMain.qryExec.ParamByName('id').AsString := FGrupoId;
  end;
  dmgMain.qryExec.ParamByName('nom').AsString := Trim(edtNombre.Text);
  dmgMain.qryExec.ParamByName('act').AsBoolean := chkActivo.Checked;
  dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
  try
    dmgMain.qryExec.ExecSQL;
    
    if LIsNew then
    begin
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 'SELECT LAST_INSERT_ID()';
      dmgMain.qryExec.Open;
      FGrupoId := dmgMain.qryExec.Fields[0].AsString;
      dmgMain.qryExec.Close;
    end;
    
    ResetModifiedState;
    ResetChangeTracking;
    if Assigned(FParentForm) and (FParentForm is TfrmClientesGrupos) then
      TfrmClientesGrupos(FParentForm).btnRefreshClick(nil);
    ShowMessage('Grupo de clientes guardado correctamente.');
  except
    on E: Exception do
      TDbErrorHandler.HandleException(E, 'Error al guardar el grupo de clientes');
  end;
end;
procedure TfrmClientesGruposEditor.btnSalirClick(Sender: TObject);
begin
  ModalResult := mrCancel;
  Close;
end;
procedure TfrmClientesGruposEditor.DoAnadir;
begin
  FGrupoId := '';
  edtCodigo.Text := '(Autonumérico)';
  edtNombre.Clear;
  chkActivo.Checked := True;
  
  // Limpiar rejillas de precios y clientes al crear un nuevo grupo
  qryPreciosGroup.Close;
  if Assigned(qryClientesGroup) and qryClientesGroup.Active then
    qryClientesGroup.Close;
  if Assigned(tsClientesGroup) then
    tsClientesGroup.Caption := 'Clientes Pertenecientes (0)';
  
  ClearEditsModified;
  edtNombre.SetFocus;
end;
procedure TfrmClientesGruposEditor.DoModificar;
begin
  edtNombre.SetFocus;
end;
procedure TfrmClientesGruposEditor.DoPrimero;
var
  LParent: TfrmClientesGrupos;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmClientesGrupos) then
    LParent := TfrmClientesGrupos(FParentForm)
  else if Assigned(frmClientesGrupos) then
    LParent := frmClientesGrupos;
  if Assigned(LParent) and Assigned(LParent.QryMain) and not LParent.QryMain.IsEmpty then
  begin
    LParent.QryMain.First;
    FGrupoId := LParent.QryMain.FieldByName('id').AsString;
    LoadData;
  end;
end;
procedure TfrmClientesGruposEditor.DoAnterior;
var
  LParent: TfrmClientesGrupos;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmClientesGrupos) then
    LParent := TfrmClientesGrupos(FParentForm)
  else if Assigned(frmClientesGrupos) then
    LParent := frmClientesGrupos;
  if Assigned(LParent) and Assigned(LParent.QryMain) and not LParent.QryMain.IsEmpty then
  begin
    LParent.QryMain.Prior;
    FGrupoId := LParent.QryMain.FieldByName('id').AsString;
    LoadData;
  end;
end;
procedure TfrmClientesGruposEditor.DoSiguiente;
var
  LParent: TfrmClientesGrupos;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmClientesGrupos) then
    LParent := TfrmClientesGrupos(FParentForm)
  else if Assigned(frmClientesGrupos) then
    LParent := frmClientesGrupos;
  if Assigned(LParent) and Assigned(LParent.QryMain) and not LParent.QryMain.IsEmpty then
  begin
    LParent.QryMain.Next;
    FGrupoId := LParent.QryMain.FieldByName('id').AsString;
    LoadData;
  end;
end;
procedure TfrmClientesGruposEditor.DoUltimo;
var
  LParent: TfrmClientesGrupos;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmClientesGrupos) then
    LParent := TfrmClientesGrupos(FParentForm)
  else if Assigned(frmClientesGrupos) then
    LParent := frmClientesGrupos;
  if Assigned(LParent) and Assigned(LParent.QryMain) and not LParent.QryMain.IsEmpty then
  begin
    LParent.QryMain.Last;
    FGrupoId := LParent.QryMain.FieldByName('id').AsString;
    LoadData;
  end;
end;
procedure TfrmClientesGruposEditor.btnEliminarClick(Sender: TObject);
var
  LNextId: string;
  LParent: TfrmClientesGrupos;
begin
  if FGrupoId = '' then
  begin
    ShowMessage('No hay ningún registro guardado para eliminar.');
    Exit;
  end;
  if MessageDlg('¿Está seguro de que desea eliminar este grupo de clientes?', 
                mtWarning, [mbYes, mbNo], 0) = mrYes then
  begin
    LNextId := '';
    LParent := nil;
    if Assigned(FParentForm) and (FParentForm is TfrmClientesGrupos) then
      LParent := TfrmClientesGrupos(FParentForm)
    else if Assigned(frmClientesGrupos) then
      LParent := frmClientesGrupos;
    if Assigned(LParent) and Assigned(LParent.QryMain) then
    begin
      LParent.QryMain.Next;
      if not LParent.QryMain.Eof then
      begin
        LNextId := LParent.QryMain.FieldByName('id').AsString;
        LParent.QryMain.Prior;
      end
      else
      begin
        LParent.QryMain.Prior;
        LParent.QryMain.Prior;
        if not LParent.QryMain.Bof then
        begin
          LNextId := LParent.QryMain.FieldByName('id').AsString;
          LParent.QryMain.Next;
        end
        else
        begin
          LParent.QryMain.Next;
        end;
      end;
    end;
    try
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 'DELETE FROM ge_clientes_grupos WHERE id = :id';
      dmgMain.qryExec.ParamByName('id').AsString := FGrupoId;
      dmgMain.qryExec.ExecSQL;
      if Assigned(LParent) then
      begin
        LParent.btnRefreshClick(nil);
        if LNextId <> '' then
        begin
          LParent.QryMain.Locate('id', LNextId, []);
          FGrupoId := LNextId;
          LoadData;
        end;
      end;
      if LNextId = '' then
      begin
        ShowMessage('Se ha eliminado el único registro disponible.');
        ModalResult := mrOk;
      end;
    except
      on E: Exception do
        TDbErrorHandler.HandleException(E, 'Error al eliminar el grupo de clientes');
    end;
  end;
end;

procedure TfrmClientesGruposEditor.DoDuplicar;
var
  LRes: Integer;
  LNewYear: Integer;
  LSelectForm: TfrmBaseSelect;
  LSrcGrupoId: string;
  LSrcGrupoNombre: string;
  LCount: Integer;
begin
  if FGrupoId = '' then
  begin
    ShowMessage('Debe guardar el grupo de clientes actual antes de renovar o importar precios.');
    Exit;
  end;

  LRes := MessageDlg(
    'Seleccione la operación que desea realizar:' + sLineBreak + sLineBreak +
    '[SÍ] -> Renovar / Clonar tarifas a un NUEVO EJERCICIO (Año)' + sLineBreak +
    '[NO] -> Importar precios desde OTRO grupo de clientes' + sLineBreak +
    '[CANCELAR] -> Cancelar',
    mtConfirmation, [mbYes, mbNo, mbCancel], 0);

  if LRes = mrCancel then
    Exit;

  if LRes = mrYes then
  begin
    if TdlgClonarTarifa.Execute(Self, FGrupoId, edtNombre.Text, LNewYear) then
    begin
      LoadPreciosGroup;
    end;
    Exit;
  end;

  // Si responde NO: Importar precios de otro grupo existente
  LSelectForm := TfrmBaseSelect.Create(nil);
  try
    LSelectForm.Caption := 'Seleccionar Grupo Origen de Precios';
    LSelectForm.pnlSearch.Visible := True;

    // Configurar consulta sobre ge_clientes_grupos excluyendo el grupo actual
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text :=
      'SELECT id, nombre FROM ge_clientes_grupos WHERE id <> :id_act AND activo = 1 ORDER BY nombre';
    dmgMain.qryExec.ParamByName('id_act').AsString := FGrupoId;
    dmgMain.qryExec.Open;

    if dmgMain.qryExec.IsEmpty then
    begin
      ShowMessage('No hay otros grupos de clientes disponibles desde los que copiar precios.');
      Exit;
    end;

    if LSelectForm.ShowModal = mrOk then
    begin
      LSrcGrupoId := LSelectForm.SelectedId;
      LSrcGrupoNombre := LSelectForm.SelectedDesc;

      if LSrcGrupoId = '' then Exit;

      if MessageDlg(Format('¿Está seguro de que desea importar los precios del grupo "%s" al grupo actual?', [LSrcGrupoNombre]),
                    mtConfirmation, [mbYes, mbNo], 0) = mrYes then
      begin
        dmgMain.qryExec.Close;
        dmgMain.qryExec.SQL.Text :=
          'INSERT INTO ge_clientes_grupos_precios ' +
          '(id_cliente_grupo, id_articulo, precio, valido_desde, valido_hasta, und_min, und_max, id_user_creator, id_user_update) ' +
          'SELECT :dst_grupo, id_articulo, precio, valido_desde, valido_hasta, und_min, und_max, :user, :user ' +
          'FROM ge_clientes_grupos_precios WHERE id_cliente_grupo = :src_grupo';
        dmgMain.qryExec.ParamByName('dst_grupo').AsString := FGrupoId;
        dmgMain.qryExec.ParamByName('src_grupo').AsString := LSrcGrupoId;
        dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
        dmgMain.qryExec.ExecSQL;

        dmgMain.qryExec.Close;
        dmgMain.qryExec.SQL.Text := 'SELECT ROW_COUNT()';
        dmgMain.qryExec.Open;
        LCount := dmgMain.qryExec.Fields[0].AsInteger;
        dmgMain.qryExec.Close;

        ShowMessage(Format('Se han importado %d precio(s) específico(s) correctamente.', [LCount]));
        LoadPreciosGroup;
      end;
    end;
  finally
    LSelectForm.Free;
  end;
end;

end.
