UX Redesign — Pemilik Manfaat Flow (Perubahan PP)
Expert UX design brief. Covers all three stages of the BO lifecycle
in Perubahan PP: Unggah Dokumen → Tinjauan → back to Unggah.
1. Current Flow — Pain Points
PpPerubahanFormPage (step 2 "Lengkapi Data")
└─ PpPemilikManfaatSection
├─ [Tambah Pemilik Manfaat] → TambahPemilikManfaatDialog
│ step 1: kewarganegaraan (WNI / WNA)
│ step 2: upload ID doc (+ NPWP for WNI)
│ step 3: pick kriteria
│ → POST /pemilik-manfaat/from-document ← always APPENDS ❶
│
└─ per-BO card
[Perbarui dengan Dokumen] → same dialog in "update" mode
kriteria picker (active)
identity fields (read-only if matched or seeded)
↓ user clicks Lanjutkan
PpPerubahanReviewPageV2 (Tinjauan)
└─ PpPemilikManfaatCards
shows only NEW BOs (matchedDocumentId != null) ← ❷
kriteria picker + Konfirmasi button per card
↓ user clicks ← Kembali
PpPerubahanFormPage again
└─ [Tambah Pemilik Manfaat] → uploads APPENDED to existing BOs ← ❶ triggers
Pain point ❶ — Append-not-replace (Issue #10)
Every call to /pemilik-manfaat/from-document creates a new row.
There is no way to signal "I want to replace my existing set."
Going back from Tinjauan and uploading again silently doubles the list.
The button label "Tambah" reinforces "add one more" when the user's
mental model is "set the BO roster."
Pain point ❷ — No before/after in Tinjauan (Issue #9)
PpPemilikManfaatCards renders only BOs where matchedDocumentId != null
(newly uploaded this submission). The existing BO roster (from oldData)
is invisible. The reviewer can't answer "compared to what?" and can't tell
if they're adding, replacing, or leaving unchanged.
Pain point ❸ — Kriteria friction at upload time
The dialog forces the user to know and choose kriteria at the same moment
they're uploading identity docs. These are cognitively unrelated tasks.
Users who don't know the criteria yet are blocked from uploading.
Pain point ❹ — No extraction status feedback
After clicking "Tambah Pemilik Manfaat," the dialog closes and the BO card
appears with extracted fields (or empty fields). There's no visible signal
that OCR is in progress / done / failed. Silent failures look like "no data."
Pain point ❺ — Single-file dialog inside a flow that isn't single-file
The dialog handles one BO at a time. A company with 4 BOs requires 4 dialog
open/close cycles. There is no batch-upload concept even though the mental
model is "these are all the BOs for my company."
2. Design Goals
| # | Goal | Principle |
|---|---|---|
| G1 | Make replace-vs-add intent explicit before any destructive action | Error Prevention (Nielsen #5) |
| G2 | Show the complete BO roster in Tinjauan (before + after) | Recognition over Recall (Nielsen #6) |
| G3 | Separate upload from kriteria assignment (reduce task coupling) | Progressive Disclosure |
| G4 | Give real-time extraction status per BO card | Visibility of System Status (Nielsen #1) |
| G5 | Group related things spatially (before / after, new / unchanged / removed) | Gestalt: Proximity, Common Fate |
| G6 | Use consistent visual language across form and review stages | Gestalt: Similarity |
| G7 | The primary action path must always be visually dominant | Visual Hierarchy |
3. Redesigned Flow — Stage by Stage
Stage 1 · Form Page — Upload Section
3.1 Empty state (no BOs yet)
┌─────────────────────────────────────────────────────────────┐
│ Pemilik Manfaat │
│ Unggah dokumen identitas setiap pemilik manfaat baru. │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ⊕ Tambah Pemilik Manfaat │ │
│ │ Unggah KTP/Paspor + NPWP untuk data otomatis │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
No change from current — clean starting point.
3.2 After first upload (1+ BOs exist)
┌─────────────────────────────────────────────────────────────┐
│ Pemilik Manfaat 2 akan ditambahkan │
│ │
│ ╔═══════════════════════════════════════════════════════╗ │
│ ║ Ahmad Fauzi [Baru] ✓ ║ │
│ ║ KTP · 317401xxxxx · WNI ║ │
│ ║ ───────────────────────────────────────────────── ║ │
│ ║ Kriteria: [Grup A ▾] ← required before Konfirmasi ║ │
│ ║ [🗑] ║ │
│ ╚═══════════════════════════════════════════════════════╝ │
│ │
│ ╔═══════════════════════════════════════════════════════╗ │
│ ║ Siti Rahayu [Baru] ○ ║ │
│ ║ 🔄 Mengekstrak... (status chip, animated) ║ │
│ ╚═══════════════════════════════════════════════════════╝ │
│ │
│ [+ Tambah Satu Lagi] [↺ Ganti Semua ▾] │
└─────────────────────────────────────────────────────────────┘
Key changes from current:
- "Tambah" button is now "Tambah Satu Lagi" (signals additive, not replace)
- New "Ganti Semua" button with a destructive-action affordance (chevron or secondary style)
- Extraction status chip per card (see §3.4)
- Kriteria removed from upload dialog → shown on card as required-before-confirm (see §3.3)
3.3 Upload dialog — simplified (kriteria removed)
╔══════════════════════════════════════════════════════════╗
║ ✚ Tambah Pemilik Manfaat ║
║ Unggah dokumen identitas — data diisi otomatis. ║
╠══════════════════════════════════════════════════════════╣
║ ║
║ 1 Kewarganegaraan ║
║ [ WNI ] [ WNA ] ║
║ ║
║ 2 Dokumen Identitas (KTP / SIM / Paspor) ║
║ ┌─────────────────────────────────────────────────┐ ║
║ │ 📄 Pilih atau seret file (.pdf .jpg .png) │ ║
║ └─────────────────────────────────────────────────┘ ║
║ ║
║ NPWP (WNI saja) ║
║ ┌─────────────────────────────────────────────────┐ ║
║ │ 📄 Pilih atau seret file (.pdf .jpg .png) │ ║
║ └─────────────────────────────────────────────────┘ ║
║ ║
║ ℹ Kriteria pemilik manfaat akan dipilih setelah ║
║ data berhasil diekstrak dari dokumen. ║
║ ║
╠══════════════════════════════════════════════════════════╣
║ [Batal] [Tambah Pemilik Manfaat] ║
╚══════════════════════════════════════════════════════════╝
Why remove kriteria from the dialog:
- Upload and kriteria assignment are two different cognitive tasks
- Kriteria require knowing legal definitions; upload just requires having the file
- Current dialog blocks users who don't know kriteria from uploading
- Card-level kriteria picker already exists and is well-placed (right before Konfirmasi)
The info callout at the bottom closes the loop: "you will pick kriteria after extraction."
3.4 Extraction status chips (per-card)
After dialog submits and card appears, the card shows a status region:
Phase 1 (OCR in progress) → 🔄 Mengekstrak...
Phase 2 (OCR done) → ✅ Data berhasil diekstrak
Phase 2 (OCR failed/empty) → ⚠️ Perlu verifikasi manual
Implementation note: The card can derive status from the BO's status field
(EDITABLE with no data = extraction may still be running, EDITABLE with data = done).
A short poll after upload (1500 ms interval while status is "empty data") would surface
the extraction result. This mirrors the existing KTP extraction polling elsewhere.
3.5 "Ganti Semua" — confirm-then-replace flow
╔══════════════════════════════════════════════════════════╗
║ ↺ Ganti Semua Pemilik Manfaat? ║
║ ║
║ Tindakan ini akan menghapus 2 pemilik manfaat yang ║
║ sudah diunggah dan menggantinya dengan unggahan baru. ║
║ ║
║ Pemilik manfaat yang akan dihapus: ║
║ • Ahmad Fauzi (KTP · 317401xxxxx) ║
║ • Siti Rahayu (Paspor · B1234567) ║
║ ║
║ [Batal] [Hapus & Mulai Ulang] ║
╚══════════════════════════════════════════════════════════╝
After confirmation:
1. Backend DELETE all non-founder BOs (new endpoint or sequential DELETE per boId)
2. Open upload dialog in regular "add" mode
3. User starts fresh
Gestalt: Common Fate — The list of "BOs that will be deleted" groups the affected
items together, making it clear they share a fate. This prevents the user from being
surprised by what disappears.
3.6 Seeded (existing) BOs in form view
BOs that came from the old registration (via carrier: sourceDocumentId === existingDocumentId)
are currently shown with "Terdaftar (semula)" badge. Strengthen the visual separation:
┌─────────────────────────────────────────────────────────────┐
│ PEMILIK MANFAAT SAAT INI (dari data terdaftar) │
│ ────────────────────────────────────────────────────── │
│ │
│ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │
│ │ Budi Santoso [Semula] terkonfirmasi │ │
│ │ KTP · 3171xxxxx · WNI │ │
│ │ [Perbarui dengan Dokumen ↺] │ │
│ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│ │
│ PEMILIK MANFAAT BARU / DIUBAH │
│ ────────────────────────────────────────────────────── │
│ │
│ ╔═══════════════════════════════════════════════════════╗ │
│ ║ Ahmad Fauzi [Baru] ✓ ║ │
│ ║ KTP · 317401xxxxx · WNI ║ │
│ ╚═══════════════════════════════════════════════════════╝ │
└─────────────────────────────────────────────────────────────┘
Gestalt: Figure/Ground — Seeded BOs are visually recessive (dashed border, muted text,
lower contrast). New BOs are visually prominent (solid border, normal weight). The user
immediately understands which cards they need to act on.
Gestalt: Proximity — Two labeled sections ("Saat Ini" / "Baru") group items by their
role in this submission, not by arbitrary order.
Stage 2 · Tinjauan — BO Section
3.7 Full roster view with before/after
Current: shows only matchedDocumentId != null BOs (the new ones).
Proposed: show the complete picture — old roster + new additions side by side.
┌─────────────────────────────────────────────────────────────┐
│ Data Pemilik Manfaat │
│ │
│ SEBELUM ───────────────────────────────────────────── │
│ │
│ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ │
│ ╎ Budi Santoso [Tidak berubah] [konteks] ╎ │
│ ╎ KTP · 3171xxxxx · WNI ╎ │
│ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ │
│ │
│ SESUDAH ───────────────────────────────────────────── │
│ │
│ ╔═══════════════════════════════════════════════════════╗ │
│ ║ Ahmad Fauzi [Baru +] ○ ║ │
│ ║ KTP · 317401xxxxx · WNI ║ │
│ ║ ── Kriteria ──────────────────────────────────── ║ │
│ ║ ⚠ Wajib dipilih sebelum konfirmasi ║ │
│ ║ [Grup A ▾] [Grup B ▾] ║ │
│ ║ [○ Konfirmasi] ║ │
│ ╚═══════════════════════════════════════════════════════╝ │
│ │
│ ╔═══════════════════════════════════════════════════════╗ │
│ ║ Siti Rahayu [Baru +] ○ ║ │
│ ║ Paspor · B1234567 · WNA ║ │
│ ║ ── Kriteria ──────────────────────────────────── ║ │
│ ║ [Grup B ▾] ║ │
│ ║ [○ Konfirmasi] ║ │
│ ╚═══════════════════════════════════════════════════════╝ │
└─────────────────────────────────────────────────────────────┘
"Sebelum" section:
- Visually recessive: dashed/muted border, secondary text weight, no action buttons
- Label: [Tidak berubah] or [Semula]
- Data from oldData.pemilikManfaat[] (requires backend to surface this in ReviewData)
- Collapsed by default with an expand toggle if the list is long
"Sesudah" section:
- Active cards, same design as now (solid border, confirm button, kriteria picker)
- Label: [Baru +] for newly added, [Diperbarui ↺] for replaced identity
Gestalt: Continuity — The before/after division reads top to bottom like reading a
changelog. The eye follows the natural flow from "what was" to "what will be."
Visual Hierarchy:
- "Sesudah" cards are the primary action area — they dominate visually
- "Sebelum" cards are context only — they inform but don't demand attention
- The section labels act as anchors at the top of each group
3.8 BO card status labels
Consistent badge vocabulary across both stages:
| State | Badge | Color |
|---|---|---|
| Seeded, unchanged | Semula |
muted/secondary |
| Newly added this submission | Baru + |
blue/indigo |
| Identity replaced this submission | Diperbarui ↺ |
amber |
| Marked for removal | Dihapus ✕ |
red/destructive |
| Confirmed | ✓ Dikonfirmasi |
emerald (existing) |
| Pending confirmation | ○ Perlu dikonfirmasi |
zinc |
Gestalt: Similarity — Same badge shapes and color tokens across Form and Tinjauan.
A user who understood "Baru +" in the form view immediately recognises it in Tinjauan.
Stage 3 · Back-navigation from Tinjauan to Form
3.9 Preventing re-upload confusion
When user clicks ← Kembali from Tinjauan back to the Form:
Current: Form page loads with existing BOs already listed. User may not realise that
clicking "Tambah Pemilik Manfaat" will APPEND. Confusion leads to duplicates.
Proposed:
A. The "Tambah" button is renamed "Tambah Satu Lagi" — explicitly additive.
B. Show a status summary row above the action buttons when BOs already exist:
╔══════════════════════════════════════════════════════════╗
║ ✅ 2 pemilik manfaat siap ditinjau ║
║ Lanjutkan ke Tinjau, atau tambah / ganti pemilik manfaat ║
╚══════════════════════════════════════════════════════════╝
[+ Tambah Satu Lagi] [↺ Ganti Semua]
C. If BOs are already confirmed in Tinjauan and user navigates back: show a
re-confirm warning banner (same as the existing field-edit warning pattern):
⚠ Anda kembali ke tahap unggah. Jika Anda mengubah daftar pemilik
manfaat, konfirmasi sebelumnya akan dibatalkan.
Nielsen #1: Visibility of System Status — The user always knows:
- How many BOs are currently in their list
- Whether those BOs are confirmed or pending
- What will happen if they add/replace
4. State Transition Diagram
┌─────────────────────────────────────────────────────────────────┐
│ │
│ Form Page │
│ ┌────────┐ upload doc ┌────────────┐ all confirmed │
│ │ Empty │ ────────────▶ │ BOs listed │ ─────────────────▶ │
│ └────────┘ └──────┬─────┘ │
│ │ │
│ ┌────────▼─────────┐ │
│ │ Intent picker │ │
│ │ (add one more │ │
│ │ vs ganti semua) │ │
│ └────────┬──────────┘ │
│ add ─────┴───── ganti semua │
│ ↓ ↓ │
│ upload dialog confirm delete │
│ ↓ ↓ │
│ BO appended BOs cleared │
│ upload dialog │
│ │
│ Tinjauan Page │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Before section (read-only, oldData BOs) │ │
│ │ After section (new BOs, kriteria + confirm actions) │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↕ Kembali (back-nav) │
│ Form Page (banner: re-confirm warning if BOs were confirmed) │
└─────────────────────────────────────────────────────────────────┘
5. Implementation Plan
Priority 1 — Bug fix (Issue #10): Replace-vs-Append intent
Backend:
DELETE /api/pp/perubahan/:id/pemilik-manfaat
New endpoint: deletes all non-founder BO entries for a submission.
Returns 200 { deleted: N }.
Frontend — Form page:
- Rename "Tambah Pemilik Manfaat" → "Tambah Satu Lagi" (when BOs exist)
- Add "Ganti Semua" button (destructive secondary style)
- Confirm dialog listing BOs to be deleted → calls new bulk DELETE → opens upload dialog
Files to change:
- pp-pemilik-manfaat-section.tsx — button labels, Ganti Semua trigger
- pp-perubahan-form-section.tsx — wire new delete-all mutation
- backend/src/routes/pp-perubahan.ts — new DELETE /pemilik-manfaat endpoint
Priority 2 — Extraction status feedback (Issue #4 analog)
Frontend — BO card (form view):
- Add a status chip derived from bo.nama == null && bo.nomorIdentitas == null (extraction pending or failed)
- Short 1500 ms poll on the form page while any BO has no extracted data
Files to change:
- pp-pemilik-manfaat-section.tsx — status chip per BO card
- PpPerubahanFormPage.tsx — add refetchInterval while BOs are "empty"
Priority 3 — Decouple kriteria from upload dialog
Frontend — dialog:
- Remove step 3 (kriteria) from TambahPemilikManfaatDialog
- Add info callout: "Pilih kriteria setelah dokumen diekstrak"
- Card-level kriteria picker is already implemented — just ensure it's prominent enough
Files to change:
- tambah-pemilik-manfaat-dialog.tsx — remove kriteria step, add callout
- pp-pemilik-manfaat-section.tsx — ensure kriteria section is visually prominent on new cards
Priority 4 — Before/after view in Tinjauan (Issue #9)
Backend:
Surface oldData.pemilikManfaat[] (if it exists in the PpRegistrySnapshot) in the
pp-perubahan-review.ts projector. Add a new registryEntities.bo_sebelum group
or a new diff.pemilik_manfaat section that carries both old + new rows.
Frontend — PpPemilikManfaatCards:
- Accept and render a "sebelum" section (read-only, muted) above the active cards
- Add [Baru +] / [Diperbarui ↺] / [Semula] badges
Files to change:
- pp-perubahan-review.ts (projector) — add sebelum rows to envelope
- pp-pemilik-manfaat-cards.tsx — before/after sections
- Contract review.ts — if roster needs a new shape
Priority 5 — Status summary + back-nav warning
Frontend — Form page:
- Status summary row above action buttons (when BOs exist)
- "Re-confirm" warning banner when navigating back if BOs were previously confirmed
Files to change:
- pp-perubahan-form-section.tsx — status summary component
- PpPerubahanFormPage.tsx or form section — back-nav banner
6. Visual Language Reference
State Border Background Badge text Icon
──────────────────────────────────────────────────────────────
Semula dashed muted/20% "Semula" —
Baru solid white "Baru +" —
Diperbarui solid amber/5% "Diperbarui" ↺
Dihapus solid red/5% "Dihapus" ✕
Confirmed emerald white "Dikonfirmasi" ✓
Pending zinc white — ○ (Circle)
Extracting zinc white "Mengekstrak…" 🔄
Warn (empty) amber amber/5% "Perlu verif" ⚠
Token consistency: reuse CARD_DONE_GLOW (emerald) for confirmed, border-amber-300
for warnings — same tokens used everywhere else in the review engine.
7. Principles Summary
| Section | Principle Applied |
|---|---|
| Rename "Tambah" → "Tambah Satu Lagi" | Error Prevention — label matches behaviour |
| "Ganti Semua" confirmation with list | Error Prevention + Common Fate — explicit, undo-safe |
| Seeded BOs in dashed/muted style | Figure/Ground — context vs. action |
| Before/After sections in Tinjauan | Continuity + Recognition over Recall |
| Extraction status chips | Visibility of System Status |
| Kriteria moved to card, not dialog | Progressive Disclosure |
| Consistent badge vocabulary | Gestalt: Similarity — same language in both stages |
| Status summary above action buttons | Feedback — always know where you stand |
| Back-nav warning banner | Visibility of System Status |