'use client' import { useState, useEffect } from 'react' import Link from 'next/link' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Label } from '@/components/ui/label' import { EmptyState } from '@/components/ui/empty-state' import { Skeleton } from '@/components/ui/skeleton' import { Table, TableHeader, TableHead, TableRow, TableCell, TableBody, } from '@/components/ui/table' import { Download, AlertCircle, AlertTriangle, FileText, ExternalLink } from 'lucide-react' import { formatCurrency } from '@/lib/utils' import type { PeriodiskSammanstallningReport, PsPeriodType, PsWarning, } from '@/lib/reports/periodisk-sammanstallning' function formatAmount(amount: number): string { // Hela kronor — SKV 5740 har inga öre. return amount.toLocaleString('sv-SE', { maximumFractionDigits: 0 }) } function typeBadge(row: { services: number; goods: number; triangulation: number }) { const types: string[] = [] if (row.services !== 0) types.push('Tjänster') if (row.goods !== 0) types.push('Varor') if (row.triangulation !== 0) types.push('Trepart') return types.join(' + ') || '—' } function deadlineLabel(end: string): string { // Inlämnas digitalt senast den 25:e månaden efter perioden. const endDate = new Date(end) const deadline = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 25) const months = [ 'januari', 'februari', 'mars', 'april', 'maj', 'juni', 'juli', 'augusti', 'september', 'oktober', 'november', 'december', ] return `${deadline.getDate()} ${months[deadline.getMonth()]} ${deadline.getFullYear()}` } export function PeriodiskSammanstallningView() { const currentYear = new Date().getFullYear() const currentMonth = new Date().getMonth() + 1 const currentQuarter = Math.ceil(currentMonth / 3) const [periodType, setPeriodType] = useState('quarterly') const [year, setYear] = useState(currentYear) const [period, setPeriod] = useState(currentQuarter) const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const yearOptions = Array.from({ length: 5 }, (_, i) => currentYear - i) const periodOptions = periodType === 'monthly' ? [ { value: 1, label: 'Januari' }, { value: 2, label: 'Februari' }, { value: 3, label: 'Mars' }, { value: 4, label: 'April' }, { value: 5, label: 'Maj' }, { value: 6, label: 'Juni' }, { value: 7, label: 'Juli' }, { value: 8, label: 'Augusti' }, { value: 9, label: 'September' }, { value: 10, label: 'Oktober' }, { value: 11, label: 'November' }, { value: 12, label: 'December' }, ] : [ { value: 1, label: 'Kvartal 1 (jan-mar)' }, { value: 2, label: 'Kvartal 2 (apr-jun)' }, { value: 3, label: 'Kvartal 3 (jul-sep)' }, { value: 4, label: 'Kvartal 4 (okt-dec)' }, ] useEffect(() => { setPeriod(periodType === 'monthly' ? currentMonth : currentQuarter) }, [periodType, currentMonth, currentQuarter]) const fetchReport = async () => { setLoading(true) setError(null) try { const res = await fetch( `/api/reports/periodisk-sammanstallning?periodType=${periodType}&year=${year}&period=${period}`, ) const result = await res.json() if (result.error) { setError(typeof result.error === 'string' ? result.error : result.error.message_sv ?? 'Något gick fel.') } else { setData(result.data) } } catch { setError('Kunde inte hämta periodisk sammanställning.') } finally { setLoading(false) } } const downloadCsv = () => { window.open( `/api/reports/periodisk-sammanstallning/csv?periodType=${periodType}&year=${year}&period=${period}`, '_blank', ) } const errors = data?.warnings.filter(w => w.level === 'error') ?? [] const cautions = data?.warnings.filter(w => w.level === 'warning') ?? [] const hasBlockingErrors = errors.length > 0 return (
{/* Period selection */} Välj period
{error && (
{error}
)} {loading && ( )} {data && !loading && ( <> {/* Summary */}
Periodisk sammanställning — {data.period.label} {data.totals.rowCount > 0 && ( {data.totals.rowCount} {data.totals.rowCount === 1 ? 'rad' : 'rader'} )}
Tjänster (typ 3)
{formatAmount(data.totals.services)} kr
Varor (typ 1)
{formatAmount(data.totals.goods)} kr
Trepart (typ 2)
{formatAmount(data.totals.triangulation)} kr
{/* Reconciliation */} {data.reconciliation.matches !== null && data.totals.rowCount > 0 && (
{data.reconciliation.matches ? ( ✓ Stämmer mot momsdeklarationen {' '}(Ruta 39: {formatAmount(data.reconciliation.ruta39 ?? 0)} kr, {' '}Ruta 35: {formatAmount(data.reconciliation.ruta35 ?? 0)} kr, {' '}Ruta 38: {formatAmount(data.reconciliation.ruta38 ?? 0)} kr) ) : ( ⚠ Avviker från momsdeklarationen — kontrollera bokföringen. {' '}Ruta 39: {formatAmount(data.reconciliation.ruta39 ?? 0)} kr, {' '}Ruta 35: {formatAmount(data.reconciliation.ruta35 ?? 0)} kr, {' '}Ruta 38: {formatAmount(data.reconciliation.ruta38 ?? 0)} kr. )}
)} {data.reconciliation.matches === null && data.totals.rowCount > 0 && (
Avstämning mot momsdeklarationen tillgänglig endast när PS-perioden sammanfaller med momsperioden.
)} {/* Deadline banner */} {data.totals.rowCount > 0 && (
Inlämnas digitalt senast den {deadlineLabel(data.period.end)} {' '}via e-tjänsten hos Skatteverket. Förseningsavgift: 1 250 kr.
)}
{/* Errors */} {errors.length > 0 && ( {errors.length} {errors.length === 1 ? 'fel måste' : 'fel måste'} åtgärdas {errors.map((w, i) => )}
CSV-nedladdning är blockerad tills alla fel åtgärdats.
)} {/* Warnings */} {cautions.length > 0 && ( Varningar ({cautions.length}) {cautions.map((w, i) => )} )} {/* Table */} {data.rows.length > 0 ? ( Rader Land VAT-nummer Tjänster Varor Trepart Typ Kund {data.rows.map((row, i) => ( {row.country} {row.vatNumber} {row.services !== 0 ? formatAmount(row.services) : '—'} {row.goods !== 0 ? formatAmount(row.goods) : '—'} {row.triangulation !== 0 ? formatAmount(row.triangulation) : '—'} {typeBadge(row)} {row.customerId ? {row.customerName ?? '—'} : — } ))} Totalt {formatAmount(data.totals.services)} {formatAmount(data.totals.goods)} {formatAmount(data.totals.triangulation)}
) : ( Läs mer hos Skatteverket )} )}
) } function WarningItem({ warning }: { warning: PsWarning }) { return (
•
{warning.message}
{(warning.customerId || warning.invoiceId) && (
{warning.customerId && ( Öppna kund )} {warning.invoiceId && ( Öppna faktura )} {warning.amount !== undefined && ( {formatCurrency(warning.amount)} )}
)}
) }