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,114 @@
// {
// "MixOrder": 0,
// "StringParam": "",
// "FeedParameter": 0,
// "FeedPattern": 0,
// "isUse": true,
// "materialPathId": 599501,
// "powderGram": 0,
// "powderTime": 0,
// "stirTime": 110,
// "syrupGram": 0,
// "syrupTime": 0,
// "waterCold": 0,
// "waterYield": 0
// }
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent, renderSnippet } from '../ui/data-table';
import RecipelistIsuse from './recipelist-isuse.svelte';
import RecipelistValue from './recipelist-value.svelte';
import { DragHandle } from './recipelist-table.svelte';
import { createRawSnippet } from 'svelte';
import RecipelistMatSelect from './recipelist-mat-select.svelte';
export type RecipelistMaterial = {
id: number;
material_id: string;
is_use: boolean;
values: {
string_param: string;
mix_order: number;
feed: {
pattern: number;
parameter: number;
};
powder: {
gram: number;
time: number;
};
syrup: {
gram: number;
time: number;
};
water: {
cold: number;
yield: number;
};
};
};
export const columns: ColumnDef<RecipelistMaterial>[] = [
{
id: 'id',
accessorKey: 'id',
header: () => null,
cell: () => null,
enableSorting: true
},
{
id: 'drag',
header: () =>
renderSnippet(
createRawSnippet(() => ({
render: () => '<div class="w-1 text-start"></div>'
}))
),
cell: () => renderSnippet(DragHandle)
},
{
accessorKey: 'is_use',
id: 'is_use',
header: ({ column }) =>
renderSnippet(
createRawSnippet(() => ({
render: () => '<div class="w-0.5">Enable</div>'
}))
),
cell: ({ row }) => {
return renderComponent(RecipelistIsuse, {
checked: row.original.is_use,
onCheckedChange: (value) => {
row.original.is_use = !!value;
row.toggleSelected(row.original.is_use);
}
});
},
enableSorting: false,
enableHiding: false
},
{
id: 'material_id',
header: ({ column }) => 'Material',
cell: ({ row }) => {
return renderComponent(RecipelistMatSelect, {
currentMat: row.original.material_id,
onMatChange: (value: any) => {
row.original.material_id = value;
console.log('change mat', value);
row.toggleSelected(row.original.is_use);
}
});
}
},
{
accessorKey: 'values',
id: 'values',
header: ({ column }) => 'Values',
cell: ({ row }) => {
return renderComponent(RecipelistValue, {
...row.original.values
});
}
}
];

View file

@ -0,0 +1,145 @@
<script lang="ts">
import * as Tabs from '$lib/components/ui/tabs/index';
import * as Card from '$lib/components/ui/card/index';
import Label from '$lib/components/ui/label/label.svelte';
import Input from '$lib/components/ui/input/input.svelte';
import { onDestroy, onMount } from 'svelte';
import RecipelistTable from './recipelist-table.svelte';
import { columns, type RecipelistMaterial } from './columns';
import { get, readable, writable } from 'svelte/store';
import { materialFromMachineQuery } from '$lib/core/stores/recipeStore';
import { generateIcing } from '$lib/helpers/icingGen';
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
import MachineInfo from '../machine-info.svelte';
import { v4 as uuidv4 } from 'uuid';
//
let { recipeData, onPendingChange }: { recipeData: any; onPendingChange: any } = $props();
let menuName: string = $state('');
let materialSnapshot: any = $state();
let machineInfoSnapshot: any = $state();
let recipeListMatState: RecipelistMaterial[] = $state([]);
let recipeListOriginal: RecipelistMaterial[] = $state([]);
function remappingToColumn(data: any[]): RecipelistMaterial[] {
let ret: RecipelistMaterial[] = [];
// expect recipelist
if (materialSnapshot) {
let d_cnt = 0;
for (let rpl of data) {
let mat = materialSnapshot.filter(
(x: any) => x['id'].toString() === rpl['materialPathId'].toString()
)[0];
// console.log('mat filter get', Object(mat), Object.keys(mat));
let name = mat ? mat['materialOtherName'] : rpl['materialPathId'];
if (rpl['materialPathId'] === 0) {
name = '-';
}
// let gen_id = generateRowId();
// console.log(`generated for ${rpl['materialPathId']} = ${gen_id}`);
ret.push({
id: d_cnt,
material_id: `${name} (${rpl['materialPathId']})`,
is_use: rpl['isUse'],
values: {
string_param: rpl['StringParam'],
mix_order: rpl['MixOrder'],
feed: {
pattern: rpl['feedPattern'],
parameter: rpl['feedParameter']
},
powder: {
gram: rpl['powderGram'],
time: rpl['powderTime']
},
syrup: {
gram: rpl['syrupGram'],
time: rpl['syrupTime']
},
water: {
cold: rpl['waterCold'],
yield: rpl['waterYield']
}
}
});
d_cnt++;
}
}
return ret;
}
async function checkChanges(original: any) {
console.log('old', original, 'updated', recipeListMatState);
if (recipeListOriginal.length == 0) {
recipeListOriginal = original;
}
if (original !== recipeListMatState) {
await onPendingChange({
target: 'recipeList',
value: original
});
}
}
onMount(() => {
machineInfoSnapshot = get(machineInfoStore);
if (recipeData) {
menuName =
recipeData['name'] ?? (recipeData['otherName'] ? recipeData['otherName'] : 'Not set');
materialSnapshot = get(materialFromMachineQuery);
recipeListMatState = remappingToColumn(recipeData['recipes']);
// save old value\
}
});
</script>
<!-- show info -->
<!-- latest edit date -->
<!-- Menu Status -->
<div class="-mb-4 flex w-full flex-col gap-6">
<Tabs.Root value="info">
<Tabs.List>
<Tabs.Trigger value="info">Info</Tabs.Trigger>
<Tabs.Trigger value="details">Details</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="info">
<Card.Root>
<Card.Header>
<Card.Title>Info</Card.Title>
<Card.Description>Info about this menu</Card.Description>
</Card.Header>
<Card.Content class="grid gap-6">
<div class="grid grid-flow-row gap-3">
<Label for="tabs-menu-name">Name</Label>
<Input id="tabs-menu-name" value={recipeData['name'] ?? ''} />
<Label for="tabs-menu-other-name">Other Name</Label>
<Input id="tabs-menu-other-name" value={recipeData['otherName'] ?? ''} />
</div>
<div class="grid gap-3"></div>
</Card.Content>
</Card.Root>
</Tabs.Content>
<Tabs.Content value="details">
<RecipelistTable data={recipeListMatState} {columns} onStateChange={checkChanges} />
</Tabs.Content>
</Tabs.Root>
</div>

