'use client'
import { useState, useCallback, useEffect } from 'react'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
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): string {
const diff = Date.now() - new Date(isoDate).getTime()
const minutes = Math.floor(diff / 60000)
if (minutes < 1) return 'just nu'
if (minutes < 60) return `${minutes} min sedan`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours} tim sedan`
const days = Math.floor(hours / 24)
return `${days} dag${days > 1 ? 'ar' : ''} sedan`
}
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 [profile, setProfile] = useState(null)
const [isFetching, setIsFetching] = useState(false)
const [noOrgNumber, setNoOrgNumber] = 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)
try {
// Get org_number from company settings
const settingsRes = await fetch('/api/settings')
if (!settingsRes.ok) {
toast({ title: 'Kunde inte hämta inställningar', 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 ?? 'Kunde inte hämta företagsprofil', variant: 'destructive' })
return
}
const { data } = await res.json()
setProfile(data)
await save('company_profile', data)
} catch {
toast({ title: 'Ett oväntat fel inträffade', variant: 'destructive' })
} finally {
setIsFetching(false)
}
}, [save, toast])
// Auto-fetch on first visit when no cached data
useEffect(() => {
if (!initialLoad && !profile && !noOrgNumber && !isFetching) {
fetchProfile()
}
}, [initialLoad, profile, noOrgNumber, isFetching, fetchProfile])
if (initialLoad || isDataLoading) {
return
}
if (noOrgNumber) {
return (
Inget organisationsnummer
Ange organisationsnummer under Inställningar för att visa företagsprofilen.
)
}
if (isFetching && !profile) {
return
}
if (!profile) return null
const isActive = profile.activityStatus !== 'ceased'
return (
{/* Status bar */}
{isActive ? 'Aktiv' : 'Avregistrerat'}
{profile.registration.fTax && F-skatt}
{profile.registration.vat && Moms}
{profile.registration.payroll && Arbetsgivare}
Uppdaterad {timeAgo(profile.fetchedAt)}
{/* Company info card */}
{profile.companyName}
{profile.orgNumber} · {profile.legalEntityType}
{profile.address && (
{[profile.address.street, `${profile.address.postalCode} ${profile.address.city}`]
.filter(Boolean)
.join(', ')}
)}
{profile.email && (
{profile.email}
)}
{profile.phone && (
)}
{profile.sniCodes.length > 0 && (
SNI-koder
{profile.sniCodes
.filter((sni, i, arr) => arr.findIndex(s => s.code === sni.code) === i)
.map((sni) => (
{sni.code}{' '}
{sni.name}
))}
)}
{profile.bankAccounts.length > 0 && (
Bankuppgifter
{profile.bankAccounts.map((ba, i) => (
{ba.type}:{' '}
{ba.accountNumber}
))}
)}
{profile.purpose && (
Verksamhet
{profile.purpose}
)}
{(profile.employeeRange || profile.turnoverRange) && (
{profile.employeeRange && (
Anställda: {profile.employeeRange}
)}
{profile.turnoverRange && (
Omsättning: {profile.turnoverRange}
)}
)}
{/* Financials card */}
Senaste bokslut
{profile.financials && (
{formatPeriod(profile.financials.periodStart, profile.financials.periodEnd)}
)}
{profile.financials ? (
) : (
Inga finansiella uppgifter tillgängliga.
)}
{/* Financial reports table */}
{profile.financialReports.length > 0 && (
Årsredovisningar
Period
Titel
Inlämnad
Reviderad
Revisionsutlåtande
{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 (
)
}