Refactor to generic ERP base
Remove influencer-specific features (campaigns, TikTok, gifts, shadow ledger, contracts, briefings) and consolidate into a clean ERP foundation with core bookkeeping, invoicing, receipts, tax reporting, and calendar functionality. Reorganize database migrations into a clean numbered sequence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a25d10a528
commit
0aecf5b41c
@@ -14,8 +14,6 @@ import {
|
||||
import { getSchablonavdragSummary } from '@/lib/tax/schablonavdrag'
|
||||
import FSkattWarningCard from '@/components/dashboard/FSkattWarningCard'
|
||||
import { UpcomingDeadlinesWidget } from '@/components/calendar/UpcomingDeadlinesWidget'
|
||||
import { CampaignsWidget } from '@/components/campaigns/CampaignsWidget'
|
||||
import { TikTokStatsWidget } from '@/components/tiktok'
|
||||
import NewUserChecklist from '@/components/onboarding/NewUserChecklist'
|
||||
import {
|
||||
TrendingUp,
|
||||
@@ -23,15 +21,13 @@ import {
|
||||
Receipt,
|
||||
ArrowLeftRight,
|
||||
ChevronDown,
|
||||
Gift,
|
||||
ArrowRight,
|
||||
Megaphone,
|
||||
Camera,
|
||||
HelpCircle,
|
||||
Users,
|
||||
FileText,
|
||||
} from 'lucide-react'
|
||||
import type { CompanySettings, EntityType, MileageEntry, SchablonavdragSettings, GiftSummary, Deadline, Campaign, TikTokStatsSummary, ReceiptQueueSummary, OnboardingProgress } from '@/types'
|
||||
import type { CompanySettings, EntityType, MileageEntry, SchablonavdragSettings, Deadline, ReceiptQueueSummary, OnboardingProgress } from '@/types'
|
||||
|
||||
interface DashboardContentProps {
|
||||
firstName?: string | null
|
||||
@@ -48,56 +44,15 @@ interface DashboardContentProps {
|
||||
overdueInvoicesCount: number
|
||||
bankBalance: number | null
|
||||
mileageEntries: MileageEntry[]
|
||||
giftSummary: GiftSummary | null
|
||||
deadlines: Deadline[]
|
||||
campaigns: Campaign[]
|
||||
receiptQueue: ReceiptQueueSummary | null
|
||||
}
|
||||
onboardingProgress?: OnboardingProgress
|
||||
}
|
||||
|
||||
export default function DashboardContent({ firstName, settings, summary, onboardingProgress }: DashboardContentProps) {
|
||||
const [tiktokStats, setTiktokStats] = useState<TikTokStatsSummary | null>(null)
|
||||
const [isTiktokSyncing, setIsTiktokSyncing] = useState(false)
|
||||
const [showAllAlerts, setShowAllAlerts] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchTikTokStats()
|
||||
}, [])
|
||||
|
||||
const fetchTikTokStats = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/tiktok/stats')
|
||||
const data = await response.json()
|
||||
if (data.summary) {
|
||||
setTiktokStats(data.summary)
|
||||
}
|
||||
} catch (error) {
|
||||
// TikTok not connected or error - that's fine
|
||||
}
|
||||
}
|
||||
|
||||
const handleTikTokSync = async () => {
|
||||
setIsTiktokSyncing(true)
|
||||
try {
|
||||
const accountsResponse = await fetch('/api/tiktok/accounts')
|
||||
const accountsData = await accountsResponse.json()
|
||||
const activeAccount = accountsData.accounts?.find((a: { status: string }) => a.status === 'active')
|
||||
|
||||
if (activeAccount) {
|
||||
await fetch('/api/tiktok/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ account_id: activeAccount.id, sync_type: 'full' }),
|
||||
})
|
||||
await fetchTikTokStats()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('TikTok sync failed:', error)
|
||||
}
|
||||
setIsTiktokSyncing(false)
|
||||
}
|
||||
|
||||
const entityType = (settings?.entity_type as EntityType) || 'enskild_firma'
|
||||
const preliminaryTaxMonthly = settings?.preliminary_tax_monthly || 0
|
||||
const currentMonth = new Date().getMonth() + 1
|
||||
@@ -111,11 +66,7 @@ export default function DashboardContent({ firstName, settings, summary, onboard
|
||||
currentMonth
|
||||
)
|
||||
|
||||
const giftTaxableValue = summary.giftSummary?.taxable_value || 0
|
||||
const giftDeductibleValue = summary.giftSummary?.deductible_value || 0
|
||||
const netGiftTaxableIncome = giftTaxableValue - giftDeductibleValue
|
||||
|
||||
const totalTaxableIncome = summary.ytd.net + netGiftTaxableIncome
|
||||
const totalTaxableIncome = summary.ytd.net
|
||||
|
||||
const taxEstimate =
|
||||
entityType === 'enskild_firma'
|
||||
@@ -242,36 +193,6 @@ export default function DashboardContent({ firstName, settings, summary, onboard
|
||||
)
|
||||
}
|
||||
|
||||
if (summary.giftSummary && summary.giftSummary.total_count > 0) {
|
||||
alertItems.push(
|
||||
<Link key="gifts" href="/gifts" className="group">
|
||||
<Card className="h-full border-l-4 border-l-accent hover:border-primary/30 transition-colors">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg bg-accent/10">
|
||||
<Gift className="h-4 w-4 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-sm">Gåvor & Förmåner</p>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
{summary.giftSummary.total_count} produkter
|
||||
</p>
|
||||
{summary.giftSummary.taxable_count > 0 && (
|
||||
<Badge variant="destructive" className="mt-1.5">
|
||||
{formatCurrency(summary.giftSummary.taxable_value)} skattepliktig
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-foreground group-hover:translate-x-0.5 transition-all" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const MAX_VISIBLE_ALERTS = 3
|
||||
const visibleAlerts = showAllAlerts ? alertItems : alertItems.slice(0, MAX_VISIBLE_ALERTS)
|
||||
const hasMoreAlerts = alertItems.length > MAX_VISIBLE_ALERTS
|
||||
@@ -280,7 +201,6 @@ export default function DashboardContent({ firstName, settings, summary, onboard
|
||||
const quickActions = [
|
||||
{ href: '/invoices/new', icon: Receipt, label: 'Ny faktura', desc: 'Skapa och skicka', accent: true },
|
||||
{ href: '/receipts/scan', icon: Camera, label: 'Skanna kvitto', desc: 'Fotografera & spara' },
|
||||
{ href: '/campaigns/new', icon: Megaphone, label: 'Nytt samarbete', desc: 'Spåra innehåll' },
|
||||
{ href: '/customers', icon: Users, label: 'Ny kund', desc: 'Lägg till kunduppgifter' },
|
||||
{ href: '/transactions', icon: ArrowLeftRight, label: 'Transaktioner', desc: 'Kategorisera' },
|
||||
]
|
||||
@@ -404,24 +324,6 @@ export default function DashboardContent({ firstName, settings, summary, onboard
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* TikTok stats widget */}
|
||||
{tiktokStats && (
|
||||
<section className="mb-8">
|
||||
<TikTokStatsWidget
|
||||
stats={tiktokStats}
|
||||
onSync={handleTikTokSync}
|
||||
isSyncing={isTiktokSyncing}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Active campaigns */}
|
||||
{summary.campaigns && summary.campaigns.length > 0 && (
|
||||
<section className="mb-8">
|
||||
<CampaignsWidget campaigns={summary.campaigns} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Alerts section - with urgency indicators and limit */}
|
||||
{alertItems.length > 0 && (
|
||||
<section id="alerts-section" className="mb-16">
|
||||
|
||||
@@ -16,18 +16,14 @@ import {
|
||||
Settings,
|
||||
LogOut,
|
||||
Calculator,
|
||||
Gift,
|
||||
Upload,
|
||||
Calendar,
|
||||
Megaphone,
|
||||
FileUp,
|
||||
Camera,
|
||||
Menu,
|
||||
X,
|
||||
HelpCircle,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Wallet,
|
||||
} from 'lucide-react'
|
||||
import type { EntityType } from '@/types'
|
||||
|
||||
@@ -48,19 +44,14 @@ interface NavItem {
|
||||
const navItems: NavItem[] = [
|
||||
{ href: '/', label: 'Översikt', icon: LayoutDashboard, group: 'main' },
|
||||
{ href: '/calendar', label: 'Kalender', icon: Calendar, group: 'main' },
|
||||
{ href: '/campaigns', label: 'Samarbeten', icon: Megaphone, group: 'main' },
|
||||
{ href: '/campaigns/import', label: 'Importera avtal', icon: FileUp, group: 'main' },
|
||||
{ href: '/analytics', label: 'Analytics', icon: BarChart3, group: 'main' },
|
||||
{ href: '/shadow-ledger', label: 'Utbetalningar', icon: Wallet, group: 'finans', modes: ['light'] },
|
||||
{ href: '/invoices', label: 'Fakturor', icon: Receipt, group: 'finans', modes: ['enskild_firma', 'aktiebolag'] },
|
||||
{ href: '/customers', label: 'Kunder', icon: Users, group: 'finans', modes: ['enskild_firma', 'aktiebolag'] },
|
||||
{ href: '/invoices', label: 'Fakturor', icon: Receipt, group: 'finans' },
|
||||
{ href: '/customers', label: 'Kunder', icon: Users, group: 'finans' },
|
||||
{ href: '/transactions', label: 'Transaktioner', icon: ArrowLeftRight, group: 'finans' },
|
||||
{ href: '/receipts', label: 'Kvitton', icon: Camera, group: 'finans' },
|
||||
{ href: '/gifts', label: 'Gåvor', icon: Gift, group: 'finans' },
|
||||
{ href: '/deductions', label: 'Avdrag', icon: Calculator, group: 'finans', modes: ['enskild_firma', 'aktiebolag'] },
|
||||
{ href: '/bookkeeping', label: 'Bokföring', icon: BookOpen, group: 'finans', modes: ['enskild_firma', 'aktiebolag'] },
|
||||
{ href: '/import', label: 'Importera', icon: Upload, group: 'övrigt', modes: ['enskild_firma', 'aktiebolag'] },
|
||||
{ href: '/reports', label: 'Rapporter', icon: BarChart3, group: 'övrigt', modes: ['enskild_firma', 'aktiebolag'] },
|
||||
{ href: '/deductions', label: 'Avdrag', icon: Calculator, group: 'finans' },
|
||||
{ href: '/bookkeeping', label: 'Bokföring', icon: BookOpen, group: 'finans' },
|
||||
{ href: '/import', label: 'Importera', icon: Upload, group: 'övrigt' },
|
||||
{ href: '/reports', label: 'Rapporter', icon: BarChart3, group: 'övrigt' },
|
||||
{ href: '/help', label: 'Hjälp', icon: HelpCircle, group: 'övrigt' },
|
||||
{ href: '/settings', label: 'Inställningar', icon: Settings, group: 'övrigt' },
|
||||
]
|
||||
@@ -101,20 +92,12 @@ export default function DashboardNav({ companyName, entityType }: DashboardNavPr
|
||||
const finansItems = filteredItems.filter(i => i.group === 'finans')
|
||||
const övrigtItems = filteredItems.filter(i => i.group === 'övrigt')
|
||||
|
||||
// Mobile nav items - light mode shows Utbetalningar instead of Fakturor
|
||||
const mobileNavItems = entityType === 'light'
|
||||
? [
|
||||
{ href: '/', label: 'Översikt', icon: LayoutDashboard },
|
||||
{ href: '/campaigns', label: 'Samarbeten', icon: Megaphone },
|
||||
{ href: '/receipts/scan', label: 'Skanna', icon: Camera, isScan: true },
|
||||
{ href: '/shadow-ledger', label: 'Utbetalningar', icon: Wallet },
|
||||
]
|
||||
: [
|
||||
{ href: '/', label: 'Översikt', icon: LayoutDashboard },
|
||||
{ href: '/campaigns', label: 'Samarbeten', icon: Megaphone },
|
||||
{ href: '/receipts/scan', label: 'Skanna', icon: Camera, isScan: true },
|
||||
{ href: '/invoices', label: 'Fakturor', icon: Receipt },
|
||||
]
|
||||
const mobileNavItems = [
|
||||
{ href: '/', label: 'Översikt', icon: LayoutDashboard },
|
||||
{ href: '/invoices', label: 'Fakturor', icon: Receipt },
|
||||
{ href: '/receipts/scan', label: 'Skanna', icon: Camera, isScan: true },
|
||||
{ href: '/transactions', label: 'Transaktioner', icon: ArrowLeftRight },
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -128,7 +111,7 @@ export default function DashboardNav({ companyName, entityType }: DashboardNavPr
|
||||
{companyName}
|
||||
</h1>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 tracking-wide uppercase">
|
||||
{entityType === 'light' ? 'Egenanställd' : 'Ekonomi'}
|
||||
Ekonomi
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { cn, formatCurrency } from '@/lib/utils'
|
||||
import { Gift, ArrowRight, CheckCircle2 } from 'lucide-react'
|
||||
|
||||
interface GiftTaxDebtCardProps {
|
||||
virtualTaxDebt: number
|
||||
taxableGiftCount: number
|
||||
effectiveRate: number
|
||||
}
|
||||
|
||||
export default function GiftTaxDebtCard({
|
||||
virtualTaxDebt,
|
||||
taxableGiftCount,
|
||||
effectiveRate,
|
||||
}: GiftTaxDebtCardProps) {
|
||||
const hasDebt = virtualTaxDebt > 0
|
||||
|
||||
return (
|
||||
<Link href="/gifts" className="group block">
|
||||
<Card className="h-full hover:border-primary/30 transition-colors">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Gift className="h-5 w-5 text-muted-foreground" />
|
||||
<CardTitle className="text-lg">G\u00e5voskatt</CardTitle>
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-foreground group-hover:translate-x-0.5 transition-all" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{hasDebt ? (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="font-display text-2xl font-medium tabular-nums text-amber-600">
|
||||
{formatCurrency(virtualTaxDebt)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Virtuell skatteskuld
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<span className="tabular-nums font-medium text-foreground">
|
||||
{taxableGiftCount}
|
||||
</span>{' '}
|
||||
skattepliktiga g\u00e5vor
|
||||
</div>
|
||||
<span className="text-border">{'\u00b7'}</span>
|
||||
<div>
|
||||
<span className="tabular-nums font-medium text-foreground">
|
||||
{(effectiveRate * 100).toFixed(1)}%
|
||||
</span>{' '}
|
||||
skattesats
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-500 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-emerald-700">
|
||||
Ingen g\u00e5voskatt att betala
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Du har inga skattepliktiga g\u00e5vor registrerade i \u00e5r
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import SafeToSpendGauge from '@/components/dashboard/SafeToSpendGauge'
|
||||
import SGIShieldWidget from '@/components/dashboard/SGIShieldWidget'
|
||||
import GiftTaxDebtCard from '@/components/dashboard/GiftTaxDebtCard'
|
||||
import RecentPayoutsCard from '@/components/dashboard/RecentPayoutsCard'
|
||||
import {
|
||||
Banknote,
|
||||
Camera,
|
||||
Megaphone,
|
||||
Gift,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface RecentEntry {
|
||||
id: string
|
||||
date: string
|
||||
description: string | null
|
||||
gross_amount: number
|
||||
net_amount: number
|
||||
service_fee: number
|
||||
pension_deduction: number
|
||||
social_fees: number
|
||||
income_tax_withheld: number
|
||||
platform_fee: number
|
||||
type: string
|
||||
provider: string | null
|
||||
}
|
||||
|
||||
interface LightDashboardContentProps {
|
||||
firstName: string | null
|
||||
bankBalance: number | null
|
||||
giftTaxDebt: number
|
||||
taxableGiftCount: number
|
||||
effectiveRate: number
|
||||
daysSinceLastPayout: number | null
|
||||
recentEntries: RecentEntry[]
|
||||
hobbyReserve?: number
|
||||
}
|
||||
|
||||
export default function LightDashboardContent({
|
||||
firstName,
|
||||
bankBalance,
|
||||
giftTaxDebt,
|
||||
taxableGiftCount,
|
||||
effectiveRate,
|
||||
daysSinceLastPayout,
|
||||
recentEntries,
|
||||
hobbyReserve = 0,
|
||||
}: LightDashboardContentProps) {
|
||||
const greeting = (() => {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 12) return 'Godmorgon'
|
||||
if (hour < 18) return 'God eftermiddag'
|
||||
return 'God kv\u00e4ll'
|
||||
})()
|
||||
|
||||
const quickActions = [
|
||||
{
|
||||
href: '/shadow-ledger/new',
|
||||
icon: Banknote,
|
||||
label: 'Logga utbetalning',
|
||||
},
|
||||
{
|
||||
href: '/receipts/scan',
|
||||
icon: Camera,
|
||||
label: 'Skanna kvitto',
|
||||
},
|
||||
{
|
||||
href: '/campaigns/new',
|
||||
icon: Megaphone,
|
||||
label: 'Nytt samarbete',
|
||||
},
|
||||
{
|
||||
href: '/gifts',
|
||||
icon: Gift,
|
||||
label: 'Ny g\u00e5va',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="stagger-enter">
|
||||
{/* Greeting header */}
|
||||
<header className="mb-10">
|
||||
<h1 className="font-display text-4xl md:text-5xl font-medium tracking-tight">
|
||||
{greeting}{firstName ? `, ${firstName}` : ''}!
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
{/* SafeToSpendGauge - full width */}
|
||||
<section className="mb-6">
|
||||
<SafeToSpendGauge
|
||||
bankBalance={bankBalance ?? 0}
|
||||
giftTaxDebt={giftTaxDebt}
|
||||
hobbyReserve={hobbyReserve}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* SGIShieldWidget - full width */}
|
||||
<section className="mb-6">
|
||||
<SGIShieldWidget daysSinceLastPayout={daysSinceLastPayout} />
|
||||
</section>
|
||||
|
||||
{/* GiftTaxDebtCard + RecentPayoutsCard side by side on md+ */}
|
||||
<section className="grid md:grid-cols-2 gap-6 mb-10">
|
||||
<GiftTaxDebtCard
|
||||
virtualTaxDebt={giftTaxDebt}
|
||||
taxableGiftCount={taxableGiftCount}
|
||||
effectiveRate={effectiveRate}
|
||||
/>
|
||||
<RecentPayoutsCard entries={recentEntries} />
|
||||
</section>
|
||||
|
||||
{/* Quick actions row */}
|
||||
<section className="mb-12">
|
||||
<h2 className="font-display text-xl font-medium mb-4">Snabb\u00e5tg\u00e4rder</h2>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{quickActions.map((action) => {
|
||||
const Icon = action.icon
|
||||
return (
|
||||
<Link key={action.href} href={action.href}>
|
||||
<Button variant="outline" size="default" className="gap-2">
|
||||
<Icon className="h-4 w-4" />
|
||||
{action.label}
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { formatCurrency, formatDate } from '@/lib/utils'
|
||||
import { ArrowRight, Banknote, Plus } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
interface RecentPayoutsCardProps {
|
||||
entries: Array<{
|
||||
id: string
|
||||
date: string
|
||||
description: string | null
|
||||
gross_amount: number
|
||||
net_amount: number
|
||||
service_fee: number
|
||||
pension_deduction: number
|
||||
social_fees: number
|
||||
income_tax_withheld: number
|
||||
platform_fee: number
|
||||
type: string
|
||||
provider: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
export default function RecentPayoutsCard({ entries }: RecentPayoutsCardProps) {
|
||||
const displayEntries = entries.slice(0, 5)
|
||||
const isEmpty = displayEntries.length === 0
|
||||
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Banknote className="h-5 w-5 text-muted-foreground" />
|
||||
<CardTitle className="text-lg">Senaste utbetalningar</CardTitle>
|
||||
</div>
|
||||
{!isEmpty && (
|
||||
<Link
|
||||
href="/shadow-ledger"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
||||
>
|
||||
Visa alla
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isEmpty ? (
|
||||
<div className="text-center py-6">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Inga utbetalningar registrerade
|
||||
</p>
|
||||
<Link href="/shadow-ledger/new">
|
||||
<Button variant="outline" size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Logga utbetalning
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{displayEntries.map((entry) => (
|
||||
<div
|
||||
key={entry.id}
|
||||
className="flex items-center justify-between py-2 border-b border-border/50 last:border-0"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{entry.description || entry.provider || 'Utbetalning'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatDate(entry.date)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right ml-4 flex-shrink-0">
|
||||
<p className="text-sm tabular-nums font-medium">
|
||||
{formatCurrency(entry.net_amount)}
|
||||
</p>
|
||||
{entry.gross_amount !== entry.net_amount && (
|
||||
<p className="text-xs text-muted-foreground tabular-nums">
|
||||
brutto {formatCurrency(entry.gross_amount)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Shield, Info } from 'lucide-react'
|
||||
|
||||
interface SGIShieldWidgetProps {
|
||||
daysSinceLastPayout: number | null
|
||||
}
|
||||
|
||||
export default function SGIShieldWidget({
|
||||
daysSinceLastPayout,
|
||||
}: SGIShieldWidgetProps) {
|
||||
const maxDays = 90
|
||||
const days = daysSinceLastPayout ?? 0
|
||||
const progress = daysSinceLastPayout !== null ? Math.min((days / maxDays) * 100, 100) : 0
|
||||
|
||||
// Determine status
|
||||
let level: 'green' | 'yellow' | 'red'
|
||||
let statusMessage: string | null = null
|
||||
|
||||
if (daysSinceLastPayout === null) {
|
||||
level = 'green'
|
||||
} else if (days < 30) {
|
||||
level = 'green'
|
||||
} else if (days < 75) {
|
||||
level = 'yellow'
|
||||
statusMessage = 'Varning: Logga uppdrag snart'
|
||||
} else {
|
||||
level = 'red'
|
||||
statusMessage = 'Kritiskt: SGI riskerar nollst\u00e4llning'
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield
|
||||
className={cn(
|
||||
'h-5 w-5',
|
||||
level === 'green' && 'text-emerald-500',
|
||||
level === 'yellow' && 'text-amber-500',
|
||||
level === 'red' && 'text-red-500'
|
||||
)}
|
||||
/>
|
||||
<CardTitle className="text-lg">SGI-skydd</CardTitle>
|
||||
</div>
|
||||
{daysSinceLastPayout !== null && (
|
||||
<span className="text-sm text-muted-foreground tabular-nums">
|
||||
{days} dagar sedan senast
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{daysSinceLastPayout === null ? (
|
||||
<div className="flex items-start gap-3 py-2">
|
||||
<Info className="h-4 w-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Inga utbetalningar registrerade \u00e4nnu
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
SGI (sjukpenninggrundande inkomst) baseras p\u00e5 dina l\u00f6neutbetalningar.
|
||||
Logga din f\u00f6rsta utbetalning f\u00f6r att b\u00f6rja sp\u00e5ra.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Progress bar */}
|
||||
<div className="w-full h-3 rounded-full overflow-hidden bg-muted mb-2">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all duration-500',
|
||||
level === 'green' && 'bg-emerald-500',
|
||||
level === 'yellow' && 'bg-amber-500',
|
||||
level === 'red' && 'bg-red-500'
|
||||
)}
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span className="tabular-nums">{days}/{maxDays}</span>
|
||||
<span>N\u00e4sta varning: dag 30 {'\u00b7'} Kritisk: dag 75</span>
|
||||
</div>
|
||||
|
||||
{/* Status message */}
|
||||
{statusMessage && (
|
||||
<div className="mt-3">
|
||||
<Badge
|
||||
variant={level === 'red' ? 'destructive' : 'warning'}
|
||||
className="text-xs"
|
||||
>
|
||||
{statusMessage}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { cn, formatCurrency } from '@/lib/utils'
|
||||
import { Wallet } from 'lucide-react'
|
||||
|
||||
interface SafeToSpendGaugeProps {
|
||||
bankBalance: number
|
||||
giftTaxDebt: number
|
||||
hobbyReserve: number
|
||||
}
|
||||
|
||||
export default function SafeToSpendGauge({
|
||||
bankBalance,
|
||||
giftTaxDebt,
|
||||
hobbyReserve,
|
||||
}: SafeToSpendGaugeProps) {
|
||||
const safeToSpend = Math.max(0, bankBalance - giftTaxDebt - hobbyReserve)
|
||||
const total = bankBalance || 1 // Avoid division by zero
|
||||
|
||||
const safePercent = bankBalance > 0 ? (safeToSpend / total) * 100 : 0
|
||||
const giftPercent = bankBalance > 0 ? (giftTaxDebt / total) * 100 : 0
|
||||
const hobbyPercent = bankBalance > 0 ? (hobbyReserve / total) * 100 : 0
|
||||
|
||||
// Determine color level
|
||||
let level: 'green' | 'yellow' | 'red'
|
||||
if (safePercent > 70) {
|
||||
level = 'green'
|
||||
} else if (safePercent > 30) {
|
||||
level = 'yellow'
|
||||
} else {
|
||||
level = 'red'
|
||||
}
|
||||
|
||||
const noBankConnected = !bankBalance || bankBalance === 0
|
||||
|
||||
return (
|
||||
<Card className={cn(noBankConnected && 'opacity-60')}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Wallet className="h-5 w-5 text-muted-foreground" />
|
||||
<CardTitle className="text-lg">Att spendera</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{noBankConnected ? (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-2xl font-display font-medium text-muted-foreground">
|
||||
Ingen bank ansluten
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Koppla din bank f\u00f6r att se ditt disponibla belopp
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-center mb-6">
|
||||
<p
|
||||
className={cn(
|
||||
'font-display text-4xl font-medium tabular-nums',
|
||||
level === 'green' && 'text-emerald-600',
|
||||
level === 'yellow' && 'text-amber-600',
|
||||
level === 'red' && 'text-red-600'
|
||||
)}
|
||||
>
|
||||
{formatCurrency(safeToSpend)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
av {formatCurrency(bankBalance)} p\u00e5 kontot
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stacked bar */}
|
||||
<div className="w-full h-4 rounded-full overflow-hidden flex bg-muted">
|
||||
{safePercent > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
'h-full transition-all duration-500',
|
||||
level === 'green' && 'bg-emerald-500',
|
||||
level === 'yellow' && 'bg-amber-500',
|
||||
level === 'red' && 'bg-red-500'
|
||||
)}
|
||||
style={{ width: `${safePercent}%` }}
|
||||
/>
|
||||
)}
|
||||
{giftPercent > 0 && (
|
||||
<div
|
||||
className="h-full bg-amber-400 transition-all duration-500"
|
||||
style={{ width: `${giftPercent}%` }}
|
||||
/>
|
||||
)}
|
||||
{hobbyPercent > 0 && (
|
||||
<div
|
||||
className="h-full bg-orange-400 transition-all duration-500"
|
||||
style={{ width: `${hobbyPercent}%` }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex flex-wrap gap-x-5 gap-y-1 mt-3 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block h-2.5 w-2.5 rounded-full',
|
||||
level === 'green' && 'bg-emerald-500',
|
||||
level === 'yellow' && 'bg-amber-500',
|
||||
level === 'red' && 'bg-red-500'
|
||||
)}
|
||||
/>
|
||||
Disponibelt
|
||||
</div>
|
||||
{giftTaxDebt > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="inline-block h-2.5 w-2.5 rounded-full bg-amber-400" />
|
||||
G\u00e5voskatt ({formatCurrency(giftTaxDebt)})
|
||||
</div>
|
||||
)}
|
||||
{hobbyReserve > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="inline-block h-2.5 w-2.5 rounded-full bg-orange-400" />
|
||||
Hobby ({formatCurrency(hobbyReserve)})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user