Lewati ke konten utama

Dashboard & Widget Management — Architecture & Development Guide

Tujuan Dokumen

Dokumen ini mendefinisikan arsitektur teknis dan pendekatan pengembangan untuk sistem Dashboard & Widget Management yang dideskripsikan di dashboard-widget-summary.md.

Dokumen menjawab tiga pertanyaan utama:

  1. Apakah setiap widget dikembangkan secara manual (hardcoded per role), ataukah kita membangun sistem widget yang dinamis?
  2. Bagaimana mekanisme konfigurasi widget per role, termasuk assignment dan layout?
  3. Bagaimana pola pengembangan widget yang konsisten dengan konvensi Neuron ERP?

Keputusan Arsitektur: Hybrid Approach

Setelah mempertimbangkan dua ekstrem — fully hardcoded vs fully dynamic (no-code builder) — rekomendasi adalah Hybrid Approach:

AspekFully HardcodedFully DynamicHybrid (Dipilih)
Kecepatan pengembangan awalCepatSangat lambatSedang
Fleksibilitas konfigurasiTidak adaSangat tinggiCukup tinggi
Kontrol kualitas data/tampilanPenuhSulit dikontrolPenuh
Kompleksitas sistemRendahSangat tinggiSedang
Kesesuaian dengan konvensi ERPBaikTidak sesuaiSesuai

Definisi Hybrid:

  • Widget dikembangkan manual oleh developer, masing-masing sebagai Blazor component dengan data service sendiri. Ini menjamin kualitas data, query yang dioptimasi, dan tampilan yang konsisten.
  • Konfigurasi, assignment, layout, dan visibilitas dikelola secara dinamis melalui database dan halaman administrasi — tanpa perlu menyentuh kode.
  • Tidak ada "query builder" untuk user — user tidak bisa mendefinisikan sumber data baru. Data source dan tipe chart ditentukan saat widget dibuat oleh developer.

Komponen Sistem

┌─────────────────────────────────────────────────────────┐
│ Dashboard Engine │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Widget │ │ Dashboard │ │ Layout │ │
│ │ Registry │ │ Page │ │ Engine │ │
│ │ (DB catalog) │ │ (Blazor) │ │ (Grid CSS) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Role-Widget │ │ Widget │ │
│ │ Assignment │ │ Components │ │
│ │ (DB table) │ │ (Blazor .razor) │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘

Database Schema

1. Tabel dashboard_widget_catalog

Daftar semua widget yang tersedia di sistem. Diisi oleh developer saat membuat widget baru.

CREATE TABLE dashboard_widget_catalog (
wgt_id SERIAL PRIMARY KEY,
wgt_code VARCHAR(50) NOT NULL UNIQUE, -- 'AR_AGING_SUMMARY'
wgt_name VARCHAR(100) NOT NULL, -- 'AR Aging Summary'
wgt_description TEXT,
wgt_category VARCHAR(50), -- 'Financial','Sales','HR',...
wgt_component VARCHAR(150) NOT NULL, -- nama Blazor component: 'WidgetArAgingSummary'
wgt_default_col_span INT DEFAULT 4, -- lebar default dalam grid 12 kolom
wgt_default_row_span INT DEFAULT 1,
wgt_is_active BOOLEAN DEFAULT TRUE,
wgt_created_at TIMESTAMP DEFAULT NOW()
);

2. Tabel dashboard_role_config

Mendefinisikan dashboard per role: widget apa saja yang ditampilkan, urutan, ukuran, dan status aktif.

CREATE TABLE dashboard_role_config (
drc_id SERIAL PRIMARY KEY,
drc_role_code VARCHAR(50) NOT NULL, -- 'EXECUTIVE','FINANCE_MGR','SALES_MGR',...
drc_wgt_id INT NOT NULL REFERENCES dashboard_widget_catalog(wgt_id),
drc_sort_order INT NOT NULL DEFAULT 0, -- urutan tampil dalam dashboard
drc_col_span INT NOT NULL DEFAULT 4, -- override lebar (1-12)
drc_row_span INT NOT NULL DEFAULT 1,
drc_is_enabled BOOLEAN DEFAULT TRUE, -- aktif/nonaktif widget untuk role ini
drc_refresh_interval_sec INT DEFAULT 900, -- interval refresh dalam detik
drc_created_at TIMESTAMP DEFAULT NOW(),
drc_updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE (drc_role_code, drc_wgt_id)
);

