unit frm_TrabajadoresFichajes;

interface

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

type
  TModoCalendarioView = (mcvMes, mcvSemana);

  TEstadoDiaFichaje = (
    edfSinDatos,
    edfDescanso,
    edfCompleto,
    edfSuperavit,
    edfDeficit,
    edfFalta,
    edfVacaciones,
    edfBajaMedica,
    edfAusencia,
    edfFestivo
  );

  TTramoFichajeResumen = record
    HoraInicio: string;
    HoraFin: string;
    Horas: Double;
    Centro: string;
  end;

  TDiaFichaje = record
    Fecha: TDate;
    NumeroDia: Integer;
    EsMesActual: Boolean;
    EsHoy: Boolean;
    EsSeleccionado: Boolean;
    EsLaboralPactado: Boolean;
    HorasPactadas: Double;
    HorasFichadas: Double;
    DiferenciaHoras: Double;
    TieneAusencia: Boolean;
    TipoAusencia: string;
    ObservacionAusencia: string;
    TieneFestivo: Boolean;
    DescripcionFestivo: string;
    Tramos: TArray<TTramoFichajeResumen>;
    Estado: TEstadoDiaFichaje;
  end;
  PDiaFichaje = ^TDiaFichaje;

  TfrmTrabajadoresFichajes = 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
    // Filtros existentes en panel izquierdo
    grpZona: TGroupBox;
    cbZonaFilter: TComboBox;
    grpGrupo: TGroupBox;
    cbGrupoFilter: TComboBox;
    grpTrabajador: TGroupBox;
    cbTrabajadorFilter: TComboBox;
    FZonaNames: TStringList;
    FGrupoIds: TStringList;
    FTrabajadorIds: TStringList;

    // Selector de modo de vista (Listado / Mes / Semana)
    pnlViewSelector: TPanel;
    btnVistaListado: TSpeedButton;
    btnVistaMes: TSpeedButton;
    btnVistaSemana: TSpeedButton;

    // Contenedor visual del calendario
    pnlCalendarioContainer: TPanel;
    pnlCalendarioNav: TPanel;
    btnPeriodoAnterior: TSpeedButton;
    btnPeriodoHoy: TSpeedButton;
    btnPeriodoSiguiente: TSpeedButton;
    lblPeriodoTitulo: TLabel;
    lblCalendarioTrabajador: TLabel;
    btnCalAñadirFalta: TSpeedButton;

    pnlLeyendaFichajes: TPanel;
    pnlDiasHeader: TPanel;
    FLblDiasHeader: array[0..6] of TLabel;
    gridCalendarioFichajes: TDrawGrid;

    pmCalendario: TPopupMenu;
    miVerDetalle: TMenuItem;
    miRegistrarFalta: TMenuItem;
    miEliminarFalta: TMenuItem;

    // Estado del Calendario
    FModoCalendarioFichajes: TModoCalendarioView;
    FFechaCalendarioBase: TDate;
    FSelectedFichajeFecha: TDate;
    FMatrizFichajes: array[0..41] of TDiaFichaje;
    FMatrizSemana: array[0..6] of TDiaFichaje;
    FDiaLaboralPactado: array[1..7] of Boolean;
    FHorasPactadasDia: array[1..7] of Double;

    procedure ConfigurarFiltrosAdicionales;
    procedure ConfigurarVistasYCalendario;
    procedure CargarZonas;
    procedure CargarGrupos;
    procedure CargarTrabajadores(AIdGrupo: Integer = 0);
    procedure FilterComboChange(Sender: TObject);
    procedure GrupoFilterChange(Sender: TObject);
    procedure cbTrabajadorFilterDropDown(Sender: TObject);
    procedure dbGridMainCellClick(Column: TColumn);

    procedure btnVistaClick(Sender: TObject);
    procedure btnNavegacionPeriodoClick(Sender: TObject);
    procedure btnCalAñadirFaltaClick(Sender: TObject);
    procedure pmCalendarioPopup(Sender: TObject);
    procedure miVerDetalleClick(Sender: TObject);
    procedure miRegistrarFaltaClick(Sender: TObject);
    procedure miEliminarFaltaClick(Sender: TObject);

    procedure CargarHorarioPactadoBase(const ATrabajadorId: string);
    procedure CargarCalendarioFichajes;
    procedure AjustarCabeceraDiasFichajes;
    procedure gridCalendarioFichajesDrawCell(Sender: TObject; ACol, ARow: Longint; Rect: TRect; State: TGridDrawState);
    procedure gridCalendarioFichajesClick(Sender: TObject);
    procedure gridCalendarioFichajesDblClick(Sender: TObject);
    function ObtenerFechaDeCeldaFichajes(ACol, ARow: Integer): TDate;
    procedure AbrirDetalleFichajesDia(AFecha: TDate);
    procedure RegistrarFaltaAsistencia(AFecha: TDate);
    procedure EliminarFaltaAsistencia(AFecha: TDate);
  protected
    procedure ApplyAllFilters; override;
    procedure Resize; override;
  public
    { Public declarations }
  end;

var
  frmTrabajadoresFichajes: TfrmTrabajadoresFichajes;

implementation

uses
  dmg_Main, uDbErrorHandler;

{$R *.dfm}

const
  NOMBRES_MESES: array[1..12] of string = (
    'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
    'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'
  );
  NOMBRES_DIAS: array[0..6] of string = (
    'LUNES', 'MARTES', 'MIÉRCOLES', 'JUEVES', 'VIERNES', 'SÁBADO', 'DOMINGO'
  );

function ParseTimeMinutes(const ATimeStr: string): Integer;
var
  LParts: TArray<string>;
  H, M: Integer;
begin
  Result := 0;
  if Trim(ATimeStr) = '' then Exit;
  LParts := ATimeStr.Split([':']);
  if Length(LParts) >= 2 then
  begin
    H := StrToIntDef(LParts[0], 0);
    M := StrToIntDef(LParts[1], 0);
    Result := (H * 60) + M;
  end;
end;

procedure TfrmTrabajadoresFichajes.FormShow(Sender: TObject);
begin
  DateFilterField := 'fecha';
  AlphaFilterField := 'trabajador';
  StatusFilterField := 'validado';
  StatusValues.Clear;
  StatusValues.Add('Validado');
  StatusValues.Add('Pendiente');

  inherited;
  frmTrabajadoresFichajes := Self;
  OnDestroy := FormDestroy;
  pnlSearch.Visible := True;
  lblSearch.Caption := 'Buscar Fichaje:';

  FFechaCalendarioBase := Date;
  FModoCalendarioFichajes := mcvMes;

  ConfigurarFiltrosAdicionales;
  ConfigurarVistasYCalendario;

  dbGridMain.OnCellClick := dbGridMainCellClick;

  btnRefreshClick(nil);
end;

procedure TfrmTrabajadoresFichajes.FormDestroy(Sender: TObject);
begin
  if Assigned(FZonaNames) then
    FreeAndNil(FZonaNames);
  if Assigned(FGrupoIds) then
    FreeAndNil(FGrupoIds);
  if Assigned(FTrabajadorIds) then
    FreeAndNil(FTrabajadorIds);

  if frmTrabajadoresFichajes = Self then
    frmTrabajadoresFichajes := nil;
  inherited;
end;

procedure TfrmTrabajadoresFichajes.ConfigurarFiltrosAdicionales;
begin
  if not Assigned(FZonaNames) then
    FZonaNames := TStringList.Create;
  if not Assigned(FGrupoIds) then
    FGrupoIds := TStringList.Create;
  if not Assigned(FTrabajadorIds) then
    FTrabajadorIds := TStringList.Create;

  // 1. Grupo Zona / Centro de fichaje
  if not Assigned(grpZona) then
  begin
    grpZona := TGroupBox.Create(Self);
    grpZona.Parent := pnlLeftFilter;
    grpZona.Align := alTop;
    grpZona.Caption := 'Zona de Fichaje';
    grpZona.Height := 50;
    grpZona.Tag := 999;
    grpZona.Padding.Left := 5;
    grpZona.Padding.Right := 5;
    grpZona.Padding.Top := 3;
    grpZona.Padding.Bottom := 5;

    cbZonaFilter := TComboBox.Create(Self);
    cbZonaFilter.Parent := grpZona;
    cbZonaFilter.Align := alClient;
    cbZonaFilter.Style := csDropDownList;
    cbZonaFilter.Tag := 999;
    cbZonaFilter.OnChange := FilterComboChange;
  end;

  // 2. Grupo de Trabajo
  if not Assigned(grpGrupo) then
  begin
    grpGrupo := TGroupBox.Create(Self);
    grpGrupo.Parent := pnlLeftFilter;
    grpGrupo.Align := alTop;
    grpGrupo.Caption := 'Grupo';
    grpGrupo.Height := 50;
    grpGrupo.Tag := 999;
    grpGrupo.Padding.Left := 5;
    grpGrupo.Padding.Right := 5;
    grpGrupo.Padding.Top := 3;
    grpGrupo.Padding.Bottom := 5;

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

  // 3. Trabajador en particular
  if not Assigned(grpTrabajador) then
  begin
    grpTrabajador := TGroupBox.Create(Self);
    grpTrabajador.Parent := pnlLeftFilter;
    grpTrabajador.Align := alTop;
    grpTrabajador.Caption := 'Trabajador';
    grpTrabajador.Height := 50;
    grpTrabajador.Tag := 999;
    grpTrabajador.Padding.Left := 5;
    grpTrabajador.Padding.Right := 5;
    grpTrabajador.Padding.Top := 3;
    grpTrabajador.Padding.Bottom := 5;

    cbTrabajadorFilter := TComboBox.Create(Self);
    cbTrabajadorFilter.Parent := grpTrabajador;
    cbTrabajadorFilter.Align := alClient;
    cbTrabajadorFilter.Style := csDropDownList;
    cbTrabajadorFilter.Tag := 999;
    cbTrabajadorFilter.OnDropDown := cbTrabajadorFilterDropDown;
    cbTrabajadorFilter.OnChange := FilterComboChange;
  end;

  // Orden visual vertical (alTop)
  grpStatuses.Top := 0;
  grpZona.Top := 55;
  grpGrupo.Top := 110;
  grpTrabajador.Top := 165;
  grpDates.Top := 220;

  CargarZonas;
  CargarGrupos;
  CargarTrabajadores(0);
