PENDIRIAN_PT tenant — T3b report: V2 review page (frontend)
Status: DONE
Commits (worktree ahu-ocr-tidyup, branch refactor/tidyup-foundation)
88c8aae—feat(review): use-pt-review-engine-mutations — PT V2 review page hooks (T3b D1)
frontend/src/hooks/use-pt-review-engine-mutations.ts+ test file. Optimistic
confirm/unconfirm for fields, themodal_sahamatomic control, and all three
PT rosters (kbli / pengurus_pemegang_saham / bukti_setor), plus non-optimistic
edit/phone-update/finalize hooks.8ac4c40—fix(review): usePtOptimisticConfirmContactEntry — phone attestation is not an ExtractedField (T3b self-review)
Self-caught gap (see Concerns): a dedicated hook for the identity phone
field's confirm/unconfirm, which must post{entityType:"contactInfoEntry", entryIndex}rather than the generic{fieldKey}body.2ea4c06—feat(review): PtPendirianReviewPageV2 + route — PT V2 review page (T3b D2/D3)
frontend/src/pages/PtPendirianReviewPageV2.tsx+ test file, and the
pendirian/:submissionId/review-v2route registered in the same
RoleGuard("notaris")branch as the untouched legacy
pendirian/:submissionId/review.
Test summary
Frontend npx vitest run: 825 passing / 115 files (baseline 783/113 + 42
new: 24 hook tests + 18 page tests). tsc --noEmit clean. Backend untouched,
not run (no backend change). Strict TDD throughout: both the hooks test file
and the page test file were run and confirmed failing (missing module) before
any implementation existed; the usePtOptimisticConfirmContactEntry addition
followed the same failing-test-first cycle.
What was built
usePtOptimisticConfirmField— per-field confirm/unconfirm, byte-shape
identical to tenant #1's hook.usePtOptimisticConfirmAtomic— flips EVERY fieldRef the caller passes
(the page resolves them from themodal_sahamsection at call time) in one
optimistic patch, posts{entityType:"aktaModal"}.usePtOptimisticConfirmRosterRow— one hook dispatching the polymorphic
FieldConfirmRefbody per rosterKey:kbli→{entityType:"kbli", entityId:rowId};pengurus_pemegang_saham→ strips the row id's
<role>:prefix intoentityIdand mapsdetail.roleto the backend's
entityType;bukti_setor→ parses the row id's trailing entryIndex and
posts{entityType:"buktiSetorEntry", entryIndex}against the row's OWN
document (not the roster section's, which isnullfor this roster).usePtOptimisticConfirmContactEntry(self-review addition) — confirm
for the identity phone field,{entityType:"contactInfoEntry", entryIndex}.usePtEditField,usePtUpdatePhone— non-optimistic PATCH hooks,
invalidate the engine cache key directly on success.usePtFinalizeEngine— POST/review/finalize; mapsWRONG_STATUS→
"Status permohonan tidak valid",UNCONFIRMED_FIELDS→ "Masih ada N field
belum dikonfirmasi" (N fromextra.unconfirmed.length) + calls
scrollToFirstUnconfirmed(),VALIDATION_FAILED→ "Validasi gagal:
" (never a bare ruleCode), else a generic fallback. PtPendirianReviewPageV2— assembled fromReviewEngine+
ReviewProgressHeader+ReviewPdfViewer, mirroring tenant #1's structure
but with no pernyataan/attestation card and no section-approval action (T1
projector emits every sectionapprovable:false) —submitGate()and
ReviewProgressHeaderneeded zero changes to work correctly with an empty
requiredSections/declarations. UniversalRosterSection/
IdentitySection/FieldListSectionrender as-is, nosectionRenderers
override (per the brief's explicit deferral).- Route:
pendirian/:submissionId/review-v2, sameRoleGuardbranch as
the legacy route, which is untouched.
Design decisions / self-review findings
- Phone-field confirm is NOT
{fieldKey:"teleponPengurus"}(the biggest
finding). Re-reading the T1 projector closely:IdentityPersonVM's phone
ReviewFieldVM.confirmed/confirmedAt/provenanceare derived purely from
the matchedcontactInfoExtractionentry'snotarisConfirmedflag — there
is noExtractedFieldrow backing "teleponPengurus" at all. The brief's D2
text ("confirmField ... all are documentId+fieldKey") reads as if the
generic path covers every field including identity ones; taken literally,
confirming the phone would 200-OK by upserting a phantomExtractedField
the projector never reads back on GET — the checkbox would silently revert
to unconfirmed on the next reload. I verified this against
backend/src/services/field-confirmation.ts'scontactInfoEntrycase
(reads/writescontactInfoExtraction.entries[entryIndex].notarisConfirmed)
and addedusePtOptimisticConfirmContactEntry+ a page-level
confirmFieldoverride so the phone row's confirm click actually persists.
Only the EDIT override was explicitly spec'd by the brief; the CONFIRM
override is my own addition — flagging in case the intended fix was
different (e.g. projecting an actual ExtractedField for parity instead). detail.roleis snake_case"pemegang_saham", backendentityTypeis
camelCase"pemegangSaham"— the brief's own bullet said to verify this
against T1's payload; confirmed by reading the projector
(pt-pendirian-review.ts:338) and mapped explicitly (tested).bukti_setorroster rows resolve their:docIdpath segment from
row.detail.documentId, not a fixed section documentId (that section's
SectionVM.documentIdisnullin the projector) — each Bukti Setor
document owns its ownentriesJSON blob keyed byentryIndex, verified
againstfield-confirmation.ts'sbuktiSetorEntrycase.
pengurus_pemegang_saham/kblirows use the roster section's documentId
instead, sincedireksi/komisaris/pemegangSaham/kbliresolve by
rowIdalone server-side — the URL:docIdthere is only used for a
submission-membership existence check, confirmed by reading
resolveFieldRef/confirmWithininroutes/submissions.ts.- Completed-state banner links to
SubmissionStatusPage(Lihat Status
→/pendirian/:id/status), not a packet download — PT has no
/pendirian/:id/packetroute today (unlike PP's), and adding one is
backend scope, out of bounds per the brief's constraints. - Edit dialog hides the "Alasan perubahan" field for the phone override
(PATCH identity-matches takes no reason) rather than showing it unused —
a minor UI deviation from "open the exact same dialog," flagging in case a
fully identical dialog (with an ignored reason field) was intended instead. approveSectionis a no-op inReviewActions— PT has no
section-approval concept at all (everySectionSchemais
approvable:false), so this is a deliberately-unused required method
rather than a partially-wired feature.
Concerns
- Item 1 above (phone confirm override) is a deviation I introduced beyond
the brief's literal text because the literal reading would have shipped a
silently-broken control — worth a second look from whoever wrote the T1
projector to confirm this is the intended fix, not a symptom of the
projector needing its own change instead. - Roster/detail-driven bespoke presentation (WNA badges, bukti-setor
cross-ref highlighting, kehadiranRups) is deliberately deferred to the
universalRosterSection's generic Detail-toggle grid, per the brief's
explicit instruction — expect this to come back as smoke-test feedback the
same way PP's BO roster did.
Not done (out of scope per the brief)
- Legacy
PendirianReviewPage/route, backend, contract, and engine
components (ReviewEngine.tsx,FieldListSection.tsx,RosterSection.tsx,
IdentitySection.tsx) untouched — confirmed viagit log/git diff --stat
scoped to this task's 3 commits. - No
git push.