unit dmg_Calidad;

interface

uses
  System.SysUtils, System.Classes, System.Math, System.Variants, Data.DB,
  FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Error, FireDAC.UI.Intf,
  FireDAC.Phys.Intf, FireDAC.Stan.Def, FireDAC.Stan.Pool, FireDAC.Stan.Async,
  FireDAC.Phys, FireDAC.Stan.Param, FireDAC.DatS, FireDAC.DApt.Intf, FireDAC.DApt,
  FireDAC.Comp.DataSet, FireDAC.Comp.Client, dmg_Main;

type
  TdmgCalidad = class(TDataModule)
    qryRegistrosCab: TFDQuery;
    qryRegistrosDet: TFDQuery;
    qryIncidencias: TFDQuery;
    qryAgua: TFDQuery;
    qryQuebradizos: TFDQuery;
    qryPlagas: TFDQuery;
    qryTrazabilidad: TFDQuery;
    qryExecCalidad: TFDQuery;
    qryGrupos: TFDQuery;
    qryDocumentos: TFDQuery;
    qryDocumentoPuntos: TFDQuery;
    qryAgenda: TFDQuery;
    qryPlanner: TFDQuery;
  private
    { Private declarations }
  public
    { Public declarations }
    // Métodos para el Registro General de Documentos, Periodicidades y Puntos
    function CargarGrupos: TFDQuery;
    function CargarDocumentos(const AGrupoId: Integer = 0): TFDQuery;
    function CargarPuntosControl(const ADocumentoId: Integer): TFDQuery;
    function ObtenerPuntosFabricacion(const ACodigoDoc: string): TFDQuery;

    // Métodos del Motor de Sincronización Automática de Agenda y Cumplimiento
    function SincronizarAgendaAutomatica(const AFecha: TDate = 0): Boolean;
    function GenerarAgendaParaLote(const ALoteId: Integer): Boolean;
    function GenerarAgendaParaAlbaranCompra(const AAlbaranId: Integer): Boolean;
    function VincularRegistroAAgenda(const AAgendaId, ARegistroCabId: Integer): Boolean;
    function VerificarRegistroCompleto(const ARegistroCabId: Integer): Boolean;
    function VerificarFalloPCC(const ARegistroCabId: Integer): Boolean;
    procedure ActualizarEstadoAgendaSegunRegistro(const ARegistroCabId: Integer);
    function CargarPlanificadorCumplimiento(const ADesde, AHasta: TDate; const ADepto: string = '';
      const APeriodicidad: string = ''; const AEstado: string = ''): TFDQuery;

    // Métodos de validación estricta de PCC y parámetros de Calidad IFS
    function ValidateHorneadoPCC(const ATempInterna: Double; out AAlcanzado: Boolean): Boolean;
    function ValidateMasaMadrePh(const APh: Double; out AConforme: Boolean): Boolean;
    function ValidateAguaCloro(const ACloroPpm: Double; out AConforme: Boolean): Boolean;
    
    // Control Estadístico de Contenido Efectivo conforme al RD 1801/2008
    function CalculateContenidoEfectivo(const APesos: array of Double; const AFormatoNominal: Double;
      out AMedia, ADesviacion: Double; out AConforme: Boolean): string;
      
    // Disparo automático de No Conformidad (RG-11) en caso de fallo de PCC
    function CrearIncidenciaPCC(const ARegistroCabId: Integer; const AFase, ADesviacion, AAccion, ADestino: string;
      const AUserId: Integer; const AGravedad: string = 'GRAVE'): string;
      
    // Generación dinámica de ejecuciones de registro a partir de plantillas
    function GenerarRegistroDesdeDocumento(const ADocumentoId: Integer; const AFecha: TDate; const AAgendaId: Integer = 0): Integer;

    // Generación de la Hoja Maestra de Trazabilidad (RG-13)
    function GenerarTrazabilidadMaestra(const ALotePanettoneId: Integer; const ACodigoLote: string): Boolean;
  end;

var
  dmgCalidad: TdmgCalidad;

implementation

{%CLASSGROUP 'Vcl.Controls.TControl'}

{$R *.dfm}

{ TdmgCalidad }

function TdmgCalidad.CargarGrupos: TFDQuery;
begin
  qryGrupos.Close;
  qryGrupos.SQL.Text := 'SELECT * FROM ge_calidad_grupos WHERE activo = 1 ORDER BY orden, nombre';
  qryGrupos.Open;
  Result := qryGrupos;
end;

function TdmgCalidad.CargarDocumentos(const AGrupoId: Integer): TFDQuery;
begin
  qryDocumentos.Close;
  if AGrupoId > 0 then
  begin
    qryDocumentos.SQL.Text :=
      'SELECT d.*, g.nombre AS grupo_nombre ' +
      'FROM ge_calidad_documentos d ' +
      'LEFT JOIN ge_calidad_grupos g ON d.id_grupo = g.id ' +
      'WHERE d.id_grupo = :gid AND d.activo = 1 ' +
      'ORDER BY d.codigo_doc';
    qryDocumentos.ParamByName('gid').AsInteger := AGrupoId;
  end
  else
  begin
    qryDocumentos.SQL.Text :=
      'SELECT d.*, g.nombre AS grupo_nombre ' +
      'FROM ge_calidad_documentos d ' +
      'LEFT JOIN ge_calidad_grupos g ON d.id_grupo = g.id ' +
      'WHERE d.activo = 1 ' +
      'ORDER BY g.orden, d.codigo_doc';
  end;
  qryDocumentos.Open;
  Result := qryDocumentos;
end;

function TdmgCalidad.CargarPuntosControl(const ADocumentoId: Integer): TFDQuery;
begin
  qryDocumentoPuntos.Close;
  qryDocumentoPuntos.SQL.Text :=
    'SELECT * FROM ge_calidad_documento_puntos ' +
    'WHERE id_documento = :did ' +
    'ORDER BY codigo_seccion, orden_visual, id';
  qryDocumentoPuntos.ParamByName('did').AsInteger := ADocumentoId;
  qryDocumentoPuntos.Open;
  Result := qryDocumentoPuntos;
end;

function TdmgCalidad.ObtenerPuntosFabricacion(const ACodigoDoc: string): TFDQuery;
begin
  qryDocumentoPuntos.Close;
  qryDocumentoPuntos.SQL.Text :=
    'SELECT p.*, d.codigo_doc, d.nombre_doc ' +
    'FROM ge_calidad_documento_puntos p ' +
    'JOIN ge_calidad_documentos d ON p.id_documento = d.id ' +
    'JOIN ge_calidad_grupos g ON d.id_grupo = g.id ' +
    'WHERE g.codigo = ''FABRICACION'' AND d.codigo_doc = :cdoc AND d.activo = 1 ' +
    'ORDER BY p.orden_visual';
  qryDocumentoPuntos.ParamByName('cdoc').AsString := ACodigoDoc;
  qryDocumentoPuntos.Open;
  Result := qryDocumentoPuntos;
end;

function TdmgCalidad.ValidateHorneadoPCC(const ATempInterna: Double; out AAlcanzado: Boolean): Boolean;
begin
  // PCC Horneado (RG-07): La temperatura interna de extracción del Panettone debe ser >= 96.0 °C
  AAlcanzado := ATempInterna >= 96.0;
  Result := AAlcanzado;
end;

function TdmgCalidad.ValidateMasaMadrePh(const APh: Double; out AConforme: Boolean): Boolean;
begin
  // Rango técnico IFS de pH para refrescos de masa madre de Raúl Asencio (RG-03): 3.90 a 4.25
  AConforme := (APh >= 3.90) and (APh <= 4.25);
  Result := AConforme;
end;

function TdmgCalidad.ValidateAguaCloro(const ACloroPpm: Double; out AConforme: Boolean): Boolean;
begin
  // Rango legal de Cloro Potable (RD 3/2023): 0.20 a 1.00 ppm
  AConforme := (ACloroPpm >= 0.20) and (ACloroPpm <= 1.00);
  Result := AConforme;
end;

function TdmgCalidad.CalculateContenidoEfectivo(const APesos: array of Double; const AFormatoNominal: Double;
  out AMedia, ADesviacion: Double; out AConforme: Boolean): string;
var
  i, LCount: Integer;
  LSuma, LErrorTolerado: Double;
begin
  LCount := Length(APesos);
  if LCount = 0 then
  begin
    AMedia := 0;
    ADesviacion := 0;
    AConforme := False;
    Exit('Sin datos de muestreo');
  end;

  LSuma := 0;
  for i := 0 to LCount - 1 do
    LSuma := LSuma + APesos[i];
  
  AMedia := LSuma / LCount;

  // Tolerancia según RD 1801/2008 para envases de 500g a 1.000g (15g o 1.5%)
  if AFormatoNominal >= 1000 then
    LErrorTolerado := 15.0
  else if AFormatoNominal >= 500 then
    LErrorTolerado := AFormatoNominal * 0.015
  else
    LErrorTolerado := 9.0;

  // Requisito 1: La media debe ser >= Cantidad Nominal
  // Requisito 2: Ningún envase puede tener un defecto superior al doble del error tolerado
  AConforme := AMedia >= AFormatoNominal;
  for i := 0 to LCount - 1 do
  begin
    if APesos[i] < (AFormatoNominal - (2 * LErrorTolerado)) then
    begin
      AConforme := False;
      Break;
    end;
  end;

  ADesviacion := AMedia - AFormatoNominal;
  if AConforme then
    Result := Format('Muestreo Conforme (Media: %.2fg, Desviación: %+.2fg)', [AMedia, ADesviacion])
  else
    Result := Format('Muestreo NO CONFORME RD 1801/2008 (Media: %.2fg, Defecto detectado)', [AMedia]);
end;

function TdmgCalidad.CrearIncidenciaPCC(const ARegistroCabId: Integer; const AFase, ADesviacion, AAccion, ADestino: string;
  const AUserId: Integer; const AGravedad: string): string;
var
  LCodigoNc: string;
begin
  LCodigoNc := Format('NC-%s-%03d', [FormatDateTime('yyyyMMdd', Now), Random(999) + 1]);
  
  qryExecCalidad.Close;
  qryExecCalidad.SQL.Text :=
    'INSERT INTO ge_calidad_incidencias (' +
    '  codigo_nc, id_registro_cab, fase_proceso, gravedad, descripcion_desviacion, ' +
    '  accion_inmediata, destino_producto, id_usuario_reporta, estado, id_user_creator, id_user_update' +
    ') VALUES (' +
    '  :codigo, :id_cab, :fase, :gravedad, :desviacion, ' +
    '  :accion, :destino, :user, ''ABIERTA'', :user, :user' +
    ')';
  qryExecCalidad.ParamByName('codigo').AsString := LCodigoNc;
  if ARegistroCabId > 0 then
    qryExecCalidad.ParamByName('id_cab').AsInteger := ARegistroCabId
  else
    qryExecCalidad.ParamByName('id_cab').Clear;
  qryExecCalidad.ParamByName('fase').AsString := AFase;
  qryExecCalidad.ParamByName('gravedad').AsString := AGravedad;
  qryExecCalidad.ParamByName('desviacion').AsString := ADesviacion;
  qryExecCalidad.ParamByName('accion').AsString := AAccion;
  qryExecCalidad.ParamByName('destino').AsString := ADestino;
  qryExecCalidad.ParamByName('user').AsInteger := AUserId;
  
  qryExecCalidad.ExecSQL;
  Result := LCodigoNc;
end;

function TdmgCalidad.GenerarTrazabilidadMaestra(const ALotePanettoneId: Integer; const ACodigoLote: string): Boolean;
begin
  // Reconstrucción atómica del mapa de lotes para RG-13
  qryExecCalidad.Close;
  qryExecCalidad.SQL.Text := 'DELETE FROM ge_calidad_trazabilidad_maestra WHERE id_lote_panettone = :id_lote';
  qryExecCalidad.ParamByName('id_lote').AsInteger := ALotePanettoneId;
  qryExecCalidad.ExecSQL;

  // Insertar trazabilidad de ingredientes de empastes
  qryExecCalidad.Close;
  qryExecCalidad.SQL.Text :=
    'INSERT INTO ge_calidad_trazabilidad_maestra (' +
    '  id_lote_panettone, codigo_lote_panettone, fase_proceso, tipo_componente, ' +
    '  descripcion_componente, lote_componente, cantidad_usada, unidad' +
    ') ' +
    'SELECT :id_lote, :codigo_lote, ''EMPASTES'', ''Materia Prima'', a.descripcion, ' +
    '       COALESCE(l.id, ''S/L''), 1.0, ''kg'' ' +
    'FROM ge_lotes l ' +
    'LEFT JOIN ge_articulos a ON l.id_articulo = a.id ' +
    'WHERE l.id_articulo IS NOT NULL LIMIT 50';
  qryExecCalidad.ParamByName('id_lote').AsInteger := ALotePanettoneId;
  qryExecCalidad.ParamByName('codigo_lote').AsString := ACodigoLote;
  qryExecCalidad.ExecSQL;

  Result := True;
end;

function TdmgCalidad.GenerarRegistroDesdeDocumento(const ADocumentoId: Integer; const AFecha: TDate; const AAgendaId: Integer): Integer;
var
  LQryPuntos: TFDQuery;
  LNewCabId: Integer;
