Compare commits
1 Commits
main
...
fhm2606260
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf06ca01de |
@@ -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.
|
||||
Reference in New Issue
Block a user