init
This commit is contained in:
commit
451223816b
338 changed files with 9938 additions and 0 deletions
14
src/routes/(authed)/recipe/overview/+page.server.ts
Normal file
14
src/routes/(authed)/recipe/overview/+page.server.ts
Normal 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
|
||||
};
|
||||
}
|
||||
63
src/routes/(authed)/recipe/overview/+page.svelte
Normal file
63
src/routes/(authed)/recipe/overview/+page.svelte
Normal 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>
|
||||
92
src/routes/(authed)/recipe/overview/columns.ts
Normal file
92
src/routes/(authed)/recipe/overview/columns.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
// may not use
|
||||
16
src/routes/(authed)/recipe/overview/data-table-header.svelte
Normal file
16
src/routes/(authed)/recipe/overview/data-table-header.svelte
Normal 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>
|
||||
|
|
@ -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>
|
||||
159
src/routes/(authed)/recipe/overview/data-table.svelte
Normal file
159
src/routes/(authed)/recipe/overview/data-table.svelte
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue