Compare commits

...

2 Commits

Author SHA1 Message Date
efrilm
54c7598e7a Create Unit Conventer 2025-09-12 18:49:01 +07:00
efrilm
40c417ec72 Delete and Edit Vendor 2025-09-12 15:23:19 +07:00
12 changed files with 728 additions and 372 deletions

View 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 }
}

View File

@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { Ingredients } from '../../types/services/ingredient' import { Ingredients } from '../../types/services/ingredient'
import { api } from '../api' import { api } from '../api'
import { Ingredient } from '@/types/services/productRecipe'
interface IngredientsQueryParams { interface IngredientsQueryParams {
page?: number 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
}
})
}

View File

@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { api } from '../api' import { api } from '../api'
import { Vendors } from '@/types/services/vendor' import { Vendor, Vendors } from '@/types/services/vendor'
interface VendorQueryParams { interface VendorQueryParams {
page?: number page?: number
@ -34,3 +34,13 @@ export function useVendors(params: VendorQueryParams = {}) {
} }
}) })
} }
export function useVendorById(id: string) {
return useQuery<Vendor>({
queryKey: ['vendors', id],
queryFn: async () => {
const res = await api.get(`/vendors/${id}`)
return res.data.data
}
})
}

View File

@ -1,56 +1,75 @@
export interface Product { export interface Product {
ID: string; ID: string
OrganizationID: string; OrganizationID: string
CategoryID: string; CategoryID: string
SKU: string; SKU: string
Name: string; Name: string
Description: string | null; Description: string | null
Price: number; Price: number
Cost: number; Cost: number
BusinessType: string; BusinessType: string
ImageURL: string; ImageURL: string
PrinterType: string; PrinterType: string
UnitID: string | null; UnitID: string | null
HasIngredients: boolean; HasIngredients: boolean
Metadata: Record<string, any>; Metadata: Record<string, any>
IsActive: boolean; IsActive: boolean
CreatedAt: string; // ISO date string CreatedAt: string // ISO date string
UpdatedAt: string; // ISO date string UpdatedAt: string // ISO date string
} }
export interface Ingredient { export interface Ingredient {
id: string; id: string
organization_id: string; organization_id: string
outlet_id: string | null; outlet_id: string | null
name: string; name: string
unit_id: string; unit_id: string
cost: number; cost: number
stock: number; stock: number
is_semi_finished: boolean; is_semi_finished: boolean
is_active: boolean; is_active: boolean
metadata: Record<string, any>; metadata: Record<string, any>
created_at: string; created_at: string
updated_at: string; updated_at: string
unit: IngredientUnit
} }
export interface ProductRecipe { export interface ProductRecipe {
id: string; id: string
organization_id: string; organization_id: string
outlet_id: string | null; outlet_id: string | null
product_id: string; product_id: string
variant_id: string | null; variant_id: string | null
ingredient_id: string; ingredient_id: string
quantity: number; quantity: number
created_at: string; created_at: string
updated_at: string; updated_at: string
product: Product; product: Product
ingredient: Ingredient; ingredient: Ingredient
} }
export interface ProductRecipeRequest { export interface ProductRecipeRequest {
product_id: string; product_id: string
variant_id: string | null; variant_id: string | null
ingredient_id: string; ingredient_id: string
quantity: number; quantity: number
outlet_id: string | null; 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
} }

View File

