﻿unit frm_CalidadDocumentoEditor;

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.Grids, Vcl.DBGrids,
  Vcl.ComCtrls, Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Param, dmg_Main, dmg_Calidad, uBaseForm,
  uDBGridHelper, Winapi.ShellAPI, Vcl.Buttons;

type
  TfrmCalidadDocumentoEditor = class(TfrmBase)
    pnlTop: TPanel;
    lblTitulo: TLabel;
    pgcDocumento: TPageControl;
    
    // Pestaña 1: Configuración General
    tsDocHeader: TTabSheet;
    pnlDocHeader: TPanel;
    lblCodigo: TLabel;
    edtCodigoDoc: TEdit;
    lblNombre: TLabel;
    edtNombreDoc: TEdit;
    lblGrupo: TLabel;
    cboGrupo: TComboBox;
    lblPeriodicidad: TLabel;
    cboPeriodicidad: TComboBox;
    lblDepto: TLabel;
    edtDepto: TEdit;
    lblMargenRegistro: TLabel;
    edtMargenRegistro: TEdit;
    lblInstrucciones: TLabel;
    memInstrucciones: TMemo;
    lblAdjuntoPdf: TLabel;
    edtAdjuntoPdf: TEdit;
    btnBuscarPdf: TButton;
    btnVerPdf: TButton;
    btnBorrarPdf: TButton;
    
    // Pestaña 2: Identificación del Registro
    tsIdentificacion: TTabSheet;
    pnlIdentHeader: TPanel;
    lblIdentTitulo: TLabel;
    btnNuevoIdent: TButton;
    btnEliminarIdent: TButton;
    dbGridIdent: TDBGrid;

    // Pestaña 3: Puntos de Control y PCC
    tsPuntosControl: TTabSheet;
    pnlPuntosHeader: TPanel;
    lblPuntosTitulo: TLabel;
    btnNuevoPunto: TButton;
    btnEliminarPunto: TButton;
    dbGridPuntos: TDBGrid;
    
    // Pestaña 4: Acciones Correctivas
    tsAccionesCorrectivas: TTabSheet;
    pnlAccionesTop: TPanel;
    lblAccionesHeader: TLabel;
    btnNuevoAccion: TButton;
    btnEliminarAccion: TButton;
    dbGridAcciones: TDBGrid;
    
    // Pestaña 5: Firmas de Control
    tsFirmasControl: TTabSheet;
    pnlFirmasTop: TPanel;
    lblFirmasHeader: TLabel;
    btnNuevoFirma: TButton;
    btnEliminarFirma: TButton;
    dbGridFirmas: TDBGrid;

    dsPuntos: TDataSource;
    dlgOpenPdf: TOpenDialog;

    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure pgcDocumentoChange(Sender: TObject);
    procedure qryDocumentoPuntosNewRecord(DataSet: TDataSet);
    procedure btnGuardarDocClick(Sender: TObject);
    procedure btnCancelarDocClick(Sender: TObject);
    procedure btnNuevoPuntoClick(Sender: TObject);
    procedure btnEliminarPuntoClick(Sender: TObject);
    procedure btnBuscarPdfClick(Sender: TObject);
    procedure btnVerPdfClick(Sender: TObject);
    procedure btnBorrarPdfClick(Sender: TObject);
    procedure dbGridDrawColumnCell(Sender: TObject; const Rect: TRect;
      DataCol: Integer; Column: TColumn; State: TGridDrawState);
    procedure dbGridCellClick(Column: TColumn);

    // Métodos de la barra de acciones Ribbon heredada
    procedure btnAnadirClick(Sender: TObject);
    procedure btnEliminarClick(Sender: TObject);
    procedure btnPrimeroClick(Sender: TObject);
    procedure btnAnteriorClick(Sender: TObject);
    procedure btnSiguienteClick(Sender: TObject);
    procedure btnUltimoClick(Sender: TObject);
    procedure btnDuplicarClick(Sender: TObject);
    procedure btnImprimirClick(Sender: TObject); override;
    procedure btnIAClick(Sender: TObject);
  protected
    // Sobrescribir acciones virtuales heredadas de TfrmBase
    procedure DoAnadir; override;
    procedure DoPrimero; override;
    procedure DoAnterior; override;
    procedure DoSiguiente; override;
    procedure DoUltimo; override;
    procedure DoDuplicar; override;
    procedure DoCancelar; override;
  private
    { Private declarations }
    FDocumentoId: Integer;
    FFileStreamAdjunto: TMemoryStream;
    FFileChanged: Boolean;
    procedure SetDocumentoId(const Value: Integer);
    procedure CargarDatosDocumento;
    procedure CargarGruposCombo;
    procedure NavegarA(const AId: Integer);
    procedure AnalizarDocumentoConIA(const ARutaArchivo: string);
    function ObtenerCarpetaTemp: string;
    procedure SetFieldBoolOrInt(AField: TField; const AValue: Boolean);
    function GetFieldBoolOrInt(AField: TField): Boolean;
    procedure AgregarPuntoControlIA(const ASeccion, ACodigo, ANombre, ATipo, AUnidad: string;
      const AMin, AMax: Double; const AEsPCC: Boolean; const AOrden: Integer);
    procedure AplicarFiltroSeccion;
    procedure ConfigurarPickListsGrids;
  public
    { Public declarations }
    property DocumentoId: Integer read FDocumentoId write SetDocumentoId;
    procedure GenerarPlantillaImprimibleHTML;
  end;

var
  frmCalidadDocumentoEditor: TfrmCalidadDocumentoEditor;

implementation

{$R *.dfm}

uses uDbErrorHandler;

procedure TfrmCalidadDocumentoEditor.FormCreate(Sender: TObject);
begin
  inherited;
  if not Assigned(dmgCalidad) then
    Application.CreateForm(TdmgCalidad, dmgCalidad);
  FDocumentoId := 0;
  FFileStreamAdjunto := TMemoryStream.Create;
  FFileChanged := False;
  dsPuntos.DataSet := dmgCalidad.qryDocumentoPuntos;
  dmgCalidad.qryDocumentoPuntos.OnNewRecord := qryDocumentoPuntosNewRecord;
  pgcDocumento.ActivePageIndex := 0;
end;

procedure TfrmCalidadDocumentoEditor.qryDocumentoPuntosNewRecord(DataSet: TDataSet);
var
  LSeccion: string;
begin
  if DataSet.FindField('id_documento') <> nil then
  begin
    DataSet.FieldByName('id_documento').Required := False;
    DataSet.FieldByName('id_documento').AsInteger := FDocumentoId;
  end;

  if DataSet.FindField('codigo_seccion') <> nil then
  begin
    case pgcDocumento.ActivePageIndex of
      1: LSeccion := 'IDENTIFICACION';
      2: LSeccion := 'PUNTOS_CONTROL';
      3: LSeccion := 'ACCIONES_CORRECTIVAS';
      4: LSeccion := 'FIRMAS';
    else
      LSeccion := 'PUNTOS_CONTROL';
    end;
    DataSet.FieldByName('codigo_seccion').AsString := LSeccion;
  end;
end;

procedure TfrmCalidadDocumentoEditor.FormDestroy(Sender: TObject);
begin
  if Assigned(FFileStreamAdjunto) then
    FreeAndNil(FFileStreamAdjunto);
  inherited;
end;

procedure TfrmCalidadDocumentoEditor.FormShow(Sender: TObject);
begin
  inherited;
  CargarGruposCombo;
  CargarDatosDocumento;
  ConfigurarPickListsGrids;
end;

procedure TfrmCalidadDocumentoEditor.SetDocumentoId(const Value: Integer);
begin
  FDocumentoId := Value;
  if Showing or (FDocumentoId > 0) then
  begin
    CargarGruposCombo;
    CargarDatosDocumento;
  end;
end;

procedure TfrmCalidadDocumentoEditor.pgcDocumentoChange(Sender: TObject);
begin
  AplicarFiltroSeccion;
end;

procedure TfrmCalidadDocumentoEditor.AplicarFiltroSeccion;
begin
  if not dmgCalidad.qryDocumentoPuntos.Active then Exit;

  dmgCalidad.qryDocumentoPuntos.DisableControls;
  try
    case pgcDocumento.ActivePageIndex of
      1: // Pestaña 2: Identificación del Registro
        begin
          dmgCalidad.qryDocumentoPuntos.Filter := 'codigo_seccion = ''IDENTIFICACION''';
          dmgCalidad.qryDocumentoPuntos.Filtered := True;
        end;
      2: // Pestaña 3: Puntos de Control
        begin
          dmgCalidad.qryDocumentoPuntos.Filter := 'codigo_seccion = ''PUNTOS_CONTROL''';
          dmgCalidad.qryDocumentoPuntos.Filtered := True;
        end;
      3: // Pestaña 4: Acciones Correctivas
        begin
          dmgCalidad.qryDocumentoPuntos.Filter := 'codigo_seccion = ''ACCIONES_CORRECTIVAS''';
          dmgCalidad.qryDocumentoPuntos.Filtered := True;
        end;
      4: // Pestaña 5: Firmas de Control
        begin
          dmgCalidad.qryDocumentoPuntos.Filter := 'codigo_seccion = ''FIRMAS''';
          dmgCalidad.qryDocumentoPuntos.Filtered := True;
        end;
      else
        dmgCalidad.qryDocumentoPuntos.Filtered := False;
    end;
  finally
    dmgCalidad.qryDocumentoPuntos.EnableControls;
  end;
end;

procedure TfrmCalidadDocumentoEditor.SetFieldBoolOrInt(AField: TField; const AValue: Boolean);
begin
  if AField = nil then Exit;
  if AField.DataType = ftBoolean then
    AField.AsBoolean := AValue
  else
    AField.AsInteger := Ord(AValue);
end;

function TfrmCalidadDocumentoEditor.GetFieldBoolOrInt(AField: TField): Boolean;
begin
  if (AField = nil) or AField.IsNull then
    Result := False
  else if AField.DataType = ftBoolean then
    Result := AField.AsBoolean
  else
    Result := (AField.AsInteger <> 0) or AField.AsBoolean;
end;

function TfrmCalidadDocumentoEditor.ObtenerCarpetaTemp: string;
begin
  Result := IncludeTrailingPathDelimiter(GetHomePath) + 'GESTION_CALIDAD_TEMP' + PathDelim;
  if not DirectoryExists(Result) then
    ForceDirectories(Result);
end;

procedure TfrmCalidadDocumentoEditor.CargarGruposCombo;
var
  Q: TFDQuery;
begin
  cboGrupo.Items.Clear;
  Q := dmgCalidad.CargarGrupos;
  while not Q.Eof do
  begin
    cboGrupo.Items.AddObject(Q.FieldByName('nombre').AsString, TObject(IntPtr(Q.FieldByName('id').AsInteger)));
    Q.Next;
  end;
  if cboGrupo.Items.Count > 0 then
    cboGrupo.ItemIndex := 0;
end;

procedure TfrmCalidadDocumentoEditor.CargarDatosDocumento;
var
  Q: TFDQuery;
  i, LGrupoId: Integer;
  LPer: string;
begin
  if cboGrupo.Items.Count = 0 then
    CargarGruposCombo;

  FFileStreamAdjunto.Clear;
  FFileChanged := False;

  if FDocumentoId > 0 then
  begin
    lblTitulo.Caption := Format('Edición de Documento de Calidad (ID: %d)', [FDocumentoId]);
    dmgCalidad.qryExecCalidad.Close;
    dmgCalidad.qryExecCalidad.SQL.Text := 'SELECT * FROM ge_calidad_documentos WHERE id = :id';
    dmgCalidad.qryExecCalidad.ParamByName('id').AsInteger := FDocumentoId;
    dmgCalidad.qryExecCalidad.Open;

    if not dmgCalidad.qryExecCalidad.IsEmpty then
    begin
      Q := dmgCalidad.qryExecCalidad;
      edtCodigoDoc.Text := Q.FieldByName('codigo_doc').AsString;
      edtNombreDoc.Text := Q.FieldByName('nombre_doc').AsString;
      edtDepto.Text := Q.FieldByName('departamento_responsable').AsString;
      memInstrucciones.Text := Q.FieldByName('instrucciones').AsString;
      
      LGrupoId := Q.FieldByName('id_grupo').AsInteger;
      cboGrupo.ItemIndex := -1;
      for i := 0 to cboGrupo.Items.Count - 1 do
      begin
        if Integer(IntPtr(cboGrupo.Items.Objects[i])) = LGrupoId then
        begin
          cboGrupo.ItemIndex := i;
          Break;
        end;
      end;

      LPer := Q.FieldByName('periodicidad').AsString;
      cboPeriodicidad.ItemIndex := -1;
      for i := 0 to cboPeriodicidad.Items.Count - 1 do
      begin
        if SameText(cboPeriodicidad.Items[i], LPer) then
        begin
          cboPeriodicidad.ItemIndex := i;
          Break;
        end;
      end;
      if cboPeriodicidad.ItemIndex < 0 then
        cboPeriodicidad.Text := LPer;

      if Q.FindField('margen_registro') <> nil then
        edtMargenRegistro.Text := IntToStr(Q.FieldByName('margen_registro').AsInteger)
      else
        edtMargenRegistro.Text := '24';
      if StrToIntDef(edtMargenRegistro.Text, 0) <= 0 then
        edtMargenRegistro.Text := '24';
    end;

    // Cargar nombre del archivo adjunto desde ge_documentos
    dmgCalidad.qryExecCalidad.Close;
    dmgCalidad.qryExecCalidad.SQL.Text :=
      'SELECT nombre_archivo FROM ge_documentos ' +
      'WHERE id_documento_calidad = :doc_id AND publicar = 1 ' +
      'ORDER BY id DESC LIMIT 1';
    dmgCalidad.qryExecCalidad.ParamByName('doc_id').AsInteger := FDocumentoId;
    dmgCalidad.qryExecCalidad.Open;

    if not dmgCalidad.qryExecCalidad.IsEmpty then
      edtAdjuntoPdf.Text := dmgCalidad.qryExecCalidad.FieldByName('nombre_archivo').AsString
    else
      edtAdjuntoPdf.Text := '';
  end
  else
  begin
    lblTitulo.Caption := 'Nuevo Documento de Calidad';
    edtCodigoDoc.Text := 'CHK-RECEPCION';
    edtNombreDoc.Text := 'CHECKLIST INSPECCIÓN Y RECEPCIÓN DE MERCANCÍAS';
    edtDepto.Text := 'Almacén / Obrador';
    edtMargenRegistro.Text := '24';
    memInstrucciones.Text := 'Inspección de estado del camión de proveedores, embalaje, temperatura y lote.';
    edtAdjuntoPdf.Text := '';
    if cboPeriodicidad.Items.Count > 0 then
      cboPeriodicidad.ItemIndex := 0;
  end;

  dmgCalidad.CargarPuntosControl(FDocumentoId);
  if dmgCalidad.qryDocumentoPuntos.FindField('id_documento') <> nil then
    dmgCalidad.qryDocumentoPuntos.FieldByName('id_documento').Required := False;
  dsPuntos.DataSet := dmgCalidad.qryDocumentoPuntos;
  AplicarFiltroSeccion;
  ConfigurarPickListsGrids;
