# 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.