end;

procedure TfrmTrabajadoresFichajes.ConfigurarVistasYCalendario;
var
  I: Integer;
  LLblLey: TLabel;
begin
  // A. Panel selector de vistas en la barra de búsqueda superior
  if not Assigned(pnlViewSelector) then
  begin
    pnlViewSelector := TPanel.Create(Self);
    pnlViewSelector.Parent := pnlSearch;
    pnlViewSelector.Align := alRight;
    pnlViewSelector.Width := 275;
    pnlViewSelector.BevelOuter := bvNone;
    pnlViewSelector.Tag := 999;

    btnVistaListado := TSpeedButton.Create(Self);
    btnVistaListado.Parent := pnlViewSelector;
    btnVistaListado.Left := 0;
    btnVistaListado.Top := 0;
    btnVistaListado.Width := 85;
    btnVistaListado.Height := pnlViewSelector.Height;
    btnVistaListado.Align := alLeft;
    btnVistaListado.Caption := '📋 Listado';
    btnVistaListado.GroupIndex := 20;
    btnVistaListado.Down := True;
    btnVistaListado.Flat := True;
    btnVistaListado.Font.Style := [fsBold];
    btnVistaListado.Tag := 999;
    btnVistaListado.OnClick := btnVistaClick;

    btnVistaMes := TSpeedButton.Create(Self);
    btnVistaMes.Parent := pnlViewSelector;
    btnVistaMes.Left := 88;
    btnVistaMes.Top := 0;
    btnVistaMes.Width := 92;
    btnVistaMes.Height := pnlViewSelector.Height;
    btnVistaMes.Align := alLeft;
    btnVistaMes.Caption := '📅 Calendario';
    btnVistaMes.GroupIndex := 20;
    btnVistaMes.Flat := True;
    btnVistaMes.Tag := 999;
    btnVistaMes.OnClick := btnVistaClick;

    btnVistaSemana := TSpeedButton.Create(Self);
    btnVistaSemana.Parent := pnlViewSelector;
    btnVistaSemana.Left := 183;
    btnVistaSemana.Top := 0;
    btnVistaSemana.Width := 88;
    btnVistaSemana.Height := pnlViewSelector.Height;
    btnVistaSemana.Align := alLeft;
    btnVistaSemana.Caption := '🗓️ Semanal';
    btnVistaSemana.GroupIndex := 20;
    btnVistaSemana.Flat := True;
    btnVistaSemana.Tag := 999;
    btnVistaSemana.OnClick := btnVistaClick;
  end;

  // B. Contenedor del Calendario
  if not Assigned(pnlCalendarioContainer) then
  begin
    pnlCalendarioContainer := TPanel.Create(Self);
    pnlCalendarioContainer.Parent := pnlMain;
    pnlCalendarioContainer.Align := alClient;
    pnlCalendarioContainer.BevelOuter := bvNone;
    pnlCalendarioContainer.Visible := False;
    pnlCalendarioContainer.Tag := 999;

    // 1. Barra de navegación del periodo
    pnlCalendarioNav := TPanel.Create(Self);
    pnlCalendarioNav.Parent := pnlCalendarioContainer;
    pnlCalendarioNav.Align := alTop;
    pnlCalendarioNav.Height := 40;
    pnlCalendarioNav.BevelOuter := bvNone;
    pnlCalendarioNav.Color := $00F8F9FA;
    pnlCalendarioNav.ParentBackground := False;
    pnlCalendarioNav.Tag := 999;

    btnPeriodoAnterior := TSpeedButton.Create(Self);
    btnPeriodoAnterior.Parent := pnlCalendarioNav;
    btnPeriodoAnterior.Left := 10;
    btnPeriodoAnterior.Top := 6;
    btnPeriodoAnterior.Width := 75;
    btnPeriodoAnterior.Height := 28;
    btnPeriodoAnterior.Caption := '◄ Anterior';
    btnPeriodoAnterior.Flat := True;
    btnPeriodoAnterior.Tag := 1;
    btnPeriodoAnterior.OnClick := btnNavegacionPeriodoClick;

    btnPeriodoHoy := TSpeedButton.Create(Self);
    btnPeriodoHoy.Parent := pnlCalendarioNav;
    btnPeriodoHoy.Left := 90;
    btnPeriodoHoy.Top := 6;
    btnPeriodoHoy.Width := 50;
    btnPeriodoHoy.Height := 28;
    btnPeriodoHoy.Caption := 'Hoy';
    btnPeriodoHoy.Flat := True;
    btnPeriodoHoy.Tag := 2;
    btnPeriodoHoy.OnClick := btnNavegacionPeriodoClick;

    btnPeriodoSiguiente := TSpeedButton.Create(Self);
    btnPeriodoSiguiente.Parent := pnlCalendarioNav;
    btnPeriodoSiguiente.Left := 145;
    btnPeriodoSiguiente.Top := 6;
    btnPeriodoSiguiente.Width := 80;
    btnPeriodoSiguiente.Height := 28;
    btnPeriodoSiguiente.Caption := 'Siguiente ►';
    btnPeriodoSiguiente.Flat := True;
    btnPeriodoSiguiente.Tag := 3;
    btnPeriodoSiguiente.OnClick := btnNavegacionPeriodoClick;

    lblPeriodoTitulo := TLabel.Create(Self);
    lblPeriodoTitulo.Parent := pnlCalendarioNav;
    lblPeriodoTitulo.Left := 240;
    lblPeriodoTitulo.Top := 10;
    lblPeriodoTitulo.Caption := 'Periodo';
    lblPeriodoTitulo.Font.Name := 'Segoe UI';
    lblPeriodoTitulo.Font.Size := 12;
    lblPeriodoTitulo.Font.Style := [fsBold];
    lblPeriodoTitulo.Font.Color := $001A1A1A;
    lblPeriodoTitulo.Tag := 999;

    lblCalendarioTrabajador := TLabel.Create(Self);
    lblCalendarioTrabajador.Parent := pnlCalendarioNav;
    lblCalendarioTrabajador.Left := 460;
    lblCalendarioTrabajador.Top := 11;
    lblCalendarioTrabajador.Caption := 'Trabajador: (Seleccione uno)';
    lblCalendarioTrabajador.Font.Name := 'Segoe UI';
    lblCalendarioTrabajador.Font.Size := 10;
    lblCalendarioTrabajador.Font.Style := [fsBold];
    lblCalendarioTrabajador.Font.Color := clNavy;
    lblCalendarioTrabajador.Tag := 999;

    btnCalAñadirFalta := TSpeedButton.Create(Self);
    btnCalAñadirFalta.Parent := pnlCalendarioNav;
    btnCalAñadirFalta.Left := 760;
    btnCalAñadirFalta.Top := 6;
    btnCalAñadirFalta.Width := 180;
    btnCalAñadirFalta.Height := 28;
    btnCalAñadirFalta.Caption := '⚠️ Registrar Falta Día';
    btnCalAñadirFalta.Flat := True;
    btnCalAñadirFalta.Font.Style := [fsBold];
    btnCalAñadirFalta.Font.Color := $00721C24;
    btnCalAñadirFalta.Tag := 999;
    btnCalAñadirFalta.OnClick := btnCalAñadirFaltaClick;

    // 2. Barra de leyendas de colores
    pnlLeyendaFichajes := TPanel.Create(Self);
    pnlLeyendaFichajes.Parent := pnlCalendarioContainer;
    pnlLeyendaFichajes.Align := alTop;
    pnlLeyendaFichajes.Height := 28;
    pnlLeyendaFichajes.BevelOuter := bvNone;
    pnlLeyendaFichajes.Color := $00F5F6F8;
    pnlLeyendaFichajes.ParentBackground := False;
    pnlLeyendaFichajes.Tag := 999;

    // Etiquetas de leyenda
    LLblLey := TLabel.Create(Self);
    LLblLey.Parent := pnlLeyendaFichajes;
    LLblLey.Left := 10; LLblLey.Top := 6;
    LLblLey.Caption := '  Completo  ';
    LLblLey.Color := $00D4EDDA; LLblLey.Font.Color := $00155724;
    LLblLey.Font.Style := [fsBold]; LLblLey.Transparent := False; LLblLey.Tag := 999;

    LLblLey := TLabel.Create(Self);
    LLblLey.Parent := pnlLeyendaFichajes;
    LLblLey.Left := 90; LLblLey.Top := 6;
    LLblLey.Caption := '  Superávit  ';
    LLblLey.Color := $00CCE5FF; LLblLey.Font.Color := $00004085;
    LLblLey.Font.Style := [fsBold]; LLblLey.Transparent := False; LLblLey.Tag := 999;

    LLblLey := TLabel.Create(Self);
    LLblLey.Parent := pnlLeyendaFichajes;
    LLblLey.Left := 170; LLblLey.Top := 6;
    LLblLey.Caption := '  Déficit  ';
    LLblLey.Color := $00FFF3CD; LLblLey.Font.Color := $00856404;
    LLblLey.Font.Style := [fsBold]; LLblLey.Transparent := False; LLblLey.Tag := 999;

    LLblLey := TLabel.Create(Self);
    LLblLey.Parent := pnlLeyendaFichajes;
    LLblLey.Left := 240; LLblLey.Top := 6;
    LLblLey.Caption := '  Sin Fichar (FALTA)  ';
    LLblLey.Color := $00F8D7DA; LLblLey.Font.Color := $00721C24;
    LLblLey.Font.Style := [fsBold]; LLblLey.Transparent := False; LLblLey.Tag := 999;

    LLblLey := TLabel.Create(Self);
    LLblLey.Parent := pnlLeyendaFichajes;
    LLblLey.Left := 375; LLblLey.Top := 6;
    LLblLey.Caption := '  Vacaciones / Permiso  ';
    LLblLey.Color := $00E1BEE7; LLblLey.Font.Color := $004A148C;
    LLblLey.Font.Style := [fsBold]; LLblLey.Transparent := False; LLblLey.Tag := 999;

    LLblLey := TLabel.Create(Self);
    LLblLey.Parent := pnlLeyendaFichajes;
    LLblLey.Left := 520; LLblLey.Top := 6;
    LLblLey.Caption := '  Festivo  ';
    LLblLey.Color := $00FFE082; LLblLey.Font.Color := $00B25900;
    LLblLey.Font.Style := [fsBold]; LLblLey.Transparent := False; LLblLey.Tag := 999;

    LLblLey := TLabel.Create(Self);
    LLblLey.Parent := pnlLeyendaFichajes;
    LLblLey.Left := 595; LLblLey.Top := 6;
    LLblLey.Caption := '  Día Libre  ';
    LLblLey.Color := $00E9ECEF; LLblLey.Font.Color := $006C757D;
    LLblLey.Font.Style := [fsBold]; LLblLey.Transparent := False; LLblLey.Tag := 999;

    // 3. Cabecera de días de la semana
    pnlDiasHeader := TPanel.Create(Self);
    pnlDiasHeader.Parent := pnlCalendarioContainer;
    pnlDiasHeader.Align := alTop;
    pnlDiasHeader.Height := 24;
    pnlDiasHeader.BevelOuter := bvNone;
    pnlDiasHeader.Color := $00E8ECF0;
    pnlDiasHeader.ParentBackground := False;
    pnlDiasHeader.Tag := 999;

    for I := 0 to 6 do
    begin
      FLblDiasHeader[I] := TLabel.Create(Self);
      FLblDiasHeader[I].Parent := pnlDiasHeader;
      FLblDiasHeader[I].Caption := NOMBRES_DIAS[I];
      FLblDiasHeader[I].Alignment := taCenter;
      FLblDiasHeader[I].Font.Name := 'Segoe UI';
      FLblDiasHeader[I].Font.Size := 9;
      FLblDiasHeader[I].Font.Style := [fsBold];
      FLblDiasHeader[I].Font.Color := $00333333;
      FLblDiasHeader[I].Top := 3;
      FLblDiasHeader[I].AutoSize := False;
      FLblDiasHeader[I].Height := 18;
      FLblDiasHeader[I].Tag := 999;
    end;

    // 4. Grid de dibujo del calendario
    gridCalendarioFichajes := TDrawGrid.Create(Self);
    gridCalendarioFichajes.Parent := pnlCalendarioContainer;
    gridCalendarioFichajes.Align := alClient;
    gridCalendarioFichajes.ColCount := 7;
    gridCalendarioFichajes.RowCount := 6;
    gridCalendarioFichajes.DefaultDrawing := False;
    gridCalendarioFichajes.DoubleBuffered := True;
    gridCalendarioFichajes.FixedCols := 0;
    gridCalendarioFichajes.FixedRows := 0;
    gridCalendarioFichajes.ScrollBars := ssNone;
    gridCalendarioFichajes.Options := [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine];
    gridCalendarioFichajes.Tag := 999;
    gridCalendarioFichajes.OnDrawCell := gridCalendarioFichajesDrawCell;
    gridCalendarioFichajes.OnClick := gridCalendarioFichajesClick;
    gridCalendarioFichajes.OnDblClick := gridCalendarioFichajesDblClick;

    // 5. Popup menú contextual
    pmCalendario := TPopupMenu.Create(Self);
    pmCalendario.OnPopup := pmCalendarioPopup;

    miVerDetalle := TMenuItem.Create(pmCalendario);
    miVerDetalle.Caption := 'Ver Detalle del Día (Fichajes)...';
    miVerDetalle.Default := True;
    miVerDetalle.OnClick := miVerDetalleClick;
    pmCalendario.Items.Add(miVerDetalle);

    miRegistrarFalta := TMenuItem.Create(pmCalendario);
    miRegistrarFalta.Caption := 'Registrar Falta de Asistencia...';
    miRegistrarFalta.OnClick := miRegistrarFaltaClick;
    pmCalendario.Items.Add(miRegistrarFalta);

    miEliminarFalta := TMenuItem.Create(pmCalendario);
    miEliminarFalta.Caption := 'Eliminar Falta / Ausencia';
    miEliminarFalta.OnClick := miEliminarFaltaClick;
    pmCalendario.Items.Add(miEliminarFalta);

    gridCalendarioFichajes.PopupMenu := pmCalendario;
  end;
