BOD Rollback — Panduan Tampilan Frontend
Semua behavior di bawah sudah terimplementasi di backend. Frontend tinggal merender field yang sudah ada di payload item (approvalStatus, bodResult, bodDisplayStatus, rollbackPaused, bodApprovals[], rollbackOptions[], dan status progress per baris). Contoh payload ada di §5.
1. Jawaban singkat
Ketika kamu rollback "Testing" ke "Testing 2":
- Testing 2 beserta semua anaknya dibuka ulang untuk approval. Progress terakhir tiap item paling bawah (leaf) berubah jadi PENDING / menunggu approval. Item yang masih menunggu prerequisite-nya tidak ikut dibuka — tampil NOT_STARTED abu-abu, dan tidak bisa disentuh sampai prereqnya selesai.
- Testing (item yang di-rollback) tidak di-flip saat itu juga: seluruh subtree Testing tampil NOT_STARTED (
rollbackPaused = true), data & progress tetap utuh (masih 100%),approvalStatusjadi Waiting Approval, tombol aksi disabled. Setelah "Testing 2" di-approve & ditutup lagi, barulah subtree Testing dibuka ulang bertahap — semua node (akar leaf, non-leaf, leaf rantai-awal) jadi Waiting Approval tanpa flip progress (Completed & Waiting, notif ke VP); yang masih punya prereq / blocker tetap NOT_STARTED.- Item lain yang prerequisite-nya = "Testing 2" (konsumen downstream) ikut dikunci: tampil NOT_STARTED, data tidak diubah, semua tombol edit/approve disable sampai "Testing 2" di-approve & ditutup lagi.
- Setelah "Testing 2" selesai di-approve ulang (VP + BOD bila wajib), konsumennya & subtree Testing melepas satu-satu secara berurutan: si A jadi Waiting Approval, lalu anak/rantai berikutnya setelah A ditutup, dst.
- Semua ini tercatat di log aktivitas per item (siapa vote apa, item mana yang dibuka/dikunci/dilepas).
2. Apa yang berubah di UI sesaat setelah rollback resolve
Item yang di-rollback — "Testing" (item B) — sesaat setelah resolve
| Elemen UI | Nilai / tampilan |
|---|---|
| Chip status item | NOT_STARTED (bodResult = REJECTED_ROLLBACK atau rollbackPaused = true) |
approvalStatus (badge approval) |
Waiting Approval |
bodResult |
REJECTED_ROLLBACK |
bodReason |
alasan dari vote rollback pertama |
| Detail BOD approvals | daftar nama BOD + decision (APPROVE/REVISE/ROLLBACK) + reason + rollbackTargetName = "Testing 2" + respondedAt |
rollbackOptions |
terkunci ke 1 pilihan: "Testing 2" |
| Progress milik Testing (leaf) | TIDAK diubah — tetap APPROVED 100% (nilai/approvedBy/approvedAt utuh). Tidak pernah di-flip di sisi B; flip progress hanya terjadi bila VP REJECT (atau bila item ini yang dipilih jadi target rollback X) |
| Tombol aksi | disabled (on hold) sampai "Testing 2" selesai lagi |
Item yang di-rollback — "Testing" (item B) — setelah "Testing 2" di-approve & closed
| Bentuk B | Nilai / tampilan |
|---|---|
| B leaf (akar) | approvalStatus = WAITING_APPROVAL + rollbackPaused = false; progress tidak di-flip (Completed & Waiting) → tombol approve VP aktif; bila VP REJECT barulah progress terakhir jadi PENDING |
| B non-leaf | root: WAITING_APPROVAL + unpause; leaf rantai-awal (tanpa prereq / prereq rantai awal) & non-leaf: WAITING_APPROVAL + unpause tanpa flip (Completed & Waiting; notif ke VP); leaf yang masih menunggu prereq: tetap NOT_STARTED abu-abu; blocker subtree: tetap pause |
Target rollback — "Testing 2" (item X)
| Elemen UI | Nilai / tampilan |
|---|---|
| Chip status item | Need Approval (karena progress terakhirnya PENDING) — atau NOT_STARTED jika "Testing 2" sendiri masih menunggu prerequisite-nya |
approvalStatus |
Waiting Approval |
bodResult |
null (bukan item yang di-rollback) |
| Progress terakhir (leaf) | APPROVED → PENDING |
| Progress perantara | tidak berubah |
Anak-anak "Testing 2" (X) — semua level, live (restart penuh saat resolve)
| Kondisi anak | Tampilan |
|---|---|
| Leaf (tidak punya anak) & tidak memblokir | chip Need Approval, progress terakhir PENDING, approvalStatus Waiting Approval |
| Non-leaf & tidak memblokir | chip Need Approval (atau normal sesuai derived), approvalStatus Waiting Approval, progress tetap |
| Dipakai sebagai prerequisite oleh sibling live lain (blocker) | seluruh subtree tampil NOT_STARTED + rollbackPaused = true; tombol write disable; baru terbuka saat prereq pemicunya selesai |
Anak-anak "Testing" (B) — saat resolve
Seluruh subtree B: NOT_STARTED abu-abu (rollbackPaused = true), approvalStatus = WAITING_APPROVAL, progress tetap 100%, tombol write disabled. Buka ulang baru terjadi saat "Testing 2" ditutup lagi — lalu mengikuti aturan di atas (semua node → Waiting Approval tanpa flip; punya prereq → NOT_STARTED lanjut).
Aturan display yang dipakai backend (bisa dipakai juga untuk highlight):
bodResult === 'REJECTED_ROLLBACK'ataurollbackPaused === true→ item tampil NOT_STARTED (override).bodResult === 'REJECTED_REVISE'→ tampil "Rejected by BOD" (bodDisplayStatus = REJECTED_BY_BOD).approvalStatus === 'BOD_APPROVAL'→ tampil "BOD Approval".
3. Konsumen downstream — item lain yang prereq-nya = "Testing 2"
Ini bagian yang paling sering mengejutkan: rollback tidak hanya menyentuh Testing & Testing 2. Semua item same-level yang prerequisite langsungnya "Testing 2" ikut terkunci.
| Kondisi konsumen | Tampilan saat rollback | Saat "Testing 2" selesai lagi |
|---|---|---|
| Sudah CLOSED (approved) | tampil NOT_STARTED (rollbackPaused = true); data & progress tetap utuh; semua aksi disabled |
approvalStatus → Waiting Approval (progress tidak di-flip; VP tinggal approve ulang) |
| Belum CLOSED | tampil NOT_STARTED; dikunci | mengikuti alur prereq-nya; lanjut dari status terakhir |
| Anak-anak konsumen (subtree) | ikut NOT_STARTED | release berurutan: anak baru aktif setelah konsumennya ditutup lagi |
Item B (yang di-rollback) tidak masuk kategori konsumen — B di-hold dengan rollbackPaused = true + bodResult = REJECTED_ROLLBACK saat resolve, lalu subtree-nya dibuka ulang bertahap (tanpa flip) ketika "Testing 2" ditutup lagi.
Alur release (rantai Testing 2 → A → P → Q + subtree B):
"Testing 2" closed lagi
├─► A → Waiting Approval ← mulainya di sini (subtree B ikut restart tanpa flip)
├─► B leaf → Waiting Approval (Completed & Waiting; tidak di-flip; flip bila VP REJECT)
└─► B non-leaf → anak leaf rantai-awal → Waiting Approval (Completed & Waiting; tidak di-flip)
A di-approve & closed
└─► P → Waiting Approval
P di-approve & closed
└─► Q → Waiting Approval (dan seterusnya)
Timing-nya bertahap, bukan serentak. Notifikasi saat release mengikuti routing baku: item yang jadi Completed & Waiting Approval → notif ke VP-nya — seluruh node B-side masuk kategori ini (tanpa flip, progress tetap 100%). Flip progress hanya terjadi di sisi target X saat resolve; item yang di-flip (jadi Need Approval) → notif ke Owner/Admin.
Read-only / disabled saat rollbackPaused = true
Semua action berikut mengembalikan 400 ... is on hold: waiting for its prerequisite to be approved and closed — frontend sebaiknya men-disable tombol ini di item/subtree yang di-hold:
- edit data item (PATCH)
- approval respond, re-request, BOD respond
- submit / respond progress
- simpan plans (PUT)
- tambah/hapus member
4. Urutan tampilan di layar dari waktu ke waktu
| Waktu | Yang tampil |
|---|---|
| T0 — BOD vote masuk | List detail "Testing": badge BOD Approval; tiap BOD yang sudah vote tampil di bodApprovals (beri indikator sudah/blm vote) |
| T1 — resolve rollback | "Testing 2" + subtree X: chip NOT_STARTED / Need Approval; progress leaf X jadi PENDING. "Testing" + subtree B: NOT_STARTED abu-abu (rollbackPaused = true), progress tetap 100% APPROVED (tidak di-flip), semua aksi disabled. Konsumen jadi NOT_STARTED abu-abu; notifikasi Rolled Back by BOD ke VP; log aktivitas terisi per item |
| T2 — "Testing 2" di-approve lagi | "Testing 2" → CLOSED; konsumen A → Waiting Approval; subtree B mulai di-restart — semua node B → Waiting Approval tanpa flip (Completed & Waiting, notif Ready for Approval ke VP) |
| T3 — A di-approve | A → CLOSED; P → Waiting Approval; dst |
| T4 — "Testing" di-approve ulang | "Testing" → CLOSED (handover normal); selesai |
Frontend boleh menampilkan status ini apa adanya; urutan cascade dijamin backend (data + log tersedia per item).
5. Contoh payload detail item
GET /api/v1/projects/:projectId/action-plans/:activityId → data (potongan):
{
"id": "activity-testing-b",
"name": "Testing",
"approvalStatus": "WAITING_APPROVAL",
"approvalReason": null,
"bodRoundId": "round-1",
"bodResult": "REJECTED_ROLLBACK",
"bodReason": "hasil pengujian tidak sesuai",
"bodDisplayStatus": "NOT_STARTED",
"rollbackPaused": true,
"approverId": "user-vp-1",
"approverName": "Fajar",
"bodApprovals": [
{
"userId": "bod-1",
"name": "Hendra",
"decision": "ROLLBACK",
"reason": "hasil pengujian tidak sesuai",
"rollbackItemId": "activity-testing2-x",
"rollbackTargetName": "Testing 2",
"respondedAt": "2026-09-10T03:00:00.000Z"
}
],
"rollbackOptions": [{ "id": "activity-testing2-x", "name": "Testing 2" }]
}
Item konsumen (A) yang sedang di-hold:
{
"id": "activity-a",
"name": "A",
"approvalStatus": "CLOSED",
"bodResult": null,
"bodDisplayStatus": "NOT_STARTED",
"rollbackPaused": true
}
Baris progress (leaf "Testing" — item B — setelah rollback resolve): tidak pernah di-flip; hanya berubah bila VP REJECT:
{
"id": "progress-1",
"progress": 100,
"status": "APPROVED",
"approvedBy": "user-vp-1",
"approvedAt": "2026-09-09T09:00:00.000Z",
"description": "…"
}
Baris progress (leaf "Testing 2" — item X — setelah rollback resolve; langsung di-flip):
{
"id": "progress-2",
"progress": 100,
"status": "PENDING",
"approvedBy": null,
"approvedAt": null,
"description": "…"
}
6. Mapping singkat field → tampilan
| Field | Tampilan frontend |
|---|---|
status (derived) |
chip utama item |
bodDisplayStatus = NOT_STARTED |
tampil NOT_STARTED walau approvalStatus lain |
bodDisplayStatus = REJECTED_BY_BOD |
chip/notice "Rejected by BOD" |
bodDisplayStatus = BOD_APPROVAL |
badge "BOD Approval" (menunggu vote) |
approvalStatus = WAITING_APPROVAL |
aksi approve VP aktif |
rollbackPaused = true |
abu-abu + disable semua aksi + helper "on hold" |
progress row status = PENDING |
badge PENDING di list progress |
bodApprovals[].rollbackTargetName |
label target rollback di detail |
Referensi lanjutan (backend internals, boleh diabaikan frontend): .markdown/FLOW_BOD_REJECT_ROLLBACK.md.