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.
This commit is contained in:
@@ -27,7 +27,7 @@ export const load = async ({ locals }: Parameters<PageServerLoad>[0]) => {
|
||||
return [] as StackqReceipt[];
|
||||
});
|
||||
|
||||
return { receipts: list };
|
||||
return { receipts: list, canEdit: isAdmin };
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
@@ -93,11 +93,9 @@ export const actions = {
|
||||
return fail(400, { error: "Invalid status", setStatus: null });
|
||||
}
|
||||
const isAdmin = user.Admin === true;
|
||||
if (!isAdmin) return fail(403, { error: "Only admins can change status.", setStatus: null });
|
||||
try {
|
||||
const existing = 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).getOne<StackqReceipt>(id);
|
||||
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;
|
||||
|
||||
@@ -26,7 +26,7 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
return [] as StackqReceipt[];
|
||||
});
|
||||
|
||||
return { receipts: list };
|
||||
return { receipts: list, canEdit: isAdmin };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
@@ -92,11 +92,9 @@ export const actions: Actions = {
|
||||
return fail(400, { error: "Invalid status", setStatus: null });
|
||||
}
|
||||
const isAdmin = user.Admin === true;
|
||||
if (!isAdmin) return fail(403, { error: "Only admins can change status.", setStatus: null });
|
||||
try {
|
||||
const existing = 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).getOne<StackqReceipt>(id);
|
||||
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;
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
import { STATUS_OPTIONS } from "$lib/types/receipts";
|
||||
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 createDialogOpen = $state(false);
|
||||
let activeTab = $state<(typeof STATUS_OPTIONS)[number]>("New");
|
||||
@@ -168,7 +169,15 @@
|
||||
<Card class="hover:bg-muted/50 transition-colors cursor-pointer">
|
||||
<CardContent class="p-4 flex flex-col gap-2">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<span class="font-medium">{userName(receipt)}</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<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
|
||||
data-receipt-id={receipt.id}
|
||||
method="POST"
|
||||
@@ -187,22 +196,22 @@
|
||||
class="h-8 min-w-[8rem] border-0 bg-transparent shadow-none hover:bg-muted/80"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span>{receipt.Status ?? "New"}</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each STATUS_OPTIONS as o}
|
||||
<Select.Item value={o} label={o} />
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<span>{receipt.Status ?? "New"}</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each STATUS_OPTIONS as o}
|
||||
<Select.Item value={o} label={o} />
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</form>
|
||||
{:else}
|
||||
<Badge variant="outline" class="shrink-0">{receipt.Status ?? "New"}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2 text-muted-foreground">
|
||||
<Badge variant="secondary">{receipt.Type ?? "—"}</Badge>
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
|
||||
if (!isAdmin && user?.id && record.User !== user.id) {
|
||||
throw error(404, "Receipt not found");
|
||||
}
|
||||
return { receipt: record };
|
||||
return { receipt: record, canEdit: isAdmin };
|
||||
} catch (err: unknown) {
|
||||
const is404 =
|
||||
err &&
|
||||
@@ -40,11 +40,9 @@ export const actions: Actions = {
|
||||
if (!id) return fail(400, { error: "Missing id", form: "update" });
|
||||
const user = locals.user as { id: string; Admin?: boolean } | null;
|
||||
const isAdmin = user?.Admin === true;
|
||||
if (!isAdmin) return fail(403, { error: "Only admins can edit receipts.", form: "update" });
|
||||
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: "update" });
|
||||
}
|
||||
await locals.pb.collection(COLLECTION).getOne<StackqReceipt>(id);
|
||||
} catch {
|
||||
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" });
|
||||
const user = locals.user as { id: string; Admin?: boolean } | null;
|
||||
const isAdmin = user?.Admin === true;
|
||||
if (!isAdmin) return fail(403, { error: "Only admins can delete receipts.", form: "delete" });
|
||||
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);
|
||||
} catch (e: unknown) {
|
||||
const message =
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
const COLLECTION = "Stackq_Receipts";
|
||||
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 canEdit = $derived(data?.canEdit ?? false);
|
||||
|
||||
const formData = $state<{ lineItems: ReceiptLineItem[]; tax: number; receiptTotal: number }>({
|
||||
lineItems: [{ item: "", description: "", quantity: 1, price: 0 }],
|
||||
@@ -243,7 +244,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{: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}
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
@@ -252,6 +253,7 @@
|
||||
|
||||
<Card>
|
||||
<CardContent class="p-4">
|
||||
{#if canEdit}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/update"
|
||||
@@ -475,6 +477,74 @@
|
||||
</a>
|
||||
</div>
|
||||
</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>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -499,6 +569,7 @@
|
||||
<p class="text-muted-foreground text-sm">
|
||||
User: {receipt.expand?.User?.name ?? receipt.expand?.User?.email ?? "—"} · Created {formatDate(receipt.created)} · Updated {formatDate(receipt.updated)}
|
||||
</p>
|
||||
{#if canEdit}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/delete"
|
||||
@@ -510,6 +581,7 @@
|
||||
>
|
||||
<Button type="submit" variant="destructive">Delete receipt</Button>
|
||||
</form>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else}
|
||||
|
||||
Reference in New Issue
Block a user