end;

procedure TfrmTrabajadoresFichajes.Resize;
begin
  inherited Resize;
  if Assigned(pnlCalendarioContainer) and pnlCalendarioContainer.Visible then
    AjustarCabeceraDiasFichajes;
end;

procedure TfrmTrabajadoresFichajes.btnVistaClick(Sender: TObject);
var
  LId: string;
  LIdx: Integer;
begin
  if Sender = btnVistaListado then
  begin
    pnlCalendarioContainer.Visible := False;
    dbGridMain.Visible := True;
    ApplyAllFilters;
  end
  else
  begin
    // Si no hay trabajador seleccionado en el combo, intentar tomar el de la fila actual del grid
    if cbTrabajadorFilter.ItemIndex <= 0 then
    begin
      if FQryMain.Active and not FQryMain.IsEmpty and (FQryMain.FindField('id_trabajador') <> nil) then
      begin
        LId := FQryMain.FieldByName('id_trabajador').AsString;
        LIdx := FTrabajadorIds.IndexOf(LId);
        if LIdx >= 0 then
          cbTrabajadorFilter.ItemIndex := LIdx + 1;
      end;

      if (cbTrabajadorFilter.ItemIndex <= 0) and (cbTrabajadorFilter.Items.Count > 1) then
        cbTrabajadorFilter.ItemIndex := 1;
    end;

    dbGridMain.Visible := False;
    pnlCalendarioContainer.Visible := True;

    if Sender = btnVistaMes then
      FModoCalendarioFichajes := mcvMes
    else
      FModoCalendarioFichajes := mcvSemana;

    CargarCalendarioFichajes;
  end;
end;

procedure TfrmTrabajadoresFichajes.btnNavegacionPeriodoClick(Sender: TObject);
var
  LTag: Integer;
begin
  if not (Sender is TComponent) then Exit;
  LTag := TComponent(Sender).Tag;

  case LTag of
    1: // Anterior
      begin
        if FModoCalendarioFichajes = mcvMes then
          FFechaCalendarioBase := IncMonth(FFechaCalendarioBase, -1)
        else
          FFechaCalendarioBase := IncDay(FFechaCalendarioBase, -7);
      end;
    2: // Hoy
      FFechaCalendarioBase := Date;
    3: // Siguiente
      begin
        if FModoCalendarioFichajes = mcvMes then
          FFechaCalendarioBase := IncMonth(FFechaCalendarioBase, 1)
        else
          FFechaCalendarioBase := IncDay(FFechaCalendarioBase, 7);
      end;
  end;

  CargarCalendarioFichajes;
end;

procedure TfrmTrabajadoresFichajes.CargarZonas;
var
  LQry: TFDQuery;
  LNombre: string;
