Files
accounted/components/transactions/MileageEntry.tsx
T

243 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { useToast } from '@/components/ui/use-toast'
import { formatCurrency } from '@/lib/utils'
import { SCHABLONAVDRAG_RATES, validateMileageEntry } from '@/lib/tax/schablonavdrag'
import { Car, Loader2, MapPin, Calendar, FileText, X } from 'lucide-react'
import type { CreateMileageEntryInput } from '@/types'
interface MileageEntryProps {
onSave: (entry: CreateMileageEntryInput) => Promise<void>
onCancel?: () => void
initialDate?: string
}
export default function MileageEntry({
onSave,
onCancel,
initialDate,
}: MileageEntryProps) {
const { toast } = useToast()
const [isSaving, setIsSaving] = useState(false)
const [errors, setErrors] = useState<string[]>([])
const today = new Date().toISOString().split('T')[0]
const [formData, setFormData] = useState<CreateMileageEntryInput>({
date: initialDate || today,
distance_km: 0,
purpose: '',
from_location: '',
to_location: '',
})
// Calculate deduction preview
const deductionPreview = formData.distance_km > 0
? formData.distance_km * SCHABLONAVDRAG_RATES.bil.rate_per_km
: 0
function handleChange(
field: keyof CreateMileageEntryInput,
value: string | number
) {
setFormData((prev) => ({ ...prev, [field]: value }))
setErrors([]) // Clear errors on change
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
// Validate
const validation = validateMileageEntry({
date: formData.date,
distance_km: formData.distance_km,
purpose: formData.purpose,
})
if (!validation.valid) {
setErrors(validation.errors)
return
}
setIsSaving(true)
try {
await onSave(formData)
toast({
title: 'Körning registrerad',
description: `${formData.distance_km} km - avdrag ${formatCurrency(deductionPreview)}`,
})
// Reset form
setFormData({
date: today,
distance_km: 0,
purpose: '',
from_location: '',
to_location: '',
})
} catch (error) {
toast({
title: 'Fel',
description: 'Kunde inte spara körningen',
variant: 'destructive',
})
}
setIsSaving(false)
}
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Car className="h-5 w-5" />
Registrera körning
</CardTitle>
<CardDescription>
Logga en tjänsteresa för milersättning
</CardDescription>
</div>
{onCancel && (
<Button variant="ghost" size="icon" onClick={onCancel}>
<X className="h-4 w-4" />
</Button>
)}
</div>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Error display */}
{errors.length > 0 && (
<div className="p-3 rounded-lg bg-destructive/10 border border-destructive/20">
<ul className="text-sm text-destructive space-y-1">
{errors.map((error, idx) => (
<li key={idx}>{error}</li>
))}
</ul>
</div>
)}
{/* Date and distance row */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="date" className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
Datum
</Label>
<Input
id="date"
type="date"
value={formData.date}
max={today}
onChange={(e) => handleChange('date', e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="distance_km">Avstånd (km)</Label>
<Input
id="distance_km"
type="number"
step="0.1"
min="0.1"
max="10000"
placeholder="0"
value={formData.distance_km || ''}
onChange={(e) =>
handleChange('distance_km', parseFloat(e.target.value) || 0)
}
required
/>
</div>
</div>
{/* Locations row */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="from_location" className="flex items-center gap-2">
<MapPin className="h-4 w-4" />
Från
</Label>
<Input
id="from_location"
placeholder="T.ex. Hemma"
value={formData.from_location || ''}
onChange={(e) => handleChange('from_location', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="to_location" className="flex items-center gap-2">
<MapPin className="h-4 w-4" />
Till
</Label>
<Input
id="to_location"
placeholder="T.ex. Kundens kontor"
value={formData.to_location || ''}
onChange={(e) => handleChange('to_location', e.target.value)}
/>
</div>
</div>
{/* Purpose */}
<div className="space-y-2">
<Label htmlFor="purpose" className="flex items-center gap-2">
<FileText className="h-4 w-4" />
Ändamål
</Label>
<Textarea
id="purpose"
placeholder="Beskriv syftet med resan, t.ex. kundmöte, fotografering, leverans..."
value={formData.purpose}
onChange={(e) => handleChange('purpose', e.target.value)}
rows={2}
required
/>
</div>
{/* Deduction preview */}
{formData.distance_km > 0 && (
<div className="p-4 rounded-lg bg-success/10 border border-success/20">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Beräknat avdrag</p>
<p className="text-xs text-muted-foreground">
{formData.distance_km} km × {SCHABLONAVDRAG_RATES.bil.rate_per_km.toFixed(2)} kr/km
</p>
</div>
<span className="text-xl font-bold text-success">
{formatCurrency(deductionPreview)}
</span>
</div>
</div>
)}
{/* Submit button */}
<div className="flex justify-end gap-2">
{onCancel && (
<Button type="button" variant="outline" onClick={onCancel}>
Avbryt
</Button>
)}
<Button type="submit" disabled={isSaving}>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Sparar...
</>
) : (
'Spara körning'
)}
</Button>
</div>
</form>
</CardContent>
</Card>
)
}