'use client' import { useEffect, useMemo, useState } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { useToast } from '@/components/ui/use-toast' import { SettingsGroup, SettingsRow, SettingsRowEnd, SettingsRowNote, SettingsInput, } from '@/components/settings/SettingsRows' import { ACCOUNT_NUMBER_RE } from '@/lib/invariants/account-number' import { DEFAULT_PAYMENT_ACCOUNT } from '@/lib/webshop-orders/booking-lines' import type { WebshopPaymentMethodPolicy, WebshopPlatform, WebshopStoreSettings } from '@/types' interface PaymentMethodMappingFormProps { platform: WebshopPlatform storeScope: string /** * Payment methods observed on this store's orders. Omitted: derived from a * sample of the store's synced orders. */ methods?: Array<{ method: string; title: string | null }> } type DraftPolicy = { mode: 'book'; account: string } | { mode: 'invoice' } /** * Per-store payment-method -> account mapping (prefill only; never books). * Fönster language: flat hairline rows, save appears only when dirty. * "Faktureras" marks a method as invoice-flow: the booking dialog then nudges * toward Skapa faktura for those orders. */ export function PaymentMethodMappingForm({ platform, storeScope, methods, }: PaymentMethodMappingFormProps) { const t = useTranslations('webshop_orders') const { toast } = useToast() const [saved, setSaved] = useState>({}) const [draft, setDraft] = useState>({}) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [derivedMethods, setDerivedMethods] = useState< Array<{ method: string; title: string | null }> >([]) useEffect(() => { if (methods !== undefined) return let cancelled = false fetch( `/api/webshop-orders?platform=${platform}&store_scope=${encodeURIComponent(storeScope)}&limit=200`, ) .then((r) => (r.ok ? r.json() : { data: [] })) .then( (json: { data: Array<{ payment_method: string | null; payment_method_title: string | null }> }) => { if (cancelled) return const seen = new Map() for (const row of json.data ?? []) { if (row.payment_method && !seen.has(row.payment_method)) { seen.set(row.payment_method, row.payment_method_title) } } setDerivedMethods( Array.from(seen.entries()).map(([method, title]) => ({ method, title })), ) }, ) .catch(() => undefined) return () => { cancelled = true } }, [methods, platform, storeScope]) useEffect(() => { let cancelled = false fetch( `/api/webshop-orders/settings?platform=${platform}&store_scope=${encodeURIComponent(storeScope)}`, ) .then((r) => (r.ok ? r.json() : { data: [] })) .then((json: { data: WebshopStoreSettings[] }) => { if (cancelled) return const map = json.data[0]?.payment_method_account_map ?? {} setSaved(map) setDraft(map as Record) }) .catch(() => undefined) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, [platform, storeScope]) // Methods seen on orders plus any mapped-but-no-longer-seen leftovers. const rows = useMemo(() => { const source = methods ?? derivedMethods const known = new Map(source.map((m) => [m.method, m.title])) for (const method of Object.keys(saved)) { if (!known.has(method)) known.set(method, null) } return Array.from(known.entries()).map(([method, title]) => ({ method, title })) }, [methods, derivedMethods, saved]) const dirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(saved), [draft, saved]) const setMode = (method: string, mode: 'book' | 'invoice') => { setDraft((prev) => { const current = prev[method] if (mode === 'invoice') return { ...prev, [method]: { mode: 'invoice' } } return { ...prev, [method]: { mode: 'book', // Same constant the booking prefill falls back to: a hardcoded // number here would silently drift from it (it just did). account: current?.mode === 'book' ? current.account : DEFAULT_PAYMENT_ACCOUNT, }, } }) } const setAccount = (method: string, account: string) => { setDraft((prev) => ({ ...prev, [method]: { mode: 'book', account } })) } const invalid = Object.values(draft).some( (p) => p.mode === 'book' && !ACCOUNT_NUMBER_RE.test(p.account), ) async function save() { setSaving(true) try { const res = await fetch('/api/webshop-orders/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ platform, store_scope: storeScope, payment_method_account_map: draft, }), }) if (!res.ok) throw new Error(`save failed: ${res.status}`) const json = (await res.json()) as { data: WebshopStoreSettings } setSaved(json.data.payment_method_account_map) setDraft(json.data.payment_method_account_map as Record) toast({ title: t('mapping_saved') }) } catch { toast({ title: t('mapping_save_failed'), variant: 'destructive' }) } finally { setSaving(false) } } if (loading || rows.length === 0) return null return (

{t('mapping_intro')}

{rows.map(({ method, title }) => { const policy = draft[method] const mode = policy?.mode ?? 'book' const account = policy?.mode === 'book' ? policy.account : '' return ( {title || method}}> {policy?.mode === 'book' && ( setAccount(method, e.target.value.trim())} inputMode="numeric" maxLength={4} className="w-20 text-right tabular-nums" aria-label={t('mapping_account_aria', { method: title || method })} /> )} ) })}
{t('mapping_note')} {dirty && ( )}
) }