begin
  if not Assigned(cbZonaFilter) or not Assigned(FZonaNames) then Exit;

  cbZonaFilter.Items.BeginUpdate;
  try
    cbZonaFilter.Items.Clear;
    FZonaNames.Clear;

    cbZonaFilter.Items.Add('TODAS');

    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;
      LQry.SQL.Text :=
        'SELECT DISTINCT z_nombre FROM (' +
        '  SELECT z.nombre AS z_nombre FROM ge_centros_zonas z WHERE z.activo = 1 AND z.nombre IS NOT NULL AND z.nombre <> '''' ' +
        '  UNION ' +
        '  SELECT DISTINCT COALESCE(NULLIF(f.centro, ''''), NULLIF(z.nombre, ''''), f.tipo_centro, '''') AS z_nombre ' +
        '  FROM ge_trabajadores_fichajes f ' +
        '  LEFT JOIN ge_centros_zonas z ON z.id = f.id_zona ' +
        '  WHERE f.id_empresa = :empresa_id ' +
        ') AS u WHERE z_nombre IS NOT NULL AND z_nombre <> '''' ORDER BY z_nombre ASC';
      LQry.ParamByName('empresa_id').AsString := dmgMain.CurrentCompanyId;
      LQry.Open;

      while not LQry.Eof do
      begin
        LNombre := LQry.FieldByName('z_nombre').AsString;
        cbZonaFilter.Items.Add(LNombre);
        FZonaNames.Add(LNombre);
        LQry.Next;
      end;
    finally
      LQry.Free;
    end;

    cbZonaFilter.ItemIndex := 0;
  finally
    cbZonaFilter.Items.EndUpdate;
  end;
end;

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

  cbGrupoFilter.Items.BeginUpdate;
  try
    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;
  finally
    cbGrupoFilter.Items.EndUpdate;
  end;
end;

procedure TfrmTrabajadoresFichajes.CargarTrabajadores(AIdGrupo: Integer);
var
  LQry: TFDQuery;
  LNombre: string;
begin
  if not Assigned(cbTrabajadorFilter) or not Assigned(FTrabajadorIds) then Exit;

  cbTrabajadorFilter.Items.BeginUpdate;
  try
    cbTrabajadorFilter.Items.Clear;
    FTrabajadorIds.Clear;

    cbTrabajadorFilter.Items.Add('TODOS');

    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;
      LQry.SQL.Text :=
        'SELECT DISTINCT t.id, ' +
        'CONCAT( ' +
        '  CASE ' +
        '    WHEN (t.apellidos IS NULL OR TRIM(t.apellidos) = '''' OR t.nombre LIKE CONCAT(''%'', TRIM(t.apellidos))) THEN TRIM(t.nombre) ' +
        '    ELSE TRIM(CONCAT(t.nombre, '' '', t.apellidos)) ' +
        '  END, ' +
        '  '' ('', COALESCE(te.codigo, t.codigo), '')'' ' +
        ') AS trabajador_nombre, ' +
        'CASE ' +
        '  WHEN (t.apellidos IS NULL OR TRIM(t.apellidos) = '''' OR t.nombre LIKE CONCAT(''%'', TRIM(t.apellidos))) THEN TRIM(t.nombre) ' +
        '  ELSE TRIM(CONCAT(t.nombre, '' '', t.apellidos)) ' +
        'END AS nombre_orden ' +
        'FROM ge_trabajadores t ' +
        'INNER JOIN ge_trabajadores_empresas te ON te.id_trabajador = t.id ' +
        'WHERE te.id_empresa = :empresa_id ' +
        '  AND t.activo = 1 AND te.activo = 1 ';

      if AIdGrupo > 0 then
        LQry.SQL.Text := LQry.SQL.Text + 'AND COALESCE(te.id_grupo, t.grupo, 0) = :id_grupo ';

      LQry.SQL.Text := LQry.SQL.Text + 'ORDER BY nombre_orden ASC';


      LQry.ParamByName('empresa_id').AsString := dmgMain.CurrentCompanyId;
      if AIdGrupo > 0 then
        LQry.ParamByName('id_grupo').AsInteger := AIdGrupo;

      LQry.Open;

      while not LQry.Eof do
      begin
        LNombre := LQry.FieldByName('trabajador_nombre').AsString;
        cbTrabajadorFilter.Items.Add(LNombre);
        FTrabajadorIds.Add(LQry.FieldByName('id').AsString);
        LQry.Next;
      end;
    finally
      LQry.Free;
    end;

    cbTrabajadorFilter.ItemIndex := 0;
  finally
    cbTrabajadorFilter.Items.EndUpdate;
  end;
end;

procedure TfrmTrabajadoresFichajes.cbTrabajadorFilterDropDown(Sender: TObject);
begin
  if cbTrabajadorFilter.HandleAllocated then
    SendMessage(cbTrabajadorFilter.Handle, CB_SETDROPPEDWIDTH, 320, 0);
end;

procedure TfrmTrabajadoresFichajes.FilterComboChange(Sender: TObject);
begin
  if Assigned(pnlCalendarioContainer) and pnlCalendarioContainer.Visible then
    CargarCalendarioFichajes
  else
    ApplyAllFilters;
end;

procedure TfrmTrabajadoresFichajes.GrupoFilterChange(Sender: TObject);
var
  LIdGrupo: Integer;
begin
  LIdGrupo := 0;
  if Assigned(cbGrupoFilter) and (cbGrupoFilter.ItemIndex > 0) and Assigned(FGrupoIds) and
     (cbGrupoFilter.ItemIndex - 1 < FGrupoIds.Count) then
    LIdGrupo := StrToIntDef(FGrupoIds[cbGrupoFilter.ItemIndex - 1], 0);

  CargarTrabajadores(LIdGrupo);

  if Assigned(pnlCalendarioContainer) and pnlCalendarioContainer.Visible then
  begin
    if cbTrabajadorFilter.Items.Count > 1 then
      cbTrabajadorFilter.ItemIndex := 1;
    CargarCalendarioFichajes;
  end
  else
    ApplyAllFilters;
end;

procedure TfrmTrabajadoresFichajes.ApplyAllFilters;
var
  LAddFilter: string;
begin
  inherited ApplyAllFilters;

  if not Assigned(FQryMain) or not FQryMain.Active then Exit;

  // 1. Filtro por Zona / Centro de fichaje
  if Assigned(cbZonaFilter) and (cbZonaFilter.ItemIndex > 0) and Assigned(FZonaNames) and
     (cbZonaFilter.ItemIndex - 1 < FZonaNames.Count) then
  begin
    LAddFilter := 'centro = ' + QuotedStr(FZonaNames[cbZonaFilter.ItemIndex - 1]);
    if FQryMain.Filter <> '' then
      FQryMain.Filter := '(' + FQryMain.Filter + ') AND (' + LAddFilter + ')'
    else
      FQryMain.Filter := LAddFilter;
    FQryMain.Filtered := True;
  end;

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

  // 3. Filtro por Trabajador en particular
  if Assigned(cbTrabajadorFilter) and (cbTrabajadorFilter.ItemIndex > 0) and Assigned(FTrabajadorIds) and
     (cbTrabajadorFilter.ItemIndex - 1 < FTrabajadorIds.Count) then
  begin
    LAddFilter := 'id_trabajador = ' + FTrabajadorIds[cbTrabajadorFilter.ItemIndex - 1];
    if FQryMain.Filter <> '' then
      FQryMain.Filter := '(' + FQryMain.Filter + ') AND (' + LAddFilter + ')'
    else
      FQryMain.Filter := LAddFilter;
    FQryMain.Filtered := True;
  end;

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

procedure TfrmTrabajadoresFichajes.btnRefreshClick(Sender: TObject);
var
  LSql: string;
begin
  FQryMain.Close;
  try
    LSql :=
      'SELECT f.id, f.id_empresa, f.id_trabajador, f.fecha, ' +
      'CONCAT(COALESCE(te.codigo, t.codigo), '' - '', ' +
      '  CASE ' +
      '    WHEN (t.apellidos IS NULL OR TRIM(t.apellidos) = '''' OR t.nombre LIKE CONCAT(''%'', TRIM(t.apellidos))) THEN TRIM(t.nombre) ' +
      '    ELSE TRIM(CONCAT(t.nombre, '' '', t.apellidos)) ' +
      '  END ' +
      ') AS trabajador, ' +
      'TIME_FORMAT(f.hora_inicio, ''%H:%i'') AS hora_inicio, ' +

      'TIME_FORMAT(f.hora_fin, ''%H:%i'') AS hora_fin, ' +
      'f.minutos_totales, f.horas_decimal, ' +
      'COALESCE(NULLIF(f.centro, ''''), NULLIF(z.nombre, ''''), f.tipo_centro, '''') AS centro, ' +
      'COALESCE(te.id_grupo, t.grupo, 0) AS id_grupo, ' +
      'f.id_zona, ' +
      'f.tipo_cierre, f.validado, f.observaciones ' +
      'FROM ge_trabajadores_fichajes f ' +
      'INNER JOIN ge_trabajadores t ON t.id = f.id_trabajador ' +
      'LEFT JOIN ge_trabajadores_empresas te ON (te.id_trabajador = t.id AND te.id_empresa = f.id_empresa) ' +
      'LEFT JOIN ge_centros_zonas z ON z.id = f.id_zona ' +
      'WHERE f.id_empresa = :empresa_id ';

    if Trim(edtSearch.Text) <> '' then
      LSql := LSql + 'AND (t.codigo LIKE :search OR te.codigo LIKE :search OR t.nombre LIKE :search OR t.apellidos LIKE :search) ';

    LSql := LSql + 'ORDER BY f.fecha DESC, f.id DESC, t.nombre ASC';

    FQryMain.SQL.Text := LSql;
    FQryMain.ParamByName('empresa_id').AsString := dmgMain.CurrentCompanyId;

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

    FQryMain.Open;

    // Configurar columnas
    dbGridMain.Columns.Clear;
    with dbGridMain.Columns.Add do
    begin
      FieldName := 'fecha';
      Title.Caption := 'Fecha';
      Width := 85;
      Alignment := taCenter;
    end;
    with dbGridMain.Columns.Add do
    begin
      FieldName := 'trabajador';
      Title.Caption := 'Trabajador';
      Width := 220;
    end;
    with dbGridMain.Columns.Add do
    begin
      FieldName := 'hora_inicio';
      Title.Caption := 'Entrada';
      Width := 75;
      Alignment := taCenter;
    end;
    with dbGridMain.Columns.Add do
    begin
      FieldName := 'hora_fin';
      Title.Caption := 'Salida';
      Width := 75;
      Alignment := taCenter;
    end;
    with dbGridMain.Columns.Add do
    begin
      FieldName := 'horas_decimal';
      Title.Caption := 'Horas';
      Width := 65;
      Alignment := taRightJustify;
    end;
    with dbGridMain.Columns.Add do
    begin
      FieldName := 'minutos_totales';
      Title.Caption := 'Minutos';
      Width := 65;
      Alignment := taRightJustify;
    end;
    with dbGridMain.Columns.Add do
    begin
      FieldName := 'centro';
      Title.Caption := 'Centro / Tienda';
      Width := 130;
    end;
    with dbGridMain.Columns.Add do
    begin
      FieldName := 'tipo_cierre';
      Title.Caption := 'Cierre';
      Width := 110;
    end;
    with dbGridMain.Columns.Add do
    begin
      FieldName := 'validado';
      Title.Caption := 'Validado';
      Width := 65;
      Alignment := taCenter;
    end;
    with dbGridMain.Columns.Add do
    begin
      FieldName := 'observaciones';
      Title.Caption := 'Observaciones';
      Width := 180;
    end;

    dbGridMain.AutoFitColumns;
    ApplyAllFilters;

    if Assigned(pnlCalendarioContainer) and pnlCalendarioContainer.Visible then
      CargarCalendarioFichajes;
  except
    on E: Exception do
      TDbErrorHandler.HandleException(E, 'Cargar Fichajes');
  end;
end;

procedure TfrmTrabajadoresFichajes.AjustarCabeceraDiasFichajes;
var
  I, LColWidth, LExtra, LRowHeight: Integer;
begin
  if not Assigned(gridCalendarioFichajes) or (gridCalendarioFichajes.ClientWidth <= 0) then Exit;

  LColWidth := gridCalendarioFichajes.ClientWidth div 7;
  LExtra := gridCalendarioFichajes.ClientWidth mod 7;

  for I := 0 to 6 do
  begin
    if I < 6 then
      gridCalendarioFichajes.ColWidths[I] := LColWidth
    else
      gridCalendarioFichajes.ColWidths[I] := LColWidth + LExtra;

    if Assigned(FLblDiasHeader[I]) then
    begin
      FLblDiasHeader[I].Left := I * LColWidth;
      FLblDiasHeader[I].Width := gridCalendarioFichajes.ColWidths[I];
    end;
  end;

  if FModoCalendarioFichajes = mcvMes then
  begin
    LRowHeight := (gridCalendarioFichajes.ClientHeight - 2) div 6;
    if LRowHeight < 40 then LRowHeight := 40;
    for I := 0 to 5 do
      gridCalendarioFichajes.RowHeights[I] := LRowHeight;
  end
  else
  begin
    LRowHeight := gridCalendarioFichajes.ClientHeight - 2;
    if LRowHeight < 100 then LRowHeight := 100;
    gridCalendarioFichajes.RowHeights[0] := LRowHeight;
  end;
end;

procedure TfrmTrabajadoresFichajes.CargarHorarioPactadoBase(const ATrabajadorId: string);
var
  LQry: TFDQuery;
  LDia, LMinTot: Integer;
begin
  for LDia := 1 to 7 do
  begin
    FHorasPactadasDia[LDia] := 0.0;
    FDiaLaboralPactado[LDia] := False;
  end;

  if ATrabajadorId = '' then Exit;

  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text :=
      'SELECT dia_semana, activo, horas_dia, entrada_m, salida_m, entrada_t, salida_t, descanso_minutos ' +
      'FROM ge_trabajadores_horario WHERE id_trabajador = :id';
    LQry.ParamByName('id').AsString := ATrabajadorId;
    LQry.Open;
    while not LQry.Eof do
    begin
      LDia := LQry.FieldByName('dia_semana').AsInteger;
      if (LDia >= 1) and (LDia <= 7) then
      begin
        FDiaLaboralPactado[LDia] := (LQry.FieldByName('activo').AsInteger = 1);
        if not LQry.FieldByName('horas_dia').IsNull and (LQry.FieldByName('horas_dia').AsFloat > 0) then
          FHorasPactadasDia[LDia] := LQry.FieldByName('horas_dia').AsFloat
        else if FDiaLaboralPactado[LDia] then
        begin
          LMinTot := (ParseTimeMinutes(LQry.FieldByName('salida_m').AsString) - ParseTimeMinutes(LQry.FieldByName('entrada_m').AsString)) +
                     (ParseTimeMinutes(LQry.FieldByName('salida_t').AsString) - ParseTimeMinutes(LQry.FieldByName('entrada_t').AsString)) -
                     LQry.FieldByName('descanso_minutos').AsInteger;
          if LMinTot > 0 then
            FHorasPactadasDia[LDia] := LMinTot / 60.0;
        end;
      end;
      LQry.Next;
    end;
  finally
    LQry.Free;
  end;
end;

procedure TfrmTrabajadoresFichajes.CargarCalendarioFichajes;
var
  Y, M, D: Word;
  LDow, I, LCount, LCurDow: Integer;
  LPrimerDiaMes, LInicioMatriz, LFinMatriz, LCurFecha, LFecDesde, LFecHasta, LRowFecha: TDate;
  LQry: TFDQuery;
  LTramo: TTramoFichajeResumen;
  LDiff: Double;
  LDiaRef: PDiaFichaje;
  LTrabajadorId: string;
begin
  if FFechaCalendarioBase = 0 then
    FFechaCalendarioBase := Date;

  DecodeDate(FFechaCalendarioBase, Y, M, D);

  if FModoCalendarioFichajes = mcvMes then
  begin
    LPrimerDiaMes := EncodeDate(Y, M, 1);
    LDow := DayOfTheWeek(LPrimerDiaMes); // 1 = Lunes .. 7 = Domingo
    LInicioMatriz := IncDay(LPrimerDiaMes, -(LDow - 1));
    LFinMatriz := IncDay(LInicioMatriz, 41);
    LCount := 42;
    gridCalendarioFichajes.RowCount := 6;

    for I := 0 to 41 do
    begin
      LCurFecha := IncDay(LInicioMatriz, I);
      FMatrizFichajes[I].Fecha := LCurFecha;
      FMatrizFichajes[I].NumeroDia := DayOf(LCurFecha);
      FMatrizFichajes[I].EsMesActual := (MonthOf(LCurFecha) = M);
      FMatrizFichajes[I].EsHoy := (LCurFecha = Date);
      FMatrizFichajes[I].EsSeleccionado := (LCurFecha = FSelectedFichajeFecha);
      LCurDow := DayOfTheWeek(LCurFecha);
      FMatrizFichajes[I].EsLaboralPactado := FDiaLaboralPactado[LCurDow];
      FMatrizFichajes[I].HorasPactadas := FHorasPactadasDia[LCurDow];
      FMatrizFichajes[I].HorasFichadas := 0.0;
      FMatrizFichajes[I].DiferenciaHoras := 0.0;
      FMatrizFichajes[I].TieneAusencia := False;
      FMatrizFichajes[I].TipoAusencia := '';
      FMatrizFichajes[I].ObservacionAusencia := '';
      FMatrizFichajes[I].TieneFestivo := False;
      FMatrizFichajes[I].DescripcionFestivo := '';
      SetLength(FMatrizFichajes[I].Tramos, 0);
      FMatrizFichajes[I].Estado := edfDescanso;
    end;

    lblPeriodoTitulo.Caption := Format('%s de %d', [NOMBRES_MESES[M], Y]);
  end
  else
  begin
    LDow := DayOfTheWeek(FFechaCalendarioBase);
    LInicioMatriz := IncDay(FFechaCalendarioBase, -(LDow - 1));
    LFinMatriz := IncDay(LInicioMatriz, 6);
    LCount := 7;
    gridCalendarioFichajes.RowCount := 1;

    for I := 0 to 6 do
    begin
      LCurFecha := IncDay(LInicioMatriz, I);
      FMatrizSemana[I].Fecha := LCurFecha;
      FMatrizSemana[I].NumeroDia := DayOf(LCurFecha);
      FMatrizSemana[I].EsMesActual := True;
      FMatrizSemana[I].EsHoy := (LCurFecha = Date);
      FMatrizSemana[I].EsSeleccionado := (LCurFecha = FSelectedFichajeFecha);
      LCurDow := DayOfTheWeek(LCurFecha);
      FMatrizSemana[I].EsLaboralPactado := FDiaLaboralPactado[LCurDow];
      FMatrizSemana[I].HorasPactadas := FHorasPactadasDia[LCurDow];
      FMatrizSemana[I].HorasFichadas := 0.0;
      FMatrizSemana[I].DiferenciaHoras := 0.0;
      FMatrizSemana[I].TieneAusencia := False;
      FMatrizSemana[I].TipoAusencia := '';
      FMatrizSemana[I].ObservacionAusencia := '';
      FMatrizSemana[I].TieneFestivo := False;
      FMatrizSemana[I].DescripcionFestivo := '';
      SetLength(FMatrizSemana[I].Tramos, 0);
      FMatrizSemana[I].Estado := edfDescanso;
    end;

    lblPeriodoTitulo.Caption := Format('Semana %d (%s al %s)',
      [WeekOf(LInicioMatriz), FormatDateTime('dd/mm', LInicioMatriz), FormatDateTime('dd/mm/yyyy', LFinMatriz)]);
  end;

  AjustarCabeceraDiasFichajes;

  // Determinar trabajador
  LTrabajadorId := '';
  if (cbTrabajadorFilter.ItemIndex > 0) and (cbTrabajadorFilter.ItemIndex - 1 < FTrabajadorIds.Count) then
  begin
    LTrabajadorId := FTrabajadorIds[cbTrabajadorFilter.ItemIndex - 1];
    lblCalendarioTrabajador.Caption := 'Trabajador: ' + cbTrabajadorFilter.Text;
  end
  else
  begin
    lblCalendarioTrabajador.Caption := 'Trabajador: (Seleccione uno en el filtro lateral)';
    gridCalendarioFichajes.Invalidate;
    Exit;
  end;

  CargarHorarioPactadoBase(LTrabajadorId);

  // Actualizar días laborales pactados en la matriz
  for I := 0 to LCount - 1 do
  begin
    if FModoCalendarioFichajes = mcvMes then
      LDiaRef := @FMatrizFichajes[I]
    else
      LDiaRef := @FMatrizSemana[I];

    LCurDow := DayOfTheWeek(LDiaRef^.Fecha);
    LDiaRef^.EsLaboralPactado := FDiaLaboralPactado[LCurDow];
    LDiaRef^.HorasPactadas := FHorasPactadasDia[LCurDow];
  end;

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

    // 1. Cargar Ausencias / Vacaciones / Bajas
    try
      LQry.SQL.Text :=
        'SELECT fecha_inicio, fecha_fin, tipo, observaciones ' +
        'FROM ge_trabajadores_ausencias ' +
        'WHERE id_trabajador = :id AND fecha_fin >= :desde AND fecha_inicio <= :hasta';
      LQry.ParamByName('id').AsString := LTrabajadorId;
      LQry.ParamByName('desde').AsDate := LInicioMatriz;
      LQry.ParamByName('hasta').AsDate := LFinMatriz;
      LQry.Open;
      while not LQry.Eof do
      begin
        LFecDesde := LQry.FieldByName('fecha_inicio').AsDateTime;
        LFecHasta := LQry.FieldByName('fecha_fin').AsDateTime;
        for I := 0 to LCount - 1 do
        begin
          if FModoCalendarioFichajes = mcvMes then
            LCurFecha := FMatrizFichajes[I].Fecha
          else
            LCurFecha := FMatrizSemana[I].Fecha;

          if (LCurFecha >= LFecDesde) and (LCurFecha <= LFecHasta) then
          begin
            if FModoCalendarioFichajes = mcvMes then
            begin
              FMatrizFichajes[I].TieneAusencia := True;
              FMatrizFichajes[I].TipoAusencia := LQry.FieldByName('tipo').AsString;
              FMatrizFichajes[I].ObservacionAusencia := LQry.FieldByName('observaciones').AsString;
            end
            else
            begin
              FMatrizSemana[I].TieneAusencia := True;
              FMatrizSemana[I].TipoAusencia := LQry.FieldByName('tipo').AsString;
              FMatrizSemana[I].ObservacionAusencia := LQry.FieldByName('observaciones').AsString;
            end;
          end;
        end;
        LQry.Next;
      end;
    except
    end;

    // 2. Cargar Festivos Oficiales
    try
      LQry.Close;
      LQry.SQL.Text :=
        'SELECT fecha, descripcion, tipo FROM ge_festivos ' +
        'WHERE (id_empresa = :empresa OR id_empresa IS NULL OR id_empresa = 0) ' +
        '  AND fecha >= :desde AND fecha <= :hasta';
      LQry.ParamByName('empresa').AsString := dmgMain.CurrentCompanyId;
      LQry.ParamByName('desde').AsDate := LInicioMatriz;
      LQry.ParamByName('hasta').AsDate := LFinMatriz;
      LQry.Open;
      while not LQry.Eof do
      begin
        LRowFecha := LQry.FieldByName('fecha').AsDateTime;
        for I := 0 to LCount - 1 do
        begin
          if FModoCalendarioFichajes = mcvMes then
            LCurFecha := FMatrizFichajes[I].Fecha
          else
            LCurFecha := FMatrizSemana[I].Fecha;

          if LCurFecha = LRowFecha then
          begin
            var LTipoFest := UpperCase(Trim(LQry.FieldByName('tipo').AsString));
            if Pos('VACACION', LTipoFest) > 0 then
            begin
              if FModoCalendarioFichajes = mcvMes then
              begin
                FMatrizFichajes[I].TieneAusencia := True;
                FMatrizFichajes[I].TipoAusencia := 'VACACIONES';
                FMatrizFichajes[I].ObservacionAusencia := LQry.FieldByName('descripcion').AsString;
              end
              else
              begin
                FMatrizSemana[I].TieneAusencia := True;
                FMatrizSemana[I].TipoAusencia := 'VACACIONES';
                FMatrizSemana[I].ObservacionAusencia := LQry.FieldByName('descripcion').AsString;
              end;
            end
            else
            begin
              if FModoCalendarioFichajes = mcvMes then
              begin
                FMatrizFichajes[I].TieneFestivo := True;
                FMatrizFichajes[I].DescripcionFestivo := LQry.FieldByName('descripcion').AsString;
              end
              else
              begin
                FMatrizSemana[I].TieneFestivo := True;
                FMatrizSemana[I].DescripcionFestivo := LQry.FieldByName('descripcion').AsString;
              end;
            end;
          end;
        end;
        LQry.Next;
      end;
    except
    end;

    // 3. Cargar Fichajes
    try
      LQry.Close;
      LQry.SQL.Text :=
        'SELECT f.fecha, ' +
        '       TIME_FORMAT(f.hora_inicio, ''%H:%i'') AS h_ini, ' +
        '       TIME_FORMAT(f.hora_fin, ''%H:%i'') AS h_fin, ' +
        '       f.horas_decimal, ' +
        '       COALESCE(NULLIF(f.centro, ''''), NULLIF(z.nombre, ''''), f.tipo_centro, '''') AS centro ' +
        'FROM ge_trabajadores_fichajes f ' +
        'LEFT JOIN ge_centros_zonas z ON z.id = f.id_zona ' +
        'WHERE f.id_trabajador = :id AND f.fecha >= :desde AND f.fecha <= :hasta ' +
        'ORDER BY f.fecha ASC, f.hora_inicio ASC';
      LQry.ParamByName('id').AsString := LTrabajadorId;
      LQry.ParamByName('desde').AsDate := LInicioMatriz;
      LQry.ParamByName('hasta').AsDate := LFinMatriz;
      LQry.Open;
      while not LQry.Eof do
      begin
        LRowFecha := LQry.FieldByName('fecha').AsDateTime;
        LTramo.HoraInicio := LQry.FieldByName('h_ini').AsString;
        LTramo.HoraFin := LQry.FieldByName('h_fin').AsString;
        LTramo.Horas := LQry.FieldByName('horas_decimal').AsFloat;
        LTramo.Centro := LQry.FieldByName('centro').AsString;

        for I := 0 to LCount - 1 do
        begin
          if FModoCalendarioFichajes = mcvMes then
            LCurFecha := FMatrizFichajes[I].Fecha
          else
            LCurFecha := FMatrizSemana[I].Fecha;

          if LCurFecha = LRowFecha then
          begin
            if FModoCalendarioFichajes = mcvMes then
            begin
              FMatrizFichajes[I].HorasFichadas := FMatrizFichajes[I].HorasFichadas + LTramo.Horas;
              SetLength(FMatrizFichajes[I].Tramos, Length(FMatrizFichajes[I].Tramos) + 1);
              FMatrizFichajes[I].Tramos[High(FMatrizFichajes[I].Tramos)] := LTramo;
            end
            else
            begin
              FMatrizSemana[I].HorasFichadas := FMatrizSemana[I].HorasFichadas + LTramo.Horas;
              SetLength(FMatrizSemana[I].Tramos, Length(FMatrizSemana[I].Tramos) + 1);
              FMatrizSemana[I].Tramos[High(FMatrizSemana[I].Tramos)] := LTramo;
            end;
            Break;
          end;
        end;
        LQry.Next;
      end;
    except
    end;

  finally
    LQry.Free;
  end;

  // 4. Calcular Estados y Diferencias
  for I := 0 to LCount - 1 do
  begin
    if FModoCalendarioFichajes = mcvMes then
      LDiaRef := @FMatrizFichajes[I]
    else
      LDiaRef := @FMatrizSemana[I];

    LDiaRef^.DiferenciaHoras := LDiaRef^.HorasFichadas - LDiaRef^.HorasPactadas;
    LDiff := LDiaRef^.DiferenciaHoras;

    if LDiaRef^.TieneAusencia then
    begin
      if SameText(LDiaRef^.TipoAusencia, 'VACACIONES') then
        LDiaRef^.Estado := edfVacaciones
      else if Pos('BAJA', UpperCase(LDiaRef^.TipoAusencia)) > 0 then
        LDiaRef^.Estado := edfBajaMedica
      else if SameText(LDiaRef^.TipoAusencia, 'FESTIVO') then
        LDiaRef^.Estado := edfFestivo
      else
        LDiaRef^.Estado := edfAusencia;
    end
    else if LDiaRef^.TieneFestivo then
    begin
      LDiaRef^.Estado := edfFestivo;
    end
    else if LDiaRef^.EsLaboralPactado and (LDiaRef^.HorasPactadas > 0) then
    begin
      if (LDiaRef^.HorasFichadas = 0) and (LDiaRef^.Fecha <= Date) then
        LDiaRef^.Estado := edfFalta
      else if Abs(LDiff) <= 0.05 then
        LDiaRef^.Estado := edfCompleto
      else if LDiff > 0.05 then
        LDiaRef^.Estado := edfSuperavit
      else if (LDiaRef^.HorasFichadas > 0) and (LDiff < -0.05) then
        LDiaRef^.Estado := edfDeficit
      else
        LDiaRef^.Estado := edfDescanso;
    end
    else
    begin
      if LDiaRef^.HorasFichadas > 0 then
        LDiaRef^.Estado := edfSuperavit
      else
        LDiaRef^.Estado := edfDescanso;
    end;
  end;

  gridCalendarioFichajes.Invalidate;
