This commit is contained in:
pakintada@gmail.com 2026-02-17 14:30:02 +07:00
commit 451223816b
338 changed files with 9938 additions and 0 deletions

View file

@ -0,0 +1,5 @@
<script lang="ts">
import { page } from '$app/state';
</script>
<h1>{page.status} {page.error?.message}</h1>

View file

@ -0,0 +1,14 @@
import { auth } from '$lib/core/stores/auth.js';
import { departmentStore } from '$lib/core/stores/departments.ts';
import { redirect } from '@sveltejs/kit';
import { get } from 'svelte/store';
export async function load({ cookies, url }) {
if (!cookies.get('logged_in')) {
redirect(303, `/login?redirectTo=${url.pathname}`);
}
if (url.pathname.includes('recipe') && !cookies.get('department')) {
redirect(303, `/departments`);
}
}

View file

@ -0,0 +1,30 @@
<!-- navbar select menus -->
<script lang="ts">
import favicon from '$lib/assets/favicon.svg';
import AppAccountSelect from '$lib/components/app-account-select.svelte';
import AppSidebar from '$lib/components/app-sidebar.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index';
import '../layout.css';
import ErrorLayout from '$lib/components/error-layout.svelte';
import { sidebarStore } from '$lib/core/stores/sidebar';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
<link href="https://fonts.googleapis.com/css2?family=Roboto+Flex" rel="stylesheet" />
<title>Taobin Management Tools</title>
</svelte:head>
<Sidebar.Provider
onOpenChange={(open) => {
sidebarStore.set(open);
}}
>
<AppSidebar />
<main class="h-screen w-screen overflow-hidden">
<Sidebar.Trigger />
{@render children()}
</main>
</Sidebar.Provider>

View file

@ -0,0 +1,8 @@
<script lang="ts">
import Dashboard from "$lib/components/dashboard.svelte";
</script>
<Dashboard/>

View file

@ -0,0 +1,14 @@
<script lang="ts">
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
<link href="https://fonts.googleapis.com/css2?family=Roboto+Flex" rel="stylesheet" />
<title>Taobin Management Tools</title>
</svelte:head>
{@render children()}

View file

