Enhance receipt display by adding conditional rendering for descriptions and editing capabilities. Introduce a derived state for edit permissions and update UI components to reflect these changes, improving user interaction and information visibility.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m38s

This commit is contained in:
eewing
2026-02-26 14:30:22 -06:00
parent c89329dada
commit fcd2c0342f
5 changed files with 106 additions and 34 deletions
@@ -27,7 +27,7 @@ export const load = async ({ locals }: Parameters<PageServerLoad>[0]) => {
return [] as StackqReceipt[]; return [] as StackqReceipt[];
}); });
return { receipts: list }; return { receipts: list, canEdit: isAdmin };
}; };
export const actions = { export const actions = {
@@ -93,11 +93,9 @@ export const actions = {
return fail(400, { error: "Invalid status", setStatus: null }); return fail(400, { error: "Invalid status", setStatus: null });
} }
const isAdmin = user.Admin === true; const isAdmin = user.Admin === true;
if (!isAdmin) return fail(403, { error: "Only admins can change status.", setStatus: null });
try { try {
const existing = await locals.pb.collection(COLLECTION).getOne<StackqReceipt>(id); await locals.pb.collection(COLLECTION).getOne<StackqReceipt>(id);
if (!isAdmin && existing.User !== user.id) {
return fail(404, { error: "Receipt not found", setStatus: null });
}
await locals.pb.collection(COLLECTION).update(id, { Status }); await locals.pb.collection(COLLECTION).update(id, { Status });
} catch (e: unknown) { } catch (e: unknown) {
const is404 = e && typeof e === "object" && "status" in e && (e as { status: number }).status === 404; const is404 = e && typeof e === "object" && "status" in e && (e as { status: number }).status === 404;
+3 -5
View File
@@ -26,7 +26,7 @@ export const load: PageServerLoad = async ({ locals }) => {
return [] as StackqReceipt[]; return [] as StackqReceipt[];
}); });
return { receipts: list }; return { receipts: list, canEdit: isAdmin };
}; };
export const actions: Actions = { export const actions: Actions = {
@@ -92,11 +92,9 @@ export const actions: Actions = {
return fail(400, { error: "Invalid status", setStatus: null }); return fail(400, { error: "Invalid status", setStatus: null });
} }
const isAdmin = user.Admin === true; const isAdmin = user.Admin === true;
if (!isAdmin) return fail(403, { error: "Only admins can change status.", setStatus: null });
try { try {
const existing = await locals.pb.collection(COLLECTION).getOne<StackqReceipt>(id); await locals.pb.collection(COLLECTION).getOne<StackqReceipt>(id);
if (!isAdmin && existing.User !== user.id) {
return fail(404, { error: "Receipt not found", setStatus: null });
}
await locals.pb.collection(COLLECTION).update(id, { Status }); await locals.pb.collection(COLLECTION).update(id, { Status });
} catch (e: unknown) { } catch (e: unknown) {
const is404 = e && typeof e === "object" && "status" in e && (e as { status: number }).status === 404; const is404 = e && typeof e === "object" && "status" in e && (e as { status: number }).status === 404;
+13 -4
View File
@@ -11,7 +11,8 @@
import { STATUS_OPTIONS } from "$lib/types/receipts"; import { STATUS_OPTIONS } from "$lib/types/receipts";
import { enhance } from "$app/forms"; import { enhance } from "$app/forms";
let { data, form }: { data: { receipts: StackqReceipt[] }; form?: ActionData } = $props(); let { data, form }: { data: { receipts: StackqReceipt[]; canEdit?: boolean }; form?: ActionData } = $props();
const canEdit = $derived(data?.canEdit ?? false);
let searchQuery = $state(""); let searchQuery = $state("");
let createDialogOpen = $state(false); let createDialogOpen = $state(false);
let activeTab = $state<(typeof STATUS_OPTIONS)[number]>("New"); let activeTab = $state<(typeof STATUS_OPTIONS)[number]>("New");
@@ -168,7 +169,15 @@
<Card class="hover:bg-muted/50 transition-colors cursor-pointer"> <Card class="hover:bg-muted/50 transition-colors cursor-pointer">
<CardContent class="p-4 flex flex-col gap-2"> <CardContent class="p-4 flex flex-col gap-2">
<div class="flex items-start justify-between gap-2"> <div class="flex items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<span class="font-medium">{userName(receipt)}</span> <span class="font-medium">{userName(receipt)}</span>
{#if receipt.Description}
<p class="text-muted-foreground text-sm mt-0.5 truncate" title={receipt.Description}>{receipt.Description}</p>
{:else}
<p class="text-muted-foreground text-sm mt-0.5 italic">No description</p>
{/if}
</div>
{#if canEdit}
<form <form
data-receipt-id={receipt.id} data-receipt-id={receipt.id}
method="POST" method="POST"
@@ -196,13 +205,13 @@
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</form> </form>
{:else}
<Badge variant="outline" class="shrink-0">{receipt.Status ?? "New"}</Badge>
{/if}
</div> </div>
<div class="flex flex-wrap items-center gap-2 text-muted-foreground"> <div class="flex flex-wrap items-center gap-2 text-muted-foreground">
<Badge variant="secondary">{receipt.Type ?? "—"}</Badge> <Badge variant="secondary">{receipt.Type ?? "—"}</Badge>
<span class="text-xs">{formatDateLocal(receipt.created)}</span> <span class="text-xs">{formatDateLocal(receipt.created)}</span>
{#if receipt.Description}
<span class="text-xs truncate max-w-[200px]" title={receipt.Description}>{receipt.Description}</span>
{/if}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
+4 -9
View File
@@ -22,7 +22,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
if (!isAdmin && user?.id && record.User !== user.id) { if (!isAdmin && user?.id && record.User !== user.id) {
throw error(404, "Receipt not found"); throw error(404, "Receipt not found");
} }
return { receipt: record }; return { receipt: record, canEdit: isAdmin };
} catch (err: unknown) { } catch (err: unknown) {
const is404 = const is404 =
err && err &&
@@ -40,11 +40,9 @@ export const actions: Actions = {
if (!id) return fail(400, { error: "Missing id", form: "update" }); if (!id) return fail(400, { error: "Missing id", form: "update" });
const user = locals.user as { id: string; Admin?: boolean } | null; const user = locals.user as { id: string; Admin?: boolean } | null;
const isAdmin = user?.Admin === true; const isAdmin = user?.Admin === true;
if (!isAdmin) return fail(403, { error: "Only admins can edit receipts.", form: "update" });
try { try {
const existing = await locals.pb.collection(COLLECTION).getOne<StackqReceipt>(id); await locals.pb.collection(COLLECTION).getOne<StackqReceipt>(id);
if (!isAdmin && user?.id && existing.User !== user.id) {
return fail(404, { error: "Receipt not found", form: "update" });
}
} catch { } catch {
return fail(404, { error: "Receipt not found", form: "update" }); return fail(404, { error: "Receipt not found", form: "update" });
} }
@@ -131,11 +129,8 @@ export const actions: Actions = {
if (!id) return fail(400, { error: "Missing id", form: "delete" }); if (!id) return fail(400, { error: "Missing id", form: "delete" });
const user = locals.user as { id: string; Admin?: boolean } | null; const user = locals.user as { id: string; Admin?: boolean } | null;
const isAdmin = user?.Admin === true; const isAdmin = user?.Admin === true;
if (!isAdmin) return fail(403, { error: "Only admins can delete receipts.", form: "delete" });
try { try {
const existing = await locals.pb.collection(COLLECTION).getOne<StackqReceipt>(id);
if (!isAdmin && user?.id && existing.User !== user.id) {
return fail(404, { error: "Receipt not found", form: "delete" });
}
await locals.pb.collection(COLLECTION).delete(id); await locals.pb.collection(COLLECTION).delete(id);
} catch (e: unknown) { } catch (e: unknown) {
const message = const message =
+74 -2
View File
@@ -19,9 +19,10 @@
const COLLECTION = "Stackq_Receipts"; const COLLECTION = "Stackq_Receipts";
const TYPE_OPTIONS = ["Purchase", "Gas"] as const; const TYPE_OPTIONS = ["Purchase", "Gas"] as const;
let { data, form }: { data: { receipt: StackqReceipt }; form?: { error?: string; form?: string } } = $props(); let { data, form }: { data: { receipt: StackqReceipt; canEdit?: boolean }; form?: { error?: string; form?: string } } = $props();
const receipt = $derived(data?.receipt); const receipt = $derived(data?.receipt);
const canEdit = $derived(data?.canEdit ?? false);
const formData = $state<{ lineItems: ReceiptLineItem[]; tax: number; receiptTotal: number }>({ const formData = $state<{ lineItems: ReceiptLineItem[]; tax: number; receiptTotal: number }>({
lineItems: [{ item: "", description: "", quantity: 1, price: 0 }], lineItems: [{ item: "", description: "", quantity: 1, price: 0 }],
@@ -243,7 +244,7 @@
{/if} {/if}
</div> </div>
{:else} {:else}
<p class="text-muted-foreground text-center text-sm">No image. Add one in the form and save.</p> <p class="text-muted-foreground text-center text-sm">{canEdit ? "No image. Add one in the form and save." : "No image."}</p>
{/if} {/if}
</Collapsible.Content> </Collapsible.Content>
</Collapsible.Root> </Collapsible.Root>
@@ -252,6 +253,7 @@
<Card> <Card>
<CardContent class="p-4"> <CardContent class="p-4">
{#if canEdit}
<form <form
method="POST" method="POST"
action="?/update" action="?/update"
@@ -475,6 +477,74 @@
</a> </a>
</div> </div>
</form> </form>
{:else}
<div class="flex flex-col gap-4">
<div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-2">
<p class="text-muted-foreground text-sm font-medium">Status</p>
<p>{statusValue}</p>
</div>
<div class="space-y-2">
<p class="text-muted-foreground text-sm font-medium">Type</p>
<p>{typeValue}</p>
</div>
</div>
<div class="space-y-2">
<p class="text-muted-foreground text-sm font-medium">Description</p>
<p>{descriptionValue || "—"}</p>
</div>
{#if lineItems.length > 0}
<div class="space-y-2">
<p class="text-muted-foreground text-sm font-medium">Line items</p>
<div class="overflow-x-auto">
<Table>
<TableHeader>
<TableRow class="text-left text-muted-foreground">
<TableHead class="pb-2 pr-2">Item</TableHead>
<TableHead class="pb-2 pr-2">Description</TableHead>
<TableHead class="w-20 pb-2 pr-2">Qty</TableHead>
<TableHead class="w-24 pb-2 pr-2">Price</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#each lineItems as row}
<TableRow>
<TableCell class="py-1.5 pr-2">{row.item || "—"}</TableCell>
<TableCell class="py-1.5 pr-2">{row.description || "—"}</TableCell>
<TableCell class="py-1.5 pr-2">{row.quantity}</TableCell>
<TableCell class="py-1.5 pr-2">${Number(row.price).toFixed(2)}</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
</div>
</div>
{/if}
<div class="border-t pt-4 space-y-1 text-sm">
<div class="flex justify-between gap-4">
<span class="text-muted-foreground">Subtotal</span>
<span>${subtotal.toFixed(2)}</span>
</div>
<div class="flex justify-between gap-4">
<span class="text-muted-foreground">Tax</span>
<span>${(Number(tax) || 0).toFixed(2)}</span>
</div>
<div class="flex justify-between gap-4 border-t pt-2 font-medium">
<span>Total</span>
<span>${total.toFixed(2)}</span>
</div>
<div class="flex justify-between gap-4 pt-2">
<span class="text-muted-foreground">Receipt total</span>
<span>${(Number(receiptTotal) || 0).toFixed(2)}</span>
</div>
</div>
<div class="pt-2">
<a href="/receipts">
<Button type="button" variant="outline">Back to receipts</Button>
</a>
</div>
</div>
{/if}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -499,6 +569,7 @@
<p class="text-muted-foreground text-sm"> <p class="text-muted-foreground text-sm">
User: {receipt.expand?.User?.name ?? receipt.expand?.User?.email ?? "—"} · Created {formatDate(receipt.created)} · Updated {formatDate(receipt.updated)} User: {receipt.expand?.User?.name ?? receipt.expand?.User?.email ?? "—"} · Created {formatDate(receipt.created)} · Updated {formatDate(receipt.updated)}
</p> </p>
{#if canEdit}
<form <form
method="POST" method="POST"
action="?/delete" action="?/delete"
@@ -510,6 +581,7 @@
> >
<Button type="submit" variant="destructive">Delete receipt</Button> <Button type="submit" variant="destructive">Delete receipt</Button>
</form> </form>
{/if}
</CardContent> </CardContent>
</Card> </Card>
{:else} {:else}