end;

procedure TfrmTrabajadoresFichajes.gridCalendarioFichajesDrawCell(Sender: TObject;
  ACol, ARow: Longint; Rect: TRect; State: TGridDrawState);
var
  LIdx: Integer;
  LDia: TDiaFichaje;
  LCanvas: TCanvas;
  LHeaderRect, LBadgeRect, LTramoBox: TRect;
  LNumStr, LBadgeStr, LSummaryStr: string;
  LBgColor, LBadgeBgColor, LBadgeTextColor: TColor;
  I, LTop, LMaxTramos, LBadgeWidth: Integer;
begin
  if FModoCalendarioFichajes = mcvMes then
  begin
    LIdx := ARow * 7 + ACol;
    if (LIdx < 0) or (LIdx > 41) then Exit;
    LDia := FMatrizFichajes[LIdx];
  end
  else
  begin
    if (ACol < 0) or (ACol > 6) then Exit;
    LDia := FMatrizSemana[ACol];
  end;

  LCanvas := gridCalendarioFichajes.Canvas;

  case LDia.Estado of
    edfCompleto:
      begin
        LBgColor := $00F2FAF4;
        LBadgeBgColor := $00D4EDDA;
        LBadgeTextColor := $00155724;
        LBadgeStr := FormatFloat('0.00', LDia.HorasFichadas) + ' h';
      end;
    edfSuperavit:
      begin
        LBgColor := $00F0F7FF;
        LBadgeBgColor := $00CCE5FF;
        LBadgeTextColor := $00004085;
        LBadgeStr := '+' + FormatFloat('0.00', LDia.DiferenciaHoras) + ' h';
      end;
    edfDeficit:
      begin
        LBgColor := $00FFFBF0;
        LBadgeBgColor := $00FFF3CD;
        LBadgeTextColor := $00856404;
        LBadgeStr := FormatFloat('0.00', LDia.DiferenciaHoras) + ' h';
      end;
    edfFalta:
      begin
        LBgColor := $00FFF5F5;
        LBadgeBgColor := $00F8D7DA;
        LBadgeTextColor := $00721C24;
        LBadgeStr := 'FALTA';
      end;
    edfVacaciones:
      begin
        LBgColor := $00FBF7FD;
        LBadgeBgColor := $00E1BEE7;
        LBadgeTextColor := $004A148C;
        LBadgeStr := 'VACACIONES';
      end;
    edfBajaMedica:
      begin
        LBgColor := $00F5F2F9;
        LBadgeBgColor := $00D1C4E9;
        LBadgeTextColor := $00311B92;
        LBadgeStr := 'BAJA';
      end;
    edfAusencia:
      begin
        LBgColor := $00F9F8FA;
        LBadgeBgColor := $00E0E0E0;
        LBadgeTextColor := $00424242;
        LBadgeStr := UpperCase(LDia.TipoAusencia);
        if LBadgeStr = '' then LBadgeStr := 'PERMISO';
      end;
    edfFestivo:
      begin
        LBgColor := $00FFFDEE;
        LBadgeBgColor := $00FFE082;
        LBadgeTextColor := $00B25900;
        LBadgeStr := 'FESTIVO';
      end;
    else
      begin
        LBgColor := $00F8F9FA;
        LBadgeBgColor := $00E9ECEF;
        LBadgeTextColor := $006C757D;
        LBadgeStr := 'LIBRE';
      end;
  end;

  if (FModoCalendarioFichajes = mcvMes) and (not LDia.EsMesActual) then
    LBgColor := $00F5F5F5;

  LCanvas.Brush.Color := LBgColor;
  LCanvas.FillRect(Rect);

  if LDia.EsHoy then
  begin
    LCanvas.Pen.Color := $00E26714;
    LCanvas.Pen.Width := 2;
  end
  else if (LDia.Fecha = FSelectedFichajeFecha) or (gdSelected in State) then
  begin
    LCanvas.Pen.Color := $004A90E2;
    LCanvas.Pen.Width := 2;
  end
  else
  begin
    LCanvas.Pen.Color := $00E2E5E8;
    LCanvas.Pen.Width := 1;
  end;
  LCanvas.Rectangle(Rect);

  LHeaderRect := Rect;
  LHeaderRect.Bottom := LHeaderRect.Top + 24;

  if LDia.EsHoy then
  begin
    LCanvas.Brush.Color := $00E26714;
    LCanvas.FillRect(LHeaderRect);
    LCanvas.Font.Color := clWhite;
    LCanvas.Font.Style := [fsBold];
  end
  else
  begin
    LCanvas.Brush.Color := LBgColor;
    if (FModoCalendarioFichajes = mcvMes) and (not LDia.EsMesActual) then
      LCanvas.Font.Color := clGray
    else
      LCanvas.Font.Color := clBlack;
    LCanvas.Font.Style := [fsBold];
  end;

  LCanvas.Font.Name := 'Segoe UI';
  LCanvas.Font.Size := 10;
  LNumStr := IntToStr(LDia.NumeroDia);
  LCanvas.TextOut(LHeaderRect.Left + 6, LHeaderRect.Top + 3, LNumStr);

  if LDia.EsHoy then
  begin
    LCanvas.Font.Size := 8;
    LCanvas.TextOut(LHeaderRect.Left + 26, LHeaderRect.Top + 5, 'HOY');
  end;

  if LBadgeStr <> '' then
  begin
    LCanvas.Font.Size := 8;
    LCanvas.Font.Style := [fsBold];
    LBadgeWidth := LCanvas.TextWidth(LBadgeStr) + 8;
    LBadgeRect := TRect.Create(LHeaderRect.Right - LBadgeWidth - 4, LHeaderRect.Top + 3,
                               LHeaderRect.Right - 4, LHeaderRect.Top + 21);
    LCanvas.Brush.Color := LBadgeBgColor;
    LCanvas.Pen.Color := LBadgeBgColor;
    LCanvas.RoundRect(LBadgeRect.Left, LBadgeRect.Top, LBadgeRect.Right, LBadgeRect.Bottom, 6, 6);
    LCanvas.Font.Color := LBadgeTextColor;
    LCanvas.TextOut(LBadgeRect.Left + 4, LBadgeRect.Top + 2, LBadgeStr);
  end;

  LTop := LHeaderRect.Bottom + 4;
  LCanvas.Font.Style := [];
  LCanvas.Font.Size := 8;
  LCanvas.Font.Name := 'Segoe UI';

  if FModoCalendarioFichajes = mcvMes then
  begin
    if LDia.EsLaboralPactado or (LDia.HorasPactadas > 0) then
    begin
      LCanvas.Font.Color := $00616161;
      LSummaryStr := Format('Pact: %sh | Fich: %sh', [FormatFloat('0.00', LDia.HorasPactadas), FormatFloat('0.00', LDia.HorasFichadas)]);
      LCanvas.TextOut(Rect.Left + 5, LTop, LSummaryStr);
      Inc(LTop, 16);
    end
    else if LDia.HorasFichadas > 0 then
    begin
      LCanvas.Font.Color := $00004085;
      LSummaryStr := Format('Trabajado: %sh (Libre)', [FormatFloat('0.00', LDia.HorasFichadas)]);
      LCanvas.TextOut(Rect.Left + 5, LTop, LSummaryStr);
      Inc(LTop, 16);
    end;

    if LDia.TieneAusencia and (LDia.ObservacionAusencia <> '') then
    begin
      LCanvas.Font.Color := $00721C24;
      LCanvas.Font.Style := [fsItalic];
      LCanvas.TextOut(Rect.Left + 5, LTop, 'Motivo: ' + Copy(LDia.ObservacionAusencia, 1, 24));
      Inc(LTop, 15);
      LCanvas.Font.Style := [];
    end;

    LMaxTramos := (Rect.Bottom - LTop - 4) div 15;
    for I := 0 to High(LDia.Tramos) do
    begin
      if I >= LMaxTramos then
      begin
        LCanvas.Font.Color := clGray;
        LCanvas.Font.Style := [fsItalic];
        LCanvas.TextOut(Rect.Left + 5, LTop, Format('... y %d más', [Length(LDia.Tramos) - I]));
        Break;
      end;
      LCanvas.Font.Color := $00212529;
      LCanvas.Font.Style := [];
      LCanvas.TextOut(Rect.Left + 5, LTop, Format('%s-%s (%sh)',
        [LDia.Tramos[I].HoraInicio, LDia.Tramos[I].HoraFin, FormatFloat('0.00', LDia.Tramos[I].Horas)]));
      Inc(LTop, 15);
    end;
  end
  else
  begin
    // Vista Semanal
    LCanvas.Font.Size := 9;
    LCanvas.Font.Style := [fsBold];
    LCanvas.Font.Color := $00333333;
    LCanvas.TextOut(Rect.Left + 8, LTop, Format('Pactadas: %s h', [FormatFloat('0.00', LDia.HorasPactadas)]));
    Inc(LTop, 18);
    LCanvas.TextOut(Rect.Left + 8, LTop, Format('Fichadas: %s h', [FormatFloat('0.00', LDia.HorasFichadas)]));
    Inc(LTop, 18);

    if LDia.DiferenciaHoras <> 0 then
    begin
      if LDia.DiferenciaHoras > 0 then
      begin
        LCanvas.Font.Color := $00004085;
        LCanvas.TextOut(Rect.Left + 8, LTop, Format('Superávit: +%s h', [FormatFloat('0.00', LDia.DiferenciaHoras)]));
      end
      else
      begin
        LCanvas.Font.Color := $00856404;
        LCanvas.TextOut(Rect.Left + 8, LTop, Format('Déficit: %s h', [FormatFloat('0.00', LDia.DiferenciaHoras)]));
      end;
      Inc(LTop, 22);
    end
    else
      Inc(LTop, 6);

    LCanvas.Pen.Color := $00E0E0E0;
    LCanvas.MoveTo(Rect.Left + 6, LTop);
    LCanvas.LineTo(Rect.Right - 6, LTop);
    Inc(LTop, 8);

    LCanvas.Font.Style := [fsBold];
    LCanvas.Font.Size := 8;
    LCanvas.Font.Color := $00555555;
    LCanvas.TextOut(Rect.Left + 8, LTop, 'MARCAJES DEL DÍA:');
    Inc(LTop, 18);

    if Length(LDia.Tramos) = 0 then
    begin
      LCanvas.Font.Style := [fsItalic];
      LCanvas.Font.Color := clGray;
      if LDia.TieneAusencia then
      begin
        LCanvas.Font.Color := $00721C24;
        LCanvas.TextOut(Rect.Left + 10, LTop, 'Ausencia: ' + LDia.TipoAusencia);
        if LDia.ObservacionAusencia <> '' then
          LCanvas.TextOut(Rect.Left + 10, LTop + 16, 'Motivo: ' + LDia.ObservacionAusencia);
      end
      else if LDia.TieneFestivo then
        LCanvas.TextOut(Rect.Left + 10, LTop, 'Festivo: ' + LDia.DescripcionFestivo)
      else if LDia.EsLaboralPactado then
        LCanvas.TextOut(Rect.Left + 10, LTop, 'Sin registros de fichaje')
      else
        LCanvas.TextOut(Rect.Left + 10, LTop, 'Día de descanso');
    end
    else
    begin
      for I := 0 to High(LDia.Tramos) do
      begin
        LTramoBox := TRect.Create(Rect.Left + 6, LTop, Rect.Right - 6, LTop + 44);
        LCanvas.Brush.Color := clWhite;
        LCanvas.Pen.Color := $00D0D4D8;
        LCanvas.Rectangle(LTramoBox);

        LCanvas.Font.Style := [fsBold];
        LCanvas.Font.Size := 9;
        LCanvas.Font.Color := $001A1A1A;
        LCanvas.TextOut(LTramoBox.Left + 6, LTramoBox.Top + 4,
          Format('%s  ➜  %s', [LDia.Tramos[I].HoraInicio, LDia.Tramos[I].HoraFin]));

        LCanvas.Font.Style := [];
        LCanvas.Font.Size := 8;
        LCanvas.Font.Color := $00666666;
        LCanvas.TextOut(LTramoBox.Left + 6, LTramoBox.Top + 22,
          Format('%s h  |  %s', [FormatFloat('0.00', LDia.Tramos[I].Horas), LDia.Tramos[I].Centro]));

        Inc(LTop, 50);
        if LTop > Rect.Bottom - 45 then Break;
      end;
    end;
  end;
