feat list product and category

This commit is contained in:
ferdiansyah783 2025-08-05 14:34:36 +07:00
parent fffa2ead5c
commit 0fde122ac4
8 changed files with 388 additions and 382 deletions

View File

@ -1,33 +1,29 @@
// MUI Imports import { Typography, Pagination } from '@mui/material'
import Pagination from '@mui/material/Pagination'
import Typography from '@mui/material/Typography'
// Third Party Imports const TablePaginationComponent = ({
import type { useReactTable } from '@tanstack/react-table' pageIndex,
pageSize,
totalCount,
onPageChange
}: {
pageIndex: number
pageSize: number
totalCount: number
onPageChange: (event: React.ChangeEvent<unknown>, page: number) => void
}) => {
const start = pageIndex === 1 ? pageIndex : (pageIndex - 1) * pageSize + 1
const end = Math.min((pageIndex) * pageSize, totalCount)
const TablePaginationComponent = ({ table }: { table: ReturnType<typeof useReactTable> }) => {
return ( return (
<div className='flex justify-between items-center flex-wrap pli-6 border-bs bs-auto plb-[12.5px] gap-2'> <div className='flex justify-between items-center flex-wrap pli-6 border-bs bs-auto plb-[12.5px] gap-2'>
<Typography color='text.disabled'> <Typography color='text.disabled'>{`Showing ${start} to ${end} of ${totalCount} entries`}</Typography>
{`Showing ${
table.getFilteredRowModel().rows.length === 0
? 0
: table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1
}
to ${Math.min(
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
table.getFilteredRowModel().rows.length
)} of ${table.getFilteredRowModel().rows.length} entries`}
</Typography>
<Pagination <Pagination
shape='rounded' shape='rounded'
color='primary' color='primary'
variant='tonal' variant='tonal'
count={Math.ceil(table.getFilteredRowModel().rows.length / table.getState().pagination.pageSize)} count={Math.ceil(totalCount / pageSize)}
page={table.getState().pagination.pageIndex + 1} page={pageIndex}
onChange={(_, page) => { onChange={onPageChange}
table.setPageIndex(page - 1)
}}
showFirstButton showFirstButton
showLastButton showLastButton
/> />

View File

@ -0,0 +1,41 @@
import { useQuery } from '@tanstack/react-query'
import { Categories } from '../../types/services/category'
import { api } from '../api'
interface CategoriesQueryParams {
page?: number
limit?: number
search?: string
}
export const useCategoriesQuery = {
getCategories: (params: CategoriesQueryParams = {}) => {
const { page = 1, limit = 10, search = '', ...filters } = params
return useQuery<Categories>({
queryKey: ['categories', { page, limit, search, ...filters }],
queryFn: async () => {
const queryParams = new URLSearchParams()
queryParams.append('page', page.toString())
queryParams.append('limit', limit.toString())
if (search) {
queryParams.append('search', search)
}
// Add other filters
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
queryParams.append(key, value.toString())
}
})
const res = await api.get(`/categories?${queryParams.toString()}`)
return res.data.data
},
// Cache for 5 minutes
staleTime: 5 * 60 * 1000
})
}
}

View File

@ -1,15 +1,44 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { Products } from '../../types/services/products' import { Products } from '../../types/services/product'
import { api } from '../api' import { api } from '../api'
interface ProductsQueryParams {
page?: number
limit?: number
search?: string
// Add other filter parameters as needed
category_id?: string
is_active?: boolean
}
export const useProductsQuery = { export const useProductsQuery = {
getProducts: () => { getProducts: (params: ProductsQueryParams = {}) => {
const { page = 1, limit = 10, search = '', ...filters } = params
return useQuery<Products>({ return useQuery<Products>({
queryKey: ['products'], queryKey: ['products', { page, limit, search, ...filters }],
queryFn: async () => { queryFn: async () => {
const res = await api.get('/products') const queryParams = new URLSearchParams()
queryParams.append('page', page.toString())
queryParams.append('limit', limit.toString())
if (search) {
queryParams.append('search', search)
}
// Add other filters
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
queryParams.append(key, value.toString())
}
})
const res = await api.get(`/products?${queryParams.toString()}`)
return res.data.data return res.data.data
} },
// Cache for 5 minutes
staleTime: 5 * 60 * 1000
}) })
} }
} }

