This commit is contained in:
2025-03-19 05:20:41 -04:00
commit 97402e1efc
103 changed files with 7571 additions and 0 deletions

179
src/app.css Normal file
View File

@ -0,0 +1,179 @@
@import "tailwindcss";
@plugin 'tailwindcss-animate';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
--color-sidebar: hsl(var(--sidebar-background));
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
--color-sidebar-primary: hsl(var(--sidebar-primary));
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
--color-sidebar-accent: hsl(var(--sidebar-accent));
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
--color-sidebar-border: hsl(var(--sidebar-border));
--color-sidebar-ring: hsl(var(--sidebar-ring));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@layer utilities {
body {
font-family: Arial, Helvetica, sans-serif;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

13
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="initial-scale=1, viewport-fit=cover" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

14
src/hooks.client.ts Normal file
View File

@ -0,0 +1,14 @@
import type { ClientInit } from '@sveltejs/kit';
import { SafeArea } from '@capacitor-community/safe-area';
export const init: ClientInit = async () => {
await SafeArea.enable({
config: {
customColorsForSystemBars: true,
statusBarColor: '#00000000', // transparent
statusBarContent: 'light',
navigationBarColor: '#00000000', // transparent
navigationBarContent: 'light',
},
});
};

View File

@ -0,0 +1,76 @@
<script lang="ts">
import { Haptics, ImpactStyle } from '@capacitor/haptics';
let {
focused = $bindable(false),
scanning = $bindable(false),
onscan
}: {
focused?: boolean;
scanning?: boolean;
onscan: (value: string) => void;
} = $props();
// Data for manual inputs
let inputElement: HTMLInputElement | null = null;
let unfocusTimeout: NodeJS.Timeout;
async function onKeydown(event: KeyboardEvent) {
// If we this is a manual input
if (focused) {
// Auto unfocus after 5 seconds of inactivity
clearTimeout(unfocusTimeout);
unfocusTimeout = setTimeout(() => {
inputElement?.blur();
}, 10000);
return;
}
// Check if this is an indicator that scanning has been initiated
if (event.key == 'Unidentified') {
scanning = true;
return;
}
// Send the key to onscan
if ((event.key == 'Tab' || event.key == 'Enter' || event.key == 'Backspace' || event.key == "Home" || event.key == "Delete")) {
// Prevent tab or enter from doing anything
event.preventDefault();
// Send key as text
onscan(event.key);
} else if (event.key.length == 1) {
onscan(event.key);
}
Haptics.impact({ style: ImpactStyle.Light });
}
async function onKeyup(event: KeyboardEvent) {
if (event.key == 'Unidentified' && scanning) {
scanning = false;
}
}
function focusIn(e: any) {
if (e.target?.nodeName == 'INPUT') {
focused = true;
inputElement = e.target;
Haptics.selectionStart();
}
}
function focusOut() {
focused = false;
inputElement = null;
Haptics.selectionEnd();
}
</script>
<svelte:window
on:keydown={onKeydown}
on:keyup={onKeyup}
on:focusin={focusIn}
on:focusout={focusOut}
/>

View File

@ -0,0 +1,74 @@
<script lang="ts" module>
import type { WithElementRef } from "bits-ui";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
import { cn } from "$lib/utils.js";
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{href}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View File

@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};

View File

@ -0,0 +1,38 @@
<script lang="ts">
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import X from "lucide-svelte/icons/x";
import type { Snippet } from "svelte";
import * as Dialog from "./index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
} = $props();
</script>
<Dialog.Portal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
className
)}
{...restProps}
>
{@render children?.()}
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal>

View File

@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.DescriptionProps = $props();
</script>
<DialogPrimitive.Description
bind:ref
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

View File

@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.OverlayProps = $props();
</script>
<DialogPrimitive.Overlay
bind:ref
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...restProps}
/>

View File

@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.TitleProps = $props();
</script>
<DialogPrimitive.Title
bind:ref
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...restProps}
/>

View File

@ -0,0 +1,37 @@
import { Dialog as DialogPrimitive } from "bits-ui";
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
const Root = DialogPrimitive.Root;
const Trigger = DialogPrimitive.Trigger;
const Close = DialogPrimitive.Close;
const Portal = DialogPrimitive.Portal;
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};

View File

@ -0,0 +1,7 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};

View File

@ -0,0 +1,22 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(),
class: className,
...restProps
}: WithElementRef<HTMLInputAttributes> = $props();
</script>
<input
bind:this={ref}
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
bind:value
{...restProps}
/>

View File

@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View File

@ -0,0 +1,19 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: LabelPrimitive.RootProps = $props();
</script>
<LabelPrimitive.Root
bind:ref
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...restProps}
/>

View File

@ -0,0 +1 @@
export { default as Toaster } from "./sonner.svelte";

View File

