This commit is contained in:
Jakob Wennberg
2026-02-19 14:32:54 +01:00
parent cdf1dcc4c8
commit e8743e6e03
104 changed files with 4551 additions and 4172 deletions
@@ -1,349 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { useToast } from '@/components/ui/use-toast'
import { createClient } from '@/lib/supabase/client'
import { Bell, BellOff, Loader2, Moon } from 'lucide-react'
import type { NotificationSettings as NotificationSettingsType } from '@/types'
interface NotificationSettingsProps {
onSettingsChange?: () => void
}
export function NotificationSettings({ onSettingsChange }: NotificationSettingsProps) {
const { toast } = useToast()
const supabase = createClient()
const [isLoading, setIsLoading] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const [settings, setSettings] = useState<NotificationSettingsType | null>(null)
const [isPushSupported, setIsPushSupported] = useState(false)
const [isPushSubscribed, setIsPushSubscribed] = useState(false)
useEffect(() => {
fetchSettings()
checkPushSupport()
}, [])
const checkPushSupport = async () => {
if ('serviceWorker' in navigator && 'PushManager' in window) {
setIsPushSupported(true)
try {
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.getSubscription()
setIsPushSubscribed(!!subscription)
} catch (error) {
console.error('Error checking push subscription:', error)
}
}
}
const fetchSettings = async () => {
setIsLoading(true)
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
const { data, error } = await supabase
.from('notification_settings')
.select('*')
.eq('user_id', user.id)
.single()
if (!error && data) {
setSettings(data)
} else {
// Create default settings if not found
const { data: newSettings, error: insertError } = await supabase
.from('notification_settings')
.insert({
user_id: user.id,
tax_deadlines_enabled: true,
invoice_reminders_enabled: true,
push_enabled: true,
email_enabled: true,
quiet_start: '21:00',
quiet_end: '08:00',
})
.select()
.single()
if (!insertError && newSettings) {
setSettings(newSettings)
}
}
setIsLoading(false)
}
const updateSetting = async (key: keyof NotificationSettingsType, value: boolean | string) => {
if (!settings) return
setIsSaving(true)
const { error } = await supabase
.from('notification_settings')
.update({ [key]: value })
.eq('id', settings.id)
if (error) {
toast({
title: 'Fel',
description: 'Kunde inte spara inställning',
variant: 'destructive',
})
} else {
setSettings({ ...settings, [key]: value })
onSettingsChange?.()
}
setIsSaving(false)
}
const subscribeToPush = async () => {
try {
const permission = await Notification.requestPermission()
if (permission !== 'granted') {
toast({
title: 'Aviseringar avslagna',
description: 'Du behöver ge tillåtelse för push-aviseringar i webbläsaren.',
})
return
}
const vapidResponse = await fetch('/api/push/subscribe')
const { vapidPublicKey } = await vapidResponse.json()
if (!vapidPublicKey) {
throw new Error('Push not configured')
}
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
})
const response = await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription.toJSON()),
})
if (!response.ok) {
throw new Error('Failed to save subscription')
}
setIsPushSubscribed(true)
toast({
title: 'Push-aviseringar aktiverade',
description: 'Du kommer nu få push-aviseringar.',
})
} catch (error) {
toast({
title: 'Fel',
description: 'Kunde inte aktivera push-aviseringar.',
variant: 'destructive',
})
}
}
const unsubscribeFromPush = async () => {
try {
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.getSubscription()
if (subscription) {
await subscription.unsubscribe()
await fetch('/api/push/subscribe', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ endpoint: subscription.endpoint }),
})
}
setIsPushSubscribed(false)
toast({
title: 'Push-aviseringar inaktiverade',
description: 'Du kommer inte längre få push-aviseringar.',
})
} catch (error) {
toast({
title: 'Fel',
description: 'Kunde inte inaktivera push-aviseringar.',
variant: 'destructive',
})
}
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-32">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)
}
if (!settings) {
return null
}
return (
<div className="space-y-6">
{/* Push notification status */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="h-5 w-5" />
Push-aviseringar
</CardTitle>
<CardDescription>
Få påminnelser direkt i din webbläsare
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{isPushSupported ? (
<div className="flex items-center justify-between">
<div>
<p className="font-medium">
{isPushSubscribed ? 'Aktiverade' : 'Inaktiverade'}
</p>
<p className="text-sm text-muted-foreground">
{isPushSubscribed
? 'Du får push-aviseringar på denna enhet'
: 'Aktivera för att få push-aviseringar'}
</p>
</div>
<Button
variant={isPushSubscribed ? 'outline' : 'default'}
onClick={isPushSubscribed ? unsubscribeFromPush : subscribeToPush}
>
{isPushSubscribed ? (
<>
<BellOff className="mr-2 h-4 w-4" />
Inaktivera
</>
) : (
<>
<Bell className="mr-2 h-4 w-4" />
Aktivera
</>
)}
</Button>
</div>
) : (
<p className="text-sm text-muted-foreground">
Din webbläsare stöder inte push-aviseringar.
</p>
)}
</CardContent>
</Card>
{/* Category settings */}
<Card>
<CardHeader>
<CardTitle>Aviseringskategorier</CardTitle>
<CardDescription>
Välj vilka typer av aviseringar du vill få
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="tax-deadlines">Skattedeadlines</Label>
<p className="text-sm text-muted-foreground">
Moms, F-skatt, inkomstdeklaration m.m.
</p>
</div>
<Switch
id="tax-deadlines"
checked={settings.tax_deadlines_enabled}
onCheckedChange={(checked) =>
updateSetting('tax_deadlines_enabled', checked)
}
disabled={isSaving}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="invoice-reminders">Fakturapåminnelser</Label>
<p className="text-sm text-muted-foreground">
Fakturor som förfaller eller är försenade
</p>
</div>
<Switch
id="invoice-reminders"
checked={settings.invoice_reminders_enabled}
onCheckedChange={(checked) =>
updateSetting('invoice_reminders_enabled', checked)
}
disabled={isSaving}
/>
</div>
</CardContent>
</Card>
{/* Quiet hours */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Moon className="h-5 w-5" />
Tysta timmar
</CardTitle>
<CardDescription>
Inga aviseringar skickas under dessa timmar
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="quiet-start">Från</Label>
<Input
id="quiet-start"
type="time"
value={settings.quiet_start}
onChange={(e) => updateSetting('quiet_start', e.target.value)}
disabled={isSaving}
/>
</div>
<div className="space-y-2">
<Label htmlFor="quiet-end">Till</Label>
<Input
id="quiet-end"
type="time"
value={settings.quiet_end}
onChange={(e) => updateSetting('quiet_end', e.target.value)}
disabled={isSaving}
/>
</div>
</div>
<p className="text-xs text-muted-foreground">
Standard: 21:00 - 08:00 (Sveriges tid)
</p>
</CardContent>
</Card>
</div>
)
}
function urlBase64ToUint8Array(base64String: string): ArrayBuffer {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = window.atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray.buffer
}
@@ -1,204 +0,0 @@
'use client'
import { useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
import { useToast } from '@/components/ui/use-toast'
import { formatCurrency } from '@/lib/utils'
import { SCHABLONAVDRAG_RATES, type HousingType } from '@/lib/tax/schablonavdrag'
import { Home, Car, Loader2, Info, Building } from 'lucide-react'
import type { SchablonavdragSettings as SchablonavdragSettingsType } from '@/types'
interface SchablonavdragSettingsProps {
settings: SchablonavdragSettingsType
onSave: (settings: SchablonavdragSettingsType) => Promise<void>
}
export default function SchablonavdragSettings({
settings: initialSettings,
onSave,
}: SchablonavdragSettingsProps) {
const { toast } = useToast()
const [isSaving, setIsSaving] = useState(false)
const [settings, setSettings] = useState<SchablonavdragSettingsType>(initialSettings)
const hasChanges =
settings.hemmakontor_enabled !== initialSettings.hemmakontor_enabled ||
settings.hemmakontor_housing_type !== initialSettings.hemmakontor_housing_type ||
settings.bil_enabled !== initialSettings.bil_enabled
// Get the current hemmakontor amount based on housing type
const housingType = settings.hemmakontor_housing_type || 'apartment'
const hemmakontorAmount = housingType === 'villa'
? SCHABLONAVDRAG_RATES.hemmakontor.villa
: SCHABLONAVDRAG_RATES.hemmakontor.apartment
async function handleSave() {
setIsSaving(true)
try {
await onSave(settings)
toast({
title: 'Sparat',
description: 'Dina schablonavdragsinställningar har uppdaterats',
})
} catch (error) {
toast({
title: 'Fel',
description: 'Kunde inte spara inställningar',
variant: 'destructive',
})
}
setIsSaving(false)
}
function setHousingType(type: HousingType) {
setSettings({ ...settings, hemmakontor_housing_type: type })
}
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
Schablonavdrag
</CardTitle>
<CardDescription>
Aktivera schablonmässiga avdrag som inte kräver kvitton
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Hemmakontor */}
<div className="p-4 border rounded-lg space-y-4">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
<Home className="h-5 w-5 text-primary" />
</div>
<div className="space-y-1">
<Label htmlFor="hemmakontor" className="text-base font-medium">
Hemmakontor
</Label>
<p className="text-sm text-muted-foreground">
Schablonavdrag för hemmakontor (minst 800 tim/år)
</p>
<div className="flex items-center gap-2 text-sm">
<span className="font-medium text-success">
{formatCurrency(hemmakontorAmount)}
</span>
<span className="text-muted-foreground">per år</span>
</div>
</div>
</div>
<Switch
id="hemmakontor"
checked={settings.hemmakontor_enabled}
onCheckedChange={(checked) =>
setSettings({ ...settings, hemmakontor_enabled: checked })
}
/>
</div>
{/* Housing type selection */}
{settings.hemmakontor_enabled && (
<div className="ml-14 space-y-2">
<Label className="text-sm text-muted-foreground">Typ av bostad</Label>
<div className="flex gap-2">
<Button
type="button"
variant={housingType === 'apartment' ? 'default' : 'outline'}
size="sm"
onClick={() => setHousingType('apartment')}
className="flex items-center gap-2"
>
<Building className="h-4 w-4" />
Hyresrätt/Bostadsrätt
<span className="text-xs opacity-70">({formatCurrency(SCHABLONAVDRAG_RATES.hemmakontor.apartment)})</span>
</Button>
<Button
type="button"
variant={housingType === 'villa' ? 'default' : 'outline'}
size="sm"
onClick={() => setHousingType('villa')}
className="flex items-center gap-2"
>
<Home className="h-4 w-4" />
Villa
<span className="text-xs opacity-70">({formatCurrency(SCHABLONAVDRAG_RATES.hemmakontor.villa)})</span>
</Button>
</div>
</div>
)}
</div>
{/* Bilkostnader */}
<div className="flex items-start justify-between p-4 border rounded-lg">
<div className="flex items-start gap-4">
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
<Car className="h-5 w-5 text-primary" />
</div>
<div className="space-y-1">
<Label htmlFor="bil" className="text-base font-medium">
Bilkostnader (körjournal)
</Label>
<p className="text-sm text-muted-foreground">
Milersättning för tjänsteresor med egen bil
</p>
<div className="flex items-center gap-2 text-sm">
<span className="font-medium text-success">
{SCHABLONAVDRAG_RATES.bil.rate_per_mil} kr/mil
</span>
<span className="text-muted-foreground">
({SCHABLONAVDRAG_RATES.bil.rate_per_km.toFixed(2)} kr/km)
</span>
</div>
</div>
</div>
<Switch
id="bil"
checked={settings.bil_enabled}
onCheckedChange={(checked) =>
setSettings({ ...settings, bil_enabled: checked })
}
/>
</div>
{/* Info box */}
<div className="flex items-start gap-3 p-4 rounded-lg bg-muted/50">
<Info className="h-5 w-5 text-muted-foreground flex-shrink-0 mt-0.5" />
<div className="text-sm text-muted-foreground">
<p className="font-medium text-foreground">Om schablonavdrag</p>
<ul className="mt-2 space-y-1 list-disc list-inside">
<li>
<strong>Hemmakontor:</strong> Kräver minst 800 timmars arbete hemifrån per år.
Avdrag: {formatCurrency(SCHABLONAVDRAG_RATES.hemmakontor.apartment)} (lägenhet) eller{' '}
{formatCurrency(SCHABLONAVDRAG_RATES.hemmakontor.villa)} (villa).
</li>
<li>
<strong>Bilkostnader:</strong> Kräver att du loggar dina resor i körjournalen.
Du får {SCHABLONAVDRAG_RATES.bil.rate_per_mil} kr per mil ({SCHABLONAVDRAG_RATES.bil.rate_per_km.toFixed(2)} kr/km).
</li>
<li>Schablonavdrag minskar din beskattningsbara inkomst</li>
</ul>
</div>
</div>
{/* Save button */}
{hasChanges && (
<div className="flex justify-end">
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Sparar...
</>
) : (
'Spara ändringar'
)}
</Button>
</div>
)}
</CardContent>
</Card>
)
}