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:
- Apakah setiap widget dikembangkan secara manual (hardcoded per role), ataukah kita membangun sistem widget yang dinamis?
- Bagaimana mekanisme konfigurasi widget per role, termasuk assignment dan layout?
- 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:
| Aspek | Fully Hardcoded | Fully Dynamic | Hybrid (Dipilih) |
|---|---|---|---|
| Kecepatan pengembangan awal | Cepat | Sangat lambat | Sedang |
| Fleksibilitas konfigurasi | Tidak ada | Sangat tinggi | Cukup tinggi |
| Kontrol kualitas data/tampilan | Penuh | Sulit dikontrol | Penuh |
| Kompleksitas sistem | Rendah | Sangat tinggi | Sedang |
| Kesesuaian dengan konvensi ERP | Baik | Tidak sesuai | Sesuai |
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:
- Membaca
dashboard_role_configuntuk role user yang sedang login. - Merender widget-widget yang aktif dalam grid layout.
- 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,
};
}
DynamicComponentadalah fitur bawaan Blazor yang merender komponen berdasarkanTypesecara runtime — tidak perluswitchstatement atauif/elseper 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
| ColSpan | Lebar Layar | Penggunaan Umum |
|---|---|---|
| 3 | 25% | KPI Card tunggal (angka + icon) |
| 4 | 33% | KPI Card dengan aging bar |
| 6 | 50% | Chart bar/line |
| 8 | 67% | Tabel dengan banyak kolom |
| 12 | 100% | 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_secper widget per role
Role Code Mapping
Role code di dashboard_role_config harus konsisten dengan role system yang sudah ada. Mapping awal:
| Role Code | Deskripsi |
|---|---|
EXECUTIVE | Direktur / Owner |
FINANCE_MGR | Finance Manager |
SALES_MGR | Sales Manager |
WAREHOUSE_MGR | Warehouse / Operational Manager |
PROCUREMENT_MGR | Procurement Manager |
MANUFACTURE_MGR | Manufacturing Manager |
HR_MGR | HR Manager |
SUPERVISOR | Supervisor (subset dari Manager) |
OPERATOR | Staff / 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
WidgetShellsebagai wrapper - Tentukan
DrillDownUrlke 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:
- Tidak hapus
DashboardUtamadulu — tetap berjalan sampai sistem baru stabil. - Bangun
DashboardContainerdi route baru, misalnya/Dashboard. - Widget-widget di
DashboardUtamayang lama (Kas Bank, Top 5 Barang, dll.) di-extract menjadi widget components individual. - Setelah semua widget lama sudah ada di sistem baru dan sudah diverifikasi, ganti halaman default post-login dari
/FDashboardUtamake/Dashboard. - Hapus
DashboardUtamasetelah 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
| Risiko | Mitigasi |
|---|---|
| Banyak widget load bersamaan → spike query ke DB | Stagger load: widget diload berurutan dengan delay kecil (100ms per widget), bukan serentak |
| Query berat di dashboard | Setiap widget service menggunakan query sendiri yang sudah dioptimasi — bukan reuse query report |
| Blazor Server memory per circuit | Widget tidak menyimpan data besar di state; hanya result summary |
| Auto-refresh menumpuk request | PeriodicTimer 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
| Keputusan | Pilihan |
|---|---|
| Widget development | Manual per widget (Blazor component) |
| Konfigurasi assignment | Database-driven, dikelola via Control Files |
| Layout | CSS Grid 12-kolom, col-span per widget dikonfigurasi di DB |
| Rendering engine | DynamicComponent Blazor — tidak perlu kode baru per widget baru |
| Chart library | DevExpress Blazor Chart (sudah ada, tidak tambah library baru) |
| Auto-refresh | PeriodicTimer per widget, interval dari DB |
| Data source | Masing-masing widget punya service + query sendiri (Dapper) |
| Migrasi existing dashboard | Bertahap — DashboardUtama tetap hidup sampai sistem baru stabil |