diff --git a/components/settings/VoucherSeriesPerSourceTypeForm.tsx b/components/settings/VoucherSeriesPerSourceTypeForm.tsx index 9d3c625f..c2cfba8b 100644 --- a/components/settings/VoucherSeriesPerSourceTypeForm.tsx +++ b/components/settings/VoucherSeriesPerSourceTypeForm.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { useToast } from '@/components/ui/use-toast' @@ -13,9 +13,10 @@ import { } from '@/components/settings/SettingsRows' import { cn } from '@/lib/utils' import { getErrorMessage } from '@/lib/errors/get-error-message' +import { VOUCHER_SERIES_PRESETS } from '@/lib/bookkeeping/voucher-series-resolver' import type { CompanySettings, JournalEntrySourceType } from '@/types' -const SERIES_OPTIONS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('') +const SERIES_LETTER_RE = /^[A-Z]$/ // Subset of source_types presented to the user. The DB column accepts every // JournalEntrySourceType, but several values (storno, correction, etc.) are @@ -82,6 +83,29 @@ export function VoucherSeriesPerSourceTypeForm({ settings, onSettingsUpdated }: const [isSaving, setIsSaving] = useState(false) const [showAll, setShowAll] = useState(false) + // Same closed list as the manual verifikat form and the per-bankkonto + // picker: the fixed Swedish presets, then any letter already configured in + // settings that the presets do not cover. The saved map is read alongside + // the draft so a non-preset letter stays selectable after the user changes + // that row away from it (otherwise a misclick could not be undone without + // a reload) and so a letter that lands in settings after mount is offered. + // A free A-Z list would let a typo start an undocumented series. + const seriesOptions = useMemo(() => { + const preset = new Set(VOUCHER_SERIES_PRESETS.map((p) => p.letter)) + const extras = [ + settings.default_voucher_series, + ...Object.values(settings.default_voucher_series_per_source_type ?? {}), + ...Object.values(draft), + ].filter( + (v): v is string => typeof v === 'string' && SERIES_LETTER_RE.test(v) && !preset.has(v), + ) + const uniqueExtras = Array.from(new Set(extras)).sort() + return [ + ...VOUCHER_SERIES_PRESETS, + ...uniqueExtras.map((letter) => ({ letter, label: '' })), + ] + }, [settings.default_voucher_series, settings.default_voucher_series_per_source_type, draft]) + const handleChange = (sourceType: JournalEntrySourceType, value: string) => { setDraft((prev) => ({ ...prev, [sourceType]: value })) } @@ -142,9 +166,9 @@ export function VoucherSeriesPerSourceTypeForm({ settings, onSettingsUpdated }: onChange={(e) => handleChange(key, e.target.value)} className="font-mono" > - {SERIES_OPTIONS.map((letter) => ( - ))} @@ -157,7 +181,7 @@ export function VoucherSeriesPerSourceTypeForm({ settings, onSettingsUpdated }: return ( {alwaysVisible.map((entry, i) => // The last always-visible row sits right above the fold toggle: