380 lines
13 KiB
TypeScript
380 lines
13 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { createClient } from '@/lib/supabase/client'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
import { useToast } from '@/components/ui/use-toast'
|
|
import { formatCurrency, formatDate } from '@/lib/utils'
|
|
import {
|
|
getSchablonavdragSummary,
|
|
groupMileageEntriesByMonth,
|
|
formatMonthKey,
|
|
SCHABLONAVDRAG_RATES,
|
|
} from '@/lib/tax/schablonavdrag'
|
|
import SchablonavdragSettings from '@/components/settings/SchablonavdragSettings'
|
|
import MileageEntry from '@/components/transactions/MileageEntry'
|
|
import {
|
|
Loader2,
|
|
Home,
|
|
Car,
|
|
Plus,
|
|
Trash2,
|
|
Calendar,
|
|
MapPin,
|
|
ChevronDown,
|
|
ChevronUp,
|
|
} from 'lucide-react'
|
|
import type {
|
|
CompanySettings,
|
|
SchablonavdragSettings as SchablonavdragSettingsType,
|
|
MileageEntry as MileageEntryType,
|
|
CreateMileageEntryInput,
|
|
} from '@/types'
|
|
|
|
export default function DeductionsPage() {
|
|
const router = useRouter()
|
|
const { toast } = useToast()
|
|
const supabase = createClient()
|
|
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [settings, setSettings] = useState<CompanySettings | null>(null)
|
|
const [mileageEntries, setMileageEntries] = useState<MileageEntryType[]>([])
|
|
const [showAddForm, setShowAddForm] = useState(false)
|
|
const [expandedMonths, setExpandedMonths] = useState<Set<string>>(new Set())
|
|
|
|
useEffect(() => {
|
|
fetchData()
|
|
}, [])
|
|
|
|
async function fetchData() {
|
|
setIsLoading(true)
|
|
|
|
const {
|
|
data: { user },
|
|
} = await supabase.auth.getUser()
|
|
if (!user) {
|
|
router.push('/login')
|
|
return
|
|
}
|
|
|
|
// Fetch settings
|
|
const { data: settingsData } = await supabase
|
|
.from('company_settings')
|
|
.select('*')
|
|
.eq('user_id', user.id)
|
|
.single()
|
|
|
|
setSettings(settingsData)
|
|
|
|
// Fetch mileage entries for current year
|
|
const startOfYear = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0]
|
|
const { data: entries } = await supabase
|
|
.from('mileage_entries')
|
|
.select('*')
|
|
.eq('user_id', user.id)
|
|
.gte('date', startOfYear)
|
|
.order('date', { ascending: false })
|
|
|
|
setMileageEntries(entries || [])
|
|
setIsLoading(false)
|
|
}
|
|
|
|
async function handleSaveSettings(newSettings: SchablonavdragSettingsType) {
|
|
if (!settings) return
|
|
|
|
const { error } = await supabase
|
|
.from('company_settings')
|
|
.update({ schablonavdrag_settings: newSettings })
|
|
.eq('id', settings.id)
|
|
|
|
if (error) {
|
|
throw error
|
|
}
|
|
|
|
setSettings({
|
|
...settings,
|
|
schablonavdrag_settings: newSettings,
|
|
} as CompanySettings & { schablonavdrag_settings: SchablonavdragSettingsType })
|
|
}
|
|
|
|
async function handleAddMileageEntry(entry: CreateMileageEntryInput) {
|
|
const {
|
|
data: { user },
|
|
} = await supabase.auth.getUser()
|
|
if (!user) return
|
|
|
|
const { data, error } = await supabase
|
|
.from('mileage_entries')
|
|
.insert({
|
|
user_id: user.id,
|
|
date: entry.date,
|
|
distance_km: entry.distance_km,
|
|
purpose: entry.purpose,
|
|
from_location: entry.from_location || null,
|
|
to_location: entry.to_location || null,
|
|
})
|
|
.select()
|
|
.single()
|
|
|
|
if (error) {
|
|
throw error
|
|
}
|
|
|
|
setMileageEntries([data, ...mileageEntries])
|
|
setShowAddForm(false)
|
|
}
|
|
|
|
async function handleDeleteMileageEntry(id: string) {
|
|
const { error } = await supabase.from('mileage_entries').delete().eq('id', id)
|
|
|
|
if (error) {
|
|
toast({
|
|
title: 'Fel',
|
|
description: 'Kunde inte ta bort körningen',
|
|
variant: 'destructive',
|
|
})
|
|
return
|
|
}
|
|
|
|
setMileageEntries(mileageEntries.filter((e) => e.id !== id))
|
|
toast({
|
|
title: 'Borttaget',
|
|
description: 'Körningen har tagits bort',
|
|
})
|
|
}
|
|
|
|
function toggleMonth(monthKey: string) {
|
|
const newExpanded = new Set(expandedMonths)
|
|
if (newExpanded.has(monthKey)) {
|
|
newExpanded.delete(monthKey)
|
|
} else {
|
|
newExpanded.add(monthKey)
|
|
}
|
|
setExpandedMonths(newExpanded)
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Get schablonavdrag settings
|
|
const schablonavdragSettings = ((settings as CompanySettings & { schablonavdrag_settings?: SchablonavdragSettingsType })?.schablonavdrag_settings) || {
|
|
hemmakontor_enabled: false,
|
|
bil_enabled: false,
|
|
}
|
|
|
|
// Calculate summary
|
|
const currentYear = new Date().getFullYear()
|
|
const currentMonth = new Date().getMonth() + 1
|
|
const summary = getSchablonavdragSummary(
|
|
schablonavdragSettings,
|
|
mileageEntries,
|
|
currentYear,
|
|
currentMonth
|
|
)
|
|
|
|
// Group mileage entries by month
|
|
const entriesByMonth = groupMileageEntriesByMonth(mileageEntries)
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Schablonavdrag</h1>
|
|
<p className="text-muted-foreground">
|
|
Hantera dina schablonmässiga avdrag för hemmakontor och bilkostnader
|
|
</p>
|
|
</div>
|
|
|
|
{/* Summary cards */}
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
{/* Total deduction */}
|
|
<Card className="bg-gradient-to-br from-success/10 via-success/5 to-background border-success/20">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
Totalt avdrag {currentYear}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold text-success">
|
|
{formatCurrency(summary.total_deduction)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Hemmakontor */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
|
<Home className="h-4 w-4" />
|
|
Hemmakontor
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{summary.hemmakontor.enabled ? formatCurrency(summary.hemmakontor.deduction) : '-'}
|
|
</div>
|
|
{summary.hemmakontor.enabled && (
|
|
<p className="text-xs text-muted-foreground">
|
|
{summary.hemmakontor.months_active} månader
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Bilkostnader */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
|
<Car className="h-4 w-4" />
|
|
Bilkostnader
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{summary.mileage.enabled ? formatCurrency(summary.mileage.total_deduction) : '-'}
|
|
</div>
|
|
{summary.mileage.enabled && summary.mileage.total_km > 0 && (
|
|
<p className="text-xs text-muted-foreground">
|
|
{summary.mileage.total_km.toFixed(0)} km loggade
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Tabs defaultValue="settings" className="space-y-6">
|
|
<TabsList>
|
|
<TabsTrigger value="settings">Inställningar</TabsTrigger>
|
|
<TabsTrigger value="mileage" disabled={!schablonavdragSettings.bil_enabled}>
|
|
Körjournal
|
|
{schablonavdragSettings.bil_enabled && mileageEntries.length > 0 && (
|
|
<Badge variant="secondary" className="ml-2">
|
|
{mileageEntries.length}
|
|
</Badge>
|
|
)}
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* Settings tab */}
|
|
<TabsContent value="settings">
|
|
<SchablonavdragSettings
|
|
settings={schablonavdragSettings}
|
|
onSave={handleSaveSettings}
|
|
/>
|
|
</TabsContent>
|
|
|
|
{/* Mileage log tab */}
|
|
<TabsContent value="mileage" className="space-y-6">
|
|
{/* Add new entry button/form */}
|
|
{showAddForm ? (
|
|
<MileageEntry onSave={handleAddMileageEntry} onCancel={() => setShowAddForm(false)} />
|
|
) : (
|
|
<Button onClick={() => setShowAddForm(true)}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Ny körning
|
|
</Button>
|
|
)}
|
|
|
|
{/* Entries list grouped by month */}
|
|
{mileageEntries.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="pt-6 text-center">
|
|
<Car className="mx-auto h-12 w-12 text-muted-foreground/50" />
|
|
<h3 className="mt-4 text-lg font-medium">Ingen körning loggad</h3>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
Börja logga dina tjänsteresor för att få milersättning
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{Array.from(entriesByMonth.entries()).map(([monthKey, monthData]) => (
|
|
<Card key={monthKey}>
|
|
<CardHeader
|
|
className="cursor-pointer"
|
|
onClick={() => toggleMonth(monthKey)}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="text-base">
|
|
{formatMonthKey(monthKey)}
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{monthData.entries.length} körningar · {monthData.totalKm.toFixed(0)} km
|
|
</CardDescription>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-lg font-medium text-success">
|
|
{formatCurrency(monthData.totalDeduction)}
|
|
</span>
|
|
{expandedMonths.has(monthKey) ? (
|
|
<ChevronUp className="h-5 w-5" />
|
|
) : (
|
|
<ChevronDown className="h-5 w-5" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
{expandedMonths.has(monthKey) && (
|
|
<CardContent className="pt-0">
|
|
<div className="space-y-3">
|
|
{monthData.entries.map((entry) => (
|
|
<div
|
|
key={entry.id}
|
|
className="flex items-start justify-between p-3 rounded-lg border bg-muted/30"
|
|
>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Calendar className="h-3.5 w-3.5 text-muted-foreground" />
|
|
<span>{formatDate(entry.date)}</span>
|
|
<Badge variant="secondary">
|
|
{Number(entry.distance_km).toFixed(1)} km
|
|
</Badge>
|
|
</div>
|
|
<p className="text-sm">{entry.purpose}</p>
|
|
{(entry.from_location || entry.to_location) && (
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<MapPin className="h-3 w-3" />
|
|
{entry.from_location && entry.to_location
|
|
? `${entry.from_location} → ${entry.to_location}`
|
|
: entry.from_location || entry.to_location}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm font-medium text-success">
|
|
{formatCurrency(Number(entry.total_deduction))}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
onClick={() => handleDeleteMileageEntry(entry.id)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
)}
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
)
|
|
}
|