end;

procedure TfrmCalidadDocumentoEditor.ConfigurarPickListsGrids;
var
  LGrids: array[0..3] of TDBGrid;
  i, j: Integer;
  Col: TColumn;
begin
  LGrids[0] := dbGridIdent;
  LGrids[1] := dbGridPuntos;
  LGrids[2] := dbGridAcciones;
  LGrids[3] := dbGridFirmas;

  for i := 0 to High(LGrids) do
  begin
    if not Assigned(LGrids[i]) then Continue;

    LGrids[i].OnDrawColumnCell := dbGridDrawColumnCell;
    LGrids[i].OnCellClick := dbGridCellClick;

    for j := 0 to LGrids[i].Columns.Count - 1 do
    begin
      Col := LGrids[i].Columns[j];
      if not Assigned(Col) then Continue;

      // Columnas Booleanas: Marcar como ReadOnly para suprimir el editor de texto TInplaceEdit
      if SameText(Col.FieldName, 'es_pcc') or SameText(Col.FieldName, 'es_obligatorio') then
      begin
        Col.ReadOnly := True;
      end
      // Desplegable de Opciones para TIPO DATO
      else if SameText(Col.FieldName, 'tipo_dato') then
      begin
        Col.PickList.Clear;
        Col.PickList.Add('TEXTO');
        Col.PickList.Add('NUMERICO');
        Col.PickList.Add('CHECKLIST');
        Col.PickList.Add('FECHA');
        Col.PickList.Add('FIRMA');
        Col.PickList.Add('ETIQUETA');
      end
      // Desplegable de Opciones Normalizadas para FORMATO / UNIDAD
      else if SameText(Col.FieldName, 'unidad') then
      begin
        Col.PickList.Clear;
        Col.PickList.Add('FECHA');
        Col.PickList.Add('TURNO');
        Col.PickList.Add('NOMBRE');
        Col.PickList.Add('VARIEDAD');
        Col.PickList.Add('CODIGO');
        Col.PickList.Add('KG');
        Col.PickList.Add('G');
        Col.PickList.Add('L');
        Col.PickList.Add('ML');
        Col.PickList.Add('ºC');
        Col.PickList.Add('PPM');
        Col.PickList.Add('PH');
        Col.PickList.Add('%');
        Col.PickList.Add('MINUTOS');
        Col.PickList.Add('HORAS');
        Col.PickList.Add('UNIDADES');
        Col.PickList.Add('S/N');
        Col.PickList.Add('OK/KO');
      end;
    end;
  end;
end;

procedure TfrmCalidadDocumentoEditor.dbGridDrawColumnCell(Sender: TObject;
  const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState);
const
  IsChecked: array[Boolean] of Integer = (DFCS_BUTTONCHECK, DFCS_BUTTONCHECK or DFCS_CHECKED);
var
  Grid: TDBGrid;
  R: TRect;
  VChecked: Boolean;
  W, H: Integer;
begin
  Grid := Sender as TDBGrid;
  if SameText(Column.FieldName, 'es_pcc') or SameText(Column.FieldName, 'es_obligatorio') then
  begin
    if gdSelected in State then
    begin
      Grid.Canvas.Brush.Color := clHighlight;
      Grid.Canvas.Font.Color := clHighlightText;
    end
    else
    begin
      Grid.Canvas.Brush.Color := Grid.Color;
      Grid.Canvas.Font.Color := Grid.Font.Color;
    end;
    Grid.Canvas.FillRect(Rect);

    if Column.Field <> nil then
    begin
      if (Column.Field is TBooleanField) and (TBooleanField(Column.Field).DisplayValues <> ';') then
        TBooleanField(Column.Field).DisplayValues := ';';

      VChecked := GetFieldBoolOrInt(Column.Field);
      W := 14;
      H := 14;
      R.Left := Rect.Left + (Rect.Width - W) div 2;
      R.Top := Rect.Top + (Rect.Height - H) div 2;
      R.Right := R.Left + W;
      R.Bottom := R.Top + H;
      DrawFrameControl(Grid.Canvas.Handle, R, DFC_BUTTON, IsChecked[VChecked]);
    end;
  end
  else
    Grid.DefaultDrawColumnCell(Rect, DataCol, Column, State);
end;

procedure TfrmCalidadDocumentoEditor.dbGridCellClick(Column: TColumn);
var
  CurrentVal: Boolean;
begin
  if SameText(Column.FieldName, 'es_pcc') or SameText(Column.FieldName, 'es_obligatorio') then
  begin
    if (Column.Field <> nil) and (Column.Field.DataSet <> nil) and
       (Column.Field.DataSet.State in [dsBrowse, dsEdit, dsInsert]) then
    begin
      if not (Column.Field.DataSet.State in [dsEdit, dsInsert]) then
        Column.Field.DataSet.Edit;

      CurrentVal := GetFieldBoolOrInt(Column.Field);
      SetFieldBoolOrInt(Column.Field, not CurrentVal);
    end;
  end;
end;

procedure TfrmCalidadDocumentoEditor.NavegarA(const AId: Integer);
begin
  if AId > 0 then
  begin
    FDocumentoId := AId;
    CargarDatosDocumento;
  end;
end;

procedure TfrmCalidadDocumentoEditor.DoAnadir;
begin
  btnAnadirClick(Self);
end;

procedure TfrmCalidadDocumentoEditor.DoPrimero;
begin
  btnPrimeroClick(Self);
end;

procedure TfrmCalidadDocumentoEditor.DoAnterior;
begin
  btnAnteriorClick(Self);
end;

procedure TfrmCalidadDocumentoEditor.DoSiguiente;
begin
  btnSiguienteClick(Self);
end;

procedure TfrmCalidadDocumentoEditor.DoUltimo;
begin
  btnUltimoClick(Self);
end;

procedure TfrmCalidadDocumentoEditor.DoDuplicar;
begin
  btnDuplicarClick(Self);
end;

procedure TfrmCalidadDocumentoEditor.DoCancelar;
begin
  btnCancelarDocClick(Self);
end;

procedure TfrmCalidadDocumentoEditor.btnNuevoPuntoClick(Sender: TObject);
var
  LSeccion: string;
begin
  case pgcDocumento.ActivePageIndex of
    1: LSeccion := 'IDENTIFICACION';
    2: LSeccion := 'PUNTOS_CONTROL';
    3: LSeccion := 'ACCIONES_CORRECTIVAS';
    4: LSeccion := 'FIRMAS';
  else
    LSeccion := 'PUNTOS_CONTROL';
  end;

  if dmgCalidad.qryDocumentoPuntos.Active then
  begin
    dmgCalidad.qryDocumentoPuntos.Append;
    if dmgCalidad.qryDocumentoPuntos.FindField('id_documento') <> nil then
    begin
      dmgCalidad.qryDocumentoPuntos.FieldByName('id_documento').Required := False;
      dmgCalidad.qryDocumentoPuntos.FieldByName('id_documento').AsInteger := FDocumentoId;
    end;
    if dmgCalidad.qryDocumentoPuntos.FindField('codigo_seccion') <> nil then
      dmgCalidad.qryDocumentoPuntos.FieldByName('codigo_seccion').AsString := LSeccion;
    dmgCalidad.qryDocumentoPuntos.FieldByName('codigo_punto').AsString := 'NUEVO_CAMPO';
    dmgCalidad.qryDocumentoPuntos.FieldByName('nombre_punto').AsString := 'Nuevo Campo / Registro';
    dmgCalidad.qryDocumentoPuntos.FieldByName('tipo_dato').AsString := 'TEXTO';
    dmgCalidad.qryDocumentoPuntos.FieldByName('unidad').AsString := '';
    SetFieldBoolOrInt(dmgCalidad.qryDocumentoPuntos.FieldByName('es_pcc'), False);
    SetFieldBoolOrInt(dmgCalidad.qryDocumentoPuntos.FieldByName('es_obligatorio'), True);
    dmgCalidad.qryDocumentoPuntos.FieldByName('orden_visual').AsInteger := dmgCalidad.qryDocumentoPuntos.RecordCount + 1;
    dmgCalidad.qryDocumentoPuntos.FieldByName('id_user_creator').AsString := dmgMain.CurrentUserId;
    dmgCalidad.qryDocumentoPuntos.FieldByName('id_user_update').AsString := dmgMain.CurrentUserId;
  end;
end;

