Purchase Order table
This commit is contained in:
parent
d54d623d4c
commit
98d6446b0c
@ -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>
|
||||||
|
|||||||
41
src/services/queries/purchaseOrder.ts
Normal file
41
src/services/queries/purchaseOrder.ts
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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,6 +343,9 @@ 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 => (
|
||||||
@ -406,7 +374,7 @@ const PurchaseOrderListTable = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
{filteredData.length === 0 ? (
|
{purchaseOrders.length === 0 ? (
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
||||||
@ -428,6 +396,7 @@ const PurchaseOrderListTable = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
)}
|
)}
|
||||||
</table>
|
</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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user