begin
  Result := 0;
  if ADocumentoId <= 0 then Exit;

  // Si se pasa AAgendaId, comprobar si ya existe un registro de cabecera asociado a esta entrada de agenda
  if AAgendaId > 0 then
  begin
    qryExecCalidad.Close;
    qryExecCalidad.SQL.Text :=
      'SELECT id FROM ge_calidad_registros_cab WHERE id_calidad_agenda = :aid OR id = (SELECT id_registro_cab FROM ge_calidad_agenda WHERE id = :aid2 LIMIT 1) LIMIT 1';
    qryExecCalidad.ParamByName('aid').AsInteger := AAgendaId;
    qryExecCalidad.ParamByName('aid2').AsInteger := AAgendaId;
    qryExecCalidad.Open;

    if not qryExecCalidad.IsEmpty then
    begin
      LNewCabId := qryExecCalidad.Fields[0].AsInteger;
      if LNewCabId > 0 then
      begin
        // Asegurar que la agenda guarde id_registro_cab por si estaba desvinculada
        qryExecCalidad.Close;
        qryExecCalidad.SQL.Text := 'UPDATE ge_calidad_agenda SET id_registro_cab = :reg_id WHERE id = :aid AND (id_registro_cab IS NULL OR id_registro_cab <= 0)';
        qryExecCalidad.ParamByName('reg_id').AsInteger := LNewCabId;
        qryExecCalidad.ParamByName('aid').AsInteger := AAgendaId;
        qryExecCalidad.ExecSQL;

        Exit(LNewCabId);
      end;
    end;
  end;

  // 1. Crear cabecera mínima incluyendo id_calidad_agenda
  qryExecCalidad.Close;
  if AAgendaId > 0 then
  begin
    qryExecCalidad.SQL.Text :=
      'INSERT INTO ge_calidad_registros_cab (id_documento, id_calidad_agenda, id_empresa, fecha, estado_evaluacion, id_user_creator, id_user_update) ' +
      'VALUES (:doc_id, :aid, :emp_id, :fecha, ''PENDIENTE'', :uid, :uid)';
    qryExecCalidad.ParamByName('aid').AsInteger := AAgendaId;
  end
  else
  begin
    qryExecCalidad.SQL.Text :=
      'INSERT INTO ge_calidad_registros_cab (id_documento, id_empresa, fecha, estado_evaluacion, id_user_creator, id_user_update) ' +
      'VALUES (:doc_id, :emp_id, :fecha, ''PENDIENTE'', :uid, :uid)';
  end;

  qryExecCalidad.ParamByName('doc_id').AsInteger := ADocumentoId;
  qryExecCalidad.ParamByName('emp_id').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
  qryExecCalidad.ParamByName('fecha').AsDate := AFecha;
  qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
  try
    qryExecCalidad.ExecSQL;
  except
    on E: Exception do
    begin
      // Fallback defensivo si en MySQL existe la constraint errónea fk_reg_calidad_agenda en 'id' en lugar de 'id_calidad_agenda'
      qryExecCalidad.Close;
      qryExecCalidad.SQL.Text :=
        'INSERT INTO ge_calidad_registros_cab (id_documento, id_empresa, fecha, estado_evaluacion, id_user_creator, id_user_update) ' +
        'VALUES (:doc_id, :emp_id, :fecha, ''PENDIENTE'', :uid, :uid)';
      qryExecCalidad.ParamByName('doc_id').AsInteger := ADocumentoId;
      qryExecCalidad.ParamByName('emp_id').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
      qryExecCalidad.ParamByName('fecha').AsDate := AFecha;
      qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
      qryExecCalidad.ExecSQL;
    end;
  end;

  LNewCabId := StrToIntDef(VarToStr(dmgMain.dbConn.ExecSQLScalar('SELECT LAST_INSERT_ID()')), 0);
  if LNewCabId <= 0 then Exit;

  // Vincular la agenda con el nuevo id_registro_cab
  if AAgendaId > 0 then
  begin
    qryExecCalidad.Close;
    qryExecCalidad.SQL.Text := 'UPDATE ge_calidad_agenda SET id_registro_cab = :reg_id WHERE id = :aid';
    qryExecCalidad.ParamByName('reg_id').AsInteger := LNewCabId;
    qryExecCalidad.ParamByName('aid').AsInteger := AAgendaId;
    qryExecCalidad.ExecSQL;
  end;

  // 2. Obtener puntos de la plantilla
  LQryPuntos := TFDQuery.Create(nil);
  try
    LQryPuntos.Connection := dmgMain.dbConn;
    LQryPuntos.SQL.Text :=
      'SELECT id, es_pcc FROM ge_calidad_documento_puntos ' +
      'WHERE id_documento = :doc_id ' +
      'ORDER BY codigo_seccion, orden_visual, id';
    LQryPuntos.ParamByName('doc_id').AsInteger := ADocumentoId;
    LQryPuntos.Open;

    // 3. Crear registros de detalle dinámicos para cada punto de control definido
    while not LQryPuntos.Eof do
    begin
      qryExecCalidad.Close;
      qryExecCalidad.SQL.Text :=
        'INSERT INTO ge_calidad_registros_det ' +
        '  (id_registro_cab, id_punto_control, es_pcc, id_user_creator, id_user_update) ' +
        'VALUES ' +
        '  (:cab_id, :punto_id, :pcc, :uid, :uid)';
      qryExecCalidad.ParamByName('cab_id').AsInteger := LNewCabId;
      qryExecCalidad.ParamByName('punto_id').AsInteger := LQryPuntos.FieldByName('id').AsInteger;
      qryExecCalidad.ParamByName('pcc').AsInteger := Ord(LQryPuntos.FieldByName('es_pcc').AsBoolean);
      qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
      qryExecCalidad.ExecSQL;

      LQryPuntos.Next;
    end;
  finally
    LQryPuntos.Free;
  end;

  Result := LNewCabId;
end;

function TdmgCalidad.SincronizarAgendaAutomatica(const AFecha: TDate): Boolean;
var
  LTargetDate, LInicioMes, LCurDate: TDate;
  QDocs: TFDQuery;
  LDocId, LMargenHoras: Integer;
  LPer: string;
  LFechaLimite: TDateTime;
  Year, Month, Day: Word;
