Créer un tableau de bord ERP en temps réel avec sgcHTML et Delphi

· Applications

Les systèmes ERP sont au cœur du développement Delphi : clients, commandes, factures, niveaux de stock, cycles de production. La logique métier est déjà écrite en Object Pascal. La base de données est connectée. Les rapports fonctionnent. Ce qui manque souvent, c'est un tableau de bord accessible depuis un navigateur que la direction peut ouvrir sur une tablette sans rien installer. Cet article montre comment construire exactement cela avec sgcHTML, à partir d'un scénario réaliste : un ERP de vente avec des chiffres de revenus en direct, une grille de factures, un tableau Kanban de livraison et des mises à jour en temps réel lorsque de nouvelles commandes arrivent.

Tout le code compile dans Delphi 10.4 Sydney et les versions ultérieures. Le résultat final est un seul fichier .exe qui sert le tableau de bord sur le port 8080, avec le style Bootstrap 5, une mise en page responsive et des mises à jour en direct via WebSocket, sans aucun fichier JavaScript à déployer à côté du binaire.

Configuration du serveur

sgcHTML encapsule un TsgcWSHTTPServer de sgcWebSockets. Déposez TsgcHTMLEngine_Server sur le module de données aux côtés du serveur HTTP et connectez-les ensemble. Le moteur intercepte automatiquement les requêtes de ressources statiques (Bootstrap CSS/JS, Chart.js, htmx) ; tout le reste est transmis à votre gestionnaire 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;

Cartes KPI

La première ligne du tableau de bord affiche quatre cartes KPI : revenu total, factures ouvertes, nombre de factures en retard et taux de livraison à temps. TsgcHTMLComponent_StatCard gère l'ensemble du visuel : dégradé d'arrière-plan, titre, grande valeur, flèche de tendance et étiquette de pied de page optionnelle.

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;

Graphique de tendance des revenus

Le graphique des revenus occupe la deuxième ligne. TsgcHTMLComponent_Chart accepte des tableaux d'étiquettes et des jeux de données avec des couleurs de bordure et de remplissage. LoadFromDataSet mappe le résultat d'une requête vers les données du graphique en un seul appel : passez le dataset, le champ d'étiquette (nom du mois) et un ou plusieurs champs de valeur (revenus, coûts).

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;

Tableau de factures paginé

TsgcHTMLComponent_DataTable compose un TsgcHTMLComponent_Grid et un TsgcHTMLComponent_Pagination en un seul widget avec une zone de recherche intégrée, une étiquette de nombre de lignes, un bouton d'export optionnel et des tailles de page configurables. LoadFromDataSet lit automatiquement les noms et types de colonnes ; vous pouvez ensuite remplacer les étiquettes d'en-tête et les largeurs de colonnes via 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;

Tableau Kanban pour le traitement des commandes

TsgcHTMLComponent_KanbanBoard représente un tableau multi-colonnes avec glisser-déposer. Chaque colonne contient une collection de cartes ; chaque carte comporte un titre, une description optionnelle, un responsable, une étiquette et une couleur. Dans un contexte ERP, les colonnes correspondent naturellement aux états des commandes : Reçue, Préparation, Emballée, Expédiée.

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;

Assemblage de la page complète

Les quatre sections sont assemblées par une unique procédure ServeDashboard qui crée une page, appelle chaque constructeur, enveloppe le résultat dans un modèle Bootstrap et l'écrit dans la réponse :

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;

Mises à jour en direct via WebSocket

Le tableau de bord serait statique sans données en direct. sgcHTML repose sur sgcWebSockets, de sorte que l'envoi d'une mise à jour de fragment lors de la réception d'une nouvelle commande se fait par un seul appel de diffusion. Côté client, la carte de statistique porte un attribut id et l'extension WebSocket de htmx remplace son contenu dès qu'un message arrive.

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

Le fragment remplace la carte correspondante dans le navigateur immédiatement, sans rechargement de page et sans que l'utilisateur ait à cliquer. Chaque client connecté voit la mise à jour en quelques millisecondes.

Ce que vous obtenez à la fin

L'exemple complet tient en une seule unité Delphi et un module de données. Compilez et lancez : un tableau de bord ERP accessible depuis un navigateur apparaît sur le port configuré, avec quatre cartes KPI, un graphique à barres revenus/coûts sur 12 mois, un tableau de factures recherchable et paginé et un tableau Kanban multi-colonnes pour le traitement des commandes. Lorsque de nouvelles commandes sont passées, les cartes KPI se mettent à jour en temps réel sur tous les onglets de navigateur connectés. Nombre total de lignes de code front-end écrites : zéro.

Le même schéma fonctionne pour n'importe quelle application métier Delphi : gestion des stocks, planification de production, systèmes RH, suivi de flotte. Si les données sont dans un TDataSet, elles peuvent être sur un tableau de bord web en direct en une matinée.

Téléchargez l'essai gratuit de sgcHTML depuis esegece.com/products/sgchtml/download et parcourez la démo ERP incluse, la console d'administration, le moniteur en direct et les applications Portail Client pour un exemple complet fonctionnel.

Des questions ? Contactez-nous. Vous recevrez une réponse des personnes qui ont écrit le code.