diff --git a/app/(dashboard)/settings/bookkeeping/page.tsx b/app/(dashboard)/settings/bookkeeping/page.tsx index 165ac5eb..6abcdfd0 100644 --- a/app/(dashboard)/settings/bookkeeping/page.tsx +++ b/app/(dashboard)/settings/bookkeeping/page.tsx @@ -10,6 +10,8 @@ import { Label } from '@/components/ui/label' import { ExternalLink } from 'lucide-react' import type { CompanySettings } from '@/types' +const SERIES_OPTIONS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('') + export default function BookkeepingSettingsPage() { const { settings, isLoading, updateSettings } = useSettings() @@ -19,11 +21,13 @@ export default function BookkeepingSettingsPage() { const autoLockValue = formData.get('auto_lock_period_days') as string const lockedThrough = (formData.get('bookkeeping_locked_through') as string) || null const accountingMethod = (formData.get('accounting_method') as string) || 'accrual' + const defaultVoucherSeries = (formData.get('default_voucher_series') as string) || 'A' const updates: Record = { bookkeeping_locked_through: lockedThrough, auto_lock_period_days: autoLockValue === 'none' ? null : parseInt(autoLockValue), accounting_method: accountingMethod, + default_voucher_series: defaultVoucherSeries, } return { updates, @@ -60,15 +64,40 @@ export default function BookkeepingSettingsPage() { + {/* Default voucher series */} +
+
+

+ Standardserie för verifikationer +

+
+ + +

+ Vilken serie som förväljs vid manuell bokföring. Kan ändras per verifikation. +

+
+
+
+ {/* Period locking */}
- {/* Voucher series — read-only, no form submit needed */} + {/* Voucher series — read-only display */}
- +
{/* Cross-links */} diff --git a/components/bookkeeping/JournalEntryForm.tsx b/components/bookkeeping/JournalEntryForm.tsx index 0bb227e7..a0ec0227 100644 --- a/components/bookkeeping/JournalEntryForm.tsx +++ b/components/bookkeeping/JournalEntryForm.tsx @@ -72,6 +72,7 @@ export default function JournalEntryForm({ const [lines, setLines] = useState( initialLines ?? [{ ...BLANK_LINE }, { ...BLANK_LINE }] ) + const [voucherSeries, setVoucherSeries] = useState('A') const [isSubmitting, setIsSubmitting] = useState(false) const [showReview, setShowReview] = useState(false) const [showNoDocWarning, setShowNoDocWarning] = useState(false) @@ -109,6 +110,12 @@ export default function JournalEntryForm({ useEffect(() => { fetchPeriods() fetchAccounts() + // Fetch default voucher series from company settings + if (!embedded) { + fetch('/api/settings').then(r => r.json()).then(({ data }) => { + if (data?.default_voucher_series) setVoucherSeries(data.default_voucher_series) + }).catch(() => {/* keep 'A' */}) + } }, []) // Fetch exchange rate from Riksbanken when currency changes @@ -250,6 +257,7 @@ export default function JournalEntryForm({ description, source_type: sourceType ?? 'manual', source_id: sourceId, + voucher_series: voucherSeries || 'A', lines: entryLines, }), }) @@ -311,7 +319,13 @@ export default function JournalEntryForm({ const formContent = (
-
+
{ + const v = e.target.value.toUpperCase().replace(/[^A-Z]/g, '').slice(0, 1) + setVoucherSeries(v || 'A') + }} + className="mt-1 text-center font-mono" + maxLength={1} + /> +
+ )}
{/* Currency section */} @@ -644,6 +672,7 @@ export default function JournalEntryForm({ periodName={periods.find((p) => p.id === selectedPeriod)?.name || ''} entryDate={entryDate} description={description} + voucherSeries={!embedded ? voucherSeries : undefined} lines={lines} totalDebit={totalDebit} totalCredit={totalCredit} diff --git a/components/bookkeeping/JournalEntryReviewContent.tsx b/components/bookkeeping/JournalEntryReviewContent.tsx index 8361dc1b..3fb5997f 100644 --- a/components/bookkeeping/JournalEntryReviewContent.tsx +++ b/components/bookkeeping/JournalEntryReviewContent.tsx @@ -15,6 +15,7 @@ interface JournalEntryReviewContentProps { periodName: string entryDate: string description: string + voucherSeries?: string lines: ReviewLine[] totalDebit: number totalCredit: number @@ -31,6 +32,7 @@ export function JournalEntryReviewContent({ periodName, entryDate, description, + voucherSeries, lines, totalDebit, totalCredit, @@ -46,7 +48,7 @@ export function JournalEntryReviewContent({
{/* Header info */}
-
+
Räkenskapsår

{periodName}

@@ -57,6 +59,12 @@ export function JournalEntryReviewContent({

{entryDate}

)} + {voucherSeries && ( +
+ Serie +

{voucherSeries}

+
+ )}
Beskrivning diff --git a/components/settings/VoucherSeriesManager.tsx b/components/settings/VoucherSeriesManager.tsx index 700347a7..24bf2398 100644 --- a/components/settings/VoucherSeriesManager.tsx +++ b/components/settings/VoucherSeriesManager.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react' import { createClient } from '@/lib/supabase/client' import { useCompany } from '@/contexts/CompanyContext' import { Label } from '@/components/ui/label' +import { Badge } from '@/components/ui/badge' interface VoucherSeries { voucher_series: string @@ -11,7 +12,11 @@ interface VoucherSeries { fiscal_period_id: string } -export function VoucherSeriesManager() { +interface VoucherSeriesManagerProps { + defaultSeries?: string +} + +export function VoucherSeriesManager({ defaultSeries }: VoucherSeriesManagerProps) { const { company } = useCompany() const [series, setSeries] = useState([]) const [isLoading, setIsLoading] = useState(true) @@ -52,7 +57,7 @@ export function VoucherSeriesManager() {
) : seriesEntries.length === 0 ? (

- Inga verifikationsserier ännu. Serie A skapas automatiskt vid första verifikationen. + Inga verifikationsserier ännu. Serie {defaultSeries || 'A'} skapas automatiskt vid första verifikationen.

) : (
@@ -60,7 +65,12 @@ export function VoucherSeriesManager() {
{seriesEntries.map(([letter, lastNum]) => (
- Serie {letter} +
+ Serie {letter} + {letter === (defaultSeries || 'A') && ( + standard + )} +
Senaste nr: {lastNum} @@ -69,6 +79,10 @@ export function VoucherSeriesManager() {
)} + +

+ Nya serier skapas automatiskt första gången de används vid bokföring. +

) } diff --git a/lib/api/schemas.ts b/lib/api/schemas.ts index af0ce6f2..5138c561 100644 --- a/lib/api/schemas.ts +++ b/lib/api/schemas.ts @@ -305,7 +305,7 @@ export const CreateJournalEntrySchema = z.object({ description: z.string().min(1, 'Description is required'), source_type: JournalEntrySourceTypeSchema.default('manual'), source_id: z.string().optional(), - voucher_series: z.string().optional(), + voucher_series: z.string().regex(/^[A-Z]$/, 'Verifikationsserie måste vara en bokstav A–Z').optional(), lines: z.array(CreateJournalEntryLineSchema).min(2, 'At least two lines are required for double-entry'), }) @@ -384,6 +384,8 @@ export const UpdateSettingsSchema = z.object({ // Bookkeeping lock bookkeeping_locked_through: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Ogiltigt datumformat (YYYY-MM-DD)').nullable().optional(), auto_lock_period_days: z.number().int().positive().nullable().optional(), + // Voucher series + default_voucher_series: z.string().regex(/^[A-Z]$/, 'Verifikationsserie måste vara en bokstav A–Z').optional(), // Invoice PDF settings ore_rounding: z.boolean().optional(), invoice_show_ocr: z.boolean().optional(), @@ -584,7 +586,7 @@ export const PendingOperationsQuerySchema = z.object({ export const VoucherGapQuerySchema = z.object({ fiscal_period_id: uuid, - voucher_series: z.string().optional(), + voucher_series: z.string().regex(/^[A-Z]$/, 'Verifikationsserie måste vara en bokstav A–Z').optional(), }) export const SaveGapExplanationSchema = z.object({ diff --git a/supabase/migrations/20260412120000_default_voucher_series.sql b/supabase/migrations/20260412120000_default_voucher_series.sql new file mode 100644 index 00000000..85c00454 --- /dev/null +++ b/supabase/migrations/20260412120000_default_voucher_series.sql @@ -0,0 +1,11 @@ +-- Add default voucher series to company_settings +-- Allows companies to configure which series (A-Z) is pre-selected +-- when creating manual journal entries. Defaults to 'A'. + +ALTER TABLE public.company_settings + ADD COLUMN IF NOT EXISTS default_voucher_series text NOT NULL DEFAULT 'A'; + +-- Enforce single uppercase letter +ALTER TABLE public.company_settings + ADD CONSTRAINT company_settings_default_voucher_series_check + CHECK (default_voucher_series ~ '^[A-Z]$'); diff --git a/tests/helpers.ts b/tests/helpers.ts index 7fe25d1e..b891e59f 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -520,6 +520,7 @@ export function makeCompanySettings( invoice_default_notes: null, bookkeeping_locked_through: null, auto_lock_period_days: null, + default_voucher_series: 'A', ore_rounding: true, invoice_show_ocr: true, invoice_show_bankgiro: true, diff --git a/types/index.ts b/types/index.ts index 4a97c951..d08a3c2a 100644 --- a/types/index.ts +++ b/types/index.ts @@ -191,6 +191,9 @@ export interface CompanySettings { bookkeeping_locked_through: string | null auto_lock_period_days: number | null + // Voucher series + default_voucher_series: string + // Invoice PDF settings ore_rounding: boolean invoice_show_ocr: boolean