Add automatic invoice parsing to extract purchase items
- Install pdf-parse and tesseract.js for PDF/image processing - Create /api/admin/parse-invoice endpoint to extract line items from invoices - Parser extracts quantity and description from invoice text - Attempts to match extracted items to products in database - Update PurchaseForm with "Extract items" button - Pre-fill purchase form with parsed items (user can edit/add more) - Supports PDF files and image scans (JPG, PNG, etc) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
48017facf0
commit
d596cbe836
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { createPurchase } from '@/app/admin/accounts/purchases/actions';
|
||||
import Price from './Price';
|
||||
|
||||
@@ -30,6 +30,9 @@ export default function PurchaseForm({ products }: { products: ProductOption[] }
|
||||
{ key: 1, productId: '', description: '', color: '', size: '', quantity: 1, unitCostInput: '' },
|
||||
]);
|
||||
const [nextKey, setNextKey] = useState(2);
|
||||
const [parsing, setParsing] = useState(false);
|
||||
const [parseError, setParseError] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const updateRow = (key: number, patch: Partial<Row>) => {
|
||||
setRows((prev) => prev.map((r) => (r.key === key ? { ...r, ...patch } : r)));
|
||||
@@ -61,6 +64,56 @@ export default function PurchaseForm({ products }: { products: ProductOption[] }
|
||||
setRows((prev) => (prev.length > 1 ? prev.filter((r) => r.key !== key) : prev));
|
||||
};
|
||||
|
||||
const parseInvoice = async () => {
|
||||
if (!fileInputRef.current?.files?.[0]) {
|
||||
setParseError('No file selected');
|
||||
return;
|
||||
}
|
||||
|
||||
setParsing(true);
|
||||
setParseError(null);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('invoice', fileInputRef.current.files[0]);
|
||||
|
||||
const res = await fetch('/api/admin/parse-invoice', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
setParseError(data.error || 'Failed to parse invoice');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.items || data.items.length === 0) {
|
||||
setParseError('No items found in invoice. Please add them manually.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert extracted items to form rows
|
||||
const newRows: Row[] = data.items.map((item: any, index: number) => ({
|
||||
key: nextKey + index,
|
||||
productId: item.productId || '',
|
||||
description: item.description,
|
||||
color: '',
|
||||
size: '',
|
||||
quantity: item.quantity,
|
||||
unitCostInput: '',
|
||||
}));
|
||||
|
||||
setRows(newRows);
|
||||
setNextKey(nextKey + newRows.length);
|
||||
} catch (err) {
|
||||
setParseError(err instanceof Error ? err.message : 'Failed to parse invoice');
|
||||
} finally {
|
||||
setParsing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const pence = (input: string) => Math.max(0, Math.round(Number(input || '0') * 100));
|
||||
const total = rows.reduce((sum, r) => sum + r.quantity * pence(r.unitCostInput), 0);
|
||||
|
||||
@@ -113,14 +166,32 @@ export default function PurchaseForm({ products }: { products: ProductOption[] }
|
||||
<label htmlFor="invoice" className="tag-label mb-2 block">
|
||||
Invoice file (optional)
|
||||
</label>
|
||||
<input
|
||||
id="invoice"
|
||||
name="invoice"
|
||||
type="file"
|
||||
accept="application/pdf,image/*"
|
||||
className="w-full border border-line bg-paper px-3 py-2 text-sm"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted">PDF or photo, up to 8 MB — kept with the purchase for your records.</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
id="invoice"
|
||||
name="invoice"
|
||||
type="file"
|
||||
accept="application/pdf,image/*"
|
||||
className="flex-1 border border-line bg-paper px-3 py-2 text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={parseInvoice}
|
||||
disabled={parsing}
|
||||
className="border border-ink px-4 py-2 text-sm hover:border-clay hover:bg-clay hover:text-paper disabled:opacity-50"
|
||||
>
|
||||
{parsing ? 'Parsing…' : 'Extract items'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted">
|
||||
PDF or photo, up to 8 MB. Click "Extract items" to auto-fill line items from the invoice.
|
||||
</p>
|
||||
{parseError && (
|
||||
<p className="mt-2 border border-splash-pink/40 bg-splash-pink/10 px-3 py-2 text-xs text-splash-pink">
|
||||
{parseError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user