From 567d7e3e134a524e6b0f5be83f5b864529ca1554 Mon Sep 17 00:00:00 2001 From: eewing Date: Thu, 19 Feb 2026 09:55:26 -0600 Subject: [PATCH] Implement receipt management features including creation, loading, and display of receipts. Refactor related components and enhance UI with dialogs for adding new receipts. Update type definitions and improve error handling for better user experience. --- .svelte-kit/generated/client/app.js | 2 +- .svelte-kit/generated/server/internal.js | 2 +- .svelte-kit/types/route_meta_data.json | 1 + .../types/src/routes/receipts/$types.d.ts | 8 +- .../src/routes/receipts/[id]/$types.d.ts | 13 +- .../src/routes/receipts/proxy+page.server.ts | 59 ++++++++- src/lib/types/receipts.ts | 14 +++ src/routes/HomeWithDialog.svelte | 27 ++++- src/routes/receipts/+page.server.ts | 57 ++++++++- src/routes/receipts/+page.svelte | 62 ++++++++-- src/routes/receipts/ReceiptForm.svelte | 113 ++++++++++++++++++ src/routes/receipts/[id]/+page.server.ts | 23 ++++ src/routes/receipts/[id]/+page.svelte | 105 ++++++++++++++-- 13 files changed, 449 insertions(+), 37 deletions(-) create mode 100644 src/lib/types/receipts.ts create mode 100644 src/routes/receipts/ReceiptForm.svelte create mode 100644 src/routes/receipts/[id]/+page.server.ts diff --git a/.svelte-kit/generated/client/app.js b/.svelte-kit/generated/client/app.js index 494cf3dd..49406e8f 100644 --- a/.svelte-kit/generated/client/app.js +++ b/.svelte-kit/generated/client/app.js @@ -32,7 +32,7 @@ export const dictionary = { "/logout": [~6], "/receipts": [~3], "/receipts/new": [4], - "/receipts/[id]": [5], + "/receipts/[id]": [~5], "/transactions": [~7], "/transactions/new": [8], "/transactions/[id]": [9] diff --git a/.svelte-kit/generated/server/internal.js b/.svelte-kit/generated/server/internal.js index 47ffc1c4..e3c04eea 100644 --- a/.svelte-kit/generated/server/internal.js +++ b/.svelte-kit/generated/server/internal.js @@ -24,7 +24,7 @@ export const options = { app: ({ head, body, assets, nonce, env }) => "\n\n \n \n S\" />\n \n \n \n \n \n \n \n " + head + "\n \n \n
" + body + "
\n \n\n", error: ({ status, message }) => "\n\n\t\n\t\t\n\t\t" + message + "\n\n\t\t\n\t\n\t\n\t\t
\n\t\t\t" + status + "\n\t\t\t
\n\t\t\t\t

" + message + "

\n\t\t\t
\n\t\t
\n\t\n\n" }, - version_hash: "16sw9ck" + version_hash: "11efg8s" }; export async function get_hooks() { diff --git a/.svelte-kit/types/route_meta_data.json b/.svelte-kit/types/route_meta_data.json index c9320440..86514602 100644 --- a/.svelte-kit/types/route_meta_data.json +++ b/.svelte-kit/types/route_meta_data.json @@ -49,6 +49,7 @@ "src/routes/+layout.server.ts" ], "/receipts/[id]": [ + "src/routes/receipts/[id]/+page.server.ts", "src/routes/+layout.ts", "src/routes/+layout.server.ts" ], diff --git a/.svelte-kit/types/src/routes/receipts/$types.d.ts b/.svelte-kit/types/src/routes/receipts/$types.d.ts index 968b5fac..431734f8 100644 --- a/.svelte-kit/types/src/routes/receipts/$types.d.ts +++ b/.svelte-kit/types/src/routes/receipts/$types.d.ts @@ -16,7 +16,13 @@ type PageParentData = EnsureDefined; export type PageServerLoad = OutputDataShape> = Kit.ServerLoad; export type PageServerLoadEvent = Parameters[0]; -export type ActionData = unknown; +type ExcludeActionFailure = T extends Kit.ActionFailure ? never : T extends void ? never : T; +type ActionsSuccess any>> = { [Key in keyof T]: ExcludeActionFailure>>; }[keyof T]; +type ExtractActionFailure = T extends Kit.ActionFailure ? X extends void ? never : X : never; +type ActionsFailure any>> = { [Key in keyof T]: Exclude>>, void>; }[keyof T]; +type ActionsExport = typeof import('./proxy+page.server.js').actions +export type SubmitFunction = Kit.SubmitFunction>, Expand>> +export type ActionData = Expand> | null; export type PageServerData = Expand>>>>>; export type PageData = Expand & EnsureDefined>; export type Action | void = Record | void> = Kit.Action diff --git a/.svelte-kit/types/src/routes/receipts/[id]/$types.d.ts b/.svelte-kit/types/src/routes/receipts/[id]/$types.d.ts index 9ed08e12..70426841 100644 --- a/.svelte-kit/types/src/routes/receipts/[id]/$types.d.ts +++ b/.svelte-kit/types/src/routes/receipts/[id]/$types.d.ts @@ -11,9 +11,16 @@ type OutputDataShape = MaybeWithVoid> & Pa type EnsureDefined = T extends null | undefined ? {} : T; type OptionalUnion, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude]?: never } & U : never; export type Snapshot = Kit.Snapshot; +type PageServerParentData = EnsureDefined; type PageParentData = EnsureDefined; export type EntryGenerator = () => Promise> | Array; -export type PageServerData = null; -export type PageData = Expand; -export type PageProps = { params: RouteParams; data: PageData } \ No newline at end of file +export type PageServerLoad = OutputDataShape> = Kit.ServerLoad; +export type PageServerLoadEvent = Parameters[0]; +export type ActionData = unknown; +export type PageServerData = Expand>>>>>; +export type PageData = Expand & EnsureDefined>; +export type Action | void = Record | void> = Kit.Action +export type Actions | void = Record | void> = Kit.Actions +export type PageProps = { params: RouteParams; data: PageData; form: ActionData } +export type RequestEvent = Kit.RequestEvent; \ No newline at end of file diff --git a/.svelte-kit/types/src/routes/receipts/proxy+page.server.ts b/.svelte-kit/types/src/routes/receipts/proxy+page.server.ts index 99c9386e..4c7af177 100644 --- a/.svelte-kit/types/src/routes/receipts/proxy+page.server.ts +++ b/.svelte-kit/types/src/routes/receipts/proxy+page.server.ts @@ -1,8 +1,57 @@ // @ts-nocheck -import type { PageServerLoad } from "./$types"; +import { fail } from "@sveltejs/kit"; +import type { Actions, PageServerLoad } from "./$types"; +import type { StackqReceipt } from "$lib/types/receipts"; -export const load = async () => { - // Placeholder: no PocketBase receipts collection yet. Return empty list. - return { receipts: [] as { id: string }[] }; +const COLLECTION = "Stackq_Receipts"; + +const STATUS_OPTIONS = ["New", "Data Entered", "Reviewed", "Consolidated"] as const; +const TYPE_OPTIONS = ["Purchase", "Gas"] as const; + +export const load = async ({ locals }: Parameters[0]) => { + const list = await locals.pb + .collection(COLLECTION) + .getFullList({ + sort: "-created", + expand: "User", + }) + .catch((err) => { + console.error(COLLECTION, err); + return [] as StackqReceipt[]; + }); + + return { receipts: list }; }; -;null as any as PageServerLoad; \ No newline at end of file + +export const actions = { + create: async ({ request, locals }: import('./$types').RequestEvent) => { + const user = locals.user; + if (!user?.id) return fail(401, { error: "You must be logged in to create a receipt." }); + + const form = await request.formData(); + const Status = (form.get("Status") as string)?.trim() || "New"; + const Type = (form.get("Type") as string)?.trim() || "Purchase"; + const imageFile = form.get("Image") as File | null; + const hasImage = imageFile && imageFile.size > 0; + + const body: Record = { + Status: STATUS_OPTIONS.includes(Status as (typeof STATUS_OPTIONS)[number]) ? Status : "New", + Type: TYPE_OPTIONS.includes(Type as (typeof TYPE_OPTIONS)[number]) ? Type : "Purchase", + User: user.id, + }; + if (hasImage) body.Image = imageFile; + + try { + await locals.pb.collection(COLLECTION).create(body); + } catch (err: unknown) { + const message = + err && typeof err === "object" && "message" in err + ? String((err as { message: string }).message) + : "Failed to create receipt."; + return fail(500, { error: message, Status, Type }); + } + + return { success: true }; + }, +}; +;null as any as Actions; \ No newline at end of file diff --git a/src/lib/types/receipts.ts b/src/lib/types/receipts.ts new file mode 100644 index 00000000..43d2d1f3 --- /dev/null +++ b/src/lib/types/receipts.ts @@ -0,0 +1,14 @@ +/** PocketBase Stackq_Receipts collection record (and expanded relation). */ +export interface StackqReceipt { + id: string; + Status: string; + Type: string; + User: string; + Image?: string; + Data?: Record; + created: string; + updated: string; + expand?: { + User?: { id: string; email?: string; name?: string }; + }; +} diff --git a/src/routes/HomeWithDialog.svelte b/src/routes/HomeWithDialog.svelte index eec86e7b..db6b1f15 100644 --- a/src/routes/HomeWithDialog.svelte +++ b/src/routes/HomeWithDialog.svelte @@ -2,6 +2,7 @@ import { Button } from "$lib/components/ui/button"; import * as Dialog from "$lib/components/ui/dialog"; import ItemForm from "./items/ItemForm.svelte"; + import ReceiptForm from "./receipts/ReceiptForm.svelte"; import BarcodeScanDialog from "$lib/components/BarcodeScanDialog.svelte"; import { goto } from "$app/navigation"; let { @@ -14,6 +15,7 @@ } = $props(); let dialogOpen = $state(false); + let receiptDialogOpen = $state(false); let scanDialogOpen = $state(false); const items = $derived(data?.items ?? []); const itemsForParent = $derived(data?.itemsForParent ?? []); @@ -23,6 +25,11 @@ goto("/items"); } + function onReceiptSuccess() { + receiptDialogOpen = false; + goto("/receipts"); + } + function onScanResult(value: string) { const sku = value.trim(); const found = items.find((i) => (i.SKU ?? "").trim() === sku); @@ -39,9 +46,9 @@ - - - + @@ -78,4 +85,18 @@ + + + + New receipt + Add a new receipt. Optionally attach an image. + + + + + diff --git a/src/routes/receipts/+page.server.ts b/src/routes/receipts/+page.server.ts index db8d6e07..2298aca7 100644 --- a/src/routes/receipts/+page.server.ts +++ b/src/routes/receipts/+page.server.ts @@ -1,6 +1,55 @@ -import type { PageServerLoad } from "./$types"; +import { fail } from "@sveltejs/kit"; +import type { Actions, PageServerLoad } from "./$types"; +import type { StackqReceipt } from "$lib/types/receipts"; -export const load: PageServerLoad = async () => { - // Placeholder: no PocketBase receipts collection yet. Return empty list. - return { receipts: [] as { id: string }[] }; +const COLLECTION = "Stackq_Receipts"; + +const STATUS_OPTIONS = ["New", "Data Entered", "Reviewed", "Consolidated"] as const; +const TYPE_OPTIONS = ["Purchase", "Gas"] as const; + +export const load: PageServerLoad = async ({ locals }) => { + const list = await locals.pb + .collection(COLLECTION) + .getFullList({ + sort: "-created", + expand: "User", + }) + .catch((err) => { + console.error(COLLECTION, err); + return [] as StackqReceipt[]; + }); + + return { receipts: list }; +}; + +export const actions: Actions = { + create: async ({ request, locals }) => { + const user = locals.user; + if (!user?.id) return fail(401, { error: "You must be logged in to create a receipt." }); + + const form = await request.formData(); + const Status = (form.get("Status") as string)?.trim() || "New"; + const Type = (form.get("Type") as string)?.trim() || "Purchase"; + const imageFile = form.get("Image") as File | null; + const hasImage = imageFile && imageFile.size > 0; + + const body: Record = { + Status: STATUS_OPTIONS.includes(Status as (typeof STATUS_OPTIONS)[number]) ? Status : "New", + Type: TYPE_OPTIONS.includes(Type as (typeof TYPE_OPTIONS)[number]) ? Type : "Purchase", + User: user.id, + }; + if (hasImage) body.Image = imageFile; + + try { + await locals.pb.collection(COLLECTION).create(body); + } catch (err: unknown) { + const message = + err && typeof err === "object" && "message" in err + ? String((err as { message: string }).message) + : "Failed to create receipt."; + return fail(500, { error: message, Status, Type }); + } + + return { success: true }; + }, }; diff --git a/src/routes/receipts/+page.svelte b/src/routes/receipts/+page.svelte index 4577d39e..b786f4db 100644 --- a/src/routes/receipts/+page.svelte +++ b/src/routes/receipts/+page.svelte @@ -2,18 +2,44 @@ import { Button } from "$lib/components/ui/button"; import { Card, CardContent } from "$lib/components/ui/card"; import { Input } from "$lib/components/ui/input"; + import * as Dialog from "$lib/components/ui/dialog"; + import ReceiptForm from "./ReceiptForm.svelte"; + import type { StackqReceipt } from "$lib/types/receipts"; + import type { ActionData } from "./$types"; - let { data }: { data: { receipts: { id: string }[] } } = $props(); + let { data, form }: { data: { receipts: StackqReceipt[] }; form?: ActionData } = $props(); let searchQuery = $state(""); + let createDialogOpen = $state(false); const receipts = $derived(data?.receipts ?? []); const filteredReceipts = $derived( (() => { const q = searchQuery.trim().toLowerCase(); if (!q) return receipts; - return receipts.filter((r) => r.id.toLowerCase().includes(q)); + return receipts.filter( + (r) => + r.id.toLowerCase().includes(q) || + (r.Status ?? "").toLowerCase().includes(q) || + (r.Type ?? "").toLowerCase().includes(q) + ); })() ); + + function formatDate(iso: string): string { + try { + const d = new Date(iso); + return d.toLocaleDateString(undefined, { + dateStyle: "short", + timeStyle: "short", + }); + } catch { + return iso; + } + } + + function onReceiptCreateSuccess() { + createDialogOpen = false; + } @@ -38,9 +64,9 @@ {filteredReceipts.length}{filteredReceipts.length !== receipts.length ? ` of ${receipts.length}` : ""} receipts - - - + @@ -53,9 +79,7 @@

{receipts.length === 0 ? "Create a receipt to get started." : "Try a different search."}

- - - + @@ -66,10 +90,30 @@ {#each filteredReceipts as receipt (receipt.id)} - Receipt {receipt.id} + + {receipt.Status ?? "—"} · {receipt.Type ?? "—"} · {formatDate(receipt.created)} + +

ID: {receipt.id}

{/each} {/if} + + + + + New receipt + Add a new receipt. Optionally attach an image. + + + + diff --git a/src/routes/receipts/ReceiptForm.svelte b/src/routes/receipts/ReceiptForm.svelte new file mode 100644 index 00000000..f1efd5a2 --- /dev/null +++ b/src/routes/receipts/ReceiptForm.svelte @@ -0,0 +1,113 @@ + + +
async ({ update, result }) => { + await update(); + if (result.type === "success" && result.data?.success) { + onSuccess?.(); + } + }} + class="flex flex-col gap-4" +> +
+ + +
+ +
+ + +
+ +
+ +
+ + + {#if imageFileName} +

Selected: {imageFileName}

+ {:else} +

Optional. Use “Scan receipt” to take a photo or pick an image.

+ {/if} +
+
+ + {#if error} +

{error}

+ {/if} + +
+ +
+
diff --git a/src/routes/receipts/[id]/+page.server.ts b/src/routes/receipts/[id]/+page.server.ts new file mode 100644 index 00000000..11053671 --- /dev/null +++ b/src/routes/receipts/[id]/+page.server.ts @@ -0,0 +1,23 @@ +import { error } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; +import type { StackqReceipt } from "$lib/types/receipts"; + +const COLLECTION = "Stackq_Receipts"; + +export const load: PageServerLoad = async ({ params, locals }) => { + const { id } = params; + try { + const record = await locals.pb + .collection(COLLECTION) + .getOne(id, { expand: "User" }); + return { receipt: record }; + } catch (err: unknown) { + const is404 = + err && + typeof err === "object" && + "status" in err && + (err as { status: number }).status === 404; + if (is404) throw error(404, "Receipt not found"); + throw err; + } +}; diff --git a/src/routes/receipts/[id]/+page.svelte b/src/routes/receipts/[id]/+page.svelte index cba98000..b19a17b8 100644 --- a/src/routes/receipts/[id]/+page.svelte +++ b/src/routes/receipts/[id]/+page.svelte @@ -1,12 +1,34 @@ - Receipt – Stackq + Receipt {receipt?.Status ?? receipt?.id ?? "—"} – Stackq
@@ -15,12 +37,75 @@

Receipt

- - -

Receipt {params.id}

- - - -
-
+ {#if receipt} + + +
+
+
Status
+
{receipt.Status ?? "—"}
+
+
+
Type
+
{receipt.Type ?? "—"}
+
+
+
User
+
+ {#if receipt.expand?.User} + {receipt.expand.User.name ?? receipt.expand.User.email ?? receipt.expand.User.id} + {:else} + — + {/if} +
+
+
+
Created
+
{formatDate(receipt.created)}
+
+
+
Updated
+
{formatDate(receipt.updated)}
+
+ {#if receipt.Data && Object.keys(receipt.Data).length > 0} +
+
Data
+
+
{JSON.stringify(receipt.Data, null, 2)}
+
+
+ {/if} + {#if receipt.Image} +
+
Image
+
+ {@const url = imageUrl(receipt)} + {#if url} + Receipt + {:else} + — + {/if} +
+
+ {/if} +
+ + + +
+
+ {:else} + + +

Receipt not found.

+ + + +
+
+ {/if}