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.