end;

function TfrmTrabajadoresFichajes.ObtenerFechaDeCeldaFichajes(ACol, ARow: Integer): TDate;
var
  LIdx: Integer;
begin
  Result := 0;
  if FModoCalendarioFichajes = mcvMes then
  begin
    LIdx := ARow * 7 + ACol;
    if (LIdx >= 0) and (LIdx <= 41) then
      Result := FMatrizFichajes[LIdx].Fecha;
  end
  else
  begin
    if (ACol >= 0) and (ACol <= 6) then
      Result := FMatrizSemana[ACol].Fecha;
  end;
end;

procedure TfrmTrabajadoresFichajes.gridCalendarioFichajesClick(Sender: TObject);
var
  LCol, LRow: Integer;
  LFecha: TDate;
begin
  LCol := gridCalendarioFichajes.Col;
  LRow := gridCalendarioFichajes.Row;
  LFecha := ObtenerFechaDeCeldaFichajes(LCol, LRow);
  if LFecha > 0 then
    FSelectedFichajeFecha := LFecha;
end;

procedure TfrmTrabajadoresFichajes.gridCalendarioFichajesDblClick(Sender: TObject);
var
  LCol, LRow: Integer;
  LFecha: TDate;
begin
  LCol := gridCalendarioFichajes.Col;
  LRow := gridCalendarioFichajes.Row;
  LFecha := ObtenerFechaDeCeldaFichajes(LCol, LRow);
  if LFecha > 0 then
  begin
    FSelectedFichajeFecha := LFecha;
    AbrirDetalleFichajesDia(LFecha);
  end;
