think
16px
820px

PENDIRIAN_PT tenant — T2 report: engine renderers (identity kind + atomic confirmMode)

Status: DONE

Commits (worktree ahu-ocr-tidyup, branch refactor/tidyup-foundation):
- c78e9b7 feat(review-engine): atomic confirmMode for field-list sections (PT T2 D1)
- b10f975 feat(review-engine): IdentitySection — engine DEFAULT renderer for kind identity (PT T2 D2)

Test summary: frontend npx vitest run 780 passing / 113 files (baseline 761/112 + 19 new: 6 atomic-mode + 1 section-progress + 11 IdentitySection + 1 ReviewEngine registration). tsc --noEmit clean. Backend suite not run (no backend change, per brief).

What changed

Deliverable 1 — atomic confirmMode in FieldListSection

  • types.ts: ReviewActions gains optional confirmAtomic?(sectionKey, documentId, confirmed): void.
  • FieldListSection.tsx: when section.confirmMode === "atomic" it renders ONE section-level toggle button ("Konfirmasi Modal & Saham" / "Terkonfirmasi") above the field rows, invoking actions.confirmAtomic(section.key, section.documentId, next). State derives live from section.fieldRefs.every(ref => fields[ref]?.confirmed) (all, not just one, per brief's "for safety" instruction). No per-field confirm buttons render in this mode; the Edit affordance is untouched. Disabled when readOnly. Per-field mode (absent/"per-field") is byte-identical — verified by the full pre-existing FieldListSection suite passing unchanged.
  • Extracted FieldRow.tsx out of FieldListSection.tsx's per-ref JSX (identical markup/testids/behavior, parameterized by field/manualConfirmAll/actions/readOnly/suppressConfirm) so Deliverable 2 could reuse it instead of copy-pasting ~100 lines.
  • section-progress.ts needed no change — added a unit test in section-progress.test.ts proving an atomic field-list's progress counts identically to per-field mode (the projector mirrors one DB row onto every ref, so counting each ref lands on the right total either way).

Deliverable 2 — IdentitySection (new default renderer for kind "identity")

  • New IdentitySection.tsx: one base-nova Card per IdentityPersonVM — header = personName + role badges + a matchStatus badge (AUTO/MANUAL → emerald "matched", AMBIGUOUS → amber, UNMATCHED → red-ish). Body = labeled "KTP" block (always rendered: field rows via the shared FieldRow, or a muted "Tidak ada dokumen" marker when docs.ktp is null), labeled "NPWP" block (rendered only when docs.npwp is present), and a "Kontak" row for contact.phoneFieldRef when non-null. Rows reuse FieldRow so confirm/edit/focus behave identically to every other field-list row. Per-person done-glow (CARD_DONE_GLOW ring) once every fieldRef the card can show is confirmed (empty person → never glows).
  • Registered in ReviewEngine.tsx's DEFAULT_RENDERERS["identity"]. Not added to SELF_CHROMED_KINDS — content-only, standard shell (h2/border/badge) still wraps it, confirmed via a new ReviewEngine.test.tsx case.

Self-review notes / concerns

  1. matchStatus → color mapping is an interpretive call. The contract enum (contract/enums.gen.ts) is AUTO | AMBIGUOUS | MANUAL | UNMATCHED — the brief's own language used a different vocabulary ("matched/full → emerald, partial → amber, none/missing → red-ish") and the survey only documented AUTO (green) / AMBIGUOUS (amber) explicitly, calling everything else "neutral gray". I mapped MANUAL to emerald alongside AUTO (both mean "a person IS matched", just auto vs. human-picked) and UNMATCHED to red (survey's implicit "none"). Worth a product-owner glance since it diverges from the survey's literal "else → gray" default.
  2. NPWP-block-hidden-when-null is also an inference, not explicit in the brief (which said "for docs.ktp and docs.npwp (when present)" then only spelled out the absent-doc marker for docs.ktp). I treated KTP as always-rendered-with-a-marker and NPWP as rendered-only-if-present, matching the survey's note that legacy NPWP linkage is optional/best-effort while KTP is not. Flagging in case the intended behavior was an explicit "Tidak ada dokumen" marker for NPWP too.
  3. section-progress.ts still returns {total:0, confirmed:0, done:false} for kind "identity" (unmeasured, same as "diff"/"document-set" today) — this is unchanged/out-of-scope per the brief, but it means the outer shell's confirmed/total badge and emerald done-border never activate for an identity section; only the per-person Card's own ring glows. Not a regression, just noting the visible asymmetry vs. field-list/roster sections.
  4. Neither deliverable wires into any actual page/registry — this is the frontend engine-renderer layer only, per the brief's explicit "Do NOT touch pages, backend, contract" constraint.

No blockers encountered. Did not push (per instructions).