3. Tabel dashboard_user_override (Opsional — Phase 2)

Memungkinkan user menyesuaikan layout dashboard pribadi (hide/reorder) tanpa mengubah konfigurasi role.

CREATE TABLE dashboard_user_override (
duo_id SERIAL PRIMARY KEY,
duo_userid INT NOT NULL,
duo_wgt_id INT NOT NULL REFERENCES dashboard_widget_catalog(wgt_id),
duo_is_hidden BOOLEAN DEFAULT FALSE,
duo_sort_order INT,
duo_col_span INT,
duo_updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE (duo_userid, duo_wgt_id)
);

Pola Pengembangan Widget

Setiap widget adalah satu Blazor component yang mengikuti konvensi berikut.

Lokasi File

NeuronLibraryUI/
└── Shared/
└── Components/
└── Dashboard/
├── Widgets/
│ ├── KpiCard/
│ │ ├── WidgetKpiCard.razor ← Generic KPI card (reusable)
│ │ └── WidgetKpiCard.razor.css
│ ├── WidgetArAgingSummary.razor ← Widget spesifik
│ ├── WidgetApAgingSummary.razor
│ ├── WidgetCashPosition.razor
│ ├── WidgetSalesAchievement.razor
│ ├── WidgetOutstandingSoPipeline.razor
│ ├── WidgetSoBelumWo.razor
│ ├── WidgetGradePackingRol.razor
│ ├── WidgetGradePackingKg.razor
│ └── ...
├── DashboardContainer.razor ← Engine utama
├── DashboardContainer.razor.css
└── WidgetShell.razor ← Wrapper card per widget

Data service untuk masing-masing widget berada di NeuronLibrary:

NeuronLibrary/
└── Data/
└── Smart Report/
└── Dashboard/
├── IDashboardWidgetServices.cs ← Interface shared
├── WidgetArAgingServices.cs
├── WidgetCashPositionServices.cs
├── WidgetSalesAchievementServices.cs
├── WidgetOutstandingSoServices.cs
├── WidgetGradePackingServices.cs
└── ...

Anatomi Widget Component

Setiap widget component mengimplementasikan interface sederhana:

// NeuronLibraryUI/Shared/Components/Dashboard/IDashboardWidget.cs
public interface IDashboardWidget
{
// Dipanggil oleh DashboardContainer setiap kali refresh
Task RefreshAsync();
}

Contoh implementasi widget KPI Card:

@* WidgetArAgingSummary.razor *@
@implements IDashboardWidget
@inject IWidgetArAgingServices _svc

<WidgetShell Title="AR Aging Summary"
IsLoading="@_loading"
DrillDownUrl="/FARReportByAging">

@if (!_loading && _data != null)
{
<div class="aging-grid">
<AgingBucket Label="0-30 Hari" Value="@_data.Bucket0_30" Color="success" />
<AgingBucket Label="31-60 Hari" Value="@_data.Bucket31_60" Color="warning" />
<AgingBucket Label="61-90 Hari" Value="@_data.Bucket61_90" Color="orange" />
<AgingBucket Label=">90 Hari" Value="@_data.BucketOver90" Color="danger" />
</div>
}
</WidgetShell>

@code {
[Parameter] public int EntityId { get; set; }
[Parameter] public int BranchId { get; set; }
[Parameter] public int UserId { get; set; }

private ArAgingSummaryModel _data;
private bool _loading;

public async Task RefreshAsync()
{
_loading = true;
StateHasChanged();
try
{
_data = await _svc.GetArAgingSummaryAsync(EntityId, BranchId, UserId);
}
finally
{
_loading = false;
StateHasChanged();
}
}

protected override async Task OnParametersSetAsync()
=> await RefreshAsync();
}

