unit frm_CalidadRegistroEditor;

interface

uses
  Winapi.Windows, Winapi.Messages, Winapi.ShellAPI, System.SysUtils, System.Variants, System.Classes, System.Math,
  System.NetEncoding, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ComCtrls,
  Vcl.ExtCtrls, Vcl.Grids, Vcl.DBGrids, Data.DB, FireDAC.Comp.Client, Vcl.Imaging.pngimage,
  FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Param, FireDAC.Stan.Error,
  FireDAC.DatS, FireDAC.Phys.Intf, FireDAC.DApt.Intf, FireDAC.Comp.DataSet,
  dmg_Calidad, dmg_Main;

type
  TfrmCalidadRegistroEditor = class(TForm)
    pnlBottom: TPanel;
    btnGuardar: TButton;
    btnCancelar: TButton;
    btnIncidencia: TButton;
    pcMain: TPageControl;
    tsHeader: TTabSheet;
    tsParams: TTabSheet;
    lblCodigoReg: TLabel;
    cboCodigoReg: TComboBox;
    lblFecha: TLabel;
    dtpFecha: TDateTimePicker;
    lblObs: TLabel;
    memObs: TMemo;
    pnlToolbarParams: TPanel;
    btnVerHtml: TButton;
    btnAdjuntarFoto: TButton;
    lblModoInfo: TLabel;
    pgcViews: TPageControl;
    tsFormDinamico: TTabSheet;
    sbDynamicForm: TScrollBox;
    tsGridParams: TTabSheet;
    dbgParams: TDBGrid;
    dsParams: TDataSource;

    procedure FormShow(Sender: TObject);
    procedure cboCodigoRegChange(Sender: TObject);
    procedure btnGuardarClick(Sender: TObject);
    procedure btnIncidenciaClick(Sender: TObject);
    procedure btnVerHtmlClick(Sender: TObject);
    procedure btnAdjuntarFotoClick(Sender: TObject);
  private
    FRegistroId: Integer;
    FDocumentoId: Integer;
    FIsDrawingFirma: Boolean;
    FLastFirmaPoint: TPoint;

    procedure FirmaMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure FirmaMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
    procedure FirmaMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure btnLimpiarFirmaClick(Sender: TObject);

    procedure CargarComboDocumentos;
    procedure CargarRegistroExistente;
    procedure CargarDetalles(const ARegistroCabId: Integer);
    procedure GenerarFormularioDinamicoVCL;
    procedure GuardarFormularioDinamicoVCL;
    procedure GenerarFormularioHTMLInteractivo;
    function ObtenerCarpetaTemp: string;
    function GetFieldIntSafe(AField: TField; const ADefault: Integer = -1): Integer;
    function ObtenerFirmaComoDataUrl(AField: TField): string;
    procedure CargarFirmaEnControlImage(AField: TField; AImage: TImage);
  public
    property RegistroId: Integer read FRegistroId write FRegistroId;
    property DocumentoId: Integer read FDocumentoId write FDocumentoId;
  end;

var
  frmCalidadRegistroEditor: TfrmCalidadRegistroEditor;

implementation

{$R *.dfm}

uses
  frm_CalidadIncidenciasEditor;

procedure TfrmCalidadRegistroEditor.FormShow(Sender: TObject);
begin
  dtpFecha.Date := Date;
  CargarComboDocumentos;
  
  if FRegistroId > 0 then
  begin
    Caption := Format('Editar Registro #%d - Inocuidad y Calidad IFS', [FRegistroId]);
    CargarRegistroExistente;
  end
  else
  begin
    Caption := 'Nuevo Registro de Calidad (Tomas Dinámicas)';
    if cboCodigoReg.Items.Count > 0 then
    begin
      cboCodigoReg.ItemIndex := 0;
      cboCodigoRegChange(nil);
    end;
  end;
end;

procedure TfrmCalidadRegistroEditor.CargarComboDocumentos;
var
  Q: TFDQuery;
begin
  cboCodigoReg.Items.Clear;
  Q := dmgCalidad.CargarDocumentos(0);
  while not Q.Eof do
  begin
    cboCodigoReg.Items.AddObject(
      Format('%s - %s', [Q.FieldByName('codigo_doc').AsString, Q.FieldByName('nombre_doc').AsString]),
      TObject(IntPtr(Q.FieldByName('id').AsInteger))
    );
    Q.Next;
  end;
end;

procedure TfrmCalidadRegistroEditor.cboCodigoRegChange(Sender: TObject);
var
  LDocId: Integer;
begin
  if (FRegistroId = 0) and (cboCodigoReg.ItemIndex >= 0) then
  begin
    LDocId := Integer(IntPtr(cboCodigoReg.Items.Objects[cboCodigoReg.ItemIndex]));
    FDocumentoId := LDocId;
    
    // Generar cabecera y N filas dinámicas de detalle basadas en la plantilla
    FRegistroId := dmgCalidad.GenerarRegistroDesdeDocumento(FDocumentoId, dtpFecha.Date);
    CargarDetalles(FRegistroId);
  end;
end;

procedure TfrmCalidadRegistroEditor.CargarDetalles(const ARegistroCabId: Integer);
begin
  dmgCalidad.qryRegistrosDet.Close;
  dmgCalidad.qryRegistrosDet.ParamByName('id_registro_cab').AsInteger := ARegistroCabId;
  dmgCalidad.qryRegistrosDet.Open;
  dsParams.DataSet := dmgCalidad.qryRegistrosDet;

  GenerarFormularioDinamicoVCL;
end;

function TfrmCalidadRegistroEditor.ObtenerCarpetaTemp: string;
var
  Buffer: array[0..MAX_PATH] of Char;
begin
  GetTempPath(MAX_PATH, Buffer);
  Result := IncludeTrailingPathDelimiter(Buffer);
end;

function TfrmCalidadRegistroEditor.GetFieldIntSafe(AField: TField; const ADefault: Integer = -1): Integer;
begin
  if (AField = nil) or AField.IsNull then
    Exit(ADefault);

  if AField is TBooleanField then
  begin
    if AField.AsBoolean then Exit(1) else Exit(0);
  end;

  try
    Exit(AField.AsInteger);
  except
    try
      if AField.AsBoolean then Exit(1) else Exit(0);
    except
      Exit(ADefault);
    end;
  end;
end;

procedure TfrmCalidadRegistroEditor.FirmaMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  Img: TImage;
begin
  if (Button = mbLeft) and (Sender is TImage) then
  begin
    FIsDrawingFirma := True;
    FLastFirmaPoint := Point(X, Y);
    Img := TImage(Sender);
    Img.HelpKeyword := 'FIRMADO';
  end;
end;

procedure TfrmCalidadRegistroEditor.FirmaMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
var
  Img: TImage;
begin
  if FIsDrawingFirma and (ssLeft in Shift) and (Sender is TImage) then
  begin
    Img := TImage(Sender);
    Img.Picture.Bitmap.Canvas.Pen.Color := clNavy;
    Img.Picture.Bitmap.Canvas.Pen.Width := 2;
    Img.Picture.Bitmap.Canvas.MoveTo(FLastFirmaPoint.X, FLastFirmaPoint.Y);
    Img.Picture.Bitmap.Canvas.LineTo(X, Y);
    FLastFirmaPoint := Point(X, Y);
    Img.HelpKeyword := 'FIRMADO';
  end;
end;

procedure TfrmCalidadRegistroEditor.FirmaMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  FIsDrawingFirma := False;
end;

procedure TfrmCalidadRegistroEditor.btnLimpiarFirmaClick(Sender: TObject);
var
  LDetId, i, j: Integer;
  PnlRow: TPanel;
  Img: TImage;
