diff --git a/app/(dashboard)/pending/page.tsx b/app/(dashboard)/pending/page.tsx index 0e2c48ae..2696bf03 100644 --- a/app/(dashboard)/pending/page.tsx +++ b/app/(dashboard)/pending/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useCallback, useMemo, Fragment } from 'react' +import { useState, useEffect, useCallback, useMemo, Fragment, createContext, useContext } from 'react' import { useTranslations } from 'next-intl' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' @@ -304,7 +304,52 @@ function formatRelativeTime(dateStr: string): string { return `${diffDays} dagar sedan` } +/** + * Account number -> account name, for the proposal previews. + * + * A preview line showed the account number next to the line's own description, + * so "5890 Utlägg Norwegian" hid the fact that 5890 is Övriga resekostnader. + * The number alone is not readable and the description is not the account, so + * approving meant trusting a label that never named what was being debited. + * + * Owned by the page rather than a module-level cache: the map is per company, + * and a cache that outlives the page would keep serving one company's account + * names after a switch. A failed fetch leaves the map empty, which shows the + * bare number rather than a wrong name, and retries on the next mount. + */ +const AccountNamesContext = createContext>({}) + +function useAccountNamesSource(): Record { + const [names, setNames] = useState>({}) + useEffect(() => { + let alive = true + void fetch('/api/bookkeeping/accounts') + .then((r) => r.json()) + .then(({ data }) => { + if (!alive) return + setNames( + Object.fromEntries( + ((data ?? []) as Array<{ account_number: string; account_name: string }>).map((a) => [ + a.account_number, + a.account_name, + ]), + ), + ) + }) + .catch(() => { + // Display-only: the number still shows, so a failure is not worth + // surfacing as an error the user cannot act on. + }) + return () => { + alive = false + } + }, []) + return names +} + + function CategorizePreview({ data }: { data: Record }) { + const accountNames = useContext(AccountNamesContext) // The exact journal lines the approval will post (net cost line, VAT line, // gross bank line, SEK) — staged by the server since the preview-lines fix. const lines = (data.lines as Array<{ account_number?: string; debit_amount?: number; credit_amount?: number; description?: string }>) || [] @@ -319,7 +364,21 @@ function CategorizePreview({ data }: { data: Record }) { const creditAmt = typeof line.credit_amount === 'number' ? line.credit_amount : 0 return (
- {line.account_number ?? '?'}{line.description ? ` ${line.description}` : ''} + + {line.account_number ?? '?'}{' '} + {/* The account's own name first: it is what the posting means. + The line text follows only when it adds something the name + does not already say. */} + + {(line.account_number && accountNames[line.account_number]) || line.description || ''} + + {line.description && + line.account_number && + accountNames[line.account_number] && + line.description !== accountNames[line.account_number] ? ( + · {line.description} + ) : null} + {debitAmt > 0 ? `D ${formatCurrency(debitAmt)}` : `K ${formatCurrency(creditAmt)}`} @@ -369,7 +428,14 @@ function CategorizePreview({ data }: { data: Record }) {

Momsrader

{vatLines.map((line, i) => (
- {line.account_number} {line.description} + + {line.account_number}{' '} + {accountNames[line.account_number] || line.description} + {accountNames[line.account_number] && + line.description !== accountNames[line.account_number] ? ( + · {line.description} + ) : null} + {line.debit_amount > 0 ? `D ${formatCurrency(line.debit_amount)}` : `K ${formatCurrency(line.credit_amount)}`} @@ -753,6 +819,7 @@ type ViewTab = 'pending' | 'history' export default function PendingOperationsPage() { const t = useTranslations('pending') + const accountNames = useAccountNamesSource() const [operations, setOperations] = useState([]) const [isLoading, setIsLoading] = useState(true) const [activeTab, setActiveTab] = useState('pending') @@ -1124,6 +1191,7 @@ export default function PendingOperationsPage() { ] return ( +
{/* Page header (concept scene 11): title + Godkänn alla */}
@@ -1704,5 +1772,6 @@ export default function PendingOperationsPage() {
+ ) } diff --git a/components/bookkeeping/JournalEntryForm.tsx b/components/bookkeeping/JournalEntryForm.tsx index ae4c8d46..44690543 100644 --- a/components/bookkeeping/JournalEntryForm.tsx +++ b/components/bookkeeping/JournalEntryForm.tsx @@ -1855,9 +1855,6 @@ export default function JournalEntryForm({ {t('save_as_template')}
-

- {t('fill_balance_hint')} {t('keyboard_hint')} -

{/* Document attachments: hidden when editing a draft; underlag is diff --git a/components/extensions/general/EditKonteringDialog.tsx b/components/extensions/general/EditKonteringDialog.tsx index 9d1c4fd8..f583215b 100644 --- a/components/extensions/general/EditKonteringDialog.tsx +++ b/components/extensions/general/EditKonteringDialog.tsx @@ -30,7 +30,6 @@ import { DialogContent, DialogHeader, DialogTitle, - DialogDescription, } from '@/components/ui/dialog' export interface ProposedLine { @@ -74,9 +73,6 @@ export default function EditKonteringDialog({ Ändra kontering - - Förslaget är en utgångspunkt. Ändra konto, belopp, datum eller serie innan du bokför. -
@@ -87,12 +83,21 @@ export default function EditKonteringDialog({ // props once, by design. key={itemId} embedded - initialLines={lines.map((l) => ({ - account_number: l.account_number, - debit_amount: l.debit_amount ? String(l.debit_amount) : '', - credit_amount: l.credit_amount ? String(l.credit_amount) : '', - line_description: l.description, - }))} + // An unknown supplier has no proposal, and passing [] here is + // not the same as passing nothing: the form seeds two blank rows + // only when this is undefined, so an empty array opened the + // dialog with no rows at all and a "lägg till rad" between the + // user and typing anything. + initialLines={ + lines.length > 0 + ? lines.map((l) => ({ + account_number: l.account_number, + debit_amount: l.debit_amount ? String(l.debit_amount) : '', + credit_amount: l.credit_amount ? String(l.credit_amount) : '', + line_description: l.description, + })) + : undefined + } initialDate={entryDate} initialDescription={description} submitUrl={`/api/extensions/ext/invoice-inbox/items/${itemId}/book-direct`} diff --git a/components/extensions/general/InvoiceInboxWorkspace.tsx b/components/extensions/general/InvoiceInboxWorkspace.tsx index 2ff3ada3..c5fc2963 100644 --- a/components/extensions/general/InvoiceInboxWorkspace.tsx +++ b/components/extensions/general/InvoiceInboxWorkspace.tsx @@ -568,11 +568,24 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) { hunting, progress: huntProgress, result: huntResult, + setResult: setHuntResult, } = useReceiptHunt(() => { void fetchItems() void fetchPurchases() }) + // A run that found nothing leaves nothing to act on, so the line has no + // reason to outlive the glance that reads it. A run that found something, + // or failed, stays: both name a next step (press again, or a mailbox to + // check) and both are worth still being on screen a minute later. + useEffect(() => { + if (hunting || !huntResult) return + if (huntResult.failed || huntResult.fetched > 0) return + const timer = setTimeout(() => setHuntResult(null), 6000) + return () => clearTimeout(timer) + }, [hunting, huntResult, setHuntResult]) + + const selectedPurchase = useMemo( () => purchases.find((p) => p.id === selectedPurchaseId) ?? null, [purchases, selectedPurchaseId], @@ -2506,8 +2519,7 @@ const SUGGESTION_SOURCE_LABEL: Record = { /** Why there is no proposal, said plainly rather than shown as an empty table. */ const SUGGESTION_EMPTY_REASON: Record = { - no_mapping: - 'Vi har inget förslag: leverantören är obekant och ingen regel matchar. Bokför manuellt en gång, så känns den igen nästa gång.', + no_mapping: 'Okänd leverantör. Bokför en gång, så känns den igen.', currency_unsupported: 'Köpet är i utländsk valuta och matchades av en konteringsregel. Momsen skulle bli fel, så vi visar inget förslag.', } @@ -2907,9 +2919,8 @@ function FieldsRail({ {/* Hint only: creation happens on the leverantörsfaktura form via "Skapa & välj" */} {showNoMatchHint && (
- Ingen leverantör matchade{' '} {extractedSupplierName} - {': leverantören skapas när du klickar Skapa leverantörsfaktura.'} + {' finns inte upplagd än. Den skapas när du gör leverantörsfakturan.'}
)} @@ -3039,19 +3050,6 @@ function FieldsRail({ {/* Matched-to-tx state: show the bridge to booking. The user picks one of two actions: book themselves with the deterministic dialog, or hand off to the assistant. */} -
-
- - Matchad mot transaktion -
- - Öppna transaktionen → - -
- {onAskAssistant && (
)} diff --git a/components/extensions/general/MailConnectionsPanel.tsx b/components/extensions/general/MailConnectionsPanel.tsx index 074a1df5..9b5824f7 100644 --- a/components/extensions/general/MailConnectionsPanel.tsx +++ b/components/extensions/general/MailConnectionsPanel.tsx @@ -129,7 +129,7 @@ export function MailConnectionsPanel() { } > - {connection.emailAddress} + {connection.emailAddress} {connection.status === 'needs_reconsent' ? ( {t('needs_reconsent')} ) : null} diff --git a/messages/sv.json b/messages/sv.json index 370ecd53..9ec242ec 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -326,7 +326,7 @@ "mail": { "hunt_stop": "Stoppa", "hunt_progress": "{fetched} hämtade…", - "hunt_title": "Leta efter underlag", + "hunt_title": "Kvittojakten", "hunt_help": "Vi söker i de kopplade brevlådorna efter kvitton och fakturor till köp som saknar underlag, läser beloppet ur filen och lägger fram förslagen i Granskning. Inget bokförs.", "hunt_row": "Sök igenom brevlådorna", "hunt_action": "Leta nu",