From ce1e0b7642561bb4cd89a4e80b8022704466e346 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Sun, 9 Aug 2026 20:52:25 +0200 Subject: [PATCH] feat(banking): the bank-connect payoff (#1477) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(banking): the bank-connect payoff: point the first sync onward Fifth activation slice. Every successful connect passes through the sync-progress dialog's done state, which used to end on a bare Klar that stranded the user on the settings panel. Now it is the payoff: the imported count stays, the work now waiting gets named (N att bokföra · M matchar fakturor, from the same worklist counts endpoint the dashboard pane refetches), and the primary action becomes Visa N att bokföra -> /transactions, where realtime rows, the match pills and the Att bokföra badge already deliver the rest. Zero-import syncs keep the plain Klar. Also mounts the fully-built-but-orphaned BankSyncSinceLastVisit pill on the transactions footer, so returning users get the same payoff line for the nightly cron (N nya transaktioner sedan sist). Strings stay hardcoded Swedish inside the extension, matching every neighboring string in the dialog. Co-Authored-By: Claude Fable 5 * fix(banking): review triage: no stale counts, no marker advance on error Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 --- app/(dashboard)/transactions/page.tsx | 2 + .../transactions/BankSyncSinceLastVisit.tsx | 5 +- .../components/BankSyncProgressDialog.tsx | 81 ++++++++++++++++--- 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/app/(dashboard)/transactions/page.tsx b/app/(dashboard)/transactions/page.tsx index 7e38c0f5..21855669 100644 --- a/app/(dashboard)/transactions/page.tsx +++ b/app/(dashboard)/transactions/page.tsx @@ -22,6 +22,7 @@ import BankSyncStatusChip from '@/components/transactions/BankSyncStatusChip' import { ContextPicker, type ContextPickerItem } from '@/components/common/ContextPicker' import { AttnLine } from '@/components/ui/attn-line' import BankSyncNowButton from '@/components/transactions/BankSyncNowButton' +import BankSyncSinceLastVisit from '@/components/transactions/BankSyncSinceLastVisit' import TransactionInboxCard from '@/components/transactions/TransactionInboxCard' import TransactionHistoryList from '@/components/transactions/TransactionHistoryList' import InboxZeroState from '@/components/transactions/InboxZeroState' @@ -2663,6 +2664,7 @@ export default function TransactionsPage() { )} + { + .then(({ count: rowCount, error }) => { if (cancelled) return + // A failed query must not advance the marker: that would silently + // swallow the notification for rows that DID arrive in the window. + if (error) return if (rowCount && rowCount > 0) { setCount(rowCount) } else { diff --git a/extensions/general/enable-banking/components/BankSyncProgressDialog.tsx b/extensions/general/enable-banking/components/BankSyncProgressDialog.tsx index 8c7bded6..4d391752 100644 --- a/extensions/general/enable-banking/components/BankSyncProgressDialog.tsx +++ b/extensions/general/enable-banking/components/BankSyncProgressDialog.tsx @@ -83,6 +83,34 @@ export function BankSyncProgressDialog({ // grace period the user may background the (still-running) sync. const blockClose = state.kind === 'syncing' && !overGrace + // The payoff: once the first sync lands, name the work now waiting so the + // moment points onward instead of stranding the user on the settings panel. + // Same authenticated endpoint the dashboard pane refetches; best-effort. + const [workCounts, setWorkCounts] = useState<{ book: number; matches: number } | null>(null) + useEffect(() => { + if (!open || state.kind !== 'done' || state.summary.imported === 0) { + // A later sync must not inherit the previous run's numbers. + setWorkCounts(null) + return + } + let cancelled = false + fetch('/api/worklist/counts') + .then((res) => (res.ok ? res.json() : null)) + .then((json: { data?: { counts?: Record } } | null) => { + if (cancelled || !json?.data?.counts) return + setWorkCounts({ + book: json.data.counts.book_transaction ?? 0, + matches: json.data.counts.suggested_match ?? 0, + }) + }) + .catch(() => { + // The CTA degrades to a plain link; the numbers are a nicety. + }) + return () => { + cancelled = true + } + }, [open, state]) + return ( + )} {state.kind === 'failed' && ( @@ -162,22 +190,47 @@ export function BankSyncProgressDialog({ )} - + {state.kind === 'done' && state.summary.imported > 0 ? ( + <> + + + + ) : ( + + )} ) } -function DoneBody({ summary }: { summary: SyncProgressSummary }) { +function DoneBody({ + summary, + workCounts, +}: { + summary: SyncProgressSummary + workCounts: { book: number; matches: number } | null +}) { const requestedDays = daysBetween(summary.requested_from) const returnedDays = summary.returned_min_date && summary.returned_max_date @@ -199,6 +252,12 @@ function DoneBody({ summary }: { summary: SyncProgressSummary }) { Datum: {summary.returned_min_date} → {summary.returned_max_date}

)} + {workCounts && workCounts.book > 0 && ( +

+ {workCounts.book} att bokföra + {workCounts.matches > 0 && <> · {workCounts.matches} matchar fakturor} +

+ )}