Compare commits
No commits in common. "fab31f154063f7c02e64b6476b7cceb761638d91" and "98d6446b0ce5276fdfa215c1d761a355a24f45f3" have entirely different histories.
fab31f1540
...
98d6446b0c
@ -1,18 +0,0 @@
|
|||||||
import PurchaseDetailContent from '@/views/apps/purchase/purchase-detail/PurchaseDetailContent'
|
|
||||||
import PurchaseDetailHeader from '@/views/apps/purchase/purchase-detail/PurchaseDetailHeader'
|
|
||||||
import Grid from '@mui/material/Grid2'
|
|
||||||
|
|
||||||
const PurchaseOrderDetailPage = () => {
|
|
||||||
return (
|
|
||||||
<Grid container spacing={6}>
|
|
||||||
<Grid size={{ xs: 12 }}>
|
|
||||||
<PurchaseDetailHeader title='Detail Pesanan Pembelian' />
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 12 }}>
|
|
||||||
<PurchaseDetailContent />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PurchaseOrderDetailPage
|
|
||||||
@ -91,27 +91,27 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
|||||||
<MenuItem href={`/${locale}/dashboards/daily-report`}>{dictionary['navigation'].dailyReport}</MenuItem>
|
<MenuItem href={`/${locale}/dashboards/daily-report`}>{dictionary['navigation'].dailyReport}</MenuItem>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
<MenuSection label={dictionary['navigation'].appsPages}>
|
<MenuSection label={dictionary['navigation'].appsPages}>
|
||||||
{/* <SubMenu label={dictionary['navigation'].sales} icon={<i className='tabler-receipt-2' />}>
|
<SubMenu label={dictionary['navigation'].sales} icon={<i className='tabler-receipt-2' />}>
|
||||||
<MenuItem href={`/${locale}/apps/sales/overview`}>{dictionary['navigation'].overview}</MenuItem>
|
<MenuItem href={`/${locale}/apps/sales/overview`}>{dictionary['navigation'].overview}</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/sales/sales-bills`}>{dictionary['navigation'].invoices}</MenuItem>
|
<MenuItem href={`/${locale}/apps/sales/sales-bills`}>{dictionary['navigation'].invoices}</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/sales/sales-deliveries`}>{dictionary['navigation'].deliveries}</MenuItem>
|
<MenuItem href={`/${locale}/apps/sales/sales-deliveries`}>{dictionary['navigation'].deliveries}</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/sales/sales-orders`}>{dictionary['navigation'].sales_orders}</MenuItem>
|
<MenuItem href={`/${locale}/apps/sales/sales-orders`}>{dictionary['navigation'].sales_orders}</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/sales/sales-quotes`}>{dictionary['navigation'].quotes}</MenuItem>
|
<MenuItem href={`/${locale}/apps/sales/sales-quotes`}>{dictionary['navigation'].quotes}</MenuItem>
|
||||||
</SubMenu> */}
|
</SubMenu>
|
||||||
<SubMenu label={dictionary['navigation'].purchase_text} icon={<i className='tabler-shopping-cart' />}>
|
<SubMenu label={dictionary['navigation'].purchase_text} icon={<i className='tabler-shopping-cart' />}>
|
||||||
<MenuItem href={`/${locale}/apps/purchase/overview`}>{dictionary['navigation'].overview}</MenuItem>
|
<MenuItem href={`/${locale}/apps/purchase/overview`}>{dictionary['navigation'].overview}</MenuItem>
|
||||||
{/* <MenuItem href={`/${locale}/apps/purchase/purchase-bills`}>
|
<MenuItem href={`/${locale}/apps/purchase/purchase-bills`}>
|
||||||
{dictionary['navigation'].purchase_bills}
|
{dictionary['navigation'].purchase_bills}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/purchase/purchase-deliveries`}>
|
<MenuItem href={`/${locale}/apps/purchase/purchase-deliveries`}>
|
||||||
{dictionary['navigation'].purchase_delivery}
|
{dictionary['navigation'].purchase_delivery}
|
||||||
</MenuItem> */}
|
</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/purchase/purchase-orders`}>
|
<MenuItem href={`/${locale}/apps/purchase/purchase-orders`}>
|
||||||
{dictionary['navigation'].purchase_orders}
|
{dictionary['navigation'].purchase_orders}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{/* <MenuItem href={`/${locale}/apps/purchase/purchase-quotes`}>
|
<MenuItem href={`/${locale}/apps/purchase/purchase-quotes`}>
|
||||||
{dictionary['navigation'].purchase_quotes}
|
{dictionary['navigation'].purchase_quotes}
|
||||||
</MenuItem> */}
|
</MenuItem>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href={`/${locale}/apps/expense`}
|
href={`/${locale}/apps/expense`}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { PurchaseOrder, PurchaseOrders } from '@/types/services/purchaseOrder'
|
import { PurchaseOrders } from '@/types/services/purchaseOrder'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
|
|
||||||
@ -39,13 +39,3 @@ export function usePurchaseOrders(params: PurchaseOrderQueryParams = {}) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePurchaseOrderById(id: string) {
|
|
||||||
return useQuery<PurchaseOrder>({
|
|
||||||
queryKey: ['purchase-orders', id],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await api.get(`/purchase-orders/${id}`)
|
|
||||||
return res.data.data
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@ -10,6 +10,46 @@ export type PurchaseOrderType = {
|
|||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IngredientItem {
|
||||||
|
id: number
|
||||||
|
ingredient: { label: string; value: string } | null
|
||||||
|
deskripsi: string
|
||||||
|
kuantitas: number
|
||||||
|
satuan: { label: string; value: string } | null
|
||||||
|
harga: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderFormData {
|
||||||
|
vendor: { label: string; value: string } | null
|
||||||
|
nomor: string
|
||||||
|
tglTransaksi: string
|
||||||
|
tglJatuhTempo: string
|
||||||
|
referensi: string
|
||||||
|
termin: { label: string; value: string } | null
|
||||||
|
hargaTermasukPajak: boolean
|
||||||
|
showShippingInfo: boolean
|
||||||
|
tanggalPengiriman: string
|
||||||
|
ekspedisi: { label: string; value: string } | null
|
||||||
|
noResi: string
|
||||||
|
showPesan: boolean
|
||||||
|
showAttachment: boolean
|
||||||
|
showTambahDiskon: boolean
|
||||||
|
showBiayaPengiriman: boolean
|
||||||
|
showBiayaTransaksi: boolean
|
||||||
|
showUangMuka: boolean
|
||||||
|
pesan: string
|
||||||
|
ingredientItems: IngredientItem[]
|
||||||
|
transactionCosts?: TransactionCost[]
|
||||||
|
subtotal?: number
|
||||||
|
discountType?: 'percentage' | 'fixed'
|
||||||
|
downPaymentType?: 'percentage' | 'fixed'
|
||||||
|
discountValue?: string
|
||||||
|
shippingCost?: string
|
||||||
|
transactionCost?: string
|
||||||
|
downPayment?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface TransactionCost {
|
export interface TransactionCost {
|
||||||
id: string
|
id: string
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@ -42,7 +42,6 @@ export interface ProductRecipe {
|
|||||||
variant_id: string | null
|
variant_id: string | null
|
||||||
ingredient_id: string
|
ingredient_id: string
|
||||||
quantity: number
|
quantity: number
|
||||||
waste: number
|
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
product: Product
|
product: Product
|
||||||
@ -55,7 +54,6 @@ export interface ProductRecipeRequest {
|
|||||||
ingredient_id: string
|
ingredient_id: string
|
||||||
quantity: number
|
quantity: number
|
||||||
outlet_id: string | null
|
outlet_id: string | null
|
||||||
waste: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IngredientUnit {
|
export interface IngredientUnit {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { IngredientItem } from './ingredient'
|
|
||||||
import { Vendor } from './vendor'
|
import { Vendor } from './vendor'
|
||||||
|
|
||||||
export interface PurchaseOrderRequest {
|
export interface PurchaseOrderRequest {
|
||||||
@ -94,27 +93,3 @@ export interface PurchaseOrderFile {
|
|||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PurchaseOrderFormData {
|
|
||||||
vendor: { label: string; value: string } | null
|
|
||||||
po_number: string
|
|
||||||
transaction_date: string
|
|
||||||
due_date: string
|
|
||||||
reference: string
|
|
||||||
status: 'draft' | 'sent' | 'approved' | 'received' | 'cancelled'
|
|
||||||
showPesan: boolean
|
|
||||||
showAttachment: boolean
|
|
||||||
message: string
|
|
||||||
items: PurchaseOrderFormItem[]
|
|
||||||
attachment_file_ids: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PurchaseOrderFormItem {
|
|
||||||
id: number // for UI tracking
|
|
||||||
ingredient: { label: string; value: string; originalData?: IngredientItem } | null
|
|
||||||
description: string
|
|
||||||
quantity: number
|
|
||||||
unit: { label: string; value: string } | null
|
|
||||||
amount: number
|
|
||||||
total: number // calculated field for UI
|
|
||||||
}
|
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import Typography from '@mui/material/Typography'
|
|||||||
// Third-party Imports
|
// Third-party Imports
|
||||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||||
|
|
||||||
|
// Type Imports
|
||||||
|
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import CustomTextField from '@core/components/mui/TextField'
|
import CustomTextField from '@core/components/mui/TextField'
|
||||||
import { Autocomplete } from '@mui/material'
|
import { Autocomplete } from '@mui/material'
|
||||||
@ -23,7 +25,6 @@ import { useOutlets } from '../../../../../services/queries/outlets'
|
|||||||
import { Product } from '../../../../../types/services/product'
|
import { Product } from '../../../../../types/services/product'
|
||||||
import { ProductRecipeRequest } from '../../../../../types/services/productRecipe'
|
import { ProductRecipeRequest } from '../../../../../types/services/productRecipe'
|
||||||
import { resetProductVariant } from '../../../../../redux-store/slices/productRecipe'
|
import { resetProductVariant } from '../../../../../redux-store/slices/productRecipe'
|
||||||
import { IngredientItem } from '@/types/services/ingredient'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
@ -37,8 +38,7 @@ const initialData = {
|
|||||||
product_id: '',
|
product_id: '',
|
||||||
variant_id: '',
|
variant_id: '',
|
||||||
ingredient_id: '',
|
ingredient_id: '',
|
||||||
quantity: 0,
|
quantity: 0
|
||||||
waste: 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddRecipeDrawer = (props: Props) => {
|
const AddRecipeDrawer = (props: Props) => {
|
||||||
@ -55,45 +55,23 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
const [ingredientDebouncedInput] = useDebounce(ingredientInput, 500)
|
const [ingredientDebouncedInput] = useDebounce(ingredientInput, 500)
|
||||||
const [formData, setFormData] = useState<ProductRecipeRequest>(initialData)
|
const [formData, setFormData] = useState<ProductRecipeRequest>(initialData)
|
||||||
|
|
||||||
// Add state untuk menyimpan selected ingredient
|
|
||||||
const [selectedIngredient, setSelectedIngredient] = useState<IngredientItem | null>(null)
|
|
||||||
|
|
||||||
const { data: outlets, isLoading: outletsLoading } = useOutlets({
|
const { data: outlets, isLoading: outletsLoading } = useOutlets({
|
||||||
search: outletDebouncedInput
|
search: outletDebouncedInput
|
||||||
})
|
})
|
||||||
|
|
||||||
// Modifikasi query ingredients dengan enabled condition
|
|
||||||
const { data: ingredients, isLoading: ingredientsLoading } = useIngredients({
|
const { data: ingredients, isLoading: ingredientsLoading } = useIngredients({
|
||||||
search: ingredientDebouncedInput
|
search: ingredientDebouncedInput
|
||||||
})
|
})
|
||||||
|
|
||||||
const outletOptions = useMemo(() => outlets?.outlets || [], [outlets])
|
const outletOptions = useMemo(() => outlets?.outlets || [], [outlets])
|
||||||
|
const ingredientOptions = useMemo(() => ingredients?.data || [], [ingredients])
|
||||||
// Perbaiki ingredient options untuk include selected ingredient
|
|
||||||
const ingredientOptions = useMemo(() => {
|
|
||||||
const options = ingredients?.data || []
|
|
||||||
|
|
||||||
// Jika ada selected ingredient dan tidak ada di current options, tambahkan
|
|
||||||
if (selectedIngredient && !options.find(opt => opt.id === selectedIngredient.id)) {
|
|
||||||
return [selectedIngredient, ...options]
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}, [ingredients, selectedIngredient])
|
|
||||||
|
|
||||||
const { createProductRecipe, updateProductRecipe } = useProductRecipesMutation()
|
const { createProductRecipe, updateProductRecipe } = useProductRecipesMutation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentProductRecipe.id) {
|
if (currentProductRecipe.id) {
|
||||||
setFormData(currentProductRecipe)
|
setFormData(currentProductRecipe)
|
||||||
|
|
||||||
// Set selected ingredient dari current product recipe
|
|
||||||
const currentIngredient = ingredients?.data?.find(ing => ing.id === currentProductRecipe.ingredient_id)
|
|
||||||
if (currentIngredient) {
|
|
||||||
setSelectedIngredient(currentIngredient)
|
|
||||||
}
|
}
|
||||||
}
|
}, [currentProductRecipe])
|
||||||
}, [currentProductRecipe, ingredients])
|
|
||||||
|
|
||||||
const handleSubmit = (e: any) => {
|
const handleSubmit = (e: any) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -123,16 +101,24 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
handleClose()
|
handleClose()
|
||||||
dispatch(resetProductVariant())
|
dispatch(resetProductVariant())
|
||||||
setFormData(initialData)
|
setFormData(initialData)
|
||||||
setSelectedIngredient(null) // Reset selected ingredient
|
}
|
||||||
setIngredientInput('') // Reset input
|
|
||||||
|
const handleInputChange = (e: any) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[e.target.name]: e.target.value
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const setTitleDrawer = (recipe: any) => {
|
const setTitleDrawer = (recipe: any) => {
|
||||||
const addOrEdit = currentProductRecipe.id ? 'Edit ' : 'Add '
|
const addOrEdit = currentProductRecipe.id ? 'Edit ' : 'Add '
|
||||||
|
|
||||||
let title = 'Original'
|
let title = 'Original'
|
||||||
|
|
||||||
if (recipe?.name) {
|
if (recipe?.name) {
|
||||||
title = recipe?.name
|
title = recipe?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
return addOrEdit + title
|
return addOrEdit + title
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,14 +144,13 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
<Typography color='text.primary' className='font-medium'>
|
<Typography color='text.primary' className='font-medium'>
|
||||||
Basic Information
|
Basic Information
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={outletOptions}
|
options={outletOptions}
|
||||||
loading={outletsLoading}
|
loading={outletsLoading}
|
||||||
getOptionLabel={option => option.name}
|
getOptionLabel={option => option.name}
|
||||||
value={outletOptions.find(p => p.id === formData.outlet_id) || null}
|
value={outletOptions.find(p => p.id === formData.outlet_id) || null}
|
||||||
onInputChange={(event, newOutletInput) => {
|
onInputChange={(event, newOutlettInput) => {
|
||||||
setOutletInput(newOutletInput)
|
setOutletInput(newOutlettInput)
|
||||||
}}
|
}}
|
||||||
onChange={(event, newValue) => {
|
onChange={(event, newValue) => {
|
||||||
setFormData({
|
setFormData({
|
||||||
@ -176,6 +161,7 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...params}
|
{...params}
|
||||||
|
className=''
|
||||||
label='Outlet'
|
label='Outlet'
|
||||||
fullWidth
|
fullWidth
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -185,35 +171,24 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Perbaiki Autocomplete untuk Ingredients */}
|
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={ingredientOptions || []}
|
options={ingredientOptions || []}
|
||||||
loading={ingredientsLoading}
|
loading={ingredientsLoading}
|
||||||
getOptionLabel={option => option.name}
|
getOptionLabel={option => option.name}
|
||||||
value={selectedIngredient}
|
value={ingredientOptions?.find(p => p.id === formData.ingredient_id) || null}
|
||||||
onInputChange={(event, newIngredientInput) => {
|
onInputChange={(event, newIngredientInput) => {
|
||||||
setIngredientInput(newIngredientInput)
|
setIngredientInput(newIngredientInput)
|
||||||
}}
|
}}
|
||||||
onChange={(event, newValue) => {
|
onChange={(event, newValue) => {
|
||||||
setSelectedIngredient(newValue) // Set selected ingredient
|
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
ingredient_id: newValue?.id || ''
|
ingredient_id: newValue?.id || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// Clear input search setelah selection
|
|
||||||
if (newValue) {
|
|
||||||
setIngredientInput('')
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
// Tambahkan props untuk mencegah clear on blur
|
|
||||||
clearOnBlur={false}
|
|
||||||
// Handle case ketika input kosong tapi ada selected value
|
|
||||||
inputValue={selectedIngredient ? selectedIngredient.name : ingredientInput}
|
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...params}
|
{...params}
|
||||||
|
className=''
|
||||||
label='Ingredient'
|
label='Ingredient'
|
||||||
fullWidth
|
fullWidth
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -223,18 +198,6 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Unit Field - Disabled, value from selected ingredient */}
|
|
||||||
<CustomTextField
|
|
||||||
label='Unit'
|
|
||||||
fullWidth
|
|
||||||
disabled
|
|
||||||
value={selectedIngredient?.unit?.name || ''}
|
|
||||||
InputProps={{
|
|
||||||
readOnly: true
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
type='number'
|
type='number'
|
||||||
label='Quantity'
|
label='Quantity'
|
||||||
@ -242,15 +205,6 @@ const AddRecipeDrawer = (props: Props) => {
|
|||||||
value={formData.quantity}
|
value={formData.quantity}
|
||||||
onChange={e => setFormData({ ...formData, quantity: Number(e.target.value) })}
|
onChange={e => setFormData({ ...formData, quantity: Number(e.target.value) })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CustomTextField
|
|
||||||
type='number'
|
|
||||||
label='Waste'
|
|
||||||
fullWidth
|
|
||||||
value={formData.waste}
|
|
||||||
onChange={e => setFormData({ ...formData, waste: Number(e.target.value) })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<Button
|
<Button
|
||||||
variant='contained'
|
variant='contained'
|
||||||
|
|||||||
@ -161,7 +161,7 @@ const ProductDetail = () => {
|
|||||||
<TableCell className='font-semibold text-center'>
|
<TableCell className='font-semibold text-center'>
|
||||||
<div className='flex items-center justify-center gap-2'>
|
<div className='flex items-center justify-center gap-2'>
|
||||||
<i className='tabler-package text-blue-600' />
|
<i className='tabler-package text-blue-600' />
|
||||||
Waste
|
Stock Available
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className='font-semibold text-right'>
|
<TableCell className='font-semibold text-right'>
|
||||||
@ -197,7 +197,12 @@ const ProductDetail = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className='text-center'>{formatCurrency(item.ingredient.cost)}</TableCell>
|
<TableCell className='text-center'>{formatCurrency(item.ingredient.cost)}</TableCell>
|
||||||
<TableCell className='text-center'>
|
<TableCell className='text-center'>
|
||||||
<Chip label={item.waste ?? 0} size='small' color={'success'} variant='outlined' />
|
<Chip
|
||||||
|
label={item.ingredient.stock}
|
||||||
|
size='small'
|
||||||
|
color={item.ingredient.stock > 5 ? 'success' : 'warning'}
|
||||||
|
variant='outlined'
|
||||||
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className='text-right font-medium'>
|
<TableCell className='text-right font-medium'>
|
||||||
{formatCurrency(item.ingredient.cost * item.quantity)}
|
{formatCurrency(item.ingredient.cost * item.quantity)}
|
||||||
|
|||||||
@ -1,40 +1,25 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import Grid from '@mui/material/Grid2'
|
import Grid from '@mui/material/Grid2'
|
||||||
import PurchaseDetailInformation from './PurchaseDetailInformation'
|
import PurchaseDetailInformation from './PurchaseDetailInformation'
|
||||||
import PurchaseDetailSendPayment from './PurchaseDetailSendPayment'
|
import PurchaseDetailSendPayment from './PurchaseDetailSendPayment'
|
||||||
import PurchaseDetailLog from './PurchaseDetailLog'
|
import PurchaseDetailLog from './PurchaseDetailLog'
|
||||||
import PurchaseDetailTransaction from './PurchaseDetailTransaction'
|
import PurchaseDetailTransaction from './PurchaseDetailTransaction'
|
||||||
import { useParams } from 'next/navigation'
|
|
||||||
import { usePurchaseOrderById } from '@/services/queries/purchaseOrder'
|
|
||||||
import Loading from '@/components/layout/shared/Loading'
|
|
||||||
|
|
||||||
const PurchaseDetailContent = () => {
|
const PurchaseDetailContent = () => {
|
||||||
const params = useParams()
|
|
||||||
const { data, isLoading, error, isFetching } = usePurchaseOrderById(params.id as string)
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
{isLoading ? (
|
|
||||||
<Loading />
|
|
||||||
) : (
|
|
||||||
<Grid container spacing={6}>
|
<Grid container spacing={6}>
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<PurchaseDetailInformation data={data} />
|
<PurchaseDetailInformation />
|
||||||
</Grid>
|
</Grid>
|
||||||
{data?.status == 'sent' && (
|
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<PurchaseDetailSendPayment />
|
<PurchaseDetailSendPayment />
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
<Grid size={{ xs: 12 }}>
|
||||||
{/* <Grid size={{ xs: 12 }}>
|
|
||||||
<PurchaseDetailTransaction />
|
<PurchaseDetailTransaction />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<PurchaseDetailLog />
|
<PurchaseDetailLog />
|
||||||
</Grid> */}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
</Grid>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -17,62 +15,87 @@ import {
|
|||||||
IconButton
|
IconButton
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import Grid from '@mui/material/Grid2'
|
import Grid from '@mui/material/Grid2'
|
||||||
import { PurchaseOrder } from '@/types/services/purchaseOrder'
|
|
||||||
|
|
||||||
interface Props {
|
interface Product {
|
||||||
data?: PurchaseOrder
|
produk: string
|
||||||
|
deskripsi: string
|
||||||
|
kuantitas: number
|
||||||
|
satuan: string
|
||||||
|
discount: string
|
||||||
|
harga: number
|
||||||
|
pajak: string
|
||||||
|
jumlah: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const PurchaseDetailInformation = ({ data }: Props) => {
|
interface PurchaseData {
|
||||||
const purchaseOrder = data
|
vendor: string
|
||||||
|
nomor: string
|
||||||
|
tglTransaksi: string
|
||||||
|
tglJatuhTempo: string
|
||||||
|
gudang: string
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions
|
const PurchaseDetailInformation: React.FC = () => {
|
||||||
const formatDate = (dateString: string): string => {
|
const purchaseData: PurchaseData = {
|
||||||
const date = new Date(dateString)
|
vendor: 'Bagas Rizki Sihotang S.Farm Widodo',
|
||||||
return date.toLocaleDateString('id-ID', {
|
nomor: 'PI/00053',
|
||||||
day: '2-digit',
|
tglTransaksi: '08/09/2025',
|
||||||
month: '2-digit',
|
tglJatuhTempo: '06/10/2025',
|
||||||
year: 'numeric'
|
gudang: 'Unassigned',
|
||||||
})
|
status: 'Belum Dibayar'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const products: Product[] = [
|
||||||
|
{
|
||||||
|
produk: 'CB1 - Chelsea Boots',
|
||||||
|
deskripsi: 'Ukuran XS',
|
||||||
|
kuantitas: 3,
|
||||||
|
satuan: 'Pcs',
|
||||||
|
discount: '0%',
|
||||||
|
harga: 299000,
|
||||||
|
pajak: 'PPN',
|
||||||
|
jumlah: 897000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
produk: 'CB1 - Chelsea Boots',
|
||||||
|
deskripsi: 'Ukuran M',
|
||||||
|
kuantitas: 1,
|
||||||
|
satuan: 'Pcs',
|
||||||
|
discount: '0%',
|
||||||
|
harga: 299000,
|
||||||
|
pajak: 'PPN',
|
||||||
|
jumlah: 299000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
produk: 'KH1 - Kneel High Boots',
|
||||||
|
deskripsi: 'Ukuran XL',
|
||||||
|
kuantitas: 1,
|
||||||
|
satuan: 'Pcs',
|
||||||
|
discount: '0%',
|
||||||
|
harga: 299000,
|
||||||
|
pajak: 'PPN',
|
||||||
|
jumlah: 299000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const totalKuantitas: number = products.reduce((sum, product) => sum + product.kuantitas, 0)
|
||||||
|
const subTotal: number = 1495000
|
||||||
|
const ppn: number = 98670
|
||||||
|
const total: number = 1593670
|
||||||
|
const sisaTagihan: number = 1593670
|
||||||
|
|
||||||
const formatCurrency = (amount: number): string => {
|
const formatCurrency = (amount: number): string => {
|
||||||
return new Intl.NumberFormat('id-ID').format(amount)
|
return new Intl.NumberFormat('id-ID').format(amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusLabel = (status: string): string => {
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
draft: 'Draft',
|
|
||||||
sent: 'Dikirim',
|
|
||||||
approved: 'Disetujui',
|
|
||||||
received: 'Diterima',
|
|
||||||
cancelled: 'Dibatalkan'
|
|
||||||
}
|
|
||||||
return statusMap[status] || status
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusColor = (status: string): 'error' | 'success' | 'warning' | 'info' | 'default' => {
|
|
||||||
const colorMap: Record<string, 'error' | 'success' | 'warning' | 'info' | 'default'> = {
|
|
||||||
draft: 'default',
|
|
||||||
sent: 'warning',
|
|
||||||
approved: 'success',
|
|
||||||
received: 'info',
|
|
||||||
cancelled: 'error'
|
|
||||||
}
|
|
||||||
return colorMap[status] || 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculations
|
|
||||||
const totalQuantity = (purchaseOrder?.items ?? []).reduce((sum, item) => sum + (item?.quantity ?? 0), 0)
|
|
||||||
const total = (purchaseOrder?.items ?? []).reduce((sum, item) => sum + (item?.amount ?? 0) * item?.quantity, 0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{ width: '100%' }}>
|
<Card sx={{ width: '100%' }}>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
title={
|
title={
|
||||||
<Box display='flex' justifyContent='space-between' alignItems='center'>
|
<Box display='flex' justifyContent='space-between' alignItems='center'>
|
||||||
<Typography variant='h5' color={getStatusColor(purchaseOrder?.status ?? '')} sx={{ fontWeight: 'bold' }}>
|
<Typography variant='h5' color='error' sx={{ fontWeight: 'bold' }}>
|
||||||
{getStatusLabel(purchaseOrder?.status ?? '')}
|
Belum Dibayar
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box>
|
<Box>
|
||||||
<Button startIcon={<i className='tabler-share' />} variant='outlined' size='small' sx={{ mr: 1 }}>
|
<Button startIcon={<i className='tabler-share' />} variant='outlined' size='small' sx={{ mr: 1 }}>
|
||||||
@ -98,15 +121,24 @@ const PurchaseDetailInformation = ({ data }: Props) => {
|
|||||||
Vendor
|
Vendor
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1' color='primary' sx={{ fontWeight: 'medium', cursor: 'pointer' }}>
|
<Typography variant='body1' color='primary' sx={{ fontWeight: 'medium', cursor: 'pointer' }}>
|
||||||
{purchaseOrder?.vendor?.name ?? ''}
|
{purchaseData.vendor}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Typography variant='subtitle2' color='text.secondary'>
|
||||||
|
Tgl. Transaksi
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1'>{purchaseData.tglTransaksi}</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant='subtitle2' color='text.secondary'>
|
<Typography variant='subtitle2' color='text.secondary'>
|
||||||
Tgl. Transaksi
|
Gudang
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' color='primary' sx={{ cursor: 'pointer' }}>
|
||||||
|
{purchaseData.gudang}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1'>{formatDate(purchaseOrder?.transaction_date ?? '')}</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@ -115,14 +147,14 @@ const PurchaseDetailInformation = ({ data }: Props) => {
|
|||||||
<Typography variant='subtitle2' color='text.secondary'>
|
<Typography variant='subtitle2' color='text.secondary'>
|
||||||
Nomor
|
Nomor
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1'>{purchaseOrder?.po_number}</Typography>
|
<Typography variant='body1'>{purchaseData.nomor}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant='subtitle2' color='text.secondary'>
|
<Typography variant='subtitle2' color='text.secondary'>
|
||||||
Tgl. Jatuh Tempo
|
Tgl. Jatuh Tempo
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1'>{formatDate(purchaseOrder?.due_date ?? '')}</Typography>
|
<Typography variant='body1'>{purchaseData.tglJatuhTempo}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -136,38 +168,43 @@ const PurchaseDetailInformation = ({ data }: Props) => {
|
|||||||
<TableCell>Deskripsi</TableCell>
|
<TableCell>Deskripsi</TableCell>
|
||||||
<TableCell align='center'>Kuantitas</TableCell>
|
<TableCell align='center'>Kuantitas</TableCell>
|
||||||
<TableCell align='center'>Satuan</TableCell>
|
<TableCell align='center'>Satuan</TableCell>
|
||||||
|
<TableCell align='center'>Discount</TableCell>
|
||||||
<TableCell align='right'>Harga</TableCell>
|
<TableCell align='right'>Harga</TableCell>
|
||||||
|
<TableCell align='center'>Pajak</TableCell>
|
||||||
<TableCell align='right'>Jumlah</TableCell>
|
<TableCell align='right'>Jumlah</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(purchaseOrder?.items ?? []).map((item, index) => {
|
{products.map((product, index) => (
|
||||||
return (
|
<TableRow key={index}>
|
||||||
<TableRow key={item.id}>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Typography variant='body2' color='primary' sx={{ cursor: 'pointer' }}>
|
<Typography variant='body2' color='primary' sx={{ cursor: 'pointer' }}>
|
||||||
{item.ingredient.name}
|
{product.produk}
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{item.description}</TableCell>
|
<TableCell>{product.deskripsi}</TableCell>
|
||||||
<TableCell align='center'>{item.quantity}</TableCell>
|
<TableCell align='center'>{product.kuantitas}</TableCell>
|
||||||
<TableCell align='center'>{item.unit.name}</TableCell>
|
<TableCell align='center'>{product.satuan}</TableCell>
|
||||||
<TableCell align='right'>{formatCurrency(item.amount)}</TableCell>
|
<TableCell align='center'>{product.discount}</TableCell>
|
||||||
<TableCell align='right'>{formatCurrency(item.amount * item.quantity)}</TableCell>
|
<TableCell align='right'>{formatCurrency(product.harga)}</TableCell>
|
||||||
|
<TableCell align='center'>{product.pajak}</TableCell>
|
||||||
|
<TableCell align='right'>{formatCurrency(product.jumlah)}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
))}
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Total Quantity Row */}
|
{/* Total Kuantitas Row */}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={2} sx={{ fontWeight: 'bold', borderTop: '2px solid #e0e0e0' }}>
|
<TableCell colSpan={2} sx={{ fontWeight: 'bold', borderTop: '2px solid #e0e0e0' }}>
|
||||||
Total Kuantitas
|
Total Kuantitas
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align='center' sx={{ fontWeight: 'bold', borderTop: '2px solid #e0e0e0' }}>
|
<TableCell align='center' sx={{ fontWeight: 'bold', borderTop: '2px solid #e0e0e0' }}>
|
||||||
{totalQuantity}
|
{totalKuantitas}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
||||||
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
||||||
|
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
||||||
|
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
||||||
|
<TableCell sx={{ borderTop: '2px solid #e0e0e0' }}></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
@ -179,6 +216,69 @@ const PurchaseDetailInformation = ({ data }: Props) => {
|
|||||||
<Grid size={{ xs: 12, md: 6 }}>{/* Empty space for left side */}</Grid>
|
<Grid size={{ xs: 12, md: 6 }}>{/* Empty space for left side */}</Grid>
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||||
|
transition: 'background-color 0.15s ease'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
||||||
|
Sub Total
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
||||||
|
{formatCurrency(subTotal)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||||
|
transition: 'background-color 0.15s ease'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
||||||
|
PPN
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' sx={{ fontWeight: 'medium' }}>
|
||||||
|
{formatCurrency(ppn)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||||
|
transition: 'background-color 0.15s ease'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='body1' sx={{ fontWeight: 'bold' }}>
|
||||||
|
Total
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' sx={{ fontWeight: 'bold' }}>
|
||||||
|
{formatCurrency(total)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -192,10 +292,10 @@ const PurchaseDetailInformation = ({ data }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant='h6' sx={{ fontWeight: 'bold' }}>
|
<Typography variant='h6' sx={{ fontWeight: 'bold' }}>
|
||||||
Total
|
Sisa Tagihan
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='h6' sx={{ fontWeight: 'bold' }}>
|
<Typography variant='h6' sx={{ fontWeight: 'bold' }}>
|
||||||
{formatCurrency(total)}
|
{formatCurrency(sisaTagihan)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -15,10 +15,7 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
Paper,
|
Paper,
|
||||||
CircularProgress,
|
CircularProgress
|
||||||
Alert,
|
|
||||||
Popover,
|
|
||||||
Divider
|
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import Grid from '@mui/material/Grid2'
|
import Grid from '@mui/material/Grid2'
|
||||||
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
||||||
@ -30,113 +27,88 @@ import { useIngredients } from '@/services/queries/ingredients'
|
|||||||
import { useUnits } from '@/services/queries/units'
|
import { useUnits } from '@/services/queries/units'
|
||||||
import { useFilesMutation } from '@/services/mutations/files'
|
import { useFilesMutation } from '@/services/mutations/files'
|
||||||
import { usePurchaseOrdersMutation } from '@/services/mutations/purchaseOrder'
|
import { usePurchaseOrdersMutation } from '@/services/mutations/purchaseOrder'
|
||||||
import { PurchaseOrderFormData, PurchaseOrderFormItem, PurchaseOrderRequest } from '@/types/services/purchaseOrder'
|
|
||||||
import { IngredientItem } from '@/types/services/ingredient'
|
export interface PurchaseOrderRequest {
|
||||||
|
vendor_id: string // uuid.UUID
|
||||||
|
po_number: string
|
||||||
|
transaction_date: string // ISO date string
|
||||||
|
due_date: string // ISO date string
|
||||||
|
reference?: string
|
||||||
|
status?: 'draft' | 'sent' | 'approved' | 'received' | 'cancelled'
|
||||||
|
message?: string
|
||||||
|
items: PurchaseOrderItemRequest[]
|
||||||
|
attachment_file_ids?: string[] // uuid.UUID[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderItemRequest {
|
||||||
|
ingredient_id: string // uuid.UUID
|
||||||
|
description?: string
|
||||||
|
quantity: number
|
||||||
|
unit_id: string // uuid.UUID
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IngredientItem = {
|
||||||
|
id: string
|
||||||
|
organization_id: string
|
||||||
|
outlet_id: string
|
||||||
|
name: string
|
||||||
|
unit_id: string
|
||||||
|
cost: number
|
||||||
|
stock: number
|
||||||
|
is_semi_finished: boolean
|
||||||
|
is_active: boolean
|
||||||
|
metadata: Record<string, unknown>
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
unit: Unit
|
||||||
|
}
|
||||||
|
|
||||||
export type Unit = {
|
export type Unit = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
// Add other unit properties as needed
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ValidationErrors {
|
// Internal form state interface for UI management
|
||||||
vendor?: string
|
interface PurchaseOrderFormData {
|
||||||
po_number?: string
|
vendor: { label: string; value: string } | null
|
||||||
transaction_date?: string
|
po_number: string
|
||||||
due_date?: string
|
transaction_date: string
|
||||||
items?: string
|
due_date: string
|
||||||
general?: string
|
reference: string
|
||||||
|
status: 'draft' | 'sent' | 'approved' | 'received' | 'cancelled'
|
||||||
|
showPesan: boolean
|
||||||
|
showAttachment: boolean
|
||||||
|
message: string
|
||||||
|
items: PurchaseOrderFormItem[]
|
||||||
|
attachment_file_ids: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PopoverState {
|
interface PurchaseOrderFormItem {
|
||||||
isOpen: boolean
|
id: number // for UI tracking
|
||||||
anchorEl: HTMLElement | null
|
ingredient: { label: string; value: string; originalData?: IngredientItem } | null
|
||||||
itemIndex: number | null
|
description: string
|
||||||
}
|
quantity: number
|
||||||
|
unit: { label: string; value: string } | null
|
||||||
// Komponen PricePopover
|
amount: number
|
||||||
const PricePopover: React.FC<{
|
total: number // calculated field for UI
|
||||||
anchorEl: HTMLElement | null
|
|
||||||
open: boolean
|
|
||||||
onClose: () => void
|
|
||||||
ingredientData: any
|
|
||||||
}> = ({ anchorEl, open, onClose, ingredientData }) => {
|
|
||||||
if (!ingredientData) return null
|
|
||||||
|
|
||||||
const lastPrice = ingredientData.originalData?.cost || 0
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
open={open}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
onClose={onClose}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'left'
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'left'
|
|
||||||
}}
|
|
||||||
PaperProps={{
|
|
||||||
sx: {
|
|
||||||
minWidth: 300,
|
|
||||||
maxWidth: 350,
|
|
||||||
boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
|
|
||||||
borderRadius: 2
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ p: 2 }}>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
|
||||||
<Typography variant='body2' color='text.secondary'>
|
|
||||||
Harga beli terakhir
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='h6' color='primary' fontWeight={600}>
|
|
||||||
{new Intl.NumberFormat('id-ID').format(lastPrice)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider sx={{ mb: 2 }} />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='text'
|
|
||||||
size='small'
|
|
||||||
sx={{
|
|
||||||
color: 'primary.main',
|
|
||||||
textTransform: 'none',
|
|
||||||
p: 0,
|
|
||||||
minWidth: 'auto'
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
console.log('Navigate to purchase history')
|
|
||||||
onClose()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Riwayat harga beli
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PurchaseAddForm: React.FC = () => {
|
const PurchaseAddForm: React.FC = () => {
|
||||||
const [imageUrl, setImageUrl] = useState<string>('')
|
const [imageUrl, setImageUrl] = useState<string>('')
|
||||||
const [errors, setErrors] = useState<ValidationErrors>({})
|
|
||||||
const [popoverState, setPopoverState] = useState<PopoverState>({
|
|
||||||
isOpen: false,
|
|
||||||
anchorEl: null,
|
|
||||||
itemIndex: null
|
|
||||||
})
|
|
||||||
const [formData, setFormData] = useState<PurchaseOrderFormData>({
|
const [formData, setFormData] = useState<PurchaseOrderFormData>({
|
||||||
vendor: null,
|
vendor: null,
|
||||||
po_number: '',
|
po_number: '',
|
||||||
transaction_date: '',
|
transaction_date: '',
|
||||||
due_date: '',
|
due_date: '',
|
||||||
reference: '',
|
reference: '',
|
||||||
status: 'sent',
|
status: 'draft',
|
||||||
|
// Bottom section toggles
|
||||||
showPesan: false,
|
showPesan: false,
|
||||||
showAttachment: false,
|
showAttachment: false,
|
||||||
message: '',
|
message: '',
|
||||||
|
// Items
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -182,7 +154,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
label: ingredient.name,
|
label: ingredient.name,
|
||||||
value: ingredient.id,
|
value: ingredient.id,
|
||||||
id: ingredient.id,
|
id: ingredient.id,
|
||||||
originalData: ingredient
|
originalData: ingredient // This includes the full IngredientItem with unit, cost, etc.
|
||||||
}))
|
}))
|
||||||
}, [ingredients, isLoadingIngredients])
|
}, [ingredients, isLoadingIngredients])
|
||||||
|
|
||||||
@ -200,78 +172,12 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}, [units, isLoadingUnits])
|
}, [units, isLoadingUnits])
|
||||||
|
|
||||||
// Handle price field click untuk menampilkan popover
|
|
||||||
const handlePriceFieldClick = (event: React.MouseEvent<HTMLElement>, itemIndex: number) => {
|
|
||||||
const item = formData.items[itemIndex]
|
|
||||||
if (item.ingredient) {
|
|
||||||
setPopoverState({
|
|
||||||
isOpen: true,
|
|
||||||
anchorEl: event.currentTarget,
|
|
||||||
itemIndex: itemIndex
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close popover
|
|
||||||
const handleClosePopover = () => {
|
|
||||||
setPopoverState({
|
|
||||||
isOpen: false,
|
|
||||||
anchorEl: null,
|
|
||||||
itemIndex: null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fungsi validasi
|
|
||||||
const validateForm = (): boolean => {
|
|
||||||
const newErrors: ValidationErrors = {}
|
|
||||||
|
|
||||||
if (!formData.vendor || !formData.vendor.value) {
|
|
||||||
newErrors.vendor = 'Vendor wajib dipilih'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.po_number.trim()) {
|
|
||||||
newErrors.po_number = 'Nomor PO wajib diisi'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.transaction_date) {
|
|
||||||
newErrors.transaction_date = 'Tanggal transaksi wajib diisi'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.due_date) {
|
|
||||||
newErrors.due_date = 'Tanggal jatuh tempo wajib diisi'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.transaction_date && formData.due_date) {
|
|
||||||
if (new Date(formData.due_date) < new Date(formData.transaction_date)) {
|
|
||||||
newErrors.due_date = 'Tanggal jatuh tempo tidak boleh sebelum tanggal transaksi'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validItems = formData.items.filter(
|
|
||||||
item => item.ingredient && item.unit && item.quantity > 0 && item.amount > 0
|
|
||||||
)
|
|
||||||
|
|
||||||
if (validItems.length === 0) {
|
|
||||||
newErrors.items = 'Minimal harus ada 1 item yang valid dengan bahan, satuan, kuantitas dan harga yang terisi'
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors(newErrors)
|
|
||||||
return Object.keys(newErrors).length === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler Functions
|
// Handler Functions
|
||||||
const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => {
|
const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value
|
[field]: value
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (errors[field as keyof ValidationErrors]) {
|
|
||||||
setErrors(prev => ({
|
|
||||||
...prev,
|
|
||||||
[field]: undefined
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleItemChange = (index: number, field: keyof PurchaseOrderFormItem, value: any): void => {
|
const handleItemChange = (index: number, field: keyof PurchaseOrderFormItem, value: any): void => {
|
||||||
@ -279,6 +185,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
const newItems = [...prev.items]
|
const newItems = [...prev.items]
|
||||||
newItems[index] = { ...newItems[index], [field]: value }
|
newItems[index] = { ...newItems[index], [field]: value }
|
||||||
|
|
||||||
|
// Auto-calculate total if amount or quantity changes
|
||||||
if (field === 'amount' || field === 'quantity') {
|
if (field === 'amount' || field === 'quantity') {
|
||||||
const item = newItems[index]
|
const item = newItems[index]
|
||||||
item.total = item.amount * item.quantity
|
item.total = item.amount * item.quantity
|
||||||
@ -286,31 +193,30 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
|
|
||||||
return { ...prev, items: newItems }
|
return { ...prev, items: newItems }
|
||||||
})
|
})
|
||||||
|
|
||||||
if (errors.items) {
|
|
||||||
setErrors(prev => ({
|
|
||||||
...prev,
|
|
||||||
items: undefined
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIngredientSelection = (index: number, selectedIngredient: any) => {
|
const handleIngredientSelection = (index: number, selectedIngredient: any) => {
|
||||||
handleItemChange(index, 'ingredient', selectedIngredient)
|
handleItemChange(index, 'ingredient', selectedIngredient)
|
||||||
|
|
||||||
|
// Auto-populate related fields if available in the ingredient data
|
||||||
if (selectedIngredient) {
|
if (selectedIngredient) {
|
||||||
const ingredientData: IngredientItem = selectedIngredient.originalData || selectedIngredient
|
const ingredientData: IngredientItem = selectedIngredient.originalData || selectedIngredient
|
||||||
|
|
||||||
|
// Auto-fill unit based on IngredientItem structure
|
||||||
if (ingredientData.unit_id || ingredientData.unit) {
|
if (ingredientData.unit_id || ingredientData.unit) {
|
||||||
let unitToFind = null
|
let unitToFind = null
|
||||||
|
|
||||||
|
// If ingredient has unit object (populated relation)
|
||||||
if (ingredientData.unit && typeof ingredientData.unit === 'object') {
|
if (ingredientData.unit && typeof ingredientData.unit === 'object') {
|
||||||
unitToFind = ingredientData.unit
|
unitToFind = ingredientData.unit
|
||||||
} else if (ingredientData.unit_id) {
|
}
|
||||||
|
// If ingredient has unit_id, find the unit from unitOptions
|
||||||
|
else if (ingredientData.unit_id) {
|
||||||
unitToFind = unitOptions.find(option => option.value === ingredientData.unit_id)
|
unitToFind = unitOptions.find(option => option.value === ingredientData.unit_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unitToFind) {
|
if (unitToFind) {
|
||||||
|
// Create unit option object
|
||||||
const unitOption = {
|
const unitOption = {
|
||||||
label: (unitToFind as any).label || (unitToFind as any).name || (unitToFind as any).unit_name,
|
label: (unitToFind as any).label || (unitToFind as any).name || (unitToFind as any).unit_name,
|
||||||
value: (unitToFind as any).value || ingredientData.unit_id
|
value: (unitToFind as any).value || ingredientData.unit_id
|
||||||
@ -320,10 +226,12 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-fill amount with cost from IngredientItem
|
||||||
if (ingredientData.cost !== undefined && ingredientData.cost !== null) {
|
if (ingredientData.cost !== undefined && ingredientData.cost !== null) {
|
||||||
handleItemChange(index, 'amount', ingredientData.cost)
|
handleItemChange(index, 'amount', ingredientData.cost)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-fill description with ingredient name
|
||||||
if (ingredientData.name) {
|
if (ingredientData.name) {
|
||||||
handleItemChange(index, 'description', ingredientData.name)
|
handleItemChange(index, 'description', ingredientData.name)
|
||||||
}
|
}
|
||||||
@ -353,6 +261,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to get selected vendor data
|
||||||
const getSelectedVendorData = () => {
|
const getSelectedVendorData = () => {
|
||||||
if (!formData.vendor?.value || !vendors) return null
|
if (!formData.vendor?.value || !vendors) return null
|
||||||
const selectedVendor = vendors.find(vendor => vendor.id === (formData?.vendor?.value ?? ''))
|
const selectedVendor = vendors.find(vendor => vendor.id === (formData?.vendor?.value ?? ''))
|
||||||
@ -371,26 +280,29 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
formData.append('file_type', 'image')
|
formData.append('file_type', 'image')
|
||||||
formData.append('description', 'Gambar Purchase Order')
|
formData.append('description', 'Purchase image')
|
||||||
|
|
||||||
mutate(formData, {
|
mutate(formData, {
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
|
// pemakaian:
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
attachment_file_ids: upsertAttachment(prev.attachment_file_ids, data.id)
|
attachment_file_ids: upsertAttachment(prev.attachment_file_ids, data.id)
|
||||||
}))
|
}))
|
||||||
setImageUrl(data.file_url)
|
setImageUrl(data.file_url)
|
||||||
resolve(data.id)
|
resolve(data.id) // <-- balikin id file yang berhasil diupload
|
||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
reject(error)
|
reject(error) // biar async/await bisa tangkep error
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate subtotal from items
|
||||||
const subtotal = formData.items.reduce((sum, item) => sum + (item.total || 0), 0)
|
const subtotal = formData.items.reduce((sum, item) => sum + (item.total || 0), 0)
|
||||||
|
|
||||||
|
// Convert form data to API request format
|
||||||
const convertToApiRequest = (): PurchaseOrderRequest => {
|
const convertToApiRequest = (): PurchaseOrderRequest => {
|
||||||
return {
|
return {
|
||||||
vendor_id: formData.vendor?.value || '',
|
vendor_id: formData.vendor?.value || '',
|
||||||
@ -401,7 +313,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
status: formData.status,
|
status: formData.status,
|
||||||
message: formData.message || undefined,
|
message: formData.message || undefined,
|
||||||
items: formData.items
|
items: formData.items
|
||||||
.filter(item => item.ingredient && item.unit)
|
.filter(item => item.ingredient && item.unit) // Only include valid items
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
ingredient_id: item.ingredient!.value,
|
ingredient_id: item.ingredient!.value,
|
||||||
description: item.description || undefined,
|
description: item.description || undefined,
|
||||||
@ -414,46 +326,19 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!validateForm()) {
|
|
||||||
setErrors(prev => ({
|
|
||||||
...prev,
|
|
||||||
general: 'Mohon lengkapi semua field yang wajib diisi'
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createPurchaseOrder.mutate(convertToApiRequest(), {
|
createPurchaseOrder.mutate(convertToApiRequest(), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
window.history.back()
|
window.history.back()
|
||||||
},
|
|
||||||
onError: error => {
|
|
||||||
setErrors(prev => ({
|
|
||||||
...prev,
|
|
||||||
general: 'Terjadi kesalahan saat menyimpan data. Silakan coba lagi.'
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current ingredient data for popover
|
|
||||||
const getCurrentIngredientData = () => {
|
|
||||||
if (popoverState.itemIndex !== null) {
|
|
||||||
return formData.items[popoverState.itemIndex]?.ingredient
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{errors.general && (
|
|
||||||
<Alert severity='error' sx={{ mb: 3 }}>
|
|
||||||
{errors.general}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{/* BASIC INFO SECTION */}
|
{/* BASIC INFO SECTION */}
|
||||||
|
{/* Row 1 - Vendor and PO Number */}
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
||||||
<CustomAutocomplete
|
<CustomAutocomplete
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -463,18 +348,16 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
handleInputChange('vendor', newValue)
|
handleInputChange('vendor', newValue)
|
||||||
if (newValue?.value) {
|
if (newValue?.value) {
|
||||||
const selectedVendorData = vendors?.find(vendor => vendor.id === newValue.value)
|
const selectedVendorData = vendors?.find(vendor => vendor.id === newValue.value)
|
||||||
console.log('Vendor terpilih:', selectedVendorData)
|
console.log('Vendor selected:', selectedVendorData)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
loading={isLoadingVendors}
|
loading={isLoadingVendors}
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...params}
|
{...params}
|
||||||
label='Vendor *'
|
label='Vendor'
|
||||||
placeholder={isLoadingVendors ? 'Memuat vendor...' : 'Pilih vendor'}
|
placeholder={isLoadingVendors ? 'Memuat vendor...' : 'Pilih kontak'}
|
||||||
fullWidth
|
fullWidth
|
||||||
error={!!errors.vendor}
|
|
||||||
helperText={errors.vendor}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -504,11 +387,9 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='Nomor PO *'
|
label='PO Number'
|
||||||
value={formData.po_number}
|
value={formData.po_number}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('po_number', e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('po_number', e.target.value)}
|
||||||
error={!!errors.po_number}
|
|
||||||
helperText={errors.po_number}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@ -516,7 +397,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='Tanggal Transaksi *'
|
label='Transaction Date'
|
||||||
type='date'
|
type='date'
|
||||||
value={formData.transaction_date}
|
value={formData.transaction_date}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
@ -525,29 +406,25 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
shrink: true
|
shrink: true
|
||||||
}}
|
}}
|
||||||
error={!!errors.transaction_date}
|
|
||||||
helperText={errors.transaction_date}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='Tanggal Jatuh Tempo *'
|
label='Due Date'
|
||||||
type='date'
|
type='date'
|
||||||
value={formData.due_date}
|
value={formData.due_date}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('due_date', e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('due_date', e.target.value)}
|
||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
shrink: true
|
shrink: true
|
||||||
}}
|
}}
|
||||||
error={!!errors.due_date}
|
|
||||||
helperText={errors.due_date}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='Referensi'
|
label='Reference'
|
||||||
placeholder='Referensi'
|
placeholder='Reference'
|
||||||
value={formData.reference}
|
value={formData.reference}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('reference', e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('reference', e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -556,24 +433,18 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
{/* ITEMS TABLE SECTION */}
|
{/* ITEMS TABLE SECTION */}
|
||||||
<Grid size={{ xs: 12 }} sx={{ mt: 4 }}>
|
<Grid size={{ xs: 12 }} sx={{ mt: 4 }}>
|
||||||
<Typography variant='h6' sx={{ mb: 2, fontWeight: 600 }}>
|
<Typography variant='h6' sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
Item Purchase Order
|
Purchase Order Items
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{errors.items && (
|
|
||||||
<Alert severity='error' sx={{ mb: 2 }}>
|
|
||||||
{errors.items}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TableContainer component={Paper} variant='outlined'>
|
<TableContainer component={Paper} variant='outlined'>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow sx={{ backgroundColor: 'grey.50' }}>
|
<TableRow sx={{ backgroundColor: 'grey.50' }}>
|
||||||
<TableCell sx={{ fontWeight: 'bold', minWidth: 180 }}>Bahan</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', minWidth: 180 }}>Ingredient</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 'bold', minWidth: 150 }}>Deskripsi</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', minWidth: 150 }}>Description</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 100 }}>Kuantitas</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', width: 100 }}>Quantity</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Satuan</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Unit</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Harga</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Amount</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 100, textAlign: 'right' }}>Total</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', width: 100, textAlign: 'right' }}>Total</TableCell>
|
||||||
<TableCell sx={{ width: 50 }}></TableCell>
|
<TableCell sx={{ width: 50 }}></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -601,7 +472,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...params}
|
{...params}
|
||||||
placeholder={isLoadingIngredients ? 'Memuat bahan...' : 'Pilih Bahan'}
|
placeholder={isLoadingIngredients ? 'Loading ingredients...' : 'Select Ingredient'}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
...params.InputProps,
|
...params.InputProps,
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
@ -624,7 +495,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
handleItemChange(index, 'description', e.target.value)
|
handleItemChange(index, 'description', e.target.value)
|
||||||
}
|
}
|
||||||
placeholder='Deskripsi'
|
placeholder='Description'
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -653,7 +524,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...params}
|
{...params}
|
||||||
placeholder={isLoadingUnits ? 'Memuat satuan...' : 'Pilih satuan...'}
|
placeholder={isLoadingUnits ? 'Loading units...' : 'Select...'}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
...params.InputProps,
|
...params.InputProps,
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
@ -683,10 +554,8 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
const numericValue = parseFloat(value)
|
const numericValue = parseFloat(value)
|
||||||
handleItemChange(index, 'amount', isNaN(numericValue) ? 0 : numericValue)
|
handleItemChange(index, 'amount', isNaN(numericValue) ? 0 : numericValue)
|
||||||
}}
|
}}
|
||||||
onClick={e => handlePriceFieldClick(e, index)}
|
|
||||||
inputProps={{ min: 0, step: 'any' }}
|
inputProps={{ min: 0, step: 'any' }}
|
||||||
placeholder='0'
|
placeholder='0'
|
||||||
sx={{ cursor: item.ingredient ? 'pointer' : 'text' }}
|
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -727,7 +596,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
sx={{ mt: 1 }}
|
sx={{ mt: 1 }}
|
||||||
disabled={isLoadingIngredients || isLoadingUnits}
|
disabled={isLoadingIngredients || isLoadingUnits}
|
||||||
>
|
>
|
||||||
Tambah Item
|
Add Item
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@ -767,7 +636,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
<i className='tabler-chevron-right w-4 h-4' />
|
<i className='tabler-chevron-right w-4 h-4' />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
Pesan
|
Message
|
||||||
</Button>
|
</Button>
|
||||||
{formData.showPesan && (
|
{formData.showPesan && (
|
||||||
<Box sx={{ mt: 2 }}>
|
<Box sx={{ mt: 2 }}>
|
||||||
@ -775,7 +644,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
multiline
|
multiline
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder='Tambahkan pesan...'
|
placeholder='Add message...'
|
||||||
value={formData.message || ''}
|
value={formData.message || ''}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
handleInputChange('message', e.target.value)
|
handleInputChange('message', e.target.value)
|
||||||
@ -816,7 +685,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
<i className='tabler-chevron-right w-4 h-4' />
|
<i className='tabler-chevron-right w-4 h-4' />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
Lampiran
|
Attachment
|
||||||
</Button>
|
</Button>
|
||||||
{formData.showAttachment && (
|
{formData.showAttachment && (
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
@ -824,8 +693,8 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
maxFileSize={1 * 1024 * 1024}
|
maxFileSize={1 * 1024 * 1024}
|
||||||
showUrlOption={false}
|
showUrlOption={false}
|
||||||
currentImageUrl={imageUrl}
|
currentImageUrl={imageUrl}
|
||||||
dragDropText='Letakkan gambar Anda di sini'
|
dragDropText='Drop your image here'
|
||||||
browseButtonText='Pilih Gambar'
|
browseButtonText='Choose Image'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@ -890,7 +759,6 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
color='primary'
|
color='primary'
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={createPurchaseOrder.isPending}
|
|
||||||
sx={{
|
sx={{
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@ -902,28 +770,13 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{createPurchaseOrder.isPending ? (
|
Save
|
||||||
<>
|
|
||||||
<CircularProgress size={16} sx={{ mr: 1 }} />
|
|
||||||
Menyimpan...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'Simpan'
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Price Popover */}
|
|
||||||
<PricePopover
|
|
||||||
anchorEl={popoverState.anchorEl}
|
|
||||||
open={popoverState.isOpen}
|
|
||||||
onClose={handleClosePopover}
|
|
||||||
ingredientData={getCurrentIngredientData()}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -107,16 +107,14 @@ const DebouncedInput = ({
|
|||||||
// Status color mapping
|
// Status color mapping
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'draft':
|
case 'Draft':
|
||||||
return 'secondary'
|
return 'secondary'
|
||||||
case 'approved':
|
case 'Disetujui':
|
||||||
return 'primary'
|
return 'primary'
|
||||||
case 'sent':
|
case 'Dikirim Sebagian':
|
||||||
return 'warning'
|
return 'warning'
|
||||||
case 'received':
|
case 'Selesai':
|
||||||
return 'success'
|
return 'success'
|
||||||
case 'cancelled':
|
|
||||||
return 'error'
|
|
||||||
default:
|
default:
|
||||||
return 'default'
|
return 'default'
|
||||||
}
|
}
|
||||||
@ -212,8 +210,7 @@ const PurchaseOrderListTable = () => {
|
|||||||
variant='text'
|
variant='text'
|
||||||
color='primary'
|
color='primary'
|
||||||
className='p-0 min-w-0 font-medium normal-case justify-start'
|
className='p-0 min-w-0 font-medium normal-case justify-start'
|
||||||
component={Link}
|
onClick={() => handlePOClick(row.original.id.toString())}
|
||||||
href={getLocalizedUrl(`/apps/purchase/purchase-orders/${row.original.id}/detail`, locale as Locale)}
|
|
||||||
sx={{
|
sx={{
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user