View file

@ -0,0 +1,14 @@
<script lang="ts">
import Checkbox from '../ui/checkbox/checkbox.svelte';
import { onMount, type ComponentProps } from 'svelte';
let {
checked,
onCheckedChange = (v) => (checked = v),
...restProps
}: ComponentProps<typeof Checkbox> = $props();
</script>
<div class="flex w-1.5 items-center justify-center">
<Checkbox {checked} onCheckedChange={(e) => onCheckedChange(e)} {...restProps} />
</div>

View file

@ -0,0 +1,76 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Button } from '../ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index';
import { get } from 'svelte/store';
import {
materialData,
materialFromMachineQuery,
referenceFromPage
} from '$lib/core/stores/recipeStore';
import Input from '$lib/components/ui/input/input.svelte';
import { SearchIcon, PlusIcon, BeanIcon, StarIcon } from '@lucide/svelte/icons';
let { currentMat, onMatChange } = $props();
let allMatData: any = $state();
let refPage: string | undefined = $state();
function changeMat(mat_id: string) {
currentMat = mat_id;
onMatChange(mat_id);
}
function getMaterialTypeIcon(mat_id: number) {}
onMount(() => {
refPage = get(referenceFromPage);
if (refPage === 'brew') allMatData = get(materialFromMachineQuery);
else if (refPage === 'overview') allMatData = get(materialData);
});
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="ghost" class="text-muted-foreground hover:bg-transparent">
{currentMat}
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<div class="sticky top-0 z-10 my-4 flex items-center gap-2 bg-accent">
<SearchIcon />
<Input
type="text"
placeholder="Search by mat id or name"
onchange={(e) => {}}
oninput={(e) => {}}
/>
</div>
<!-- permission create mat -->
<DropdownMenu.Item>
<div class="flex gap-2">
<PlusIcon />
<p>Create Material</p>
</div>
</DropdownMenu.Item>
{#each allMatData as mat}
<DropdownMenu.Item onclick={() => changeMat(`${mat.materialOtherName} (${mat.id})`)}>
<div class="flex gap-2">
{#if mat.BeanChannel}
<BeanIcon />
{:else if mat.id > 8110 && mat.id < 8131}
<StarIcon />
{/if}
<p>{mat.materialOtherName} ({mat.id})</p>
</div>
</DropdownMenu.Item>
{:else}
<DropdownMenu.Item>
<p class="text-muted-foreground">No materials available</p>
</DropdownMenu.Item>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>

View file

@ -0,0 +1,196 @@
<script module>
export { DragHandle };
</script>
<script lang="ts">
import * as Card from '$lib/components/ui/card/index';
import Label from '$lib/components/ui/label/label.svelte';
import Input from '$lib/components/ui/input/input.svelte';
import RecipeDetail from './recipe-detail.svelte';
import {
createTable,
getCoreRowModel,
getFacetedRowModel,
getSortedRowModel,
type ColumnDef,
type Row,
type RowSelectionState,
type SortingState
} from '@tanstack/table-core';
import { createSvelteTable } from '../ui/data-table';
import * as Table from '$lib/components/ui/table/index';
import FlexRender from '../ui/data-table/flex-render.svelte';
import ScrollArea from '../ui/scroll-area/scroll-area.svelte';
import { type Attachment } from 'svelte/attachments';
import Button from '../ui/button/button.svelte';
import { GripVerticalIcon } from '@lucide/svelte/icons';
import { DragDropProvider } from '@dnd-kit-svelte/svelte';
import { move } from '@dnd-kit/helpers';
import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
import { type UniqueIdentifier } from '@dnd-kit/abstract';
import { type RecipelistMaterial } from './columns';
import { useSortable } from '@dnd-kit-svelte/svelte/sortable';
import { onMount } from 'svelte';
import { get, writable, type Writable } from 'svelte/store';
// type DataTableProps<TData, TValue> = {
// columns: ColumnDef<TData, TValue>[];
// data: TData[];
// };
let {
data,
columns,
onStateChange
}: {
data: RecipelistMaterial[];
columns: ColumnDef<any, any>[];
onStateChange: any;
} = $props();
let sorting = $state<SortingState>([]);
let rowSelection = $state<RowSelectionState>({});
// let recipeMatState = $state<RecipelistMaterial[]>(data);
const table = createSvelteTable({
get data() {
return data;
},
columns,
enableRowSelection: true,
enableMultiRowSelection: true,
getRowId: (row: any) => row.id.toString(),
state: {
get sorting() {
return sorting;
},
get rowSelection() {
return rowSelection;
}
},
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
onStateChange: async (updater) => {
console.log('table state change', data);
await onStateChange(table.getRowModel().rows.map((x) => x.original));
},
onSortingChange: async (updater) => {
console.log('triggering sorting');
if (typeof updater === 'function') {
sorting = updater(sorting);
} else {
sorting = updater;
}
await onStateChange(table.getRowModel().rows.map((x) => x.original));
},
onRowSelectionChange: async (updater) => {
// table.getRowModel().rows.find((x) => x.original.id == )
if (typeof updater === 'function') {
rowSelection = updater(rowSelection);
let rows = table.getRowModel().rows;
console.log('state size', data, rows);
} else {
rowSelection = updater;
}
await onStateChange(table.getRowModel().rows.map((x) => x.original));
}
});
</script>
<!-- use card -->
<Card.Root>
<Card.Header>
<Card.Title>Recipe List</Card.Title>
<Card.Description>Material used in this menu's brewing process</Card.Description>
<Card.Content>
<!-- table -->
<DragDropProvider
modifiers={[
// @ts-expect-error @dnd-kit/adbstract types are botched atm
RestrictToVerticalAxis
]}
onDragEnd={(e) => {
// snap
data = move(data as any, e as any);
//
}}
>
<ScrollArea class="h-[60vh] w-full rounded-md border" type="always">
<Table.Root class="relative w-full">
<Table.Header class="sticky top-0 z-10">
{#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 class="**:data-[slot=table-cell]:first:w-8">
{#if table.getRowModel().rows?.length}
{#each table.getRowModel().rows as row, index (row.id)}
{@render DraggableRow({ row, index })}
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center"
>Empty recipe.</Table.Cell
>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</ScrollArea>
</DragDropProvider>
</Card.Content>
</Card.Header>
</Card.Root>
{#snippet DraggableRow({ row, index }: { row: Row<any>; index: number })}
{@const { ref, isDragging, handleRef } = useSortable({
id: row.original.id,
index: () => index
})}
<Table.Row
data-state={row.original.is_use ? 'selected' : undefined}
data-dragging={isDragging.current}
class="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
{@attach ref}
>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
attach={handleRef}
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{/snippet}
{#snippet DragHandle({ attach }: { attach: Attachment })}
<Button
{@attach attach}
variant="ghost"
size="icon"
class="size-7 text-muted-foreground hover:bg-transparent"
>
<GripVerticalIcon class="size-3 text-muted-foreground" />
<span class="sr-only">Drag to reorder</span>
</Button>
{/snippet}

View file

@ -0,0 +1,6 @@
<script lang="ts">
import type { RecipelistMaterial } from './columns';
let { string_param, mix_order, feed, water, powder, syrup }: RecipelistMaterial['values'] =
$props();
</script>