Construindo um Dashboard ERP em Tempo Real com sgcHTML e Delphi

· Aplicações

Sistemas ERP são o coração do desenvolvimento em Delphi: clientes, pedidos, faturas, níveis de estoque, execuções de produção. A lógica de negócios já vive em Object Pascal. O banco de dados está conectado. Os relatórios funcionam. O que frequentemente falta é um dashboard acessível pelo navegador que a gerência possa abrir em um tablet sem instalar nada. Este artigo mostra como construir exatamente isso com o sgcHTML, partindo de um cenário realista: um ERP de vendas com valores de receita ao vivo, uma grade de faturas, um quadro Kanban de entregas e atualizações em tempo real quando novos pedidos chegam.

Todo o código compila no Delphi 10.4 Sydney e versões posteriores. O resultado final é um único .exe que serve o dashboard na porta 8080, com estilo Bootstrap 5, layout responsivo e atualizações ao vivo via WebSocket, sem nenhum arquivo JavaScript para implantar junto ao binário.

Configurando o servidor

O sgcHTML envolve um TsgcWSHTTPServer do sgcWebSockets. Adicione o TsgcHTMLEngine_Server ao módulo de dados ao lado do servidor HTTP e conecte-os. O motor intercepta automaticamente as requisições de ativos estáticos (Bootstrap CSS/JS, Chart.js, htmx); todo o resto vai para o seu handler OnCommandGet.

uses
  sgcWebSocket_Server, sgcHTML_Engine_Server,
  sgcHTML_Template_Bootstrap, sgcHTML_Page;

type
  TERP_Server = class(TDataModule)
    HTTPServer:    TsgcWSHTTPServer;
    HTMLEngine:    TsgcHTMLEngine_Server;
  private
    procedure HandleGet(AContext: TIdContext;
      AReq: TIdHTTPRequestInfo; AResp: TIdHTTPResponseInfo);
  public
    procedure Start(aPort: Integer);
  end;

procedure TERP_Server.Start(aPort: Integer);
begin
  HTTPServer.Port := aPort;
  HTMLEngine.Server := HTTPServer;
  HTTPServer.OnCommandGet := HandleGet;
  HTTPServer.Active := True;
end;

procedure TERP_Server.HandleGet(AContext: TIdContext;
  AReq: TIdHTTPRequestInfo; AResp: TIdHTTPResponseInfo);
begin
  if AReq.Document = '/' then
    ServeDashboard(AResp)
  else if AReq.Document = '/invoices' then
    ServeInvoices(AResp)
  else if AReq.Document = '/kanban' then
    ServeKanban(AResp)
  else
    AResp.ResponseNo := 404;
end;

Cartões de KPI

A primeira linha do dashboard exibe quatro cartões de KPI: receita total, faturas em aberto, quantidade de faturas vencidas e taxa de entrega no prazo. O TsgcHTMLComponent_StatCard cuida de toda a apresentação visual: gradiente de fundo, título, valor em destaque, seta de tendência e rótulo de rodapé opcional.

uses
  sgcHTML_Component_StatCard;

procedure TERP_Server.BuildKPIRow(aPage: TsgcHTMLPage; aDB: TFDConnection);

  function MakeCard(aPage: TsgcHTMLPage; const aTitle, aValue: string;
    aColor: TsgcHTMLStatColor; aTrend: TsgcHTMLStatTrend;
    const aTrendValue, aFooter: string; aOrder: Integer): TsgcHTMLComponent_StatCard;
  begin
    Result := TsgcHTMLComponent_StatCard.Create(nil);
    Result.PageBuilder  := aPage.PageBuilder;
    Result.Section      := 'kpi';
    Result.SectionOrder := aOrder;
    Result.ColumnWidth  := cw3;
    Result.Title        := aTitle;
    Result.Value        := aValue;
    Result.Color        := aColor;
    Result.Trend        := aTrend;
    Result.TrendValue   := aTrendValue;
    Result.FooterText   := aFooter;
    Result.Gradient     := sgBlueViolet;
  end;

var
  oQ: TFDQuery;
begin
  oQ := TFDQuery.Create(nil);
  try
    oQ.Connection := aDB;

    oQ.SQL.Text := 'SELECT SUM(total) FROM invoices WHERE YEAR(issued)=YEAR(NOW())';
    oQ.Open;
    MakeCard(aPage, 'YTD Revenue', FormatCurr('$#,##0', oQ.Fields[0].AsFloat),
      scPrimary, stUp, '+14%', 'vs last year', 1);
    oQ.Close;

    oQ.SQL.Text := 'SELECT COUNT(*) FROM invoices WHERE status=''open''';
    oQ.Open;
    MakeCard(aPage, 'Open Invoices', oQ.Fields[0].AsString,
      scInfo, stNeutral, '', 'awaiting payment', 2);
    oQ.Close;

    oQ.SQL.Text := 'SELECT COUNT(*) FROM invoices WHERE status=''overdue''';
    oQ.Open;
    MakeCard(aPage, 'Overdue', oQ.Fields[0].AsString,
      scDanger, stDown, '-3', 'vs last month', 3);
    oQ.Close;

    oQ.SQL.Text := 'SELECT ROUND(100.0*SUM(on_time)/COUNT(*),1) FROM deliveries';
    oQ.Open;
    MakeCard(aPage, 'On-Time Delivery', oQ.Fields[0].AsString + '%',
      scSuccess, stUp, '+2pp', 'last 30 days', 4);
    oQ.Close;
  finally
    oQ.Free;
  end;
