'use client' import { useState, useMemo } from 'react' import { useTranslations } from 'next-intl' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import { Loader2, ChevronDown, ChevronRight } from 'lucide-react' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Separator } from '@/components/ui/separator' import { useToast } from '@/components/ui/use-toast' import { BankNameCombobox } from '@/components/settings/BankNameCombobox' import { validateBankgiroNumber, formatBankgiroNumber } from '@/lib/bankgiro/luhn' interface BankDetailsSetupDialogProps { open: boolean onOpenChange: (open: boolean) => void onComplete: () => void } export function BankDetailsSetupDialog({ open, onOpenChange, onComplete }: BankDetailsSetupDialogProps) { const { toast } = useToast() const t = useTranslations('invoice_bank_setup') const [isSaving, setIsSaving] = useState(false) const [showInternational, setShowInternational] = useState(false) const [bankName, setBankName] = useState('') const bankSetupSchema = useMemo( () => z .object({ bank_name: z.string().max(100).optional().or(z.literal('')), clearing_number: z .string() .regex(/^\d{4,5}$/, t('validation_clearing_format')) .optional() .or(z.literal('')), account_number: z .string() .regex(/^\d{6,12}$/, t('validation_account_format')) .optional() .or(z.literal('')), bankgiro: z.string().optional().or(z.literal('')), iban: z.string().optional().or(z.literal('')), bic: z.string().optional().or(z.literal('')), invoice_prefix: z.string().optional().or(z.literal('')), next_invoice_number: z.string().optional().or(z.literal('')), }) .refine( (data) => { const hasAccount = !!data.clearing_number && !!data.account_number const hasBankgiro = !!data.bankgiro return hasAccount || hasBankgiro }, { message: t('validation_either_required'), path: ['clearing_number'] }, ) .refine( (data) => { if (data.clearing_number && !data.account_number) return false if (!data.clearing_number && data.account_number) return false return true }, { message: t('validation_both_required'), path: ['account_number'] }, ) .refine( (data) => { if (!data.bankgiro) return true return validateBankgiroNumber(data.bankgiro) }, { message: t('validation_bankgiro_invalid'), path: ['bankgiro'] }, ) .refine( (data) => { if (!data.next_invoice_number) return true const num = parseInt(data.next_invoice_number, 10) return !isNaN(num) && num >= 1 }, { message: t('validation_start_number_positive'), path: ['next_invoice_number'] }, ), [t], ) type BankSetupData = z.infer const { register, handleSubmit, formState: { errors }, setValue, trigger, } = useForm({ resolver: zodResolver(bankSetupSchema), defaultValues: { bank_name: '', clearing_number: '', account_number: '', bankgiro: '', iban: '', bic: '', invoice_prefix: '', next_invoice_number: '', }, }) async function onSubmit(data: BankSetupData) { setIsSaving(true) // Format bankgiro if valid if (data.bankgiro) { data.bankgiro = formatBankgiroNumber(data.bankgiro) } // Include bank name from combobox data.bank_name = bankName // Omit empty strings — API schema accepts undefined but not null const payload: Record = {} for (const [key, val] of Object.entries(data)) { if (key === 'next_invoice_number') { const num = val ? parseInt(val as string, 10) : null if (num !== null) payload[key] = num } else { const str = val as string if (str) payload[key] = str } } try { const response = await fetch('/api/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) if (!response.ok) { const result = await response.json() throw new Error(result.error || t('save_failed_fallback')) } toast({ title: t('saved_title'), description: t('saved_description'), }) onComplete() } catch (error) { toast({ title: t('save_failed_title'), description: error instanceof Error ? error.message : t('save_failed_fallback'), variant: 'destructive', }) } finally { setIsSaving(false) } } // Flatten refine errors so they appear on the right fields const clearingError = errors.clearing_number?.message const accountError = errors.account_number?.message const bankgiroError = errors.bankgiro?.message return ( e.preventDefault()} > {t('title')} {t('description_prefix')} {t('settings_link')} {t('description_suffix')}
{/* Bank name */}
{/* Clearing + Account number */}
trigger(['clearing_number', 'account_number'])} />
trigger(['clearing_number', 'account_number'])} />
{clearingError && (

{clearingError}

)} {accountError && !clearingError && (

{accountError}

)} {/* Bankgiro */}
{ const val = e.target.value.trim() if (val && validateBankgiroNumber(val)) { setValue('bankgiro', formatBankgiroNumber(val)) } trigger('bankgiro') }} /> {bankgiroError && (

{bankgiroError}

)}
{/* International payments — collapsible */}
{showInternational && (
)}
{/* Invoice prefix + starting number */}

{t('prefix_hint')}

{errors.next_invoice_number && (

{errors.next_invoice_number.message}

)} {/* Actions */}
) }