unit frm_Trabajadores;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, System.Types,
  System.Generics.Collections, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls,
  Vcl.StdCtrls, Vcl.Grids, Vcl.DBGrids, Data.DB, Vcl.Buttons, Vcl.Menus, uAppTheme,
  FireDAC.Stan.Param, FireDAC.Comp.Client, System.UITypes, uDBGridHelper, frm_BaseList, frxClass;

type
  TfrmTrabajadores = class(TfrmBaseList)
    procedure FormShow(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure btnRefreshClick(Sender: TObject); override;
    procedure btnNewClick(Sender: TObject); override;
    procedure btnEditClick(Sender: TObject); override;
    procedure btnDeleteClick(Sender: TObject); override;
  private
    grpGrupos: TGroupBox;
    cbGrupoFilter: TComboBox;
    FGrupoIds: TStringList;
    grpEmpresa: TGroupBox;
    cbEmpresaFilter: TComboBox;
    FEmpresaIds: TStringList;
    grpFechasCampo: TGroupBox;
    cbFechaCampo: TComboBox;
    btnOperaciones: TSpeedButton;
    pmOperaciones: TPopupMenu;
    FQryReportTarjetas: TFDQuery;
    procedure frxInformeBeforePrint(Sender: TfrxReportComponent);
    procedure ConfigurarBotonOperaciones;
    procedure btnOperacionesClick(Sender: TObject);
    procedure miImprimirTarjetaClick(Sender: TObject);
    procedure miSeleccionarTodosClick(Sender: TObject);
    procedure miDeseleccionarTodosClick(Sender: TObject);
    procedure miInvertirSeleccionClick(Sender: TObject);
    procedure ActualizarContadorSeleccion;
    procedure SeleccionarTodos;
    procedure DeseleccionarTodos;
    procedure InvertirSeleccion;
    procedure ImprimirTarjetas(const AIds: TArray<string>);
    function ObtenerTrabajadoresSeleccionados: TArray<string>;
    procedure ConfigurarFiltroGrupos;
    procedure CargarGrupos;
    procedure GrupoFilterChange(Sender: TObject);
    procedure ConfigurarFiltroEmpresa;
    procedure CargarEmpresas;
    procedure EmpresaFilterChange(Sender: TObject);
    procedure ConfigurarFiltroFechaCampo;
    procedure FechaCampoChange(Sender: TObject);
    procedure dbGridMainCellClick(Column: TColumn);
    procedure dbGridMainDblClick(Sender: TObject);
    procedure dbGridMainKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure dbGridMainKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure dbGridMainMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure dbGridMainDrawColumnCellCustom(Sender: TObject; const Rect: TRect;
      DataCol: Integer; Column: TColumn; State: TGridDrawState);
    procedure AbrirHistorialContratos;
    procedure CambiarEstadoActivo;
    function ConvertirFechaASql(const AFechaStr: string; var AFechaSQL: string): Boolean;
  protected
    procedure ApplyAllFilters; override;
  public
    { Public declarations }
  end;

var
  frmTrabajadores: TfrmTrabajadores;

implementation

uses
  dmg_Main, uDbErrorHandler, frm_TrabajadoresEditor, frxDBSet, frxBarcode, frm_PrintOptions,
  DelphiZXingQRCode;

{$R *.dfm}

procedure GenerarQRCodeBitmap(const AText: string; ABitmap: TBitmap; AScale: Integer = 4);
var
  QRCode: TDelphiZXingQRCode;
  Row, Column: Integer;
begin
  QRCode := TDelphiZXingQRCode.Create;
  try
    QRCode.Encoding := qrUTF8NoBOM;
    QRCode.QuietZone := 1;
    QRCode.Data := AText;

    ABitmap.SetSize(QRCode.Columns * AScale, QRCode.Rows * AScale);
    ABitmap.PixelFormat := pf24bit;
    ABitmap.Canvas.Brush.Color := clWhite;
    ABitmap.Canvas.FillRect(Rect(0, 0, ABitmap.Width, ABitmap.Height));

    ABitmap.Canvas.Brush.Color := clBlack;
    for Row := 0 to QRCode.Rows - 1 do
    begin
      for Column := 0 to QRCode.Columns - 1 do
      begin
        if QRCode.IsBlack[Row, Column] then
        begin
          ABitmap.Canvas.FillRect(Rect(
            Column * AScale,
            Row * AScale,
            (Column + 1) * AScale,
            (Row + 1) * AScale
          ));
        end;
      end;
    end;
  finally
    QRCode.Free;
  end;
end;

procedure TfrmTrabajadores.frxInformeBeforePrint(Sender: TfrxReportComponent);
var
  LNif: string;
  LBitmap: TBitmap;
  LStream: TMemoryStream;
  LLogoLoaded: Boolean;
  LLogoPath: string;
begin
  if not (Sender is TfrxPictureView) then Exit;
  if not Assigned(FQryReportTarjetas) or not FQryReportTarjetas.Active or FQryReportTarjetas.IsEmpty then Exit;

  // 1. Código QR con DNI/NIF del trabajador
  if SameText(Sender.Name, 'QR') then
  begin
    LNif := Trim(FQryReportTarjetas.FieldByName('NIF').AsString);
    if LNif = '' then
      LNif := Trim(FQryReportTarjetas.FieldByName('ID').AsString);

    if LNif <> '' then
    begin
      LBitmap := TBitmap.Create;
      try
        GenerarQRCodeBitmap(LNif, LBitmap, 4);
        TfrxPictureView(Sender).Picture.Assign(LBitmap);
      finally
        LBitmap.Free;
      end;
    end
    else
      TfrxPictureView(Sender).Picture.Assign(nil);
  end
  // 2. Logo de la empresa
  else if SameText(Sender.Name, 'ImgLogo') then
  begin
    LLogoLoaded := False;
    if (FQryReportTarjetas.FindField('logo_empresa') <> nil) and
       not TBlobField(FQryReportTarjetas.FieldByName('logo_empresa')).IsNull and
       (TBlobField(FQryReportTarjetas.FieldByName('logo_empresa')).BlobSize > 0) then
    begin
      LStream := TMemoryStream.Create;
      try
        TBlobField(FQryReportTarjetas.FieldByName('logo_empresa')).SaveToStream(LStream);
        LStream.Position := 0;
        if LStream.Size > 0 then
        begin
          TfrxPictureView(Sender).Picture.LoadFromStream(LStream);
          LLogoLoaded := True;
        end;
      finally
        LStream.Free;
      end;
    end;

    if not LLogoLoaded then
    begin
      LLogoPath := ExtractFilePath(ParamStr(0)) + 'logo_raul.png';
      if not FileExists(LLogoPath) then
        LLogoPath := ExtractFilePath(ParamStr(0)) + 'logo.png';
      if not FileExists(LLogoPath) then
        LLogoPath := 'c:\fuentes\proyectos delphi\RAUL_ASENCIO\GESTION\logo_raul.png';

      if FileExists(LLogoPath) then
        TfrxPictureView(Sender).Picture.LoadFromFile(LLogoPath)
      else
        TfrxPictureView(Sender).Picture.Assign(nil);
    end;
  end
  // 3. Fotografía del trabajador si existe en la base de datos
  else if SameText(Sender.Name, 'ImgTrabajador') or SameText(Sender.Name, 'Picture1') then
  begin
    if (FQryReportTarjetas.FindField('imagen_1') <> nil) and
       not TBlobField(FQryReportTarjetas.FieldByName('imagen_1')).IsNull and
       (TBlobField(FQryReportTarjetas.FieldByName('imagen_1')).BlobSize > 0) then
    begin
      LStream := TMemoryStream.Create;
      try
        TBlobField(FQryReportTarjetas.FieldByName('imagen_1')).SaveToStream(LStream);
        LStream.Position := 0;
        if LStream.Size > 0 then
          TfrxPictureView(Sender).Picture.LoadFromStream(LStream)
        else
          TfrxPictureView(Sender).Picture.Assign(nil);
      finally
        LStream.Free;
      end;
    end
    else
    begin
      TfrxPictureView(Sender).Picture.Assign(nil);
    end;
  end;
end;

procedure TfrmTrabajadores.FormShow(Sender: TObject);
var
  I: Integer;
  LIdx: Integer;
begin
  AlphaFilterField := 'nombre_completo';
  StatusFilterField := 'activo';
  DateFilterField := 'fecha_alta';
  StatusValues.Clear;
  StatusValues.Add('Activo');
  StatusValues.Add('Inactivo');

  inherited;
  frmTrabajadores := Self;
  OnDestroy := FormDestroy;
  pnlSearch.Visible := True;
  lblSearch.Caption := 'Buscar Trabajador:';

  // Configurar ancho adecuado y holgado para el panel lateral de filtros
  pnlLeftFilter.Width := 210;
  pnlLeftFilter.Visible := True;

  // Configurar filtros desplegables
  ConfigurarFiltroGrupos;
  ConfigurarFiltroEmpresa;
  ConfigurarFiltroFechaCampo;

  // Personalizar captions de los radio buttons de fechas
  if Assigned(rbDateAll) then rbDateAll.Caption := 'Todas las fechas';
  if Assigned(rbDateToday) then rbDateToday.Caption := 'Hoy';
  if Assigned(rbDateWeek) then rbDateWeek.Caption := 'Última semana';
  if Assigned(rbDateMonth) then rbDateMonth.Caption := 'Último mes';

  // Habilitar multiselección nativa por filas con soporte para Ctrl y Mayúsculas (Shift)
  dbGridMain.Options := dbGridMain.Options + [dgMultiSelect, dgRowSelect] - [dgEditing];

  // Configurar eventos de interacción de la rejilla
  dbGridMain.OnCellClick := dbGridMainCellClick;
  dbGridMain.OnDblClick := dbGridMainDblClick;
  dbGridMain.OnKeyDown := dbGridMainKeyDown;
  dbGridMain.OnKeyUp := dbGridMainKeyUp;
  dbGridMain.OnMouseUp := dbGridMainMouseUp;
  dbGridMain.OnDrawColumnCell := dbGridMainDrawColumnCellCustom;

  ConfigurarBotonOperaciones;
  dbGridMain.PopupMenu := pmOperaciones;

  // Seleccionar por defecto el filtro de 'Activo'
  for I := 0 to grpStatuses.ControlCount - 1 do
  begin
    if grpStatuses.Controls[I] is TComboBox then
    begin
      LIdx := TComboBox(grpStatuses.Controls[I]).Items.IndexOf('Activo');
      if LIdx >= 0 then
        TComboBox(grpStatuses.Controls[I]).ItemIndex := LIdx;
      Break;
    end;
  end;

  btnRefreshClick(nil);
end;

procedure TfrmTrabajadores.FormDestroy(Sender: TObject);
begin
  if Assigned(FGrupoIds) then
    FreeAndNil(FGrupoIds);
  if Assigned(FEmpresaIds) then
    FreeAndNil(FEmpresaIds);

  if frmTrabajadores = Self then
    frmTrabajadores := nil;
end;

procedure TfrmTrabajadores.ConfigurarFiltroGrupos;
begin
  if not Assigned(FGrupoIds) then
    FGrupoIds := TStringList.Create;

  if not Assigned(grpGrupos) then
  begin
    grpGrupos := TGroupBox.Create(Self);
    grpGrupos.Parent := pnlLeftFilter;
    grpGrupos.Align := alTop;
    grpGrupos.Caption := 'Grupo';
    grpGrupos.Height := 55;
    grpGrupos.Padding.Left := 5;
    grpGrupos.Padding.Right := 5;
    grpGrupos.Padding.Top := 3;
    grpGrupos.Padding.Bottom := 5;

    cbGrupoFilter := TComboBox.Create(Self);
    cbGrupoFilter.Parent := grpGrupos;
    cbGrupoFilter.Align := alClient;
    cbGrupoFilter.Style := csDropDownList;
    cbGrupoFilter.OnChange := GrupoFilterChange;
  end;

  // Asegurar orden visual: Estado arriba, Grupo abajo, Empresa después
  grpStatuses.Top := 0;
  grpGrupos.Top := 65;

  CargarGrupos;
end;

procedure TfrmTrabajadores.ConfigurarFiltroEmpresa;
begin
  if not Assigned(FEmpresaIds) then
    FEmpresaIds := TStringList.Create;

  if not Assigned(grpEmpresa) then
  begin
    grpEmpresa := TGroupBox.Create(Self);
    grpEmpresa.Parent := pnlLeftFilter;
    grpEmpresa.Align := alTop;
    grpEmpresa.Caption := 'Empresa';
    grpEmpresa.Height := 55;
    grpEmpresa.Padding.Left := 5;
    grpEmpresa.Padding.Right := 5;
    grpEmpresa.Padding.Top := 3;
    grpEmpresa.Padding.Bottom := 5;

    cbEmpresaFilter := TComboBox.Create(Self);
    cbEmpresaFilter.Parent := grpEmpresa;
    cbEmpresaFilter.Align := alClient;
    cbEmpresaFilter.Style := csDropDownList;
    cbEmpresaFilter.Tag := 999;
    cbEmpresaFilter.OnChange := EmpresaFilterChange;
  end;

  grpEmpresa.Top := 130;

  CargarEmpresas;
end;

procedure TfrmTrabajadores.CargarEmpresas;
var
  LQry: TFDQuery;
begin
  if not Assigned(cbEmpresaFilter) or not Assigned(FEmpresaIds) then Exit;

  cbEmpresaFilter.Items.Clear;
  FEmpresaIds.Clear;

  // Primera opción: empresa actual (por defecto)
  cbEmpresaFilter.Items.Add('[Empresa actual]');
  FEmpresaIds.Add(dmgMain.CurrentCompanyId);

  // Segunda opción: todas las empresas
  cbEmpresaFilter.Items.Add('[Todas las empresas]');
  FEmpresaIds.Add('ALL');

  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text := 'SELECT id, nombre FROM ge_empresas WHERE activo = 1 ORDER BY nombre';
    LQry.Open;
    while not LQry.Eof do
    begin
      cbEmpresaFilter.Items.Add(LQry.FieldByName('nombre').AsString);
      FEmpresaIds.Add(LQry.FieldByName('id').AsString);
      LQry.Next;
    end;
  finally
    LQry.Free;
  end;

  // Seleccionar la empresa actual por defecto
  cbEmpresaFilter.ItemIndex := 0;
end;

procedure TfrmTrabajadores.EmpresaFilterChange(Sender: TObject);
begin
  btnRefreshClick(nil);
end;

procedure TfrmTrabajadores.ConfigurarFiltroFechaCampo;
begin
  if not Assigned(grpFechasCampo) then
  begin
    grpFechasCampo := TGroupBox.Create(Self);
    grpFechasCampo.Parent := pnlLeftFilter;
    grpFechasCampo.Align := alTop;
    grpFechasCampo.Caption := 'Filtrar fecha por';
    grpFechasCampo.Height := 55;
    grpFechasCampo.Padding.Left := 5;
    grpFechasCampo.Padding.Right := 5;
    grpFechasCampo.Padding.Top := 3;
    grpFechasCampo.Padding.Bottom := 5;

    cbFechaCampo := TComboBox.Create(Self);
    cbFechaCampo.Parent := grpFechasCampo;
    cbFechaCampo.Align := alClient;
    cbFechaCampo.Style := csDropDownList;
    cbFechaCampo.Tag := 999;
    cbFechaCampo.Items.Add('Fecha Alta');
    cbFechaCampo.Items.Add('Fecha Baja');
    cbFechaCampo.Items.Add('Fecha Fin Contrato');
    cbFechaCampo.ItemIndex := 0;
    cbFechaCampo.OnChange := FechaCampoChange;
  end;

  grpFechasCampo.Top := 195;
end;

procedure TfrmTrabajadores.FechaCampoChange(Sender: TObject);
begin
  if not Assigned(cbFechaCampo) then Exit;

  case cbFechaCampo.ItemIndex of
    0: DateFilterField := 'fecha_alta';
    1: DateFilterField := 'fecha_baja';
    2: DateFilterField := 'fecha_fin_contrato';
  else
    DateFilterField := 'fecha_alta';
  end;

  ApplyAllFilters;
end;

procedure TfrmTrabajadores.CargarGrupos;
var
  LQry: TFDQuery;
begin
  if not Assigned(cbGrupoFilter) or not Assigned(FGrupoIds) then Exit;

  cbGrupoFilter.Items.Clear;
  FGrupoIds.Clear;

  cbGrupoFilter.Items.Add('TODOS');

  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text := 'SELECT id, codigo, nombre FROM ge_trabajadores_grupos WHERE activo = 1 ORDER BY codigo, nombre';
    LQry.Open;
    while not LQry.Eof do
    begin
      cbGrupoFilter.Items.Add(LQry.FieldByName('nombre').AsString);
      FGrupoIds.Add(LQry.FieldByName('id').AsString);
      LQry.Next;
    end;
  finally
    LQry.Free;
  end;

  cbGrupoFilter.ItemIndex := 0;
end;

procedure TfrmTrabajadores.GrupoFilterChange(Sender: TObject);
begin
  ApplyAllFilters;
end;

procedure TfrmTrabajadores.ApplyAllFilters;
var
  LGrupoFilter, LEmpresaFilter: string;
  LEmpIdx: Integer;
  LEmpId: string;
begin
  inherited ApplyAllFilters;

  // Filtro por grupo de trabajo
  if Assigned(cbGrupoFilter) and (cbGrupoFilter.ItemIndex > 0) and Assigned(FGrupoIds) and
     (cbGrupoFilter.ItemIndex - 1 < FGrupoIds.Count) then
  begin
    LGrupoFilter := 'id_grupo = ' + FGrupoIds[cbGrupoFilter.ItemIndex - 1];
    if FQryMain.Filter <> '' then
      FQryMain.Filter := '(' + FQryMain.Filter + ') AND (' + LGrupoFilter + ')'
    else
      FQryMain.Filter := LGrupoFilter;
    FQryMain.Filtered := True;
  end;

  // Filtro por empresa (solo para filtrado local cuando se seleccionó una empresa concreta
  // diferente a la actual y no se trata de 'Todas')
  if Assigned(cbEmpresaFilter) and Assigned(FEmpresaIds) and (cbEmpresaFilter.ItemIndex >= 2) then
  begin
    LEmpIdx := cbEmpresaFilter.ItemIndex;
    if LEmpIdx < FEmpresaIds.Count then
    begin
      LEmpId := FEmpresaIds[LEmpIdx];
      LEmpresaFilter := 'id_empresa = ' + LEmpId;
      if FQryMain.Filter <> '' then
        FQryMain.Filter := '(' + FQryMain.Filter + ') AND (' + LEmpresaFilter + ')'
      else
        FQryMain.Filter := LEmpresaFilter;
      FQryMain.Filtered := True;
    end;
  end;

  lblRecordCount.Caption := Format('Registros: %d', [FQryMain.RecordCount]);
end;

procedure TfrmTrabajadores.btnRefreshClick(Sender: TObject);
var
  LSql, LEmpWhere: string;
  I: Integer;
  LFilterAllEmpresas: Boolean;
  LFilterEmpresaId: string;
begin
  FQryMain.Close;

  // Determinar filtro de empresa según selección del combo
  LFilterAllEmpresas := False;
  LFilterEmpresaId := dmgMain.CurrentCompanyId;
  if Assigned(cbEmpresaFilter) and Assigned(FEmpresaIds) then
  begin
    if cbEmpresaFilter.ItemIndex = 1 then // [Todas las empresas]
      LFilterAllEmpresas := True
    else if (cbEmpresaFilter.ItemIndex >= 2) and (cbEmpresaFilter.ItemIndex < FEmpresaIds.Count) then
      LFilterEmpresaId := FEmpresaIds[cbEmpresaFilter.ItemIndex]
    else
      LFilterEmpresaId := dmgMain.CurrentCompanyId;
  end;

  LSql := 'SELECT t.id, te.id_empresa, te.codigo, t.nif, ' +
          'CASE ' +
          '  WHEN (t.apellidos IS NULL OR TRIM(t.apellidos) = '''' OR t.nombre LIKE CONCAT(''%'', TRIM(t.apellidos))) THEN t.nombre ' +
          '  ELSE TRIM(CONCAT(t.nombre, '' '', t.apellidos)) ' +
          'END AS nombre_completo, ' +
          't.nombre, t.apellidos, ' +
          'COALESCE(te.id_grupo, t.grupo, 0) AS id_grupo, ' +
          'COALESCE(tg.nombre, '''') AS grupo_nombre, ' +
          'COALESCE(emp.nombre, '''') AS empresa_nombre, ' +
          't.convocatoria, t.telefono1, t.email, te.horas_contrato, ' +
          '(SELECT COALESCE(SUM(h.horas_dia), 0) FROM ge_trabajadores_horario h WHERE h.id_trabajador = t.id AND h.activo = 1) AS horas_horario, ' +
          'te.fecha_alta, ' +
          'te.fecha_baja, ' +
          'COALESCE( ' +
          '  (SELECT c.fecha_fin_contrato FROM ge_trabajadores_contratos c ' +
          '   WHERE c.id_trabajador = t.id AND c.id_empresa = te.id_empresa AND c.fecha_fin_contrato IS NOT NULL ' +
          '   ORDER BY c.activo DESC, c.fecha_alta DESC, c.id DESC LIMIT 1), ' +
          '  te.fecha_baja ' +
          ') AS fecha_fin_contrato, ' +
          'te.activo, ' +
          '''Contratos'' AS btn_contratos ' +
          'FROM ge_trabajadores t ' +
          'INNER JOIN ge_trabajadores_empresas te ON te.id_trabajador = t.id ' +
          'LEFT JOIN ge_trabajadores_grupos tg ON tg.id = COALESCE(te.id_grupo, t.grupo) ' +
          'LEFT JOIN ge_empresas emp ON te.id_empresa = emp.id ';

  if LFilterAllEmpresas then
    LEmpWhere := 'WHERE 1=1 '
  else
    LEmpWhere := 'WHERE te.id_empresa = :empresa_id ';

  LSql := LSql + LEmpWhere;

  if Trim(edtSearch.Text) <> '' then
  begin
    LSql := LSql + 'AND (te.codigo LIKE :search OR t.nif LIKE :search OR t.nombre LIKE :search OR t.apellidos LIKE :search OR t.convocatoria LIKE :search OR tg.nombre LIKE :search OR emp.nombre LIKE :search) ';
  end;
  LSql := LSql + 'ORDER BY te.activo DESC, t.nombre ASC, t.apellidos ASC';

  FQryMain.SQL.Text := LSql;

  if not LFilterAllEmpresas then
    FQryMain.ParamByName('empresa_id').AsString := LFilterEmpresaId;

  if Trim(edtSearch.Text) <> '' then
    FQryMain.ParamByName('search').AsString := '%' + Trim(edtSearch.Text) + '%';

  FQryMain.Open;

  // Formato de fechas en rejilla
  if FQryMain.FindField('fecha_alta') <> nil then
  begin
    if FQryMain.FieldByName('fecha_alta') is TDateTimeField then
      TDateTimeField(FQryMain.FieldByName('fecha_alta')).DisplayFormat := 'dd/mm/yyyy'
    else if FQryMain.FieldByName('fecha_alta') is TDateField then
      TDateField(FQryMain.FieldByName('fecha_alta')).DisplayFormat := 'dd/mm/yyyy';
  end;

  if FQryMain.FindField('fecha_baja') <> nil then
  begin
    if FQryMain.FieldByName('fecha_baja') is TDateTimeField then
      TDateTimeField(FQryMain.FieldByName('fecha_baja')).DisplayFormat := 'dd/mm/yyyy'
    else if FQryMain.FieldByName('fecha_baja') is TDateField then
      TDateField(FQryMain.FieldByName('fecha_baja')).DisplayFormat := 'dd/mm/yyyy';
  end;

  if FQryMain.FindField('fecha_fin_contrato') <> nil then
  begin
    if FQryMain.FieldByName('fecha_fin_contrato') is TDateTimeField then
      TDateTimeField(FQryMain.FieldByName('fecha_fin_contrato')).DisplayFormat := 'dd/mm/yyyy'
    else if FQryMain.FieldByName('fecha_fin_contrato') is TDateField then
      TDateField(FQryMain.FieldByName('fecha_fin_contrato')).DisplayFormat := 'dd/mm/yyyy';
  end;

  // Configurar títulos y anchos de columnas de la rejilla
  dbGridMain.Columns.Clear;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'empresa_nombre';
    Title.Caption := 'Empresa';
    Width := 120;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'codigo';
    Title.Caption := 'Código';
    Width := 65;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'nif';
    Title.Caption := 'NIF/NIE';
    Width := 95;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'nombre_completo';
    Title.Caption := 'Nombre Completo';
    Width := 220;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'grupo_nombre';
    Title.Caption := 'Grupo';
    Width := 140;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'convocatoria';
    Title.Caption := 'Puesto / Convocatoria';
    Width := 130;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'telefono1';
    Title.Caption := 'Teléfono';
    Width := 95;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'email';
    Title.Caption := 'Email';
    Width := 160;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'horas_contrato';
    Title.Caption := 'H. Contrato';
    Width := 75;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'horas_horario';
    Title.Caption := 'H. Horario';
    Width := 75;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'fecha_alta';
    Title.Caption := 'F. Alta';
    Width := 85;
    Alignment := taCenter;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'fecha_baja';
    Title.Caption := 'F. Baja';
    Width := 85;
    Alignment := taCenter;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'fecha_fin_contrato';
    Title.Caption := 'F. Fin Contrato';
    Width := 100;
    Alignment := taCenter;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'activo';
    Title.Caption := 'Activo';
    Width := 60;
    ReadOnly := True;
  end;
  with dbGridMain.Columns.Add do
  begin
    FieldName := 'btn_contratos';
    Title.Caption := 'Contr.';
    Title.Alignment := taCenter;
    Width := 45;
    Alignment := taCenter;
    ReadOnly := True;
  end;

  // Habilitar multiselección nativa por filas con soporte para Ctrl y Mayúsculas (Shift)
  dbGridMain.Options := dbGridMain.Options + [dgMultiSelect, dgRowSelect] - [dgEditing];

  dbGridMain.AutoFitColumns;

  // Fijar ancho compacto para columnas interactivas
  for I := 0 to dbGridMain.Columns.Count - 1 do
  begin
    if SameText(dbGridMain.Columns[I].FieldName, 'btn_contratos') then
      dbGridMain.Columns[I].Width := 45
    else if SameText(dbGridMain.Columns[I].FieldName, 'activo') then
      dbGridMain.Columns[I].Width := 55;
  end;

  ActualizarContadorSeleccion;
  ApplyAllFilters;
end;

procedure TfrmTrabajadores.dbGridMainDrawColumnCellCustom(Sender: TObject; const Rect: TRect;
  DataCol: Integer; Column: TColumn; State: TGridDrawState);
var
  LBtnRect: TRect;
begin
  if SameText(Column.FieldName, 'btn_contratos') then
  begin
    if gdSelected in State then
      dbGridMain.Canvas.Brush.Color := $E8EFF7
    else
      dbGridMain.Canvas.Brush.Color := clWhite;
    dbGridMain.Canvas.FillRect(Rect);

    LBtnRect := Rect;
    InflateRect(LBtnRect, -2, -2);

    dbGridMain.Canvas.Brush.Color := $F4F4F4;
    dbGridMain.Canvas.Pen.Color := $C5C5C5;
    dbGridMain.Canvas.RoundRect(LBtnRect.Left, LBtnRect.Top, LBtnRect.Right, LBtnRect.Bottom, 4, 4);

    dbGridMain.Canvas.Font.Name := 'Segoe UI Emoji';
    dbGridMain.Canvas.Font.Size := 10;
    dbGridMain.Canvas.Font.Style := [];
    dbGridMain.Canvas.Font.Color := clWindowText;

    DrawText(dbGridMain.Canvas.Handle, PChar('📋'), -1, LBtnRect,
      DT_CENTER or DT_VCENTER or DT_SINGLELINE);
    Exit;
  end;

  dbGridMainDrawColumnCell(Sender, Rect, DataCol, Column, State);
end;

procedure TfrmTrabajadores.dbGridMainCellClick(Column: TColumn);
begin
  if (Column = nil) or FQryMain.IsEmpty then Exit;

  if SameText(Column.FieldName, 'btn_contratos') then
  begin
    AbrirHistorialContratos;
  end
  else if SameText(Column.FieldName, 'activo') then
  begin
    CambiarEstadoActivo;
  end;
end;

procedure TfrmTrabajadores.dbGridMainMouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  LCoord: TGridCoord;
  LColIdx: Integer;
  LCol: TColumn;
begin
  if (Button = mbLeft) and (Shift = []) then
  begin
    LCoord := dbGridMain.MouseCoord(X, Y);
    if (LCoord.Y > 0) and (LCoord.X >= 0) then
    begin
      LColIdx := LCoord.X;
      if dgIndicator in dbGridMain.Options then
        Dec(LColIdx);

      if (LColIdx >= 0) and (LColIdx < dbGridMain.Columns.Count) then
      begin
        LCol := dbGridMain.Columns[LColIdx];
        if SameText(LCol.FieldName, 'btn_contratos') then
        begin
          AbrirHistorialContratos;
          Exit;
        end
        else if SameText(LCol.FieldName, 'activo') then
        begin
          CambiarEstadoActivo;
          Exit;
        end;
      end;
    end;
  end;

  ActualizarContadorSeleccion;
end;

procedure TfrmTrabajadores.dbGridMainKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  ActualizarContadorSeleccion;
end;

procedure TfrmTrabajadores.dbGridMainDblClick(Sender: TObject);
begin
  if Assigned(dbGridMain.SelectedField) then
  begin
    if SameText(dbGridMain.SelectedField.FieldName, 'btn_contratos') then
    begin
      AbrirHistorialContratos;
      Exit;
    end
    else if SameText(dbGridMain.SelectedField.FieldName, 'activo') then
    begin
      CambiarEstadoActivo;
      Exit;
    end;
  end;

  inherited;
end;

procedure TfrmTrabajadores.dbGridMainKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  // Ctrl + A para seleccionar todos los trabajadores visibles
  if (Key = Ord('A')) and (ssCtrl in Shift) then
  begin
    Key := 0;
    SeleccionarTodos;
    Exit;
  end;

  if Assigned(dbGridMain.SelectedField) then
  begin
    if (Key = VK_SPACE) and SameText(dbGridMain.SelectedField.FieldName, 'activo') then
    begin
      Key := 0;
      CambiarEstadoActivo;
      Exit;
    end
    else if ((Key = VK_SPACE) or (Key = VK_RETURN)) and SameText(dbGridMain.SelectedField.FieldName, 'btn_contratos') then
    begin
      Key := 0;
      AbrirHistorialContratos;
      Exit;
    end;
  end;
end;

procedure TfrmTrabajadores.AbrirHistorialContratos;
var
  LEditor: TfrmTrabajadoresEditor;
  LId: string;
begin
  if FQryMain.IsEmpty then Exit;

  LId := FQryMain.FieldByName('id').AsString;
  LEditor := TfrmTrabajadoresEditor.Create(Self);
  try
    LEditor.TrabajadorId := LId;
    LEditor.InitialTab := LEditor.tsContratos;
    LEditor.ShowModal;
    btnRefreshClick(nil);
    if LId <> '' then
      FQryMain.Locate('id', LId, []);
  finally
    LEditor.Free;
  end;
end;

function TfrmTrabajadores.ConvertirFechaASql(const AFechaStr: string; var AFechaSQL: string): Boolean;
var
  LDate: TDateTime;
  LFS: TFormatSettings;
  LParts: TArray<string>;
  D, M, Y: Word;
begin
  Result := False;
  AFechaSQL := '';
  if Trim(AFechaStr) = '' then Exit;

  LFS := TFormatSettings.Create('es-ES');
  LFS.DateSeparator := '/';
  LFS.ShortDateFormat := 'dd/mm/yyyy';

  // 1. Probar formato dd/mm/yyyy estándar
  if TryStrToDate(AFechaStr, LDate, LFS) then
  begin
    AFechaSQL := FormatDateTime('yyyy-mm-dd', LDate);
    Exit(True);
  end;

  // 2. Probar si el usuario escribió con barras o guiones
  if Pos('/', AFechaStr) > 0 then
    LParts := AFechaStr.Split(['/'])
  else if Pos('-', AFechaStr) > 0 then
    LParts := AFechaStr.Split(['-'])
  else
    LParts := nil;

  if Length(LParts) = 3 then
  begin
    if Length(LParts[0]) = 4 then // yyyy-mm-dd
    begin
      Y := StrToIntDef(LParts[0], 0);
      M := StrToIntDef(LParts[1], 0);
      D := StrToIntDef(LParts[2], 0);
    end
    else // dd-mm-yyyy o dd/mm/yyyy
    begin
      D := StrToIntDef(LParts[0], 0);
      M := StrToIntDef(LParts[1], 0);
      Y := StrToIntDef(LParts[2], 0);
    end;

    if (Y >= 1900) and (Y <= 2100) and (M in [1..12]) and (D in [1..31]) then
    begin
      if TryEncodeDate(Y, M, D, LDate) then
      begin
        AFechaSQL := FormatDateTime('yyyy-mm-dd', LDate);
        Exit(True);
      end;
    end;
  end;
end;

procedure TfrmTrabajadores.CambiarEstadoActivo;
var
  LId, LNombre, LFechaFin, LFechaFinSQL, LMotivo, LFechaAlta, LFechaAltaSQL, LTipoContrato: string;
  LEmpId: Integer;
  LIsActive: Boolean;
  LQry: TFDQuery;
begin
  if FQryMain.IsEmpty then Exit;

  LId := FQryMain.FieldByName('id').AsString;
  LNombre := FQryMain.FieldByName('nombre_completo').AsString;
  LEmpId := FQryMain.FieldByName('id_empresa').AsInteger;
  if LEmpId = 0 then
    LEmpId := StrToIntDef(dmgMain.CurrentCompanyId, 1);
  LIsActive := FQryMain.FieldByName('activo').AsInteger <> 0;

  if LIsActive then
  begin
    // El empleado está ACTIVO y se solicita dar de baja / finalización de contrato
    LFechaFin := FormatDateTime('dd/mm/yyyy', Date);
    if not InputQuery('Baja de Empleado - Fin de Contrato',
      'El trabajador "' + LNombre + '" está actualmente ACTIVO.'#13#10#13#10 +
      'Para darlo de baja, introduzca la Fecha de Finalización de Contrato (DD/MM/AAAA): [OBLIGATORIO]',
      LFechaFin) then
    begin
      // El usuario pulsó el botón Cancelar: cancelar todo sin realizar cambios
      Exit;
    end;

    if not ConvertirFechaASql(LFechaFin, LFechaFinSQL) then
    begin
      ShowMessage('Operación cancelada: La fecha de finalización de contrato introducida (' + LFechaFin + ') no es válida.'#13#10 +
                  'Por favor, introduzca una fecha en formato DD/MM/AAAA (ej. ' + FormatDateTime('dd/mm/yyyy', Date) + ').'#13#10 +
                  'El empleado continúa en estado ACTIVO.');
      Exit;
    end;

    LMotivo := 'Fin de Contrato';
    if not InputQuery('Motivo de Finalización de Contrato',
      'Indique el motivo del cese / baja laboral:',
      LMotivo) then
    begin
      // Si cancela en el motivo, abortar también
      Exit;
    end;

    if Trim(LMotivo) = '' then
      LMotivo := 'Fin de Contrato';

    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;

      // 1. Actualizar contrato activo en ge_trabajadores_contratos
      LQry.SQL.Text :=
        'UPDATE ge_trabajadores_contratos ' +
        'SET fecha_baja = :baja, fecha_fin_contrato = :fin, motivo_baja = :motivo, activo = 0, ' +
        '    id_user_update = :user, updated_at = CURRENT_TIMESTAMP ' +
        'WHERE id_trabajador = :trab AND id_empresa = :emp AND activo = 1';
      LQry.ParamByName('baja').AsString := LFechaFinSQL;
      LQry.ParamByName('fin').AsString := LFechaFinSQL;
      LQry.ParamByName('motivo').AsString := LMotivo;
      LQry.ParamByName('user').AsString := dmgMain.CurrentUserId;
      LQry.ParamByName('trab').AsString := LId;
      LQry.ParamByName('emp').AsInteger := LEmpId;
      LQry.ExecSQL;

      // Si no existía contrato previo en ge_trabajadores_contratos, insertar registro de cierre
      if LQry.RowsAffected = 0 then
      begin
        LQry.SQL.Text :=
          'INSERT INTO ge_trabajadores_contratos ' +
          '  (id_empresa, id_trabajador, fecha_alta, fecha_baja, fecha_fin_contrato, tipo_contrato, motivo_alta, motivo_baja, activo, id_user_creator, id_user_update) ' +
          'VALUES ' +
          '  (:emp, :trab, COALESCE((SELECT fecha_alta FROM ge_trabajadores_empresas WHERE id_trabajador = :trab2 AND id_empresa = :emp2), :baja), :baja, :fin, ''General'', ''Alta anterior'', :motivo, 0, :user, :user)';
        LQry.ParamByName('emp').AsInteger := LEmpId;
        LQry.ParamByName('trab').AsString := LId;
        LQry.ParamByName('trab2').AsString := LId;
        LQry.ParamByName('emp2').AsInteger := LEmpId;
        LQry.ParamByName('baja').AsString := LFechaFinSQL;
        LQry.ParamByName('fin').AsString := LFechaFinSQL;
        LQry.ParamByName('motivo').AsString := LMotivo;
        LQry.ParamByName('user').AsString := dmgMain.CurrentUserId;
        LQry.ExecSQL;
      end;

      // 2. Actualizar en ge_trabajadores_empresas
      LQry.SQL.Text :=
        'UPDATE ge_trabajadores_empresas ' +
        'SET fecha_baja = :baja, activo = 0, id_user_update = :user, updated_at = CURRENT_TIMESTAMP ' +
        'WHERE id_trabajador = :trab AND id_empresa = :emp';
      LQry.ParamByName('baja').AsString := LFechaFinSQL;
      LQry.ParamByName('user').AsString := dmgMain.CurrentUserId;
      LQry.ParamByName('trab').AsString := LId;
      LQry.ParamByName('emp').AsInteger := LEmpId;
      LQry.ExecSQL;

      // 3. Actualizar en ge_trabajadores
      LQry.SQL.Text :=
        'UPDATE ge_trabajadores ' +
        'SET fecha_baja = :baja, activo = 0, id_user_update = :user, updated_at = CURRENT_TIMESTAMP ' +
        'WHERE id = :trab';
      LQry.ParamByName('baja').AsString := LFechaFinSQL;
      LQry.ParamByName('user').AsString := dmgMain.CurrentUserId;
      LQry.ParamByName('trab').AsString := LId;
      LQry.ExecSQL;

      ShowMessage('El empleado "' + LNombre + '" ha sido dado de baja correctamente con fecha ' + LFechaFin + '.');
      btnRefreshClick(nil);
      if LId <> '' then
        FQryMain.Locate('id', LId, []);
    finally
      LQry.Free;
    end;
  end
  else
  begin
    // El empleado está INACTIVO y se solicita activar con un nuevo periodo de contratación
    if MessageDlg('El trabajador "' + LNombre + '" está actualmente INACTIVO.'#13#10#13#10 +
                  '¿Desea registrar un nuevo periodo de contratación para ponerlo en ACTIVO?',
                  mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
      Exit;

    LFechaAlta := FormatDateTime('dd/mm/yyyy', Date);
    if not InputQuery('Nuevo Periodo de Contratación',
      'Fecha de Alta / Inicio de Periodo (DD/MM/AAAA): [OBLIGATORIO]',
      LFechaAlta) then
    begin
      // El usuario pulsó Cancelar -> no hacer nada
      Exit;
    end;

    if not ConvertirFechaASql(LFechaAlta, LFechaAltaSQL) then
    begin
      ShowMessage('Operación cancelada: La fecha de inicio introducida (' + LFechaAlta + ') no es válida.'#13#10 +
                  'Por favor, introduzca una fecha en formato DD/MM/AAAA (ej. ' + FormatDateTime('dd/mm/yyyy', Date) + ').'#13#10 +
                  'El empleado permanece INACTIVO.');
      Exit;
    end;

    LTipoContrato := 'Fijo Discontinuo';
    if not InputQuery('Tipo de Contrato',
      'Indique el tipo de contrato:',
      LTipoContrato) then
    begin
      Exit;
    end;

    if Trim(LTipoContrato) = '' then
      LTipoContrato := 'Fijo Discontinuo';

    LMotivo := 'Campaña / Nueva Incorporación';
    if not InputQuery('Motivo de Contratación',
      'Indique el motivo de la incorporación:',
      LMotivo) then
    begin
      Exit;
    end;

    if Trim(LMotivo) = '' then
      LMotivo := 'Campaña / Nueva Incorporación';

    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;

      // 1. Insertar nuevo contrato activo en ge_trabajadores_contratos
      LQry.SQL.Text :=
        'INSERT INTO ge_trabajadores_contratos ' +
        '  (id_empresa, id_trabajador, fecha_alta, tipo_contrato, motivo_alta, activo, id_user_creator, id_user_update) ' +
        'VALUES ' +
        '  (:emp, :trab, :alta, :tipo, :motivo, 1, :user, :user)';
      LQry.ParamByName('emp').AsInteger := LEmpId;
      LQry.ParamByName('trab').AsString := LId;
      LQry.ParamByName('alta').AsString := LFechaAltaSQL;
      LQry.ParamByName('tipo').AsString := LTipoContrato;
      LQry.ParamByName('motivo').AsString := LMotivo;
      LQry.ParamByName('user').AsString := dmgMain.CurrentUserId;
      LQry.ExecSQL;

      // 2. Sincronizar en ge_trabajadores_empresas a ACTIVO con fecha_baja = NULL
      LQry.SQL.Text :=
        'UPDATE ge_trabajadores_empresas ' +
        'SET fecha_alta = :alta, fecha_baja = NULL, activo = 1, id_user_update = :user, updated_at = CURRENT_TIMESTAMP ' +
        'WHERE id_trabajador = :trab AND id_empresa = :emp';
      LQry.ParamByName('alta').AsString := LFechaAltaSQL;
      LQry.ParamByName('user').AsString := dmgMain.CurrentUserId;
      LQry.ParamByName('trab').AsString := LId;
      LQry.ParamByName('emp').AsInteger := LEmpId;
      LQry.ExecSQL;

      // 3. Sincronizar en ge_trabajadores a ACTIVO con fecha_baja = NULL
      LQry.SQL.Text :=
        'UPDATE ge_trabajadores ' +
        'SET fecha_alta = :alta, fecha_baja = NULL, activo = 1, id_user_update = :user, updated_at = CURRENT_TIMESTAMP ' +
        'WHERE id = :trab';
      LQry.ParamByName('alta').AsString := LFechaAltaSQL;
      LQry.ParamByName('user').AsString := dmgMain.CurrentUserId;
      LQry.ParamByName('trab').AsString := LId;
      LQry.ExecSQL;

      ShowMessage('Se ha registrado el nuevo periodo de contratación con fecha ' + LFechaAlta + '. El empleado "' + LNombre + '" está ahora ACTIVO.');
      btnRefreshClick(nil);
      if LId <> '' then
        FQryMain.Locate('id', LId, []);
    finally
      LQry.Free;
    end;
  end;
end;

procedure TfrmTrabajadores.btnNewClick(Sender: TObject);
var
  LEditor: TfrmTrabajadoresEditor;
begin
  LEditor := TfrmTrabajadoresEditor.Create(Self);
  try
    LEditor.TrabajadorId := '';
    if LEditor.ShowModal = mrOk then
      btnRefreshClick(nil);
  finally
    LEditor.Free;
  end;
end;

procedure TfrmTrabajadores.btnEditClick(Sender: TObject);
var
  LEditor: TfrmTrabajadoresEditor;
begin
  if FQryMain.IsEmpty then Exit;

  LEditor := TfrmTrabajadoresEditor.Create(Self);
  try
    LEditor.TrabajadorId := FQryMain.FieldByName('id').AsString;
    if LEditor.ShowModal = mrOk then
      btnRefreshClick(nil);
  finally
    LEditor.Free;
  end;
end;

procedure TfrmTrabajadores.btnDeleteClick(Sender: TObject);
var
  LQry: TFDQuery;
  LId: string;
begin
  if FQryMain.IsEmpty then Exit;
  LId := FQryMain.FieldByName('id').AsString;

  if MessageDlg('¿Está seguro de que desea eliminar este trabajador de la base de datos?',
                mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;
      LQry.SQL.Text := 'DELETE FROM ge_trabajadores WHERE id = :id';
      LQry.ParamByName('id').AsString := LId;
      LQry.ExecSQL;
      btnRefreshClick(nil);
    except
      on E: Exception do
        TDbErrorHandler.HandleException(E, 'Eliminar Trabajador');
    end;
    LQry.Free;
  end;
end;

procedure TfrmTrabajadores.ConfigurarBotonOperaciones;
var
  LItem: TMenuItem;
begin
  if not Assigned(pmOperaciones) then
  begin
    pmOperaciones := TPopupMenu.Create(Self);

    LItem := TMenuItem.Create(pmOperaciones);
    LItem.Caption := '🪪 Imprimir Tarjeta de Empleado (TARJETA2)...';
    LItem.OnClick := miImprimirTarjetaClick;
    pmOperaciones.Items.Add(LItem);

    LItem := TMenuItem.Create(pmOperaciones);
    LItem.Caption := '-';
    pmOperaciones.Items.Add(LItem);

    LItem := TMenuItem.Create(pmOperaciones);
    LItem.Caption := '☑ Seleccionar Todos';
    LItem.OnClick := miSeleccionarTodosClick;
    pmOperaciones.Items.Add(LItem);

    LItem := TMenuItem.Create(pmOperaciones);
    LItem.Caption := '☐ Deseleccionar Todos';
    LItem.OnClick := miDeseleccionarTodosClick;
    pmOperaciones.Items.Add(LItem);

    LItem := TMenuItem.Create(pmOperaciones);
    LItem.Caption := '🔄 Invertir Selección';
    LItem.OnClick := miInvertirSeleccionClick;
    pmOperaciones.Items.Add(LItem);
  end;

  if not Assigned(btnOperaciones) then
  begin
    btnOperaciones := TSpeedButton.Create(Self);
    btnOperaciones.Parent := pnlActions;
    btnOperaciones.Align := alLeft;
    btnOperaciones.Left := btnPrint.Left + btnPrint.Width + 5;
    btnOperaciones.Width := 85;
    btnOperaciones.Height := btnPrint.Height;
    btnOperaciones.Caption := 'Operaciones';
    btnOperaciones.Flat := True;
    btnOperaciones.Layout := blGlyphTop;
    btnOperaciones.Images := dmgMain.imgListRibbon;
    btnOperaciones.ImageIndex := 140;
    btnOperaciones.Font.Name := 'Segoe UI';
    btnOperaciones.Font.Size := 11;
    btnOperaciones.Font.Style := [fsBold];
    btnOperaciones.ParentFont := False;
    btnOperaciones.OnClick := btnOperacionesClick;
  end;
end;

procedure TfrmTrabajadores.btnOperacionesClick(Sender: TObject);
var
  LPoint: TPoint;
begin
  if not Assigned(pmOperaciones) then Exit;
  LPoint := btnOperaciones.ClientToScreen(Point(0, btnOperaciones.Height));
  pmOperaciones.Popup(LPoint.X, LPoint.Y);
end;

procedure TfrmTrabajadores.miImprimirTarjetaClick(Sender: TObject);
var
  LIds: TArray<string>;
begin
  LIds := ObtenerTrabajadoresSeleccionados;
  if Length(LIds) = 0 then
  begin
    ShowMessage('No hay ningún trabajador seleccionado para imprimir la tarjeta.');
    Exit;
  end;

  ImprimirTarjetas(LIds);
end;

procedure TfrmTrabajadores.miSeleccionarTodosClick(Sender: TObject);
begin
  SeleccionarTodos;
end;

procedure TfrmTrabajadores.miDeseleccionarTodosClick(Sender: TObject);
begin
  DeseleccionarTodos;
end;

procedure TfrmTrabajadores.miInvertirSeleccionClick(Sender: TObject);
begin
  InvertirSeleccion;
end;

procedure TfrmTrabajadores.SeleccionarTodos;
var
  LBookmark: TBookmark;
begin
  if not Assigned(FQryMain) or not FQryMain.Active or FQryMain.IsEmpty then Exit;

  FQryMain.DisableControls;
  LBookmark := FQryMain.GetBookmark;
  try
    dbGridMain.SelectedRows.Clear;
    FQryMain.First;
    while not FQryMain.Eof do
    begin
      dbGridMain.SelectedRows.CurrentRowSelected := True;
      FQryMain.Next;
    end;
  finally
    if FQryMain.BookmarkValid(LBookmark) then
      FQryMain.GotoBookmark(LBookmark);
    FQryMain.FreeBookmark(LBookmark);
    FQryMain.EnableControls;
  end;

  ActualizarContadorSeleccion;
  dbGridMain.Invalidate;
end;

procedure TfrmTrabajadores.DeseleccionarTodos;
begin
  dbGridMain.SelectedRows.Clear;
  ActualizarContadorSeleccion;
  dbGridMain.Invalidate;
end;

procedure TfrmTrabajadores.InvertirSeleccion;
var
  LBookmark: TBookmark;
begin
  if not Assigned(FQryMain) or not FQryMain.Active or FQryMain.IsEmpty then Exit;

  FQryMain.DisableControls;
  LBookmark := FQryMain.GetBookmark;
  try
    FQryMain.First;
    while not FQryMain.Eof do
    begin
      dbGridMain.SelectedRows.CurrentRowSelected := not dbGridMain.SelectedRows.CurrentRowSelected;
      FQryMain.Next;
    end;
  finally
    if FQryMain.BookmarkValid(LBookmark) then
      FQryMain.GotoBookmark(LBookmark);
    FQryMain.FreeBookmark(LBookmark);
    FQryMain.EnableControls;
  end;

  ActualizarContadorSeleccion;
  dbGridMain.Invalidate;
end;

procedure TfrmTrabajadores.ActualizarContadorSeleccion;
var
  LCount: Integer;
begin
  LCount := dbGridMain.SelectedRows.Count;
  if Assigned(btnOperaciones) then
  begin
    if LCount > 1 then
      btnOperaciones.Caption := Format('Operaciones (%d)', [LCount])
    else
      btnOperaciones.Caption := 'Operaciones';
  end;

  if Assigned(lblRecordCount) and Assigned(FQryMain) and FQryMain.Active then
  begin
    if LCount > 1 then
      lblRecordCount.Caption := Format('Registros: %d (%d seleccionados)', [FQryMain.RecordCount, LCount])
    else
      lblRecordCount.Caption := Format('Registros: %d', [FQryMain.RecordCount]);
  end;
end;

function TfrmTrabajadores.ObtenerTrabajadoresSeleccionados: TArray<string>;
var
  LList: TList<string>;
  LBookmark: TBookmark;
  I: Integer;
begin
  SetLength(Result, 0);
  if not Assigned(FQryMain) or not FQryMain.Active or FQryMain.IsEmpty then
    Exit;

  LList := TList<string>.Create;
  try
    if dbGridMain.SelectedRows.Count > 0 then
    begin
      FQryMain.DisableControls;
      LBookmark := FQryMain.GetBookmark;
      try
        for I := 0 to dbGridMain.SelectedRows.Count - 1 do
        begin
          FQryMain.GotoBookmark(dbGridMain.SelectedRows[I]);
          if (FQryMain.FindField('id') <> nil) and (FQryMain.FieldByName('id').AsString <> '') then
            LList.Add(FQryMain.FieldByName('id').AsString);
        end;
      finally
        if FQryMain.BookmarkValid(LBookmark) then
          FQryMain.GotoBookmark(LBookmark);
        FQryMain.FreeBookmark(LBookmark);
        FQryMain.EnableControls;
      end;
    end
    else
    begin
      if (FQryMain.FindField('id') <> nil) and (FQryMain.FieldByName('id').AsString <> '') then
        LList.Add(FQryMain.FieldByName('id').AsString);
    end;

    Result := LList.ToArray;
  finally
    LList.Free;
  end;
end;

procedure TfrmTrabajadores.ImprimirTarjetas(const AIds: TArray<string>);
var
  LReportPath: string;
  LSql: string;
  LInClause: string;
  LEmpresaId: string;
  I: Integer;
  LFrxInformedb: TfrxDBDataset;
  LFrxRTarjeta: TfrxDBDataset;
  LMaster: TfrxMasterData;
  LBarcode: TfrxBarCodeView;
  LImgLogo, LImgTrabajador, LQR: TfrxPictureView;
begin
  if Length(AIds) = 0 then Exit;

  // 1. Localizar la plantilla TARJETA2.fr3
  LReportPath := ExtractFilePath(ParamStr(0)) + 'reportes\TARJETA2.fr3';
  if not FileExists(LReportPath) then
    LReportPath := ExtractFilePath(ParamStr(0)) + '..\reportes\TARJETA2.fr3';
  if not FileExists(LReportPath) then
    LReportPath := 'c:\fuentes\proyectos delphi\RAUL_ASENCIO\GESTION\reportes\TARJETA2.fr3';
  if not FileExists(LReportPath) then
    LReportPath := 'reportes\TARJETA2.fr3';

  if not FileExists(LReportPath) then
  begin
    MessageDlg('No se ha encontrado el archivo de reporte:' + sLineBreak +
      'TARJETA2.fr3' + sLineBreak +
      'Verifique que exista en la carpeta reportes.', mtError, [mbOK], 0);
    Exit;
  end;

  // 2. Determinar empresa para el código de trabajador
  LEmpresaId := dmgMain.CurrentCompanyId;
  if Assigned(cbEmpresaFilter) and Assigned(FEmpresaIds) then
  begin
    if (cbEmpresaFilter.ItemIndex >= 2) and (cbEmpresaFilter.ItemIndex < FEmpresaIds.Count) then
      LEmpresaId := FEmpresaIds[cbEmpresaFilter.ItemIndex];
  end;

  // 3. Montar cláusula IN
  LInClause := '';
  for I := 0 to High(AIds) do
  begin
    if LInClause <> '' then LInClause := LInClause + ', ';
    LInClause := LInClause + QuotedStr(AIds[I]);
  end;

  LSql := 'SELECT ' +
          '  t.id, ' +
          '  COALESCE(te.codigo, t.codigo, t.id) AS ID, ' +
          '  CASE ' +
          '    WHEN TRIM(COALESCE(t.nif, '''')) <> '''' THEN TRIM(t.nif) ' +
          '    ELSE CAST(t.id AS CHAR) ' +
          '  END AS NIF, ' +
          '  CASE ' +
          '    WHEN (t.apellidos IS NULL OR TRIM(t.apellidos) = '''' OR t.nombre LIKE CONCAT(''%'', TRIM(t.apellidos))) THEN t.nombre ' +
          '    ELSE TRIM(CONCAT(t.nombre, '' '', t.apellidos)) ' +
          '  END AS nombre, ' +
          '  IF(t.num_taquilla IS NULL OR t.num_taquilla = 0, '''', CAST(t.num_taquilla AS CHAR)) AS num_taquilla, ' +
          '  doc.archivo AS imagen_1, ' +
          '  emp.logo AS logo_empresa ' +
          'FROM ge_trabajadores t ' +
          'LEFT JOIN ge_trabajadores_empresas te ON te.id = (' +
          '  SELECT te_sub.id FROM ge_trabajadores_empresas te_sub ' +
          '  WHERE te_sub.id_trabajador = t.id ' +
          '  ORDER BY (te_sub.id_empresa = :empresa_id) DESC, te_sub.activo DESC, te_sub.id DESC ' +
          '  LIMIT 1' +
          ') ' +
          'LEFT JOIN ge_empresas emp ON emp.id = te.id_empresa ' +
          'LEFT JOIN ( ' +
          '  SELECT id_trabajador, MAX(id) AS max_id ' +
          '  FROM ge_documentos ' +
          '  WHERE descripcion = ''FOTO_TRABAJADOR'' ' +
          '  GROUP BY id_trabajador ' +
          ') d_max ON d_max.id_trabajador = t.id ' +
          'LEFT JOIN ge_documentos doc ON doc.id = d_max.max_id ' +
          'WHERE t.id IN (' + LInClause + ') ' +
          'GROUP BY t.id ' +
          'ORDER BY t.apellidos ASC, t.nombre ASC';

  FQryReportTarjetas := TFDQuery.Create(nil);
  LFrxInformedb := TfrxDBDataset.Create(nil);
  LFrxRTarjeta := TfrxDBDataset.Create(nil);
  try
    FQryReportTarjetas.Connection := dmgMain.dbConn;
    FQryReportTarjetas.SQL.Text := LSql;
    FQryReportTarjetas.ParamByName('empresa_id').AsString := LEmpresaId;
    FQryReportTarjetas.Open;

    if FQryReportTarjetas.IsEmpty then
    begin
      ShowMessage('No se han encontrado datos para los trabajadores seleccionados.');
      Exit;
    end;

    LFrxInformedb.Name := 'informedb';
    LFrxInformedb.UserName := 'frxDBDataset1';
    LFrxInformedb.DataSet := FQryReportTarjetas;

    LFrxRTarjeta.Name := 'frxDSRTarjeta';
    LFrxRTarjeta.UserName := 'rtarjeta';
    LFrxRTarjeta.DataSet := FQryReportTarjetas;

    dmgMain.frxInforme.Clear;
    dmgMain.frxInforme.LoadFromFile(LReportPath);

    dmgMain.frxInforme.DataSets.Clear;
    dmgMain.frxInforme.DataSets.Add(LFrxInformedb);
    dmgMain.frxInforme.DataSets.Add(LFrxRTarjeta);

    dmgMain.frxInforme.EnabledDataSets.Clear;
    dmgMain.frxInforme.EnabledDataSets.Add(LFrxInformedb);
    dmgMain.frxInforme.EnabledDataSets.Add(LFrxRTarjeta);

    LMaster := dmgMain.frxInforme.FindObject('MasterData1') as TfrxMasterData;
    if Assigned(LMaster) then
      LMaster.DataSet := LFrxInformedb;

    LBarcode := dmgMain.frxInforme.FindObject('BarCode1') as TfrxBarCodeView;
    if Assigned(LBarcode) then
    begin
      LBarcode.DataSet := LFrxInformedb;
      LBarcode.DataField := 'NIF';
      LBarcode.Expression := '<frxDBDataset1."NIF">';
    end;

    // Configurar componentes de imagen (Logo, Foto de Trabajador, Código QR)
    LImgLogo := dmgMain.frxInforme.FindObject('ImgLogo') as TfrxPictureView;
    if Assigned(LImgLogo) then
    begin
      LImgLogo.DataSet := nil;
      LImgLogo.DataField := '';
      LImgLogo.KeepAspectRatio := True;
      LImgLogo.Center := True;
    end;

    LImgTrabajador := dmgMain.frxInforme.FindObject('ImgTrabajador') as TfrxPictureView;
    if not Assigned(LImgTrabajador) then
      LImgTrabajador := dmgMain.frxInforme.FindObject('Picture1') as TfrxPictureView;
    if Assigned(LImgTrabajador) then
    begin
      LImgTrabajador.DataSet := nil;
      LImgTrabajador.DataField := '';
      LImgTrabajador.KeepAspectRatio := True;
      LImgTrabajador.Center := True;
    end;

    LQR := dmgMain.frxInforme.FindObject('QR') as TfrxPictureView;
    if Assigned(LQR) then
    begin
      LQR.DataSet := nil;
      LQR.DataField := '';
      LQR.KeepAspectRatio := True;
      LQR.Center := True;
    end;

    // Enganchar el evento OnBeforePrint para cargar dinámicamente Logo, Foto y QR por cada trabajador
    dmgMain.frxInforme.OnBeforePrint := frxInformeBeforePrint;

    // Limpiar páginas previas y regenerar informe con los datos de los trabajadores seleccionados
    dmgMain.frxInforme.PreviewPages.Clear;
    dmgMain.frxInforme.PrepareReport(True);

    TfrmPrintOptions.ShowOptions(
      Self,
      dmgMain.frxInforme,
      dbGridMain,
      'Tarjetas_Trabajadores',
      '', '',
      Length(AIds)
    );
  finally
    dmgMain.frxInforme.OnBeforePrint := nil;
    LFrxRTarjeta.Free;
    LFrxInformedb.Free;
    FreeAndNil(FQryReportTarjetas);
  end;
end;

end.
