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:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
a977a67063
commit
4dbd19aeb0
@@ -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>
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user