@ -0,0 +1,19 @@
<script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
let restProps: SonnerProps = $props();
</script>
<Sonner
theme={"light"}
class="toaster group"
toastOptions={{
classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...restProps}
/>

17
src/lib/cutils.ts Normal file
View File

@ -0,0 +1,17 @@
import { Filesystem } from "@capacitor/filesystem";
import type { GetUriOptions, MkdirOptions } from "@capacitor/filesystem";
export async function checkFileExists(getUriOptions: GetUriOptions): Promise<boolean> {
try {
await Filesystem.stat(getUriOptions);
return true;
} catch (_) {
return false;
}
}
export async function createDirectory(mkdirOptions: MkdirOptions) {
try {
await Filesystem.mkdir(mkdirOptions);
} catch (_) { }
}

1
src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

70
src/lib/utils.ts Normal file
View File

@ -0,0 +1,70 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + key + ":" + style[key] + ";";
}, "");
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform:
transform +
"translate3d(" +
x +
"px, " +
y +
"px, 0) scale(" +
scale +
")",
opacity: t,
});
},
easing: cubicOut,
};
};

30
src/routes/+layout.svelte Normal file
View File

@ -0,0 +1,30 @@
<script lang="ts">
import '../app.css';
import { Toaster } from '$lib/components/ui/sonner';
let { children } = $props();
</script>
<Toaster />
<div class="flex flex-col h-screen">
<div id="header"></div>
<div class="overflow-y-auto">
{@render children()}
</div>
<div id="footer"></div>
</div>
<style>
#header {
height: var(--safe-area-inset-top);
background-color: white;
}
#footer {
height: var(--safe-area-inset-bottom);
background-color: white;
}
</style>

1
src/routes/+layout.ts Normal file
View File

@ -0,0 +1 @@
export const prerender = true;

316
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,316 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Button, buttonVariants } from '$lib/components/ui/button';
import ScanCapture from '$lib/ScanCapture.svelte';
import { cn } from '$lib/utils';
import { toast } from 'svelte-sonner';
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
import { checkFileExists, createDirectory } from '$lib/cutils';
import { Haptics, ImpactStyle } from '@capacitor/haptics';
import { SvelteMap } from 'svelte/reactivity';
import { onMount } from 'svelte';
let showdialog = $state(false);
let focused = $state(false);
let confirmation = $state(false);
const formValues: SvelteMap<string, string | undefined> = new SvelteMap();
let sessioncount = $state(0);
let daycount = $state(0);
let selected = $state('service');
function onscan(value: string) {
// Set as empty if undefined
if (formValues.get(selected) == undefined) {
formValues.set(selected, '');
}
if (value == 'Home') {
showdialog = !showdialog;
return;
}
if (value == 'Delete') {
reset();
toast.success('Cleared');
return;
}
// Advance to next input field
if (value == 'Tab' || value == 'Enter') {
if (selected == 'service') {
selected = 'serial';
} else if (selected == 'serial') {
selected = 'description';
} else if (selected == 'description') {
selected = 'manufacturer';
} else if (selected == 'manufacturer') {
selected = 'model';
} else if (selected == 'model') {
selected = 'condition';
} else if (confirmation == false) {
confirmation = true;
} else if (confirmation == true) {
save();
}
return;
}
// Cancel confirmation
if (value == 'Backspace' && confirmation == true) {
confirmation = false;
return;
}
// Go back to the previous input field
if (value == 'Backspace' && formValues.get(selected)?.length == 0) {
if (selected == 'condition') {
selected = 'model';
} else if (selected == 'model') {
selected = 'manufacturer';
} else if (selected == 'manufacturer') {
selected = 'description';
} else if (selected == 'description') {
selected = 'serial';
} else if (selected == 'serial') {
selected = 'service';
}
return;
}
// Subtract one from the current input
if (value == 'Backspace') {
formValues.set(selected, formValues.get(selected)?.slice(0, -1));
return;
}
// Append current input with new text
formValues.set(selected, formValues.get(selected) + value);
}
async function onsubmit(event: SubmitEvent & { currentTarget: EventTarget & HTMLFormElement }) {
event.preventDefault();
Haptics.impact({ style: ImpactStyle.Medium });
confirmation = true;
}
async function save() {
Haptics.impact({ style: ImpactStyle.Medium });
// Create directory
await createDirectory({
path: 'inventory',
directory: Directory.Documents
});
const filename = `inventory/${new Date().toLocaleDateString().replaceAll('/', '-')}.csv`;
// Check if CSV file exists
const exists = await checkFileExists({
path: filename,
directory: Directory.Documents
});
// Create file if it does not exist
if (!exists) {
await Filesystem.writeFile({
path: filename,
data: '',
directory: Directory.Documents,
encoding: Encoding.UTF8
});
}
// Append data to file
await Filesystem.appendFile({
path: filename,
data: `${formValues.get('service')},${formValues.get('serial')},${formValues.get('manufacturer')},${formValues.get('model')},${formValues.get('description')},${formValues.get('condition')},${new Date().toLocaleDateString()}\n`,
directory: Directory.Documents,
encoding: Encoding.UTF8
});
sessioncount = sessioncount + 1;
daycount = daycount + 1;
toast.success('Added item to inventory');
// Reset values
reset();
}
function reset() {
confirmation = false;
selected = 'service';
formValues.set('serial', '');
formValues.set('description', '');
formValues.set('service', '');
formValues.set('manufacturer', '');
formValues.set('model', '');
formValues.set('condition', '');
}
onMount(async () => {
const filename = `inventory/${new Date().toLocaleDateString().replaceAll('/', '-')}.csv`;
// Check if CSV file exists
const exists = await checkFileExists({
path: filename,
directory: Directory.Documents
});
if (!exists) {
return;
}
// Read CSV file
const file = await Filesystem.readFile({
path: filename,
directory: Directory.Documents,
encoding: Encoding.UTF8
});
// Parse CSV file
if (typeof file.data !== 'string') {
return;
}
const lines = file.data.split("\n");
daycount = lines.length - 1;
});
</script>
<ScanCapture {onscan} bind:focused />
<Dialog.Root bind:open={showdialog}>
<Dialog.Content
onEscapeKeydown={(e) => {
e.preventDefault();
}}
>
<Dialog.Header>
<Dialog.Title>Stats</Dialog.Title>
<div class="grid grid-cols-2 gap-2 py-4">
<p>day count:</p>
<p>{daycount}</p>
<p>session count:</p>
<p>{sessioncount}</p>
</div>
<Button
type="button"
class={cn(buttonVariants({ variant: 'outline' }), 'text-black cursor-pointer')}
onclick={() => {
sessioncount = 0;
toast.success('Session count reset');
}}>Reset session count</Button
>
</Dialog.Header>
</Dialog.Content>
</Dialog.Root>
<form class="flex flex-col gap-4 py-2 px-3 py-4" {onsubmit}>
<div class="flex gap-2 items-center">
<Label class="basis-1/4" for="service">Asset Tag</Label>
<Input
id="service"
name="service"
type="text"
disabled={confirmation}
class={cn(!focused && selected == 'service' && 'ring-offset-2 ring-2 ring-green-600')}
bind:value={() => formValues.get('service'), (v) => formValues.set('service', v)}
/>
</div>
<div class="flex gap-2 items-center">
<Label class="basis-1/4" for="serial">Serial #</Label>
<Input
id="serial"
name="serial"
type="text"
disabled={confirmation}
class={cn(!focused && selected == 'serial' && 'ring-offset-2 ring-2 ring-green-600')}
bind:value={() => formValues.get('serial'), (v) => formValues.set('serial', v)}
/>
</div>
<div class="flex gap-2 items-center">
<Label class="basis-1/4" for="description">Machine Type</Label>
<Input
id="description"
name="description"
type="text"
disabled={confirmation}
class={cn(!focused && selected == 'description' && 'ring-offset-2 ring-2 ring-green-600')}
bind:value={() => formValues.get('description'), (v) => formValues.set('description', v)}
/>
</div>
<div class="flex gap-2 items-center">
<Label class="basis-1/4" for="manufacturer">Maker</Label>
<Input
id="manufacturer"
name="manufacturer"
type="text"
disabled={confirmation}
class={cn(!focused && selected == 'manufacturer' && 'ring-offset-2 ring-2 ring-green-600')}
bind:value={() => formValues.get('manufacturer'), (v) => formValues.set('manufacturer', v)}
/>
</div>
<div class="flex gap-2 items-center">
<Label class="basis-1/4" for="model">Model</Label>
<Input
id="model"
name="model"
type="text"
disabled={confirmation}
class={cn(!focused && selected == 'model' && 'ring-offset-2 ring-2 ring-green-600')}
bind:value={() => formValues.get('model'), (v) => formValues.set('model', v)}
/>
</div>
<div class="flex gap-2 items-center">
<Label class="basis-1/4" for="condition">Condition</Label>
<Input
id="condition"
name="condition"
type="text"
disabled={confirmation}
class={cn(!focused && selected == 'condition' && 'ring-offset-2 ring-2 ring-green-600')}
bind:value={() => formValues.get('condition'), (v) => formValues.set('condition', v)}
/>
</div>
{#if confirmation}
<div class="flex flex-col gap-2">
<Button
type="button"
class={cn(buttonVariants({ variant: 'outline' }), 'text-black cursor-pointer')}
onclick={() => {
reset();
toast.success('Canceled');
}}>Cancel</Button
>
<Button type="button" class="cursor-pointer" onclick={() => save()}>Confirm</Button>
</div>
{:else}
<div class="flex flex-col gap-2">
<div class="flex justify-center gap-1">
<Button
type="button"
class={cn(buttonVariants({ variant: 'outline' }), 'text-black cursor-pointer grow')}
onclick={() => {
showdialog = !showdialog;
}}>Stats</Button
>
<Button
type="button"
class={cn(buttonVariants({ variant: 'outline' }), 'text-black cursor-pointer grow')}
onclick={() => {
reset();
toast.success('Cleared');
}}>Reset</Button
>
</div>
<Button type="submit" class="cursor-pointer">Submit</Button>
</div>
{/if}
</form>