Create Unit Conventer
This commit is contained in:
parent
40c417ec72
commit
54c7598e7a
52
src/services/mutations/unitConventor.ts
Normal file
52
src/services/mutations/unitConventor.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'react-toastify'
|
||||
import { api } from '../api'
|
||||
import { IngredientUnitConverterRequest } from '@/types/services/productRecipe'
|
||||
|
||||
export const useUnitConventorMutation = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const createUnitConventer = useMutation({
|
||||
mutationFn: async (newUnitConventer: IngredientUnitConverterRequest) => {
|
||||
const response = await api.post('/unit-converters', newUnitConventer)
|
||||
return response.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('UnitConventer created successfully!')
|
||||
queryClient.invalidateQueries({ queryKey: ['unitConventers'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
|
||||
}
|
||||
})
|
||||
|
||||
const updateUnitConventer = useMutation({
|
||||
mutationFn: async ({ id, payload }: { id: string; payload: IngredientUnitConverterRequest }) => {
|
||||
const response = await api.put(`/unit-converters/${id}`, payload)
|
||||
return response.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('UnitConventer updated successfully!')
|
||||
queryClient.invalidateQueries({ queryKey: ['unit-converters'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
|
||||
}
|
||||
})
|
||||
|
||||
const deleteUnitConventer = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const response = await api.delete(`/unit-converters/${id}`)
|
||||
return response.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('UnitConventer deleted successfully!')
|
||||
queryClient.invalidateQueries({ queryKey: ['unitConventers'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
|
||||
}
|
||||
})
|
||||
|
||||
return { createUnitConventer, updateUnitConventer, deleteUnitConventer }
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Ingredients } from '../../types/services/ingredient'
|
||||
import { api } from '../api'
|
||||
import { Ingredient } from '@/types/services/productRecipe'
|
||||
|
||||
interface IngredientsQueryParams {
|
||||
page?: number
|
||||
@ -34,3 +35,13 @@ export function useIngredients(params: IngredientsQueryParams = {}) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function useIngredientById(id: string) {
|
||||
return useQuery<Ingredient>({
|
||||
queryKey: ['ingredients', id],
|
||||
queryFn: async () => {
|
||||
const res = await api.get(`/ingredients/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,56 +1,75 @@
|
||||
export interface Product {
|
||||
ID: string;
|
||||
OrganizationID: string;
|
||||
CategoryID: string;
|
||||
SKU: string;
|
||||
Name: string;
|
||||
Description: string | null;
|
||||
Price: number;
|
||||
Cost: number;
|
||||
BusinessType: string;
|
||||
ImageURL: string;
|
||||
PrinterType: string;
|
||||
UnitID: string | null;
|
||||
HasIngredients: boolean;
|
||||
Metadata: Record<string, any>;
|
||||
IsActive: boolean;
|
||||
CreatedAt: string; // ISO date string
|
||||
UpdatedAt: string; // ISO date string
|
||||
ID: string
|
||||
OrganizationID: string
|
||||
CategoryID: string
|
||||
SKU: string
|
||||
Name: string
|
||||
Description: string | null
|
||||
Price: number
|
||||
Cost: number
|
||||
BusinessType: string
|
||||
ImageURL: string
|
||||
PrinterType: string
|
||||
UnitID: string | null
|
||||
HasIngredients: boolean
|
||||
Metadata: Record<string, any>
|
||||
IsActive: boolean
|
||||
CreatedAt: string // ISO date string
|
||||
UpdatedAt: string // ISO date string
|
||||
}
|
||||
|
||||
export interface Ingredient {
|
||||
id: string;
|
||||
organization_id: string;
|
||||
outlet_id: string | null;
|
||||
name: string;
|
||||
unit_id: string;
|
||||
cost: number;
|
||||
stock: number;
|
||||
is_semi_finished: boolean;
|
||||
is_active: boolean;
|
||||
metadata: Record<string, any>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
id: string
|
||||
organization_id: string
|
||||
outlet_id: string | null
|
||||
name: string
|
||||
unit_id: string
|
||||
cost: number
|
||||
stock: number
|
||||
is_semi_finished: boolean
|
||||
is_active: boolean
|
||||
metadata: Record<string, any>
|
||||
created_at: string
|
||||
updated_at: string
|
||||
unit: IngredientUnit
|
||||
}
|
||||
|
||||
export interface ProductRecipe {
|
||||
id: string;
|
||||
organization_id: string;
|
||||
outlet_id: string | null;
|
||||
product_id: string;
|
||||
variant_id: string | null;
|
||||
ingredient_id: string;
|
||||
quantity: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
product: Product;
|
||||
ingredient: Ingredient;
|
||||
id: string
|
||||
organization_id: string
|
||||
outlet_id: string | null
|
||||
product_id: string
|
||||
variant_id: string | null
|
||||
ingredient_id: string
|
||||
quantity: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
product: Product
|
||||
ingredient: Ingredient
|
||||
}
|
||||
|
||||
export interface ProductRecipeRequest {
|
||||
product_id: string;
|
||||
variant_id: string | null;
|
||||
ingredient_id: string;
|
||||
quantity: number;
|
||||
outlet_id: string | null;
|
||||
product_id: string
|
||||
variant_id: string | null
|
||||
ingredient_id: string
|
||||
quantity: number
|
||||
outlet_id: string | null
|
||||
}
|
||||
|
||||
export interface IngredientUnit {
|
||||
id: string
|
||||
organization_id: string
|
||||
outlet_id: string
|
||||
name: string
|
||||
abbreviation: string
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface IngredientUnitConverterRequest {
|
||||
ingredient_id: string
|
||||
from_unit_id: string
|
||||
to_unit_id: string
|
||||
conversion_factor: number
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Button from '@mui/material/Button'
|
||||
@ -17,101 +17,121 @@ import { useForm, Controller } from 'react-hook-form'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
import { Ingredient } from '@/types/services/productRecipe'
|
||||
import { useUnits } from '@/services/queries/units'
|
||||
import { useUnitConventorMutation } from '@/services/mutations/unitConventor'
|
||||
|
||||
// Interface Integration
|
||||
export interface IngredientUnitConverterRequest {
|
||||
ingredient_id: string
|
||||
from_unit_id: string
|
||||
to_unit_id: string
|
||||
conversion_factor: number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
handleClose: () => void
|
||||
setData?: (data: any) => void
|
||||
setData?: (data: IngredientUnitConverterRequest) => void
|
||||
data?: Ingredient // Contains ingredientId, unit info, and cost
|
||||
}
|
||||
|
||||
type UnitConversionType = {
|
||||
satuan: string
|
||||
satuan: string // This will be from_unit_id
|
||||
quantity: number
|
||||
unit: string
|
||||
hargaBeli: number
|
||||
unit: string // This will be to_unit_id (from data)
|
||||
hargaBeli: number // Calculated as factor * ingredientCost
|
||||
hargaJual: number
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
type FormValidateType = {
|
||||
conversions: UnitConversionType[]
|
||||
}
|
||||
|
||||
// Vars
|
||||
const initialConversion: UnitConversionType = {
|
||||
satuan: 'Box',
|
||||
quantity: 12,
|
||||
unit: 'Pcs',
|
||||
hargaBeli: 3588000,
|
||||
hargaJual: 5988000,
|
||||
isDefault: false
|
||||
}
|
||||
|
||||
const IngedientUnitConversionDrawer = (props: Props) => {
|
||||
// Props
|
||||
const { open, handleClose, setData } = props
|
||||
const { open, handleClose, setData, data } = props
|
||||
|
||||
// Extract values from data prop with safe defaults
|
||||
const ingredientId = data?.id || ''
|
||||
const toUnitId = data?.unit_id || data?.unit?.id || ''
|
||||
const ingredientCost = data?.cost || 0
|
||||
|
||||
const {
|
||||
data: units,
|
||||
isLoading,
|
||||
error,
|
||||
isFetching
|
||||
} = useUnits({
|
||||
page: 1,
|
||||
limit: 20
|
||||
})
|
||||
|
||||
// Vars - initial state with values from data
|
||||
const getInitialConversion = () => ({
|
||||
satuan: '',
|
||||
quantity: 1,
|
||||
unit: toUnitId, // Set from data
|
||||
hargaBeli: ingredientCost, // Will be calculated as factor * ingredientCost
|
||||
hargaJual: 0,
|
||||
isDefault: true
|
||||
})
|
||||
|
||||
// States
|
||||
const [conversions, setConversions] = useState<UnitConversionType[]>([initialConversion])
|
||||
const [conversion, setConversion] = useState<UnitConversionType>(getInitialConversion())
|
||||
const { createUnitConventer } = useUnitConventorMutation()
|
||||
|
||||
// Hooks
|
||||
const {
|
||||
control,
|
||||
reset: resetForm,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors }
|
||||
} = useForm<FormValidateType>({
|
||||
defaultValues: {
|
||||
conversions: [initialConversion]
|
||||
}
|
||||
} = useForm<UnitConversionType>({
|
||||
defaultValues: getInitialConversion()
|
||||
})
|
||||
|
||||
// Update form when data changes
|
||||
useEffect(() => {
|
||||
if (toUnitId || ingredientCost) {
|
||||
const updatedConversion = getInitialConversion()
|
||||
setConversion(updatedConversion)
|
||||
resetForm(updatedConversion)
|
||||
}
|
||||
}, [toUnitId, ingredientCost, resetForm])
|
||||
|
||||
// Functions untuk konversi unit
|
||||
const handleTambahBaris = () => {
|
||||
const newConversion: UnitConversionType = {
|
||||
satuan: '',
|
||||
quantity: 0,
|
||||
unit: '',
|
||||
hargaBeli: 0,
|
||||
hargaJual: 0,
|
||||
isDefault: false
|
||||
}
|
||||
setConversions([...conversions, newConversion])
|
||||
const handleChangeConversion = (field: keyof UnitConversionType, value: any) => {
|
||||
const newConversion = { ...conversion, [field]: value }
|
||||
setConversion(newConversion)
|
||||
setValue(field, value)
|
||||
}
|
||||
|
||||
const handleHapusBaris = (index: number) => {
|
||||
if (conversions.length > 1) {
|
||||
const newConversions = conversions.filter((_, i) => i !== index)
|
||||
setConversions(newConversions)
|
||||
}
|
||||
const onSubmit = (data: UnitConversionType) => {
|
||||
// Transform form data to IngredientUnitConverterRequest
|
||||
const converterRequest: IngredientUnitConverterRequest = {
|
||||
ingredient_id: ingredientId,
|
||||
from_unit_id: conversion.satuan,
|
||||
to_unit_id: toUnitId, // Use toUnitId from data prop
|
||||
conversion_factor: conversion.quantity
|
||||
}
|
||||
|
||||
const handleChangeConversion = (index: number, field: keyof UnitConversionType, value: any) => {
|
||||
const newConversions = [...conversions]
|
||||
newConversions[index] = { ...newConversions[index], [field]: value }
|
||||
setConversions(newConversions)
|
||||
}
|
||||
console.log('Unit conversion request:', converterRequest)
|
||||
|
||||
const handleToggleDefault = (index: number) => {
|
||||
const newConversions = conversions.map((conversion, i) => ({
|
||||
...conversion,
|
||||
isDefault: i === index
|
||||
}))
|
||||
setConversions(newConversions)
|
||||
}
|
||||
|
||||
const onSubmit = (data: FormValidateType) => {
|
||||
console.log('Unit conversions:', conversions)
|
||||
if (setData) {
|
||||
setData(conversions)
|
||||
}
|
||||
// if (setData) {
|
||||
// setData(converterRequest)
|
||||
// }
|
||||
createUnitConventer.mutate(converterRequest, {
|
||||
onSuccess: () => {
|
||||
handleClose()
|
||||
resetForm(getInitialConversion())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
handleClose()
|
||||
setConversions([initialConversion])
|
||||
resetForm({ conversions: [initialConversion] })
|
||||
const resetData = getInitialConversion()
|
||||
setConversion(resetData)
|
||||
resetForm(resetData)
|
||||
}
|
||||
|
||||
const formatNumber = (value: number) => {
|
||||
@ -122,6 +142,12 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
||||
return parseInt(value.replace(/\./g, '')) || 0
|
||||
}
|
||||
|
||||
// Calculate total purchase price: factor * ingredientCost
|
||||
const totalPurchasePrice = conversion.quantity * ingredientCost
|
||||
|
||||
// Validation to ensure all required fields are provided
|
||||
const isValidForSubmit = ingredientId && conversion.satuan && toUnitId && conversion.quantity > 0
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
@ -155,17 +181,37 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
||||
<i className='tabler-x text-2xl text-textPrimary' />
|
||||
</IconButton>
|
||||
</div>
|
||||
{!ingredientId && (
|
||||
<Box sx={{ px: 3, pb: 2 }}>
|
||||
<Typography variant='body2' color='error'>
|
||||
Warning: Ingredient data is required for conversion
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{ingredientId && (
|
||||
<Box sx={{ px: 3, pb: 2 }}>
|
||||
<Typography variant='body2' color='text.secondary'>
|
||||
Converting for: {data?.name || `Ingredient ${ingredientId}`}
|
||||
</Typography>
|
||||
{ingredientCost > 0 && (
|
||||
<Typography variant='body2' color='text.secondary'>
|
||||
Base cost per {units?.data.find(u => u.id === toUnitId)?.name || 'unit'}: Rp{' '}
|
||||
{formatNumber(ingredientCost)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
||||
<form id='unit-conversion-form' onSubmit={handleSubmit(data => onSubmit(data))}>
|
||||
<form id='unit-conversion-form' onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className='flex flex-col gap-6 p-6'>
|
||||
{/* Header Kolom */}
|
||||
<Grid container spacing={2} alignItems='center' className='bg-gray-50 p-3 rounded-lg'>
|
||||
<Grid size={2}>
|
||||
<Typography variant='body2' fontWeight='medium'>
|
||||
Satuan
|
||||
From Unit
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={1} className='text-center'>
|
||||
@ -175,20 +221,20 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
||||
</Grid>
|
||||
<Grid size={1.5}>
|
||||
<Typography variant='body2' fontWeight='medium'>
|
||||
Jumlah
|
||||
Factor
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={1.5}>
|
||||
<Typography variant='body2' fontWeight='medium'>
|
||||
Unit
|
||||
To Unit
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Grid size={2.5}>
|
||||
<Typography variant='body2' fontWeight='medium'>
|
||||
Harga Beli
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Grid size={2.5}>
|
||||
<Typography variant='body2' fontWeight='medium'>
|
||||
Harga Jual
|
||||
</Typography>
|
||||
@ -198,37 +244,48 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
||||
Default
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={1}>
|
||||
</Grid>
|
||||
|
||||
{/* Form Input Row */}
|
||||
<Grid container spacing={2} alignItems='center' className='py-2'>
|
||||
{/* From Unit (Satuan) */}
|
||||
<Grid size={2}>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Typography variant='body2' fontWeight='medium'>
|
||||
Action
|
||||
1
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Baris Konversi */}
|
||||
{conversions.map((conversion, index) => (
|
||||
<Grid container spacing={2} alignItems='center' key={index} className='py-2'>
|
||||
<Grid size={0.5}>
|
||||
<Typography variant='body2' color='text.secondary'>
|
||||
{index + 1}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* Satuan */}
|
||||
<Grid size={1.5}>
|
||||
<Controller
|
||||
name='satuan'
|
||||
control={control}
|
||||
rules={{ required: 'From unit wajib dipilih' }}
|
||||
render={({ field }) => (
|
||||
<CustomTextField
|
||||
{...field}
|
||||
select
|
||||
fullWidth
|
||||
size='small'
|
||||
value={conversion.satuan}
|
||||
onChange={e => handleChangeConversion(index, 'satuan', e.target.value)}
|
||||
error={!!errors.satuan}
|
||||
onChange={e => {
|
||||
field.onChange(e.target.value)
|
||||
handleChangeConversion('satuan', e.target.value)
|
||||
}}
|
||||
>
|
||||
<MenuItem value='Box'>Box</MenuItem>
|
||||
<MenuItem value='Kg'>Kg</MenuItem>
|
||||
<MenuItem value='Liter'>Liter</MenuItem>
|
||||
<MenuItem value='Pack'>Pack</MenuItem>
|
||||
<MenuItem value='Pcs'>Pcs</MenuItem>
|
||||
{units?.data
|
||||
.filter(unit => unit.id !== toUnitId) // Prevent selecting same unit as target
|
||||
.map(unit => (
|
||||
<MenuItem key={unit.id} value={unit.id}>
|
||||
{unit.name}
|
||||
</MenuItem>
|
||||
)) ?? []}
|
||||
</CustomTextField>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{errors.satuan && (
|
||||
<Typography variant='caption' color='error' className='mt-1'>
|
||||
{errors.satuan.message}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* Tanda sama dengan */}
|
||||
@ -236,59 +293,106 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
||||
<Typography variant='h6'>=</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* Quantity */}
|
||||
{/* Conversion Factor (Quantity) */}
|
||||
<Grid size={1.5}>
|
||||
<Controller
|
||||
name='quantity'
|
||||
control={control}
|
||||
rules={{
|
||||
required: 'Conversion factor wajib diisi',
|
||||
min: { value: 0.01, message: 'Minimal 0.01' }
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<CustomTextField
|
||||
{...field}
|
||||
fullWidth
|
||||
size='small'
|
||||
type='number'
|
||||
value={conversion.quantity}
|
||||
onChange={e => handleChangeConversion(index, 'quantity', parseInt(e.target.value) || 0)}
|
||||
error={!!errors.quantity}
|
||||
onChange={e => {
|
||||
const value = parseFloat(e.target.value) || 0
|
||||
field.onChange(value)
|
||||
handleChangeConversion('quantity', value)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.quantity && (
|
||||
<Typography variant='caption' color='error' className='mt-1'>
|
||||
{errors.quantity.message}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* Unit */}
|
||||
{/* To Unit - Disabled because it comes from data */}
|
||||
<Grid size={1.5}>
|
||||
<CustomTextField
|
||||
select
|
||||
fullWidth
|
||||
size='small'
|
||||
value={conversion.unit}
|
||||
onChange={e => handleChangeConversion(index, 'unit', e.target.value)}
|
||||
value={toUnitId}
|
||||
disabled
|
||||
InputProps={{
|
||||
sx: { backgroundColor: 'grey.100' }
|
||||
}}
|
||||
>
|
||||
<MenuItem value='Pcs'>Pcs</MenuItem>
|
||||
<MenuItem value='Kg'>Kg</MenuItem>
|
||||
<MenuItem value='Gram'>Gram</MenuItem>
|
||||
<MenuItem value='Liter'>Liter</MenuItem>
|
||||
<MenuItem value='ML'>ML</MenuItem>
|
||||
{units?.data.map(unit => (
|
||||
<MenuItem key={unit.id} value={unit.id}>
|
||||
{unit.name}
|
||||
</MenuItem>
|
||||
)) ?? []}
|
||||
</CustomTextField>
|
||||
</Grid>
|
||||
|
||||
{/* Harga Beli */}
|
||||
<Grid size={2}>
|
||||
{/* Harga Beli - Calculated as factor * ingredientCost */}
|
||||
<Grid size={2.5}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
size='small'
|
||||
value={formatNumber(conversion.hargaBeli)}
|
||||
onChange={e => handleChangeConversion(index, 'hargaBeli', parseNumber(e.target.value))}
|
||||
value={formatNumber(totalPurchasePrice)}
|
||||
disabled
|
||||
InputProps={{
|
||||
sx: { backgroundColor: 'grey.100' }
|
||||
}}
|
||||
placeholder='Calculated purchase price'
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Harga Jual */}
|
||||
<Grid size={2}>
|
||||
<Grid size={2.5}>
|
||||
<Controller
|
||||
name='hargaJual'
|
||||
control={control}
|
||||
rules={{
|
||||
min: { value: 0, message: 'Tidak boleh negatif' }
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
size='small'
|
||||
error={!!errors.hargaJual}
|
||||
value={formatNumber(conversion.hargaJual)}
|
||||
onChange={e => handleChangeConversion(index, 'hargaJual', parseNumber(e.target.value))}
|
||||
onChange={e => {
|
||||
const value = parseNumber(e.target.value)
|
||||
field.onChange(value)
|
||||
handleChangeConversion('hargaJual', value)
|
||||
}}
|
||||
placeholder='Optional'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.hargaJual && (
|
||||
<Typography variant='caption' color='error' className='mt-1'>
|
||||
{errors.hargaJual.message}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* Default Star */}
|
||||
<Grid size={1} className='text-center'>
|
||||
<IconButton
|
||||
size='small'
|
||||
onClick={() => handleToggleDefault(index)}
|
||||
onClick={() => handleChangeConversion('isDefault', !conversion.isDefault)}
|
||||
sx={{
|
||||
color: conversion.isDefault ? 'warning.main' : 'grey.400'
|
||||
}}
|
||||
@ -296,48 +400,67 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
||||
<i className={conversion.isDefault ? 'tabler-star-filled' : 'tabler-star'} />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Delete Button */}
|
||||
<Grid size={1} className='text-center'>
|
||||
{conversions.length > 1 && (
|
||||
<IconButton
|
||||
size='small'
|
||||
onClick={() => handleHapusBaris(index)}
|
||||
sx={{
|
||||
color: 'error.main',
|
||||
border: 1,
|
||||
borderColor: 'error.main',
|
||||
'&:hover': {
|
||||
backgroundColor: 'error.light',
|
||||
borderColor: 'error.main'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className='tabler-trash' />
|
||||
</IconButton>
|
||||
{/* Conversion Preview */}
|
||||
{conversion.quantity > 0 && conversion.satuan && toUnitId && (
|
||||
<Box className='bg-green-50 p-4 rounded-lg border-l-4 border-green-500'>
|
||||
<Typography variant='body2' fontWeight='medium' className='mb-2'>
|
||||
Conversion Preview:
|
||||
</Typography>
|
||||
<Typography variant='body2' className='mb-1'>
|
||||
<strong>1 {units?.data.find(u => u.id === conversion.satuan)?.name || 'Unit'}</strong> ={' '}
|
||||
<strong>
|
||||
{conversion.quantity} {units?.data.find(u => u.id === toUnitId)?.name || 'Unit'}
|
||||
</strong>
|
||||
</Typography>
|
||||
<Typography variant='caption' color='text.secondary'>
|
||||
Conversion Factor: {conversion.quantity}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
{/* Tambah Baris Button */}
|
||||
<div className='flex items-center justify-start'>
|
||||
<Button
|
||||
variant='outlined'
|
||||
startIcon={<i className='tabler-plus' />}
|
||||
onClick={handleTambahBaris}
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
borderColor: 'primary.main',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.light',
|
||||
borderColor: 'primary.main'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Tambah baris
|
||||
</Button>
|
||||
</div>
|
||||
{/* Price Summary */}
|
||||
{conversion.quantity > 0 && (ingredientCost > 0 || conversion.hargaJual > 0) && (
|
||||
<Box className='bg-blue-50 p-4 rounded-lg border-l-4 border-blue-500'>
|
||||
<Typography variant='body2' fontWeight='medium' className='mb-2'>
|
||||
Price Summary:
|
||||
</Typography>
|
||||
{ingredientCost > 0 && (
|
||||
<>
|
||||
<Typography variant='body2'>
|
||||
Total Purchase Price (1 {units?.data.find(u => u.id === conversion.satuan)?.name || 'From Unit'}):
|
||||
Rp {formatNumber(totalPurchasePrice)}
|
||||
</Typography>
|
||||
<Typography variant='body2'>
|
||||
Unit Cost per {units?.data.find(u => u.id === toUnitId)?.name || 'To Unit'}: Rp{' '}
|
||||
{formatNumber(ingredientCost)}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{conversion.hargaJual > 0 && (
|
||||
<>
|
||||
<Typography variant='body2'>
|
||||
Total Selling Price (1 {units?.data.find(u => u.id === conversion.satuan)?.name || 'From Unit'}):
|
||||
Rp {formatNumber(conversion.hargaJual)}
|
||||
</Typography>
|
||||
<Typography variant='body2'>
|
||||
Unit Selling Price per {units?.data.find(u => u.id === toUnitId)?.name || 'To Unit'}: Rp{' '}
|
||||
{formatNumber(Math.round(conversion.hargaJual / conversion.quantity))}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{ingredientCost > 0 && conversion.hargaJual > 0 && (
|
||||
<Typography variant='body2' className='mt-2 text-blue-700'>
|
||||
Total Margin: Rp {formatNumber(conversion.hargaJual - totalPurchasePrice)} (
|
||||
{totalPurchasePrice > 0
|
||||
? (((conversion.hargaJual - totalPurchasePrice) / totalPurchasePrice) * 100).toFixed(1)
|
||||
: 0}
|
||||
%)
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Box>
|
||||
@ -355,13 +478,21 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Button variant='contained' type='submit' form='unit-conversion-form'>
|
||||
Simpan
|
||||
<Button variant='contained' type='submit' form='unit-conversion-form' disabled={!isValidForSubmit}>
|
||||
Simpan Konversi
|
||||
</Button>
|
||||
<Button variant='tonal' color='error' onClick={() => handleReset()}>
|
||||
<Button variant='tonal' color='error' onClick={handleReset}>
|
||||
Batal
|
||||
</Button>
|
||||
</div>
|
||||
{!isValidForSubmit && (
|
||||
<Typography variant='caption' color='error' className='mt-2'>
|
||||
Please fill in all required fields: {!ingredientId && 'Ingredient Data, '}
|
||||
{!conversion.satuan && 'From Unit, '}
|
||||
{!toUnitId && 'To Unit (from ingredient data), '}
|
||||
{conversion.quantity <= 0 && 'Conversion Factor'}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Drawer>
|
||||
)
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
import { Ingredient } from '@/types/services/productRecipe'
|
||||
import { formatCurrency } from '@/utils/transform'
|
||||
import { Card, CardHeader, Chip, Typography } from '@mui/material'
|
||||
|
||||
const IngredientDetailInfo = () => {
|
||||
interface Props {
|
||||
data: Ingredient | undefined
|
||||
}
|
||||
|
||||
const IngredientDetailInfo = ({ data }: Props) => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title={
|
||||
<div className='flex items-center gap-3'>
|
||||
<Typography variant='h4' component='h1' className='font-bold'>
|
||||
Tepung Terigu
|
||||
{data?.name ?? '-'}
|
||||
</Typography>
|
||||
<Chip label={'Active'} color={'success'} size='small' />
|
||||
</div>
|
||||
@ -17,7 +22,7 @@ const IngredientDetailInfo = () => {
|
||||
<div className='flex flex-col gap-1 mt-2'>
|
||||
<div className='flex gap-4'>
|
||||
<Typography variant='body2'>
|
||||
<span className='font-semibold'>Cost:</span> {formatCurrency(5000)}
|
||||
<span className='font-semibold'>Cost:</span> {formatCurrency(data?.cost ?? 0)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,8 +2,13 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Card, CardContent, CardHeader, Typography, Button, Box, Stack } from '@mui/material'
|
||||
import IngedientUnitConversionDrawer from './IngedientUnitConversionDrawer' // Sesuaikan dengan path file Anda
|
||||
import { Ingredient } from '@/types/services/productRecipe'
|
||||
|
||||
const IngredientDetailUnit = () => {
|
||||
interface Props {
|
||||
data: Ingredient | undefined
|
||||
}
|
||||
|
||||
const IngredientDetailUnit = ({ data }: Props) => {
|
||||
// State untuk mengontrol drawer
|
||||
const [openConversionDrawer, setOpenConversionDrawer] = useState(false)
|
||||
|
||||
@ -34,7 +39,7 @@ const IngredientDetailUnit = () => {
|
||||
Satuan Dasar
|
||||
</Typography>
|
||||
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
||||
: Pcs
|
||||
: {data?.unit.name ?? '-'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
@ -61,6 +66,7 @@ const IngredientDetailUnit = () => {
|
||||
open={openConversionDrawer}
|
||||
handleClose={handleCloseConversionDrawer}
|
||||
setData={handleSetConversionData}
|
||||
data={data}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -6,11 +6,18 @@ import IngredientDetailInfo from './IngredientDetailInfo'
|
||||
import IngredientDetailUnit from './IngredientDetailUnit'
|
||||
import IngredientDetailStockAdjustmentDrawer from './IngredientDetailStockAdjustmentDrawer' // Sesuaikan dengan path file Anda
|
||||
import { Button } from '@mui/material'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useIngredientById } from '@/services/queries/ingredients'
|
||||
|
||||
const IngredientDetail = () => {
|
||||
// State untuk mengontrol stock adjustment drawer
|
||||
const [openStockAdjustmentDrawer, setOpenStockAdjustmentDrawer] = useState(false)
|
||||
|
||||
const params = useParams()
|
||||
const id = params?.id
|
||||
|
||||
const { data, isLoading } = useIngredientById(id as string)
|
||||
|
||||
// Function untuk membuka stock adjustment drawer
|
||||
const handleOpenStockAdjustmentDrawer = () => {
|
||||
setOpenStockAdjustmentDrawer(true)
|
||||
@ -32,7 +39,7 @@ const IngredientDetail = () => {
|
||||
<>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, lg: 8, md: 7 }}>
|
||||
<IngredientDetailInfo />
|
||||
<IngredientDetailInfo data={data} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 4, md: 5 }}>
|
||||
<Button
|
||||
@ -51,7 +58,7 @@ const IngredientDetail = () => {
|
||||
>
|
||||
Penyesuaian Stok
|
||||
</Button>
|
||||
<IngredientDetailUnit />
|
||||
<IngredientDetailUnit data={data} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user