'use client' import { useCallback, useEffect, useState } from 'react' import { useLocale, useTranslations } from 'next-intl' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Download, Loader2, CheckCircle2, ExternalLink } from 'lucide-react' import { useToast } from '@/components/ui/use-toast' import { downloadFile } from '@/lib/browser/download-file' import { postAction } from '@/lib/browser/post-action' import { failureDescription } from '@/lib/browser/action-failure' import type { ErrorLocale } from '@/lib/errors/get-error-message' import { formatCurrency } from '@/lib/utils' interface TaxPaymentPanelProps { /** YYYY-MM */ period: string totalTax: number totalAvgifter: number paymentFileGeneratedAt: string | null taxPaidAt: string | null readOnly?: boolean onChange?: () => void } /** * Generates a Bankgirot LB-fil for paying skatt + arbetsgivaravgifter for an * AGI period to Skatteverket Bankgiro 5050-1055 with the company's * Skattekontot OCR. */ export function TaxPaymentPanel({ period, totalTax, totalAvgifter, paymentFileGeneratedAt, taxPaidAt, readOnly, onChange, }: TaxPaymentPanelProps) { const t = useTranslations('salary_payments') const locale = useLocale() as ErrorLocale const { toast } = useToast() const [downloading, setDownloading] = useState(false) const [marking, setMarking] = useState(false) const [paymentDeadline, setPaymentDeadline] = useState('') useEffect(() => { const m = /^(\d{4})-(\d{2})$/.exec(period) if (!m) return const year = parseInt(m[1], 10) const month = parseInt(m[2], 10) const dlMonth = month === 12 ? 1 : month + 1 const dlYear = month === 12 ? year + 1 : year setPaymentDeadline(`${dlYear}-${String(dlMonth).padStart(2, '0')}-12`) }, [period]) const totalAmount = Math.round((totalTax + totalAvgifter) * 100) / 100 const handleDownload = useCallback(async () => { // Both buttons are disabled while either is in flight; this guard closes the // double-click race before React has re-rendered them. A second file for the // same period is a second payable instruction to Skattekontot. if (downloading || marking) return setDownloading(true) try { // Bounded, and no file is written unless the server answered 2xx with a // complete body: an error envelope saved as bg_lb_skatt_2026-04.txt is a // file the user would upload to the bank before discovering it pays no tax. const result = await downloadFile({ url: `/api/skatteverket/tax-payments/${period}/payment-file`, filename: `bg_lb_skatt_${period}.txt`, locale, }) // Exactly one toast per outcome: TOAST_LIMIT is 1. if (!result.ok) { toast({ title: t('tax_download_failed_title'), description: failureDescription(result, { timeout: t('download_timeout'), network: t('download_network'), }), variant: 'destructive', }) return } toast({ title: t('tax_downloaded') }) onChange?.() } finally { setDownloading(false) } }, [period, toast, onChange, t, locale, downloading, marking]) const handleMarkPaid = useCallback(async () => { if (downloading || marking) return setMarking(true) try { const result = await postAction({ url: `/api/skatteverket/tax-payments/${period}/mark-paid`, locale, }) if (!result.ok) { toast({ title: t('tax_mark_paid_failed_title'), description: failureDescription(result, { // A timeout on a write is genuinely ambiguous: the update may have // landed. Say that instead of claiming it failed. timeout: t('tax_mark_paid_timeout'), network: t('tax_mark_paid_network'), }), variant: 'destructive', }) return } toast({ title: t('tax_marked_paid') }) onChange?.() } finally { setMarking(false) } }, [period, toast, onChange, t, locale, downloading, marking]) if (totalAmount <= 0) return null return ( {t('tax_title')}

{t('tax_label_tax')}

{formatCurrency(totalTax)}

{t('tax_label_avgifter')}

{formatCurrency(totalAvgifter)}

{t('tax_label_total')}

{formatCurrency(totalAmount)}

{t('tax_recipient')} {t('tax_recipient_value')}

{t('tax_due_date')} {paymentDeadline}

{t('tax_ocr_note')}

{paymentFileGeneratedAt && (
{t('tax_file_generated')}{' '} {new Date(paymentFileGeneratedAt).toLocaleString('sv-SE')}
)} {taxPaidAt && (
{t('tax_marked_paid')}{' '} {new Date(taxPaidAt).toLocaleString('sv-SE')}
)} {!readOnly && (
{!taxPaidAt && ( )}
)}
) }