Compare commits

..

No commits in common. "fab31f154063f7c02e64b6476b7cceb761638d91" and "98d6446b0ce5276fdfa215c1d761a355a24f45f3" have entirely different histories.

12 changed files with 367 additions and 488 deletions

View File

@ -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

View File

@ -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`}

View File

@ -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
}
})
}

View File

@ -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

View File

@ -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 {

View File

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

View File

@ -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, ingredients]) }, [currentProductRecipe])
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'

View File

@ -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)}

View File

@ -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 (
<> <Grid container spacing={6}>
{isLoading ? ( <Grid size={{ xs: 12 }}>
<Loading /> <PurchaseDetailInformation />
) : ( </Grid>
<Grid container spacing={6}> <Grid size={{ xs: 12 }}>
<Grid size={{ xs: 12 }}> <PurchaseDetailSendPayment />
<PurchaseDetailInformation data={data} /> </Grid>
</Grid> <Grid size={{ xs: 12 }}>
{data?.status == 'sent' && ( <PurchaseDetailTransaction />
<Grid size={{ xs: 12 }}> </Grid>
<PurchaseDetailSendPayment /> <Grid size={{ xs: 12 }}>
</Grid> <PurchaseDetailLog />
)} </Grid>
{/* <Grid size={{ xs: 12 }}> </Grid>
<PurchaseDetailTransaction />
</Grid>
<Grid size={{ xs: 12 }}>
<PurchaseDetailLog />
</Grid> */}
</Grid>
)}
</>
) )
} }

View File

@ -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' }}> {product.produk}
{item.ingredient.name} </Typography>
</Typography> </TableCell>
</TableCell> <TableCell>{product.deskripsi}</TableCell>
<TableCell>{item.description}</TableCell> <TableCell align='center'>{product.kuantitas}</TableCell>
<TableCell align='center'>{item.quantity}</TableCell> <TableCell align='center'>{product.satuan}</TableCell>
<TableCell align='center'>{item.unit.name}</TableCell> <TableCell align='center'>{product.discount}</TableCell>
<TableCell align='right'>{formatCurrency(item.amount)}</TableCell> <TableCell align='right'>{formatCurrency(product.harga)}</TableCell>
<TableCell align='right'>{formatCurrency(item.amount * item.quantity)}</TableCell> <TableCell align='center'>{product.pajak}</TableCell>
</TableRow> <TableCell align='right'>{formatCurrency(product.jumlah)}</TableCell>
) </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>

View File

@ -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>
) )

View File

@ -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,