end;

procedure TfrmTrabajadoresFichajes.AbrirDetalleFichajesDia(AFecha: TDate);
var
  I: Integer;
  LDia: TDiaFichaje;
  LFound: Boolean;
  LTrabajadorId, LNombreCompleto: string;
begin
  if (AFecha = 0) or (cbTrabajadorFilter.ItemIndex <= 0) then Exit;

  LTrabajadorId := FTrabajadorIds[cbTrabajadorFilter.ItemIndex - 1];
  LNombreCompleto := cbTrabajadorFilter.Text;

  LFound := False;
  FillChar(LDia, SizeOf(LDia), 0);

  if FModoCalendarioFichajes = mcvMes then
  begin
    for I := 0 to 41 do
    begin
      if FMatrizFichajes[I].Fecha = AFecha then
      begin
        LDia := FMatrizFichajes[I];
        LFound := True;
        Break;
      end;
    end;
  end
  else
  begin
    for I := 0 to 6 do
    begin
      if FMatrizSemana[I].Fecha = AFecha then
      begin
        LDia := FMatrizSemana[I];
        LFound := True;
        Break;
      end;
    end;
  end;

  if not LFound then Exit;

  if TfrmDiaFichajesDetalleModal.Execute(LTrabajadorId, LNombreCompleto, AFecha,
    LDia.HorasPactadas, LDia.TipoAusencia, LDia.ObservacionAusencia) then
  begin
    CargarCalendarioFichajes;
    btnRefreshClick(nil);
  end;
