feat: payment & customer

This commit is contained in:
ferdiansyah783 2025-08-07 14:31:42 +07:00
parent a72a64215a
commit a5d22db27b
35 changed files with 2033 additions and 599 deletions

View File

@ -1,7 +1,8 @@
// MUI Imports // MUI Imports
import StockListTable from "../../../../../../../../views/apps/ecommerce/stock/adjustment/StockListTable"
// Component Imports // Component Imports
import StockListTable from '../../../../../../../views/apps/stock/adjustment/StockListTable'
// Data Imports // Data Imports

View File

@ -1,7 +1,6 @@
// MUI Imports // MUI Imports
// Component Imports import StockListTable from "../../../../../../../../views/apps/ecommerce/stock/list/StockListTable"
import StockListTable from '../../../../../../../views/apps/stock/list/StockListTable'
// Data Imports // Data Imports

View File

@ -1,8 +1,8 @@
// Component Imports // Component Imports
import ProductUnitTable from '../../../../../../../../views/apps/ecommerce/products/units/ProductUnitTable' import ProductIngredientTable from '../../../../../../../../views/apps/ecommerce/products/ingredient/ProductIngredientTable'
const eCommerceProductsIngredient = () => { const eCommerceProductsIngredient = () => {
return <ProductUnitTable /> return <ProductIngredientTable />
} }
export default eCommerceProductsIngredient export default eCommerceProductsIngredient

View File

@ -0,0 +1,25 @@
import PaymentMethodListTable from '../../../../../../../../views/apps/finance/payment-methods/list/PaymentMethodListTable'
/**
* ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the
* ! `.env` file found at root of your project and also update the API endpoints like `/apps/ecommerce` in below example.
* ! Also, remove the above server action import and the action itself from the `src/app/server/actions.ts` file to clean up unused code
* ! because we've used the server action for getting our static data.
*/
/* const getEcommerceData = async () => {
// Vars
const res = await fetch(`${process.env.API_URL}/apps/ecommerce`)
if (!res.ok) {
throw new Error('Failed to fetch ecommerce data')
}
return res.json()
} */
const PaymentMethodListTablePage = async () => {
return <PaymentMethodListTable />
}
export default PaymentMethodListTablePage

View File

@ -0,0 +1,26 @@
/**
* ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the
* ! `.env` file found at root of your project and also update the API endpoints like `/apps/ecommerce` in below example.
* ! Also, remove the above server action import and the action itself from the `src/app/server/actions.ts` file to clean up unused code
* ! because we've used the server action for getting our static data.
*/
import OrganizationOutletListTable from "../../../../../../../../views/apps/organization/outlets/list/OrganizationOutletListTable"
/* const getEcommerceData = async () => {
// Vars
const res = await fetch(`${process.env.API_URL}/apps/ecommerce`)
if (!res.ok) {
throw new Error('Failed to fetch ecommerce data')
}
return res.json()
} */
const OutletListTablePage = async () => {
return <OrganizationOutletListTable />
}
export default OutletListTablePage

View File

@ -90,6 +90,8 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
<MenuItem href={`/${locale}/apps/ecommerce/dashboard`}>{dictionary['navigation'].dashboard}</MenuItem> <MenuItem href={`/${locale}/apps/ecommerce/dashboard`}>{dictionary['navigation'].dashboard}</MenuItem>
<SubMenu label={dictionary['navigation'].products}> <SubMenu label={dictionary['navigation'].products}>
<MenuItem href={`/${locale}/apps/ecommerce/products/list`}>{dictionary['navigation'].list}</MenuItem> <MenuItem href={`/${locale}/apps/ecommerce/products/list`}>{dictionary['navigation'].list}</MenuItem>
<MenuItem className='hidden' href={`/${locale}/apps/ecommerce/products/${params.id}/detail`}>{dictionary['navigation'].details}</MenuItem>
<MenuItem className='hidden' href={`/${locale}/apps/ecommerce/products/${params.id}/edit`}>{dictionary['navigation'].edit}</MenuItem>
<MenuItem href={`/${locale}/apps/ecommerce/products/add`}>{dictionary['navigation'].add}</MenuItem> <MenuItem href={`/${locale}/apps/ecommerce/products/add`}>{dictionary['navigation'].add}</MenuItem>
<MenuItem href={`/${locale}/apps/ecommerce/products/category`}> <MenuItem href={`/${locale}/apps/ecommerce/products/category`}>
{dictionary['navigation'].category} {dictionary['navigation'].category}
@ -105,15 +107,21 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
<SubMenu label={dictionary['navigation'].customers}> <SubMenu label={dictionary['navigation'].customers}>
<MenuItem href={`/${locale}/apps/ecommerce/customers/list`}>{dictionary['navigation'].list}</MenuItem> <MenuItem href={`/${locale}/apps/ecommerce/customers/list`}>{dictionary['navigation'].list}</MenuItem>
</SubMenu> </SubMenu>
{/* <MenuItem href={`/${locale}/apps/ecommerce/manage-reviews`}> <SubMenu label={dictionary['navigation'].stock}>
{dictionary['navigation'].manageReviews} <MenuItem href={`/${locale}/apps/ecommerce/inventory/list`}>{dictionary['navigation'].list}</MenuItem>
</MenuItem> <MenuItem href={`/${locale}/apps/ecommerce/inventory/adjustment`}>{dictionary['navigation'].addjustment}</MenuItem>
<MenuItem href={`/${locale}/apps/ecommerce/referrals`}>{dictionary['navigation'].referrals}</MenuItem> */} </SubMenu>
<MenuItem href={`/${locale}/apps/ecommerce/settings`}>{dictionary['navigation'].settings}</MenuItem> <MenuItem href={`/${locale}/apps/ecommerce/settings`}>{dictionary['navigation'].settings}</MenuItem>
</SubMenu> </SubMenu>
<SubMenu label={dictionary['navigation'].stock} icon={<i className='tabler-basket-down' />}> <SubMenu label={dictionary['navigation'].organization} icon={<i className='tabler-sitemap' />}>
<MenuItem href={`/${locale}/apps/stock/list`}>{dictionary['navigation'].list}</MenuItem> <SubMenu label={dictionary['navigation'].outlet}>
<MenuItem href={`/${locale}/apps/stock/adjustment`}>{dictionary['navigation'].addjustment}</MenuItem> <MenuItem href={`/${locale}/apps/organization/outlets/list`}>{dictionary['navigation'].list}</MenuItem>
</SubMenu>
</SubMenu>
<SubMenu label={dictionary['navigation'].finance} icon={<i className='tabler-coins' />}>
<SubMenu label={dictionary['navigation'].paymentMethods}>
<MenuItem href={`/${locale}/apps/finance/payment-methods/list`}>{dictionary['navigation'].list}</MenuItem>
</SubMenu>
</SubMenu> </SubMenu>
<SubMenu label={dictionary['navigation'].user} icon={<i className='tabler-user' />}> <SubMenu label={dictionary['navigation'].user} icon={<i className='tabler-user' />}>
<MenuItem href={`/${locale}/apps/user/list`}>{dictionary['navigation'].list}</MenuItem> <MenuItem href={`/${locale}/apps/user/list`}>{dictionary['navigation'].list}</MenuItem>

View File

@ -21,6 +21,10 @@
"add": "يضيف", "add": "يضيف",
"addjustment": "تعديل", "addjustment": "تعديل",
"category": "فئة", "category": "فئة",
"finance": "مالية",
"paymentMethods": "طرق الدفع",
"organization": "المنظمة",
"outlet": "مخزن",
"units": "وحدات", "units": "وحدات",
"ingredients": "مكونات", "ingredients": "مكونات",
"orders": "أوامر", "orders": "أوامر",

View File

@ -22,6 +22,10 @@
"addjustment": "Addjustment", "addjustment": "Addjustment",
"category": "Category", "category": "Category",
"units": "Units", "units": "Units",
"finance": "Finance",
"paymentMethods": "Payment Methods",
"organization": "Organization",
"outlet": "Outlet",
"ingredients": "Ingredients", "ingredients": "Ingredients",
"orders": "Orders", "orders": "Orders",
"details": "Details", "details": "Details",

View File

@ -21,6 +21,10 @@
"add": "Ajouter", "add": "Ajouter",
"addjustment": "Ajustement", "addjustment": "Ajustement",
"category": "Catégorie", "category": "Catégorie",
"finance": "Finance",
"paymentMethods": "Méthodes de paiement",
"organization": "Organisation",
"outlet": "Point de vente",
"units": "Unites", "units": "Unites",
"ingredients": "Ingrédients", "ingredients": "Ingrédients",
"orders": "Ordres", "orders": "Ordres",

View File

@ -3,11 +3,13 @@ import { configureStore } from '@reduxjs/toolkit'
import productReducer from '@/redux-store/slices/product' import productReducer from '@/redux-store/slices/product'
import customerReducer from '@/redux-store/slices/customer' import customerReducer from '@/redux-store/slices/customer'
import paymentMethodReducer from '@/redux-store/slices/paymentMethod'
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
productReducer, productReducer,
customerReducer customerReducer,
paymentMethodReducer
}, },
middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false }) middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false })
}) })

View File

@ -0,0 +1,37 @@
// Third-party Imports
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
// Type Imports
// Data Imports
import { PaymentMethod } from '../../types/services/paymentMethod'
const initialState: { currentPaymentMethod: PaymentMethod } = {
currentPaymentMethod: {
id: '',
organization_id: '',
name: '',
type: '',
is_active: true,
created_at: '',
updated_at: ''
}
}
export const paymentMethodSlice = createSlice({
name: 'paymentMethod',
initialState,
reducers: {
setPaymentMethod: (state, action: PayloadAction<PaymentMethod>) => {
state.currentPaymentMethod = action.payload
},
resetPaymentMethod: state => {
state.currentPaymentMethod = initialState.currentPaymentMethod
}
}
})
export const { setPaymentMethod, resetPaymentMethod } = paymentMethodSlice.actions
export default paymentMethodSlice.reducer

