fix(transactions): the underlag column is one surface, not a dropzone and an orphan button (#1628)

* fix(transactions): the underlag column is one surface, not a dropzone and an orphan button

The Bokfor transaktion dialog stretched its empty dropzone into a
45/72vh well and pinned "Valj befintligt underlag" alone at the very
bottom of the column, visually disconnected from the dropzone it
belongs to.

- Empty state: the underlag column now sizes to its content and
  top-aligns; the inbox picker renders as a quiet full-width dashed
  row directly under the dropzone ("eller valj befintligt underlag
  fran inkorgen"), so drop-a-file and pick-from-inbox read as one
  intake surface. The fixed-height sticky column returns as soon as
  a document previews there (uploaded, picked, or pre-linked).
- Grid rebalanced from 1fr/520px to 2fr/3fr so the kontering side
  dominates while nothing is being previewed on the left.
- QuickReviewDialog gets the same footer-row treatment for its picker
  trigger inside the underlag collapsible, so both #1620 surfaces
  present the affordance identically (disabled-while-booking kept).

Presentation only: upload path, select-mode picker held until booking,
linkDocuments with inbox_item_id, picked-state resets on close and
transaction change, and duplicate-match wiring are all unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: record the QuickReviewDialog picker-trigger consistency decision

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-16 18:06:53 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent a977a67063
commit 4dbd19aeb0
5 changed files with 83 additions and 58 deletions
+1
View File
@@ -1019,3 +1019,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-08-15] Confirmed intentional (Swedish-review note): with override=true and an unresolvable filename, the attach endpoint links a document to any same-company, same-declared-year, posted verifikat, migrated or not. This mirrors /api/documents/[id]/link, which imposes no filename check at all, so it introduces no new capability class; tenant, year and period-lock enforcement always apply.
[2026-08-15] BankID tabs bind to a random non-secret `flowId` signed into the shared flow cookie and sent as a request header after start or explicit resume: mode pinning alone cannot distinguish two same-mode tabs, so an older tab could otherwise silently follow, cancel, or complete a newer person's identification after `/start` replaced the origin-wide cookie. This supersedes the 2026-08-15 decision that deliberately skipped mode matching on active polls.
[2026-08-15] Did not apply BankID migration `20260815120000` to Supabase staging during PR #1625 follow-through: read-only reconciliation found 14 staging-only and 99 branch-only migration versions, so applying on top of that divergent ledger would violate the no-orphan rule. Production is reconciled with zero remote-only versions and exactly this PR migration local-only; hosted pg-real validates the migration until staging is reconciled.
[2026-08-16] Restyled QuickReviewDialog's inbox-picker trigger to the same full-width dropzone-footer row as TransactionBookingDialog even though it did not share the orphan-button layout: both surfaces come from #1620 and should present the same underlag affordance; the alternative (leaving a small outline button in one dialog and a footer row in the other) would split the visual language of one control. Presentation only, disabled-while-booking kept (PR #1628).
@@ -663,17 +663,19 @@ export default function QuickReviewDialog({
{/* Locked while the booking is in flight: handleConfirm captured
pickedInboxDocs when it started, so anything picked now would
never be linked and would then be cleared on completion,
vanishing from the list with no error to explain it. */}
<Button
vanishing from the list with no error to explain it.
Styled as the dropzone's footer (same treatment as
TransactionBookingDialog) so upload and inbox-pick read as
one underlag surface. */}
<button
type="button"
variant="outline"
size="sm"
disabled={isProcessing}
onClick={() => setInboxPickerOpen(true)}
className="flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-muted-foreground/25 px-3 py-2 text-[13px] text-muted-foreground transition-colors duration-150 hover:border-primary/50 hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
>
<Inbox className="mr-2 h-4 w-4" />
{t('doc_pick_existing')}
</Button>
<Inbox className="h-4 w-4" />
<span>{t('doc_pick_existing_inline')}</span>
</button>
</div>
)}
</div>
@@ -282,10 +282,19 @@ export default function TransactionBookingDialog({
</p>
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,520px)]">
{/* Document column: sticky on desktop so the receipt stays visible
while the form scrolls; stacks above the form on smaller screens. */}
<div className="flex h-[45vh] flex-col gap-3 lg:sticky lg:top-0 lg:h-[72vh] lg:self-start">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]">
{/* Underlag column: one coherent surface. Empty state sizes to its
content (dropzone + attached inbox-picker row) and top-aligns;
once a document exists the column takes the fixed viewer height
and stays sticky on desktop while the form scrolls. Stacks above
the form on smaller screens. */}
<div
className={
currentDocId
? 'flex h-[45vh] flex-col gap-3 lg:sticky lg:top-0 lg:h-[72vh] lg:self-start'
: 'flex flex-col gap-2 lg:sticky lg:top-0 lg:self-start'
}
>
{currentDocId ? (
<DocumentViewerPane
documentId={currentDocId}
@@ -294,57 +303,55 @@ export default function TransactionBookingDialog({
className="min-h-0 flex-1"
/>
) : (
<div className="min-h-0 flex-1">
<DocumentUploadZone
files={uploadedFiles}
onFilesChange={setUploadedFiles}
/>
</div>
<DocumentUploadZone
files={uploadedFiles}
onFilesChange={setUploadedFiles}
/>
)}
{/* Attach controls: only when the transaction has no pre-linked
document (a pre-linked one is already the verifikat's underlag). */}
{!preexistingDocId && (
<div className="shrink-0 space-y-2">
{pickedInboxDocs.length > 0 && (
<div className="space-y-1">
{pickedInboxDocs.map((doc) => (
<div
key={doc.document_id}
className="flex items-center gap-2 text-sm py-1.5 px-2 rounded-sm bg-muted/50"
>
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="truncate flex-1">
{doc.supplier_name ?? doc.file_name}
</span>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 shrink-0"
aria-label={t('doc_picked_remove')}
onClick={() =>
setPickedInboxDocs((prev) =>
prev.filter((d) => d.document_id !== doc.document_id),
)
}
currentDocId ? (
<div className="shrink-0 space-y-2">
{pickedInboxDocs.length > 0 && (
<div className="space-y-1">
{pickedInboxDocs.map((doc) => (
<div
key={doc.document_id}
className="flex items-center gap-2 text-sm py-1.5 px-2 rounded-sm bg-muted/50"
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setInboxPickerOpen(true)}
>
<Inbox className="h-4 w-4 mr-2" />
{t('doc_pick_existing')}
</Button>
{inDialogDocId && (
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="truncate flex-1">
{doc.supplier_name ?? doc.file_name}
</span>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 shrink-0"
aria-label={t('doc_picked_remove')}
onClick={() =>
setPickedInboxDocs((prev) =>
prev.filter((d) => d.document_id !== doc.document_id),
)
}
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setInboxPickerOpen(true)}
>
<Inbox className="h-4 w-4 mr-2" />
{t('doc_pick_existing')}
</Button>
<Button
type="button"
variant="ghost"
@@ -357,9 +364,20 @@ export default function TransactionBookingDialog({
<X className="h-3.5 w-3.5 mr-1.5" />
{t('doc_clear')}
</Button>
)}
</div>
</div>
</div>
) : (
/* Secondary intake path, rendered as the dropzone's footer so
"drop a file" and "pick from the inbox" read as one surface. */
<button
type="button"
onClick={() => setInboxPickerOpen(true)}
className="flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-muted-foreground/25 px-3 py-2 text-[13px] text-muted-foreground transition-colors duration-150 hover:border-primary/50 hover:text-foreground"
>
<Inbox className="h-4 w-4" />
<span>{t('doc_pick_existing_inline')}</span>
</button>
)
)}
</div>
+2
View File
@@ -2785,6 +2785,7 @@
"doc_label": "Receipt",
"doc_attached_count": "{count} attached",
"doc_pick_existing": "Choose from the inbox",
"doc_pick_existing_inline": "or choose an existing document from the inbox",
"doc_picked_remove": "Remove document",
"doc_link_failed_title": "Receipt could not be attached",
"doc_link_failed_description": "{count} file(s) could not be linked to the journal entry.",
@@ -2803,6 +2804,7 @@
"doc_label": "Receipt (optional)",
"doc_attached_count": "{count} attached",
"doc_pick_existing": "Choose existing document",
"doc_pick_existing_inline": "or choose an existing document from the inbox",
"doc_picked_remove": "Remove document",
"doc_clear": "Remove document",
"doc_link_failed_title": "Receipt could not be attached",
+2
View File
@@ -2785,6 +2785,7 @@
"doc_label": "Underlag",
"doc_attached_count": "{count} bifogade",
"doc_pick_existing": "Välj från inkorgen",
"doc_pick_existing_inline": "eller välj befintligt underlag från inkorgen",
"doc_picked_remove": "Ta bort underlag",
"doc_link_failed_title": "Underlag kunde inte bifogas",
"doc_link_failed_description": "{count} fil(er) kunde inte länkas till verifikationen.",
@@ -2803,6 +2804,7 @@
"doc_label": "Underlag (valfritt)",
"doc_attached_count": "{count} bifogade",
"doc_pick_existing": "Välj befintligt underlag",
"doc_pick_existing_inline": "eller välj befintligt underlag från inkorgen",
"doc_picked_remove": "Ta bort underlag",
"doc_clear": "Ta bort underlag",
"doc_link_failed_title": "Underlag kunde inte bifogas",