Compare commits

..

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

7 changed files with 189 additions and 947 deletions

View File

@ -7,14 +7,6 @@ import Menu from '@mui/material/Menu'
import MenuItem from '@mui/material/MenuItem' import MenuItem from '@mui/material/MenuItem'
import { styled } from '@mui/material/styles' import { styled } from '@mui/material/styles'
function toTitleCase(str: string): string {
return str
.toLowerCase()
.split(/\s+/) // split by spaces
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
const DropdownButton = styled(Button)(({ theme }) => ({ const DropdownButton = styled(Button)(({ theme }) => ({
textTransform: 'none', textTransform: 'none',
fontWeight: 400, fontWeight: 400,
@ -110,7 +102,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
}) })
}} }}
> >
{toTitleCase(status)} {status}
</Button> </Button>
))} ))}
</div> </div>
@ -143,7 +135,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
}) })
}} }}
> >
{toTitleCase(status)} {status}
</Button> </Button>
))} ))}
@ -166,7 +158,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
}) })
}} }}
> >
{isDropdownItemSelected ? toTitleCase(selectedStatus) : dropdownLabel} {isDropdownItemSelected ? selectedStatus : dropdownLabel}
</DropdownButton> </DropdownButton>
<Menu <Menu
@ -195,7 +187,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
color: selectedStatus === status ? 'primary.main' : 'text.primary' color: selectedStatus === status ? 'primary.main' : 'text.primary'
}} }}
> >
{toTitleCase(status)} {status}
</MenuItem> </MenuItem>
))} ))}
</Menu> </Menu>

View File

@ -3,22 +3,20 @@ import { toast } from 'react-toastify'
import { api } from '../api' import { api } from '../api'
import { PurchaseOrderRequest } from '@/types/services/purchaseOrder' import { PurchaseOrderRequest } from '@/types/services/purchaseOrder'
export const usePurchaseOrdersMutation = () => { export const useVendorsMutation = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const createPurchaseOrder = useMutation({ const createVendor = useMutation({
mutationFn: async (newPurchaseOrder: PurchaseOrderRequest) => { mutationFn: async (newVendor: PurchaseOrderRequest) => {
const response = await api.post('/purchase-orders', newPurchaseOrder) const response = await api.post('/vendors', newVendor)
return response.data return response.data
}, },
onSuccess: () => { onSuccess: () => {
toast.success('Purchase Order created successfully!') toast.success('Vendor created successfully!')
queryClient.invalidateQueries({ queryKey: ['purchase-orders'] }) queryClient.invalidateQueries({ queryKey: ['vendors'] })
}, },
onError: (error: any) => { onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed') toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
} }
}) })
return { createPurchaseOrder }
} }

View File

@ -1,41 +0,0 @@
import { PurchaseOrders } from '@/types/services/purchaseOrder'
import { useQuery } from '@tanstack/react-query'
import { api } from '../api'
interface PurchaseOrderQueryParams {
page?: number
limit?: number
search?: string
status?: string
}
export function usePurchaseOrders(params: PurchaseOrderQueryParams = {}) {
const { page = 1, limit = 10, search = '', status = '', ...filters } = params
return useQuery<PurchaseOrders>({
queryKey: ['purchase-orders', { page, limit, search, status, ...filters }],
queryFn: async () => {
const queryParams = new URLSearchParams()
queryParams.append('page', page.toString())
queryParams.append('limit', limit.toString())
if (search) {
queryParams.append('search', search)
}
if (status) {
queryParams.append('status', status)
}
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
queryParams.append(key, value.toString())
}
})
const res = await api.get(`/purchase-orders?${queryParams.toString()}`)
return res.data.data
}
})
}

View File

@ -16,7 +16,10 @@ export interface IngredientItem {
deskripsi: string deskripsi: string
kuantitas: number kuantitas: number
satuan: { label: string; value: string } | null satuan: { label: string; value: string } | null
discount: string
harga: number harga: number
pajak: { label: string; value: string } | null
waste: { label: string; value: string } | null
total: number total: number
} }

View File

