Imported from bugarnusantarajaya-berjaya678/efm-admins (
REACT-APP/.claude/skills/efm-design-standards/SKILL.md). Install upstream withnpx skills add bugarnusantarajaya-berjaya678/efm-admins --skill efm-design-standards. Copyright stays with the author.
EFM V2 Design Standards
Reference rules + ready-to-use code patterns for the EFM V2 admin dashboard. Always check this skill before writing or editing any UI code, generating dummy data, or creating document/ID numbers in this project.
1. Brand Colors & Fonts
Primary Colors
| Color | Hex | Usage | Tailwind |
|---|---|---|---|
| Navy (primary/dark) | #1E1C43 |
sidebar bg, headers, primary buttons, bold important values, active states | bg-[#1E1C43], text-[#1E1C43] |
| Accent/Orange | #E05945 |
active menu highlight, CTA buttons, alerts, price/total emphasis, left border accent (border-l-4) |
bg-[#E05945], text-[#E05945] |
| Background | #F5F5F7 |
page background (outside cards) | bg-[#F5F5F7] |
| Surface/Card | #FFFFFF |
all cards, tables, modals | bg-white |
Status/Semantic Colors (badges, alerts)
| Meaning | Tailwind |
|---|---|
| Success / Lunas / Aktif | bg-green-50 text-green-700 border-green-200 |
| Warning / Pending / Draft | bg-yellow-50 text-yellow-700 border-yellow-200 |
| Error / Overdue / Gagal | bg-red-50 text-red-700 border-red-200 |
| Info / Terkirim / Proses | bg-blue-50 text-blue-700 border-blue-200 |
| Neutral / Inactive | bg-gray-50 text-gray-500 border-gray-200 |
Badge base pattern: px-2 py-1 text-xs rounded-full font-medium + color combo above.
Font
- Family: Poppins globally (body). No other font families anywhere in the project.
Font Weight Usage
| Weight | Where |
|---|---|
font-black |
ONLY large document titles (e.g. "INVOICE" header) |
font-bold |
section titles, KPI numbers, important values |
font-semibold |
table data, names, values in info cards |
font-medium |
buttons, badges |
| normal | body text, descriptions |
2. ID / Document Numbering Format
General pattern: [DOCTYPE]-[MODULE]-[YY]-[SEQUENCE] (e.g. INV-PP-26-0001)
YY= 2-digit year, sequence resets to0001every Jan 1SEQUENCE= 4-digit zero-padded, increments per doc type per module per year- Exception: Lead IDs are permanent — no year, never reset
| Doc | PP | B2B | Event |
|---|---|---|---|
| Lead | LP-0001 |
LB-0001 |
LE-0001 |
| Screening/Survei/Konsultasi | SCR-26-0001 |
SVY-26-0001 |
KNS-26-0001 |
| Order | PP-26-0001 (#PP-26-0001) |
B2B-26-0001 (#B2B-26-0001) |
EV-26-0001 (#EV-26-0001) |
| Invoice | INV-PP-26-0001 |
INV-B2B-26-0001 |
INV-EV-26-0001 |
| Receipt | RCP-PP-26-0001 |
RCP-B2B-26-0001 |
RCP-EV-26-0001 |
| Agreement/LOI | AGR-PP-26-0001 |
LOI-EFM-B2B-26-0001 |
LOI-EFM-EVENT-26-0001 |
Display rules
- Order IDs always shown with
#prefix in UI headers/titles and when referenced inside other documents (e.g. "Invoice #INV-PP-26-0011") - Table cells: no
#prefix in general, EXCEPT the Order ID column, which does use# - All ID values in tables:
font-semibold text-[#1E1C43] whitespace-nowrap— JANGANfont-mediumataufont-normal, selalu semibold navy
Dummy/placeholder data: always follow this exact format. Never generic IDs like ORD-001 or INV-001 without module code and year.
3. Font Sizes (Tables, Labels, KPI Cards)
⚠️ Recurring bug in this project: fonts too large/inconsistent. Follow exactly.
Info card fields
- Label:
text-xs text-gray-400 uppercase tracking-wide - Value:
text-sm font-semibold text-gray-800 - Container:
bg-gray-50 rounded-lg p-3
Form input fields (detail/form pages — bukan tabel list)
- Normal (editable):
w-full text-sm border border-gray-200 rounded-lg px-3 py-2 focus:outline-none focus:border-[#1E1C43] bg-white - Read-only / auto-filled (locked):
w-full text-sm border border-gray-100 rounded-lg px-3 py-2 bg-gray-50 text-gray-500 cursor-not-allowed - Label form:
text-[10px] font-semibold text-gray-500 uppercase tracking-wide mb-1 block - Gunakan per-field locking (Section 11 di
efm-component-patterns) untuk field auto-fill dari order, bukan wrapperpointer-events-none
Section titles
text-sm font-bold text-[#1E1C43], often withborder-l-4 border-[#E05945] pl-3
Table
- Header:
text-[10px] font-semibold text-gray-500 uppercase tracking-wider whitespace-nowrap - Body/data:
text-xs text-gray-600(list pages pakaitext-xs, bukantext-sm) - Names/important data:
text-xs font-medium text-gray-900 - ID columns:
text-xs font-semibold text-[#1E1C43] whitespace-nowrap— WAJIB semibold navy, berlaku untuk SEMUA kolom ID: No. Invoice, No. Receipt, No. Agreement, Order ID, Lead ID, dsb. - Amount/angka:
text-xs font-semibold text-gray-600
KPI cards / Stat cards (list page)
- Big number:
text-2xl font-bold text-[#1E1C43] - Label:
text-xs uppercase tracking-wide text-gray-400 - Container:
bg-white rounded-xl border border-gray-200 p-4
⚠️ Wajib Bahasa Indonesia: semua nilai label dan sub di stat card HARUS dalam Bahasa Indonesia — TIDAK boleh Inggris ("Paid" → "Lunas", "Overdue" → "Jatuh Tempo", "Expired" → "Kadaluarsa", dll).
⚠️ Dilarang emoji di nilai sub — cukup teks deskriptif singkat (bukan "✅ Terbayar", tapi "Sudah dibayar").
Document headers (Invoice/Receipt titles)
text-4xl font-black— ONLY place this scale/weight is used, ever
Badges: text-xs font-medium, px-2 py-1 rounded-full
⚠️ DILARANG text-[10px] untuk badge dan tombol inline — selalu gunakan text-xs:
- Badge status di dalam tabel, kartu, atau modal:
text-xs font-medium rounded-full— BUKANtext-[10px] font-medium - Tombol Edit/Simpan/Batal/Upload yang berada di dalam nested sub-section card:
text-xs— BUKANtext-[10px] text-[10px]hanya boleh dipakai untuk field label (label di atas input) dan table header (<th>), tidak untuk nilai/konten apapun
Nested sub-section info card (field di dalam kartu klien, kartu kesehatan, dsb.)
- Nilai field:
text-xs font-semibold text-gray-700— WAJIBfont-semibold, bukantext-gray-600tanpa semibold - Italic fallback placeholder (mis. "Belum diisi"): tambahkan
font-normaleksplisit ke<span>italic supaya tidak ikut bold dari parent — contoh:<span className="italic text-gray-400 font-normal">Belum diisi</span>
Buttons — Warna & Ukuran
Aturan Warna Tombol (FINAL — berlaku semua modul PP, B2B, Event)
| Warna | Hex | Gunakan untuk | Contoh |
|---|---|---|---|
| Orange (CTA) | #E05945 |
Aksi bisnis utama yang menggerakkan workflow — maks 1 per halaman/section | Buat Order Baru, Konfirmasi Pembayaran, Kirim Invoice, Tambah Lead |
| Navy (Primary) | #1E1C43 |
Save/submit standard di form dan modal | Simpan, Lanjut, Terapkan |
| Green (Approve/Upload) | #27AE60 hover #1E8449 |
Aksi konfirmasi/upload yang menyelesaikan sebuah siklus pembayaran atau verifikasi | Upload Bukti Bayar, Konfirmasi Honorarium, Approve |
| Gray secondary | border-gray-300 text-gray-600 |
Navigasi & pembatalan | Kembali, Batal, Reset, Download PDF |
| Red | bg-red-600 |
Aksi destruktif | Hapus, Delete |
⚠️ Kembali SELALU gray secondary — BUKAN orange, BUKAN navy. Berlaku di semua modul dan semua page.
⚠️ Orange adalah CTA eksklusif — jika 2+ tombol orange ada di 1 baris, salah satunya salah; ganti yang bukan CTA utama ke navy atau gray.
⚠️ Dilarang: text-[13px] (ganti ke text-sm), py-[7px] (ganti ke py-2), rounded-xl pada tombol (standar rounded-lg).
Aturan Ukuran Tombol per Konteks
| Konteks | Class dasar (tambahkan warna sesuai tipe) |
|---|---|
| Header page — action row di kanan atas halaman | flex items-center gap-1.5 h-8 px-3 rounded-lg text-xs font-semibold transition-colors |
| Header page — tombol dropdown navigasi (mis. Dokumen, Pengaturan) | Tombol sama persis: h-8 px-3 text-xs font-semibold + ChevronDown size={12}. Dropdown panel: absolute top-full mt-1.5 w-48 bg-white border border-gray-200 rounded-xl shadow-lg overflow-hidden z-30. Item: w-full flex items-center gap-2.5 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors. Divider antar item: <div className="border-t border-gray-100" />. ⚠️ Selalu pakai divider antar item — dropdown tanpa divider tidak konsisten. |
| List page — tombol Buat/Tambah Baru | flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold transition-colors |
| Modal footer — Batal / Simpan | px-4 py-2 rounded-lg text-sm font-semibold transition-colors |
| Section CTA standalone — Konfirmasi, Kirim | flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-semibold transition-colors |
| Inline kecil — Edit/Lihat di dalam baris tabel | px-2.5 py-1.5 rounded-lg text-xs font-medium transition-colors |
Common mistake to avoid: never use text-base, text-lg, or text-xl for field values, table data, or labels. text-sm is correct for form inputs and section titles in detail/form pages — but NOT for table body cells in list pages (use text-xs there). text-[10px] is standard for all field labels (both form pages and list-page table headers).
3b. Full-Page Form / Sub-page — Header Card Standard
Lima varian header ada di project ini — pilih berdasarkan tipe halaman:
- First-class entity list page (Leads, Orders, ProgramDB, dll — berdiri sendiri, akses dari sidebar): gunakan Tier 1 telanjang — lihat Section 3c, tanpa card,
text-[22px], tidak ada tombol Kembali.- Sub-document list page (Invoice list, Receipt list, Screening list, Documents list — turunan dari order/lead): gunakan Tier 2 pola 3c —
flex-col sm:flex-row, ada card, ada tombol Kembali.- Form / sub-page (Assessment Detail, Screening Detail, Konsultasi Detail, Order New, dll): gunakan ikon bulat navy — pola di section ini (3b).
- Entity detail page (Lead Detail, Order Detail): gunakan initials avatar berwarna — lihat
efm-component-patternsSection 15.- Document detail page (Invoice Detail, Receipt Detail, Agreement Detail): gunakan pola flat single-row — lihat Section 3d.
⚠️ Jangan gunakan pola 3b (eyebrow + flex items-start) untuk list page — list page pakai pola 3c.
⚠️ Document detail page (Invoice/Receipt/Agreement) bukan "entity detail" — gunakan pola 3d (flat row, ikon w-10, judul text-base), bukan 3b (ikon w-12, judul text-lg).
Header card untuk semua sub-page forms (Assessment Detail, Screening Detail, Konsultasi Detail, Order New, dll). Ini adalah struktur WAJIB — jangan improvisasi dengan flex-col sm:flex-row atau variasi lain.
{/* Header Card — selalu full-bleed tanpa outer horizontal padding */}
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-5 mb-6">
<div className="flex items-start justify-between gap-4 flex-wrap">
{/* Kiri: ikon bulat + judul + sub-info */}
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-[#1E1C43] flex items-center justify-center shrink-0">
<IconName size={22} className="text-white" />
</div>
<div>
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider mb-0.5">
Module — Sub-context
</p>
<h1 className="text-lg font-bold text-[#1E1C43] leading-tight">
Judul Halaman / ID Record
</h1>
<p className="text-xs text-gray-400 mt-1">
Sub-info opsional (nama klien, status, dll)
</p>
</div>
</div>
{/* Kanan: tombol Kembali (dan tombol lain jika perlu) */}
<div className="flex items-center gap-2 flex-shrink-0">
<button
onClick={handleBack}
className="flex items-center gap-1.5 h-8 px-3 rounded-lg border border-gray-300 text-gray-600 text-xs font-semibold hover:bg-gray-50 transition-colors"
>
<ArrowLeft size={12} /> Kembali
</button>
</div>
</div>
</div>
Aturan wajib:
- Outer wrapper:
flex items-start justify-between gap-4 flex-wrap— JANGANflex-col sm:flex-row sm:items-center sm:justify-between items-start(bukanitems-center): ikon dan teks di-align top, tombol Kembali di top-right — tidak bergeser ke tengah saat tinggi berbeda- Tombol kanan: selalu dalam wrapper
flex items-center gap-2 flex-shrink-0agar tidak shrink saat ruang sempit - Tombol Kembali: selalu gray secondary (
border border-gray-300 text-gray-600 hover:bg-gray-50), BUKAN orange atau navy flex-wrappada outer: tombol turun ke baris baru di mobile, tidak menimpa judul- Ikon:
w-12 h-12 rounded-full bg-[#1E1C43] shrink-0— lingkaran navy, JANGAN shrink
3c. List Page — Header Card Standard
Dua Tier Header untuk List Page
Tidak semua list page menggunakan header yang sama. Ada 2 tier berdasarkan apakah halaman adalah "first-class entity" atau "sub-document":
| Tier | Kapan digunakan | Contoh halaman |
|---|---|---|
| Tier 1 — Telanjang (First-class entity) | Entitas berdiri sendiri, tidak punya parent natural, akses langsung dari sidebar | PPLeadsPage, PPOrdersPage, PPProgramDBPage |
| Tier 2 — Pola 3c (Sub-document list) | Daftar dokumen turunan dari suatu order/lead, ada back button ke parent | PPInvoicePage, PPReceiptPage, PPDocumentsPage |
Cara cepat menentukan tier: Apakah halaman ini punya parent alami (mis. "semua invoice dari order X")? → Tier 2. Apakah halaman ini berdiri sendiri tanpa parent? → Tier 1.
Tier 1 — Telanjang (First-class Entity)
Header tanpa card wrapper, langsung di atas konten. Dipakai untuk entitas first-class yang akses langsung dari sidebar menu.
{/* Telanjang — tanpa card */}
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h1 className="text-[22px] font-bold text-text-primary leading-tight">
Nama Halaman
</h1>
<p className="text-sm text-text-muted mt-1">Deskripsi singkat konten halaman</p>
</div>
{/* Kanan: tombol CTA utama (tidak ada tombol Kembali — tidak punya parent) */}
<div className="flex items-center gap-2">
<button className="flex items-center gap-2 px-4 py-2 rounded-lg bg-[#E05945] hover:bg-[#c94a38] text-white text-sm font-semibold transition-colors">
<Plus size={14} /> Tambah Baru
</button>
</div>
</div>
Aturan Tier 1:
- Tidak ada card wrapper (
bg-white rounded-2xl ...) — header langsung inline di halaman - Tidak ada ikon bulat navy — bukan fitur first-class entity header
- Judul:
text-[22px] font-bold text-text-primary— LEBIH BESAR dari pola 3c (text-lg) - Tidak ada tombol Kembali — first-class entity tidak punya parent natural
- Subtitle:
text-sm text-text-muted mt-1 - Outer wrapper:
flex flex-wrap items-start justify-between gap-3
Tier 2 — Pola 3c (Sub-document List)
Pola untuk list halaman dokumen turunan (Invoice, Receipt, Agreement, Screening, dll). Ada card wrapper, ikon navy, dan tombol Kembali ke parent.
Berbeda dari 3b: tidak ada eyebrow label, tombol pakai flex-col sm:flex-row bukan flex-shrink-0.
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-5">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
{/* Kiri: ikon bulat + judul + subtitle */}
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-[#1E1C43] flex items-center justify-center shrink-0">
<IconName size={20} className="text-white" />
</div>
<div>
<h1 className="text-lg font-bold text-[#1E1C43] leading-tight">Judul Halaman</h1>
<p className="text-sm text-text-muted mt-0.5">Deskripsi singkat konten halaman</p>
</div>
</div>
{/* Kanan: tombol aksi (CTA + Kembali) */}
<div className="flex flex-col sm:flex-row gap-2">
<button className="flex items-center justify-center gap-1.5 px-3.5 py-2 rounded-lg bg-[#E05945] hover:bg-[#c94a38] text-white text-xs font-semibold transition-colors">
<Plus size={13} /> Buat Baru
</button>
<button className="flex items-center gap-1.5 h-8 px-3 rounded-lg border border-gray-300 text-gray-600 text-xs font-semibold hover:bg-gray-50 transition-colors">
<ArrowLeft size={12} /> Kembali ke ...
</button>
</div>
</div>
</div>
Aturan wajib:
- Outer wrapper:
flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3— JANGANflex items-start justify-between gap-4 flex-wrap - Tidak ada eyebrow label (
<p className="text-[10px]...">) di atas judul — eyebrow hanya di pola 3b - Subtitle:
text-sm text-text-muted mt-0.5— bukantext-xs text-gray-400 - Tombol kanan:
flex flex-col sm:flex-row gap-2— di mobile tombol susun vertikal, di desktop horizontal - Ikon tetap:
w-12 h-12 rounded-full bg-[#1E1C43] shrink-0 - Tombol Kembali: selalu gray secondary, BUKAN orange
- Jika ada tombol toggle (Template Invoice/Agreement), gunakan navy outline bukan orange
3d. Document Detail Page — Header Card Standard
Pola untuk halaman detail dokumen (Invoice Detail, Receipt Detail, Agreement Detail) — berbeda dari entity detail (Lead/Order) dan sub-page form. Header ini lebih kompak: single flat row, ikon lebih kecil, judul lebih kecil, tombol aksi langsung dalam row yang sama.
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm px-5 py-4">
<div className="flex items-center gap-2 flex-wrap">
{/* Ikon bulat kecil navy */}
<div className="w-10 h-10 rounded-full bg-[#1E1C43] flex items-center justify-center shrink-0">
<IconName size={16} className="text-white" />
</div>
{/* Judul + meta info */}
<div className="min-w-0 flex-1">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">
Tipe Dokumen (mis. "Invoice Klien" / "Agreement Klien")
</p>
<h1 className="text-base font-bold text-[#1E1C43] leading-snug truncate">
#{doc.displayId} {/* mis. #INV-PP-27-0001 — TANPA prefix tipe ("Invoice", "Receipt", dll) */}
</h1>
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
<span className="text-xs text-gray-500">{doc.namaKlien}</span>
<span className="text-gray-300 text-xs">·</span>
<StatusBadge />
</div>
</div>
{/* Tombol aksi — langsung di row yang sama, bukan di div terpisah */}
<button
onClick={handleBack}
className="inline-flex items-center gap-1.5 h-8 px-3 rounded-lg border border-gray-200 text-gray-500 text-xs font-medium hover:bg-gray-50 transition-colors shrink-0"
>
<ArrowLeft size={13} /> Kembali ke ...
</button>
{/* Tombol aksi lain (Edit, Download) jika ada, tambahkan di sini */}
</div>
</div>
Perbedaan kunci dari pola 3b:
| 3b (Sub-page form) | 3d (Document detail) | |
|---|---|---|
| Layout | flex items-start justify-between gap-4 flex-wrap |
flex items-center gap-2 flex-wrap |
| Ikon | w-12 h-12, size={18–22} |
w-10 h-10, size={16} |
| Judul | text-lg font-bold |
text-base font-bold truncate |
| Tombol | dalam wrapper div terpisah di kanan |
langsung dalam row yang sama |
| Padding | p-5 |
px-5 py-4 |
Aturan wajib:
- Ikon:
w-10 h-10 rounded-full bg-[#1E1C43] shrink-0— LEBIH KECIL dari 3b (yangw-12) - Judul
h1: hanya ID dokumen dengan prefix#— mis.#INV-PP-27-0001,#RCP-PP-27-0001,#AGR-PP-27-0001 - ⚠️ Dilarang mengulangi tipe dokumen di
h1— tipe sudah ada di eyebrow label. BUKAN"Invoice #INV-PP-27-0001", cukup"#INV-PP-27-0001". - Eyebrow label: TETAP ada di 3d (beda dari 3c list page) — menunjukkan tipe dokumen (mis. "Invoice Private Training", "Receipt Klien", "Agreement Klien")
- Tombol kembali: LANGSUNG dalam flex row utama (bukan
div justify-betweenterpisah) - Padding card:
px-5 py-4(bukanp-5seperti 3b)
Aturan Warna Tombol di Header Document Detail Page
Tombol-tombol di dalam header card Section 3d mengikuti aturan warna global (lihat Section 3), dengan catatan khusus:
| Tombol | Warna yang benar | Contoh class |
|---|---|---|
| Kembali ke ... | Gray secondary | border border-gray-200 text-gray-500 font-medium |
| Download PDF | Gray secondary | border border-gray-300 text-gray-600 font-semibold |
| Edit | Gray secondary | border border-gray-300 text-gray-600 font-semibold |
| Konfirmasi Pembayaran / Approve | CTA — green/orange | bg-[#27AE60] text-white atau bg-[#E05945] text-white |
⚠️ Download PDF BUKAN tombol navy — meskipun terlihat "penting", ia adalah aksi navigasi/ekspor, bukan CTA bisnis. Selalu gunakan gray secondary.
⚠️ Jangan gunakan bg-[#1E1C43] (navy) untuk Download PDF atau Edit di header document detail — navy hanya untuk Save/submit di form dan modal.
Aturan Label Tombol "Kembali" — Document Detail Page
| Tujuan navigasi | Label tombol |
|---|---|
| Kembali ke halaman list Orders global | Kembali ke Order |
| Kembali ke order spesifik (fromOrderId tersedia) | Kembali ke Order #PP-27-0001 |
| Kembali ke halaman list Invoice | Kembali ke Invoice |
| Kembali ke halaman list Agreement/Documents | Kembali ke Daftar Agreement |
| Kembali ke halaman list Screening | Kembali ke Screening |
| Kembali ke Lead (leadId tersedia) | Kembali ke Lead |
Pola implementasi (ternary dinamis):
// Jika ada fromOrderId:
{fromOrderId ? `Kembali ke Order #${fromOrderId}` : 'Kembali ke Invoice'}
// Jika bisa dari order atau list:
{fromOrderId ? `Kembali ke Order #${fromOrderId}` : leadId ? 'Kembali ke Lead' : 'Kembali ke Screening'}
⚠️ Selalu prefix "Kembali ke" — BUKAN label pendek seperti "Order #X" atau "Invoice" saja.
⚠️ Back button style di document detail: border-gray-200 text-gray-500 font-medium — LEBIH TERANG dari pola lama (border-gray-300 text-gray-600 font-semibold).
3a. Standard Column Minimum Widths (Table)
Gunakan nilai ini sebagai acuan style={{minWidth:'Xpx'}} per <th> di semua halaman list.
Ini mencegah kolom "Nama Klien" di Invoice berbeda lebar dengan "Nama Klien" di Orders, yang menyebabkan wrap berbeda.
| Tipe Kolom | minWidth |
|---|---|
| No. Invoice / No. Receipt / No. Agreement | 160–175px |
| Lead ID | 110px |
| Order ID | 130px |
| Nama Klien / Nama Perusahaan | 160px |
| Paket / Program / Layanan / Nama Event | 160px |
| PIC EFM / Pelatih / Koordinator | 130px |
| Tanggal / Tgl Bayar / Jatuh Tempo / Tgl Event | 120px |
| Kota / Tipe / Jenis | 110px |
| Stage / Tahapan | 120px |
| Status | 120px |
| No. HP / No. WA | 130px |
| Nilai Kontrak / Total | 130px |
| Aksi | 100px |
Cara terapkan di <th>:
<th style={{minWidth:'160px'}} className="px-3 py-2.5 text-left text-[10px] font-semibold text-gray-500 uppercase tracking-wider whitespace-nowrap">
Nama Klien
</th>
Cara terapkan dengan loop [h, mw] (dipakai di halaman dengan banyak kolom):
{[['No Invoice',165],['Order ID',130],['Nama Klien',160],['Status',120],['Aksi',100]].map(([h, mw]) => (
<th key={h} style={{minWidth:mw}} className="px-3 py-2.5 text-left text-[10px] font-semibold text-gray-500 uppercase tracking-wider whitespace-nowrap">
{h}
</th>
))}
4. Component Patterns
Table Wrapper — List Page (gunakan pola ini untuk semua halaman list)
List pages menggunakan CSS token classes (bukan literal Tailwind colors), dengan sticky <thead> dan maxHeight agar tabel scroll di dalam viewport:
<div className="bg-bg-surface border border-border rounded-xl shadow-sm overflow-hidden">
<div className="overflow-auto" style={{ maxHeight: 'calc(100vh - 360px)', minHeight: '280px' }}>
<table className="w-full text-sm" style={{ minWidth: '1000px' }}>
<thead className="sticky top-0 z-10">
<tr className="bg-gray-50 border-b border-gray-100">
<th style={{minWidth:'160px'}} className="px-3 py-2.5 text-left text-[10px] font-semibold text-gray-500 uppercase tracking-wider whitespace-nowrap">
Column Name
</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-100 hover:bg-gray-50 cursor-pointer transition-colors duration-150">
<td className="px-3 py-2.5 text-xs text-gray-600 whitespace-nowrap">
Data
</td>
</tr>
</tbody>
</table>
</div>
{/* Pagination footer */}
<div className="px-4 py-3 border-t border-border flex items-center justify-between">
<span className="text-xs text-text-muted">Menampilkan X–Y dari Z</span>
{/* PBtn controls */}
</div>
</div>
Token classes wajib (list page):
- Wrapper:
bg-bg-surface border border-border— BUKANbg-white border-gray-200 - Inner scroll:
overflow-autodenganmaxHeight: 'calc(100vh - 360px)'danminHeight: '280px' <thead>:sticky top-0 z-10- Pagination footer:
border-t border-border
Teks per kolom:
- Header
<th>:text-[10px] font-semibold text-gray-500 uppercase tracking-wider whitespace-nowrap - Data
<td>:text-xs text-gray-600 whitespace-nowrap - ID columns:
text-xs font-semibold text-[#1E1C43] whitespace-nowrap - Nama/penting:
text-xs font-medium text-gray-900 whitespace-nowrap
Pagination standar:
ROWS = 10per halaman (bukan 8, bukan 15)- Teks footer:
"Menampilkan {start}–{end} dari {total} [entitas]"— Bahasa Indonesia - Gunakan komponen
PBtn(w-8 h-8 rounded-lg text-xs font-semibold) dengan SVG arrow prev/next + nomor halaman
Filter Bar
Urutan elemen wajib (kiri → kanan): select dropdowns → search input → reset button
<div className="bg-bg-surface border border-border rounded-xl px-4 py-2.5 flex items-center gap-2.5 flex-wrap">
{/* 1. Select dropdowns (kiri) */}
<select className="px-3 py-[7px] border-[1.5px] border-border rounded-lg text-xs text-text-primary bg-white outline-none focus:border-primary hover:border-primary transition-colors">
<option value="">Semua Status</option>
{/* ... */}
</select>
<select className="px-3 py-[7px] border-[1.5px] border-border rounded-lg text-xs text-text-primary bg-white outline-none focus:border-primary hover:border-primary transition-colors">
<option value="">Semua Jenis</option>
{/* ... */}
</select>
{/* 2. Search input (mengisi sisa ruang, flex-1) */}
<div className="flex items-center gap-2 flex-1 min-w-[200px] bg-bg-page border-[1.5px] border-border rounded-lg px-3 py-[7px] focus-within:border-primary focus-within:bg-white transition-colors">
<Search size={14} className="text-text-muted shrink-0" />
<input
className="border-none bg-transparent text-xs outline-none w-full text-text-primary placeholder:text-text-muted"
placeholder="Cari nama, ID..."
/>
</div>
{/* 3. Reset button (paling kanan) */}
<button className="px-3.5 py-[7px] bg-primary hover:bg-primary-2 text-white text-xs font-semibold rounded-lg transition-colors shrink-0 flex items-center gap-1.5">
<RotateCcw size={12} /> Reset
</button>
</div>
Aturan urutan:
- Selects selalu di kiri — jangan diletakkan di kanan search
- Search input pakai
flex-1 min-w-[200px]supaya mengisi ruang tersisa - Reset selalu paling kanan dengan
shrink-0supaya tidak mengecil - Container:
bg-bg-surface border border-border rounded-xl px-4 py-2.5 flex items-center gap-2.5 flex-wrap
⚠️ Semua <option> di filter dropdown WAJIB Bahasa Indonesia — tidak boleh Inggris. Contoh: Expired → Kadaluarsa, Sent → Terkirim, Failed → Gagal, Draft → Draft.
⚠️ Placeholder teks search juga harus Indonesia: "Cari nama klien, ID...", bukan "Search...".
⚠️ Jangan balik urutan — search SETELAH selects, reset PALING KANAN. Ini sudah diterapkan di semua halaman list (PPProgramDBPage, PPOrdersPage, PPLeadsPage, PPKlienListPage, dll).
Modal
- Outer:
fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 - Inner:
bg-white rounded-2xl shadow-2xl w-full max-w-md flex flex-col max-h-[90vh] - Header:
flex-shrink-0 - Body:
overflow-y-auto flex-1 - Footer:
flex-shrink-0
KPI Card Row
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-4">
<p className="text-xs uppercase tracking-wide text-gray-400">Label</p>
<p className="text-2xl font-bold text-[#1E1C43] mt-1">Value</p>
</div>
</div>
Status Badge
const statusColors = {
draft: 'bg-yellow-50 text-yellow-700 border-yellow-200',
terkirim: 'bg-blue-50 text-blue-700 border-blue-200',
lunas: 'bg-green-50 text-green-700 border-green-200',
overdue: 'bg-red-50 text-red-700 border-red-200',
}
<span className={`px-2 py-1 text-xs rounded-full font-medium border ${statusColors[status]}`}>
{statusLabel}
</span>
5. Invoice & Receipt Document Standards
5a. Status Label — WAJIB Bahasa Indonesia (PP, B2B, Event)
Semua status invoice/receipt di seluruh modul harus konsisten berbahasa Indonesia. Sumber kebenaran di ppInvoiceData.js (STATUS_LABEL), pastikan filter dropdown, badge, dan label di semua halaman menggunakan mapping yang sama.
| Key | Label UI |
|---|---|
paid |
Lunas |
pending |
Menunggu Pembayaran |
overdue |
Jatuh Tempo |
draft |
Draft |
⚠️ Dilarang: 'Paid', 'Awaiting Payment', 'Overdue' (Inggris) — selalu ganti ke label Indonesia di atas.
⚠️ Penulisan Lunas: selalu title case (Lunas), BUKAN all-caps (LUNAS) — berlaku di badge document header, teks konfirmasi, label field, dan semua UI. All-caps LUNAS adalah pola lama yang sudah dihapus.
5b. Invoice/Receipt — Navy Header (kiri: info perusahaan)
<div className="bg-[#1E1C43] rounded-t-2xl px-6 py-5 sm:px-8 sm:py-6 grid grid-cols-2 gap-4 text-white">
{/* Kiri — logo + info perusahaan */}
<div className="flex items-start gap-3">
{cs.logoPerusahaan ? (
<img src={cs.logoPerusahaan} alt="EFM Logo"
className="w-20 h-20 rounded-full object-contain shrink-0" />
) : (
<div className="w-20 h-20 rounded-full bg-white/10 flex items-center justify-center shrink-0">
<span className="text-white font-black text-base">EFM</span>
</div>
)}
<div className="min-w-0">
<p className="text-base font-bold leading-snug">{cs.namaPerusahaan}</p>
<p className="text-xs text-white/70 mt-0.5">{cs.namaLegal}</p>
<p className="text-xs text-white/70 mt-0.5 leading-relaxed max-w-xs">{cs.alamat}</p>
<p className="text-xs text-white/70 mt-0.5">{cs.email}</p>
<p className="text-xs text-white/70 mt-0.5">{cs.telepon}</p>
</div>
</div>
{/* Kanan — lihat 5c/5d */}
</div>
Aturan wajib:
- Logo:
w-20 h-20 rounded-full object-contain shrink-0 namaPerusahaan:text-base font-bold(bukantext-sm)- Sub-info:
text-xs text-white/70(bukantext-white/60) - Selalu tampilkan:
namaPerusahaan,namaLegal,alamat,email,telepon
5c. Invoice — Header Kanan
<div className="text-right">
<div className="text-4xl font-black tracking-widest uppercase">INVOICE</div>
<div className="text-sm text-gray-300 mt-0.5">{invoice.invNo}</div>
<div className="flex justify-end items-center gap-2 mb-0.5 mt-0.5">
<span className="text-xs text-gray-400">Tanggal:</span>
<span className="font-semibold text-sm">{invoice.tanggal}</span>
</div>
<div className="flex justify-end items-center gap-2 mb-0.5">
<span className="text-xs text-gray-400">Jatuh Tempo:</span>
<span className="font-semibold text-sm">{invoice.due}</span>
</div>
<span className={`px-4 py-1 rounded-full text-white text-sm font-semibold inline-block mt-0.5 ${statusBadgeCls}`}>
{STATUS_LABEL[invoice.status]}
</span>
</div>
5d. Receipt — Header Kanan
<div className="text-right">
<div className="text-4xl font-black tracking-widest uppercase">RECEIPT</div>
<div className="text-sm text-gray-300 mt-0.5">{rcp.rcpNo}</div>
<div className="flex justify-end items-center gap-2 mb-0.5 mt-0.5">
<span className="text-xs text-gray-400">Ref. Invoice</span>
<button onClick={() => onGoToInvoice(rcp.invNo)}
className="font-semibold text-sm hover:underline">{rcp.invNo}</button>
</div>
<div className="flex justify-end items-center gap-2 mb-0.5">
<span className="text-xs text-gray-400">Order ID</span>
<button onClick={() => onGoToOrder(rcp.orderId)}
className="font-semibold text-sm hover:underline">#{rcp.orderId}</button>
</div>
<span className="px-4 py-1 rounded-full text-white text-sm font-semibold inline-block mt-0.5 bg-green-500">
Lunas
</span>
</div>
Aturan wajib (berlaku Invoice & Receipt):
- Nomor dokumen di bawah judul:
text-sm text-gray-300(bukantext-xs) - Meta rows (Tanggal, Ref. Invoice, dll): label
text-xs text-gray-400, valuefont-semibold text-sm - Status badge di header navy:
px-4 py-1 rounded-full text-white text-sm font-semibold— lebih besar dari badge tabel biasa - Page header
h1:Receipt #RCP-PP-27-0001/Invoice #INV-PP-27-0001— selalu prefix#di judul page
5e-pre. Document Header Kanan — Prinsip Anti-Duplikat
Kolom kanan di navy header (Section 5b) hanya untuk: judul dokumen, nomor dokumen, tanggal, dan status badge. Jangan tampilkan ulang data yang sudah ada di body document.
| Informasi | Tempat yang benar | Di header kanan? |
|---|---|---|
| Judul dokumen (INVOICE, RECEIPT, REKAP ABSENSI) | Header kanan | ✅ Ya |
| Nomor dokumen (INV-PP-27-0001, dll) | Header kanan | ✅ Ya |
| Tanggal terbit / jatuh tempo | Header kanan | ✅ Ya |
| Status (Lunas, Menunggu Pembayaran, dll) | Header kanan (badge) | ✅ Ya |
| Order ID, nama klien, nama pelatih | Section "Tagihan Kepada" / "Ditujukan Untuk" di body | ❌ Tidak — redundant |
⚠️ Rekap Absensi: header kanan = judul REKAP ABSENSI + nomor rekap + tanggal pengajuan + status badge — tanpa Order ID dan nama klien, karena sudah ada di section "Ditujukan Untuk" di body.
Status badge di document header (dark bg):
<span className={`px-4 py-1 rounded-full text-white text-sm font-semibold inline-block ${statusCls}`}>
{statusLabel}
</span>
px-4 py-1 text-sm font-semibold— LEBIH BESAR dari badge tabel biasa- Tidak ada
border— badge di atas background navy tidak perlu border text-white— selalu putih (bukan warna semantik gelap) saat di atas navy dark bg- Berlaku untuk semua document print area: Invoice, Receipt, Rekap Absensi, dsb.
5e. Invoice/Receipt — Section Body (Divider Pattern)
Body dokumen TIDAK menggunakan satu wrapper space-y-5. Setiap section adalah full-bleed dengan padding dan border sendiri:
{/* Tiap section */}
<div className="px-6 sm:px-8 py-4 border-b border-gray-100">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wide mb-2">
Nama Section
</p>
<div className="bg-gray-50 border border-gray-200 rounded-xl p-4">
{/* isi */}
</div>
</div>
{/* Footer — section terakhir tanpa border-b */}
<div className="px-6 sm:px-8 py-4 text-center space-y-1">
{/* footer */}
</div>
⚠️ Jangan gunakan <div className="px-6 sm:px-8 py-6 space-y-5"> sebagai wrapper body — ini pola lama yang tidak punya divider antar section.
5f. Total Strip di Dalam Card (Invoice & Receipt)
<div className="bg-[#1E1C43] px-4 py-2.5 flex items-center justify-between">
<span className="text-xs font-bold text-white uppercase tracking-wide">Total Tagihan</span>
<span className="text-base font-black text-white">{formatRp(total)}</span>
</div>
Aturan:
- Label:
text-xs font-bold text-white uppercase tracking-wide - Angka:
text-base font-black text-white— bukan orange, bukantext-xl - Padding:
py-2.5(bukanpy-4)
5g. Invoice & Receipt — Footer Copywriting
Footer dokumen selalu 2 baris: satu kalimat kontekstual + satu baris brand.
Invoice footer:
<div className="px-6 sm:px-8 py-4 border-t border-gray-100 text-center space-y-1">
<p className="text-xs text-gray-400">Terima kasih atas kepercayaan Anda. Harap selesaikan pembayaran sesuai tenggat waktu yang tertera.</p>
<p className="text-xs font-semibold text-gray-500">Powered by {cs.namaPerusahaan} | {cs.namaLegal}</p>
</div>
Receipt footer:
<div className="px-6 sm:px-8 py-4 text-center space-y-1">
<p className="text-xs text-gray-400">Terima kasih atas kepercayaan Anda. Simpan receipt ini sebagai bukti pembayaran yang sah.</p>
<p className="text-xs font-semibold text-gray-500">Powered by {cs.namaPerusahaan} | {cs.namaLegal}</p>
</div>
Aturan:
- Selalu 2 baris — satu kalimat pesan + satu baris
Powered by - Brand + legal digabung satu baris:
Powered by {cs.namaPerusahaan} | {cs.namaLegal}— BUKAN 2 baris terpisah - BUKAN "program fitness Anda" — cukup "kepercayaan Anda" (EFM bukan hanya fitness)
- HAPUS: "Dokumen ini digenerate oleh sistem EFM V2" — tidak perlu ditampilkan ke klien
5h. Receipt — Barcode Absensi Sesi
Barcode di receipt digunakan untuk absensi pelatih/terapis per sesi, bukan verifikasi pembayaran.
- Judul section:
Barcode Absensi Sesi— BUKAN "Kode Verifikasi Pembayaran" - Label di bawah QR: hanya nomor receipt (
{rcp.rcpNo}) —text-[10px] font-semibold text-[#1E1C43] mt-1.5 text-center tracking-wide - TIDAK ADA sub-label "Tunjukkan setiap sesi" di bawah nomor — redundant, sudah ada deskripsi di section
- Deskripsi di dalam card: "Tunjukkan barcode ini kepada pelatih / terapis di setiap sesi pertemuan berlangsung"
- Size QR di section dedicated:
size={160}(bukan default 72)
5i. Receipt — Sections yang Ada (Urutan Wajib)
Receipt body berisi section-section berikut (urutan ini wajib, jangan tambah section baru tanpa konfirmasi):
- Tagihan Kepada — info klien
- Rincian Program — paket, sesi, harga
- Total strip (navy bar di dalam card Rincian Program)
- Detail Pembayaran — tanggal bayar, metode
- Barcode Absensi Sesi — QR/barcode untuk absensi
- Footer — 2 baris (lihat 5g)
⚠️ Section "Catatan" di receipt adalah template global yang dikelola dari halaman list (PPReceiptPage), bukan hardcoded JSX dan bukan diedit per-receipt dari detail page. Lihat Section 5j untuk prinsip selengkapnya.
5j. Invoice & Receipt — Prinsip Read-Only Document
Invoice dan Receipt adalah dokumen turunan yang bersifat read-only terhadap data bisnisnya. Desain ini disengaja:
| Data | Tempat edit yang benar | Di Invoice/Receipt Detail? |
|---|---|---|
| Harga paket, pilihan program | Order Detail (/pp/orders/[id]) |
❌ Tidak bisa diedit |
| Kode diskon / promo | Order Detail (/pp/orders/[id]) |
❌ Tidak bisa diedit |
| Tanggal invoice, jatuh tempo | Order Detail | ❌ Tidak bisa diedit |
| Biaya Tambahan | Order Detail (atau edit biaya lain di Invoice) | ⚠️ Terbatas — hanya field tambahan, bukan paket utama |
| Syarat & Ketentuan invoice | Template editor di PPInvoicePage (list page) |
❌ Tidak bisa diedit per-invoice |
| Catatan receipt | Template editor di PPReceiptPage (list page) |
❌ Tidak bisa diedit per-receipt |
| Pasal-pasal agreement (Syarat & Ketentuan) | Template editor di PPDocumentsPage (list page) |
❌ Tidak bisa diedit per-agreement |
Prinsip arsitektur:
- Data bisnis (harga, program, diskon) dikontrol dari Order — invoice/receipt hanya mencerminkan data order
- Template teks (Syarat & Ketentuan, Catatan, Pasal) dikontrol dari halaman list masing-masing dokumen — berlaku global untuk semua dokumen
- Admin yang ingin mengubah isi Syarat invoice pergi ke halaman Invoice list, bukan buka invoice tertentu
- Admin yang ingin mengubah isi Catatan receipt pergi ke halaman Receipt list, bukan buka receipt tertentu
- Admin yang ingin mengubah pasal agreement pergi ke halaman Documents list, bukan buka agreement tertentu
- Desain ini mencegah admin sembarangan mengubah data per-dokumen dan memastikan konsistensi
⚠️ Jangan tambahkan field editable ke Invoice/Receipt/Agreement detail untuk data yang sudah dikelola di Order atau template list page — ini melanggar prinsip di atas.
6. Sidebar Navigation — Aturan Item per Modul
6a. Prinsip Dasar: Entitas vs Sub-Dokumen
⚠️ Recurring mistake: menambahkan sub-dokumen (Invoice, Receipt, Agreement) sebagai item sidebar — ini SALAH. Hanya entitas first-class yang boleh masuk sidebar modul.
Masuk sidebar (*_SUB array):
- Entitas workflow utama: Dashboard, Leads, Orders
- Halaman referensi teknis yang tidak punya parent natural: Program DB (PP), Survei (B2B), Kalender (B2B/Event)
- Prinsip: seminimal mungkin — hanya halaman yang benar-benar tidak bisa diakses dari alur navigasi lain
TIDAK masuk sidebar:
- Invoice — sub-dokumen dari Order, diakses dari Order Detail
- Receipt — sub-dokumen dari Order, diakses dari Order Detail
- Agreement / LOI — sub-dokumen dari Order, diakses dari Order Detail
- Klien (PP) — diakses dari Lead Detail atau Order Detail
- Semua halaman yang secara alami berada di dalam konteks satu order/lead spesifik
6b. Struktur Sidebar per Modul (FINAL)
// PP — 5 item
const PP_SUB = [
{ label: 'Dashboard', path: '/pp/dashboard' },
{ label: 'Leads', path: '/pp/leads', also: ['/pp/klien'] },
{ label: 'Orders', path: '/pp/orders', also: ['/pp/invoice', '/pp/receipt'] },
{ label: 'Promo', path: '/pp/promo' },
{ label: 'Program DB', path: '/pp/program-db' },
]
// B2B — 5 item
const B2B_SUB = [
{ label: 'Dashboard', path: '/b2b/dashboard' },
{ label: 'Leads', path: '/b2b/leads' },
{ label: 'Survei', path: '/b2b/survei' },
{ label: 'Orders', path: '/b2b/orders' },
{ label: 'Kalender', path: '/b2b/kalender' },
]
// B2B Event — 4 item
const EVENT_SUB = [
{ label: 'Dashboard', path: '/event/dashboard' },
{ label: 'Leads', path: '/event/leads' },
{ label: 'Orders', path: '/event/orders' },
{ label: 'Kalender', path: '/event/kalender' },
]
⚠️ Jangan tambah item ke array ini tanpa konfirmasi — khususnya jika item yang akan ditambahkan adalah sub-dokumen dari Order (Invoice, Receipt, Agreement, dll).
6c. Aturan Highlight Sidebar
Saat halaman sub-dokumen (Invoice, Receipt, Agreement) dibuka melalui navigasi dari Order Detail:
- Yang harus highlight: item Orders di sidebar (karena konteksnya masih dalam alur Order)
- Caranya: tambahkan path ke
also[]di entry Orders:also: ['/pp/invoice', '/pp/receipt'] - Jangan fix "tidak ada highlight" dengan menambah sub-dokumen ke
*_SUBarray — itu solusi yang salah - Promo sudah punya entry sendiri di sidebar (
/pp/promo) — TIDAK perlu ada dialso[]Orders
Jika halaman Invoice/Receipt/Agreement memiliki route standalone (/pp/invoice, /pp/receipt) yang tidak berawalan /pp/orders/, highlight sidebar akan hilang — dan solusi yang benar adalah menerima kondisi ini (tidak perlu highlight saat di halaman cross-order list view), bukan menambahnya ke sidebar.
7. Document Footer Standard (Dokumen Operasional)
Dipakai untuk: Rekap Absensi, dan dokumen operasional sejenis yang diterima pelatih/terapis.
Struktur footer (2 baris):
<div className="rkp-sec px-6 sm:px-8 py-4 border-t border-gray-100 text-center space-y-1">
<p className="text-xs text-gray-500">Simpan dokumen ini sebagai bukti sesi dan konfirmasi honorarium Anda.</p>
<p className="text-xs font-semibold text-gray-500">
Powered by {cs.namaPerusahaan} | {cs.namaLegal}
</p>
</div>
Aturan:
- Baris 1: kalimat fungsional dari POV penerima dokumen (pelatih/terapis) — menjelaskan untuk apa dokumen ini disimpan
- Baris 2: identitas perusahaan via
getCompanySettings()— JANGAN hardcode - JANGAN gunakan "Terima kasih atas kepercayaan Anda." — terlalu generik, tidak memberi informasi
- JANGAN gunakan kalimat basa-basi lain yang tidak menambah nilai fungsional
- Jika dokumen ditujukan ke klien (bukan pelatih), sesuaikan baris 1 dengan konteks: mis. "Simpan dokumen ini sebagai bukti program dan pembayaran Anda."