fix: inventory

This commit is contained in:
ferdiansyah783
2025-08-06 03:57:45 +07:00
parent 799837e82e
commit 0906188c12
37 changed files with 2342 additions and 340 deletions
@@ -1,49 +1,38 @@
// React Imports
import { useState, useEffect, useMemo } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
// Next Imports
import Link from 'next/link'
import { useParams } from 'next/navigation'
// MUI Imports
import Button from '@mui/material/Button'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Button from '@mui/material/Button'
import Typography from '@mui/material/Typography'
import Checkbox from '@mui/material/Checkbox'
import Chip from '@mui/material/Chip'
import MenuItem from '@mui/material/MenuItem'
import TablePagination from '@mui/material/TablePagination'
import type { TextFieldProps } from '@mui/material/TextField'
import MenuItem from '@mui/material/MenuItem'
import Typography from '@mui/material/Typography'
// 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 { 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
import type { ThemeColor } from '@core/types'
import type { OrderType } from '@/types/apps/ecommerceTypes'
import type { Locale } from '@configs/i18n'
import type { ThemeColor } from '@core/types'
// Component Imports
import CustomAvatar from '@core/components/mui/Avatar'
import OptionMenu from '@core/components/option-menu'
import CustomTextField from '@core/components/mui/TextField'
import TablePaginationComponent from '@components/TablePaginationComponent'
import CustomAvatar from '@core/components/mui/Avatar'
import CustomTextField from '@core/components/mui/TextField'
import OptionMenu from '@core/components/option-menu'
// Util Imports
import { getInitials } from '@/utils/getInitials'
@@ -51,6 +40,10 @@ import { getLocalizedUrl } from '@/utils/i18n'
// Style Imports
import tableStyles from '@core/styles/table.module.css'
import { useOrdersQuery } from '../../../../../services/queries/orders'
import { Order } from '../../../../../types/services/order'
import { Box, CircularProgress } from '@mui/material'
import Loading from '../../../../../components/layout/shared/Loading'
declare module '@tanstack/table-core' {
interface FilterFns {
@@ -85,7 +78,7 @@ export const statusChipColor: { [key: string]: StatusChipColorType } = {
Dispatched: { color: 'warning' }
}
type ECommerceOrderTypeWithAction = OrderType & {
type ECommerceOrderTypeWithAction = Order & {
action?: string
}
@@ -134,15 +127,34 @@ const DebouncedInput = ({
// Column Definitions
const columnHelper = createColumnHelper<ECommerceOrderTypeWithAction>()
const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
const OrderListTable = () => {
// States
const [rowSelection, setRowSelection] = useState({})
const [data, setData] = useState(...[orderData])
const [globalFilter, setGlobalFilter] = useState('')
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const { data, isLoading, error, isFetching } = useOrdersQuery.getOrders({
page: currentPage,
limit: pageSize
})
// Hooks
const { lang: locale } = useParams()
const orders = data?.orders ?? []
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
}, [])
// Vars
const paypal = '/images/apps/ecommerce/paypal.png'
const mastercard = '/images/apps/ecommerce/mastercard.png'
@@ -171,84 +183,95 @@ const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
/>
)
},
columnHelper.accessor('order', {
header: 'Order',
columnHelper.accessor('order_number', {
header: 'Order Number',
cell: ({ row }) => (
<Typography
component={Link}
href={getLocalizedUrl(`/apps/ecommerce/orders/details/${row.original.order}`, locale as Locale)}
href={getLocalizedUrl(`/apps/ecommerce/orders/details/${row.original.order_number}`, locale as Locale)}
color='primary.main'
>{`#${row.original.order}`}</Typography>
>{`#${row.original.order_number}`}</Typography>
)
}),
columnHelper.accessor('date', {
header: 'Date',
cell: ({ row }) => (
<Typography>{`${new Date(row.original.date).toDateString()}, ${row.original.time}`}</Typography>
)
columnHelper.accessor('table_number', {
header: 'Table',
cell: ({ row }) => <Typography>{row.original.table_number}</Typography>
}),
columnHelper.accessor('customer', {
header: 'Customers',
cell: ({ row }) => (
<div className='flex items-center gap-3'>
{getAvatar({ avatar: row.original.avatar, customer: row.original.customer })}
<div className='flex flex-col'>
<Typography
component={Link}
href={getLocalizedUrl('/apps/ecommerce/customers/details/879861', locale as Locale)}
color='text.primary'
className='font-medium hover:text-primary'
>
{row.original.customer}
</Typography>
<Typography variant='body2'>{row.original.email}</Typography>
</div>
</div>
)
}),
columnHelper.accessor('payment', {
header: 'Payment',
cell: ({ row }) => (
<div className='flex items-center gap-1'>
<i
className={classnames(
'tabler-circle-filled bs-2.5 is-2.5',
paymentStatus[row.original.payment].colorClassName
)}
/>
<Typography color={`${paymentStatus[row.original.payment].color}.main`} className='font-medium'>
{paymentStatus[row.original.payment].text}
</Typography>
</div>
)
columnHelper.accessor('order_type', {
header: 'Order Type',
cell: ({ row }) => <Typography>{row.original.order_type}</Typography>
}),
// columnHelper.accessor('order_type', {
// header: 'Customers',
// cell: ({ row }) => (
// <div className='flex items-center gap-3'>
// {getAvatar({ avatar: row.original.avatar, customer: row.original.customer })}
// <div className='flex flex-col'>
// <Typography
// component={Link}
// href={getLocalizedUrl('/apps/ecommerce/customers/details/879861', locale as Locale)}
// color='text.primary'
// className='font-medium hover:text-primary'
// >
// {row.original.customer}
// </Typography>
// <Typography variant='body2'>{row.original.email}</Typography>
// </div>
// </div>
// )
// }),
// columnHelper.accessor('payment', {
// header: 'Payment',
// cell: ({ row }) => (
// <div className='flex items-center gap-1'>
// <i
// className={classnames(
// 'tabler-circle-filled bs-2.5 is-2.5',
// paymentStatus[row.original.payment].colorClassName
// )}
// />
// <Typography color={`${paymentStatus[row.original.payment].color}.main`} className='font-medium'>
// {paymentStatus[row.original.payment].text}
// </Typography>
// </div>
// )
// }),
columnHelper.accessor('status', {
header: 'Status',
cell: ({ row }) => (
<Chip
label={row.original.status}
color={statusChipColor[row.original.status].color}
variant='tonal'
size='small'
/>
)
cell: ({ row }) => <Chip label={row.original.status} color={'default'} variant='tonal' size='small' />
}),
columnHelper.accessor('method', {
header: 'Method',
cell: ({ row }) => (
<div className='flex items-center'>
<div className='flex justify-center items-center bg-[#F6F8FA] rounded-sm is-[29px] bs-[18px]'>
<img
src={row.original.method === 'mastercard' ? mastercard : paypal}
height={row.original.method === 'mastercard' ? 11 : 14}
/>
</div>
<Typography>
{`...${row.original.method === 'mastercard' ? row.original.methodNumber : '@gmail.com'}`}
</Typography>
</div>
)
columnHelper.accessor('subtotal', {
header: 'SubTotal',
cell: ({ row }) => <Typography>{row.original.subtotal}</Typography>
}),
columnHelper.accessor('total_amount', {
header: 'Total',
cell: ({ row }) => <Typography>{row.original.total_amount}</Typography>
}),
columnHelper.accessor('tax_amount', {
header: 'Tax',
cell: ({ row }) => <Typography>{row.original.tax_amount}</Typography>
}),
columnHelper.accessor('discount_amount', {
header: 'Discount',
cell: ({ row }) => <Typography>{row.original.discount_amount}</Typography>
}),
// columnHelper.accessor('method', {
// header: 'Method',
// cell: ({ row }) => (
// <div className='flex items-center'>
// <div className='flex justify-center items-center bg-[#F6F8FA] rounded-sm is-[29px] bs-[18px]'>
// <img
// src={row.original.method === 'mastercard' ? mastercard : paypal}
// height={row.original.method === 'mastercard' ? 11 : 14}
// />
// </div>
// <Typography>
// {`...${row.original.method === 'mastercard' ? row.original.methodNumber : '@gmail.com'}`}
// </Typography>
// </div>
// )
// }),
columnHelper.accessor('action', {
header: 'Action',
cell: ({ row }) => (
@@ -260,14 +283,17 @@ const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
{
text: 'View',
icon: 'tabler-eye',
href: getLocalizedUrl(`/apps/ecommerce/orders/details/${row.original.order}`, locale as Locale),
href: getLocalizedUrl(
`/apps/ecommerce/orders/details/${row.original.order_number}`,
locale as Locale
),
linkProps: { className: 'flex items-center gap-2 is-full plb-2 pli-4' }
},
{
text: 'Delete',
icon: 'tabler-trash',
menuItemProps: {
onClick: () => setData(data?.filter(order => order.id !== row.original.id)),
onClick: () => {},
className: 'flex items-center'
}
}
@@ -283,32 +309,24 @@ const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
)
const table = useReactTable({
data: data as OrderType[],
data: orders as Order[],
columns,
filterFns: {
fuzzy: fuzzyFilter
},
state: {
rowSelection,
globalFilter
},
initialState: {
pagination: {
pageSize: 10
pageIndex: currentPage, // <= penting!
pageSize
}
},
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,
getCoreRowModel: getCoreRowModel(),
onGlobalFilterChange: setGlobalFilter,
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues()
// Disable client-side pagination since we're handling it server-side
manualPagination: true,
pageCount: Math.ceil(totalCount / pageSize)
})
const getAvatar = (params: Pick<OrderType, 'avatar' | 'customer'>) => {
@@ -329,8 +347,8 @@ const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
<Card>
<CardContent className='flex justify-between max-sm:flex-col sm:items-center gap-4'>
<DebouncedInput
value={globalFilter ?? ''}
onChange={value => setGlobalFilter(String(value))}
value={''}
onChange={value => console.log('click')}
placeholder='Search Order'
className='sm:is-auto'
/>
@@ -357,68 +375,98 @@ const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
</div>
</CardContent>
<div className='overflow-x-auto'>
<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>
{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 table={table} />}
count={table.getFilteredRowModel().rows.length}
rowsPerPage={table.getState().pagination.pageSize}
page={table.getState().pagination.pageIndex}
onPageChange={(_, page) => {
table.setPageIndex(page)
}}
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>
)
@@ -4,20 +4,19 @@
import Grid from '@mui/material/Grid2'
// Type Imports
import type { OrderType } from '@/types/apps/ecommerceTypes'
// Component Imports
import OrderCard from './OrderCard'
import OrderListTable from './OrderListTable'
const OrderList = ({ orderData }: { orderData?: OrderType[] }) => {
const OrderList = () => {
return (
<Grid container spacing={6}>
<Grid size={{ xs: 12 }}>
<OrderCard />
</Grid>
<Grid size={{ xs: 12 }}>
<OrderListTable orderData={orderData} />
<OrderListTable />
</Grid>
</Grid>
)
@@ -8,28 +8,46 @@ import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../../../../../redux-store'
import { CircularProgress } from '@mui/material'
import { resetProduct } from '../../../../../redux-store/slices/product'
import { useParams } from 'next/navigation'
const ProductAddHeader = () => {
const dispatch = useDispatch()
const { mutate, isPending } = useProductsMutation.createProduct()
const params = useParams()
const { mutate: createProduct, isPending: isCreating } = useProductsMutation.createProduct()
const { mutate: updateProduct, isPending: isUpdating } = useProductsMutation.updateProduct()
const { productRequest } = useSelector((state: RootState) => state.productReducer)
const isEdit = !!params?.id
const handleSubmit = () => {
const { cost, price, ...rest } = productRequest
const newProductRequest = { ...rest, cost: Number(cost), price: Number(price) }
mutate(newProductRequest, {
onSuccess: () => {
dispatch(resetProduct())
}
})
if (isEdit) {
updateProduct(
{ id: params?.id as string, payload: newProductRequest },
{
onSuccess: () => {
dispatch(resetProduct())
}
}
)
} else {
createProduct(newProductRequest, {
onSuccess: () => {
dispatch(resetProduct())
}
})
}
}
return (
<div className='flex flex-wrap sm:items-center justify-between max-sm:flex-col gap-6'>
<div>
<Typography variant='h4' className='mbe-1'>
Add a new product
{isEdit ? 'Edit Product' : 'Add a new product'}
</Typography>
<Typography>Orders placed across your store</Typography>
</div>
@@ -38,8 +56,9 @@ const ProductAddHeader = () => {
Discard
</Button>
<Button variant='tonal'>Save Draft</Button>
<Button variant='contained' disabled={isPending} onClick={handleSubmit}>
Publish Product {isPending && <CircularProgress color='inherit' size={16} className='ml-2' />}
<Button variant='contained' disabled={isEdit ? isUpdating : isCreating} onClick={handleSubmit}>
{isEdit ? 'Update Product' : 'Publish Product'}
{(isCreating || isUpdating) && <CircularProgress color='inherit' size={16} className='ml-2' />}
</Button>
</div>
</div>
@@ -26,8 +26,11 @@ import '@/libs/styles/tiptapEditor.css'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../../../../../redux-store'
import { setProductField } from '@/redux-store/slices/product'
import { setProduct, setProductField } from '@/redux-store/slices/product'
import { useEffect } from 'react'
import { useParams } from 'next/navigation'
import { useProductsQuery } from '../../../../../services/queries/products'
import Loading from '../../../../../components/layout/shared/Loading'
const EditorToolbar = ({ editor }: { editor: Editor | null }) => {
if (!editor) {
@@ -120,8 +123,19 @@ const EditorToolbar = ({ editor }: { editor: Editor | null }) => {
const ProductInformation = () => {
const dispatch = useDispatch()
const params = useParams()
const { data: product, isLoading, error } = useProductsQuery.getProductById(params?.id as string)
const { name, sku, barcode, description } = useSelector((state: RootState) => state.productReducer.productRequest)
const isEdit = !!params?.id
useEffect(() => {
if (product) {
dispatch(setProduct(product))
}
}, [product, dispatch])
const handleInputChange = (field: any, value: any) => {
dispatch(setProductField({ field, value }))
}
@@ -160,6 +174,8 @@ const ProductInformation = () => {
}
}, [editor])
if (isLoading) return <Loading />
return (
<Card>
<CardHeader title='Product Information' />
@@ -170,7 +186,7 @@ const ProductInformation = () => {
fullWidth
label='Product Name'
placeholder='iPhone 14'
value={name}
value={name || ''}
onChange={e => handleInputChange('name', e.target.value)}
/>
</Grid>
@@ -179,7 +195,7 @@ const ProductInformation = () => {
fullWidth
label='SKU'
placeholder='FXSK123U'
value={sku}
value={sku || ''}
onChange={e => handleInputChange('sku', e.target.value)}
/>
</Grid>
@@ -188,7 +204,7 @@ const ProductInformation = () => {
fullWidth
label='Barcode'
placeholder='0123-4567'
value={barcode}
value={barcode || ''}
onChange={e => handleInputChange('barcode', e.target.value)}
/>
</Grid>
@@ -1,95 +1,67 @@
// React Imports
import { useState, useRef } from 'react'
import type { ChangeEvent } from 'react'
import { useState } from 'react'
// MUI Imports
import Button from '@mui/material/Button'
import Divider from '@mui/material/Divider'
import Drawer from '@mui/material/Drawer'
import IconButton from '@mui/material/IconButton'
import MenuItem from '@mui/material/MenuItem'
import Typography from '@mui/material/Typography'
import Divider from '@mui/material/Divider'
import InputAdornment from '@mui/material/InputAdornment'
// Third-party Imports
import { useForm, Controller } from 'react-hook-form'
// Type Imports
import type { categoryType } from './ProductCategoryTable'
// Components Imports
import CustomTextField from '@core/components/mui/TextField'
import { useCategoriesMutation } from '../../../../../services/mutations/categories'
import { CategoryRequest } from '../../../../../types/services/category'
type Props = {
open: boolean
handleClose: () => void
categoryData: categoryType[]
setData: (data: categoryType[]) => void
}
type FormValues = {
title: string
description: string
}
const AddCategoryDrawer = (props: Props) => {
// Props
const { open, handleClose, categoryData, setData } = props
const { open, handleClose } = props
const { mutate: createCategory, isPending: isCreating } = useCategoriesMutation.createCategory()
// States
const [fileName, setFileName] = useState('')
const [category, setCategory] = useState('')
const [comment, setComment] = useState('')
const [status, setStatus] = useState('')
// Refs
const fileInputRef = useRef<HTMLInputElement>(null)
// Hooks
const {
control,
reset: resetForm,
handleSubmit,
formState: { errors }
} = useForm<FormValues>({
defaultValues: {
title: '',
description: ''
}
const [formData, setFormData] = useState<CategoryRequest>({
name: '',
description: '',
business_type: ''
})
// Handle Form Submit
const handleFormSubmit = (data: FormValues) => {
const newData = {
id: categoryData.length + 1,
categoryTitle: data.title,
description: data.description,
totalProduct: Math.floor(Math.random() * 9000) + 1000,
totalEarning: Math.floor(Math.random() * 90000) + 10000,
image: `/images/apps/ecommerce/product-${Math.floor(Math.random() * 20) + 1}.png`
}
const handleFormSubmit = (e: any) => {
e.preventDefault()
setData([...categoryData, newData])
handleReset()
createCategory(formData, {
onSuccess: () => {
handleReset()
}
})
}
const handleInputChange = (e: any) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
// Handle Form Reset
const handleReset = () => {
handleClose()
resetForm({ title: '', description: '' })
setFileName('')
setCategory('')
setComment('')
setStatus('')
}
// Handle File Upload
const handleFileUpload = (event: ChangeEvent<HTMLInputElement>) => {
const { files } = event.target
if (files && files.length !== 0) {
setFileName(files[0].name)
}
setFormData({
name: '',
description: '',
business_type: ''
})
}
return (
@@ -109,95 +81,37 @@ const AddCategoryDrawer = (props: Props) => {
</div>
<Divider />
<div className='p-6'>
<form onSubmit={handleSubmit(data => handleFormSubmit(data))} className='flex flex-col gap-5'>
<Controller
name='title'
control={control}
rules={{ required: true }}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
label='Title'
placeholder='Fashion'
{...(errors.title && { error: true, helperText: 'This field is required.' })}
/>
)}
<form onSubmit={handleFormSubmit} className='flex flex-col gap-5'>
<CustomTextField
fullWidth
label='Title'
name='name'
value={formData.name}
onChange={handleInputChange}
placeholder='Minuman'
/>
<Controller
name='description'
control={control}
rules={{ required: true }}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
label='Description'
placeholder='Enter a description...'
{...(errors.description && { error: true, helperText: 'This field is required.' })}
/>
)}
/>
<div className='flex items-end gap-4'>
<CustomTextField
label='Attachment'
placeholder='No file chosen'
value={fileName}
className='flex-auto'
slotProps={{
input: {
readOnly: true,
endAdornment: fileName ? (
<InputAdornment position='end'>
<IconButton size='small' edge='end' onClick={() => setFileName('')}>
<i className='tabler-x' />
</IconButton>
</InputAdornment>
) : null
}
}}
/>
<Button component='label' variant='tonal' htmlFor='contained-button-file' className='min-is-fit'>
Choose
<input hidden id='contained-button-file' type='file' onChange={handleFileUpload} ref={fileInputRef} />
</Button>
</div>
<CustomTextField
select
fullWidth
label='Parent Category'
value={category}
onChange={e => setCategory(e.target.value)}
label='Business Type'
value={formData.business_type}
onChange={e => setFormData({ ...formData, business_type: e.target.value })}
>
<MenuItem value='HouseHold'>HouseHold</MenuItem>
<MenuItem value='Management'>Management</MenuItem>
<MenuItem value='Electronics'>Electronics</MenuItem>
<MenuItem value='Office'>Office</MenuItem>
<MenuItem value='Accessories'>Accessories</MenuItem>
<MenuItem value='restaurant'>Restaurant</MenuItem>
</CustomTextField>
<CustomTextField
fullWidth
label='Comment'
value={comment}
onChange={e => setComment(e.target.value)}
label='Description'
value={formData.description}
name='description'
onChange={handleInputChange}
multiline
rows={4}
placeholder='Write a Comment...'
/>
<CustomTextField
select
fullWidth
label='Category Status'
value={status}
onChange={e => setStatus(e.target.value)}
>
<MenuItem value='Published'>Published</MenuItem>
<MenuItem value='Inactive'>Inactive</MenuItem>
<MenuItem value='Scheduled'>Scheduled</MenuItem>
</CustomTextField>
<div className='flex items-center gap-4'>
<Button variant='contained' type='submit'>
Add
<Button variant='contained' type='submit' disabled={isCreating}>
{isCreating ? 'Add...' : 'Add'}
</Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Discard
@@ -0,0 +1,137 @@
// React Imports
import { useEffect, useState } from 'react'
// MUI Imports
import Button from '@mui/material/Button'
import Divider from '@mui/material/Divider'
import Drawer from '@mui/material/Drawer'
import IconButton from '@mui/material/IconButton'
import MenuItem from '@mui/material/MenuItem'
import Typography from '@mui/material/Typography'
// Third-party Imports
// Type Imports
// Components Imports
import CustomTextField from '@core/components/mui/TextField'
import { useCategoriesMutation } from '../../../../../services/mutations/categories'
import { Category, CategoryRequest } from '../../../../../types/services/category'
type Props = {
open: boolean
handleClose: () => void
data: Category
}
const EditCategoryDrawer = (props: Props) => {
// Props
const { open, handleClose, data } = props
const { mutate: updateCategory, isPending: isCreating } = useCategoriesMutation.updateCategory()
// States
const [formData, setFormData] = useState<CategoryRequest>({
name: '',
description: '',
business_type: ''
})
useEffect(() => {
if (data) {
setFormData({
name: data.name,
description: data.description,
business_type: data.business_type
})
}
}, [data])
// Handle Form Submit
const handleFormSubmit = (e: any) => {
e.preventDefault()
updateCategory({ id: data.id, payload: formData }, {
onSuccess: () => {
handleReset()
}
})
}
const handleInputChange = (e: any) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
// Handle Form Reset
const handleReset = () => {
handleClose()
setFormData({
name: '',
description: '',
business_type: ''
})
}
return (
<Drawer
open={open}
anchor='right'
variant='temporary'
onClose={handleReset}
ModalProps={{ keepMounted: true }}
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
>
<div className='flex items-center justify-between pli-6 plb-5'>
<Typography variant='h5'>Edit Category</Typography>
<IconButton size='small' onClick={handleReset}>
<i className='tabler-x text-textSecondary text-2xl' />
</IconButton>
</div>
<Divider />
<div className='p-6'>
<form onSubmit={handleFormSubmit} className='flex flex-col gap-5'>
<CustomTextField
fullWidth
label='Title'
name='name'
value={formData.name}
onChange={handleInputChange}
placeholder='Minuman'
/>
<CustomTextField
select
fullWidth
label='Business Type'
value={formData.business_type}
onChange={e => setFormData({ ...formData, business_type: e.target.value })}
>
<MenuItem value='restaurant'>Restaurant</MenuItem>
</CustomTextField>
<CustomTextField
fullWidth
label='Description'
value={formData.description || ''}
name='description'
onChange={handleInputChange}
multiline
rows={4}
placeholder='Write a Comment...'
/>
<div className='flex items-center gap-4'>
<Button variant='contained' type='submit' disabled={isCreating}>
{isCreating ? 'Updating...' : 'Update'}
</Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Discard
</Button>
</div>
</form>
</div>
</Drawer>
)
}
export default EditCategoryDrawer
@@ -43,6 +43,9 @@ 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'
import { useCategoriesMutation } from '../../../../../services/mutations/categories'
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
import EditCategoryDrawer from './EditCategoryDrawer'
declare module '@tanstack/table-core' {
interface FilterFns {
@@ -105,9 +108,13 @@ const columnHelper = createColumnHelper<CategoryWithActionsType>()
const ProductCategoryTable = () => {
// States
const [addCategoryOpen, setAddCategoryOpen] = useState(false)
const [editCategoryOpen, setEditCategoryOpen] = useState(false)
const [rowSelection, setRowSelection] = useState({})
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [categoryId, setCategoryId] = useState('')
const [openConfirm, setOpenConfirm] = useState(false)
const [currentCategory, setCurrentCategory] = useState<Category>()
// Fetch products with pagination and search
const { data, isLoading, error, isFetching } = useCategoriesQuery.getCategories({
@@ -115,6 +122,8 @@ const ProductCategoryTable = () => {
limit: pageSize
})
const { mutate: deleteCategory, isPending: isDeleting } = useCategoriesMutation.deleteCategory()
const categories = data?.categories ?? []
const totalCount = data?.total_count ?? 0
@@ -129,6 +138,12 @@ const ProductCategoryTable = () => {
setCurrentPage(0) // Reset to first page
}, [])
const handleDelete = () => {
deleteCategory(categoryId, {
onSuccess: () => setOpenConfirm(false)
})
}
const columns = useMemo<ColumnDef<CategoryWithActionsType, any>[]>(
() => [
{
@@ -162,7 +177,6 @@ const ProductCategoryTable = () => {
<Typography className='font-medium' color='text.primary'>
{row.original.name}
</Typography>
<Typography variant='body2'>{row.original.description}</Typography>
</div>
</div>
)
@@ -175,11 +189,18 @@ const ProductCategoryTable = () => {
header: 'Business Type',
cell: ({ row }) => <Typography>{row.original.business_type}</Typography>
}),
columnHelper.accessor('created_at', {
header: 'Created At',
cell: ({ row }) => <Typography>{row.original.created_at}</Typography>
}),
columnHelper.accessor('actions', {
header: 'Actions',
cell: ({ row }) => (
<div className='flex items-center'>
<IconButton>
<IconButton onClick={() => {
setCurrentCategory(row.original)
setEditCategoryOpen(!editCategoryOpen)
}}>
<i className='tabler-edit text-textSecondary' />
</IconButton>
<OptionMenu
@@ -190,7 +211,12 @@ const ProductCategoryTable = () => {
{
text: 'Delete',
icon: 'tabler-trash',
menuItemProps: { onClick: () => console.log('click') }
menuItemProps: {
onClick: () => {
setCategoryId(row.original.id)
setOpenConfirm(true)
}
}
},
{ text: 'Duplicate', icon: 'tabler-copy' }
]}
@@ -350,12 +376,26 @@ const ProductCategoryTable = () => {
disabled={isLoading}
/>
</Card>
<AddCategoryDrawer
open={addCategoryOpen}
categoryData={categories}
setData={() => {}}
handleClose={() => setAddCategoryOpen(!addCategoryOpen)}
/>
<EditCategoryDrawer
open={editCategoryOpen}
handleClose={() => setEditCategoryOpen(!editCategoryOpen)}
data={currentCategory!}
/>
<ConfirmDeleteDialog
open={openConfirm}
onClose={() => setOpenConfirm(false)}
onConfirm={handleDelete}
isLoading={isDeleting}
title='Delete Category'
message='Are you sure you want to delete this category? This action cannot be undone.'
/>
</>
)
}
@@ -45,6 +45,8 @@ import { Box, CircularProgress } from '@mui/material'
import Loading from '../../../../../components/layout/shared/Loading'
import { useProductsQuery } from '../../../../../services/queries/products'
import { Product } from '../../../../../types/services/product'
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
import { useProductsMutation } from '../../../../../services/mutations/products'
declare module '@tanstack/table-core' {
interface FilterFns {
@@ -108,6 +110,8 @@ const ProductListTable = () => {
const [rowSelection, setRowSelection] = useState({})
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [openConfirm, setOpenConfirm] = useState(false)
const [productId, setProductId] = useState('')
// Hooks
const { lang: locale } = useParams()
@@ -118,6 +122,8 @@ const ProductListTable = () => {
limit: pageSize
})
const { mutate: deleteProduct, isPending: isDeleting } = useProductsMutation.deleteProduct()
const products = data?.products ?? []
const totalCount = data?.total_count ?? 0
@@ -132,6 +138,12 @@ const ProductListTable = () => {
setCurrentPage(0) // Reset to first page
}, [])
const handleDelete = () => {
deleteProduct(productId, {
onSuccess: () => setOpenConfirm(false)
})
}
const columns = useMemo<ColumnDef<ProductWithActionsType, any>[]>(
() => [
{
@@ -213,7 +225,10 @@ const ProductListTable = () => {
header: 'Actions',
cell: ({ row }) => (
<div className='flex items-center'>
<IconButton>
<IconButton
LinkComponent={Link}
href={getLocalizedUrl(`/apps/ecommerce/products/${row.original.id}/edit`, locale as Locale)}
>
<i className='tabler-edit text-textSecondary' />
</IconButton>
<OptionMenu
@@ -224,7 +239,12 @@ const ProductListTable = () => {
{
text: 'Delete',
icon: 'tabler-trash',
menuItemProps: { onClick: () => console.log('click') }
menuItemProps: {
onClick: () => {
setOpenConfirm(true)
setProductId(row.original.id)
}
}
},
{ text: 'Duplicate', icon: 'tabler-copy' }
]}
@@ -397,6 +417,13 @@ const ProductListTable = () => {
disabled={isLoading}
/>
</Card>
<ConfirmDeleteDialog
open={openConfirm}
onClose={() => setOpenConfirm(false)}
onConfirm={handleDelete}
isLoading={isDeleting}
/>
</>
)
}
@@ -0,0 +1,203 @@
// React Imports
import { useMemo, useState } from 'react'
// MUI Imports
import Button from '@mui/material/Button'
import Divider from '@mui/material/Divider'
import Drawer from '@mui/material/Drawer'
import IconButton from '@mui/material/IconButton'
import Typography from '@mui/material/Typography'
// Third-party Imports
// Type Imports
// Components Imports
import CustomTextField from '@core/components/mui/TextField'
import { Autocomplete, CircularProgress } from '@mui/material'
import { useDebounce } from 'use-debounce'
import { useInventoriesMutation } from '../../../../services/mutations/inventories'
import { useOutletsQuery } from '../../../../services/queries/outlets'
import { useProductsQuery } from '../../../../services/queries/products'
import { InventoryAdjustRequest } from '../../../../types/services/inventory'
type Props = {
open: boolean
handleClose: () => void
}
const AdjustmentStockDrawer = (props: Props) => {
// Props
const { open, handleClose } = props
const { mutate: adjustInventory, isPending: isCreating } = useInventoriesMutation.adjustInventory()
// States
const [productInput, setProductInput] = useState('')
const [productDebouncedInput] = useDebounce(productInput, 500) // debounce for better UX
const [outletInput, setOutletInput] = useState('')
const [outletDebouncedInput] = useDebounce(outletInput, 500) // debounce for better UX
const [formData, setFormData] = useState<InventoryAdjustRequest>({
product_id: '',
outlet_id: '',
delta: 0,
reason: ''
})
const { data: outlets, isLoading: outletsLoading } = useOutletsQuery.getOutlets({
search: outletDebouncedInput
})
const { data: products, isLoading } = useProductsQuery.getProducts({
search: productDebouncedInput
})
const outletOptions = useMemo(() => outlets?.outlets || [], [outlets])
const options = useMemo(() => products?.products || [], [products])
// Handle Form Submit
const handleFormSubmit = (e: any) => {
e.preventDefault()
adjustInventory(
{ ...formData, delta: Number(formData.delta) },
{
onSuccess: () => {
handleReset()
}
}
)
}
const handleInputChange = (e: any) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
// Handle Form Reset
const handleReset = () => {
handleClose()
setFormData({
product_id: '',
outlet_id: '',
delta: 0,
reason: ''
})
}
return (
<Drawer
open={open}
anchor='right'
variant='temporary'
onClose={handleReset}
ModalProps={{ keepMounted: true }}
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
>
<div className='flex items-center justify-between pli-6 plb-5'>
<Typography variant='h5'>Adjust Inventory</Typography>
<IconButton size='small' onClick={handleReset}>
<i className='tabler-x text-textSecondary text-2xl' />
</IconButton>
</div>
<Divider />
<div className='p-6'>
<form onSubmit={handleFormSubmit} className='flex flex-col gap-5'>
<Autocomplete
options={outletOptions}
loading={outletsLoading}
getOptionLabel={option => option.name}
value={outletOptions.find(p => p.id === formData.outlet_id) || null}
onInputChange={(event, newOutlettInput) => {
setOutletInput(newOutlettInput)
}}
onChange={(event, newValue) => {
setFormData({
...formData,
outlet_id: newValue?.id || ''
})
}}
renderInput={params => (
<CustomTextField
{...params}
className=''
label='Outlet'
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{outletsLoading && <CircularProgress size={18} />}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
/>
<Autocomplete
options={options}
loading={isLoading}
getOptionLabel={option => option.name}
value={options.find(p => p.id === formData.product_id) || null}
onInputChange={(event, newProductInput) => {
setProductInput(newProductInput)
}}
onChange={(event, newValue) => {
setFormData({
...formData,
product_id: newValue?.id || ''
})
}}
renderInput={params => (
<CustomTextField
{...params}
className=''
label='Product'
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{isLoading && <CircularProgress size={18} />}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
/>
<CustomTextField
fullWidth
label='Delta'
name='delta'
value={formData.delta}
onChange={handleInputChange}
placeholder='0'
/>
<CustomTextField
fullWidth
label='Reason'
value={formData.reason}
name='reason'
onChange={handleInputChange}
multiline
rows={4}
placeholder='Write a Comment...'
/>
<div className='flex items-center gap-4'>
<Button variant='contained' type='submit' disabled={isCreating}>
{isCreating ? 'Adjusting...' : 'Adjust'}
</Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Discard
</Button>
</div>
</form>
</div>
</Drawer>
)
}
export default AdjustmentStockDrawer
@@ -0,0 +1,392 @@
'use client'
// React Imports
import { useCallback, useEffect, useMemo, useState } from 'react'
// Next Imports
import { useParams } from 'next/navigation'
// 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 { useInventoriesQuery } from '../../../../services/queries/inventories'
import { Inventory } from '../../../../types/services/inventory'
import AdjustmentStockDrawer from './AdjustmentStockDrawer'
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 } = useInventoriesQuery.getInventories({
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' />}
>
Adjust 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>
<AdjustmentStockDrawer 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
@@ -0,0 +1,191 @@
// React Imports
import { useMemo, useState } from 'react'
// MUI Imports
import Button from '@mui/material/Button'
import Divider from '@mui/material/Divider'
import Drawer from '@mui/material/Drawer'
import IconButton from '@mui/material/IconButton'
import Typography from '@mui/material/Typography'
// Third-party Imports
// Type Imports
// Components Imports
import CustomTextField from '@core/components/mui/TextField'
import { Autocomplete, CircularProgress } from '@mui/material'
import { useDebounce } from 'use-debounce'
import { useInventoriesMutation } from '../../../../services/mutations/inventories'
import { useOutletsQuery } from '../../../../services/queries/outlets'
import { useProductsQuery } from '../../../../services/queries/products'
import { InventoryRequest } from '../../../../types/services/inventory'
type Props = {
open: boolean
handleClose: () => void
}
const AddStockDrawer = (props: Props) => {
// Props
const { open, handleClose } = props
const { mutate: createInventory, isPending: isCreating } = useInventoriesMutation.createInventory()
// States
const [productInput, setProductInput] = useState('')
const [productDebouncedInput] = useDebounce(productInput, 500) // debounce for better UX
const [outletInput, setOutletInput] = useState('')
const [outletDebouncedInput] = useDebounce(outletInput, 500) // debounce for better UX
const [formData, setFormData] = useState<InventoryRequest>({
product_id: '',
outlet_id: '',
quantity: 0
})
const { data: outlets, isLoading: outletsLoading } = useOutletsQuery.getOutlets({
search: outletDebouncedInput
})
const { data: products, isLoading } = useProductsQuery.getProducts({
search: productDebouncedInput
})
const outletOptions = useMemo(() => outlets?.outlets || [], [outlets])
const options = useMemo(() => products?.products || [], [products])
// Handle Form Submit
const handleFormSubmit = (e: any) => {
e.preventDefault()
createInventory(
{ ...formData, quantity: Number(formData.quantity) },
{
onSuccess: () => {
handleReset()
}
}
)
}
const handleInputChange = (e: any) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
// Handle Form Reset
const handleReset = () => {
handleClose()
setFormData({
product_id: '',
outlet_id: '',
quantity: 0
})
}
return (
<Drawer
open={open}
anchor='right'
variant='temporary'
onClose={handleReset}
ModalProps={{ keepMounted: true }}
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
>
<div className='flex items-center justify-between pli-6 plb-5'>
<Typography variant='h5'>Add Inventory</Typography>
<IconButton size='small' onClick={handleReset}>
<i className='tabler-x text-textSecondary text-2xl' />
</IconButton>
</div>
<Divider />
<div className='p-6'>
<form onSubmit={handleFormSubmit} className='flex flex-col gap-5'>
<Autocomplete
options={outletOptions}
loading={outletsLoading}
getOptionLabel={option => option.name}
value={outletOptions.find(p => p.id === formData.outlet_id) || null}
onInputChange={(event, newOutlettInput) => {
setOutletInput(newOutlettInput)
}}
onChange={(event, newValue) => {
setFormData({
...formData,
outlet_id: newValue?.id || ''
})
}}
renderInput={params => (
<CustomTextField
{...params}
className=''
label='Outlet'
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{outletsLoading && <CircularProgress size={18} />}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
/>
<Autocomplete
options={options}
loading={isLoading}
getOptionLabel={option => option.name}
value={options.find(p => p.id === formData.product_id) || null}
onInputChange={(event, newProductInput) => {
setProductInput(newProductInput)
}}
onChange={(event, newValue) => {
setFormData({
...formData,
product_id: newValue?.id || ''
})
}}
renderInput={params => (
<CustomTextField
{...params}
className=''
label='Product'
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{isLoading && <CircularProgress size={18} />}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
/>
<CustomTextField
fullWidth
label='Quantity'
name='quantity'
value={formData.quantity}
onChange={handleInputChange}
placeholder='0'
/>
<div className='flex items-center gap-4'>
<Button variant='contained' type='submit' disabled={isCreating}>
{isCreating ? 'Add...' : 'Add'}
</Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Discard
</Button>
</div>
</form>
</div>
</Drawer>
)
}
export default AddStockDrawer
@@ -0,0 +1,392 @@
'use client'
// React Imports
import { useCallback, useEffect, useMemo, useState } from 'react'
// Next Imports
import { useParams } from 'next/navigation'
// 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 { useInventoriesQuery } 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 } = useInventoriesQuery.getInventories({
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
+107
View File
@@ -0,0 +1,107 @@
// React Imports
import { useState, useEffect } from 'react'
// MUI Imports
import Grid from '@mui/material/Grid2'
import CardContent from '@mui/material/CardContent'
import MenuItem from '@mui/material/MenuItem'
// Type Imports
import type { ProductType } from '@/types/apps/ecommerceTypes'
// Component Imports
import CustomTextField from '@core/components/mui/TextField'
import { Product } from '../../../../types/services/product'
type ProductStockType = { [key: string]: boolean }
// Vars
const productStockObj: ProductStockType = {
'In Stock': true,
'Out of Stock': false
}
const TableFilters = ({ setData, productData }: { setData: (data: Product[]) => void; productData?: Product[] }) => {
// States
const [category, setCategory] = useState<Product['category_id']>('')
const [stock, setStock] = useState('')
const [status, setStatus] = useState<Product['name']>('')
useEffect(
() => {
const filteredData = productData?.filter(product => {
if (category && product.category_id !== category) return false
if (stock && product.name !== stock) return false
if (status && product.name !== status) return false
return true
})
setData(filteredData ?? [])
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[category, stock, status, productData]
)
return (
<CardContent>
<Grid container spacing={6}>
<Grid size={{ xs: 12, sm: 4 }}>
<CustomTextField
select
fullWidth
id='select-status'
value={status}
onChange={e => setStatus(e.target.value)}
slotProps={{
select: { displayEmpty: true }
}}
>
<MenuItem value=''>Select Status</MenuItem>
<MenuItem value='Scheduled'>Scheduled</MenuItem>
<MenuItem value='Published'>Publish</MenuItem>
<MenuItem value='Inactive'>Inactive</MenuItem>
</CustomTextField>
</Grid>
<Grid size={{ xs: 12, sm: 4 }}>
<CustomTextField
select
fullWidth
id='select-category'
value={category}
onChange={e => setCategory(e.target.value)}
slotProps={{
select: { displayEmpty: true }
}}
>
<MenuItem value=''>Select Category</MenuItem>
<MenuItem value='Accessories'>Accessories</MenuItem>
<MenuItem value='Home Decor'>Home Decor</MenuItem>
<MenuItem value='Electronics'>Electronics</MenuItem>
<MenuItem value='Shoes'>Shoes</MenuItem>
<MenuItem value='Office'>Office</MenuItem>
<MenuItem value='Games'>Games</MenuItem>
</CustomTextField>
</Grid>
<Grid size={{ xs: 12, sm: 4 }}>
<CustomTextField
select
fullWidth
id='select-stock'
value={stock}
onChange={e => setStock(e.target.value as string)}
slotProps={{
select: { displayEmpty: true }
}}
>
<MenuItem value=''>Select Stock</MenuItem>
<MenuItem value='In Stock'>In Stock</MenuItem>
<MenuItem value='Out of Stock'>Out of Stock</MenuItem>
</CustomTextField>
</Grid>
</Grid>
</CardContent>
)
}
export default TableFilters