-
-
{extractedFieldCount ? "Details read from receipt" : "Enter receipt details"}
-
{document.status === "failed" ? "Automatic reading failed. Enter the receipt details to continue." : extractedFieldCount ? "Edit anything that was read incorrectly." : "Automatic reading did not return any details. Fill in the fields to continue."}
+ ) : null}
+
+ {stage === "review" && document ? (
+
+
+
+ {isPdf ? (
+
+ Open receipt PDF
+
+ ) : previewUrl ? (
+ // eslint-disable-next-line @next/next/no-img-element
+

+ ) : (
+
Receipt preview unavailable.
+ )}
+
+
+
03 / 03 · review
+
+ {extractedFieldCount
+ ? `${extractedFieldCount} field${extractedFieldCount === 1 ? " was" : "s were"} read from this receipt. Review and correct the values before confirming.`
+ : "No details were read automatically. Enter the receipt details below before confirming."}
+
-
{document.status === "failed" ? "Needs details" : !extractedFieldCount ? "No details read" : document.issues.length ? "Check details" : "Ready to review"}
- {document.issues.length ?
-
{document.issues.map((issue, index) => - {issueText(issue)}
)}
-
: null}
-
- {!schema ?
{error || "Receipt fields could not be loaded."}
:
+ ) : null}
);
diff --git a/src/lib/demo-data.ts b/src/lib/demo-data.ts
index b3d64de..e8ba4a1 100644
--- a/src/lib/demo-data.ts
+++ b/src/lib/demo-data.ts
@@ -26,24 +26,115 @@ export const categories: Category[] = [
];
export const receipts: Receipt[] = [
- { id: "rcp-1042", merchant: "Dhanmondi Kitchen", category: "Food", amount: 860, date: "2026-10-20", status: "Needs review" },
- { id: "rcp-1041", merchant: "Agora", category: "Groceries", amount: 2_450, date: "2026-10-19", status: "Ready" },
- { id: "rcp-1040", merchant: "Pathao", category: "Transport", amount: 320, date: "2026-10-18", status: "Ready" },
- { id: "rcp-1039", merchant: "Shwapno", category: "Groceries", amount: 1_830, date: "2026-10-17", status: "Ready" },
- { id: "rcp-1038", merchant: "Lazz Pharma", category: "Medicine", amount: 2_650, date: "2026-10-16", status: "Needs review" },
- { id: "rcp-1037", merchant: "Titas Gas", category: "Utilities", amount: 1_000, date: "2026-10-15", status: "Ready" },
- { id: "rcp-1036", merchant: "Bengal Boi", category: "Shopping", amount: 1_900, date: "2026-10-12", status: "Ready" },
- { id: "rcp-1035", merchant: "Star Kabab", category: "Food", amount: 11_620, date: "2026-10-11", status: "Ready" },
- { id: "rcp-1034", merchant: "Agora", category: "Groceries", amount: 4_280, date: "2026-10-10", status: "Ready" },
- { id: "rcp-1033", merchant: "Pathao", category: "Transport", amount: 3_400, date: "2026-10-09", status: "Ready" },
- { id: "rcp-1032", merchant: "DESCO", category: "Utilities", amount: 2_640, date: "2026-10-08", status: "Ready" },
- { id: "rcp-1031", merchant: "Stationery House", category: "Other", amount: 1_870, date: "2026-10-07", status: "Ready" },
+ {
+ id: "rcp-1042",
+ merchant: "Dhanmondi Kitchen",
+ category: "Food",
+ amount: 860,
+ date: "2026-10-20",
+ status: "Needs review",
+ },
+ {
+ id: "rcp-1041",
+ merchant: "Agora",
+ category: "Groceries",
+ amount: 2_450,
+ date: "2026-10-19",
+ status: "Ready",
+ },
+ {
+ id: "rcp-1040",
+ merchant: "Pathao",
+ category: "Transport",
+ amount: 320,
+ date: "2026-10-18",
+ status: "Ready",
+ },
+ {
+ id: "rcp-1039",
+ merchant: "Shwapno",
+ category: "Groceries",
+ amount: 1_830,
+ date: "2026-10-17",
+ status: "Ready",
+ },
+ {
+ id: "rcp-1038",
+ merchant: "Lazz Pharma",
+ category: "Medicine",
+ amount: 2_650,
+ date: "2026-10-16",
+ status: "Needs review",
+ },
+ {
+ id: "rcp-1037",
+ merchant: "Titas Gas",
+ category: "Utilities",
+ amount: 1_000,
+ date: "2026-10-15",
+ status: "Ready",
+ },
+ {
+ id: "rcp-1036",
+ merchant: "Bengal Boi",
+ category: "Shopping",
+ amount: 1_900,
+ date: "2026-10-12",
+ status: "Ready",
+ },
+ {
+ id: "rcp-1035",
+ merchant: "Star Kabab",
+ category: "Food",
+ amount: 11_620,
+ date: "2026-10-11",
+ status: "Ready",
+ },
+ {
+ id: "rcp-1034",
+ merchant: "Agora",
+ category: "Groceries",
+ amount: 4_280,
+ date: "2026-10-10",
+ status: "Ready",
+ },
+ {
+ id: "rcp-1033",
+ merchant: "Pathao",
+ category: "Transport",
+ amount: 3_400,
+ date: "2026-10-09",
+ status: "Ready",
+ },
+ {
+ id: "rcp-1032",
+ merchant: "DESCO",
+ category: "Utilities",
+ amount: 2_640,
+ date: "2026-10-08",
+ status: "Ready",
+ },
+ {
+ id: "rcp-1031",
+ merchant: "Stationery House",
+ category: "Other",
+ amount: 1_870,
+ date: "2026-10-07",
+ status: "Ready",
+ },
];
export const budgetTotal = categories.reduce((sum, category) => sum + category.budget, 0);
export const spentTotal = categories.reduce((sum, category) => sum + category.amount, 0);
-const currencySymbol: Record
= { BDT: "৳", INR: "₹", PKR: "Rs ", USD: "$", EUR: "€", GBP: "£" };
+const currencySymbol: Record = {
+ BDT: "৳",
+ INR: "₹",
+ PKR: "Rs ",
+ USD: "$",
+ EUR: "€",
+ GBP: "£",
+};
export function money(amount: number, currency = "BDT"): string {
const symbol = currencySymbol[currency] ?? `${currency} `;
diff --git a/test/receipt-upload-dialog.test.tsx b/test/receipt-upload-dialog.test.tsx
index a125145..d0fc2cf 100644
--- a/test/receipt-upload-dialog.test.tsx
+++ b/test/receipt-upload-dialog.test.tsx
@@ -41,10 +41,15 @@ const receiptSchema = {
currency: { "x-kind": "code" },
line_items: {
"x-kind": "rows",
- items: { properties: {
- description: { "x-kind": "text" }, quantity: { "x-kind": "number" },
- unit_price: { "x-kind": "amount" }, discount: { "x-kind": "amount" }, amount: { "x-kind": "amount" },
- } },
+ items: {
+ properties: {
+ description: { "x-kind": "text" },
+ quantity: { "x-kind": "number" },
+ unit_price: { "x-kind": "amount" },
+ discount: { "x-kind": "amount" },
+ amount: { "x-kind": "amount" },
+ },
+ },
},
subtotal: { "x-kind": "amount" },
discount: { "x-kind": "amount" },
@@ -110,7 +115,10 @@ beforeEach(() => {
upload: mocks.upload,
refresh: mocks.refresh,
});
- mocks.request.mockResolvedValue({ ok: true, blob: async () => new Blob(["receipt"], { type: "image/png" }) });
+ mocks.request.mockResolvedValue({
+ ok: true,
+ blob: async () => new Blob(["receipt"], { type: "image/png" }),
+ });
mocks.refresh.mockResolvedValue(undefined);
mocks.api.mockImplementation(async (path: string, options?: { method?: string; body?: unknown }) => {
if (options?.method === "PATCH") return { ...documentOut("ready"), ...(options.body as object) };
@@ -155,14 +163,22 @@ describe("receipt upload dialog", () => {
expect(save).toBeTruthy();
await act(async () => save?.click());
- const patchCall = mocks.api.mock.calls.find(([, options]) => (options as { method?: string } | undefined)?.method === "PATCH");
- expect((patchCall?.[1] as { body: { confirmed: boolean; data: { shop_name: string } } }).body).toMatchObject({
+ const patchCall = mocks.api.mock.calls.find(
+ ([, options]) => (options as { method?: string } | undefined)?.method === "PATCH",
+ );
+ expect(
+ (patchCall?.[1] as { body: { confirmed: boolean; data: { shop_name: string } } } | undefined)?.body,
+ ).toMatchObject({
confirmed: true,
data: { shop_name: "Corrected shop name" },
});
expect(onClose).toHaveBeenCalledOnce();
expect(mocks.refresh).toHaveBeenCalledOnce();
- expect(mocks.api.mock.calls.some(([, options]) => (options as { method?: string } | undefined)?.method === "DELETE")).toBe(false);
+ expect(
+ mocks.api.mock.calls.some(
+ ([, options]) => (options as { method?: string } | undefined)?.method === "DELETE",
+ ),
+ ).toBe(false);
});
it("deletes an unfinished uploaded receipt when the user cancels", async () => {
diff --git a/tsconfig.json b/tsconfig.json
index 57190a4..5597cdf 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ],
+ "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -14,9 +10,7 @@
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
- "@/*": [
- "./*"
- ]
+ "@/*": ["./*"]
},
"resolveJsonModule": true,
"isolatedModules": true,
@@ -45,8 +39,5 @@
"src/store.ts",
".next/dev/types/**/*.ts"
],
- "exclude": [
- "node_modules",
- "dist"
- ]
+ "exclude": ["node_modules", "dist"]
}
diff --git a/vitest.config.ts b/vitest.config.ts
index 63ed319..38294c6 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,5 +1,5 @@
-import { defineConfig } from "vitest/config";
import { fileURLToPath } from "node:url";
+import { defineConfig } from "vitest/config";
export default defineConfig({
resolve: {