end;

Gráfico de tendência de receita

O gráfico de receita ocupa a segunda linha. O TsgcHTMLComponent_Chart aceita arrays de rótulos e conjuntos de dados com cores de borda e preenchimento. O LoadFromDataSet mapeia o resultado de uma consulta para os dados do gráfico com uma única chamada: passe o dataset, o campo de rótulo (nome do mês) e um ou mais campos de valor (receita, custo).

uses
  sgcHTML_Component_Chart;

procedure TERP_Server.BuildRevenueChart(aPage: TsgcHTMLPage; aDB: TFDConnection);
var
  oChart: TsgcHTMLComponent_Chart;
  oQ:     TFDQuery;
begin
  oChart := TsgcHTMLComponent_Chart.Create(nil);
  oChart.PageBuilder  := aPage.PageBuilder;
  oChart.Section      := 'charts';
  oChart.SectionTitle := 'Revenue';
  oChart.SectionOrder := 1;
  oChart.ColumnWidth  := cw8;
  oChart.ChartType    := ctBar;
  oChart.Title        := 'Monthly Revenue vs Cost (last 12 months)';
  oChart.ShowLegend   := True;
  oChart.Stacked      := False;
  oChart.Responsive   := True;

  oQ := TFDQuery.Create(nil);
  try
    oQ.Connection := aDB;
    oQ.SQL.Text   :=
      'SELECT DATE_FORMAT(issued,''%b'') AS month, ' +
      '       SUM(total) AS revenue, ' +
      '       SUM(cost)  AS cost ' +
      'FROM invoices ' +
      'WHERE issued >= DATE_SUB(NOW(), INTERVAL 12 MONTH) ' +
      'GROUP BY YEAR(issued), MONTH(issued) ' +
      'ORDER BY YEAR(issued), MONTH(issued)';
    oQ.Open;
    // One call maps the query to labels + two datasets
    oChart.LoadFromDataSet(oQ, 'month', ['revenue', 'cost']);
  finally
    oQ.Free;
  end;
end;

Tabela de faturas paginada

O TsgcHTMLComponent_DataTable combina um TsgcHTMLComponent_Grid e um TsgcHTMLComponent_Pagination em um único widget com caixa de pesquisa integrada, rótulo de contagem de linhas, botão de exportação opcional e tamanhos de página configuráveis. O LoadFromDataSet lê os nomes e tipos das colunas automaticamente; você pode substituir os rótulos do cabeçalho e as larguras de coluna depois através de Grid.Columns.

uses
  sgcHTML_Component_DataTable;

procedure TERP_Server.BuildInvoiceTable(aPage: TsgcHTMLPage; aDB: TFDConnection);
var
  oTable: TsgcHTMLComponent_DataTable;
  oQ:     TFDQuery;
begin
  oTable := TsgcHTMLComponent_DataTable.Create(nil);
  oTable.PageBuilder    := aPage.PageBuilder;
  oTable.Section        := 'invoices';
  oTable.SectionTitle   := 'Invoices';
  oTable.SectionOrder   := 1;
  oTable.ColumnWidth    := cw12;
  oTable.Title          := 'Recent Invoices';
  oTable.ShowSearch     := True;
  oTable.ShowExport     := True;
  oTable.ShowPageSize   := True;
  oTable.PageSizes      := '10,25,50';

  oQ := TFDQuery.Create(nil);
  try
    oQ.Connection := aDB;
    oQ.SQL.Text   :=
      'SELECT number, customer, issued, due, total, status ' +
      'FROM invoices ' +
      'ORDER BY issued DESC ' +
      'LIMIT 200';
    oQ.Open;
    oTable.LoadFromDataSet(oQ, 20);
  finally
    oQ.Free;
  end;
end;

Quadro Kanban para atendimento de pedidos

O TsgcHTMLComponent_KanbanBoard representa um quadro com múltiplas colunas e suporte a arrastar e soltar. Cada coluna contém uma coleção de cartões; cada cartão possui título, descrição opcional, responsável, tag e cor. Em um contexto de ERP, as colunas se mapeiam naturalmente aos estados dos pedidos: Recebido, Separação, Embalado, Enviado.

uses
  sgcHTML_Component_KanbanBoard;

procedure TERP_Server.BuildKanban(aPage: TsgcHTMLPage; aDB: TFDConnection);
var
  oBoard:  TsgcHTMLComponent_KanbanBoard;
  oCol:    TsgcHTMLKanbanColumn;
  oQ:      TFDQuery;
  vStatus: string;
