Dashboard & Widget Framework — Analysis Design & Solution Modeling
Tujuan Dokumen
Dokumen ini berisi analisis desain dan pemodelan solusi untuk Dashboard & Widget Management Framework pada TessaERP.
Merupakan kelanjutan dari dashboard-widget-brd.md dan dashboard-widget-architecture.md, mencakup use case, flowchart, component diagram, sequence diagram, dan schema data.
Use Case
Aktor
| Aktor | Deskripsi |
|---|---|
| Administrator | Mengelola widget catalog, konfigurasi dashboard per role, dan assignment dashboard role ke user |
| User (semua role) | Melihat dashboard sesuai dashboard role yang ditetapkan; me-refresh widget |
| Developer | Membuat widget baru — data service + (opsional) custom component |
| System | Merender dashboard secara dinamis berdasarkan konfigurasi database |
Use Case Diagram
graph TD
Admin((Administrator))
User((User\nSemua Role))
Dev((Developer))
Sys((System))
UC01[UC-01: Kelola Widget Catalog\naktivasi / deaktivasi]
UC02[UC-02: Konfigurasi Dashboard per Role\nassign widget, layout, refresh interval]
UC03[UC-03: Set Dashboard Role pada User\nvia User Management]
UC04[UC-04: Lihat Dashboard\nsesuai dashboard role]
UC05[UC-05: Refresh Widget\nmanual atau auto]
UC06[UC-06: Drill-down ke Report / Transaksi]
UC07[UC-07: Daftarkan Widget Baru\nINSERT ke catalog]
UC08[UC-08: Resolve Dashboard Role saat Login]
UC09[UC-09: Render Dynamic Component]
UC10[UC-10: Auto Refresh via PeriodicTimer]
Admin --> UC01
Admin --> UC02
Admin --> UC03
User --> UC04
User --> UC05
User --> UC06
Dev --> UC07
Sys --> UC08
Sys --> UC09
Sys --> UC10
UC04 --> UC08
UC08 --> UC09
UC05 --> UC10
Detail Use Case
| ID | Use Case | Aktor | Pre-Condition | Post-Condition |
|---|---|---|---|---|
| UC-01 | Kelola Widget Catalog | Administrator | Login sebagai admin | Widget aktif/nonaktif diperbarui |
| UC-02 | Konfigurasi Dashboard per Role | Administrator | Widget catalog tersedia | Konfigurasi role tersimpan di DB |
| UC-03 | Set Dashboard Role pada User | Administrator | User master tersedia | user_dashboard_role diperbarui |
| UC-04 | Lihat Dashboard | User | Login berhasil | Dashboard sesuai role ditampilkan |
| UC-05 | Refresh Widget | User | Dashboard ditampilkan | Data widget diperbarui |
| UC-06 | Drill-down | User | Widget memiliki DrillDownUrl | Halaman report/transaksi terbuka |
| UC-07 | Daftarkan Widget Baru | Developer | Widget component dan service sudah dibuat | Widget tersedia di catalog |
| UC-08 | Resolve Dashboard Role saat Login | System | User login, user_dashboard_role tersedia | Role terbaca, konfigurasi widget di-load |
| UC-09 | Render Dynamic Component | System | Konfigurasi widget di-load | Widget dirender via DynamicComponent |
| UC-10 | Auto Refresh | System | Widget ditampilkan, interval > 0 | Data widget diperbarui periodik |
Flowchart
Alur Login → Render Dashboard
flowchart TD
A([User Login]) --> B[Authentication berhasil]
B --> C[Load User Information]
C --> D{user_dashboard_role\nterisi?}
D -- Tidak --> E[Gunakan role: OPERATOR]
D -- Ya --> F[Gunakan assigned role]
E & F --> G[Load Dashboard Configuration\ndari dashboard_role_config]
G --> H[Untuk setiap widget aktif:\nResolve widget definition\ndari dashboard_widget_catalog]
H --> I{Template type\ntersedia?}
I -- Ya --> J[Instansiasi Generic Template\npass IWidgetXxxSource service]
I -- Tidak --> K[Instansiasi Custom Component\nvia DynamicComponent]
J & K --> L[Render dalam CSS Grid\nsesuai col_span & sort_order]
L --> M([Dashboard ditampilkan])
M --> N[Start PeriodicTimer\nper widget sesuai refresh_interval_sec]
Alur Menambahkan Widget Baru (Developer)
flowchart TD
A([Developer: widget baru diperlukan]) --> B[Step 1: Buat Data Service\nIWidgetXxxService + WidgetXxxService\ndi NeuronLibrary/Data/Smart Report/Dashboard]
B --> C{Widget butuh\ninteraksi khusus?}
C -- Tidak --> D[Step 2: Pilih Generic Template\nKPI_CARD / KPI_PROGRESS / CHART\nALERT_TABLE / GROUPED_TABLE]
C -- Ya --> E[Step 2b: Buat Custom Component\nWidgetXxx.razor\ndi NeuronLibraryUI]
D --> F[Step 3: INSERT ke dashboard_widget_catalog\nwgt_template_type = template dipilih\nwgt_service_type = fully-qualified service name]
E --> G[Step 3b: INSERT ke dashboard_widget_catalog\nwgt_component = nama component\nwgt_template_type = NULL]
F & G --> H[Step 4: INSERT ke dashboard_role_config\nassign ke role yang membutuhkan]
H --> I[Step 5: Verifikasi\nbuka dashboard sebagai user role terkait]
I --> J([Widget tampil dengan data yang benar])
Alur Auto Refresh Widget
flowchart TD
A([Widget selesai render]) --> B{refresh_interval_sec > 0?}
B -- Tidak --> Z([Tidak ada auto refresh])
B -- Ya --> C[Start PeriodicTimer\ninterval = refresh_interval_sec]
C --> D[WaitForNextTickAsync]
D --> E[Panggil RefreshAsync\natau DataSource.GetDataAsync]
E --> F[Update UI\nStateHasChanged]
F --> D
Component Diagram
graph TD
subgraph Neuron_ERP["Neuron_ERP — Blazor Server"]
subgraph Dashboard_Pages["Pages / Administrative Tools / Dashboard"]
P1[WidgetCatalog.razor\nView & aktivasi/deaktivasi widget]
P2[RoleDashboardConfig.razor\nAssign widget ke role & layout]
end
subgraph Dashboard_Route["Pages / Dashboard"]
P3[DashboardPage.razor\n/Dashboard]
end
P3 --> DC
end
subgraph NeuronLibraryUI["NeuronLibraryUI — UI Component Library"]
subgraph DashboardComponents["Shared/Components/Dashboard"]
DC[DashboardContainer.razor\nEngine utama]
WS[WidgetShell.razor\nWrapper standar tiap widget]
subgraph Templates["Templates/"]
T1[TemplateKpiCard.razor]
T2[TemplateKpiProgress.razor]
T3[TemplateChart.razor]
T4[TemplateAlertTable.razor]
T5[TemplateGroupedTable.razor]
end
subgraph CustomWidgets["Widgets/ — custom only"]
CW1[WidgetSoPipeline.razor]
CW2[WidgetGradePackingToggle.razor]
CW3[WidgetPendingTasks.razor]
end
end
DC --> WS
DC --> T1
DC --> T2
DC --> T3
DC --> T4
DC --> T5
DC --> CW1
DC --> CW2
DC --> CW3
T1 & T2 & T3 & T4 & T5 --> WS
end
subgraph NeuronLibrary["NeuronLibrary — Business Logic"]
subgraph DashboardSvc["Data/Smart Report/Dashboard"]
IS[IDashboardConfigServices]
CS[DashboardConfigServices.cs\nDapper + SqlHelper]
subgraph WidgetServices["Services/ — satu file per widget"]
WS1[WidgetCashPositionService.cs]
WS2[WidgetArAgingService.cs]
WS3[WidgetSalesAchievementService.cs]
WS4[WidgetSoBelumWoService.cs]
WSn[...dst]
end
subgraph Interfaces["Interfaces/"]
IF1[IWidgetKpiCardSource]
IF2[IWidgetKpiProgressSource]
IF3[IWidgetChartSource]
IF4[IWidgetAlertTableSource]
IF5[IWidgetGroupedTableSource]
end
end
subgraph DashboardModels["Models/Smart Report/Dashboard"]
M1[WidgetQueryParams.cs]
M2[DashboardWidgetConfigModel.cs]
M3[WidgetKpiCardData.cs]
M4[WidgetAlertTableData.cs]
end
IS --> CS
WS1 --> IF1
WS2 --> IF3
WS3 --> IF2
WS4 --> IF4
end
subgraph DB["PostgreSQL Database"]
T_CAT[(dashboard_widget_catalog)]
T_ROLE[(dashboard_role_config)]
T_USR[(tconfuser\n+user_dashboard_role)]
end
P1 --> IS
P2 --> IS
DC --> IS
CS --> T_CAT
CS --> T_ROLE
DC --> WS1
DC --> WS2
DC --> WS3
DC --> WS4
WS1 & WS2 & WS3 & WS4 --> M1
Sequence Diagram
Login → Load Dashboard
sequenceDiagram
actor User
participant Auth as Authentication
participant DashPage as DashboardPage.razor
participant DC as DashboardContainer.razor
participant ConfigSvc as IDashboardConfigServices
participant DB as PostgreSQL
User->>Auth: login berhasil
Auth->>Auth: load user info + user_dashboard_role
Auth->>DashPage: navigate /Dashboard
DashPage->>DC: [Parameter] RoleCode = user_dashboard_role ?? "OPERATOR"
DC->>ConfigSvc: GetWidgetConfigsByRoleAsync(RoleCode)
ConfigSvc->>DB: SELECT drc.* JOIN wgt.*\nWHERE drc_role_code = @role\nAND drc_is_enabled = TRUE\nAND wgt_is_active = TRUE\nORDER BY drc_sort_order
DB-->>ConfigSvc: List<DashboardWidgetConfigModel>
ConfigSvc-->>DC: configs
loop Setiap widget config
DC->>DC: RenderWidget(cfg)
alt template_type != null
DC->>DC: Resolve TemplateType dari TemplateType switch
DC->>DC: Resolve DataSource service dari wgt_service_type
DC->>DC: DynamicComponent Type=TemplateType, DataSource=service
else component_name != null
DC->>DC: Type.GetType(componentName)
DC->>DC: DynamicComponent Type=componentType
end
end
DC-->>User: Dashboard ditampilkan
Auto Refresh Widget
sequenceDiagram
participant WS as WidgetShell.razor
participant Timer as PeriodicTimer
participant Svc as Widget Data Service
participant DB as PostgreSQL
WS->>WS: OnAfterRenderAsync (firstRender)
WS->>Timer: new PeriodicTimer(RefreshIntervalSec)
loop WaitForNextTickAsync
Timer-->>WS: tick
WS->>Svc: GetDataAsync(queryParams)
Svc->>DB: Query (dioptimasi per widget)
DB-->>Svc: data
Svc-->>WS: WidgetXxxData
WS->>WS: StateHasChanged()
end
Konfigurasi Dashboard oleh Administrator
sequenceDiagram
actor Admin
participant ConfigPage as RoleDashboardConfig.razor
participant Svc as IDashboardConfigServices
participant DB as PostgreSQL
Admin->>ConfigPage: pilih Role = SALES_MGR
ConfigPage->>Svc: GetWidgetConfigsByRoleAsync("SALES_MGR")
Svc->>DB: SELECT + JOIN catalog
DB-->>Svc: current config
Svc-->>ConfigPage: tampilkan widget list
Admin->>ConfigPage: aktifkan widget, atur col_span, atur sort_order
Admin->>ConfigPage: klik [Simpan Konfigurasi]
ConfigPage->>Svc: SaveRoleConfigAsync(roleCode, configs)
Svc->>DB: UPSERT dashboard_role_config
DB-->>Svc: OK
Svc-->>ConfigPage: sukses
ConfigPage-->>Admin: ✅ Konfigurasi tersimpan
Schema Data
Tabel dashboard_widget_catalog
CREATE TABLE dashboard_widget_catalog (
wgt_id SERIAL PRIMARY KEY,
wgt_code VARCHAR(50) NOT NULL UNIQUE,
wgt_name VARCHAR(100) NOT NULL,
wgt_description TEXT,
wgt_category VARCHAR(50), -- Financial, Sales, HR, ...
wgt_template_type VARCHAR(30), -- KPI_CARD | KPI_PROGRESS | CHART | ALERT_TABLE | GROUPED_TABLE | NULL
wgt_service_type VARCHAR(200), -- fully-qualified type name service
wgt_component VARCHAR(150), -- nama Blazor component jika custom
wgt_interaction_type VARCHAR(20) DEFAULT 'READ', -- READ | ACT
wgt_default_col_span INT DEFAULT 4,
wgt_default_row_span INT DEFAULT 1,
wgt_is_active BOOLEAN DEFAULT TRUE,
wgt_created_at TIMESTAMP DEFAULT NOW()
);
-- Constraint: harus ada salah satu dari template_type atau component
ALTER TABLE dashboard_widget_catalog
ADD CONSTRAINT chk_wgt_source
CHECK (wgt_template_type IS NOT NULL OR wgt_component IS NOT NULL);
Tabel dashboard_role_config
CREATE TABLE dashboard_role_config (
drc_id SERIAL PRIMARY KEY,
drc_role_code VARCHAR(50) NOT NULL,
drc_wgt_id INT NOT NULL REFERENCES dashboard_widget_catalog(wgt_id),
drc_sort_order INT NOT NULL DEFAULT 0,
drc_col_span INT NOT NULL DEFAULT 4,
drc_row_span INT NOT NULL DEFAULT 1,
drc_is_enabled BOOLEAN DEFAULT TRUE,
drc_refresh_interval_sec INT DEFAULT 900,
drc_created_at TIMESTAMP DEFAULT NOW(),
drc_updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE (drc_role_code, drc_wgt_id)
);
Perubahan Tabel tconfuser
ALTER TABLE tconfuser
ADD COLUMN IF NOT EXISTS user_dashboard_role VARCHAR(50) DEFAULT NULL;
-- Contoh nilai: EXECUTIVE | FINANCE_MGR | SALES_MGR | WAREHOUSE_MGR |
-- PROCUREMENT_MGR | MANUFACTURE_MGR | HR_MGR | SUPERVISOR | OPERATOR
Tabel dashboard_user_override (Phase 2 — opsional)
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)
);
Entity Relationship Diagram
erDiagram
tconfuser {
int user_id PK
string username
string user_dashboard_role
}
dashboard_widget_catalog {
int wgt_id PK
string wgt_code
string wgt_name
string wgt_category
string wgt_template_type
string wgt_service_type
string wgt_component
string wgt_interaction_type
int wgt_default_col_span
bool wgt_is_active
}
dashboard_role_config {
int drc_id PK
string drc_role_code
int drc_wgt_id FK
int drc_sort_order
int drc_col_span
bool drc_is_enabled
int drc_refresh_interval_sec
}
dashboard_user_override {
int duo_id PK
int duo_userid FK
int duo_wgt_id FK
bool duo_is_hidden
int duo_sort_order
}
tconfuser ||--o{ dashboard_user_override : "overrides"
dashboard_widget_catalog ||--o{ dashboard_role_config : "configured in"
dashboard_widget_catalog ||--o{ dashboard_user_override : "overridden by"
Class Diagram
classDiagram
class IDashboardConfigServices {
<<interface>>
+GetWidgetConfigsByRoleAsync(roleCode: string) Task~List~DashboardWidgetConfigModel~~
+SaveRoleConfigAsync(roleCode: string, configs: List) Task
+GetAllWidgetsAsync() Task~List~DashboardWidgetCatalogModel~~
+ToggleWidgetActiveAsync(wgtId: int, isActive: bool) Task
}
class DashboardWidgetConfigModel {
+WgtId: int
+WgtCode: string
+WgtName: string
+TemplateType: string
+ServiceType: string
+ComponentName: string
+InteractionType: string
+ColSpan: int
+SortOrder: int
+IsEnabled: bool
+RefreshIntervalSec: int
}
class IWidgetKpiCardSource {
<<interface>>
+GetDataAsync(p: WidgetQueryParams) Task~WidgetKpiCardData~
}
class IWidgetChartSource {
<<interface>>
+GetDataAsync(p: WidgetQueryParams) Task~WidgetChartData~
}
class IWidgetAlertTableSource {
<<interface>>
+GetDataAsync(p: WidgetQueryParams) Task~WidgetAlertTableData~
}
class IWidgetGroupedTableSource {
<<interface>>
+GetDataAsync(p: WidgetQueryParams) Task~WidgetGroupedTableData~
}
class WidgetQueryParams {
+EntityIds: int[]
+BranchIds: int[]
+UserId: int
+DateFrom: DateTime
+DateTo: DateTime
+Extra1: string?
+Extra2: string?
}
class WidgetKpiCardData {
+Value: string
+SubLabel: string
+Icon: string
+ColorScheme: string
+DrillDownUrl: string?
}
class WidgetAlertTableData {
+Columns: List~string~
+Rows: List~AlertTableRow~
+DrillDownUrl: string?
+MaxRows: int
}
class AlertTableRow {
+Cells: List~string~
+Severity: string
+RowDrillDownUrl: string?
}
IWidgetKpiCardSource --> WidgetQueryParams
IWidgetChartSource --> WidgetQueryParams
IWidgetAlertTableSource --> WidgetQueryParams
IWidgetGroupedTableSource --> WidgetQueryParams
IWidgetKpiCardSource --> WidgetKpiCardData
IWidgetAlertTableSource --> WidgetAlertTableData
WidgetAlertTableData --> AlertTableRow
Referensi
- BRD:
dashboard-widget-brd.md - Architecture:
dashboard-widget-architecture.md - Template-Based Architecture:
dashboard-widget-architecture-template-based.md - Widget Master List:
dashboard-widget-master-list.md - Gap Analysis:
dashboard-widget-gap-analysis.md - User Master Enhancement:
dashboard-widget-user-master-enhancement.md