From c89329dada942ede91adf29923b8f4893090f1f8 Mon Sep 17 00:00:00 2001 From: eewing Date: Thu, 26 Feb 2026 14:26:15 -0600 Subject: [PATCH] Add receipt status management functionality, allowing users to update receipt statuses through a new server action. Introduce STATUS_OPTIONS for consistent status handling across components. Enhance UI to support status selection and display, improving overall user experience in receipt management. --- .svelte-kit/generated/server/internal.js | 2 +- .../src/routes/receipts/proxy+page.server.ts | 26 ++++ src/lib/types/receipts.ts | 4 + src/routes/receipts/+page.server.ts | 26 ++++ src/routes/receipts/+page.svelte | 129 +++++++++++++++--- src/routes/receipts/[id]/+page.server.ts | 5 +- src/routes/receipts/[id]/+page.svelte | 5 +- 7 files changed, 174 insertions(+), 23 deletions(-) diff --git a/.svelte-kit/generated/server/internal.js b/.svelte-kit/generated/server/internal.js index 5a3cd5bc..5a1ff777 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 \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: "1gfvo51" + version_hash: "49o61d" }; export async function get_hooks() { 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 e4c051a4..1d6152c8 100644 --- a/.svelte-kit/types/src/routes/receipts/proxy+page.server.ts +++ b/.svelte-kit/types/src/routes/receipts/proxy+page.server.ts @@ -2,6 +2,7 @@ import { fail } from "@sveltejs/kit"; import type { Actions, PageServerLoad } from "./$types"; import type { StackqReceipt } from "$lib/types/receipts"; +import { STATUS_OPTIONS } from "$lib/types/receipts"; import { convertHeicFileToJpegIfNeeded } from "$lib/convert-heic-server"; import { validateImageUpload } from "$lib/validate-image-upload"; @@ -80,5 +81,30 @@ export const actions = { return { success: true }; }, + + setStatus: async ({ request, locals }: import('./$types').RequestEvent) => { + const user = locals.user as { id: string; Admin?: boolean } | null; + if (!user?.id) return fail(401, { error: "Not logged in", setStatus: null }); + const form = await request.formData(); + const id = (form.get("id") as string)?.trim(); + const Status = (form.get("Status") as string)?.trim() || "New"; + if (!id) return fail(400, { error: "Missing receipt id", setStatus: null }); + if (!STATUS_OPTIONS.includes(Status as (typeof STATUS_OPTIONS)[number])) { + return fail(400, { error: "Invalid status", setStatus: null }); + } + const isAdmin = user.Admin === true; + try { + const existing = await locals.pb.collection(COLLECTION).getOne(id); + if (!isAdmin && existing.User !== user.id) { + return fail(404, { error: "Receipt not found", setStatus: null }); + } + await locals.pb.collection(COLLECTION).update(id, { Status }); + } catch (e: unknown) { + const is404 = e && typeof e === "object" && "status" in e && (e as { status: number }).status === 404; + if (is404) return fail(404, { error: "Receipt not found", setStatus: null }); + return fail(500, { error: "Failed to update status", setStatus: null }); + } + return { success: true, setStatus: id }; + }, }; ;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 index 93e06ce3..9aabf0fc 100644 --- a/src/lib/types/receipts.ts +++ b/src/lib/types/receipts.ts @@ -1,3 +1,7 @@ +/** Receipt status values for Stackq_Receipts */ +export const STATUS_OPTIONS = ["New", "Data Entered", "Reviewed", "Consolidated"] as const; +export type ReceiptStatus = (typeof STATUS_OPTIONS)[number]; + /** Line item stored in receipt Data.lineItems */ export interface ReceiptLineItem { item: string; diff --git a/src/routes/receipts/+page.server.ts b/src/routes/receipts/+page.server.ts index 6a3e0741..ebc7b27f 100644 --- a/src/routes/receipts/+page.server.ts +++ b/src/routes/receipts/+page.server.ts @@ -1,6 +1,7 @@ import { fail } from "@sveltejs/kit"; import type { Actions, PageServerLoad } from "./$types"; import type { StackqReceipt } from "$lib/types/receipts"; +import { STATUS_OPTIONS } from "$lib/types/receipts"; import { convertHeicFileToJpegIfNeeded } from "$lib/convert-heic-server"; import { validateImageUpload } from "$lib/validate-image-upload"; @@ -79,4 +80,29 @@ export const actions: Actions = { return { success: true }; }, + + setStatus: async ({ request, locals }) => { + const user = locals.user as { id: string; Admin?: boolean } | null; + if (!user?.id) return fail(401, { error: "Not logged in", setStatus: null }); + const form = await request.formData(); + const id = (form.get("id") as string)?.trim(); + const Status = (form.get("Status") as string)?.trim() || "New"; + if (!id) return fail(400, { error: "Missing receipt id", setStatus: null }); + if (!STATUS_OPTIONS.includes(Status as (typeof STATUS_OPTIONS)[number])) { + return fail(400, { error: "Invalid status", setStatus: null }); + } + const isAdmin = user.Admin === true; + try { + const existing = await locals.pb.collection(COLLECTION).getOne(id); + if (!isAdmin && existing.User !== user.id) { + return fail(404, { error: "Receipt not found", setStatus: null }); + } + await locals.pb.collection(COLLECTION).update(id, { Status }); + } catch (e: unknown) { + const is404 = e && typeof e === "object" && "status" in e && (e as { status: number }).status === 404; + if (is404) return fail(404, { error: "Receipt not found", setStatus: null }); + return fail(500, { error: "Failed to update status", setStatus: null }); + } + return { success: true, setStatus: id }; + }, }; diff --git a/src/routes/receipts/+page.svelte b/src/routes/receipts/+page.svelte index 88e8258b..c758ff47 100644 --- a/src/routes/receipts/+page.svelte +++ b/src/routes/receipts/+page.svelte @@ -3,14 +3,18 @@ import { Button } from "$lib/components/ui/button"; import { Card, CardContent } from "$lib/components/ui/card"; import { Input } from "$lib/components/ui/input"; + import * as Select from "$lib/components/ui/select"; 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"; + import { STATUS_OPTIONS } from "$lib/types/receipts"; + import { enhance } from "$app/forms"; let { data, form }: { data: { receipts: StackqReceipt[] }; form?: ActionData } = $props(); let searchQuery = $state(""); let createDialogOpen = $state(false); + let activeTab = $state<(typeof STATUS_OPTIONS)[number]>("New"); const receipts = $derived(data?.receipts ?? []); const filteredReceipts = $derived( @@ -24,11 +28,29 @@ r.id.toLowerCase().includes(q) || (r.Status ?? "").toLowerCase().includes(q) || (r.Type ?? "").toLowerCase().includes(q) || + (r.Description ?? "").toLowerCase().includes(q) || userName(r).includes(q) ); })() ); + const receiptsByStatus = $derived( + (() => { + const by: Record = {}; + for (const s of STATUS_OPTIONS) by[s] = []; + for (const r of filteredReceipts) { + const s = (r.Status ?? "New") as (typeof STATUS_OPTIONS)[number]; + if (STATUS_OPTIONS.includes(s)) by[s].push(r); + else by["New"].push(r); + } + return by; + })() + ); + + const tabReceipts = $derived(receiptsByStatus[activeTab] ?? []); + const countNew = $derived(receiptsByStatus["New"]?.length ?? 0); + const countDataEntered = $derived(receiptsByStatus["Data Entered"]?.length ?? 0); + function userName(receipt: StackqReceipt): string { const u = receipt.expand?.User; return u?.name ?? u?.email ?? "—"; @@ -50,6 +72,15 @@ function onReceiptCreateSuccess() { createDialogOpen = false; } + + function submitStatusForm(receiptId: string, newStatus: string) { + const form = document.querySelector(`form[data-receipt-id="${receiptId}"]`); + if (form) { + const statusInput = form.querySelector('input[name="Status"]'); + if (statusInput) statusInput.value = newStatus; + form.requestSubmit(); + } + } @@ -96,23 +127,89 @@ {:else} -
- {#each filteredReceipts as receipt (receipt.id)} +
+
+ {#each STATUS_OPTIONS as status} + {@const count = status === "New" ? countNew : status === "Data Entered" ? countDataEntered : null} + + {/each} +
+ + {#if tabReceipts.length === 0} - - - {userName(receipt)} - -
- {receipt.Type ?? "—"} - {receipt.Status ?? "—"} - - {formatDateLocal(receipt.created)} - -
+ + No receipts with status “{activeTab}”.
- {/each} + {:else} + + {/if}
{/if} @@ -127,8 +224,8 @@ action="/receipts?/create" submitLabel="Create" error={form?.error} - defaultType={form?.Type ?? "Purchase"} - defaultDescription={form?.Description ?? ""} + defaultType={(form as { Type?: string })?.Type ?? "Purchase"} + defaultDescription={(form as { Description?: string })?.Description ?? ""} onSuccess={onReceiptCreateSuccess} /> diff --git a/src/routes/receipts/[id]/+page.server.ts b/src/routes/receipts/[id]/+page.server.ts index 0181ba9c..bd93ceed 100644 --- a/src/routes/receipts/[id]/+page.server.ts +++ b/src/routes/receipts/[id]/+page.server.ts @@ -1,14 +1,13 @@ import { error, fail, redirect } from "@sveltejs/kit"; import type { Actions, PageServerLoad } from "./$types"; -import type { StackqReceipt } from "$lib/types/receipts"; -import type { ReceiptDataShape } from "$lib/types/receipts"; +import type { StackqReceipt, ReceiptDataShape } from "$lib/types/receipts"; +import { STATUS_OPTIONS } from "$lib/types/receipts"; import { convertHeicFileToJpegIfNeeded } from "$lib/convert-heic-server"; import { parseNumWithDefault } from "$lib/parse-form"; import { validateImageUpload } from "$lib/validate-image-upload"; const COLLECTION = "Stackq_Receipts"; -const STATUS_OPTIONS = ["New", "Data Entered", "Reviewed", "Consolidated"] as const; const TYPE_OPTIONS = ["Purchase", "Gas"] as const; const MAX_LINE_ITEMS = 500; diff --git a/src/routes/receipts/[id]/+page.svelte b/src/routes/receipts/[id]/+page.svelte index d26f6082..6b74bbd5 100644 --- a/src/routes/receipts/[id]/+page.svelte +++ b/src/routes/receipts/[id]/+page.svelte @@ -13,11 +13,10 @@ import ChevronDownIcon from "@lucide/svelte/icons/chevron-down"; import ChevronUpIcon from "@lucide/svelte/icons/chevron-up"; import Loader2Icon from "@lucide/svelte/icons/loader-2"; - import type { StackqReceipt } from "$lib/types/receipts"; - import type { ReceiptLineItem } from "$lib/types/receipts"; + import type { StackqReceipt, ReceiptLineItem } from "$lib/types/receipts"; + import { STATUS_OPTIONS } from "$lib/types/receipts"; const COLLECTION = "Stackq_Receipts"; - const STATUS_OPTIONS = ["New", "Data Entered", "Reviewed", "Consolidated"] as const; const TYPE_OPTIONS = ["Purchase", "Gas"] as const; let { data, form }: { data: { receipt: StackqReceipt }; form?: { error?: string; form?: string } } = $props();