pos-dashboard-v2/src/views/apps/stock/list/StockListTable.tsx
2025-08-06 14:41:23 +07:00

392 lines
12 KiB
TypeScript

'use client'
// React Imports
import { useCallback, useEffect, useMemo, useState } from 'react'
// Next Imports
// MUI Imports
import Button from '@mui/material/Button'
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
import Checkbox from '@mui/material/Checkbox'
import Chip from '@mui/material/Chip'
import Divider from '@mui/material/Divider'
import MenuItem from '@mui/material/MenuItem'
import TablePagination from '@mui/material/TablePagination'
import type { TextFieldProps } from '@mui/material/TextField'
import Typography from '@mui/material/Typography'
// Third-party Imports
import type { RankingInfo } from '@tanstack/match-sorter-utils'
import { rankItem } from '@tanstack/match-sorter-utils'
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import classnames from 'classnames'
// Type Imports
// Component Imports
import TablePaginationComponent from '@components/TablePaginationComponent'
import CustomTextField from '@core/components/mui/TextField'
import OptionMenu from '@core/components/option-menu'
// Util Imports
// Style Imports
import tableStyles from '@core/styles/table.module.css'
import { Box, CircularProgress } from '@mui/material'
import ConfirmDeleteDialog from '../../../../components/dialogs/confirm-delete'
import Loading from '../../../../components/layout/shared/Loading'
import { useInventoriesMutation } from '../../../../services/mutations/inventories'
import { useInventories } from '../../../../services/queries/inventories'
import { Inventory } from '../../../../types/services/inventory'
import AddStockDrawer from './AddStockDrawer'
declare module '@tanstack/table-core' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface FilterMeta {
itemRank: RankingInfo
}
}
type InventoryWithActionsType = Inventory & {
actions?: string
}
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
// Rank the item
const itemRank = rankItem(row.getValue(columnId), value)
// Store the itemRank info
addMeta({
itemRank
})
// Return if the item should be filtered in/out
return itemRank.passed
}
const DebouncedInput = ({
value: initialValue,
onChange,
debounce = 500,
...props
}: {
value: string | number
onChange: (value: string | number) => void
debounce?: number
} & Omit<TextFieldProps, 'onChange'>) => {
// States
const [value, setValue] = useState(initialValue)
useEffect(() => {
setValue(initialValue)
}, [initialValue])
useEffect(() => {
const timeout = setTimeout(() => {
onChange(value)
}, debounce)
return () => clearTimeout(timeout)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value])
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
}
// Column Definitions
const columnHelper = createColumnHelper<InventoryWithActionsType>()
const StockListTable = () => {
const [rowSelection, setRowSelection] = useState({})
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [openConfirm, setOpenConfirm] = useState(false)
const [productId, setProductId] = useState('')
const [addInventoryOpen, setAddInventoryOpen] = useState(false)
// Fetch products with pagination and search
const { data, isLoading, error, isFetching } = useInventories({
page: currentPage,
limit: pageSize
})
const { mutate: deleteInventory, isPending: isDeleting } = useInventoriesMutation().deleteInventory
const inventories = data?.inventory ?? []
const totalCount = data?.total_count ?? 0
const handlePageChange = useCallback((event: unknown, newPage: number) => {
setCurrentPage(newPage)
}, [])
// Handle page size change
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10)
setPageSize(newPageSize)
setCurrentPage(0) // Reset to first page
}, [])
const handleDelete = () => {
deleteInventory(productId, {
onSuccess: () => setOpenConfirm(false)
})
}
const columns = useMemo<ColumnDef<InventoryWithActionsType, any>[]>(
() => [
{
id: 'select',
header: ({ table }) => (
<Checkbox
{...{
checked: table.getIsAllRowsSelected(),
indeterminate: table.getIsSomeRowsSelected(),
onChange: table.getToggleAllRowsSelectedHandler()
}}
/>
),
cell: ({ row }) => (
<Checkbox
{...{
checked: row.getIsSelected(),
disabled: !row.getCanSelect(),
indeterminate: row.getIsSomeSelected(),
onChange: row.getToggleSelectedHandler()
}}
/>
)
},
columnHelper.accessor('product_id', {
header: 'Product',
cell: ({ row }) => <Typography>{row.original.product_id}</Typography>
}),
columnHelper.accessor('quantity', {
header: 'Quantity',
cell: ({ row }) => <Typography>{row.original.quantity}</Typography>
}),
columnHelper.accessor('reorder_level', {
header: 'Reorder Level',
cell: ({ row }) => <Typography>{row.original.reorder_level}</Typography>
}),
columnHelper.accessor('is_low_stock', {
header: 'Status',
cell: ({ row }) => (
<Chip
label={row.original.is_low_stock ? 'Low' : 'Normal'}
variant='tonal'
color={row.original.is_low_stock ? 'error' : 'success'}
size='small'
/>
)
}),
columnHelper.accessor('actions', {
header: 'Actions',
cell: ({ row }) => (
<div className='flex items-center'>
<OptionMenu
iconButtonProps={{ size: 'medium' }}
iconClassName='text-textSecondary'
options={[
{ text: 'Download', icon: 'tabler-download' },
{
text: 'Delete',
icon: 'tabler-trash',
menuItemProps: {
onClick: () => {
setOpenConfirm(true)
setProductId(row.original.id)
}
}
},
{ text: 'Duplicate', icon: 'tabler-copy' }
]}
/>
</div>
),
enableSorting: false
})
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
const table = useReactTable({
data: inventories as Inventory[],
columns,
filterFns: {
fuzzy: fuzzyFilter
},
state: {
rowSelection,
pagination: {
pageIndex: currentPage, // <= penting!
pageSize
}
},
enableRowSelection: true, //enable row selection for all rows
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
// Disable client-side pagination since we're handling it server-side
manualPagination: true,
pageCount: Math.ceil(totalCount / pageSize)
})
return (
<>
<Card>
<CardHeader title='Filters' />
{/* <TableFilters setData={() => {}} productData={[]} /> */}
<Divider />
<div className='flex flex-wrap justify-between gap-4 p-6'>
<DebouncedInput
value={'search'}
onChange={value => console.log(value)}
placeholder='Search Product'
className='max-sm:is-full'
/>
<div className='flex flex-wrap items-center max-sm:flex-col gap-4 max-sm:is-full is-auto'>
<CustomTextField
select
value={table.getState().pagination.pageSize}
onChange={e => table.setPageSize(Number(e.target.value))}
className='flex-auto is-[70px] max-sm:is-full'
>
<MenuItem value='10'>10</MenuItem>
<MenuItem value='25'>25</MenuItem>
<MenuItem value='50'>50</MenuItem>
</CustomTextField>
<Button
color='secondary'
variant='tonal'
className='max-sm:is-full is-auto'
startIcon={<i className='tabler-upload' />}
>
Export
</Button>
<Button
variant='contained'
className='max-sm:is-full'
onClick={() => setAddInventoryOpen(!addInventoryOpen)}
startIcon={<i className='tabler-plus' />}
>
Add Inventory
</Button>
</div>
</div>
<div className='overflow-x-auto'>
{isLoading ? (
<Loading />
) : (
<table className={tableStyles.table}>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
{header.isPlaceholder ? null : (
<>
<div
className={classnames({
'flex items-center': header.column.getIsSorted(),
'cursor-pointer select-none': header.column.getCanSort()
})}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{
asc: <i className='tabler-chevron-up text-xl' />,
desc: <i className='tabler-chevron-down text-xl' />
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
</div>
</>
)}
</th>
))}
</tr>
))}
</thead>
{table.getFilteredRowModel().rows.length === 0 ? (
<tbody>
<tr>
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
No data available
</td>
</tr>
</tbody>
) : (
<tbody>
{table
.getRowModel()
.rows.slice(0, table.getState().pagination.pageSize)
.map(row => {
return (
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
))}
</tr>
)
})}
</tbody>
)}
</table>
)}
{isFetching && !isLoading && (
<Box
position='absolute'
top={0}
left={0}
right={0}
bottom={0}
display='flex'
alignItems='center'
justifyContent='center'
bgcolor='rgba(255,255,255,0.7)'
zIndex={1}
>
<CircularProgress size={24} />
</Box>
)}
</div>
<TablePagination
component={() => (
<TablePaginationComponent
pageIndex={currentPage}
pageSize={pageSize}
totalCount={totalCount}
onPageChange={handlePageChange}
/>
)}
count={totalCount}
rowsPerPage={pageSize}
page={currentPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
disabled={isLoading}
/>
</Card>
<AddStockDrawer open={addInventoryOpen} handleClose={() => setAddInventoryOpen(!addInventoryOpen)} />
<ConfirmDeleteDialog
open={openConfirm}
onClose={() => setOpenConfirm(false)}
onConfirm={handleDelete}
isLoading={isDeleting}
title='Delete Inventory'
message='Are you sure you want to delete this inventory? This action cannot be undone.'
/>
</>
)
}
export default StockListTable