begin
  Result := False;
  if not Assigned(qryAgenda) then
  begin
    qryAgenda := TFDQuery.Create(Self);
    qryAgenda.Connection := dmgMain.dbConn;
  end;

  if AFecha <= 0 then
    LTargetDate := Date
  else
    LTargetDate := AFecha;

  DecodeDate(LTargetDate, Year, Month, Day);
  LInicioMes := EncodeDate(Year, Month, 1);

  QDocs := TFDQuery.Create(nil);
  try
    QDocs.Connection := dmgMain.dbConn;
    QDocs.SQL.Text :=
      'SELECT id, codigo_doc, periodicidad, COALESCE(margen_registro, 24) AS margen_registro ' +
      'FROM ge_calidad_documentos ' +
      'WHERE (activo = 1 OR activo IS NULL) AND periodicidad IN (''DIARIO'', ''TURNO'', ''SEMANAL'', ''QUINCENAL'', ''MENSUAL'', ''TRIMESTRAL'', ''ANUAL'')';
    QDocs.Open;

    while not QDocs.Eof do
    begin
      LDocId := QDocs.FieldByName('id').AsInteger;
      LPer := UpperCase(QDocs.FieldByName('periodicidad').AsString);
      LMargenHoras := QDocs.FieldByName('margen_registro').AsInteger;
      if LMargenHoras <= 0 then LMargenHoras := 24;

      if SameText(LPer, 'DIARIO') then
      begin
        // Generar un registro por cada día transcurrido del mes hasta la fecha
        LCurDate := LInicioMes;
        while LCurDate <= LTargetDate do
        begin
          LFechaLimite := LCurDate + (LMargenHoras / 24.0);
          qryExecCalidad.Close;
          qryExecCalidad.SQL.Text :=
            'INSERT INTO ge_calidad_agenda ' +
            '  (id_empresa, id_documento, fecha_programada, turno, fecha_limite_toma, estado, id_user_creator, id_user_update) ' +
            'SELECT :emp, :doc_id, :fecha, ''UNICO'', :limite, ''PENDIENTE'', :uid, :uid ' +
            'FROM DUAL ' +
            'WHERE NOT EXISTS ( ' +
            '  SELECT 1 FROM ge_calidad_agenda ' +
            '  WHERE id_documento = :doc_chk ' +
            '    AND fecha_programada = :fecha_chk ' +
            '    AND id_lote_fabricacion IS NULL AND id_albaran_compra IS NULL ' +
            ')';
          qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
          qryExecCalidad.ParamByName('doc_id').AsInteger := LDocId;
          qryExecCalidad.ParamByName('fecha').AsDate := LCurDate;
          qryExecCalidad.ParamByName('limite').AsDateTime := LFechaLimite;
          qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
          qryExecCalidad.ParamByName('doc_chk').AsInteger := LDocId;
          qryExecCalidad.ParamByName('fecha_chk').AsDate := LCurDate;
          qryExecCalidad.ExecSQL;

          LCurDate := LCurDate + 1;
        end;
      end
      else if SameText(LPer, 'TURNO') then
      begin
        // Generar 2 registros (Mañana y Tarde) por cada día transcurrido del mes hasta la fecha
        LCurDate := LInicioMes;
        while LCurDate <= LTargetDate do
        begin
          LFechaLimite := LCurDate + (LMargenHoras / 24.0);

          // Slot Mañana
          qryExecCalidad.Close;
          qryExecCalidad.SQL.Text :=
            'INSERT INTO ge_calidad_agenda ' +
            '  (id_empresa, id_documento, fecha_programada, turno, fecha_limite_toma, estado, id_user_creator, id_user_update) ' +
            'SELECT :emp, :doc_id, :fecha, ''MANANA'', :limite, ''PENDIENTE'', :uid, :uid ' +
            'FROM DUAL ' +
            'WHERE NOT EXISTS ( ' +
            '  SELECT 1 FROM ge_calidad_agenda ' +
            '  WHERE id_documento = :doc_chk ' +
            '    AND fecha_programada = :fecha_chk AND turno = ''MANANA'' ' +
            '    AND id_lote_fabricacion IS NULL AND id_albaran_compra IS NULL ' +
            ')';
          qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
          qryExecCalidad.ParamByName('doc_id').AsInteger := LDocId;
          qryExecCalidad.ParamByName('fecha').AsDate := LCurDate;
          qryExecCalidad.ParamByName('limite').AsDateTime := LFechaLimite;
          qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
          qryExecCalidad.ParamByName('doc_chk').AsInteger := LDocId;
          qryExecCalidad.ParamByName('fecha_chk').AsDate := LCurDate;
          qryExecCalidad.ExecSQL;

          // Slot Tarde
          qryExecCalidad.Close;
          qryExecCalidad.SQL.Text :=
            'INSERT INTO ge_calidad_agenda ' +
            '  (id_empresa, id_documento, fecha_programada, turno, fecha_limite_toma, estado, id_user_creator, id_user_update) ' +
            'SELECT :emp, :doc_id, :fecha, ''TARDE'', :limite, ''PENDIENTE'', :uid, :uid ' +
            'FROM DUAL ' +
            'WHERE NOT EXISTS ( ' +
            '  SELECT 1 FROM ge_calidad_agenda ' +
            '  WHERE id_documento = :doc_chk ' +
            '    AND fecha_programada = :fecha_chk AND turno = ''TARDE'' ' +
            '    AND id_lote_fabricacion IS NULL AND id_albaran_compra IS NULL ' +
            ')';
          qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
          qryExecCalidad.ParamByName('doc_id').AsInteger := LDocId;
          qryExecCalidad.ParamByName('fecha').AsDate := LCurDate;
          qryExecCalidad.ParamByName('limite').AsDateTime := LFechaLimite;
          qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
          qryExecCalidad.ParamByName('doc_chk').AsInteger := LDocId;
          qryExecCalidad.ParamByName('fecha_chk').AsDate := LCurDate;
          qryExecCalidad.ExecSQL;

          LCurDate := LCurDate + 1;
        end;
      end
      else if SameText(LPer, 'SEMANAL') then
      begin
        // Generar 1 único registro por cada semana transcurrida del mes (Lunes de esa semana)
        LCurDate := LInicioMes;
        while LCurDate <= LTargetDate do
        begin
          while (DayOfWeek(LCurDate) <> 2) and (LCurDate <= LTargetDate) do // 2 = Lunes
            LCurDate := LCurDate + 1;

          if LCurDate <= LTargetDate then
          begin
            LFechaLimite := LCurDate + (LMargenHoras / 24.0);
            qryExecCalidad.Close;
            qryExecCalidad.SQL.Text :=
              'INSERT INTO ge_calidad_agenda ' +
              '  (id_empresa, id_documento, fecha_programada, turno, fecha_limite_toma, estado, id_user_creator, id_user_update) ' +
              'SELECT :emp, :doc_id, :fecha, ''UNICO'', :limite, ''PENDIENTE'', :uid, :uid ' +
              'FROM DUAL ' +
              'WHERE NOT EXISTS ( ' +
              '  SELECT 1 FROM ge_calidad_agenda ' +
              '  WHERE id_documento = :doc_chk ' +
              '    AND YEARWEEK(fecha_programada, 1) = YEARWEEK(:fecha_chk, 1) ' +
              '    AND id_lote_fabricacion IS NULL AND id_albaran_compra IS NULL ' +
              ')';
            qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
            qryExecCalidad.ParamByName('doc_id').AsInteger := LDocId;
            qryExecCalidad.ParamByName('fecha').AsDate := LCurDate;
            qryExecCalidad.ParamByName('limite').AsDateTime := LFechaLimite;
            qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
            qryExecCalidad.ParamByName('doc_chk').AsInteger := LDocId;
            qryExecCalidad.ParamByName('fecha_chk').AsDate := LCurDate;
            qryExecCalidad.ExecSQL;
          end;

          LCurDate := LCurDate + 7;
        end;
      end
      else if SameText(LPer, 'QUINCENAL') then
      begin
        // Generar 2 registros al mes (días 1 y 15 de cada mes)
        // Slot 1: Día 1
        LCurDate := LInicioMes;
        if LCurDate <= LTargetDate then
        begin
          LFechaLimite := LCurDate + (LMargenHoras / 24.0);
          qryExecCalidad.Close;
          qryExecCalidad.SQL.Text :=
            'INSERT INTO ge_calidad_agenda ' +
            '  (id_empresa, id_documento, fecha_programada, turno, fecha_limite_toma, estado, id_user_creator, id_user_update) ' +
            'SELECT :emp, :doc_id, :fecha, ''UNICO'', :limite, ''PENDIENTE'', :uid, :uid ' +
            'FROM DUAL ' +
            'WHERE NOT EXISTS ( ' +
            '  SELECT 1 FROM ge_calidad_agenda ' +
            '  WHERE id_documento = :doc_chk ' +
            '    AND fecha_programada = :fecha_chk ' +
            '    AND id_lote_fabricacion IS NULL AND id_albaran_compra IS NULL ' +
            ')';
          qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
          qryExecCalidad.ParamByName('doc_id').AsInteger := LDocId;
          qryExecCalidad.ParamByName('fecha').AsDate := LCurDate;
          qryExecCalidad.ParamByName('limite').AsDateTime := LFechaLimite;
          qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
          qryExecCalidad.ParamByName('doc_chk').AsInteger := LDocId;
          qryExecCalidad.ParamByName('fecha_chk').AsDate := LCurDate;
          qryExecCalidad.ExecSQL;
        end;

        // Slot 2: Día 15
        LCurDate := EncodeDate(Year, Month, 15);
        if LCurDate <= LTargetDate then
        begin
          LFechaLimite := LCurDate + (LMargenHoras / 24.0);
          qryExecCalidad.Close;
          qryExecCalidad.SQL.Text :=
            'INSERT INTO ge_calidad_agenda ' +
            '  (id_empresa, id_documento, fecha_programada, turno, fecha_limite_toma, estado, id_user_creator, id_user_update) ' +
            'SELECT :emp, :doc_id, :fecha, ''UNICO'', :limite, ''PENDIENTE'', :uid, :uid ' +
            'FROM DUAL ' +
            'WHERE NOT EXISTS ( ' +
            '  SELECT 1 FROM ge_calidad_agenda ' +
            '  WHERE id_documento = :doc_chk ' +
            '    AND fecha_programada = :fecha_chk ' +
            '    AND id_lote_fabricacion IS NULL AND id_albaran_compra IS NULL ' +
            ')';
          qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
          qryExecCalidad.ParamByName('doc_id').AsInteger := LDocId;
          qryExecCalidad.ParamByName('fecha').AsDate := LCurDate;
          qryExecCalidad.ParamByName('limite').AsDateTime := LFechaLimite;
          qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
          qryExecCalidad.ParamByName('doc_chk').AsInteger := LDocId;
          qryExecCalidad.ParamByName('fecha_chk').AsDate := LCurDate;
          qryExecCalidad.ExecSQL;
        end;
      end
      else if SameText(LPer, 'MENSUAL') then
      begin
        // Generar 1 único registro mensual (programado el día 1 del mes)
        LFechaLimite := LInicioMes + (LMargenHoras / 24.0);
        qryExecCalidad.Close;
        qryExecCalidad.SQL.Text :=
          'INSERT INTO ge_calidad_agenda ' +
          '  (id_empresa, id_documento, fecha_programada, turno, fecha_limite_toma, estado, id_user_creator, id_user_update) ' +
          'SELECT :emp, :doc_id, :fecha, ''UNICO'', :limite, ''PENDIENTE'', :uid, :uid ' +
          'FROM DUAL ' +
          'WHERE NOT EXISTS ( ' +
          '  SELECT 1 FROM ge_calidad_agenda ' +
          '  WHERE id_documento = :doc_chk ' +
          '    AND YEAR(fecha_programada) = YEAR(:fecha_chk) ' +
          '    AND MONTH(fecha_programada) = MONTH(:fecha_chk) ' +
          '    AND id_lote_fabricacion IS NULL AND id_albaran_compra IS NULL ' +
          ')';
        qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
        qryExecCalidad.ParamByName('doc_id').AsInteger := LDocId;
        qryExecCalidad.ParamByName('fecha').AsDate := LInicioMes;
        qryExecCalidad.ParamByName('limite').AsDateTime := LFechaLimite;
        qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
        qryExecCalidad.ParamByName('doc_chk').AsInteger := LDocId;
        qryExecCalidad.ParamByName('fecha_chk').AsDate := LInicioMes;
        qryExecCalidad.ExecSQL;
      end
      else if SameText(LPer, 'TRIMESTRAL') then
      begin
        // Generar 1 registro el primer día del trimestre (Meses 1, 4, 7, 10)
        LCurDate := EncodeDate(Year, ((Month - 1) div 3) * 3 + 1, 1);
        LFechaLimite := LCurDate + (LMargenHoras / 24.0);
        qryExecCalidad.Close;
        qryExecCalidad.SQL.Text :=
          'INSERT INTO ge_calidad_agenda ' +
          '  (id_empresa, id_documento, fecha_programada, turno, fecha_limite_toma, estado, id_user_creator, id_user_update) ' +
          'SELECT :emp, :doc_id, :fecha, ''UNICO'', :limite, ''PENDIENTE'', :uid, :uid ' +
          'FROM DUAL ' +
          'WHERE NOT EXISTS ( ' +
          '  SELECT 1 FROM ge_calidad_agenda ' +
          '  WHERE id_documento = :doc_chk ' +
          '    AND YEAR(fecha_programada) = YEAR(:fecha_chk) ' +
          '    AND QUARTER(fecha_programada) = QUARTER(:fecha_chk) ' +
          '    AND id_lote_fabricacion IS NULL AND id_albaran_compra IS NULL ' +
          ')';
        qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
        qryExecCalidad.ParamByName('doc_id').AsInteger := LDocId;
        qryExecCalidad.ParamByName('fecha').AsDate := LCurDate;
        qryExecCalidad.ParamByName('limite').AsDateTime := LFechaLimite;
        qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
        qryExecCalidad.ParamByName('doc_chk').AsInteger := LDocId;
        qryExecCalidad.ParamByName('fecha_chk').AsDate := LCurDate;
        qryExecCalidad.ExecSQL;
      end
      else if SameText(LPer, 'ANUAL') then
      begin
        // Generar 1 único registro anual (programado el 01/01 del año)
        LCurDate := EncodeDate(Year, 1, 1);
        LFechaLimite := LCurDate + (LMargenHoras / 24.0);
        qryExecCalidad.Close;
        qryExecCalidad.SQL.Text :=
          'INSERT INTO ge_calidad_agenda ' +
          '  (id_empresa, id_documento, fecha_programada, turno, fecha_limite_toma, estado, id_user_creator, id_user_update) ' +
          'SELECT :emp, :doc_id, :fecha, ''UNICO'', :limite, ''PENDIENTE'', :uid, :uid ' +
          'FROM DUAL ' +
          'WHERE NOT EXISTS ( ' +
          '  SELECT 1 FROM ge_calidad_agenda ' +
          '  WHERE id_documento = :doc_chk ' +
          '    AND YEAR(fecha_programada) = YEAR(:fecha_chk) ' +
          '    AND id_lote_fabricacion IS NULL AND id_albaran_compra IS NULL ' +
          ')';
        qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
        qryExecCalidad.ParamByName('doc_id').AsInteger := LDocId;
        qryExecCalidad.ParamByName('fecha').AsDate := LCurDate;
        qryExecCalidad.ParamByName('limite').AsDateTime := LFechaLimite;
        qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
        qryExecCalidad.ParamByName('doc_chk').AsInteger := LDocId;
        qryExecCalidad.ExecSQL;
      end;

      QDocs.Next;
    end;

    // Purgar duplicados redundantes del mismo periodo sin registro
    qryExecCalidad.Close;
    qryExecCalidad.SQL.Text :=
      'DELETE a1 FROM ge_calidad_agenda a1 ' +
      'INNER JOIN ge_calidad_agenda a2 ' +
      'WHERE a1.id > a2.id ' +
      '  AND a1.id_documento = a2.id_documento ' +
      '  AND a1.fecha_programada = a2.fecha_programada ' +
      '  AND a1.turno = a2.turno ' +
      '  AND a1.id_lote_fabricacion IS NULL AND a2.id_lote_fabricacion IS NULL ' +
      '  AND a1.id_albaran_compra IS NULL AND a2.id_albaran_compra IS NULL ' +
      '  AND a1.id_registro_cab IS NULL';
    qryExecCalidad.ExecSQL;

    // Actualizar automáticamente a VENCIDO los registros superados en plazo que siguen PENDIENTE
    qryExecCalidad.Close;
    qryExecCalidad.SQL.Text :=
      'UPDATE ge_calidad_agenda ' +
      'SET estado = ''VENCIDO'' ' +
      'WHERE estado = ''PENDIENTE'' AND fecha_limite_toma < NOW()';
    qryExecCalidad.ExecSQL;

    Result := True;
  finally
    QDocs.Free;
  end;
end;

function TdmgCalidad.GenerarAgendaParaLote(const ALoteId: Integer): Boolean;
var
  QDocs: TFDQuery;
  LMargenHoras: Integer;
begin
  Result := False;
  if ALoteId <= 0 then Exit;

  QDocs := TFDQuery.Create(nil);
  try
    QDocs.Connection := dmgMain.dbConn;
    QDocs.SQL.Text :=
      'SELECT id, COALESCE(margen_registro, 24) AS margen_registro FROM ge_calidad_documentos ' +
      'WHERE (activo = 1 OR activo IS NULL) AND (periodicidad = ''LOTE_FABRICACION'' OR periodicidad = ''POR FABRICACION'')';
    QDocs.Open;

    while not QDocs.Eof do
    begin
      LMargenHoras := QDocs.FieldByName('margen_registro').AsInteger;
      if LMargenHoras <= 0 then LMargenHoras := 24;

      qryExecCalidad.Close;
      qryExecCalidad.SQL.Text :=
        'INSERT INTO ge_calidad_agenda ' +
        '  (id_empresa, id_documento, id_lote_fabricacion, fecha_programada, turno, fecha_limite_toma, estado, id_user_creator, id_user_update) ' +
        'SELECT :emp, :doc_id, :lote_id, :fecha, ''UNICO'', :limite, ''PENDIENTE'', :uid, :uid ' +
        'FROM DUAL ' +
        'WHERE NOT EXISTS ( ' +
        '  SELECT 1 FROM ge_calidad_agenda ' +
        '  WHERE id_documento = :doc_chk ' +
        '    AND id_lote_fabricacion = :lote_chk ' +
        ')';
      qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
      qryExecCalidad.ParamByName('doc_id').AsInteger := QDocs.FieldByName('id').AsInteger;
      qryExecCalidad.ParamByName('lote_id').AsInteger := ALoteId;
      qryExecCalidad.ParamByName('fecha').AsDate := Date;
      qryExecCalidad.ParamByName('limite').AsDateTime := Now + (LMargenHoras / 24.0);
      qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
      qryExecCalidad.ParamByName('doc_chk').AsInteger := QDocs.FieldByName('id').AsInteger;
      qryExecCalidad.ParamByName('lote_chk').AsInteger := ALoteId;
      qryExecCalidad.ExecSQL;
      QDocs.Next;
    end;
    Result := True;
  finally
    QDocs.Free;
  end;
