Files
Stackq/src/routes/receipts/+page.svelte
T

135 lines
4.8 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { Badge } from "$lib/components/ui/badge";
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, 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;
const userName = (r: StackqReceipt) =>
(r.expand?.User?.name ?? r.expand?.User?.email ?? "").toLowerCase();
return receipts.filter(
(r) =>
r.id.toLowerCase().includes(q) ||
(r.Status ?? "").toLowerCase().includes(q) ||
(r.Type ?? "").toLowerCase().includes(q) ||
userName(r).includes(q)
);
})()
);
function userName(receipt: StackqReceipt): string {
const u = receipt.expand?.User;
return u?.name ?? u?.email ?? "—";
}
/** Format ISO (UTC) date string as local date/time */
function formatDateLocal(iso: string): string {
try {
const d = new Date(iso);
return d.toLocaleString(undefined, {
dateStyle: "short",
timeStyle: "short",
});
} catch {
return iso;
}
}
function onReceiptCreateSuccess() {
createDialogOpen = false;
}
</script>
<svelte:head>
<title>Receipts Stackq</title>
</svelte:head>
<main class="container mx-auto flex flex-col gap-6 px-4 py-6 sm:px-6 sm:py-8">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-3 min-w-0">
<a href="/" class="text-muted-foreground hover:text-foreground shrink-0 text-sm" title="Back to main">Back</a>
<h1 class="text-xl font-semibold truncate sm:text-2xl">Receipts</h1>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-2">
<div class="w-full sm:w-64 sm:min-w-0">
<Input
type="search"
placeholder="Search receipts…"
bind:value={searchQuery}
class="h-11 w-full sm:h-9"
/>
</div>
<span class="text-muted-foreground text-sm shrink-0">
{filteredReceipts.length}{filteredReceipts.length !== receipts.length ? ` of ${receipts.length}` : ""} receipts
</span>
<Button class="w-full sm:w-auto min-h-11" onclick={() => (createDialogOpen = true)}>
New receipt
</Button>
</div>
</div>
{#if filteredReceipts.length === 0}
<Card>
<CardContent class="flex flex-col items-center justify-center gap-4 py-12">
<p class="text-muted-foreground text-center text-sm">
{receipts.length === 0 ? "No receipts yet." : "No receipts match your search."}
</p>
<p class="text-muted-foreground text-center text-xs">
{receipts.length === 0 ? "Create a receipt to get started." : "Try a different search."}
</p>
<Button variant="outline" onclick={() => (createDialogOpen = true)}>New receipt</Button>
<a href="/">
<Button variant="ghost">Back to home</Button>
</a>
</CardContent>
</Card>
{:else}
<div class="flex flex-col gap-2">
{#each filteredReceipts as receipt (receipt.id)}
<Card>
<CardContent class="p-4">
<a href="/receipts/{receipt.id}" class="block font-medium hover:underline">
{userName(receipt)}
</a>
<div class="mt-2 flex flex-wrap items-center gap-2">
<Badge variant="secondary">{receipt.Type ?? "—"}</Badge>
<Badge variant="outline">{receipt.Status ?? "—"}</Badge>
<span class="text-muted-foreground text-xs">
{formatDateLocal(receipt.created)}
</span>
</div>
</CardContent>
</Card>
{/each}
</div>
{/if}
</main>
<Dialog.Root bind:open={createDialogOpen}>
<Dialog.Content class="flex min-h-0 max-h-[90dvh] w-[calc(100vw-2rem)] max-w-[500px] flex-col overflow-hidden rounded-lg sm:w-full">
<Dialog.Header class="shrink-0">
<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}
defaultType={form?.Type ?? "Purchase"}
onSuccess={onReceiptCreateSuccess}
/>
</Dialog.Content>
</Dialog.Root>