Compare commits

1 Commits

Author SHA1 Message Date
sas.fajri
bf06ca01de FHM26062601IBL - tambah PRD dashboard sales owner marketing 2026-06-22 16:51:54 +07:00

View File

@@ -0,0 +1,891 @@
# PRD: IBL Sales Dashboard for Marketing Manager and Owner
**Date:** 2026-06-22
**Scope:** Design only. No code changes yet.
**Project:** `one-api-lab`
**Primary source database:** `one_lab`
**Primary ETL target database:** `one_lab_etl`
---
## Problem
Saat ini data order, pembayaran, company, MOU, omzet type, dan grouping test masih tersebar di banyak tabel operasional. Ini menyulitkan dua audiens utama:
1. Manager marketing yang butuh melihat pertumbuhan omzet, kontribusi company, komposisi layanan, dan peluang follow-up.
2. Pemilik perusahaan yang butuh ringkasan revenue, cash-in, piutang, kolektibilitas, dan tren bisnis secara cepat.
Query langsung ke tabel transaksi operasional juga berisiko:
- definisi angka bisa beda-beda antar report,
- agregasi menjadi berat untuk dashboard,
- filter lintas `company`, `company type group`, `omzet type`, `lunas/tagihan`, dan `nat_subgroup` sulit distandarkan,
- level order dan level item test tercampur.
---
## Goals
1. Menyediakan satu sumber data dashboard yang konsisten untuk owner dan manager marketing.
2. Mendukung filter:
- tanggal,
- company,
- company type group,
- omzet type,
- status lunas,
- status tagihan/outstanding,
- nat subgroup.
3. Memisahkan metrik level order dan level test detail agar angka tidak double count.
4. Menyediakan API PHP yang ringan untuk summary, chart, ranking, dan detail table.
5. Menjaga definisi bisnis agar konsisten dengan pola join report existing di repo.
---
## Non Goals
1. Belum mencakup prediksi sales atau forecasting.
2. Belum mencakup target marketing per staff.
3. Belum mencakup mutasi invoice, retur, atau akuntansi GL penuh.
4. Belum mencakup writeback atau input manual dari dashboard.
---
## Target Users
### A. Marketing Manager
Butuh membaca:
- omzet per periode,
- top company,
- kontribusi per `omzet type`,
- kontribusi per `nat_subgroup`,
- company mana yang turun,
- company mana yang outstanding-nya tinggi.
### B. Owner
Butuh membaca:
- gross sales,
- cash-in,
- outstanding,
- rasio collection,
- distribusi revenue,
- aging tagihan,
- konsentrasi revenue per customer.
---
## Source Tables
Tabel utama yang sudah diverifikasi:
- `t_orderheader`
- `t_orderdetailorder`
- `t_orderdetail`
- `t_test`
- `nat_subgroup`
- `m_company`
- `m_companytypegroup`
- `m_companytype`
- `m_mou`
- `m_omzettype`
- `f_payment`
- `f_payment_orderheader`
### Relasi Utama
#### Relasi order dan company
- `t_orderheader.T_OrderHeaderM_CompanyID -> m_company.M_CompanyID`
- `m_company.M_CompanyM_CompanyGroupTypeID -> m_companytypegroup.M_CompanyTypeGroupID`
- `m_company.M_CompanyM_CompanyTypeID -> m_companytype.M_CompanyTypeID`
#### Relasi order dan MOU / omzet
- `t_orderheader.T_OrderHeaderM_MouID -> m_mou.M_MouID`
- `m_mou.M_MouM_OmzetTypeID -> m_omzettype.M_OmzetTypeID`
#### Relasi order dan payment
- `f_payment.F_PaymentT_OrderHeaderID -> t_orderheader.T_OrderHeaderID`
- `f_payment_orderheader.F_Payment_OrderHeaderID -> t_orderheader.T_OrderHeaderID`
#### Relasi detail dan subgroup
- `t_orderdetail.T_OrderDetailT_TestID -> t_test.T_TestID`
- `t_test.T_TestNat_SubgroupID -> nat_subgroup.Nat_SubGroupID`
---
## Key Business Rules
1. `sales order amount` memakai basis `t_orderheader.T_OrderHeaderTotal`.
2. `cash-in amount` memakai basis akumulasi `f_payment.F_PaymentTotal` aktif.
3. `lunas` memakai flag utama `f_payment_orderheader.F_Payment_OrderHeaderIsLunas`.
4. `outstanding amount` memakai prioritas:
- `f_payment_orderheader.F_Payment_OrderHeaderChargeAmount` bila tersedia,
- fallback `order_total - payment_total`.
5. `nat_subgroup` adalah atribut level test detail, bukan level order header.
6. Satu order bisa punya banyak `nat_subgroup`, sehingga filter subgroup harus memakai mart detail atau bridge.
7. Hanya data aktif yang dipakai:
- `T_OrderHeaderIsActive = 'Y'`
- `T_OrderDetailIsActive = 'Y'`
- `M_CompanyIsActive = 'Y'`
- `M_MouIsActive = 'Y'`
- `M_OmzetTypeIsActive = 'Y'`
- `Nat_SubGroupIsActive = 'Y'`
- `F_PaymentIsActive = 'Y'`
8. Semua tabel ETL baru ditempatkan di database `one_lab_etl`, bukan di `one_lab`, agar query dashboard tidak membebani database transaksional.
9. Struktur tabel ETL baru tidak memakai foreign key constraint. Relasi dijaga di level query dan index saja.
10. Naming tabel dan kolom baru mengikuti gaya schema inti `one_lab`:
- nama tabel: lowercase dengan underscore,
- nama kolom: prefix nama entitas tabel sendiri,
- hindari gaya generik seperti `created_at`, `updated_at`, atau nama kolom tanpa prefix domain.
---
## Product Scope
Dashboard dibagi menjadi 4 blok:
1. Executive summary
2. Sales composition
3. Collection and receivable health
4. Customer and subgroup drilldown
---
## KPI Definitions
### Core KPI
- `total_sales`: total nilai order
- `total_paid`: total pembayaran masuk
- `total_outstanding`: sisa tagihan
- `collection_rate`: `total_paid / total_sales`
- `lunas_order_count`: jumlah order lunas
- `open_order_count`: jumlah order belum lunas
- `active_company_count`: jumlah company yang punya transaksi
### Breakdown KPI
- `sales_by_company`
- `sales_by_company_type_group`
- `sales_by_omzet_type`
- `sales_by_nat_subgroup`
- `outstanding_by_company`
- `outstanding_by_aging_bucket`
### Trend KPI
- `daily_sales_trend`
- `monthly_sales_trend`
- `daily_cashin_trend`
- `monthly_cashin_trend`
---
## Dashboard Views
### A. Executive Summary
Cards:
- total sales
- total paid
- total outstanding
- collection rate
- lunas order
- active company
Charts:
- line chart `sales vs cash-in`
- donut `lunas vs belum lunas`
- bar chart `outstanding aging bucket`
### B. Sales Composition
Charts:
- bar `sales by omzet type`
- bar `sales by company type group`
- horizontal bar `top 10 company by sales`
- pareto `sales by nat_subgroup`
### C. Collection Health
Charts:
- bar `top outstanding company`
- stacked bar `lunas vs belum lunas by omzet type`
- aging chart `0-30 / 31-60 / 61-90 / 90+ days`
### D. Drilldown Tables
Tables:
- order summary table
- company ranking table
- subgroup contribution table
- outstanding exception table
---
## Filter Requirements
Global filters:
1. date range
2. company
3. company type group
4. omzet type
5. payment status:
- all
- lunas
- belum lunas
6. tagihan status:
- all
- ada outstanding
- outstanding nol
7. nat subgroup
### Filter Semantics
#### Jika filter `nat_subgroup` dipakai
- KPI summary tetap harus akurat di level order.
- Implementasi awal yang aman:
- summary berbasis mart detail dengan `COUNT(DISTINCT order_id)` untuk order count,
- nominal sales untuk subgroup memakai `detail_total`,
- summary order total saat filter subgroup aktif harus diberi label jelas: `sales contribution by subgroup`, bukan `full order revenue`, agar tidak misleading.
#### Rekomendasi produk
Pisahkan dua mode saat filter subgroup aktif:
1. `Order view`
- subgroup dipakai sebagai `has subgroup in order`
- nilai sales tetap full order total
2. `Contribution view`
- subgroup dipakai pada level detail
- nilai sales memakai `detail_total`
Untuk phase 1, lebih aman default ke `Contribution view`.
---
## Data Model Design
### Naming Convention for New ETL Tables
Prinsip penamaan:
- database target: `one_lab_etl`
- nama tabel mengikuti pola lowercase underscore
- nama kolom mengikuti pola prefix entitas tabel seperti `T_OrderHeader...`, `M_Company...`, `F_Payment...`
- tidak memakai foreign key
- relasi dioptimalkan dengan `PRIMARY KEY`, `UNIQUE KEY`, dan `INDEX`
### 1. `sales_dashboard_order`
Grain: `1 row = 1 orderheader`
Tujuan:
- dashboard utama owner,
- KPI order-level,
- summary payment / receivable.
#### Proposed Columns
```sql
SalesDashboardOrderID BIGINT PK AUTO_INCREMENT
SalesDashboardOrderT_OrderHeaderID BIGINT NOT NULL
SalesDashboardOrderDate DATETIME NOT NULL
SalesDashboardOrderDateOnly DATE NOT NULL
SalesDashboardOrderT_OrderHeaderLabNumber VARCHAR(25) NULL
SalesDashboardOrderT_OrderHeaderStatus VARCHAR(150) NULL
SalesDashboardOrderM_CompanyID INT NULL
SalesDashboardOrderM_CompanyName VARCHAR(100) NULL
SalesDashboardOrderM_CompanyTypeID INT NULL
SalesDashboardOrderM_CompanyTypeName VARCHAR(50) NULL
SalesDashboardOrderM_CompanyTypeGroupID INT NULL
SalesDashboardOrderM_CompanyTypeGroupName VARCHAR(100) NULL
SalesDashboardOrderM_MouID INT NULL
SalesDashboardOrderM_MouName VARCHAR(100) NULL
SalesDashboardOrderM_OmzetTypeID INT NULL
SalesDashboardOrderM_OmzetTypeName VARCHAR(25) NULL
SalesDashboardOrderTotal DECIMAL(18,2) NOT NULL DEFAULT 0.00
SalesDashboardOrderPaymentTotal DECIMAL(18,2) NOT NULL DEFAULT 0.00
SalesDashboardOrderOutstandingTotal DECIMAL(18,2) NOT NULL DEFAULT 0.00
SalesDashboardOrderIsLunas CHAR(1) NOT NULL DEFAULT 'N'
SalesDashboardOrderHasOutstanding CHAR(1) NOT NULL DEFAULT 'N'
SalesDashboardOrderLastPaymentDate DATE NULL
SalesDashboardOrderAgingDays INT NOT NULL DEFAULT 0
SalesDashboardOrderAgingBucket VARCHAR(20) NOT NULL DEFAULT '0-30'
SalesDashboardOrderDetailCount INT NOT NULL DEFAULT 0
SalesDashboardOrderSourceLastUpdated DATETIME NOT NULL
SalesDashboardOrderCreated DATETIME NOT NULL
SalesDashboardOrderLastUpdated DATETIME NOT NULL
PRIMARY KEY (SalesDashboardOrderID)
UNIQUE KEY SalesDashboardOrderT_OrderHeaderID (SalesDashboardOrderT_OrderHeaderID)
KEY SalesDashboardOrderDateOnly (SalesDashboardOrderDateOnly)
KEY SalesDashboardOrderM_CompanyID (SalesDashboardOrderM_CompanyID)
KEY SalesDashboardOrderM_CompanyTypeGroupID (SalesDashboardOrderM_CompanyTypeGroupID)
KEY SalesDashboardOrderM_OmzetTypeID (SalesDashboardOrderM_OmzetTypeID)
KEY SalesDashboardOrderIsLunas (SalesDashboardOrderIsLunas)
KEY SalesDashboardOrderHasOutstanding (SalesDashboardOrderHasOutstanding)
KEY SalesDashboardOrderAgingBucket (SalesDashboardOrderAgingBucket)
```
### 2. `sales_dashboard_order_subgroup`
Grain: `1 row = 1 orderdetail`
Tujuan:
- breakdown `nat_subgroup`,
- analisis kontribusi layanan,
- drilldown marketing.
#### Proposed Columns
```sql
SalesDashboardOrderSubGroupID BIGINT PK AUTO_INCREMENT
SalesDashboardOrderSubGroupT_OrderDetailID BIGINT NOT NULL
SalesDashboardOrderSubGroupT_OrderHeaderID BIGINT NOT NULL
SalesDashboardOrderSubGroupDate DATETIME NOT NULL
SalesDashboardOrderSubGroupDateOnly DATE NOT NULL
SalesDashboardOrderSubGroupM_CompanyID INT NULL
SalesDashboardOrderSubGroupM_CompanyName VARCHAR(100) NULL
SalesDashboardOrderSubGroupM_CompanyTypeGroupID INT NULL
SalesDashboardOrderSubGroupM_CompanyTypeGroupName VARCHAR(100) NULL
SalesDashboardOrderSubGroupM_MouID INT NULL
SalesDashboardOrderSubGroupM_MouName VARCHAR(100) NULL
SalesDashboardOrderSubGroupM_OmzetTypeID INT NULL
SalesDashboardOrderSubGroupM_OmzetTypeName VARCHAR(25) NULL
SalesDashboardOrderSubGroupT_TestID INT NOT NULL
SalesDashboardOrderSubGroupT_TestCode VARCHAR(25) NULL
SalesDashboardOrderSubGroupT_TestName VARCHAR(100) NOT NULL
SalesDashboardOrderSubGroupNat_SubGroupID INT NULL
SalesDashboardOrderSubGroupNat_SubGroupCode VARCHAR(10) NULL
SalesDashboardOrderSubGroupNat_SubGroupName VARCHAR(50) NULL
SalesDashboardOrderSubGroupPriceTotal DECIMAL(18,2) NOT NULL DEFAULT 0.00
SalesDashboardOrderSubGroupNetTotal DECIMAL(18,2) NOT NULL DEFAULT 0.00
SalesDashboardOrderSubGroupIsLunas CHAR(1) NOT NULL DEFAULT 'N'
SalesDashboardOrderSubGroupSourceLastUpdated DATETIME NOT NULL
SalesDashboardOrderSubGroupCreated DATETIME NOT NULL
SalesDashboardOrderSubGroupLastUpdated DATETIME NOT NULL
PRIMARY KEY (SalesDashboardOrderSubGroupID)
UNIQUE KEY SalesDashboardOrderSubGroupT_OrderDetailID (SalesDashboardOrderSubGroupT_OrderDetailID)
KEY SalesDashboardOrderSubGroupT_OrderHeaderID (SalesDashboardOrderSubGroupT_OrderHeaderID)
KEY SalesDashboardOrderSubGroupDateOnly (SalesDashboardOrderSubGroupDateOnly)
KEY SalesDashboardOrderSubGroupM_CompanyID (SalesDashboardOrderSubGroupM_CompanyID)
KEY SalesDashboardOrderSubGroupM_CompanyTypeGroupID (SalesDashboardOrderSubGroupM_CompanyTypeGroupID)
KEY SalesDashboardOrderSubGroupM_OmzetTypeID (SalesDashboardOrderSubGroupM_OmzetTypeID)
KEY SalesDashboardOrderSubGroupNat_SubGroupID (SalesDashboardOrderSubGroupNat_SubGroupID)
KEY SalesDashboardOrderSubGroupIsLunas (SalesDashboardOrderSubGroupIsLunas)
```
### 3. `sales_dashboard_etl_log`
Tujuan:
- audit ETL,
- troubleshooting refresh,
- monitoring last successful sync.
#### Proposed Columns
```sql
SalesDashboardEtlLogID BIGINT PK AUTO_INCREMENT
SalesDashboardEtlLogJobName VARCHAR(100) NOT NULL
SalesDashboardEtlLogStarted DATETIME NOT NULL
SalesDashboardEtlLogFinished DATETIME NULL
SalesDashboardEtlLogStatus VARCHAR(20) NOT NULL
SalesDashboardEtlLogLastWatermark DATETIME NULL
SalesDashboardEtlLogRowsInserted INT NOT NULL DEFAULT 0
SalesDashboardEtlLogRowsUpdated INT NOT NULL DEFAULT 0
SalesDashboardEtlLogRowsDeleted INT NOT NULL DEFAULT 0
SalesDashboardEtlLogErrorMessage TEXT NULL
SalesDashboardEtlLogCreated DATETIME NOT NULL
PRIMARY KEY (SalesDashboardEtlLogID)
KEY SalesDashboardEtlLogJobName (SalesDashboardEtlLogJobName)
KEY SalesDashboardEtlLogStatus (SalesDashboardEtlLogStatus)
KEY SalesDashboardEtlLogStarted (SalesDashboardEtlLogStarted)
```
---
## ETL Strategy
### Recommended Approach
Gunakan ETL incremental ke tabel fisik di database `one_lab_etl`, bukan query live langsung dari tabel operasional `one_lab` untuk semua chart.
### Why
1. Query dashboard akan jauh lebih cepat.
2. Definisi bisnis menjadi terkunci dan konsisten.
3. Filter multi-dimensi lebih gampang.
4. Aman untuk kebutuhan chart owner yang sering membuka data agregat.
### Load Frequency
Phase 1:
- scheduled refresh tiap `30 menit`
- plus endpoint manual refresh untuk dev/admin
Phase 2 opsional:
- refresh per `5 menit` atau near real-time
### Watermark Incremental
Gunakan `MAX(last_updated)` dari sumber:
- `t_orderheader.T_OrderHeaderLastUpdated`
- `t_orderdetail.T_OrderDetailLastUpdated`
- `f_payment.F_PaymentLastUpdated`
Untuk order-level ETL, satu order harus di-rebuild jika salah satu dari tiga sumber berubah.
### ETL Steps
#### Job 1: Build `one_lab_etl.sales_dashboard_order`
1. Ambil order aktif yang berubah sejak watermark.
2. Join:
- `m_company`
- `m_companytype`
- `m_companytypegroup`
- `m_mou`
- `m_omzettype`
3. Aggregate payment aktif per order dari `f_payment`.
4. Ambil status lunas dari `f_payment_orderheader`.
5. Hitung:
- `payment_amount`
- `outstanding_amount`
- `has_outstanding`
- `aging_days`
- `aging_bucket`
6. Upsert ke `one_lab_etl.sales_dashboard_order`.
#### Job 2: Build `one_lab_etl.sales_dashboard_order_subgroup`
1. Ambil detail aktif yang berubah sejak watermark, atau detail milik order yang berubah.
2. Join:
- `t_test`
- `nat_subgroup`
- `m_company`
- `m_companytypegroup`
- `m_mou`
- `m_omzettype`
3. Tempel status lunas dari hasil order fact.
4. Upsert ke `one_lab_etl.sales_dashboard_order_subgroup`.
#### Job 3: ETL log
1. Tulis status awal job.
2. Simpan jumlah row insert/update.
3. Simpan watermark akhir.
4. Simpan error jika gagal.
---
## API Design
### Controller Proposal
File baru:
`application/controllers/dashboard/Sales.php`
### Endpoint Proposal
#### 1. Summary KPI
**Route:** `GET /dashboard/sales/summary`
Purpose:
- cards executive summary
Query params:
- `date_start`
- `date_end`
- `company_id`
- `company_type_group_id`
- `omzet_type_id`
- `is_lunas`
- `has_outstanding`
- `nat_subgroup_id`
- `view_mode` = `order|contribution`
Response:
```json
{
"status": "OK",
"data": {
"total_sales": 0,
"total_paid": 0,
"total_outstanding": 0,
"collection_rate": 0,
"lunas_order_count": 0,
"open_order_count": 0,
"active_company_count": 0
}
}
```
#### 2. Sales Trend
**Route:** `GET /dashboard/sales/trend`
Purpose:
- line chart sales vs cash-in
Query params:
- semua filter global
- `granularity` = `day|month`
Response:
```json
{
"status": "OK",
"data": [
{
"period": "2026-06-01",
"sales_amount": 0,
"paid_amount": 0,
"outstanding_amount": 0
}
]
}
```
#### 3. Breakdown
**Route:** `GET /dashboard/sales/breakdown`
Purpose:
- reusable endpoint untuk donut/bar by dimension
Query params:
- semua filter global
- `dimension` = `company|company_type_group|omzet_type|nat_subgroup|aging_bucket|lunas_status`
- `metric` = `sales|paid|outstanding|order_count`
- `limit`
Response:
```json
{
"status": "OK",
"data": [
{
"dimension_id": "1",
"dimension_name": "Kimia Klinik",
"metric_value": 0,
"order_count": 0
}
]
}
```
#### 4. Company Ranking
**Route:** `GET /dashboard/sales/company-ranking`
Purpose:
- top/bottom company
Query params:
- semua filter global
- `sort_by` = `sales|paid|outstanding|collection_rate`
- `sort_dir` = `asc|desc`
- `limit`
Response:
```json
{
"status": "OK",
"data": [
{
"company_id": 0,
"company_name": "",
"company_type_group_name": "",
"omzet_type_name": "",
"sales_amount": 0,
"paid_amount": 0,
"outstanding_amount": 0,
"collection_rate": 0,
"order_count": 0
}
]
}
```
#### 5. Order Detail Table
**Route:** `GET /dashboard/sales/orders`
Purpose:
- detail table order-level
Query params:
- semua filter global
- `page`
- `page_size`
- `sort_by`
- `sort_dir`
Response:
```json
{
"status": "OK",
"data": {
"rows": [
{
"order_id": 0,
"order_date": "",
"lab_number": "",
"company_name": "",
"company_type_group_name": "",
"mou_name": "",
"omzet_type_name": "",
"gross_sales_amount": 0,
"payment_amount": 0,
"outstanding_amount": 0,
"is_lunas": "N",
"aging_bucket": "0-30"
}
],
"pagination": {
"page": 1,
"page_size": 20,
"total_rows": 0
}
}
}
```
#### 6. Subgroup Contribution Table
**Route:** `GET /dashboard/sales/subgroup-contribution`
Purpose:
- detail analisis subgroup untuk marketing
Query params:
- semua filter global
- `page`
- `page_size`
Response:
```json
{
"status": "OK",
"data": {
"rows": [
{
"nat_subgroup_id": 0,
"nat_subgroup_name": "",
"sales_amount": 0,
"detail_count": 0,
"company_count": 0
}
]
}
}
```
#### 7. ETL Refresh
**Route:** `POST /dashboard/sales/refresh`
Purpose:
- manual trigger ETL oleh admin/dev
Response:
```json
{
"status": "OK",
"data": {
"job_name": "sales_dashboard_refresh",
"message": "Refresh started"
}
}
```
---
## PHP Function Proposal
Di controller `dashboard/Sales.php`, fungsi yang disarankan:
- `summary()`
- `trend()`
- `breakdown()`
- `company_ranking()`
- `orders()`
- `subgroup_contribution()`
- `refresh()`
Di library/service baru, misalnya `application/libraries/SalesDashboardService.php`:
- `buildFilter(array $params): array`
- `getSummary(array $filters): array`
- `getTrend(array $filters, string $granularity): array`
- `getBreakdown(array $filters, string $dimension, string $metric): array`
- `getCompanyRanking(array $filters, array $paging): array`
- `getOrders(array $filters, array $paging): array`
- `getSubgroupContribution(array $filters, array $paging): array`
- `runRefresh(?string $forcedFrom = null): array`
- `refreshOrderFact(?string $watermark = null): array`
- `refreshSubgroupFact(?string $watermark = null): array`
---
## SQL Semantics by Use Case
### Summary without subgroup filter
Source utama:
- `one_lab_etl.sales_dashboard_order`
### Summary with subgroup filter
Source utama:
- `one_lab_etl.sales_dashboard_order_subgroup`
Logic:
- bila `view_mode = contribution`, agregasi nominal dari detail fact
- bila `view_mode = order`, filter order yang punya subgroup terkait lalu join ke order fact
### Trend sales vs payment
Gunakan:
- sales berdasarkan `order_date_key`
- payment berdasarkan `payment_date_last` untuk phase 1 sederhana
Catatan:
Jika owner butuh cash-in by actual payment day yang akurat penuh, phase berikutnya perlu mart payment harian terpisah.
---
## Recommended Future Extension
### Optional Table: `sales_dashboard_payment`
Grain: `1 row = 1 payment`
Dipakai bila nanti butuh:
- cash-in trend murni berdasarkan tanggal bayar,
- payment method analysis,
- rekonsiliasi koleksi lebih detail.
Untuk phase 1, tabel ini opsional.
---
## UAT Checklist
1. Summary total sales sesuai order aktif pada periode yang sama.
2. Total paid sesuai akumulasi `f_payment` aktif.
3. Lunas count sesuai flag `f_payment_orderheader`.
4. Outstanding count dan nominal sesuai expected manual sample.
5. Filter `company` bekerja konsisten di summary, chart, dan table.
6. Filter `company type group` bekerja konsisten.
7. Filter `omzet type` bekerja konsisten.
8. Filter `nat_subgroup` menampilkan kontribusi subgroup yang benar.
9. Top company dan subgroup ranking konsisten dengan sample query manual.
10. Refresh ETL tidak membuat duplicate row pada mart.
---
## Risks
1. Definisi `lunas` dan `outstanding` bisa berbeda antara report lama dan kebutuhan dashboard baru.
2. Jika filter subgroup dipaksa memakai full order value tanpa mode khusus, angka bisa misleading.
3. Data payment historis bisa perlu mart terpisah bila owner ingin cashflow by actual payment day.
4. Jika update transaksi lama sering terjadi, ETL incremental harus cukup hati-hati memilih order yang direbuild.
---
## Open Questions for Approval
1. Saat filter `nat_subgroup` aktif, apakah default yang diinginkan:
- `contribution view` berbasis detail amount, atau
- `order view` berbasis full order amount?
2. Untuk KPI owner, apakah `sales` mau dianggap:
- `gross order total`, atau
- `net detail total` dari item setelah diskon?
3. Apakah cash-in trend perlu by `actual payment date` dari awal phase 1?
4. Apakah perlu dimensi tambahan `marketing/staff` karena `m_company` punya `M_CompanyM_StaffID`?
---
## Recommended Phase Plan
### Phase 1
- buat tabel mart:
- `sales_dashboard_order`
- `sales_dashboard_order_subgroup`
- `sales_dashboard_etl_log`
- buat endpoint:
- `summary`
- `trend`
- `breakdown`
- `company_ranking`
- `orders`
- `subgroup_contribution`
- `refresh`
### Phase 2
- tambah `sales_dashboard_payment`
- tambah chart cash-in by payment date murni
- tambah analisis staff marketing
- tambah alert outstanding company besar
---
## Recommendation
Saya rekomendasikan lanjut implementasi dengan pendekatan:
1. mart fisik untuk order-level dan subgroup-level,
2. API PHP berbasis mart, bukan query operasional mentah,
3. default subgroup mode = `contribution view`,
4. phase 1 fokus owner + marketing summary yang cepat dan stabil,
5. payment mart detail dijadikan phase 2 bila dibutuhkan owner untuk cashflow yang lebih presisi.