Ein Echtzeit-ERP-Dashboard mit sgcHTML und Delphi erstellen

· Anwendungen

ERP-Systeme sind das Kerngebiet der Delphi-Entwicklung: Kunden, Bestellungen, Rechnungen, Lagerbestände, Produktionsläufe. Die Geschäftslogik lebt bereits in Object Pascal. Die Datenbank ist eingebunden. Die Berichte laufen. Was oft fehlt, ist ein browserbasiertes Dashboard, das das Management auf einem Tablet öffnen kann, ohne etwas installieren zu müssen. Dieser Beitrag zeigt, wie man genau das mit sgcHTML erstellt, ausgehend von einem realistischen Szenario: einem Vertriebs-ERP mit Live-Umsatzzahlen, einem Rechnungsraster, einem Lieferungs-Kanban-Board und Echtzeit-Push bei neuen Bestellungen.

Der gesamte Code kompiliert in Delphi 10.4 Sydney und höher. Das fertige Ergebnis ist eine einzelne .exe, die das Dashboard auf Port 8080 bereitstellt, mit Bootstrap 5-Styling, einem responsiven Layout und WebSocket-Live-Updates, ohne dass JavaScript-Dateien neben dem Binary deployt werden müssen.

Den Server einrichten

sgcHTML umhüllt einen TsgcWSHTTPServer aus sgcWebSockets. Platzieren Sie TsgcHTMLEngine_Server auf dem Datenmodul neben dem HTTP-Server und verbinden Sie beide. Die Engine fängt Anfragen nach statischen Assets (Bootstrap CSS/JS, Chart.js, htmx) automatisch ab; alles andere wird an Ihren OnCommandGet-Handler weitergeleitet.

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;

KPI-Stat-Karten

Die oberste Zeile des Dashboards zeigt vier KPI-Karten: Gesamtumsatz, offene Rechnungen, überfällige Anzahl und pünktliche Lieferquote. TsgcHTMLComponent_StatCard übernimmt die gesamte visuelle Darstellung: Hintergrundverlauf, Titel, großer Wert, Trendpfeil und optionale Fußzeilenbeschriftung.

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;

Umsatz-Trenddiagramm

Das Umsatzdiagramm erstreckt sich über die zweite Zeile. TsgcHTMLComponent_Chart akzeptiert Label-Arrays und Datensätze mit Rahmen- und Füllfarben. LoadFromDataSet überführt ein Abfrageergebnis mit einem einzigen Aufruf in Diagrammdaten: Übergeben Sie den Datensatz, das Label-Feld (Monatsname) und ein oder mehrere Wertefelder (Umsatz, Kosten).

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;

Seitennummerierte Rechnungstabelle

TsgcHTMLComponent_DataTable kombiniert ein TsgcHTMLComponent_Grid und eine TsgcHTMLComponent_Pagination zu einem Widget mit integriertem Suchfeld, einer Zeilenanzahl-Beschriftung, einem optionalen Export-Button und konfigurierbaren Seitengrößen. LoadFromDataSet liest Spaltennamen und -typen automatisch aus; Sie können danach Kopfzeilenbeschriftungen und Spaltenbreiten über Grid.Columns überschreiben.

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;

Kanban-Board für die Auftragsabwicklung

TsgcHTMLComponent_KanbanBoard stellt ein mehrspaliges, drag-freundliches Board dar. Jede Spalte enthält eine Sammlung von Karten; jede Karte hat einen Titel, eine optionale Beschreibung, einen Verantwortlichen, ein Tag und eine Farbe. In einem ERP-Kontext entsprechen die Spalten natürlich den Auftragsstatus: Empfangen, Kommissionierung, Verpackt, Versendet.

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;

Die vollständige Seite zusammensetzen

Die vier Abschnitte werden durch eine einzige ServeDashboard-Prozedur zusammengesetzt, die eine Seite erstellt, jeden Builder aufruft, das Ergebnis in ein Bootstrap-Template einbettet und es in die Antwort schreibt:

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;

Live-Updates über WebSocket

Das Dashboard wäre ohne Live-Daten statisch. sgcHTML baut auf sgcWebSockets auf, daher ist das Pushen eines Fragment-Updates bei einer neuen Bestellung ein einziger Broadcast-Aufruf. Auf der Client-Seite trägt die Stat-Karte ein id-Attribut, und die htmx-WebSocket-Erweiterung ersetzt ihren Inhalt, wenn eine Nachricht eintrifft.

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

Das Fragment ersetzt die passende Karte im Browser sofort, ohne Seitenreload und ohne dass der Benutzer etwas anklicken muss. Jeder verbundene Client sieht die Aktualisierung innerhalb von Millisekunden.

Was Sie am Ende erhalten

Das vollständige Beispiel besteht aus einer einzelnen Delphi-Unit und einem Datenmodul. Kompilieren und starten: Ein browserbasiertes ERP-Dashboard erscheint auf dem konfigurierten Port mit vier KPI-Karten, einem 12-Monats-Balkendiagramm für Umsatz vs. Kosten, einer durchsuchbaren und seitennummerierten Rechnungstabelle und einem mehrspaltigem Kanban-Board für die Auftragsabwicklung. Wenn neue Bestellungen eingehen, aktualisieren sich die KPI-Karten in Echtzeit in allen verbundenen Browser-Tabs. Die Gesamtmenge des von Ihnen geschriebenen Frontend-Codes: null Zeilen.

Das gleiche Muster funktioniert für jede Delphi-Geschäftsanwendung: Lagerverwaltung, Produktionsplanung, HR-Systeme, Flottenüberwachung. Wenn die Daten in einem TDataSet vorhanden sind, können sie an einem Vormittag auf einem Live-Web-Dashboard verfügbar sein.

Laden Sie die kostenlose Testversion von sgcHTML unter esegece.com/products/sgchtml/download herunter und durchsuchen Sie die enthaltenen ERP-Demo-, Admin-Konsolen-, Live-Monitor- und Kundenportal-Anwendungen für ein vollständiges funktionierendes Beispiel.

Fragen? Nehmen Sie Kontakt auf. Sie erhalten eine Antwort von den Personen, die den Code geschrieben haben.