wip: transaction form and list updates (pre-theme-fixes snapshot)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-04-23 12:44:14 +00:00
parent 6111424f47
commit da59fa9f23
2 changed files with 60 additions and 6 deletions

View file

@ -2,7 +2,7 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { format } from "date-fns";
import { X, Loader2, Sparkles } from "lucide-react";
import { X, Loader2, Sparkles, RotateCcw } from "lucide-react";
import type { Account } from "@/api/accounts";
const schema = z.object({
@ -40,9 +40,11 @@ interface Props {
initialValues?: TransactionInitialValues;
parsedFromReceipt?: boolean;
showAiDebug?: boolean;
onRescan?: () => void;
rescanLoading?: boolean;
}
export default function TransactionFormModal({ accounts, categories, onClose, onSubmit, isLoading, initialValues, parsedFromReceipt, showAiDebug }: Props) {
export default function TransactionFormModal({ accounts, categories, onClose, onSubmit, isLoading, initialValues, parsedFromReceipt, showAiDebug, onRescan, rescanLoading }: Props) {
const { register, handleSubmit, watch, formState: { errors } } = useForm<Form>({
resolver: zodResolver(schema),
defaultValues: {
@ -89,14 +91,40 @@ export default function TransactionFormModal({ accounts, categories, onClose, on
{parsedFromReceipt && !showAiDebug && (
<div className="flex items-center gap-2 bg-primary/10 border border-primary/20 rounded-lg px-3 py-2 text-xs text-primary">
<Sparkles className="w-3.5 h-3.5 shrink-0" />
Fields pre-filled from receipt review before saving
<span className="flex-1">Fields pre-filled from receipt review before saving</span>
{onRescan && (
<button
type="button"
onClick={onRescan}
disabled={rescanLoading || isLoading}
className="flex items-center gap-1 text-primary/70 hover:text-primary disabled:opacity-40 transition-colors shrink-0"
title="Rescan the same receipt"
>
{rescanLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <RotateCcw className="w-3.5 h-3.5" />}
Rescan
</button>
)}
</div>
)}
{parsedFromReceipt && showAiDebug && (
<div className="bg-primary/5 border border-primary/20 rounded-lg p-3 space-y-2">
<p className="text-xs font-semibold text-primary flex items-center gap-1.5">
<Sparkles className="w-3.5 h-3.5" /> AI scan result review before saving
</p>
<div className="flex items-center justify-between">
<p className="text-xs font-semibold text-primary flex items-center gap-1.5">
<Sparkles className="w-3.5 h-3.5" /> AI scan result review before saving
</p>
{onRescan && (
<button
type="button"
onClick={onRescan}
disabled={rescanLoading || isLoading}
className="flex items-center gap-1 text-xs text-primary/70 hover:text-primary disabled:opacity-40 transition-colors"
title="Rescan the same receipt"
>
{rescanLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <RotateCcw className="w-3.5 h-3.5" />}
Rescan
</button>
)}
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
<span className="text-muted-foreground">Merchant</span><span className="font-medium truncate">{initialValues?.merchant ?? <span className="text-destructive/70 italic">not detected</span>}</span>
<span className="text-muted-foreground">Amount</span><span className="font-medium">{initialValues?.amount != null ? initialValues.amount : <span className="text-destructive/70 italic">not detected</span>}</span>

View file

@ -55,6 +55,8 @@ export default function TransactionList() {
const receiptFileRef = useRef<File | null>(null);
const [scanError, setScanError] = useState<string | null>(null);
const [scanning, setScanning] = useState(false);
const [rescanLoading, setRescanLoading] = useState(false);
const [rescanKey, setRescanKey] = useState(0);
const receiptInputRef = useRef<HTMLInputElement>(null);
const [search, setSearch] = useState("");
const [filterType, setFilterType] = useState("");
@ -113,6 +115,27 @@ export default function TransactionList() {
}
}
async function handleRescan() {
if (!receiptFileRef.current) return;
setRescanLoading(true);
setScanError(null);
try {
const parsed = await parseReceiptFile(receiptFileRef.current);
setReceiptParsed(parsed);
setRescanKey((k) => k + 1);
} catch (e: any) {
const detail = e?.response?.data?.detail;
const status = e?.response?.status;
if (detail) {
setScanError(typeof detail === "string" ? detail : `HTTP ${status}: ${JSON.stringify(detail)}`);
} else {
setScanError(`Rescan failed — ${e?.message ?? "unknown error"}`);
}
} finally {
setRescanLoading(false);
}
}
async function handleReceiptFile(file: File) {
setScanning(true);
setScanError(null);
@ -368,6 +391,7 @@ export default function TransactionList() {
{showForm && (
<TransactionFormModal
key={rescanKey}
accounts={accounts}
categories={categories}
onClose={() => { setShowForm(false); setReceiptParsed(null); receiptFileRef.current = null; }}
@ -384,6 +408,8 @@ export default function TransactionList() {
} : undefined}
parsedFromReceipt={!!receiptParsed}
showAiDebug={aiSettings?.debug ?? false}
onRescan={handleRescan}
rescanLoading={rescanLoading}
/>
)}