From a82c89674a44b47e25f292d81a1eb73b0c7a2a75 Mon Sep 17 00:00:00 2001 From: eewing Date: Wed, 18 Feb 2026 10:39:29 -0600 Subject: [PATCH] Enhance routing and layout in SvelteKit project by adding new routes for accounts and transactions, updating existing routes, and improving layout parameters. Refactor item handling and integrate barcode scanning functionality for better user experience. --- .svelte-kit/generated/client/app.js | 14 +- .svelte-kit/generated/client/nodes/4.js | 2 +- .svelte-kit/generated/client/nodes/5.js | 2 +- .svelte-kit/generated/client/nodes/6.js | 1 + .svelte-kit/generated/client/nodes/7.js | 1 + .svelte-kit/generated/client/nodes/8.js | 1 + .svelte-kit/generated/server/internal.js | 4 +- .svelte-kit/non-ambient.d.ts | 15 +- .svelte-kit/types/route_meta_data.json | 10 + .svelte-kit/types/src/routes/$types.d.ts | 6 +- .../types/src/routes/accounts/$types.d.ts | 18 + .../types/src/routes/items/[id]/$types.d.ts | 32 ++ .../routes/items/[id]/proxy+page.server.ts | 91 +++++ .../types/src/routes/proxy+page.server.ts | 2 +- .../types/src/routes/transactions/$types.d.ts | 18 + src/app.html | 5 +- src/lib/components/BarcodeScanDialog.svelte | 185 ++++++++++ src/routes/+page.server.ts | 2 +- src/routes/+page.svelte | 29 +- src/routes/HomeWithDialog.svelte | 83 ++++- src/routes/accounts/+page.svelte | 28 ++ src/routes/items/+page.svelte | 148 +++----- src/routes/items/ItemsCrud.svelte | 323 +++++++++--------- src/routes/items/[id]/+page.server.ts | 89 +++++ src/routes/items/[id]/+page.svelte | 219 ++++++++++++ src/routes/layout.css | 33 ++ src/routes/transactions/+page.svelte | 28 ++ 27 files changed, 1080 insertions(+), 309 deletions(-) create mode 100644 .svelte-kit/generated/client/nodes/6.js create mode 100644 .svelte-kit/generated/client/nodes/7.js create mode 100644 .svelte-kit/generated/client/nodes/8.js create mode 100644 .svelte-kit/types/src/routes/accounts/$types.d.ts create mode 100644 .svelte-kit/types/src/routes/items/[id]/$types.d.ts create mode 100644 .svelte-kit/types/src/routes/items/[id]/proxy+page.server.ts create mode 100644 .svelte-kit/types/src/routes/transactions/$types.d.ts create mode 100644 src/lib/components/BarcodeScanDialog.svelte create mode 100644 src/routes/accounts/+page.svelte create mode 100644 src/routes/items/[id]/+page.server.ts create mode 100644 src/routes/items/[id]/+page.svelte create mode 100644 src/routes/transactions/+page.svelte diff --git a/.svelte-kit/generated/client/app.js b/.svelte-kit/generated/client/app.js index 0beb8d63..2c8c82d8 100644 --- a/.svelte-kit/generated/client/app.js +++ b/.svelte-kit/generated/client/app.js @@ -6,16 +6,22 @@ export const nodes = [ () => import('./nodes/2'), () => import('./nodes/3'), () => import('./nodes/4'), - () => import('./nodes/5') + () => import('./nodes/5'), + () => import('./nodes/6'), + () => import('./nodes/7'), + () => import('./nodes/8') ]; export const server_loads = []; export const dictionary = { "/": [~2], - "/items": [~4], - "/login": [~5], - "/logout": [~3] + "/accounts": [5], + "/items": [~6], + "/items/[id]": [~7], + "/login": [~8], + "/logout": [~3], + "/transactions": [4] }; export const hooks = { diff --git a/.svelte-kit/generated/client/nodes/4.js b/.svelte-kit/generated/client/nodes/4.js index 3e8c3e68..37a1a420 100644 --- a/.svelte-kit/generated/client/nodes/4.js +++ b/.svelte-kit/generated/client/nodes/4.js @@ -1 +1 @@ -export { default as component } from "../../../../src/routes/items/+page.svelte"; \ No newline at end of file +export { default as component } from "../../../../src/routes/transactions/+page.svelte"; \ No newline at end of file diff --git a/.svelte-kit/generated/client/nodes/5.js b/.svelte-kit/generated/client/nodes/5.js index f2b26cd1..e442b828 100644 --- a/.svelte-kit/generated/client/nodes/5.js +++ b/.svelte-kit/generated/client/nodes/5.js @@ -1 +1 @@ -export { default as component } from "../../../../src/routes/login/+page.svelte"; \ No newline at end of file +export { default as component } from "../../../../src/routes/accounts/+page.svelte"; \ No newline at end of file diff --git a/.svelte-kit/generated/client/nodes/6.js b/.svelte-kit/generated/client/nodes/6.js new file mode 100644 index 00000000..3e8c3e68 --- /dev/null +++ b/.svelte-kit/generated/client/nodes/6.js @@ -0,0 +1 @@ +export { default as component } from "../../../../src/routes/items/+page.svelte"; \ No newline at end of file diff --git a/.svelte-kit/generated/client/nodes/7.js b/.svelte-kit/generated/client/nodes/7.js new file mode 100644 index 00000000..389ba1f3 --- /dev/null +++ b/.svelte-kit/generated/client/nodes/7.js @@ -0,0 +1 @@ +export { default as component } from "../../../../src/routes/items/[id]/+page.svelte"; \ No newline at end of file diff --git a/.svelte-kit/generated/client/nodes/8.js b/.svelte-kit/generated/client/nodes/8.js new file mode 100644 index 00000000..f2b26cd1 --- /dev/null +++ b/.svelte-kit/generated/client/nodes/8.js @@ -0,0 +1 @@ +export { default as component } from "../../../../src/routes/login/+page.svelte"; \ No newline at end of file diff --git a/.svelte-kit/generated/server/internal.js b/.svelte-kit/generated/server/internal.js index e08579cd..56ebe112 100644 --- a/.svelte-kit/generated/server/internal.js +++ b/.svelte-kit/generated/server/internal.js @@ -21,10 +21,10 @@ export const options = { service_worker: false, service_worker_options: undefined, templates: { - app: ({ head, body, assets, nonce, env }) => "\n\n \n \n S\" />\n \n " + head + "\n \n \n
" + body + "
\n \n\n", + app: ({ head, body, assets, nonce, env }) => "\n\n \n \n S\" />\n \n \n \n \n " + head + "\n \n \n
" + body + "
\n \n\n", error: ({ status, message }) => "\n\n\t\n\t\t\n\t\t" + message + "\n\n\t\t\n\t\n\t\n\t\t
\n\t\t\t" + status + "\n\t\t\t
\n\t\t\t\t

" + message + "

\n\t\t\t
\n\t\t
\n\t\n\n" }, - version_hash: "6si2m4" + version_hash: "ekcm0i" }; export async function get_hooks() { diff --git a/.svelte-kit/non-ambient.d.ts b/.svelte-kit/non-ambient.d.ts index d6fdfc21..70cfd294 100644 --- a/.svelte-kit/non-ambient.d.ts +++ b/.svelte-kit/non-ambient.d.ts @@ -27,18 +27,21 @@ export {}; declare module "$app/types" { export interface AppTypes { - RouteId(): "/" | "/access-denied" | "/items" | "/login" | "/logout"; + RouteId(): "/" | "/access-denied" | "/accounts" | "/items" | "/items/[id]" | "/login" | "/logout" | "/transactions"; RouteParams(): { - + "/items/[id]": { id: string } }; LayoutParams(): { - "/": Record; + "/": { id?: string }; "/access-denied": Record; - "/items": Record; + "/accounts": Record; + "/items": { id?: string }; + "/items/[id]": { id: string }; "/login": Record; - "/logout": Record + "/logout": Record; + "/transactions": Record }; - Pathname(): "/" | "/items" | "/login" | "/logout"; + Pathname(): "/" | "/accounts" | "/items" | `/items/${string}` & {} | "/login" | "/logout" | "/transactions"; ResolvedPathname(): `${"" | `/${string}`}${ReturnType}`; Asset(): string & {}; } diff --git a/.svelte-kit/types/route_meta_data.json b/.svelte-kit/types/route_meta_data.json index 812cf9ee..84272ceb 100644 --- a/.svelte-kit/types/route_meta_data.json +++ b/.svelte-kit/types/route_meta_data.json @@ -4,10 +4,17 @@ "src/routes/+layout.ts", "src/routes/+layout.ts" ], + "/accounts": [ + "src/routes/+layout.ts" + ], "/items": [ "src/routes/items/+page.server.ts", "src/routes/+layout.ts" ], + "/items/[id]": [ + "src/routes/items/[id]/+page.server.ts", + "src/routes/+layout.ts" + ], "/login": [ "src/routes/login/+page.server.ts", "src/routes/+layout.ts" @@ -15,5 +22,8 @@ "/logout": [ "src/routes/logout/+page.server.ts", "src/routes/+layout.ts" + ], + "/transactions": [ + "src/routes/+layout.ts" ] } \ No newline at end of file diff --git a/.svelte-kit/types/src/routes/$types.d.ts b/.svelte-kit/types/src/routes/$types.d.ts index 206726a0..4985b1c0 100644 --- a/.svelte-kit/types/src/routes/$types.d.ts +++ b/.svelte-kit/types/src/routes/$types.d.ts @@ -13,8 +13,8 @@ type OptionalUnion, A extends keyof U = U extends export type Snapshot = Kit.Snapshot; type PageServerParentData = EnsureDefined; type PageParentData = EnsureDefined; -type LayoutRouteId = RouteId | "/" | "/logout" | "/items" | "/login" | null -type LayoutParams = RouteParams & { } +type LayoutRouteId = RouteId | "/" | "/logout" | "/transactions" | "/accounts" | "/items" | "/items/[id]" | "/login" | null +type LayoutParams = RouteParams & { id?: string } type LayoutParentData = EnsureDefined<{}>; export type PageServerLoad = OutputDataShape> = Kit.ServerLoad; @@ -26,7 +26,7 @@ export type Action | void = Record | void = Record | void> = Kit.Actions export type PageProps = { params: RouteParams; data: PageData; form: ActionData } export type LayoutServerData = null; -export type LayoutLoad & Record | void = Partial & Record | void> = Kit.Load; +export type LayoutLoad = OutputDataShape> = Kit.Load; export type LayoutLoadEvent = Parameters[0]; export type LayoutData = Expand> & OptionalUnion>>>; export type LayoutProps = { params: LayoutParams; data: LayoutData; children: import("svelte").Snippet } diff --git a/.svelte-kit/types/src/routes/accounts/$types.d.ts b/.svelte-kit/types/src/routes/accounts/$types.d.ts new file mode 100644 index 00000000..8fde87a6 --- /dev/null +++ b/.svelte-kit/types/src/routes/accounts/$types.d.ts @@ -0,0 +1,18 @@ +import type * as Kit from '@sveltejs/kit'; + +type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; +// @ts-ignore +type MatcherParam = M extends (param : string) => param is infer U ? U extends string ? U : string : string; +type RouteParams = { }; +type RouteId = '/accounts'; +type MaybeWithVoid = {} extends T ? T | void : T; +export type RequiredKeys = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T]; +type OutputDataShape = MaybeWithVoid> & Partial> & Record> +type EnsureDefined = T extends null | undefined ? {} : T; +type OptionalUnion, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude]?: never } & U : never; +export type Snapshot = Kit.Snapshot; +type PageParentData = EnsureDefined; + +export type PageServerData = null; +export type PageData = Expand; +export type PageProps = { params: RouteParams; data: PageData } \ No newline at end of file diff --git a/.svelte-kit/types/src/routes/items/[id]/$types.d.ts b/.svelte-kit/types/src/routes/items/[id]/$types.d.ts new file mode 100644 index 00000000..986bc20d --- /dev/null +++ b/.svelte-kit/types/src/routes/items/[id]/$types.d.ts @@ -0,0 +1,32 @@ +import type * as Kit from '@sveltejs/kit'; + +type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; +// @ts-ignore +type MatcherParam = M extends (param : string) => param is infer U ? U extends string ? U : string : string; +type RouteParams = { id: string }; +type RouteId = '/items/[id]'; +type MaybeWithVoid = {} extends T ? T | void : T; +export type RequiredKeys = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T]; +type OutputDataShape = MaybeWithVoid> & Partial> & Record> +type EnsureDefined = T extends null | undefined ? {} : T; +type OptionalUnion, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude]?: never } & U : never; +export type Snapshot = Kit.Snapshot; +type PageServerParentData = EnsureDefined; +type PageParentData = EnsureDefined; + +export type EntryGenerator = () => Promise> | Array; +export type PageServerLoad = OutputDataShape> = Kit.ServerLoad; +export type PageServerLoadEvent = Parameters[0]; +type ExcludeActionFailure = T extends Kit.ActionFailure ? never : T extends void ? never : T; +type ActionsSuccess any>> = { [Key in keyof T]: ExcludeActionFailure>>; }[keyof T]; +type ExtractActionFailure = T extends Kit.ActionFailure ? X extends void ? never : X : never; +type ActionsFailure any>> = { [Key in keyof T]: Exclude>>, void>; }[keyof T]; +type ActionsExport = typeof import('./proxy+page.server.js').actions +export type SubmitFunction = Kit.SubmitFunction>, Expand>> +export type ActionData = Expand> | null; +export type PageServerData = Expand>>>>>; +export type PageData = Expand & EnsureDefined>; +export type Action | void = Record | void> = Kit.Action +export type Actions | void = Record | void> = Kit.Actions +export type PageProps = { params: RouteParams; data: PageData; form: ActionData } +export type RequestEvent = Kit.RequestEvent; \ No newline at end of file diff --git a/.svelte-kit/types/src/routes/items/[id]/proxy+page.server.ts b/.svelte-kit/types/src/routes/items/[id]/proxy+page.server.ts new file mode 100644 index 00000000..79aca696 --- /dev/null +++ b/.svelte-kit/types/src/routes/items/[id]/proxy+page.server.ts @@ -0,0 +1,91 @@ +// @ts-nocheck +import { error, fail, redirect } from "@sveltejs/kit"; +import type { Actions, PageServerLoad } from "./$types"; +import type { StackqItem } from "$lib/types/items"; + +const COLLECTION = "Stackq_Items"; + +function parseNum(val: unknown): number | undefined { + if (val === "" || val === null || val === undefined) return undefined; + const n = Number(val); + return Number.isFinite(n) ? n : undefined; +} + +function parseBool(val: unknown): boolean { + return val === "on" || val === "true" || val === true; +} + +export const load = async ({ params, locals }: Parameters[0]) => { + const id = params.id; + if (!id) throw error(404, "Item not found"); + + try { + const item = await locals.pb + .collection(COLLECTION) + .getOne(id, { expand: "Parent" }); + const list = await locals.pb + .collection(COLLECTION) + .getFullList({ sort: "-created", expand: "Parent" }) + .catch(() => [] as StackqItem[]); + const itemsForParent = list.map((i) => ({ id: i.id, Item: i.Item, SKU: i.SKU })); + return { item, itemsForParent }; + } catch { + throw error(404, "Item not found"); + } +}; + +export const actions = { + update: async ({ request, locals, params }: import('./$types').RequestEvent) => { + const id = params.id; + if (!id) return fail(400, { error: "Missing id", form: "update" }); + const form = await request.formData(); + const Item = (form.get("Item") as string)?.trim() ?? ""; + const SKU = (form.get("SKU") as string)?.trim() ?? ""; + const Description = (form.get("Description") as string)?.trim() ?? ""; + const UOM = (form.get("UOM") as string)?.trim() ?? ""; + const Cost = parseNum(form.get("Cost")); + const Vendor = (form.get("Vendor") as string)?.trim() ?? ""; + const Catagory = (form.get("Catagory") as string)?.trim() ?? ""; + const Sub_Catagory = (form.get("Sub_Catagory") as string)?.trim() ?? ""; + const Has_Parent = parseBool(form.get("Has_Parent")); + const Parent = (form.get("Parent") as string)?.trim() ?? ""; + const Stock = parseBool(form.get("Stock")); + const Image = form.get("Image") as File | null; + const imageFile = Image && Image.size > 0 ? Image : undefined; + if (!Item) return fail(400, { error: "Item name is required", form: "update" }); + try { + const body: Record = { + Item, + SKU, + Description, + UOM: UOM || undefined, + Cost: Cost ?? 0, + Vendor, + Catagory: Catagory || undefined, + Sub_Catagory: Sub_Catagory || undefined, + Has_Parent: !!Has_Parent, + Parent: Parent || null, + Stock: !!Stock, + }; + if (imageFile) body.Image = imageFile; + await locals.pb.collection(COLLECTION).update(id, body); + } catch (e: unknown) { + const message = e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Update failed"; + return fail(500, { error: message, form: "update" }); + } + return { success: true }; + }, + + delete: async ({ locals, params }: import('./$types').RequestEvent) => { + const id = params.id; + if (!id) return fail(400, { error: "Missing id", form: "delete" }); + try { + await locals.pb.collection(COLLECTION).delete(id); + } catch (e: unknown) { + const message = e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Delete failed"; + return fail(500, { error: message, form: "delete" }); + } + throw redirect(303, "/items"); + }, +}; +;null as any as Actions; \ No newline at end of file diff --git a/.svelte-kit/types/src/routes/proxy+page.server.ts b/.svelte-kit/types/src/routes/proxy+page.server.ts index 96547889..028d2ed6 100644 --- a/.svelte-kit/types/src/routes/proxy+page.server.ts +++ b/.svelte-kit/types/src/routes/proxy+page.server.ts @@ -11,5 +11,5 @@ export const load = async ({ locals }: Parameters[0]) => { .catch(() => [] as StackqItem[]); const itemsForParent = list.map((i) => ({ id: i.id, Item: i.Item, SKU: i.SKU })); - return { itemsForParent }; + return { items: list, itemsForParent }; }; diff --git a/.svelte-kit/types/src/routes/transactions/$types.d.ts b/.svelte-kit/types/src/routes/transactions/$types.d.ts new file mode 100644 index 00000000..85606c53 --- /dev/null +++ b/.svelte-kit/types/src/routes/transactions/$types.d.ts @@ -0,0 +1,18 @@ +import type * as Kit from '@sveltejs/kit'; + +type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; +// @ts-ignore +type MatcherParam = M extends (param : string) => param is infer U ? U extends string ? U : string : string; +type RouteParams = { }; +type RouteId = '/transactions'; +type MaybeWithVoid = {} extends T ? T | void : T; +export type RequiredKeys = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T]; +type OutputDataShape = MaybeWithVoid> & Partial> & Record> +type EnsureDefined = T extends null | undefined ? {} : T; +type OptionalUnion, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude]?: never } & U : never; +export type Snapshot = Kit.Snapshot; +type PageParentData = EnsureDefined; + +export type PageServerData = null; +export type PageData = Expand; +export type PageProps = { params: RouteParams; data: PageData } \ No newline at end of file diff --git a/src/app.html b/src/app.html index 5e257da3..5794599d 100644 --- a/src/app.html +++ b/src/app.html @@ -3,7 +3,10 @@ - + + + + %sveltekit.head% diff --git a/src/lib/components/BarcodeScanDialog.svelte b/src/lib/components/BarcodeScanDialog.svelte new file mode 100644 index 00000000..bd0c4f3b --- /dev/null +++ b/src/lib/components/BarcodeScanDialog.svelte @@ -0,0 +1,185 @@ + + + + stopScan()} + > + + {title} + Scan a barcode or type a code to look up an item. + +
+
+ e.key === "Enter" && submitSearch()} + class="min-h-11 flex-1" + /> + +
+

or use camera below

+ {#if scanError} +

{scanError}

+ {:else if stream} + + {:else} +

Starting camera…

+ {/if} +
+ +
+
+
+
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 66d3d00c..73f8c3a4 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -10,5 +10,5 @@ export const load: PageServerLoad = async ({ locals }) => { .catch(() => [] as StackqItem[]); const itemsForParent = list.map((i) => ({ id: i.id, Item: i.Item, SKU: i.SKU })); - return { itemsForParent }; + return { items: list, itemsForParent }; }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index fb0d1bd4..581ef55d 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,29 +2,44 @@ import { browser } from "$app/environment"; import { Button } from "$lib/components/ui/button"; - let { data }: { data: { itemsForParent: { id: string; Item: string; SKU: string }[] } } = $props(); + let { data }: { data: { items: { id: string; SKU?: string }[]; itemsForParent: { id: string; Item: string; SKU: string }[] } } = $props(); {#if browser} {#await import("./HomeWithDialog.svelte")} -
+

Loading…

{:then { default: HomeWithDialog }} {/await} {:else} -
+

Stackq

+ +
Go to
diff --git a/src/routes/HomeWithDialog.svelte b/src/routes/HomeWithDialog.svelte index 49f60a26..78ce00c3 100644 --- a/src/routes/HomeWithDialog.svelte +++ b/src/routes/HomeWithDialog.svelte @@ -2,40 +2,99 @@ import { Button } from "$lib/components/ui/button"; import * as Dialog from "$lib/components/ui/dialog"; import ItemForm from "./items/ItemForm.svelte"; + import BarcodeScanDialog from "$lib/components/BarcodeScanDialog.svelte"; import { goto } from "$app/navigation"; + import { Camera, Package, Receipt, Wallet } from "@lucide/svelte"; let { data, }: { - data: { itemsForParent: { id: string; Item: string; SKU: string }[] }; + data: { + items: { id: string; SKU?: string }[]; + itemsForParent: { id: string; Item: string; SKU: string }[]; + }; } = $props(); let dialogOpen = $state(false); + let scanDialogOpen = $state(false); + const items = $derived(data?.items ?? []); const itemsForParent = $derived(data?.itemsForParent ?? []); - function onSuccess() { + function onItemSuccess() { dialogOpen = false; goto("/items"); } + + function onScanResult(value: string) { + const sku = value.trim(); + const found = items.find((i) => (i.SKU ?? "").trim() === sku); + if (found) goto("/items/" + found.id); + else goto("/items?q=" + encodeURIComponent(sku)); + } -
-

Stackq

+
+

Stackq

+ +
+

Quick actions

+ + + + + +
+ +
Go to
+
- + New item Add a new Stackq item. @@ -44,7 +103,9 @@ itemsForParent={itemsForParent} action="/items?/create" submitLabel="Create" - onSuccess={onSuccess} + onSuccess={onItemSuccess} /> + + diff --git a/src/routes/accounts/+page.svelte b/src/routes/accounts/+page.svelte new file mode 100644 index 00000000..e52bf9b5 --- /dev/null +++ b/src/routes/accounts/+page.svelte @@ -0,0 +1,28 @@ + + + + Accounts – Stackq + + +
+
+ + + +

Accounts

+
+ + + +

No accounts yet.

+

Accounts will appear here once this feature is available.

+ + + +
+
+
diff --git a/src/routes/items/+page.svelte b/src/routes/items/+page.svelte index 23903194..1f4a05e5 100644 --- a/src/routes/items/+page.svelte +++ b/src/routes/items/+page.svelte @@ -1,12 +1,14 @@ Items – Stackq -
-
-

Stackq Items

-
- {items.length} items - +
+
+
+ + + +

Stackq Items

+
+
+
+ + +
+ + {filteredItems.length}{filteredItems.length !== items.length ? ` of ${items.length}` : ""} items + +
+ + +
- - - - - - Image - Item - SKU - Description - UOM - Cost - Vendor - Catagory - Sub_Catagory - Has Parent - Parent - Stock - created - updated - Actions - - - - {#if items.length === 0} - - - No items. Create one or check PocketBase collection name and auth. - - - {:else} - {#each items as item (item.id)} - - - {#if imageUrl(item)} - - - - {:else} - - {/if} - - {item.Item ?? "—"} - {item.SKU ?? "—"} - - {item.Description ?? "—"} - - - {item.UOM ?? "—"} - - - {item.Cost != null ? Number(item.Cost).toLocaleString() : "—"} - - {item.Vendor ?? "—"} - - {#each asList(item.Catagory) as cat} - {cat} - {/each} - {#if asList(item.Catagory).length === 0} - - {/if} - - - {#each asList(item.Sub_Catagory) as sub} - {sub} - {/each} - {#if asList(item.Sub_Catagory).length === 0} - - {/if} - - - {#if item.Has_Parent} - Yes - {:else} - No - {/if} - - - {#if item.expand?.Parent} - {item.expand.Parent.Item ?? item.expand.Parent.SKU ?? item.Parent} - {:else if item.Parent} - {item.Parent} - {:else} - — - {/if} - - - {#if item.Stock} - Stock - {:else} - - {/if} - - - {formatDate(item.created)} - - - {formatDate(item.updated)} - - - - - - - - - openEdit(item)}> - - Edit - - - { - if (confirm("Delete this item? This cannot be undone.")) { - document.getElementById("delete-form-" + item.id)?.requestSubmit(); - } - }} - > - - Delete - - - - - - - {/each} - {/if} - - - - + {#if filteredItems.length === 0} + + +

+ {items.length === 0 + ? "No items. Create one or check PocketBase collection name and auth." + : "No items match your search."} +

+
+
+ {:else} +
+ {#each filteredItems as item (item.id)} + + +
+ {#if imageUrl(item)} + + + + {:else} + + {/if} +
+
+ {item.Item ?? "—"} + · {item.SKU ?? "—"} + {#if item.Description} + · {item.Description} + {/if} +
+
+ {item.UOM ?? "—"} + {item.Cost != null ? Number(item.Cost).toLocaleString() : "—"} + {#if item.Stock}Stock{/if} +
+ + + + + + + goto("/items/" + item.id)}> + + View + + openEdit(item)}> + + Edit + + + { + if (confirm("Delete this item? This cannot be undone.")) { + document.getElementById("delete-form-" + item.id)?.requestSubmit(); + } + }} + > + + Delete + + + + +
+
+ {/each} +
+ {/if} - + {editingItem ? "Edit item" : "New item"} @@ -265,4 +258,6 @@ /> + +
diff --git a/src/routes/items/[id]/+page.server.ts b/src/routes/items/[id]/+page.server.ts new file mode 100644 index 00000000..4d661077 --- /dev/null +++ b/src/routes/items/[id]/+page.server.ts @@ -0,0 +1,89 @@ +import { error, fail, redirect } from "@sveltejs/kit"; +import type { Actions, PageServerLoad } from "./$types"; +import type { StackqItem } from "$lib/types/items"; + +const COLLECTION = "Stackq_Items"; + +function parseNum(val: unknown): number | undefined { + if (val === "" || val === null || val === undefined) return undefined; + const n = Number(val); + return Number.isFinite(n) ? n : undefined; +} + +function parseBool(val: unknown): boolean { + return val === "on" || val === "true" || val === true; +} + +export const load: PageServerLoad = async ({ params, locals }) => { + const id = params.id; + if (!id) throw error(404, "Item not found"); + + try { + const item = await locals.pb + .collection(COLLECTION) + .getOne(id, { expand: "Parent" }); + const list = await locals.pb + .collection(COLLECTION) + .getFullList({ sort: "-created", expand: "Parent" }) + .catch(() => [] as StackqItem[]); + const itemsForParent = list.map((i) => ({ id: i.id, Item: i.Item, SKU: i.SKU })); + return { item, itemsForParent }; + } catch { + throw error(404, "Item not found"); + } +}; + +export const actions: Actions = { + update: async ({ request, locals, params }) => { + const id = params.id; + if (!id) return fail(400, { error: "Missing id", form: "update" }); + const form = await request.formData(); + const Item = (form.get("Item") as string)?.trim() ?? ""; + const SKU = (form.get("SKU") as string)?.trim() ?? ""; + const Description = (form.get("Description") as string)?.trim() ?? ""; + const UOM = (form.get("UOM") as string)?.trim() ?? ""; + const Cost = parseNum(form.get("Cost")); + const Vendor = (form.get("Vendor") as string)?.trim() ?? ""; + const Catagory = (form.get("Catagory") as string)?.trim() ?? ""; + const Sub_Catagory = (form.get("Sub_Catagory") as string)?.trim() ?? ""; + const Has_Parent = parseBool(form.get("Has_Parent")); + const Parent = (form.get("Parent") as string)?.trim() ?? ""; + const Stock = parseBool(form.get("Stock")); + const Image = form.get("Image") as File | null; + const imageFile = Image && Image.size > 0 ? Image : undefined; + if (!Item) return fail(400, { error: "Item name is required", form: "update" }); + try { + const body: Record = { + Item, + SKU, + Description, + UOM: UOM || undefined, + Cost: Cost ?? 0, + Vendor, + Catagory: Catagory || undefined, + Sub_Catagory: Sub_Catagory || undefined, + Has_Parent: !!Has_Parent, + Parent: Parent || null, + Stock: !!Stock, + }; + if (imageFile) body.Image = imageFile; + await locals.pb.collection(COLLECTION).update(id, body); + } catch (e: unknown) { + const message = e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Update failed"; + return fail(500, { error: message, form: "update" }); + } + return { success: true }; + }, + + delete: async ({ locals, params }) => { + const id = params.id; + if (!id) return fail(400, { error: "Missing id", form: "delete" }); + try { + await locals.pb.collection(COLLECTION).delete(id); + } catch (e: unknown) { + const message = e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Delete failed"; + return fail(500, { error: message, form: "delete" }); + } + throw redirect(303, "/items"); + }, +}; diff --git a/src/routes/items/[id]/+page.svelte b/src/routes/items/[id]/+page.svelte new file mode 100644 index 00000000..eb0433ca --- /dev/null +++ b/src/routes/items/[id]/+page.svelte @@ -0,0 +1,219 @@ + + + + {item?.Item ?? "Item"} – Stackq + + +{#if item} +
+
+ + + + + + +

{item.Item ?? "—"}

+ {#if browser} +
+ +
+ +
+
+ {/if} +
+ +
+ + +

Details

+
+ + {#if imageUrl(item)} +
+ + + +
+ {/if} +
+
+
SKU
+
{item.SKU ?? "—"}
+
+
+
Description
+
{item.Description ?? "—"}
+
+
+
UOM
+
{item.UOM ?? "—"}
+
+
+
Cost
+
{item.Cost != null ? Number(item.Cost).toLocaleString() : "—"}
+
+
+
Vendor
+
{item.Vendor ?? "—"}
+
+
+
Category
+
+ {#each asList(item.Catagory) as cat} + {cat} + {/each} + {#if asList(item.Catagory).length === 0}—{/if} +
+
+
+
Sub-category
+
+ {#each asList(item.Sub_Catagory) as sub} + {sub} + {/each} + {#if asList(item.Sub_Catagory).length === 0}—{/if} +
+
+
+
Has Parent
+
{item.Has_Parent ? "Yes" : "No"}
+
+ {#if item.Has_Parent && item.expand?.Parent} + + {/if} +
+
Stock
+
{item.Stock ? "Yes" : "No"}
+
+
+
Created
+
{formatDate(item.created)}
+
+
+
Updated
+
{formatDate(item.updated)}
+
+
+
+
+
+
+ + {#if browser} + + + + Edit item + Update the item below. + + (dialogOpen = false)} + /> + + + {/if} +{:else} +
+

Item not found.

+
+{/if} diff --git a/src/routes/layout.css b/src/routes/layout.css index a215062a..2c3af2de 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -104,5 +104,38 @@ } body { @apply bg-background text-foreground; + /* Safe area for notched devices */ + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); + padding-bottom: env(safe-area-inset-bottom); + } + /* Larger tap targets on touch devices (buttons only; links use .touch-target where needed) */ + @media (pointer: coarse) { + button:not([class*="size-"]) { + min-height: 44px; + } + } +} + +/* Use on icon-only links for 44px touch target */ +.touch-target { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 44px; + min-height: 44px; + padding: 0.5rem; +} + +/* Prevent iOS zoom on input focus (requires font-size >= 16px) */ +@media (max-width: 640px) { + input[type="text"], + input[type="search"], + input[type="number"], + input[type="email"], + input[type="password"], + select, + textarea { + font-size: 16px; } } diff --git a/src/routes/transactions/+page.svelte b/src/routes/transactions/+page.svelte new file mode 100644 index 00000000..b10d877a --- /dev/null +++ b/src/routes/transactions/+page.svelte @@ -0,0 +1,28 @@ + + + + Transactions – Stackq + + +
+
+ + + +

Transactions

+
+ + + +

No transactions yet.

+

Transactions will appear here once this feature is available.

+ + + +
+
+