'use client' import { useState } from 'react' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { AlertTriangle, Loader2, Lock } from 'lucide-react' import { useToast } from '@/components/ui/use-toast' import { useCanWrite } from '@/lib/hooks/use-can-write' import { getErrorMessage } from '@/lib/errors/get-error-message' import type { Asset, AssetCategory, DepreciationMethod } from '@/types' /** The list route annotates each asset with whether any depreciation has been * posted against it. When true, the acquisition-basis fields are locked. */ type EditableAsset = Asset & { has_posted_depreciation?: boolean } interface EditAssetDialogProps { asset: EditableAsset open: boolean onOpenChange: (open: boolean) => void onSaved: () => void } // Same category labels and depreciation hints as CreateAssetDialog (kept in // sync deliberately — this register surface is Swedish-only, like creation). const CATEGORY_OPTIONS: { value: AssetCategory; label: string }[] = [ { value: 'computer', label: 'Dator / IT-utrustning' }, { value: 'equipment', label: 'Inventarier' }, { value: 'machinery', label: 'Maskiner' }, { value: 'vehicle', label: 'Fordon' }, { value: 'building', label: 'Byggnad' }, { value: 'land_improvement', label: 'Markanläggning' }, { value: 'immaterial', label: 'Immateriell tillgång' }, { value: 'other_tangible', label: 'Övrig materiell tillgång' }, ] const DEPRECIATION_METHOD_OPTIONS: { value: DepreciationMethod; label: string; hint: string }[] = [ { value: 'linear', label: 'Linjär', hint: 'Planenlig raklinje över nyttjandeperioden (ÅRL 4 kap 4§).', }, { value: 'declining_balance_30', label: 'Räkenskapsenlig 30 %', hint: 'Huvudregeln (IL 18 kap 13§) — 30 % degressivt på avskrivningsunderlaget.', }, { value: 'declining_balance_20', label: 'Räkenskapsenlig 20 %', hint: 'Kompletteringsregeln (IL 18 kap 17§) — 20 % degressivt. Vanlig för byggnader.', }, { value: 'restvardesavskrivning_25', label: 'Restvärdeavskrivning 25 %', hint: 'IL 18 kap 13§ st.3 — 25 % degressivt ner till angivet restvärde.', }, ] export function EditAssetDialog({ asset, open, onOpenChange, onSaved }: EditAssetDialogProps) { const { toast } = useToast() const { canWrite } = useCanWrite() // Once depreciation has been booked, acquisition date/cost/category are // locked — a real change has to go through storno. The server enforces the // same rule (ASSET_CORRECTION_BLOCKED); this just makes it visible up front. const basisLocked = asset.has_posted_depreciation === true const [name, setName] = useState(asset.name) const [category, setCategory] = useState(asset.category) const [acquisitionDate, setAcquisitionDate] = useState(asset.acquisition_date) const [acquisitionCost, setAcquisitionCost] = useState(String(asset.acquisition_cost)) const [usefulLifeYears, setUsefulLifeYears] = useState( String(Math.round(asset.useful_life_months / 12)), ) const [depreciationMethod, setDepreciationMethod] = useState( asset.depreciation_method, ) const [restvardeTarget, setRestvardeTarget] = useState( asset.restvarde_target != null ? String(asset.restvarde_target) : '', ) const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) const isRestvarde = depreciationMethod === 'restvardesavskrivning_25' const methodHint = DEPRECIATION_METHOD_OPTIONS.find((o) => o.value === depreciationMethod)?.hint ?? '' const handleSubmit = async () => { setError(null) const trimmedName = name.trim() if (!trimmedName) { setError('Namnet får inte vara tomt.') return } // Send only what actually changed. This keeps a name-only edit on a // depreciated asset from tripping the acquisition-basis guard, and avoids // clobbering a useful_life_months value that isn't a clean multiple of 12. const patch: Record = {} if (trimmedName !== asset.name) patch.name = trimmedName if (!basisLocked) { if (category !== asset.category) patch.category = category if (acquisitionDate !== asset.acquisition_date) patch.acquisition_date = acquisitionDate const cost = parseFloat(acquisitionCost) if (!Number.isFinite(cost) || cost <= 0) { setError('Anskaffningsvärdet måste vara större än 0.') return } if (cost !== Number(asset.acquisition_cost)) patch.acquisition_cost = cost } const years = parseInt(usefulLifeYears, 10) if (!Number.isFinite(years) || years <= 0) { setError('Ange en avskrivningstid (minst 1 år).') return } const months = years * 12 if (months !== asset.useful_life_months) patch.useful_life_months = months if (depreciationMethod !== asset.depreciation_method) { patch.depreciation_method = depreciationMethod } if (isRestvarde) { const target = parseFloat(restvardeTarget) const cost = !basisLocked ? parseFloat(acquisitionCost) : Number(asset.acquisition_cost) if (!Number.isFinite(target) || target < 0) { setError('Ange ett restvärde (0 kr eller högre).') return } if (Number.isFinite(cost) && target >= cost) { setError('Restvärdet måste vara lägre än anskaffningsvärdet.') return } // Send the target when switching into restvärde or when it changed, so // the method/target biconditional always holds. if ( depreciationMethod !== asset.depreciation_method || target !== Number(asset.restvarde_target) ) { patch.restvarde_target = target } } if (Object.keys(patch).length === 0) { toast({ title: 'Inga ändringar', description: 'Inget att spara.' }) onOpenChange(false) return } setSubmitting(true) try { const res = await fetch(`/api/assets/${asset.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch), }) const body = await res.json() if (!res.ok) { setError(getErrorMessage(body?.error ?? body) || 'Kunde inte spara ändringen.') return } toast({ title: 'Tillgång uppdaterad', description: trimmedName }) onSaved() } catch (err) { setError(getErrorMessage(err)) } finally { setSubmitting(false) } } return ( Ändra anläggningstillgång
setName(e.target.value)} autoFocus />
setAcquisitionDate(e.target.value)} disabled={basisLocked} className="tabular-nums" />
setAcquisitionCost(e.target.value)} disabled={basisLocked} className="tabular-nums" />
{basisLocked && (
Anskaffningsdatum, anskaffningsvärde och kategori är låsta eftersom avskrivningar redan har bokförts. Återför avskrivningen (storno) eller använd avyttring för att ändra grunduppgifterna. Namn, avskrivningstid och metod kan fortfarande justeras.
)}
setUsefulLifeYears(e.target.value)} className="tabular-nums" />

{methodHint}

{basisLocked && depreciationMethod !== asset.depreciation_method && (
Byte av avskrivningsmetod efter att avskrivning påbörjats. Enligt K2 (BFNAR 2016:10 p. 10.26) ska vald metod tillämpas konsekvent — ändra bara vid särskilda skäl och lämna i så fall upplysning i bokslutet. Ändringen gäller framåt; redan bokförda avskrivningar påverkas inte.
)} {isRestvarde && (
setRestvardeTarget(e.target.value)} placeholder="t.ex. 5000" className="tabular-nums" />

Avskrivningen stannar när bokfört värde når restvärdet. Restvärdet måste vara lägre än anskaffningsvärdet.

)} {error && (
{error}
)}
) }