'use client' import { useState, useCallback, useEffect } from 'react' import { useTranslations } from 'next-intl' import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { useExtensionData } from '@/lib/extensions/use-extension-data' import { useToast } from '@/components/ui/use-toast' import { Building2, CheckCircle, XCircle, MapPin, Mail, Phone, Settings, } from 'lucide-react' import Link from 'next/link' import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry' import type { TICCompanyProfile } from '@/extensions/general/tic/lib/tic-types' function formatKSEK(value: number | null): string { if (value === null) return '—' return `${(value * 1000).toLocaleString('sv-SE')} kr` } function formatPercent(value: number | null): string { if (value === null) return '—' return `${value.toFixed(1)} %` } function toMs(epoch: number): number { // TIC returns epoch seconds; Date() expects milliseconds return epoch < 1e12 ? epoch * 1000 : epoch } function formatPeriod(start: number, end: number): string { const s = new Date(toMs(start)) const e = new Date(toMs(end)) const fmt = (d: Date) => d.toLocaleDateString('sv-SE', { year: 'numeric', month: '2-digit', day: '2-digit' }) return `${fmt(s)} – ${fmt(e)}` } function timeAgo(isoDate: string, t: (key: string, values?: Record) => string): string { const diff = Date.now() - new Date(isoDate).getTime() const minutes = Math.floor(diff / 60000) if (minutes < 1) return t('time_just_now') if (minutes < 60) return t('time_minutes_ago', { n: minutes }) const hours = Math.floor(minutes / 60) if (hours < 24) return t('time_hours_ago', { n: hours }) const days = Math.floor(hours / 24) return t('time_days_ago', { n: days }) } // Mirrors the live layout (two cards: company info + financials) so the // transition from the route-level loading.tsx to data-loaded content has no // visible reflow. Keep in sync with app/(dashboard)/e/[sector]/[slug]/loading.tsx. function ProfileSkeleton() { return (
{Array.from({ length: 6 }).map((_, i) => (
))}
) } export default function TicWorkspace({ userId }: WorkspaceComponentProps) { const { getByKey, save, isLoading: isDataLoading } = useExtensionData('general', 'tic') const { toast } = useToast() const t = useTranslations('tic_workspace') const [profile, setProfile] = useState(null) const [isFetching, setIsFetching] = useState(false) const [noOrgNumber, setNoOrgNumber] = useState(false) const [fetchFailed, setFetchFailed] = useState(false) const [initialLoad, setInitialLoad] = useState(true) // Load cached profile from extension data useEffect(() => { if (isDataLoading) return const cached = getByKey('company_profile') if (cached?.value) { setProfile(cached.value as unknown as TICCompanyProfile) } setInitialLoad(false) }, [isDataLoading, getByKey]) const fetchProfile = useCallback(async () => { setIsFetching(true) setNoOrgNumber(false) setFetchFailed(false) try { // Get org_number from company settings const settingsRes = await fetch('/api/settings') if (!settingsRes.ok) { toast({ title: t('toast_settings_failed'), variant: 'destructive' }) return } const { data: settings } = await settingsRes.json() const orgNumber = settings?.org_number if (!orgNumber) { setNoOrgNumber(true) return } const res = await fetch( `/api/extensions/ext/tic/profile?org_number=${encodeURIComponent(orgNumber)}` ) if (!res.ok) { const { error } = await res.json() toast({ title: error ?? t('toast_profile_failed'), variant: 'destructive' }) setFetchFailed(true) return } const { data } = await res.json() setProfile(data) await save('company_profile', data) } catch { toast({ title: t('toast_unexpected_error'), variant: 'destructive' }) setFetchFailed(true) } finally { setIsFetching(false) } }, [save, toast, t]) // Auto-fetch on first visit when no cached data useEffect(() => { if (!initialLoad && !profile && !noOrgNumber && !isFetching && !fetchFailed) { fetchProfile() } }, [initialLoad, profile, noOrgNumber, isFetching, fetchFailed, fetchProfile]) if (initialLoad || isDataLoading) { return } if (noOrgNumber) { return (

{t('no_org_number_title')}

{t('no_org_number_description')}

) } if (isFetching && !profile) { return } if (fetchFailed && !profile) { return (

{t('fetch_failed_title')}

{t('fetch_failed_description')}

) } if (!profile) return null const isActive = profile.activityStatus !== 'ceased' const registrations = [ profile.registration.fTax && t('reg_f_tax'), profile.registration.vat && t('reg_vat'), profile.registration.payroll && t('reg_employer'), ].filter((label): label is string => Boolean(label)) return (
{/* Company info card */} {profile.companyName} {profile.orgNumber} · {profile.legalEntityType} {!isActive && ( · {t('deregistered')} )} {profile.address && (
{[profile.address.street, `${profile.address.postalCode} ${profile.address.city}`] .filter(Boolean) .join(', ')}
)} {profile.email && (
{profile.email}
)} {profile.phone && (
{profile.phone}
)} {registrations.length > 0 && (

{t('registered_for')}

{registrations.join(' · ')}

)} {profile.sniCodes.length > 0 && (

{t('sni_codes')}

{profile.sniCodes .filter((sni, i, arr) => arr.findIndex(s => s.code === sni.code) === i) .map((sni) => (

{sni.code}{' '} {sni.name}

))}
)} {profile.bankAccounts.length > 0 && (

{t('bank_accounts')}

{profile.bankAccounts.map((ba, i) => (

{ba.type}:{' '} {ba.accountNumber}

))}
)} {profile.purpose && (

{t('purpose')}

{profile.purpose}

)} {(profile.employeeRange || profile.turnoverRange) && (
{profile.employeeRange && (

{t('employees_range', { range: profile.employeeRange })}

)} {profile.turnoverRange && (

{t('turnover_range', { range: profile.turnoverRange })}

)}
)}

{t('updated_ago', { ago: timeAgo(profile.fetchedAt, t) })}

{/* Financials card */} {t('latest_closing')} {profile.financials && ( {formatPeriod(profile.financials.periodStart, profile.financials.periodEnd)} )} {profile.financials ? (
) : (

{t('no_financials')}

)}
{/* Financial reports table */} {profile.financialReports.length > 0 && ( {t('annual_reports')} {t('col_period')} {t('col_title')} {t('col_filed')} {t('col_audited')} {t('col_audit_opinion')} {profile.financialReports .filter((r) => !r.isInterimReport) .sort((a, b) => { const aEnd = a.periodEnd ? new Date(a.periodEnd).getTime() : 0 const bEnd = b.periodEnd ? new Date(b.periodEnd).getTime() : 0 return bEnd - aEnd }) .slice(0, 10) .map((report, i) => ( {report.periodStart && report.periodEnd ? `${report.periodStart.slice(0, 10)} – ${report.periodEnd.slice(0, 10)}` : '—'} {report.title ?? '—'} {report.arrivalDate ? new Date(report.arrivalDate).toLocaleDateString('sv-SE') : '—'} {report.isAudited === true ? ( ) : report.isAudited === false ? ( ) : ( — )} {report.auditOpinion ?? '—'} ))}
)}
) } function FinancialCell({ label, value, negative = false, }: { label: string value: string negative?: boolean }) { return (

{label}

{value}

) }