278 lines
9.6 KiB
TypeScript
278 lines
9.6 KiB
TypeScript
import { Input } from '@/components/ui/input'
|
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
|
|
import { Separator } from '@/components/ui/separator'
|
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
|
import { cn } from '@/lib/utils'
|
|
import { type Recipes } from '@/models/recipe/schema'
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
import { Search, CupSoda, Wheat, Dessert, Cherry, WineOff, Server, Loader2 } from 'lucide-react'
|
|
import Nav from './nav'
|
|
import RecipeList from './recipe-list'
|
|
import { format } from 'date-fns'
|
|
import RecipeDisplay from './recipe-display'
|
|
import { Button } from '@/components/ui/button'
|
|
import { useMutation } from '@tanstack/react-query'
|
|
import taoAxios from '@/lib/taoAxios'
|
|
import type { ItemMetadata, ListMetadata } from '@/hooks/recipe-dashboard'
|
|
import useRecipeDashboard, { EditorShowStateEnum } from '@/hooks/recipe-dashboard'
|
|
import { useShallow } from 'zustand/react/shallow'
|
|
|
|
interface RecipeEditorProps {
|
|
isDevBranch: boolean
|
|
recipes: Recipes
|
|
defaultLayout: number[] | undefined
|
|
defaultCollapsed?: boolean
|
|
navCollapsedSize: number
|
|
}
|
|
|
|
export const RecipeEditor: React.FC<RecipeEditorProps> = ({
|
|
recipes,
|
|
isDevBranch,
|
|
defaultLayout = [265, 440, 655],
|
|
defaultCollapsed = false,
|
|
navCollapsedSize
|
|
}) => {
|
|
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed)
|
|
|
|
const [editorShowState, setEditorShowState] = useState(EditorShowStateEnum.RECIPES_IN_USE)
|
|
|
|
const { recipesEnable, recipesDisable } = useMemo<{
|
|
recipesEnable: ItemMetadata[]
|
|
recipesDisable: ItemMetadata[]
|
|
}>(() => {
|
|
return {
|
|
recipesEnable: recipes.Recipe01.filter(r => r.isUse).map(x => ({
|
|
id: x.productCode,
|
|
name: x.name,
|
|
lastChange: x.LastChange
|
|
})),
|
|
recipesDisable: recipes.Recipe01.filter(r => !r.isUse).map(x => ({
|
|
id: x.productCode,
|
|
name: x.name,
|
|
lastChange: x.LastChange
|
|
}))
|
|
}
|
|
}, [recipes])
|
|
|
|
const [currentItems, setCurrentItems] = useState<ItemMetadata[]>(recipesEnable)
|
|
|
|
const [listMetadata, setListMetadata] = useState<ListMetadata>({
|
|
items: recipesEnable,
|
|
currentSelectedId: undefined,
|
|
onSelectFn: id => setSelectedRecipe(id.toString())
|
|
})
|
|
|
|
const { selectedMaterial, setSelectedMaterial, selectedRecipe, setSelectedRecipe } = useRecipeDashboard(
|
|
useShallow(state => ({
|
|
selectedMaterial: state.selectedMaterial,
|
|
setSelectedMaterial: state.setSelectedMaterial,
|
|
selectedRecipe: state.selectedRecipe,
|
|
setSelectedRecipe: state.setSelectedRecipe
|
|
}))
|
|
)
|
|
|
|
// user click button from nav
|
|
useEffect(() => {
|
|
let list: ItemMetadata[] = []
|
|
let currentSelectId: string | number | undefined
|
|
let onSelectedFn: (id: string | number) => void = () => {}
|
|
|
|
if (editorShowState === EditorShowStateEnum.RECIPES_IN_USE) {
|
|
list = recipesEnable
|
|
currentSelectId = selectedRecipe
|
|
onSelectedFn = id => setSelectedRecipe(id.toString())
|
|
} else if (editorShowState === EditorShowStateEnum.RECIPES_NOT_IN_USE) {
|
|
list = recipesDisable
|
|
currentSelectId = selectedRecipe
|
|
onSelectedFn = id => setSelectedRecipe(id.toString())
|
|
} else if (editorShowState === EditorShowStateEnum.MATERIALS_SETTING) {
|
|
list = recipes.MaterialSetting.map(x => ({
|
|
id: x.id,
|
|
name: x.materialName
|
|
}))
|
|
currentSelectId = selectedMaterial
|
|
onSelectedFn = id => setSelectedMaterial(Number(id))
|
|
} else if (editorShowState === EditorShowStateEnum.TOPPING_GROUPS) {
|
|
list = recipes.Topping.ToppingGroup.map(x => ({
|
|
id: x.groupID,
|
|
name: x.name
|
|
}))
|
|
currentSelectId = selectedMaterial
|
|
onSelectedFn = id => setSelectedMaterial(Number(id))
|
|
} else if (editorShowState === EditorShowStateEnum.TOPPING_LIST) {
|
|
list = recipes.Topping.ToppingList.map(x => ({
|
|
id: x.id,
|
|
name: x.name
|
|
}))
|
|
}
|
|
|
|
setListMetadata({
|
|
items: list,
|
|
currentSelectedId: currentSelectId,
|
|
onSelectFn: onSelectedFn
|
|
})
|
|
setCurrentItems(list)
|
|
setSearch('')
|
|
}, [editorShowState])
|
|
|
|
const saveRecipeState = useMutation({
|
|
mutationFn: async () => {
|
|
return taoAxios.post('/v2/recipes/', recipes, {
|
|
params: {
|
|
country_id: 'tha'
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
const [search, setSearch] = useState('')
|
|
|
|
useEffect(() => {
|
|
if (search) {
|
|
const recipesFiltered = currentItems.filter(
|
|
item =>
|
|
item.id.toString().toLowerCase().includes(search.toLowerCase()) ||
|
|
(item.name && item.name.toLowerCase().includes(search.toLowerCase()))
|
|
)
|
|
setListMetadata({
|
|
...listMetadata,
|
|
items: recipesFiltered
|
|
})
|
|
} else {
|
|
setListMetadata({
|
|
...listMetadata,
|
|
items: currentItems
|
|
})
|
|
}
|
|
}, [search])
|
|
|
|
return (
|
|
<TooltipProvider delayDuration={0}>
|
|
<ResizablePanelGroup
|
|
direction="horizontal"
|
|
onLayout={(sizes: number[]) => {
|
|
document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`
|
|
}}
|
|
className="h-full max-h-screen items-stretch"
|
|
>
|
|
<ResizablePanel
|
|
defaultSize={defaultLayout[0]}
|
|
collapsedSize={navCollapsedSize}
|
|
collapsible={true}
|
|
minSize={15}
|
|
maxSize={20}
|
|
onCollapse={() => {
|
|
setIsCollapsed(true)
|
|
document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(true)}`
|
|
}}
|
|
onExpand={() => {
|
|
setIsCollapsed(false)
|
|
document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(false)}`
|
|
}}
|
|
className={cn(isCollapsed && 'min-w-[50px] transition-all duration-300 ease-in-out')}
|
|
>
|
|
<div className={cn('flex h-[52px] items-center justify-center', isCollapsed ? 'h-[52px]' : 'px-2')}>
|
|
<span className={cn('text-muted-foreground text-xs ', isCollapsed && 'hidden')}>
|
|
TimeStamp: {format(recipes.Timestamp, 'dd-MM-yyyy HH:mm:ss')}
|
|
</span>
|
|
</div>
|
|
<Separator />
|
|
<Nav
|
|
isCollapsed={isCollapsed}
|
|
showListIndex={editorShowState}
|
|
setShowListIndex={setEditorShowState}
|
|
links={[
|
|
{
|
|
index: EditorShowStateEnum.RECIPES_IN_USE,
|
|
title: 'Menu (Enabled)',
|
|
label: recipesEnable.length.toString(),
|
|
icon: CupSoda
|
|
},
|
|
{
|
|
index: EditorShowStateEnum.RECIPES_NOT_IN_USE,
|
|
title: 'Menu (Disabled)',
|
|
label: recipesDisable.length.toString(),
|
|
icon: WineOff
|
|
}
|
|
]}
|
|
/>
|
|
<Separator />
|
|
<Nav
|
|
isCollapsed={isCollapsed}
|
|
showListIndex={editorShowState}
|
|
setShowListIndex={setEditorShowState}
|
|
links={[
|
|
{
|
|
index: EditorShowStateEnum.MATERIALS_SETTING,
|
|
title: 'Materials',
|
|
label: recipes.MaterialSetting.length.toString(),
|
|
icon: Wheat
|
|
},
|
|
{
|
|
index: EditorShowStateEnum.TOPPING_GROUPS,
|
|
title: 'ToppingsGroups',
|
|
label: recipes.Topping.ToppingGroup.length.toString(),
|
|
icon: Dessert
|
|
},
|
|
{
|
|
index: EditorShowStateEnum.TOPPING_LIST,
|
|
title: 'ToppingsList',
|
|
label: recipes.Topping.ToppingList.length.toString(),
|
|
icon: Cherry
|
|
}
|
|
]}
|
|
/>
|
|
</ResizablePanel>
|
|
<ResizableHandle withHandle />
|
|
<ResizablePanel id="recipe-panel" defaultSize={defaultLayout[1]} minSize={30}>
|
|
<div className="flex items-center px-4 py-2">
|
|
<h1 className="text-xl font-bold">
|
|
Recipe Version: {recipes.MachineSetting.configNumber} {isDevBranch ? '(Dev)' : ''}
|
|
</h1>
|
|
</div>
|
|
<Separator />
|
|
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<div className="pb-3 flex justify-end items-end">
|
|
<Button className="bg-primary text-white" onClick={() => saveRecipeState.mutate()}>
|
|
{saveRecipeState.isPending ? (
|
|
<div className="flex items-center gap-2">
|
|
<Loader2 size={20} className="animate-spin" />
|
|
<span>Updating...</span>
|
|
</div>
|
|
) : saveRecipeState.isSuccess ? (
|
|
'Updated'
|
|
) : saveRecipeState.isError ? (
|
|
'Error'
|
|
) : (
|
|
<div className="flex items-center gap-2">
|
|
<Server size={20} />
|
|
Up Recipe to Server
|
|
</div>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
<form>
|
|
<div className="relative">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input placeholder="Search" className="pl-8" value={search} onChange={e => setSearch(e.target.value)} />
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{listMetadata ? (
|
|
<RecipeList
|
|
items={listMetadata.items}
|
|
onSelect={listMetadata.onSelectFn}
|
|
currentSelectId={listMetadata.currentSelectedId}
|
|
/>
|
|
) : null}
|
|
</ResizablePanel>
|
|
<ResizableHandle withHandle />
|
|
<ResizablePanel defaultSize={defaultLayout[1]}>
|
|
<RecipeDisplay recipes={recipes} />
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</TooltipProvider>
|
|
)
|
|
}
|
|
|
|
export default RecipeEditor
|