WidgetShell — Wrapper Standar

WidgetShell.razor menyediakan frame yang konsisten untuk semua widget: header dengan judul, tombol refresh, state loading, dan link drill-down.

@* WidgetShell.razor *@
<div class="widget-shell @(IsLoading ? "loading" : "")">
<div class="widget-header">
<span class="widget-title">@Title</span>
<div class="widget-actions">
<button @onclick="OnRefresh" title="Refresh"><i class="fas fa-sync-alt" /></button>
@if (!string.IsNullOrEmpty(DrillDownUrl))
{
<a href="@DrillDownUrl" target="_blank" title="Lihat Detail">
<i class="fas fa-external-link-alt" />
</a>
}
</div>
</div>
<div class="widget-body">
@if (IsLoading)
{
<div class="widget-loading"><DxWaitIndicator Visible="true" /></div>
}
else
{
@ChildContent
}
</div>
</div>

@code {
[Parameter] public string Title { get; set; }
[Parameter] public bool IsLoading { get; set; }
[Parameter] public string DrillDownUrl { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public EventCallback OnRefresh { get; set; }
}

Dashboard Container — Engine Utama

DashboardContainer.razor adalah komponen yang:

  1. Membaca dashboard_role_config untuk role user yang sedang login.
  2. Merender widget-widget yang aktif dalam grid layout.
  3. Mengelola auto-refresh per widget sesuai drc_refresh_interval_sec.
@* DashboardContainer.razor *@
@inject IDashboardConfigServices _configSvc
@inject IServiceProvider _sp

<div class="dashboard-grid">
@foreach (var cfg in _widgetConfigs.Where(w => w.IsEnabled))
{
<div class="col-md-@cfg.ColSpan widget-wrapper" style="order: @cfg.SortOrder">
<DynamicComponent Type="@GetWidgetType(cfg.ComponentName)"
Parameters="@GetWidgetParams(cfg)" />
</div>
}
</div>

@code {
[Parameter] public string RoleCode { get; set; }
[Parameter] public int EntityId { get; set; }
[Parameter] public int BranchId { get; set; }
[Parameter] public int UserId { get; set; }

private List<DashboardWidgetConfigModel> _widgetConfigs = new();

protected override async Task OnInitializedAsync()
{
_widgetConfigs = await _configSvc.GetWidgetConfigsByRoleAsync(RoleCode);
}

private Type GetWidgetType(string componentName)
=> Type.GetType($"NeuronLibraryUI.Shared.Components.Dashboard.Widgets.{componentName}")
?? throw new InvalidOperationException($"Widget component '{componentName}' not found.");

private Dictionary<string, object> GetWidgetParams(DashboardWidgetConfigModel cfg)
=> new()
{
[nameof(IEntityBranchAware.EntityId)] = EntityId,
[nameof(IEntityBranchAware.BranchId)] = BranchId,
[nameof(IEntityBranchAware.UserId)] = UserId,
};
}

DynamicComponent adalah fitur bawaan Blazor yang merender komponen berdasarkan Type secara runtime — tidak perlu switch statement atau if/else per widget.


Layout System

Dashboard menggunakan CSS Grid 12-kolom (selaras dengan Bootstrap yang sudah ada di proyek). drc_col_span di database menentukan lebar setiap widget.

/* DashboardContainer.razor.css */
.dashboard-grid {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}

.widget-wrapper {
box-sizing: border-box;
/* col-md-3 = 25%, col-md-4 = 33%, col-md-6 = 50%, col-md-12 = 100% */
}

Ukuran Standar Widget

ColSpanLebar LayarPenggunaan Umum
325%KPI Card tunggal (angka + icon)
433%KPI Card dengan aging bar
650%Chart bar/line
867%Tabel dengan banyak kolom
12100%Pipeline table, alert panel

Ordering

Widget diurutkan berdasarkan drc_sort_order (ascending). Administrator mengubah urutan via halaman Control Files → Dashboard Config.


Halaman Administrasi (Control Files)

Dua halaman administrasi diperlukan, mengikuti pola Control File yang sudah ada di proyek.

1. Widget Catalog

Neuron_ERP/Pages/Administrative Tools/Dashboard/WidgetCatalog.razor

  • Menampilkan daftar semua widget yang terdaftar (dashboard_widget_catalog)
  • Administrator dapat mengaktifkan/menonaktifkan widget secara global
  • Read-only untuk kolom wgt_component (diisi developer, bukan admin)

2. Role Dashboard Configuration

Neuron_ERP/Pages/Administrative Tools/Dashboard/RoleDashboardConfig.razor

Tampilan dua panel:

┌────────────────────────┬────────────────────────────────────────┐
│ Pilih Role │ Widget untuk Role: SALES_MGR │
│ ───────────────── │ ──────────────────────────────────── │
│ ○ Executive │ ☑ Sales Achievement [↑↓] Col: [6▾] │
│ ● Sales Manager │ ☑ SO Pipeline [↑↓] Col: [12▾]│
│ ○ Finance Manager │ ☑ Outstanding SO/SP [↑↓] Col: [6▾] │
│ ○ HR Manager │ ☐ Cash Position [↑↓] Col: [4▾] │
│ ○ Supervisor │ ☑ AR Overdue Alert [↑↓] Col: [12▾]│
│ ○ Operator │ Refresh: [900▾] detik │
└────────────────────────┴────────────────────────────────────────┘
[ Simpan Konfigurasi ]

Fitur:

  • Centang/uncentang widget per role (drc_is_enabled)
  • Drag-and-drop atau tombol ↑↓ untuk mengubah drc_sort_order
  • Dropdown untuk drc_col_span (3, 4, 6, 8, 12)
  • Input drc_refresh_interval_sec per widget per role

Role Code Mapping

Role code di dashboard_role_config harus konsisten dengan role system yang sudah ada. Mapping awal:

Role CodeDeskripsi
EXECUTIVEDirektur / Owner
FINANCE_MGRFinance Manager
SALES_MGRSales Manager
WAREHOUSE_MGRWarehouse / Operational Manager
PROCUREMENT_MGRProcurement Manager
MANUFACTURE_MGRManufacturing Manager
HR_MGRHR Manager
SUPERVISORSupervisor (subset dari Manager)
OPERATORStaff / Operator

Role code di-assign ke user melalui halaman User Management yang sudah ada (Administrative Tools → User). Kolom baru user_dashboard_role ditambahkan ke tabel user_mstr, atau bisa menggunakan group user yang sudah ada.


Cara Menambahkan Widget Baru

Berikut checklist lengkap untuk developer yang ingin menambahkan widget baru ke sistem.

Step 1 — Buat Data Service

NeuronLibrary/Data/Smart Report/Dashboard/WidgetNamaBaruServices.cs
NeuronLibrary/Data/Smart Report/Dashboard/IWidgetNamaBaruServices.cs
  • Implementasi menggunakan SqlHelper.GetConnection() + Dapper (sesuai konvensi proyek)
  • Query dioptimasi — jangan reuse query dari report yang berat
  • Parameter minimal: entity_id, branch_id, user_id, date_from, date_to (sesuai kebutuhan)
  • Daftarkan di Program.cs: builder.Services.AddScoped<IWidgetNamaBaruServices, WidgetNamaBaruServices>();

Step 2 — Buat Widget Component

NeuronLibraryUI/Shared/Components/Dashboard/Widgets/WidgetNamaBaru.razor
  • @implements IDashboardWidget
  • Gunakan WidgetShell sebagai wrapper
  • Tentukan DrillDownUrl ke halaman report yang relevan
  • Ekspos parameter: EntityId, BranchId, UserId, dan parameter tambahan jika perlu

Step 3 — Daftarkan di Catalog

Insert ke tabel dashboard_widget_catalog:

INSERT INTO dashboard_widget_catalog
(wgt_code, wgt_name, wgt_category, wgt_component, wgt_default_col_span)
VALUES
('WIDGET_NAMA_BARU', 'Nama Widget', 'Sales', 'WidgetNamaBaru', 6);

Step 4 — Assign ke Role

Insert ke dashboard_role_config untuk role yang membutuhkan widget ini:

INSERT INTO dashboard_role_config
(drc_role_code, drc_wgt_id, drc_sort_order, drc_col_span, drc_refresh_interval_sec)
VALUES
('SALES_MGR', (SELECT wgt_id FROM dashboard_widget_catalog WHERE wgt_code = 'WIDGET_NAMA_BARU'), 5, 6, 900);

Atau melalui halaman Role Dashboard Configuration di UI.

Step 5 — Verifikasi

Buka dashboard sebagai user dengan role terkait dan pastikan widget muncul dengan data yang benar.


Migrasi dari DashboardUtama yang Existing

DashboardUtama.razor (/FDashboardUtama) adalah dashboard hardcoded yang saat ini ada. Strategi migrasi:

  1. Tidak hapus DashboardUtama dulu — tetap berjalan sampai sistem baru stabil.
  2. Bangun DashboardContainer di route baru, misalnya /Dashboard.
  3. Widget-widget di DashboardUtama yang lama (Kas Bank, Top 5 Barang, dll.) di-extract menjadi widget components individual.
  4. Setelah semua widget lama sudah ada di sistem baru dan sudah diverifikasi, ganti halaman default post-login dari /FDashboardUtama ke /Dashboard.
  5. Hapus DashboardUtama setelah masa transisi.

Data Refresh — Implementasi

Auto-refresh dilakukan di level WidgetShell menggunakan System.Timers.Timer atau PeriodicTimer (lebih clean di .NET 8):

// WidgetShell.razor — @code section (simplified)
[Parameter] public int RefreshIntervalSec { get; set; } = 900;
[Parameter] public EventCallback OnRefresh { get; set; }

private PeriodicTimer? _timer;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && RefreshIntervalSec > 0)
{
_timer = new PeriodicTimer(TimeSpan.FromSeconds(RefreshIntervalSec));
_ = RunTimerAsync();
}
}

