feat: ingredient product
This commit is contained in:
@@ -221,7 +221,6 @@ const CustomerListTable = () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
{ text: 'Duplicate', icon: 'tabler-copy' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Dialog from '@mui/material/Dialog'
|
||||
import DialogContent from '@mui/material/DialogContent'
|
||||
import DialogTitle from '@mui/material/DialogTitle'
|
||||
|
||||
// Third-party Imports
|
||||
import { Autocomplete, Button, Grid2, MenuItem } from '@mui/material'
|
||||
import { useMemo, useState } from 'react'
|
||||
import CustomTextField from '../../../../../@core/components/mui/TextField'
|
||||
import DialogCloseButton from '../../../../../components/dialogs/DialogCloseButton'
|
||||
import { Product } from '../../../../../types/services/product'
|
||||
import { ProductRecipeRequest } from '../../../../../types/services/productRecipe'
|
||||
import { useOutlets } from '../../../../../services/queries/outlets'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { useIngredients } from '../../../../../services/queries/ingredients'
|
||||
|
||||
// Component Imports
|
||||
|
||||
type PaymentMethodProps = {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
product: Product
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
product_id: '',
|
||||
variant_id: '',
|
||||
ingredient_id: '',
|
||||
quantity: 0,
|
||||
outlet_id: ''
|
||||
}
|
||||
|
||||
const AddRecipeDialog = ({ open, setOpen, product }: PaymentMethodProps) => {
|
||||
const [formData, setFormData] = useState<ProductRecipeRequest>(initialValues)
|
||||
|
||||
const [outletInput, setOutletInput] = useState('')
|
||||
const [outletDebouncedInput] = useDebounce(outletInput, 500)
|
||||
const [ingredientInput, setIngredientInput] = useState('')
|
||||
const [ingredientDebouncedInput] = useDebounce(ingredientInput, 500)
|
||||
|
||||
const { data: outlets, isLoading: outletsLoading } = useOutlets({
|
||||
search: outletDebouncedInput
|
||||
})
|
||||
const { data: ingredients, isLoading: ingredientsLoading } = useIngredients({
|
||||
search: ingredientDebouncedInput
|
||||
})
|
||||
|
||||
const outletOptions = useMemo(() => outlets?.outlets || [], [outlets])
|
||||
const ingredientOptions = useMemo(() => ingredients?.data || [], [ingredients])
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
fullWidth
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
maxWidth='sm'
|
||||
scroll='body'
|
||||
closeAfterTransition={false}
|
||||
sx={{ '& .MuiDialog-paper': { overflow: 'visible' } }}
|
||||
>
|
||||
<DialogCloseButton onClick={() => setOpen(false)} disableRipple>
|
||||
<i className='tabler-x' />
|
||||
</DialogCloseButton>
|
||||
<DialogTitle variant='h4' className='flex gap-2 flex-col text-center sm:pbs-16 sm:pbe-10 sm:pli-16'>
|
||||
Create Recipe
|
||||
</DialogTitle>
|
||||
<DialogContent className='pbs-0 sm:pli-16 sm:pbe-20 space-y-4'>
|
||||
{product.variants && (
|
||||
<CustomTextField
|
||||
select
|
||||
fullWidth
|
||||
label='Variant'
|
||||
value={formData.variant_id}
|
||||
onChange={e => setFormData({ ...formData, variant_id: e.target.value })}
|
||||
>
|
||||
{product.variants.map((variant, index) => (
|
||||
<MenuItem value={variant.id} key={index}>
|
||||
{variant.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</CustomTextField>
|
||||
)}
|
||||
<Autocomplete
|
||||
options={outletOptions}
|
||||
loading={outletsLoading}
|
||||
getOptionLabel={option => option.name}
|
||||
value={outletOptions.find(p => p.id === formData.outlet_id) || null}
|
||||
onInputChange={(event, newOutlettInput) => {
|
||||
setOutletInput(newOutlettInput)
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
outlet_id: newValue?.id || ''
|
||||
})
|
||||
}}
|
||||
renderInput={params => (
|
||||
<CustomTextField
|
||||
{...params}
|
||||
className=''
|
||||
label='Outlet'
|
||||
fullWidth
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: <>{params.InputProps.endAdornment}</>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{ xs: 6 }}>
|
||||
<Autocomplete
|
||||
options={ingredientOptions || []}
|
||||
loading={ingredientsLoading}
|
||||
getOptionLabel={option => option.name}
|
||||
value={ingredientOptions?.find(p => p.id === formData.ingredient_id) || null}
|
||||
onInputChange={(event, newIngredientInput) => {
|
||||
setIngredientInput(newIngredientInput)
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
ingredient_id: newValue?.id || ''
|
||||
})
|
||||
}}
|
||||
renderInput={params => (
|
||||
<CustomTextField
|
||||
{...params}
|
||||
className=''
|
||||
label='Ingredient'
|
||||
fullWidth
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: <>{params.InputProps.endAdornment}</>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 4 }}>
|
||||
<CustomTextField
|
||||
type='number'
|
||||
label='Quantity'
|
||||
fullWidth
|
||||
value={formData.quantity}
|
||||
onChange={e => setFormData({ ...formData, quantity: Number(e.target.value) })}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 2 }}>
|
||||
<Button variant='contained' color='primary' className='rounded-full' startIcon={<i className='tabler-plus' />} />
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddRecipeDialog
|
||||
@@ -72,25 +72,14 @@ const AddRecipeDrawer = (props: Props) => {
|
||||
const handleSubmit = (e: any) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (currentProductRecipe.id) {
|
||||
updateProductRecipe.mutate(
|
||||
{ id: currentProductRecipe.id, payload: formData },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleReset()
|
||||
}
|
||||
createProductRecipe.mutate(
|
||||
{ ...formData, product_id: product.id, variant_id: currentProductRecipe.id || '' },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleReset()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
createProductRecipe.mutate(
|
||||
{ ...formData, product_id: product.id, variant_id: currentProductRecipe.variant?.ID || '' },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleReset()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
@@ -109,8 +98,8 @@ const AddRecipeDrawer = (props: Props) => {
|
||||
const setTitleDrawer = (recipe: any) => {
|
||||
let title = 'Original'
|
||||
|
||||
if (recipe?.variant?.Name) {
|
||||
title = recipe?.variant?.Name
|
||||
if (recipe?.name) {
|
||||
title = recipe?.name
|
||||
}
|
||||
|
||||
return title
|
||||
@@ -205,11 +194,7 @@ const AddRecipeDrawer = (props: Props) => {
|
||||
type='submit'
|
||||
disabled={createProductRecipe.isPending || updateProductRecipe.isPending}
|
||||
>
|
||||
{currentProductRecipe?.id
|
||||
? updateProductRecipe.isPending
|
||||
? 'Updating...'
|
||||
: 'Update'
|
||||
: createProductRecipe.isPending
|
||||
{createProductRecipe.isPending
|
||||
? 'Adding...'
|
||||
: 'Add'}
|
||||
</Button>
|
||||
|
||||
@@ -25,6 +25,7 @@ import Loading from '../../../../../components/layout/shared/Loading'
|
||||
import { setProductRecipe } from '../../../../../redux-store/slices/productRecipe'
|
||||
import { useProductRecipesByProduct } from '../../../../../services/queries/productRecipes'
|
||||
import { useProductById } from '../../../../../services/queries/products'
|
||||
import { ProductVariant } from '../../../../../types/services/product'
|
||||
import { formatCurrency } from '../../../../../utils/transform'
|
||||
import AddRecipeDrawer from './AddRecipeDrawer'
|
||||
|
||||
@@ -37,19 +38,6 @@ const ProductDetail = () => {
|
||||
const { data: product, isLoading, error } = useProductById(params?.id as string)
|
||||
const { data: productRecipe, isLoading: isLoadingProductRecipe } = useProductRecipesByProduct(params?.id as string)
|
||||
|
||||
const groupedByVariant = productRecipe?.reduce((acc: any, item: any) => {
|
||||
const variantId = item.variant_id
|
||||
if (!acc[variantId]) {
|
||||
acc[variantId] = {
|
||||
variant: item.product_variant,
|
||||
product: item.product,
|
||||
ingredients: []
|
||||
}
|
||||
}
|
||||
acc[variantId].ingredients.push(item)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const handleOpenProductRecipe = (recipe: any) => {
|
||||
setOpenProductRecipe(true)
|
||||
dispatch(setProductRecipe(recipe))
|
||||
@@ -94,174 +82,37 @@ const ProductDetail = () => {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{productRecipe && (
|
||||
<div className='space-y-6'>
|
||||
{/* Recipe Details by Variant */}
|
||||
<div className='space-y-4'>
|
||||
<div className='flex items-center gap-2 mb-4'>
|
||||
<i className='tabler-chef-hat text-textPrimary text-xl' />
|
||||
<Typography variant='h5' component='h2' className='font-semibold'>
|
||||
Recipe Details
|
||||
</Typography>
|
||||
</div>
|
||||
{/* {productRecipe && ( */}
|
||||
<div className='space-y-6'>
|
||||
{/* Recipe Details by Variant */}
|
||||
<div className='space-y-4'>
|
||||
<div className='flex items-center gap-2 mb-4'>
|
||||
<i className='tabler-chef-hat text-textPrimary text-xl' />
|
||||
<Typography variant='h5' component='h2' className='font-semibold'>
|
||||
Recipe Details
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
{Object.keys(groupedByVariant).length > 0 ? (
|
||||
Object.entries(groupedByVariant).map(([variantId, variantData]: any) => (
|
||||
<Card key={variantId} className=''>
|
||||
<CardHeader
|
||||
title={
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<i className='tabler-variant text-blue-600 text-lg' />
|
||||
<Typography variant='h6' className='font-semibold'>
|
||||
{variantData?.variant?.Name || 'Original'} Variant
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex gap-4 text-sm'>
|
||||
<Chip
|
||||
label={`Cost: ${formatCurrency(variantData?.variant?.Cost || variantData.product?.Cost)}`}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
/>
|
||||
<Chip
|
||||
label={`Price Modifier: ${formatCurrency(variantData?.variant?.PriceModifier || 0)}`}
|
||||
variant='outlined'
|
||||
color='secondary'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<TableContainer component={Paper} variant='outlined'>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow className='bg-gray-50'>
|
||||
<TableCell className='font-semibold'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<i className='tabler-ingredients text-green-600' />
|
||||
Ingredient
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='font-semibold text-center'>
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<i className='tabler-scale text-orange-600' />
|
||||
Quantity
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='font-semibold text-center'>
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<i className='tabler-currency-dollar text-purple-600' />
|
||||
Unit Cost
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='font-semibold text-center'>
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<i className='tabler-package text-blue-600' />
|
||||
Stock Available
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='font-semibold text-right'>
|
||||
<div className='flex items-center justify-end gap-2'>
|
||||
<i className='tabler-calculator text-red-600' />
|
||||
Total Cost
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{variantData.ingredients.map((item: any) => (
|
||||
<TableRow key={item.id} className='hover:bg-gray-50'>
|
||||
<TableCell>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='w-2 h-2 rounded-full bg-green-500' />
|
||||
<div>
|
||||
<Typography variant='body2' className='font-medium capitalize'>
|
||||
{item.ingredient.name}
|
||||
</Typography>
|
||||
<Typography variant='caption' color='textSecondary'>
|
||||
{item.ingredient.is_semi_finished ? 'Semi-finished' : 'Raw ingredient'}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='text-center'>
|
||||
<Chip label={item.quantity} size='small' variant='outlined' color='primary' />
|
||||
</TableCell>
|
||||
<TableCell className='text-center'>{formatCurrency(item.ingredient.cost)}</TableCell>
|
||||
<TableCell className='text-center'>
|
||||
<Chip
|
||||
label={item.ingredient.stock}
|
||||
size='small'
|
||||
color={item.ingredient.stock > 5 ? 'success' : 'warning'}
|
||||
variant='outlined'
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className='text-right font-medium'>
|
||||
{formatCurrency(item.ingredient.cost * item.quantity)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Variant Summary */}
|
||||
<Box className='mt-4 p-4 bg-blue-50 rounded-lg'>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant='body2' className='flex items-center gap-2'>
|
||||
<i className='tabler-list-numbers text-blue-600' />
|
||||
<span className='font-semibold'>Total Ingredients:</span>
|
||||
{variantData.ingredients.length}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant='body2' className='flex items-center gap-2'>
|
||||
<i className='tabler-sum text-green-600' />
|
||||
<span className='font-semibold'>Total Recipe Cost:</span>
|
||||
{formatCurrency(
|
||||
variantData.ingredients.reduce(
|
||||
(sum: any, item: any) => sum + item.ingredient.cost * item.quantity,
|
||||
0
|
||||
)
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant='outlined'
|
||||
fullWidth
|
||||
className='mt-4'
|
||||
startIcon={<i className='tabler-plus' />}
|
||||
onClick={() => handleOpenProductRecipe(variantData)}
|
||||
>
|
||||
Add Ingredient
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Card className=''>
|
||||
{product?.variants?.length &&
|
||||
product.variants.map((variantData: ProductVariant, index: number) => (
|
||||
<Card key={index}>
|
||||
<CardHeader
|
||||
title={
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<i className='tabler-variant text-blue-600 text-lg' />
|
||||
<Typography variant='h6' className='font-semibold'>
|
||||
Original Variant
|
||||
{variantData?.name || 'Original'} Variant
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex gap-4 text-sm'>
|
||||
<Chip
|
||||
label={`Cost: ${formatCurrency(product?.cost || 0)}`}
|
||||
label={`Cost: ${formatCurrency(variantData?.cost || 0)}`}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
/>
|
||||
<Chip
|
||||
label={`Price Modifier: ${formatCurrency(product?.price || 0)}`}
|
||||
label={`Price Modifier: ${formatCurrency(variantData?.price_modifier || 0)}`}
|
||||
variant='outlined'
|
||||
color='secondary'
|
||||
/>
|
||||
@@ -306,25 +157,86 @@ const ProductDetail = () => {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody></TableBody>
|
||||
<TableBody>
|
||||
{productRecipe?.length &&
|
||||
productRecipe
|
||||
.filter((item: any) => item.variant_id === variantData.id)
|
||||
.map((item: any, index: number) => (
|
||||
<TableRow key={index} className='hover:bg-gray-50'>
|
||||
<TableCell>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='w-2 h-2 rounded-full bg-green-500' />
|
||||
<div>
|
||||
<Typography variant='body2' className='font-medium capitalize'>
|
||||
{item.ingredient.name}
|
||||
</Typography>
|
||||
<Typography variant='caption' color='textSecondary'>
|
||||
{item.ingredient.is_semi_finished ? 'Semi-finished' : 'Raw ingredient'}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='text-center'>
|
||||
<Chip label={item.quantity} size='small' variant='outlined' color='primary' />
|
||||
</TableCell>
|
||||
<TableCell className='text-center'>{formatCurrency(item.ingredient.cost)}</TableCell>
|
||||
<TableCell className='text-center'>
|
||||
<Chip
|
||||
label={item.ingredient.stock}
|
||||
size='small'
|
||||
color={item.ingredient.stock > 5 ? 'success' : 'warning'}
|
||||
variant='outlined'
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className='text-right font-medium'>
|
||||
{formatCurrency(item.ingredient.cost * item.quantity)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Variant Summary */}
|
||||
{productRecipe?.length && (
|
||||
<Box className='mt-4 p-4 bg-blue-50 rounded-lg'>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant='body2' className='flex items-center gap-2'>
|
||||
<i className='tabler-list-numbers text-blue-600' />
|
||||
<span className='font-semibold'>Total Ingredients:</span>
|
||||
{productRecipe.filter((item: any) => item.variant_id === variantData.id).length}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant='body2' className='flex items-center gap-2'>
|
||||
<i className='tabler-sum text-green-600' />
|
||||
<span className='font-semibold'>Total Recipe Cost:</span>
|
||||
{formatCurrency(
|
||||
productRecipe
|
||||
.filter((item: any) => item.variant_id === variantData.id)
|
||||
.reduce((sum: any, item: any) => sum + item.ingredient.cost * item.quantity, 0)
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant='outlined'
|
||||
fullWidth
|
||||
className='mt-4'
|
||||
startIcon={<i className='tabler-plus' />}
|
||||
onClick={() => handleOpenProductRecipe({ variant: undefined })}
|
||||
onClick={() => handleOpenProductRecipe(variantData)}
|
||||
>
|
||||
Add Ingredient
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddRecipeDrawer open={openProductRecipe} handleClose={() => setOpenProductRecipe(false)} product={product!} />
|
||||
|
||||
Reference in New Issue
Block a user