end;

function TdmgCalidad.GenerarAgendaParaAlbaranCompra(const AAlbaranId: Integer): Boolean;
var
  QDocs: TFDQuery;
  LMargenHoras: Integer;
begin
  Result := False;
  if AAlbaranId <= 0 then Exit;

  QDocs := TFDQuery.Create(nil);
  try
    QDocs.Connection := dmgMain.dbConn;
    QDocs.SQL.Text :=
      'SELECT id, COALESCE(margen_registro, 24) AS margen_registro FROM ge_calidad_documentos ' +
      'WHERE (activo = 1 OR activo IS NULL) AND (codigo_doc = ''CHK-RECEPCION'' OR periodicidad = ''RECEPCION_MERCANCIA'' OR periodicidad = ''POR RECEPCION'')';
    QDocs.Open;

    while not QDocs.Eof do
    begin
      LMargenHoras := QDocs.FieldByName('margen_registro').AsInteger;
      if LMargenHoras <= 0 then LMargenHoras := 24;

      qryExecCalidad.Close;
      qryExecCalidad.SQL.Text :=
        'INSERT INTO ge_calidad_agenda ' +
        '  (id_empresa, id_documento, id_albaran_compra, fecha_programada, turno, fecha_limite_toma, estado, id_user_creator, id_user_update) ' +
        'SELECT :emp, :doc_id, :alb_id, :fecha, ''UNICO'', :limite, ''PENDIENTE'', :uid, :uid ' +
        'FROM DUAL ' +
        'WHERE NOT EXISTS ( ' +
        '  SELECT 1 FROM ge_calidad_agenda ' +
        '  WHERE id_documento = :doc_chk ' +
        '    AND id_albaran_compra = :alb_chk ' +
        ')';
      qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
      qryExecCalidad.ParamByName('doc_id').AsInteger := QDocs.FieldByName('id').AsInteger;
      qryExecCalidad.ParamByName('alb_id').AsInteger := AAlbaranId;
      qryExecCalidad.ParamByName('fecha').AsDate := Date;
      qryExecCalidad.ParamByName('limite').AsDateTime := Now + (LMargenHoras / 24.0);
      qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
      qryExecCalidad.ParamByName('doc_chk').AsInteger := QDocs.FieldByName('id').AsInteger;
      qryExecCalidad.ParamByName('alb_chk').AsInteger := AAlbaranId;
      qryExecCalidad.ExecSQL;
      QDocs.Next;
    end;
    Result := True;
  finally
    QDocs.Free;
  end;
