Compare commits
No commits in common. "54c7598e7a60015d972174bbd865525a1baa8d7b" and "c91be1812b1239fc203f47bc787c763c35497e28" have entirely different histories.
54c7598e7a
...
c91be1812b
@ -1,52 +0,0 @@
|
|||||||
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,7 +1,6 @@
|
|||||||
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
|
||||||
@ -35,13 +34,3 @@ 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,6 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
import { Vendor, Vendors } from '@/types/services/vendor'
|
import { Vendors } from '@/types/services/vendor'
|
||||||
|
|
||||||
interface VendorQueryParams {
|
interface VendorQueryParams {
|
||||||
page?: number
|
page?: number
|
||||||
@ -34,13 +34,3 @@ 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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,75 +1,56 @@
|
|||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
// React Imports
|
// React Imports
|
||||||
import { useState, useEffect } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
// MUI Imports
|
// MUI Imports
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
@ -17,121 +17,101 @@ 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: IngredientUnitConverterRequest) => void
|
setData?: (data: any) => void
|
||||||
data?: Ingredient // Contains ingredientId, unit info, and cost
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnitConversionType = {
|
type UnitConversionType = {
|
||||||
satuan: string // This will be from_unit_id
|
satuan: string
|
||||||
quantity: number
|
quantity: number
|
||||||
unit: string // This will be to_unit_id (from data)
|
unit: string
|
||||||
hargaBeli: number // Calculated as factor * ingredientCost
|
hargaBeli: number
|
||||||
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, data } = props
|
const { open, handleClose, setData } = 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 [conversion, setConversion] = useState<UnitConversionType>(getInitialConversion())
|
const [conversions, setConversions] = useState<UnitConversionType[]>([initialConversion])
|
||||||
const { createUnitConventer } = useUnitConventorMutation()
|
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
reset: resetForm,
|
reset: resetForm,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
|
||||||
formState: { errors }
|
formState: { errors }
|
||||||
} = useForm<UnitConversionType>({
|
} = useForm<FormValidateType>({
|
||||||
defaultValues: getInitialConversion()
|
defaultValues: {
|
||||||
})
|
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 handleChangeConversion = (field: keyof UnitConversionType, value: any) => {
|
const handleTambahBaris = () => {
|
||||||
const newConversion = { ...conversion, [field]: value }
|
const newConversion: UnitConversionType = {
|
||||||
setConversion(newConversion)
|
satuan: '',
|
||||||
setValue(field, value)
|
quantity: 0,
|
||||||
|
unit: '',
|
||||||
|
hargaBeli: 0,
|
||||||
|
hargaJual: 0,
|
||||||
|
isDefault: false
|
||||||
|
}
|
||||||
|
setConversions([...conversions, newConversion])
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = (data: UnitConversionType) => {
|
const handleHapusBaris = (index: number) => {
|
||||||
// Transform form data to IngredientUnitConverterRequest
|
if (conversions.length > 1) {
|
||||||
const converterRequest: IngredientUnitConverterRequest = {
|
const newConversions = conversions.filter((_, i) => i !== index)
|
||||||
ingredient_id: ingredientId,
|
setConversions(newConversions)
|
||||||
from_unit_id: conversion.satuan,
|
}
|
||||||
to_unit_id: toUnitId, // Use toUnitId from data prop
|
|
||||||
conversion_factor: conversion.quantity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Unit conversion request:', converterRequest)
|
const handleChangeConversion = (index: number, field: keyof UnitConversionType, value: any) => {
|
||||||
|
const newConversions = [...conversions]
|
||||||
|
newConversions[index] = { ...newConversions[index], [field]: value }
|
||||||
|
setConversions(newConversions)
|
||||||
|
}
|
||||||
|
|
||||||
// if (setData) {
|
const handleToggleDefault = (index: number) => {
|
||||||
// setData(converterRequest)
|
const newConversions = conversions.map((conversion, i) => ({
|
||||||
// }
|
...conversion,
|
||||||
createUnitConventer.mutate(converterRequest, {
|
isDefault: i === index
|
||||||
onSuccess: () => {
|
}))
|
||||||
|
setConversions(newConversions)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = (data: FormValidateType) => {
|
||||||
|
console.log('Unit conversions:', conversions)
|
||||||
|
if (setData) {
|
||||||
|
setData(conversions)
|
||||||
|
}
|
||||||
handleClose()
|
handleClose()
|
||||||
resetForm(getInitialConversion())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
handleClose()
|
handleClose()
|
||||||
const resetData = getInitialConversion()
|
setConversions([initialConversion])
|
||||||
setConversion(resetData)
|
resetForm({ conversions: [initialConversion] })
|
||||||
resetForm(resetData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatNumber = (value: number) => {
|
const formatNumber = (value: number) => {
|
||||||
@ -142,12 +122,6 @@ 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}
|
||||||
@ -181,37 +155,17 @@ 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(onSubmit)}>
|
<form id='unit-conversion-form' onSubmit={handleSubmit(data => onSubmit(data))}>
|
||||||
<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'>
|
||||||
From Unit
|
Satuan
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={1} className='text-center'>
|
<Grid size={1} className='text-center'>
|
||||||
@ -221,20 +175,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'>
|
||||||
Factor
|
Jumlah
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={1.5}>
|
<Grid size={1.5}>
|
||||||
<Typography variant='body2' fontWeight='medium'>
|
<Typography variant='body2' fontWeight='medium'>
|
||||||
To Unit
|
Unit
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={2.5}>
|
<Grid size={2}>
|
||||||
<Typography variant='body2' fontWeight='medium'>
|
<Typography variant='body2' fontWeight='medium'>
|
||||||
Harga Beli
|
Harga Beli
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={2.5}>
|
<Grid size={2}>
|
||||||
<Typography variant='body2' fontWeight='medium'>
|
<Typography variant='body2' fontWeight='medium'>
|
||||||
Harga Jual
|
Harga Jual
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -244,48 +198,37 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
|||||||
Default
|
Default
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid size={1}>
|
||||||
|
<Typography variant='body2' fontWeight='medium'>
|
||||||
|
Action
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Form Input Row */}
|
{/* Baris Konversi */}
|
||||||
<Grid container spacing={2} alignItems='center' className='py-2'>
|
{conversions.map((conversion, index) => (
|
||||||
{/* From Unit (Satuan) */}
|
<Grid container spacing={2} alignItems='center' key={index} className='py-2'>
|
||||||
<Grid size={2}>
|
<Grid size={0.5}>
|
||||||
<div className='flex items-center gap-2'>
|
<Typography variant='body2' color='text.secondary'>
|
||||||
<Typography variant='body2' fontWeight='medium'>
|
{index + 1}
|
||||||
1
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Controller
|
</Grid>
|
||||||
name='satuan'
|
|
||||||
control={control}
|
{/* Satuan */}
|
||||||
rules={{ required: 'From unit wajib dipilih' }}
|
<Grid size={1.5}>
|
||||||
render={({ field }) => (
|
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...field}
|
|
||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
size='small'
|
size='small'
|
||||||
error={!!errors.satuan}
|
value={conversion.satuan}
|
||||||
onChange={e => {
|
onChange={e => handleChangeConversion(index, 'satuan', e.target.value)}
|
||||||
field.onChange(e.target.value)
|
|
||||||
handleChangeConversion('satuan', e.target.value)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{units?.data
|
<MenuItem value='Box'>Box</MenuItem>
|
||||||
.filter(unit => unit.id !== toUnitId) // Prevent selecting same unit as target
|
<MenuItem value='Kg'>Kg</MenuItem>
|
||||||
.map(unit => (
|
<MenuItem value='Liter'>Liter</MenuItem>
|
||||||
<MenuItem key={unit.id} value={unit.id}>
|
<MenuItem value='Pack'>Pack</MenuItem>
|
||||||
{unit.name}
|
<MenuItem value='Pcs'>Pcs</MenuItem>
|
||||||
</MenuItem>
|
|
||||||
)) ?? []}
|
|
||||||
</CustomTextField>
|
</CustomTextField>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.satuan && (
|
|
||||||
<Typography variant='caption' color='error' className='mt-1'>
|
|
||||||
{errors.satuan.message}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Tanda sama dengan */}
|
{/* Tanda sama dengan */}
|
||||||
@ -293,106 +236,59 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
|||||||
<Typography variant='h6'>=</Typography>
|
<Typography variant='h6'>=</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Conversion Factor (Quantity) */}
|
{/* Quantity */}
|
||||||
<Grid size={1.5}>
|
<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
|
<CustomTextField
|
||||||
{...field}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
size='small'
|
size='small'
|
||||||
type='number'
|
type='number'
|
||||||
error={!!errors.quantity}
|
value={conversion.quantity}
|
||||||
onChange={e => {
|
onChange={e => handleChangeConversion(index, 'quantity', parseInt(e.target.value) || 0)}
|
||||||
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>
|
</Grid>
|
||||||
|
|
||||||
{/* To Unit - Disabled because it comes from data */}
|
{/* Unit */}
|
||||||
<Grid size={1.5}>
|
<Grid size={1.5}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
size='small'
|
size='small'
|
||||||
value={toUnitId}
|
value={conversion.unit}
|
||||||
disabled
|
onChange={e => handleChangeConversion(index, 'unit', e.target.value)}
|
||||||
InputProps={{
|
|
||||||
sx: { backgroundColor: 'grey.100' }
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{units?.data.map(unit => (
|
<MenuItem value='Pcs'>Pcs</MenuItem>
|
||||||
<MenuItem key={unit.id} value={unit.id}>
|
<MenuItem value='Kg'>Kg</MenuItem>
|
||||||
{unit.name}
|
<MenuItem value='Gram'>Gram</MenuItem>
|
||||||
</MenuItem>
|
<MenuItem value='Liter'>Liter</MenuItem>
|
||||||
)) ?? []}
|
<MenuItem value='ML'>ML</MenuItem>
|
||||||
</CustomTextField>
|
</CustomTextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Harga Beli - Calculated as factor * ingredientCost */}
|
{/* Harga Beli */}
|
||||||
<Grid size={2.5}>
|
<Grid size={2}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
size='small'
|
size='small'
|
||||||
value={formatNumber(totalPurchasePrice)}
|
value={formatNumber(conversion.hargaBeli)}
|
||||||
disabled
|
onChange={e => handleChangeConversion(index, 'hargaBeli', parseNumber(e.target.value))}
|
||||||
InputProps={{
|
|
||||||
sx: { backgroundColor: 'grey.100' }
|
|
||||||
}}
|
|
||||||
placeholder='Calculated purchase price'
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Harga Jual */}
|
{/* Harga Jual */}
|
||||||
<Grid size={2.5}>
|
<Grid size={2}>
|
||||||
<Controller
|
|
||||||
name='hargaJual'
|
|
||||||
control={control}
|
|
||||||
rules={{
|
|
||||||
min: { value: 0, message: 'Tidak boleh negatif' }
|
|
||||||
}}
|
|
||||||
render={({ field }) => (
|
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
size='small'
|
size='small'
|
||||||
error={!!errors.hargaJual}
|
|
||||||
value={formatNumber(conversion.hargaJual)}
|
value={formatNumber(conversion.hargaJual)}
|
||||||
onChange={e => {
|
onChange={e => handleChangeConversion(index, 'hargaJual', parseNumber(e.target.value))}
|
||||||
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>
|
</Grid>
|
||||||
|
|
||||||
{/* Default Star */}
|
{/* Default Star */}
|
||||||
<Grid size={1} className='text-center'>
|
<Grid size={1} className='text-center'>
|
||||||
<IconButton
|
<IconButton
|
||||||
size='small'
|
size='small'
|
||||||
onClick={() => handleChangeConversion('isDefault', !conversion.isDefault)}
|
onClick={() => handleToggleDefault(index)}
|
||||||
sx={{
|
sx={{
|
||||||
color: conversion.isDefault ? 'warning.main' : 'grey.400'
|
color: conversion.isDefault ? 'warning.main' : 'grey.400'
|
||||||
}}
|
}}
|
||||||
@ -400,67 +296,48 @@ const IngedientUnitConversionDrawer = (props: Props) => {
|
|||||||
<i className={conversion.isDefault ? 'tabler-star-filled' : 'tabler-star'} />
|
<i className={conversion.isDefault ? 'tabler-star-filled' : 'tabler-star'} />
|
||||||
</IconButton>
|
</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>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* Conversion Preview */}
|
{/* Tambah Baris Button */}
|
||||||
{conversion.quantity > 0 && conversion.satuan && toUnitId && (
|
<div className='flex items-center justify-start'>
|
||||||
<Box className='bg-green-50 p-4 rounded-lg border-l-4 border-green-500'>
|
<Button
|
||||||
<Typography variant='body2' fontWeight='medium' className='mb-2'>
|
variant='outlined'
|
||||||
Conversion Preview:
|
startIcon={<i className='tabler-plus' />}
|
||||||
</Typography>
|
onClick={handleTambahBaris}
|
||||||
<Typography variant='body2' className='mb-1'>
|
sx={{
|
||||||
<strong>1 {units?.data.find(u => u.id === conversion.satuan)?.name || 'Unit'}</strong> ={' '}
|
color: 'primary.main',
|
||||||
<strong>
|
borderColor: 'primary.main',
|
||||||
{conversion.quantity} {units?.data.find(u => u.id === toUnitId)?.name || 'Unit'}
|
'&:hover': {
|
||||||
</strong>
|
backgroundColor: 'primary.light',
|
||||||
</Typography>
|
borderColor: 'primary.main'
|
||||||
<Typography variant='caption' color='text.secondary'>
|
}
|
||||||
Conversion Factor: {conversion.quantity}
|
}}
|
||||||
</Typography>
|
>
|
||||||
</Box>
|
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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Box>
|
</Box>
|
||||||
@ -478,21 +355,13 @@ 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' disabled={!isValidForSubmit}>
|
<Button variant='contained' type='submit' form='unit-conversion-form'>
|
||||||
Simpan Konversi
|
Simpan
|
||||||
</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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,19 +1,14 @@
|
|||||||
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'
|
||||||
|
|
||||||
interface Props {
|
const IngredientDetailInfo = () => {
|
||||||
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'>
|
||||||
{data?.name ?? '-'}
|
Tepung Terigu
|
||||||
</Typography>
|
</Typography>
|
||||||
<Chip label={'Active'} color={'success'} size='small' />
|
<Chip label={'Active'} color={'success'} size='small' />
|
||||||
</div>
|
</div>
|
||||||
@ -22,7 +17,7 @@ const IngredientDetailInfo = ({ data }: Props) => {
|
|||||||
<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(data?.cost ?? 0)}
|
<span className='font-semibold'>Cost:</span> {formatCurrency(5000)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,13 +2,8 @@
|
|||||||
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'
|
|
||||||
|
|
||||||
interface Props {
|
const IngredientDetailUnit = () => {
|
||||||
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)
|
||||||
|
|
||||||
@ -39,7 +34,7 @@ const IngredientDetailUnit = ({ data }: Props) => {
|
|||||||
Satuan Dasar
|
Satuan Dasar
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
||||||
: {data?.unit.name ?? '-'}
|
: Pcs
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
@ -66,7 +61,6 @@ const IngredientDetailUnit = ({ data }: Props) => {
|
|||||||
open={openConversionDrawer}
|
open={openConversionDrawer}
|
||||||
handleClose={handleCloseConversionDrawer}
|
handleClose={handleCloseConversionDrawer}
|
||||||
setData={handleSetConversionData}
|
setData={handleSetConversionData}
|
||||||
data={data}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -6,18 +6,11 @@ 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)
|
||||||
@ -39,7 +32,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 data={data} />
|
<IngredientDetailInfo />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, lg: 4, md: 5 }}>
|
<Grid size={{ xs: 12, lg: 4, md: 5 }}>
|
||||||
<Button
|
<Button
|
||||||
@ -58,7 +51,7 @@ const IngredientDetail = () => {
|
|||||||
>
|
>
|
||||||
Penyesuaian Stok
|
Penyesuaian Stok
|
||||||
</Button>
|
</Button>
|
||||||
<IngredientDetailUnit data={data} />
|
<IngredientDetailUnit />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +1,20 @@
|
|||||||
'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 = {
|
||||||
@ -41,25 +33,7 @@ const userData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VendorDetails = () => {
|
const VendorDetails = () => {
|
||||||
const [editVendorOpen, setEditVendorOpen] = useState(false)
|
// Vars
|
||||||
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,
|
||||||
@ -68,31 +42,13 @@ const VendorDetails = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading ? (
|
|
||||||
<Box
|
|
||||||
position='absolute'
|
|
||||||
top={0}
|
|
||||||
left={0}
|
|
||||||
right={0}
|
|
||||||
bottom={0}
|
|
||||||
display='flex'
|
|
||||||
alignItems='center'
|
|
||||||
justifyContent='center'
|
|
||||||
bgcolor='rgba(255,255,255,0.7)'
|
|
||||||
zIndex={1}
|
|
||||||
>
|
|
||||||
<CircularProgress size={24} />
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className='flex flex-col pbs-12 gap-6'>
|
<CardContent className='flex flex-col pbs-12 gap-6'>
|
||||||
<div className='flex flex-col gap-6'>
|
<div className='flex flex-col gap-6'>
|
||||||
<div className='flex items-center justify-center flex-col gap-4'>
|
<div className='flex items-center justify-center flex-col gap-4'>
|
||||||
<div className='flex flex-col items-center gap-4'>
|
<div className='flex flex-col items-center gap-4'>
|
||||||
{/* <CustomAvatar alt='vendor-profile' variant='rounded' size={120}>
|
<CustomAvatar alt='user-profile' src='/images/avatars/1.png' variant='rounded' size={120} />
|
||||||
{getInitials(vendor?.name as string)}
|
<Typography variant='h5'>{`${userData.firstName} ${userData.lastName}`}</Typography>
|
||||||
</CustomAvatar> */}
|
|
||||||
<Typography variant='h5'>{vendor?.name}</Typography>
|
|
||||||
</div>
|
</div>
|
||||||
<Chip label='Vendor' color='primary' size='small' variant='tonal' />
|
<Chip label='Vendor' color='primary' size='small' variant='tonal' />
|
||||||
</div>
|
</div>
|
||||||
@ -105,22 +61,22 @@ const VendorDetails = () => {
|
|||||||
<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'>
|
||||||
Contact Person:
|
Nama:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography>{vendor?.contact_person}</Typography>
|
<Typography>{`${userData.firstName} ${userData.lastName}`}</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>{vendor?.name}</Typography>
|
<Typography>{userData.perusahaan}</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' }}>
|
||||||
{vendor?.email}
|
{userData.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'>
|
||||||
@ -128,7 +84,7 @@ const VendorDetails = () => {
|
|||||||
Telepon:
|
Telepon:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
|
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
|
||||||
{vendor?.phone_number}
|
{userData.telepon}
|
||||||
</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'>
|
||||||
@ -136,7 +92,7 @@ const VendorDetails = () => {
|
|||||||
Alamat Penagihan:
|
Alamat Penagihan:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
|
<Typography color='primary' sx={{ textDecoration: 'none', cursor: 'pointer' }}>
|
||||||
{vendor?.address ?? '-'}
|
{userData.alamatPenagihan}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -169,31 +125,8 @@ const VendorDetails = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex gap-4 justify-center'>
|
|
||||||
<Button variant='contained' onClick={() => setEditVendorOpen(!editVendorOpen)} className='max-sm:is-full'>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='contained'
|
|
||||||
color='error'
|
|
||||||
onClick={() => setOpenConfirm(!openConfirm)}
|
|
||||||
className='max-sm:is-full'
|
|
||||||
>
|
|
||||||
Hapus
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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.'
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
82
src/views/apps/vendor/list/AddVendorDrawer.tsx
vendored
82
src/views/apps/vendor/list/AddVendorDrawer.tsx
vendored
@ -1,5 +1,5 @@
|
|||||||
// React Imports
|
// React Imports
|
||||||
import { useState, useEffect } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
// MUI Imports
|
// MUI Imports
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
@ -18,13 +18,12 @@ 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 { Vendor, VendorRequest } from '@/types/services/vendor'
|
import { 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 = {
|
||||||
@ -52,9 +51,9 @@ const initialData: FormValidateType = {
|
|||||||
is_active: true
|
is_active: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddEditVendorDrawer = (props: Props) => {
|
const AddVendorDrawer = (props: Props) => {
|
||||||
// Props
|
// Props
|
||||||
const { open, handleClose, data } = props
|
const { open, handleClose } = props
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const [showMore, setShowMore] = useState(false)
|
const [showMore, setShowMore] = useState(false)
|
||||||
@ -62,9 +61,6 @@ const AddEditVendorDrawer = (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,
|
||||||
@ -75,73 +71,29 @@ const AddEditVendorDrawer = (props: Props) => {
|
|||||||
defaultValues: initialData
|
defaultValues: initialData
|
||||||
})
|
})
|
||||||
|
|
||||||
// Effect to populate form when editing
|
const handleFormSubmit = async (data: FormValidateType) => {
|
||||||
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: formData.name,
|
name: data.name,
|
||||||
email: formData.email || undefined,
|
email: data.email || undefined,
|
||||||
phone_number: formData.phone_number || undefined,
|
phone_number: data.phone_number || undefined,
|
||||||
address: formData.address || undefined,
|
address: data.address || undefined,
|
||||||
contact_person: formData.contact_person || undefined,
|
contact_person: data.contact_person || undefined,
|
||||||
tax_number: formData.tax_number || undefined,
|
tax_number: data.tax_number || undefined,
|
||||||
payment_terms: formData.payment_terms || undefined,
|
payment_terms: data.payment_terms || undefined,
|
||||||
notes: formData.notes || undefined,
|
notes: data.notes || undefined,
|
||||||
is_active: formData.is_active
|
is_active: data.is_active
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEditMode && data?.id) {
|
|
||||||
// Update existing vendor
|
|
||||||
updateVendor.mutate(
|
|
||||||
{ id: data.id, payload: vendorRequest },
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
handleReset()
|
|
||||||
handleClose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Create new vendor
|
|
||||||
createVendor.mutate(vendorRequest, {
|
createVendor.mutate(vendorRequest, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
handleReset()
|
handleReset()
|
||||||
handleClose()
|
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.)
|
||||||
@ -184,7 +136,7 @@ const AddEditVendorDrawer = (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'>{isEditMode ? 'Edit Vendor' : 'Tambah Vendor Baru'}</Typography>
|
<Typography variant='h5'>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>
|
||||||
@ -407,7 +359,7 @@ const AddEditVendorDrawer = (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 ? (isEditMode ? 'Mengupdate...' : 'Menyimpan...') : isEditMode ? 'Update' : 'Simpan'}
|
{isSubmitting ? 'Menyimpan...' : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='outlined' color='error' onClick={handleReset} disabled={isSubmitting}>
|
<Button variant='outlined' color='error' onClick={handleReset} disabled={isSubmitting}>
|
||||||
Batal
|
Batal
|
||||||
@ -418,4 +370,4 @@ const AddEditVendorDrawer = (props: Props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddEditVendorDrawer
|
export default AddVendorDrawer
|
||||||
|
|||||||
@ -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/${row.original.id}/detail`, locale as Locale)}>
|
<Link href={getLocalizedUrl(`/apps/vendor/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>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user