Purchase Order table

This commit is contained in:
efrilm 2025-09-13 02:42:39 +07:00
parent d54d623d4c
commit 98d6446b0c
4 changed files with 214 additions and 119 deletions

View File

@ -7,6 +7,14 @@ import Menu from '@mui/material/Menu'
import MenuItem from '@mui/material/MenuItem'
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 }) => ({
textTransform: 'none',
fontWeight: 400,
@ -102,7 +110,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
})
}}
>
{status}
{toTitleCase(status)}
</Button>
))}
</div>
@ -135,7 +143,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
})
}}
>
{status}
{toTitleCase(status)}
</Button>
))}
@ -158,7 +166,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
})
}}
>
{isDropdownItemSelected ? selectedStatus : dropdownLabel}
{isDropdownItemSelected ? toTitleCase(selectedStatus) : dropdownLabel}
</DropdownButton>
<Menu
@ -187,7 +195,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
color: selectedStatus === status ? 'primary.main' : 'text.primary'
}}
>
{status}
{toTitleCase(status)}
</MenuItem>
))}
</Menu>

View File

@ -0,0 +1,41 @@
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

@ -1,3 +1,5 @@
import { Vendor } from './vendor'
export interface PurchaseOrderRequest {
vendor_id: string // uuid.UUID
po_number: string
@ -17,3 +19,77 @@ export interface PurchaseOrderItemRequest {
unit_id: string // uuid.UUID
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

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