New base
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user