﻿unit dmg_Main;

interface

uses
  System.SysUtils, System.Classes, FireDAC.Stan.Intf, FireDAC.Stan.Option,
  FireDAC.Stan.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Def,
  FireDAC.Stan.Pool, FireDAC.Stan.Async, FireDAC.Phys, FireDAC.VCLUI.Wait,
  Data.DB, FireDAC.Comp.Client, FireDAC.Phys.PG, FireDAC.Phys.PGDef,
  FireDAC.Stan.Param, FireDAC.DatS, FireDAC.DApt.Intf, FireDAC.DApt,
  FireDAC.Comp.DataSet, Vcl.ImgList, Vcl.ImageCollection, Vcl.VirtualImageList,
  System.ImageList, Vcl.BaseImageCollection, FireDAC.Phys.MySQL,
  FireDAC.Phys.MySQLDef, frxClass, frxDBSet, frxExportBaseDialog, frxExportPDF;

type
  TdmgMain = class(TDataModule)
    dbConn: TFDConnection;
    qryExec: TFDQuery;
    qryLotes: TFDQuery;
    qryProductos: TFDQuery;
    imgCollection: TImageCollection;
    imgListRibbon: TVirtualImageList;
    qryLotesid: TStringField;
    qryLotesupdated_at: TSQLTimeStampField;
    qryLotesdeleted_at: TSQLTimeStampField;
    qryLotesid_user_creator: TStringField;
    qryLotesid_user_update: TStringField;
    qryLotesactivo: TBooleanField;
    qryLotescreated_at: TSQLTimeStampField;
    qryLotesfecha: TDateField;
    qryLotesid_obrador: TStringField;
    qryLotesid_articulo: TStringField;
    qryLotesobservaciones: TStringField;
    qryProductosid: TStringField;
    qryProductoscreated_at: TSQLTimeStampField;
    qryProductosupdate_at: TSQLTimeStampField;
    qryProductosdeleted_at: TSQLTimeStampField;
    qryProductosid_user_creator: TStringField;
    qryProductosid_user_update: TStringField;
    qryProductosactivo: TBooleanField;
    qryProductosid_familia: TStringField;
    qryProductosdescripcion: TStringField;
    qryProductospvp1: TBCDField;
    qryProductospvp2: TBCDField;
    qryProductospvp3: TBCDField;
    qryProductospvp4: TBCDField;
    qryProductosid_tipo_iva: TStringField;
    qryProductosingrediente: TBooleanField;
    qryAlbaranesid: TStringField;
    qryAlbaranesempresa_id: TStringField;
    qryAlbaranescliente_id: TStringField;
    qryAlbaranesserie: TStringField;
    qryAlbaranesnumero: TIntegerField;
    qryAlbaranesreferencia: TStringField;
    qryAlbaranesfecha: TDateTimeField;
    qryAlbaranesbase_imponible: TBCDField;
    qryAlbaranestotal: TBCDField;
    qryAlbaranesiva: TBCDField;
    qryAlbaranesobservaciones1: TMemoField;
    qryAlbaranesobservaciones2: TMemoField;
    qryAlbaranesupdated_at: TSQLTimeStampField;
    qryAlbaranesdeleted_at: TSQLTimeStampField;
    qryAlbaranesid_user_creator: TStringField;
    qryAlbaranesid_user_update: TStringField;
    qryAlbaranescreated_at: TSQLTimeStampField;
    qryAlbaranesid_pedido: TStringField;
    qryAlbaranesid_factura: TStringField;
    qryEmpresasid: TStringField;
    qryEmpresasnombre: TStringField;
    qryEmpresascif_nif: TStringField;
    qryEmpresasdireccion: TMemoField;
    qryEmpresasactivo: TBooleanField;
    qryEmpresascreated_at: TSQLTimeStampField;
    qryEmpresasupdated_at: TSQLTimeStampField;
    qryEmpresasdeleted_at: TSQLTimeStampField;
    frxInforme: TfrxReport;
    frxDBMaster: TfrxDBDataset;
    frxPDFExport1: TfrxPDFExport;
    frxDBDetalle: TfrxDBDataset;
    frxDBDetalle2: TfrxDBDataset;
    procedure DataModuleCreate(Sender: TObject);
  private
    FCurrentUserId: string;
    FCurrentCompanyId: string;
    FCurrentCompanyName: string;
  public
    function EncryptStr(const AText: string): string;
    function DecryptStr(const AText: string): string;
    procedure Connect;
    procedure Disconnect;
    procedure GuardarLote(ACodigo: Integer; AIdArticulo, AIdObrador: Integer; AFecha: TDateTime; const AObservaciones: string); overload;
    procedure GuardarLote(const AIDLote, AIDProducto: string; AFecha: TDateTime); overload;
    function GetNextCodigoLote: Integer;
    procedure RefreshLotes;
    procedure RefreshProductos(const AFilter: string = '');
    function GetNextIDS(const ATable: string): string;
    class function CleanUUID(const AGUID: string): string; static;
    function GetPrecioArticuloCliente(const AClienteId, AArticuloId: string; AFecha: TDateTime; ADefaultPrice: Double; ACantidad: Double = 1.0): Double;
    function GetSerieYNumeroDocumento(const AEmpresaId, ATipoDoc: string; AFecha: TDateTime; const ACurrentSerie: string; var AOutSerie: string; var AOutNumero: Integer): Boolean;
    
    function HasConfig: Boolean;
    procedure LoadConfig;
    procedure SynchronizeHelpFiles;
    procedure SaveConfig(const AServer, APort, ADatabase, AUserName, APassword, ADriverID: string);
    
    property CurrentUserId: string read FCurrentUserId write FCurrentUserId;
    property CurrentCompanyId: string read FCurrentCompanyId write FCurrentCompanyId;
    property CurrentCompanyName: string read FCurrentCompanyName write FCurrentCompanyName;
  end;

