8a9162b948
The "Synka bank nu" row in the transactions Importera split button fired syncAll() with zero visual feedback. SplitButton now takes busy/busyLabel per option: the primary face and the menu row swap to a spinning Loader2, show the busy label and go inert until the action resolves. useBankSync holds isBusy across the whole syncAll loop so the spinner does not flicker between per-connection syncs. Sweep of the rest of the system for async buttons missing the same feedback (convention: disabled + Loader2 animate-spin + label swap): - bokslut DigitalInlamning "Uppdatera status" (Bolagsverket poll): had no feedback at all; now disabled + spinner + "Uppdaterar ..." while polling - Stripe settings "Synka nu": had disabled + label swap but a static icon - Skatteverket "Verifiera": had disabled + label swap but no spinner - AgentMemoryPanel "Dolj"/"Aterstall" row actions: static icons on async patch; now swap to spinner for the busy row Checked and intentionally unchanged: Arcim migration "Synka igen" and "Ateranslut" (the whole step flips to a spinner view synchronously on click), skattekonto "Forsok igen" (page flips to loading view), AgentChat "Generera om" (streaming indicator is the feedback). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
'use client'
|
|
|
|
import { useRouter } from 'next/navigation'
|
|
import { useTranslations } from 'next-intl'
|
|
import { Upload, Plus, RefreshCw } from 'lucide-react'
|
|
import { SplitButton, type SplitButtonOption } from '@/components/ui/split-button'
|
|
import { useCanWrite } from '@/lib/hooks/use-can-write'
|
|
import { useUiState } from '@/lib/hooks/use-ui-state'
|
|
import { resolveInitialMode } from '@/lib/ui-state/client'
|
|
import { useBankSync } from '@/components/transactions/BankSyncNowButton'
|
|
import { useAgeFormatter } from '@/components/transactions/BankSyncStatusChip'
|
|
|
|
interface TransactionStatusBarProps {
|
|
onOpenCreateDialog: () => void
|
|
}
|
|
|
|
/**
|
|
* Page header (concept scene 10): title + one Importera split button
|
|
* holding the ways transactions arrive (bank sync, import guide for CSV/SIE,
|
|
* manual entry for cash/outlays).
|
|
*/
|
|
export default function TransactionStatusBar({
|
|
onOpenCreateDialog,
|
|
}: TransactionStatusBarProps) {
|
|
const { canWrite } = useCanWrite()
|
|
const t = useTranslations('transactions')
|
|
const router = useRouter()
|
|
const { uiState, loaded } = useUiState()
|
|
const { connections, hasBankSync, syncAll, lastSyncedAt, isBusy } = useBankSync()
|
|
const formatAge = useAgeFormatter()
|
|
|
|
// "Synka bank nu" (concept: first menu row) only renders once a bank is
|
|
// actually connected and the plan includes PSD2 sync; the footer
|
|
// BankSyncNowButton stays the gated conversion surface for free users.
|
|
const showSync = hasBankSync && (connections?.length ?? 0) > 0
|
|
|
|
const options: SplitButtonOption[] = [
|
|
...(showSync
|
|
? [
|
|
{
|
|
key: 'synka',
|
|
label: t('create_synka'),
|
|
icon: RefreshCw,
|
|
busy: isBusy,
|
|
busyLabel: t('bank_sync_button_syncing'),
|
|
description: lastSyncedAt
|
|
? t('create_synka_desc_last', { age: formatAge(lastSyncedAt) })
|
|
: t('create_synka_desc'),
|
|
onSelect: () => {
|
|
void syncAll()
|
|
},
|
|
} satisfies SplitButtonOption,
|
|
]
|
|
: []),
|
|
{
|
|
key: 'importera',
|
|
label: t('action_import'),
|
|
icon: Upload,
|
|
description: t('create_import_desc'),
|
|
onSelect: () => router.push('/import'),
|
|
},
|
|
{
|
|
key: 'manuell',
|
|
label: t('action_new_transaction'),
|
|
icon: Plus,
|
|
description: t('create_manual_desc'),
|
|
disabled: !canWrite,
|
|
disabledTitle: t('viewer_disabled_tooltip'),
|
|
onSelect: () => onOpenCreateDialog(),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<h1 className="font-display text-2xl leading-8 tracking-tight">{t('page_title')}</h1>
|
|
<SplitButton
|
|
key={`${loaded ? 'loaded' : 'initial'}-${showSync ? 'sync' : 'nosync'}`}
|
|
persistKey="transactions"
|
|
initialModeKey={resolveInitialMode(
|
|
uiState,
|
|
'transactions',
|
|
options.map((o) => o.key),
|
|
'importera',
|
|
)}
|
|
options={options}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|