View File

@ -0,0 +1,18 @@
export interface Category {
id: string;
organization_id: string;
name: string;
description: string | null;
business_type: string;
metadata: Record<string, any>;
created_at: string;
updated_at: string;
}
export interface Categories {
categories: Category[];
total_count: number;
page: number;
limit: number;
total_pages: number;
}

View File

@ -1,7 +1,7 @@
'use client' 'use client'
// React Imports // React Imports
import { useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
// MUI Imports // MUI Imports
import Card from '@mui/material/Card' import Card from '@mui/material/Card'
@ -39,6 +39,10 @@ import TablePaginationComponent from '@components/TablePaginationComponent'
// Style Imports // Style Imports
import tableStyles from '@core/styles/table.module.css' import tableStyles from '@core/styles/table.module.css'
import { useCategoriesQuery } from '../../../../../services/queries/categories'
import { Category } from '../../../../../types/services/category'
import { Box, CircularProgress } from '@mui/material'
import Loading from '../../../../../components/layout/shared/Loading'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -49,16 +53,7 @@ declare module '@tanstack/table-core' {
} }
} }
export type categoryType = { type CategoryWithActionsType = Category & {
id: number
categoryTitle: string
description: string
totalProduct: number
totalEarning: number
image: string
}
type CategoryWithActionsType = categoryType & {
actions?: string actions?: string
} }
@ -104,106 +99,6 @@ const DebouncedInput = ({
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} /> return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
} }
// Vars
const categoryData: categoryType[] = [
{
id: 1,
categoryTitle: 'Smart Phone',
description: 'Choose from wide range of smartphones online at best prices.',
totalProduct: 12548,
totalEarning: 98784,
image: '/images/apps/ecommerce/product-1.png'
},
{
id: 2,
categoryTitle: 'Clothing, Shoes, and jewellery',
description: 'Fashion for a wide selection of clothing, shoes, jewellery and watches.',
totalProduct: 4689,
totalEarning: 45627,
image: '/images/apps/ecommerce/product-9.png'
},
{
id: 3,
categoryTitle: 'Home and Kitchen',
description: 'Browse through the wide range of Home and kitchen products.',
totalProduct: 11297,
totalEarning: 51097,
image: '/images/apps/ecommerce/product-10.png'
},
{
id: 4,
categoryTitle: 'Beauty and Personal Care',
description: 'Explore beauty and personal care products, shop makeup and etc.',
totalProduct: 9474,
totalEarning: 74829,
image: '/images/apps/ecommerce/product-19.png'
},
{
id: 5,
categoryTitle: 'Books',
description: 'Over 25 million titles across categories such as business and etc.',
totalProduct: 10257,
totalEarning: 63618,
image: '/images/apps/ecommerce/product-25.png'
},
{
id: 6,
categoryTitle: 'Games',
description: 'Every month, get exclusive in-game loot, free games, a free subscription.',
totalProduct: 14501,
totalEarning: 65920,
image: '/images/apps/ecommerce/product-12.png'
},
{
id: 7,
categoryTitle: 'Baby Products',
description: 'Buy baby products across different categories from top brands.',
totalProduct: 8624,
totalEarning: 38838,
image: '/images/apps/ecommerce/product-14.png'
},
{
id: 8,
categoryTitle: 'Growsari',
description: 'Shop grocery Items through at best prices in India.',
totalProduct: 7389,
totalEarning: 72652,
image: '/images/apps/ecommerce/product-26.png'
},
{
id: 9,
categoryTitle: 'Computer Accessories',
description: 'Enhance your computing experience with our range of computer accessories.',
totalProduct: 9876,
totalEarning: 65421,
image: '/images/apps/ecommerce/product-17.png'
},
{
id: 10,
categoryTitle: 'Fitness Tracker',
description: 'Monitor your health and fitness goals with our range of advanced fitness trackers.',
totalProduct: 1987,
totalEarning: 32067,
image: '/images/apps/ecommerce/product-10.png'
},
{
id: 11,
categoryTitle: 'Smart Home Devices',
description: 'Transform your home into a smart home with our innovative smart home devices.',
totalProduct: 2345,
totalEarning: 87654,
image: '/images/apps/ecommerce/product-11.png'
},
{
id: 12,
categoryTitle: 'Audio Speakers',
description: 'Immerse yourself in rich audio quality with our wide range of speakers.',
totalProduct: 5678,
totalEarning: 32145,
image: '/images/apps/ecommerce/product-2.png'
}
]
// Column Definitions // Column Definitions
const columnHelper = createColumnHelper<CategoryWithActionsType>() const columnHelper = createColumnHelper<CategoryWithActionsType>()
@ -211,8 +106,28 @@ const ProductCategoryTable = () => {
// States // States
const [addCategoryOpen, setAddCategoryOpen] = useState(false) const [addCategoryOpen, setAddCategoryOpen] = useState(false)
const [rowSelection, setRowSelection] = useState({}) const [rowSelection, setRowSelection] = useState({})
const [data, setData] = useState(...[categoryData]) const [currentPage, setCurrentPage] = useState(1)
const [globalFilter, setGlobalFilter] = useState('') const [pageSize, setPageSize] = useState(10)
// Fetch products with pagination and search
const { data, isLoading, error, isFetching } = useCategoriesQuery.getCategories({
page: currentPage,
limit: pageSize
})
const categories = data?.categories ?? []
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 columns = useMemo<ColumnDef<CategoryWithActionsType, any>[]>( const columns = useMemo<ColumnDef<CategoryWithActionsType, any>[]>(
() => [ () => [
@ -238,31 +153,27 @@ const ProductCategoryTable = () => {
/> />
) )
}, },
columnHelper.accessor('categoryTitle', { columnHelper.accessor('name', {
header: 'Categories', header: 'Name',
cell: ({ row }) => ( cell: ({ row }) => (
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<img src={row.original.image} width={38} height={38} className='rounded bg-actionHover' /> {/* <img src={row.original.image} width={38} height={38} className='rounded bg-actionHover' /> */}
<div className='flex flex-col items-start'> <div className='flex flex-col items-start'>
<Typography className='font-medium' color='text.primary'> <Typography className='font-medium' color='text.primary'>
{row.original.categoryTitle} {row.original.name}
</Typography> </Typography>
<Typography variant='body2'>{row.original.description}</Typography> <Typography variant='body2'>{row.original.description}</Typography>
</div> </div>
</div> </div>
) )
}), }),
columnHelper.accessor('totalProduct', { columnHelper.accessor('description', {
header: 'Total Products', header: 'Decription',
cell: ({ row }) => <Typography>{row.original.totalProduct.toLocaleString()}</Typography> cell: ({ row }) => <Typography>{row.original.description || '-'}</Typography>
}), }),
columnHelper.accessor('totalEarning', { columnHelper.accessor('business_type', {
header: 'Total Earning', header: 'Business Type',
cell: ({ row }) => ( cell: ({ row }) => <Typography>{row.original.business_type}</Typography>
<Typography>
{row.original.totalEarning.toLocaleString('en-IN', { style: 'currency', currency: 'USD' })}
</Typography>
)
}), }),
columnHelper.accessor('actions', { columnHelper.accessor('actions', {
header: 'Actions', header: 'Actions',
@ -279,7 +190,7 @@ const ProductCategoryTable = () => {
{ {
text: 'Delete', text: 'Delete',
icon: 'tabler-trash', icon: 'tabler-trash',
menuItemProps: { onClick: () => setData(data.filter(category => category.id !== row.original.id)) } menuItemProps: { onClick: () => console.log('click') }
}, },
{ text: 'Duplicate', icon: 'tabler-copy' } { text: 'Duplicate', icon: 'tabler-copy' }
]} ]}
@ -294,32 +205,24 @@ const ProductCategoryTable = () => {
) )
const table = useReactTable({ const table = useReactTable({
data: data as categoryType[], data: categories as Category[],
columns, columns,
filterFns: { filterFns: {
fuzzy: fuzzyFilter fuzzy: fuzzyFilter
}, },
state: { state: {
rowSelection, rowSelection,
globalFilter
},
initialState: {
pagination: { pagination: {
pageSize: 10 pageIndex: currentPage, // <= penting!
pageSize
} }
}, },
enableRowSelection: true, //enable row selection for all rows enableRowSelection: true, //enable row selection for all rows
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
globalFilterFn: fuzzyFilter,
onRowSelectionChange: setRowSelection, onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
onGlobalFilterChange: setGlobalFilter, // Disable client-side pagination since we're handling it server-side
getFilteredRowModel: getFilteredRowModel(), manualPagination: true,
getSortedRowModel: getSortedRowModel(), pageCount: Math.ceil(totalCount / pageSize)
getPaginationRowModel: getPaginationRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues()
}) })
return ( return (
@ -327,9 +230,9 @@ const ProductCategoryTable = () => {
<Card> <Card>
<div className='flex flex-wrap justify-between gap-4 p-6'> <div className='flex flex-wrap justify-between gap-4 p-6'>
<DebouncedInput <DebouncedInput
value={globalFilter ?? ''} value={'search'}
onChange={value => setGlobalFilter(String(value))} onChange={value => console.log(value)}
placeholder='Search' placeholder='Search Product'
className='max-sm:is-full' className='max-sm:is-full'
/> />
<div className='flex max-sm:flex-col items-start sm:items-center gap-4 max-sm:is-full'> <div className='flex max-sm:flex-col items-start sm:items-center gap-4 max-sm:is-full'>
@ -354,74 +257,103 @@ const ProductCategoryTable = () => {
</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> ))}
{table.getFilteredRowModel().rows.length === 0 ? ( </tr>
<tbody> ))}
<tr> </thead>
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'> {table.getFilteredRowModel().rows.length === 0 ? (
No data available <tbody>
</td> <tr>
</tr> <td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
</tbody> No data available
) : ( </td>
<tbody> </tr>
{table </tbody>
.getRowModel() ) : (
.rows.slice(0, table.getState().pagination.pageSize) <tbody>
.map(row => { {table
return ( .getRowModel()
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}> .rows.slice(0, table.getState().pagination.pageSize)
{row.getVisibleCells().map(cell => ( .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>
)}
{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> </div>
<TablePagination <TablePagination
component={() => <TablePaginationComponent table={table} />} component={() => (
count={table.getFilteredRowModel().rows.length} <TablePaginationComponent
rowsPerPage={table.getState().pagination.pageSize} pageIndex={currentPage}
page={table.getState().pagination.pageIndex} pageSize={pageSize}
onPageChange={(_, page) => { totalCount={totalCount}
table.setPageIndex(page) onPageChange={handlePageChange}
}} />
)}
count={totalCount}
rowsPerPage={pageSize}
page={currentPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
disabled={isLoading}
/> />
</Card> </Card>
<AddCategoryDrawer <AddCategoryDrawer
open={addCategoryOpen} open={addCategoryOpen}
categoryData={data} categoryData={categories}
setData={setData} setData={() => {}}
handleClose={() => setAddCategoryOpen(!addCategoryOpen)} handleClose={() => setAddCategoryOpen(!addCategoryOpen)}
/> />
</> </>

View File

@ -1,63 +1,50 @@
'use client' 'use client'
// React Imports // React Imports
import { useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
// Next Imports // Next Imports
import Link from 'next/link' import Link from 'next/link'
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
// MUI Imports // MUI Imports
import Button from '@mui/material/Button'
import Card from '@mui/material/Card' import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader' import CardHeader from '@mui/material/CardHeader'
import Button from '@mui/material/Button'
import Chip from '@mui/material/Chip'
import Checkbox from '@mui/material/Checkbox' import Checkbox from '@mui/material/Checkbox'
import Chip from '@mui/material/Chip'
import Divider from '@mui/material/Divider' import Divider from '@mui/material/Divider'
import IconButton from '@mui/material/IconButton' import IconButton from '@mui/material/IconButton'
import Switch from '@mui/material/Switch'
import MenuItem from '@mui/material/MenuItem' import MenuItem from '@mui/material/MenuItem'
import TablePagination from '@mui/material/TablePagination' import TablePagination from '@mui/material/TablePagination'
import Typography from '@mui/material/Typography'
import type { TextFieldProps } from '@mui/material/TextField' import type { TextFieldProps } from '@mui/material/TextField'
import Typography from '@mui/material/Typography'
// Third-party Imports // Third-party Imports
import classnames from 'classnames'
import { rankItem } from '@tanstack/match-sorter-utils'
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
getFilteredRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFacetedMinMaxValues,
getPaginationRowModel,
getSortedRowModel
} from '@tanstack/react-table'
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
import type { RankingInfo } from '@tanstack/match-sorter-utils' 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 // Type Imports
import type { ThemeColor } from '@core/types'
import type { Locale } from '@configs/i18n' import type { Locale } from '@configs/i18n'
import type { ProductType } from '@/types/apps/ecommerceTypes'
// Component Imports // Component Imports
import TableFilters from './TableFilters' import TablePaginationComponent from '@components/TablePaginationComponent'
import CustomAvatar from '@core/components/mui/Avatar'
import CustomTextField from '@core/components/mui/TextField' import CustomTextField from '@core/components/mui/TextField'
import OptionMenu from '@core/components/option-menu' import OptionMenu from '@core/components/option-menu'
import TablePaginationComponent from '@components/TablePaginationComponent' import TableFilters from './TableFilters'
// Util Imports // Util Imports
import { getLocalizedUrl } from '@/utils/i18n' import { getLocalizedUrl } from '@/utils/i18n'
// Style Imports // Style Imports
import tableStyles from '@core/styles/table.module.css' import tableStyles from '@core/styles/table.module.css'
import { Box, CircularProgress } from '@mui/material'
import Loading from '../../../../../components/layout/shared/Loading'
import { useProductsQuery } from '../../../../../services/queries/products' import { useProductsQuery } from '../../../../../services/queries/products'
import { Product } from '../../../../../types/services/products' import { Product } from '../../../../../types/services/product'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -72,20 +59,6 @@ type ProductWithActionsType = Product & {
actions?: string actions?: string
} }
type ProductCategoryType = {
[key: string]: {
icon: string
color: ThemeColor
}
}
type productStatusType = {
[key: string]: {
title: string
color: ThemeColor
}
}
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => { const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
// Rank the item // Rank the item
const itemRank = rankItem(row.getValue(columnId), value) const itemRank = rankItem(row.getValue(columnId), value)
@ -128,36 +101,37 @@ const DebouncedInput = ({
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} /> return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
} }
// Vars
const productCategoryObj: ProductCategoryType = {
Accessories: { icon: 'tabler-headphones', color: 'error' },
'Home Decor': { icon: 'tabler-smart-home', color: 'info' },
Electronics: { icon: 'tabler-device-laptop', color: 'primary' },
Shoes: { icon: 'tabler-shoe', color: 'success' },
Office: { icon: 'tabler-briefcase', color: 'warning' },
Games: { icon: 'tabler-device-gamepad-2', color: 'secondary' }
}
const productStatusObj: productStatusType = {
Scheduled: { title: 'Scheduled', color: 'warning' },
Published: { title: 'Publish', color: 'success' },
Inactive: { title: 'Inactive', color: 'error' }
}
// Column Definitions // Column Definitions
const columnHelper = createColumnHelper<ProductWithActionsType>() const columnHelper = createColumnHelper<ProductWithActionsType>()
const ProductListTable = () => { const ProductListTable = () => {
// States
const { data } = useProductsQuery.getProducts()
const [rowSelection, setRowSelection] = useState({}) const [rowSelection, setRowSelection] = useState({})
const [filteredData, setFilteredData] = useState(data?.products ?? []) const [currentPage, setCurrentPage] = useState(1)
const [globalFilter, setGlobalFilter] = useState('') const [pageSize, setPageSize] = useState(10)
// Hooks // Hooks
const { lang: locale } = useParams() const { lang: locale } = useParams()
// Fetch products with pagination and search
const { data, isLoading, error, isFetching } = useProductsQuery.getProducts({
page: currentPage,
limit: pageSize
})
const products = data?.products ?? []
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 columns = useMemo<ColumnDef<ProductWithActionsType, any>[]>( const columns = useMemo<ColumnDef<ProductWithActionsType, any>[]>(
() => [ () => [
{ {
@ -226,7 +200,7 @@ const ProductListTable = () => {
}), }),
columnHelper.accessor('is_active', { columnHelper.accessor('is_active', {
header: 'Status', header: 'Status',
cell: ({row}) => ( cell: ({ row }) => (
<Chip <Chip
label={row.original.is_active ? 'Active' : 'Inactive'} label={row.original.is_active ? 'Active' : 'Inactive'}
variant='tonal' variant='tonal'
@ -261,48 +235,40 @@ const ProductListTable = () => {
}) })
], ],
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[data, filteredData] []
) )
const table = useReactTable({ const table = useReactTable({
data: filteredData as Product[], data: products as Product[],
columns, columns,
filterFns: { filterFns: {
fuzzy: fuzzyFilter fuzzy: fuzzyFilter
}, },
state: { state: {
rowSelection, rowSelection,
globalFilter
},
initialState: {
pagination: { pagination: {
pageSize: 10 pageIndex: currentPage, // <= penting!
pageSize
} }
}, },
enableRowSelection: true, //enable row selection for all rows enableRowSelection: true, //enable row selection for all rows
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
globalFilterFn: fuzzyFilter,
onRowSelectionChange: setRowSelection, onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
onGlobalFilterChange: setGlobalFilter, // Disable client-side pagination since we're handling it server-side
getFilteredRowModel: getFilteredRowModel(), manualPagination: true,
getSortedRowModel: getSortedRowModel(), pageCount: Math.ceil(totalCount / pageSize)
getPaginationRowModel: getPaginationRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues()
}) })
return ( return (
<> <>
<Card> <Card>
<CardHeader title='Filters' /> <CardHeader title='Filters' />
<TableFilters setData={setFilteredData} productData={data?.products} /> <TableFilters setData={() => {}} productData={[]} />
<Divider /> <Divider />
<div className='flex flex-wrap justify-between gap-4 p-6'> <div className='flex flex-wrap justify-between gap-4 p-6'>
<DebouncedInput <DebouncedInput
value={globalFilter ?? ''} value={'search'}
onChange={value => setGlobalFilter(String(value))} onChange={value => console.log(value)}
placeholder='Search Product' placeholder='Search Product'
className='max-sm:is-full' className='max-sm:is-full'
/> />
@ -337,68 +303,98 @@ const ProductListTable = () => {
</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> ))}
{table.getFilteredRowModel().rows.length === 0 ? ( </tr>
<tbody> ))}
<tr> </thead>
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'> {table.getFilteredRowModel().rows.length === 0 ? (
No data available <tbody>
</td> <tr>
</tr> <td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
</tbody> No data available
) : ( </td>
<tbody> </tr>
{table </tbody>
.getRowModel() ) : (
.rows.slice(0, table.getState().pagination.pageSize) <tbody>
.map(row => { {table
return ( .getRowModel()
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}> .rows.slice(0, table.getState().pagination.pageSize)
{row.getVisibleCells().map(cell => ( .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>
)}
{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> </div>
<TablePagination <TablePagination
component={() => <TablePaginationComponent table={table} />} component={() => (
count={table.getFilteredRowModel().rows.length} <TablePaginationComponent
rowsPerPage={table.getState().pagination.pageSize} pageIndex={currentPage}
page={table.getState().pagination.pageIndex} pageSize={pageSize}
onPageChange={(_, page) => { totalCount={totalCount}
table.setPageIndex(page) onPageChange={handlePageChange}
}} />
)}
count={totalCount}
rowsPerPage={pageSize}
page={currentPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
disabled={isLoading}
/> />
</Card> </Card>
</> </>

View File

@ -11,7 +11,7 @@ import type { ProductType } from '@/types/apps/ecommerceTypes'
// Component Imports // Component Imports
import CustomTextField from '@core/components/mui/TextField' import CustomTextField from '@core/components/mui/TextField'
import { Product } from '../../../../../types/services/products' import { Product } from '../../../../../types/services/product'
type ProductStockType = { [key: string]: boolean } type ProductStockType = { [key: string]: boolean }
@ -21,13 +21,7 @@ const productStockObj: ProductStockType = {
'Out of Stock': false 'Out of Stock': false
} }
const TableFilters = ({ const TableFilters = ({ setData, productData }: { setData: (data: Product[]) => void; productData?: Product[] }) => {
setData,
productData
}: {
setData: (data: Product[]) => void
productData?: Product[]
}) => {
// States // States
const [category, setCategory] = useState<Product['category_id']>('') const [category, setCategory] = useState<Product['category_id']>('')
const [stock, setStock] = useState('') const [stock, setStock] = useState('')