'use client' import { useEffect, useMemo, useState } from 'react' import { useForm, Controller } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import { useTranslations } from 'next-intl' 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { ChevronDown, Loader2, Lock } from 'lucide-react' import { cn } from '@/lib/utils' import { useCanWrite } from '@/lib/hooks/use-can-write' import { useCompany } from '@/contexts/CompanyContext' import { createClient } from '@/lib/supabase/client' import AccountCombobox from '@/components/bookkeeping/AccountCombobox' import { AddAccountDialog } from '@/components/bookkeeping/AddAccountDialog' import type { BASAccount, CreateArticleInput } from '@/types' // Unit list mirrors the invoice line editor (app/(dashboard)/invoices/new/page.tsx). const UNITS = ['st', 'tim', 'dag', 'månad', 'km', 'kg'] as const // Legal Swedish VAT rates as integer percent. Matches vatRatePercent in // lib/api/schemas.ts (25 | 12 | 6 | 0). const VAT_RATES = [25, 12, 6, 0] as const interface ArticleFormProps { onSubmit: (data: CreateArticleInput) => Promise isLoading: boolean initialData?: Partial } export default function ArticleForm({ onSubmit, isLoading, initialData, }: ArticleFormProps) { const { canWrite } = useCanWrite() const { company } = useCompany() const supabase = createClient() const t = useTranslations('form_article') // Active class-3 (revenue) accounts for the combobox. The combobox accepts // unknown 4-digit numbers optimistically: the API answers with // ACCOUNTS_NOT_IN_CHART for activatable BAS accounts, and the host page's // ActivateAccountsDialog flow takes over (same UX as the journal entry form). const [revenueAccounts, setRevenueAccounts] = useState([]) // Inline account creation: what the user typed in the combobox when they hit // "Skapa konto": non-null opens AddAccountDialog prefilled with it. const [createAccountPrefill, setCreateAccountPrefill] = useState(null) // Momsregistrerad? A non-VAT-registered company never charges moms, so the // VAT field is hidden and the rate forced to 0: mirrors the invoice editor. const [vatRegistered, setVatRegistered] = useState(true) async function fetchRevenueAccounts() { try { const res = await fetch('/api/bookkeeping/accounts?class=3') const body = await res.json() setRevenueAccounts((body?.data as BASAccount[]) || []) } catch { // Non-fatal: the combobox degrades to free 4-digit entry. } } useEffect(() => { fetchRevenueAccounts() }, []) useEffect(() => { if (!company?.id) return let cancelled = false supabase .from('company_settings') .select('vat_registered') .eq('company_id', company.id) .single() .then(({ data }) => { if (!cancelled && typeof data?.vat_registered === 'boolean') { setVatRegistered(data.vat_registered) } }) return () => { cancelled = true } // eslint-disable-next-line react-hooks/exhaustive-deps }, [company?.id]) // Open the advanced section by default when it already holds data, so an // edit never hides a value the user previously set. const [advancedOpen, setAdvancedOpen] = useState( Boolean( initialData?.revenue_account || initialData?.cost_price != null || initialData?.ean || initialData?.housework_type || initialData?.notes, ), ) // UI-local schema mirroring CreateArticleSchema (lib/api/schemas.ts). const schema = useMemo( () => z.object({ name: z.string().min(1, t('name_required')), name_en: z.string().optional(), type: z.enum(['vara', 'tjanst']), unit: z.string().min(1), price_excl_vat: z.number({ message: t('price_required') }).nonnegative(t('price_required')), vat_rate: z.union([z.literal(25), z.literal(12), z.literal(6), z.literal(0)]), revenue_account: z.string().optional(), cost_price: z.number().nonnegative().optional(), ean: z.string().optional(), housework_type: z.string().optional(), notes: z.string().optional(), }), [t], ) type FormData = z.infer const { register, handleSubmit, watch, control, setValue, formState: { errors }, } = useForm({ resolver: zodResolver(schema), defaultValues: { name: initialData?.name || '', name_en: initialData?.name_en || '', type: initialData?.type || 'tjanst', unit: initialData?.unit || 'st', price_excl_vat: initialData?.price_excl_vat ?? 0, vat_rate: (initialData?.vat_rate as FormData['vat_rate']) ?? 25, revenue_account: initialData?.revenue_account || '', cost_price: initialData?.cost_price ?? undefined, ean: initialData?.ean || '', housework_type: initialData?.housework_type || '', notes: initialData?.notes || '', }, }) const type = watch('type') const onFormSubmit = (data: FormData) => { onSubmit({ name: data.name, name_en: data.name_en || null, type: data.type, unit: data.unit, price_excl_vat: data.price_excl_vat, vat_rate: vatRegistered ? data.vat_rate : 0, revenue_account: data.revenue_account || null, cost_price: data.cost_price ?? null, ean: data.ean || null, housework_type: type === 'tjanst' ? data.housework_type || null : null, notes: data.notes || null, }) } return (
{/* Type */}
( )} />
{/* Name */}
{errors.name && (

{errors.name.message}

)}
{/* English name */}

{t('name_en_hint')}

{/* Unit + price + VAT (moms hidden for non-momsregistrerade) */}
( )} />
{errors.price_excl_vat && (

{errors.price_excl_vat.message}

)}
{vatRegistered && (
( )} />
)}
{/* Advanced (collapsible) */}
{advancedOpen && (
{/* Revenue account */}
( setCreateAccountPrefill(prefill)} /> )} />

{t('revenue_account_hint')}

{/* Cost price */}
(v === '' || v == null ? undefined : Number(v)), })} />

{t('cost_price_hint')}

{/* EAN */}
{/* Housework type (tjänst only) */} {type === 'tjanst' && (
( )} />

{t('housework_hint')}

)} {/* Notes */}