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.
