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.
CI / build (push) Has been skipped
CI / deploy (push) Failing after 1m15s

This commit is contained in:
eewing
2026-02-19 09:55:26 -06:00
parent c6b30100bd
commit 567d7e3e13
13 changed files with 449 additions and 37 deletions
+53 -9
View File
@@ -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;
}
</script>
<svelte:head>
@@ -38,9 +64,9 @@
<span class="text-muted-foreground text-sm shrink-0">
{filteredReceipts.length}{filteredReceipts.length !== receipts.length ? ` of ${receipts.length}` : ""} receipts
</span>
<a href="/receipts/new" class="block shrink-0">
<Button class="w-full sm:w-auto min-h-11">New receipt</Button>
</a>
<Button class="w-full sm:w-auto min-h-11" onclick={() => (createDialogOpen = true)}>
New receipt
</Button>
</div>
</div>
@@ -53,9 +79,7 @@
<p class="text-muted-foreground text-center text-xs">
{receipts.length === 0 ? "Create a receipt to get started." : "Try a different search."}
</p>
<a href="/receipts/new">
<Button variant="outline">New receipt</Button>
</a>
<Button variant="outline" onclick={() => (createDialogOpen = true)}>New receipt</Button>
<a href="/">
<Button variant="ghost">Back to home</Button>
</a>
@@ -66,10 +90,30 @@
{#each filteredReceipts as receipt (receipt.id)}
<Card>
<CardContent class="p-4">
<a href="/receipts/{receipt.id}" class="font-medium hover:underline">Receipt {receipt.id}</a>
<a href="/receipts/{receipt.id}" class="font-medium hover:underline">
{receipt.Status ?? "—"} · {receipt.Type ?? "—"} · {formatDate(receipt.created)}
</a>
<p class="text-muted-foreground mt-1 text-xs">ID: {receipt.id}</p>
</CardContent>
</Card>
{/each}
</div>
{/if}
</main>
<Dialog.Root bind:open={createDialogOpen}>
<Dialog.Content class="max-h-[90dvh] w-[calc(100vw-2rem)] max-w-[500px] overflow-y-auto rounded-lg sm:w-full">
<Dialog.Header>
<Dialog.Title>New receipt</Dialog.Title>
<Dialog.Description>Add a new receipt. Optionally attach an image.</Dialog.Description>
</Dialog.Header>
<ReceiptForm
action="/receipts?/create"
submitLabel="Create"
error={form?.error}
defaultStatus={form?.Status ?? "New"}
defaultType={form?.Type ?? "Purchase"}
onSuccess={onReceiptCreateSuccess}
/>
</Dialog.Content>
</Dialog.Root>