Files
BE_IBL/docs/superpowers/specs/2026-06-22-ibl-sales-dashboard-owner-marketing-prd.md

22 KiB

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

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

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

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

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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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.


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?

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.