'use client' import { useState, useEffect, useMemo } from 'react' import { useTranslations } from 'next-intl' import { createClient } from '@/lib/supabase/client' import { useForm, useFieldArray, Controller } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' 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 { useToast } from '@/components/ui/use-toast' import { useCompany } from '@/contexts/CompanyContext' import { Plus, Trash2 } from 'lucide-react' import type { Customer, Currency } from '@/types' import { formatCurrency } from '@/lib/utils' const currencies: Currency[] = ['SEK', 'EUR', 'USD', 'GBP', 'NOK', 'DKK'] const units = ['st', 'tim', 'dag', 'månad', 'km', 'kg'] interface Props { open: boolean onOpenChange: (open: boolean) => void /** Fired after a successful create. Hosts close the dialog and refresh their list. */ onCreated: () => void } /** * "Nytt schema" as a modal: mirrors NewInvoiceDialog now that regular * invoice creation opens in one. Card sections carry over from the old * /invoices/recurring/new page. */ export default function NewRecurringScheduleDialog({ open, onOpenChange, onCreated }: Props) { const t = useTranslations('invoice_recurring_new') return ( e.preventDefault()} onPointerDownOutside={(e) => e.preventDefault()} onInteractOutside={(e) => e.preventDefault()} > {t('title')} onOpenChange(false)} /> ) } // Inner component so form state resets whenever the dialog reopens (Radix // unmounts DialogContent children on close). function NewRecurringScheduleForm({ onCreated, onCancel }: { onCreated: () => void; onCancel: () => void }) { const { toast } = useToast() const { company } = useCompany() const supabase = createClient() const t = useTranslations('invoice_recurring_new') const [customers, setCustomers] = useState([]) const [isSubmitting, setIsSubmitting] = useState(false) const schema = useMemo(() => { const itemSchema = z.object({ description: z.string().min(1, t('validation_description_required')), quantity: z.number().min(0.01, t('validation_quantity_min')), unit: z.string().min(1, t('validation_unit_required')), unit_price: z.number(), vat_rate: z .union([z.literal(0), z.literal(6), z.literal(12), z.literal(25)]) .nullable() .optional(), }) return z.object({ customer_id: z.string().uuid(t('validation_customer_required')), name: z.string().min(1, t('validation_name_required')), day_of_month: z.number().int().min(1).max(31), payment_terms_days: z.number().int().min(0).max(90), currency: z.enum(['SEK', 'EUR', 'USD', 'GBP', 'NOK', 'DKK']), auto_send: z.boolean(), your_reference: z.string().optional(), our_reference: z.string().optional(), notes: z.string().optional(), items: z.array(itemSchema).min(1, t('validation_min_one_row')), }) }, [t]) type FormData = z.infer const { register, control, handleSubmit, watch, formState: { errors }, } = useForm({ resolver: zodResolver(schema), defaultValues: { customer_id: '', name: '', day_of_month: 15, payment_terms_days: 30, currency: 'SEK', auto_send: false, items: [{ description: '', quantity: 1, unit: 'st', unit_price: 0, vat_rate: 25 }], }, }) const { fields, append, remove } = useFieldArray({ control, name: 'items' }) useEffect(() => { if (!company) return supabase .from('customers') .select('*') .eq('company_id', company.id) .order('name') .then(({ data }) => setCustomers(data ?? [])) }, [company]) async function onSubmit(data: FormData) { setIsSubmitting(true) try { const res = await fetch('/api/invoices/recurring', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!res.ok) { const body = await res.json().catch(() => ({})) throw new Error(body.error || t('create_failed_fallback')) } toast({ title: t('created_title') }) onCreated() } catch (err) { toast({ title: t('create_failed_title'), description: err instanceof Error ? err.message : undefined, variant: 'destructive', }) } finally { setIsSubmitting(false) } } const items = watch('items') const watchCurrency = watch('currency') const subtotalRaw = items.reduce( (sum, it) => sum + (it.quantity || 0) * (it.unit_price || 0), 0, ) // Round to öre using the project monetary rule, then format. const subtotal = Math.round(subtotalRaw * 100) / 100 return (
{t('schedule_card_title')}
{errors.name && (

{errors.name.message}

)}
( )} /> {errors.customer_id && (

{errors.customer_id.message}

)}

{t('day_hint')}

( )} />
( field.onChange(e.target.checked)} className="mt-1 h-4 w-4" /> )} />

{t('auto_send_description')}

{t('items_card_title')} {fields.map((field, index) => (
( )} />
))}
{t('subtotal_ex_vat', { amount: formatCurrency(subtotal, watchCurrency) })}
{t('other_card_title')}