Compare commits
No commits in common. "98d6446b0ce5276fdfa215c1d761a355a24f45f3" and "140822763ed6fd82500031b04c169351530b439e" have entirely different histories.
98d6446b0c
...
140822763e
@ -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>
|
||||||
|
|||||||
@ -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 }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,60 +378,56 @@ const PurchaseOrderListTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto'>
|
||||||
{isLoading ? (
|
<table className={tableStyles.table}>
|
||||||
<Loading />
|
<thead>
|
||||||
) : (
|
{table.getHeaderGroups().map(headerGroup => (
|
||||||
<table className={tableStyles.table}>
|
<tr key={headerGroup.id}>
|
||||||
<thead>
|
{headerGroup.headers.map(header => (
|
||||||
{table.getHeaderGroups().map(headerGroup => (
|
<th key={header.id}>
|
||||||
<tr key={headerGroup.id}>
|
{header.isPlaceholder ? null : (
|
||||||
{headerGroup.headers.map(header => (
|
<>
|
||||||
<th key={header.id}>
|
<div
|
||||||
{header.isPlaceholder ? null : (
|
className={classnames({
|
||||||
<>
|
'flex items-center': header.column.getIsSorted(),
|
||||||
<div
|
'cursor-pointer select-none': header.column.getCanSort()
|
||||||
className={classnames({
|
})}
|
||||||
'flex items-center': header.column.getIsSorted(),
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
'cursor-pointer select-none': header.column.getCanSort()
|
>
|
||||||
})}
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
onClick={header.column.getToggleSortingHandler()}
|
{{
|
||||||
>
|
asc: <i className='tabler-chevron-up text-xl' />,
|
||||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
desc: <i className='tabler-chevron-down text-xl' />
|
||||||
{{
|
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
|
||||||
asc: <i className='tabler-chevron-up text-xl' />,
|
</div>
|
||||||
desc: <i className='tabler-chevron-down text-xl' />
|
</>
|
||||||
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
|
)}
|
||||||
</div>
|
</th>
|
||||||
</>
|
))}
|
||||||
)}
|
</tr>
|
||||||
</th>
|
))}
|
||||||
))}
|
</thead>
|
||||||
</tr>
|
{filteredData.length === 0 ? (
|
||||||
))}
|
<tbody>
|
||||||
</thead>
|
<tr>
|
||||||
{purchaseOrders.length === 0 ? (
|
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
||||||
<tbody>
|
Tidak ada data tersedia
|
||||||
<tr>
|
</td>
|
||||||
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
</tr>
|
||||||
Tidak ada data tersedia
|
</tbody>
|
||||||
</td>
|
) : (
|
||||||
</tr>
|
<tbody>
|
||||||
</tbody>
|
{table.getRowModel().rows.map(row => {
|
||||||
) : (
|
return (
|
||||||
<tbody>
|
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
|
||||||
{table.getRowModel().rows.map(row => {
|
{row.getVisibleCells().map(cell => (
|
||||||
return (
|
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
||||||
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
|
))}
|
||||||
{row.getVisibleCells().map(cell => (
|
</tr>
|
||||||
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
)
|
||||||
))}
|
})}
|
||||||
</tr>
|
</tbody>
|
||||||
)
|
)}
|
||||||
})}
|
</table>
|
||||||
</tbody>
|
|
||||||
)}
|
|
||||||
</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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user