begin
  if not (Sender is TComponent) then Exit;
  LDetId := TComponent(Sender).Tag;

  for i := 0 to sbDynamicForm.ControlCount - 1 do
  begin
    if sbDynamicForm.Controls[i] is TPanel then
    begin
      PnlRow := TPanel(sbDynamicForm.Controls[i]);
      for j := 0 to PnlRow.ControlCount - 1 do
      begin
        if (PnlRow.Controls[j] is TImage) and (PnlRow.Controls[j].Tag = LDetId) then
        begin
          Img := TImage(PnlRow.Controls[j]);
          Img.Picture.Bitmap.Canvas.Brush.Color := clWhite;
          Img.Picture.Bitmap.Canvas.FillRect(Rect(0, 0, 300, 60));
          Img.Picture.Bitmap.Canvas.Pen.Color := clGray;
          Img.Picture.Bitmap.Canvas.Pen.Width := 1;
          Img.Picture.Bitmap.Canvas.MoveTo(10, 48);
          Img.Picture.Bitmap.Canvas.LineTo(290, 48);
          Img.HelpKeyword := '';
          Break;
        end;
      end;
    end;
  end;
end;

procedure TfrmCalidadRegistroEditor.GenerarFormularioDinamicoVCL;
var
  Q: TFDQuery;
  LTopPos, LRowHeight: Integer;
  LCurSeccion: string;
  PnlSec: TPanel;
  PnlRow: TPanel;
  LblSec: TLabel;
  LblName: TLabel;
  LblPcc: TLabel;
  LblLim: TLabel;
  EdtValNum: TEdit;
  EdtValTxt: TEdit;
  DtpValDate: TDateTimePicker;
  CboChk: TComboBox;
  ImgFirma: TImage;
  BtnClearSig: TButton;
  LDetId: Integer;
  LTipoDato, LNombrePunto, LUnidad, LValFirma, LBase64Data: string;
  LEsPcc: Boolean;
  LMin, LMax: Double;
  LDateVal: TDateTime;
  MS: TMemoryStream;
  Bytes: TBytes;