end;

function TdmgCalidad.VerificarRegistroCompleto(const ARegistroCabId: Integer): Boolean;
var
  Q: TFDQuery;
  LCountSinDatos, LCountTotal: Integer;
begin
  Result := False;
  if ARegistroCabId <= 0 then Exit;

  Q := TFDQuery.Create(nil);
  try
    Q.Connection := dmgMain.dbConn;
    Q.SQL.Text :=
      'SELECT ' +
      '  COUNT(*) AS total_puntos, ' +
      '  SUM(CASE ' +
      '    WHEN (p.tipo_dato = ''ETIQUETA'') THEN 0 ' +
      '    WHEN (p.tipo_dato = ''NUMERICO'' AND d.valor_num IS NULL) THEN 1 ' +
      '    WHEN (p.tipo_dato = ''CHECKLIST'' AND d.conforme IS NULL) THEN 1 ' +
      '    WHEN (p.tipo_dato = ''TEXTO'' AND (d.valor_texto IS NULL OR TRIM(d.valor_texto) = '''')) THEN 1 ' +
      '    WHEN (p.tipo_dato = ''FIRMA'' AND (d.valor_firma IS NULL OR TRIM(d.valor_firma) = '''')) THEN 1 ' +
      '    ELSE 0 ' +
      '  END) AS puntos_sin_datos ' +
      'FROM ge_calidad_registros_det d ' +
      'JOIN ge_calidad_documento_puntos p ON d.id_punto_control = p.id ' +
      'WHERE d.id_registro_cab = :cab_id';
    Q.ParamByName('cab_id').AsInteger := ARegistroCabId;
    Q.Open;

    LCountTotal := Q.FieldByName('total_puntos').AsInteger;
    LCountSinDatos := Q.FieldByName('puntos_sin_datos').AsInteger;

    Result := (LCountTotal > 0) and (LCountSinDatos = 0);
  finally
    Q.Free;
  end;
end;

function TdmgCalidad.VerificarFalloPCC(const ARegistroCabId: Integer): Boolean;
var
  Q: TFDQuery;
begin
  Result := False;
  if ARegistroCabId <= 0 then Exit;

  Q := TFDQuery.Create(nil);
  try
    Q.Connection := dmgMain.dbConn;
    Q.SQL.Text :=
      'SELECT COUNT(*) FROM ge_calidad_registros_det ' +
      'WHERE id_registro_cab = :cab_id AND es_pcc = 1 AND (conforme = 0 OR pcc_alcanzado = 0)';
    Q.ParamByName('cab_id').AsInteger := ARegistroCabId;
    Q.Open;

    Result := Q.Fields[0].AsInteger > 0;
  finally
    Q.Free;
  end;
end;

procedure TdmgCalidad.ActualizarEstadoAgendaSegunRegistro(const ARegistroCabId: Integer);
var
  Q: TFDQuery;
  LAgendaId: Integer;
begin
  if ARegistroCabId <= 0 then Exit;

  Q := TFDQuery.Create(nil);
  try
    Q.Connection := dmgMain.dbConn;
    Q.SQL.Text := 'SELECT id FROM ge_calidad_agenda WHERE id_registro_cab = :cab_id OR id = (SELECT id_calidad_agenda FROM ge_calidad_registros_cab WHERE id = :cab_id2 LIMIT 1)';
    Q.ParamByName('cab_id').AsInteger := ARegistroCabId;
    Q.ParamByName('cab_id2').AsInteger := ARegistroCabId;
    Q.Open;

    while not Q.Eof do
    begin
      LAgendaId := Q.Fields[0].AsInteger;
      VincularRegistroAAgenda(LAgendaId, ARegistroCabId);
      Q.Next;
    end;
  finally
    Q.Free;
  end;
end;

function TdmgCalidad.VincularRegistroAAgenda(const AAgendaId, ARegistroCabId: Integer): Boolean;
var
  LCompleto, LHasPccFallo: Boolean;
  LEstadoNuevo, LEstadoEval: string;
