'use client' import { useEffect, useState } from 'react' import Link from 'next/link' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Loader2, CheckCircle2, AlertTriangle } from 'lucide-react' import { daysBetween } from '@/lib/company/fiscal-year' import type { StoredAccount } from '../types' export interface SyncProgressSummary { imported: number duplicates: number requested_from: string returned_min_date: string | null returned_max_date: string | null } export interface SyncProgressError { message: string } export type SyncProgressState = | { kind: 'syncing' } | { kind: 'done'; summary: SyncProgressSummary } | { kind: 'failed'; error: SyncProgressError } interface BankSyncProgressDialogProps { open: boolean onOpenChange: (open: boolean) => void bankName: string accounts: StoredAccount[] state: SyncProgressState } // Past this point the sync has run longer than the promised "up to a minute". // We stop hard-locking the modal so the user isn't trapped: the request keeps // running server-side (idempotent) and completion still resolves the state. const GRACE_SEC = 75 function formatElapsed(sec: number): string { if (sec < 60) return `${sec}s` return `${Math.floor(sec / 60)}m ${String(sec % 60).padStart(2, '0')}s` } export function BankSyncProgressDialog({ open, onOpenChange, bankName, accounts, state, }: BankSyncProgressDialogProps) { const enabledAccounts = accounts.filter((a) => a.enabled !== false) // Tick a visible elapsed counter while syncing so a slow bank doesn't look // frozen, and so we know when to release the close-lock (GRACE_SEC). State is // only ever set from the timer callbacks (never synchronously in the effect // body), and elapsed is never computed from Date.now() during render, so this // stays clear of the react-hooks purity rules. const [elapsedSec, setElapsedSec] = useState(0) useEffect(() => { if (!open || state.kind !== 'syncing') return const started = Date.now() const tick = () => setElapsedSec(Math.max(0, Math.floor((Date.now() - started) / 1000))) // Reset to ~0 on the next tick (async, so not a synchronous effect setState). const reset = setTimeout(tick, 0) const id = setInterval(tick, 1000) return () => { clearTimeout(reset) clearInterval(id) } }, [open, state.kind]) const overGrace = state.kind === 'syncing' && elapsedSec >= GRACE_SEC // Only hard-block the close affordances during the expected window; after the // grace period the user may background the (still-running) sync. const blockClose = state.kind === 'syncing' && !overGrace return ( ) } function DoneBody({ summary }: { summary: SyncProgressSummary }) { const requestedDays = daysBetween(summary.requested_from) const returnedDays = summary.returned_min_date && summary.returned_max_date ? daysBetween(summary.returned_min_date, new Date(summary.returned_max_date)) : 0 const wasTruncated = requestedDays - returnedDays > 7 return (
{summary.imported} nya transaktioner importerade.
{summary.returned_min_date && summary.returned_max_date && (Datum: {summary.returned_min_date} → {summary.returned_max_date}
)}