Hwgui Turbo!

Projeto HwGui - Biblioteca visual para Harbour/xHarbour

Moderador: Moderadores

Avatar do usuário
Itamar M. Lins Jr.
Administrador
Administrador
Mensagens: 8304
Registrado em: 30 Mai 2007 11:31
Localização: Ilheus Bahia
Curtiu: 7 vezes
Curtiram: 2 vezes

Hwgui Turbo!

Mensagem por Itamar M. Lins Jr. »

Olá!
Ô! coisinha, tão bonitinha do pai!
HwguiTurbo1.PNG
HwguiTurbo1.PNG (38.13 KiB) Exibido 63 vezes

Código: Selecionar todos

#include "hwgui.ch"
#include "hbdyn.ch"

STATIC pLib     := NIL
STATIC hWebView := NIL

//=============================================================================
FUNCTION Main()
   LOCAL oForm

   hb_cdpSelect( "UTF8" )

   // Cria o DBF se ainda nao existir
   IF !File( "clientes.dbf" )
      DbCreate( "clientes.dbf", { ;
         { "NOME",  "C",  60, 0 }, ;
         { "EMAIL", "C",  80, 0 } } )
   ENDIF

   pLib := hb_LibLoad( "webview_wrapper.dll" )
   IF pLib == NIL
      hwg_MsgStop( "Falhou hb_LibLoad" )
      RETURN NIL
   ENDIF

   INIT WINDOW oForm TITLE "Hwgui + WebView2" SIZE 900, 700 ;
      ON INIT { || InicializaWebView( oForm ) } ;
      ON EXIT { || FinalizaWebView() }

   MENU OF oForm
      MENU TITLE "&Arquivo"
         MENUITEM "Sai&r" ACTION oForm:Close()
      ENDMENU
      MENU TITLE "A&juda"
         MENUITEM "&Sobre..." ACTION hwg_MsgInfo( "Hwgui + WebView2" )
      ENDMENU
   ENDMENU

   // Captura WM_USER+100 enviada pelo C via PostMessage
   oForm:bOther := { |o, msg, wp, lp| OnOtherMsg( o, msg, wp, lp ) }

   ACTIVATE WINDOW oForm CENTER

   IF pLib != NIL
      hb_LibFree( pLib )
   ENDIF
RETURN NIL

//=============================================================================
STATIC FUNCTION OnOtherMsg( o, nMsg, nWParam, nLParam )
   // WM_USER = 0x0400 = 1024 ; WM_USER+100 = 1124
   IF nMsg == 1124
      CheckMensagem()
      RETURN 0
   ENDIF
RETURN -1

//=============================================================================
STATIC FUNCTION InicializaWebView( oForm )
   LOCAL nBind

   hWebView := hb_DynCall( { "create_webview_embedded", pLib, ;
                             hb_bitOr( HB_DYN_CTYPE_VOID_PTR, HB_DYN_CALLCONV_CDECL ), ;
                             HB_DYN_CTYPE_CHAR_PTR, ;
                             HB_DYN_CTYPE_CHAR_PTR, ;
                             HB_DYN_CTYPE_INT, ;
                             HB_DYN_CTYPE_INT }, ;
                           "Hwgui + WebView2", "WebView2", 900, 660 )

   IF hWebView == NIL
      hwg_MsgStop( "Nao criou WebView" )
      RETURN NIL
   ENDIF

   // Registra as funcoes que o JavaScript podera chamar
   nBind := BindJS( "GravarCliente" )
   IF nBind != 0
      hwg_MsgStop( "bind GravarCliente falhou: " + hb_ValToStr( nBind ) )
   ENDIF

   BindJS( "ListarClientes" )
   BindJS( "ApagarCliente"  )
   BindJS( "BuscarCliente"  )

   hb_DynCall( { "navigate_webview", pLib, ;
                 hb_bitOr( HB_DYN_CTYPE_VOID, HB_DYN_CALLCONV_CDECL ), ;
                 HB_DYN_CTYPE_VOID_PTR, ;
                 HB_DYN_CTYPE_CHAR_PTR }, ;
               hWebView, "file:///C:/dev/hwgui/contrib/webview2/clientes.html" )
RETURN NIL

//=============================================================================
STATIC FUNCTION BindJS( cName )
RETURN hb_DynCall( { "bind_webview", pLib, ;
                     hb_bitOr( HB_DYN_CTYPE_INT, HB_DYN_CALLCONV_CDECL ), ;
                     HB_DYN_CTYPE_VOID_PTR, ;
                     HB_DYN_CTYPE_CHAR_PTR }, ;
                   hWebView, cName )

//=============================================================================
STATIC FUNCTION CheckMensagem()
   LOCAL cId, cJson, cFunc, cResult

   IF hWebView == NIL
      RETURN NIL
   ENDIF

   cId := hb_DynCall( { "get_message_id", pLib, ;
                        hb_bitOr( HB_DYN_CTYPE_CHAR_PTR, HB_DYN_CALLCONV_CDECL ) } )
   IF ValType( cId ) == "A" .AND. Len( cId ) >= 1
      cId := cId[1]
   ENDIF
   IF cId == NIL .OR. Empty( cId )
      RETURN NIL
   ENDIF

   cFunc := hb_DynCall( { "get_message_func", pLib, ;
                          hb_bitOr( HB_DYN_CTYPE_CHAR_PTR, HB_DYN_CALLCONV_CDECL ) } )
   IF ValType( cFunc ) == "A" .AND. Len( cFunc ) >= 1
      cFunc := cFunc[1]
   ENDIF

   cJson := hb_DynCall( { "get_message_data", pLib, ;
                          hb_bitOr( HB_DYN_CTYPE_CHAR_PTR, HB_DYN_CALLCONV_CDECL ) } )
   IF ValType( cJson ) == "A" .AND. Len( cJson ) >= 1
      cJson := cJson[1]
   ENDIF

   cResult := Despachar( cFunc, cJson )

   hb_DynCall( { "reply_webview", pLib, ;
                 hb_bitOr( HB_DYN_CTYPE_INT, HB_DYN_CALLCONV_CDECL ), ;
                 HB_DYN_CTYPE_VOID_PTR, ;
                 HB_DYN_CTYPE_CHAR_PTR }, ;
               hWebView, cResult )
RETURN NIL

//=============================================================================
STATIC FUNCTION Despachar( cFunc, cJson )
   LOCAL hDados

   hDados := hb_jsonDecode( cJson )
   IF ValType( hDados ) == "A" .AND. Len( hDados ) >= 1
      hDados := hDados[1]
   ENDIF
   IF ValType( hDados ) != "H"
      hDados := {=>}
   ENDIF

   DO CASE
   CASE cFunc == "GravarCliente"
      RETURN GravarCliente( hDados )
   CASE cFunc == "ListarClientes"
      RETURN ListarClientes()
   CASE cFunc == "ApagarCliente"
      RETURN ApagarCliente( hDados )
   CASE cFunc == "BuscarCliente"
      RETURN BuscarCliente( hDados )
   ENDCASE

RETURN hb_jsonEncode( { "ok" => .F., "msg" => "Funcao desconhecida: " + hb_ValToStr( cFunc ) } )

//=============================================================================
STATIC FUNCTION GravarCliente( hDados )
   LOCAL cNome  := hb_HGetDef( hDados, "nome",  "" )
   LOCAL cEmail := hb_HGetDef( hDados, "email", "" )
   LOCAL nRecno

   IF Empty( cNome )
      RETURN hb_jsonEncode( { "ok" => .F., "msg" => "Nome obrigatorio" } )
   ENDIF

   AbreClientes()

   clientes->( DbAppend() )
   REPLACE clientes->NOME  WITH cNome
   REPLACE clientes->EMAIL WITH cEmail
   nRecno := clientes->( RecNo() )
   clientes->( DbCommit() )

RETURN hb_jsonEncode( { "ok" => .T., "recno" => nRecno } )

//=============================================================================
STATIC FUNCTION ListarClientes()
   LOCAL aLista := {}
   LOCAL nRecno

   AbreClientes()
   clientes->( DbGoTop() )
   DO WHILE !clientes->( Eof() )
      nRecno := clientes->( RecNo() )
      AAdd( aLista, { "recno" => nRecno, ;
                      "nome"  => AllTrim( clientes->NOME ), ;
                      "email" => AllTrim( clientes->EMAIL ) } )
      clientes->( DbSkip() )
   ENDDO

RETURN hb_jsonEncode( { "ok" => .T., "clientes" => aLista } )

//=============================================================================
STATIC FUNCTION ApagarCliente( hDados )
   LOCAL nRecno := hb_HGetDef( hDados, "recno", 0 )

   IF nRecno <= 0
      RETURN hb_jsonEncode( { "ok" => .F., "msg" => "RECNO invalido" } )
   ENDIF

   AbreClientes()
   clientes->( DbGoTo( nRecno ) )
   IF !clientes->( Eof() )
      clientes->( DbDelete() )
      clientes->( DbCommit() )
      RETURN hb_jsonEncode( { "ok" => .T. } )
   ENDIF

RETURN hb_jsonEncode( { "ok" => .F., "msg" => "Registro nao encontrado" } )

//=============================================================================
STATIC FUNCTION BuscarCliente( hDados )
   LOCAL cNome  := hb_HGetDef( hDados, "nome", "" )
   LOCAL aLista := {}

   AbreClientes()
   clientes->( DbGoTop() )
   DO WHILE !clientes->( Eof() )
      IF Upper( cNome ) $ Upper( AllTrim( clientes->NOME ) )
         AAdd( aLista, { "recno" => clientes->( RecNo() ), ;
                         "nome"  => AllTrim( clientes->NOME ), ;
                         "email" => AllTrim( clientes->EMAIL ) } )
      ENDIF
      clientes->( DbSkip() )
   ENDDO

