unit frm_TrabajadoresEditor;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.Buttons,
  Vcl.ComCtrls, Data.DB, FireDAC.Comp.Client, uAppTheme, FireDAC.Stan.Param, uBaseForm,
  Vcl.Grids, Vcl.DBGrids, System.IOUtils, Winapi.ShellAPI, System.UITypes, uDBGridHelper,
  FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Error, FireDAC.DatS,
  FireDAC.Phys.Intf, FireDAC.DApt.Intf, FireDAC.Stan.Async, FireDAC.DApt,
  FireDAC.Comp.DataSet;

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

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

  PDiaFichaje = ^TDiaFichaje;
  TDiaFichaje = record
    Fecha: TDate;
    NumeroDia: Word;
    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;

  TModoCalendarioFichajes = (mcvMes, mcvSemana);

  TfrmTrabajadoresEditor = class(TfrmBase)
    tsDatosLaborales: TTabSheet;
    tsHorario: TTabSheet;
    tsFormacion: TTabSheet;
    tsDocumentos: TTabSheet;
    tsHistorial: TTabSheet;

    // Tab 1: Datos Personales
    lblCodigo: TLabel;
    edtCodigo: TEdit;
    lblNif: TLabel;
    edtNif: TEdit;
    lblNombre: TLabel;
    edtNombre: TEdit;
    lblApellidos: TLabel;
    edtApellidos: TEdit;
    chkActivo: TCheckBox;
    lblNombreCorto: TLabel;
    edtNombreCorto: TEdit;
    lblFechaNac: TLabel;
    dtpFechaNac: TDateTimePicker;
    lblNacionalidad: TLabel;
    edtNacionalidad: TEdit;
    lblSexo: TLabel;
    cmbSexo: TComboBox;
    lblConvocatoria: TLabel;
    cmbConvocatoria: TComboBox;
    lblDireccion: TLabel;
    edtDireccion: TEdit;
    lblCp: TLabel;
    edtCp: TEdit;
    lblPoblacion: TLabel;
    edtPoblacion: TEdit;
    lblProvincia: TLabel;
    edtProvincia: TEdit;
    lblTelefono1: TLabel;
    edtTelefono1: TEdit;
    lblTelefono2: TLabel;
    edtTelefono2: TEdit;
    lblEmail: TLabel;
    edtEmail: TEdit;
    lblNumTaquilla: TLabel;
    edtNumTaquilla: TEdit;
    lblGrupo: TLabel;
    cmbGrupo: TComboBox;
    lblObservaciones: TLabel;
    memObservaciones: TMemo;
    pnlPhotoBox: TPanel;
    pnlPhotoImg: TPanel;
    imgFoto: TImage;
    btnSubirFoto: TButton;
    btnEliminarFoto: TButton;

    // Tab 2: Laboral y Nómina
    lblEmpresa: TLabel;
    cmbEmpresa: TComboBox;
    lblGrupoLaboral: TLabel;
    cmbGrupoLaboral: TComboBox;
    lblRegimenCot: TLabel;
    cmbRegimenCot: TComboBox;
    lblNumSS: TLabel;
    edtNumSS: TEdit;
    lblHorasContrato: TLabel;
    edtHorasContrato: TEdit;
    chkJornadaContinua: TCheckBox;
    lblSalarioBase: TLabel;
    edtSalarioBase: TEdit;
    lblCosteHora: TLabel;
    edtCosteHora: TEdit;
    lblCosteHoraHint: TLabel;
    lblFechaAlta: TLabel;
    dtpFechaAlta: TDateTimePicker;
    lblFechaBaja: TLabel;
    dtpFechaBaja: TDateTimePicker;

    grpTipoContrato: TGroupBox;
    chkFijo: TCheckBox;
    chkFijoDiscontinuo: TCheckBox;
    chkTemporal: TCheckBox;

    grpResponsabilidades: TGroupBox;
    chkControlCalidad: TCheckBox;
    chkResponsable: TCheckBox;
    chkFumigador: TCheckBox;

    grpDatosBancarios: TGroupBox;
    chkPagarPorBanco: TCheckBox;
    lblIban: TLabel;
    edtIban: TEdit;
    lblBanco: TLabel;
    edtBanco: TEdit;
    lblOficina: TLabel;
    edtOficina: TEdit;
    lblDc: TLabel;
    edtDc: TEdit;
    lblCc: TLabel;
    edtCc: TEdit;

    grpRetenciones: TGroupBox;
    lblIrpf: TLabel;
    edtIrpf: TEdit;
    lblDesempleo: TLabel;
    edtDesempleo: TEdit;
    lblFormacion: TLabel;
    edtFormacion: TEdit;
    lblContingencias: TLabel;
    edtContingencias: TEdit;
    lblRetencionMe: TLabel;
    edtRetencionMe: TEdit;

    // Tab 3: Horario y Turnos Previstos
    pnlHorarioTop: TPanel;
    lblTituloHorario: TLabel;
    lblDescHorario: TLabel;
    lblPlantillas: TLabel;
    btnPreset40hPartida: TButton;
    btnPreset40hContinua: TButton;
    btnPresetMananaLS: TButton;
    btnCopiarLunes: TButton;
    btnLimpiarHorario: TButton;

    pnlHorarioResumen: TPanel;
    lblResumenDias: TLabel;
    lblResumenHorasSemana: TLabel;
    lblResumenHorasContrato: TLabel;
    pnlAlertaHorario: TPanel;
    lblAlertaHorario: TLabel;
    lblAlertaDetalle: TLabel;

    pnlHorarioGrid: TPanel;
    lblHdrDia: TLabel;
    lblHdrEntradaM: TLabel;
    lblHdrSalidaM: TLabel;
    lblHdrEntradaT: TLabel;
    lblHdrSalidaT: TLabel;
    lblHdrDescanso: TLabel;
    lblHdrTotalDia: TLabel;
    lblHdrObs: TLabel;

    chkDia1: TCheckBox;
    edtEntradaM1: TEdit;
    edtSalidaM1: TEdit;
    edtEntradaT1: TEdit;
    edtSalidaT1: TEdit;
    edtDescanso1: TEdit;
    lblTotalDia1: TLabel;
    edtObs1: TEdit;

    chkDia2: TCheckBox;
    edtEntradaM2: TEdit;
    edtSalidaM2: TEdit;
    edtEntradaT2: TEdit;
    edtSalidaT2: TEdit;
    edtDescanso2: TEdit;
    lblTotalDia2: TLabel;
    edtObs2: TEdit;

    chkDia3: TCheckBox;
    edtEntradaM3: TEdit;
    edtSalidaM3: TEdit;
    edtEntradaT3: TEdit;
    edtSalidaT3: TEdit;
    edtDescanso3: TEdit;
    lblTotalDia3: TLabel;
    edtObs3: TEdit;

    chkDia4: TCheckBox;
    edtEntradaM4: TEdit;
    edtSalidaM4: TEdit;
    edtEntradaT4: TEdit;
    edtSalidaT4: TEdit;
    edtDescanso4: TEdit;
    lblTotalDia4: TLabel;
    edtObs4: TEdit;

    chkDia5: TCheckBox;
    edtEntradaM5: TEdit;
    edtSalidaM5: TEdit;
    edtEntradaT5: TEdit;
    edtSalidaT5: TEdit;
    edtDescanso5: TEdit;
    lblTotalDia5: TLabel;
    edtObs5: TEdit;

    chkDia6: TCheckBox;
    edtEntradaM6: TEdit;
    edtSalidaM6: TEdit;
    edtEntradaT6: TEdit;
    edtSalidaT6: TEdit;
    edtDescanso6: TEdit;
    lblTotalDia6: TLabel;
    edtObs6: TEdit;

    chkDia7: TCheckBox;
    edtEntradaM7: TEdit;
    edtSalidaM7: TEdit;
    edtEntradaT7: TEdit;
    edtSalidaT7: TEdit;
    edtDescanso7: TEdit;
    lblTotalDia7: TLabel;
    edtObs7: TEdit;

    // Tab 4: Formación y Calidad
    grpAlimentaria: TGroupBox;
    chkManipulador: TCheckBox;
    lblFechaManipulador: TLabel;
    dtpFechaManipulador: TDateTimePicker;
    chkCursoAlergenos: TCheckBox;
    lblFechaAlergenos: TLabel;
    dtpFechaAlergenos: TDateTimePicker;

    grpSaludLaboral: TGroupBox;
    lblRecMedico: TLabel;
    dtpRecMedico: TDateTimePicker;
    lblRenRecMedico: TLabel;
    dtpRenRecMedico: TDateTimePicker;
    chkPrlEpis: TCheckBox;
    chkPrimerosAuxilios: TCheckBox;

    grpConduccionPermisos: TGroupBox;
    lblCarnetConducir: TLabel;
    edtCarnetConducir: TEdit;
    lblCaducidadConducir: TLabel;
    dtpCaducidadConducir: TDateTimePicker;
    lblPermisoTrabajo: TLabel;
    dtpPermisoTrabajo: TDateTimePicker;

    grpObsCapacitacion: TGroupBox;
    memObsCapacitacion: TMemo;

    // Tab 4: Documentos
    pnlDocsToolbar: TPanel;
    btnSubirDoc: TButton;
    btnVerDoc: TButton;
    btnEliminarDoc: TButton;
    dbgDocumentos: TDBGrid;
    // Tab 5: Documentos
    qryDocumentos: TFDQuery;
    dsDocumentos: TDataSource;
    odDocumento: TOpenDialog;
    odFoto: TOpenDialog;

    // Tab 6: Control Horario y Fichajes (Calendario y Listado)
    pnlFichajesToolbar: TPanel;
    lblPeriodoTitulo: TLabel;
    btnModoMes: TButton;
    btnModoSemana: TButton;
    btnPeriodoAnterior: TButton;
    btnPeriodoHoy: TButton;
    btnPeriodoSiguiente: TButton;
    cboFichajeMes: TComboBox;
    cboFichajeAnio: TComboBox;
    btnRefrescarHistorial: TButton;
    btnAlternarVista: TButton;
    pnlLeyendaFichajes: TPanel;
    lblLeyendaCompleto: TLabel;
    lblLeyendaSuperavit: TLabel;
    lblLeyendaDeficit: TLabel;
    lblLeyendaFalta: TLabel;
    lblLeyendaVacaciones: TLabel;
    lblLeyendaFestivo: TLabel;
    lblLeyendaDescanso: TLabel;
    pnlCalendarioContainer: TPanel;
    pnlDiasHeader: TPanel;
    gridCalendarioFichajes: TDrawGrid;
    pnlListadoContainer: TPanel;
    dbgHistorial: TDBGrid;
    qryHistorial: TFDQuery;
    dsHistorial: TDataSource;

    // Tab 7: Historial Contratos y Periodos
    tsContratos: TTabSheet;
    pnlContratosTop: TPanel;
    lblContratosInfo: TLabel;
    btnNuevoContrato: TButton;
    btnModificarContrato: TButton;
    btnEliminarContrato: TButton;
    btnRefrescarContratos: TButton;
    dbgContratos: TDBGrid;
    qryContratos: TFDQuery;
    dsContratos: TDataSource;

    // Botones de desplazamiento de pestañas
    pnlTabNavLeft: TPanel;
    btnTabLeft: TSpeedButton;
    pnlTabNavRight: TPanel;
    btnTabRight: TSpeedButton;

    procedure FormShow(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure btnTabLeftClick(Sender: TObject);
    procedure btnTabRightClick(Sender: TObject);
    procedure pgcDetailsChange(Sender: TObject);
    procedure btnSaveClick(Sender: TObject);
    procedure btnSalirClick(Sender: TObject);
    procedure btnSubirFotoClick(Sender: TObject);
    procedure btnEliminarFotoClick(Sender: TObject);
    procedure btnSubirDocClick(Sender: TObject);
    procedure btnVerDocClick(Sender: TObject);
    procedure btnEliminarDocClick(Sender: TObject);
    procedure dbgDocumentosDblClick(Sender: TObject);
    procedure dbgDocumentosTitleClick(Column: TColumn);
    procedure btnRefrescarHistorialClick(Sender: TObject);
    procedure dbgHistorialTitleClick(Column: TColumn);
    procedure btnNuevoContratoClick(Sender: TObject);
    procedure btnModificarContratoClick(Sender: TObject);
    procedure btnEliminarContratoClick(Sender: TObject);
    procedure btnRefrescarContratosClick(Sender: TObject);
    procedure dbgContratosDblClick(Sender: TObject);
    procedure dbgContratosTitleClick(Column: TColumn);
    procedure edtHorasContratoChange(Sender: TObject);
    procedure edtCosteHoraExit(Sender: TObject);
    procedure HorarioControlChanged(Sender: TObject);
    procedure btnPreset40hPartidaClick(Sender: TObject);
    procedure btnPreset40hContinuaClick(Sender: TObject);
    procedure btnPresetMananaLSClick(Sender: TObject);
    procedure btnCopiarLunesClick(Sender: TObject);
    procedure btnLimpiarHorarioClick(Sender: TObject);

    // Toolbar actions
    procedure btnEliminarClick(Sender: TObject);
    procedure btnBuscarClick(Sender: TObject);
    procedure cmbGrupoChange(Sender: TObject);
    procedure cmbGrupoLaboralChange(Sender: TObject);

    // Acciones de la vista Calendario de Fichajes
    procedure btnModoMesClick(Sender: TObject);
    procedure btnModoSemanaClick(Sender: TObject);
    procedure btnPeriodoAnteriorClick(Sender: TObject);
    procedure btnPeriodoHoyClick(Sender: TObject);
    procedure btnPeriodoSiguienteClick(Sender: TObject);
    procedure cboFichajePeriodoChange(Sender: TObject);
    procedure btnAlternarVistaClick(Sender: TObject);
    procedure gridCalendarioFichajesDrawCell(Sender: TObject; ACol, ARow: Longint;
      Rect: TRect; State: TGridDrawState);
    procedure gridCalendarioFichajesClick(Sender: TObject);
    procedure gridCalendarioFichajesDblClick(Sender: TObject);

  protected
    function HasUnsavedChanges: Boolean; override;
    procedure DoAnadir; override;
    procedure DoModificar; override;
    procedure DoPrimero; override;
    procedure DoAnterior; override;
    procedure DoSiguiente; override;
    procedure DoUltimo; override;
    procedure DoDuplicar; override;

  private
    FTrabajadorId: string;
    FInitialTab: TTabSheet;
    FchkDias: array[1..7] of TCheckBox;
    FedtEntradaM: array[1..7] of TEdit;
    FedtSalidaM: array[1..7] of TEdit;
    FedtEntradaT: array[1..7] of TEdit;
    FedtSalidaT: array[1..7] of TEdit;
    FedtDescanso: array[1..7] of TEdit;
    FlblTotalDia: array[1..7] of TLabel;
    FedtObs: array[1..7] of TEdit;
    FUpdatingHorario: Boolean;
    FHasHorarioAsignado: Boolean;
    FHorarioModificado: Boolean;

    // Estado del Calendario de Fichajes
    FModoCalendarioFichajes: TModoCalendarioFichajes;
    FFechaCalendarioBase: TDate;
    FSelectedFichajeFecha: TDate;
    FMatrizFichajes: array[0..41] of TDiaFichaje;
    FMatrizSemana: array[0..6] of TDiaFichaje;
    FVistaListadoFichajes: Boolean;
    FLblDiasHeader: array[0..6] of TLabel;
    FHorasPactadasDia: array[1..7] of Double;
    FDiaLaboralPactado: array[1..7] of Boolean;

    function CheckSavePrompt: Boolean;
    function GetListDataset: TFDQuery;
    procedure ClearFields;
    procedure InitHorarioControls;
    function ParseTimeMinutes(const AStr: string): Integer;
    procedure RecalcularHorario;
    procedure LoadHorario;
    procedure SaveHorario(const ATrabajadorId: string);
    procedure LoadCompanies;
    procedure LoadConvocatorias;
    procedure LoadGrupos;
    procedure LoadData;
    procedure LoadWorkerPhoto;
    procedure LoadCapacitacion;
    procedure SaveCapacitacion(const ATrabajadorId: string);
    procedure LoadDocuments;
    procedure LoadHistorial;
    procedure LoadContratos;
    procedure ConfigurarColumnasDocumentos;
    procedure ConfigurarColumnasHistorial;
    procedure ConfigurarColumnasContratos;
    procedure UpdateTabNavButtons;

    // Métodos del Calendario de Fichajes
    procedure InicializarCalendarioFichajes;
    procedure AjustarCabeceraDiasFichajes;
    procedure CargarHorarioPactadoBase;
    procedure CargarCalendarioFichajes;
    procedure AbrirDetalleFichajesDia(AFecha: TDate);
    function ObtenerFechaDeCeldaFichajes(ACol, ARow: Integer): TDate;

  public
    property TrabajadorId: string read FTrabajadorId write FTrabajadorId;
    property InitialTab: TTabSheet read FInitialTab write FInitialTab;
  end;

var
  frmTrabajadoresEditor: TfrmTrabajadoresEditor;

implementation

uses
  dmg_Main, uDbErrorHandler, frm_Trabajadores, frm_BaseList, System.DateUtils,
  frm_DiaFichajesDetalleModal, frm_TrabajadorFichajeEditorModal, frm_TrabajadorContratoModal;

{$R *.dfm}

function SafeStrToFloat(const AText: string; const ADefault: Double = 0.0): Double;
var
  LStr: string;
  LVal: Double;
  LSettings: TFormatSettings;
begin
  LStr := Trim(AText);
  if LStr = '' then
    Exit(ADefault);

  if TryStrToFloat(LStr, LVal) then
    Exit(LVal);

  LSettings := TFormatSettings.Create('en-US');
  LSettings.DecimalSeparator := '.';
  if TryStrToFloat(StringReplace(LStr, ',', '.', [rfReplaceAll]), LVal, LSettings) then
    Exit(LVal);

  LSettings := TFormatSettings.Create('es-ES');
  LSettings.DecimalSeparator := ',';
  if TryStrToFloat(StringReplace(LStr, '.', ',', [rfReplaceAll]), LVal, LSettings) then
    Exit(LVal);

  Result := ADefault;
end;

function SafeTryStrToFloat(const AText: string; out AValue: Double): Boolean;
var
  LStr: string;
  LSettings: TFormatSettings;
begin
  AValue := 0.0;
  LStr := Trim(AText);
  if LStr = '' then
    Exit(True);

  if TryStrToFloat(LStr, AValue) then
    Exit(True);

  LSettings := TFormatSettings.Create('en-US');
  LSettings.DecimalSeparator := '.';
  if TryStrToFloat(StringReplace(LStr, ',', '.', [rfReplaceAll]), AValue, LSettings) then
    Exit(True);

  LSettings := TFormatSettings.Create('es-ES');
  LSettings.DecimalSeparator := ',';
  if TryStrToFloat(StringReplace(LStr, '.', ',', [rfReplaceAll]), AValue, LSettings) then
    Exit(True);

  Result := False;
end;

procedure TfrmTrabajadoresEditor.FormShow(Sender: TObject);
begin
  TAppTheme.ApplyToForm(Self);

  // Enlazar botones de barra de herramientas superior
  if Assigned(btnGuardar) then btnGuardar.OnClick := btnSaveClick;
  if Assigned(btnSalir) then btnSalir.OnClick := btnSalirClick;
  if Assigned(btnEliminar) then btnEliminar.OnClick := btnEliminarClick;
  if Assigned(btnBuscar) then btnBuscar.OnClick := btnBuscarClick;
  if Assigned(btnAnadir) then btnAnadir.OnClick := btnAnadirClick;
  if Assigned(btnModificar) then btnModificar.OnClick := btnModificarClick;
  if Assigned(btnPrimero) then btnPrimero.OnClick := btnPrimeroClick;
  if Assigned(btnAnterior) then btnAnterior.OnClick := btnAnteriorClick;
  if Assigned(btnSiguiente) then btnSiguiente.OnClick := btnSiguienteClick;
  if Assigned(btnUltimo) then btnUltimo.OnClick := btnUltimoClick;

  if Assigned(FInitialTab) then
    pgcDetails.ActivePage := FInitialTab
  else
    pgcDetails.ActivePage := tsDatosEnvio;

  tsDatosEnvio.Caption := '   1. Datos Personales';
  pgcDetails.OnChange := pgcDetailsChange;

  InitHorarioControls;
  LoadCompanies;
  LoadConvocatorias;
  LoadGrupos;
  InicializarCalendarioFichajes;

  if FTrabajadorId <> '' then
  begin
    Caption := 'Editar Ficha del Trabajador (ID: ' + FTrabajadorId + ')';
    LoadData;
  end
  else
  begin
    ClearFields;
  end;

  FormResize(Self);
  UpdateTabNavButtons;
end;

procedure TfrmTrabajadoresEditor.FormResize(Sender: TObject);
begin
  inherited;
  if Assigned(pnlTabNavLeft) and Assigned(pnlMain) then
  begin
    pnlTabNavLeft.Top := pnlMain.ClientHeight - pnlTabNavLeft.Height - 1;
    pnlTabNavLeft.Left := 1;
    pnlTabNavLeft.BringToFront;
  end;
  if Assigned(pnlTabNavRight) and Assigned(pnlMain) then
  begin
    pnlTabNavRight.Top := pnlMain.ClientHeight - pnlTabNavRight.Height - 1;
    pnlTabNavRight.Left := pnlMain.ClientWidth - pnlTabNavRight.Width - 1;
    pnlTabNavRight.BringToFront;
  end;
  AjustarCabeceraDiasFichajes;
end;

procedure TfrmTrabajadoresEditor.btnTabLeftClick(Sender: TObject);
begin
  if pgcDetails.ActivePageIndex > 0 then
    pgcDetails.ActivePageIndex := pgcDetails.ActivePageIndex - 1
  else
    pgcDetails.ActivePageIndex := pgcDetails.PageCount - 1;
  UpdateTabNavButtons;
end;

procedure TfrmTrabajadoresEditor.btnTabRightClick(Sender: TObject);
begin
  if pgcDetails.ActivePageIndex < pgcDetails.PageCount - 1 then
    pgcDetails.ActivePageIndex := pgcDetails.ActivePageIndex + 1
  else
    pgcDetails.ActivePageIndex := 0;
  UpdateTabNavButtons;
end;

procedure TfrmTrabajadoresEditor.pgcDetailsChange(Sender: TObject);
begin
  UpdateTabNavButtons;
  if pgcDetails.ActivePage = tsHistorial then
  begin
    AjustarCabeceraDiasFichajes;
    CargarHorarioPactadoBase;
    CargarCalendarioFichajes;
  end;
end;

procedure TfrmTrabajadoresEditor.UpdateTabNavButtons;
begin
  if Assigned(btnTabLeft) then
  begin
    btnTabLeft.Enabled := (pgcDetails.PageCount > 1);
    if pgcDetails.ActivePageIndex > 0 then
      btnTabLeft.Hint := 'Ir a: ' + pgcDetails.Pages[pgcDetails.ActivePageIndex - 1].Caption
    else
      btnTabLeft.Hint := 'Ir a última pestaña';
    btnTabLeft.ShowHint := True;
  end;

  if Assigned(btnTabRight) then
  begin
    btnTabRight.Enabled := (pgcDetails.PageCount > 1);
    if pgcDetails.ActivePageIndex < pgcDetails.PageCount - 1 then
      btnTabRight.Hint := 'Ir a: ' + pgcDetails.Pages[pgcDetails.ActivePageIndex + 1].Caption
    else
      btnTabRight.Hint := 'Ir a primera pestaña';
    btnTabRight.ShowHint := True;
  end;
end;

procedure TfrmTrabajadoresEditor.LoadCompanies;
var
  LQry: TFDQuery;
  LDefIdx: Integer;
begin
  cmbEmpresa.Items.Clear;
  LDefIdx := -1;
  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
      cmbEmpresa.Items.AddObject(LQry.FieldByName('nombre').AsString, TObject(LQry.FieldByName('id').AsInteger));
      if (dmgMain.CurrentCompanyId <> '') and (LQry.FieldByName('id').AsString = dmgMain.CurrentCompanyId) then
        LDefIdx := cmbEmpresa.Items.Count - 1;
      LQry.Next;
    end;
  finally
    LQry.Free;
  end;

  if (LDefIdx >= 0) and (cmbEmpresa.ItemIndex < 0) then
    cmbEmpresa.ItemIndex := LDefIdx
  else if (cmbEmpresa.Items.Count > 0) and (cmbEmpresa.ItemIndex < 0) then
    cmbEmpresa.ItemIndex := 0;
end;

procedure TfrmTrabajadoresEditor.LoadConvocatorias;
var
  LQry: TFDQuery;
begin
  cmbConvocatoria.Items.Clear;
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text :=
      'SELECT DISTINCT convocatoria FROM ge_trabajadores ' +
      'WHERE convocatoria IS NOT NULL AND TRIM(convocatoria) <> '''' ' +
      'ORDER BY convocatoria ASC';
    LQry.Open;
    while not LQry.Eof do
    begin
      cmbConvocatoria.Items.Add(LQry.FieldByName('convocatoria').AsString);
      LQry.Next;
    end;
  finally
    LQry.Free;
  end;
end;

procedure TfrmTrabajadoresEditor.LoadGrupos;
var
  LQry: TFDQuery;
  LId: Integer;
  LNom: string;
begin
  cmbGrupo.Items.BeginUpdate;
  cmbGrupoLaboral.Items.BeginUpdate;
  try
    cmbGrupo.Items.Clear;
    cmbGrupoLaboral.Items.Clear;

    cmbGrupo.Items.AddObject('[SIN GRUPO ASIGNADO]', TObject(0));
    cmbGrupoLaboral.Items.AddObject('[SIN GRUPO ASIGNADO]', TObject(0));

    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
        LId := LQry.FieldByName('id').AsInteger;
        LNom := LQry.FieldByName('nombre').AsString;
        cmbGrupo.Items.AddObject(LNom, TObject(LId));
        cmbGrupoLaboral.Items.AddObject(LNom, TObject(LId));
        LQry.Next;
      end;
    finally
      LQry.Free;
    end;
  finally
    cmbGrupo.Items.EndUpdate;
    cmbGrupoLaboral.Items.EndUpdate;
  end;

  if cmbGrupo.Items.Count > 0 then
  begin
    cmbGrupo.ItemIndex := 0;
    cmbGrupoLaboral.ItemIndex := 0;
  end;
end;

procedure TfrmTrabajadoresEditor.cmbGrupoChange(Sender: TObject);
begin
  if Assigned(cmbGrupoLaboral) then
    cmbGrupoLaboral.ItemIndex := cmbGrupo.ItemIndex;
end;

procedure TfrmTrabajadoresEditor.cmbGrupoLaboralChange(Sender: TObject);
begin
  if Assigned(cmbGrupo) then
    cmbGrupo.ItemIndex := cmbGrupoLaboral.ItemIndex;
end;

procedure TfrmTrabajadoresEditor.LoadData;
var
  LQry: TFDQuery;
  I, LEmpId, LGrupoId: Integer;
begin
  if FTrabajadorId = '' then Exit;

  Caption := 'Editar Ficha del Trabajador (ID: ' + FTrabajadorId + ')';

  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text :=
      'SELECT t.*, ' +
      '  COALESCE(' +
      '    (SELECT c.id_empresa FROM ge_trabajadores_contratos c WHERE c.id_trabajador = t.id ORDER BY c.activo DESC, c.fecha_alta DESC, c.id DESC LIMIT 1), ' +
      '    te_act.id_empresa, ' +
      '    te_last.id_empresa ' +
      '  ) AS id_empresa, ' +
      '  COALESCE(te_act.codigo, te_last.codigo, t.codigo) AS cod_empresa, ' +
      '  COALESCE(' +
      '    (SELECT c.activo FROM ge_trabajadores_contratos c WHERE c.id_trabajador = t.id ORDER BY c.activo DESC, c.fecha_alta DESC, c.id DESC LIMIT 1), ' +
      '    te_act.activo, ' +
      '    te_last.activo, ' +
      '    t.activo ' +
      '  ) AS act_empresa, ' +
      '  COALESCE(' +
      '    (SELECT c.fecha_alta FROM ge_trabajadores_contratos c WHERE c.id_trabajador = t.id ORDER BY c.activo DESC, c.fecha_alta DESC, c.id DESC LIMIT 1), ' +
      '    te_act.fecha_alta, ' +
      '    te_last.fecha_alta, ' +
      '    t.fecha_alta ' +
      '  ) AS alta_empresa, ' +
      '  COALESCE(' +
      '    (SELECT c.fecha_baja FROM ge_trabajadores_contratos c WHERE c.id_trabajador = t.id ORDER BY c.activo DESC, c.fecha_alta DESC, c.id DESC LIMIT 1), ' +
      '    te_act.fecha_baja, ' +
      '    te_last.fecha_baja, ' +
      '    t.fecha_baja ' +
      '  ) AS baja_empresa, ' +
      '  COALESCE(te_act.horas_contrato, te_last.horas_contrato, t.horas_contrato) AS horas_empresa, ' +
      '  COALESCE(te_act.id_grupo, te_last.id_grupo, t.grupo) AS id_grupo ' +
      'FROM ge_trabajadores t ' +
      'LEFT JOIN ge_trabajadores_empresas te_act ON te_act.id_trabajador = t.id AND te_act.activo = 1 ' +
      'LEFT JOIN ge_trabajadores_empresas te_last ON te_last.id_trabajador = t.id ' +
      'WHERE t.id = :id ' +
      'ORDER BY te_act.id DESC, te_last.id DESC LIMIT 1';
    LQry.ParamByName('id').AsString := FTrabajadorId;
    LQry.Open;

    if LQry.IsEmpty then
    begin
      ShowMessage('No se ha encontrado el trabajador solicitado.');
      Exit;
    end;

    // Tab 1: Datos Personales
    if not LQry.FieldByName('cod_empresa').IsNull then
      edtCodigo.Text := LQry.FieldByName('cod_empresa').AsString
    else
      edtCodigo.Text := LQry.FieldByName('codigo').AsString;

    edtNif.Text := LQry.FieldByName('nif').AsString;
    edtNombre.Text := LQry.FieldByName('nombre').AsString;
    edtApellidos.Text := LQry.FieldByName('apellidos').AsString;
    edtNombreCorto.Text := LQry.FieldByName('nombre_corto').AsString;

    if not LQry.FieldByName('act_empresa').IsNull then
      chkActivo.Checked := (LQry.FieldByName('act_empresa').AsInteger = 1)
    else
      chkActivo.Checked := (LQry.FieldByName('activo').AsInteger = 1);

    if not LQry.FieldByName('fecha_nacimiento').IsNull then
    begin
      dtpFechaNac.Checked := True;
      dtpFechaNac.Date := LQry.FieldByName('fecha_nacimiento').AsDateTime;
    end
    else
      dtpFechaNac.Checked := False;

    edtNacionalidad.Text := LQry.FieldByName('nacionalidad').AsString;
    cmbSexo.ItemIndex := cmbSexo.Items.IndexOf(LQry.FieldByName('sexo').AsString);
    if cmbSexo.ItemIndex < 0 then cmbSexo.ItemIndex := 0;

    cmbConvocatoria.Text := LQry.FieldByName('convocatoria').AsString;
    if (cmbConvocatoria.Text <> '') and (cmbConvocatoria.Items.IndexOf(cmbConvocatoria.Text) < 0) then
      cmbConvocatoria.Items.Add(cmbConvocatoria.Text);

    edtDireccion.Text := LQry.FieldByName('direccion').AsString;
    edtCp.Text := LQry.FieldByName('cp').AsString;
    edtPoblacion.Text := LQry.FieldByName('poblacion').AsString;
    edtProvincia.Text := LQry.FieldByName('provincia').AsString;
    edtTelefono1.Text := LQry.FieldByName('telefono1').AsString;
    edtTelefono2.Text := LQry.FieldByName('telefono2').AsString;
    edtEmail.Text := LQry.FieldByName('email').AsString;
    edtNumTaquilla.Text := LQry.FieldByName('num_taquilla').AsString;

    // Grupo de Trabajo
    LGrupoId := 0;
    if not LQry.FieldByName('id_grupo').IsNull then
      LGrupoId := LQry.FieldByName('id_grupo').AsInteger
    else if not LQry.FieldByName('grupo').IsNull then
      LGrupoId := LQry.FieldByName('grupo').AsInteger;

    cmbGrupo.ItemIndex := 0;
    for I := 0 to cmbGrupo.Items.Count - 1 do
    begin
      if Integer(cmbGrupo.Items.Objects[I]) = LGrupoId then
      begin
        cmbGrupo.ItemIndex := I;
        Break;
      end;
    end;
    if Assigned(cmbGrupoLaboral) then
      cmbGrupoLaboral.ItemIndex := cmbGrupo.ItemIndex;

    memObservaciones.Text := LQry.FieldByName('observaciones').AsString;

    // Tab 2: Laboral y Nómina
    if not LQry.FieldByName('id_empresa').IsNull then
      LEmpId := LQry.FieldByName('id_empresa').AsInteger
    else
      LEmpId := StrToIntDef(dmgMain.CurrentCompanyId, 1);

    for I := 0 to cmbEmpresa.Items.Count - 1 do
    begin
      if Integer(cmbEmpresa.Items.Objects[I]) = LEmpId then
      begin
        cmbEmpresa.ItemIndex := I;
        Break;
      end;
    end;

    cmbRegimenCot.ItemIndex := cmbRegimenCot.Items.IndexOf(LQry.FieldByName('regimen_cot').AsString);
    if cmbRegimenCot.ItemIndex < 0 then cmbRegimenCot.ItemIndex := 0;

    edtNumSS.Text := LQry.FieldByName('num_seg_social').AsString;

    if not LQry.FieldByName('horas_empresa').IsNull then
      edtHorasContrato.Text := FormatFloat('0.00', LQry.FieldByName('horas_empresa').AsFloat)
    else
      edtHorasContrato.Text := FormatFloat('0.00', LQry.FieldByName('horas_contrato').AsFloat);

    chkJornadaContinua.Checked := (LQry.FieldByName('jornada_continua').AsInteger = 1);
    edtSalarioBase.Text := FormatFloat('0.00', LQry.FieldByName('salario_base_dia').AsFloat);
    edtCosteHora.Text := FormatFloat('0.00', LQry.FieldByName('coste_hora').AsFloat);

    if not LQry.FieldByName('alta_empresa').IsNull then
      dtpFechaAlta.Date := LQry.FieldByName('alta_empresa').AsDateTime
    else if not LQry.FieldByName('fecha_alta').IsNull then
      dtpFechaAlta.Date := LQry.FieldByName('fecha_alta').AsDateTime
    else
      dtpFechaAlta.Date := Date;

    if not LQry.FieldByName('baja_empresa').IsNull then
    begin
      dtpFechaBaja.Checked := True;
      dtpFechaBaja.Date := LQry.FieldByName('baja_empresa').AsDateTime;
    end
    else if not LQry.FieldByName('fecha_baja').IsNull then
    begin
      dtpFechaBaja.Checked := True;
      dtpFechaBaja.Date := LQry.FieldByName('fecha_baja').AsDateTime;
    end
    else
      dtpFechaBaja.Checked := False;

    chkFijo.Checked := (LQry.FieldByName('fijo').AsInteger = 1);
    chkFijoDiscontinuo.Checked := (LQry.FieldByName('fijo_discontinuo').AsInteger = 1);
    chkTemporal.Checked := (LQry.FieldByName('temporal').AsInteger = 1);

    chkControlCalidad.Checked := (LQry.FieldByName('control_calidad').AsInteger = 1);
    chkResponsable.Checked := (LQry.FieldByName('responsable').AsInteger = 1);
    chkFumigador.Checked := (LQry.FieldByName('fumigador').AsInteger = 1);

    chkPagarPorBanco.Checked := (LQry.FieldByName('pagar_por_banco').AsInteger = 1);
    edtIban.Text := LQry.FieldByName('cb_iban').AsString;
    if edtIban.Text = '' then edtIban.Text := 'ES';
    edtBanco.Text := LQry.FieldByName('cb_banco').AsString;
    edtOficina.Text := LQry.FieldByName('cb_oficina').AsString;
    edtDc.Text := LQry.FieldByName('cb_dc').AsString;
    edtCc.Text := LQry.FieldByName('cb_cc').AsString;

    edtIrpf.Text := FormatFloat('0.00', LQry.FieldByName('irpf').AsFloat);
    edtDesempleo.Text := FormatFloat('0.00', LQry.FieldByName('desempleo').AsFloat);
    edtFormacion.Text := FormatFloat('0.00', LQry.FieldByName('formacion').AsFloat);
    edtContingencias.Text := FormatFloat('0.00', LQry.FieldByName('contingencias').AsFloat);
    edtRetencionMe.Text := FormatFloat('0.00', LQry.FieldByName('retencion_me').AsFloat);

  finally
    LQry.Free;
  end;

  LoadCapacitacion;
  LoadHorario;
  LoadDocuments;
  LoadHistorial;
  CargarHorarioPactadoBase;
  CargarCalendarioFichajes;
  LoadWorkerPhoto;
  LoadContratos;

  ResetChangeTracking;
end;

procedure TfrmTrabajadoresEditor.LoadCapacitacion;
var
  LQry: TFDQuery;
begin
  if FTrabajadorId = '' then Exit;

  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    try
      LQry.SQL.Text := 'SELECT * FROM ge_trabajadores_capacitacion WHERE id_trabajador = :id';
      LQry.ParamByName('id').AsString := FTrabajadorId;
      LQry.Open;

      if not LQry.IsEmpty then
      begin
        chkManipulador.Checked := (LQry.FieldByName('manipulador_alimentos').AsInteger = 1);
        if not LQry.FieldByName('fecha_carnet_manipulador').IsNull then
        begin
          dtpFechaManipulador.Checked := True;
          dtpFechaManipulador.Date := LQry.FieldByName('fecha_carnet_manipulador').AsDateTime;
        end
        else
          dtpFechaManipulador.Checked := False;

        chkCursoAlergenos.Checked := (LQry.FieldByName('curso_alergenos_higiene').AsInteger = 1);
        if not LQry.FieldByName('fecha_curso_alergenos').IsNull then
        begin
          dtpFechaAlergenos.Checked := True;
          dtpFechaAlergenos.Date := LQry.FieldByName('fecha_curso_alergenos').AsDateTime;
        end
        else
          dtpFechaAlergenos.Checked := False;

        if not LQry.FieldByName('fecha_rec_medico').IsNull then
        begin
          dtpRecMedico.Checked := True;
          dtpRecMedico.Date := LQry.FieldByName('fecha_rec_medico').AsDateTime;
        end
        else
          dtpRecMedico.Checked := False;

        if not LQry.FieldByName('fecha_ren_rec_medico').IsNull then
        begin
          dtpRenRecMedico.Checked := True;
          dtpRenRecMedico.Date := LQry.FieldByName('fecha_ren_rec_medico').AsDateTime;
        end
        else
          dtpRenRecMedico.Checked := False;

        chkPrlEpis.Checked := (LQry.FieldByName('prevencion_riesgos_epis').AsInteger = 1);
        chkPrimerosAuxilios.Checked := (LQry.FieldByName('primeros_auxilios').AsInteger = 1);

        edtCarnetConducir.Text := LQry.FieldByName('carnet_conducir').AsString;
        if not LQry.FieldByName('fecha_caducidad_conducir').IsNull then
        begin
          dtpCaducidadConducir.Checked := True;
          dtpCaducidadConducir.Date := LQry.FieldByName('fecha_caducidad_conducir').AsDateTime;
        end
        else
          dtpCaducidadConducir.Checked := False;

        if not LQry.FieldByName('vto_permiso_trabajo').IsNull then
        begin
          dtpPermisoTrabajo.Checked := True;
          dtpPermisoTrabajo.Date := LQry.FieldByName('vto_permiso_trabajo').AsDateTime;
        end
        else
          dtpPermisoTrabajo.Checked := False;

        memObsCapacitacion.Text := LQry.FieldByName('observaciones').AsString;
      end;
    except
      // Si la tabla satélite aún no existe, omitir limpiamente
    end;
  finally
    LQry.Free;
  end;
end;

procedure TfrmTrabajadoresEditor.SaveCapacitacion(const ATrabajadorId: string);
var
  LQry: TFDQuery;
  LEmpresaId: Integer;
begin
  if ATrabajadorId = '' then Exit;

  LEmpresaId := StrToIntDef(dmgMain.CurrentCompanyId, 1);
  if cmbEmpresa.ItemIndex >= 0 then
    LEmpresaId := Integer(cmbEmpresa.Items.Objects[cmbEmpresa.ItemIndex]);

  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    try
      LQry.SQL.Text :=
        'INSERT INTO ge_trabajadores_capacitacion ' +
        '  (id_empresa, id_trabajador, vto_permiso_trabajo, fecha_rec_medico, fecha_ren_rec_medico, ' +
        '   manipulador_alimentos, fecha_carnet_manipulador, curso_alergenos_higiene, fecha_curso_alergenos, ' +
        '   prevencion_riesgos_epis, primeros_auxilios, carnet_conducir, fecha_caducidad_conducir, observaciones, ' +
        '   id_user_creator, id_user_update) ' +
        'VALUES ' +
        '  (:id_empresa, :id_trabajador, :vto_permiso_trabajo, :fecha_rec_medico, :fecha_ren_rec_medico, ' +
        '   :manipulador_alimentos, :fecha_carnet_manipulador, :curso_alergenos_higiene, :fecha_curso_alergenos, ' +
        '   :prevencion_riesgos_epis, :primeros_auxilios, :carnet_conducir, :fecha_caducidad_conducir, :observaciones, ' +
        '   :user, :user) ' +
        'ON DUPLICATE KEY UPDATE ' +
        '  vto_permiso_trabajo = VALUES(vto_permiso_trabajo), ' +
        '  fecha_rec_medico = VALUES(fecha_rec_medico), ' +
        '  fecha_ren_rec_medico = VALUES(fecha_ren_rec_medico), ' +
        '  manipulador_alimentos = VALUES(manipulador_alimentos), ' +
        '  fecha_carnet_manipulador = VALUES(fecha_carnet_manipulador), ' +
        '  curso_alergenos_higiene = VALUES(curso_alergenos_higiene), ' +
        '  fecha_curso_alergenos = VALUES(fecha_curso_alergenos), ' +
        '  prevencion_riesgos_epis = VALUES(prevencion_riesgos_epis), ' +
        '  primeros_auxilios = VALUES(primeros_auxilios), ' +
        '  carnet_conducir = VALUES(carnet_conducir), ' +
        '  fecha_caducidad_conducir = VALUES(fecha_caducidad_conducir), ' +
        '  observaciones = VALUES(observaciones), ' +
        '  id_user_update = VALUES(id_user_update)';

      LQry.ParamByName('id_empresa').AsInteger := LEmpresaId;
      LQry.ParamByName('id_trabajador').AsString := ATrabajadorId;
      LQry.ParamByName('user').AsString := dmgMain.CurrentUserId;

      LQry.ParamByName('vto_permiso_trabajo').DataType := ftDate;
      if dtpPermisoTrabajo.Checked then
        LQry.ParamByName('vto_permiso_trabajo').AsDate := dtpPermisoTrabajo.Date
      else
        LQry.ParamByName('vto_permiso_trabajo').Clear;

      LQry.ParamByName('fecha_rec_medico').DataType := ftDate;
      if dtpRecMedico.Checked then
        LQry.ParamByName('fecha_rec_medico').AsDate := dtpRecMedico.Date
      else
        LQry.ParamByName('fecha_rec_medico').Clear;

      LQry.ParamByName('fecha_ren_rec_medico').DataType := ftDate;
      if dtpRenRecMedico.Checked then
        LQry.ParamByName('fecha_ren_rec_medico').AsDate := dtpRenRecMedico.Date
      else
        LQry.ParamByName('fecha_ren_rec_medico').Clear;

      LQry.ParamByName('manipulador_alimentos').AsInteger := Ord(chkManipulador.Checked);
      LQry.ParamByName('fecha_carnet_manipulador').DataType := ftDate;
      if dtpFechaManipulador.Checked then
        LQry.ParamByName('fecha_carnet_manipulador').AsDate := dtpFechaManipulador.Date
      else
        LQry.ParamByName('fecha_carnet_manipulador').Clear;

      LQry.ParamByName('curso_alergenos_higiene').AsInteger := Ord(chkCursoAlergenos.Checked);
      LQry.ParamByName('fecha_curso_alergenos').DataType := ftDate;
      if dtpFechaAlergenos.Checked then
        LQry.ParamByName('fecha_curso_alergenos').AsDate := dtpFechaAlergenos.Date
      else
        LQry.ParamByName('fecha_curso_alergenos').Clear;

      LQry.ParamByName('prevencion_riesgos_epis').AsInteger := Ord(chkPrlEpis.Checked);
      LQry.ParamByName('primeros_auxilios').AsInteger := Ord(chkPrimerosAuxilios.Checked);
      LQry.ParamByName('carnet_conducir').AsString := Trim(edtCarnetConducir.Text);

      LQry.ParamByName('fecha_caducidad_conducir').DataType := ftDate;
      if dtpCaducidadConducir.Checked then
        LQry.ParamByName('fecha_caducidad_conducir').AsDate := dtpCaducidadConducir.Date
      else
        LQry.ParamByName('fecha_caducidad_conducir').Clear;

      LQry.ParamByName('observaciones').AsString := Trim(memObsCapacitacion.Text);

      LQry.ExecSQL;
    except
      // Si la tabla no ha sido creada en BD, no abortar el guardado principal
    end;
  finally
    LQry.Free;
  end;
end;

procedure TfrmTrabajadoresEditor.InitHorarioControls;
begin
  FchkDias[1] := chkDia1; FchkDias[2] := chkDia2; FchkDias[3] := chkDia3;
  FchkDias[4] := chkDia4; FchkDias[5] := chkDia5; FchkDias[6] := chkDia6; FchkDias[7] := chkDia7;

  FedtEntradaM[1] := edtEntradaM1; FedtEntradaM[2] := edtEntradaM2; FedtEntradaM[3] := edtEntradaM3;
  FedtEntradaM[4] := edtEntradaM4; FedtEntradaM[5] := edtEntradaM5; FedtEntradaM[6] := edtEntradaM6; FedtEntradaM[7] := edtEntradaM7;

  FedtSalidaM[1] := edtSalidaM1; FedtSalidaM[2] := edtSalidaM2; FedtSalidaM[3] := edtSalidaM3;
  FedtSalidaM[4] := edtSalidaM4; FedtSalidaM[5] := edtSalidaM5; FedtSalidaM[6] := edtSalidaM6; FedtSalidaM[7] := edtSalidaM7;

  FedtEntradaT[1] := edtEntradaT1; FedtEntradaT[2] := edtEntradaT2; FedtEntradaT[3] := edtEntradaT3;
  FedtEntradaT[4] := edtEntradaT4; FedtEntradaT[5] := edtEntradaT5; FedtEntradaT[6] := edtEntradaT6; FedtEntradaT[7] := edtEntradaT7;

  FedtSalidaT[1] := edtSalidaT1; FedtSalidaT[2] := edtSalidaT2; FedtSalidaT[3] := edtSalidaT3;
  FedtSalidaT[4] := edtSalidaT4; FedtSalidaT[5] := edtSalidaT5; FedtSalidaT[6] := edtSalidaT6; FedtSalidaT[7] := edtSalidaT7;

  FedtDescanso[1] := edtDescanso1; FedtDescanso[2] := edtDescanso2; FedtDescanso[3] := edtDescanso3;
  FedtDescanso[4] := edtDescanso4; FedtDescanso[5] := edtDescanso5; FedtDescanso[6] := edtDescanso6; FedtDescanso[7] := edtDescanso7;

  FlblTotalDia[1] := lblTotalDia1; FlblTotalDia[2] := lblTotalDia2; FlblTotalDia[3] := lblTotalDia3;
  FlblTotalDia[4] := lblTotalDia4; FlblTotalDia[5] := lblTotalDia5; FlblTotalDia[6] := lblTotalDia6; FlblTotalDia[7] := lblTotalDia7;

  FedtObs[1] := edtObs1; FedtObs[2] := edtObs2; FedtObs[3] := edtObs3;
  FedtObs[4] := edtObs4; FedtObs[5] := edtObs5; FedtObs[6] := edtObs6; FedtObs[7] := edtObs7;
end;

function TfrmTrabajadoresEditor.ParseTimeMinutes(const AStr: string): Integer;
var
  LParts: TArray<string>;
  H, M: Integer;
begin
  Result := -1;
  if Trim(AStr) = '' then Exit;
  LParts := AStr.Split([':']);
  if Length(LParts) >= 2 then
  begin
    if TryStrToInt(Trim(LParts[0]), H) and TryStrToInt(Trim(LParts[1]), M) then
    begin
      if (H >= 0) and (H <= 23) and (M >= 0) and (M <= 59) then
        Result := H * 60 + M;
    end;
  end
  else if Length(LParts) = 1 then
  begin
    if TryStrToInt(Trim(LParts[0]), H) and (H >= 0) and (H <= 23) then
      Result := H * 60;
  end;
end;

procedure TfrmTrabajadoresEditor.RecalcularHorario;
const
  NombresDias: array[1..7] of string = ('Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo');
var
  I, LEntM, LSalM, LEntT, LSalT, LDesc: Integer;
  LMinM, LMinT, LTotMin: Integer;
  LDayHours, LTotalHorasPlanificadas, LHorasContrato, LDiff: Double;
  LTotalDiasTrabajo: Integer;
  LDiasExcesivos, LAlertas: string;
begin
  if FUpdatingHorario then Exit;

  LTotalHorasPlanificadas := 0.0;
  LTotalDiasTrabajo := 0;
  LDiasExcesivos := '';

  for I := 1 to 7 do
  begin
    LDayHours := 0.0;
    if (FchkDias[I] <> nil) and FchkDias[I].Checked then
    begin
      Inc(LTotalDiasTrabajo);
      LMinM := 0;
      LMinT := 0;

      LEntM := ParseTimeMinutes(FedtEntradaM[I].Text);
      LSalM := ParseTimeMinutes(FedtSalidaM[I].Text);
      if (LEntM >= 0) and (LSalM >= LEntM) then
        LMinM := LSalM - LEntM;

      LEntT := ParseTimeMinutes(FedtEntradaT[I].Text);
      LSalT := ParseTimeMinutes(FedtSalidaT[I].Text);
      if (LEntT >= 0) and (LSalT >= LEntT) then
        LMinT := LSalT - LEntT;

      LDesc := StrToIntDef(Trim(FedtDescanso[I].Text), 0);
      LTotMin := LMinM + LMinT - LDesc;
      if LTotMin < 0 then LTotMin := 0;
      LDayHours := LTotMin / 60.0;

      if LDayHours > 9.0 then
      begin
        if LDiasExcesivos <> '' then LDiasExcesivos := LDiasExcesivos + ', ';
        LDiasExcesivos := LDiasExcesivos + Format('%s (%s h)', [NombresDias[I], FormatFloat('0.00', LDayHours)]);
      end;
    end;

    LTotalHorasPlanificadas := LTotalHorasPlanificadas + LDayHours;
    if FlblTotalDia[I] <> nil then
    begin
      FlblTotalDia[I].Caption := FormatFloat('0.00', LDayHours) + ' h';
      if LDayHours > 9.0 then
        FlblTotalDia[I].Font.Color := clRed
      else if LDayHours > 0.0 then
        FlblTotalDia[I].Font.Color := clNavy
      else
        FlblTotalDia[I].Font.Color := clGray;
    end;
  end;

  LHorasContrato := SafeStrToFloat(edtHorasContrato.Text, 40.0);
  LDiff := LTotalHorasPlanificadas - LHorasContrato;

  lblResumenDias.Caption := Format('Días laborables: %d días  |  Descanso: %d días', [LTotalDiasTrabajo, 7 - LTotalDiasTrabajo]);
  lblResumenHorasSemana.Caption := Format('Horas Planificadas: %s h / semana', [FormatFloat('0.00', LTotalHorasPlanificadas)]);
  lblResumenHorasContrato.Caption := Format('Horas según Contrato: %s h / semana', [FormatFloat('0.00', LHorasContrato)]);

  if (not FHasHorarioAsignado) and (not FHorarioModificado) and (LTotalHorasPlanificadas = 0) then
  begin
    pnlAlertaHorario.Color := $00FFF5EA; // Ámbar suave
    lblAlertaHorario.Font.Color := $00994D00;
    lblAlertaHorario.Caption := '⚠️ SIN HORARIO ASIGNADO: Este trabajador no tiene turnos configurados. Aplique una plantilla rápida o defina sus días y horas de trabajo.';
  end
  else if Abs(LDiff) < 0.01 then
  begin
    pnlAlertaHorario.Color := $00EDF8ED; // Verde suave
    lblAlertaHorario.Font.Color := $001A7B1A;
    lblAlertaHorario.Caption := Format('✅ HORARIO CONFORME: El horario planificado (%s h) coincide exactamente con las horas estipuladas en contrato (%s h/sem).',
      [FormatFloat('0.00', LTotalHorasPlanificadas), FormatFloat('0.00', LHorasContrato)]);
  end
  else if LDiff > 0.01 then
  begin
    pnlAlertaHorario.Color := $00ECECFF; // Rojo/rosa suave
    lblAlertaHorario.Font.Color := $000000CC;
    lblAlertaHorario.Caption := Format('⚠️ EXCESO DE JORNADA: El horario planificado (%s h) SUPERA el contrato (%s h) en +%s horas/semana.',
      [FormatFloat('0.00', LTotalHorasPlanificadas), FormatFloat('0.00', LHorasContrato), FormatFloat('0.00', LDiff)]);
  end
  else
  begin
    pnlAlertaHorario.Color := $00FFF5EA; // Azul/ámbar suave
    lblAlertaHorario.Font.Color := $00994D00;
    lblAlertaHorario.Caption := Format('ℹ️ JORNADA INCOMPLETA: El horario planificado (%s h) es INFERIOR al contrato (%s h) en %s horas/semana.',
      [FormatFloat('0.00', LTotalHorasPlanificadas), FormatFloat('0.00', LHorasContrato), FormatFloat('0.00', LDiff)]);
  end;

  LAlertas := '';
  if LTotalDiasTrabajo = 7 then
    LAlertas := '⚠️ Alerta legal: El trabajador tiene asignados 7 días laborales sin descanso semanal obligatorio.';

  if LDiasExcesivos <> '' then
  begin
    if LAlertas <> '' then LAlertas := LAlertas + '   |   ';
    LAlertas := LAlertas + '⚠️ Días que superan la jornada diaria ordinaria de 9h: ' + LDiasExcesivos;
  end;

  lblAlertaDetalle.Caption := LAlertas;
  lblAlertaDetalle.Visible := (LAlertas <> '');
end;

procedure TfrmTrabajadoresEditor.HorarioControlChanged(Sender: TObject);
begin
  if not FUpdatingHorario then
  begin
    FHorarioModificado := True;
    lblTituloHorario.Caption := 'Planificación del Horario Semanal  [ ✏️ Horario modificado (pendiente de guardar) ]';
    lblTituloHorario.Font.Color := $000066CC;
    RecalcularHorario;
    UpdateActionButtonsState;
    if Assigned(btnGuardar) then
      btnGuardar.Enabled := True;
  end;
end;

procedure TfrmTrabajadoresEditor.edtHorasContratoChange(Sender: TObject);
begin
  if not FUpdatingHorario then
    RecalcularHorario;
end;

procedure TfrmTrabajadoresEditor.edtCosteHoraExit(Sender: TObject);
var
  LVal: Double;
  LStr: string;
begin
  LStr := Trim(edtCosteHora.Text);
  if LStr = '' then
  begin
    edtCosteHora.Text := '0,00';
    Exit;
  end;

  if SafeTryStrToFloat(LStr, LVal) and (LVal >= 0.0) then
    edtCosteHora.Text := FormatFloat('0.00', LVal)
  else
  begin
    ShowMessage('El coste por hora debe ser una cantidad numérica válida mayor o igual a cero.');
    edtCosteHora.Text := '0,00';
  end;
end;

procedure TfrmTrabajadoresEditor.btnPreset40hPartidaClick(Sender: TObject);
var
  I: Integer;
begin
  FUpdatingHorario := True;
  try
    for I := 1 to 5 do
    begin
      FchkDias[I].Checked := True;
      FedtEntradaM[I].Text := '08:00';
      FedtSalidaM[I].Text := '14:00';
      FedtEntradaT[I].Text := '16:00';
      FedtSalidaT[I].Text := '18:00';
      FedtDescanso[I].Text := '0';
      FedtObs[I].Text := 'Turno partido';
    end;
    for I := 6 to 7 do
    begin
      FchkDias[I].Checked := False;
      FedtEntradaM[I].Text := '';
      FedtSalidaM[I].Text := '';
      FedtEntradaT[I].Text := '';
      FedtSalidaT[I].Text := '';
      FedtDescanso[I].Text := '0';
      FedtObs[I].Text := 'Descanso';
    end;
  finally
    FUpdatingHorario := False;
  end;
  FHorarioModificado := True;
  lblTituloHorario.Caption := 'Planificación del Horario Semanal  [ ✏️ Horario modificado (pendiente de guardar) ]';
  lblTituloHorario.Font.Color := $000066CC;
  RecalcularHorario;
  UpdateActionButtonsState;
  if Assigned(btnGuardar) then
    btnGuardar.Enabled := True;
end;

procedure TfrmTrabajadoresEditor.btnPreset40hContinuaClick(Sender: TObject);
var
  I: Integer;
begin
  FUpdatingHorario := True;
  try
    for I := 1 to 5 do
    begin
      FchkDias[I].Checked := True;
      FedtEntradaM[I].Text := '07:00';
      FedtSalidaM[I].Text := '15:00';
      FedtEntradaT[I].Text := '';
      FedtSalidaT[I].Text := '';
      FedtDescanso[I].Text := '0';
      FedtObs[I].Text := 'Jornada continua';
    end;
    for I := 6 to 7 do
    begin
      FchkDias[I].Checked := False;
      FedtEntradaM[I].Text := '';
      FedtSalidaM[I].Text := '';
      FedtEntradaT[I].Text := '';
      FedtSalidaT[I].Text := '';
      FedtDescanso[I].Text := '0';
      FedtObs[I].Text := 'Descanso';
    end;
  finally
    FUpdatingHorario := False;
  end;
  FHorarioModificado := True;
  lblTituloHorario.Caption := 'Planificación del Horario Semanal  [ ✏️ Horario modificado (pendiente de guardar) ]';
  lblTituloHorario.Font.Color := $000066CC;
  RecalcularHorario;
  UpdateActionButtonsState;
  if Assigned(btnGuardar) then
    btnGuardar.Enabled := True;
end;

procedure TfrmTrabajadoresEditor.btnPresetMananaLSClick(Sender: TObject);
var
  I: Integer;
begin
  FUpdatingHorario := True;
  try
    for I := 1 to 6 do
    begin
      FchkDias[I].Checked := True;
      FedtEntradaM[I].Text := '08:00';
      FedtSalidaM[I].Text := '14:40';
      FedtEntradaT[I].Text := '';
      FedtSalidaT[I].Text := '';
      FedtDescanso[I].Text := '0';
      FedtObs[I].Text := 'Turno mañana L-S';
    end;
    FchkDias[7].Checked := False;
    FedtEntradaM[7].Text := '';
    FedtSalidaM[7].Text := '';
    FedtEntradaT[7].Text := '';
    FedtSalidaT[7].Text := '';
    FedtDescanso[7].Text := '0';
    FedtObs[7].Text := 'Descanso dominical';
  finally
    FUpdatingHorario := False;
  end;
  FHorarioModificado := True;
  lblTituloHorario.Caption := 'Planificación del Horario Semanal  [ ✏️ Horario modificado (pendiente de guardar) ]';
  lblTituloHorario.Font.Color := $000066CC;
  RecalcularHorario;
  UpdateActionButtonsState;
  if Assigned(btnGuardar) then
    btnGuardar.Enabled := True;
end;

procedure TfrmTrabajadoresEditor.btnCopiarLunesClick(Sender: TObject);
var
  I: Integer;
begin
  FUpdatingHorario := True;
  try
    for I := 2 to 5 do
    begin
      FchkDias[I].Checked := FchkDias[1].Checked;
      FedtEntradaM[I].Text := FedtEntradaM[1].Text;
      FedtSalidaM[I].Text := FedtSalidaM[1].Text;
      FedtEntradaT[I].Text := FedtEntradaT[1].Text;
      FedtSalidaT[I].Text := FedtSalidaT[1].Text;
      FedtDescanso[I].Text := FedtDescanso[1].Text;
      FedtObs[I].Text := FedtObs[1].Text;
    end;
  finally
    FUpdatingHorario := False;
  end;
  FHorarioModificado := True;
  lblTituloHorario.Caption := 'Planificación del Horario Semanal  [ ✏️ Horario modificado (pendiente de guardar) ]';
  lblTituloHorario.Font.Color := $000066CC;
  RecalcularHorario;
  UpdateActionButtonsState;
  if Assigned(btnGuardar) then
    btnGuardar.Enabled := True;
end;

procedure TfrmTrabajadoresEditor.btnLimpiarHorarioClick(Sender: TObject);
var
  I: Integer;
begin
  FUpdatingHorario := True;
  try
    for I := 1 to 7 do
    begin
      FchkDias[I].Checked := False;
      FedtEntradaM[I].Text := '';
      FedtSalidaM[I].Text := '';
      FedtEntradaT[I].Text := '';
      FedtSalidaT[I].Text := '';
      FedtDescanso[I].Text := '0';
      FedtObs[I].Text := '';
    end;
  finally
    FUpdatingHorario := False;
  end;
  FHorarioModificado := True;
  lblTituloHorario.Caption := 'Planificación del Horario Semanal  [ ✏️ Horario modificado (pendiente de guardar) ]';
  lblTituloHorario.Font.Color := $000066CC;
  RecalcularHorario;
  UpdateActionButtonsState;
  if Assigned(btnGuardar) then
    btnGuardar.Enabled := True;
end;

procedure TfrmTrabajadoresEditor.LoadHorario;
var
  LQry: TFDQuery;
  LDia, LMin: Integer;
  LHasAnyDia: Boolean;
begin
  FHasHorarioAsignado := False;
  FHorarioModificado := False;

  if FTrabajadorId = '' then
  begin
    btnLimpiarHorarioClick(nil);
    FHorarioModificado := False;
    lblTituloHorario.Caption := 'Planificación del Horario Semanal  [ ⚠️ SIN HORARIO ASIGNADO ]';
    lblTituloHorario.Font.Color := $000055B3;
    Exit;
  end;

  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text := 'SELECT * FROM ge_trabajadores_horario WHERE id_trabajador = :id ORDER BY dia_semana';
    LQry.ParamByName('id').AsString := FTrabajadorId;
    LQry.Open;

    if LQry.IsEmpty then
    begin
      btnLimpiarHorarioClick(nil);
      FHasHorarioAsignado := False;
      FHorarioModificado := False;
      lblTituloHorario.Caption := 'Planificación del Horario Semanal  [ ⚠️ SIN HORARIO ASIGNADO ]';
      lblTituloHorario.Font.Color := $000055B3;
      RecalcularHorario;
    end
    else
    begin
      LHasAnyDia := False;
      FUpdatingHorario := True;
      try
        for LDia := 1 to 7 do
        begin
          FchkDias[LDia].Checked := False;
          FedtEntradaM[LDia].Text := '';
          FedtSalidaM[LDia].Text := '';
          FedtEntradaT[LDia].Text := '';
          FedtSalidaT[LDia].Text := '';
          FedtDescanso[LDia].Text := '0';
          FedtObs[LDia].Text := '';
        end;

        while not LQry.Eof do
        begin
          LDia := LQry.FieldByName('dia_semana').AsInteger;
          if (LDia >= 1) and (LDia <= 7) then
          begin
            FchkDias[LDia].Checked := (LQry.FieldByName('activo').AsInteger = 1);
            if FchkDias[LDia].Checked then
              LHasAnyDia := True;

            if not LQry.FieldByName('entrada_m').IsNull then
            begin
              LMin := ParseTimeMinutes(LQry.FieldByName('entrada_m').AsString);
              if LMin >= 0 then
                FedtEntradaM[LDia].Text := Format('%.2d:%.2d', [LMin div 60, LMin mod 60])
              else
                FedtEntradaM[LDia].Text := '';
            end
            else
              FedtEntradaM[LDia].Text := '';

            if not LQry.FieldByName('salida_m').IsNull then
            begin
              LMin := ParseTimeMinutes(LQry.FieldByName('salida_m').AsString);
              if LMin >= 0 then
                FedtSalidaM[LDia].Text := Format('%.2d:%.2d', [LMin div 60, LMin mod 60])
              else
                FedtSalidaM[LDia].Text := '';
            end
            else
              FedtSalidaM[LDia].Text := '';

            if not LQry.FieldByName('entrada_t').IsNull then
            begin
              LMin := ParseTimeMinutes(LQry.FieldByName('entrada_t').AsString);
              if LMin >= 0 then
                FedtEntradaT[LDia].Text := Format('%.2d:%.2d', [LMin div 60, LMin mod 60])
              else
                FedtEntradaT[LDia].Text := '';
            end
            else
              FedtEntradaT[LDia].Text := '';

            if not LQry.FieldByName('salida_t').IsNull then
            begin
              LMin := ParseTimeMinutes(LQry.FieldByName('salida_t').AsString);
              if LMin >= 0 then
                FedtSalidaT[LDia].Text := Format('%.2d:%.2d', [LMin div 60, LMin mod 60])
              else
                FedtSalidaT[LDia].Text := '';
            end
            else
              FedtSalidaT[LDia].Text := '';

            FedtDescanso[LDia].Text := LQry.FieldByName('descanso_minutos').AsString;
            FedtObs[LDia].Text := LQry.FieldByName('observaciones').AsString;
          end;
          LQry.Next;
        end;
      finally
        FUpdatingHorario := False;
      end;

      FHasHorarioAsignado := LHasAnyDia;
      FHorarioModificado := False;
      if FHasHorarioAsignado then
      begin
        lblTituloHorario.Caption := 'Planificación del Horario Semanal  [ ✅ Horario asignado en BD ]';
        lblTituloHorario.Font.Color := $001A7B1A;
      end
      else
      begin
        lblTituloHorario.Caption := 'Planificación del Horario Semanal  [ ⚠️ SIN HORARIO ASIGNADO ]';
        lblTituloHorario.Font.Color := $000055B3;
      end;
      RecalcularHorario;
    end;
  finally
    LQry.Free;
  end;
end;

procedure TfrmTrabajadoresEditor.SaveHorario(const ATrabajadorId: string);
var
  LQry, LQryEmp: TFDQuery;
  LEmpresaId, LUserId: Integer;
  I, LEntM, LSalM, LEntT, LSalT, LMinM, LMinT, LTotMin, LMin: Integer;
  LDayHours: Double;
begin
  if ATrabajadorId = '' then Exit;

  LEmpresaId := 0;
  if cmbEmpresa.ItemIndex >= 0 then
    LEmpresaId := Integer(cmbEmpresa.Items.Objects[cmbEmpresa.ItemIndex]);

  if LEmpresaId <= 0 then
  begin
    LQryEmp := TFDQuery.Create(nil);
    try
      LQryEmp.Connection := dmgMain.dbConn;
      LQryEmp.SQL.Text := 'SELECT id_empresa FROM ge_trabajadores_empresas WHERE id_trabajador = :id ORDER BY activo DESC, id DESC LIMIT 1';
      LQryEmp.ParamByName('id').AsString := ATrabajadorId;
      LQryEmp.Open;
      if not LQryEmp.IsEmpty and not LQryEmp.FieldByName('id_empresa').IsNull then
        LEmpresaId := LQryEmp.FieldByName('id_empresa').AsInteger;
    finally
      LQryEmp.Free;
    end;
  end;

  if LEmpresaId <= 0 then
  begin
    LQryEmp := TFDQuery.Create(nil);
    try
      LQryEmp.Connection := dmgMain.dbConn;
      LQryEmp.SQL.Text := 'SELECT id_empresa FROM ge_trabajadores_contratos WHERE id_trabajador = :id ORDER BY activo DESC, id DESC LIMIT 1';
      LQryEmp.ParamByName('id').AsString := ATrabajadorId;
      LQryEmp.Open;
      if not LQryEmp.IsEmpty and not LQryEmp.FieldByName('id_empresa').IsNull then
        LEmpresaId := LQryEmp.FieldByName('id_empresa').AsInteger;
    finally
      LQryEmp.Free;
    end;
  end;

  if LEmpresaId <= 0 then
    LEmpresaId := StrToIntDef(dmgMain.CurrentCompanyId, 1);

  LUserId := StrToIntDef(dmgMain.CurrentUserId, 1);
  if LUserId <= 0 then LUserId := 1;

  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text :=
      'INSERT INTO ge_trabajadores_horario ( ' +
      '  id_empresa, id_trabajador, dia_semana, activo, ' +
      '  entrada_m, salida_m, entrada_t, salida_t, descanso_minutos, horas_dia, observaciones, ' +
      '  id_user_creator, id_user_update ' +
      ') VALUES ( ' +
      '  :id_empresa, :id_trabajador, :dia_semana, :activo, ' +
      '  :entrada_m, :salida_m, :entrada_t, :salida_t, :descanso_minutos, :horas_dia, :observaciones, ' +
      '  :user_creator, :user_update ' +
      ') ON DUPLICATE KEY UPDATE ' +
      '  id_empresa = VALUES(id_empresa), ' +
      '  activo = VALUES(activo), ' +
      '  entrada_m = VALUES(entrada_m), ' +
      '  salida_m = VALUES(salida_m), ' +
      '  entrada_t = VALUES(entrada_t), ' +
      '  salida_t = VALUES(salida_t), ' +
      '  descanso_minutos = VALUES(descanso_minutos), ' +
      '  horas_dia = VALUES(horas_dia), ' +
      '  observaciones = VALUES(observaciones), ' +
      '  id_user_update = VALUES(id_user_update)';

    for I := 1 to 7 do
    begin
      LDayHours := 0.0;
      if FchkDias[I].Checked then
      begin
        LMinM := 0;
        LMinT := 0;

        LEntM := ParseTimeMinutes(FedtEntradaM[I].Text);
        LSalM := ParseTimeMinutes(FedtSalidaM[I].Text);
        if (LEntM >= 0) and (LSalM >= LEntM) then
          LMinM := LSalM - LEntM;

        LEntT := ParseTimeMinutes(FedtEntradaT[I].Text);
        LSalT := ParseTimeMinutes(FedtSalidaT[I].Text);
        if (LEntT >= 0) and (LSalT >= LEntT) then
          LMinT := LSalT - LEntT;

        LTotMin := LMinM + LMinT - StrToIntDef(Trim(FedtDescanso[I].Text), 0);
        if LTotMin < 0 then LTotMin := 0;
        LDayHours := LTotMin / 60.0;
      end;

      LQry.ParamByName('id_empresa').AsInteger := LEmpresaId;
      LQry.ParamByName('id_trabajador').AsInteger := StrToIntDef(ATrabajadorId, 0);
      LQry.ParamByName('dia_semana').AsInteger := I;
      LQry.ParamByName('activo').AsInteger := Ord(FchkDias[I].Checked);

      LMin := ParseTimeMinutes(FedtEntradaM[I].Text);
      LQry.ParamByName('entrada_m').DataType := ftString;
      if (LMin >= 0) and FchkDias[I].Checked then
        LQry.ParamByName('entrada_m').AsString := Format('%.2d:%.2d:00', [LMin div 60, LMin mod 60])
      else
        LQry.ParamByName('entrada_m').Clear;

      LMin := ParseTimeMinutes(FedtSalidaM[I].Text);
      LQry.ParamByName('salida_m').DataType := ftString;
      if (LMin >= 0) and FchkDias[I].Checked then
        LQry.ParamByName('salida_m').AsString := Format('%.2d:%.2d:00', [LMin div 60, LMin mod 60])
      else
        LQry.ParamByName('salida_m').Clear;

      LMin := ParseTimeMinutes(FedtEntradaT[I].Text);
      LQry.ParamByName('entrada_t').DataType := ftString;
      if (LMin >= 0) and FchkDias[I].Checked then
        LQry.ParamByName('entrada_t').AsString := Format('%.2d:%.2d:00', [LMin div 60, LMin mod 60])
      else
        LQry.ParamByName('entrada_t').Clear;

      LMin := ParseTimeMinutes(FedtSalidaT[I].Text);
      LQry.ParamByName('salida_t').DataType := ftString;
      if (LMin >= 0) and FchkDias[I].Checked then
        LQry.ParamByName('salida_t').AsString := Format('%.2d:%.2d:00', [LMin div 60, LMin mod 60])
      else
        LQry.ParamByName('salida_t').Clear;

      LQry.ParamByName('descanso_minutos').AsInteger := StrToIntDef(Trim(FedtDescanso[I].Text), 0);
      LQry.ParamByName('horas_dia').AsFloat := LDayHours;
      LQry.ParamByName('observaciones').DataType := ftWideString;
      LQry.ParamByName('observaciones').AsWideString := Trim(FedtObs[I].Text);
      LQry.ParamByName('user_creator').AsInteger := LUserId;
      LQry.ParamByName('user_update').AsInteger := LUserId;

      LQry.ExecSQL;
    end;

    FHorarioModificado := False;
    FHasHorarioAsignado := True;
    lblTituloHorario.Caption := 'Planificación del Horario Semanal  [ ✅ Horario asignado en BD ]';
    lblTituloHorario.Font.Color := $001A7B1A;
  finally
    LQry.Free;
  end;
end;

procedure TfrmTrabajadoresEditor.btnSaveClick(Sender: TObject);
var
  LQry, LQryEmp: TFDQuery;
  LEmpresaId, LGrupoId, I: Integer;
  LIsInsert: Boolean;
  LNewId: string;
  LCosteHora: Double;
  LCosteStr: string;
begin
  if Trim(edtCodigo.Text) = '' then
  begin
    ShowMessage('Debe indicar un código de trabajador.');
    pgcDetails.ActivePage := tsDatosEnvio;
    edtCodigo.SetFocus;
    Exit;
  end;

  if Trim(edtNif.Text) = '' then
  begin
    ShowMessage('Debe indicar el NIF / NIE del trabajador.');
    pgcDetails.ActivePage := tsDatosEnvio;
    edtNif.SetFocus;
    Exit;
  end;

  if Trim(edtNombre.Text) = '' then
  begin
    ShowMessage('Debe indicar el nombre del trabajador.');
    pgcDetails.ActivePage := tsDatosEnvio;
    edtNombre.SetFocus;
    Exit;
  end;

  if cmbEmpresa.ItemIndex < 0 then
  begin
    ShowMessage('Debe seleccionar una empresa.');
    pgcDetails.ActivePage := tsDatosLaborales;
    cmbEmpresa.SetFocus;
    Exit;
  end;

  LEmpresaId := Integer(cmbEmpresa.Items.Objects[cmbEmpresa.ItemIndex]);

  LIsInsert := (FTrabajadorId = '');

  // Si el trabajador ya tiene un contrato registrado, mantener SIEMPRE la empresa del contrato
  if not LIsInsert then
  begin
    LQryEmp := TFDQuery.Create(nil);
    try
      LQryEmp.Connection := dmgMain.dbConn;
      LQryEmp.SQL.Text :=
        'SELECT id_empresa FROM ge_trabajadores_contratos ' +
        'WHERE id_trabajador = :id ' +
        'ORDER BY activo DESC, fecha_alta DESC, id DESC LIMIT 1';
      LQryEmp.ParamByName('id').AsString := FTrabajadorId;
      LQryEmp.Open;
      if not LQryEmp.IsEmpty and not LQryEmp.FieldByName('id_empresa').IsNull and (LQryEmp.FieldByName('id_empresa').AsInteger > 0) then
      begin
        LEmpresaId := LQryEmp.FieldByName('id_empresa').AsInteger;
        for I := 0 to cmbEmpresa.Items.Count - 1 do
        begin
          if Integer(cmbEmpresa.Items.Objects[I]) = LEmpresaId then
          begin
            cmbEmpresa.ItemIndex := I;
            Break;
          end;
        end;
      end;
    finally
      LQryEmp.Free;
    end;
  end;

  if not SafeTryStrToFloat(edtCosteHora.Text, LCosteHora) or (LCosteHora < 0.0) then
  begin
    ShowMessage('Debe indicar un coste por hora válido (mayor o igual a cero).');
    pgcDetails.ActivePage := tsDatosLaborales;
    edtCosteHora.SetFocus;
    Exit;
  end;

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

    LGrupoId := 0;
    if cmbGrupo.ItemIndex >= 0 then
      LGrupoId := Integer(cmbGrupo.Items.Objects[cmbGrupo.ItemIndex]);

    if LIsInsert then
    begin
      LQry.SQL.Text :=
        'INSERT INTO ge_trabajadores ( ' +
        '  codigo, nif, nombre, apellidos, nombre_corto, direccion, cp, ' +
        '  poblacion, provincia, telefono1, telefono2, email, fecha_nacimiento, ' +
        '  nacionalidad, sexo, pagar_por_banco, cb_iban, cb_banco, cb_oficina, cb_dc, ' +
        '  cb_cc, regimen_cot, num_seg_social, horas_contrato, jornada_continua, ' +
        '  salario_base_dia, coste_hora, fecha_alta, fecha_baja, irpf, desempleo, formacion, ' +
        '  contingencias, retencion_me, activo, fijo, fijo_discontinuo, temporal, ' +
        '  control_calidad, responsable, fumigador, num_taquilla, observaciones, ' +
        '  convocatoria, grupo, id_user_creator, id_user_update ' +
        ') VALUES ( ' +
        '  :codigo, :nif, :nombre, :apellidos, :nombre_corto, :direccion, :cp, ' +
        '  :poblacion, :provincia, :telefono1, :telefono2, :email, :fecha_nacimiento, ' +
        '  :nacionalidad, :sexo, :pagar_por_banco, :cb_iban, :cb_banco, :cb_oficina, :cb_dc, ' +
        '  :cb_cc, :regimen_cot, :num_seg_social, :horas_contrato, :jornada_continua, ' +
        '  :salario_base_dia, :coste_hora, :fecha_alta, :fecha_baja, :irpf, :desempleo, :formacion, ' +
        '  :contingencias, :retencion_me, :activo, :fijo, :fijo_discontinuo, :temporal, ' +
        '  :control_calidad, :responsable, :fumigador, :num_taquilla, :observaciones, ' +
        '  :convocatoria, :grupo, :user, :user ' +
        ')';
      LQry.ParamByName('user').AsString := dmgMain.CurrentUserId;
    end
    else
    begin
      LQry.SQL.Text :=
        'UPDATE ge_trabajadores SET ' +
        '  codigo = :codigo, nif = :nif, nombre = :nombre, ' +
        '  apellidos = :apellidos, nombre_corto = :nombre_corto, direccion = :direccion, ' +
        '  cp = :cp, poblacion = :poblacion, provincia = :provincia, telefono1 = :telefono1, ' +
        '  telefono2 = :telefono2, email = :email, fecha_nacimiento = :fecha_nacimiento, ' +
        '  nacionalidad = :nacionalidad, sexo = :sexo, pagar_por_banco = :pagar_por_banco, ' +
        '  cb_iban = :cb_iban, cb_banco = :cb_banco, cb_oficina = :cb_oficina, cb_dc = :cb_dc, ' +
        '  cb_cc = :cb_cc, regimen_cot = :regimen_cot, num_seg_social = :num_seg_social, ' +
        '  horas_contrato = :horas_contrato, jornada_continua = :jornada_continua, ' +
        '  salario_base_dia = :salario_base_dia, coste_hora = :coste_hora, fecha_alta = :fecha_alta, ' +
        '  fecha_baja = :fecha_baja, irpf = :irpf, desempleo = :desempleo, ' +
        '  formacion = :formacion, contingencias = :contingencias, ' +
        '  retencion_me = :retencion_me, activo = :activo, fijo = :fijo, ' +
        '  fijo_discontinuo = :fijo_discontinuo, temporal = :temporal, ' +
        '  control_calidad = :control_calidad, responsable = :responsable, ' +
        '  fumigador = :fumigador, num_taquilla = :num_taquilla, ' +
        '  convocatoria = :convocatoria, grupo = :grupo, ' +
        '  observaciones = :observaciones, id_user_update = :user ' +
        'WHERE id = :id';
      LQry.ParamByName('id').AsString := FTrabajadorId;
      LQry.ParamByName('user').AsString := dmgMain.CurrentUserId;
    end;

    LQry.ParamByName('codigo').AsInteger := StrToIntDef(Trim(edtCodigo.Text), 0);
    LQry.ParamByName('nif').AsString := Trim(edtNif.Text);
    LQry.ParamByName('nombre').AsString := Trim(edtNombre.Text);
    LQry.ParamByName('apellidos').AsString := Trim(edtApellidos.Text);
    LQry.ParamByName('nombre_corto').AsString := Trim(edtNombreCorto.Text);
    LQry.ParamByName('direccion').AsString := Trim(edtDireccion.Text);
    LQry.ParamByName('cp').AsString := Trim(edtCp.Text);
    LQry.ParamByName('poblacion').AsString := Trim(edtPoblacion.Text);
    LQry.ParamByName('provincia').AsString := Trim(edtProvincia.Text);
    LQry.ParamByName('telefono1').AsString := Trim(edtTelefono1.Text);
    LQry.ParamByName('telefono2').AsString := Trim(edtTelefono2.Text);
    LQry.ParamByName('email').AsString := Trim(edtEmail.Text);

    LQry.ParamByName('fecha_nacimiento').DataType := ftDate;
    if dtpFechaNac.Checked then
      LQry.ParamByName('fecha_nacimiento').AsDate := dtpFechaNac.Date
    else
      LQry.ParamByName('fecha_nacimiento').Clear;

    LQry.ParamByName('nacionalidad').AsString := Trim(edtNacionalidad.Text);
    LQry.ParamByName('sexo').AsString := cmbSexo.Text;
    LQry.ParamByName('pagar_por_banco').AsInteger := Ord(chkPagarPorBanco.Checked);
    LQry.ParamByName('cb_iban').AsString := Trim(edtIban.Text);
    LQry.ParamByName('cb_banco').AsString := Trim(edtBanco.Text);
    LQry.ParamByName('cb_oficina').AsString := Trim(edtOficina.Text);
    LQry.ParamByName('cb_dc').AsString := Trim(edtDc.Text);
    LQry.ParamByName('cb_cc').AsString := Trim(edtCc.Text);
    LQry.ParamByName('regimen_cot').AsString := cmbRegimenCot.Text;
    LQry.ParamByName('num_seg_social').AsString := Trim(edtNumSS.Text);
    LQry.ParamByName('horas_contrato').AsFloat := SafeStrToFloat(edtHorasContrato.Text, 40.0);
    LQry.ParamByName('jornada_continua').AsInteger := Ord(chkJornadaContinua.Checked);
    LQry.ParamByName('salario_base_dia').AsFloat := SafeStrToFloat(edtSalarioBase.Text, 0.0);
    LQry.ParamByName('coste_hora').AsFloat := LCosteHora;
    LQry.ParamByName('fecha_alta').AsDate := dtpFechaAlta.Date;

    LQry.ParamByName('fecha_baja').DataType := ftDate;
    if dtpFechaBaja.Checked then
      LQry.ParamByName('fecha_baja').AsDate := dtpFechaBaja.Date
    else
      LQry.ParamByName('fecha_baja').Clear;

    LQry.ParamByName('irpf').AsFloat := SafeStrToFloat(edtIrpf.Text, 0.0);
    LQry.ParamByName('desempleo').AsFloat := SafeStrToFloat(edtDesempleo.Text, 0.0);
    LQry.ParamByName('formacion').AsFloat := SafeStrToFloat(edtFormacion.Text, 0.0);
    LQry.ParamByName('contingencias').AsFloat := SafeStrToFloat(edtContingencias.Text, 0.0);
    LQry.ParamByName('retencion_me').AsFloat := SafeStrToFloat(edtRetencionMe.Text, 0.0);
    LQry.ParamByName('activo').AsInteger := Ord(chkActivo.Checked);

    LQry.ParamByName('fijo').AsInteger := Ord(chkFijo.Checked);
    LQry.ParamByName('fijo_discontinuo').AsInteger := Ord(chkFijoDiscontinuo.Checked);
    LQry.ParamByName('temporal').AsInteger := Ord(chkTemporal.Checked);

    LQry.ParamByName('control_calidad').AsInteger := Ord(chkControlCalidad.Checked);
    LQry.ParamByName('responsable').AsInteger := Ord(chkResponsable.Checked);
    LQry.ParamByName('fumigador').AsInteger := Ord(chkFumigador.Checked);

    LQry.ParamByName('num_taquilla').AsInteger := StrToIntDef(Trim(edtNumTaquilla.Text), 0);
    LQry.ParamByName('observaciones').AsString := Trim(memObservaciones.Text);
    LQry.ParamByName('convocatoria').AsString := Trim(cmbConvocatoria.Text);
    LQry.ParamByName('grupo').DataType := ftInteger;
    if LGrupoId > 0 then
      LQry.ParamByName('grupo').AsInteger := LGrupoId
    else
      LQry.ParamByName('grupo').Clear;

    LQry.ExecSQL;

    if LIsInsert then
    begin
      LQry.SQL.Text := 'SELECT LAST_INSERT_ID() AS new_id';
      LQry.Open;
      LNewId := LQry.FieldByName('new_id').AsString;
      FTrabajadorId := LNewId;
    end
    else
      LNewId := FTrabajadorId;

    // Guardar asignación en ge_trabajadores_empresas
    LQry.Close;
    LQry.SQL.Text :=
      'INSERT INTO ge_trabajadores_empresas ( ' +
      '  id_trabajador, id_empresa, codigo, id_grupo, activo, fecha_alta, fecha_baja, horas_contrato, id_user_creator, id_user_update ' +
      ') VALUES ( ' +
      '  :id_trab, :id_emp, :codigo, :id_grupo, :activo, :alta, :baja, :horas, :user, :user ' +
      ') ON DUPLICATE KEY UPDATE ' +
      '  codigo = VALUES(codigo), ' +
      '  id_grupo = VALUES(id_grupo), ' +
      '  activo = VALUES(activo), ' +
      '  fecha_alta = VALUES(fecha_alta), ' +
      '  fecha_baja = VALUES(fecha_baja), ' +
      '  horas_contrato = VALUES(horas_contrato), ' +
      '  id_user_update = VALUES(id_user_update)';
    LQry.ParamByName('id_trab').AsString := LNewId;
    LQry.ParamByName('id_emp').AsInteger := LEmpresaId;
    LQry.ParamByName('codigo').AsInteger := StrToIntDef(Trim(edtCodigo.Text), 0);
    LQry.ParamByName('id_grupo').DataType := ftInteger;
    if LGrupoId > 0 then
      LQry.ParamByName('id_grupo').AsInteger := LGrupoId
    else
      LQry.ParamByName('id_grupo').Clear;
    LQry.ParamByName('activo').AsInteger := Ord(chkActivo.Checked);
    LQry.ParamByName('alta').AsDate := dtpFechaAlta.Date;
    LQry.ParamByName('baja').DataType := ftDate;
    if dtpFechaBaja.Checked then
      LQry.ParamByName('baja').AsDate := dtpFechaBaja.Date
    else
      LQry.ParamByName('baja').Clear;
    LQry.ParamByName('horas').AsFloat := SafeStrToFloat(edtHorasContrato.Text, 40.0);
    LQry.ParamByName('user').AsString := dmgMain.CurrentUserId;
    LQry.ExecSQL;

    // Garantizar que no esté activo en dos empresas a la vez
    if chkActivo.Checked then
    begin
      LQry.Close;
      LQry.SQL.Text := 'UPDATE ge_trabajadores_empresas SET activo = 0 WHERE id_trabajador = :id_trab AND id_empresa <> :id_emp';
      LQry.ParamByName('id_trab').AsString := LNewId;
      LQry.ParamByName('id_emp').AsInteger := LEmpresaId;
      LQry.ExecSQL;
    end;

    SaveCapacitacion(LNewId);
    SaveHorario(LNewId);
    FHorarioModificado := False;

    ResetChangeTracking;
    Caption := 'Editar Ficha del Trabajador (ID: ' + FTrabajadorId + ')';
    if Assigned(frmTrabajadores) then
      frmTrabajadores.btnRefreshClick(nil);
    ShowMessage('Ficha de trabajador guardada correctamente.');
    ModalResult := mrOk;
  except
    on E: Exception do
      TDbErrorHandler.HandleException(E, 'Guardar Trabajador');
  end;
  LQry.Free;
end;

procedure TfrmTrabajadoresEditor.btnSalirClick(Sender: TObject);
begin
  ModalResult := mrCancel;
end;

function TfrmTrabajadoresEditor.CheckSavePrompt: Boolean;
begin
  Result := True;
  if HasUnsavedChanges then
  begin
    case MessageDlg('Hay modificaciones sin guardar en la ficha actual.' + sLineBreak +
                    '¿Desea guardarlas antes de continuar?',
                    mtConfirmation, [mbYes, mbNo, mbCancel], 0) of
      mrYes:
        begin
          btnSaveClick(nil);
          Result := not HasUnsavedChanges;
        end;
      mrNo:
        Result := True;
      mrCancel:
        Result := False;
    end;
  end;
end;

function TfrmTrabajadoresEditor.GetListDataset: TFDQuery;
begin
  Result := nil;
  if Assigned(Owner) and (Owner is TfrmBaseList) then
    Result := TfrmBaseList(Owner).QryMain
  else if Assigned(frmTrabajadores) and Assigned(frmTrabajadores.QryMain) and frmTrabajadores.QryMain.Active then
    Result := frmTrabajadores.QryMain;
end;

procedure TfrmTrabajadoresEditor.ClearFields;
var
  LQry: TFDQuery;
  LNextCod: Integer;
begin
  FTrabajadorId := '';
  Caption := 'Nuevo Trabajador';

  LNextCod := 1;
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text := 'SELECT COALESCE(MAX(codigo), 0) + 1 AS next_cod FROM ge_trabajadores';
    LQry.Open;
    if not LQry.IsEmpty then
      LNextCod := LQry.FieldByName('next_cod').AsInteger;
  finally
    LQry.Free;
  end;

  // Tab 1: Personales
  edtCodigo.Text := IntToStr(LNextCod);
  edtNif.Clear;
  edtNombre.Clear;
  edtApellidos.Clear;
  edtNombreCorto.Clear;
  chkActivo.Checked := True;
  dtpFechaNac.Checked := False;
  edtNacionalidad.Text := 'ESPAÑOLA';
  if cmbSexo.Items.Count > 0 then cmbSexo.ItemIndex := 0;
  cmbConvocatoria.Text := '';
  edtDireccion.Clear;
  edtCp.Clear;
  edtPoblacion.Clear;
  edtProvincia.Clear;
  edtTelefono1.Clear;
  edtTelefono2.Clear;
  edtEmail.Clear;
  edtNumTaquilla.Clear;
  if cmbGrupo.Items.Count > 0 then cmbGrupo.ItemIndex := 0;
  if cmbGrupoLaboral.Items.Count > 0 then cmbGrupoLaboral.ItemIndex := 0;
  memObservaciones.Clear;
  imgFoto.Picture := nil;

  // Tab 2: Laborales y Nómina
  if cmbEmpresa.Items.Count > 0 then cmbEmpresa.ItemIndex := 0;
  if cmbRegimenCot.Items.Count > 0 then cmbRegimenCot.ItemIndex := 0;
  edtNumSS.Clear;
  edtHorasContrato.Text := '40.00';
  chkJornadaContinua.Checked := True;
  edtSalarioBase.Text := '0.00';
  edtCosteHora.Text := '0.00';
  dtpFechaAlta.Date := Date;
  dtpFechaBaja.Checked := False;

  chkFijo.Checked := False;
  chkFijoDiscontinuo.Checked := False;
  chkTemporal.Checked := False;

  chkControlCalidad.Checked := False;
  chkResponsable.Checked := False;
  chkFumigador.Checked := False;

  chkPagarPorBanco.Checked := False;
  edtIban.Text := 'ES';
  edtBanco.Clear;
  edtOficina.Clear;
  edtDc.Clear;
  edtCc.Clear;

  edtIrpf.Text := '0.00';
  edtDesempleo.Text := '0.00';
  edtFormacion.Text := '0.00';
  edtContingencias.Text := '0.00';
  edtRetencionMe.Text := '0.00';

  // Tab 4: Formación
  chkManipulador.Checked := False;
  dtpFechaManipulador.Checked := False;
  chkCursoAlergenos.Checked := False;
  dtpFechaAlergenos.Checked := False;
  dtpRecMedico.Checked := False;
  dtpRenRecMedico.Checked := False;
  chkPrlEpis.Checked := False;
  chkPrimerosAuxilios.Checked := False;
  edtCarnetConducir.Clear;
  dtpCaducidadConducir.Checked := False;
  dtpPermisoTrabajo.Checked := False;
  memObsCapacitacion.Clear;

  // Tab 5, 6, 7
  qryDocumentos.Close;
  qryHistorial.Close;
  qryContratos.Close;
  CargarHorarioPactadoBase;
  CargarCalendarioFichajes;
  btnLimpiarHorarioClick(nil);
  FHasHorarioAsignado := False;
  FHorarioModificado := False;
  lblTituloHorario.Caption := 'Planificación del Horario Semanal  [ ⚠️ SIN HORARIO ASIGNADO ]';
  lblTituloHorario.Font.Color := $000055B3;

  pgcDetails.ActivePage := tsDatosEnvio;
  edtNif.SetFocus;
  ResetChangeTracking;
end;

function TfrmTrabajadoresEditor.HasUnsavedChanges: Boolean;
begin
  Result := inherited HasUnsavedChanges or FHorarioModificado;
end;

procedure TfrmTrabajadoresEditor.DoAnadir;
begin
  if not CheckSavePrompt then Exit;
  ClearFields;
end;

procedure TfrmTrabajadoresEditor.DoModificar;
begin
  pgcDetails.ActivePage := tsDatosEnvio;
  edtNombre.SetFocus;
end;

procedure TfrmTrabajadoresEditor.DoPrimero;
var
  LDS: TFDQuery;
  LQry: TFDQuery;
begin
  if not CheckSavePrompt then Exit;

  LDS := GetListDataset;
  if Assigned(LDS) and LDS.Active and not LDS.IsEmpty then
  begin
    LDS.First;
    FTrabajadorId := LDS.FieldByName('id').AsString;
  end
  else
  begin
    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;
      LQry.SQL.Text := 'SELECT id FROM ge_trabajadores ORDER BY nombre ASC, apellidos ASC LIMIT 1';
      LQry.Open;
      if not LQry.IsEmpty then
        FTrabajadorId := LQry.FieldByName('id').AsString
      else
        Exit;
    finally
      LQry.Free;
    end;
  end;

  LoadData;
end;

procedure TfrmTrabajadoresEditor.DoAnterior;
var
  LDS: TFDQuery;
  LQry: TFDQuery;
begin
  if not CheckSavePrompt then Exit;

  LDS := GetListDataset;
  if Assigned(LDS) and LDS.Active and not LDS.IsEmpty then
  begin
    if not LDS.Bof then
      LDS.Prior;
    FTrabajadorId := LDS.FieldByName('id').AsString;
  end
  else
  begin
    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;
      LQry.SQL.Text := 'SELECT id FROM ge_trabajadores WHERE id < :cur_id ORDER BY id DESC LIMIT 1';
      LQry.ParamByName('cur_id').AsString := FTrabajadorId;
      LQry.Open;
      if not LQry.IsEmpty then
        FTrabajadorId := LQry.FieldByName('id').AsString
      else
        Exit;
    finally
      LQry.Free;
    end;
  end;

  LoadData;
end;

procedure TfrmTrabajadoresEditor.DoSiguiente;
var
  LDS: TFDQuery;
  LQry: TFDQuery;
begin
  if not CheckSavePrompt then Exit;

  LDS := GetListDataset;
  if Assigned(LDS) and LDS.Active and not LDS.IsEmpty then
  begin
    if not LDS.Eof then
      LDS.Next;
    FTrabajadorId := LDS.FieldByName('id').AsString;
  end
  else
  begin
    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;
      LQry.SQL.Text := 'SELECT id FROM ge_trabajadores WHERE id > :cur_id ORDER BY id ASC LIMIT 1';
      LQry.ParamByName('cur_id').AsString := FTrabajadorId;
      LQry.Open;
      if not LQry.IsEmpty then
        FTrabajadorId := LQry.FieldByName('id').AsString
      else
        Exit;
    finally
      LQry.Free;
    end;
  end;

  LoadData;
end;

procedure TfrmTrabajadoresEditor.DoUltimo;
var
  LDS: TFDQuery;
  LQry: TFDQuery;
begin
  if not CheckSavePrompt then Exit;

  LDS := GetListDataset;
  if Assigned(LDS) and LDS.Active and not LDS.IsEmpty then
  begin
    LDS.Last;
    FTrabajadorId := LDS.FieldByName('id').AsString;
  end
  else
  begin
    LQry := TFDQuery.Create(nil);
    try
      LQry.Connection := dmgMain.dbConn;
      LQry.SQL.Text := 'SELECT id FROM ge_trabajadores ORDER BY nombre DESC, apellidos DESC LIMIT 1';
      LQry.Open;
      if not LQry.IsEmpty then
        FTrabajadorId := LQry.FieldByName('id').AsString
      else
        Exit;
    finally
      LQry.Free;
    end;
  end;

  LoadData;
end;

procedure TfrmTrabajadoresEditor.DoDuplicar;
var
  LQry: TFDQuery;
  LNextCod: Integer;
begin
  if not CheckSavePrompt then Exit;

  FTrabajadorId := '';
  Caption := 'Nuevo Trabajador (Copia)';

  LNextCod := 1;
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text := 'SELECT COALESCE(MAX(codigo), 0) + 1 AS next_cod FROM ge_trabajadores';
    LQry.Open;
    if not LQry.IsEmpty then
      LNextCod := LQry.FieldByName('next_cod').AsInteger;
  finally
    LQry.Free;
  end;

  edtCodigo.Text := IntToStr(LNextCod);
  edtNif.Clear;
  edtNombre.Text := Trim(edtNombre.Text) + ' (COPIA)';
  imgFoto.Picture := nil;
  qryDocumentos.Close;
  qryHistorial.Close;
  qryContratos.Close;

  pgcDetails.ActivePage := tsDatosEnvio;
  edtNif.SetFocus;
  ResetChangeTracking;
end;

procedure TfrmTrabajadoresEditor.btnEliminarClick(Sender: TObject);
var
  LQry: TFDQuery;
  LCount: Integer;
begin
  if FTrabajadorId = '' then
  begin
    ShowMessage('No hay ningún trabajador seleccionado para eliminar.');
    Exit;
  end;

  if MessageDlg('¿Está seguro de que desea eliminar al trabajador "' + Trim(edtNombre.Text + ' ' + edtApellidos.Text) + '"?',
                mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
    Exit;

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

    // Comprobar si tiene fichajes asociados
    LQry.SQL.Text := 'SELECT COUNT(*) AS total FROM ge_trabajadores_fichajes WHERE id_trabajador = :id';
    LQry.ParamByName('id').AsString := FTrabajadorId;
    LQry.Open;
    LCount := LQry.FieldByName('total').AsInteger;
    LQry.Close;

    if LCount > 0 then
    begin
      if MessageDlg('Este trabajador tiene ' + IntToStr(LCount) + ' fichajes registrados.' + sLineBreak +
                    '¿Desea darlo de baja (marcar como Inactivo) en lugar de borrarlo físicamente para conservar el histórico?',
                    mtConfirmation, [mbYes, mbNo], 0) = mrYes then
      begin
        LQry.SQL.Text := 'UPDATE ge_trabajadores SET activo = 0 WHERE id = :id';
        LQry.ParamByName('id').AsString := FTrabajadorId;
        LQry.ExecSQL;

        LQry.SQL.Text := 'UPDATE ge_trabajadores_empresas SET activo = 0 WHERE id_trabajador = :id';
        LQry.ParamByName('id').AsString := FTrabajadorId;
        LQry.ExecSQL;

        chkActivo.Checked := False;
        ResetChangeTracking;
        ShowMessage('El trabajador ha sido marcado como Inactivo.');
        if Assigned(frmTrabajadores) then
          frmTrabajadores.btnRefreshClick(nil);
        Exit;
      end
      else
        Exit;
    end;

    // Borrado de registros vinculados
    LQry.SQL.Text := 'DELETE FROM ge_trabajadores_empresas WHERE id_trabajador = :id';
    LQry.ParamByName('id').AsString := FTrabajadorId;
    LQry.ExecSQL;

    LQry.SQL.Text := 'DELETE FROM ge_trabajadores_horario WHERE id_trabajador = :id';
    LQry.ParamByName('id').AsString := FTrabajadorId;
    LQry.ExecSQL;

    LQry.SQL.Text := 'DELETE FROM ge_trabajadores_capacitacion WHERE id_trabajador = :id';
    LQry.ParamByName('id').AsString := FTrabajadorId;
    LQry.ExecSQL;

    LQry.SQL.Text := 'DELETE FROM ge_documentos WHERE id_trabajador = :id';
    LQry.ParamByName('id').AsString := FTrabajadorId;
    LQry.ExecSQL;

    LQry.SQL.Text := 'DELETE FROM ge_trabajadores WHERE id = :id';
    LQry.ParamByName('id').AsString := FTrabajadorId;
    LQry.ExecSQL;

    ShowMessage('Trabajador eliminado correctamente.');

    if Assigned(frmTrabajadores) then
      frmTrabajadores.btnRefreshClick(nil);

    DoAnterior;
  finally
    LQry.Free;
  end;
end;

procedure TfrmTrabajadoresEditor.btnBuscarClick(Sender: TObject);
var
  LSearch: string;
  LQry: TFDQuery;
  LCode: Integer;
begin
  if not CheckSavePrompt then Exit;

  LSearch := InputBox('Buscar Trabajador', 'Introduzca Código, NIF o Nombre a buscar:', '');
  LSearch := Trim(LSearch);
  if LSearch = '' then Exit;

  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := dmgMain.dbConn;
    if TryStrToInt(LSearch, LCode) then
    begin
      LQry.SQL.Text :=
        'SELECT id FROM ge_trabajadores ' +
        'WHERE codigo = :code OR nif LIKE :s OR nombre LIKE :s OR apellidos LIKE :s ' +
        'ORDER BY nombre ASC LIMIT 1';
      LQry.ParamByName('code').AsInteger := LCode;
      LQry.ParamByName('s').AsString := '%' + LSearch + '%';
    end
    else
    begin
      LQry.SQL.Text :=
        'SELECT id FROM ge_trabajadores ' +
        'WHERE nif LIKE :s OR nombre LIKE :s OR apellidos LIKE :s ' +
        'ORDER BY nombre ASC LIMIT 1';
      LQry.ParamByName('s').AsString := '%' + LSearch + '%';
    end;
    LQry.Open;

    if not LQry.IsEmpty then
    begin
      FTrabajadorId := LQry.FieldByName('id').AsString;
      Caption := 'Editar Ficha del Trabajador (ID: ' + FTrabajadorId + ')';
      LoadData;
      pgcDetails.ActivePage := tsDatosEnvio;
    end
    else
      ShowMessage('No se ha encontrado ningún trabajador que coincida con la búsqueda.');
  finally
    LQry.Free;
  end;
end;

// ---------------------------------------------------------------------------
// Pestaña 4: Documentación Digitalizada (ge_documentos)
// ---------------------------------------------------------------------------
procedure TfrmTrabajadoresEditor.LoadDocuments;
begin
  if FTrabajadorId = '' then
  begin
    qryDocumentos.Close;
    Exit;
  end;

  qryDocumentos.Close;
  qryDocumentos.SQL.Text :=
    'SELECT id, fecha, descripcion, nombre_archivo, extension ' +
    'FROM ge_documentos ' +
    'WHERE id_trabajador = :id ' +
    'ORDER BY fecha DESC, id DESC';
  qryDocumentos.ParamByName('id').AsString := FTrabajadorId;
  qryDocumentos.Open;

  ConfigurarColumnasDocumentos;
end;

procedure TfrmTrabajadoresEditor.ConfigurarColumnasDocumentos;
var
  i: Integer;
  Col: TColumn;
begin
  if not Assigned(dbgDocumentos) or not qryDocumentos.Active then Exit;

  for i := 0 to dbgDocumentos.Columns.Count - 1 do
  begin
    Col := dbgDocumentos.Columns[i];
    if SameText(Col.FieldName, 'fecha') then
    begin
      Col.Title.Caption := 'Fecha';
      Col.Width := 90;
      Col.Alignment := taCenter;
    end
    else if SameText(Col.FieldName, 'descripcion') then
    begin
      Col.Title.Caption := 'Descripción';
      Col.Width := 260;
    end
    else if SameText(Col.FieldName, 'nombre_archivo') then
    begin
      Col.Title.Caption := 'Nombre de Archivo';
      Col.Width := 300;
    end
    else if SameText(Col.FieldName, 'extension') then
    begin
      Col.Title.Caption := 'Ext.';
      Col.Width := 50;
      Col.Alignment := taCenter;
    end
    else
      Col.Visible := False;
  end;
end;

procedure TfrmTrabajadoresEditor.dbgDocumentosTitleClick(Column: TColumn);
begin
  if Assigned(qryDocumentos) and qryDocumentos.Active then
  begin
    try
      qryDocumentos.IndexFieldNames := Column.FieldName;
    except
    end;
  end;
end;

procedure TfrmTrabajadoresEditor.dbgDocumentosDblClick(Sender: TObject);
begin
  btnVerDocClick(Sender);
end;

procedure TfrmTrabajadoresEditor.btnSubirDocClick(Sender: TObject);
var
  LDesc, LFileName, LExtension: string;
  Stream: TMemoryStream;
  Qry: TFDQuery;
  LEmpId: string;
begin
  if FTrabajadorId = '' then
  begin
    ShowMessage('Guarde primero los datos del trabajador antes de adjuntar documentos.');
    Exit;
  end;

  LDesc := InputBox('Adjuntar Documento', 'Introduzca una descripción para el documento (ej. DNI, Contrato, Nómina):', '');
  if Trim(LDesc) = '' then Exit;

  if odDocumento.Execute then
  begin
    LFileName := ExtractFileName(odDocumento.FileName);
    LExtension := LowerCase(ExtractFileExt(odDocumento.FileName));
    if (LExtension <> '') and (LExtension[1] = '.') then
      LExtension := Copy(LExtension, 2, Length(LExtension) - 1);

    LEmpId := dmgMain.CurrentCompanyId;
    if cmbEmpresa.ItemIndex >= 0 then
      LEmpId := IntToStr(Integer(cmbEmpresa.Items.Objects[cmbEmpresa.ItemIndex]));

    Stream := TMemoryStream.Create;
    try
      Stream.LoadFromFile(odDocumento.FileName);
      Stream.Position := 0;

      Qry := TFDQuery.Create(nil);
      try
        Qry.Connection := dmgMain.dbConn;
        Qry.SQL.Text :=
          'INSERT INTO ge_documentos ' +
          '  (id_empresa, fecha, descripcion, archivo, nombre_archivo, extension, id_trabajador, id_user_creator, id_user_update) ' +
          'VALUES ' +
          '  (:id_empresa, :fecha, :descripcion, :archivo, :nombre_archivo, :extension, :id_trabajador, :user, :user)';

        Qry.ParamByName('id_empresa').AsString := LEmpId;
        Qry.ParamByName('fecha').AsDate := Date;
        Qry.ParamByName('descripcion').DataType := ftWideString;
        Qry.ParamByName('descripcion').AsWideString := LDesc;
        Qry.ParamByName('archivo').LoadFromStream(Stream, ftBlob);
        Qry.ParamByName('nombre_archivo').DataType := ftWideString;
        Qry.ParamByName('nombre_archivo').AsWideString := LFileName;
        Qry.ParamByName('extension').DataType := ftWideString;
        Qry.ParamByName('extension').AsWideString := LExtension;
        Qry.ParamByName('id_trabajador').AsString := FTrabajadorId;
        Qry.ParamByName('user').AsString := dmgMain.CurrentUserId;
        Qry.ExecSQL;
      finally
        Qry.Free;
      end;

      LoadDocuments;
    finally
      Stream.Free;
    end;
  end;
end;

procedure TfrmTrabajadoresEditor.btnVerDocClick(Sender: TObject);
var
  Qry: TFDQuery;
  BlobField: TBlobField;
  TempPath, TempFile: string;
  Stream: TMemoryStream;
  LDocId, DocNombre: string;
begin
  if not Assigned(qryDocumentos) or not qryDocumentos.Active or qryDocumentos.IsEmpty then Exit;

  LDocId := qryDocumentos.FieldByName('id').AsString;
  if LDocId = '' then Exit;

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text := 'SELECT archivo, nombre_archivo, extension FROM ge_documentos WHERE id = :id';
    Qry.ParamByName('id').AsString := LDocId;
    Qry.Open;
    if not Qry.IsEmpty then
    begin
      BlobField := TBlobField(Qry.FieldByName('archivo'));
      if BlobField.IsNull or (BlobField.BlobSize = 0) then
      begin
        ShowMessage('El documento seleccionado no tiene contenido.');
        Exit;
      end;

      DocNombre := Qry.FieldByName('nombre_archivo').AsString;
      TempPath := TPath.GetTempPath;
      TempFile := TPath.Combine(TempPath, DocNombre);
      if TempFile = '' then
        TempFile := TPath.Combine(TempPath, 'doc_' + LDocId + '.' + Qry.FieldByName('extension').AsString);

      Stream := TMemoryStream.Create;
      try
        BlobField.SaveToStream(Stream);
        Stream.Position := 0;
        Stream.SaveToFile(TempFile);
      finally
        Stream.Free;
      end;

      ShellExecute(0, 'open', PChar(TempFile), nil, nil, SW_SHOWNORMAL);
    end;
  finally
    Qry.Free;
  end;
end;

procedure TfrmTrabajadoresEditor.btnEliminarDocClick(Sender: TObject);
var
  LDocId: string;
  Qry: TFDQuery;
begin
  if not Assigned(qryDocumentos) or not qryDocumentos.Active or qryDocumentos.IsEmpty then Exit;

  LDocId := qryDocumentos.FieldByName('id').AsString;
  if LDocId = '' then Exit;

  if MessageDlg('¿Está seguro de que desea eliminar este documento adjunto?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    Qry := TFDQuery.Create(nil);
    try
      Qry.Connection := dmgMain.dbConn;
      Qry.SQL.Text := 'DELETE FROM ge_documentos WHERE id = :id';
      Qry.ParamByName('id').AsString := LDocId;
      Qry.ExecSQL;
    finally
      Qry.Free;
    end;

    LoadDocuments;
  end;
end;

// ---------------------------------------------------------------------------
// Pestaña 5: Historial de Fichajes y Control Horario
// ---------------------------------------------------------------------------
procedure TfrmTrabajadoresEditor.LoadHistorial;
begin
  if FTrabajadorId = '' then
  begin
    qryHistorial.Close;
    Exit;
  end;

  try
    qryHistorial.Close;
    qryHistorial.SQL.Text :=
      'SELECT fecha, ' +
      '       TIME_FORMAT(hora_inicio, ''%H:%i'') AS hora_inicio, ' +
      '       TIME_FORMAT(hora_fin, ''%H:%i'') AS hora_fin, ' +
      '       minutos_totales, horas_decimal, ' +
      '       centro, tipo_cierre, validado, observaciones ' +
      'FROM ge_trabajadores_fichajes ' +
      'WHERE id_trabajador = :id ' +
      'ORDER BY fecha DESC, id DESC LIMIT 100';
    qryHistorial.ParamByName('id').AsString := FTrabajadorId;
    qryHistorial.Open;
    ConfigurarColumnasHistorial;
  except
    // Si la tabla no ha sido creada por el usuario aún, continuar en silencio
  end;
end;

procedure TfrmTrabajadoresEditor.ConfigurarColumnasHistorial;
var
  i: Integer;
  Col: TColumn;
begin
  if not Assigned(dbgHistorial) or not qryHistorial.Active then Exit;

  for i := 0 to dbgHistorial.Columns.Count - 1 do
  begin
    Col := dbgHistorial.Columns[i];
    if SameText(Col.FieldName, 'fecha') then
    begin
      Col.Title.Caption := 'Fecha';
      Col.Width := 85;
      Col.Alignment := taCenter;
    end
    else if SameText(Col.FieldName, 'hora_inicio') then
    begin
      Col.Title.Caption := 'Entrada';
      Col.Width := 75;
      Col.Alignment := taCenter;
    end
    else if SameText(Col.FieldName, 'hora_fin') then
    begin
      Col.Title.Caption := 'Salida';
      Col.Width := 75;
      Col.Alignment := taCenter;
    end
    else if SameText(Col.FieldName, 'horas_decimal') then
    begin
      Col.Title.Caption := 'Horas';
      Col.Width := 65;
      Col.Alignment := taRightJustify;
    end
    else if SameText(Col.FieldName, 'minutos_totales') then
    begin
      Col.Title.Caption := 'Minutos';
      Col.Width := 65;
      Col.Alignment := taRightJustify;
    end
    else if SameText(Col.FieldName, 'centro') then
    begin
      Col.Title.Caption := 'Centro / Tienda';
      Col.Width := 140;
    end
    else if SameText(Col.FieldName, 'tipo_cierre') then
    begin
      Col.Title.Caption := 'Cierre';
      Col.Width := 110;
    end
    else if SameText(Col.FieldName, 'validado') then
    begin
      Col.Title.Caption := 'Validado';
      Col.Width := 65;
      Col.Alignment := taCenter;
    end
    else if SameText(Col.FieldName, 'observaciones') then
    begin
      Col.Title.Caption := 'Observaciones';
      Col.Width := 180;
    end;
  end;
end;

procedure TfrmTrabajadoresEditor.dbgHistorialTitleClick(Column: TColumn);
begin
  if Assigned(qryHistorial) and qryHistorial.Active then
  begin
    try
      qryHistorial.IndexFieldNames := Column.FieldName;
    except
    end;
  end;
end;

procedure TfrmTrabajadoresEditor.btnRefrescarHistorialClick(Sender: TObject);
begin
  LoadHistorial;
  CargarHorarioPactadoBase;
  CargarCalendarioFichajes;
end;

// ---------------------------------------------------------------------------
// Calendario de Control Horario y Fichajes (Vistas Mes y Semana)
// ---------------------------------------------------------------------------

procedure TfrmTrabajadoresEditor.InicializarCalendarioFichajes;
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'
  );
var
  I, Y, M, D, LYear: Word;
begin
  DecodeDate(Date, Y, M, D);
  FFechaCalendarioBase := Date;
  FSelectedFichajeFecha := Date;
  FModoCalendarioFichajes := mcvMes;
  FVistaListadoFichajes := False;

  if Assigned(cboFichajeMes) and (cboFichajeMes.Items.Count = 0) then
  begin
    cboFichajeMes.Items.BeginUpdate;
    for I := 1 to 12 do
      cboFichajeMes.Items.Add(NOMBRES_MESES[I]);
    cboFichajeMes.Items.EndUpdate;
    cboFichajeMes.ItemIndex := M - 1;
  end;

  if Assigned(cboFichajeAnio) and (cboFichajeAnio.Items.Count = 0) then
  begin
    cboFichajeAnio.Items.BeginUpdate;
    for LYear := Y - 3 to Y + 2 do
      cboFichajeAnio.Items.Add(IntToStr(LYear));
    cboFichajeAnio.Items.EndUpdate;
    cboFichajeAnio.ItemIndex := cboFichajeAnio.Items.IndexOf(IntToStr(Y));
  end;

  if Assigned(pnlDiasHeader) then
  begin
    for I := 0 to 6 do
    begin
      if not Assigned(FLblDiasHeader[I]) then
      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 := $004A3B32;
        FLblDiasHeader[I].Top := 4;
        FLblDiasHeader[I].AutoSize := False;
        FLblDiasHeader[I].Height := 18;
      end;
    end;
  end;

  gridCalendarioFichajes.DoubleBuffered := True;
end;

procedure TfrmTrabajadoresEditor.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 TfrmTrabajadoresEditor.CargarHorarioPactadoBase;
var
  LQry: TFDQuery;
  LDia, LMinTot: Integer;
begin
  for LDia := 1 to 7 do
  begin
    FHorasPactadasDia[LDia] := 0.0;
    FDiaLaboralPactado[LDia] := False;
  end;

  if FTrabajadorId = '' 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 := FTrabajadorId;
    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 TfrmTrabajadoresEditor.CargarCalendarioFichajes;
const
  NOMBRES_MESES: array[1..12] of string = (
    'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
    'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'
  );
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;
begin
  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]);
    if Assigned(cboFichajeMes) then cboFichajeMes.ItemIndex := M - 1;
    if Assigned(cboFichajeAnio) then cboFichajeAnio.ItemIndex := cboFichajeAnio.Items.IndexOf(IntToStr(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)]);
    if Assigned(cboFichajeMes) then cboFichajeMes.ItemIndex := MonthOf(LInicioMatriz) - 1;
    if Assigned(cboFichajeAnio) then cboFichajeAnio.ItemIndex := cboFichajeAnio.Items.IndexOf(IntToStr(YearOf(LInicioMatriz)));
  end;

  AjustarCabeceraDiasFichajes;

  if FTrabajadorId = '' then
  begin
    gridCalendarioFichajes.Invalidate;
    Exit;
  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 := FTrabajadorId;
      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 y Vacaciones 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 fecha, ' +
        '       TIME_FORMAT(hora_inicio, ''%H:%i'') AS h_ini, ' +
        '       TIME_FORMAT(hora_fin, ''%H:%i'') AS h_fin, ' +
        '       horas_decimal, centro ' +
        'FROM ge_trabajadores_fichajes ' +
        'WHERE id_trabajador = :id AND fecha >= :desde AND fecha <= :hasta ' +
        'ORDER BY fecha ASC, hora_inicio ASC';
      LQry.ParamByName('id').AsString := FTrabajadorId;
      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 TfrmTrabajadoresEditor.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 := '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 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;

    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
    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
        LCanvas.TextOut(Rect.Left + 10, LTop, 'Ausencia: ' + LDia.TipoAusencia)
      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 TfrmTrabajadoresEditor.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 TfrmTrabajadoresEditor.AbrirDetalleFichajesDia(AFecha: TDate);
var
  I: Integer;
  LDia: TDiaFichaje;
  LFound: Boolean;
  LNombreCompleto: string;
begin
  if (AFecha = 0) or (FTrabajadorId = '') then Exit;

  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;

  LNombreCompleto := Trim(edtNombre.Text + ' ' + edtApellidos.Text);
  if TfrmDiaFichajesDetalleModal.Execute(FTrabajadorId, LNombreCompleto, AFecha,
    LDia.HorasPactadas, LDia.TipoAusencia, LDia.ObservacionAusencia) then
  begin
    CargarCalendarioFichajes;
    LoadHistorial;
  end;
end;

procedure TfrmTrabajadoresEditor.gridCalendarioFichajesClick(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 TfrmTrabajadoresEditor.gridCalendarioFichajesDblClick(Sender: TObject);
begin
  gridCalendarioFichajesClick(Sender);
end;

procedure TfrmTrabajadoresEditor.btnModoMesClick(Sender: TObject);
begin
  FModoCalendarioFichajes := mcvMes;
  btnModoMes.Font.Style := [fsBold];
  btnModoSemana.Font.Style := [];
  CargarCalendarioFichajes;
end;

procedure TfrmTrabajadoresEditor.btnModoSemanaClick(Sender: TObject);
begin
  FModoCalendarioFichajes := mcvSemana;
  btnModoSemana.Font.Style := [fsBold];
  btnModoMes.Font.Style := [];
  CargarCalendarioFichajes;
end;

procedure TfrmTrabajadoresEditor.btnPeriodoAnteriorClick(Sender: TObject);
begin
  if FModoCalendarioFichajes = mcvMes then
    FFechaCalendarioBase := IncMonth(FFechaCalendarioBase, -1)
  else
    FFechaCalendarioBase := IncDay(FFechaCalendarioBase, -7);

  CargarCalendarioFichajes;
end;

procedure TfrmTrabajadoresEditor.btnPeriodoHoyClick(Sender: TObject);
begin
  FFechaCalendarioBase := Date;
  CargarCalendarioFichajes;
end;

procedure TfrmTrabajadoresEditor.btnPeriodoSiguienteClick(Sender: TObject);
begin
  if FModoCalendarioFichajes = mcvMes then
    FFechaCalendarioBase := IncMonth(FFechaCalendarioBase, 1)
  else
    FFechaCalendarioBase := IncDay(FFechaCalendarioBase, 7);

  CargarCalendarioFichajes;
end;

procedure TfrmTrabajadoresEditor.cboFichajePeriodoChange(Sender: TObject);
var
  LYear, LMonth: Integer;
begin
  if (cboFichajeMes.ItemIndex >= 0) and (cboFichajeAnio.ItemIndex >= 0) then
  begin
    LMonth := cboFichajeMes.ItemIndex + 1;
    LYear := StrToIntDef(cboFichajeAnio.Text, YearOf(Date));
    FFechaCalendarioBase := EncodeDate(LYear, LMonth, 1);
    CargarCalendarioFichajes;
  end;
end;

procedure TfrmTrabajadoresEditor.btnAlternarVistaClick(Sender: TObject);
begin
  FVistaListadoFichajes := not FVistaListadoFichajes;
  pnlCalendarioContainer.Visible := not FVistaListadoFichajes;
  pnlListadoContainer.Visible := FVistaListadoFichajes;

  if FVistaListadoFichajes then
    btnAlternarVista.Caption := 'Ver Calendario'
  else
    btnAlternarVista.Caption := 'Ver Listado';
end;

// ---------------------------------------------------------------------------
// Fotografía del Trabajador (ge_documentos con descripción 'FOTO_TRABAJADOR')
// ---------------------------------------------------------------------------
procedure TfrmTrabajadoresEditor.LoadWorkerPhoto;
var
  Qry: TFDQuery;
  Stream: TMemoryStream;
begin
  imgFoto.Picture := nil;
  if FTrabajadorId = '' then Exit;

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text := 'SELECT archivo FROM ge_documentos WHERE id_trabajador = :id AND descripcion = ''FOTO_TRABAJADOR'' LIMIT 1';
    Qry.ParamByName('id').AsString := FTrabajadorId;
    Qry.Open;
    if not Qry.IsEmpty and not TBlobField(Qry.FieldByName('archivo')).IsNull then
    begin
      Stream := TMemoryStream.Create;
      try
        TBlobField(Qry.FieldByName('archivo')).SaveToStream(Stream);
        Stream.Position := 0;
        if Stream.Size > 0 then
          imgFoto.Picture.LoadFromStream(Stream);
      finally
        Stream.Free;
      end;
    end;
  finally
    Qry.Free;
  end;
end;

procedure TfrmTrabajadoresEditor.btnSubirFotoClick(Sender: TObject);
var
  Qry: TFDQuery;
  Stream: TMemoryStream;
  LFileName, LExtension: string;
  LEmpId: string;
begin
  if FTrabajadorId = '' then
  begin
    ShowMessage('Debe guardar primero la ficha del trabajador antes de asignarle una fotografía.');
    Exit;
  end;

  if odFoto.Execute then
  begin
    LFileName := ExtractFileName(odFoto.FileName);
    LExtension := LowerCase(ExtractFileExt(odFoto.FileName));
    if (LExtension <> '') and (LExtension[1] = '.') then
      LExtension := Copy(LExtension, 2, Length(LExtension) - 1);

    LEmpId := dmgMain.CurrentCompanyId;
    if cmbEmpresa.ItemIndex >= 0 then
      LEmpId := IntToStr(Integer(cmbEmpresa.Items.Objects[cmbEmpresa.ItemIndex]));

    Stream := TMemoryStream.Create;
    try
      Stream.LoadFromFile(odFoto.FileName);
      Stream.Position := 0;

      Qry := TFDQuery.Create(nil);
      try
        Qry.Connection := dmgMain.dbConn;
        Qry.SQL.Text := 'SELECT id FROM ge_documentos WHERE id_trabajador = :id AND descripcion = ''FOTO_TRABAJADOR'' LIMIT 1';
        Qry.ParamByName('id').AsString := FTrabajadorId;
        Qry.Open;

        if Qry.IsEmpty then
        begin
          Qry.Close;
          Qry.SQL.Text :=
            'INSERT INTO ge_documentos (id_empresa, fecha, descripcion, archivo, nombre_archivo, extension, id_trabajador, id_user_creator, id_user_update) ' +
            'VALUES (:id_empresa, :fecha, ''FOTO_TRABAJADOR'', :archivo, :nombre_archivo, :extension, :id_trabajador, :user, :user)';
        end
        else
        begin
          LFileName := Qry.FieldByName('id').AsString;
          Qry.Close;
          Qry.SQL.Text :=
            'UPDATE ge_documentos SET fecha = :fecha, archivo = :archivo, nombre_archivo = :nombre_archivo, ' +
            '  extension = :extension, id_user_update = :user, update_at = CURRENT_TIMESTAMP ' +
            'WHERE id = :id';
          Qry.ParamByName('id').AsString := LFileName;
        end;

        Qry.ParamByName('fecha').AsDate := Date;
        Qry.ParamByName('archivo').LoadFromStream(Stream, ftBlob);
        Qry.ParamByName('nombre_archivo').DataType := ftWideString;
        Qry.ParamByName('nombre_archivo').AsWideString := ExtractFileName(odFoto.FileName);
        Qry.ParamByName('extension').DataType := ftWideString;
        Qry.ParamByName('extension').AsWideString := LExtension;
        Qry.ParamByName('user').AsString := dmgMain.CurrentUserId;
        if Qry.SQL.Text.StartsWith('INSERT') then
        begin
          Qry.ParamByName('id_empresa').AsString := LEmpId;
          Qry.ParamByName('id_trabajador').AsString := FTrabajadorId;
        end;

        Qry.ExecSQL;
      finally
        Qry.Free;
      end;

      LoadWorkerPhoto;
    finally
      Stream.Free;
    end;
  end;
end;

procedure TfrmTrabajadoresEditor.btnEliminarFotoClick(Sender: TObject);
var
  Qry: TFDQuery;
begin
  if FTrabajadorId = '' then
  begin
    imgFoto.Picture := nil;
    Exit;
  end;

  if MessageDlg('¿Desea eliminar la fotografía de este trabajador?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    Qry := TFDQuery.Create(nil);
    try
      Qry.Connection := dmgMain.dbConn;
      Qry.SQL.Text := 'DELETE FROM ge_documentos WHERE id_trabajador = :id AND descripcion = ''FOTO_TRABAJADOR''';
      Qry.ParamByName('id').AsString := FTrabajadorId;
      Qry.ExecSQL;
      imgFoto.Picture := nil;
    finally
      Qry.Free;
    end;
  end;
end;

// ---------------------------------------------------------------------------
// Pestaña 7: Historial de Contratos y Periodos Laborales
// ---------------------------------------------------------------------------
procedure TfrmTrabajadoresEditor.LoadContratos;
begin
  if FTrabajadorId = '' then
  begin
    qryContratos.Close;
    Exit;
  end;

  try
    qryContratos.Close;
    qryContratos.SQL.Text :=
      'SELECT c.id, c.id_empresa, e.nombre AS empresa_nombre, c.fecha_alta, c.fecha_incorporacion, ' +
      '       c.fecha_desconexion, c.fecha_baja, c.fecha_fin_contrato, c.tipo_contrato, ' +
      '       c.motivo_alta, c.motivo_baja, c.activo, c.observaciones ' +
      'FROM ge_trabajadores_contratos c ' +
      'LEFT JOIN ge_empresas e ON e.id = c.id_empresa ' +
      'WHERE c.id_trabajador = :id ' +
      'ORDER BY c.fecha_alta DESC, c.id DESC';
    qryContratos.ParamByName('id').AsString := FTrabajadorId;
    qryContratos.Open;
    ConfigurarColumnasContratos;
  except
  end;
end;

procedure TfrmTrabajadoresEditor.ConfigurarColumnasContratos;
var
  i: Integer;
  Col: TColumn;
begin
  if not Assigned(dbgContratos) or not qryContratos.Active then Exit;

  for i := 0 to dbgContratos.Columns.Count - 1 do
  begin
    Col := dbgContratos.Columns[i];
    if SameText(Col.FieldName, 'empresa_nombre') then
    begin
      Col.Title.Caption := 'Empresa';
      Col.Width := 140;
    end
    else if SameText(Col.FieldName, 'fecha_alta') then
    begin
      Col.Title.Caption := 'Fecha Alta';
      Col.Width := 90;
      Col.Alignment := taCenter;
    end
    else if SameText(Col.FieldName, 'fecha_incorporacion') then
    begin
      Col.Title.Caption := 'Incorporación';
      Col.Width := 95;
      Col.Alignment := taCenter;
    end
    else if SameText(Col.FieldName, 'fecha_desconexion') then
    begin
      Col.Title.Caption := 'Desconexión';
      Col.Width := 95;
      Col.Alignment := taCenter;
    end
    else if SameText(Col.FieldName, 'fecha_baja') then
    begin
      Col.Title.Caption := 'Fecha Baja';
      Col.Width := 90;
      Col.Alignment := taCenter;
    end
    else if SameText(Col.FieldName, 'fecha_fin_contrato') then
    begin
      Col.Title.Caption := 'Fin Previsto';
      Col.Width := 90;
      Col.Alignment := taCenter;
    end
    else if SameText(Col.FieldName, 'tipo_contrato') then
    begin
      Col.Title.Caption := 'Tipo Contrato';
      Col.Width := 120;
    end
    else if SameText(Col.FieldName, 'motivo_alta') then
    begin
      Col.Title.Caption := 'Motivo Contratación';
      Col.Width := 150;
    end
    else if SameText(Col.FieldName, 'motivo_baja') then
    begin
      Col.Title.Caption := 'Motivo Baja';
      Col.Width := 150;
    end
    else if SameText(Col.FieldName, 'activo') then
    begin
      Col.Title.Caption := 'Vigente';
      Col.Width := 65;
    end
    else if SameText(Col.FieldName, 'observaciones') then
    begin
      Col.Title.Caption := 'Observaciones';
      Col.Width := 180;
    end;
  end;
end;

procedure TfrmTrabajadoresEditor.btnNuevoContratoClick(Sender: TObject);
begin
  if FTrabajadorId = '' then
  begin
    ShowMessage('Guarde primero la ficha del trabajador antes de registrar un periodo de contratación.');
    Exit;
  end;

  if TfrmTrabajadorContratoModal.Execute(FTrabajadorId, 0) then
  begin
    LoadContratos;
    LoadData;
    if Assigned(frmTrabajadores) then
      frmTrabajadores.btnRefreshClick(nil);
  end;
end;

procedure TfrmTrabajadoresEditor.btnModificarContratoClick(Sender: TObject);
var
  LId: Integer;
begin
  if not Assigned(qryContratos) or not qryContratos.Active or qryContratos.IsEmpty then Exit;

  LId := qryContratos.FieldByName('id').AsInteger;
  if TfrmTrabajadorContratoModal.Execute(FTrabajadorId, LId) then
  begin
    LoadContratos;
    LoadData;
    if Assigned(frmTrabajadores) then
      frmTrabajadores.btnRefreshClick(nil);
  end;
end;

procedure TfrmTrabajadoresEditor.btnEliminarContratoClick(Sender: TObject);
var
  Qry: TFDQuery;
  LId: string;
begin
  if not Assigned(qryContratos) or not qryContratos.Active or qryContratos.IsEmpty then Exit;

  LId := qryContratos.FieldByName('id').AsString;
  if MessageDlg('¿Está seguro de eliminar este registro del historial de contratación?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    Qry := TFDQuery.Create(nil);
    try
      Qry.Connection := dmgMain.dbConn;
      Qry.SQL.Text := 'DELETE FROM ge_trabajadores_contratos WHERE id = :id';
      Qry.ParamByName('id').AsString := LId;
      Qry.ExecSQL;
    finally
      Qry.Free;
    end;
    LoadContratos;
  end;
end;

procedure TfrmTrabajadoresEditor.btnRefrescarContratosClick(Sender: TObject);
begin
  LoadContratos;
end;

procedure TfrmTrabajadoresEditor.dbgContratosDblClick(Sender: TObject);
begin
  btnModificarContratoClick(nil);
end;

procedure TfrmTrabajadoresEditor.dbgContratosTitleClick(Column: TColumn);
begin
  if Assigned(qryContratos) and qryContratos.Active then
  begin
    try
      qryContratos.IndexFieldNames := Column.FieldName;
    except
    end;
  end;
end;

end.
