feat: add ui block while brewing

- fix case value show invalid after come back from brewing finish (topping not save)
- add update machine status

Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
pakintada@gmail.com 2026-04-29 17:41:36 +07:00
parent 4cb98f8672
commit e25881d016
16 changed files with 371 additions and 17 deletions

View file

@ -4,6 +4,9 @@
import Label from '$lib/components/ui/label/label.svelte';
import Input from '$lib/components/ui/input/input.svelte';
import Button from '$lib/components/ui/button/button.svelte';
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
import { Badge } from '$lib/components/ui/badge/index';
import * as Item from '$lib/components/ui/item/index';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import RecipelistTable from './recipelist-table.svelte';
@ -17,7 +20,7 @@
materialFromServerQuery
} from '$lib/core/stores/recipeStore';
import { generateIcing } from '$lib/helpers/icingGen';
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
import { getMachineStatus, machineInfoStore } from '$lib/core/stores/machineInfoStore';
import MachineInfo from '../machine-info.svelte';
import { v4 as uuidv4 } from 'uuid';
import { addNotification } from '$lib/core/stores/noti';
@ -212,13 +215,13 @@
</Tabs.List>
{#if refPage === 'brew'}
<div>
<Button type="button" variant="default" onclick={() => resetAllPendingCmds()}
>Force Reset Brewing</Button
>
<Button type="button" variant="default" onclick={() => saveRecipe()}>Save</Button>
<Button type="button" variant="default" onclick={async () => sendTriggerBrewNow()}
>Test Brew</Button
>
{#if $machineInfoStore?.status == 'IDLE' || $machineInfoStore?.status == ''}
<Button type="button" variant="default" onclick={async () => sendTriggerBrewNow()}
>Test Brew</Button
>
{/if}
</div>
{/if}
</div>
@ -244,12 +247,26 @@
</Tabs.Content>
<Tabs.Content value="details">
<RecipelistTable
data={recipeListMatState}
{columns}
onStateChange={checkChanges}
{productCode}
/>
<!-- hide by machine state -->
{#if $machineInfoStore?.status == 'IDLE' || $machineInfoStore?.status == ''}
<RecipelistTable
data={recipeListMatState}
{columns}
onStateChange={checkChanges}
{productCode}
/>
{:else}
<Card.Root>
<Card.Content>
<div class="grid h-[60vh] w-full place-items-center rounded-md border">
<div class="m-4 flex flex-row gap-2">
<Spinner />Machine is working. Please wait for a moment.
</div>
</div>
</Card.Content>
</Card.Root>
{/if}
</Tabs.Content>
</Tabs.Root>
</div>

View file

@ -11,6 +11,8 @@
import { get } from 'svelte/store';
import * as Dialog from '$lib/components/ui/dialog/index';
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
import { Badge } from '$lib/components/ui/badge/index';
import RecipeDetail from './recipe-details/recipe-detail.svelte';
import { MenuStatus, matchMenuStatus } from '$lib/core/types/menuStatus';
@ -23,6 +25,7 @@
import { sendMessage } from '$lib/core/handlers/ws_messageSender';
import { auth } from '$lib/core/stores/auth';
import { departmentStore } from '$lib/core/stores/departments';
import { getMachineStatus, machineInfoStore } from '$lib/core/stores/machineInfoStore';
const isDesktop = new MediaQuery('(min-width: 768px)');
@ -227,6 +230,9 @@
console.log('sending brewing payload', ready_to_send_brew);
// save topping
latestRecipeToppingData.set(ready_to_send_brew[0]['ToppingSet']);
await sendToAndroid({
type: 'brew',
payload: {
@ -285,7 +291,16 @@
>
<Dialog.Content class="sm:max-w-3/4">
<Dialog.Header>
<Dialog.Title>Edit Recipe {productCode}</Dialog.Title>
<Dialog.Title
>Edit Recipe {productCode}
<Badge class="m-2">
{#if $machineInfoStore?.status == 'IDLE' || $machineInfoStore?.status == ''}
Ready
{:else}
<Spinner /> Brewing
{/if}
</Badge>
</Dialog.Title>
<Dialog.Description
>Make changes to selected menu here. Click "save" when done or "test" for testing with
connected machine

View file

@ -0,0 +1,34 @@
import Root from "./item.svelte";
import Group from "./item-group.svelte";
import Separator from "./item-separator.svelte";
import Header from "./item-header.svelte";
import Footer from "./item-footer.svelte";
import Content from "./item-content.svelte";
import Title from "./item-title.svelte";
import Description from "./item-description.svelte";
import Actions from "./item-actions.svelte";
import Media from "./item-media.svelte";
export {
Root,
Group,
Separator,
Header,
Footer,
Content,
Title,
Description,
Actions,
Media,
//
Root as Item,
Group as ItemGroup,
Separator as ItemSeparator,
Header as ItemHeader,
Footer as ItemFooter,
Content as ItemContent,
Title as ItemTitle,
Description as ItemDescription,
Actions as ItemActions,
Media as ItemMedia,
};

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-actions"
class={cn("gap-2 flex items-center", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-content"
class={cn(
"gap-1 group-data-[size=xs]/item:gap-0 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="item-description"
class={cn(
"text-muted-foreground text-left text-sm leading-normal group-data-[size=xs]/item:text-xs [&>a:hover]:text-primary line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...restProps}
>
{@render children?.()}
</p>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-footer"
class={cn("gap-2 flex basis-full items-center justify-between", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
role="list"
data-slot="item-group"
class={cn("gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2 group/item-group flex w-full flex-col", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-header"
class={cn("gap-2 flex basis-full items-center justify-between", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,42 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const itemMediaVariants = tv({
base: "gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none",
variants: {
variant: {
default: "bg-transparent",
icon: "[&_svg:not([class*='size-'])]:size-4",
image: "size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
});
export type ItemMediaVariant = VariantProps<typeof itemMediaVariants>["variant"];
</script>
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
variant = "default",
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { variant?: ItemMediaVariant } = $props();
</script>
<div
bind:this={ref}
data-slot="item-media"
data-variant={variant}
class={cn(itemMediaVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
...restProps
}: ComponentProps<typeof Separator> = $props();
</script>
<Separator
bind:ref
data-slot="item-separator"
orientation="horizontal"
class={cn("my-2", className)}
{...restProps}
/>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="item-title"
class={cn("gap-2 text-sm leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,61 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const itemVariants = tv({
base: "[a]:hover:bg-muted rounded-lg border text-sm group/item focus-visible:border-ring focus-visible:ring-ring/50 flex w-full flex-wrap items-center transition-colors duration-100 outline-none focus-visible:ring-[3px] [a]:transition-colors",
variants: {
variant: {
default: "border-transparent",
outline: "border-border",
muted: "bg-muted/50 border-transparent",
},
size: {
default: "gap-2.5 px-3 py-2.5",
sm: "gap-2.5 px-3 py-2.5",
xs: "gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ItemSize = VariantProps<typeof itemVariants>["size"];
export type ItemVariant = VariantProps<typeof itemVariants>["variant"];
</script>
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
class: className,
child,
variant,
size,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
variant?: ItemVariant;
size?: ItemSize;
} = $props();
const mergedProps = $derived({
class: cn(itemVariants({ variant, size }), className),
"data-slot": "item",
"data-variant": variant,
"data-size": size,
...restProps,
});
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div bind:this={ref} {...mergedProps}>
{@render mergedProps.children?.()}
</div>
{/if}

View file

@ -14,7 +14,9 @@
bind:ref
data-slot={dataSlot}
class={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:min-h-full data-[orientation=vertical]:w-px",
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
// this is different in shadcn/ui but self-stretch breaks things for us
"data-[orientation=vertical]:h-full",
className
)}
{...restProps}

View file

@ -1,3 +1,4 @@
import { updateMachineStatus } from '../stores/machineInfoStore';
import { addNotification } from '../stores/noti';
type AdbPayload = { type: string; payload: any };
@ -36,6 +37,7 @@ async function handleAdbPayload(raw_payload: string) {
console.log('current state', curr, 'next state', next);
addNotification('INFO:Machine Status Updated, ' + next);
updateMachineStatus(next);
}
break;
case 'error':

View file

@ -1,4 +1,19 @@
import type { MachineInfo } from '$lib/models/machineInfo.model';
import { writable } from 'svelte/store';
import { get, writable } from 'svelte/store';
export const machineInfoStore = writable<MachineInfo | undefined>();
const machineInfoStore = writable<MachineInfo | undefined>();
function updateMachineStatus(new_status: string) {
let current = get(machineInfoStore);
if (current) {
current.status = new_status;
machineInfoStore.set(current);
}
}
function getMachineStatus() {
return get(machineInfoStore)?.status;
}
export { machineInfoStore, updateMachineStatus, getMachineStatus };