View File

@ -0,0 +1,52 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '../api'
import { toast } from 'react-toastify'
import { PaymentMethodRequest } from '../../types/services/paymentMethod'
export const usePaymentMethodsMutation = () => {
const queryClient = useQueryClient()
const createPaymentMethod = useMutation({
mutationFn: async (newPaymentMethod: PaymentMethodRequest) => {
const response = await api.post('/payment-methods', newPaymentMethod)
return response.data
},
onSuccess: () => {
toast.success('PaymentMethod created successfully!')
queryClient.invalidateQueries({ queryKey: ['payment-methods'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
}
})
const updatePaymentMethod = useMutation({
mutationFn: async ({ id, payload }: { id: string; payload: PaymentMethodRequest }) => {
const response = await api.put(`/payment-methods/${id}`, payload)
return response.data
},
onSuccess: () => {
toast.success('PaymentMethod updated successfully!')
queryClient.invalidateQueries({ queryKey: ['payment-methods'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
}
})
const deletePaymentMethod = useMutation({
mutationFn: async (id: string) => {
const response = await api.delete(`/payment-methods/${id}`)
return response.data
},
onSuccess: () => {
toast.success('PaymentMethod deleted successfully!')
queryClient.invalidateQueries({ queryKey: ['payment-methods'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
}
})
return { createPaymentMethod, updatePaymentMethod, deletePaymentMethod }
}

View File

@ -13,6 +13,7 @@ export const useProductsMutation = () => {
}, },
onSuccess: () => { onSuccess: () => {
toast.success('Product created successfully!') toast.success('Product created successfully!')
queryClient.invalidateQueries({ queryKey: ['products'] })
}, },
onError: (error: any) => { onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed') toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
@ -26,6 +27,7 @@ export const useProductsMutation = () => {
}, },
onSuccess: () => { onSuccess: () => {
toast.success('Product updated successfully!') toast.success('Product updated successfully!')
queryClient.invalidateQueries({ queryKey: ['products'] })
}, },
onError: (error: any) => { onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed') toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')

View File

@ -0,0 +1,36 @@
import { useQuery } from '@tanstack/react-query'
import { PaymentMethods } from '../../types/services/paymentMethod'
import { api } from '../api'
interface PaymentMethodsQueryParams {
page?: number
limit?: number
search?: string
}
export function usePaymentMethods(params: PaymentMethodsQueryParams = {}) {
const { page = 1, limit = 10, search = '', ...filters } = params
return useQuery<PaymentMethods>({
queryKey: ['payment-methods', { 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)
}
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
queryParams.append(key, value.toString())
}
})
const res = await api.get(`/payment-methods?${queryParams.toString()}`)
return res.data.data
}
})
}

View File

@ -12,8 +12,8 @@ export interface Order {
outlet_id: string outlet_id: string
user_id: string user_id: string
table_number: string table_number: string
order_type: 'dineIn' | 'takeAway' | 'delivery' order_type: string
status: 'pending' | 'inProgress' | 'completed' | 'cancelled' status: string
subtotal: number subtotal: number
tax_amount: number tax_amount: number
discount_amount: number discount_amount: number
@ -39,7 +39,7 @@ export interface OrderItem {
total_price: number total_price: number
modifiers: any[] modifiers: any[]
notes: string notes: string
status: 'pending' | 'completed' | 'cancelled' status: string
created_at: string created_at: string
updated_at: string updated_at: string
} }

View File

@ -10,10 +10,14 @@ export interface PaymentMethod {
id: string; id: string;
organization_id: string; organization_id: string;
name: string; name: string;
type: PaymentMethodType; type: string;
is_active: boolean; is_active: boolean;
created_at: string; // ISO 8601 timestamp created_at: string; // ISO 8601 timestamp
updated_at: string; // ISO 8601 timestamp updated_at: string; // ISO 8601 timestamp
} }
export type PaymentMethodType = "cash" | "card" | "edc" | "delivery" | string; export interface PaymentMethodRequest {
name: string;
type: string;
is_active: boolean;
}

17
src/utils/transform.ts Normal file
View File

@ -0,0 +1,17 @@
export const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(amount)
}
export const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}

View File

@ -1,117 +1,93 @@
// React Imports // React Imports
import { useState } from 'react' import { use, useEffect, useState } from 'react'
// MUI Imports // MUI Imports
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import Drawer from '@mui/material/Drawer'
import Divider from '@mui/material/Divider' import Divider from '@mui/material/Divider'
import Drawer from '@mui/material/Drawer'
import IconButton from '@mui/material/IconButton' import IconButton from '@mui/material/IconButton'
import MenuItem from '@mui/material/MenuItem'
import Switch from '@mui/material/Switch' import Switch from '@mui/material/Switch'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
// Third-party Imports // Third-party Imports
import PerfectScrollbar from 'react-perfect-scrollbar' import PerfectScrollbar from 'react-perfect-scrollbar'
import { useForm, Controller } from 'react-hook-form'
// Type Imports // Type Imports
import type { Customer } from '@/types/apps/ecommerceTypes'
// Component Imports // Component Imports
import CustomTextField from '@core/components/mui/TextField' import CustomTextField from '@core/components/mui/TextField'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../../../../../redux-store'
import { useCustomersMutation } from '../../../../../services/mutations/customers'
import { CustomerRequest } from '../../../../../types/services/customer'
import { resetCustomer } from '../../../../../redux-store/slices/customer'
type Props = { type Props = {
open: boolean open: boolean
handleClose: () => void handleClose: () => void
setData: (data: Customer[]) => void
customerData?: Customer[]
}
type FormValidateType = {
fullName: string
email: string
country: string
}
type FormNonValidateType = {
contact: string
address1: string
address2: string
town: string
state: string
postcode: string
}
type countryType = {
country: string
}
export const country: { [key: string]: countryType } = {
india: { country: 'India' },
australia: { country: 'Australia' },
france: { country: 'France' },
brazil: { country: 'Brazil' },
us: { country: 'United States' },
china: { country: 'China' }
} }
// Vars // Vars
const initialData = { const initialData = {
contact: '', name: '',
address1: '', email: '',
address2: '', phone: '',
town: '', address: '',
state: '', is_active: true
postcode: ''
} }
const AddCustomerDrawer = (props: Props) => { const AddCustomerDrawer = (props: Props) => {
const dispatch = useDispatch()
// Props // Props
const { open, handleClose, setData, customerData } = props const { open, handleClose } = props
const { createCustomer, updateCustomer } = useCustomersMutation()
const { currentCustomer } = useSelector((state: RootState) => state.customerReducer)
// States // States
const [formData, setFormData] = useState<FormNonValidateType>(initialData) const [formData, setFormData] = useState<CustomerRequest>(initialData)
// Hooks useEffect(() => {
const { if (currentCustomer.id) {
control, setFormData(currentCustomer)
reset: resetForm, }
handleSubmit, }, [currentCustomer])
formState: { errors }
} = useForm<FormValidateType>({ const handleSubmit = (e: any) => {
defaultValues: { e.preventDefault()
fullName: '',
email: '', if (currentCustomer.id) {
country: '' updateCustomer.mutate(
{ id: currentCustomer.id, payload: formData },
{
onSuccess: () => {
handleReset()
}
}
)
} else {
createCustomer.mutate(formData, {
onSuccess: () => {
handleReset()
} }
}) })
const onSubmit = (data: FormValidateType) => {
const newData: Customer = {
id: (customerData?.length && customerData?.length + 1) || 1,
customer: data.fullName,
customerId: customerData?.[Math.floor(Math.random() * 100) + 1].customerId ?? '1',
email: data.email,
country: `${country[data.country].country}`,
countryCode: 'st',
countryFlag: `/images/cards/${data.country}.png`,
order: Math.floor(Math.random() * 1000) + 1,
totalSpent: Math.floor(Math.random() * (1000000 - 100) + 100) / 100,
avatar: `/images/avatars/${Math.floor(Math.random() * 8) + 1}.png`
} }
setData([...(customerData ?? []), newData])
resetForm({ fullName: '', email: '', country: '' })
setFormData(initialData)
handleClose()
} }
const handleReset = () => { const handleReset = () => {
handleClose() handleClose()
resetForm({ fullName: '', email: '', country: '' }) dispatch(resetCustomer())
setFormData(initialData) setFormData(initialData)
} }
const handleInputChange = (e: any) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
return ( return (
<Drawer <Drawer
open={open} open={open}
@ -122,7 +98,7 @@ const AddCustomerDrawer = (props: Props) => {
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }} sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
> >
<div className='flex items-center justify-between pli-6 plb-5'> <div className='flex items-center justify-between pli-6 plb-5'>
<Typography variant='h5'>Add a Customer</Typography> <Typography variant='h5'>{currentCustomer.id ? 'Edit' : 'Add'} Customer</Typography>
<IconButton size='small' onClick={handleReset}> <IconButton size='small' onClick={handleReset}>
<i className='tabler-x text-2xl' /> <i className='tabler-x text-2xl' />
</IconButton> </IconButton>
@ -130,60 +106,26 @@ const AddCustomerDrawer = (props: Props) => {
<Divider /> <Divider />
<PerfectScrollbar options={{ wheelPropagation: false, suppressScrollX: true }}> <PerfectScrollbar options={{ wheelPropagation: false, suppressScrollX: true }}>
<div className='p-6'> <div className='p-6'>
<form onSubmit={handleSubmit(data => onSubmit(data))} className='flex flex-col gap-5'> <form onSubmit={handleSubmit} className='flex flex-col gap-5'>
<Typography color='text.primary' className='font-medium'> <Typography color='text.primary' className='font-medium'>
Basic Information Basic Information
</Typography> </Typography>
<Controller
name='fullName'
control={control}
rules={{ required: true }}
render={({ field }) => (
<CustomTextField <CustomTextField
{...field}
fullWidth fullWidth
label='Name' label='Name'
name='name'
placeholder='John Doe' placeholder='John Doe'
{...(errors.fullName && { error: true, helperText: 'This field is required.' })} value={formData.name}
onChange={handleInputChange}
/> />
)}
/>
<Controller
name='email'
control={control}
rules={{ required: true }}
render={({ field }) => (
<CustomTextField <CustomTextField
{...field}
fullWidth fullWidth
type='email' type='email'
label='Email' label='Email'
placeholder='johndoe@gmail.com' name='email'
{...(errors.email && { error: true, helperText: 'This field is required.' })} placeholder='johndoe@email'
/> value={formData.email}
)} onChange={handleInputChange}
/>
<Controller
name='country'
control={control}
rules={{ required: true }}
render={({ field }) => (
<CustomTextField
select
fullWidth
id='country'
label='Country'
{...field}
{...(errors.country && { error: true, helperText: 'This field is required.' })}
>
<MenuItem value='india'>India</MenuItem>
<MenuItem value='australia'>Australia</MenuItem>
<MenuItem value='france'>France</MenuItem>
<MenuItem value='brazil'>Brazil</MenuItem>
<MenuItem value='us'>USA</MenuItem>
<MenuItem value='china'>China</MenuItem>
</CustomTextField>
)}
/> />
<Typography color='text.primary' className='font-medium'> <Typography color='text.primary' className='font-medium'>
Shipping Information Shipping Information
@ -191,63 +133,41 @@ const AddCustomerDrawer = (props: Props) => {
<CustomTextField <CustomTextField
fullWidth fullWidth
label='Address Line 1' label='Address Line 1'
name='address1' name='address'
placeholder='45 Roker Terrace' placeholder='45 Roker Terrace'
value={formData.address1} value={formData.address}
onChange={e => setFormData({ ...formData, address1: e.target.value })} onChange={handleInputChange}
/>
<CustomTextField
fullWidth
label='Address Line 2'
name='address2'
placeholder='Street 69'
value={formData.address2}
onChange={e => setFormData({ ...formData, address2: e.target.value })}
/>
<CustomTextField
fullWidth
label='Town'
name='town'
placeholder='New York'
value={formData.town}
onChange={e => setFormData({ ...formData, town: e.target.value })}
/>
<CustomTextField
fullWidth
label='State/Province'
name='state'
placeholder='Southern tip'
value={formData.state}
onChange={e => setFormData({ ...formData, state: e.target.value })}
/>
<CustomTextField
fullWidth
label='Post Code'
name='postcode'
placeholder='734990'
value={formData.postcode}
onChange={e => setFormData({ ...formData, postcode: e.target.value })}
/> />
<CustomTextField <CustomTextField
label='Mobile' label='Mobile'
type='number' type='number'
fullWidth fullWidth
placeholder='+(123) 456-7890' placeholder='+(123) 456-7890'
value={formData.contact} name='phone'
onChange={e => setFormData({ ...formData, contact: e.target.value })} value={formData.phone}
onChange={handleInputChange}
/> />
<div className='flex justify-between'> <div className='flex items-center'>
<div className='flex flex-col items-start gap-1'> <div className='flex flex-col items-start gap-1'>
<Typography color='text.primary' className='font-medium'> <Typography color='text.primary' className='font-medium'>
Use as a billing address? Active
</Typography> </Typography>
<Typography variant='body2'>Please check budget for more info.</Typography>
</div> </div>
<Switch defaultChecked /> <Switch
checked={formData.is_active}
name='is_active'
onChange={e => setFormData({ ...formData, is_active: e.target.checked })}
/>
</div> </div>
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
<Button variant='contained' type='submit'> <Button variant='contained' type='submit' disabled={createCustomer.isPending || updateCustomer.isPending}>
Add {currentCustomer.id
? updateCustomer.isPending
? 'Updating...'
: 'Update'
: createCustomer.isPending
? 'Creating...'
: 'Create'}
</Button> </Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}> <Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Discard Discard

View File

@ -1,11 +1,9 @@
'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 { useParams } from 'next/navigation'
// MUI Imports // MUI Imports
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
@ -24,32 +22,30 @@ import {
createColumnHelper, createColumnHelper,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
getFacetedMinMaxValues,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable useReactTable
} from '@tanstack/react-table' } from '@tanstack/react-table'
import classnames from 'classnames' import classnames from 'classnames'
// Type Imports // Type Imports
import type { Customer } from '@/types/apps/ecommerceTypes'
import type { Locale } from '@configs/i18n'
import type { ThemeColor } from '@core/types'
// Component Imports // Component Imports
import CustomAvatar from '@core/components/mui/Avatar'
import CustomTextField from '@core/components/mui/TextField' import CustomTextField from '@core/components/mui/TextField'
import AddCustomerDrawer from './AddCustomerDrawer' import AddCustomerDrawer from './AddCustomerDrawer'
// Util Imports // Util Imports
import { getInitials } from '@/utils/getInitials'
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, Chip, CircularProgress, IconButton, TablePagination } from '@mui/material'
import { useDispatch } from 'react-redux'
import OptionMenu from '../../../../../@core/components/option-menu'
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
import Loading from '../../../../../components/layout/shared/Loading'
import TablePaginationComponent from '../../../../../components/TablePaginationComponent'
import { setCustomer } from '../../../../../redux-store/slices/customer'
import { useCustomersMutation } from '../../../../../services/mutations/customers'
import { useCustomers } from '../../../../../services/queries/customers'
import { Customer } from '../../../../../types/services/customer'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -60,31 +56,8 @@ declare module '@tanstack/table-core' {
} }
} }
type PayementStatusType = {
text: string
color: ThemeColor
}
type StatusChipColorType = {
color: ThemeColor
}
export const paymentStatus: { [key: number]: PayementStatusType } = {
1: { text: 'Paid', color: 'success' },
2: { text: 'Pending', color: 'warning' },
3: { text: 'Cancelled', color: 'secondary' },
4: { text: 'Failed', color: 'error' }
}
export const statusChipColor: { [key: string]: StatusChipColorType } = {
Delivered: { color: 'success' },
'Out for Delivery': { color: 'primary' },
'Ready to Pickup': { color: 'info' },
Dispatched: { color: 'warning' }
}
type ECommerceOrderTypeWithAction = Customer & { type ECommerceOrderTypeWithAction = Customer & {
action?: string actions?: string
} }
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => { const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
@ -132,15 +105,45 @@ const DebouncedInput = ({
// Column Definitions // Column Definitions
const columnHelper = createColumnHelper<ECommerceOrderTypeWithAction>() const columnHelper = createColumnHelper<ECommerceOrderTypeWithAction>()
const CustomerListTable = ({ customerData }: { customerData?: Customer[] }) => { const CustomerListTable = () => {
const dispatch = useDispatch()
// States // States
const [customerUserOpen, setCustomerUserOpen] = useState(false) const [customerUserOpen, setCustomerUserOpen] = useState(false)
const [rowSelection, setRowSelection] = useState({}) const [rowSelection, setRowSelection] = useState({})
const [data, setData] = useState(...[customerData]) const [currentPage, setCurrentPage] = useState(1)
const [globalFilter, setGlobalFilter] = useState('') const [pageSize, setPageSize] = useState(10)
const [openConfirm, setOpenConfirm] = useState(false)
const [customerId, setCustomerId] = useState('')
const [search, setSearch] = useState('')
// Hooks const { data, isLoading, error, isFetching } = useCustomers({
const { lang: locale } = useParams() page: currentPage,
limit: pageSize,
search
})
const { deleteCustomer } = useCustomersMutation()
const customers = data?.data ?? []
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(1) // Reset to first page
}, [])
const handleDelete = () => {
deleteCustomer.mutate(customerId, {
onSuccess: () => setOpenConfirm(false)
})
}
const columns = useMemo<ColumnDef<ECommerceOrderTypeWithAction, any>[]>( const columns = useMemo<ColumnDef<ECommerceOrderTypeWithAction, any>[]>(
() => [ () => [
@ -166,49 +169,64 @@ const CustomerListTable = ({ customerData }: { customerData?: Customer[] }) => {
/> />
) )
}, },
columnHelper.accessor('customer', { columnHelper.accessor('name', {
header: 'Customers', header: 'Name',
cell: ({ row }) => <Typography color='text.primary'>{row.original.name || '-'}</Typography>
}),
columnHelper.accessor('email', {
header: 'Email',
cell: ({ row }) => <Typography color='text.primary'>{row.original.email || '-'}</Typography>
}),
columnHelper.accessor('phone', {
header: 'Phone',
cell: ({ row }) => <Typography>{row.original.phone || '-'}</Typography>
}),
columnHelper.accessor('address', {
header: 'Address',
cell: ({ row }) => <Typography>{row.original.address || '-'}</Typography>
}),
columnHelper.accessor('is_active', {
header: 'Status',
cell: ({ row }) => ( cell: ({ row }) => (
<div className='flex items-center gap-3'> <Chip
{getAvatar({ avatar: row.original.avatar, customer: row.original.customer })} label={row.original.is_active ? 'Active' : 'Inactive'}
<div className='flex flex-col items-start'> variant='tonal'
<Typography color={row.original.is_active ? 'success' : 'error'}
component={Link} size='small'
color='text.primary' />
href={getLocalizedUrl(`/apps/ecommerce/customers/details/${row.original.customerId}`, locale as Locale)}
className='font-medium hover:text-primary'
>
{row.original.customer}
</Typography>
<Typography variant='body2'>{row.original.email}</Typography>
</div>
</div>
) )
}), }),
columnHelper.accessor('customerId', { columnHelper.accessor('actions', {
header: 'Customer Id', header: 'Actions',
cell: ({ row }) => <Typography color='text.primary'>#{row.original.customerId}</Typography>
}),
columnHelper.accessor('country', {
header: 'Country',
cell: ({ row }) => ( cell: ({ row }) => (
<div className='flex items-center gap-2'> <div className='flex items-center'>
<img src={row.original.countryFlag} height={22} /> <IconButton onClick={() => {
<Typography>{row.original.country}</Typography> dispatch(setCustomer(row.original))
setCustomerUserOpen(true)
}}>
<i className='tabler-edit text-textSecondary' />
</IconButton>
<OptionMenu
iconButtonProps={{ size: 'medium' }}
iconClassName='text-textSecondary'
options={[
{ text: 'Download', icon: 'tabler-download' },
{
text: 'Delete',
icon: 'tabler-trash',
menuItemProps: {
onClick: () => {
setOpenConfirm(true)
setCustomerId(row.original.id)
}
}
},
{ text: 'Duplicate', icon: 'tabler-copy' }
]}
/>
</div> </div>
) ),
}), enableSorting: false
columnHelper.accessor('order', {
header: 'Orders',
cell: ({ row }) => <Typography>{row.original.order}</Typography>
}),
columnHelper.accessor('totalSpent', {
header: 'Total Spent',
cell: ({ row }) => (
<Typography className='font-medium' color='text.primary'>
${row.original.totalSpent.toLocaleString()}
</Typography>
)
}) })
], ],
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -216,63 +234,41 @@ const CustomerListTable = ({ customerData }: { customerData?: Customer[] }) => {
) )
const table = useReactTable({ const table = useReactTable({
data: data as Customer[], data: customers as Customer[],
columns, columns,
filterFns: { filterFns: {
fuzzy: fuzzyFilter fuzzy: fuzzyFilter
}, },
state: { state: {
rowSelection, rowSelection,
globalFilter
},
initialState: {
pagination: { pagination: {
pageSize: 10 pageIndex: currentPage,
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()
}) })
const getAvatar = (params: Pick<Customer, 'avatar' | 'customer'>) => {
const { avatar, customer } = params
if (avatar) {
return <CustomAvatar src={avatar} skin='light' size={34} />
} else {
return (
<CustomAvatar skin='light' size={34}>
{getInitials(customer as string)}
</CustomAvatar>
)
}
}
return ( return (
<> <>
<Card> <Card>
<CardContent className='flex justify-between flex-wrap max-sm:flex-col sm:items-center gap-4'> <CardContent className='flex justify-between flex-wrap max-sm:flex-col sm:items-center gap-4'>
<DebouncedInput <DebouncedInput
value={globalFilter ?? ''} value={search}
onChange={value => setGlobalFilter(String(value))} onChange={value => setSearch(value as string)}
placeholder='Search' placeholder='Search'
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'>
<CustomTextField <CustomTextField
select select
value={table.getState().pagination.pageSize} value={pageSize}
onChange={e => table.setPageSize(Number(e.target.value))} onChange={handlePageSizeChange}
className='is-full sm:is-[70px]' className='is-full sm:is-[70px]'
> >
<MenuItem value='10'>10</MenuItem> <MenuItem value='10'>10</MenuItem>
@ -300,6 +296,9 @@ const CustomerListTable = ({ customerData }: { customerData?: Customer[] }) => {
</div> </div>
</CardContent> </CardContent>
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
{isLoading ? (
<Loading />
) : (
<table className={tableStyles.table}> <table className={tableStyles.table}>
<thead> <thead>
{table.getHeaderGroups().map(headerGroup => ( {table.getHeaderGroups().map(headerGroup => (
@ -353,22 +352,54 @@ const CustomerListTable = ({ customerData }: { customerData?: Customer[] }) => {
</tbody> </tbody>
)} )}
</table> </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
component={() => <TablePaginationComponent table={table} />} <TablePagination
count={table.getFilteredRowModel().rows.length} component={() => (
rowsPerPage={table.getState().pagination.pageSize} <TablePaginationComponent
page={table.getState().pagination.pageIndex} pageIndex={currentPage}
onPageChange={(_, page) => { pageSize={pageSize}
table.setPageIndex(page) totalCount={totalCount}
}} onPageChange={handlePageChange}
/> */} />
)}
count={totalCount}
rowsPerPage={pageSize}
page={currentPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
disabled={isLoading}
/>
</Card> </Card>
<AddCustomerDrawer
open={customerUserOpen} <AddCustomerDrawer open={customerUserOpen} handleClose={() => setCustomerUserOpen(!customerUserOpen)} />
handleClose={() => setCustomerUserOpen(!customerUserOpen)}
setData={setData} <ConfirmDeleteDialog
customerData={data} open={openConfirm}
onClose={() => setOpenConfirm(false)}
onConfirm={handleDelete}
isLoading={deleteCustomer.isPending}
title='Delete Customer'
message='Are you sure you want to delete this customer? This action cannot be undone.'
/> />
</> </>
) )

View File

@ -44,6 +44,7 @@ import { Box, CircularProgress } from '@mui/material'
import Loading from '../../../../../components/layout/shared/Loading' import Loading from '../../../../../components/layout/shared/Loading'
import { useOrders } from '../../../../../services/queries/orders' import { useOrders } from '../../../../../services/queries/orders'
import { Order } from '../../../../../types/services/order' import { Order } from '../../../../../types/services/order'
import { formatCurrency } from '../../../../../utils/transform'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -54,30 +55,6 @@ declare module '@tanstack/table-core' {
} }
} }
type PayementStatusType = {
text: string
color: ThemeColor
colorClassName: string
}
type StatusChipColorType = {
color: ThemeColor
}
export const paymentStatus: { [key: number]: PayementStatusType } = {
1: { text: 'Paid', color: 'success', colorClassName: 'text-success' },
2: { text: 'Pending', color: 'warning', colorClassName: 'text-warning' },
3: { text: 'Cancelled', color: 'secondary', colorClassName: 'text-secondary' },
4: { text: 'Failed', color: 'error', colorClassName: 'text-error' }
}
export const statusChipColor: { [key: string]: StatusChipColorType } = {
Delivered: { color: 'success' },
'Out for Delivery': { color: 'primary' },
'Ready to Pickup': { color: 'info' },
Dispatched: { color: 'warning' }
}
type ECommerceOrderTypeWithAction = Order & { type ECommerceOrderTypeWithAction = Order & {
action?: string action?: string
} }
@ -132,10 +109,12 @@ const OrderListTable = () => {
const [rowSelection, setRowSelection] = useState({}) const [rowSelection, setRowSelection] = useState({})
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10) const [pageSize, setPageSize] = useState(10)
const [search, setSearch] = useState('')
const { data, isLoading, error, isFetching } = useOrders({ const { data, isLoading, error, isFetching } = useOrders({
page: currentPage, page: currentPage,
limit: pageSize limit: pageSize,
search
}) })
// Hooks // Hooks
@ -152,13 +131,9 @@ const OrderListTable = () => {
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10) const newPageSize = parseInt(event.target.value, 10)
setPageSize(newPageSize) setPageSize(newPageSize)
setCurrentPage(0) // Reset to first page setCurrentPage(1) // Reset to first page
}, []) }, [])
// Vars
const paypal = '/images/apps/ecommerce/paypal.png'
const mastercard = '/images/apps/ecommerce/mastercard.png'
const columns = useMemo<ColumnDef<ECommerceOrderTypeWithAction, any>[]>( const columns = useMemo<ColumnDef<ECommerceOrderTypeWithAction, any>[]>(
() => [ () => [
{ {
@ -195,83 +170,39 @@ const OrderListTable = () => {
}), }),
columnHelper.accessor('table_number', { columnHelper.accessor('table_number', {
header: 'Table', header: 'Table',
cell: ({ row }) => <Typography>{row.original.table_number}</Typography> cell: ({ row }) => <Typography>{row.original.table_number || '-'}</Typography>
}), }),
columnHelper.accessor('order_type', { columnHelper.accessor('order_type', {
header: 'Order Type', header: 'Order Type',
cell: ({ row }) => <Typography>{row.original.order_type}</Typography> 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', { columnHelper.accessor('status', {
header: 'Status', header: 'Status',
cell: ({ row }) => <Chip label={row.original.status} color={'default'} variant='tonal' size='small' /> cell: ({ row }) => (
<Chip
label={row.original.status}
color={row.original.status === 'completed' ? 'success' : 'warning'}
variant='tonal'
size='small'
/>
)
}), }),
columnHelper.accessor('subtotal', { columnHelper.accessor('subtotal', {
header: 'SubTotal', header: 'SubTotal',
cell: ({ row }) => <Typography>{row.original.subtotal}</Typography> cell: ({ row }) => <Typography>{formatCurrency(row.original.subtotal)}</Typography>
}), }),
columnHelper.accessor('total_amount', { columnHelper.accessor('total_amount', {
header: 'Total', header: 'Total',
cell: ({ row }) => <Typography>{row.original.total_amount}</Typography> cell: ({ row }) => <Typography>{formatCurrency(row.original.total_amount)}</Typography>
}), }),
columnHelper.accessor('tax_amount', { columnHelper.accessor('tax_amount', {
header: 'Tax', header: 'Tax',
cell: ({ row }) => <Typography>{row.original.tax_amount}</Typography> cell: ({ row }) => <Typography>{formatCurrency(row.original.tax_amount)}</Typography>
}), }),
columnHelper.accessor('discount_amount', { columnHelper.accessor('discount_amount', {
header: 'Discount', header: 'Discount',
cell: ({ row }) => <Typography>{row.original.discount_amount}</Typography> cell: ({ row }) => <Typography>{formatCurrency(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', { columnHelper.accessor('action', {
header: 'Action', header: 'Action',
cell: ({ row }) => ( cell: ({ row }) => (
@ -329,34 +260,20 @@ const OrderListTable = () => {
pageCount: Math.ceil(totalCount / pageSize) pageCount: Math.ceil(totalCount / pageSize)
}) })
const getAvatar = (params: Pick<OrderType, 'avatar' | 'customer'>) => {
const { avatar, customer } = params
if (avatar) {
return <CustomAvatar src={avatar} skin='light' size={34} />
} else {
return (
<CustomAvatar skin='light' size={34}>
{getInitials(customer as string)}
</CustomAvatar>
)
}
}
return ( return (
<Card> <Card>
<CardContent className='flex justify-between max-sm:flex-col sm:items-center gap-4'> <CardContent className='flex justify-between max-sm:flex-col sm:items-center gap-4'>
<DebouncedInput <DebouncedInput
value={''} value={search}
onChange={value => console.log('click')} onChange={value => setSearch(value as string)}
placeholder='Search Order' placeholder='Search Order'
className='sm:is-auto' className='sm:is-auto'
/> />
<div className='flex items-center max-sm:flex-col gap-4 max-sm:is-full is-auto'> <div className='flex items-center max-sm:flex-col gap-4 max-sm:is-full is-auto'>
<CustomTextField <CustomTextField
select select
value={table.getState().pagination.pageSize} value={pageSize}
onChange={e => table.setPageSize(Number(e.target.value))} onChange={handlePageSizeChange}
className='is-[70px] max-sm:is-full' className='is-[70px] max-sm:is-full'
> >
<MenuItem value='10'>10</MenuItem> <MenuItem value='10'>10</MenuItem>

View File

@ -154,7 +154,7 @@ const ProductInformation = () => {
immediatelyRender: false, immediatelyRender: false,
content: ` content: `
<p> <p>
${description} ${description || ''}
</p> </p>
` `
}) })

View File

@ -21,8 +21,12 @@ const ProductVariants = () => {
const { variants } = useSelector((state: RootState) => state.productReducer.productRequest) const { variants } = useSelector((state: RootState) => state.productReducer.productRequest)
const handleAddVariant = () => { const handleAddVariant = () => {
if (!variants) {
dispatch(setProductField({ field: 'variants', value: [{ name: '', cost: 0, price_modifier: 0 }] }))
} else {
dispatch(setProductField({ field: 'variants', value: [...variants, { name: '', cost: 0, price_modifier: 0 }] })) dispatch(setProductField({ field: 'variants', value: [...variants, { name: '', cost: 0, price_modifier: 0 }] }))
} }
}
const handleRemoveVariant = (index: number) => { const handleRemoveVariant = (index: number) => {
const updated = variants.filter((_, i) => i !== index) const updated = variants.filter((_, i) => i !== index)
@ -44,7 +48,8 @@ const ProductVariants = () => {
<CardHeader title='Product Variants' /> <CardHeader title='Product Variants' />
<CardContent> <CardContent>
<Grid container spacing={6}> <Grid container spacing={6}>
{variants && variants.map((variant, index) => ( {variants &&
variants.map((variant, index) => (
<Grid key={index} size={{ xs: 12 }} className='repeater-item'> <Grid key={index} size={{ xs: 12 }} className='repeater-item'>
<Grid container spacing={6}> <Grid container spacing={6}>
<Grid size={{ xs: 12, sm: 4 }}> <Grid size={{ xs: 12, sm: 4 }}>

View File

@ -35,6 +35,7 @@ import { useCategoriesMutation } from '../../../../../services/mutations/categor
import { useCategories } from '../../../../../services/queries/categories' import { useCategories } from '../../../../../services/queries/categories'
import { Category } from '../../../../../types/services/category' import { Category } from '../../../../../types/services/category'
import EditCategoryDrawer from './EditCategoryDrawer' import EditCategoryDrawer from './EditCategoryDrawer'
import { formatDate } from '../../../../../utils/transform'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -104,6 +105,7 @@ const ProductCategoryTable = () => {
const [categoryId, setCategoryId] = useState('') const [categoryId, setCategoryId] = useState('')
const [openConfirm, setOpenConfirm] = useState(false) const [openConfirm, setOpenConfirm] = useState(false)
const [currentCategory, setCurrentCategory] = useState<Category>() const [currentCategory, setCurrentCategory] = useState<Category>()
const [search, setSearch] = useState('')
// Fetch products with pagination and search // Fetch products with pagination and search
const { data, isLoading, error, isFetching } = useCategories({ const { data, isLoading, error, isFetching } = useCategories({
@ -124,7 +126,7 @@ const ProductCategoryTable = () => {
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10) const newPageSize = parseInt(event.target.value, 10)
setPageSize(newPageSize) setPageSize(newPageSize)
setCurrentPage(0) // Reset to first page setCurrentPage(1) // Reset to first page
}, []) }, [])
const handleDelete = () => { const handleDelete = () => {
@ -180,7 +182,7 @@ const ProductCategoryTable = () => {
}), }),
columnHelper.accessor('created_at', { columnHelper.accessor('created_at', {
header: 'Created At', header: 'Created At',
cell: ({ row }) => <Typography>{row.original.created_at}</Typography> cell: ({ row }) => <Typography>{formatDate(row.original.created_at)}</Typography>
}), }),
columnHelper.accessor('actions', { columnHelper.accessor('actions', {
header: 'Actions', header: 'Actions',
@ -247,16 +249,16 @@ 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={'search'} value={search}
onChange={value => console.log(value)} onChange={value => setSearch(value as string)}
placeholder='Search Product' 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'>
<CustomTextField <CustomTextField
select select
value={table.getState().pagination.pageSize} value={pageSize}
onChange={e => table.setPageSize(Number(e.target.value))} onChange={handlePageSizeChange}
className='flex-auto max-sm:is-full sm:is-[70px]' className='flex-auto max-sm:is-full sm:is-[70px]'
> >
<MenuItem value='10'>10</MenuItem> <MenuItem value='10'>10</MenuItem>

View File

@ -22,6 +22,7 @@ import Loading from '../../../../../components/layout/shared/Loading'
import { setProduct } from '../../../../../redux-store/slices/product' import { setProduct } from '../../../../../redux-store/slices/product'
import { useProductById } from '../../../../../services/queries/products' import { useProductById } from '../../../../../services/queries/products'
import { ProductVariant } from '../../../../../types/services/product' import { ProductVariant } from '../../../../../types/services/product'
import { formatCurrency, formatDate } from '../../../../../utils/transform'
// Tabler icons (using class names) // Tabler icons (using class names)
const TablerIcon = ({ name, className = '' }: { name: string; className?: string }) => ( const TablerIcon = ({ name, className = '' }: { name: string; className?: string }) => (
<i className={`tabler-${name} ${className}`} /> <i className={`tabler-${name} ${className}`} />
@ -39,24 +40,6 @@ const ProductDetail = () => {
} }
}, [product, dispatch]) }, [product, dispatch])
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(amount)
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const getBusinessTypeColor = (type: string) => { const getBusinessTypeColor = (type: string) => {
switch (type.toLowerCase()) { switch (type.toLowerCase()) {
case 'restaurant': case 'restaurant':
@ -83,6 +66,11 @@ const ProductDetail = () => {
} }
} }
const getPlainText = (html: string) => {
const doc = new DOMParser().parseFromString(html, 'text/html')
return doc.body.textContent || ''
}
if (isLoading) return <Loading /> if (isLoading) return <Loading />
return ( return (
@ -126,7 +114,7 @@ const ProductDetail = () => {
{product.description && ( {product.description && (
<Typography variant='body1' className='text-gray-600 mb-4'> <Typography variant='body1' className='text-gray-600 mb-4'>
{product.description} {getPlainText(product.description)}
</Typography> </Typography>
)} )}

View File

@ -0,0 +1,393 @@
'use client'
// React Imports
import { useCallback, useEffect, useMemo, useState } from 'react'
// MUI Imports
import Button from '@mui/material/Button'
import Card from '@mui/material/Card'
import Checkbox from '@mui/material/Checkbox'
import IconButton from '@mui/material/IconButton'
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'
// Component Imports
import TablePaginationComponent from '@components/TablePaginationComponent'
import CustomTextField from '@core/components/mui/TextField'
import OptionMenu from '@core/components/option-menu'
// Style Imports
import tableStyles from '@core/styles/table.module.css'
import { Box, Chip, CircularProgress } from '@mui/material'
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
import Loading from '../../../../../components/layout/shared/Loading'
import { useUnitsMutation } from '../../../../../services/mutations/units'
import { useUnits } from '../../../../../services/queries/units'
import { Unit } from '../../../../../types/services/unit'
import { formatDate } from '../../../../../utils/transform'
declare module '@tanstack/table-core' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface FilterMeta {
itemRank: RankingInfo
}
}
type UnitWithActionsType = Unit & {
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<UnitWithActionsType>()
const ProductIngredientTable = () => {
// States
const [addUnitOpen, setAddUnitOpen] = useState(false)
const [editUnitOpen, setEditUnitOpen] = useState(false)
const [rowSelection, setRowSelection] = useState({})
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [unitId, setUnitId] = useState('')
const [openConfirm, setOpenConfirm] = useState(false)
const [currentUnit, setCurrentUnit] = useState<Unit>()
// Fetch products with pagination and search
const { data, isLoading, error, isFetching } = useUnits({
page: currentPage,
limit: pageSize
})
const { mutate: deleteUnit, isPending: isDeleting } = useUnitsMutation().deleteUnit
const units = data?.data ?? []
const totalCount = data?.pagination.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(1) // Reset to first page
}, [])
const handleDelete = () => {
deleteUnit(unitId, {
onSuccess: () => setOpenConfirm(false)
})
}
const columns = useMemo<ColumnDef<UnitWithActionsType, 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('name', {
header: 'Name',
cell: ({ row }) => (
<div className='flex items-center gap-3'>
{/* <img src={row.original.image} width={38} height={38} className='rounded bg-actionHover' /> */}
<div className='flex flex-col items-start'>
<Typography className='font-medium' color='text.primary'>
{row.original.name || '-'}
</Typography>
</div>
</div>
)
}),
columnHelper.accessor('abbreviation', {
header: 'Abbreviation',
cell: ({ row }) => <Typography>{row.original.abbreviation || '-'}</Typography>
}),
columnHelper.accessor('is_active', {
header: 'Status',
cell: ({ row }) => (
<Chip
label={row.original.is_active ? 'Active' : 'Inactive'}
variant='tonal'
color={row.original.is_active ? 'success' : 'error'}
size='small'
/>
)
}),
columnHelper.accessor('created_at', {
header: 'Created Date',
cell: ({ row }) => <Typography>{formatDate(row.original.created_at)}</Typography>
}),
columnHelper.accessor('actions', {
header: 'Actions',
cell: ({ row }) => (
<div className='flex items-center'>
<IconButton
onClick={() => {
setCurrentUnit(row.original)
setEditUnitOpen(!editUnitOpen)
}}
>
<i className='tabler-edit text-textSecondary' />
</IconButton>
<OptionMenu
iconButtonProps={{ size: 'medium' }}
iconClassName='text-textSecondary'
options={[
{ text: 'Download', icon: 'tabler-download' },
{
text: 'Delete',
icon: 'tabler-trash',
menuItemProps: {
onClick: () => {
setUnitId(row.original.id)
setOpenConfirm(true)
}
}
},
{ text: 'Duplicate', icon: 'tabler-copy' }
]}
/>
</div>
),
enableSorting: false
})
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[data]
)
const table = useReactTable({
data: units as Unit[],
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>
<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 max-sm:flex-col items-start sm:items-center gap-4 max-sm:is-full'>
<CustomTextField
select
value={table.getState().pagination.pageSize}
onChange={e => table.setPageSize(Number(e.target.value))}
className='flex-auto max-sm:is-full sm:is-[70px]'
>
<MenuItem value='10'>10</MenuItem>
<MenuItem value='15'>15</MenuItem>
<MenuItem value='25'>25</MenuItem>
</CustomTextField>
<Button
variant='contained'
className='max-sm:is-full'
onClick={() => setAddUnitOpen(!addUnitOpen)}
startIcon={<i className='tabler-plus' />}
>
Add Ingredient
</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>
{/* <AddUnitDrawer open={addUnitOpen} handleClose={() => setAddUnitOpen(!addUnitOpen)} />
<EditUnitDrawer open={editUnitOpen} handleClose={() => setEditUnitOpen(!editUnitOpen)} data={currentUnit!} /> */}
<ConfirmDeleteDialog
open={openConfirm}
onClose={() => setOpenConfirm(false)}
onConfirm={handleDelete}
isLoading={isDeleting}
title='Delete Unit'
message='Are you sure you want to delete this Unit? This action cannot be undone.'
/>
</>
)
}
export default ProductIngredientTable

View File

@ -47,6 +47,7 @@ import { useProducts } from '../../../../../services/queries/products'
import { Product } from '../../../../../types/services/product' import { Product } from '../../../../../types/services/product'
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete' import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
import { useProductsMutation } from '../../../../../services/mutations/products' import { useProductsMutation } from '../../../../../services/mutations/products'
import { formatCurrency } from '../../../../../utils/transform'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -112,6 +113,7 @@ const ProductListTable = () => {
const [pageSize, setPageSize] = useState(10) const [pageSize, setPageSize] = useState(10)
const [openConfirm, setOpenConfirm] = useState(false) const [openConfirm, setOpenConfirm] = useState(false)
const [productId, setProductId] = useState('') const [productId, setProductId] = useState('')
const [search, setSearch] = useState('')
// Hooks // Hooks
const { lang: locale } = useParams() const { lang: locale } = useParams()
@ -119,7 +121,8 @@ const ProductListTable = () => {
// Fetch products with pagination and search // Fetch products with pagination and search
const { data, isLoading, error, isFetching } = useProducts({ const { data, isLoading, error, isFetching } = useProducts({
page: currentPage, page: currentPage,
limit: pageSize limit: pageSize,
search
}) })
const { mutate: deleteProduct, isPending: isDeleting } = useProductsMutation().deleteProduct const { mutate: deleteProduct, isPending: isDeleting } = useProductsMutation().deleteProduct
@ -135,7 +138,7 @@ const ProductListTable = () => {
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10) const newPageSize = parseInt(event.target.value, 10)
setPageSize(newPageSize) setPageSize(newPageSize)
setCurrentPage(0) // Reset to first page setCurrentPage(1) // Reset to first page
}, []) }, [])
const handleDelete = () => { const handleDelete = () => {
@ -182,33 +185,17 @@ const ProductListTable = () => {
</div> </div>
) )
}), }),
// columnHelper.accessor('category_id', {
// header: 'Category',
// cell: ({ row }) => (
// <div className='flex items-center gap-4'>
// <CustomAvatar skin='light' color={'info'} size={30}>
// <i className={classnames('text-red-300', 'text-lg')} />
// </CustomAvatar>
// <Typography color='text.primary'>{row.original.category_id || '-'}</Typography>
// </div>
// )
// }),
// columnHelper.accessor('stock', {
// header: 'Stock',
// cell: ({ row }) => <Switch defaultChecked={row.original.stock} />,
// enableSorting: false
// }),
columnHelper.accessor('sku', { columnHelper.accessor('sku', {
header: 'SKU', header: 'SKU',
cell: ({ row }) => <Typography>{row.original.sku}</Typography> cell: ({ row }) => <Typography>{row.original.sku}</Typography>
}), }),
columnHelper.accessor('price', { columnHelper.accessor('price', {
header: 'Price', header: 'Price',
cell: ({ row }) => <Typography>{row.original.price}</Typography> cell: ({ row }) => <Typography>{formatCurrency(row.original.price)}</Typography>
}), }),
columnHelper.accessor('cost', { columnHelper.accessor('cost', {
header: 'Cost', header: 'Cost',
cell: ({ row }) => <Typography>{row.original.cost}</Typography> cell: ({ row }) => <Typography>{formatCurrency(row.original.cost)}</Typography>
}), }),
columnHelper.accessor('is_active', { columnHelper.accessor('is_active', {
header: 'Status', header: 'Status',
@ -273,7 +260,7 @@ const ProductListTable = () => {
state: { state: {
rowSelection, rowSelection,
pagination: { pagination: {
pageIndex: currentPage, // <= penting! pageIndex: currentPage,
pageSize pageSize
} }
}, },
@ -293,8 +280,8 @@ const ProductListTable = () => {
<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={'search'} value={search}
onChange={value => console.log(value)} onChange={value => setSearch(value as string)}
placeholder='Search Product' placeholder='Search Product'
className='max-sm:is-full' className='max-sm:is-full'
/> />

View File

@ -27,7 +27,7 @@ import OptionMenu from '@core/components/option-menu'
// 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 { Box, Chip, CircularProgress } from '@mui/material'
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete' import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
import Loading from '../../../../../components/layout/shared/Loading' import Loading from '../../../../../components/layout/shared/Loading'
import { useUnitsMutation } from '../../../../../services/mutations/units' import { useUnitsMutation } from '../../../../../services/mutations/units'
@ -35,6 +35,7 @@ import { useUnits } from '../../../../../services/queries/units'
import { Unit } from '../../../../../types/services/unit' import { Unit } from '../../../../../types/services/unit'
import AddUnitDrawer from './AddUnitDrawer' import AddUnitDrawer from './AddUnitDrawer'
import EditUnitDrawer from './EditUnitDrawer' import EditUnitDrawer from './EditUnitDrawer'
import { formatDate } from '../../../../../utils/transform'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -124,7 +125,7 @@ const ProductUnitTable = () => {
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10) const newPageSize = parseInt(event.target.value, 10)
setPageSize(newPageSize) setPageSize(newPageSize)
setCurrentPage(0) // Reset to first page setCurrentPage(1) // Reset to first page
}, []) }, [])
const handleDelete = () => { const handleDelete = () => {
@ -176,11 +177,18 @@ const ProductUnitTable = () => {
}), }),
columnHelper.accessor('is_active', { columnHelper.accessor('is_active', {
header: 'Status', header: 'Status',
cell: ({ row }) => <Typography>{row.original.is_active ? 'Active' : 'Inactive'}</Typography> cell: ({ row }) => (
<Chip
label={row.original.is_active ? 'Active' : 'Inactive'}
variant='tonal'
color={row.original.is_active ? 'success' : 'error'}
size='small'
/>
)
}), }),
columnHelper.accessor('created_at', { columnHelper.accessor('created_at', {
header: 'Created Date', header: 'Created Date',
cell: ({ row }) => <Typography>{row.original.created_at}</Typography> cell: ({ row }) => <Typography>{formatDate(row.original.created_at)}</Typography>
}), }),
columnHelper.accessor('actions', { columnHelper.accessor('actions', {
header: 'Actions', header: 'Actions',

View File

@ -16,10 +16,10 @@ import Typography from '@mui/material/Typography'
import CustomTextField from '@core/components/mui/TextField' import CustomTextField from '@core/components/mui/TextField'
import { Autocomplete, CircularProgress } from '@mui/material' import { Autocomplete, CircularProgress } from '@mui/material'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { useInventoriesMutation } from '../../../../services/mutations/inventories' import { useInventoriesMutation } from '../../../../../services/mutations/inventories'
import { useOutlets } from '../../../../services/queries/outlets' import { useOutlets } from '../../../../../services/queries/outlets'
import { useProducts } from '../../../../services/queries/products' import { useProducts } from '../../../../../services/queries/products'
import { InventoryAdjustRequest } from '../../../../types/services/inventory' import { InventoryAdjustRequest } from '../../../../../types/services/inventory'
type Props = { type Props = {
open: boolean open: boolean

View File

@ -35,10 +35,10 @@ import CustomTextField from '@core/components/mui/TextField'
// 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 { Box, CircularProgress } from '@mui/material'
import Loading from '../../../../components/layout/shared/Loading'
import { useInventories } from '../../../../services/queries/inventories'
import { Inventory } from '../../../../types/services/inventory'
import AdjustmentStockDrawer from './AdjustmentStockDrawer' import AdjustmentStockDrawer from './AdjustmentStockDrawer'
import { Inventory } from '../../../../../types/services/inventory'
import Loading from '../../../../../components/layout/shared/Loading'
import { useInventories } from '../../../../../services/queries/inventories'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -121,7 +121,7 @@ const StockListTable = () => {
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10) const newPageSize = parseInt(event.target.value, 10)
setPageSize(newPageSize) setPageSize(newPageSize)
setCurrentPage(0) // Reset to first page setCurrentPage(1) // Reset to first page
}, []) }, [])
const columns = useMemo<ColumnDef<InventoryWithActionsType, any>[]>( const columns = useMemo<ColumnDef<InventoryWithActionsType, any>[]>(

View File

@ -16,10 +16,10 @@ import Typography from '@mui/material/Typography'
import CustomTextField from '@core/components/mui/TextField' import CustomTextField from '@core/components/mui/TextField'
import { Autocomplete, CircularProgress } from '@mui/material' import { Autocomplete, CircularProgress } from '@mui/material'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { useInventoriesMutation } from '../../../../services/mutations/inventories' import { useInventoriesMutation } from '../../../../../services/mutations/inventories'
import { useOutlets } from '../../../../services/queries/outlets' import { useOutlets } from '../../../../../services/queries/outlets'
import { useProducts } from '../../../../services/queries/products' import { useProducts } from '../../../../../services/queries/products'
import { InventoryRequest } from '../../../../types/services/inventory' import { InventoryRequest } from '../../../../../types/services/inventory'
type Props = { type Props = {
open: boolean open: boolean

View File

@ -36,12 +36,12 @@ import OptionMenu from '@core/components/option-menu'
// 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 { Box, CircularProgress } from '@mui/material'
import ConfirmDeleteDialog from '../../../../components/dialogs/confirm-delete'
import Loading from '../../../../components/layout/shared/Loading'
import { useInventoriesMutation } from '../../../../services/mutations/inventories'
import { useInventories } from '../../../../services/queries/inventories'
import { Inventory } from '../../../../types/services/inventory'
import AddStockDrawer from './AddStockDrawer' import AddStockDrawer from './AddStockDrawer'
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
import Loading from '../../../../../components/layout/shared/Loading'
import { useInventoriesMutation } from '../../../../../services/mutations/inventories'
import { useInventories } from '../../../../../services/queries/inventories'
import { Inventory } from '../../../../../types/services/inventory'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -128,7 +128,7 @@ const StockListTable = () => {
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10) const newPageSize = parseInt(event.target.value, 10)
setPageSize(newPageSize) setPageSize(newPageSize)
setCurrentPage(0) // Reset to first page setCurrentPage(1) // Reset to first page
}, []) }, [])
const handleDelete = () => { const handleDelete = () => {
@ -252,8 +252,8 @@ const StockListTable = () => {
<div className='flex flex-wrap items-center max-sm:flex-col gap-4 max-sm:is-full is-auto'> <div className='flex flex-wrap items-center max-sm:flex-col gap-4 max-sm:is-full is-auto'>
<CustomTextField <CustomTextField
select select
value={table.getState().pagination.pageSize} value={pageSize}
onChange={e => table.setPageSize(Number(e.target.value))} onChange={handlePageSizeChange}
className='flex-auto is-[70px] max-sm:is-full' className='flex-auto is-[70px] max-sm:is-full'
> >
<MenuItem value='10'>10</MenuItem> <MenuItem value='10'>10</MenuItem>

View File

@ -1,17 +1,16 @@
// React Imports // React Imports
import { useState, useEffect } from 'react' import { useEffect, useState } from 'react'
// MUI Imports // MUI Imports
import Grid from '@mui/material/Grid2'
import CardContent from '@mui/material/CardContent' import CardContent from '@mui/material/CardContent'
import Grid from '@mui/material/Grid2'
import MenuItem from '@mui/material/MenuItem' import MenuItem from '@mui/material/MenuItem'
// Type Imports // Type Imports
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/product' import { Product } from '../../../../../types/services/product'
type ProductStockType = { [key: string]: boolean } type ProductStockType = { [key: string]: boolean }

View File

@ -0,0 +1,167 @@
// 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 Switch from '@mui/material/Switch'
import Typography from '@mui/material/Typography'
// Third-party Imports
import PerfectScrollbar from 'react-perfect-scrollbar'
// Type Imports
// Component Imports
import CustomTextField from '@core/components/mui/TextField'
import { MenuItem } from '@mui/material'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../../../../../redux-store'
import { usePaymentMethodsMutation } from '../../../../../services/mutations/paymentMethods'
import { PaymentMethodRequest } from '../../../../../types/services/paymentMethod'
import { resetPaymentMethod } from '../../../../../redux-store/slices/paymentMethod'
type Props = {
open: boolean
handleClose: () => void
}
// Vars
const initialData = {
name: '',
type: '',
is_active: true
}
const AddPaymentMethodDrawer = (props: Props) => {
const dispatch = useDispatch()
// Props
const { open, handleClose } = props
const { createPaymentMethod, updatePaymentMethod } = usePaymentMethodsMutation()
const { currentPaymentMethod } = useSelector((state: RootState) => state.paymentMethodReducer)
// States
const [formData, setFormData] = useState<PaymentMethodRequest>(initialData)
useEffect(() => {
if (currentPaymentMethod.id) {
setFormData(currentPaymentMethod)
}
}, [currentPaymentMethod])
const handleSubmit = (e: any) => {
e.preventDefault()
if (currentPaymentMethod.id) {
updatePaymentMethod.mutate(
{ id: currentPaymentMethod.id, payload: formData },
{
onSuccess: () => {
handleReset()
}
}
)
} else {
createPaymentMethod.mutate(formData, {
onSuccess: () => {
handleReset()
}
})
}
}
const handleReset = () => {
handleClose()
dispatch(resetPaymentMethod())
setFormData(initialData)
}
const handleInputChange = (e: any) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
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'>{currentPaymentMethod.id ? 'Edit' : 'Add'} Payment Method</Typography>
<IconButton size='small' onClick={handleReset}>
<i className='tabler-x text-2xl' />
</IconButton>
</div>
<Divider />
<PerfectScrollbar options={{ wheelPropagation: false, suppressScrollX: true }}>
<div className='p-6'>
<form onSubmit={handleSubmit} className='flex flex-col gap-5'>
<Typography color='text.primary' className='font-medium'>
Basic Information
</Typography>
<CustomTextField
fullWidth
label='Name'
name='name'
placeholder='BCA'
value={formData.name}
onChange={handleInputChange}
/>
<CustomTextField
select
fullWidth
label='Type'
value={formData.type}
onChange={e => setFormData({ ...formData, type: e.target.value })}
>
<MenuItem value={'cash'}>Cash</MenuItem>
<MenuItem value={'card'}>Card</MenuItem>
<MenuItem value={'digital_wallet'}>Digital Wallet</MenuItem>
</CustomTextField>
<div className='flex items-center'>
<div className='flex flex-col items-start gap-1'>
<Typography color='text.primary' className='font-medium'>
Active
</Typography>
</div>
<Switch
checked={formData.is_active}
name='is_active'
onChange={e => setFormData({ ...formData, is_active: e.target.checked })}
/>
</div>
<div className='flex items-center gap-4'>
<Button
variant='contained'
type='submit'
disabled={createPaymentMethod.isPending || updatePaymentMethod.isPending}
>
{currentPaymentMethod.id
? updatePaymentMethod.isPending
? 'Updating...'
: 'Update'
: createPaymentMethod.isPending
? 'Creating...'
: 'Create'}
</Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Discard
</Button>
</div>
</form>
</div>
</PerfectScrollbar>
</Drawer>
)
}
export default AddPaymentMethodDrawer

View File

@ -0,0 +1,401 @@
'use client'
// React Imports
import { useCallback, useEffect, useMemo, useState } from 'react'
// Next Imports
// MUI Imports
import Button from '@mui/material/Button'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Checkbox from '@mui/material/Checkbox'
import MenuItem from '@mui/material/MenuItem'
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 CustomTextField from '@core/components/mui/TextField'
// Util Imports
// Style Imports
import tableStyles from '@core/styles/table.module.css'
import { Box, Chip, CircularProgress, IconButton, TablePagination } from '@mui/material'
import { useDispatch } from 'react-redux'
import OptionMenu from '../../../../../@core/components/option-menu'
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
import Loading from '../../../../../components/layout/shared/Loading'
import TablePaginationComponent from '../../../../../components/TablePaginationComponent'
import { setPaymentMethod } from '../../../../../redux-store/slices/paymentMethod'
import { usePaymentMethodsMutation } from '../../../../../services/mutations/paymentMethods'
import { usePaymentMethods } from '../../../../../services/queries/paymentMethods'
import { PaymentMethod } from '../../../../../types/services/paymentMethod'
import AddPaymentMethodDrawer from './AddPaymentMethodDrawer'
declare module '@tanstack/table-core' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface FilterMeta {
itemRank: RankingInfo
}
}
type FinancePaymentMethodTypeWithAction = PaymentMethod & {
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<FinancePaymentMethodTypeWithAction>()
const PaymentMethodListTable = () => {
const dispatch = useDispatch()
// States
const [paymentMethodOpen, setPaymentMethodOpen] = useState(false)
const [rowSelection, setRowSelection] = useState({})
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [openConfirm, setOpenConfirm] = useState(false)
const [paymentMethodId, setPaymentMethodId] = useState('')
const [search, setSearch] = useState('')
const { data, isLoading, error, isFetching } = usePaymentMethods({
page: currentPage,
limit: pageSize,
search
})
const { deletePaymentMethod } = usePaymentMethodsMutation()
const paymentMethods = data?.payment_methods ?? []
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(1) // Reset to first page
}, [])
const handleDelete = () => {
deletePaymentMethod.mutate(paymentMethodId, {
onSuccess: () => setOpenConfirm(false)
})
}
const columns = useMemo<ColumnDef<FinancePaymentMethodTypeWithAction, 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('name', {
header: 'Name',
cell: ({ row }) => <Typography color='text.primary'>{row.original.name || '-'}</Typography>
}),
columnHelper.accessor('type', {
header: 'Type',
cell: ({ row }) => <Typography color='text.primary'>{row.original.type || '-'}</Typography>
}),
columnHelper.accessor('is_active', {
header: 'Status',
cell: ({ row }) => (
<Chip
label={row.original.is_active ? 'Active' : 'Inactive'}
variant='tonal'
color={row.original.is_active ? 'success' : 'error'}
size='small'
/>
)
}),
columnHelper.accessor('created_at', {
header: 'Created Date',
cell: ({ row }) => <Typography color='text.primary'>{row.original.created_at || '-'}</Typography>
}),
columnHelper.accessor('actions', {
header: 'Actions',
cell: ({ row }) => (
<div className='flex items-center'>
<IconButton
onClick={() => {
dispatch(setPaymentMethod(row.original))
setPaymentMethodOpen(true)
}}
>
<i className='tabler-edit text-textSecondary' />
</IconButton>
<OptionMenu
iconButtonProps={{ size: 'medium' }}
iconClassName='text-textSecondary'
options={[
{ text: 'Download', icon: 'tabler-download' },
{
text: 'Delete',
icon: 'tabler-trash',
menuItemProps: {
onClick: () => {
setOpenConfirm(true)
setPaymentMethodId(row.original.id)
}
}
},
{ text: 'Duplicate', icon: 'tabler-copy' }
]}
/>
</div>
),
enableSorting: false
})
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
const table = useReactTable({
data: paymentMethods as PaymentMethod[],
columns,
filterFns: {
fuzzy: fuzzyFilter
},
state: {
rowSelection,
pagination: {
pageIndex: currentPage,
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>
<CardContent className='flex justify-between flex-wrap max-sm:flex-col sm:items-center gap-4'>
<DebouncedInput
value={search}
onChange={value => setSearch(value as string)}
placeholder='Search'
className='max-sm:is-full'
/>
<div className='flex max-sm:flex-col items-start sm:items-center gap-4 max-sm:is-full'>
<CustomTextField
select
value={pageSize}
onChange={handlePageSizeChange}
className='is-full sm:is-[70px]'
>
<MenuItem value='10'>10</MenuItem>
<MenuItem value='25'>25</MenuItem>
<MenuItem value='50'>50</MenuItem>
<MenuItem value='100'>100</MenuItem>
</CustomTextField>
<Button
variant='tonal'
className='max-sm:is-full'
color='secondary'
startIcon={<i className='tabler-upload' />}
>
Export
</Button>
<Button
variant='contained'
color='primary'
className='max-sm:is-full'
startIcon={<i className='tabler-plus' />}
onClick={() => setPaymentMethodOpen(!paymentMethodOpen)}
>
Add Payment Method
</Button>
</div>
</CardContent>
<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>
<AddPaymentMethodDrawer open={paymentMethodOpen} handleClose={() => setPaymentMethodOpen(!paymentMethodOpen)} />
<ConfirmDeleteDialog
open={openConfirm}
onClose={() => setOpenConfirm(false)}
onConfirm={handleDelete}
isLoading={deletePaymentMethod.isPending}
title='Delete paymentMethod'
message='Are you sure you want to delete this paymentMethod? This action cannot be undone.'
/>
</>
)
}
export default PaymentMethodListTable

View File

@ -0,0 +1,395 @@
'use client'
// React Imports
import { useCallback, useEffect, useMemo, useState } from 'react'
// Next Imports
// MUI Imports
import Button from '@mui/material/Button'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Checkbox from '@mui/material/Checkbox'
import MenuItem from '@mui/material/MenuItem'
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 CustomTextField from '@core/components/mui/TextField'
// Util Imports
// Style Imports
import tableStyles from '@core/styles/table.module.css'
import { Box, Chip, CircularProgress, IconButton, TablePagination } from '@mui/material'
import { useDispatch } from 'react-redux'
import OptionMenu from '../../../../../@core/components/option-menu'
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
import Loading from '../../../../../components/layout/shared/Loading'
import TablePaginationComponent from '../../../../../components/TablePaginationComponent'
import { usePaymentMethodsMutation } from '../../../../../services/mutations/paymentMethods'
import { useOutlets } from '../../../../../services/queries/outlets'
import { Outlet } from '../../../../../types/services/outlet'
declare module '@tanstack/table-core' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface FilterMeta {
itemRank: RankingInfo
}
}
type OrganizationOutletTypeWithAction = Outlet & {
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<OrganizationOutletTypeWithAction>()
const OrganizationOutletListTable = () => {
const dispatch = useDispatch()
// States
const [paymentMethodOpen, setPaymentMethodOpen] = useState(false)
const [rowSelection, setRowSelection] = useState({})
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [openConfirm, setOpenConfirm] = useState(false)
const [paymentMethodId, setPaymentMethodId] = useState('')
const [search, setSearch] = useState('')
const { data, isLoading, error, isFetching } = useOutlets({
page: currentPage,
limit: pageSize,
search
})
const { deletePaymentMethod } = usePaymentMethodsMutation()
const outlets = data?.outlets ?? []
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(1) // Reset to first page
}, [])
const handleDelete = () => {
deletePaymentMethod.mutate(paymentMethodId, {
onSuccess: () => setOpenConfirm(false)
})
}
const columns = useMemo<ColumnDef<OrganizationOutletTypeWithAction, 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('name', {
header: 'Name',
cell: ({ row }) => <Typography color='text.primary'>{row.original.name || '-'}</Typography>
}),
columnHelper.accessor('address', {
header: 'Address',
cell: ({ row }) => <Typography color='text.primary'>{row.original.address || '-'}</Typography>
}),
columnHelper.accessor('phone_number', {
header: 'Phone',
cell: ({ row }) => <Typography color='text.primary'>{row.original.phone_number || '-'}</Typography>
}),
columnHelper.accessor('business_type', {
header: 'Business',
cell: ({ row }) => <Typography color='text.primary'>{row.original.business_type || '-'}</Typography>
}),
columnHelper.accessor('is_active', {
header: 'Status',
cell: ({ row }) => (
<Chip
label={row.original.is_active ? 'Active' : 'Inactive'}
variant='tonal'
color={row.original.is_active ? 'success' : 'error'}
size='small'
/>
)
}),
columnHelper.accessor('currency', {
header: 'Currency',
cell: ({ row }) => <Typography color='text.primary'>{row.original.currency || '-'}</Typography>
}),
columnHelper.accessor('tax_rate', {
header: 'Tax',
cell: ({ row }) => <Typography color='text.primary'>{row.original.tax_rate || '-'}</Typography>
}),
columnHelper.accessor('actions', {
header: 'Actions',
cell: ({ row }) => (
<div className='flex items-center'>
<IconButton>
<i className='tabler-edit text-textSecondary' />
</IconButton>
<OptionMenu
iconButtonProps={{ size: 'medium' }}
iconClassName='text-textSecondary'
options={[
{ text: 'Download', icon: 'tabler-download' },
{
text: 'Delete',
icon: 'tabler-trash',
menuItemProps: {
onClick: () => {
setOpenConfirm(true)
setPaymentMethodId(row.original.id)
}
}
},
{ text: 'Duplicate', icon: 'tabler-copy' }
]}
/>
</div>
),
enableSorting: false
})
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
const table = useReactTable({
data: outlets as Outlet[],
columns,
filterFns: {
fuzzy: fuzzyFilter
},
state: {
rowSelection,
pagination: {
pageIndex: currentPage,
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>
<CardContent className='flex justify-between flex-wrap max-sm:flex-col sm:items-center gap-4'>
<DebouncedInput
value={search}
onChange={value => setSearch(value as string)}
placeholder='Search'
className='max-sm:is-full'
/>
<div className='flex max-sm:flex-col items-start sm:items-center gap-4 max-sm:is-full'>
<CustomTextField
select
value={pageSize}
onChange={handlePageSizeChange}
className='is-full sm:is-[70px]'
>
<MenuItem value='10'>10</MenuItem>
<MenuItem value='25'>25</MenuItem>
<MenuItem value='50'>50</MenuItem>
<MenuItem value='100'>100</MenuItem>
</CustomTextField>
<Button
variant='tonal'
className='max-sm:is-full'
color='secondary'
startIcon={<i className='tabler-upload' />}
>
Export
</Button>
</div>
</CardContent>
<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>
<ConfirmDeleteDialog
open={openConfirm}
onClose={() => setOpenConfirm(false)}
onConfirm={handleDelete}
isLoading={deletePaymentMethod.isPending}
title='Delete paymentMethod'
message='Are you sure you want to delete this paymentMethod? This action cannot be undone.'
/>
</>
)
}
export default OrganizationOutletListTable