Lewati ke konten utama

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

AktorDeskripsi
AdministratorMengelola 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
DeveloperMembuat widget baru — data service + (opsional) custom component
SystemMerender 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

IDUse CaseAktorPre-ConditionPost-Condition
UC-01Kelola Widget CatalogAdministratorLogin sebagai adminWidget aktif/nonaktif diperbarui
UC-02Konfigurasi Dashboard per RoleAdministratorWidget catalog tersediaKonfigurasi role tersimpan di DB
UC-03Set Dashboard Role pada UserAdministratorUser master tersediauser_dashboard_role diperbarui
UC-04Lihat DashboardUserLogin berhasilDashboard sesuai role ditampilkan
UC-05Refresh WidgetUserDashboard ditampilkanData widget diperbarui
UC-06Drill-downUserWidget memiliki DrillDownUrlHalaman report/transaksi terbuka
UC-07Daftarkan Widget BaruDeveloperWidget component dan service sudah dibuatWidget tersedia di catalog
UC-08Resolve Dashboard Role saat LoginSystemUser login, user_dashboard_role tersediaRole terbaca, konfigurasi widget di-load
UC-09Render Dynamic ComponentSystemKonfigurasi widget di-loadWidget dirender via DynamicComponent
UC-10Auto RefreshSystemWidget ditampilkan, interval > 0Data 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