fix(ux): book documents directly from inbox + attach existing underlag when booking transactions (#670)
* fix(inbox): re-add Bokför manuellt on unmatched documents Pilot feedback: a document in Dokumentinkorg could not be booked without first matching it to a bank transaction, which is impossible for cash expenses and other entries with no bank movement. The backend (/items/:id/book-direct) and BookDirectlyDialog already support standalone booking — re-expose the button in the unmatched state. The dialog still offers optional transaction selection inside. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(transactions): pick existing inbox document when booking manually Pilot feedback: "Bokför manuellt" from a transaction only allowed uploading new files — an already-uploaded underlag from the inbox could not be attached. Add a select mode to InboxDocumentPicker (onSelect prop; journalEntryId now optional) and mount it in TransactionBookingDialog: picked documents are linked after the journal entry is created via /api/documents/{id}/link with inbox_item_id, which also stamps the inbox item as consumed so it drops out of every pending surface. Non-ok link responses now count toward the failure toast (previously only network errors did). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(documents): address PR #670 review — stale preview dialog, JE tenancy check Review findings: - InboxDocumentPicker left the preview dialog floating open when a pick was confirmed from inside it (previewItem was never cleared before onClose; the component stays mounted, so the on-open reset never ran). Clear it in both select and link mode. (greptile) - linkToJournalEntry verified the document's company but trusted the client-supplied journal_entry_id (FK only requires existence). Add an explicit company-scoped journal entry lookup; misses map to the existing DOC_LINK_ENTRY_NOT_FOUND envelope. RLS prevented any data leak either way — this makes the rejection explicit. New regression test covers the cross-tenant case. (compliance-swarm A.8.28) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f7cd1b86e7
commit
cac692e293
@@ -20,19 +20,25 @@ import { FileText, ImageIcon, Loader2, Search, Inbox, Eye } from 'lucide-react'
|
||||
|
||||
// InboxDocumentPicker
|
||||
//
|
||||
// Opens from JournalEntryAttachments ("Välj från inkorgen"). Lists invoice-inbox
|
||||
// documents that have not yet been consumed (no supplier invoice, no journal
|
||||
// entry, not matched to a transaction, document not already linked) so the user
|
||||
// can attach one as underlag to the current verifikat. Picking one links the
|
||||
// document to the journal entry AND stamps the inbox item so it drops out of the
|
||||
// active inbox — see app/api/documents/[id]/link/route.ts.
|
||||
// Lists invoice-inbox documents that have not yet been consumed (no supplier
|
||||
// invoice, no journal entry, not matched to a transaction, document not
|
||||
// already linked) so the user can attach one as underlag to a verifikat.
|
||||
//
|
||||
// Two modes:
|
||||
// - Link mode (JournalEntryAttachments, "Välj från inkorgen"): `journalEntryId`
|
||||
// is set and picking a document immediately links it to the journal entry AND
|
||||
// stamps the inbox item so it drops out of the active inbox — see
|
||||
// app/api/documents/[id]/link/route.ts.
|
||||
// - Select mode (TransactionBookingDialog, "Välj befintligt underlag"): the
|
||||
// journal entry does not exist yet, so `onSelect` is provided instead and the
|
||||
// pick is reported to the parent, which links after the entry is created.
|
||||
//
|
||||
// Each row carries a preview button (eye) that opens a quick dialog rendering
|
||||
// the document inline, so the user can confirm the right file before attaching.
|
||||
// Attaching is the row's primary click (fast path) and is also offered from
|
||||
// inside the preview dialog (preview → confirm).
|
||||
|
||||
interface AvailableInboxDoc {
|
||||
export interface AvailableInboxDoc {
|
||||
inbox_item_id: string
|
||||
document_id: string
|
||||
file_name: string
|
||||
@@ -49,9 +55,12 @@ interface AvailableInboxDoc {
|
||||
interface Props {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
journalEntryId: string
|
||||
/** Called after a successful link so the parent can refresh its document list. */
|
||||
onLinked: () => void
|
||||
/** Link mode: the journal entry to link the picked document to. */
|
||||
journalEntryId?: string
|
||||
/** Link mode: called after a successful link so the parent can refresh its document list. */
|
||||
onLinked?: () => void
|
||||
/** Select mode: report the picked document to the parent instead of linking. */
|
||||
onSelect?: (doc: AvailableInboxDoc) => void
|
||||
}
|
||||
|
||||
function isImageType(type: string | null): boolean {
|
||||
@@ -69,7 +78,7 @@ function DocIcon({ mime }: { mime: string | null }) {
|
||||
return <FileText className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
}
|
||||
|
||||
export default function InboxDocumentPicker({ open, onClose, journalEntryId, onLinked }: Props) {
|
||||
export default function InboxDocumentPicker({ open, onClose, journalEntryId, onLinked, onSelect }: Props) {
|
||||
const t = useTranslations('journal_attachments')
|
||||
const { toast } = useToast()
|
||||
|
||||
@@ -113,6 +122,18 @@ export default function InboxDocumentPicker({ open, onClose, journalEntryId, onL
|
||||
}, [items, search])
|
||||
|
||||
async function handlePick(item: AvailableInboxDoc) {
|
||||
// Select mode: the journal entry doesn't exist yet — hand the pick to the
|
||||
// parent and let it link after creation. Clear the preview first: the
|
||||
// preview dialog's open state is `previewItem !== null`, so leaving it set
|
||||
// would strand a floating preview after the picker closes (the component
|
||||
// stays mounted; the on-open reset only runs on the next open).
|
||||
if (onSelect) {
|
||||
setPreviewItem(null)
|
||||
onSelect(item)
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
if (!journalEntryId) return
|
||||
setLinkingId(item.document_id)
|
||||
try {
|
||||
const res = await fetch(`/api/documents/${item.document_id}/link`, {
|
||||
@@ -133,7 +154,8 @@ export default function InboxDocumentPicker({ open, onClose, journalEntryId, onL
|
||||
return
|
||||
}
|
||||
toast({ title: t('picker_linked') })
|
||||
onLinked()
|
||||
setPreviewItem(null)
|
||||
onLinked?.()
|
||||
onClose()
|
||||
} catch {
|
||||
toast({ title: t('picker_link_failed'), variant: 'destructive' })
|
||||
|
||||
Reference in New Issue
Block a user