end;

procedure TfrmTrabajadoresFichajes.RegistrarFaltaAsistencia(AFecha: TDate);
var
  LTrabajadorId, LTrabajadorNombre, LMotivo: string;
  LQry: TFDQuery;
begin
  if cbTrabajadorFilter.ItemIndex <= 0 then
  begin
    ShowMessage('Por favor, seleccione un trabajador en el filtro lateral antes de registrar la falta.');
    Exit;
  end;

  LTrabajadorId := FTrabajadorIds[cbTrabajadorFilter.ItemIndex - 1];
  LTrabajadorNombre := cbTrabajadorFilter.Text;

  LMotivo := 'Falta de asistencia';
  if not InputQuery('Registrar Falta de Asistencia',
    'Trabajador: ' + LTrabajadorNombre + sLineBreak +
    'Fecha: ' + FormatDateTime('dddd, dd/mm/yyyy', AFecha) + sLineBreak + sLineBreak +
    'Introduzca el motivo de la falta de asistencia:', LMotivo) then
    Exit;

  if Trim(LMotivo) = '' then
    LMotivo := 'Falta de asistencia sin especificar';

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

    // Eliminar ausencia previa del mismo día si existiese
    LQry.SQL.Text := 'DELETE FROM ge_trabajadores_ausencias WHERE id_trabajador = :id AND fecha_inicio <= :fec AND fecha_fin >= :fec';
    LQry.ParamByName('id').AsString := LTrabajadorId;
    LQry.ParamByName('fec').AsDate := AFecha;
    LQry.ExecSQL;

    // Insertar la falta
    LQry.SQL.Text :=
      'INSERT INTO ge_trabajadores_ausencias ' +
      '(id_empresa, id_trabajador, fecha_inicio, fecha_fin, tipo, dias_laborables, estado, observaciones, created_at, id_user_creator) ' +
      'VALUES (:emp, :trab, :fec_ini, :fec_fin, :tipo, 1, ''CONFIRMADA'', :obs, NOW(), :user)';
    LQry.ParamByName('emp').AsString := dmgMain.CurrentCompanyId;
    LQry.ParamByName('trab').AsString := LTrabajadorId;
    LQry.ParamByName('fec_ini').AsDate := AFecha;
    LQry.ParamByName('fec_fin').AsDate := AFecha;
    LQry.ParamByName('tipo').AsString := 'FALTA';
    LQry.ParamByName('obs').AsString := LMotivo;
    LQry.ParamByName('user').AsString := dmgMain.CurrentUserId;
    LQry.ExecSQL;


    ShowMessage('Falta de asistencia registrada con éxito para el ' + FormatDateTime('dd/mm/yyyy', AFecha) + '.');
    CargarCalendarioFichajes;
  finally
    LQry.Free;
  end;
end;

procedure TfrmTrabajadoresFichajes.EliminarFaltaAsistencia(AFecha: TDate);
var
  LTrabajadorId: string;
  LQry: TFDQuery;
begin
  if cbTrabajadorFilter.ItemIndex <= 0 then Exit;
  LTrabajadorId := FTrabajadorIds[cbTrabajadorFilter.ItemIndex - 1];

  if MessageDlg('¿Desea eliminar la falta o ausencia registrada para el ' + FormatDateTime('dd/mm/yyyy', AFecha) + '?',
    mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;
      LQry.SQL.Text := 'DELETE FROM ge_trabajadores_ausencias WHERE id_trabajador = :id AND fecha_inicio <= :fec AND fecha_fin >= :fec';
      LQry.ParamByName('id').AsString := LTrabajadorId;
      LQry.ParamByName('fec').AsDate := AFecha;
      LQry.ExecSQL;

      ShowMessage('Falta o ausencia eliminada correctamente.');
      CargarCalendarioFichajes;
    finally
      LQry.Free;
    end;
  end;
end;

procedure TfrmTrabajadoresFichajes.btnCalAñadirFaltaClick(Sender: TObject);
var
  LFecha: TDate;
begin
  if FSelectedFichajeFecha > 0 then
    LFecha := FSelectedFichajeFecha
  else
    LFecha := Date;

  RegistrarFaltaAsistencia(LFecha);
end;

procedure TfrmTrabajadoresFichajes.pmCalendarioPopup(Sender: TObject);
var
  LFecha: TDate;
  I: Integer;
  LTieneAus: Boolean;
begin
  LFecha := ObtenerFechaDeCeldaFichajes(gridCalendarioFichajes.Col, gridCalendarioFichajes.Row);
  LTieneAus := False;
  if FModoCalendarioFichajes = mcvMes then
  begin
    for I := 0 to 41 do
      if FMatrizFichajes[I].Fecha = LFecha then
      begin
        LTieneAus := FMatrizFichajes[I].TieneAusencia;
        Break;
      end;
  end
  else
  begin
    for I := 0 to 6 do
      if FMatrizSemana[I].Fecha = LFecha then
      begin
        LTieneAus := FMatrizSemana[I].TieneAusencia;
        Break;
      end;
  end;
  miEliminarFalta.Enabled := LTieneAus;
end;

procedure TfrmTrabajadoresFichajes.miVerDetalleClick(Sender: TObject);
var
  LFecha: TDate;
begin
  LFecha := ObtenerFechaDeCeldaFichajes(gridCalendarioFichajes.Col, gridCalendarioFichajes.Row);
  if LFecha > 0 then
    AbrirDetalleFichajesDia(LFecha);
end;

procedure TfrmTrabajadoresFichajes.miRegistrarFaltaClick(Sender: TObject);
var
  LFecha: TDate;
begin
  LFecha := ObtenerFechaDeCeldaFichajes(gridCalendarioFichajes.Col, gridCalendarioFichajes.Row);
  if LFecha > 0 then
    RegistrarFaltaAsistencia(LFecha);
end;

procedure TfrmTrabajadoresFichajes.miEliminarFaltaClick(Sender: TObject);
var
  LFecha: TDate;
begin
  LFecha := ObtenerFechaDeCeldaFichajes(gridCalendarioFichajes.Col, gridCalendarioFichajes.Row);
  if LFecha > 0 then
    EliminarFaltaAsistencia(LFecha);
end;

procedure TfrmTrabajadoresFichajes.dbGridMainCellClick(Column: TColumn);
var
  LQry: TFDQuery;
  LId: string;
  LNewVal: Integer;
begin
  if (Column <> nil) and SameText(Column.FieldName, 'validado') and not FQryMain.IsEmpty then
  begin
    LId := FQryMain.FieldByName('id').AsString;
    LNewVal := 1 - FQryMain.FieldByName('validado').AsInteger;

    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;
      LQry.SQL.Text := 'UPDATE ge_trabajadores_fichajes SET validado = :val WHERE id = :id';
      LQry.ParamByName('val').AsInteger := LNewVal;
      LQry.ParamByName('id').AsString := LId;
      LQry.ExecSQL;

      FQryMain.Edit;
      FQryMain.FieldByName('validado').AsInteger := LNewVal;
      FQryMain.Post;
    except
      on E: Exception do
        TDbErrorHandler.HandleException(E, 'Cambiar Estado Validación');
    end;
    LQry.Free;
  end;
end;

procedure TfrmTrabajadoresFichajes.btnNewClick(Sender: TObject);
begin
  ShowMessage('Para registrar fichajes masivos o automáticos, utilice el módulo de reloj/presencia o inserte desde la ficha del trabajador.');
end;

procedure TfrmTrabajadoresFichajes.btnEditClick(Sender: TObject);
var
  i: Integer;
begin
  if FQryMain.IsEmpty then Exit;
  // Permite conmutar la validación de la fila seleccionada
  for i := 0 to dbGridMain.Columns.Count - 1 do
  begin
    if SameText(dbGridMain.Columns[i].FieldName, 'validado') then
    begin
      dbGridMainCellClick(dbGridMain.Columns[i]);
      Break;
    end;
  end;
end;

procedure TfrmTrabajadoresFichajes.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 marcaje de fichaje?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;
      LQry.SQL.Text := 'DELETE FROM ge_trabajadores_fichajes WHERE id = :id';
      LQry.ParamByName('id').AsString := LId;
      LQry.ExecSQL;
      btnRefreshClick(nil);
    except
      on E: Exception do
        TDbErrorHandler.HandleException(E, 'Eliminar Fichaje');
    end;
    LQry.Free;
  end;
end;

end.
