ERP システムは Delphi 開発の中心的な領域です。顧客、受注、請求書、在庫レベル、生産実行など、ビジネスロジックはすでに Object Pascal に実装されており、データベースも接続済みで、レポートも動作しています。しかし、管理職がタブレットで何もインストールせずにアクセスできるブラウザ対応のダッシュボードが欠けていることがよくあります。本記事では、sgcHTML を使って、リアルな ERP シナリオをもとにその実装方法を紹介します。具体的には、ライブ売上情報、請求書グリッド、配送かんばんボード、そして新しい注文が届いた際のリアルタイムプッシュ機能を持つ ERP 販売ダッシュボードを構築します。
すべてのコードは Delphi 10.4 Sydney 以降でコンパイルできます。完成形は単一の .exe ファイルで、ポート 8080 でダッシュボードを提供します。Bootstrap 5 スタイリング、レスポンシブレイアウト、WebSocket によるライブ更新を備え、バイナリと一緒にデプロイする JavaScript ファイルは不要です。
サーバーのセットアップ
sgcHTML は sgcWebSockets の TsgcWSHTTPServer をラップしています。データモジュールに TsgcHTMLEngine_Server を HTTP サーバーとともに配置し、両者を接続します。エンジンは静的アセットのリクエスト(Bootstrap CSS/JS、Chart.js、htmx)を自動的に処理し、それ以外はすべて 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;
KPI 統計カード
ダッシュボードの上部には 4 枚の KPI カードが並びます。年間累計売上、未払い請求書件数、延滞件数、そして納期遵守率です。TsgcHTMLComponent_StatCard がビジュアル全体を担当します。背景グラデーション、タイトル、大きな値、トレンドの矢印、オプションのフッターラベルをすべて処理します。
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;
売上トレンドグラフ
売上グラフは第 2 行全体を占めます。TsgcHTMLComponent_Chart はラベル配列と、境界線色および塗りつぶし色を持つデータセットを受け取ります。LoadFromDataSet はクエリ結果をグラフデータに 1 回の呼び出しでマッピングします。データセット、ラベルフィールド(月名)、および 1 つ以上の値フィールド(売上、コスト)を渡すだけです。
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;
ページ付き請求書テーブル
TsgcHTMLComponent_DataTable は TsgcHTMLComponent_Grid と TsgcHTMLComponent_Pagination を 1 つのウィジェットにまとめており、組み込みの検索ボックス、行数ラベル、オプションのエクスポートボタン、設定可能なページサイズを備えています。LoadFromDataSet は列名と型を自動的に読み取ります。その後、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;
受注処理のかんばんボード
TsgcHTMLComponent_KanbanBoard は複数列のドラッグ対応ボードを表します。各列にはカードのコレクションが含まれ、各カードにはタイトル、オプションの説明、担当者、タグ、色が付きます。ERP の文脈では、列は受注状態に自然にマッピングされます。受注済み、ピッキング中、梱包済み、出荷済みといった状態です。
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;
ページ全体の組み立て
4 つのセクションは、単一の ServeDashboard プロシージャによって組み立てられます。ページを作成し、各ビルダーを呼び出し、結果を Bootstrap テンプレートでラップして、レスポンスに書き出します。
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;
WebSocket によるライブ更新
ライブデータがなければダッシュボードは静的なままです。sgcHTML は sgcWebSockets の上に構築されているため、新しい注文が入ったときにフラグメント更新をプッシュするのは、単一のブロードキャスト呼び出しで済みます。クライアント側では、統計カードが id 属性を持ち、htmx の WebSocket 拡張機能がメッセージを受信するとその内容を置き換えます。
// 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;
このフラグメントはページをリロードすることなく、またユーザーが何かをクリックすることなく、即座にブラウザの該当カードを置き換えます。接続されているすべてのクライアントは、ミリ秒以内に更新を確認できます。
完成時に得られるもの
完全なサンプルは、単一の Delphi ユニットとデータモジュールで構成されています。コンパイルして実行すると、設定したポートでブラウザからアクセスできる ERP ダッシュボードが表示されます。4 枚の KPI カード、12 か月分の売上対コスト棒グラフ、検索可能でページ付きの請求書テーブル、そして受注処理用の複数列かんばんボードが含まれます。新しい注文が入ると、接続されているすべてのブラウザタブで KPI カードがリアルタイムに更新されます。フロントエンドコードの記述量はゼロ行です。
同じパターンは、どんな Delphi ビジネスアプリケーションにも適用できます。在庫管理、生産スケジューリング、人事システム、フリート追跡など、データが TDataSet に入っていれば、半日でライブ Web ダッシュボードを実現できます。
sgcHTML の無料トライアルは esegece.com/products/sgchtml/download からダウンロードできます。付属の ERP デモ、管理コンソール、ライブモニター、カスタマーポータルアプリケーションで、完全な動作サンプルをご覧ください。
ご質問は お問い合わせ ページからどうぞ。コードを書いたメンバーが直接お答えします。