var
  dmgMain: TdmgMain;

implementation

uses
  System.IniFiles, System.IOUtils;

{%CLASSGROUP 'Vcl.Controls.TControl'}

{$R *.dfm}

procedure TdmgMain.DataModuleCreate(Sender: TObject);
begin
  // SOLUCIÓN DEFINITIVA: Forzar que TINYINT(1) de MySQL se mapee siempre como
  // TIntegerField (entero), NUNCA como TBooleanField.
  // Esto evita el error "Cannot access field 'X' as type Integer/Boolean"
  // en todos los módulos del proyecto que usan .AsInteger <> 0 para leer booleanos.
  dbConn.Params.Values['TinyIntFormat'] := 'Int';

  // Configurar parámetros de codificación Unicode estricta para MySQL/MariaDB
  dbConn.Params.Values['CharacterSet'] := 'utf8mb4';
  dbConn.Params.Values['StringFormat'] := 'Unicode';

  // Establecer por defecto parámetros de texto en Unicode (ftWideString) para prevenir desbordamientos o caracteres extraños
  dbConn.FormatOptions.DefaultParamDataType := ftWideString;
  dbConn.FormatOptions.StrsEmpty2Null := False;

  // Habilitar compresión de red zlib del protocolo MySQL/MariaDB para clientes remotos (WAN)
  dbConn.Params.Values['Compress'] := 'True';

  // Optimización de lectura de registros (fetching) para conexiones WAN
  dbConn.FetchOptions.Mode := fmOnDemand;
  dbConn.FetchOptions.RowsetSize := 100;
end;


procedure TdmgMain.Connect;
begin
  try
    dbConn.Params.Values['CharacterSet'] := 'utf8mb4';
    dbConn.Params.Values['StringFormat'] := 'Unicode';
    dbConn.Params.Values['Compress'] := 'True';
    dbConn.Connected := True;

    // Forzar la codificación de la sesión MySQL a UTF-8 con cotejo Unicode (utf8mb4_unicode_ci).
    // Con este cotejo las búsquedas son insensibles a mayúsculas/minúsculas y a acentos/caracteres especiales
    // (por ejemplo: TURRON encuentra TURRÓN, MAIZ encuentra MAÍZ, y PENA encuentra PEÑA).
    dbConn.ExecSQL('SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci');
    dbConn.ExecSQL('SET CHARACTER SET utf8mb4');
  except
    on E: Exception do
      raise Exception.Create('Error al conectar con la Base de Datos: ' + E.Message);
  end;
end;

procedure TdmgMain.Disconnect;
begin
  dbConn.Connected := False;
end;