RETURN hb_jsonEncode( { "ok" => .T., "clientes" => aLista } )

//=============================================================================
STATIC FUNCTION AbreClientes()
   IF !Used()
      IF !File( "clientes.dbf" )
         DbCreate( "clientes.dbf", { ;
            { "NOME",  "C",  60, 0 }, ;
            { "EMAIL", "C",  80, 0 } } )
      ENDIF
      USE clientes.dbf ALIAS clientes NEW
   ENDIF
RETURN NIL

//=============================================================================
STATIC FUNCTION FinalizaWebView()
   IF hWebView != NIL
      hb_DynCall( { "destroy_webview", pLib, ;
                    hb_bitOr( HB_DYN_CTYPE_VOID, HB_DYN_CALLCONV_CDECL ), ;
                    HB_DYN_CTYPE_VOID_PTR }, ;
                  hWebView )
      hWebView := NIL
   ENDIF
RETURN NIL
Saudações,
Itamar M. Lins Jr.
Avatar do usuário
Itamar M. Lins Jr.
Administrador
Administrador
Mensagens: 8304
Registrado em: 30 Mai 2007 11:31
Localização: Ilheus Bahia
Curtiu: 7 vezes
Curtiram: 2 vezes

Re: Hwgui Turbo!

Mensagem por Itamar M. Lins Jr. »

Olá!
Gosta de telas em html com CSS ? Quer rodar misturado ? É caixa, URL, gravar, DBF, SQL ?
Quer misturar com Telas Windows ? Tá ai a solução.
Anexos
HwguiTurbo2.PNG
HwguiTurbo2.PNG (34.55 KiB) Exibido 52 vezes
Saudações,
Itamar M. Lins Jr.
Avatar do usuário
Itamar M. Lins Jr.
Administrador
Administrador
Mensagens: 8304
Registrado em: 30 Mai 2007 11:31
Localização: Ilheus Bahia
Curtiu: 7 vezes
Curtiram: 2 vezes

Re: Hwgui Turbo!

Mensagem por Itamar M. Lins Jr. »

Olá!
Tela dos testes.
HwguiTurbo0.PNG
HwguiTurbo0.PNG (36.9 KiB) Exibido 51 vezes
Saudações,
Itamar M. Lins Jr.
Avatar do usuário
Itamar M. Lins Jr.
Administrador
Administrador
Mensagens: 8304
Registrado em: 30 Mai 2007 11:31
Localização: Ilheus Bahia
Curtiu: 7 vezes
Curtiram: 2 vezes

Re: Hwgui Turbo!

Mensagem por Itamar M. Lins Jr. »

Olá!
Anexos
HwguiTurbo3.PNG
HwguiTurbo3.PNG (40.5 KiB) Exibido 50 vezes
Saudações,
Itamar M. Lins Jr.
Avatar do usuário
Itamar M. Lins Jr.
Administrador
Administrador
Mensagens: 8304
Registrado em: 30 Mai 2007 11:31
Localização: Ilheus Bahia
Curtiu: 7 vezes
Curtiram: 2 vezes

Re: Hwgui Turbo!

Mensagem por Itamar M. Lins Jr. »

Olá!
Brinquem ai.
É a IA que faz as telas em HTML. Desenhar tela cansa muito. :lol:
Basta vc definir o DBF, gerar as telas usando IA.
Com ABAS que vc pode mudar de posição... Enfim é coisa que qualquer navegador faz.
Visual é o mesmo.
WebView4.PNG
WebView4.PNG (33.94 KiB) Exibido 47 vezes
webview2.rar
Atualização Bind unico. Paginação só 50 registros.
Só mudou no Prg e no Html
(836.92 KiB) Baixado 3 vezes
https://developer.microsoft.com/en-us/m ... 3#download
WebView4.PNG
WebView4.PNG (33.94 KiB) Exibido 47 vezes
Anexos
Captura_de_tela_20260916_011129.png
Captura_de_tela_20260916_011129.png (137.44 KiB) Exibido 47 vezes
Saudações,
Itamar M. Lins Jr.
Avatar do usuário
Itamar M. Lins Jr.
Administrador
Administrador
Mensagens: 8304
Registrado em: 30 Mai 2007 11:31
Localização: Ilheus Bahia
Curtiu: 7 vezes
Curtiram: 2 vezes

Re: Hwgui Turbo!

Mensagem por Itamar M. Lins Jr. »

