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,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>