private async Task RunTimerAsync()
{
while (_timer != null && await _timer.WaitForNextTickAsync())
{
await OnRefresh.InvokeAsync();
}
}

public void Dispose()
{
_timer?.Dispose();
}

Interval refresh per widget diambil dari drc_refresh_interval_sec di database dan diteruskan sebagai parameter ke WidgetShell.


Performance Considerations

RisikoMitigasi
Banyak widget load bersamaan → spike query ke DBStagger load: widget diload berurutan dengan delay kecil (100ms per widget), bukan serentak
Query berat di dashboardSetiap widget service menggunakan query sendiri yang sudah dioptimasi — bukan reuse query report
Blazor Server memory per circuitWidget tidak menyimpan data besar di state; hanya result summary
Auto-refresh menumpuk requestPeriodicTimer per widget dengan interval yang beda-beda; tidak ada sentralisasi refresh

Out of Scope (Phase 1)

Sesuai batasan di dashboard-widget-summary.md, hal berikut tidak diimplementasikan di Phase 1:

  • Real-time WebSocket push per widget
  • Custom widget builder — user mendefinisikan query sendiri
  • Dashboard export ke PDF
  • Drag-and-drop layout oleh end-user (layout diatur administrator)
  • Embedded AI commentary per widget

Summary Keputusan

KeputusanPilihan
Widget developmentManual per widget (Blazor component)
Konfigurasi assignmentDatabase-driven, dikelola via Control Files
LayoutCSS Grid 12-kolom, col-span per widget dikonfigurasi di DB
Rendering engineDynamicComponent Blazor — tidak perlu kode baru per widget baru
Chart libraryDevExpress Blazor Chart (sudah ada, tidak tambah library baru)
Auto-refreshPeriodicTimer per widget, interval dari DB
Data sourceMasing-masing widget punya service + query sendiri (Dapper)
Migrasi existing dashboardBertahap — DashboardUtama tetap hidup sampai sistem baru stabil