begin
  if FRegistroId <= 0 then Exit;

  sbDynamicForm.DisableAutoRange;
  try
    while sbDynamicForm.ControlCount > 0 do
      sbDynamicForm.Controls[0].Free;

    Q := TFDQuery.Create(nil);
    try
      Q.Connection := dmgMain.dbConn;
      Q.SQL.Text :=
        'SELECT d.id AS id_det, d.id_punto_control, d.valor_num, d.valor_texto, d.valor_firma, d.conforme, d.es_pcc, d.observaciones, ' +
        '       p.codigo_seccion, p.codigo_punto, p.nombre_punto, p.tipo_dato, p.unidad, p.limite_min, p.limite_max ' +
        'FROM ge_calidad_registros_det d ' +
        'JOIN ge_calidad_documento_puntos p ON d.id_punto_control = p.id ' +
        'WHERE d.id_registro_cab = :cab_id ' +
        'ORDER BY FIELD(p.codigo_seccion, ''IDENTIFICACION'', ''PUNTOS_CONTROL'', ''ACCIONES_CORRECTIVAS'', ''FIRMAS''), p.orden_visual, d.id';
      Q.ParamByName('cab_id').AsInteger := FRegistroId;
      Q.Open;

      LTopPos := 10;
      LCurSeccion := '';

      while not Q.Eof do
      begin
        if not SameText(LCurSeccion, Q.FieldByName('codigo_seccion').AsString) then
        begin
          LCurSeccion := Q.FieldByName('codigo_seccion').AsString;

          PnlSec := TPanel.Create(sbDynamicForm);
          PnlSec.Parent := sbDynamicForm;
          PnlSec.SetBounds(10, LTopPos, sbDynamicForm.Width - 40, 32);
          PnlSec.Anchors := [akLeft, akTop, akRight];
          PnlSec.BevelOuter := bvNone;
          PnlSec.Color := $006A3B00;
          PnlSec.ParentBackground := False;

          LblSec := TLabel.Create(PnlSec);
          LblSec.Parent := PnlSec;
          LblSec.Left := 12;
          LblSec.Top := 7;
          LblSec.Font.Color := clWhite;
          LblSec.Font.Style := [fsBold];
          LblSec.Font.Size := 10;

          if SameText(LCurSeccion, 'IDENTIFICACION') then
            LblSec.Caption := '1. IDENTIFICACIÓN Y TOMAS GENERALES'
          else if SameText(LCurSeccion, 'PUNTOS_CONTROL') then
            LblSec.Caption := '2. PUNTOS DE CONTROL / PCC CRÍTICOS'
          else if SameText(LCurSeccion, 'ACCIONES_CORRECTIVAS') then
            LblSec.Caption := '3. ACCIONES CORRECTIVAS Y REGISTRO DE DESVIACIONES'
          else if SameText(LCurSeccion, 'FIRMAS') then
            LblSec.Caption := '4. FIRMAS DE AUDITORÍA Y VERIFICACIÓN DE CALIDAD'
          else
            LblSec.Caption := UpperCase(LCurSeccion);

          Inc(LTopPos, 38);
        end;

        LDetId := Q.FieldByName('id_det').AsInteger;
        LNombrePunto := Q.FieldByName('nombre_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 := Q.FieldByName('es_pcc').AsBoolean;

        LRowHeight := 44;
        if SameText(LTipoDato, 'FIRMA') then
          LRowHeight := 75
        else if SameText(LTipoDato, 'ETIQUETA') then
          LRowHeight := 36;

        PnlRow := TPanel.Create(sbDynamicForm);
        PnlRow.Parent := sbDynamicForm;
        PnlRow.SetBounds(10, LTopPos, sbDynamicForm.Width - 40, LRowHeight);
        PnlRow.Anchors := [akLeft, akTop, akRight];

        if SameText(LTipoDato, 'ETIQUETA') then
        begin
          PnlRow.BevelOuter := bvNone;
          PnlRow.Color := $00F0E6D8;
          PnlRow.ParentBackground := False;

          LblName := TLabel.Create(PnlRow);
          LblName.Parent := PnlRow;
          LblName.Left := 12;
          LblName.Top := 9;
          LblName.Caption := LNombrePunto;
          LblName.Font.Style := [fsBold];
          LblName.Font.Size := 10;
          LblName.Font.Color := $006A3B00;
        end
        else
        begin
          PnlRow.BevelOuter := bvLowered;
          PnlRow.Color := clWhite;
          PnlRow.ParentBackground := False;

          LblName := TLabel.Create(PnlRow);
          LblName.Parent := PnlRow;
          LblName.Left := 12;
          LblName.Top := 12;
          LblName.Caption := LNombrePunto;
          LblName.Font.Style := [fsBold];
          LblName.Font.Size := 9;
        end;

        if LEsPcc then
        begin
          LblPcc := TLabel.Create(PnlRow);
          LblPcc.Parent := PnlRow;
          LblPcc.Left := LblName.Left + LblName.Width + 8;
          LblPcc.Top := 12;
          LblPcc.Caption := '🔴 [PCC CRÍTICO]';
          LblPcc.Font.Color := $000000B7;
          LblPcc.Font.Style := [fsBold];
          PnlRow.Color := $00F0E6FF;
        end;

        if SameText(LTipoDato, 'CHECKLIST') then
        begin
          CboChk := TComboBox.Create(PnlRow);
          CboChk.Parent := PnlRow;
          CboChk.SetBounds(PnlRow.Width - 210, 8, 195, 26);
          CboChk.Anchors := [akTop, akRight];
          CboChk.Style := csDropDownList;
          CboChk.Items.Add('(Pendiente de Evaluación)');
          CboChk.Items.Add('🟢 OK - CONFORME');
          CboChk.Items.Add('🔴 KO - NO CONFORME');
          CboChk.Tag := LDetId;
          CboChk.Name := Format('CboChk_%d', [LDetId]);

          if Q.FieldByName('conforme').IsNull then
            CboChk.ItemIndex := 0
          else if GetFieldIntSafe(Q.FieldByName('conforme')) = 1 then
            CboChk.ItemIndex := 1
          else
            CboChk.ItemIndex := 2;
        end
        else if SameText(LTipoDato, 'NUMERICO') then
        begin
          if (LMin <> 0) or (LMax <> 0) then
          begin
            LblLim := TLabel.Create(PnlRow);
            LblLim.Parent := PnlRow;
            LblLim.SetBounds(PnlRow.Width - 360, 14, 140, 16);
            LblLim.Anchors := [akTop, akRight];
            LblLim.Caption := Format('(Mín: %.1f / Máx: %.1f %s)', [LMin, LMax, LUnidad]);
            LblLim.Font.Color := clGray;
            LblLim.Font.Size := 8;
          end;

          EdtValNum := TEdit.Create(PnlRow);
          EdtValNum.Parent := PnlRow;
          EdtValNum.SetBounds(PnlRow.Width - 210, 8, 195, 26);
          EdtValNum.Anchors := [akTop, akRight];
          EdtValNum.Tag := LDetId;
          EdtValNum.Name := Format('EdtNum_%d', [LDetId]);

          if not Q.FieldByName('valor_num').IsNull then
            EdtValNum.Text := FloatToStr(Q.FieldByName('valor_num').AsFloat)
          else
            EdtValNum.Text := '';
        end
        else if SameText(LTipoDato, 'FECHA') or SameText(LTipoDato, 'DATE') or SameText(LTipoDato, 'DATETIME') or
                (Pos('FECHA', UpperCase(LTipoDato)) > 0) or (Pos('FECHA', UpperCase(LNombrePunto)) > 0) or
                (Pos('FEC.', UpperCase(LNombrePunto)) > 0) then
        begin
          DtpValDate := TDateTimePicker.Create(PnlRow);
          DtpValDate.Parent := PnlRow;
          DtpValDate.SetBounds(PnlRow.Width - 210, 8, 195, 26);
          DtpValDate.Anchors := [akTop, akRight];
          DtpValDate.Kind := dtkDate;
          DtpValDate.Format := 'dd/MM/yyyy';
          DtpValDate.Tag := LDetId;
          DtpValDate.Name := Format('DtpDate_%d', [LDetId]);

          if not Q.FieldByName('valor_texto').IsNull and TryStrToDate(Q.FieldByName('valor_texto').AsString, LDateVal) then
            DtpValDate.Date := LDateVal
          else
            DtpValDate.Date := Date;
        end
        else if SameText(LTipoDato, 'TEXTO') then
        begin
          EdtValTxt := TEdit.Create(PnlRow);
          EdtValTxt.Parent := PnlRow;
          EdtValTxt.SetBounds(PnlRow.Width - 260, 8, 245, 26);
          EdtValTxt.Anchors := [akTop, akRight];
          EdtValTxt.Tag := LDetId;
          EdtValTxt.Name := Format('EdtTxt_%d', [LDetId]);

          if not Q.FieldByName('valor_texto').IsNull then
            EdtValTxt.Text := Q.FieldByName('valor_texto').AsString
          else
            EdtValTxt.Text := '';
        end
        else if SameText(LTipoDato, 'FIRMA') then
        begin
          ImgFirma := TImage.Create(PnlRow);
          ImgFirma.Parent := PnlRow;
          ImgFirma.SetBounds(PnlRow.Width - 380, 7, 300, 60);
          ImgFirma.Anchors := [akTop, akRight];
          ImgFirma.Tag := LDetId;
          ImgFirma.Name := Format('ImgFirma_%d', [LDetId]);
          ImgFirma.Proportional := True;
          ImgFirma.Center := True;
          ImgFirma.OnMouseDown := FirmaMouseDown;
          ImgFirma.OnMouseMove := FirmaMouseMove;
          ImgFirma.OnMouseUp := FirmaMouseUp;

          ImgFirma.Picture.Bitmap.SetSize(300, 60);
          ImgFirma.Picture.Bitmap.Canvas.Brush.Color := clWhite;
          ImgFirma.Picture.Bitmap.Canvas.FillRect(Rect(0, 0, 300, 60));
          ImgFirma.Picture.Bitmap.Canvas.Pen.Color := clGray;
          ImgFirma.Picture.Bitmap.Canvas.Pen.Width := 1;
          ImgFirma.Picture.Bitmap.Canvas.MoveTo(10, 48);
          ImgFirma.Picture.Bitmap.Canvas.LineTo(290, 48);

          // Cargar la firma guardada de la BD (soporta BLOB binario PNG y Base64)
          CargarFirmaEnControlImage(Q.FieldByName('valor_firma'), ImgFirma);

          BtnClearSig := TButton.Create(PnlRow);
          BtnClearSig.Parent := PnlRow;
          BtnClearSig.SetBounds(PnlRow.Width - 72, 24, 65, 26);
          BtnClearSig.Anchors := [akTop, akRight];
          BtnClearSig.Caption := '🧹 Borrar';
          BtnClearSig.Tag := LDetId;
          BtnClearSig.OnClick := btnLimpiarFirmaClick;
        end;

        Inc(LTopPos, LRowHeight + 6);
        Q.Next;
      end;
    finally
      Q.Free;
    end;
  finally
    sbDynamicForm.EnableAutoRange;
  end;
end;

procedure TfrmCalidadRegistroEditor.GuardarFormularioDinamicoVCL;
var
  i, j, LDetId: Integer;
  Ctrl: TControl;
  PnlRow: TPanel;
  CboChk: TComboBox;
  EdtValNum, EdtValTxt: TEdit;
  DtpValDate: TDateTimePicker;
  ImgFirma: TImage;
  LValNum: Double;
  LStrVal, LStrFirma: string;
  MS: TMemoryStream;
  PNG: TPngImage;
  Bytes: TBytes;
begin
  if FRegistroId <= 0 then Exit;

  for i := 0 to sbDynamicForm.ControlCount - 1 do
  begin
    if sbDynamicForm.Controls[i] is TPanel then
    begin
      PnlRow := TPanel(sbDynamicForm.Controls[i]);
      for j := 0 to PnlRow.ControlCount - 1 do
      begin
        Ctrl := PnlRow.Controls[j];

        // Guardar ComboBox (CHECKLIST)
        if (Ctrl is TComboBox) and (Ctrl.Tag > 0) then
        begin
          CboChk := TComboBox(Ctrl);
          LDetId := CboChk.Tag;
          dmgCalidad.qryExecCalidad.Close;
          dmgCalidad.qryExecCalidad.SQL.Text := 'UPDATE ge_calidad_registros_det SET conforme = :conf WHERE id = :id';
          dmgCalidad.qryExecCalidad.ParamByName('id').AsInteger := LDetId;
          dmgCalidad.qryExecCalidad.ParamByName('conf').DataType := ftInteger;
          if CboChk.ItemIndex = 1 then dmgCalidad.qryExecCalidad.ParamByName('conf').AsInteger := 1
          else if CboChk.ItemIndex = 2 then dmgCalidad.qryExecCalidad.ParamByName('conf').AsInteger := 0
          else dmgCalidad.qryExecCalidad.ParamByName('conf').Clear;
          dmgCalidad.qryExecCalidad.ExecSQL;
        end
        // Guardar Edit Numérico (NUMERICO)
        else if (Ctrl is TEdit) and (Pos('EdtNum_', Ctrl.Name) = 1) and (Ctrl.Tag > 0) then
        begin
          EdtValNum := TEdit(Ctrl);
          LDetId := EdtValNum.Tag;
          LStrVal := Trim(EdtValNum.Text);
          dmgCalidad.qryExecCalidad.Close;
          dmgCalidad.qryExecCalidad.SQL.Text := 'UPDATE ge_calidad_registros_det SET valor_num = :vnum WHERE id = :id';
          dmgCalidad.qryExecCalidad.ParamByName('id').AsInteger := LDetId;
          dmgCalidad.qryExecCalidad.ParamByName('vnum').DataType := ftFloat;
          if TryStrToFloat(LStrVal, LValNum) then dmgCalidad.qryExecCalidad.ParamByName('vnum').AsFloat := LValNum
          else dmgCalidad.qryExecCalidad.ParamByName('vnum').Clear;
          dmgCalidad.qryExecCalidad.ExecSQL;
        end
        // Guardar Campo Fecha (FECHA) con TDateTimePicker
        else if (Ctrl is TDateTimePicker) and (Pos('DtpDate_', Ctrl.Name) = 1) and (Ctrl.Tag > 0) then
        begin
          DtpValDate := TDateTimePicker(Ctrl);
          LDetId := DtpValDate.Tag;
          LStrVal := DateToStr(DtpValDate.Date);
          dmgCalidad.qryExecCalidad.Close;
          dmgCalidad.qryExecCalidad.SQL.Text := 'UPDATE ge_calidad_registros_det SET valor_texto = :vtxt WHERE id = :id';
          dmgCalidad.qryExecCalidad.ParamByName('id').AsInteger := LDetId;
          dmgCalidad.qryExecCalidad.ParamByName('vtxt').DataType := ftWideString;
          dmgCalidad.qryExecCalidad.ParamByName('vtxt').AsWideString := LStrVal;
          dmgCalidad.qryExecCalidad.ExecSQL;
        end
        // Guardar Edit Texto (TEXTO)
        else if (Ctrl is TEdit) and (Pos('EdtTxt_', Ctrl.Name) = 1) and (Ctrl.Tag > 0) then
        begin
          EdtValTxt := TEdit(Ctrl);
          LDetId := EdtValTxt.Tag;
          LStrVal := Trim(EdtValTxt.Text);
          dmgCalidad.qryExecCalidad.Close;
          dmgCalidad.qryExecCalidad.SQL.Text := 'UPDATE ge_calidad_registros_det SET valor_texto = :vtxt WHERE id = :id';
          dmgCalidad.qryExecCalidad.ParamByName('id').AsInteger := LDetId;
          dmgCalidad.qryExecCalidad.ParamByName('vtxt').DataType := ftWideString;
          if LStrVal <> '' then dmgCalidad.qryExecCalidad.ParamByName('vtxt').AsWideString := LStrVal
          else dmgCalidad.qryExecCalidad.ParamByName('vtxt').Clear;
          dmgCalidad.qryExecCalidad.ExecSQL;
        end
        // Guardar Firma Manuscrita (FIRMA)
        else if (Ctrl is TImage) and (Pos('ImgFirma_', Ctrl.Name) = 1) and (Ctrl.Tag > 0) then
        begin
          ImgFirma := TImage(Ctrl);
          LDetId := ImgFirma.Tag;
          LStrFirma := '';

          if (ImgFirma.HelpKeyword = 'FIRMADO') or (ImgFirma.HelpKeyword = 'CARGADO') then
          begin
            if Assigned(ImgFirma.Picture.Graphic) and not ImgFirma.Picture.Graphic.Empty then
            begin
              MS := TMemoryStream.Create;
              try
                if ImgFirma.Picture.Graphic is TPngImage then
                begin
                  ImgFirma.Picture.Graphic.SaveToStream(MS);
                end
                else
                begin
                  PNG := TPngImage.Create;
                  try
                    PNG.Assign(ImgFirma.Picture.Graphic);
                    PNG.SaveToStream(MS);
                  finally
                    PNG.Free;
                  end;
                end;
                MS.Position := 0;
                SetLength(Bytes, MS.Size);
                if Length(Bytes) > 0 then
                begin
                  MS.ReadBuffer(Bytes[0], Length(Bytes));
                  LStrFirma := 'data:image/png;base64,' + TNetEncoding.Base64.EncodeBytesToString(Bytes);
                  LStrFirma := StringReplace(LStrFirma, #13, '', [rfReplaceAll]);
                  LStrFirma := StringReplace(LStrFirma, #10, '', [rfReplaceAll]);
                  LStrFirma := Trim(LStrFirma);
                end;
              finally
                MS.Free;
              end;
            end;
          end;

          dmgCalidad.qryExecCalidad.Close;
          dmgCalidad.qryExecCalidad.SQL.Text := 'UPDATE ge_calidad_registros_det SET valor_firma = :vfirm WHERE id = :id';
          dmgCalidad.qryExecCalidad.ParamByName('id').AsInteger := LDetId;
          dmgCalidad.qryExecCalidad.ParamByName('vfirm').DataType := ftWideMemo;
          dmgCalidad.qryExecCalidad.ParamByName('vfirm').Size := 500000;
          if LStrFirma <> '' then dmgCalidad.qryExecCalidad.ParamByName('vfirm').AsWideMemo := LStrFirma
          else dmgCalidad.qryExecCalidad.ParamByName('vfirm').Clear;
          dmgCalidad.qryExecCalidad.ExecSQL;
        end;
      end;
    end;
  end;

  // Actualizar automáticamente pcc_alcanzado en la BD para todos los puntos del registro
  dmgCalidad.qryExecCalidad.Close;
  dmgCalidad.qryExecCalidad.SQL.Text :=
    'UPDATE ge_calidad_registros_det d ' +
    'JOIN ge_calidad_documento_puntos p ON d.id_punto_control = p.id ' +
    'SET d.pcc_alcanzado = CASE ' +
    '  WHEN p.tipo_dato = ''ETIQUETA'' THEN 1 ' +
    '  WHEN d.es_pcc = 0 THEN 1 ' +
    '  WHEN p.tipo_dato = ''CHECKLIST'' THEN IF(d.conforme = 1, 1, 0) ' +
    '  WHEN p.tipo_dato = ''NUMERICO'' THEN IF(d.valor_num IS NOT NULL AND d.valor_num >= p.limite_min AND (p.limite_max = 0 OR d.valor_num <= p.limite_max), 1, 0) ' +
    '  WHEN p.tipo_dato = ''FIRMA'' THEN IF(d.valor_firma IS NOT NULL AND CHAR_LENGTH(d.valor_firma) > 0, 1, 0) ' +
    '  WHEN p.tipo_dato = ''FECHA'' THEN IF(d.valor_texto IS NOT NULL AND CHAR_LENGTH(d.valor_texto) > 0, 1, 0) ' +
    '  ELSE IF(d.valor_texto IS NOT NULL AND CHAR_LENGTH(d.valor_texto) > 0, 1, 0) ' +
    'END ' +
    'WHERE d.id_registro_cab = :cab_id';
  dmgCalidad.qryExecCalidad.ParamByName('cab_id').AsInteger := FRegistroId;
  dmgCalidad.qryExecCalidad.ExecSQL;
end;

procedure TfrmCalidadRegistroEditor.btnAdjuntarFotoClick(Sender: TObject);
var
  OpenDlg: TOpenDialog;
  FS: TFileStream;
  Q: TFDQuery;
  LFileName, LShortName, LExt: string;
begin
  if FRegistroId <= 0 then
  begin
    MessageDlg('Debe guardar el registro antes de adjuntar imágenes.', mtWarning, [mbOK], 0);
    Exit;
  end;

  OpenDlg := TOpenDialog.Create(nil);
  try
    OpenDlg.Title := 'Seleccionar Foto para el Registro de Calidad';
    OpenDlg.Filter := 'Imágenes (*.jpg;*.jpeg;*.png;*.bmp)|*.jpg;*.jpeg;*.png;*.bmp|Todos los archivos (*.*)|*.*';
    if OpenDlg.Execute then
    begin
      LFileName := OpenDlg.FileName;
      LShortName := ExtractFileName(LFileName);
      LExt := ExtractFileExt(LFileName);
      if LExt = '' then LExt := '.jpg';

      FS := TFileStream.Create(LFileName, fmOpenRead or fmShareDenyNone);
      try
        Q := TFDQuery.Create(nil);
        try
          Q.Connection := dmgMain.dbConn;
          Q.SQL.Text :=
            'INSERT INTO ge_documentos ' +
            '  (id_empresa, fecha, descripcion, archivo, nombre_archivo, extension, publicar, id_registro_calidad) ' +
            'VALUES ' +
            '  (1, CURDATE(), ''Evidencia Fotográfica Calidad'', :archivo, :nombre, :ext, 1, :reg_id)';
          Q.ParamByName('reg_id').AsInteger := FRegistroId;
          Q.ParamByName('nombre').AsString := Format('foto_registro_%d_%s_%s', [FRegistroId, FormatDateTime('yyyymmdd_hhnnss', Now), LShortName]);
          Q.ParamByName('ext').AsString := LowerCase(LExt);
          Q.ParamByName('archivo').LoadFromStream(FS, ftBlob);
          Q.ExecSQL;
        finally
          Q.Free;
        end;
      finally
        FS.Free;
      end;

      MessageDlg('¡Foto adjuntada correctamente al registro de calidad!', mtInformation, [mbOK], 0);
    end;
  finally
    OpenDlg.Free;
  end;
end;

procedure TfrmCalidadRegistroEditor.btnVerHtmlClick(Sender: TObject);
begin
  GenerarFormularioHTMLInteractivo;
end;

procedure TfrmCalidadRegistroEditor.GenerarFormularioHTMLInteractivo;
var
  LHtml: TStringList;
  LTempFile, LNombreDoc, LCodigoDoc, LDepto, LInstrucciones, LEstadoEval: string;
  LFechaStr, LFechaLimiteStr, LValFirma, LNombrePunto, LTipoDato, LUnidad, LSecCodigo, LCurrSeccion, LSecTitulo: string;
  LValStr, LSubTipoLabel, LFotoB64: string;
  Q, QFotos: TFDQuery;
  LEsPcc: Boolean;
  LConfVal: Integer;
  DFecha, DFechaLim: TDateTime;
  LValNum: Double;

  function GetSecTitle(const ASec: string): string;
  begin
    if SameText(ASec, 'IDENTIFICACION') then Result := '📑 IDENTIFICACIÓN DEL REGISTRO'
    else if SameText(ASec, 'PUNTOS_CONTROL') then Result := '📝 PUNTOS DE CONTROL'
    else if SameText(ASec, 'ACCIONES_CORRECTIVAS') then Result := '⚠️ ACCIONES CORRECTIVAS'
    else if SameText(ASec, 'FIRMAS') then Result := '✍️ FIRMAS Y VALIDACIÓN'
    else Result := '📌 ' + ASec;
  end;

  function GetSubtypeLabel(const ANombre, ATipo, AUnidad: string): string;
  var
    LUpper: string;
  begin
    LUpper := UpperCase(ANombre);
    if SameText(ATipo, 'CHECKLIST') then Result := 'OK/KO'
    else if SameText(ATipo, 'FECHA') then Result := 'FECHA'
    else if SameText(ATipo, 'FIRMA') then Result := 'FIRMA'
    else if (Pos('INSPECTOR', LUpper) > 0) or (Pos('OPERARIO', LUpper) > 0) or (Pos('VERIFICADOR', LUpper) > 0) or (Pos('NOMBRE', LUpper) > 0) then Result := 'NOMBRE'
    else if (Pos('OBSERVACION', LUpper) > 0) or (Pos('ACCION', LUpper) > 0) or (Pos('DESVIACION', LUpper) > 0) then Result := 'DESVIACION'
    else if Trim(AUnidad) <> '' then Result := AUnidad
    else Result := ATipo;
  end;

begin
  if FRegistroId <= 0 then Exit;

  GuardarFormularioDinamicoVCL;

  LNombreDoc := 'REGISTRO DE CONTROL Y CALIDAD';
  LCodigoDoc := cboCodigoReg.Text;
  LDepto := 'Obrador';
  LInstrucciones := '';
  LEstadoEval := 'PENDIENTE';
  LFechaStr := DateToStr(dtpFecha.Date);
  LFechaLimiteStr := '';

  Q := TFDQuery.Create(nil);
  try
    Q.Connection := dmgMain.dbConn;
    Q.SQL.Text :=
      'SELECT r.*, d.nombre_doc, d.codigo_doc, d.departamento_responsable, d.instrucciones, ' +
      '       a.fecha_limite_toma ' +
      'FROM ge_calidad_registros_cab r ' +
      'LEFT JOIN ge_calidad_documentos d ON r.id_documento = d.id ' +
      'LEFT JOIN ge_calidad_agenda a ON (r.id_calidad_agenda = a.id OR a.id_registro_cab = r.id) ' +
      'WHERE r.id = :id';
    Q.ParamByName('id').AsInteger := FRegistroId;
    Q.Open;
    if not Q.IsEmpty then
    begin
      if not Q.FieldByName('nombre_doc').IsNull then LNombreDoc := Q.FieldByName('nombre_doc').AsString;
      if not Q.FieldByName('codigo_doc').IsNull then LCodigoDoc := Q.FieldByName('codigo_doc').AsString;
      if not Q.FieldByName('instrucciones').IsNull then LInstrucciones := Q.FieldByName('instrucciones').AsString;
      if not Q.FieldByName('estado_evaluacion').IsNull then LEstadoEval := Q.FieldByName('estado_evaluacion').AsString;

      if not Q.FieldByName('fecha').IsNull then
        LFechaStr := FormatDateTime('dd "de" mmmm "de" yyyy', Q.FieldByName('fecha').AsDateTime);

      if (Q.FindField('fecha_limite_toma') <> nil) and not Q.FieldByName('fecha_limite_toma').IsNull then
        LFechaLimiteStr := FormatDateTime('dd/mm/yyyy, hh:nn', Q.FieldByName('fecha_limite_toma').AsDateTime);
    end;
  finally
    Q.Free;
  end;

  LTempFile := ObtenerCarpetaTemp + Format('registro_calidad_%d.html', [FRegistroId]);
  LHtml := TStringList.Create;
  try
    LHtml.Add('<!DOCTYPE html>');
    LHtml.Add('<html lang="es">');
    LHtml.Add('<head>');
    LHtml.Add('  <meta charset="UTF-8">');
    LHtml.Add('  <meta name="viewport" content="width=device-width, initial-scale=1.0">');
    LHtml.Add(Format('  <title>%s - %s</title>', [LCodigoDoc, LNombreDoc]));
    LHtml.Add('  <link rel="preconnect" href="https://fonts.googleapis.com">');
    LHtml.Add('  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>');
    LHtml.Add('  <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">');
    LHtml.Add('  <style>');
    LHtml.Add('    :root {');
    LHtml.Add('      --bg: #fff8f6;');
    LHtml.Add('      --panel: #fffdfc;');
    LHtml.Add('      --text: #2d1717;');
    LHtml.Add('      --muted: #816565;');
    LHtml.Add('      --line: rgba(129, 50, 50, 0.14);');
    LHtml.Add('      --primary: #b21f1f;');
    LHtml.Add('      --primary-strong: #8f1313;');
    LHtml.Add('      --primary-soft: #fce8e4;');
    LHtml.Add('      --c-verde: #18794e;');
    LHtml.Add('      --c-rojo: #b42318;');
    LHtml.Add('    }');
    LHtml.Add('    * { box-sizing: border-box; margin: 0; padding: 0; -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }');
    LHtml.Add('    body { font-family: "Manrope", -apple-system, BlinkMacSystemFont, sans-serif; background: #ffffff; color: var(--text); ' +
              'line-height: 1.3; padding: 12px 20px; max-width: 860px; margin: 0 auto; }');
    LHtml.Add('    .no-print-bar { display: flex; justify-content: flex-end; margin-bottom: 12px; }');
    LHtml.Add('    .btn-print { background: linear-gradient(135deg, #cf3d2f 0%, #8f1313 100%); color: #ffffff; border: none; padding: 8px 16px; ' +
              'border-radius: 10px; font-weight: 700; font-size: 13px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; box-shadow: 0 4px 10px rgba(178, 31, 31, 0.2); }');
    LHtml.Add('    .header-card { margin-bottom: 10px; }');
    LHtml.Add('    .doc-title { font-size: 1.15rem; font-weight: 800; color: #2d1717; text-transform: uppercase; letter-spacing: -0.01em; margin-bottom: 4px; }');
    LHtml.Add('    .meta-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 0.8rem; color: #64748b; margin-bottom: 10px; }');
    LHtml.Add('    .meta-code { font-weight: 800; color: #b21f1f; }');
    LHtml.Add('    .instructions-box { background: rgba(37, 99, 235, 0.06); border: 1px solid rgba(37, 99, 235, 0.15); border-radius: 8px; ' +
              'padding: 6px 12px; margin-bottom: 12px; font-size: 0.78rem; color: #1e3a8a; }');
    LHtml.Add('    .seccion { margin-bottom: 10px; page-break-inside: avoid; break-inside: avoid; }');
    LHtml.Add('    .seccion__titulo { display: flex; align-items: center; gap: 6px; padding: 4px 10px; background: #fce8e4 !important; ' +
              'border-left: 4px solid #b21f1f !important; border-radius: 0 6px 6px 0; margin-bottom: 6px; font-weight: 800; font-size: 0.78rem; ' +
              'text-transform: uppercase; color: #8f1313; letter-spacing: 0.02em; }');
    LHtml.Add('    .seccion__puntos { display: flex; flex-direction: column; gap: 4px; }');
    LHtml.Add('    .punto { display: flex; justify-content: space-between; align-items: center; padding: 5px 10px; background: #ffffff; ' +
              'border: 1px solid rgba(129, 50, 50, 0.14); border-radius: 10px; page-break-inside: avoid; break-inside: avoid; gap: 10px; }');
    LHtml.Add('    .punto--pcc { border-left: 4px solid #b42318 !important; background: linear-gradient(90deg, rgba(180,35,24,0.04), #ffffff) !important; }');
    LHtml.Add('    .punto--etiqueta { background: #fce8e4 !important; border: none; border-bottom: 1px solid rgba(129, 50, 50, 0.14); border-radius: 0; padding: 4px 10px; }');
    LHtml.Add('    .punto__label { font-size: 0.8rem; font-weight: 600; color: #2d1717; display: flex; align-items: center; gap: 6px; }');
    LHtml.Add('    .pcc-badge { font-size: 0.6rem; font-weight: 800; background: #b42318 !important; color: white !important; padding: 1px 5px; border-radius: 4px; }');
    LHtml.Add('    .punto__subtype { font-size: 0.68rem; color: #816565; margin-top: 1px; text-transform: uppercase; }');
    LHtml.Add('    .punto__etiqueta-text { font-size: 0.88rem; font-weight: 800; color: #8f1313; text-transform: uppercase; }');
    LHtml.Add('    .form-input-box { padding: 4px 10px; font-size: 0.8rem; border-radius: 8px; border: 1px solid rgba(129, 50, 50, 0.2); ' +
              'background: #ffffff; min-width: 120px; max-width: 380px; text-align: left; word-break: break-word; }');
    LHtml.Add('    .checklist-toggle { display: inline-flex; gap: 2px; border-radius: 8px; overflow: hidden; border: 1px solid rgba(129, 50, 50, 0.2); flex-shrink: 0; }');
    LHtml.Add('    .checklist-toggle__btn { padding: 4px 12px; font-size: 0.75rem; font-weight: 700; white-space: nowrap !important; border: none; ' +
              'background: #ffffff; color: #816565; display: inline-flex; align-items: center; justify-content: center; gap: 4px; min-width: 54px; }');
    LHtml.Add('    .checklist-toggle__btn--ok.active { background: #18794e !important; color: #ffffff !important; }');
    LHtml.Add('    .checklist-toggle__btn--ko.active { background: #b42318 !important; color: #ffffff !important; }');
    LHtml.Add('    .firma-container { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }');
    LHtml.Add('    .firma-img { max-height: 55px; max-width: 200px; object-fit: contain; }');
    LHtml.Add('    .badge-firma { font-size: 0.68rem; font-weight: 700; color: #18794e; background: rgba(24, 121, 78, 0.1) !important; padding: 1px 6px; border-radius: 8px; }');
    LHtml.Add('    .fotos-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 6px; page-break-inside: avoid; break-inside: avoid; }');
    LHtml.Add('    .foto-card { border: 1px solid rgba(129, 50, 50, 0.14); border-radius: 10px; overflow: hidden; background: #ffffff; padding: 4px; page-break-inside: avoid; break-inside: avoid; }');
    LHtml.Add('    .foto-card img { width: 100%; height: 180px; object-fit: cover; border-radius: 8px; }');
    LHtml.Add('    .foto-card__info { font-size: 0.68rem; color: #816565; text-align: center; margin-top: 2px; word-break: break-all; }');
    LHtml.Add('    .print-footer { margin-top: 16px; padding-top: 8px; border-top: 1px dashed rgba(129, 50, 50, 0.2); text-align: center; font-size: 0.68rem; color: #816565; line-height: 1.3; }');
    LHtml.Add('    @page { size: A4 portrait; margin: 0; }');
    LHtml.Add('    @media print {');
    LHtml.Add('      html, body { font-size: 8.5pt !important; line-height: 1.25 !important; padding: 0 10mm !important; margin: 0 !important; width: 100% !important; background: #ffffff !important; }');
    LHtml.Add('      .no-print-bar, .btn-print { display: none !important; }');
    LHtml.Add('      .seccion { margin-bottom: 6px !important; page-break-inside: avoid; break-inside: avoid; }');
    LHtml.Add('      .seccion__titulo { padding: 3px 8px !important; font-size: 0.75rem !important; margin-bottom: 3px !important; background: #fce8e4 !important; }');
    LHtml.Add('      .punto { padding: 3px 8px !important; margin-bottom: 2px !important; page-break-inside: avoid; break-inside: avoid; }');
    LHtml.Add('      .checklist-toggle__btn { padding: 2px 8px !important; font-size: 0.72rem !important; height: 22px !important; white-space: nowrap !important; }');
    LHtml.Add('    }');
    LHtml.Add('  </style>');
    LHtml.Add('</head>');
    LHtml.Add('<body>');

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

    LHtml.Add('  <div class="header-card">');
    LHtml.Add(Format('    <div class="doc-title">%s</div>', [LNombreDoc]));
    LHtml.Add('    <div class="meta-row">');
    LHtml.Add(Format('      <span class="meta-code">%s</span>', [LCodigoDoc]));
    LHtml.Add(Format('      <span>📅 Fecha: %s</span>', [LFechaStr]));
    if LFechaLimiteStr <> '' then
      LHtml.Add(Format('      <span style="color:#b21f1f; font-weight:600;">⏰ Límite toma: %s</span>', [LFechaLimiteStr]));

    LHtml.Add(Format('      <span style="color:#b21f1f; font-weight:700;">%s</span>', [LEstadoEval]));
    LHtml.Add('    </div>');
    LHtml.Add('  </div>');

    if Trim(LInstrucciones) <> '' then
    begin
      LHtml.Add('  <div class="instructions-box">');
      LHtml.Add(Format('    <strong>ℹ️ Instrucciones:</strong> %s', [LInstrucciones]));
      LHtml.Add('  </div>');
    end;

    Q := TFDQuery.Create(nil);
    try
      Q.Connection := dmgMain.dbConn;
      Q.SQL.Text :=
        'SELECT d.id AS id_det, d.valor_num, d.valor_texto, d.valor_firma, d.conforme, d.es_pcc, ' +
        '       p.nombre_punto, p.tipo_dato, p.unidad, COALESCE(p.codigo_seccion, ''PUNTOS_CONTROL'') AS codigo_seccion ' +
        'FROM ge_calidad_registros_det d ' +
        'JOIN ge_calidad_documento_puntos p ON d.id_punto_control = p.id ' +
        'WHERE d.id_registro_cab = :cab_id ' +
        'ORDER BY FIELD(p.codigo_seccion, ''IDENTIFICACION'', ''PUNTOS_CONTROL'', ''ACCIONES_CORRECTIVAS'', ''FIRMAS''), p.orden_visual, d.id';
      Q.ParamByName('cab_id').AsInteger := FRegistroId;
      Q.Open;

      LCurrSeccion := '';
      while not Q.Eof do
      begin
        LSecCodigo := UpperCase(Q.FieldByName('codigo_seccion').AsString);
        if Trim(LSecCodigo) = '' then LSecCodigo := 'PUNTOS_CONTROL';

        if LCurrSeccion <> LSecCodigo then
        begin
          if LCurrSeccion <> '' then
          begin
            LHtml.Add('    </div>');
            LHtml.Add('  </div>');
          end;

          LCurrSeccion := LSecCodigo;
          LSecTitulo := GetSecTitle(LCurrSeccion);

          LHtml.Add('  <div class="seccion">');
          LHtml.Add(Format('    <div class="seccion__titulo">%s</div>', [LSecTitulo]));
          LHtml.Add('    <div class="seccion__puntos">');
        end;

        LNombrePunto := Q.FieldByName('nombre_punto').AsString;
        LTipoDato := UpperCase(Q.FieldByName('tipo_dato').AsString);
        LUnidad := Q.FieldByName('unidad').AsString;
        LEsPcc := Q.FieldByName('es_pcc').AsBoolean;

        if SameText(LTipoDato, 'FECHA') or SameText(LTipoDato, 'DATE') or SameText(LTipoDato, 'DATETIME') or
           (Pos('FECHA', UpperCase(LNombrePunto)) > 0) or (Pos('FEC.', UpperCase(LNombrePunto)) > 0) then
          LTipoDato := 'FECHA';

        LSubTipoLabel := GetSubtypeLabel(LNombrePunto, LTipoDato, LUnidad);

        if SameText(LTipoDato, 'ETIQUETA') then
        begin
          LHtml.Add('      <div class="punto punto--etiqueta">');
          LHtml.Add(Format('        <div class="punto__etiqueta-text">%s</div>', [LNombrePunto]));
          LHtml.Add('      </div>');
        end
        else
        begin
          if LEsPcc then
            LHtml.Add('      <div class="punto punto--pcc">')
          else
            LHtml.Add('      <div class="punto">');

          LHtml.Add('        <div>');
          if LEsPcc then
            LHtml.Add(Format('          <div class="punto__label"><span class="pcc-badge">PCC</span> %s</div>', [LNombrePunto]))
          else
            LHtml.Add(Format('          <div class="punto__label">%s</div>', [LNombrePunto]));

          LHtml.Add(Format('          <div class="punto__subtype">%s</div>', [LSubTipoLabel]));
          LHtml.Add('        </div>');

          if SameText(LTipoDato, 'CHECKLIST') then
          begin
            LConfVal := GetFieldIntSafe(Q.FieldByName('conforme'), -1);
            LHtml.Add('        <div class="checklist-toggle">');
            if LConfVal = 1 then
              LHtml.Add('          <button class="checklist-toggle__btn checklist-toggle__btn--ok active">✓ OK</button>')
            else
              LHtml.Add('          <button class="checklist-toggle__btn checklist-toggle__btn--ok">✓ OK</button>');

            if LConfVal = 0 then
              LHtml.Add('          <button class="checklist-toggle__btn checklist-toggle__btn--ko active">✗ KO</button>')
            else
              LHtml.Add('          <button class="checklist-toggle__btn checklist-toggle__btn--ko">✗ KO</button>');
            LHtml.Add('        </div>');
          end
          else if SameText(LTipoDato, 'NUMERICO') then
          begin
            if not Q.FieldByName('valor_num').IsNull then
              LHtml.Add(Format('        <div class="form-input-box"><b>%.2f %s</b></div>', [Q.FieldByName('valor_num').AsFloat, LUnidad]))
            else
              LHtml.Add('        <div class="form-input-box" style="color:#816565;"><i>(Sin medir)</i></div>');
          end
          else if SameText(LTipoDato, 'FIRMA') then
          begin
            LValFirma := ObtenerFirmaComoDataUrl(Q.FieldByName('valor_firma'));
            if LValFirma <> '' then
            begin
              LHtml.Add('        <div class="firma-container">');
              LHtml.Add(Format('          <img class="firma-img" src="%s" alt="Firma"/>', [LValFirma]));
              LHtml.Add('          <span class="badge-firma">🔒 Firma Guardada</span>');
              LHtml.Add('        </div>');
            end
            else
              LHtml.Add('        <div class="form-input-box" style="color:#816565;"><i>(Sin firma)</i></div>');
          end
          else
          begin
            LValStr := Q.FieldByName('valor_texto').AsString;
            if Trim(LValStr) <> '' then
              LHtml.Add(Format('        <div class="form-input-box">%s</div>', [LValStr]))
            else
              LHtml.Add('        <div class="form-input-box" style="color:#816565;">&nbsp;</div>');
          end;
          LHtml.Add('      </div>');
        end;
        Q.Next;
      end;
      if LCurrSeccion <> '' then
      begin
        LHtml.Add('    </div>');
        LHtml.Add('  </div>');
      end;
    finally
      Q.Free;
    end;

    if Trim(memObs.Text) <> '' then
    begin
      LHtml.Add('  <div class="seccion">');
      LHtml.Add('    <div class="seccion__titulo">📝 OBSERVACIONES GENERALES</div>');
      LHtml.Add(Format('    <div class="form-input-box" style="max-width:100%%; min-height:48px;">%s</div>', [memObs.Text]));
      LHtml.Add('  </div>');
    end;

    QFotos := TFDQuery.Create(nil);
    try
      QFotos.Connection := dmgMain.dbConn;
      QFotos.SQL.Text := 'SELECT id, nombre_archivo, TO_BASE64(archivo) AS foto_b64 FROM ge_documentos WHERE id_registro_calidad = :id';
      QFotos.ParamByName('id').AsInteger := FRegistroId;
      QFotos.Open;
      if not QFotos.IsEmpty then
      begin
        LHtml.Add('  <div class="seccion">');
        LHtml.Add('    <div class="seccion__titulo">📷 EVIDENCIAS FOTOGRÁFICAS</div>');
        LHtml.Add('    <div class="fotos-grid">');
        while not QFotos.Eof do
        begin
          LFotoB64 := StringReplace(StringReplace(QFotos.FieldByName('foto_b64').AsString, #13, '', [rfReplaceAll]), #10, '', [rfReplaceAll]);
          LHtml.Add('      <div class="foto-card">');
          LHtml.Add(Format('        <img src="data:image/jpeg;base64,%s" alt="%s"/>', [LFotoB64, QFotos.FieldByName('nombre_archivo').AsString]));
          LHtml.Add(Format('        <div class="foto-card__info">%s</div>', [QFotos.FieldByName('nombre_archivo').AsString]));
          LHtml.Add('      </div>');
          QFotos.Next;
        end;
        LHtml.Add('    </div>');
        LHtml.Add('  </div>');
      end;
    finally
      QFotos.Free;
    end;

    LHtml.Add('  <div class="print-footer">');
    LHtml.Add('    <strong>GESTIGEST · Control de Calidad IFS</strong> — Documento Oficial de Registro<br>');
    LHtml.Add(Format('    Formulario: %s %s — Fecha: %s — Estado: %s<br>', [LCodigoDoc, LNombreDoc, LFechaStr, LEstadoEval]));
    LHtml.Add(Format('    Impreso el %s', [FormatDateTime('dd/mm/yyyy, hh:nn:ss', Now)]));
    LHtml.Add('  </div>');
    LHtml.Add('</body>');
    LHtml.Add('</html>');

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

procedure TfrmCalidadRegistroEditor.CargarRegistroExistente;
var
  i, LDocId: Integer;
begin
  dmgCalidad.qryExecCalidad.Close;
  dmgCalidad.qryExecCalidad.SQL.Text := 'SELECT * FROM ge_calidad_registros_cab WHERE id = :id';
  dmgCalidad.qryExecCalidad.ParamByName('id').AsInteger := FRegistroId;
  dmgCalidad.qryExecCalidad.Open;
  
  if not dmgCalidad.qryExecCalidad.IsEmpty then
  begin
    FDocumentoId := dmgCalidad.qryExecCalidad.FieldByName('id_documento').AsInteger;
    dtpFecha.Date := dmgCalidad.qryExecCalidad.FieldByName('fecha').AsDateTime;
    memObs.Text := dmgCalidad.qryExecCalidad.FieldByName('observaciones').AsString;
    
    // Deshabilitar modificación de documento y fecha en registros existentes
    cboCodigoReg.Enabled := False;
    dtpFecha.Enabled := False;
    memObs.Enabled := True;

    cboCodigoReg.ItemIndex := -1;
    for i := 0 to cboCodigoReg.Items.Count - 1 do
    begin
      LDocId := Integer(IntPtr(cboCodigoReg.Items.Objects[i]));
      if LDocId = FDocumentoId then
      begin
        cboCodigoReg.ItemIndex := i;
        Break;
      end;
    end;
    
    CargarDetalles(FRegistroId);
  end;
end;

procedure TfrmCalidadRegistroEditor.btnGuardarClick(Sender: TObject);
var
  LHasPccFallo, LIsCompleto: Boolean;
  LQryDet: TFDQuery;
  LConforme, LPccAlcanzado: Integer;
  FLDPcc, FLDConf: TField;
begin
  if FRegistroId <= 0 then
  begin
    ShowMessage('Por favor, seleccione un documento de calidad válido.');
    Exit;
  end;

  GuardarFormularioDinamicoVCL;

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

  LHasPccFallo := False;
  LQryDet := dmgCalidad.qryRegistrosDet;
  LQryDet.First;
  while not LQryDet.Eof do
  begin
    if LQryDet.FieldByName('es_pcc').AsBoolean then
    begin
      FLDConf := LQryDet.FindField('conforme');
      FLDPcc := LQryDet.FindField('pcc_alcanzado');

      LConforme := GetFieldIntSafe(FLDConf, -1);
      LPccAlcanzado := GetFieldIntSafe(FLDPcc, 1);

      if (LConforme = 0) or (LPccAlcanzado = 0) then
        LHasPccFallo := True;
    end;
    LQryDet.Next;
  end;

  LIsCompleto := dmgCalidad.VerificarRegistroCompleto(FRegistroId);

  dmgCalidad.qryExecCalidad.Close;
  dmgCalidad.qryExecCalidad.SQL.Text :=
    'UPDATE ge_calidad_registros_cab SET ' +
    '  fecha = :fecha, observaciones = :obs, ' +
    '  tiene_desviacion = :desv, ' +
    '  estado_evaluacion = :eval ' +
    'WHERE id = :id';
  dmgCalidad.qryExecCalidad.ParamByName('id').AsInteger := FRegistroId;
  dmgCalidad.qryExecCalidad.ParamByName('fecha').AsDate := dtpFecha.Date;
  dmgCalidad.qryExecCalidad.ParamByName('obs').DataType := ftWideString;
  dmgCalidad.qryExecCalidad.ParamByName('obs').AsWideString := Trim(memObs.Text);
  dmgCalidad.qryExecCalidad.ParamByName('desv').AsInteger := Ord(LHasPccFallo);

  if LHasPccFallo then
    dmgCalidad.qryExecCalidad.ParamByName('eval').AsString := 'NO_CONFORME'
  else if LIsCompleto then
    dmgCalidad.qryExecCalidad.ParamByName('eval').AsString := 'CONFORME'
  else
    dmgCalidad.qryExecCalidad.ParamByName('eval').AsString := 'CONFORME';

  dmgCalidad.qryExecCalidad.ExecSQL;

  if LHasPccFallo then
  begin
    if MessageDlg('⚠ SE HA DETECTADO UNA DESVIACIÓN EN UN PUNTO CRÍTICO DE CONTROL (PCC).' + #13#10 +
                  '¿Desea registrar inmediatamente la Hoja de No Conformidad y Acción Correctora (RG-11)?',
                  mtWarning, [mbYes, mbNo], 0) = mrYes then
    begin
      btnIncidenciaClick(nil);
    end;
  end;

  dmgCalidad.ActualizarEstadoAgendaSegunRegistro(FRegistroId);

  ShowMessage('Registro de calidad guardado correctamente.');
end;

procedure TfrmCalidadRegistroEditor.btnIncidenciaClick(Sender: TObject);
var
  LEditorNc: TfrmCalidadIncidenciasEditor;
begin
  LEditorNc := TfrmCalidadIncidenciasEditor.Create(Self);
  try
    LEditorNc.RegistroCabId := FRegistroId;
    LEditorNc.FaseProceso := cboCodigoReg.Text;
    LEditorNc.ShowModal;
  finally
    LEditorNc.Free;
  end;
end;

function TfrmCalidadRegistroEditor.ObtenerFirmaComoDataUrl(AField: TField): string;
var
  MS: TMemoryStream;
  LRawStr, LBase64: string;
  Bytes: TBytes;
  PosBase64: Integer;
begin
  Result := '';
  if (AField = nil) or AField.IsNull then Exit;

  LRawStr := Trim(AField.AsString);
  if LRawStr <> '' then
  begin
    PosBase64 := Pos('base64,', LRawStr);
    if PosBase64 > 0 then
      LBase64 := Copy(LRawStr, PosBase64 + 7, MaxInt)
    else if (Pos('iVBOR', LRawStr) > 0) or (Pos('Qk0', LRawStr) > 0) or (Pos('/9j/', LRawStr) > 0) then
      LBase64 := LRawStr
    else
      LBase64 := '';

    LBase64 := StringReplace(LBase64, #13, '', [rfReplaceAll]);
    LBase64 := StringReplace(LBase64, #10, '', [rfReplaceAll]);
    LBase64 := Trim(LBase64);

    if LBase64 <> '' then
    begin
      if Pos('Qk0', LBase64) = 1 then
        Result := 'data:image/bmp;base64,' + LBase64
      else
        Result := 'data:image/png;base64,' + LBase64;
    end;
  end;

  if Result = '' then
  begin
    MS := TMemoryStream.Create;
    try
      (AField as TBlobField).SaveToStream(MS);
      if MS.Size > 10 then
      begin
        MS.Position := 0;
        SetLength(Bytes, MS.Size);
        MS.ReadBuffer(Bytes[0], MS.Size);
        LBase64 := TNetEncoding.Base64.EncodeBytesToString(Bytes);
        LBase64 := StringReplace(LBase64, #13, '', [rfReplaceAll]);
        LBase64 := StringReplace(LBase64, #10, '', [rfReplaceAll]);
        LBase64 := Trim(LBase64);
        if LBase64 <> '' then
          Result := 'data:image/png;base64,' + LBase64;
      end;
    finally
      MS.Free;
    end;
  end;
end;

procedure TfrmCalidadRegistroEditor.CargarFirmaEnControlImage(AField: TField; AImage: TImage);
var
  MS: TMemoryStream;
  WIC: TWICImage;
  LRawStr, LBase64: string;
  Bytes: TBytes;
  PosBase64: Integer;
  IsLoaded: Boolean;
begin
  if (AField = nil) or AField.IsNull then Exit;

  IsLoaded := False;
  MS := TMemoryStream.Create;
  try
    LRawStr := Trim(AField.AsString);
    if LRawStr <> '' then
    begin
      PosBase64 := Pos('base64,', LRawStr);
      if PosBase64 > 0 then
        LBase64 := Copy(LRawStr, PosBase64 + 7, MaxInt)
      else if (Pos('iVBOR', LRawStr) > 0) or (Pos('Qk0', LRawStr) > 0) or (Pos('/9j/', LRawStr) > 0) then
        LBase64 := LRawStr
      else
        LBase64 := '';

      LBase64 := StringReplace(LBase64, #13, '', [rfReplaceAll]);
      LBase64 := StringReplace(LBase64, #10, '', [rfReplaceAll]);
      LBase64 := Trim(LBase64);

      if LBase64 <> '' then
      begin
        try
          Bytes := TNetEncoding.Base64.DecodeStringToBytes(LBase64);
          if Length(Bytes) > 10 then
          begin
            MS.WriteBuffer(Bytes[0], Length(Bytes));
            MS.Position := 0;
            IsLoaded := True;
          end;
        except
        end;
      end;
    end;

    if not IsLoaded then
    begin
      MS.Clear;
      (AField as TBlobField).SaveToStream(MS);
      if MS.Size > 10 then
      begin
        MS.Position := 0;
        IsLoaded := True;
      end;
    end;

    if IsLoaded and (MS.Size > 10) then
    begin
      WIC := TWICImage.Create;
      try
        WIC.LoadFromStream(MS);
        AImage.Picture.Bitmap.Canvas.Draw(0, 0, WIC);
        AImage.HelpKeyword := 'CARGADO';
      finally
        WIC.Free;
      end;
    end;
  except
    // Absorber si el gráfico no se pudo decodificar
  end;
  MS.Free;
end;

end.
