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 [] 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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
<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
|
<form
|
||||||
data-receipt-id={receipt.id}
|
data-receipt-id={receipt.id}
|
||||||
method="POST"
|
method="POST"
|
||||||
@@ -187,22 +196,22 @@
|
|||||||
class="h-8 min-w-[8rem] border-0 bg-transparent shadow-none hover:bg-muted/80"
|
class="h-8 min-w-[8rem] border-0 bg-transparent shadow-none hover:bg-muted/80"
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<span>{receipt.Status ?? "New"}</span>
|
<span>{receipt.Status ?? "New"}</span>
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
{#each STATUS_OPTIONS as o}
|
{#each STATUS_OPTIONS as o}
|
||||||
<Select.Item value={o} label={o} />
|
<Select.Item value={o} label={o} />
|
||||||
{/each}
|
{/each}
|
||||||
</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>
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user