'use client' import { useEffect, useMemo, useState } from 'react' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { useToast } from '@/components/ui/use-toast' import { Loader2 } from 'lucide-react' import { createClient } from '@/lib/supabase/client' import { useCompany } from '@/contexts/CompanyContext' import type { StoredAccount } from '../types' interface AccountPickerDialogProps { open: boolean onOpenChange: (open: boolean) => void connectionId: string bankName: string accounts: StoredAccount[] // True when the connection is still in pending_selection — closing without // saving is allowed but the user is reminded that no sync runs until they // confirm. isInitialSelection: boolean onSaved: () => void } const LOOKBACK_OPTIONS = [ { days: 90, label: 'Senaste 90 dagar (PSD2 standard, rekommenderas)' }, { days: 180, label: 'Senaste 180 dagar' }, { days: 365, label: 'Senaste 365 dagar' }, ] as const export function AccountPickerDialog({ open, onOpenChange, connectionId, bankName, accounts, isInitialSelection, onSaved, }: AccountPickerDialogProps) { const { toast } = useToast() // Memoise so the client has a stable reference across re-renders. Without this, // listing `supabase` in the SIE-fetch effect's deps would re-fire that query on // every checkbox tick or parent re-render. const supabase = useMemo(() => createClient(), []) const { company } = useCompany() const [selected, setSelected] = useState>(new Set()) const [isSaving, setIsSaving] = useState(false) const [lookbackDays, setLookbackDays] = useState(90) const [sieLastDate, setSieLastDate] = useState(null) const [showCustomLookback, setShowCustomLookback] = useState(false) useEffect(() => { if (open) { // Start the dialog reflecting the current state. Accounts without an // explicit enabled flag are treated as enabled (back-compat). const initial = new Set( accounts.filter(a => a.enabled !== false).map(a => a.uid) ) setSelected(initial) setShowCustomLookback(false) } }, [open, accounts]) // Fetch the latest SIE import end date so we can anchor the backfill to // "day after last SIE entry" (SpeedLedger pattern). Only matters on the // initial activation flow — selection edits don't re-run sync. useEffect(() => { if (!open || !isInitialSelection || !company?.id) { setSieLastDate(null) return } let cancelled = false ;(async () => { const { data } = await supabase .from('sie_imports') .select('fiscal_year_end') .eq('company_id', company.id) .eq('status', 'completed') .order('fiscal_year_end', { ascending: false }) .limit(1) .maybeSingle() if (cancelled) return const fye = (data as { fiscal_year_end?: string } | null)?.fiscal_year_end || null setSieLastDate(fye) if (fye) { // Anchor lookback to (today - (fye + 1 day)), clamped to [30, 365]. const dayAfter = new Date(fye) dayAfter.setDate(dayAfter.getDate() + 1) const days = Math.ceil((Date.now() - dayAfter.getTime()) / (24 * 60 * 60 * 1000)) setLookbackDays(Math.min(365, Math.max(30, days))) } else { setLookbackDays(90) } })() return () => { cancelled = true } }, [open, isInitialSelection, company?.id, supabase]) const allSelected = accounts.length > 0 && selected.size === accounts.length const noneSelected = selected.size === 0 const sortedAccounts = useMemo( () => [...accounts].sort((a, b) => (a.name || a.iban || '').localeCompare(b.name || b.iban || '')), [accounts] ) function toggle(uid: string) { setSelected(prev => { const next = new Set(prev) if (next.has(uid)) next.delete(uid) else next.add(uid) return next }) } function selectAll() { setSelected(new Set(accounts.map(a => a.uid))) } function selectNone() { setSelected(new Set()) } async function handleSave() { if (noneSelected) { toast({ title: 'Välj minst ett konto', description: 'Avmarkera alla konton och koppla bort banken istället om inga konton ska synkas.', variant: 'destructive', }) return } setIsSaving(true) try { const response = await fetch('/api/extensions/ext/enable-banking/accounts', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ connection_id: connectionId, enabled_uids: Array.from(selected), ...(isInitialSelection ? { initial_lookback_days: lookbackDays } : {}), }), }) const data = await response.json() if (!response.ok) { throw new Error(data.error || 'Kunde inte spara kontoval') } // Surface the actual backfill result so the user sees what the bank returned. if (isInitialSelection && data.initial_sync) { const { imported, returned_min_date, returned_max_date } = data.initial_sync as { imported: number returned_min_date: string | null returned_max_date: string | null } const range = returned_min_date && returned_max_date ? ` från ${returned_min_date} till ${returned_max_date}` : '' toast({ title: 'Konton sparade', description: `Importerade ${imported} transaktioner${range}.`, }) } else if (isInitialSelection && data.initial_sync_error) { toast({ title: 'Konton sparade — bakgrundssync misslyckades', description: 'Vi försöker igen vid nästa körning. Bankanslutningen är aktiv.', variant: 'destructive', }) } else { toast({ title: 'Kontoval sparat', description: `${data.enabled_count} av ${data.total_count} konton kommer synkas.`, }) } onOpenChange(false) onSaved() } catch (error) { toast({ title: 'Fel', description: error instanceof Error ? error.message : 'Kunde inte spara kontoval', variant: 'destructive', }) } finally { setIsSaving(false) } } // Day-after-SIE date for the callout const dayAfterSie = useMemo(() => { if (!sieLastDate) return null const d = new Date(sieLastDate) d.setDate(d.getDate() + 1) return d.toISOString().split('T')[0] }, [sieLastDate]) return ( Välj konton att synka — {bankName} {isInitialSelection ? 'Banken har gett åtkomst till följande konton. Avmarkera de konton du inte vill synka transaktioner från. Inga transaktioner hämtas innan du sparar.' : 'Justera vilka konton som ska synkas. Konton du avmarkerar slutar synkas från nästa körning; redan importerade transaktioner ligger kvar.'} {isInitialSelection && (
{sieLastDate && dayAfterSie ? (

Vi hittade en SIE-import som täcker fram till{' '} {sieLastDate}. Vi hämtar bankhistorik från{' '} {dayAfterSie}{' '} så att inget överlappar din tidigare bokföring.

{showCustomLookback && ( )}
) : (

PSD2-bankregler begränsar oftast historiken till 90 dagar bakåt. Vi visar exakt vad banken returnerade efter sparat val. För äldre data, använd SIE- eller CSV-import.

)}
)}
{selected.size} av {accounts.length} valda
·
{sortedAccounts.map(account => { const isChecked = selected.has(account.uid) return ( ) })}
) }