begin
  oBoard := TsgcHTMLComponent_KanbanBoard.Create(nil);
  oBoard.PageBuilder  := aPage.PageBuilder;
  oBoard.Section      := 'kanban';
  oBoard.SectionTitle := 'Order Fulfilment';
  oBoard.SectionOrder := 1;
  oBoard.ColumnWidth  := cw12;

  // Create the four columns
  for vStatus in ['Received', 'Picking', 'Packed', 'Shipped'] do
  begin
    oCol := oBoard.Columns.Add;
    oCol.Title := vStatus;
    oCol.Color := hcLight;
  end;

  oQ := TFDQuery.Create(nil);
  try
    oQ.Connection := aDB;
    oQ.SQL.Text   :=
      'SELECT order_no, customer, qty_total, assigned_to, status ' +
      'FROM orders ' +
      'WHERE status IN (''Received'',''Picking'',''Packed'',''Shipped'') ' +
      'ORDER BY created DESC LIMIT 100';
    oQ.Open;
    while not oQ.Eof do
    begin
      // Find the column whose title matches the order status
      oCol := oBoard.Columns.FindByTitle(oQ.FieldByName('status').AsString);
      if Assigned(oCol) then
        oCol.AddCard(
          oQ.FieldByName('order_no').AsString,
          oQ.FieldByName('customer').AsString + ' — ' +
            oQ.FieldByName('qty_total').AsString + ' items',
          hcLight,
          oQ.FieldByName('assigned_to').AsString
        );
      oQ.Next;
    end;
  finally
    oQ.Free;
  end;
end;

Montando a página completa

As quatro seções são montadas por um único procedimento ServeDashboard que cria uma página, chama cada construtor, envolve o resultado em um template Bootstrap e grava na resposta:

procedure TERP_Server.ServeDashboard(AResp: TIdHTTPResponseInfo);
var
  oPage:     TsgcHTMLPage;
  oTemplate: TsgcHTMLTemplate_Bootstrap;
begin
  oPage     := TsgcHTMLPage.Create(nil);
  oTemplate := TsgcHTMLTemplate_Bootstrap.Create(nil);
  try
    BuildKPIRow(oPage, FDB);
    BuildRevenueChart(oPage, FDB);
    BuildInvoiceTable(oPage, FDB);
    BuildKanban(oPage, FDB);

    oTemplate.Title       := 'ERP Dashboard';
    oTemplate.Page        := oPage;
    oTemplate.DarkMode    := False;
    oTemplate.BodyClass   := 'bg-light';

    AResp.ContentType := 'text/html; charset=utf-8';
    AResp.ContentText := oTemplate.GetHTML;
    AResp.ResponseNo  := 200;
  finally
    oPage.Free;
    oTemplate.Free;
  end;
end;

Atualizações ao vivo via WebSocket

O dashboard seria estático sem dados ao vivo. O sgcHTML é construído sobre o sgcWebSockets, portanto enviar uma atualização de fragmento quando um novo pedido é registrado é uma única chamada de broadcast. No lado do cliente, o cartão de KPI possui um atributo id e a extensão WebSocket do htmx substitui seu conteúdo quando uma mensagem chega.

// Called from the order-processing thread when a new order is saved
procedure TERP_Server.OnNewOrderSaved(aOrder: TERPOrder);
var
  oCard: TsgcHTMLComponent_StatCard;
begin
  // Rebuild the "Open Invoices" KPI card with the updated count
  oCard := TsgcHTMLComponent_StatCard.Create(nil);
  try
    oCard.CardID  := 'kpi-open-invoices';
    oCard.Title   := 'Open Invoices';
    oCard.Value   := IntToStr(FDB.OpenInvoiceCount);
    oCard.Color   := scInfo;
    oCard.Trend   := stUp;
    // Broadcast the new card HTML to all connected dashboard clients
    HTTPServer.Broadcast(oCard.HTML, '/dashboard');
  finally
    oCard.Free;
  end;
end;

O fragmento substitui o cartão correspondente no navegador imediatamente, sem recarregar a página e sem que o usuário precise clicar em nada. Cada cliente conectado vê a atualização em milissegundos.

O que você obtém ao final

O exemplo completo é uma única unit Delphi e um módulo de dados. Compile e execute: um dashboard ERP acessível pelo navegador aparece na porta configurada, com quatro cartões de KPI, um gráfico de barras de receita versus custo dos últimos 12 meses, uma tabela de faturas pesquisável e paginada e um quadro Kanban com múltiplas colunas para atendimento de pedidos. Quando novos pedidos são registrados, os cartões de KPI atualizam em tempo real em todas as abas do navegador conectadas. Total de código front-end escrito: zero linhas.

O mesmo padrão funciona para qualquer aplicação de negócios em Delphi: gestão de estoque, programação de produção, sistemas de RH, rastreamento de frotas. Se os dados estão em um TDataSet, podem estar em um dashboard web ao vivo em uma manhã.

Baixe a versão de avaliação gratuita do sgcHTML em esegece.com/products/sgchtml/download e explore os aplicativos de demonstração incluídos: ERP Demo, Admin Console, Live Monitor e Customer Portal, para um exemplo completo e funcional.

Dúvidas? Entre em contato. Você receberá uma resposta das pessoas que escreveram o código.