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.

+ + + +
+
+