Files
accounted/components/transactions/BankSyncNowButton.tsx
T
MattssonandClaude Opus 4.7 ea1bf01f1e Fix/m sprint fixes (#613)
* fix(dashboard): exclude ignored and already-triaged transactions from stale count

The "Gamla transaktioner" widget counted transactions that had been ignored
or already marked as is_business=true but not yet booked, so users saw a
nag for a row they had already dealt with — and the /transactions inbox
correctly hid it. Align the count with the inbox criterion (is_business
IS NULL, is_ignored = false) so the widget clears when the row leaves
the inbox.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(transactions): read entity_type from settings response wrapper

The transactions page read entityRes.entity_type directly, but
/api/settings returns { data: { entity_type, ... } }. The expression
was always undefined, so setEntityType never fired and entityType
stayed at its initial 'enskild_firma'. The template picker's
entity_type filter then dropped every aktiebolag-tagged user template
for AB customers — only entity_type='all' templates made it through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* stale templates
bank sync
journal entry from transaction

* fixed pr comments

* fixed pr comment

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 01:28:41 +02:00

139 lines
3.9 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { Loader2, RefreshCw } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useToast } from '@/components/ui/use-toast'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { createClient } from '@/lib/supabase/client'
import { useCompany } from '@/contexts/CompanyContext'
interface ActiveConnection {
id: string
bank_name: string
}
/**
* On-demand "Sync now" button beside BankSyncStatusChip. Reuses the
* per-connection sync endpoint that BankingSettingsPanel already calls;
* if the user has multiple active connections, a dropdown lets them
* pick which one to sync.
*/
export default function BankSyncNowButton() {
const t = useTranslations('transactions')
const { toast } = useToast()
const router = useRouter()
const { company } = useCompany()
const [connections, setConnections] = useState<ActiveConnection[] | null>(null)
const [syncingId, setSyncingId] = useState<string | null>(null)
useEffect(() => {
if (!company?.id) return
let cancelled = false
const supabase = createClient()
supabase
.from('bank_connections')
.select('id, bank_name')
.eq('company_id', company.id)
.eq('status', 'active')
.then(({ data }) => {
if (!cancelled) setConnections(data ?? [])
})
return () => {
cancelled = true
}
}, [company?.id])
if (!connections || connections.length === 0) return null
async function syncConnection(connectionId: string) {
setSyncingId(connectionId)
try {
const res = await fetch('/api/extensions/ext/enable-banking/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ connection_id: connectionId }),
})
const data = await res.json()
if (!res.ok) {
throw new Error(data.error || 'Sync failed')
}
toast({
title: t('bank_sync_button_now'),
description: data.imported === 1
? t('bank_sync_new_since_last_visit_one')
: t('bank_sync_new_since_last_visit_many', { count: data.imported ?? 0 }),
})
router.refresh()
} catch (error) {
toast({
title: t('bank_sync_button_now'),
description: error instanceof Error ? error.message : 'Sync failed',
variant: 'destructive',
})
} finally {
setSyncingId(null)
}
}
const isSyncing = syncingId !== null
const label = isSyncing ? t('bank_sync_button_syncing') : t('bank_sync_button_now')
if (connections.length === 1) {
return (
<Button
variant="outline"
size="sm"
className="h-7 gap-1.5 px-2.5 text-xs"
disabled={isSyncing}
onClick={() => syncConnection(connections[0].id)}
>
{isSyncing ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<RefreshCw className="h-3.5 w-3.5" />
)}
<span>{label}</span>
</Button>
)
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-7 gap-1.5 px-2.5 text-xs"
disabled={isSyncing}
>
{isSyncing ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<RefreshCw className="h-3.5 w-3.5" />
)}
<span>{label}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{connections.map((conn) => (
<DropdownMenuItem
key={conn.id}
disabled={isSyncing}
onSelect={() => syncConnection(conn.id)}
>
{conn.bank_name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}