Skip to content

Commit ddf2090

Browse files
author
CF
committed
feat: enhance Gemini OCR and update schema for home currency support
1 parent acf0e69 commit ddf2090

File tree

4 files changed

+77
-16
lines changed

4 files changed

+77
-16
lines changed

src/components/ManualInvoiceForm.tsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export const ManualInvoiceForm: React.FC<ManualInvoiceFormProps> = ({ initialDat
1515
const [invoice, setInvoice] = useState({
1616
tripId: initialData?.tripId || '',
1717
shopName: initialData?.shopName || '',
18+
shopAddress: initialData?.shopAddress || '',
19+
tel: initialData?.tel || '',
1820
country: initialData?.country || '',
1921
currency: initialData?.currency || 'HKD',
2022
txDate: initialData?.txDate || new Date().toISOString().split('T')[0],
@@ -23,7 +25,7 @@ export const ManualInvoiceForm: React.FC<ManualInvoiceFormProps> = ({ initialDat
2325
});
2426

2527
const [items, setItems] = useState<Partial<Item>[]>(
26-
initialData?.items || [{ id: crypto.randomUUID(), nameOriginal: '', nameChinese: '', type: '食品', qty: 1, price: 0, currency: 'HKD' }]
28+
initialData?.items || [{ id: crypto.randomUUID(), nameOriginal: '', nameChinese: '', type: '食品', qty: 1, price: 0, discount: 0, currency: 'HKD' }]
2729
);
2830

2931
const [isTranslating, setIsTranslating] = useState<string | null>(null);
@@ -33,7 +35,7 @@ export const ManualInvoiceForm: React.FC<ManualInvoiceFormProps> = ({ initialDat
3335
}, []);
3436

3537
const addItem = () => {
36-
setItems([...items, { id: crypto.randomUUID(), nameOriginal: '', nameChinese: '', type: '食品', qty: 1, price: 0, currency: invoice.currency }]);
38+
setItems([...items, { id: crypto.randomUUID(), nameOriginal: '', nameChinese: '', type: '食品', qty: 1, price: 0, discount: 0, currency: invoice.currency }]);
3739
};
3840

3941
const removeItem = (id: string) => {
@@ -61,7 +63,7 @@ export const ManualInvoiceForm: React.FC<ManualInvoiceFormProps> = ({ initialDat
6163
const newInvoice: Invoice = {
6264
...invoice,
6365
id: invoiceId,
64-
totalAmount: items.reduce((sum, item) => sum + (Number(item.price) || 0) * (Number(item.qty) || 1), 0)
66+
totalAmount: items.reduce((sum, item) => sum + ((Number(item.price) || 0) * (Number(item.qty) || 1)) - (Number(item.discount) || 0), 0)
6567
};
6668

6769
await dbService.saveInvoice(newInvoice);
@@ -72,7 +74,8 @@ export const ManualInvoiceForm: React.FC<ManualInvoiceFormProps> = ({ initialDat
7274
id: item.id || crypto.randomUUID(),
7375
invoiceId,
7476
currency: item.currency || invoice.currency,
75-
status: '未開封'
77+
status: '未開封',
78+
discount: Number(item.discount) || 0
7679
});
7780
}
7881

@@ -103,6 +106,23 @@ export const ManualInvoiceForm: React.FC<ManualInvoiceFormProps> = ({ initialDat
103106
placeholder="例如:驚安之殿堂"
104107
/>
105108
</div>
109+
<div className="col-span-2">
110+
<label className="text-xs font-bold text-slate-500 mb-1 block">商店地址</label>
111+
<input
112+
type="text"
113+
value={invoice.shopAddress}
114+
onChange={e => setInvoice({...invoice, shopAddress: e.target.value})}
115+
placeholder="例如:東京都新宿區..."
116+
/>
117+
</div>
118+
<div className="col-span-2">
119+
<label className="text-xs font-bold text-slate-500 mb-1 block">電話</label>
120+
<input
121+
type="text"
122+
value={invoice.tel}
123+
onChange={e => setInvoice({...invoice, tel: e.target.value})}
124+
/>
125+
</div>
106126
<div>
107127
<label className="text-xs font-bold text-slate-500 mb-1 block">日期</label>
108128
<input
@@ -185,7 +205,7 @@ export const ManualInvoiceForm: React.FC<ManualInvoiceFormProps> = ({ initialDat
185205
placeholder="翻譯或手動輸入"
186206
/>
187207
</div>
188-
<div className="col-span-6">
208+
<div className="col-span-4">
189209
<label className="text-[10px] font-bold text-slate-400 uppercase">類別</label>
190210
<select
191211
value={item.type}
@@ -200,7 +220,7 @@ export const ManualInvoiceForm: React.FC<ManualInvoiceFormProps> = ({ initialDat
200220
<option value="其他">其他</option>
201221
</select>
202222
</div>
203-
<div className="col-span-3">
223+
<div className="col-span-2">
204224
<label className="text-[10px] font-bold text-slate-400 uppercase">數量</label>
205225
<input
206226
type="number"
@@ -216,6 +236,14 @@ export const ManualInvoiceForm: React.FC<ManualInvoiceFormProps> = ({ initialDat
216236
onChange={e => updateItem(item.id!, 'price', Number(e.target.value))}
217237
/>
218238
</div>
239+
<div className="col-span-3">
240+
<label className="text-[10px] font-bold text-slate-400 uppercase">折扣</label>
241+
<input
242+
type="number"
243+
value={item.discount}
244+
onChange={e => updateItem(item.id!, 'discount', Number(e.target.value))}
245+
/>
246+
</div>
219247
</div>
220248
</div>
221249
))}

src/services/ocr.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@ import { createWorker } from 'tesseract.js';
22

33
export interface ParsedReceipt {
44
shopName: string;
5+
shopAddress?: string;
6+
country?: string;
7+
tel?: string;
58
txDate: string;
69
txTime: string;
7-
items: { name: string; nameChinese?: string; price: number; qty: number }[];
10+
items: {
11+
name: string;
12+
nameChinese?: string;
13+
price: number;
14+
qty: number;
15+
type?: string;
16+
discount?: number;
17+
}[];
818
totalAmount: number;
919
currency?: string;
1020
}
@@ -29,15 +39,32 @@ export async function processReceiptWithGemini(imageFile: File, apiKey: string,
2939
reader.readAsDataURL(imageFile);
3040
});
3141

32-
const prompt = `分析這張收據圖片。請提取以下資訊並以 JSON 格式返回:
33-
- 商店名稱 (shopName)
34-
- 交易日期 (txDate, 格式 YYYY-MM-DD)
35-
- 交易時間 (txTime, 格式 HH:mm)
36-
- 幣別 (currency, 如 HKD, JPY, TWD)
37-
- 總金額 (totalAmount, 數字)
38-
- 項目清單 (items): 每個項目包含名稱 (name, 原始語言)、中文翻譯 (nameChinese)、單價 (price) 和數量 (qty)。
42+
const prompt = `你是一個專業的收據識別助手。請分析這張圖片,並提取以下詳細資訊,以 JSON 格式返回:
43+
44+
1. 商店資訊:
45+
- shopName: 商店名稱
46+
- shopAddress: 商店地址 (如有)
47+
- country: 國家/地區 (如 Japan, Taiwan, Hong Kong)
48+
- tel: 電話號碼 (如有)
49+
50+
2. 交易資訊:
51+
- txDate: 交易日期 (格式 YYYY-MM-DD)
52+
- txTime: 交易時間 (格式 HH:mm)
53+
- currency: 貨幣代碼 (如 JPY, TWD, HKD, USD)
54+
- totalAmount: 總金額 (純數字)
3955
40-
請只返回 JSON 代碼塊。`;
56+
3. 商品清單 (items 陣列),每項包含:
57+
- name: 原始品名 (保留原文)
58+
- nameChinese: 中文翻譯品名 (繁體中文)
59+
- price: 單價 (純數字)
60+
- qty: 數量 (純數字,預設 1)
61+
- discount: 此項目的折扣金額 (如有,正數表示扣減金額,無則為 0)
62+
- type: 商品類別 (從以下選擇:食品, 藥品, 生活用品, 化妝品, 衣物, 電子產品, 其他)
63+
64+
請注意:
65+
- 如果無法識別某些欄位,請省略或設為 null。
66+
- 確保 JSON 格式正確且無 Markdown 標記。
67+
- 若有折扣,請確保 totalAmount 是折扣後的最終金額。`;
4168

4269
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
4370
method: 'POST',

src/types/schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface Invoice {
1919
txDate: string;
2020
txTime: string;
2121
totalAmount: number;
22+
totalAmountHomeCurrency?: number;
2223
}
2324

2425
export interface Item {
@@ -30,6 +31,7 @@ export interface Item {
3031
type: string;
3132
qty: number;
3233
price: number;
34+
priceHomeCurrency?: number;
3335
currency: string;
3436
discount: number;
3537
expiryDate?: string;

src/views/Record.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export const RecordView: React.FC = () => {
3636

3737
setInitialData({
3838
shopName: parsed.shopName,
39+
shopAddress: parsed.shopAddress,
40+
country: parsed.country,
41+
tel: parsed.tel,
3942
txDate: parsed.txDate,
4043
txTime: parsed.txTime,
4144
totalAmount: parsed.totalAmount,
@@ -46,7 +49,8 @@ export const RecordView: React.FC = () => {
4649
nameChinese: item.nameChinese || '',
4750
price: item.price,
4851
qty: item.qty,
49-
type: '食品'
52+
type: item.type || '食品',
53+
discount: item.discount || 0
5054
}))
5155
});
5256
setMode('review');

0 commit comments

Comments
 (0)