@ -0,0 +1,67 @@
<script lang="ts">
import { asset } from '$app/paths';
import { permission as currentPerms } from '$lib/core/stores/permissions';
import { get } from 'svelte/store';
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
import { departmentStore } from '$lib/core/stores/departments';
import { socketStore } from '$lib/core/stores/websocketStore';
import { goto } from '$app/navigation';
import { addNotification } from '$lib/core/stores/noti';
import { browser } from '$app/environment';
import { setCookieOnNonBrowser } from '$lib/helpers/cookie';
let enabledAccessibleCountries: string[] = $state([]);
function onCountrySelected(cnt: string) {
departmentStore.set(cnt);
if (browser && 'cookieStore' in window) cookieStore.set('department', cnt);
else setCookieOnNonBrowser('department', cnt);
addNotification(`INFO:Selected ${cnt}`);
setTimeout(async () => {
console.log(get(departmentStore));
await goto('/recipe/overview');
}, 1000);
}
// read or write permission
let userCurrentPerms = get(currentPerms).filter(
(x) => x.startsWith('document.read') || x.startsWith('document.write')
);
// show country by enabled `document.read.{country}`
enabledAccessibleCountries = userCurrentPerms
.filter((x) => x.startsWith('document.read'))
.map((x) => x.split('.')[2]);
// update every 30s
if (enabledAccessibleCountries.length == 0) {
setTimeout(() => {
// read or write permission
let userCurrentPerms = get(currentPerms).filter(
(x) => x.startsWith('document.read') || x.startsWith('document.write')
);
// show country by enabled `document.read.{country}`
enabledAccessibleCountries = userCurrentPerms
.filter((x) => x.startsWith('document.read'))
.map((x) => x.split('.')[2]);
}, 1000);
}
</script>
<h1 class="m-8 text-4xl font-bold">Country Selection</h1>
<p class="m-8 text-muted-foreground">Select country to view/edit recipe</p>
{#if enabledAccessibleCountries.length == 0}
<div class="flex h-max w-max items-center justify-center">
<Spinner class="size-48" />
</div>
{:else}
<div class="m-8 grid grid-flow-dense grid-cols-5 gap-4">
{#each enabledAccessibleCountries as country}
<div>
<button class="hover:cursor-pointer" onclick={() => onCountrySelected(country)}>
<img src={asset(`/departments/logo/${country}_plate.png`)} alt={country} loading="lazy" />
</button>
</div>
{/each}
</div>
{/if}

View file

@ -0,0 +1,14 @@
<script lang="ts">
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
<link href="https://fonts.googleapis.com/css2?family=Roboto+Flex" rel="stylesheet" />
<title>Taobin Management Tools</title>
</svelte:head>
{@render children()}

View file

@ -0,0 +1,138 @@
<script lang="ts">
import RecipeModuleBtn from "$lib/assets/modules/recipe_btn.png";
import MachineInspectBtn from "$lib/assets/modules/monitoring_btn.png";
import {animate, JSAnimation, remove as removeAnime, stagger} from "animejs";
import { goto } from "$app/navigation";
import Button from "$lib/components/ui/button/button.svelte";
import ArrowRight from '@lucide/svelte/icons/arrow-right';
import { permission as currentPermissions } from "$lib/core/stores/permissions";
import { get } from "svelte/store";
let recipeModBtn = $state<HTMLElement | null>(null);
let monitorModBtn = $state<HTMLElement | null>(null);
let gotoDashboardBtn = $state<HTMLElement | null>(null);
let animationPulseGoto: JSAnimation;
setInterval(() => {
if(gotoDashboardBtn && !animationPulseGoto){
animationPulseGoto = animate(gotoDashboardBtn, {
opacity: [0.8, 1], // Slight pulse in opacity
duration: 800, // Duration of one pulse
loop: true, // Repeat forever
alternate: true,
delay: stagger(100),
ease: 'inOutQuad',
boxShadow: [
'0 0 0px 0 none',
'0 0 0px 7px lightblue',
],
});
}
}, 1000);
let perms = get(currentPermissions);
</script>
<div class="flex h-screen items-center justify-center">
<div class="columns-md place-content-center gap-8">
<h1 class="text-center font-bold text-4xl m-8">Module Selection</h1>
<div class="flex justify-between items-center">
<!-- need permission `document` -->
{#if perms.filter((x) => x.startsWith("document.read") || x.startsWith("document.write")).length > 0}
<button
class="button"
id="recipe_mod_btn"
bind:this={recipeModBtn}
onclick={() => goto('/departments')}
onmouseenter={() => {
if(recipeModBtn){
animate(recipeModBtn, {
scale: 1.1,
duration: 300,
ease: "inOutSine"
});
}
}}
onmouseleave={() => {
if(recipeModBtn){
animate(recipeModBtn, {
scale: 1.0,
duration: 200,
ease: "inOutSine"
});
}
}}>
<img
src={RecipeModuleBtn}
alt="Recipes"
loading="lazy"
/>
</button>
{/if}
<!-- need permission `tools` -->
{#if perms.filter((x) => x.startsWith("tools")).length > 0}
<button
class="button"
id="monitor_mod_btn"
bind:this={monitorModBtn}
onclick={() => goto('/tools')}
onmouseenter={() => {
if(monitorModBtn){
animate(monitorModBtn, {
scale: 1.1,
duration: 300,
ease: "inOutSine"
});
}
}}
onmouseleave={() => {
if(monitorModBtn){
animate(monitorModBtn, {
scale: 1.0,
duration: 200,
ease: "inOutSine"
});
}
}}>
<img
src={MachineInspectBtn}
alt="Recipes"
loading="lazy"
/>
</button>
{/if}
</div>
{#if perms.filter((x) => x.startsWith("document.read") || x.startsWith("document.write") || x.startsWith("tools")).length == 0}
<p class="text-center text-2xl m-8">No modules are available.
Please check your account with admin.</p>
{/if}
<div class="flex justify-center items-center m-16">
<button
id="goto_dash_btn"
class="focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-primary text-primary-foreground shadow-xs hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 rounded-full h-9 px-4 py-2 has-[>svg]:px-3"
bind:this={gotoDashboardBtn}
onclick={() => goto('/dashboard')}
>
Go to Dashboard <ArrowRight size={32}/>
</button>
</div>
</div>
</div>

View file

@ -0,0 +1,14 @@
import { getRecipes } from '$lib/core/client/server';
import { recipeData } from '$lib/core/stores/recipeStore';
import { get } from 'svelte/store';
export async function load({ cookies, params }) {
let dep = cookies.get('department');
console.log('load recipe ', dep);
let recipes = await getRecipes();
recipes = get(recipeData);
return {
recipes
};
}

View file

@ -0,0 +1,63 @@
<script lang="ts">
import Button from '$lib/components/ui/button/button.svelte';
import Input from '$lib/components/ui/input/input.svelte';
import { SearchIcon } from '@lucide/svelte/icons';
import DataTable from './data-table.svelte';
import { columns, type RecipeOverview } from './columns';
import { onDestroy, onMount } from 'svelte';
import { loadRecipe, recipeData } from '$lib/core/stores/recipeStore.js';
import { sendMessage } from '$lib/core/handlers/ws_messageSender.js';
import { auth } from '$lib/core/stores/auth.js';
import { get } from 'svelte/store';
import { getRecipes } from '$lib/core/client/server.js';
let data: { recipes: RecipeOverview[] } = $state({
recipes: []
});
let unsubRecipeData = recipeData.subscribe((rd) => {
if (rd) {
data.recipes = rd == null ? [] : rd;
}
});
onMount(async () => {
// do load recipe
// loadRecipe();
await getRecipes();
});
onDestroy(() => {
unsubRecipeData();
});
</script>
<div class="mx-8 flex">
<!-- header -->
<div class="w-full">
<div class="mb-4 flex items-center justify-between">
<div>
<h1 class="m-8 text-4xl font-bold">Overview</h1>
<p class="mx-8 my-0 text-muted-foreground">
Display menus from the current selected country
</p>
</div>
<div class="mx-8 my-4 flex gap-2">
<Button variant="default">+ Create Menu</Button>
</div>
</div>
<!-- search bar -->
<!-- <div class="mx-4 my-8 flex w-full items-center justify-center gap-2">
<SearchIcon />
<Input type="text" placeholder="Search by id, product code, name or material" class="" />
</div> -->
<!-- filter -->
<!-- table -->
<div class="w-full overflow-auto">
<DataTable data={data.recipes} refPage="overview" {columns} />
</div>
</div>
</div>

View file

@ -0,0 +1,92 @@
import { renderComponent } from '$lib/components/ui/data-table';
import type { ColumnDef } from '@tanstack/table-core';
import DataTableTagsBadge from './data-table-tags-badge.svelte';
import DataTableHeader from './data-table-header.svelte';
import type { FilterFn } from '@tanstack/table-core';
import { rankItem } from '@tanstack/match-sorter-utils';
import DataTableActions from './data-table-actions.svelte';
import { get } from 'svelte/store';
import { referenceFromPage } from '$lib/core/stores/recipeStore';
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
const itemRank = rankItem(row.getValue(columnId), value);
addMeta({ itemRank });
return itemRank.passed;
};
export type RecipeOverview = {
productCode: string;
name: string;
description: string;
tags: string;
status: 'ready' | 'obsolete' | 'drafted' | 'pending/online' | 'pending/offline';
};
export const columns: ColumnDef<RecipeOverview>[] = [
{
accessorKey: 'productCode',
header: ({ column }) =>
renderComponent(DataTableHeader, {
onclick: column.getToggleSortingHandler(),
data: 'Product Code'
}),
enableGlobalFilter: true,
filterFn: 'includesString'
},
{
accessorKey: 'name',
header: ({ column }) =>
renderComponent(DataTableHeader, {
onclick: column.getToggleSortingHandler(),
data: 'Name'
}),
enableGlobalFilter: true,
filterFn: 'includesString'
},
{
accessorKey: 'description',
header: ({ column }) =>
renderComponent(DataTableHeader, {
onclick: column.getToggleSortingHandler(),
data: 'Description'
}),
enableGlobalFilter: true,
filterFn: 'includesString'
},
{
accessorKey: 'tags',
header: ({ column }) =>
renderComponent(DataTableHeader, {
onclick: column.getToggleSortingHandler(),
data: 'Tags'
}),
cell: ({ row }) => {
return renderComponent(DataTableTagsBadge, { tags: row.original.tags });
},
enableGlobalFilter: true,
filterFn: 'includesString'
},
{
accessorKey: 'status',
header: ({ column }) =>
renderComponent(DataTableHeader, {
onclick: column.getToggleSortingHandler(),
data: 'Status'
}),
cell: ({ row }) => {
return renderComponent(DataTableTagsBadge, { tags: row.original.status });
},
enableGlobalFilter: true,
filterFn: 'includesString'
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
refPage: get(referenceFromPage),
...row.original
});
}
}
];

View file

@ -0,0 +1,57 @@
<script lang="ts">
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index';
import { EllipsisIcon } from '@lucide/svelte/icons';
import type { RecipeOverview } from './columns';
import Button from '$lib/components/ui/button/button.svelte';
import { onMount } from 'svelte';
import Separator from '$lib/components/ui/separator/separator.svelte';
import RecipeEditorDialog from '$lib/components/recipe-editor-dialog.svelte';
let {
productCode = '',
name = '',
description = '',
tags = '',
status,
refPage
}: RecipeOverview & { refPage: string } = $props();
let dataForCopy = $state('');
onMount(() => {
dataForCopy = `${productCode}\t${name}\t${description}\t${tags}\t${status ?? 'drafted'}`;
});
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<EllipsisIcon />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<!-- open dialog need permission: recipe edit tool brew-->
{#if refPage !== ''}
<DropdownMenu.Item onclick={(e) => e.preventDefault()}>
<RecipeEditorDialog {productCode} {refPage} />
</DropdownMenu.Item>
{:else}
<DropdownMenu.Item>
<p class="text-muted-foreground">Cannot Edit</p>
</DropdownMenu.Item>
{/if}
<Separator />
<DropdownMenu.Group>
<DropdownMenu.Label>Actions</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => navigator.clipboard.writeText(dataForCopy)}
>Copy For Sheets</DropdownMenu.Item
>
<!-- note: require permission level dev -->
<DropdownMenu.Item onclick={() => navigator.clipboard.writeText(dataForCopy)}
>Export</DropdownMenu.Item
>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>

View file

@ -0,0 +1 @@
// may not use

View file

@ -0,0 +1,16 @@
<script lang="ts">
import Button from '$lib/components/ui/button/button.svelte';
import { ArrowUpDownIcon } from '@lucide/svelte';
import type { ComponentProps } from 'svelte';
let {
variant = 'ghost',
data,
...restProps
}: ComponentProps<typeof Button> & { data: string } = $props();
</script>
<Button {variant} {...restProps}>
{data}
<ArrowUpDownIcon class="ms-2" />
</Button>

View file

@ -0,0 +1,75 @@
<script lang="ts">
import Badge from '$lib/components/ui/badge/badge.svelte';
import { type BadgeVariant } from '$lib/components/ui/badge/badge.svelte';
import { BadgeCheckIcon } from '@lucide/svelte/icons';
import * as Tooltip from '$lib/components/ui/tooltip/index';
import { onMount } from 'svelte';
let { tags }: { tags: string } = $props();
let tagList: string[] = $state([]);
let extendTags: string[] = $state([]);
function getVariantByKeyword(tag: string): BadgeVariant {
if (tag === 'ready') {
return 'default';
} else if (tag === 'drafted') {
return 'secondary';
} else if (tag === 'obsolete') {
return 'destructive';
} else {
return 'default';
}
}
function getClassByKeyword(tag: string): string {
switch (tag) {
case 'ready':
return 'bg-green-500 text-white dark:bg-green-600';
case 'pending/online':
return 'bg-yellow-500 text-black dark:bg-yellow-600';
default:
return '';
}
}
onMount(() => {
// build tags
if (tags) {
tagList = tags.split(',');
// do check if too long tags
if (tagList.length > 2) {
let into_ext = tagList.splice(2);
extendTags = into_ext;
}
}
});
</script>
<div class="flex w-full flex-wrap gap-2">
{#each tagList as tag}
<Badge variant={getVariantByKeyword(tag)} class={getClassByKeyword(tag)}>
{#if tag === 'ready'}
<BadgeCheckIcon />
{/if}
{tag}
</Badge>
{/each}
<!-- requre permission: view mat extend -->
{#if extendTags.length > 0}
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
<Badge id="tags-ext-count" variant="secondary">
{extendTags.length}+
</Badge>
</Tooltip.Trigger>
<Tooltip.Content>
{extendTags.join(', ')}
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
{/if}
</div>

View file

@ -0,0 +1,159 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
type ColumnFiltersState,
type FilterFn,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
type GlobalFilterTableState,
type PaginationState,
type SortingState
} from '@tanstack/table-core';
import { rankItem } from '@tanstack/match-sorter-utils';
import { createSvelteTable, FlexRender } from '$lib/components/ui/data-table/index';
import * as Table from '$lib/components/ui/table/index';
import Button from '$lib/components/ui/button/button.svelte';
import Input from '$lib/components/ui/input/input.svelte';
import { SearchIcon } from '@lucide/svelte/icons';
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
};
let { data, columns, refPage }: DataTableProps<TData, TValue> & { refPage: string } = $props();
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
let sorting = $state<SortingState>([]);
let columnFilter = $state<ColumnFiltersState>([]);
let globalFilter = $state<GlobalFilterTableState>();
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
const itemRank = rankItem(row.getValue(columnId), value);
addMeta({ itemRank });
return itemRank.passed;
};
const table = createSvelteTable({
get data() {
return data;
},
columns,
state: {
get pagination() {
return pagination;
},
get sorting() {
return sorting;
},
get globalFilter() {
return globalFilter;
}
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
filterFns: {
fuzzy: fuzzyFilter
},
globalFilterFn: fuzzyFilter,
onSortingChange: (updater) => {
if (typeof updater === 'function') {
sorting = updater(sorting);
} else {
sorting = updater;
}
},
onPaginationChange: (updater) => {
if (typeof updater === 'function') {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
onColumnFiltersChange: (updater) => {
if (typeof updater === 'function') {
columnFilter = updater(columnFilter);
} else {
columnFilter = updater;
}
},
onGlobalFilterChange: (updater) => {
if (typeof updater === 'function') {
globalFilter = updater(globalFilter);
} else {
globalFilter = updater;
}
}
});
</script>
<div>
<div class="flex items-center gap-2 py-4">
<SearchIcon />
<Input
type="text"
placeholder="Search by id, product code, name or material"
onchange={(e) => {
table.setGlobalFilter(e.currentTarget.value);
}}
oninput={(e) => {
table.setGlobalFilter(e.currentTarget.value);
}}
/>
</div>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head colspan={header.colSpan}>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && 'selected'}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">No results.</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
<div class="mx-4 flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onclick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}>Previous</Button
>
<Button
variant="outline"
size="sm"
onclick={() => table.nextPage()}
disabled={!table.getCanNextPage()}>Next</Button
>
</div>
</div>
</div>

View file

@ -0,0 +1,352 @@
<script lang="ts">
import Button from '$lib/components/ui/button/button.svelte';
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
import { onMount } from 'svelte';
import * as adb from '$lib/core/adb/adb';
import { addNotification } from '$lib/core/stores/noti';
import { columns, type RecipeOverview } from '../../recipe/overview/columns';
import {
materialFromMachineQuery,
recipeFromMachine,
recipeFromMachineLoading,
recipeFromMachineQuery,
referenceFromPage
} from '$lib/core/stores/recipeStore';
import DataTable from '../../recipe/overview/data-table.svelte';
import { handleIncomingMessages } from '$lib/core/handlers/messageHandler';
import { auth as authStore } from '$lib/core/stores/auth';
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
import { get } from 'svelte/store';
import { AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb';
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager';
const sourceDir = '/sdcard/coffeevending';
// fetched recipe
let devRecipe: any | undefined = $state();
// data to display
let data: { recipes: RecipeOverview[] } = $state({
recipes: []
});
async function startFetchRecipeFromMachine() {
let instance = adb.getAdbInstance();
recipeFromMachineLoading.set(true);
referenceFromPage.set('brew');
if (instance) {
let dev_recipe = await adb.pull(`${sourceDir}/cfg/recipe_branch_dev.json`);
if (dev_recipe) {
if (dev_recipe.length == 0) {
// case error, do last retry
dev_recipe = await adb.pull(`${sourceDir}/coffeethai02.json`);
if (dev_recipe && dev_recipe.length == 0)
addNotification('ERROR:Cannot fetch recipe from machine');
else if (dev_recipe) {
// From coffeethai02
devRecipe = JSON.parse(dev_recipe);
recipeFromMachineLoading.set(false);
addNotification('INFO:Fetch recipe success!');
buildOverviewForBrewing();
}
} else {
// from recipe_branch_dev
devRecipe = JSON.parse(dev_recipe);
recipeFromMachineLoading.set(false);
addNotification('INFO:Fetch recipe success!');
buildOverviewForBrewing();
}
}
} else {
addNotification('ERROR:Cannot connect to machine');
recipeFromMachineLoading.set(false);
}
}
async function loadEssentialFiles() {
let instance = adb.getAdbInstance();
if (instance) {
// check country
let country = await adb.pull('/sdcard/coffeevending/country/short');
// check dev
let devMode = await adb.pull('/sdcard/coffeevending/CURR_TEST');
// check .bid
let boxid = await adb.pull('/sdcard/coffeevending/.bid');
machineInfoStore.set({
boxId: boxid,
versions: {
firmware: '',
brew: '',
xmlengine: '',
netcore: '',
devbox: ''
},
devMode: devMode?.includes('1') ?? false,
country: country ?? '',
status: '',
errors: []
});
handleIncomingMessages(
JSON.stringify({
type: 'chat',
payload: `${new Date().toLocaleTimeString()}: ${get(authStore)?.displayName} has connected to ${boxid}`
})
);
} else {
addNotification('ERROR:Failed to get machine info');
}
}
async function connectAdb() {
try {
if (!('usb' in navigator)) {
throw new Error('WebUSB not supported, try using fallback or different browser');
}
await adb.connnectViaWebUSB();
let instance = adb.getAdbInstance();
if (instance) {
await startFetchRecipeFromMachine();
await loadEssentialFiles();
}
} catch (e: any) {
addNotification(`ERROR:${e}`);
}
}
async function tryAutoConnect() {
try {
if (!('usb' in navigator) || !AdbDaemonWebUsbDeviceManager.BROWSER) {
throw new Error('WebUSB not supported, try using fallback or different browser');
}
const devices = await AdbDaemonWebUsbDeviceManager.BROWSER.getDevices();
if (!devices || devices.length == 0) {
throw new Error('No device found');
}
if (devices.length > 1) {
throw new Error('Too many connected devices');
}
const device = devices[0];
const credStore = new AdbWebCredentialStore();
try {
await adb.connectDeviceByCred(device, credStore);
return true;
} catch (e: any) {
if (e.message === 'CREDENTIAL_EXPIRED') {
try {
await deviceCredentialManager.clearAllCredentials();
} catch (ignored) {}
}
return false;
}
} catch (e) {
console.error('error on auto connect brew page', e);
addNotification('ERROR:Failed to auto connect, please try again');
}
}
onMount(async () => {
// do auto connect
if (!adb.getAdbInstance()) await tryAutoConnect();
await startFetchRecipeFromMachine();
});
function getMenuStatus(ms: number): RecipeOverview['status'] {
switch (ms) {
case 0:
return 'ready';
case 2:
return 'obsolete';
case 11:
return 'pending/online';
case 12:
return 'pending/offline';
default:
return 'drafted';
}
}
function getMenuCategory(pd: string): string {
// [country_code]-[category_code]-[drink_Type]-[id]
let pd_spl = pd.split('-');
let category = pd_spl[1] ?? '';
if (category) {
if (category.endsWith('1')) {
let result = 'coffee';
if (category === '01') {
result += ',v1';
} else {
result += ',v2+';
}
return result;
} else if (category.endsWith('2')) {
return 'tea';
} else if (category.endsWith('3')) {
return 'milk';
} else if (category.endsWith('4')) {
return 'whey';
} else if (category.endsWith('5')) {
return 'soda';
} else if (category == '99') {
return 'special';
}
}
return 'unknown';
}
function getDrinkType(pd: string): string {
// [country_code]-[category_code]-[drink_Type]-[id]
let pd_spl = pd.split('-');
let drink_type = pd_spl[2] ?? '';
if (drink_type) {
if (drink_type.endsWith('1')) {
return 'hot';
} else if (drink_type.endsWith('2')) {
return 'cold / iced';
} else if (drink_type.endsWith('3')) {
return 'smoothie / frappe';
}
}
return '';
}
// set material used in recipe to tags for using in filter
function getMainMaterialOfRecipe(rp: any): string {
let recipeList = rp['recipes'] ?? [];
let mat_lists = '';
for (let rpl of recipeList) {
let mat_id = rpl['materialPathId'];
let mat_in_use = rpl['isUse'];
if (mat_in_use && !mat_lists.includes(mat_id)) {
mat_lists += mat_id + ',';
}
}
mat_lists = mat_lists.substring(0, mat_lists.length - 1);
return mat_lists;
}
function buildTags(rp: any): string {
let result = '';
result += getMenuCategory(rp['productCode']);
let dt = getDrinkType(rp['productCode']);
let mats = getMainMaterialOfRecipe(rp);
if (dt !== '') result += ',' + dt;
if (mats !== '') result += ',' + mats;
return result;
}
function buildOverviewForBrewing() {
if (devRecipe) {
let recipe01_query: any = {};
recipeFromMachine.set(devRecipe);
data.recipes = [];
for (let rp of devRecipe['Recipe01']) {
data.recipes.push({
productCode: rp['productCode'] ?? '<not set>',
name: rp['name'] ? rp['name'] : (rp['otherName'] ?? '<not set>'),
description: rp['desciption']
? rp['desciption']
: (rp['otherDescription'] ?? '<not set>'),
tags: buildTags(rp),
status: getMenuStatus(rp['MenuStatus'])
});
recipe01_query[rp['productCode']] = rp;
if (rp['SubMenu'] && rp['SubMenu'].length > 0) {
for (let rps of rp['SubMenu']) {
data.recipes.push({
productCode: rps['productCode'] ?? '<not set>',
name: rps['name'] ? rps['name'] : (rps['otherName'] ?? '<not set>'),
description: rps['desciption']
? rps['desciption']
: (rps['otherDescription'] ?? '<not set>'),
tags: buildTags(rps),
status: getMenuStatus(rps['MenuStatus'])
});
recipe01_query[rps['productCode']] = rps;
}
}
}
let materialFromMachine = devRecipe['MaterialSetting'];
let currentQuery = get(recipeFromMachineQuery);
currentQuery = {
recipe: recipe01_query,
...currentQuery
};
let currentMaterialsQuery = materialFromMachine;
currentQuery = {
material: currentMaterialsQuery,
...currentQuery
};
recipeFromMachineQuery.set(currentQuery);
materialFromMachineQuery.set(currentMaterialsQuery);
}
}
</script>
<div class="mx-8 flex">
<div class="w-full">
<div class="mb-4 flex items-center justify-between">
<div>
<h1 class="m-8 text-4xl font-bold">Brew</h1>
<p class="mx-8 my-0 text-muted-foreground">Brewing directly from web to machine</p>
<p class="mx-8 my-0 text-muted-foreground">
Note: refreshing page may cut connection with machine
</p>
</div>
<div class="mx-8 my-4 flex gap-2">
{#if !adb.getAdbInstance()}
<Button variant="default" onclick={() => connectAdb()}>Connect</Button>
{:else}
<Button variant="default">+ Create Menu</Button>
{/if}
</div>
<!-- <DashboardQuickAdb enableComponent={true} /> -->
</div>
<!-- search bar -->
<div class="w-full">
{#if $recipeFromMachineLoading}
<div class="flex items-center justify-center">
<p class="mx-4">Please wait</p>
<Spinner />
</div>
{:else}
<DataTable data={data.recipes} refPage="brew" {columns} />
{/if}
</div>
</div>
</div>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import Button from '$lib/components/ui/button/button.svelte';
</script>
<div class="mx-8 flex">
<div class="w-full">
<div class="mb-4 flex items-center justify-between">
<div>
<h1 class="m-8 text-4xl font-bold">Debug Toolkits</h1>
<p class="mx-8 my-0 text-muted-foreground">Tools for hotfix machine or viewing infos</p>
</div>
<div class="mx-8 my-4 flex gap-2">
<Button variant="default">Help</Button>
</div>
</div>
</div>
</div>