diff --git a/docs/superpowers/specs/2026-06-22-ibl-sales-dashboard-owner-marketing-prd.md b/docs/superpowers/specs/2026-06-22-ibl-sales-dashboard-owner-marketing-prd.md new file mode 100644 index 00000000..e1f7c77f --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-ibl-sales-dashboard-owner-marketing-prd.md @@ -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.