procedure TfrmCalidadDocumentoEditor.btnEliminarPuntoClick(Sender: TObject);
begin
  if not dmgCalidad.qryDocumentoPuntos.IsEmpty then
  begin
    if MessageDlg('¿Confirma eliminar este campo/punto de control?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
      dmgCalidad.qryDocumentoPuntos.Delete;
  end;
end;

procedure TfrmCalidadDocumentoEditor.btnBuscarPdfClick(Sender: TObject);
begin
  dlgOpenPdf.Title := 'Seleccionar Documento Físico para Almacenar en ge_documentos';
  if dlgOpenPdf.Execute then
  begin
    FFileStreamAdjunto.Clear;
    FFileStreamAdjunto.LoadFromFile(dlgOpenPdf.FileName);
    FFileChanged := True;
    edtAdjuntoPdf.Text := ExtractFileName(dlgOpenPdf.FileName);
    MessageDlg(Format('Archivo "%s" cargado (%.2f KB).'#13#10'Al pulsar Guardar se almacenará en la tabla central ge_documentos.', 
      [edtAdjuntoPdf.Text, FFileStreamAdjunto.Size / 1024.0]), mtInformation, [mbOK], 0);
  end;
end;

procedure TfrmCalidadDocumentoEditor.btnBorrarPdfClick(Sender: TObject);
begin
  if Trim(edtAdjuntoPdf.Text) <> '' then
  begin
    if MessageDlg('¿Confirma eliminar el documento físico adjunto de este registro y de la tabla documentos?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
    begin
      if FDocumentoId > 0 then
      begin
        try
          dmgCalidad.qryExecCalidad.Close;
          dmgCalidad.qryExecCalidad.SQL.Text := 'DELETE FROM ge_documentos WHERE id_documento_calidad = :doc_id';
          dmgCalidad.qryExecCalidad.ParamByName('doc_id').AsInteger := FDocumentoId;
          dmgCalidad.qryExecCalidad.ExecSQL;
        except
          // Se asegurará también al pulsar Guardar
        end;
      end;
      FFileStreamAdjunto.Clear;
      FFileChanged := True;
      edtAdjuntoPdf.Text := '';
    end;
  end;
end;

procedure TfrmCalidadDocumentoEditor.btnVerPdfClick(Sender: TObject);
var
  LRutaOut: string;
  QBlob: TFDQuery;
  LBlobStream: TStream;
begin
  if Trim(edtAdjuntoPdf.Text) = '' then
  begin
    MessageDlg('No se ha adjuntado ningún documento físico. Utilice el botón "Adjuntar..." para cargar un archivo en ge_documentos.', mtInformation, [mbOK], 0);
    Exit;
  end;

  LRutaOut := ObtenerCarpetaTemp + edtAdjuntoPdf.Text;

  // Si se ha seleccionado un archivo nuevo en memoria y aún no se ha guardado
  if FFileChanged and (FFileStreamAdjunto.Size > 0) then
  begin
    FFileStreamAdjunto.SaveToFile(LRutaOut);
    ShellExecute(0, 'open', PChar(LRutaOut), nil, nil, SW_SHOWNORMAL);
    Exit;
  end;

  // Extraer el BLOB binario directo de la tabla ge_documentos
  if FDocumentoId > 0 then
  begin
    QBlob := TFDQuery.Create(nil);
    try
      QBlob.Connection := dmgMain.dbConn;
      QBlob.SQL.Text :=
        'SELECT nombre_archivo, archivo FROM ge_documentos ' +
        'WHERE id_documento_calidad = :doc_id AND publicar = 1 ' +
        'ORDER BY id DESC LIMIT 1';
      QBlob.ParamByName('doc_id').AsInteger := FDocumentoId;
      QBlob.Open;

      if not QBlob.IsEmpty and not QBlob.FieldByName('archivo').IsNull then
      begin
        LBlobStream := QBlob.CreateBlobStream(QBlob.FieldByName('archivo'), bmRead);
        try
          TMemoryStream(LBlobStream).SaveToFile(LRutaOut);
        finally
          LBlobStream.Free;
        end;
        ShellExecute(0, 'open', PChar(LRutaOut), nil, nil, SW_SHOWNORMAL);
      end
      else
      begin
        MessageDlg('El documento adjunto no contiene datos de archivo almacenados en ge_documentos.', mtWarning, [mbOK], 0);
      end;
    finally
      QBlob.Free;
    end;
  end;
end;

procedure TfrmCalidadDocumentoEditor.btnGuardarDocClick(Sender: TObject);
var
  LGrupoId: Integer;
  LPer: string;
begin
  if Trim(edtCodigoDoc.Text) = '' then
  begin
    MessageDlg('El código del documento es obligatorio.', mtWarning, [mbOK], 0);
    Exit;
  end;

  if cboGrupo.ItemIndex < 0 then
  begin
    MessageDlg('Debe seleccionar un grupo de informes.', mtWarning, [mbOK], 0);
    Exit;
  end;

  LGrupoId := Integer(IntPtr(cboGrupo.Items.Objects[cboGrupo.ItemIndex]));
  LPer := cboPeriodicidad.Text;

  // Desactivar temporalmente los filtros para guardar la totalidad de puntos de todas las pestañas
  dmgCalidad.qryDocumentoPuntos.DisableControls;
  try
    dmgCalidad.qryDocumentoPuntos.Filtered := False;

    // 1. Guardar o actualizar la cabecera en ge_calidad_documentos
    dmgCalidad.qryExecCalidad.Close;
    if FDocumentoId > 0 then
    begin
      dmgCalidad.qryExecCalidad.SQL.Text :=
        'UPDATE ge_calidad_documentos SET ' +
        '  id_grupo = :gid, codigo_doc = :cdoc, nombre_doc = :ndoc, ' +
        '  periodicidad = :per, margen_registro = :margen, departamento_responsable = :depto, ' +
        '  instrucciones = :inst, id_user_update = :uid ' +
        'WHERE id = :id';
      dmgCalidad.qryExecCalidad.ParamByName('id').AsInteger := FDocumentoId;
    end
    else
    begin
      dmgCalidad.qryExecCalidad.SQL.Text :=
        'INSERT INTO ge_calidad_documentos (' +
        '  id_grupo, codigo_doc, nombre_doc, periodicidad, margen_registro, ' +
        '  departamento_responsable, instrucciones, id_user_creator, id_user_update' +
        ') VALUES (' +
        '  :gid, :cdoc, :ndoc, :per, :margen, :depto, :inst, :uid, :uid' +
        ')';
    end;

    dmgCalidad.qryExecCalidad.ParamByName('gid').AsInteger := LGrupoId;
    dmgCalidad.qryExecCalidad.ParamByName('cdoc').DataType := ftWideString;
    dmgCalidad.qryExecCalidad.ParamByName('cdoc').AsWideString := Trim(edtCodigoDoc.Text);
    dmgCalidad.qryExecCalidad.ParamByName('ndoc').DataType := ftWideString;
    dmgCalidad.qryExecCalidad.ParamByName('ndoc').AsWideString := Trim(edtNombreDoc.Text);
    dmgCalidad.qryExecCalidad.ParamByName('per').DataType := ftWideString;
    dmgCalidad.qryExecCalidad.ParamByName('per').AsWideString := Trim(LPer);
    dmgCalidad.qryExecCalidad.ParamByName('margen').AsInteger := StrToIntDef(edtMargenRegistro.Text, 24);
    dmgCalidad.qryExecCalidad.ParamByName('depto').DataType := ftWideString;
    dmgCalidad.qryExecCalidad.ParamByName('depto').AsWideString := Trim(edtDepto.Text);
    dmgCalidad.qryExecCalidad.ParamByName('inst').DataType := ftWideString;
    dmgCalidad.qryExecCalidad.ParamByName('inst').AsWideString := Trim(memInstrucciones.Text);
    dmgCalidad.qryExecCalidad.ParamByName('uid').AsString := dmgMain.CurrentUserId;
    dmgCalidad.qryExecCalidad.ExecSQL;

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

    // 2. Guardar o actualizar el documento físico en la tabla central ge_documentos
    if FFileChanged then
    begin
      if Trim(edtAdjuntoPdf.Text) = '' then
      begin
        dmgCalidad.qryExecCalidad.Close;
        dmgCalidad.qryExecCalidad.SQL.Text :=
          'DELETE FROM ge_documentos WHERE id_documento_calidad = :doc_id';
        dmgCalidad.qryExecCalidad.ParamByName('doc_id').AsInteger := FDocumentoId;
        dmgCalidad.qryExecCalidad.ExecSQL;
      end
      else if FFileStreamAdjunto.Size > 0 then
      begin
        dmgCalidad.qryExecCalidad.Close;
        dmgCalidad.qryExecCalidad.SQL.Text :=
          'SELECT id FROM ge_documentos WHERE id_documento_calidad = :doc_id AND publicar = 1';
        dmgCalidad.qryExecCalidad.ParamByName('doc_id').AsInteger := FDocumentoId;
        dmgCalidad.qryExecCalidad.Open;

        if not dmgCalidad.qryExecCalidad.IsEmpty then
        begin
          dmgCalidad.qryExecCalidad.Close;
          dmgCalidad.qryExecCalidad.SQL.Text :=
            'UPDATE ge_documentos SET ' +
            '  id_empresa = :emp, fecha = :fecha, descripcion = :desc, ' +
            '  archivo = :blob, nombre_archivo = :nombre, extension = :ext, ' +
            '  publicar = 1, id_user_update = :user ' +
            'WHERE id_documento_calidad = :doc_id';
        end
        else
        begin
          dmgCalidad.qryExecCalidad.Close;
          dmgCalidad.qryExecCalidad.SQL.Text :=
            'INSERT INTO ge_documentos (' +
            '  id_empresa, fecha, descripcion, archivo, nombre_archivo, extension, ' +
            '  publicar, id_documento_calidad, id_user_creator, id_user_update' +
            ') VALUES (' +
            '  :emp, :fecha, :desc, :blob, :nombre, :ext, ' +
            '  1, :doc_id, :user, :user' +
            ')';
        end;

        dmgCalidad.qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
        dmgCalidad.qryExecCalidad.ParamByName('fecha').AsDate := Date;
        dmgCalidad.qryExecCalidad.ParamByName('desc').AsString := 'Plantilla / Documento Físico de Calidad ' + edtCodigoDoc.Text;
        dmgCalidad.qryExecCalidad.ParamByName('nombre').AsString := edtAdjuntoPdf.Text;
        dmgCalidad.qryExecCalidad.ParamByName('ext').AsString := Copy(ExtractFileExt(edtAdjuntoPdf.Text), 1, 10);
        dmgCalidad.qryExecCalidad.ParamByName('doc_id').AsInteger := FDocumentoId;
        dmgCalidad.qryExecCalidad.ParamByName('user').AsInteger := StrToIntDef(dmgMain.CurrentUserId, 1);

        FFileStreamAdjunto.Position := 0;
        dmgCalidad.qryExecCalidad.ParamByName('blob').LoadFromStream(FFileStreamAdjunto, ftBlob);
        dmgCalidad.qryExecCalidad.ExecSQL;
      end;
    end;

    // 3. Guardar cambios en los puntos de control de todas las secciones
    if dmgCalidad.qryDocumentoPuntos.Active then
    begin
      dmgCalidad.qryDocumentoPuntos.First;
      while not dmgCalidad.qryDocumentoPuntos.Eof do
      begin
        if dmgCalidad.qryDocumentoPuntos.FieldByName('id_documento').AsInteger <> FDocumentoId then
        begin
          dmgCalidad.qryDocumentoPuntos.Edit;
          dmgCalidad.qryDocumentoPuntos.FieldByName('id_documento').AsInteger := FDocumentoId;
          dmgCalidad.qryDocumentoPuntos.Post;
        end;
        dmgCalidad.qryDocumentoPuntos.Next;
      end;

      if dmgCalidad.qryDocumentoPuntos.State in [dsEdit, dsInsert] then
        dmgCalidad.qryDocumentoPuntos.Post;
        
      if dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    end;

    FFileChanged := False;
    ResetChangeTracking;
    MessageDlg('Configuración completa del Documento de Calidad (Identificación, Puntos de Control, Acciones Correctivas y Firmas) guardada con éxito.', mtInformation, [mbOK], 0);
  finally
    dmgCalidad.qryDocumentoPuntos.EnableControls;
    AplicarFiltroSeccion;
  end;
end;

procedure TfrmCalidadDocumentoEditor.btnCancelarDocClick(Sender: TObject);
begin
  if dmgCalidad.qryDocumentoPuntos.Active and (dmgCalidad.qryDocumentoPuntos.State in [dsEdit, dsInsert]) then
    dmgCalidad.qryDocumentoPuntos.Cancel;
  ModalResult := mrCancel;
end;

procedure TfrmCalidadDocumentoEditor.btnAnadirClick(Sender: TObject);
begin
  FDocumentoId := 0;
  FFileStreamAdjunto.Clear;
  FFileChanged := False;
  edtCodigoDoc.Text := 'CHK-RECEPCION';
  edtNombreDoc.Text := 'CHECKLIST INSPECCIÓN Y RECEPCIÓN DE MERCANCÍAS';
  edtDepto.Text := 'Almacén / Obrador';
  memInstrucciones.Text := 'Inspección de estado del camión de proveedores, embalaje, temperatura y lote.';
  edtAdjuntoPdf.Text := '';
  if cboGrupo.Items.Count > 0 then
    cboGrupo.ItemIndex := 0;
    
  dmgCalidad.CargarPuntosControl(0);
  dsPuntos.DataSet := dmgCalidad.qryDocumentoPuntos;
  lblTitulo.Caption := 'Nuevo Documento de Calidad';
  pgcDocumento.ActivePageIndex := 0;
  AplicarFiltroSeccion;
end;

procedure TfrmCalidadDocumentoEditor.btnEliminarClick(Sender: TObject);
begin
  if FDocumentoId = 0 then Exit;

  if MessageDlg('¿Confirma desactivar este documento del catálogo de Calidad?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    try
      dmgCalidad.qryExecCalidad.Close;
      dmgCalidad.qryExecCalidad.SQL.Text := 'UPDATE ge_calidad_documentos SET activo = 0 WHERE id = :id';
      dmgCalidad.qryExecCalidad.ParamByName('id').AsInteger := FDocumentoId;
      dmgCalidad.qryExecCalidad.ExecSQL;
      
      MessageDlg('Documento desactivado.', mtInformation, [mbOK], 0);
      ModalResult := mrOk;
    except
      on E: Exception do
        TDbErrorHandler.HandleException(E, 'Error al desactivar documento', Self.ClassName, 'btnEliminarClick');
    end;
  end;
end;

procedure TfrmCalidadDocumentoEditor.btnPrimeroClick(Sender: TObject);
var
  LId: Integer;
begin
  LId := StrToIntDef(VarToStr(dmgMain.dbConn.ExecSQLScalar('SELECT id FROM ge_calidad_documentos WHERE (activo=1 OR activo IS NULL) ORDER BY codigo_doc ASC LIMIT 1')), 0);
  NavegarA(LId);
end;

procedure TfrmCalidadDocumentoEditor.btnAnteriorClick(Sender: TObject);
var
  LId: Integer;
begin
  LId := StrToIntDef(VarToStr(dmgMain.dbConn.ExecSQLScalar(Format('SELECT id FROM ge_calidad_documentos WHERE (activo=1 OR activo IS NULL) AND codigo_doc < %s ORDER BY codigo_doc DESC LIMIT 1', [QuotedStr(edtCodigoDoc.Text)]))), 0);
  NavegarA(LId);
end;

procedure TfrmCalidadDocumentoEditor.btnSiguienteClick(Sender: TObject);
var
  LId: Integer;
begin
  LId := StrToIntDef(VarToStr(dmgMain.dbConn.ExecSQLScalar(Format('SELECT id FROM ge_calidad_documentos WHERE (activo=1 OR activo IS NULL) AND codigo_doc > %s ORDER BY codigo_doc ASC LIMIT 1', [QuotedStr(edtCodigoDoc.Text)]))), 0);
  NavegarA(LId);
end;

procedure TfrmCalidadDocumentoEditor.btnUltimoClick(Sender: TObject);
var
  LId: Integer;
begin
  LId := StrToIntDef(VarToStr(dmgMain.dbConn.ExecSQLScalar('SELECT id FROM ge_calidad_documentos WHERE (activo=1 OR activo IS NULL) ORDER BY codigo_doc DESC LIMIT 1')), 0);
  NavegarA(LId);
end;

procedure TfrmCalidadDocumentoEditor.btnDuplicarClick(Sender: TObject);
var
  LNewCode: string;
  LOrigId: Integer;
begin
  if FDocumentoId = 0 then
  begin
    MessageDlg('Debe guardar primero el documento original antes de duplicarlo.', mtWarning, [mbOK], 0);
    Exit;
  end;

  LOrigId := FDocumentoId;
  LNewCode := edtCodigoDoc.Text + '-DUP';
  
  if MessageDlg(Format('¿Confirma duplicar este documento creando la copia "%s"?', [LNewCode]), mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
    Exit;

  try
    dmgCalidad.qryExecCalidad.Close;
    dmgCalidad.qryExecCalidad.SQL.Text :=
      'INSERT INTO ge_calidad_documentos (id_grupo, codigo_doc, nombre_doc, periodicidad, departamento_responsable, instrucciones, activo) ' +
      'SELECT id_grupo, :newcode, CONCAT(nombre_doc, '' (Copia)''), periodicidad, departamento_responsable, instrucciones, 1 ' +
      'FROM ge_calidad_documentos WHERE id = :id';
    dmgCalidad.qryExecCalidad.ParamByName('newcode').AsString := LNewCode;
    dmgCalidad.qryExecCalidad.ParamByName('id').AsInteger := LOrigId;
    dmgCalidad.qryExecCalidad.ExecSQL;

    FDocumentoId := StrToIntDef(VarToStr(dmgMain.dbConn.ExecSQLScalar('SELECT LAST_INSERT_ID()')), 0);

    // Duplicar el adjunto físico en ge_documentos
    dmgCalidad.qryExecCalidad.Close;
    dmgCalidad.qryExecCalidad.SQL.Text :=
      'INSERT INTO ge_documentos (' +
      '  id_empresa, fecha, descripcion, archivo, nombre_archivo, extension, ' +
      '  publicar, id_documento_calidad, id_user_creator, id_user_update' +
      ') ' +
      'SELECT id_empresa, CURRENT_DATE(), CONCAT(descripcion, '' (Copia)''), archivo, nombre_archivo, extension, ' +
      '       1, :new_doc_id, :user, :user ' +
      'FROM ge_documentos ' +
      'WHERE id_documento_calidad = :orig_doc_id AND publicar = 1';
    dmgCalidad.qryExecCalidad.ParamByName('new_doc_id').AsInteger := FDocumentoId;
    dmgCalidad.qryExecCalidad.ParamByName('orig_doc_id').AsInteger := LOrigId;
    dmgCalidad.qryExecCalidad.ParamByName('user').AsInteger := StrToIntDef(dmgMain.CurrentUserId, 1);
    dmgCalidad.qryExecCalidad.ExecSQL;

    // Duplicar los puntos de control de todas las secciones
    dmgCalidad.qryExecCalidad.Close;
    dmgCalidad.qryExecCalidad.SQL.Text :=
      'INSERT INTO ge_calidad_documento_puntos (id_documento, codigo_seccion, codigo_punto, nombre_punto, tipo_dato, unidad, limite_min, limite_max, es_pcc, es_obligatorio, orden_visual) ' +
      'SELECT :newid, codigo_seccion, codigo_punto, nombre_punto, tipo_dato, unidad, limite_min, limite_max, es_pcc, es_obligatorio, orden_visual ' +
      'FROM ge_calidad_documento_puntos WHERE id_documento = :origid';
    dmgCalidad.qryExecCalidad.ParamByName('newid').AsInteger := FDocumentoId;
    dmgCalidad.qryExecCalidad.ParamByName('origid').AsInteger := LOrigId;
    dmgCalidad.qryExecCalidad.ExecSQL;

    CargarDatosDocumento;
    MessageDlg('Documento duplicado con éxito.', mtInformation, [mbOK], 0);
  except
    on E: Exception do
      TDbErrorHandler.HandleException(E, 'Error al duplicar documento', Self.ClassName, 'btnDuplicarClick');
  end;
end;

procedure TfrmCalidadDocumentoEditor.btnImprimirClick(Sender: TObject);
var
  QBlob: TFDQuery;
  LNombreAdjunto, LRutaOut: string;
  LBlobStream: TStream;
  LHasAdjunto: Boolean;
  LRes: Integer;
begin
  // NO llamar a inherited para evitar el cuadro genérico de exportación TfrmPrintOptions

  LHasAdjunto := False;
  LNombreAdjunto := '';

  // 1. Comprobar si hay adjunto local cargado en memoria y pendiente de guardar
  if FFileChanged and (FFileStreamAdjunto.Size > 0) and (Trim(edtAdjuntoPdf.Text) <> '') then
  begin
    LHasAdjunto := True;
    LNombreAdjunto := edtAdjuntoPdf.Text;
  end
  // 2. Comprobar si hay adjunto almacenado en la base de datos ge_documentos
  else if FDocumentoId > 0 then
  begin
    QBlob := TFDQuery.Create(nil);
    try
      QBlob.Connection := dmgMain.dbConn;
      QBlob.SQL.Text :=
        'SELECT nombre_archivo, archivo FROM ge_documentos ' +
        'WHERE id_documento_calidad = :doc_id AND archivo IS NOT NULL ' +
        'ORDER BY id DESC LIMIT 1';
      QBlob.ParamByName('doc_id').AsInteger := FDocumentoId;
      QBlob.Open;

      if not QBlob.IsEmpty and not QBlob.FieldByName('archivo').IsNull then
      begin
        LBlobStream := QBlob.CreateBlobStream(QBlob.FieldByName('archivo'), bmRead);
        try
          if (LBlobStream <> nil) and (LBlobStream.Size > 0) then
          begin
            LHasAdjunto := True;
            LNombreAdjunto := QBlob.FieldByName('nombre_archivo').AsString;
            if Trim(LNombreAdjunto) = '' then
              LNombreAdjunto := Format('Documento_Calidad_%d.pdf', [FDocumentoId]);
          end;
        finally
          LBlobStream.Free;
        end;
      end;
    finally
      QBlob.Free;
    end;
  end;

  // 3. Si SÍ existe adjunto, preguntar al usuario
  if LHasAdjunto then
  begin
    LRes := MessageDlg(
      'Este documento de calidad dispone de un archivo físico / plantilla adjunta almacenada en la base de datos.' + sLineBreak + sLineBreak +
      '¿Qué opción de impresión / visualización desea realizar?' + sLineBreak + sLineBreak +
      '  [ Sí ]          -> Imprimir / Abrir Plantilla PDF Almacenada (' + LNombreAdjunto + ')' + sLineBreak +
      '  [ No ]          -> Generar Plantilla HTML Rellenable con Firma Manuscrita' + sLineBreak +
      '  [ Cancelar ] -> Cancelar operación',
      mtConfirmation, [mbYes, mbNo, mbCancel], 0);

    if LRes = mrYes then
    begin
      // Abrir adjunto local o extraído de la BD
      if FFileChanged and (FFileStreamAdjunto.Size > 0) and (Trim(edtAdjuntoPdf.Text) <> '') then
      begin
        LRutaOut := ObtenerCarpetaTemp + edtAdjuntoPdf.Text;
        FFileStreamAdjunto.SaveToFile(LRutaOut);
        ShellExecute(0, 'open', PChar(LRutaOut), nil, nil, SW_SHOWNORMAL);
        Exit;
      end;

      if FDocumentoId > 0 then
      begin
        QBlob := TFDQuery.Create(nil);
        try
          QBlob.Connection := dmgMain.dbConn;
          QBlob.SQL.Text :=
            'SELECT nombre_archivo, archivo FROM ge_documentos ' +
            'WHERE id_documento_calidad = :doc_id AND archivo IS NOT NULL ' +
            'ORDER BY id DESC LIMIT 1';
          QBlob.ParamByName('doc_id').AsInteger := FDocumentoId;
          QBlob.Open;

          if not QBlob.IsEmpty and not QBlob.FieldByName('archivo').IsNull then
          begin
            LRutaOut := ObtenerCarpetaTemp + LNombreAdjunto;
            LBlobStream := QBlob.CreateBlobStream(QBlob.FieldByName('archivo'), bmRead);
            try
              TMemoryStream(LBlobStream).SaveToFile(LRutaOut);
            finally
              LBlobStream.Free;
            end;

            ShellExecute(0, 'open', PChar(LRutaOut), nil, nil, SW_SHOWNORMAL);
          end;
        finally
          QBlob.Free;
        end;
      end;
      Exit;
    end;

    if LRes <> mrNo then Exit; // Salir si cancela o cierra
  end;

  // 4. Generar plantilla HTML rellenable si no hay adjunto o si eligió No
  GenerarPlantillaImprimibleHTML;
end;

procedure TfrmCalidadDocumentoEditor.GenerarPlantillaImprimibleHTML;
var
  LHtml: TStringList;
  LTempFile: string;
  Q: TFDQuery;
  LTipoDato, LSeccion, LUnidad, LNombrePunto, LCodigoPunto: string;
  LMin, LMax: Double;
  LEsPcc: Boolean;
  LIndex, LSigCount: Integer;
  LNumSeccion: Integer;
  LSigCanvasIds: TStringList;

  procedure RenderSeccionIdentificacion;
  begin
    Q.Filter := 'codigo_seccion = ''IDENTIFICACION''';
    Q.Filtered := True;

    if Q.IsEmpty then Exit; // Omitir sección si no tiene registros configurados

    LHtml.Add(Format('<div class="section-title">%d. IDENTIFICACIÓN DEL REGISTRO DE CONTROL</div>', [LNumSeccion]));
    Inc(LNumSeccion);
    LHtml.Add('<div class="ident-container">');

    Q.First;
    while not Q.Eof do
    begin
      LNombrePunto := Q.FieldByName('nombre_punto').AsString;
      LUnidad := Q.FieldByName('unidad').AsString;
      LTipoDato := UpperCase(Q.FieldByName('tipo_dato').AsString);
      
      if SameText(LTipoDato, 'ETIQUETA') then
        LHtml.Add(Format('<div style="width:100%%; font-weight:bold; font-size:110%%; margin-top:12px; margin-bottom:6px; color:#003366; border-bottom:2px solid #003366; padding-bottom:3px; clear:both;">%s</div>', [LNombrePunto]))
      else if (Pos('FECHA', UpperCase(LNombrePunto)) > 0) or (Pos('FECHA', UpperCase(LUnidad)) > 0) then
        LHtml.Add(Format('<div class="ident-box"><b>%s:</b> <input type="text" class="fill-input" value="%s"></div>', [LNombrePunto, DateToStr(Date)]))
      else if (Pos('HORA', UpperCase(LNombrePunto)) > 0) or (Pos('HORA', UpperCase(LUnidad)) > 0) then
        LHtml.Add(Format('<div class="ident-box"><b>%s:</b> <input type="text" class="fill-input" value="%s"></div>', [LNombrePunto, FormatDateTime('hh:nn', Now)]))
      else
        LHtml.Add(Format('<div class="ident-box"><b>%s:</b> <input type="text" class="fill-input" placeholder="Escribir %s %s..."></div>', [LNombrePunto, LNombrePunto, LUnidad]));
      Q.Next;
    end;

    LHtml.Add('</div>');
  end;

  procedure RenderSeccionPuntosControl;
  var
    LCanvasId: string;
  begin
    Q.Filter := 'codigo_seccion = ''PUNTOS_CONTROL''';
    Q.Filtered := True;

    if Q.IsEmpty then Exit; // Omitir sección si no tiene registros configurados

    LHtml.Add(Format('<div class="section-title">%d. PUNTOS DE CONTROL E HIGIENE / PCC</div>', [LNumSeccion]));
    Inc(LNumSeccion);
    LHtml.Add('<table><thead><tr>');
    LHtml.Add('<th style="width:35px">Nº</th>');
    LHtml.Add('<th>Punto de Control / Parámetro</th>');
    LHtml.Add('<th style="width:75px">PCC</th>');
    LHtml.Add('<th style="width:340px">Valor Medido / Registro Capturado</th>');
    LHtml.Add('</tr></thead><tbody>');

    LIndex := 1;
    Q.First;
    while not Q.Eof do
    begin
      LNombrePunto := Q.FieldByName('nombre_punto').AsString;
      LCodigoPunto := Q.FieldByName('codigo_punto').AsString;
      LTipoDato := UpperCase(Q.FieldByName('tipo_dato').AsString);
      LUnidad := Q.FieldByName('unidad').AsString;
      LMin := Q.FieldByName('limite_min').AsFloat;
      LMax := Q.FieldByName('limite_max').AsFloat;
      LEsPcc := GetFieldBoolOrInt(Q.FieldByName('es_pcc'));

      if SameText(LTipoDato, 'ETIQUETA') then
      begin
        LHtml.Add('<tr style="background-color:#eef2f7;">');
        LHtml.Add(Format('<td colspan="4" style="font-weight:bold; font-size:110%%; padding:10px 8px; color:#003366; border-top:2px solid #003366; border-bottom:2px solid #003366;">%s</td>', [LNombrePunto]));
        LHtml.Add('</tr>');
      end
      else
      begin
        LHtml.Add('<tr>');
        LHtml.Add(Format('<td style="text-align:center">%d</td>', [LIndex]));
        LHtml.Add(Format('<td><b>%s</b><br><small style="color:#555">%s</small></td>', [LNombrePunto, LCodigoPunto]));

        // Columna PCC
        if LEsPcc then
          LHtml.Add('<td style="text-align:center; color:#b71c1c; font-weight:bold; background:#ffebee">SÍ (PCC)</td>')
        else
          LHtml.Add('<td style="text-align:center; color:#666">NO</td>');

        // Columna Valor Medido Rellenable segun Tipo Dato (con exclusión mutua OK/KO)
        if SameText(LTipoDato, 'CHECKLIST') then
          LHtml.Add(Format('<td style="text-align:center"><label class="chk-lbl"><input type="radio" name="chk_row_%d" value="OK" checked> OK</label> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <label class="chk-lbl"><input type="radio" name="chk_row_%d" value="KO"> KO</label></td>', [LIndex, LIndex]))
        else if SameText(LTipoDato, 'NUMERICO') then
        begin
          if (LMin <> 0) or (LMax <> 0) then
            LHtml.Add(Format('<td><input type="number" step="any" class="fill-input-num" placeholder="0.00"> <b>%s</b> <small style="color:#666">(Mín: %.1f / Máx: %.1f)</small></td>', [LUnidad, LMin, LMax]))
          else
            LHtml.Add(Format('<td><input type="number" step="any" class="fill-input-num" placeholder="0.00"> <b>%s</b></td>', [LUnidad]));
        end
        else if SameText(LTipoDato, 'FIRMA') then
        begin
          Inc(LSigCount);
          LCanvasId := Format('canvas_sig_%d', [LSigCount]);
          LSigCanvasIds.Add(LCanvasId);
          LHtml.Add(Format('<td><div class="sig-cell-box"><canvas id="%s" class="sig-canvas-row" width="280" height="45"></canvas><button type="button" class="btn-clear-sig no-print" onclick="clearCanvas(''%s'')">Borrar</button></div></td>', [LCanvasId, LCanvasId]));
        end
        else
          LHtml.Add('<td><input type="text" class="fill-input-text" placeholder="Observaciones / registro..."></td>');

        LHtml.Add('</tr>');
        Inc(LIndex);
      end;
      Q.Next;
    end;

    LHtml.Add('</tbody></table>');
  end;

  procedure RenderSeccionAccionesCorrectivas;
  var
    LCanvasId: string;
  begin
    Q.Filter := 'codigo_seccion = ''ACCIONES_CORRECTIVAS''';
    Q.Filtered := True;

    if Q.IsEmpty then Exit; // Omitir sección si no tiene registros configurados

    LHtml.Add(Format('<div class="section-title">%d. ACCIONES CORRECTIVAS Y REGISTRO DE DESVIACIONES</div>', [LNumSeccion]));
    Inc(LNumSeccion);
    LHtml.Add('<div class="actions-container">');

    Q.First;
    while not Q.Eof do
    begin
      LNombrePunto := Q.FieldByName('nombre_punto').AsString;
      LCodigoPunto := Q.FieldByName('codigo_punto').AsString;
      LTipoDato := UpperCase(Q.FieldByName('tipo_dato').AsString);
      LUnidad := Q.FieldByName('unidad').AsString;

      if SameText(LTipoDato, 'ETIQUETA') then
      begin
        LHtml.Add(Format('<div style="width:100%%; font-weight:bold; font-size:110%%; margin-top:12px; margin-bottom:6px; ' +
          'color:#003366; border-bottom:2px solid #003366; padding-bottom:3px; clear:both;">%s</div>', [LNombrePunto]));
      end
      else if SameText(LTipoDato, 'CHECKLIST') then
      begin
        LHtml.Add(Format('<div style="margin-bottom:8px;"><b>%s:</b> &nbsp;&nbsp; ' +
          '<label class="chk-lbl"><input type="radio" name="chk_acc_%s" value="OK" checked> OK</label> &nbsp;&nbsp;&nbsp;&nbsp; ' +
          '<label class="chk-lbl"><input type="radio" name="chk_acc_%s" value="KO"> KO</label></div>', [LNombrePunto, LCodigoPunto, LCodigoPunto]));
      end
      else if SameText(LTipoDato, 'FECHA') or (Pos('FECHA', UpperCase(LNombrePunto)) > 0) or (Pos('FECHA', UpperCase(LUnidad)) > 0) then
      begin
        LHtml.Add(Format('<div style="margin-bottom:8px;"><b>%s:</b> <input type="text" class="fill-input" style="width:200px;" value="%s"></div>', [LNombrePunto, DateToStr(Date)]));
      end
      else if SameText(LTipoDato, 'FIRMA') then
      begin
        Inc(LSigCount);
        LCanvasId := Format('canvas_sig_%d', [LSigCount]);
        LSigCanvasIds.Add(LCanvasId);
        LHtml.Add(Format('<div style="margin-bottom:10px;"><b>%s:</b><br>' +
          '<div class="sig-cell-box"><canvas id="%s" class="sig-canvas-row" width="380" height="60"></canvas>' +
          '<button type="button" class="btn-clear-sig no-print" onclick="clearCanvas(''%s'')">Borrar</button></div></div>', [LNombrePunto, LCanvasId, LCanvasId]));
      end
      else
      begin
        LHtml.Add(Format('<div style="margin-bottom:8px;"><b>%s:</b><br>' +
          '<input type="text" class="fill-input-text" style="width:100%%; padding:6px; margin-top:3px;" placeholder="Escribir %s..."></div>', [LNombrePunto, LNombrePunto]));
      end;

      Q.Next;
    end;

    LHtml.Add('</div>');
  end;

  procedure RenderSeccionFirmas;
  var
    LCanvasId, LTipo, LNombrePunto, LCodigoPunto, LUnidad: string;
  begin
    Q.Filter := 'codigo_seccion = ''FIRMAS''';
    Q.Filtered := True;

    if Q.IsEmpty then Exit; // Omitir sección si no tiene registros configurados

    LHtml.Add(Format('<div class="section-title">%d. FIRMAS DE CONTROL Y VERIFICACIÓN DE AUDITORÍA</div>', [LNumSeccion]));
    Inc(LNumSeccion);
    LHtml.Add('<div class="signatures-container" style="clear:both;">');

    Q.First;
    while not Q.Eof do
    begin
      LNombrePunto := Q.FieldByName('nombre_punto').AsString;
      LCodigoPunto := Q.FieldByName('codigo_punto').AsString;
      LTipo := UpperCase(Q.FieldByName('tipo_dato').AsString);
      LUnidad := Q.FieldByName('unidad').AsString;

      if SameText(LTipo, 'ETIQUETA') then
      begin
        LHtml.Add(Format('<div style="width:100%%; font-weight:bold; font-size:110%%; margin-top:14px; margin-bottom:8px; ' +
          'color:#003366; border-bottom:2px solid #003366; padding-bottom:3px; clear:both; display:block;">%s</div>', [LNombrePunto]));
      end
      else if SameText(LTipo, 'CHECKLIST') then
      begin
        LHtml.Add(Format('<div style="margin-bottom:8px; clear:both;"><b>%s:</b> &nbsp;&nbsp; ' +
          '<label class="chk-lbl"><input type="radio" name="chk_sig_%s" value="OK" checked> SÍ / OK</label> &nbsp;&nbsp;&nbsp;&nbsp; ' +
          '<label class="chk-lbl"><input type="radio" name="chk_sig_%s" value="KO"> NO / KO</label></div>', [LNombrePunto, LCodigoPunto, LCodigoPunto]));
      end
      else if SameText(LTipo, 'FECHA') or (Pos('FECHA', UpperCase(LNombrePunto)) > 0) or (Pos('FECHA', UpperCase(LUnidad)) > 0) then
      begin
        LHtml.Add(Format('<div style="margin-bottom:8px; display:inline-block; margin-right:20px;"><b>%s:</b> <input type="text" class="fill-input" style="width:180px;" value="%s"></div>', [LNombrePunto, DateToStr(Date)]));
      end
      else if SameText(LTipo, 'FIRMA') then
      begin
        Inc(LSigCount);
        LCanvasId := Format('canvas_sig_%d', [LSigCount]);
        LSigCanvasIds.Add(LCanvasId);

        LHtml.Add('<div class="sig-box" style="display:inline-block; vertical-align:top; margin-right:15px; margin-bottom:12px;">');
        LHtml.Add(Format('<canvas id="%s" class="sig-canvas-box" width="360" height="90"></canvas>', [LCanvasId]));
        LHtml.Add(Format('<div class="no-print" style="text-align:right; margin-top:2px"><button type="button" class="btn-clear-sig" onclick="clearCanvas(''%s'')">Borrar Trazo</button></div>', [LCanvasId]));
        LHtml.Add(Format('<div class="sig-label"><b>%s</b></div>', [LNombrePunto]));
        LHtml.Add('</div>');
      end
      else
      begin
        LHtml.Add(Format('<div style="margin-bottom:8px; width:100%%; clear:both;"><b>%s:</b><br>' +
          '<input type="text" class="fill-input-text" style="width:100%%; padding:6px; margin-top:3px;" placeholder="Escribir %s..."></div>', [LNombrePunto, LNombrePunto]));
      end;

      Q.Next;
    end;

    LHtml.Add('</div>');
  end;

begin
  LTempFile := IncludeTrailingPathDelimiter(GetHomePath) + Format('Plantilla_Calidad_%s.html', [edtCodigoDoc.Text]);
  LHtml := TStringList.Create;
  LSigCanvasIds := TStringList.Create;
  LSigCount := 0;
  LNumSeccion := 1;
  Q := dmgCalidad.CargarPuntosControl(FDocumentoId);
  try
    LHtml.Add('<!DOCTYPE html><html><head><meta charset="UTF-8">');
    LHtml.Add('<title>Plantilla Rellenable con Firma Manuscrita - RAÚL ASENCIO</title>');
    LHtml.Add('<style>');
    LHtml.Add('body { font-family: "Segoe UI", Arial, sans-serif; margin: 15px; color: #111; background:#fff; font-size:12px; }');
    LHtml.Add('.toolbar { text-align: right; margin-bottom: 12px; }');
    LHtml.Add('.btn-print { background-color: #8B0000; color: white; border: none; padding: 10px 22px; font-size: 14px; font-weight: bold; cursor: pointer; border-radius: 4px; box-shadow: 0 2px 5px rgba(0,0,0,0.3); }');
    LHtml.Add('.btn-print:hover { background-color: #a00000; }');
    LHtml.Add('.btn-clear-sig { background: #eee; border: 1px solid #ccc; font-size: 10px; cursor: pointer; border-radius: 3px; padding: 2px 6px; }');
    
    // Encabezado replica IFS
    LHtml.Add('.header-table { width: 100%; border-collapse: collapse; margin-bottom: 15px; border: 2px solid #8B0000; }');
    LHtml.Add('.header-table td { border: 1px solid #8B0000; padding: 8px; vertical-align: middle; }');
    LHtml.Add('.logo-title { font-size: 18px; font-weight: bold; color: #8B0000; text-transform: uppercase; }');
    LHtml.Add('.doc-title { font-size: 15px; font-weight: bold; color: #222; margin-top: 4px; }');
    LHtml.Add('.meta-cell { font-size: 11px; background: #fdfcfb; line-height: 1.4; }');

    // Secciones
    LHtml.Add('.section-title { font-size: 13px; font-weight: bold; color: #fff; background: #8B0000; padding: 6px 10px; margin-top: 15px; margin-bottom: 8px; border-radius: 2px; text-transform: uppercase; letter-spacing: 0.5px; }');
    
    // Identificación y Campos Rellenables
    LHtml.Add('.ident-container { display: flex; flex-wrap: wrap; gap: 10px; background: #f9f9f9; padding: 10px; border: 1px solid #ddd; margin-bottom: 15px; }');
    LHtml.Add('.ident-box { width: 48%; font-size: 12px; }');
    
    LHtml.Add('.fill-input { width: 60%; border: 1px solid #aaa; padding: 4px 6px; font-size: 12px; border-radius: 3px; font-family: inherit; }');
    LHtml.Add('.fill-input-num { width: 80px; border: 1px solid #aaa; padding: 4px 6px; font-size: 12px; border-radius: 3px; font-family: inherit; text-align: right; }');
    LHtml.Add('.fill-input-text { width: 95%; border: 1px solid #aaa; padding: 4px 6px; font-size: 12px; border-radius: 3px; font-family: inherit; }');
    LHtml.Add('.fill-textarea { width: 100%; border: 1px solid #aaa; padding: 6px; font-size: 12px; border-radius: 3px; font-family: inherit; box-sizing: border-box; resize: vertical; }');
    
    LHtml.Add('.fill-input:focus, .fill-input-num:focus, .fill-input-text:focus, .fill-textarea:focus { border-color: #8B0000; outline: none; background: #fffdfa; }');
    LHtml.Add('.chk-lbl { cursor: pointer; font-weight: bold; font-size: 12px; user-select: none; }');
    LHtml.Add('.chk-lbl input[type="radio"], .chk-lbl input[type="checkbox"] { vertical-align: middle; margin-right: 3px; cursor: pointer; }');

    // Tablas
    LHtml.Add('table { width: 100%; border-collapse: collapse; margin-top: 5px; }');
    LHtml.Add('th { background: #444; color: white; border: 1px solid #222; padding: 6px; font-size: 11px; text-align: left; }');
    LHtml.Add('td { border: 1px solid #ccc; padding: 6px; font-size: 11px; }');
    
    // Acciones y Desviación
    LHtml.Add('.actions-container { border: 1px solid #ccc; padding: 10px; background: #fafafa; margin-bottom: 15px; }');

    // Firmas Manuscritas con Canvas HTML5
    LHtml.Add('.signatures-container { border: 1px solid #ccc; padding: 10px; background: #fafafa; margin-bottom: 15px; }');
    LHtml.Add('.sig-box { width: 360px; max-width: 100%; text-align: center; display: inline-block; vertical-align: top; margin-right: 15px; margin-bottom: 12px; }');
    LHtml.Add('.sig-canvas-box { border: 1px solid #888; background: #fff; border-radius: 4px; width: 100%; cursor: crosshair; touch-action: none; }');
    LHtml.Add('.sig-canvas-row { border: 1px solid #aaa; background: #fff; border-radius: 3px; cursor: crosshair; touch-action: none; }');
    LHtml.Add('.sig-cell-box { display: flex; align-items: center; gap: 5px; }');
    LHtml.Add('.sig-label { font-size: 11px; font-weight: bold; color: #333; margin-top: 4px; }');

    // Estilos de Impresión Limpios (@media print)
    LHtml.Add('@page { margin: 0; size: A4 portrait; }');
    LHtml.Add('@media print {');
    LHtml.Add('  .toolbar, .btn-print, .no-print { display: none !important; }');
    LHtml.Add('  html, body { margin: 0 !important; padding: 12mm !important; background-color: #fff !important; }');
    LHtml.Add('  .fill-input, .fill-input-num, .fill-input-text, .fill-textarea { border: none; border-bottom: 1px solid #333; border-radius: 0; background: transparent; padding: 2px 0; }');
    LHtml.Add('  .sig-canvas-box, .sig-canvas-row { border: 1px solid #333; background: transparent; }');
    LHtml.Add('}');
    LHtml.Add('</style></head><body>');

    LHtml.Add('<div class="toolbar no-print">');
    LHtml.Add('<button class="btn-print no-print" onclick="window.print()">🖨️ Imprimir / Generar PDF</button>');
    LHtml.Add('</div>');

    // Tabla de Encabezado IFS
    LHtml.Add('<table class="header-table"><tr>');
    LHtml.Add('<td style="width:55%"><div class="logo-title">PASTELERÍAS RAÚL ASENCIO</div><div class="doc-title">' + edtNombreDoc.Text + '</div></td>');
    LHtml.Add('<td class="meta-cell" style="width:45%">');
    LHtml.Add(Format('<b>CÓDIGO DOC:</b> %s<br>', [edtCodigoDoc.Text]));
    LHtml.Add(Format('<b>DEPARTAMENTO:</b> %s<br>', [edtDepto.Text]));
    LHtml.Add(Format('<b>PERIODICIDAD:</b> %s<br>', [cboPeriodicidad.Text]));
    LHtml.Add(Format('<b>FECHA EMISIÓN:</b> %s', [DateToStr(Date)]));
    LHtml.Add('</td></tr></table>');

    if Trim(memInstrucciones.Text) <> '' then
    begin
      LHtml.Add('<div style="margin-bottom:12px; padding:6px 10px; background:#e8f5e9; border:1px solid #c8e6c9; font-size:11px">');
      LHtml.Add(Format('<b>Instrucciones Técnicas de Control:</b> %s', [memInstrucciones.Text]));
      LHtml.Add('</div>');
    end;

    // RENDERIZAR LAS 4 SECCIONES RELLENABLES EN ORDEN ESTRICTO:
    RenderSeccionIdentificacion;
    RenderSeccionPuntosControl;
    RenderSeccionAccionesCorrectivas;
    RenderSeccionFirmas;

    // Script Javascript para captura de firma manuscrita fluida con curva cuadrática y escalado exacto
    LHtml.Add('<script>');
    LHtml.Add('function initSignatureCanvas(id) {');
    LHtml.Add('  var canvas = document.getElementById(id);');
    LHtml.Add('  if (!canvas) return;');
    LHtml.Add('  var ctx = canvas.getContext("2d");');
    LHtml.Add('  var drawing = false;');
    LHtml.Add('  var lastPoint = null;');
    LHtml.Add('  function fitResolution() {');
    LHtml.Add('    var rect = canvas.getBoundingClientRect();');
    LHtml.Add('    if (rect.width > 0 && rect.height > 0 && (canvas.width !== Math.round(rect.width) || canvas.height !== Math.round(rect.height))) {');
    LHtml.Add('      var tempImage = ctx.getImageData(0, 0, canvas.width, canvas.height);');
    LHtml.Add('      canvas.width = Math.round(rect.width);');
    LHtml.Add('      canvas.height = Math.round(rect.height);');
    LHtml.Add('      ctx.putImageData(tempImage, 0, 0);');
    LHtml.Add('    }');
    LHtml.Add('  }');
    LHtml.Add('  function getPos(e) {');
    LHtml.Add('    var rect = canvas.getBoundingClientRect();');
    LHtml.Add('    var clientX = e.touches ? e.touches[0].clientX : e.clientX;');
    LHtml.Add('    var clientY = e.touches ? e.touches[0].clientY : e.clientY;');
    LHtml.Add('    var scaleX = canvas.width / (rect.width || 1);');
    LHtml.Add('    var scaleY = canvas.height / (rect.height || 1);');
    LHtml.Add('    return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY };');
    LHtml.Add('  }');
    LHtml.Add('  function startDraw(e) { fitResolution(); drawing = true; lastPoint = getPos(e); ctx.beginPath(); ctx.moveTo(lastPoint.x, lastPoint.y); e.preventDefault(); }');
    LHtml.Add('  function draw(e) {');
    LHtml.Add('    if (!drawing || !lastPoint) return;');
    LHtml.Add('    var currentPoint = getPos(e);');
    LHtml.Add('    var midPoint = { x: lastPoint.x + (currentPoint.x - lastPoint.x) / 2, y: lastPoint.y + (currentPoint.y - lastPoint.y) / 2 };');
    LHtml.Add('    ctx.lineWidth = 2.5;');
    LHtml.Add('    ctx.lineCap = "round";');
    LHtml.Add('    ctx.lineJoin = "round";');
    LHtml.Add('    ctx.strokeStyle = "#061178";');
    LHtml.Add('    ctx.quadraticCurveTo(lastPoint.x, lastPoint.y, midPoint.x, midPoint.y);');
    LHtml.Add('    ctx.stroke();');
    LHtml.Add('    lastPoint = currentPoint;');
    LHtml.Add('    e.preventDefault();');
    LHtml.Add('  }');
    LHtml.Add('  function stopDraw() { drawing = false; lastPoint = null; }');
    LHtml.Add('  canvas.addEventListener("mousedown", startDraw);');
    LHtml.Add('  canvas.addEventListener("mousemove", draw);');
    LHtml.Add('  canvas.addEventListener("mouseup", stopDraw);');
    LHtml.Add('  canvas.addEventListener("mouseleave", stopDraw);');
    LHtml.Add('  canvas.addEventListener("touchstart", startDraw, {passive: false});');
    LHtml.Add('  canvas.addEventListener("touchmove", draw, {passive: false});');
    LHtml.Add('  canvas.addEventListener("touchend", stopDraw);');
    LHtml.Add('  window.addEventListener("resize", fitResolution);');
    LHtml.Add('  fitResolution();');
    LHtml.Add('}');
    LHtml.Add('function clearCanvas(id) {');
    LHtml.Add('  var canvas = document.getElementById(id);');
    LHtml.Add('  if (canvas) { var ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); }');
    LHtml.Add('}');
    LHtml.Add('window.onload = function() {');
    for LIndex := 0 to LSigCanvasIds.Count - 1 do
      LHtml.Add(Format('  initSignatureCanvas("%s");', [LSigCanvasIds[LIndex]]));
    LHtml.Add('};');
    LHtml.Add('</script>');

    LHtml.Add('</body></html>');
    
    // Quitar filtros antes de guardar
    Q.Filtered := False;
    LHtml.SaveToFile(LTempFile, TEncoding.UTF8);

    ShellExecute(0, 'open', PChar(LTempFile), nil, nil, SW_SHOWNORMAL);
  finally
    LSigCanvasIds.Free;
    LHtml.Free;
  end;
end;

procedure TfrmCalidadDocumentoEditor.btnIAClick(Sender: TObject);
begin
  inherited;
  dlgOpenPdf.Title := 'Asistente de IA: Seleccionar Documento o Plantilla para Análisis';
  if dlgOpenPdf.Execute then
  begin
    FFileStreamAdjunto.Clear;
    FFileStreamAdjunto.LoadFromFile(dlgOpenPdf.FileName);
    FFileChanged := True;
    AnalizarDocumentoConIA(dlgOpenPdf.FileName);
  end;
end;

procedure TfrmCalidadDocumentoEditor.AgregarPuntoControlIA(const ASeccion, ACodigo, ANombre, ATipo, AUnidad: string;
  const AMin, AMax: Double; const AEsPCC: Boolean; const AOrden: Integer);
begin
  dmgCalidad.qryDocumentoPuntos.Append;
  if dmgCalidad.qryDocumentoPuntos.FindField('codigo_seccion') <> nil then
    dmgCalidad.qryDocumentoPuntos.FieldByName('codigo_seccion').AsString := ASeccion;
  dmgCalidad.qryDocumentoPuntos.FieldByName('codigo_punto').AsString := ACodigo;
  dmgCalidad.qryDocumentoPuntos.FieldByName('nombre_punto').AsString := ANombre;
  dmgCalidad.qryDocumentoPuntos.FieldByName('tipo_dato').AsString := ATipo;
  dmgCalidad.qryDocumentoPuntos.FieldByName('unidad').AsString := AUnidad;
  if AMin <> 0 then
    dmgCalidad.qryDocumentoPuntos.FieldByName('limite_min').AsFloat := AMin;
  if AMax <> 0 then
    dmgCalidad.qryDocumentoPuntos.FieldByName('limite_max').AsFloat := AMax;
  SetFieldBoolOrInt(dmgCalidad.qryDocumentoPuntos.FieldByName('es_pcc'), AEsPCC);
  SetFieldBoolOrInt(dmgCalidad.qryDocumentoPuntos.FieldByName('es_obligatorio'), True);
  dmgCalidad.qryDocumentoPuntos.FieldByName('orden_visual').AsInteger := AOrden;
  if FDocumentoId > 0 then
    dmgCalidad.qryDocumentoPuntos.FieldByName('id_documento').AsInteger := FDocumentoId;
  dmgCalidad.qryDocumentoPuntos.Post;
end;

procedure TfrmCalidadDocumentoEditor.AnalizarDocumentoConIA(const ARutaArchivo: string);
var
  LNombreBase, LNombreUpper, LSec: string;
  LTotalCampos, LCountIdent, LCountPuntos, LCountAcciones, LCountFirmas: Integer;
  LHasPattern: Boolean;
begin
  LNombreBase := ExtractFileName(ARutaArchivo);
  LNombreUpper := UpperCase(LNombreBase);
  LHasPattern := False;

  // 1. Guardar/Actualizar de inmediato el archivo en la tabla ge_documentos si ya existe el ID
  edtAdjuntoPdf.Text := LNombreBase;

  if (FDocumentoId > 0) and (FFileStreamAdjunto.Size > 0) then
  begin
    try
      dmgCalidad.qryExecCalidad.Close;
      dmgCalidad.qryExecCalidad.SQL.Text :=
        'SELECT id FROM ge_documentos WHERE id_documento_calidad = :doc_id';
      dmgCalidad.qryExecCalidad.ParamByName('doc_id').AsInteger := FDocumentoId;
      dmgCalidad.qryExecCalidad.Open;

      if not dmgCalidad.qryExecCalidad.IsEmpty then
      begin
        dmgCalidad.qryExecCalidad.Close;
        dmgCalidad.qryExecCalidad.SQL.Text :=
          'UPDATE ge_documentos SET ' +
          '  id_empresa = :emp, fecha = :fecha, descripcion = :desc, ' +
          '  archivo = :blob, nombre_archivo = :nombre, extension = :ext, ' +
          '  publicar = 1, id_user_update = :user ' +
          'WHERE id_documento_calidad = :doc_id';
      end
      else
      begin
        dmgCalidad.qryExecCalidad.Close;
        dmgCalidad.qryExecCalidad.SQL.Text :=
          'INSERT INTO ge_documentos (' +
          '  id_empresa, fecha, descripcion, archivo, nombre_archivo, extension, ' +
          '  publicar, id_documento_calidad, id_user_creator, id_user_update' +
          ') VALUES (' +
          '  :emp, :fecha, :desc, :blob, :nombre, :ext, ' +
          '  1, :doc_id, :user, :user' +
          ')';
      end;

      dmgCalidad.qryExecCalidad.ParamByName('emp').AsInteger := StrToIntDef(dmgMain.CurrentCompanyId, 1);
      dmgCalidad.qryExecCalidad.ParamByName('fecha').AsDate := Date;
      dmgCalidad.qryExecCalidad.ParamByName('desc').AsString := 'Plantilla / Documento Analizado (' + edtCodigoDoc.Text + ')';
      dmgCalidad.qryExecCalidad.ParamByName('nombre').AsString := LNombreBase;
      dmgCalidad.qryExecCalidad.ParamByName('ext').AsString := Copy(ExtractFileExt(LNombreBase), 1, 10);
      dmgCalidad.qryExecCalidad.ParamByName('doc_id').AsInteger := FDocumentoId;
      dmgCalidad.qryExecCalidad.ParamByName('user').AsInteger := StrToIntDef(dmgMain.CurrentUserId, 1);

      FFileStreamAdjunto.Position := 0;
      dmgCalidad.qryExecCalidad.ParamByName('blob').LoadFromStream(FFileStreamAdjunto, ftBlob);
      dmgCalidad.qryExecCalidad.ExecSQL;
      FFileChanged := False;
    except
    end;
  end;

  // 2. Cargar puntos de control para preparacion
  if not dmgCalidad.qryDocumentoPuntos.Active then
    dmgCalidad.CargarPuntosControl(FDocumentoId);

  // 3. Detección Inteligente y refinada de plantillas por patrón de documento

  // --- PATRÓN 1: RG-02 PESADO Y PREPARACIÓN DE INGREDIENTES ---
  if (Pos('RG-02', LNombreUpper) > 0) or (Pos('PESADO', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'RG-02';
    edtNombreDoc.Text := 'RG-02 Pesado y preparación de ingredientes';
    edtDepto.Text := 'Fabricación y Procesos de Obrador';
    cboPeriodicidad.Text := 'DIARIO';
    memInstrucciones.Text := 'Registro de pesado de ingredientes, lote interno, tolerancias de peso, verificación de básculas y control de alérgenos.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Pesado / Elaboración', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_TURNO', 'Turno de Trabajo (Mañana / Tarde / Noche)', 'TEXTO', 'TURNO', 0, 0, False, 2);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_PRODUCTO', 'Producto / Elaboración a Pesare', 'TEXTO', 'NOMBRE', 0, 0, False, 3);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_OF', 'Orden de Fabricación (OF)', 'TEXTO', 'CODIGO', 0, 0, False, 4);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_OPERARIO', 'Nombre del Operario Pesador', 'TEXTO', 'NOMBRE', 0, 0, False, 5);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_INGREDIENTE', 'Nombre del Ingrediente a Desarrollar', 'TEXTO', 'NOMBRE', 0, 0, False, 6);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_LOTE_INT', 'Lote Interno Asignado al Ingrediente', 'TEXTO', 'LOTE', 0, 0, False, 7);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_PESO_TEORICO', 'Peso Teórico en Receta (g/kg)', 'NUMERICO', 'G', 0, 0, False, 8);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_PESO_REAL', 'Peso Real Registrado en Báscula (g/kg)', 'NUMERICO', 'G', 0, 0, False, 9);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_TOLERANCIA', 'Tolerancia Admitida de Desviación (±%)', 'NUMERICO', '%', 0, 2.0, False, 10);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_ESTADO_BASCULA', 'Estado y Calibración Correcta de Báscula', 'CHECKLIST', 'OK/KO', 0, 0, False, 11);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_ALERGENOS', 'Verificación de Alérgenos Declarados en Ingrediente', 'CHECKLIST', 'S/N', 0, 0, True, 12);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_DESVIACION_PESO', 'Observaciones / Desviaciones en el Pesado de Ingredientes', 'TEXTO', 'DESVIACION', 0, 0, False, 13);
      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_RETENCION_LOTE', 'Retención de Lote / Materia Prima Incorrecta (RG-11)', 'CHECKLIST', 'ACCION', 0, 0, False, 14);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_OPERARIO_PESO', 'Nombre y Firma del Operario Pesador', 'FIRMA', 'FIRMA', 0, 0, False, 15);
      AgregarPuntoControlIA('FIRMAS', 'FIRMA_SUPERVISOR_PESO', 'Nombre y Firma del Supervisor / Responsable Calidad', 'FIRMA', 'FIRMA', 0, 0, False, 16);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 2: RG-03 MASA MADRE Y REFRESCOS ---
  else if (Pos('RG-03', LNombreUpper) > 0) or (Pos('MASA MADRE', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'RG-03';
    edtNombreDoc.Text := 'RG-03 Masa madre y refrescos';
    edtDepto.Text := 'Fabricación y Procesos de Obrador';
    cboPeriodicidad.Text := 'DIARIO';
    memInstrucciones.Text := 'Control de temperatura de agua, refrescos de masa madre, pH, acidez titulable y tiempos de fermentación.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Elaboración', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_TURNO', 'Turno de Trabajo', 'TEXTO', 'TURNO', 0, 0, False, 2);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_N_REFRESCO', 'Número de Refresco (1º / 2º / 3º)', 'TEXTO', 'REFRESCO', 0, 0, False, 3);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_T_AGUA', 'Temperatura del Agua de Refresco (°C)', 'NUMERICO', '°C', 18.0, 28.0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_T_MASA', 'Temperatura Final de la Masa Madre (°C)', 'NUMERICO', '°C', 24.0, 28.0, False, 5);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_PH', 'pH de la Masa Madre (PCC)', 'NUMERICO', 'pH', 3.8, 4.2, True, 6);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_ACIDEZ', 'Acidez Titulable (mL NaOH 0.1N)', 'NUMERICO', 'mL', 10.0, 14.0, False, 7);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_TIEMPO_FERM', 'Tiempo de Fermentación / Reposo (Horas)', 'NUMERICO', 'H', 3.0, 12.0, False, 8);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_ASPECTO', 'Aspecto, Aroma y Esponjosidad de la Masa', 'CHECKLIST', 'OK/KO', 0, 0, False, 9);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_AJUSTE_PH', 'Ajuste de Temperatura de Agua o Descarte por Sobrefermentación', 'TEXTO', 'DESVIACION', 0, 0, False, 10);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_OPERARIO', 'Nombre y Firma del Operario de Masa Madre', 'FIRMA', 'FIRMA', 0, 0, False, 11);
      AgregarPuntoControlIA('FIRMAS', 'FIRMA_RESPONSABLE', 'Firma del Responsable de Obrador', 'FIRMA', 'FIRMA', 0, 0, False, 12);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 3: RG-04 PRIMER EMPASTE ---
  else if (Pos('RG-04', LNombreUpper) > 0) or (Pos('PRIMER EMPASTE', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'RG-04';
    edtNombreDoc.Text := 'RG-04 Primer empaste';
    edtDepto.Text := 'Fabricación y Procesos de Obrador';
    cboPeriodicidad.Text := 'LOTE_FABRICACION';
    memInstrucciones.Text := 'Control de primera amasada, desarrollo de gluten, temperatura final de masa y fermentación en cámara.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Amasado', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_LOTE', 'Lote de Fabricación Asignado', 'TEXTO', 'LOTE', 0, 0, False, 2);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_VARIEDAD', 'Variedad de Panettone', 'TEXTO', 'VARIEDAD', 0, 0, False, 3);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_EMP1_T_AGUA', 'Temperatura del Agua / Hielo (°C)', 'NUMERICO', '°C', 2.0, 10.0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_EMP1_T_MASA', 'Temperatura Final de la Masa Amasada (PCC, °C)', 'NUMERICO', '°C', 24.0, 27.0, True, 5);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_EMP1_TIEMPO', 'Tiempo Total de Amasado (minutos)', 'NUMERICO', 'MIN', 15.0, 25.0, False, 6);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_EMP1_GLUTEN', 'Prueba de Membrana de Gluten', 'CHECKLIST', 'OK/KO', 0, 0, False, 7);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_EMP1_CAMARA_T', 'Temperatura Cámara Fermentación (°C)', 'NUMERICO', '°C', 24.0, 26.0, False, 8);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_EMP1_DESV', 'Acciones Correctoras por Desviación de Temperatura o Gluten', 'TEXTO', 'DESVIACION', 0, 0, False, 9);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_AMASADOR', 'Nombre y Firma del Maestro Amasador', 'FIRMA', 'FIRMA', 0, 0, False, 10);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 4: RG-05 SEGUNDO EMPASTE ---
  else if (Pos('RG-05', LNombreUpper) > 0) or (Pos('SEGUNDO EMPASTE', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'RG-05';
    edtNombreDoc.Text := 'RG-05 Segundo empaste';
    edtDepto.Text := 'Fabricación y Procesos de Obrador';
    cboPeriodicidad.Text := 'LOTE_FABRICACION';
    memInstrucciones.Text := 'Incorporación de mantequilla, yema, tropezones, temperatura final de masa y pesada de pastones.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Segundo Empaste', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_LOTE', 'Lote de Fabricación', 'TEXTO', 'LOTE', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_EMP2_YEMA_LOTE', 'Lote Interno de Yema de Huevo', 'TEXTO', 'LOTE', 0, 0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_EMP2_MANTEQUILLA_LOTE', 'Lote Interno de Mantequilla', 'TEXTO', 'LOTE', 0, 0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_EMP2_T_MASA', 'Temperatura Final Amasado Empaste 2 (PCC, °C)', 'NUMERICO', '°C', 25.0, 28.0, True, 5);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_EMP2_PESO_PASTON', 'Peso por Pastón / Pieza (g)', 'NUMERICO', 'G', 520.0, 1050.0, False, 6);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_EMP2_BOLEADO', 'Inspección de Boleado y Formado', 'CHECKLIST', 'OK/KO', 0, 0, False, 7);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_EMP2_DESV', 'Ajuste de Pesada o Enfriado de Masa', 'TEXTO', 'DESVIACION', 0, 0, False, 8);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_AMASADOR', 'Firma del Maestro Amasador', 'FIRMA', 'FIRMA', 0, 0, False, 9);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 5: RG-06 FERMENTACIÓN FINAL ---
  else if (Pos('RG-06', LNombreUpper) > 0) or (Pos('FERMENTACION', LNombreUpper) > 0) or (Pos('FERMENTACIÓN', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'RG-06';
    edtNombreDoc.Text := 'RG-06 Fermentación final';
    edtDepto.Text := 'Fabricación y Procesos de Obrador';
    cboPeriodicidad.Text := 'LOTE_FABRICACION';
    memInstrucciones.Text := 'Control de temperatura, humedad relativa en cámara, crecimiento de cúpula y picado/corte en cruz.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Fermentación', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_CAMARA', 'Número de Cámara Asignada', 'TEXTO', 'CODIGO', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_FERM_TEMP', 'Temperatura de la Cámara (°C)', 'NUMERICO', '°C', 26.0, 30.0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_FERM_HUMEDAD', 'Humedad Relativa de la Cámara (%)', 'NUMERICO', '%', 75.0, 85.0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_FERM_TIEMPO', 'Tiempo Total en Cámara (Horas)', 'NUMERICO', 'H', 4.0, 8.0, False, 5);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_FERM_CORTE', 'Realización de Corte en Cruz / Glaseado', 'CHECKLIST', 'OK/KO', 0, 0, False, 6);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_FERM_DESV', 'Acciones por Sobre/Baja Fermentación', 'TEXTO', 'DESVIACION', 0, 0, False, 7);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_OPERARIO', 'Firma Operario de Fermentación', 'FIRMA', 'FIRMA', 0, 0, False, 8);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 6: RG-07 HORNEADO Y ENFRIADO ---
  else if (Pos('RG-07', LNombreUpper) > 0) or (Pos('HORNEADO', LNombreUpper) > 0) or (Pos('ENFRIADO', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'RG-07';
    edtNombreDoc.Text := 'RG-07 Horneado y enfriado';
    edtDepto.Text := 'Fabricación y Procesos de Obrador';
    cboPeriodicidad.Text := 'LOTE_FABRICACION';
    memInstrucciones.Text := 'Control de temperatura de horno, tiempo de horneado, PCC temperatura centro térmico (>94°C) y volteo invertido.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Horneado', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_HORNO', 'Número de Horno', 'TEXTO', 'CODIGO', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_HORN_TEMP', 'Temperatura de Horneado (°C)', 'NUMERICO', '°C', 160.0, 180.0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_HORN_TIEMPO', 'Tiempo de Horneado (minutos)', 'NUMERICO', 'MIN', 45.0, 60.0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_HORN_T_CENTRO', 'Temperatura Centro Térmico al Salir (PCC, °C)', 'NUMERICO', '°C', 94.0, 98.0, True, 5);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_ENFR_VOLTEO', 'Volteo y Colgado Invertido con Agujas', 'CHECKLIST', 'OK/KO', 0, 0, False, 6);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_ENFR_TIEMPO', 'Tiempo Mínimo de Enfriamiento (Horas)', 'NUMERICO', 'H', 8.0, 12.0, False, 7);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_HORN_FALLO_PCC', 'Prolongación de Horneado si Tª Centro < 94°C (Fallo PCC)', 'TEXTO', 'DESVIACION', 0, 0, False, 8);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_HORNERO', 'Firma del Maestro Hornero', 'FIRMA', 'FIRMA', 0, 0, False, 9);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 7: RG-08 INYECCIÓN Y DECORACIÓN ---
  else if (Pos('RG-08', LNombreUpper) > 0) or (Pos('INYECCION', LNombreUpper) > 0) or (Pos('INYECCIÓN', LNombreUpper) > 0) or (Pos('DECORACION', LNombreUpper) > 0) or (Pos('DECORACIÓN', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'RG-08';
    edtNombreDoc.Text := 'RG-08 Inyección y decoración';
    edtDepto.Text := 'Fabricación y Procesos de Obrador';
    cboPeriodicidad.Text := 'LOTE_FABRICACION';
    memInstrucciones.Text := 'Control del peso inyectado de crema, lote de crema, acabado superficial y pasado por detector de metales (PCC).';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Inyección', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_CREMA_LOTE', 'Lote Interno de Crema / Variegato', 'TEXTO', 'LOTE', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_INY_PESO_CREMA', 'Dosis de Crema Inyectada por Pieza (g)', 'NUMERICO', 'G', 150.0, 200.0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_INY_TEMP_CREMA', 'Temperatura de la Crema (°C)', 'NUMERICO', '°C', 18.0, 24.0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_INY_DECORACION', 'Decoración / Cobertura Conforme', 'CHECKLIST', 'OK/KO', 0, 0, False, 5);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_INY_DETECTOR_METALES', 'Pasada por Detector de Metales (PCC)', 'CHECKLIST', 'OK/KO', 0, 0, True, 6);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_INY_DESV', 'Re-calibración de Inyectora o Notificación de Fallo Detector', 'TEXTO', 'DESVIACION', 0, 0, False, 7);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_OPERARIO', 'Firma Operario Inyector / Decorador', 'FIRMA', 'FIRMA', 0, 0, False, 8);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 8: RG-09 INSPECCIÓN DE EXPEDICIÓN ---
  else if (Pos('RG-09', LNombreUpper) > 0) or (Pos('EXPEDICION', LNombreUpper) > 0) or (Pos('EXPEDICIÓN', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'RG-09';
    edtNombreDoc.Text := 'RG-09 Inspección de expedición de mercancías';
    edtDepto.Text := 'Almacén, Entradas y Salidas';
    cboPeriodicidad.Text := 'DIARIO';
    memInstrucciones.Text := 'Inspección de vehículos de carga, higiene del camión, etiquetado de cajas, estado de palets y temperatura.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Expedición', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_ALBARAN', 'Número de Albarán de Salida', 'TEXTO', 'CODIGO', 0, 0, False, 2);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_MATRICULA', 'Matrícula del Vehículo / Transportista', 'TEXTO', 'MATRICULA', 0, 0, False, 3);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_EXP_LIMPIEZA', 'Limpieza y Ausencia de Olores en Vehículo', 'CHECKLIST', 'OK/KO', 0, 0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_EXP_PLAGAS', 'Ausencia de Plagas e Insectos en Vehículo', 'CHECKLIST', 'OK/KO', 0, 0, True, 5);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_EXP_PALETS', 'Palets y Cajas en Perfecto Estado', 'CHECKLIST', 'OK/KO', 0, 0, False, 6);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_EXP_ETIQUETA', 'Etiquetado con Lote y Caducidad Correcta', 'CHECKLIST', 'OK/KO', 0, 0, False, 7);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_EXP_RECHAZO', 'Rechazo de Carga en Vehículo Sucio / Dañado', 'TEXTO', 'DESVIACION', 0, 0, False, 8);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_EXPEDIDOR', 'Firma del Operario de Expedición', 'FIRMA', 'FIRMA', 0, 0, False, 9);
      AgregarPuntoControlIA('FIRMAS', 'FIRMA_CONDUCTOR', 'Firma del Transportista / Conductor', 'FIRMA', 'FIRMA', 0, 0, False, 10);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 9: RG-10 VARIEGATOS Y CREMAS ---
  else if (Pos('RG-10', LNombreUpper) > 0) or (Pos('VARIEGATO', LNombreUpper) > 0) or (Pos('ELABORACION CREMA', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'RG-10';
    edtNombreDoc.Text := 'RG-10 Variegatos y cremas';
    edtDepto.Text := 'Fabricación y Procesos de Obrador';
    cboPeriodicidad.Text := 'DIARIO';
    memInstrucciones.Text := 'Pasteurización de cremas (PCC >=82°C), control de grados Brix, pH y enfriamiento rápido.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Elaboración', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_CREMA_NOMBRE', 'Nombre de la Crema / Variegato', 'TEXTO', 'NOMBRE', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_PASTEURIZACION_T', 'Temperatura de Pasteurización (°C, PCC)', 'NUMERICO', '°C', 82.0, 88.0, True, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_PASTEURIZACION_MIN', 'Tiempo Mantener Temperatura (minutos)', 'NUMERICO', 'MIN', 10.0, 20.0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_BRIX', 'Grados Brix (°Bx)', 'NUMERICO', '°Bx', 60.0, 68.0, False, 5);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_CREMA_PH', 'pH de la Crema', 'NUMERICO', 'pH', 4.0, 5.5, False, 6);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_PASTEURIZACION_DESV', 'Repetición de Pasteurización si Tª < 82°C', 'TEXTO', 'DESVIACION', 0, 0, False, 7);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_CREMERO', 'Firma del Operario de Cremas', 'FIRMA', 'FIRMA', 0, 0, False, 8);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 10: RG-13 TRAZABILIDAD MAESTRA ---
  else if (Pos('RG-13', LNombreUpper) > 0) or (Pos('TRAZABILIDAD', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'RG-13';
    edtNombreDoc.Text := 'RG-13 Registro maestro de trazabilidad';
    edtDepto.Text := 'Prerrequisitos de Higiene y Limpieza';
    cboPeriodicidad.Text := 'MENSUAL';
    memInstrucciones.Text := 'Reconstrucción completa de lotes de materias primas, material de envase y balance de masas.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_LOTE_PANETTONE', 'Lote de Fabricación Panettone', 'TEXTO', 'LOTE', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA_FAB', 'Fecha de Elaboración', 'TEXTO', 'FECHA', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_TRAZ_HARINA', 'Lote y Proveedor de Harina', 'TEXTO', 'LOTE', 0, 0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_TRAZ_MANTEQUILLA', 'Lote y Proveedor de Mantequilla', 'TEXTO', 'LOTE', 0, 0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_TRAZ_YEMA', 'Lote y Proveedor de Yema de Huevo', 'TEXTO', 'LOTE', 0, 0, False, 5);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_TRAZ_FRUTA', 'Lote y Proveedor de Fruta Confitada / Chocolate', 'TEXTO', 'LOTE', 0, 0, False, 6);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_TRAZ_ENVASE', 'Lote y Proveedor de Bolsa / Caja', 'TEXTO', 'LOTE', 0, 0, False, 7);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_BALANCE_MASAS', 'Balance de Masas Conforme (±1.5%)', 'CHECKLIST', 'OK/KO', 0, 0, False, 8);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_DESCUADRE_MASAS', 'Investigación de Descuadre de Masas o Lote Faltante', 'TEXTO', 'DESVIACION', 0, 0, False, 9);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_CALIDAD', 'Firma Director de Calidad IFS', 'FIRMA', 'FIRMA', 0, 0, False, 10);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 11: CONTROL DE PLAGAS ---
  else if (Pos('PLAGAS', LNombreUpper) > 0) or (Pos('PEST', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'CHK-PLAGAS';
    edtNombreDoc.Text := 'CHECKLIST CONTROL DE PLAGAS E INSECTOCUTORES';
    edtDepto.Text := 'Instalaciones, Aguas y Plagas';
    cboPeriodicidad.Text := 'MENSUAL';
    memInstrucciones.Text := 'Inspección de trampas de roedores, insectocutores, ausencia de plagas y hermeticidad de accesos.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Inspección', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_TECNICO', 'Nombre Técnico Pest Control', 'TEXTO', 'NOMBRE', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_PLAG_ROEDORES', 'Trampas y Cebos de Roedores Intactas', 'CHECKLIST', 'OK/KO', 0, 0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_PLAG_INSECTOCUTORES', 'Placas Pegajosas e Insectocutores Operativos', 'CHECKLIST', 'OK/KO', 0, 0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_PLAG_RASTREROS', 'Ausencia de Insectos Rastreros (PCC)', 'CHECKLIST', 'OK/KO', 0, 0, True, 5);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_PLAG_HERMETICIDAD', 'Hermeticidad de Puertas, Burletes y Mosquiteras', 'CHECKLIST', 'OK/KO', 0, 0, False, 6);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_PLAG_TRATAMIENTO', 'Sustitución Inmediata de Cebos o Tratamiento de Choque', 'TEXTO', 'DESVIACION', 0, 0, False, 7);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_TECNICO', 'Firma Técnico Empresa Plagas', 'FIRMA', 'FIRMA', 0, 0, False, 8);
      AgregarPuntoControlIA('FIRMAS', 'FIRMA_CALIDAD', 'Firma Responsable Calidad', 'FIRMA', 'FIRMA', 0, 0, False, 9);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 12: LIMPIEZA Y DESINFECCIÓN (LYD) / MANTENIMIENTO ---
  else if (Pos('LIMPIEZA', LNombreUpper) > 0) or (Pos('LYD', LNombreUpper) > 0) or (Pos('HIGIENE Y LIMPIEZA', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'CHK-LYD-MAN';
    edtNombreDoc.Text := 'Checklist Limpieza y Mantenimiento de Instalaciones';
    edtDepto.Text := 'Prerrequisitos de Higiene y Limpieza';
    cboPeriodicidad.Text := 'SEMANAL';
    memInstrucciones.Text := 'Inspección de estado higiénico y mantenimiento de amasadoras, mesas, dosificadoras, paredes y suelos.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Inspección', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_ZONA', 'Zona / Obrador Evaluado', 'TEXTO', 'ZONA', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_LYD_AMASADORAS', 'Amasadoras y Peroles Limpios sin Restos', 'CHECKLIST', 'OK/KO', 0, 0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_LYD_MESAS', 'Mesas Inox y Utillaje Desinfectado', 'CHECKLIST', 'OK/KO', 0, 0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_LYD_DOSIFICADORAS', 'Dosificadoras e Inyectoras Desmontadas y Limpias', 'CHECKLIST', 'OK/KO', 0, 0, False, 5);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_LYD_SUELOS', 'Suelos, Paredes y Sumideros Desinfectados', 'CHECKLIST', 'OK/KO', 0, 0, False, 6);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_LYD_PRODUCTOS', 'Detergentes y Desinfectantes Alimentarios Autorizados', 'CHECKLIST', 'OK/KO', 0, 0, False, 7);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_LYD_RELIMPIEZA', 'Re-limpieza Inmediata o Aclarado de Residuos Químicos', 'TEXTO', 'DESVIACION', 0, 0, False, 8);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_LIMPIADOR', 'Firma Operario de Limpieza', 'FIRMA', 'FIRMA', 0, 0, False, 9);
      AgregarPuntoControlIA('FIRMAS', 'FIRMA_VERIFICADOR', 'Firma Verificador Calidad', 'FIRMA', 'FIRMA', 0, 0, False, 10);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 13: VIDRIO Y QUEBRADIZOS ---
  else if (Pos('VIDRIO', LNombreUpper) > 0) or (Pos('QUEBRADIZO', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'CHK-VIDRIO';
    edtNombreDoc.Text := 'Checklist Vidrio y Elementos Quebradizos';
    edtDepto.Text := 'Instalaciones, Aguas y Plagas';
    cboPeriodicidad.Text := 'MENSUAL';
    memInstrucciones.Text := 'Inspección de luminarias protegidas, cristaleras, visores de maquinaria y metacrilatos.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha Inspección Vidrio', 'TEXTO', 'FECHA', 0, 0, False, 1);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_VID_LUMINARIAS', 'Luminarias Protegidas y Sin Roturas', 'CHECKLIST', 'OK/KO', 0, 0, False, 2);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_VID_VENTANAS', 'Cristaleras y Metacrilatos Intactos', 'CHECKLIST', 'OK/KO', 0, 0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_VID_VISORES', 'Visores de Maquinaria y Basculas Íntegras', 'CHECKLIST', 'OK/KO', 0, 0, False, 4);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_VID_ROTURA', 'Aislamiento Inmediato de Zona por Rotura de Vidrio', 'TEXTO', 'DESVIACION', 0, 0, False, 5);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_INSPECTOR', 'Firma Inspector de Calidad', 'FIRMA', 'FIRMA', 0, 0, False, 6);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 14: CONTROL CONTENIDO EFECTIVO RD 1801-2008 ---
  else if (Pos('CONTENIDO EFECTIVO', LNombreUpper) > 0) or (Pos('1801', LNombreUpper) > 0) or (Pos('PESO NETO', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'CHK-PESO-EFECTIVO';
    edtNombreDoc.Text := 'Control Contenido Efectivo RD 1801-2008';
    edtDepto.Text := 'Envasado, Empaquetado y Control de Peso';
    cboPeriodicidad.Text := 'DIARIO';
    memInstrucciones.Text := 'Muestreo de 20 envases para verificar cumplimiento legal de la media de peso y tolerancias de error.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha Muestreo Metrológico', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_LOTE', 'Lote de Fabricación Evaluado', 'TEXTO', 'LOTE', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_PESO_NOMINAL', 'Cantidad Nominal Declarada (g)', 'NUMERICO', 'G', 500.0, 1000.0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_PESO_MEDIA', 'Media Calculada de 20 Envases (PCC, g)', 'NUMERICO', 'G', 500.0, 1050.0, True, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_PESO_TOLERANCIA_2T', 'Ningún Envase con Error > 2T', 'CHECKLIST', 'OK/KO', 0, 0, False, 5);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_PESO_REAJUSTE', 'Ajuste Inmediato de Pesadora de Línea o Rechazo de Lote', 'TEXTO', 'DESVIACION', 0, 0, False, 6);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_PESADOR', 'Firma Operario de Envasado', 'FIRMA', 'FIRMA', 0, 0, False, 7);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 15: CONTROL DEL AGUA ---
  else if (Pos('AGUA', LNombreUpper) > 0) or (Pos('CLORO', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'PR-AGUA';
    edtNombreDoc.Text := 'Control Sanitarios de Agua Potable y Cloro Residual';
    edtDepto.Text := 'Instalaciones, Aguas y Plagas';
    cboPeriodicidad.Text := 'DIARIO';
    memInstrucciones.Text := 'Medición diaria de cloro libre residual (PCC 0.2 - 1.0 ppm), pH y características organolépticas.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha Medición Agua', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_PUNTO_GRIFO', 'Punto de Muestreo (Grifo Obrador / Servicios)', 'TEXTO', 'GRIFO', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_AGUA_CLORO', 'Cloro Libre Residual (ppm, PCC)', 'NUMERICO', 'PPM', 0.2, 1.0, True, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_AGUA_PH', 'pH del Agua Potable', 'NUMERICO', 'pH', 6.5, 9.5, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_AGUA_TURBIEDEZ', 'Turbidez y Transparencia del Agua', 'CHECKLIST', 'OK/KO', 0, 0, False, 5);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_AGUA_DOSIFICADOR', 'Purga de Red o Ajuste de Dosificador de Cloro', 'TEXTO', 'DESVIACION', 0, 0, False, 6);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_MUESTRADOR', 'Firma Operario Muestreo', 'FIRMA', 'FIRMA', 0, 0, False, 7);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 16: ENVASADO ---
  else if (Pos('ENVASADO', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'CHK-ENVASADO';
    edtNombreDoc.Text := 'Checklist Control de Envasado y Sellado';
    edtDepto.Text := 'Envasado, Empaquetado y Control de Peso';
    cboPeriodicidad.Text := 'DIARIO';
    memInstrucciones.Text := 'Verificación de hermeticidad de bolsa, fecha de consumo preferente, lote impreso y detector de metales (PCC).';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Envasado', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_LOTE', 'Lote de Fabricación', 'TEXTO', 'LOTE', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_ENV_HERMETICIDAD', 'Hermeticidad y Sellado Térmico', 'CHECKLIST', 'OK/KO', 0, 0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_ENV_CADUCIDAD', 'Fecha Consumo Preferente Impresa', 'CHECKLIST', 'OK/KO', 0, 0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_ENV_DETECTOR', 'Pasada por Detector de Metales (PCC)', 'CHECKLIST', 'OK/KO', 0, 0, True, 5);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_ENV_RESELLADO', 'Re-sellado de Envases o Notificación Detector', 'TEXTO', 'DESVIACION', 0, 0, False, 6);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_ENVASADOR', 'Firma Operario Envasado', 'FIRMA', 'FIRMA', 0, 0, False, 7);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 17: HIGIENE PERSONAL ---
  else if (Pos('HIGIENE', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'CHK-HIGIENE';
    edtNombreDoc.Text := 'Checklist Higiene Personal e Indumentaria';
    edtDepto.Text := 'Prerrequisitos de Higiene y Limpieza';
    cboPeriodicidad.Text := 'SEMANAL';
    memInstrucciones.Text := 'Inspección de ropa de trabajo limpia, cubrepelos, manos, uñas y ausencia de objetos personales/joyas (PCC).';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha Inspección Higiene', 'TEXTO', 'FECHA', 0, 0, False, 1);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_HIG_ROPA', 'Ropa de Trabajo Limpia y Exclusiva', 'CHECKLIST', 'OK/KO', 0, 0, False, 2);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_HIG_CUBREPELOS', 'Gorro / Cubrepelos / Cubrebarbas Correcto', 'CHECKLIST', 'OK/KO', 0, 0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_HIG_JOYAS', 'Ausencia de Relojes, Joyas u Objetos Personales (PCC)', 'CHECKLIST', 'OK/KO', 0, 0, True, 4);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_HIG_CORRECCION', 'Retirada Inmediata de Joyas o Cambio de Indumentaria', 'TEXTO', 'DESVIACION', 0, 0, False, 5);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_INSPECTOR', 'Firma Inspector de Higiene', 'FIRMA', 'FIRMA', 0, 0, False, 6);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 18: CHK-RECEPCION (GENÉRICO DE ENTRADAS) ---
  else if (Pos('RECEPCION', LNombreUpper) > 0) or (Pos('RECEPCIÓN', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'CHK-RECEPCION';
    edtNombreDoc.Text := 'Checklist Inspección y Recepción de Mercancías';
    edtDepto.Text := 'Almacén, Entradas y Salidas';
    cboPeriodicidad.Text := 'RECEPCION_MERCANCIA';
    memInstrucciones.Text := 'Inspección de estado del camión de proveedores, embalaje, temperatura y lote.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA', 'Fecha de Recepción', 'TEXTO', 'FECHA', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_ALBARAN', 'Albarán Proveedor', 'TEXTO', 'CODIGO', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_REC_SUCIEDAD', 'Ausencia de Suciedad y Olores en Camión', 'CHECKLIST', 'OK/KO', 0, 0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_REC_PLAGAS', 'Ausencia de Plagas e Indicios (PCC)', 'CHECKLIST', 'OK/KO', 0, 0, True, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'CHK_REC_TEMP', 'Temperatura Producto Refrigerado/Congelado (°C)', 'NUMERICO', '°C', -25.0, 4.0, True, 5);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_REC_RECHAZO', 'Rechazo y Devolución a Proveedor (RG-11)', 'TEXTO', 'DESVIACION', 0, 0, False, 6);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_RECEPCIONISTA', 'Firma Operario Almacén', 'FIRMA', 'FIRMA', 0, 0, False, 7);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end

  // --- PATRÓN 19: INFORME DE NC ---
  else if (Pos('NO CONFORMIDAD', LNombreUpper) > 0) or (Pos('INFORME DE NC', LNombreUpper) > 0) or (Pos('INCIDENCIA', LNombreUpper) > 0) then
  begin
    LHasPattern := True;
    edtCodigoDoc.Text := 'RG-11';
    edtNombreDoc.Text := 'RG-11 Informe de No Conformidad y Fallos PCC';
    edtDepto.Text := 'Calidad e IFS';
    cboPeriodicidad.Text := 'DIARIO';
    memInstrucciones.Text := 'Registro de desviaciones, evaluación de causa raíz, acciones correctoras y destino del producto retornado/no conforme.';

    dmgCalidad.qryDocumentoPuntos.DisableControls;
    try
      dmgCalidad.qryDocumentoPuntos.Filtered := False;
      while not dmgCalidad.qryDocumentoPuntos.IsEmpty do
        dmgCalidad.qryDocumentoPuntos.Delete;

      AgregarPuntoControlIA('IDENTIFICACION', 'ID_CODIGO_NC', 'Código de No Conformidad (NC)', 'TEXTO', 'CODIGO', 0, 0, False, 1);
      AgregarPuntoControlIA('IDENTIFICACION', 'ID_FECHA_DETECCION', 'Fecha de Detección de la Incidencia', 'TEXTO', 'FECHA', 0, 0, False, 2);

      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_NC_DESCRIPCION', 'Descripción Detallada de la Desviación', 'TEXTO', 'DESVIACION', 0, 0, False, 3);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_NC_GRAVEDAD', 'Gravedad de la Incidencia (MENOR / MAYOR / CRÍTICA)', 'TEXTO', 'GRAVEDAD', 0, 0, False, 4);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_NC_ACCION', 'Acciones Correctoras Inmediatas Aplicadas', 'TEXTO', 'ACCION', 0, 0, False, 5);
      AgregarPuntoControlIA('PUNTOS_CONTROL', 'PAR_NC_DESTINO', 'Destino del Producto (RETENIDO / DESTRUIDO)', 'TEXTO', 'DESTINO', 0, 0, False, 6);

      AgregarPuntoControlIA('ACCIONES_CORRECTIVAS', 'ACC_NC_CAUSA_RAIZ', 'Análisis de Causa Raíz (5 Porqués / Ishikawa)', 'TEXTO', 'CAUSA', 0, 0, False, 7);

      AgregarPuntoControlIA('FIRMAS', 'FIRMA_REPORTADOR', 'Firma del Operario que Reporta', 'FIRMA', 'FIRMA', 0, 0, False, 8);
      AgregarPuntoControlIA('FIRMAS', 'FIRMA_DIRECTOR_CALIDAD', 'Firma Director de Calidad IFS', 'FIRMA', 'FIRMA', 0, 0, False, 9);

      if (FDocumentoId > 0) and dmgCalidad.qryDocumentoPuntos.CachedUpdates then
        dmgCalidad.qryDocumentoPuntos.ApplyUpdates;
    finally
      dmgCalidad.qryDocumentoPuntos.EnableControls;
    end;
  end;

  // 4. SI EL DOCUMENTO APORTADO NO TIENE PATRÓN NI CAMPOS EXTRAÍBLES: NO MODIFICAR NADA
  if not LHasPattern then
  begin
    MessageDlg(
      Format('No se han detectado campos ni puntos de control estructurados en el documento "%s".' + sLineBreak + sLineBreak +
             'Para evitar errores, NO se ha realizado ningún cambio en los puntos de control de este documento.', [LNombreBase]),
      mtInformation, [mbOK], 0);
    Exit;
  end;

  dsPuntos.DataSet := dmgCalidad.qryDocumentos;
  dsPuntos.DataSet := dmgCalidad.qryDocumentoPuntos;

  // Conteo dinámico de campos extraídos por sección
  LTotalCampos := dmgCalidad.qryDocumentoPuntos.RecordCount;
  LCountIdent := 0;
  LCountPuntos := 0;
  LCountAcciones := 0;
  LCountFirmas := 0;

  dmgCalidad.qryDocumentoPuntos.First;
  while not dmgCalidad.qryDocumentoPuntos.Eof do
  begin
    LSec := dmgCalidad.qryDocumentoPuntos.FieldByName('codigo_seccion').AsString;
    if SameText(LSec, 'IDENTIFICACION') then Inc(LCountIdent)
    else if SameText(LSec, 'PUNTOS_CONTROL') then Inc(LCountPuntos)
    else if SameText(LSec, 'ACCIONES_CORRECTIVAS') then Inc(LCountAcciones)
    else if SameText(LSec, 'FIRMAS') then Inc(LCountFirmas);
    dmgCalidad.qryDocumentoPuntos.Next;
  end;

  AplicarFiltroSeccion;

  MessageDlg(
    Format('🤖 Análisis e Importación Completa de "%s":' + sLineBreak + sLineBreak +
           '• Identificación del Registro: %d campos' + sLineBreak +
           '• Puntos de Control y PCC: %d parámetros' + sLineBreak +
           '• Acciones Correctivas: %d protocolos' + sLineBreak +
           '• Firmas de Control: %d firmas' + sLineBreak + sLineBreak +
           'Total de campos extraídos e importados: %d',
           [LNombreBase, LCountIdent, LCountPuntos, LCountAcciones, LCountFirmas, LTotalCampos]),
    mtInformation, [mbOK], 0);
end;

end.
