think
16px
820px

One record, one page

Decision: /documents/d/{id} becomes the only detail page. /letters/{id} redirects to it.
The two LISTS (/letters, /documents) stay separate.

Why: a letter is a document (migration 00210). Two detail pages is the UI holding a model
the backend gave up. That disagreement already produced one duplicate — both views render an
activity timeline; P5 unified the composer and left two surfaces.


The shape

The detail page becomes a shell that renders what the record has, not a document page with
a letter tab bolted on.

every record + letter facet
Header title, Document ID, status + register number / agenda, direction, lifecycle
Tabs Overview · Versions · Access · Share · Signatures · Workflow · Activity · Related + Correspondence
Primary action Download whatever the record needs next (below)

Primary action follows record state, not which page you came from:

state primary
draft letter Edit
numbered, not sent Mark as sent
anything filed with nothing pending Download

The document view already has nextStepKey for exactly this — it just doesn't know about letters.


Where Dispose goes

It doesn't move. Action row, tertiary, lifecycle group — same place as today.

The row for a letter reads:
Download · Preview · │ · Edit details · Assign number · Dispose · Mark as sent · │ · kebab

That fits because the letter-irrelevant document actions drop out (Convert to PDF, Page setup,
version upload). Seven visible against the document's current eight.

The Correspondence tab holds the result — assignments, Mark done, Revoke, timeline —
exactly where they sit today. The button is not duplicated into it.

A letter opens on the Correspondence tab, not Overview. Today a letter opens straight onto
its disposisi list; defaulting to document metadata would put a click in front of the thing
inbound letters exist for.


Zero loss — I inventoried all 11 actions and all 9 fields

Full table in the impl spec §8. The result:

  • 9 of 11 actions keep their exact slot and prominence.
  • 9 register fields (nomor, agenda, type, sifat, direction, counterparty, created, received,
    letter status) all land in the header + Correspondence tab.
  • 2 collisions resolved, not merged away. Letter status and records status are different
    facts — both show. Sifat and document classification are the same field — one tag.
  • 1 thing has to be BUILT: attachments (lampiran). The document view has no attachments UI
    at all. Missing this would be the silent loss.

Removing 2 things — APPROVED

Both are links to the page you'd already be on:

  1. "Open in Documents" button on the letter — points at the merged page.
  2. CorrespondencePanel on the document — a summary card plus "Open in Correspondence",
    pointing back here. Its four facts (number, direction, status, type) are preserved in the
    header; only the wrapper and button die.

Nothing else goes. They get deleted in step 3, not before — until /letters/{id} redirects,
these two are the only way to cross between the faces.


The external-share gap — don't nuke it

What's actually exposed is narrower than I first said. Migration 00210:106-117 maps a
letter's sifat into documents.classification (and protection_floor). So DLP forward, the
protection floor, the egress watermark and the access log all already govern letters correctly —
a confidential letter is treated exactly like a confidential document today.

What's ungoverned is status. A draft/in_review/approved/rejected letter has no number, no
approval, no dispatch record. An external link puts a document that officially doesn't exist yet
outside the building, with nothing in correspondence saying it left.

Nuking is the wrong shape. That "no external sharing" note was written when letters weren't
documents — no folder, no ACL, no retention, so refusal was the only answer available. A filed
letter is now an archive record, and records get shared. Blocking the link doesn't stop the
handover; it just stops the evidence, since the share log is the only record an external handover
ever leaves.

The fix: one predicate, ~15 lines, beside the existing refuseSecureFolderShare in
CreateShareLink — refuse when the letter is unissued, allow once numbered/sent/registered. Not
admin-exempt, same reasoning as the secure-folder gate: it enforces the record's construction,
which no role can satisfy. Refusal code correspondence.letter.not_issued.

Three follow-ons: retire the now-false Share tab note · leave List/Revoke open so links minted
during the silent window stay revocable · count existing share links on letter ids in demo and
prod before assuming zero.


What does NOT move

  • /letters the list. "What correspondence is in flight" ≠ "what does the organisation
    hold". Different working sets. Merging the lists was already declined in P5 and nothing here
    changes that.
  • Correspondence stays in the nav. One detail page, two entry points.

Order of work — DONE

  1. Shell + facet contracts (0c0cf21d) — no behaviour change; verified by class fingerprint.
  2. Correspondence as a real tab (bb18ca2e) — and it uncovered a live ?tab= off-by-one.
  3. Actions + register identity (6b42b73b), then the flip (b051d56b).

Two surfaces turned up only by driving the page, not reading it: Activity showed
"Version 1 added" and nothing else on a numbered, disposed letter, and a draft letter
lost its body preview. Both are fixed; both would have shipped silently.


Risks

Regression, not effort. The letter view is dense workflow UI. Step 2 is where quality is won
or lost.

Size. DocumentDetailView.tsx is 3,801 lines; LetterDetailView.tsx is 1,914. Merging
without step 1 produces a ~5,000-line component nobody can change safely. Step 1 is what makes
the merge survivable — and it is overdue on its own.

Links. Disposisi notifications, inbox tasks and register rows must point at the canonical
URL, or the split reappears through the back door.


One thing to decide

After the merge, does Correspondence stay a top-level nav item, or become a saved view of
Documents? I'd keep it — it is a different question, not a different record. Say if you disagree;
it changes step 3.