b5df2fb292
* fix: consolidate commit_journal_entry to single 4-arg signature Replaces the phantom-overload drop migration with an idempotent consolidation that leaves only the 4-arg-with-defaults signature, callable with either 2 or 4 named args. Fixes the "Could not choose the best candidate function" ambiguity caused when the commit-metadata migration CREATE OR REPLACE'd a 4-arg version alongside the existing 2-arg one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: preserve SIE source voucher identity on journal entries Adds source_voucher_series / source_voucher_number columns to journal_entries so per-verifikat traceability survives the importer's skip-empty-voucher logic. The SIE importer populates the original series/number even when skipped vouchers cause gnubok's target numbering to drift from the source file's sequence. Required for BFNAR 2013:2 kap 8 behandlingshistorik. - Migration adds columns + partial index + extends immutability trigger - importVouchers() records rawSeries/rawNumber per voucher - JournalEntry type + test fixtures gain the new fields - Bookkeeping detail page surfaces "Ursprungligt verifikat" when present Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: polish invoice-inbox workspace for production use - Bedrock image fit: shrink images > 5 MB via sharp before Bedrock upload so HEIC/high-res phone photos don't fail with the 5 MB cap - Swedish error mapping: toSwedishInboxError translates Bedrock / infrastructure errors to Swedish sentences stored in error_message - History timeline endpoint (GET /items/:id/history) returns the processing_history events correlated to the inbox item - Workspace UI: inline diagnostic timeline inside the convert dialog, same-email row grouping ("+N dokument" chip), inferred-VAT affordance with "needs review" signalling, Riksbanken exchange-rate prefill for foreign-currency invoices so the supplier-invoice create path populates *_sek audit columns Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: extend inbox-smart-match to supplier invoices Both receipts and supplier invoices expose structurally identical match anchors (date, amount, currency, counterparty name) so the matcher can reuse the same narrowing + LLM prompt. Adds getMatchAnchors() as a shared extractor across ReceiptExtractionResult / InvoiceExtractionResult, and updates the event handlers to process supplier_invoice items alongside receipts. LLM prompt re-phrased as "dokument" rather than "kvitto" and loosened the date-window heuristic since invoice payments can lag behind the invoice date by weeks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: drop unused category selector from TransactionForm The manual "Lägg till transaktion" dialog predates the current categorization flow (SwipeCategorizationView, BatchCategorySelector, AI suggestions). The category dropdown here never drove journal-entry creation — onSubmit fanned it out to CreateTransactionInput.category, which is optional. Removes the dropdown, the unused watch() hook, and the categories lookup table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(migrations): restore drop-phantom file and rebump timestamps Supabase branch DB failed with PK violation on schema_migrations because my two migrations collided with timestamps already on main: 20260421120000 → journal_entries_with_related_rpc (PR #298) 20260421130000 → drop_legacy_supplier_invoice_user_id_uniqueness (PR #296) Rebumped to 20260421140000 and 20260421150000 so each migration has a unique version (Supabase uses only the 14-digit prefix as the PK). Also restored the 20260420130000_drop_phantom_commit_journal_entry_overload migration I had deleted — CLAUDE.md rule #5 forbids modifying existing migrations. My consolidate migration is still compatible: drop_phantom drops the 4-arg overload (no-op where absent), then consolidate recreates it with defaults. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(inbox-smart-match): anchor invoices on dueDate with wider window The original ±7d window around invoiceDate filtered out all real payments for invoices with standard 30–60 day terms — the matcher would see zero candidates before the LLM was called, making the supplier-invoice matcher effectively dead. New anchor selection: - Receipts: receipt date ±7 days (unchanged; paid on the spot) - Invoices with dueDate: dueDate ±14 days (covers early/late payments) - Invoices without dueDate: invoiceDate -7/+45 days (covers 30-day terms) MatchAnchors now carries windowDaysBefore/After so the window can vary per document shape. Added three getMatchAnchors tests asserting window sizes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
4.4 KiB
TypeScript
150 lines
4.4 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect } from 'react'
|
|
import { useForm, Controller } from 'react-hook-form'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { z } from 'zod'
|
|
import { format } from 'date-fns'
|
|
import { Button } from '@/components/ui/button'
|
|
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 { Loader2 } from 'lucide-react'
|
|
import type { CreateTransactionInput, Currency } from '@/types'
|
|
|
|
const schema = z.object({
|
|
date: z.string().min(1, 'Datum krävs'),
|
|
description: z.string().min(1, 'Beskrivning krävs'),
|
|
amount: z.number().refine((n) => n !== 0, 'Belopp måste anges'),
|
|
currency: z.enum(['SEK', 'EUR', 'USD', 'GBP', 'NOK', 'DKK']),
|
|
notes: z.string().optional(),
|
|
})
|
|
|
|
type FormData = z.infer<typeof schema>
|
|
|
|
interface TransactionFormProps {
|
|
onSubmit: (data: CreateTransactionInput) => Promise<void>
|
|
isLoading: boolean
|
|
}
|
|
|
|
const currencies: Currency[] = ['SEK', 'EUR', 'USD', 'GBP', 'NOK', 'DKK']
|
|
|
|
export default function TransactionForm({ onSubmit, isLoading }: TransactionFormProps) {
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
control,
|
|
setValue,
|
|
formState: { errors },
|
|
} = useForm<FormData>({
|
|
resolver: zodResolver(schema),
|
|
defaultValues: {
|
|
date: '',
|
|
description: '',
|
|
amount: 0,
|
|
currency: 'SEK',
|
|
notes: '',
|
|
},
|
|
})
|
|
|
|
// Set date default on client only to avoid hydration mismatch
|
|
useEffect(() => {
|
|
setValue('date', format(new Date(), 'yyyy-MM-dd'))
|
|
}, [])
|
|
|
|
const onFormSubmit = (data: FormData) => {
|
|
onSubmit({
|
|
date: data.date,
|
|
description: data.description,
|
|
amount: data.amount,
|
|
currency: data.currency,
|
|
notes: data.notes,
|
|
})
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-4">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="date">Datum *</Label>
|
|
<Input id="date" type="date" {...register('date')} />
|
|
{errors.date && (
|
|
<p className="text-sm text-destructive">{errors.date.message}</p>
|
|
)}
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="currency">Valuta</Label>
|
|
<Controller
|
|
name="currency"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{currencies.map((currency) => (
|
|
<SelectItem key={currency} value={currency}>
|
|
{currency}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="description">Beskrivning *</Label>
|
|
<Input
|
|
id="description"
|
|
placeholder="T.ex. Adobe Creative Cloud"
|
|
{...register('description')}
|
|
/>
|
|
{errors.description && (
|
|
<p className="text-sm text-destructive">{errors.description.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="amount">Belopp * (negativt för utgift)</Label>
|
|
<Input
|
|
id="amount"
|
|
type="number"
|
|
step="0.01"
|
|
placeholder="-500"
|
|
{...register('amount', { valueAsNumber: true })}
|
|
/>
|
|
{errors.amount && (
|
|
<p className="text-sm text-destructive">{errors.amount.message}</p>
|
|
)}
|
|
<p className="text-xs text-muted-foreground">
|
|
Ange positivt belopp för intäkter, negativt för kostnader
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="notes">Anteckningar</Label>
|
|
<Textarea
|
|
id="notes"
|
|
placeholder="Valfria anteckningar..."
|
|
{...register('notes')}
|
|
/>
|
|
</div>
|
|
|
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Sparar...
|
|
</>
|
|
) : (
|
|
'Spara transaktion'
|
|
)}
|
|
</Button>
|
|
</form>
|
|
)
|
|
}
|