Olá!
O exemplo deve ter falhando, atualizei para ler o exemplo na pasta onde se encontra o HTML.
Já atualizei. Baixar novamente do mesmo lugar.
Saudações,
Itamar M. Lins Jr.
Avatar do usuário
Itamar M. Lins Jr.
Administrador
Administrador
Mensagens: 8304
Registrado em: 30 Mai 2007 11:31
Localização: Ilheus Bahia
Curtiu: 7 vezes
Curtiram: 2 vezes

Re: Hwgui Turbo!

Mensagem por Itamar M. Lins Jr. »

Olá!
Modifiquei mais algumas coisas.
Não vou ficar mexendo nisso, são infinitas possíbilidades.
Eu estava preocupado com a quantidade de BindJS funções que chamam do JS para o EXE do Harbour.
Segundo a IA uns 50 vai de boa, mas não tem limites. 1000, 10000...
WebView5.PNG
WebView5.PNG (35.33 KiB) Exibido 23 vezes
Ai é no HTML, JS.

Código: Selecionar todos

/* ============================================================
   PONTE ÚNICA — TODO o backend passa por aqui
   ------------------------------------------------------------
   Executar("cliente.gravar",  { nome: "...", email: "..." })
   Executar("cliente.listar",  { pagina: 1, por_pagina: 50 })
   Executar("cliente.apagar",  { recno: 5 })
   Executar("cliente.buscar",  { nome: "jo", pagina: 1, ... })
   Executar("cliente.resumo",  {})
   ============================================================ */
async function Executar(acao, args) {
   return await ExecutarAcao({ acao: acao, args: args || {} });
}
Todo o HTML. 100% gerado por IA.
Nisso vc vai pedido, quero isso, quero aquilo... pq isso ?, pq aquilo...?

Código: Selecionar todos

