feat: payment & customer
This commit is contained in:
parent
a72a64215a
commit
a5d22db27b
@ -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
|
||||||
|
|
||||||
@ -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
|
||||||
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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>
|
||||||
|
|||||||
@ -21,6 +21,10 @@
|
|||||||
"add": "يضيف",
|
"add": "يضيف",
|
||||||
"addjustment": "تعديل",
|
"addjustment": "تعديل",
|
||||||
"category": "فئة",
|
"category": "فئة",
|
||||||
|
"finance": "مالية",
|
||||||
|
"paymentMethods": "طرق الدفع",
|
||||||
|
"organization": "المنظمة",
|
||||||
|
"outlet": "مخزن",
|
||||||
"units": "وحدات",
|
"units": "وحدات",
|
||||||
"ingredients": "مكونات",
|
"ingredients": "مكونات",
|
||||||
"orders": "أوامر",
|
"orders": "أوامر",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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 })
|
||||||
})
|
})
|
||||||
|
|||||||
37
src/redux-store/slices/paymentMethod.ts
Normal file
37
src/redux-store/slices/paymentMethod.ts
Normal 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
|
||||||
52
src/services/mutations/paymentMethods.ts
Normal file
52
src/services/mutations/paymentMethods.ts
Normal 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 }
|
||||||
|
}
|
||||||
@ -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')
|
||||||
|
|||||||
36
src/services/queries/paymentMethods.ts
Normal file
36
src/services/queries/paymentMethods.ts
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
17
src/utils/transform.ts
Normal 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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.'
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -154,7 +154,7 @@ const ProductInformation = () => {
|
|||||||
immediatelyRender: false,
|
immediatelyRender: false,
|
||||||
content: `
|
content: `
|
||||||
<p>
|
<p>
|
||||||
${description}
|
${description || ''}
|
||||||
</p>
|
</p>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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 }}>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
@ -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'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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
|
||||||
@ -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>[]>(
|
||||||
@ -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
|
||||||
@ -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>
|
||||||
@ -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 }
|
||||||
|
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
Loading…
x
Reference in New Issue
Block a user