@ -1,5 +1,3 @@
import { Vendor } from './vendor'
export interface PurchaseOrderRequest { export interface PurchaseOrderRequest {
vendor_id: string // uuid.UUID vendor_id: string // uuid.UUID
po_number: string po_number: string
@ -19,77 +17,3 @@ export interface PurchaseOrderItemRequest {
unit_id: string // uuid.UUID unit_id: string // uuid.UUID
amount: number amount: number
} }
export interface PurchaseOrders {
purchase_orders: PurchaseOrder[]
total_count: number
page: number
limit: number
total_pages: number
}
export interface PurchaseOrder {
id: string
organization_id: string
vendor_id: string
po_number: string
transaction_date: string // RFC3339
due_date: string // RFC3339
reference: string | null
status: string
message: string | null
total_amount: number
created_at: string
updated_at: string
vendor: Vendor
items: PurchaseOrderItem[]
attachments: PurchaseOrderAttachment[]
}
export interface PurchaseOrderItem {
id: string
purchase_order_id: string
ingredient_id: string
description: string
quantity: number
unit_id: string
amount: number
created_at: string
updated_at: string
ingredient: PurchaseOrderIngredient
unit: PurchaseOrderUnit
}
export interface PurchaseOrderIngredient {
id: string
name: string
}
export interface PurchaseOrderUnit {
id: string
name: string
}
export interface PurchaseOrderAttachment {
id: string
purchase_order_id: string
file_id: string
created_at: string
file: PurchaseOrderFile
}
export interface PurchaseOrderFile {
id: string
organization_id: string
user_id: string
file_name: string
original_name: string
file_url: string
file_size: number
mime_type: string
file_type: string
upload_path: string
is_public: boolean
created_at: string
updated_at: string
}

View File

@ -1,178 +1,52 @@
'use client' 'use client'
import React, { useState, useMemo } from 'react' import React, { useState } from 'react'
import { import { Card, CardContent } from '@mui/material'
Card,
CardContent,
Button,
Box,
Typography,
IconButton,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
CircularProgress
} from '@mui/material'
import Grid from '@mui/material/Grid2' import Grid from '@mui/material/Grid2'
import CustomAutocomplete from '@/@core/components/mui/Autocomplete' import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
import CustomTextField from '@/@core/components/mui/TextField' import PurchaseBasicInfo from './PurchaseBasicInfo'
import ImageUpload from '@/components/ImageUpload' import PurchaseIngredientsTable from './PurchaseIngredientsTable'
import { DropdownOption } from '@/types/apps/purchaseOrderTypes' import PurchaseSummary from './PurchaseSummary'
import { useVendorActive } from '@/services/queries/vendor'
import { useIngredients } from '@/services/queries/ingredients'
import { useUnits } from '@/services/queries/units'
import { useFilesMutation } from '@/services/mutations/files'
import { usePurchaseOrdersMutation } from '@/services/mutations/purchaseOrder'
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 = {
id: string
name: string
// Add other unit properties as needed
}
// Internal form state interface for UI management
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[]
}
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
}
const PurchaseAddForm: React.FC = () => { const PurchaseAddForm: React.FC = () => {
const [imageUrl, setImageUrl] = useState<string>('')
const [formData, setFormData] = useState<PurchaseOrderFormData>({ const [formData, setFormData] = useState<PurchaseOrderFormData>({
vendor: null, vendor: null,
po_number: '', nomor: 'PO/00043',
transaction_date: '', tglTransaksi: '2025-09-09',
due_date: '', tglJatuhTempo: '2025-09-10',
reference: '', referensi: '',
status: 'draft', termin: null,
hargaTermasukPajak: true,
// Shipping info
showShippingInfo: false,
tanggalPengiriman: '',
ekspedisi: null,
noResi: '',
// Bottom section toggles // Bottom section toggles
showPesan: false, showPesan: false,
showAttachment: false, showAttachment: false,
message: '', showTambahDiskon: false,
// Items showBiayaPengiriman: false,
items: [ showBiayaTransaksi: false,
showUangMuka: false,
pesan: '',
// Ingredient items (updated from productItems)
ingredientItems: [
{ {
id: 1, id: 1,
ingredient: null, ingredient: null,
description: '', deskripsi: '',
quantity: 1, kuantitas: 1,
unit: null, satuan: null,
amount: 0, discount: '0',
harga: 0,
pajak: null,
waste: null,
total: 0 total: 0
} }
], ]
attachment_file_ids: []
}) })
// API Hooks
const { data: vendors, isLoading: isLoadingVendors } = useVendorActive()
const { data: ingredients, isLoading: isLoadingIngredients } = useIngredients()
const { data: units, isLoading: isLoadingUnits } = useUnits({
page: 1,
limit: 50
})
const { mutate, isPending } = useFilesMutation().uploadFile
const { createPurchaseOrder } = usePurchaseOrdersMutation()
// Transform vendors data to dropdown options
const vendorOptions: DropdownOption[] = useMemo(() => {
return (
vendors?.map(vendor => ({
label: vendor.name,
value: vendor.id
})) || []
)
}, [vendors])
// Transform ingredients data to autocomplete options format
const ingredientOptions = useMemo(() => {
if (!ingredients || isLoadingIngredients) {
return []
}
return ingredients?.data.map((ingredient: IngredientItem) => ({
label: ingredient.name,
value: ingredient.id,
id: ingredient.id,
originalData: ingredient // This includes the full IngredientItem with unit, cost, etc.
}))
}, [ingredients, isLoadingIngredients])
// Transform units data to dropdown options
const unitOptions = useMemo(() => {
if (!units || isLoadingUnits) {
return []
}
return (
units?.data?.map((unit: any) => ({
label: unit.name || unit.nama || unit.unit_name,
value: unit.id || unit.code || unit.value
})) || []
)
}, [units, isLoadingUnits])
// Handler Functions
const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => { const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => {
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
@ -180,602 +54,64 @@ const PurchaseAddForm: React.FC = () => {
})) }))
} }
const handleItemChange = (index: number, field: keyof PurchaseOrderFormItem, value: any): void => { const handleIngredientChange = (index: number, field: keyof IngredientItem, value: any): void => {
setFormData(prev => { setFormData(prev => {
const newItems = [...prev.items] const newItems = [...prev.ingredientItems]
newItems[index] = { ...newItems[index], [field]: value } newItems[index] = { ...newItems[index], [field]: value }
// Auto-calculate total if amount or quantity changes // Auto-calculate total if price or quantity changes
if (field === 'amount' || field === 'quantity') { if (field === 'harga' || field === 'kuantitas') {
const item = newItems[index] const item = newItems[index]
item.total = item.amount * item.quantity item.total = item.harga * item.kuantitas
} }
return { ...prev, items: newItems } return { ...prev, ingredientItems: newItems }
}) })
} }
const handleIngredientSelection = (index: number, selectedIngredient: any) => { const addIngredientItem = (): void => {
handleItemChange(index, 'ingredient', selectedIngredient) const newItem: IngredientItem = {
// Auto-populate related fields if available in the ingredient data
if (selectedIngredient) {
const ingredientData: IngredientItem = selectedIngredient.originalData || selectedIngredient
// Auto-fill unit based on IngredientItem structure
if (ingredientData.unit_id || ingredientData.unit) {
let unitToFind = null
// If ingredient has unit object (populated relation)
if (ingredientData.unit && typeof ingredientData.unit === 'object') {
unitToFind = ingredientData.unit
}
// If ingredient has unit_id, find the unit from unitOptions
else if (ingredientData.unit_id) {
unitToFind = unitOptions.find(option => option.value === ingredientData.unit_id)
}
if (unitToFind) {
// Create unit option object
const unitOption = {
label: (unitToFind as any).label || (unitToFind as any).name || (unitToFind as any).unit_name,
value: (unitToFind as any).value || ingredientData.unit_id
}
handleItemChange(index, 'unit', unitOption)
}
}
// Auto-fill amount with cost from IngredientItem
if (ingredientData.cost !== undefined && ingredientData.cost !== null) {
handleItemChange(index, 'amount', ingredientData.cost)
}
// Auto-fill description with ingredient name
if (ingredientData.name) {
handleItemChange(index, 'description', ingredientData.name)
}
}
}
const addItem = (): void => {
const newItem: PurchaseOrderFormItem = {
id: Date.now(), id: Date.now(),
ingredient: null, ingredient: null,
description: '', deskripsi: '',
quantity: 1, kuantitas: 1,
unit: null, satuan: null,
amount: 0, discount: '0%',
harga: 0,
pajak: null,
waste: null,
total: 0 total: 0
} }
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
items: [...prev.items, newItem] ingredientItems: [...prev.ingredientItems, newItem]
})) }))
} }
const removeItem = (index: number): void => { const removeIngredientItem = (index: number): void => {
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
items: prev.items.filter((_, i) => i !== index) ingredientItems: prev.ingredientItems.filter((_, i) => i !== index)
})) }))
} }
// Function to get selected vendor data
const getSelectedVendorData = () => {
if (!formData.vendor?.value || !vendors) return null
const selectedVendor = vendors.find(vendor => vendor.id === (formData?.vendor?.value ?? ''))
return selectedVendor
}
const upsertAttachment = (attachments: string[], newId: string, index = 0) => {
if (attachments.length === 0) {
return [newId]
}
return attachments.map((id, i) => (i === index ? newId : id))
}
const handleUpload = async (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const formData = new FormData()
formData.append('file', file)
formData.append('file_type', 'image')
formData.append('description', 'Purchase image')
mutate(formData, {
onSuccess: data => {
// pemakaian:
setFormData(prev => ({
...prev,
attachment_file_ids: upsertAttachment(prev.attachment_file_ids, data.id)
}))
setImageUrl(data.file_url)
resolve(data.id) // <-- balikin id file yang berhasil diupload
},
onError: 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)
// Convert form data to API request format
const convertToApiRequest = (): PurchaseOrderRequest => {
return {
vendor_id: formData.vendor?.value || '',
po_number: formData.po_number,
transaction_date: formData.transaction_date,
due_date: formData.due_date,
reference: formData.reference || undefined,
status: formData.status,
message: formData.message || undefined,
items: formData.items
.filter(item => item.ingredient && item.unit) // Only include valid items
.map(item => ({
ingredient_id: item.ingredient!.value,
description: item.description || undefined,
quantity: item.quantity,
unit_id: item.unit!.value,
amount: item.amount
})),
attachment_file_ids: formData.attachment_file_ids.length > 0 ? formData.attachment_file_ids : undefined
}
}
const handleSave = () => {
createPurchaseOrder.mutate(convertToApiRequest(), {
onSuccess: () => {
window.history.back()
}
})
}
return ( return (
<Card> <Card>
<CardContent> <CardContent>
<Grid container spacing={3}> <Grid container spacing={3}>
{/* BASIC INFO SECTION */} {/* Basic Info Section */}
{/* Row 1 - Vendor and PO Number */} <PurchaseBasicInfo formData={formData} handleInputChange={handleInputChange} />
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
<CustomAutocomplete
fullWidth
options={vendorOptions}
value={formData.vendor}
onChange={(event, newValue) => {
handleInputChange('vendor', newValue)
if (newValue?.value) {
const selectedVendorData = vendors?.find(vendor => vendor.id === newValue.value)
console.log('Vendor selected:', selectedVendorData)
}
}}
loading={isLoadingVendors}
renderInput={params => (
<CustomTextField
{...params}
label='Vendor'
placeholder={isLoadingVendors ? 'Memuat vendor...' : 'Pilih kontak'}
fullWidth
/>
)}
/>
{getSelectedVendorData() && (
<Box className='space-y-1 mt-3'>
<Box className='flex items-center space-x-2'>
<i className='tabler-user text-gray-500 w-3 h-3' />
<Typography className='text-gray-700 font-medium text-xs'>
{getSelectedVendorData()?.contact_person ?? ''}
</Typography>
</Box>
<Box className='flex items-start space-x-2'>
<i className='tabler-map text-gray-500 w-3 h-3' />
<Typography className='text-gray-700 font-medium text-xs'>
{getSelectedVendorData()?.address ?? '-'}
</Typography>
</Box>
<Box className='flex items-center space-x-2'>
<i className='tabler-phone text-gray-500 w-3 h-3' />
<Typography className='text-gray-700 font-medium text-xs'>
{getSelectedVendorData()?.phone_number ?? '-'}
</Typography>
</Box>
</Box>
)}
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
<CustomTextField
fullWidth
label='PO Number'
value={formData.po_number}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('po_number', e.target.value)}
/>
</Grid>
{/* Row 2 - Transaction Date, Due Date, Status */} {/* Ingredients Table Section */}
<Grid size={{ xs: 12, sm: 4, md: 4 }}> <PurchaseIngredientsTable
<CustomTextField formData={formData}
fullWidth handleIngredientChange={handleIngredientChange}
label='Transaction Date' addIngredientItem={addIngredientItem}
type='date' removeIngredientItem={removeIngredientItem}
value={formData.transaction_date}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
handleInputChange('transaction_date', e.target.value)
}
InputLabelProps={{
shrink: true
}}
/> />
</Grid>
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
<CustomTextField
fullWidth
label='Due Date'
type='date'
value={formData.due_date}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('due_date', e.target.value)}
InputLabelProps={{
shrink: true
}}
/>
</Grid>
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
<CustomTextField
fullWidth
label='Reference'
placeholder='Reference'
value={formData.reference}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('reference', e.target.value)}
/>
</Grid>
{/* ITEMS TABLE SECTION */} {/* Summary Section */}
<Grid size={{ xs: 12 }} sx={{ mt: 4 }}> <PurchaseSummary formData={formData} handleInputChange={handleInputChange} />
<Typography variant='h6' sx={{ mb: 2, fontWeight: 600 }}>
Purchase Order Items
</Typography>
<TableContainer component={Paper} variant='outlined'>
<Table>
<TableHead>
<TableRow sx={{ backgroundColor: 'grey.50' }}>
<TableCell sx={{ fontWeight: 'bold', minWidth: 180 }}>Ingredient</TableCell>
<TableCell sx={{ fontWeight: 'bold', minWidth: 150 }}>Description</TableCell>
<TableCell sx={{ fontWeight: 'bold', width: 100 }}>Quantity</TableCell>
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Unit</TableCell>
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Amount</TableCell>
<TableCell sx={{ fontWeight: 'bold', width: 100, textAlign: 'right' }}>Total</TableCell>
<TableCell sx={{ width: 50 }}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{formData.items.map((item: PurchaseOrderFormItem, index: number) => (
<TableRow key={item.id}>
<TableCell>
<CustomAutocomplete
size='small'
options={ingredientOptions}
value={item.ingredient || null}
onChange={(event, newValue) => handleIngredientSelection(index, newValue)}
loading={isLoadingIngredients}
getOptionLabel={(option: any) => {
if (!option) return ''
return option.label || option.name || option.nama || ''
}}
isOptionEqualToValue={(option: any, value: any) => {
if (!option || !value) return false
const optionId = option.value || option.id
const valueId = value.value || value.id
return optionId === valueId
}}
renderInput={params => (
<CustomTextField
{...params}
placeholder={isLoadingIngredients ? 'Loading ingredients...' : 'Select Ingredient'}
InputProps={{
...params.InputProps,
endAdornment: (
<>
{isLoadingIngredients ? <CircularProgress color='inherit' size={20} /> : null}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
disabled={isLoadingIngredients}
/>
</TableCell>
<TableCell>
<CustomTextField
fullWidth
size='small'
value={item.description}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
handleItemChange(index, 'description', e.target.value)
}
placeholder='Description'
/>
</TableCell>
<TableCell>
<CustomTextField
fullWidth
size='small'
type='number'
value={item.quantity}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
handleItemChange(index, 'quantity', parseInt(e.target.value) || 1)
}
inputProps={{ min: 1 }}
/>
</TableCell>
<TableCell>
<CustomAutocomplete
size='small'
options={unitOptions}
value={item.unit}
onChange={(event, newValue) => handleItemChange(index, 'unit', newValue)}
loading={isLoadingUnits}
getOptionLabel={(option: any) => {
if (!option) return ''
return option.label || option.name || option.nama || ''
}}
renderInput={params => (
<CustomTextField
{...params}
placeholder={isLoadingUnits ? 'Loading units...' : 'Select...'}
InputProps={{
...params.InputProps,
endAdornment: (
<>
{isLoadingUnits ? <CircularProgress color='inherit' size={20} /> : null}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
disabled={isLoadingUnits}
/>
</TableCell>
<TableCell>
<CustomTextField
fullWidth
size='small'
type='number'
value={item.amount === 0 ? '' : item.amount?.toString() || ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
if (value === '') {
handleItemChange(index, 'amount', 0)
return
}
const numericValue = parseFloat(value)
handleItemChange(index, 'amount', isNaN(numericValue) ? 0 : numericValue)
}}
inputProps={{ min: 0, step: 'any' }}
placeholder='0'
/>
</TableCell>
<TableCell>
<CustomTextField
fullWidth
size='small'
value={item.total}
InputProps={{ readOnly: true }}
sx={{
'& .MuiInputBase-input': {
textAlign: 'right'
}
}}
/>
</TableCell>
<TableCell>
<IconButton
size='small'
color='error'
onClick={() => removeItem(index)}
disabled={formData.items.length === 1}
>
<i className='tabler-trash' />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{/* Add New Item Button */}
<Button
startIcon={<i className='tabler-plus' />}
onClick={addItem}
variant='outlined'
size='small'
sx={{ mt: 1 }}
disabled={isLoadingIngredients || isLoadingUnits}
>
Add Item
</Button>
</Grid>
{/* SUMMARY SECTION */}
<Grid size={12} sx={{ mt: 4 }}>
<Grid container spacing={3}>
{/* Left Side - Message and Attachment */}
<Grid size={{ xs: 12, md: 7 }}>
{/* Message Section */}
<Box sx={{ mb: 3 }}>
<Button
variant='text'
color='inherit'
onClick={() => handleInputChange('showPesan', !formData.showPesan)}
sx={{
textTransform: 'none',
fontSize: '14px',
fontWeight: 500,
padding: '12px 16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
width: '100%',
backgroundColor: '#f5f5f5',
border: '1px solid #e0e0e0',
borderRadius: '4px',
color: 'text.primary',
'&:hover': {
backgroundColor: '#eeeeee'
}
}}
>
<Box component='span' sx={{ mr: 1 }}>
{formData.showPesan ? (
<i className='tabler-chevron-down w-4 h-4' />
) : (
<i className='tabler-chevron-right w-4 h-4' />
)}
</Box>
Message
</Button>
{formData.showPesan && (
<Box sx={{ mt: 2 }}>
<CustomTextField
fullWidth
multiline
rows={3}
placeholder='Add message...'
value={formData.message || ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
handleInputChange('message', e.target.value)
}
/>
</Box>
)}
</Box>
{/* Attachment Section */}
<Box>
<Button
variant='text'
color='inherit'
onClick={() => handleInputChange('showAttachment', !formData.showAttachment)}
sx={{
textTransform: 'none',
fontSize: '14px',
fontWeight: 500,
padding: '12px 16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
width: '100%',
backgroundColor: '#f5f5f5',
border: '1px solid #e0e0e0',
borderRadius: '4px',
color: 'text.primary',
'&:hover': {
backgroundColor: '#eeeeee'
}
}}
>
<Box component='span' sx={{ mr: 1 }}>
{formData.showAttachment ? (
<i className='tabler-chevron-down w-4 h-4' />
) : (
<i className='tabler-chevron-right w-4 h-4' />
)}
</Box>
Attachment
</Button>
{formData.showAttachment && (
<ImageUpload
onUpload={handleUpload}
maxFileSize={1 * 1024 * 1024}
showUrlOption={false}
currentImageUrl={imageUrl}
dragDropText='Drop your image here'
browseButtonText='Choose Image'
/>
)}
</Box>
</Grid>
{/* Right Side - Totals */}
<Grid size={{ xs: 12, md: 5 }}>
<Box sx={{ backgroundColor: '#ffffff', p: 3, borderRadius: '8px' }}>
{/* Sub Total */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
py: 2,
borderBottom: '1px solid #e0e0e0',
'&:hover': {
backgroundColor: '#f8f8f8'
}
}}
>
<Typography variant='body1' color='text.secondary' sx={{ fontSize: '16px' }}>
Sub Total
</Typography>
<Typography variant='body1' fontWeight={600} sx={{ fontSize: '16px', textAlign: 'right' }}>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(subtotal)}
</Typography>
</Box>
{/* Total */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
py: 2,
borderBottom: '1px solid #e0e0e0',
'&:hover': {
backgroundColor: '#f8f8f8'
}
}}
>
<Typography variant='h6' fontWeight={600} sx={{ fontSize: '18px' }}>
Total
</Typography>
<Typography variant='h6' fontWeight={600} sx={{ fontSize: '18px', textAlign: 'right' }}>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(subtotal)}
</Typography>
</Box>
{/* Save Button */}
<Button
variant='contained'
color='primary'
fullWidth
onClick={handleSave}
sx={{
textTransform: 'none',
fontWeight: 600,
py: 1.5,
mt: 3,
boxShadow: 'none',
'&:hover': {
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}
}}
>
Save
</Button>
</Box>
</Grid>
</Grid>
</Grid>
</Grid> </Grid>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -42,9 +42,6 @@ import Loading from '@/components/layout/shared/Loading'
import { PurchaseOrderType } from '@/types/apps/purchaseOrderTypes' import { PurchaseOrderType } from '@/types/apps/purchaseOrderTypes'
import { purchaseOrdersData } from '@/data/dummy/purchase-order' import { purchaseOrdersData } from '@/data/dummy/purchase-order'
import { getLocalizedUrl } from '@/utils/i18n' import { getLocalizedUrl } from '@/utils/i18n'
import { PurchaseOrder } from '@/types/services/purchaseOrder'
import { usePurchaseOrders } from '@/services/queries/purchaseOrder'
import StatusFilterTabs from '@/components/StatusFilterTab'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -55,7 +52,7 @@ declare module '@tanstack/table-core' {
} }
} }
type PurchaseOrderTypeWithAction = PurchaseOrder & { type PurchaseOrderTypeWithAction = PurchaseOrderType & {
actions?: string actions?: string
} }
@ -138,24 +135,46 @@ const PurchaseOrderListTable = () => {
// States // States
const [addPOOpen, setAddPOOpen] = useState(false) const [addPOOpen, setAddPOOpen] = useState(false)
const [rowSelection, setRowSelection] = useState({}) const [rowSelection, setRowSelection] = useState({})
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(0)
const [pageSize, setPageSize] = useState(10) const [pageSize, setPageSize] = useState(10)
const [openConfirm, setOpenConfirm] = useState(false) const [openConfirm, setOpenConfirm] = useState(false)
const [poId, setPOId] = useState('') const [poId, setPOId] = useState('')
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState<string>('Semua') const [statusFilter, setStatusFilter] = useState<string>('Semua')
const [filteredData, setFilteredData] = useState<PurchaseOrderType[]>(purchaseOrdersData)
// Hooks // Hooks
const { lang: locale } = useParams() const { lang: locale } = useParams()
const { data, isLoading, error, isFetching } = usePurchaseOrders({ // Filter data based on search and status
page: currentPage, useEffect(() => {
limit: pageSize, let filtered = purchaseOrdersData
search,
status: statusFilter === 'Semua' ? '' : statusFilter
})
const purchaseOrders = data?.purchase_orders ?? [] // Filter by search
const totalCount = data?.total_count ?? 0 if (search) {
filtered = filtered.filter(
po =>
po.number.toLowerCase().includes(search.toLowerCase()) ||
po.vendorName.toLowerCase().includes(search.toLowerCase()) ||
po.vendorCompany.toLowerCase().includes(search.toLowerCase()) ||
po.status.toLowerCase().includes(search.toLowerCase())
)
}
// Filter by status
if (statusFilter !== 'Semua') {
filtered = filtered.filter(po => po.status === statusFilter)
}
setFilteredData(filtered)
setCurrentPage(0)
}, [search, statusFilter])
const totalCount = filteredData.length
const paginatedData = useMemo(() => {
const startIndex = currentPage * pageSize
return filteredData.slice(startIndex, startIndex + pageSize)
}, [filteredData, currentPage, pageSize])
const handlePageChange = useCallback((event: unknown, newPage: number) => { const handlePageChange = useCallback((event: unknown, newPage: number) => {
setCurrentPage(newPage) setCurrentPage(newPage)
@ -203,7 +222,7 @@ const PurchaseOrderListTable = () => {
/> />
) )
}, },
columnHelper.accessor('po_number', { columnHelper.accessor('number', {
header: 'Nomor PO', header: 'Nomor PO',
cell: ({ row }) => ( cell: ({ row }) => (
<Button <Button
@ -220,19 +239,19 @@ const PurchaseOrderListTable = () => {
} }
}} }}
> >
{row.original.po_number} {row.original.number}
</Button> </Button>
) )
}), }),
columnHelper.accessor('vendor.name', { columnHelper.accessor('vendorName', {
header: 'Vendor', header: 'Vendor',
cell: ({ row }) => ( cell: ({ row }) => (
<div className='flex flex-col'> <div className='flex flex-col'>
<Typography color='text.primary' className='font-medium'> <Typography color='text.primary' className='font-medium'>
{row.original.vendor.contact_person} {row.original.vendorName}
</Typography> </Typography>
<Typography variant='body2' color='text.secondary'> <Typography variant='body2' color='text.secondary'>
{row.original.vendor.name} {row.original.vendorCompany}
</Typography> </Typography>
</div> </div>
) )
@ -241,13 +260,13 @@ const PurchaseOrderListTable = () => {
header: 'Referensi', header: 'Referensi',
cell: ({ row }) => <Typography color='text.secondary'>{row.original.reference || '-'}</Typography> cell: ({ row }) => <Typography color='text.secondary'>{row.original.reference || '-'}</Typography>
}), }),
columnHelper.accessor('transaction_date', { columnHelper.accessor('date', {
header: 'Tanggal', header: 'Tanggal',
cell: ({ row }) => <Typography>{row.original.transaction_date}</Typography> cell: ({ row }) => <Typography>{row.original.date}</Typography>
}), }),
columnHelper.accessor('due_date', { columnHelper.accessor('dueDate', {
header: 'Tanggal Jatuh Tempo', header: 'Tanggal Jatuh Tempo',
cell: ({ row }) => <Typography>{row.original.due_date}</Typography> cell: ({ row }) => <Typography>{row.original.dueDate}</Typography>
}), }),
columnHelper.accessor('status', { columnHelper.accessor('status', {
header: 'Status', header: 'Status',
@ -263,16 +282,16 @@ const PurchaseOrderListTable = () => {
</div> </div>
) )
}), }),
columnHelper.accessor('total_amount', { columnHelper.accessor('total', {
header: 'Total', header: 'Total',
cell: ({ row }) => <Typography className='font-medium'>{formatCurrency(row.original.total_amount)}</Typography> cell: ({ row }) => <Typography className='font-medium'>{formatCurrency(row.original.total)}</Typography>
}) })
], ],
[] []
) )
const table = useReactTable({ const table = useReactTable({
data: purchaseOrders as PurchaseOrder[], data: paginatedData as PurchaseOrderType[],
columns, columns,
filterFns: { filterFns: {
fuzzy: fuzzyFilter fuzzy: fuzzyFilter
@ -297,11 +316,27 @@ const PurchaseOrderListTable = () => {
{/* Filter Status Tabs */} {/* Filter Status Tabs */}
<div className='p-6 border-bs'> <div className='p-6 border-bs'>
<div className='flex flex-wrap gap-2'> <div className='flex flex-wrap gap-2'>
<StatusFilterTabs {['Semua', 'Draft', 'Disetujui', 'Dikirim Sebagian', 'Selesai', 'Lainnya'].map(status => (
statusOptions={['Semua', 'draft', 'sent', 'approved', 'received', 'cancelled']} <Button
selectedStatus={statusFilter} key={status}
onStatusChange={handleStatusFilter} variant={statusFilter === status ? 'contained' : 'outlined'}
/> color={statusFilter === status ? 'primary' : 'inherit'}
onClick={() => handleStatusFilter(status)}
size='small'
className='rounded-lg'
sx={{
textTransform: 'none',
fontWeight: statusFilter === status ? 600 : 400,
borderRadius: '8px',
...(statusFilter !== status && {
borderColor: '#e0e0e0',
color: '#666'
})
}}
>
{status}
</Button>
))}
</div> </div>
</div> </div>
@ -343,9 +378,6 @@ const PurchaseOrderListTable = () => {
</div> </div>
</div> </div>
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
{isLoading ? (
<Loading />
) : (
<table className={tableStyles.table}> <table className={tableStyles.table}>
<thead> <thead>
{table.getHeaderGroups().map(headerGroup => ( {table.getHeaderGroups().map(headerGroup => (
@ -374,7 +406,7 @@ const PurchaseOrderListTable = () => {
</tr> </tr>
))} ))}
</thead> </thead>
{purchaseOrders.length === 0 ? ( {filteredData.length === 0 ? (
<tbody> <tbody>
<tr> <tr>
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'> <td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
@ -396,7 +428,6 @@ const PurchaseOrderListTable = () => {
</tbody> </tbody>
)} )}
</table> </table>
)}
</div> </div>
<TablePagination <TablePagination
@ -414,7 +445,6 @@ const PurchaseOrderListTable = () => {
onPageChange={handlePageChange} onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange} onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50]} rowsPerPageOptions={[10, 25, 50]}
disabled={isLoading}
/> />
</Card> </Card>
</> </>