Creando un Panel ERP en Tiempo Real con sgcHTML y Delphi

· Aplicaciones

Los sistemas ERP son el territorio natural del desarrollo en Delphi: clientes, pedidos, facturas, niveles de stock, producción. La lógica de negocio ya vive en Object Pascal. La base de datos está conectada. Los informes funcionan. Lo que suele faltar es un panel accesible desde el navegador que la dirección pueda abrir en una tableta sin instalar nada. Esta entrada muestra cómo construir exactamente eso con sgcHTML, a partir de un escenario realista: un ERP de ventas con cifras de ingresos en vivo, una rejilla de facturas, un tablero Kanban de entregas y actualizaciones en tiempo real cuando llegan nuevos pedidos.

Todo el código compila en Delphi 10.4 Sydney y versiones posteriores. El resultado final es un único .exe que sirve el panel en el puerto 8080, con estilos Bootstrap 5, diseño responsive y actualizaciones en vivo por WebSocket, sin ningún archivo JavaScript que desplegar junto al binario.

Configurando el servidor

sgcHTML envuelve un TsgcWSHTTPServer de sgcWebSockets. Coloca TsgcHTMLEngine_Server en el módulo de datos junto al servidor HTTP y conéctalos. El motor intercepta automáticamente las peticiones de recursos estáticos (CSS/JS de Bootstrap, Chart.js, htmx); todo lo demás pasa a tu manejador 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;

Tarjetas KPI

La fila superior del panel muestra cuatro tarjetas KPI: ingresos totales, facturas abiertas, número de vencidas y tasa de entrega a tiempo. TsgcHTMLComponent_StatCard gestiona todo el aspecto visual: degradado de fondo, título, valor grande, flecha de tendencia y etiqueta de pie 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áfica de tendencia de ingresos

La gráfica de ingresos ocupa la segunda fila. TsgcHTMLComponent_Chart acepta arrays de etiquetas y conjuntos de datos con colores de borde y relleno. LoadFromDataSet mapea el resultado de una consulta a datos del gráfico con una sola llamada: pasa el dataset, el campo de etiqueta (nombre del mes) y uno o más campos de valor (ingresos, costes).

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;

Tabla de facturas paginada

TsgcHTMLComponent_DataTable combina un TsgcHTMLComponent_Grid y un TsgcHTMLComponent_Pagination en un único widget con cuadro de búsqueda integrado, etiqueta de número de filas, botón de exportación opcional y tamaños de página configurables. LoadFromDataSet lee los nombres y tipos de columna automáticamente; después puedes modificar las cabeceras y anchos de columna a travé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;

Tablero Kanban para la gestión de pedidos

TsgcHTMLComponent_KanbanBoard representa un tablero de varias columnas con soporte para arrastre. Cada columna contiene una colección de tarjetas; cada tarjeta incluye un título, una descripción opcional, un responsable, una etiqueta y un color. En un contexto ERP las columnas se corresponden de forma natural con los estados del pedido: Recibido, Preparando, Empaquetado, 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;

Ensamblando la página completa

Las cuatro secciones se ensamblan mediante un único procedimiento ServeDashboard que crea una página, llama a cada constructor, envuelve el resultado en una plantilla Bootstrap y lo escribe en la respuesta:

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;

Actualizaciones en vivo por WebSocket

El panel sería estático sin datos en tiempo real. sgcHTML se apoya en sgcWebSockets, por lo que enviar una actualización de fragmento cuando se registra un nuevo pedido es una única llamada de difusión. En el lado del cliente, la tarjeta de estadísticas lleva un atributo id y la extensión WebSocket de htmx reemplaza su contenido cuando llega un mensaje.

// 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;

El fragmento reemplaza la tarjeta correspondiente en el navegador de inmediato, sin recargar la página y sin que el usuario tenga que hacer nada. Cada cliente conectado recibe la actualización en cuestión de milisegundos.

Qué obtienes al final

El ejemplo completo es una única unidad Delphi y un módulo de datos. Compila y ejecuta: aparece un panel ERP accesible desde el navegador en el puerto configurado, con cuatro tarjetas KPI, una gráfica de barras de ingresos frente a costes de 12 meses, una tabla de facturas con búsqueda y paginación, y un tablero Kanban de varias columnas para la gestión de pedidos. Cuando se registran nuevos pedidos, las tarjetas KPI se actualizan en tiempo real en todas las pestañas del navegador conectadas. La cantidad total de código de front-end que has escrito: cero líneas.

El mismo patrón funciona con cualquier aplicación de negocio en Delphi: gestión de inventario, planificación de producción, sistemas de RRHH, seguimiento de flotas. Si los datos están en un TDataSet, pueden estar en un panel web en vivo en una mañana.

Descarga la prueba gratuita de sgcHTML desde esegece.com/products/sgchtml/download y explora las aplicaciones de demostración incluidas: ERP Demo, Admin Console, Live Monitor y Customer Portal, para un ejemplo completo y funcional.

¿Preguntas? Contáctanos. Recibirás respuesta de las personas que escribieron el código.