procedure TdmgMain.GuardarLote(ACodigo: Integer; AIdArticulo, AIdObrador: Integer; AFecha: TDateTime; const AObservaciones: string);
begin
  qryExec.Close;
  qryExec.SQL.Text := 
    'INSERT INTO ge_lotes (codigo, codigo_antiguo, id_articulo, id_obrador, fecha, observaciones, activo, id_user_creator, id_user_update) ' +
    'VALUES (:codigo, :codigo_antiguo, :id_articulo, :id_obrador, :fecha, :observaciones, 1, :user, :user)';
  
  qryExec.ParamByName('codigo').DataType := ftInteger;
  qryExec.ParamByName('codigo').AsInteger := ACodigo;

  qryExec.ParamByName('codigo_antiguo').DataType := ftWideString;
  qryExec.ParamByName('codigo_antiguo').AsWideString := IntToStr(ACodigo);
  
  qryExec.ParamByName('id_articulo').DataType := ftInteger;
  if AIdArticulo > 0 then
    qryExec.ParamByName('id_articulo').AsInteger := AIdArticulo
  else
    qryExec.ParamByName('id_articulo').Clear;

  qryExec.ParamByName('id_obrador').DataType := ftInteger;
  if AIdObrador > 0 then
    qryExec.ParamByName('id_obrador').AsInteger := AIdObrador
  else
    qryExec.ParamByName('id_obrador').Clear;

  qryExec.ParamByName('fecha').DataType := ftDate;
  qryExec.ParamByName('fecha').AsDate := AFecha;
  
  qryExec.ParamByName('observaciones').DataType := ftWideString;
  if Trim(AObservaciones) <> '' then
    qryExec.ParamByName('observaciones').AsWideString := Trim(AObservaciones)
  else
    qryExec.ParamByName('observaciones').Clear;

  qryExec.ParamByName('user').DataType := ftInteger;
  if (CurrentUserId <> '') and (StrToIntDef(CurrentUserId, 0) > 0) then
    qryExec.ParamByName('user').AsInteger := StrToIntDef(CurrentUserId, 1)
  else
    qryExec.ParamByName('user').Clear;

  qryExec.ExecSQL;
end;

procedure TdmgMain.GuardarLote(const AIDLote, AIDProducto: string; AFecha: TDateTime);
begin
  GuardarLote(StrToIntDef(AIDLote, 0), StrToIntDef(AIDProducto, 0), 0, AFecha, '');
end;

function TdmgMain.GetNextCodigoLote: Integer;
begin
  qryExec.Close;
  qryExec.SQL.Text := 'SELECT COALESCE(MAX(codigo), 0) + 1 AS next_codigo FROM ge_lotes';
  qryExec.Open;
  Result := qryExec.FieldByName('next_codigo').AsInteger;
  qryExec.Close;
end;

procedure TdmgMain.RefreshLotes;
begin
  qryLotes.Close;
  qryLotes.SQL.Text := 
    'SELECT l.id, COALESCE(NULLIF(l.codigo_antiguo, ''''), l.codigo) as codigo, a.descripcion as articulo, o.nombre as obrador, l.fecha, l.observaciones ' +
    'FROM ge_lotes l ' +
    'LEFT JOIN ge_articulos a ON l.id_articulo = a.id ' +
    'LEFT JOIN ge_obradores o ON l.id_obrador = o.id ' +
    'WHERE l.activo = 1 ' +
    'ORDER BY l.fecha DESC, l.id DESC';
  qryLotes.Open;
end;

procedure TdmgMain.RefreshProductos(const AFilter: string);
begin
  qryProductos.Close;
  qryProductos.SQL.Text := 'SELECT id, codigo, descripcion FROM ge_articulos';
  if AFilter <> '' then
  begin
    qryProductos.SQL.Add('WHERE descripcion LIKE :filter');
    qryProductos.ParamByName('filter').AsString := '%' + AFilter + '%';
  end;
  qryProductos.Open;
end;

function TdmgMain.GetNextIDS(const ATable: string): string;
begin
  Result := '1';
  if FCurrentCompanyId = '' then
    Exit;

  qryExec.Close;
  qryExec.SQL.Text := 'SELECT COALESCE(MAX(numero), 0) + 1 as next_num FROM ' + ATable +
                      ' WHERE empresa_id = :emp';
  qryExec.ParamByName('emp').AsString := FCurrentCompanyId;
  qryExec.Open;
  Result := qryExec.FieldByName('next_num').AsString;
  qryExec.Close;
end;

class function TdmgMain.CleanUUID(const AGUID: string): string;
begin
  // FireDAC devuelve UUIDs de PostgreSQL con llaves: {GUID}
  // Hay que eliminarlas para que PostgreSQL pueda comparar correctamente.
  Result := StringReplace(StringReplace(AGUID, '{', '', [rfReplaceAll]), '}', '', [rfReplaceAll]);
end;

function TdmgMain.GetPrecioArticuloCliente(const AClienteId, AArticuloId: string; AFecha: TDateTime; ADefaultPrice: Double; ACantidad: Double = 1.0): Double;
var
  Qry: TFDQuery;
  LClienteIdInt, LArticuloIdInt, LGrupoIdInt: Integer;
  LCantidadInt: Integer;
begin
  Result := ADefaultPrice;
  if (AClienteId = '') or (AArticuloId = '') or (AClienteId = '0') or (AArticuloId = '0') then
    Exit;

  LCantidadInt := Trunc(ACantidad);
  if LCantidadInt <= 0 then LCantidadInt := 1;

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dbConn;

    // 1. PASO 1: Si el cliente pertenece a un grupo, buscar primero si existe tarifa especial para ese grupo en ge_clientes_grupos_precios
    Qry.SQL.Text := 'SELECT clientesgrupo_id FROM ge_clientes WHERE id = :cli';
    if TryStrToInt(AClienteId, LClienteIdInt) then
      Qry.ParamByName('cli').AsInteger := LClienteIdInt
    else
      Qry.ParamByName('cli').AsString := AClienteId;
    Qry.Open;

    LGrupoIdInt := 0;
    if not Qry.IsEmpty and not Qry.Fields[0].IsNull then
      LGrupoIdInt := Qry.Fields[0].AsInteger;

    Qry.Close;

    if LGrupoIdInt > 0 then
    begin
      Qry.SQL.Text :=
        'SELECT precio FROM ge_clientes_grupos_precios ' +
        'WHERE id_cliente_grupo = :grp AND id_articulo = :art ' +
        'AND (und_min IS NULL OR und_min <= :cant) ' +
        'AND (und_max IS NULL OR und_max >= :cant) ' +
        'AND (valido_desde IS NULL OR valido_desde <= :fecha) ' +
        'AND (valido_hasta IS NULL OR valido_hasta >= :fecha) ' +
        'ORDER BY (und_min IS NOT NULL) DESC, (und_max IS NOT NULL) DESC, (valido_desde IS NOT NULL) DESC, (valido_hasta IS NOT NULL) DESC, created_at DESC LIMIT 1';

      Qry.ParamByName('grp').AsInteger := LGrupoIdInt;
      if TryStrToInt(AArticuloId, LArticuloIdInt) then
        Qry.ParamByName('art').AsInteger := LArticuloIdInt
      else
        Qry.ParamByName('art').AsString := AArticuloId;
      Qry.ParamByName('cant').AsInteger := LCantidadInt;
      Qry.ParamByName('fecha').AsDate := AFecha;
      Qry.Open;

      if not Qry.IsEmpty then
      begin
        Result := Qry.FieldByName('precio').AsFloat;
        Exit; // Precio de grupo de cliente obtenido
      end;
      Qry.Close;
    end;

    // 2. PASO 2: Si no hay precio de grupo o no pertenece a grupo, buscar tarifa especial individual en ge_clientes_precios
    Qry.SQL.Text :=
      'SELECT precio FROM ge_clientes_precios ' +
      'WHERE id_cliente = :cli AND id_articulo = :art ' +
      'AND (valido_desde IS NULL OR valido_desde <= :fecha) ' +
      'AND (valido_hasta IS NULL OR valido_hasta >= :fecha) ' +
      'ORDER BY (valido_desde IS NOT NULL) DESC, (valido_hasta IS NOT NULL) DESC, created_at DESC LIMIT 1';

    if TryStrToInt(AClienteId, LClienteIdInt) then
      Qry.ParamByName('cli').AsInteger := LClienteIdInt
    else
      Qry.ParamByName('cli').AsString := AClienteId;

    if TryStrToInt(AArticuloId, LArticuloIdInt) then
      Qry.ParamByName('art').AsInteger := LArticuloIdInt
    else
      Qry.ParamByName('art').AsString := AArticuloId;

    Qry.ParamByName('fecha').AsDate := AFecha;
    Qry.Open;

    if not Qry.IsEmpty then
    begin
      Result := Qry.FieldByName('precio').AsFloat;
      Exit; // Precio especial individual obtenido
    end;

    // 3. PASO 3: Si no encuentra registros en las tablas anteriores, devuelve ADefaultPrice (pvp1 del artículo)
  finally
    Qry.Free;
  end;
end;

function TdmgMain.GetSerieYNumeroDocumento(const AEmpresaId, ATipoDoc: string; AFecha: TDateTime; const ACurrentSerie: string; var AOutSerie: string; var AOutNumero: Integer): Boolean;
var
  Qry: TFDQuery;
  LFieldName: string;
  LFechaDoc: TDate;
  LNeedNewSerie: Boolean;
begin
  Result := False;
  LNeedNewSerie := False;
  LFechaDoc := Trunc(AFecha);

  if SameText(ATipoDoc, 'ALBARAN') then
    LFieldName := 'sig_numero_albaran'
  else if SameText(ATipoDoc, 'FACTURA') then
    LFieldName := 'sig_numero_factura'
  else if SameText(ATipoDoc, 'PRESUPUESTO') then
    LFieldName := 'sig_numero_presupuesto'
  else
    LFieldName := 'sig_numero_albaran';

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dbConn;

    // 1. Si tenemos serie actual, comprobar si la fecha está dentro de su rango válido
    if ACurrentSerie <> '' then
    begin
      Qry.SQL.Text :=
        'SELECT serie, ' + LFieldName + ' as sig_num, fecha_inicio, fecha_fin ' +
        'FROM ge_series ' +
        'WHERE empresa_id = :emp AND serie = :ser AND activo = 1';
      Qry.ParamByName('emp').AsString := AEmpresaId;
      Qry.ParamByName('ser').AsString := ACurrentSerie;
      Qry.Open;

      if not Qry.IsEmpty then
      begin
        if (Qry.FieldByName('fecha_inicio').IsNull or (Qry.FieldByName('fecha_inicio').AsDateTime <= LFechaDoc)) and
           (Qry.FieldByName('fecha_fin').IsNull or (Qry.FieldByName('fecha_fin').AsDateTime >= LFechaDoc)) then
        begin
          AOutSerie := Qry.FieldByName('serie').AsString;
          AOutNumero := Qry.FieldByName('sig_num').AsInteger;
          Result := True;
          Exit;
        end
        else
          LNeedNewSerie := True;
      end
      else
        LNeedNewSerie := True;
      Qry.Close;
    end
    else
      LNeedNewSerie := True;


    // 2. Si no hay serie o la fecha no entra en la serie actual, buscar la serie adecuada según fecha
    if LNeedNewSerie then
    begin
      Qry.SQL.Text :=
        'SELECT serie, ' + LFieldName + ' as sig_num ' +
        'FROM ge_series ' +
        'WHERE empresa_id = :emp AND activo = 1 ' +
        'AND (fecha_inicio IS NULL OR fecha_inicio <= :fecha) ' +
        'AND (fecha_fin IS NULL OR fecha_fin >= :fecha) ' +
        'ORDER BY por_defecto DESC, (fecha_inicio IS NOT NULL) DESC, (fecha_fin IS NOT NULL) DESC LIMIT 1';
      Qry.ParamByName('emp').AsString := AEmpresaId;
      Qry.ParamByName('fecha').AsDate := LFechaDoc;
      Qry.Open;

      if not Qry.IsEmpty then
      begin
        AOutSerie := Qry.FieldByName('serie').AsString;
        AOutNumero := Qry.FieldByName('sig_num').AsInteger;
        Result := True;
      end
      else
      begin
        // Si no hay ninguna coincidente por fecha, obtener la serie por defecto
        Qry.Close;
        Qry.SQL.Text :=
          'SELECT serie, ' + LFieldName + ' as sig_num ' +
          'FROM ge_series ' +
          'WHERE empresa_id = :emp AND activo = 1 ' +
          'ORDER BY por_defecto DESC LIMIT 1';
        Qry.ParamByName('emp').AsString := AEmpresaId;
        Qry.Open;
        if not Qry.IsEmpty then
        begin
          AOutSerie := Qry.FieldByName('serie').AsString;
          AOutNumero := Qry.FieldByName('sig_num').AsInteger;
          Result := True;
        end;
      end;
    end;
  finally
    Qry.Free;
  end;
end;

function TdmgMain.HasConfig: Boolean;
begin
  Result := FileExists(IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + 'db_config.ini');
end;

function TdmgMain.EncryptStr(const AText: string): string;
var
  I: Integer;
  LVal: Word;
  LKey: Word;
begin
  Result := 'ENC:';
  LKey := $A5C3; // Llave de encriptación fija
  for I := 1 to Length(AText) do
  begin
    LVal := Ord(AText[I]) xor LKey;
    Result := Result + Format('%.4X', [LVal]);
  end;
end;

function TdmgMain.DecryptStr(const AText: string): string;
var
  I: Integer;
  LVal: Integer;
  LKey: Word;
  LCipherText: string;
begin
  if not SameText(Copy(AText, 1, 4), 'ENC:') then
  begin
    Result := AText;
    Exit;
  end;

  LCipherText := Copy(AText, 5, MaxInt);
  Result := '';
  LKey := $A5C3;
  I := 1;
  while I < Length(LCipherText) do
  begin
    if TryStrToInt('$' + Copy(LCipherText, I, 4), LVal) then
      Result := Result + Char(LVal xor LKey);
    Inc(I, 4);
  end;
end;

procedure TdmgMain.LoadConfig;
var
  LIni: TIniFile;
  LPath: string;
begin
  LPath := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + 'db_config.ini';
  LIni := TIniFile.Create(LPath);
  try
    dbConn.Params.Values['Server'] := DecryptStr(LIni.ReadString('Database', 'Server', EncryptStr('85.215.144.168')));
    dbConn.Params.Values['Port'] := DecryptStr(LIni.ReadString('Database', 'Port', EncryptStr('3306')));
    dbConn.Params.Values['Database'] := DecryptStr(LIni.ReadString('Database', 'Database', EncryptStr('pastelerias')));
    dbConn.Params.Values['User_Name'] := DecryptStr(LIni.ReadString('Database', 'User_Name', EncryptStr('asesoft')));
    dbConn.Params.Values['Password'] := DecryptStr(LIni.ReadString('Database', 'Password', EncryptStr('Pantera1')));
    dbConn.Params.Values['DriverID'] := DecryptStr(LIni.ReadString('Database', 'DriverID', EncryptStr('MySQL')));
    dbConn.Params.Values['CharacterSet'] := 'utf8mb4';
  finally
    LIni.Free;
  end;
  
  // Sincronizar ayudas en segundo plano desde la NAS
  SynchronizeHelpFiles;
end;

procedure TdmgMain.SynchronizeHelpFiles;
var
  LIniPath, LNetworkPath, LLocalPath: string;
  LIni: TIniFile;
  LSearchRec: TSearchRec;
  LSourceFile, LDestFile: string;
begin
  LLocalPath := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + 'help\';
  LIniPath := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + 'db_config.ini';
  
  if not FileExists(LIniPath) then Exit;
  
  LIni := TIniFile.Create(LIniPath);
  try
    LNetworkPath := LIni.ReadString('Help', 'NetworkPath', '');
  finally
    LIni.Free;
  end;
  
  if LNetworkPath = '' then Exit;
  LNetworkPath := IncludeTrailingPathDelimiter(LNetworkPath);
  
  // Si la carpeta de la NAS no es accesible, salir de inmediato sin dar error
  if not DirectoryExists(LNetworkPath) then Exit;
  
  // Asegurar que la carpeta local exista
  if not DirectoryExists(LLocalPath) then
    ForceDirectories(LLocalPath);
    
  // Sincronizar todos los archivos .txt de ayuda en segundo plano
  if FindFirst(LNetworkPath + '*.txt', faAnyFile, LSearchRec) = 0 then
  begin
    try
      repeat
        LSourceFile := LNetworkPath + LSearchRec.Name;
        LDestFile := LLocalPath + LSearchRec.Name;
        
        // Copiar si el archivo local no existe o el del servidor es más reciente
        if not FileExists(LDestFile) or 
           (TFile.GetLastWriteTime(LSourceFile) > TFile.GetLastWriteTime(LDestFile)) then
        begin
          try
            TFile.Copy(LSourceFile, LDestFile, True); // Sobrescribir local
          except
            // Ignorar errores de copia (ej. si el archivo local está bloqueado)
          end;
        end;
      until FindNext(LSearchRec) <> 0;
    finally
      FindClose(LSearchRec);
    end;
  end;
end;

procedure TdmgMain.SaveConfig(const AServer, APort, ADatabase, AUserName, APassword, ADriverID: string);
var
  LIni: TIniFile;
  LPath: string;
begin
  LPath := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + 'db_config.ini';
  LIni := TIniFile.Create(LPath);
  try
    LIni.WriteString('Database', 'Server', EncryptStr(AServer));
    LIni.WriteString('Database', 'Port', EncryptStr(APort));
    LIni.WriteString('Database', 'Database', EncryptStr(ADatabase));
    LIni.WriteString('Database', 'User_Name', EncryptStr(AUserName));
    LIni.WriteString('Database', 'Password', EncryptStr(APassword));
    LIni.WriteString('Database', 'DriverID', EncryptStr(ADriverID));
  finally
    LIni.Free;
  end;
  
  LoadConfig;
end;

end.
