think
16px
820px

PENDIRIAN_PT tenant — T3b report: V2 review page (frontend)

Status: DONE

Commits (worktree ahu-ocr-tidyup, branch refactor/tidyup-foundation)

  1. 88c8aaefeat(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, the modal_saham atomic control, and all three
    PT rosters (kbli / pengurus_pemegang_saham / bukti_setor), plus non-optimistic
    edit/phone-update/finalize hooks.
  2. 8ac4c40fix(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.
  3. 2ea4c06feat(review): PtPendirianReviewPageV2 + route — PT V2 review page (T3b D2/D3)
    frontend/src/pages/PtPendirianReviewPageV2.tsx + test file, and the
    pendirian/:submissionId/review-v2 route 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 the modal_saham section at call time) in one
    optimistic patch, posts {entityType:"aktaModal"}.
  • usePtOptimisticConfirmRosterRow — one hook dispatching the polymorphic
    FieldConfirmRef body per rosterKey: kbli{entityType:"kbli", entityId:rowId}; pengurus_pemegang_saham → strips the row id's
    <role>: prefix into entityId and maps detail.role to 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 is null for 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; maps WRONG_STATUS
    "Status permohonan tidak valid", UNCONFIRMED_FIELDS → "Masih ada N field
    belum dikonfirmasi" (N from extra.unconfirmed.length) + calls
    scrollToFirstUnconfirmed(), VALIDATION_FAILED → "Validasi gagal:
    " (never a bare ruleCode), else a generic fallback.
  • PtPendirianReviewPageV2 — assembled from ReviewEngine +
    ReviewProgressHeader + ReviewPdfViewer, mirroring tenant #1's structure
    but with no pernyataan/attestation card and no section-approval action (T1
    projector emits every section approvable:false) — submitGate() and
    ReviewProgressHeader needed zero changes to work correctly with an empty
    requiredSections/declarations. Universal RosterSection/
    IdentitySection/FieldListSection render as-is, no sectionRenderers
    override (per the brief's explicit deferral).
  • Route: pendirian/:submissionId/review-v2, same RoleGuard branch as
    the legacy route, which is untouched.

Design decisions / self-review findings

  1. Phone-field confirm is NOT {fieldKey:"teleponPengurus"} (the biggest
    finding). Re-reading the T1 projector closely: IdentityPersonVM's phone
    ReviewFieldVM.confirmed/confirmedAt/provenance are derived purely from
    the matched contactInfoExtraction entry's notarisConfirmed flag — there
    is no ExtractedField row 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 phantom ExtractedField
    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's contactInfoEntry case
    (reads/writes contactInfoExtraction.entries[entryIndex].notarisConfirmed)
    and added usePtOptimisticConfirmContactEntry + a page-level
    confirmField override 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).
  2. detail.role is snake_case "pemegang_saham", backend entityType is
    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).
  3. bukti_setor roster rows resolve their :docId path segment from
    row.detail.documentId
    , not a fixed section documentId (that section's
    SectionVM.documentId is null in the projector) — each Bukti Setor
    document owns its own entries JSON blob keyed by entryIndex, verified
    against field-confirmation.ts's buktiSetorEntry case.
    pengurus_pemegang_saham/kbli rows use the roster section's documentId
    instead, since direksi/komisaris/pemegangSaham/kbli resolve by
    rowId alone server-side — the URL :docId there is only used for a
    submission-membership existence check, confirmed by reading
    resolveFieldRef/confirmWithin in routes/submissions.ts.
  4. Completed-state banner links to SubmissionStatusPage (Lihat Status
    /pendirian/:id/status), not a packet download — PT has no
    /pendirian/:id/packet route today (unlike PP's), and adding one is
    backend scope, out of bounds per the brief's constraints.
  5. 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.
  6. approveSection is a no-op in ReviewActions — PT has no
    section-approval concept at all (every SectionSchema is
    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
    universal RosterSection'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 via git log/git diff --stat
    scoped to this task's 3 commits.
  • No git push.