@ -1,6 +1,6 @@
'use client' 'use client'
// React Imports // React Imports
import { useState } from 'react' import { useState, useEffect } from 'react'
// MUI Imports // MUI Imports
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
@ -17,101 +17,121 @@ import { useForm, Controller } from 'react-hook-form'
// Component Imports // Component Imports
import CustomTextField from '@core/components/mui/TextField' 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 = { type Props = {
open: boolean open: boolean
handleClose: () => void handleClose: () => void
setData?: (data: any) => void setData?: (data: IngredientUnitConverterRequest) => void
data?: Ingredient // Contains ingredientId, unit info, and cost
} }
type UnitConversionType = { type UnitConversionType = {
satuan: string satuan: string // This will be from_unit_id
quantity: number quantity: number
unit: string unit: string // This will be to_unit_id (from data)
hargaBeli: number hargaBeli: number // Calculated as factor * ingredientCost
hargaJual: number hargaJual: number
isDefault: boolean 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) => { const IngedientUnitConversionDrawer = (props: 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 // States
const [conversions, setConversions] = useState<UnitConversionType[]>([initialConversion]) const [conversion, setConversion] = useState<UnitConversionType>(getInitialConversion())
const { createUnitConventer } = useUnitConventorMutation()
// Hooks // Hooks
const { const {
control, control,
reset: resetForm, reset: resetForm,
handleSubmit, handleSubmit,
setValue,
formState: { errors } formState: { errors }
} = useForm<FormValidateType>({ } = useForm<UnitConversionType>({
defaultValues: { defaultValues: getInitialConversion()
conversions: [initialConversion]
}
}) })
// Update form when data changes
useEffect(() => {
if (toUnitId || ingredientCost) {
const updatedConversion = getInitialConversion()
setConversion(updatedConversion)
resetForm(updatedConversion)
}
}, [toUnitId, ingredientCost, resetForm])
// Functions untuk konversi unit // Functions untuk konversi unit
const handleTambahBaris = () => { const handleChangeConversion = (field: keyof UnitConversionType, value: any) => {
const newConversion: UnitConversionType = { const newConversion = { ...conversion, [field]: value }
satuan: '', setConversion(newConversion)
quantity: 0, setValue(field, value)
unit: '', }
hargaBeli: 0,
hargaJual: 0, const onSubmit = (data: UnitConversionType) => {
isDefault: false // 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
} }
setConversions([...conversions, newConversion])
}
const handleHapusBaris = (index: number) => { console.log('Unit conversion request:', converterRequest)
if (conversions.length > 1) {
const newConversions = conversions.filter((_, i) => i !== index)
setConversions(newConversions)
}
}
const handleChangeConversion = (index: number, field: keyof UnitConversionType, value: any) => { // if (setData) {
const newConversions = [...conversions] // setData(converterRequest)
newConversions[index] = { ...newConversions[index], [field]: value } // }
setConversions(newConversions) createUnitConventer.mutate(converterRequest, {
} onSuccess: () => {
handleClose()
const handleToggleDefault = (index: number) => { resetForm(getInitialConversion())
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)
}
handleClose()
} }
const handleReset = () => { const handleReset = () => {
handleClose() handleClose()
setConversions([initialConversion]) const resetData = getInitialConversion()
resetForm({ conversions: [initialConversion] }) setConversion(resetData)
resetForm(resetData)
} }
const formatNumber = (value: number) => { const formatNumber = (value: number) => {
@ -122,6 +142,12 @@ const IngedientUnitConversionDrawer = (props: Props) => {
return parseInt(value.replace(/\./g, '')) || 0 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 ( return (
<Drawer <Drawer
open={open} open={open}
@ -155,17 +181,37 @@ const IngedientUnitConversionDrawer = (props: Props) => {
<i className='tabler-x text-2xl text-textPrimary' /> <i className='tabler-x text-2xl text-textPrimary' />
</IconButton> </IconButton>
</div> </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> </Box>
{/* Scrollable Content */} {/* Scrollable Content */}
<Box sx={{ flex: 1, overflowY: 'auto' }}> <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'> <div className='flex flex-col gap-6 p-6'>
{/* Header Kolom */} {/* Header Kolom */}
<Grid container spacing={2} alignItems='center' className='bg-gray-50 p-3 rounded-lg'> <Grid container spacing={2} alignItems='center' className='bg-gray-50 p-3 rounded-lg'>
<Grid size={2}> <Grid size={2}>
<Typography variant='body2' fontWeight='medium'> <Typography variant='body2' fontWeight='medium'>
Satuan From Unit
</Typography> </Typography>
</Grid> </Grid>
<Grid size={1} className='text-center'> <Grid size={1} className='text-center'>
@ -175,20 +221,20 @@ const IngedientUnitConversionDrawer = (props: Props) => {
</Grid> </Grid>
<Grid size={1.5}> <Grid size={1.5}>
<Typography variant='body2' fontWeight='medium'> <Typography variant='body2' fontWeight='medium'>
Jumlah Factor
</Typography> </Typography>
</Grid> </Grid>
<Grid size={1.5}> <Grid size={1.5}>
<Typography variant='body2' fontWeight='medium'> <Typography variant='body2' fontWeight='medium'>
Unit To Unit
</Typography> </Typography>
</Grid> </Grid>
<Grid size={2}> <Grid size={2.5}>
<Typography variant='body2' fontWeight='medium'> <Typography variant='body2' fontWeight='medium'>
Harga Beli Harga Beli
</Typography> </Typography>
</Grid> </Grid>
<Grid size={2}> <Grid size={2.5}>
<Typography variant='body2' fontWeight='medium'> <Typography variant='body2' fontWeight='medium'>
Harga Jual Harga Jual
</Typography> </Typography>
@ -198,146 +244,223 @@ const IngedientUnitConversionDrawer = (props: Props) => {
Default Default
</Typography> </Typography>
</Grid> </Grid>
<Grid size={1}> </Grid>
<Typography variant='body2' fontWeight='medium'>
Action {/* Form Input Row */}
</Typography> <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'>
1
</Typography>
<Controller
name='satuan'
control={control}
rules={{ required: 'From unit wajib dipilih' }}
render={({ field }) => (
<CustomTextField
{...field}
select
fullWidth
size='small'
error={!!errors.satuan}
onChange={e => {
field.onChange(e.target.value)
handleChangeConversion('satuan', e.target.value)
}}
>
{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 */}
<Grid size={1} className='text-center'>
<Typography variant='h6'>=</Typography>
</Grid>
{/* 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'
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>
{/* To Unit - Disabled because it comes from data */}
<Grid size={1.5}>
<CustomTextField
select
fullWidth
size='small'
value={toUnitId}
disabled
InputProps={{
sx: { backgroundColor: 'grey.100' }
}}
>
{units?.data.map(unit => (
<MenuItem key={unit.id} value={unit.id}>
{unit.name}
</MenuItem>
)) ?? []}
</CustomTextField>
</Grid>
{/* Harga Beli - Calculated as factor * ingredientCost */}
<Grid size={2.5}>
<CustomTextField
fullWidth
size='small'
value={formatNumber(totalPurchasePrice)}
disabled
InputProps={{
sx: { backgroundColor: 'grey.100' }
}}
placeholder='Calculated purchase price'
/>
</Grid>
{/* Harga Jual */}
<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 => {
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={() => handleChangeConversion('isDefault', !conversion.isDefault)}
sx={{
color: conversion.isDefault ? 'warning.main' : 'grey.400'
}}
>
<i className={conversion.isDefault ? 'tabler-star-filled' : 'tabler-star'} />
</IconButton>
</Grid> </Grid>
</Grid> </Grid>
{/* Baris Konversi */} {/* Conversion Preview */}
{conversions.map((conversion, index) => ( {conversion.quantity > 0 && conversion.satuan && toUnitId && (
<Grid container spacing={2} alignItems='center' key={index} className='py-2'> <Box className='bg-green-50 p-4 rounded-lg border-l-4 border-green-500'>
<Grid size={0.5}> <Typography variant='body2' fontWeight='medium' className='mb-2'>
<Typography variant='body2' color='text.secondary'> Conversion Preview:
{index + 1} </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>
)}
{/* 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> </Typography>
</Grid> )}
</Box>
{/* Satuan */} )}
<Grid size={1.5}>
<CustomTextField
select
fullWidth
size='small'
value={conversion.satuan}
onChange={e => handleChangeConversion(index, '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>
</CustomTextField>
</Grid>
{/* Tanda sama dengan */}
<Grid size={1} className='text-center'>
<Typography variant='h6'>=</Typography>
</Grid>
{/* Quantity */}
<Grid size={1.5}>
<CustomTextField
fullWidth
size='small'
type='number'
value={conversion.quantity}
onChange={e => handleChangeConversion(index, 'quantity', parseInt(e.target.value) || 0)}
/>
</Grid>
{/* Unit */}
<Grid size={1.5}>
<CustomTextField
select
fullWidth
size='small'
value={conversion.unit}
onChange={e => handleChangeConversion(index, 'unit', e.target.value)}
>
<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>
</CustomTextField>
</Grid>
{/* Harga Beli */}
<Grid size={2}>
<CustomTextField
fullWidth
size='small'
value={formatNumber(conversion.hargaBeli)}
onChange={e => handleChangeConversion(index, 'hargaBeli', parseNumber(e.target.value))}
/>
</Grid>
{/* Harga Jual */}
<Grid size={2}>
<CustomTextField
fullWidth
size='small'
value={formatNumber(conversion.hargaJual)}
onChange={e => handleChangeConversion(index, 'hargaJual', parseNumber(e.target.value))}
/>
</Grid>
{/* Default Star */}
<Grid size={1} className='text-center'>
<IconButton
size='small'
onClick={() => handleToggleDefault(index)}
sx={{
color: conversion.isDefault ? 'warning.main' : 'grey.400'
}}
>
<i className={conversion.isDefault ? 'tabler-star-filled' : 'tabler-star'} />
</IconButton>
</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>
)}
</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>
</div> </div>
</form> </form>
</Box> </Box>
@ -355,13 +478,21 @@ const IngedientUnitConversionDrawer = (props: Props) => {
}} }}
> >
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
<Button variant='contained' type='submit' form='unit-conversion-form'> <Button variant='contained' type='submit' form='unit-conversion-form' disabled={!isValidForSubmit}>
Simpan Simpan Konversi
</Button> </Button>
<Button variant='tonal' color='error' onClick={() => handleReset()}> <Button variant='tonal' color='error' onClick={handleReset}>
Batal Batal
</Button> </Button>
</div> </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> </Box>
</Drawer> </Drawer>
) )

View File

@ -1,14 +1,19 @@
import { Ingredient } from '@/types/services/productRecipe'
import { formatCurrency } from '@/utils/transform' import { formatCurrency } from '@/utils/transform'
import { Card, CardHeader, Chip, Typography } from '@mui/material' import { Card, CardHeader, Chip, Typography } from '@mui/material'
const IngredientDetailInfo = () => { interface Props {
data: Ingredient | undefined
}
const IngredientDetailInfo = ({ data }: Props) => {
return ( return (
<Card> <Card>
<CardHeader <CardHeader
title={ title={
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<Typography variant='h4' component='h1' className='font-bold'> <Typography variant='h4' component='h1' className='font-bold'>
Tepung Terigu {data?.name ?? '-'}
</Typography> </Typography>
<Chip label={'Active'} color={'success'} size='small' /> <Chip label={'Active'} color={'success'} size='small' />
</div> </div>
@ -17,7 +22,7 @@ const IngredientDetailInfo = () => {
<div className='flex flex-col gap-1 mt-2'> <div className='flex flex-col gap-1 mt-2'>
<div className='flex gap-4'> <div className='flex gap-4'>
<Typography variant='body2'> <Typography variant='body2'>
<span className='font-semibold'>Cost:</span> {formatCurrency(5000)} <span className='font-semibold'>Cost:</span> {formatCurrency(data?.cost ?? 0)}
</Typography> </Typography>
</div> </div>
</div> </div>

View File

@ -2,8 +2,13 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { Card, CardContent, CardHeader, Typography, Button, Box, Stack } from '@mui/material' import { Card, CardContent, CardHeader, Typography, Button, Box, Stack } from '@mui/material'
import IngedientUnitConversionDrawer from './IngedientUnitConversionDrawer' // Sesuaikan dengan path file Anda 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 // State untuk mengontrol drawer
const [openConversionDrawer, setOpenConversionDrawer] = useState(false) const [openConversionDrawer, setOpenConversionDrawer] = useState(false)
@ -34,7 +39,7 @@ const IngredientDetailUnit = () => {
Satuan Dasar Satuan Dasar
</Typography> </Typography>
<Typography variant='body1' sx={{ fontWeight: 'medium' }}> <Typography variant='body1' sx={{ fontWeight: 'medium' }}>
: Pcs : {data?.unit.name ?? '-'}
</Typography> </Typography>
</Box> </Box>
</Stack> </Stack>
@ -61,6 +66,7 @@ const IngredientDetailUnit = () => {
open={openConversionDrawer} open={openConversionDrawer}
handleClose={handleCloseConversionDrawer} handleClose={handleCloseConversionDrawer}
setData={handleSetConversionData} setData={handleSetConversionData}
data={data}
/> />
</> </>
) )

View File

@ -6,11 +6,18 @@ import IngredientDetailInfo from './IngredientDetailInfo'
import IngredientDetailUnit from './IngredientDetailUnit' import IngredientDetailUnit from './IngredientDetailUnit'
import IngredientDetailStockAdjustmentDrawer from './IngredientDetailStockAdjustmentDrawer' // Sesuaikan dengan path file Anda import IngredientDetailStockAdjustmentDrawer from './IngredientDetailStockAdjustmentDrawer' // Sesuaikan dengan path file Anda
import { Button } from '@mui/material' import { Button } from '@mui/material'
import { useParams } from 'next/navigation'
import { useIngredientById } from '@/services/queries/ingredients'
const IngredientDetail = () => { const IngredientDetail = () => {
// State untuk mengontrol stock adjustment drawer // State untuk mengontrol stock adjustment drawer
const [openStockAdjustmentDrawer, setOpenStockAdjustmentDrawer] = useState(false) 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 // Function untuk membuka stock adjustment drawer
const handleOpenStockAdjustmentDrawer = () => { const handleOpenStockAdjustmentDrawer = () => {
setOpenStockAdjustmentDrawer(true) setOpenStockAdjustmentDrawer(true)
@ -32,7 +39,7 @@ const IngredientDetail = () => {
<> <>
<Grid container spacing={6}> <Grid container spacing={6}>
<Grid size={{ xs: 12, lg: 8, md: 7 }}> <Grid size={{ xs: 12, lg: 8, md: 7 }}>
<IngredientDetailInfo /> <IngredientDetailInfo data={data} />
</Grid> </Grid>
<Grid size={{ xs: 12, lg: 4, md: 5 }}> <Grid size={{ xs: 12, lg: 4, md: 5 }}>
<Button <Button
@ -51,7 +58,7 @@ const IngredientDetail = () => {
> >
Penyesuaian Stok Penyesuaian Stok
</Button> </Button>
<IngredientDetailUnit /> <IngredientDetailUnit data={data} />
</Grid> </Grid>
</Grid> </Grid>

View File

@ -1,20 +1,28 @@
'use client'
// MUI Imports // MUI Imports
import Card from '@mui/material/Card' import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent' import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import Chip from '@mui/material/Chip' import Chip from '@mui/material/Chip'
import Divider from '@mui/material/Divider' import Divider from '@mui/material/Divider'
import Button from '@mui/material/Button'
import type { ButtonProps } from '@mui/material/Button'
// Type Imports
import type { ThemeColor } from '@core/types'
// Component Imports // Component Imports
import EditUserInfo from '@components/dialogs/edit-user-info'
import ConfirmationDialog from '@components/dialogs/confirmation-dialog'
import OpenDialogOnElementClick from '@components/dialogs/OpenDialogOnElementClick'
import CustomAvatar from '@core/components/mui/Avatar' import CustomAvatar from '@core/components/mui/Avatar'
import { useParams } from 'next/navigation'
import { useVendorById } from '@/services/queries/vendor'
import Loading from '@/components/layout/shared/Loading'
import { getInitials } from '@/utils/getInitials'
import OpenDialogOnElementClick from '@/components/dialogs/OpenDialogOnElementClick'
import { Box, Button, ButtonProps, CircularProgress } from '@mui/material'
import ConfirmationDialog from '@/components/dialogs/confirmation-dialog'
import EditUserInfo from '@/components/dialogs/edit-user-info'
import { ThemeColor } from '@/@core/types'
import { useState } from 'react'
import AddVendorDrawer from '../../list/AddVendorDrawer'
import ConfirmDeleteDialog from '@/components/dialogs/confirm-delete'
import { useRouter } from 'next/router'
import { useVendorsMutation } from '@/services/mutations/vendor'
// Vars // Vars
const userData = { const userData = {
@ -33,7 +41,25 @@ const userData = {
} }
const VendorDetails = () => { const VendorDetails = () => {
// Vars const [editVendorOpen, setEditVendorOpen] = useState(false)
const [openConfirm, setOpenConfirm] = useState(false)
const params = useParams()
const id = params?.id ?? ''
const { data: vendor, isLoading, error } = useVendorById(id as string)
const { deleteVendor } = useVendorsMutation()
const handleDelete = () => {
deleteVendor.mutate(id as string, {
onSuccess: () => {
setOpenConfirm(false)
window.history.back()
}
})
}
const buttonProps = (children: string, color: ThemeColor, variant: ButtonProps['variant']): ButtonProps => ({ const buttonProps = (children: string, color: ThemeColor, variant: ButtonProps['variant']): ButtonProps => ({
children, children,
color, color,
@ -42,91 +68,132 @@ const VendorDetails = () => {
return ( return (
<> <>
<Card> {isLoading ? (
<CardContent className='flex flex-col pbs-12 gap-6'> <Box
<div className='flex flex-col gap-6'> position='absolute'
<div className='flex items-center justify-center flex-col gap-4'> top={0}
<div className='flex flex-col items-center gap-4'> left={0}
<CustomAvatar alt='user-profile' src='/images/avatars/1.png' variant='rounded' size={120} /> right={0}
<Typography variant='h5'>{`${userData.firstName} ${userData.lastName}`}</Typography> bottom={0}
display='flex'
alignItems='center'
justifyContent='center'
bgcolor='rgba(255,255,255,0.7)'
zIndex={1}
>
<CircularProgress size={24} />
</Box>
) : (
<Card>
<CardContent className='flex flex-col pbs-12 gap-6'>
<div className='flex flex-col gap-6'>
<div className='flex items-center justify-center flex-col gap-4'>
<div className='flex flex-col items-center gap-4'>
{/* <CustomAvatar alt='vendor-profile' variant='rounded' size={120}>
{getInitials(vendor?.name as string)}
</CustomAvatar> */}
<Typography variant='h5'>{vendor?.name}</Typography>
</div>
<Chip label='Vendor' color='primary' size='small' variant='tonal' />
</div> </div>
<Chip label='Vendor' color='primary' size='small' variant='tonal' />
</div> </div>
</div>
{/* Detail Kontak Section */} {/* Detail Kontak Section */}
<div> <div>
<Typography variant='h5'>Detail Kontak</Typography> <Typography variant='h5'>Detail Kontak</Typography>
<Divider className='mlb-4' /> <Divider className='mlb-4' />
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<div className='flex items-center flex-wrap gap-x-1.5'> <div className='flex items-center flex-wrap gap-x-1.5'>
<Typography className='font-medium' color='text.primary'> <Typography className='font-medium' color='text.primary'>
Nama: Contact Person:
</Typography> </Typography>
<Typography>{`${userData.firstName} ${userData.lastName}`}</Typography> <Typography>{vendor?.contact_person}</Typography>
</div> </div>
<div className='flex items-center flex-wrap gap-x-1.5'> <div className='flex items-center flex-wrap gap-x-1.5'>
<Typography className='font-medium' color='text.primary'> <Typography className='font-medium' color='text.primary'>
Perusahaan: Perusahaan:
</Typography> </Typography>
<Typography>{userData.perusahaan}</Typography> <Typography>{vendor?.name}</Typography>
</div> </div>
<div className='flex items-center flex-wrap gap-x-1.5'> <div className='flex items-center flex-wrap gap-x-1.5'>
<Typography className='font-medium' color='text.primary'> <Typography className='font-medium' color='text.primary'>
Email: Email:
</Typography> </Typography>
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}> <Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
{userData.email} {vendor?.email}
</Typography> </Typography>
</div> </div>
<div className='flex items-center flex-wrap gap-x-1.5'> <div className='flex items-center flex-wrap gap-x-1.5'>
<Typography className='font-medium' color='text.primary'> <Typography className='font-medium' color='text.primary'>
Telepon: Telepon:
</Typography> </Typography>
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}> <Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
{userData.telepon} {vendor?.phone_number}
</Typography> </Typography>
</div> </div>
<div className='flex items-center flex-wrap gap-x-1.5'> <div className='flex items-center flex-wrap gap-x-1.5'>
<Typography className='font-medium' color='text.primary'> <Typography className='font-medium' color='text.primary'>
Alamat Penagihan: Alamat Penagihan:
</Typography> </Typography>
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}> <Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
{userData.alamatPenagihan} {vendor?.address ?? '-'}
</Typography> </Typography>
</div>
</div> </div>
</div> </div>
</div>
{/* Pemetaan Akun Section */} {/* Pemetaan Akun Section */}
<div> <div>
<Typography variant='h5'>Pemetaan Akun</Typography> <Typography variant='h5'>Pemetaan Akun</Typography>
<Divider className='mlb-4' /> <Divider className='mlb-4' />
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<div className='flex items-center flex-wrap gap-x-1.5'> <div className='flex items-center flex-wrap gap-x-1.5'>
<Typography className='font-medium' color='text.primary'> <Typography className='font-medium' color='text.primary'>
Akun Hutang: Akun Hutang:
</Typography> </Typography>
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}> <Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
{userData.akunHutang} {userData.akunHutang}
</Typography> </Typography>
</div> </div>
<div className='flex items-center flex-wrap gap-x-1.5'> <div className='flex items-center flex-wrap gap-x-1.5'>
<Typography className='font-medium' color='text.primary'> <Typography className='font-medium' color='text.primary'>
Akun Piutang: Akun Piutang:
</Typography> </Typography>
<Typography color='text.secondary'>{userData.akunPiutang || '-'}</Typography> <Typography color='text.secondary'>{userData.akunPiutang || '-'}</Typography>
</div> </div>
<div className='flex items-center flex-wrap gap-x-1.5'> <div className='flex items-center flex-wrap gap-x-1.5'>
<Typography className='font-medium' color='text.primary'> <Typography className='font-medium' color='text.primary'>
Kena Pajak: Kena Pajak:
</Typography> </Typography>
<Typography color='text.primary'>{userData.kenaPajak}</Typography> <Typography color='text.primary'>{userData.kenaPajak}</Typography>
</div>
</div> </div>
</div> </div>
</div> <div className='flex gap-4 justify-center'>
</CardContent> <Button variant='contained' onClick={() => setEditVendorOpen(!editVendorOpen)} className='max-sm:is-full'>
</Card> Edit
</Button>
<Button
variant='contained'
color='error'
onClick={() => setOpenConfirm(!openConfirm)}
className='max-sm:is-full'
>
Hapus
</Button>
</div>
</CardContent>
</Card>
)}
<AddVendorDrawer open={editVendorOpen} handleClose={() => setEditVendorOpen(!editVendorOpen)} data={vendor} />
<ConfirmDeleteDialog
open={openConfirm}
onClose={() => setOpenConfirm(false)}
onConfirm={handleDelete}
isLoading={deleteVendor.isPending}
title='Delete Vendor'
message='Are you sure you want to delete this Vendor? This action cannot be undone.'
/>
</> </>
) )
} }

View File

@ -1,5 +1,5 @@
// React Imports // React Imports
import { useState } from 'react' import { useState, useEffect } from 'react'
// MUI Imports // MUI Imports
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
@ -18,12 +18,13 @@ import { useForm, Controller } from 'react-hook-form'
// Component Imports // Component Imports
import CustomTextField from '@core/components/mui/TextField' import CustomTextField from '@core/components/mui/TextField'
import { VendorRequest } from '@/types/services/vendor' import { Vendor, VendorRequest } from '@/types/services/vendor'
import { useVendorsMutation } from '@/services/mutations/vendor' import { useVendorsMutation } from '@/services/mutations/vendor'
type Props = { type Props = {
open: boolean open: boolean
handleClose: () => void handleClose: () => void
data?: Vendor // Data vendor untuk edit (jika ada)
} }
type FormValidateType = { type FormValidateType = {
@ -51,9 +52,9 @@ const initialData: FormValidateType = {
is_active: true is_active: true
} }
const AddVendorDrawer = (props: Props) => { const AddEditVendorDrawer = (props: Props) => {
// Props // Props
const { open, handleClose } = props const { open, handleClose, data } = props
// States // States
const [showMore, setShowMore] = useState(false) const [showMore, setShowMore] = useState(false)
@ -61,6 +62,9 @@ const AddVendorDrawer = (props: Props) => {
const { createVendor, updateVendor } = useVendorsMutation() const { createVendor, updateVendor } = useVendorsMutation()
// Determine if this is edit mode
const isEditMode = Boolean(data?.id)
// Hooks // Hooks
const { const {
control, control,
@ -71,29 +75,73 @@ const AddVendorDrawer = (props: Props) => {
defaultValues: initialData defaultValues: initialData
}) })
const handleFormSubmit = async (data: FormValidateType) => { // Effect to populate form when editing
useEffect(() => {
if (isEditMode && data) {
// Populate form with existing data
const formData: FormValidateType = {
name: data.name || '',
email: data.email || '',
phone_number: data.phone_number || '',
address: data.address || '',
contact_person: data.contact_person || '',
tax_number: data.tax_number || '',
payment_terms: data.payment_terms || '',
notes: data.notes || '',
is_active: data.is_active ?? true
}
resetForm(formData)
// Show more fields if any optional field has data
const hasOptionalData = data.address || data.tax_number || data.payment_terms || data.notes
if (hasOptionalData) {
setShowMore(true)
}
} else {
// Reset to initial data for add mode
resetForm(initialData)
setShowMore(false)
}
}, [data, isEditMode, resetForm])
const handleFormSubmit = async (formData: FormValidateType) => {
try { try {
setIsSubmitting(true) setIsSubmitting(true)
// Create VendorRequest object // Create VendorRequest object
const vendorRequest: VendorRequest = { const vendorRequest: VendorRequest = {
name: data.name, name: formData.name,
email: data.email || undefined, email: formData.email || undefined,
phone_number: data.phone_number || undefined, phone_number: formData.phone_number || undefined,
address: data.address || undefined, address: formData.address || undefined,
contact_person: data.contact_person || undefined, contact_person: formData.contact_person || undefined,
tax_number: data.tax_number || undefined, tax_number: formData.tax_number || undefined,
payment_terms: data.payment_terms || undefined, payment_terms: formData.payment_terms || undefined,
notes: data.notes || undefined, notes: formData.notes || undefined,
is_active: data.is_active is_active: formData.is_active
} }
createVendor.mutate(vendorRequest, { if (isEditMode && data?.id) {
onSuccess: () => { // Update existing vendor
handleReset() updateVendor.mutate(
handleClose() { id: data.id, payload: vendorRequest },
} {
}) onSuccess: () => {
handleReset()
handleClose()
}
}
)
} else {
// Create new vendor
createVendor.mutate(vendorRequest, {
onSuccess: () => {
handleReset()
handleClose()
}
})
}
} catch (error) { } catch (error) {
console.error('Error submitting vendor:', error) console.error('Error submitting vendor:', error)
// Handle error (show toast, etc.) // Handle error (show toast, etc.)
@ -136,7 +184,7 @@ const AddVendorDrawer = (props: Props) => {
}} }}
> >
<div className='flex items-center justify-between plb-5 pli-6'> <div className='flex items-center justify-between plb-5 pli-6'>
<Typography variant='h5'>Tambah Vendor Baru</Typography> <Typography variant='h5'>{isEditMode ? 'Edit Vendor' : 'Tambah Vendor Baru'}</Typography>
<IconButton size='small' onClick={handleReset}> <IconButton size='small' onClick={handleReset}>
<i className='tabler-x text-2xl text-textPrimary' /> <i className='tabler-x text-2xl text-textPrimary' />
</IconButton> </IconButton>
@ -359,7 +407,7 @@ const AddVendorDrawer = (props: Props) => {
> >
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
<Button variant='contained' type='submit' form='vendor-form' disabled={isSubmitting}> <Button variant='contained' type='submit' form='vendor-form' disabled={isSubmitting}>
{isSubmitting ? 'Menyimpan...' : 'Simpan'} {isSubmitting ? (isEditMode ? 'Mengupdate...' : 'Menyimpan...') : isEditMode ? 'Update' : 'Simpan'}
</Button> </Button>
<Button variant='outlined' color='error' onClick={handleReset} disabled={isSubmitting}> <Button variant='outlined' color='error' onClick={handleReset} disabled={isSubmitting}>
Batal Batal
@ -370,4 +418,4 @@ const AddVendorDrawer = (props: Props) => {
) )
} }
export default AddVendorDrawer export default AddEditVendorDrawer

View File

@ -183,12 +183,12 @@ const VendorListTable = () => {
cell: ({ row }) => ( cell: ({ row }) => (
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
<div className='flex flex-col'> <div className='flex flex-col'>
<Link href={getLocalizedUrl(`/apps/vendor/detail`, locale as Locale)}> <Link href={getLocalizedUrl(`/apps/vendor/${row.original.id}/detail`, locale as Locale)}>
<Typography className='font-medium cursor-pointer hover:underline text-primary'> <Typography className='font-medium cursor-pointer hover:underline text-primary'>
{row.original.contact_person} {row.original.contact_person}
</Typography> </Typography>
<Typography variant='body2'>{row.original.email}</Typography>
</Link> </Link>
<Typography variant='body2'>{row.original.email}</Typography>
</div> </div>
</div> </div>
) )