Commit 57d7b4b7 authored by ThinhNC's avatar ThinhNC

fix(guest): fix receipt extraction crash and add defensive normalization in ReceiptScannerView

parent 941d4805
...@@ -1528,11 +1528,28 @@ export function handleGuestRequest(config: InternalAxiosRequestConfig): AxiosRes ...@@ -1528,11 +1528,28 @@ export function handleGuestRequest(config: InternalAxiosRequestConfig): AxiosRes
responseData = { responseData = {
success: true, success: true,
data: { data: {
merchant: "Siêu thị Co.opmart",
merchantName: "Siêu thị Co.opmart", merchantName: "Siêu thị Co.opmart",
totalAmount: "250000", totalAmount: "250000",
currency: "VND",
taxAmount: "20000",
transactionDate: todayStr, transactionDate: todayStr,
category: {
id: "cat-expense-1",
name: "Ăn uống",
icon: "utensils",
color: "#EF4444",
},
lineItems: [ lineItems: [
{ name: "Sữa chua hoa quả", quantity: 2, unitPrice: "35000", totalAmount: "70000" }, { name: "Sữa chua hoa quả Co.op", quantity: 2, unitPrice: "35000", totalAmount: "70000" },
{ name: "Bánh mì sandwich bơ", quantity: 1, unitPrice: "45000", totalAmount: "45000" },
{ name: "Nước ép cam ép tươi 1L", quantity: 2, unitPrice: "55000", totalAmount: "110000" },
{ name: "Khăn giấy ướt cao cấp", quantity: 1, unitPrice: "25000", totalAmount: "25000" },
],
rawText: `SIEU THI CO.OPMART\nNgay: ${todayStr}\n1. Sua chua hoa qua: 70.000\n2. Banh mi sandwich: 45.000\n3. Nuoc ep cam: 110.000\n4. Khan giay: 25.000\nTong tien: 250.000 VND\nVAT 8%: 20.000 VND`,
confidence: 0.96,
warnings: [
"Hóa đơn mẫu mô phỏng trong Chế độ Dùng thử (100% xử lý tại máy, không tốn token AI thật).",
], ],
}, },
}; };
......
...@@ -65,7 +65,20 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -65,7 +65,20 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
{ {
onSuccess: (res) => { onSuccess: (res) => {
if (res.success && res.data) { if (res.success && res.data) {
setExtractedData(res.data); const raw = res.data as any;
const normalizedData: ExtractReceiptData = {
merchant: raw.merchant || raw.merchantName || null,
transactionDate: raw.transactionDate || null,
totalAmount: raw.totalAmount || null,
currency: raw.currency || "VND",
taxAmount: raw.taxAmount || null,
category: raw.category || null,
lineItems: Array.isArray(raw.lineItems) ? raw.lineItems : [],
rawText: raw.rawText || "",
confidence: typeof raw.confidence === "number" ? raw.confidence : 0.95,
warnings: Array.isArray(raw.warnings) ? raw.warnings : [],
};
setExtractedData(normalizedData);
} }
}, },
} }
...@@ -98,19 +111,22 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -98,19 +111,22 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
const handleConfirmTransaction = () => { const handleConfirmTransaction = () => {
if (!extractedData) return; if (!extractedData) return;
const lineItemsCount = Array.isArray(extractedData.lineItems) ? extractedData.lineItems.length : 0;
const merchantName = extractedData.merchant || (extractedData as any).merchantName || "";
const prefillData = { const prefillData = {
amount: extractedData.totalAmount || "", amount: extractedData.totalAmount || "",
type: "EXPENSE" as const, type: "EXPENSE" as const,
categoryId: extractedData.category?.id || "", categoryId: extractedData.category?.id || "",
date: extractedData.transactionDate || "", date: extractedData.transactionDate || "",
description: extractedData.merchant description: merchantName
? `${extractedData.merchant}${ ? `${merchantName}${
extractedData.lineItems.length > 0 lineItemsCount > 0
? ` (${t("ai.ocr.itemCount", { count: extractedData.lineItems.length })})` ? ` (${t("ai.ocr.itemCount", { count: lineItemsCount })})`
: "" : ""
}` }`
: "", : "",
location: extractedData.merchant || "", location: merchantName,
}; };
onTransactionPrefill(prefillData, selectedFile); onTransactionPrefill(prefillData, selectedFile);
...@@ -125,10 +141,11 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -125,10 +141,11 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
if (fileInputRef.current) fileInputRef.current.value = ""; if (fileInputRef.current) fileInputRef.current.value = "";
}; };
const confidence = typeof extractedData?.confidence === "number" ? extractedData.confidence : 0.95;
const confidenceClassName = extractedData const confidenceClassName = extractedData
? extractedData.confidence >= 0.8 ? confidence >= 0.8
? "border-clay-income/30 bg-clay-income/20 text-clay-income" ? "border-clay-income/30 bg-clay-income/20 text-clay-income"
: extractedData.confidence >= 0.5 : confidence >= 0.5
? "border-clay-warning/30 bg-clay-warning/20 text-clay-warning" ? "border-clay-warning/30 bg-clay-warning/20 text-clay-warning"
: "border-clay-expense/30 bg-clay-expense/20 text-clay-expense" : "border-clay-expense/30 bg-clay-expense/20 text-clay-expense"
: ""; : "";
...@@ -263,7 +280,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -263,7 +280,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
{t("ai.ocr.resultTitle")} {t("ai.ocr.resultTitle")}
</h4> </h4>
<span className={`shrink-0 rounded-full border px-3 py-1 font-baloo text-xs font-bold shadow-clay-pressed ${confidenceClassName}`}> <span className={`shrink-0 rounded-full border px-3 py-1 font-baloo text-xs font-bold shadow-clay-pressed ${confidenceClassName}`}>
{t("ai.ocr.confidence")}: {formatNumber(extractedData.confidence, { {t("ai.ocr.confidence")}: {formatNumber(confidence, {
style: "percent", style: "percent",
maximumFractionDigits: 0, maximumFractionDigits: 0,
})} })}
...@@ -276,7 +293,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -276,7 +293,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
{t("ai.ocr.merchant")} {t("ai.ocr.merchant")}
</span> </span>
<span className="break-words font-nunito text-sm font-bold text-clay-text"> <span className="break-words font-nunito text-sm font-bold text-clay-text">
{extractedData.merchant || "—"} {extractedData.merchant || (extractedData as any).merchantName || "—"}
</span> </span>
</div> </div>
...@@ -318,7 +335,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -318,7 +335,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
</div> </div>
{/* Line Items Table */} {/* Line Items Table */}
{extractedData.lineItems.length > 0 && ( {Array.isArray(extractedData.lineItems) && extractedData.lineItems.length > 0 && (
<div className="mt-2 border-t border-clay-highlight/40 pt-3"> <div className="mt-2 border-t border-clay-highlight/40 pt-3">
<h5 className="mb-2 font-nunito text-xs font-bold text-clay-text"> <h5 className="mb-2 font-nunito text-xs font-bold text-clay-text">
{t("ai.ocr.lineItems")} ({extractedData.lineItems.length}) {t("ai.ocr.lineItems")} ({extractedData.lineItems.length})
...@@ -368,7 +385,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa ...@@ -368,7 +385,7 @@ export const ReceiptScannerView: React.FC<ReceiptScannerViewProps> = ({ onTransa
)} )}
{/* Warnings */} {/* Warnings */}
{extractedData.warnings.length > 0 && ( {Array.isArray(extractedData.warnings) && extractedData.warnings.length > 0 && (
<div className="mt-4 rounded-clay-sm bg-clay-warning/10 p-3 border border-clay-warning/30 text-xs font-medium text-clay-text space-y-1"> <div className="mt-4 rounded-clay-sm bg-clay-warning/10 p-3 border border-clay-warning/30 text-xs font-medium text-clay-text space-y-1">
<span className="block font-bold text-clay-warning"> <span className="block font-bold text-clay-warning">
{t("ai.ocr.warnings")}: {t("ai.ocr.warnings")}:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment