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:
parent
6111424f47
commit
da59fa9f23
2 changed files with 60 additions and 6 deletions
|
|
@ -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">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue