diff --git a/components/bokslut/DigitalInlamning.tsx b/components/bokslut/DigitalInlamning.tsx index 37d76a9e..0014fe02 100644 --- a/components/bokslut/DigitalInlamning.tsx +++ b/components/bokslut/DigitalInlamning.tsx @@ -182,6 +182,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) { const [submissions, setSubmissions] = useState([]) const [loadingSubmissions, setLoadingSubmissions] = useState(false) const [submissionsError, setSubmissionsError] = useState(null) + const [pollingEvents, setPollingEvents] = useState(false) useEffect(() => { const signer = versions.find((version) => version.id === selectedVersionId) @@ -384,6 +385,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) { } const handlePollEvents = async () => { + setPollingEvents(true) try { const res = await fetch('/api/extensions/ext/bolagsverket/poll-events', { method: 'POST', @@ -399,6 +401,8 @@ export function DigitalInlamning({ periodId }: { periodId: string }) { toast({ title: 'Status uppdaterad från Bolagsverket' }) } catch { toast({ title: 'Kunde inte hämta händelser', variant: 'destructive' }) + } finally { + setPollingEvents(false) } } @@ -794,8 +798,19 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
-
{submissionsError &&

{submissionsError}

} diff --git a/components/settings/AgentMemoryPanel.tsx b/components/settings/AgentMemoryPanel.tsx index 42bdba4b..dba4b522 100644 --- a/components/settings/AgentMemoryPanel.tsx +++ b/components/settings/AgentMemoryPanel.tsx @@ -371,7 +371,11 @@ export function AgentMemoryPanel() { onClick={() => patch(row.id, { is_active: false })} disabled={isBusy} > - + {isBusy ? ( + + ) : ( + + )} Dölj @@ -382,7 +386,11 @@ export function AgentMemoryPanel() { onClick={() => patch(row.id, { is_active: true })} disabled={isBusy} > - + {isBusy ? ( + + ) : ( + + )} Återställ )} diff --git a/components/settings/SkatteverketConnectPanel.tsx b/components/settings/SkatteverketConnectPanel.tsx index 6dabb6a7..76ad9ca4 100644 --- a/components/settings/SkatteverketConnectPanel.tsx +++ b/components/settings/SkatteverketConnectPanel.tsx @@ -10,7 +10,7 @@ import { useCapability } from '@/contexts/CompanyContext' import { isAllowedSkvPopupOrigin } from '@/lib/skatteverket/popup-origin' import { CAPABILITY } from '@/lib/entitlements/keys' import { UpgradeNote } from '@/components/billing/UpgradeNote' -import { CheckCircle2, ExternalLink, ShieldOff, FlaskConical, ShieldAlert } from 'lucide-react' +import { CheckCircle2, ExternalLink, Loader2, ShieldOff, FlaskConical, ShieldAlert } from 'lucide-react' import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message' type Environment = 'test' | 'prod' @@ -552,6 +552,7 @@ function SkatteverketSystemConnectionCard() { )} diff --git a/components/transactions/BankSyncNowButton.tsx b/components/transactions/BankSyncNowButton.tsx index 28d0a0c3..a6a2c177 100644 --- a/components/transactions/BankSyncNowButton.tsx +++ b/components/transactions/BankSyncNowButton.tsx @@ -46,6 +46,9 @@ export function useBankSync() { const hasBankSync = useCapability(CAPABILITY.bank_sync) const [connections, setConnections] = useState(null) const [busyId, setBusyId] = useState(null) + // Holds isBusy true across the whole syncAll loop so the spinner doesn't + // flicker off between per-connection syncs. + const [syncingAll, setSyncingAll] = useState(false) useEffect(() => { if (!company?.id) return @@ -156,14 +159,19 @@ export function useBankSync() { // active connection in turn; with only dead connections it jumps straight // to re-authorizing the first one (a retry can't revive a closed session). async function syncAll() { - const conns = connections ?? [] - const active = conns.filter((c) => c.status === 'active') - if (active.length === 0) { - if (conns[0]) await reconnect(conns[0]) - return - } - for (const conn of active) { - await syncConnection(conn) + setSyncingAll(true) + try { + const conns = connections ?? [] + const active = conns.filter((c) => c.status === 'active') + if (active.length === 0) { + if (conns[0]) await reconnect(conns[0]) + return + } + for (const conn of active) { + await syncConnection(conn) + } + } finally { + setSyncingAll(false) } } @@ -177,7 +185,7 @@ export function useBankSync() { return { connections, busyId, - isBusy: busyId !== null, + isBusy: busyId !== null || syncingAll, hasBankSync, reconnect, syncConnection, diff --git a/components/transactions/TransactionStatusBar.tsx b/components/transactions/TransactionStatusBar.tsx index 63bfd86a..4b6c7482 100644 --- a/components/transactions/TransactionStatusBar.tsx +++ b/components/transactions/TransactionStatusBar.tsx @@ -26,7 +26,7 @@ export default function TransactionStatusBar({ const t = useTranslations('transactions') const router = useRouter() const { uiState, loaded } = useUiState() - const { connections, hasBankSync, syncAll, lastSyncedAt } = useBankSync() + const { connections, hasBankSync, syncAll, lastSyncedAt, isBusy } = useBankSync() const formatAge = useAgeFormatter() // "Synka bank nu" (concept: first menu row) only renders once a bank is @@ -41,6 +41,8 @@ export default function TransactionStatusBar({ 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'), diff --git a/components/ui/split-button.tsx b/components/ui/split-button.tsx index fb070f05..ec7d157c 100644 --- a/components/ui/split-button.tsx +++ b/components/ui/split-button.tsx @@ -6,7 +6,7 @@ import { useTranslations } from 'next-intl' import { cn } from '@/lib/utils' import { Button, type ButtonProps } from '@/components/ui/button' import { rememberCreateMode } from '@/lib/ui-state/client' -import { Check, ChevronDown, type LucideIcon } from 'lucide-react' +import { Check, ChevronDown, Loader2, type LucideIcon } from 'lucide-react' export interface SplitButtonOption { key: string @@ -18,6 +18,11 @@ export interface SplitButtonOption { * as the tooltip instead of silently no-opping. */ disabled?: boolean disabledTitle?: string + /** In-flight async action (e.g. bank sync): spinner replaces the icon and + * the option is inert until it resolves. */ + busy?: boolean + /** Label shown on the primary face while busy (e.g. "Synkar…"). */ + busyLabel?: string onSelect: () => void } @@ -102,7 +107,7 @@ export function SplitButton({ if (!active) return null const runOption = (option: SplitButtonOption) => { - if (option.disabled) return + if (option.disabled || option.busy) return setActiveKey(option.key) if (persistKey) rememberCreateMode(persistKey, option.key) option.onSelect() @@ -113,12 +118,17 @@ export function SplitButton({