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 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,
@ -102,7 +110,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
}) })
}} }}
> >
{status} {toTitleCase(status)}
</Button> </Button>
))} ))}
</div> </div>
@ -135,7 +143,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
}) })
}} }}
> >
{status} {toTitleCase(status)}
</Button> </Button>
))} ))}
@ -158,7 +166,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
}) })
}} }}
> >
{isDropdownItemSelected ? selectedStatus : dropdownLabel} {isDropdownItemSelected ? toTitleCase(selectedStatus) : dropdownLabel}
</DropdownButton> </DropdownButton>
<Menu <Menu
@ -187,7 +195,7 @@ const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
color: selectedStatus === status ? 'primary.main' : 'text.primary' color: selectedStatus === status ? 'primary.main' : 'text.primary'
}} }}
> >
{status} {toTitleCase(status)}
</MenuItem> </MenuItem>
))} ))}
</Menu> </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 { export interface PurchaseOrderRequest {
vendor_id: string // uuid.UUID vendor_id: string // uuid.UUID
po_number: string po_number: string
@ -17,3 +19,77 @@ 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

@ -42,6 +42,9 @@ 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 {
@ -52,7 +55,7 @@ declare module '@tanstack/table-core' {
} }
} }
type PurchaseOrderTypeWithAction = PurchaseOrderType & { type PurchaseOrderTypeWithAction = PurchaseOrder & {
actions?: string actions?: string
} }
@ -135,46 +138,24 @@ 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(0) const [currentPage, setCurrentPage] = useState(1)
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()
// Filter data based on search and status const { data, isLoading, error, isFetching } = usePurchaseOrders({
useEffect(() => { page: currentPage,
let filtered = purchaseOrdersData limit: pageSize,
search,
status: statusFilter === 'Semua' ? '' : statusFilter
})
// Filter by search const purchaseOrders = data?.purchase_orders ?? []
if (search) { const totalCount = data?.total_count ?? 0
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)
@ -222,7 +203,7 @@ const PurchaseOrderListTable = () => {
/> />
) )
}, },
columnHelper.accessor('number', { columnHelper.accessor('po_number', {
header: 'Nomor PO', header: 'Nomor PO',
cell: ({ row }) => ( cell: ({ row }) => (
<Button <Button
@ -239,19 +220,19 @@ const PurchaseOrderListTable = () => {
} }
}} }}
> >
{row.original.number} {row.original.po_number}
</Button> </Button>
) )
}), }),
columnHelper.accessor('vendorName', { columnHelper.accessor('vendor.name', {
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.vendorName} {row.original.vendor.contact_person}
</Typography> </Typography>
<Typography variant='body2' color='text.secondary'> <Typography variant='body2' color='text.secondary'>
{row.original.vendorCompany} {row.original.vendor.name}
</Typography> </Typography>
</div> </div>
) )
@ -260,13 +241,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('date', { columnHelper.accessor('transaction_date', {
header: 'Tanggal', 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', header: 'Tanggal Jatuh Tempo',
cell: ({ row }) => <Typography>{row.original.dueDate}</Typography> cell: ({ row }) => <Typography>{row.original.due_date}</Typography>
}), }),
columnHelper.accessor('status', { columnHelper.accessor('status', {
header: 'Status', header: 'Status',
@ -282,16 +263,16 @@ const PurchaseOrderListTable = () => {
</div> </div>
) )
}), }),
columnHelper.accessor('total', { columnHelper.accessor('total_amount', {
header: 'Total', 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({ const table = useReactTable({
data: paginatedData as PurchaseOrderType[], data: purchaseOrders as PurchaseOrder[],
columns, columns,
filterFns: { filterFns: {
fuzzy: fuzzyFilter fuzzy: fuzzyFilter
@ -316,27 +297,11 @@ 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'>
{['Semua', 'Draft', 'Disetujui', 'Dikirim Sebagian', 'Selesai', 'Lainnya'].map(status => ( <StatusFilterTabs
<Button statusOptions={['Semua', 'draft', 'sent', 'approved', 'received', 'cancelled']}
key={status} selectedStatus={statusFilter}
variant={statusFilter === status ? 'contained' : 'outlined'} onStatusChange={handleStatusFilter}
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>
@ -378,56 +343,60 @@ const PurchaseOrderListTable = () => {
</div> </div>
</div> </div>
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
<table className={tableStyles.table}> {isLoading ? (
<thead> <Loading />
{table.getHeaderGroups().map(headerGroup => ( ) : (
<tr key={headerGroup.id}> <table className={tableStyles.table}>
{headerGroup.headers.map(header => ( <thead>
<th key={header.id}> {table.getHeaderGroups().map(headerGroup => (
{header.isPlaceholder ? null : ( <tr key={headerGroup.id}>
<> {headerGroup.headers.map(header => (
<div <th key={header.id}>
className={classnames({ {header.isPlaceholder ? null : (
'flex items-center': header.column.getIsSorted(), <>
'cursor-pointer select-none': header.column.getCanSort() <div
})} className={classnames({
onClick={header.column.getToggleSortingHandler()} 'flex items-center': header.column.getIsSorted(),
> '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' />, >
desc: <i className='tabler-chevron-down text-xl' /> {flexRender(header.column.columnDef.header, header.getContext())}
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} {{
</div> asc: <i className='tabler-chevron-up text-xl' />,
</> desc: <i className='tabler-chevron-down text-xl' />
)} }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
</th> </div>
))} </>
</tr> )}
))} </th>
</thead> ))}
{filteredData.length === 0 ? ( </tr>
<tbody> ))}
<tr> </thead>
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'> {purchaseOrders.length === 0 ? (
Tidak ada data tersedia <tbody>
</td> <tr>
</tr> <td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
</tbody> Tidak ada data tersedia
) : ( </td>
<tbody> </tr>
{table.getRowModel().rows.map(row => { </tbody>
return ( ) : (
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}> <tbody>
{row.getVisibleCells().map(cell => ( {table.getRowModel().rows.map(row => {
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td> return (
))} <tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
</tr> {row.getVisibleCells().map(cell => (
) <td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
})} ))}
</tbody> </tr>
)} )
</table> })}
</tbody>
)}
</table>
)}
</div> </div>
<TablePagination <TablePagination
@ -445,6 +414,7 @@ const PurchaseOrderListTable = () => {
onPageChange={handlePageChange} onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange} onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50]} rowsPerPageOptions={[10, 25, 50]}
disabled={isLoading}
/> />
</Card> </Card>
</> </>