sgcHTML ve Delphi ile Gerçek Zamanlı ERP Panosu Oluşturma

· Uygulamalar

ERP sistemleri, Delphi geliştirmenin tam merkezindedir: müşteriler, siparişler, faturalar, stok seviyeleri, üretim süreçleri. İş mantığı zaten Object Pascal'da yazılmıştır. Veritabanı bağlıdır. Raporlar çalışmaktadır. Genellikle eksik olan şey, yönetimin herhangi bir şey kurmadan bir tablette açabileceği, tarayıcı üzerinden erişilebilen bir panodur. Bu yazıda tam olarak bunun sgcHTML ile nasıl yapılacağını gösteriyoruz; gerçekçi bir senaryo üzerinden çalışıyoruz: canlı gelir rakamları, bir fatura tablosu, teslimat Kanban panosu ve yeni siparişler geldiğinde gerçek zamanlı anlık güncellemeler içeren bir satış ERP'si.

Tüm kod Delphi 10.4 Sydney ve sonraki sürümlerde derlenir. Sonuç olarak elde ettiğiniz şey, panoyu 8080 numaralı bağlantı noktasında sunan, Bootstrap 5 stil tasarımı, duyarlı düzen ve WebSocket canlı güncellemeleri içeren ve ikili dosyanın yanına dağıtılacak hiçbir JavaScript dosyası gerektirmeyen tek bir .exe dosyasıdır.

Sunucuyu kurma

sgcHTML, sgcWebSockets'tan bir TsgcWSHTTPServer bileşenini sarar. TsgcHTMLEngine_Server bileşenini veri modülüne HTTP sunucusunun yanına yerleştirin ve ikisini birbirine bağlayın. Motor, statik kaynak isteklerini (Bootstrap CSS/JS, Chart.js, htmx) otomatik olarak karşılar; geri kalanlar OnCommandGet işleyicinize yönlendirilir.

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 istatistik kartları

Panonun üst satırı dört KPI kartı gösterir: toplam gelir, açık faturalar, vadesi geçmiş fatura sayısı ve zamanında teslimat oranı. TsgcHTMLComponent_StatCard tüm görseli üstlenir: arka plan gradyanı, başlık, büyük değer, trend oku ve isteğe bağlı alt bilgi etiketi.

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;

Gelir trendi grafiği

Gelir grafiği ikinci satıra yayılır. TsgcHTMLComponent_Chart, etiket dizilerini ve kenarlık ile dolgu renkleri olan veri kümelerini kabul eder. LoadFromDataSet, bir sorgu sonucunu tek bir çağrıyla grafik verisine eşler: veri kümesini, etiket alanını (ay adı) ve bir veya daha fazla değer alanını (gelir, maliyet) geçirin.

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;

Sayfalandırılmış fatura tablosu

TsgcHTMLComponent_DataTable, bir TsgcHTMLComponent_Grid ile bir TsgcHTMLComponent_Pagination bileşenini yerleşik arama kutusu, satır sayısı etiketi, isteğe bağlı dışa aktarma düğmesi ve yapılandırılabilir sayfa boyutlarıyla tek bir widget'ta bir araya getirir. LoadFromDataSet, sütun adlarını ve türlerini otomatik olarak okur; daha sonra Grid.Columns aracılığıyla başlık etiketlerini ve sütun genişliklerini geçersiz kılabilirsiniz.

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;

Sipariş karşılama için Kanban panosu

TsgcHTMLComponent_KanbanBoard, çok sütunlu, sürükle-bırak destekli bir panoyu temsil eder. Her sütun bir kart koleksiyonu barındırır; her kart bir başlık, isteğe bağlı açıklama, sorumlu kişi, etiket ve renk taşır. ERP bağlamında sütunlar doğal olarak sipariş durumlarıyla eşleşir: Alındı, Hazırlanıyor, Paketlendi, Gönderildi.

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;

Tam sayfayı bir araya getirme

Dört bölüm, tek bir ServeDashboard yordamı tarafından bir araya getirilir; bu yordam bir sayfa oluşturur, her oluşturucuyu çağırır, sonucu Bootstrap şablonuyla sarar ve yanıta yazar:

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 üzerinden canlı güncellemeler

Pano, canlı veri olmadan statik kalırdı. sgcHTML, sgcWebSockets'ın üzerine kurulduğundan, yeni bir sipariş verildiğinde bir parça güncellemesi yayımlamak tek bir yayın çağrısından ibarettir. İstemci tarafında istatistik kartı bir id niteliği taşır; htmx WebSocket uzantısı ise bir mesaj geldiğinde içeriğini değiştirir.

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

Parça, sayfa yenilenmeksizin ve kullanıcının herhangi bir şeye tıklaması gerekmeksizin tarayıcıdaki eşleşen kartın yerini hemen alır. Bağlı olan her istemci güncellemeyi milisaniyeler içinde görür.

Sonuçta elde ettiğiniz şey

Tam örnek tek bir Delphi birimi ve bir veri modülünden oluşur. Derleyin ve çalıştırın: yapılandırılan bağlantı noktasında dört KPI kartı, 12 aylık gelir-maliyet karşılaştırmalı çubuk grafik, aranabilir ve sayfalandırılmış fatura tablosu ile sipariş karşılama için çok sütunlu bir Kanban panosu içeren, tarayıcı üzerinden erişilebilen bir ERP panosu görüntülenir. Yeni siparişler verildiğinde KPI kartları, bağlı her tarayıcı sekmesinde gerçek zamanlı olarak güncellenir. Yazdığınız ön yüz kodu: sıfır satır.

Aynı yaklaşım her türlü Delphi iş uygulamasında işe yarar: stok yönetimi, üretim planlaması, İK sistemleri, filo takibi. Veriler bir TDataSet'te bulunuyorsa bir sabahın içinde canlı bir web panosuna taşınabilir.

sgcHTML ücretsiz deneme sürümünü esegece.com/products/sgchtml/download adresinden indirin ve tam çalışan bir örnek için içinde yer alan ERP demosu, Admin Konsolu, Canlı İzleme ve Müşteri Portalı uygulamalarına göz atın.

Sorularınız mı var? Bizimle iletişime geçin. Kodu yazan kişilerden yanıt alacaksınız.