begin
  Result := False;
  if (AAgendaId <= 0) and (ARegistroCabId <= 0) then Exit;

  LCompleto := VerificarRegistroCompleto(ARegistroCabId);
  LHasPccFallo := VerificarFalloPCC(ARegistroCabId);

  if LHasPccFallo then
  begin
    LEstadoEval := 'NO_CONFORME';
    LEstadoNuevo := 'COMPLETADO';
  end
  else if LCompleto then
  begin
    LEstadoEval := 'CONFORME';
    LEstadoNuevo := 'COMPLETADO';
  end
  else
  begin
    LEstadoEval := 'PENDIENTE';
    qryExecCalidad.Close;
    qryExecCalidad.SQL.Text :=
      'SELECT CASE WHEN fecha_limite_toma < NOW() THEN ''VENCIDO'' ELSE ''PENDIENTE'' END ' +
      'FROM ge_calidad_agenda WHERE id = :id';
    qryExecCalidad.ParamByName('id').AsInteger := AAgendaId;
    qryExecCalidad.Open;

    if not qryExecCalidad.IsEmpty then
      LEstadoNuevo := qryExecCalidad.Fields[0].AsString
    else
      LEstadoNuevo := 'PENDIENTE';
  end;

  // 1. Actualizar ge_calidad_agenda
  qryExecCalidad.Close;
  qryExecCalidad.SQL.Text :=
    'UPDATE ge_calidad_agenda SET ' +
    '  id_registro_cab = :reg_id, ' +
    '  estado = :estado, ' +
    '  updated_at = CURRENT_TIMESTAMP, ' +
    '  id_user_update = :uid ' +
    'WHERE id = :agenda_id OR id_registro_cab = :reg_id';
  qryExecCalidad.ParamByName('reg_id').AsInteger := ARegistroCabId;
  qryExecCalidad.ParamByName('estado').AsString := LEstadoNuevo;
  qryExecCalidad.ParamByName('agenda_id').AsInteger := AAgendaId;
  qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
  qryExecCalidad.ExecSQL;

  // 2. Actualizar ge_calidad_registros_cab (estado_evaluacion y tiene_desviacion)
  if ARegistroCabId > 0 then
  begin
    qryExecCalidad.Close;
    qryExecCalidad.SQL.Text :=
      'UPDATE ge_calidad_registros_cab SET ' +
      '  estado_evaluacion = :eval, ' +
      '  tiene_desviacion = :desv ' +
      'WHERE id = :reg_id';
    qryExecCalidad.ParamByName('eval').AsString := LEstadoEval;
    qryExecCalidad.ParamByName('desv').AsInteger := Ord(LHasPccFallo);
    qryExecCalidad.ParamByName('reg_id').AsInteger := ARegistroCabId;
    qryExecCalidad.ExecSQL;
  end;

  if AAgendaId > 0 then
  begin
    qryExecCalidad.Close;
    qryExecCalidad.SQL.Text :=
      'UPDATE ge_calidad_registros_cab SET id_calidad_agenda = :aid WHERE id = :reg_id AND (id_calidad_agenda IS NULL OR id_calidad_agenda = 0)';
    qryExecCalidad.ParamByName('aid').AsInteger := AAgendaId;
    qryExecCalidad.ParamByName('reg_id').AsInteger := ARegistroCabId;
    qryExecCalidad.ExecSQL;
  end;

  Result := True;
end;

function TdmgCalidad.CargarPlanificadorCumplimiento(const ADesde, AHasta: TDate; const ADepto, APeriodicidad, AEstado: string): TFDQuery;
var
  LSql: string;
begin
  if not Assigned(qryPlanner) then
  begin
    qryPlanner := TFDQuery.Create(Self);
    qryPlanner.Connection := dmgMain.dbConn;
  end;

  // Ejecutar auto-sincronizador de la jornada primero
  SincronizarAgendaAutomatica(AHasta);

  qryPlanner.Close;
  LSql :=
    'SELECT a.id AS id_agenda, a.fecha_programada, a.turno, a.fecha_limite_toma, a.estado AS estado_agenda, ' +
    '       a.id_lote_fabricacion, a.id_albaran_compra, a.id_envio_recogida, a.id_hito, ' +
    '       h.nombre AS hito_nombre, a.referencia_origen, a.id_registro_cab, ' +
    '       d.id AS id_documento, d.codigo_doc, d.nombre_doc, COALESCE(g.nombre, ''General'') AS departamento, d.periodicidad, ' +
    '       g.nombre AS grupo_nombre, ' +
    '       r.fecha AS fecha_registro, r.estado_evaluacion, ' +
    '       CASE ' +
    '         WHEN (a.estado = ''COMPLETADO'' AND r.estado_evaluacion = ''NO_CONFORME'') OR r.tiene_desviacion = 1 THEN ''MORADO'' ' +
    '         WHEN a.estado = ''COMPLETADO'' THEN ''VERDE'' ' +
    '         WHEN a.estado = ''VENCIDO'' OR (a.fecha_limite_toma < NOW() AND a.estado = ''PENDIENTE'') THEN ''ROJO'' ' +
    '         ELSE ''AMARILLO'' ' +
    '       END AS semaforo_color ' +
    'FROM ge_calidad_agenda a ' +
    'JOIN ge_calidad_documentos d ON a.id_documento = d.id ' +
    'LEFT JOIN ge_calidad_hitos h ON a.id_hito = h.id ' +
    'LEFT JOIN ge_calidad_grupos g ON d.id_grupo = g.id ' +
    'LEFT JOIN ge_calidad_registros_cab r ON (a.id_registro_cab = r.id OR r.id_calidad_agenda = a.id) ' +
    'WHERE a.fecha_programada BETWEEN :fdesde AND :fhasta ';

  if Trim(ADepto) <> '' then
    LSql := LSql + 'AND COALESCE(g.nombre, ''General'') LIKE ' + QuotedStr('%' + ADepto + '%') + ' ';

  if Trim(APeriodicidad) <> '' then
    LSql := LSql + 'AND d.periodicidad = ' + QuotedStr(APeriodicidad) + ' ';

  if SameText(AEstado, 'VERDE') or SameText(AEstado, 'COMPLETADO') then
    LSql := LSql + 'AND a.estado = ''COMPLETADO'' AND (r.estado_evaluacion IS NULL OR r.estado_evaluacion <> ''NO_CONFORME'') '
  else if SameText(AEstado, 'MORADO') or SameText(AEstado, 'PCC') then
    LSql := LSql + 'AND ((a.estado = ''COMPLETADO'' AND r.estado_evaluacion = ''NO_CONFORME'') OR r.tiene_desviacion = 1) '
  else if SameText(AEstado, 'ROJO') or SameText(AEstado, 'VENCIDO') then
    LSql := LSql + 'AND (a.estado = ''VENCIDO'' OR (a.fecha_limite_toma < NOW() AND a.estado = ''PENDIENTE'')) '
  else if SameText(AEstado, 'AMARILLO') or SameText(AEstado, 'PENDIENTE') then
    LSql := LSql + 'AND a.estado = ''PENDIENTE'' AND a.fecha_limite_toma >= NOW() '
  else if SameText(AEstado, 'PENDIENTES_TODOS') then
    LSql := LSql + 'AND a.estado <> ''COMPLETADO'' ';

  LSql := LSql + 'ORDER BY a.fecha_programada ASC, a.fecha_limite_toma ASC, d.codigo_doc';

  qryPlanner.SQL.Text := LSql;
  qryPlanner.ParamByName('fdesde').AsDate := ADesde;
  qryPlanner.ParamByName('fhasta').AsDate := AHasta;
  qryPlanner.Open;

  Result := qryPlanner;
end;

end.
