diff --git a/DECISIONS.md b/DECISIONS.md index cc1e3fba..85c81639 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1636,5 +1636,9 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-09-06] Bundled SKV ROT/RUT payout books ONE voucher (one 1513 leg per begäran) and the set is suggested at read time with no hint column: one bank row = one verifikat (match-batch precedent) and a uuid[] hint would need six clear paths and go stale; N vouchers + the 1:N reconciliation split was rejected because its half-failure state has no UI exit, and begäran, not the invoice, is the unit under fakturamodellen. [2026-09-06] Utlägg via lön settles claims with an idempotent RPC after the salary verifikat is posted (pre-checked before posting), not with a trigger on salary_runs -> booked: a raise inside that trigger after the entries exist would leave a paid run with posted verifikat and a retry would double-post; the RPC path fails to "booked, claims still open, re-runnable". [2026-09-06] A privately paid supplier invoice is booked through registerExpenseClaim (verifikat + expense_claims row, source_type expense_claim) with the invoice's kontering as custom lines, and a person-paid inbox document goes to the core route with inbox_item_id instead of the extension's convert endpoint: the form's switch, the second entry generator and the convert bypass were three write paths for one fact, so one writer wins over adding a claims insert beside the old generator (the issue's shape) or copying the branch into the convert handler. +[2026-09-07] Draft stamp moved to the page margin (absolute + fixed) instead of the reporter's position:fixed corner badge: the 40pt top margin is the only place that is guaranteed empty on every page, and the stamp must not overlap the header title on the right. +[2026-09-07] Hyphenation disabled per Text node in the invoice template, not via a global Font.registerHyphenationCallback: the global hook would also change line breaking in årsredovisning, payslips and every report PDF; that is a separate decision. +[2026-09-07] Invoice PDF word wrapping keeps a word whole whenever an ink-width estimate says it fits its column (ordinary Swedish compounds of 17 to 25 characters always do); only a wider token (URL, e-mail, reference) gets break points, after separators and where the column is full, with react-pdf's hyphen at the break (a break after "/" prints "/-"; unavoidable in textkit, accepted over a dropped token). A flat character cap was tried first and rejected: it split ordinary compounds at arbitrary positions. Free-text rows and notes are kept on one page only while a rendered-line estimate (chunks counted, 12 lines max, safe for any font) says they fit; past that they may split, because a non-splittable block taller than a page is clipped silently. +[2026-09-07] Line breaks in line descriptions are collapsed by every single-line consumer (Peppol cbc:Name, accrual voucher text) and additionally in the SIE writer's quoted-text escaper: SIE is one record per line by spec, so the writer guards the format regardless of where the text came from. [2026-09-07] Enable Banking callbacks return to the initiating white-label host by recording the allowlisted request origin on the pending row and replaying the callback there, instead of extending the provider_otc handoff from PR #2305: the brand host already holds the session, so a /login?next= bounce on that host forwards straight back into the callback with cookies, needing one nullable column and no encrypted payload, no second table and no cron. The provider redirect URI stays canonical, so nothing changes in the Enable Banking console. Stripe, Gmail and cloud backup have the same shape but no partner-domain users yet; tracked as a follow-up issue rather than built speculatively. [2026-09-07] Stripe checkout and portal return URLs resolve through the existing resolveRequestAppOrigin allowlist (NEXT_PUBLIC_WHITELABEL_DOMAINS), not a DB brand lookup: it is the same trust boundary invites and email-change links already use, so one allowlist governs every host we redirect a browser to. Return paths stay fixed literals; no caller-supplied URL is accepted. Session-expiry and company-switch handling were left alone: the middleware already bounces to /login on the same host with the path preserved, and the webhook keys on company_id metadata. diff --git a/app/(dashboard)/invoices/[id]/page.tsx b/app/(dashboard)/invoices/[id]/page.tsx index ded7e83d..9a5f3467 100644 --- a/app/(dashboard)/invoices/[id]/page.tsx +++ b/app/(dashboard)/invoices/[id]/page.tsx @@ -2256,13 +2256,13 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st {invoice.items.map((item) => isTextLikeLine(item) ? ( - + {item.description || ' '} ) : ( - + {item.description} {lineSubInfo(item)} @@ -2284,11 +2284,11 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
{invoice.items.map((item) => isTextLikeLine(item) ? ( -

{item.description || ' '}

+

{item.description || ' '}

) : (
-

{item.description}

+

{item.description}

{item.quantity} {item.unit} × {formatCurrency(item.unit_price, invoice.currency)}

diff --git a/components/invoices/AutoGrowTextarea.tsx b/components/invoices/AutoGrowTextarea.tsx new file mode 100644 index 00000000..b5c3e3a6 --- /dev/null +++ b/components/invoices/AutoGrowTextarea.tsx @@ -0,0 +1,51 @@ +'use client' + +import { forwardRef, useCallback, useLayoutEffect, useRef, type TextareaHTMLAttributes } from 'react' +import { cn } from '@/lib/utils' + +/** + * A single-line-looking textarea that grows with its content. + * + * Invoice line descriptions used to be ``s, so a user could never put + * a line break where they wanted one; the PDF then wrapped wherever the + * column ran out. This keeps the dense one-row look of the editor grid while + * letting Enter insert a newline, and re-measures on every render so a + * prefilled multi-line description (edit flow) opens at the right height. + */ +export const AutoGrowTextarea = forwardRef>( + function AutoGrowTextarea({ className, onInput, ...props }, ref) { + const inner = useRef(null) + + const setRef = useCallback( + (el: HTMLTextAreaElement | null) => { + inner.current = el + if (typeof ref === 'function') ref(el) + else if (ref) ref.current = el + }, + [ref], + ) + + useLayoutEffect(() => { + resize(inner.current) + }) + + return ( +