<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>HwguiTurbo — Cadastro de Clientes</title>
<style>
  * { box-sizing: border-box; }

  body {
    font-family: 'Segoe UI', Arial, sans-serif;
    margin: 0;
    padding: 0;
    background: #f5f6f7;
    color: #222;
    height: 100vh;
    display: flex;
    flex-direction: column;
  }

  /* ---- Barra de abas ---- */
  .tabs {
    display: flex;
    background: #e8eaed;
    border-bottom: 1px solid #ccc;
    padding: 0 10px;
    flex-shrink: 0;
    overflow-x: auto;
  }

  .tab {
    padding: 12px 24px;
    cursor: grab;
    border: 1px solid transparent;
    border-bottom: none;
    margin-bottom: -1px;
    user-select: none;
    font-size: 14px;
    color: #555;
    transition: background 0.15s, opacity 0.15s;
    border-radius: 6px 6px 0 0;
    white-space: nowrap;
  }

  .tab:active { cursor: grabbing; }
  .tab:hover { background: #dfe1e5; }

  .tab.active {
    background: #fff;
    color: #0078d4;
    border-color: #ccc;
    font-weight: 600;
  }

  .tab.dragging { opacity: 0.4; }
  .tab.drop-target { border-left: 3px solid #0078d4; }

  /* ---- Painéis ---- */
  .panel {
    display: none;
    padding: 24px;
    flex: 1;
    overflow-y: auto;
    background: #fff;
  }

  .panel.active { display: block; }

  h2 {
    margin-top: 0;
    color: #2c3e50;
    border-bottom: 1px solid #eee;
    padding-bottom: 8px;
  }

  /* ---- Formulário ---- */
  .form-row {
    margin-bottom: 14px;
    max-width: 500px;
  }

  .form-row label {
    display: block;
    font-weight: 600;
    font-size: 13px;
    margin-bottom: 4px;
    color: #555;
  }

  .form-row input {
    width: 100%;
    padding: 9px 12px;
    border: 1px solid #ccc;
    border-radius: 5px;
    font-size: 14px;
  }

  .form-row input:focus {
    outline: none;
    border-color: #0078d4;
    box-shadow: 0 0 0 2px rgba(0,120,212,0.15);
  }

  /* ---- Botões ---- */
  button {
    padding: 9px 20px;
    background: #0078d4;
    color: #fff;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 500;
    transition: background 0.15s;
  }

  button:hover { background: #006cbd; }
  button:active { background: #005a9e; }

  button.danger { background: #c0392b; padding: 4px 10px; font-size: 12px; }
  button.danger:hover { background: #a93226; }

  button.secondary { background: #6c757d; padding: 4px 10px; font-size: 12px; }
  button.secondary:hover { background: #5a6268; }

  button.restore { background: #27ae60; padding: 4px 10px; font-size: 12px; }
  button.restore:hover { background: #1e8449; }

  .del-label {
    display: inline-block;
    background: #f8d7da;
    color: #721c24;
    font-size: 11px;
    font-weight: 700;
    padding: 2px 6px;
    border-radius: 3px;
    letter-spacing: 0.5px;
    vertical-align: middle;
    margin-left: 6px;
  }

  /* ---- Mensagens ---- */
  .msg {
    margin-top: 12px;
    padding: 10px 14px;
    border-radius: 5px;
    font-weight: 500;
    display: none;
    max-width: 500px;
  }

  .msg.ok   { display: block; background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
  .msg.erro { display: block; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }

  /* ---- Tabela ---- */
  table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 16px;
  }

  th {
    background: #f0f2f5;
    padding: 10px 12px;
    text-align: left;
    font-size: 13px;
    color: #444;
    border-bottom: 2px solid #ddd;
  }

  td {
    padding: 9px 12px;
    border-bottom: 1px solid #eee;
    font-size: 14px;
  }

  tr:hover td { background: #f9fafb; }

  tr.row-deleted td {
    opacity: 0.55;
    text-decoration: line-through;
  }

  .empty {
    text-align: center;
    padding: 30px;
    color: #999;
    font-style: italic;
  }

  /* ---- Paginador ---- */
  .pager {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 12px;
    flex-wrap: wrap;
  }

  .pager button {
    padding: 6px 12px;
    font-size: 16px;
    min-width: 38px;
  }

  .pager span {
    padding: 0 12px;
    font-size: 13px;
    color: #555;
    font-weight: 500;
  }

  .pager select {
    padding: 6px 8px;
    border: 1px solid #ccc;
    border-radius: 5px;
    font-size: 13px;
  }

  /* ---- Busca ---- */
  .busca-row {
    display: flex;
    gap: 10px;
    max-width: 500px;
    margin-bottom: 4px;
  }

  .busca-row input {
    flex: 1;
    padding: 9px 12px;
    border: 1px solid #ccc;
    border-radius: 5px;
    font-size: 14px;
  }

  /* ---- Resumo / Relatório ---- */
  .cards {
    display: flex;
    gap: 16px;
    flex-wrap: wrap;
    margin-top: 16px;
  }

  .card {
    background: #fff;
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    padding: 20px 24px;
    min-width: 180px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.05);
  }

  .card .label {
    font-size: 12px;
    color: #888;
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }

  .card .value {
    font-size: 32px;
    font-weight: 700;
    color: #0078d4;
    margin-top: 6px;
  }
</style>
</head>
<body>

<!-- ============ BARRA DE ABAS ============ -->
<div class="tabs" id="tabs">
  <div class="tab active" data-tab="cadastro"  onclick="showTab('cadastro',  this)">Cadastro</div>
  <div class="tab"        data-tab="lista"     onclick="showTab('lista',     this)">Lista</div>
  <div class="tab"        data-tab="busca"     onclick="showTab('busca',     this)">Buscar</div>
  <div class="tab"        data-tab="relatorio" onclick="showTab('relatorio', this)">Relatório</div>
  <div class="tab"        data-tab="sobre"     onclick="showTab('sobre',     this)">Sobre</div>
</div>

<!-- ============ ABA CADASTRO ============ -->
<div id="cadastro" class="panel active">
  <h2>Novo Cliente</h2>

  <div class="form-row">
    <label>Nome</label>
    <input type="text" id="cad_nome" placeholder="Digite o nome completo">
  </div>

  <div class="form-row">
    <label>Email</label>
    <input type="text" id="cad_email" placeholder="exemplo@dominio.com">
  </div>

  <button onclick="salvar()">Salvar</button>
  <button class="secondary" onclick="limparForm()" style="padding:9px 20px;font-size:14px;">Limpar</button>

  <div id="cad_msg" class="msg"></div>
</div>

<!-- ============ ABA LISTA ============ -->
<div id="lista" class="panel">
  <h2>Clientes Cadastrados</h2>

  <div class="pager">
    <button onclick="listaPrimeira()" title="Primeira">&laquo;</button>
    <button onclick="listaAnterior()" title="Anterior">&lsaquo;</button>
    <span id="lista_info">—</span>
    <button onclick="listaProxima()"  title="Próxima">&rsaquo;</button>
    <button onclick="listaUltima()"   title="Última">&raquo;</button>
    <select id="lista_por_pagina" onchange="listaMudarPorPagina()">
      <option value="25">25 / página</option>
      <option value="50" selected>50 / página</option>
      <option value="100">100 / página</option>
      <option value="200">200 / página</option>
    </select>
    <button onclick="listar()">Atualizar</button>
  </div>

  <table>
    <thead>
      <tr>
        <th style="width:80px">RECNO</th>
        <th>Nome</th>
        <th>Email</th>
        <th style="width:180px">Ações</th>
      </tr>
    </thead>
    <tbody id="lista_tbody">
      <tr><td colspan="4" class="empty">Nenhum cliente carregado</td></tr>
    </tbody>
  </table>
</div>

<!-- ============ ABA BUSCAR ============ -->
<div id="busca" class="panel">
  <h2>Buscar Cliente</h2>

  <div class="busca-row">
    <input type="text" id="busca_nome" placeholder="Digite parte do nome...">
    <button onclick="buscar(1)">Buscar</button>
  </div>

  <div class="pager" style="margin-top:12px;">
    <button onclick="buscaPrimeira()" title="Primeira">&laquo;</button>
    <button onclick="buscaAnterior()" title="Anterior">&lsaquo;</button>
    <span id="busca_info">—</span>
    <button onclick="buscaProxima()"  title="Próxima">&rsaquo;</button>
    <button onclick="buscaUltima()"   title="Última">&raquo;</button>
    <select id="busca_por_pagina" onchange="buscaMudarPorPagina()">
      <option value="25">25 / página</option>
      <option value="50" selected>50 / página</option>
      <option value="100">100 / página</option>
      <option value="200">200 / página</option>
    </select>
  </div>

  <table>
    <thead>
      <tr>
        <th style="width:80px">RECNO</th>
        <th>Nome</th>
        <th>Email</th>
        <th style="width:80px">Status</th>
      </tr>
    </thead>
    <tbody id="busca_tbody">
      <tr><td colspan="4" class="empty">Digite um nome para buscar</td></tr>
    </tbody>
  </table>
</div>

<!-- ============ ABA RELATÓRIO ============ -->
<div id="relatorio" class="panel">
  <h2>Relatório</h2>
  <button onclick="gerarRelatorio()">Gerar Relatório</button>

  <div class="cards">
    <div class="card">
      <div class="label">Total (ativos)</div>
      <div class="value" id="rel_total">—</div>
    </div>
    <div class="card">
      <div class="label">Último RECNO</div>
      <div class="value" id="rel_ultimo">—</div>
    </div>
    <div class="card">
      <div class="label">Com email</div>
      <div class="value" id="rel_com_email">—</div>
    </div>
    <div class="card">
      <div class="label">Deletados</div>
      <div class="value" id="rel_deletados">—</div>
    </div>
  </div>
</div>

<!-- ============ ABA SOBRE ============ -->
<div id="sobre" class="panel">
  <h2>Sobre</h2>
  <p><strong>HwguiTurbo</strong> — versão 1.0.0</p>
  <p>Integração do WebView2 (Chromium) com Hwgui/Harbour.</p>
  <p>
    <strong>Stack:</strong><br>
    &bull; Clang 22 (MSYS2 CLANG64)<br>
    &bull; Harbour (Git)<br>
    &bull; Hwgui (dev)<br>
    &bull; Microsoft WebView2 Runtime
  </p>
  <p><strong>Arquitetura:</strong> um único bind (<code>Executar</code>) despacha todas as ações via string.</p>
  <p><strong>Recursos:</strong></p>
  <ul>
    <li>WebView2 embutido na janela Hwgui</li>
    <li>Comunicação bidirecional JS ↔ Harbour via JSON</li>
    <li>Bind único com despacho por comando</li>
    <li>Redimensionamento automático</li>
    <li>Abas arrastáveis — segure e mova para reordenar</li>
    <li>Registros deletados visíveis com marcação DEL</li>
    <li>Restaurar registro deletado</li>
    <li>Paginação na Lista e na Busca</li>
    <li>Compilação pura com Clang, sem MSVC ou GCC</li>
  </ul>
</div>

<!-- ============ SCRIPT ============ -->
<script>

/* ============================================================
   PONTE ÚNICA — TODO o backend passa por aqui
   ------------------------------------------------------------
   Executar("cliente.gravar",  { nome: "...", email: "..." })
   Executar("cliente.listar",  { pagina: 1, por_pagina: 50 })
   Executar("cliente.apagar",  { recno: 5 })
   Executar("cliente.buscar",  { nome: "jo", pagina: 1, ... })
   Executar("cliente.resumo",  {})
   ============================================================ */
async function Executar(acao, args) {
   return await ExecutarAcao({ acao: acao, args: args || {} });
}

/* ============================================================
   ESTADO
   ============================================================ */
let listaPaginaAtual = 1;
let listaTotalPaginas = 1;
let buscaPaginaAtual = 1;
let buscaTotalPaginas = 1;
let buscaUltimoTermo  = "";

/* ============================================================
   NAVEGAÇÃO DE ABAS
   ============================================================ */
function showTab(id, el) {
   document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
   document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
   el.classList.add('active');
   document.getElementById(id).classList.add('active');

   if (id === 'lista')      listar(1);
   if (id === 'relatorio')  gerarRelatorio();
}

/* ============================================================
   UTILITÁRIOS
   ============================================================ */
function mostrarMsg(idEl, texto, tipo) {
   const el = document.getElementById(idEl);
   el.className = 'msg ' + tipo;
   el.innerText = texto;
   if (tipo === 'ok') {
      setTimeout(() => { el.className = 'msg'; }, 4000);
   }
}

function limparForm() {
   document.getElementById('cad_nome').value  = '';
   document.getElementById('cad_email').value = '';
   document.getElementById('cad_nome').focus();
}

function escapeHtml(s) {
   if (s == null) return '';
   return String(s)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
}

/* ============================================================
   CADASTRO
   ============================================================ */
async function salvar() {
   const nome  = document.getElementById('cad_nome').value.trim();
   const email = document.getElementById('cad_email').value.trim();

   if (!nome) {
      mostrarMsg('cad_msg', 'O campo Nome é obrigatório.', 'erro');
      return;
   }

   try {
      const obj = await Executar("cliente.gravar", { nome: nome, email: email });

      if (obj.ok) {
         mostrarMsg('cad_msg', 'Cliente gravado com sucesso! RECNO = ' + obj.recno, 'ok');
         limparForm();
      } else {
         mostrarMsg('cad_msg', obj.msg || 'Erro ao gravar.', 'erro');
      }
   } catch (e) {
      mostrarMsg('cad_msg', 'Erro: ' + e.message, 'erro');
   }
}

/* ============================================================
   LISTA (com paginação)
   ============================================================ */
async function listar(pagina) {
   if (pagina !== undefined) listaPaginaAtual = pagina;
   if (listaPaginaAtual < 1) listaPaginaAtual = 1;

   const porPagina = parseInt(document.getElementById('lista_por_pagina').value);

   try {
      const obj = await Executar("cliente.listar", {
         pagina:     listaPaginaAtual,
         por_pagina: porPagina
      });

      const tb = document.getElementById('lista_tbody');
      tb.innerHTML = '';

      if (!obj.ok || !obj.clientes || obj.clientes.length === 0) {
         tb.innerHTML = '<tr><td colspan="4" class="empty">Nenhum cliente cadastrado</td></tr>';
         document.getElementById('lista_info').innerText = "0 registros";
         listaTotalPaginas = 1;
         return;
      }

      listaTotalPaginas = obj.total_pag;
      listaPaginaAtual  = obj.pagina;

      obj.clientes.forEach(c => {
         const tr = document.createElement('tr');
         if (c.deleted) tr.className = 'row-deleted';

         const btn = c.deleted
            ? '<button class="restore" onclick="toggleApagar(' + c.recno + ')">Restaurar</button>'
            : '<button class="danger"  onclick="toggleApagar(' + c.recno + ')">Apagar</button>';
         const del = c.deleted ? '<span class="del-label">DEL</span>' : '';

         tr.innerHTML =
            '<td>' + c.recno + '</td>' +
            '<td>' + escapeHtml(c.nome) + '</td>' +
            '<td>' + escapeHtml(c.email) + '</td>' +
            '<td>' + btn + del + '</td>';
         tb.appendChild(tr);
      });

      document.getElementById('lista_info').innerText =
         "Página " + listaPaginaAtual + " de " + listaTotalPaginas +
         "  (" + obj.total + " registros)";

   } catch (e) {
      alert('Erro ao listar: ' + e.message);
   }
}

function listaPrimeira() { listar(1); }
function listaUltima()   { listar(listaTotalPaginas); }
function listaProxima()  { if (listaPaginaAtual < listaTotalPaginas) listar(listaPaginaAtual + 1); }
function listaAnterior() { if (listaPaginaAtual > 1) listar(listaPaginaAtual - 1); }
function listaMudarPorPagina() { listar(1); }

/* ============================================================
   APAGAR / RESTAURAR
   ============================================================ */
async function toggleApagar(recno) {
   try {
      const obj = await Executar("cliente.apagar", { recno: recno });
      if (obj.ok) {
         listar();
      } else {
         alert('Erro: ' + (obj.msg || 'Não foi possível alterar o registro.'));
      }
   } catch (e) {
      alert('Erro: ' + e.message);
   }
}

/* ============================================================
   BUSCA (com paginação)
   ============================================================ */
async function buscar(pagina) {
   if (pagina !== undefined) buscaPaginaAtual = pagina;
   if (buscaPaginaAtual < 1) buscaPaginaAtual = 1;

   const nome = document.getElementById('busca_nome').value.trim();
   const porPagina = parseInt(document.getElementById('busca_por_pagina').value);

   if (nome !== buscaUltimoTermo) {
      buscaPaginaAtual = 1;
      buscaUltimoTermo = nome;
   }

   const tb = document.getElementById('busca_tbody');
   tb.innerHTML = '';

   if (!nome) {
      tb.innerHTML = '<tr><td colspan="4" class="empty">Digite um nome para buscar</td></tr>';
      document.getElementById('busca_info').innerText = "—";
      buscaTotalPaginas = 1;
      return;
   }

   try {
      const obj = await Executar("cliente.buscar", {
         nome:       nome,
         pagina:     buscaPaginaAtual,
         por_pagina: porPagina
      });

      if (!obj.ok || !obj.clientes || obj.clientes.length === 0) {
         tb.innerHTML = '<tr><td colspan="4" class="empty">Nenhum resultado encontrado</td></tr>';
         document.getElementById('busca_info').innerText = "0 registros";
         buscaTotalPaginas = 1;
         return;
      }

      buscaTotalPaginas = obj.total_pag;
      buscaPaginaAtual  = obj.pagina;

      obj.clientes.forEach(c => {
         const tr = document.createElement('tr');
         if (c.deleted) tr.className = 'row-deleted';

         const status = c.deleted
            ? '<span class="del-label">DEL</span>'
            : '<span style="color:#27ae60;font-weight:600;font-size:12px;">ATIVO</span>';

         tr.innerHTML =
            '<td>' + c.recno + '</td>' +
            '<td>' + escapeHtml(c.nome) + '</td>' +
            '<td>' + escapeHtml(c.email) + '</td>' +
            '<td>' + status + '</td>';
         tb.appendChild(tr);
      });

      document.getElementById('busca_info').innerText =
         "Página " + buscaPaginaAtual + " de " + buscaTotalPaginas +
         "  (" + obj.total + " encontrados)";

   } catch (e) {
      alert('Erro ao buscar: ' + e.message);
   }
}

function buscaPrimeira() { buscar(1); }
function buscaUltima()   { buscar(buscaTotalPaginas); }
function buscaProxima()  { if (buscaPaginaAtual < buscaTotalPaginas) buscar(buscaPaginaAtual + 1); }
function buscaAnterior() { if (buscaPaginaAtual > 1) buscar(buscaPaginaAtual - 1); }
function buscaMudarPorPagina() { buscar(1); }

/* ============================================================
   RELATÓRIO — usa o comando "cliente.resumo" no Harbour
   ============================================================ */
async function gerarRelatorio() {
   try {
      const obj = await Executar("cliente.resumo", {});

      if (!obj.ok) {
         document.getElementById('rel_total').innerText     = '0';
         document.getElementById('rel_ultimo').innerText    = '0';
         document.getElementById('rel_com_email').innerText = '0';
         document.getElementById('rel_deletados').innerText = '0';
         return;
      }

      document.getElementById('rel_total').innerText     = obj.ativos;
      document.getElementById('rel_ultimo').innerText    = obj.ultimo_recno;
      document.getElementById('rel_com_email').innerText = obj.com_email;
      document.getElementById('rel_deletados').innerText = obj.deletados;

   } catch (e) {
      alert('Erro ao gerar relatório: ' + e.message);
   }
}

/* ============================================================
   ATALHOS DE TECLADO
   ============================================================ */
document.getElementById('cad_email').addEventListener('keypress', e => {
   if (e.key === 'Enter') salvar();
});

document.getElementById('busca_nome').addEventListener('keypress', e => {
   if (e.key === 'Enter') buscar(1);
});

/* ============================================================
   INICIALIZAÇÃO
   ============================================================ */
window.addEventListener('load', () => {
   document.getElementById('cad_nome').focus();
   ativarDrag();
});

/* ============================================================
   ABAS ARRASTÁVEIS
   ============================================================ */
let dragSrcEl = null;

function handleDragStart(e) {
   dragSrcEl = this;
   e.dataTransfer.effectAllowed = 'move';
   e.dataTransfer.setData('text/html', this.outerHTML);
   this.classList.add('dragging');
}

function handleDragOver(e) {
   e.preventDefault();
   e.dataTransfer.dropEffect = 'move';
   return false;
}

function handleDragEnter(e) {
   if (this !== dragSrcEl) this.classList.add('drop-target');
}

function handleDragLeave(e) {
   this.classList.remove('drop-target');
}

function handleDrop(e) {
   e.stopPropagation();
   e.preventDefault();

   if (dragSrcEl !== this) {
      const tabs = Array.from(document.querySelectorAll('.tab'));
      const idxSrc  = tabs.indexOf(dragSrcEl);
      const idxDest = tabs.indexOf(this);

      if (idxSrc < idxDest) {
         this.parentNode.insertBefore(dragSrcEl, this.nextSibling);
      } else {
         this.parentNode.insertBefore(dragSrcEl, this);
      }
   }
   this.classList.remove('drop-target');
   return false;
}

function handleDragEnd(e) {
   this.classList.remove('dragging');
   document.querySelectorAll('.tab').forEach(t => t.classList.remove('drop-target'));
}

function ativarDrag() {
   document.querySelectorAll('.tab').forEach(tab => {
      tab.setAttribute('draggable', 'true');
      tab.addEventListener('dragstart', handleDragStart);
      tab.addEventListener('dragover',  handleDragOver);
      tab.addEventListener('dragenter', handleDragEnter);
      tab.addEventListener('dragleave', handleDragLeave);
      tab.addEventListener('drop',      handleDrop);
      tab.addEventListener('dragend',   handleDragEnd);
   });
}

</script>
</body>
</html>
Saudações,
Itamar M. Lins Jr.
Responder