Merge remote-tracking branch 'origin/main' into efril
This commit is contained in:
commit
054ca015fd
@ -352,7 +352,7 @@ const CreateOrganization = () => {
|
||||
required
|
||||
label='Organization Name'
|
||||
placeholder='My Company Ltd.'
|
||||
value={formData.organization_name}
|
||||
value={formData.organization_name || ''}
|
||||
onChange={handleInputChange('organization_name')}
|
||||
error={!!errors.organization_name}
|
||||
helperText={errors.organization_name}
|
||||
@ -395,7 +395,7 @@ const CreateOrganization = () => {
|
||||
required
|
||||
label='Admin Name'
|
||||
placeholder='John Doe'
|
||||
value={formData.admin_name}
|
||||
value={formData.admin_name || ''}
|
||||
onChange={handleInputChange('admin_name')}
|
||||
error={!!errors.admin_name}
|
||||
helperText={errors.admin_name}
|
||||
@ -408,7 +408,7 @@ const CreateOrganization = () => {
|
||||
label='Admin Email'
|
||||
placeholder='admin@mycompany.com'
|
||||
type='email'
|
||||
value={formData.admin_email}
|
||||
value={formData.admin_email || ''}
|
||||
onChange={handleInputChange('admin_email')}
|
||||
error={!!errors.admin_email}
|
||||
helperText={errors.admin_email}
|
||||
@ -421,7 +421,7 @@ const CreateOrganization = () => {
|
||||
type='password'
|
||||
label='Admin Password'
|
||||
placeholder='Minimum 6 characters'
|
||||
value={formData.admin_password}
|
||||
value={formData.admin_password || ''}
|
||||
onChange={handleInputChange('admin_password')}
|
||||
error={!!errors.admin_password}
|
||||
helperText={errors.admin_password}
|
||||
@ -443,7 +443,7 @@ const CreateOrganization = () => {
|
||||
required
|
||||
label='Outlet Name'
|
||||
placeholder='Main Store'
|
||||
value={formData.outlet_name}
|
||||
value={formData.outlet_name || ''}
|
||||
onChange={handleInputChange('outlet_name')}
|
||||
error={!!errors.outlet_name}
|
||||
helperText={errors.outlet_name}
|
||||
|
||||
@ -3,12 +3,11 @@ import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import ProductAddHeader from '@views/apps/ecommerce/products/add/ProductAddHeader'
|
||||
import ProductInformation from '@views/apps/ecommerce/products/add/ProductInformation'
|
||||
import ProductImage from '@views/apps/ecommerce/products/add/ProductImage'
|
||||
import ProductVariants from '@views/apps/ecommerce/products/add/ProductVariants'
|
||||
import ProductInventory from '@views/apps/ecommerce/products/add/ProductInventory'
|
||||
import ProductPricing from '@views/apps/ecommerce/products/add/ProductPricing'
|
||||
import ProductInformation from '@views/apps/ecommerce/products/add/ProductInformation'
|
||||
import ProductOrganize from '@views/apps/ecommerce/products/add/ProductOrganize'
|
||||
import ProductPricing from '@views/apps/ecommerce/products/add/ProductPricing'
|
||||
import ProductVariants from '@views/apps/ecommerce/products/add/ProductVariants'
|
||||
|
||||
const eCommerceProductsEdit = () => {
|
||||
return (
|
||||
@ -27,9 +26,6 @@ const eCommerceProductsEdit = () => {
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<ProductVariants />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<ProductInventory />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
|
||||
@ -14,12 +14,12 @@ const DashboardOverview = () => {
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
const MetricCard = ({ iconClass, title, value, subtitle, bgColor = 'bg-blue-500' }: any) => (
|
||||
const MetricCard = ({ iconClass, title, value, subtitle, bgColor = 'bg-blue-500', isCurrency = false }: any) => (
|
||||
<div className='bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 p-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex-1'>
|
||||
<h3 className='text-sm font-medium text-gray-600 mb-2'>{title}</h3>
|
||||
<p className='text-2xl font-bold text-gray-900 mb-1'>{value}</p>
|
||||
<p className='text-2xl font-bold text-gray-900 mb-1'>{isCurrency ? 'Rp ' + value : value}</p>
|
||||
{subtitle && <p className='text-sm text-gray-500'>{subtitle}</p>}
|
||||
</div>
|
||||
<div className={`px-4 py-3 rounded-full ${bgColor} bg-opacity-10`}>
|
||||
@ -52,12 +52,6 @@ const DashboardOverview = () => {
|
||||
|
||||
{/* Overview Metrics */}
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8'>
|
||||
<MetricCard
|
||||
iconClass='tabler-cash'
|
||||
title='Total Sales'
|
||||
value={formatShortCurrency(salesData.overview.total_sales)}
|
||||
bgColor='bg-green-500'
|
||||
/>
|
||||
<MetricCard
|
||||
iconClass='tabler-shopping-cart'
|
||||
title='Total Orders'
|
||||
@ -65,11 +59,19 @@ const DashboardOverview = () => {
|
||||
subtitle={`${salesData.overview.voided_orders} voided, ${salesData.overview.refunded_orders} refunded`}
|
||||
bgColor='bg-blue-500'
|
||||
/>
|
||||
<MetricCard
|
||||
iconClass='tabler-cash'
|
||||
title='Total Sales'
|
||||
value={formatShortCurrency(salesData.overview.total_sales)}
|
||||
bgColor='bg-green-500'
|
||||
isCurrency={true}
|
||||
/>
|
||||
<MetricCard
|
||||
iconClass='tabler-trending-up'
|
||||
title='Average Order Value'
|
||||
value={formatShortCurrency(salesData.overview.average_order_value)}
|
||||
bgColor='bg-purple-500'
|
||||
isCurrency={true}
|
||||
/>
|
||||
<MetricCard
|
||||
iconClass='tabler-users'
|
||||
|
||||
@ -39,7 +39,7 @@ const DashboardProfitloss = () => {
|
||||
function formatMetricName(metric: string): string {
|
||||
const nameMap: { [key: string]: string } = {
|
||||
revenue: 'Revenue',
|
||||
net_profit: 'Net Profit',
|
||||
net_profit: 'Net Profit'
|
||||
}
|
||||
|
||||
return nameMap[metric] || metric.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
||||
@ -61,15 +61,25 @@ const DashboardProfitloss = () => {
|
||||
]
|
||||
}
|
||||
|
||||
const MetricCard = ({ iconClass, title, value, subtitle, bgColor = 'bg-blue-500', isNegative = false }: any) => (
|
||||
const MetricCard = ({
|
||||
iconClass,
|
||||
title,
|
||||
value,
|
||||
subtitle,
|
||||
bgColor = 'bg-blue-500',
|
||||
isNegative = false,
|
||||
isCurrency = false
|
||||
}: any) => (
|
||||
<div className='bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 p-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex-1'>
|
||||
<h3 className='text-sm font-medium text-gray-600 mb-2'>{title}</h3>
|
||||
<p className={`text-2xl font-bold mb-1 ${isNegative ? 'text-red-600' : 'text-gray-900'}`}>{value}</p>
|
||||
<p className={`text-2xl font-bold mb-1 ${isNegative ? 'text-red-600' : 'text-gray-900'}`}>
|
||||
{isCurrency ? 'Rp ' + value : value}
|
||||
</p>
|
||||
{subtitle && <p className='text-sm text-gray-500'>{subtitle}</p>}
|
||||
</div>
|
||||
<div className={`p-3 rounded-full ${bgColor} bg-opacity-10`}>
|
||||
<div className={`px-4 py-3 rounded-full ${bgColor} bg-opacity-10`}>
|
||||
<i className={`${iconClass} text-[32px] ${bgColor.replace('bg-', 'text-')}`}></i>
|
||||
</div>
|
||||
</div>
|
||||
@ -95,12 +105,14 @@ const DashboardProfitloss = () => {
|
||||
title='Total Revenue'
|
||||
value={formatShortCurrency(profitData.summary.total_revenue)}
|
||||
bgColor='bg-green-500'
|
||||
isCurrency={true}
|
||||
/>
|
||||
<MetricCard
|
||||
iconClass='tabler-receipt'
|
||||
title='Total Cost'
|
||||
value={formatShortCurrency(profitData.summary.total_cost)}
|
||||
bgColor='bg-red-500'
|
||||
isCurrency={true}
|
||||
/>
|
||||
<MetricCard
|
||||
iconClass='tabler-trending-up'
|
||||
@ -109,6 +121,7 @@ const DashboardProfitloss = () => {
|
||||
subtitle={`Margin: ${formatPercentage(profitData.summary.gross_profit_margin)}`}
|
||||
bgColor='bg-blue-500'
|
||||
isNegative={profitData.summary.gross_profit < 0}
|
||||
isCurrency={true}
|
||||
/>
|
||||
<MetricCard
|
||||
iconClass='tabler-percentage'
|
||||
@ -127,7 +140,7 @@ const DashboardProfitloss = () => {
|
||||
<h3 className='text-lg font-semibold text-gray-900'>Net Profit</h3>
|
||||
</div>
|
||||
<p className='text-3xl font-bold text-green-600 mb-2'>
|
||||
{formatShortCurrency(profitData.summary.net_profit)}
|
||||
Rp {formatShortCurrency(profitData.summary.net_profit)}
|
||||
</p>
|
||||
<p className='text-sm text-gray-600'>Margin: {formatPercentage(profitData.summary.net_profit_margin)}</p>
|
||||
</div>
|
||||
@ -144,7 +157,7 @@ const DashboardProfitloss = () => {
|
||||
<h3 className='text-lg font-semibold text-gray-900'>Tax & Discount</h3>
|
||||
</div>
|
||||
<p className='text-xl font-bold text-orange-600 mb-1'>
|
||||
{formatShortCurrency(profitData.summary.total_tax + profitData.summary.total_discount)}
|
||||
Rp {formatShortCurrency(profitData.summary.total_tax + profitData.summary.total_discount)}
|
||||
</p>
|
||||
<p className='text-sm text-gray-600'>
|
||||
Tax: {formatShortCurrency(profitData.summary.total_tax)} | Discount:{' '}
|
||||
|
||||
@ -91,7 +91,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
||||
<MenuItem href={`/${locale}/dashboards/daily-report`}>{dictionary['navigation'].dailyReport}</MenuItem>
|
||||
</SubMenu>
|
||||
<MenuSection label={dictionary['navigation'].appsPages}>
|
||||
<SubMenu label={dictionary['navigation'].eCommerce} icon={<i className='tabler-shopping-cart' />}>
|
||||
<SubMenu label={dictionary['navigation'].eCommerce} icon={<i className='tabler-salad' />}>
|
||||
{/* <MenuItem href={`/${locale}/apps/ecommerce/dashboard`}>{dictionary['navigation'].dashboard}</MenuItem> */}
|
||||
<SubMenu label={dictionary['navigation'].products}>
|
||||
<MenuItem href={`/${locale}/apps/ecommerce/products/list`}>{dictionary['navigation'].list}</MenuItem>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"navigation": {
|
||||
"dashboards": "لوحات القيادة",
|
||||
"analytics": "تحليلات",
|
||||
"eCommerce": "التجارة الإلكترونية",
|
||||
"eCommerce": "تجزئة الكترونية",
|
||||
"stock": "المخزون",
|
||||
"academy": "أكاديمية",
|
||||
"logistics": "اللوجستية",
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"navigation": {
|
||||
"dashboards": "Dashboards",
|
||||
"analytics": "Analytics",
|
||||
"eCommerce": "eCommerce",
|
||||
"eCommerce": "Inventory",
|
||||
"stock": "Stock",
|
||||
"academy": "Academy",
|
||||
"logistics": "Logistics",
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"navigation": {
|
||||
"dashboards": "Tableaux de bord",
|
||||
"analytics": "Analytique",
|
||||
"eCommerce": "commerce électronique",
|
||||
"eCommerce": "Inventaire",
|
||||
"stock": "Stock",
|
||||
"academy": "Académie",
|
||||
"logistics": "Logistique",
|
||||
|
||||
@ -13,7 +13,6 @@ const initialState: { productRequest: ProductRequest } = {
|
||||
sku: '',
|
||||
name: '',
|
||||
description: '',
|
||||
barcode: '',
|
||||
price: 0,
|
||||
cost: 0,
|
||||
printer_type: '',
|
||||
|
||||
@ -1,20 +1,71 @@
|
||||
// Third-party Imports
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import { ProductRecipe } from '../../types/services/productRecipe'
|
||||
|
||||
// Type Imports
|
||||
|
||||
// Data Imports
|
||||
|
||||
const initialState: { currentProductRecipe: any } = {
|
||||
currentProductRecipe: {}
|
||||
const initialState: { currentVariant: any, currentProductRecipe: ProductRecipe } = {
|
||||
currentVariant: {},
|
||||
currentProductRecipe: {
|
||||
id: '',
|
||||
organization_id: '',
|
||||
outlet_id: null,
|
||||
product_id: '',
|
||||
variant_id: null,
|
||||
ingredient_id: '',
|
||||
quantity: 0,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
product: {
|
||||
ID: '',
|
||||
OrganizationID: '',
|
||||
CategoryID: '',
|
||||
SKU: '',
|
||||
Name: '',
|
||||
Description: null,
|
||||
Price: 0,
|
||||
Cost: 0,
|
||||
BusinessType: '',
|
||||
ImageURL: '',
|
||||
PrinterType: '',
|
||||
UnitID: null,
|
||||
HasIngredients: false,
|
||||
Metadata: {},
|
||||
IsActive: false,
|
||||
CreatedAt: '',
|
||||
UpdatedAt: ''
|
||||
},
|
||||
ingredient: {
|
||||
id: '',
|
||||
organization_id: '',
|
||||
outlet_id: null,
|
||||
name: '',
|
||||
unit_id: '',
|
||||
cost: 0,
|
||||
stock: 0,
|
||||
is_semi_finished: false,
|
||||
is_active: false,
|
||||
metadata: {},
|
||||
created_at: '',
|
||||
updated_at: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const productRecipeSlice = createSlice({
|
||||
name: 'productRecipe',
|
||||
initialState,
|
||||
reducers: {
|
||||
setProductRecipe: (state, action: PayloadAction<any>) => {
|
||||
setProductVariant: (state, action: PayloadAction<any>) => {
|
||||
state.currentVariant = action.payload
|
||||
},
|
||||
resetProductVariant: state => {
|
||||
state.currentVariant = initialState.currentVariant
|
||||
},
|
||||
setProductRecipe: (state, action: PayloadAction<ProductRecipe>) => {
|
||||
state.currentProductRecipe = action.payload
|
||||
},
|
||||
resetProductRecipe: state => {
|
||||
@ -23,6 +74,6 @@ export const productRecipeSlice = createSlice({
|
||||
}
|
||||
})
|
||||
|
||||
export const { setProductRecipe, resetProductRecipe } = productRecipeSlice.actions
|
||||
export const { setProductVariant, resetProductVariant, setProductRecipe, resetProductRecipe } = productRecipeSlice.actions
|
||||
|
||||
export default productRecipeSlice.reducer
|
||||
|
||||
@ -38,8 +38,23 @@ export const useProductRecipesMutation = () => {
|
||||
}
|
||||
})
|
||||
|
||||
const deleteProductRecipe = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const response = await api.delete(`/product-recipes/${id}`)
|
||||
return response.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Product Recipe deleted successfully!')
|
||||
queryClient.invalidateQueries({ queryKey: ['product-recipes'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
createProductRecipe,
|
||||
updateProductRecipe
|
||||
updateProductRecipe,
|
||||
deleteProductRecipe
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Product, Products } from '../../types/services/product'
|
||||
import { api } from '../api'
|
||||
import { ProductRecipe } from '../../types/services/productRecipe'
|
||||
|
||||
interface ProductsQueryParams {
|
||||
export interface ProductsQueryParams {
|
||||
page?: number
|
||||
limit?: number
|
||||
search?: string
|
||||
// Add other filter parameters as needed
|
||||
category_id?: string
|
||||
is_active?: boolean
|
||||
is_active?: boolean | string
|
||||
}
|
||||
|
||||
export function useProducts(params: ProductsQueryParams = {}) {
|
||||
|
||||
@ -13,6 +13,7 @@ export interface Inventory {
|
||||
quantity: number
|
||||
reorder_level: number
|
||||
is_low_stock: boolean
|
||||
product: any
|
||||
updated_at: string // ISO 8601 timestamp
|
||||
}
|
||||
|
||||
|
||||
@ -47,7 +47,6 @@ export type ProductRequest = {
|
||||
sku: string
|
||||
name: string
|
||||
description: string
|
||||
barcode: string
|
||||
price: number
|
||||
cost: number
|
||||
printer_type: string
|
||||
|
||||
@ -133,11 +133,22 @@ const Login = ({ mode }: { mode: SystemMode }) => {
|
||||
const handleClickShowPassword = () => setIsPasswordShown(show => !show)
|
||||
|
||||
const onSubmit: SubmitHandler<FormData> = async (data: FormData) => {
|
||||
login.mutate(data)
|
||||
login.mutate(data, {
|
||||
onSuccess: (data: any) => {
|
||||
if (data?.user?.role === 'admin') {
|
||||
const redirectURL = searchParams.get('redirectTo') ?? '/dashboards/overview'
|
||||
|
||||
const redirectURL = searchParams.get('redirectTo') ?? '/dashboards/overview'
|
||||
router.replace(getLocalizedUrl(redirectURL, locale as Locale))
|
||||
} else {
|
||||
const redirectURL = searchParams.get('redirectTo') ?? '/sa/organizations/list'
|
||||
|
||||
router.replace(getLocalizedUrl(redirectURL, locale as Locale))
|
||||
router.replace(getLocalizedUrl(redirectURL, locale as Locale))
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
setErrorState(error.response.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@ -243,7 +254,11 @@ const Login = ({ mode }: { mode: SystemMode }) => {
|
||||
</Button>
|
||||
<div className='flex justify-center items-center flex-wrap gap-2'>
|
||||
<Typography>New on our platform?</Typography>
|
||||
<Typography component={Link} href={getLocalizedUrl('/organization', locale as Locale)} color='primary.main'>
|
||||
<Typography
|
||||
component={Link}
|
||||
href={getLocalizedUrl('/organization', locale as Locale)}
|
||||
color='primary.main'
|
||||
>
|
||||
Create an account
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
@ -17,6 +17,7 @@ import type { SystemMode } from '@core/types'
|
||||
|
||||
// Hook Imports
|
||||
import { useImageVariant } from '@core/hooks/useImageVariant'
|
||||
import { useAuth } from '../contexts/authContext'
|
||||
|
||||
// Styled Components
|
||||
const MaskImg = styled('img')({
|
||||
@ -29,6 +30,8 @@ const MaskImg = styled('img')({
|
||||
})
|
||||
|
||||
const NotFound = ({ mode }: { mode: SystemMode }) => {
|
||||
const { currentUser } = useAuth()
|
||||
|
||||
// Vars
|
||||
const darkImg = '/images/pages/misc-mask-dark.png'
|
||||
const lightImg = '/images/pages/misc-mask-light.png'
|
||||
@ -48,7 +51,11 @@ const NotFound = ({ mode }: { mode: SystemMode }) => {
|
||||
<Typography variant='h4'>Page Not Found ⚠️</Typography>
|
||||
<Typography>we couldn't find the page you are looking for.</Typography>
|
||||
</div>
|
||||
<Button href='/' component={Link} variant='contained'>
|
||||
<Button
|
||||
href={currentUser?.role === 'admin' ? '/' : '/sa/organizations/list'}
|
||||
component={Link}
|
||||
variant='contained'
|
||||
>
|
||||
Back To Home
|
||||
</Button>
|
||||
<img
|
||||
|
||||
@ -43,11 +43,11 @@ const BillingAddress = ({ data }: { data: Order }) => {
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<Typography variant='h5'>
|
||||
Payment Details ({data.payments.length} {data.payments.length === 1 ? 'Payment' : 'Payments'})
|
||||
Payment Details ({data?.payments?.length ?? 0} {data?.payments?.length === 1 ? 'Payment' : 'Payments'})
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
{data.payments.map((payment, index) => (
|
||||
{data?.payments?.length ? data.payments.map((payment, index) => (
|
||||
<div key={index}>
|
||||
<div className='flex items-center gap-3'>
|
||||
<CustomAvatar skin='light' color='secondary' size={40}>
|
||||
@ -74,7 +74,9 @@ const BillingAddress = ({ data }: { data: Order }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)) : (
|
||||
<Typography variant='body2' className='text-secondary'>No payments found</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@ -276,10 +276,10 @@ const OrderDetailsCard = ({ data }: { data: Order }) => {
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-12'>
|
||||
<Typography color='text.primary' className='font-medium min-is-[100px]'>
|
||||
<Typography color='text.primary' className='font-semibold min-is-[100px]'>
|
||||
Total:
|
||||
</Typography>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
<Typography color='text.primary' className='font-semibold'>
|
||||
{formatCurrency(data.total_amount)}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
@ -39,9 +39,6 @@ const OrderDetails = () => {
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<OrderDetailsCard data={data} />
|
||||
</Grid>
|
||||
{/* <Grid size={{ xs: 12 }}>
|
||||
<ShippingActivity order={data.order_number} />
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
@ -49,9 +46,6 @@ const OrderDetails = () => {
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CustomerDetails orderData={data} />
|
||||
</Grid>
|
||||
{/* <Grid size={{ xs: 12 }}>
|
||||
<ShippingAddress />
|
||||
</Grid> */}
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<BillingAddress data={data} />
|
||||
</Grid>
|
||||
|
||||
@ -12,9 +12,9 @@ import OrderListTable from './OrderListTable'
|
||||
const OrderList = () => {
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
{/* <Grid size={{ xs: 12 }}>
|
||||
<OrderCard />
|
||||
</Grid>
|
||||
</Grid> */}
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<OrderListTable />
|
||||
</Grid>
|
||||
|
||||
@ -55,7 +55,7 @@ const ProductAddHeader = () => {
|
||||
<Button variant='tonal' color='secondary'>
|
||||
Discard
|
||||
</Button>
|
||||
<Button variant='tonal'>Save Draft</Button>
|
||||
{/* <Button variant='tonal'>Save Draft</Button> */}
|
||||
<Button variant='contained' disabled={isEdit ? isUpdating : isCreating} onClick={handleSubmit}>
|
||||
{isEdit ? 'Update Product' : 'Publish Product'}
|
||||
{(isCreating || isUpdating) && <CircularProgress color='inherit' size={16} className='ml-2' />}
|
||||
|
||||
@ -126,13 +126,27 @@ const ProductInformation = () => {
|
||||
const params = useParams()
|
||||
|
||||
const { data: product, isLoading, error } = useProductById(params?.id as string)
|
||||
const { name, sku, barcode, description } = useSelector((state: RootState) => state.productReducer.productRequest)
|
||||
const { name, sku, description } = useSelector((state: RootState) => state.productReducer.productRequest)
|
||||
|
||||
console.log('desc', description)
|
||||
|
||||
const isEdit = !!params?.id
|
||||
|
||||
useEffect(() => {
|
||||
if (product) {
|
||||
dispatch(setProduct(product))
|
||||
dispatch(
|
||||
setProduct({
|
||||
name: product.name,
|
||||
sku: product.sku || '',
|
||||
description: product.description || '',
|
||||
price: product.price,
|
||||
cost: product.cost,
|
||||
category_id: product.category_id,
|
||||
printer_type: product.printer_type,
|
||||
image_url: product.image_url || '',
|
||||
variants: product.variants || []
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [product, dispatch])
|
||||
|
||||
@ -152,9 +166,11 @@ const ProductInformation = () => {
|
||||
Underline
|
||||
],
|
||||
immediatelyRender: false,
|
||||
content: `
|
||||
content: params?.id
|
||||
? description
|
||||
: `
|
||||
<p>
|
||||
${description || ''}
|
||||
${description}
|
||||
</p>
|
||||
`
|
||||
})
|
||||
@ -181,7 +197,7 @@ const ProductInformation = () => {
|
||||
<CardHeader title='Product Information' />
|
||||
<CardContent>
|
||||
<Grid container spacing={6} className='mbe-6'>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Product Name'
|
||||
@ -199,15 +215,6 @@ const ProductInformation = () => {
|
||||
onChange={e => handleInputChange('sku', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Barcode'
|
||||
placeholder='0123-4567'
|
||||
value={barcode || ''}
|
||||
onChange={e => handleInputChange('barcode', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography className='mbe-1'>Description (Optional)</Typography>
|
||||
<Card className='p-0 border shadow-none'>
|
||||
|
||||
@ -16,12 +16,22 @@ import { RootState } from '../../../../../redux-store'
|
||||
import { setProductField } from '../../../../../redux-store/slices/product'
|
||||
import { useCategories } from '../../../../../services/queries/categories'
|
||||
import { Category } from '../../../../../types/services/category'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Autocomplete, CircularProgress } from '@mui/material'
|
||||
|
||||
const ProductOrganize = () => {
|
||||
const dispatch = useDispatch()
|
||||
const { category_id, printer_type } = useSelector((state: RootState) => state.productReducer.productRequest)
|
||||
|
||||
const { data: categoriesApi } = useCategories()
|
||||
const [categoryInput, setCategoryInput] = useState('')
|
||||
const [categoryDebouncedInput] = useDebounce(categoryInput, 500)
|
||||
|
||||
const { data: categoriesApi, isLoading: categoriesLoading } = useCategories({
|
||||
search: categoryDebouncedInput
|
||||
})
|
||||
|
||||
const categoryOptions = useMemo(() => categoriesApi?.categories || [], [categoriesApi])
|
||||
|
||||
const handleSelectChange = (field: any, value: any) => {
|
||||
dispatch(setProductField({ field, value }))
|
||||
@ -33,25 +43,36 @@ const ProductOrganize = () => {
|
||||
<CardContent>
|
||||
<form onSubmit={e => e.preventDefault()} className='flex flex-col gap-6'>
|
||||
<div className='flex items-end gap-4'>
|
||||
<CustomTextField
|
||||
select
|
||||
<Autocomplete
|
||||
options={categoryOptions}
|
||||
loading={categoriesLoading}
|
||||
fullWidth
|
||||
label='Category'
|
||||
value={category_id}
|
||||
onChange={e => handleSelectChange('category_id', e.target.value)}
|
||||
>
|
||||
{categoriesApi?.categories.length ? (
|
||||
categoriesApi?.categories.map((item: Category, index: number) => (
|
||||
<MenuItem key={index} value={item.id}>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))
|
||||
) : (
|
||||
<MenuItem disabled value=''>
|
||||
Loading categories...
|
||||
</MenuItem>
|
||||
getOptionLabel={option => option.name}
|
||||
value={categoryOptions.find(p => p.id === category_id) || null}
|
||||
onInputChange={(event, newCategoryInput) => {
|
||||
setCategoryInput(newCategoryInput)
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
dispatch(setProductField({ field: 'category_id', value: newValue?.id || '' }))
|
||||
}}
|
||||
renderInput={params => (
|
||||
<CustomTextField
|
||||
{...params}
|
||||
className=''
|
||||
label='Category'
|
||||
fullWidth
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<>
|
||||
{categoriesLoading && <CircularProgress size={18} />}
|
||||
{params.InputProps.endAdornment}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</CustomTextField>
|
||||
/>
|
||||
<CustomIconButton variant='tonal' color='primary' className='min-is-fit'>
|
||||
<i className='tabler-plus' />
|
||||
</CustomIconButton>
|
||||
@ -65,12 +86,6 @@ const ProductOrganize = () => {
|
||||
>
|
||||
<MenuItem value={`kitchen`}>Kitchen</MenuItem>
|
||||
</CustomTextField>
|
||||
{/* <CustomTextField select fullWidth label='Status' value={status} onChange={e => setStatus(e.target.value)}>
|
||||
<MenuItem value='Published'>Published</MenuItem>
|
||||
<MenuItem value='Inactive'>Inactive</MenuItem>
|
||||
<MenuItem value='Scheduled'>Scheduled</MenuItem>
|
||||
</CustomTextField>
|
||||
<CustomTextField fullWidth label='Enter Tags' placeholder='Fashion, Trending, Summer' /> */}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// React Imports
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Button from '@mui/material/Button'
|
||||
@ -19,12 +19,12 @@ import { Autocomplete } from '@mui/material'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { RootState } from '../../../../../redux-store'
|
||||
import { resetProductRecipe } from '../../../../../redux-store/slices/productRecipe'
|
||||
import { useProductRecipesMutation } from '../../../../../services/mutations/productRecipes'
|
||||
import { useIngredients } from '../../../../../services/queries/ingredients'
|
||||
import { useOutlets } from '../../../../../services/queries/outlets'
|
||||
import { Product } from '../../../../../types/services/product'
|
||||
import { ProductRecipeRequest } from '../../../../../types/services/productRecipe'
|
||||
import { resetProductVariant } from '../../../../../redux-store/slices/productRecipe'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
@ -47,7 +47,7 @@ const AddRecipeDrawer = (props: Props) => {
|
||||
// Props
|
||||
const { open, handleClose, product } = props
|
||||
|
||||
const { currentProductRecipe } = useSelector((state: RootState) => state.productRecipeReducer)
|
||||
const { currentVariant, currentProductRecipe } = useSelector((state: RootState) => state.productRecipeReducer)
|
||||
|
||||
const [outletInput, setOutletInput] = useState('')
|
||||
const [outletDebouncedInput] = useDebounce(outletInput, 500)
|
||||
@ -67,22 +67,39 @@ const AddRecipeDrawer = (props: Props) => {
|
||||
|
||||
const { createProductRecipe, updateProductRecipe } = useProductRecipesMutation()
|
||||
|
||||
useEffect(() => {
|
||||
if (currentProductRecipe.id) {
|
||||
setFormData(currentProductRecipe)
|
||||
}
|
||||
}, [currentProductRecipe])
|
||||
|
||||
const handleSubmit = (e: any) => {
|
||||
e.preventDefault()
|
||||
|
||||
createProductRecipe.mutate(
|
||||
{ ...formData, product_id: product.id, variant_id: currentProductRecipe.id || '' },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleReset()
|
||||
if (currentProductRecipe.id) {
|
||||
updateProductRecipe.mutate(
|
||||
{ id: currentProductRecipe.id, payload: formData },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleReset()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
createProductRecipe.mutate(
|
||||
{ ...formData, product_id: product.id, variant_id: currentVariant.id || '' },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleReset()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
handleClose()
|
||||
dispatch(resetProductRecipe())
|
||||
dispatch(resetProductVariant())
|
||||
setFormData(initialData)
|
||||
}
|
||||
|
||||
@ -94,13 +111,15 @@ const AddRecipeDrawer = (props: Props) => {
|
||||
}
|
||||
|
||||
const setTitleDrawer = (recipe: any) => {
|
||||
const addOrEdit = currentProductRecipe.id ? 'Edit ' : 'Add '
|
||||
|
||||
let title = 'Original'
|
||||
|
||||
if (recipe?.name) {
|
||||
title = recipe?.name
|
||||
}
|
||||
|
||||
return title
|
||||
return addOrEdit + title
|
||||
}
|
||||
|
||||
return (
|
||||
@ -113,7 +132,7 @@ const AddRecipeDrawer = (props: Props) => {
|
||||
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
|
||||
>
|
||||
<div className='flex items-center justify-between pli-6 plb-5'>
|
||||
<Typography variant='h5'>{setTitleDrawer(currentProductRecipe)} Variant Ingredient</Typography>
|
||||
<Typography variant='h5'>{setTitleDrawer(currentVariant)} Variant Ingredient</Typography>
|
||||
<IconButton size='small' onClick={handleReset}>
|
||||
<i className='tabler-x text-2xl' />
|
||||
</IconButton>
|
||||
@ -192,9 +211,13 @@ const AddRecipeDrawer = (props: Props) => {
|
||||
type='submit'
|
||||
disabled={createProductRecipe.isPending || updateProductRecipe.isPending}
|
||||
>
|
||||
{createProductRecipe.isPending
|
||||
? 'Adding...'
|
||||
: 'Add'}
|
||||
{currentProductRecipe.id
|
||||
? updateProductRecipe.isPending
|
||||
? 'Updating...'
|
||||
: 'Update'
|
||||
: createProductRecipe.isPending
|
||||
? 'Creating...'
|
||||
: 'Create'}
|
||||
</Button>
|
||||
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
|
||||
Discard
|
||||
|
||||
@ -16,33 +16,54 @@ import {
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import Loading from '../../../../../components/layout/shared/Loading'
|
||||
import { setProductRecipe } from '../../../../../redux-store/slices/productRecipe'
|
||||
import { useProductRecipesByProduct } from '../../../../../services/queries/productRecipes'
|
||||
import { useProductById } from '../../../../../services/queries/products'
|
||||
import { ProductVariant } from '../../../../../types/services/product'
|
||||
import { formatCurrency } from '../../../../../utils/transform'
|
||||
import AddRecipeDrawer from './AddRecipeDrawer'
|
||||
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
|
||||
import { useProductRecipesMutation } from '../../../../../services/mutations/productRecipes'
|
||||
import { setProductRecipe, setProductVariant } from '../../../../../redux-store/slices/productRecipe'
|
||||
import { ProductRecipe } from '../../../../../types/services/productRecipe'
|
||||
|
||||
const ProductDetail = () => {
|
||||
const dispatch = useDispatch()
|
||||
const params = useParams()
|
||||
|
||||
const [openProductRecipe, setOpenProductRecipe] = useState(false)
|
||||
const [openConfirm, setOpenConfirm] = useState(false)
|
||||
const [productRecipeId, setProductRecipeId] = useState('')
|
||||
|
||||
const { data: product, isLoading, error } = useProductById(params?.id as string)
|
||||
const { data: productRecipe, isLoading: isLoadingProductRecipe } = useProductRecipesByProduct(params?.id as string)
|
||||
|
||||
const handleOpenProductRecipe = (recipe: any) => {
|
||||
const { deleteProductRecipe } = useProductRecipesMutation()
|
||||
|
||||
const handleOpenProductRecipe = (variant: any) => {
|
||||
setOpenProductRecipe(true)
|
||||
dispatch(setProductVariant(variant))
|
||||
}
|
||||
|
||||
const handleOpenEditProductRecipe = (recipe: ProductRecipe) => {
|
||||
setOpenProductRecipe(true)
|
||||
dispatch(setProductRecipe(recipe))
|
||||
}
|
||||
|
||||
const handleDeleteRecipe = () => {
|
||||
deleteProductRecipe.mutate(productRecipeId, {
|
||||
onSuccess: () => {
|
||||
setOpenConfirm(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (isLoading || isLoadingProductRecipe) return <Loading />
|
||||
|
||||
return (
|
||||
@ -149,13 +170,14 @@ const ProductDetail = () => {
|
||||
Total Cost
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{productRecipe?.length &&
|
||||
{productRecipe?.length ? (
|
||||
productRecipe
|
||||
.filter((item: any) => item.variant_id === null)
|
||||
.map((item: any, index: number) => (
|
||||
.filter((item: ProductRecipe) => item.variant_id === null)
|
||||
.map((item: ProductRecipe, index: number) => (
|
||||
<TableRow key={index} className='hover:bg-gray-50'>
|
||||
<TableCell>
|
||||
<div className='flex items-center gap-3'>
|
||||
@ -185,14 +207,42 @@ const ProductDetail = () => {
|
||||
<TableCell className='text-right font-medium'>
|
||||
{formatCurrency(item.ingredient.cost * item.quantity)}
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Button size='small' color='info' onClick={() => handleOpenEditProductRecipe(item)}>
|
||||
<Tooltip title='Edit'>
|
||||
<i className='tabler-pencil' />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
color='error'
|
||||
onClick={() => {
|
||||
setProductRecipeId(item.id)
|
||||
setOpenConfirm(true)
|
||||
}}
|
||||
>
|
||||
<Tooltip title='Delete'>
|
||||
<i className='tabler-trash' />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className='text-center'>
|
||||
<Typography variant='body2' color='textSecondary'>
|
||||
No ingredients found for this variant
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Variant Summary */}
|
||||
{productRecipe?.length && (
|
||||
{productRecipe?.length ? (
|
||||
<Box className='mt-4 p-4 bg-blue-50 rounded-lg'>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
@ -215,6 +265,13 @@ const ProductDetail = () => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
) : (
|
||||
<Box className='mt-4 p-4 bg-blue-50 rounded-lg'>
|
||||
<Typography variant='body2' className='flex items-center gap-2'>
|
||||
<i className='tabler-list-numbers text-blue-600' />
|
||||
<span className='font-semibold'>Total Ingredients:</span>
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Button
|
||||
@ -294,7 +351,7 @@ const ProductDetail = () => {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{productRecipe?.length &&
|
||||
{productRecipe?.length ? (
|
||||
productRecipe
|
||||
.filter((item: any) => item.variant_id === variantData.id)
|
||||
.map((item: any, index: number) => (
|
||||
@ -327,14 +384,42 @@ const ProductDetail = () => {
|
||||
<TableCell className='text-right font-medium'>
|
||||
{formatCurrency(item.ingredient.cost * item.quantity)}
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Button size='small' color='info' onClick={() => handleOpenEditProductRecipe(item)}>
|
||||
<Tooltip title='Edit'>
|
||||
<i className='tabler-pencil' />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
color='error'
|
||||
onClick={() => {
|
||||
setProductRecipeId(item.id)
|
||||
setOpenConfirm(true)
|
||||
}}
|
||||
>
|
||||
<Tooltip title='Delete'>
|
||||
<i className='tabler-trash' />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className='text-center'>
|
||||
<Typography variant='body2' color='textSecondary'>
|
||||
No ingredients found for this variant
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Variant Summary */}
|
||||
{productRecipe?.length && (
|
||||
{productRecipe?.length ? (
|
||||
<Box className='mt-4 p-4 bg-blue-50 rounded-lg'>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
@ -357,6 +442,13 @@ const ProductDetail = () => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
) : (
|
||||
<Box className='mt-4 p-4 bg-blue-50 rounded-lg'>
|
||||
<Typography variant='body2' className='flex items-center gap-2'>
|
||||
<i className='tabler-list-numbers text-blue-600' />
|
||||
<span className='font-semibold'>Total Ingredients:</span>
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Button
|
||||
@ -376,6 +468,15 @@ const ProductDetail = () => {
|
||||
</div>
|
||||
|
||||
<AddRecipeDrawer open={openProductRecipe} handleClose={() => setOpenProductRecipe(false)} product={product!} />
|
||||
|
||||
<ConfirmDeleteDialog
|
||||
open={openConfirm}
|
||||
onClose={() => setOpenConfirm(false)}
|
||||
onConfirm={handleDeleteRecipe}
|
||||
isLoading={deleteProductRecipe.isPending}
|
||||
title='Delete Product Ingredient'
|
||||
message='Are you sure you want to delete this product ingredient? This action cannot be undone.'
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ import { getLocalizedUrl } from '@/utils/i18n'
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
import { Box, CircularProgress } from '@mui/material'
|
||||
import Loading from '../../../../../components/layout/shared/Loading'
|
||||
import { useProducts } from '../../../../../services/queries/products'
|
||||
import { ProductsQueryParams, useProducts } from '../../../../../services/queries/products'
|
||||
import { Product } from '../../../../../types/services/product'
|
||||
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
|
||||
import { useProductsMutation } from '../../../../../services/mutations/products'
|
||||
@ -115,6 +115,11 @@ const ProductListTable = () => {
|
||||
const [productId, setProductId] = useState('')
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const [filter, setFilter] = useState<ProductsQueryParams>({
|
||||
is_active: '',
|
||||
category_id: ''
|
||||
})
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
@ -122,7 +127,8 @@ const ProductListTable = () => {
|
||||
const { data, isLoading, error, isFetching } = useProducts({
|
||||
page: currentPage,
|
||||
limit: pageSize,
|
||||
search
|
||||
search: search,
|
||||
is_active: filter.is_active
|
||||
})
|
||||
|
||||
const { mutate: deleteProduct, isPending: isDeleting } = useProductsMutation().deleteProduct
|
||||
@ -276,7 +282,7 @@ const ProductListTable = () => {
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader title='Filters' />
|
||||
<TableFilters setData={() => {}} productData={[]} />
|
||||
<TableFilters filter={filter} setFilter={setFilter} />
|
||||
<Divider />
|
||||
<div className='flex flex-wrap justify-between gap-4 p-6'>
|
||||
<DebouncedInput
|
||||
|
||||
@ -1,47 +1,45 @@
|
||||
// React Imports
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
|
||||
// Type Imports
|
||||
import type { ProductType } from '@/types/apps/ecommerceTypes'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
import { ProductsQueryParams } from '../../../../../services/queries/products'
|
||||
import { Product } from '../../../../../types/services/product'
|
||||
import { useCategories } from '../../../../../services/queries/categories'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { Autocomplete, CircularProgress } from '@mui/material'
|
||||
|
||||
type ProductStockType = { [key: string]: boolean }
|
||||
|
||||
// Vars
|
||||
const productStockObj: ProductStockType = {
|
||||
'In Stock': true,
|
||||
'Out of Stock': false
|
||||
}
|
||||
|
||||
const TableFilters = ({ setData, productData }: { setData: (data: Product[]) => void; productData?: Product[] }) => {
|
||||
const TableFilters = ({
|
||||
filter,
|
||||
setFilter
|
||||
}: {
|
||||
filter: ProductsQueryParams
|
||||
setFilter: (data: ProductsQueryParams) => void
|
||||
}) => {
|
||||
// States
|
||||
const [category, setCategory] = useState<Product['category_id']>('')
|
||||
const [stock, setStock] = useState('')
|
||||
const [status, setStatus] = useState<Product['name']>('')
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const filteredData = productData?.filter(product => {
|
||||
if (category && product.category_id !== category) return false
|
||||
if (stock && product.name !== stock) return false
|
||||
if (status && product.name !== status) return false
|
||||
const [categoryInput, setCategoryInput] = useState('')
|
||||
const [categoryDebouncedInput] = useDebounce(categoryInput, 500)
|
||||
|
||||
return true
|
||||
})
|
||||
const { data: categoriesApi, isLoading: categoriesLoading } = useCategories({
|
||||
search: categoryDebouncedInput
|
||||
})
|
||||
|
||||
setData(filteredData ?? [])
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[category, stock, status, productData]
|
||||
)
|
||||
const categoryOptions = useMemo(() => categoriesApi?.categories || [], [categoriesApi])
|
||||
|
||||
const handleStatusChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilter({ ...filter, is_active: e.target.value === 'Active' ? true : e.target.value === 'Inactive' ? false : '' })
|
||||
}
|
||||
|
||||
return (
|
||||
<CardContent>
|
||||
@ -51,37 +49,47 @@ const TableFilters = ({ setData, productData }: { setData: (data: Product[]) =>
|
||||
select
|
||||
fullWidth
|
||||
id='select-status'
|
||||
value={status}
|
||||
onChange={e => setStatus(e.target.value)}
|
||||
value={filter.is_active ? 'Active' : filter.is_active === false ? 'Inactive' : ''}
|
||||
onChange={handleStatusChange}
|
||||
slotProps={{
|
||||
select: { displayEmpty: true }
|
||||
}}
|
||||
>
|
||||
<MenuItem value=''>Select Status</MenuItem>
|
||||
<MenuItem value='Scheduled'>Scheduled</MenuItem>
|
||||
<MenuItem value='Published'>Publish</MenuItem>
|
||||
<MenuItem value='Active'>Active</MenuItem>
|
||||
<MenuItem value='Inactive'>Inactive</MenuItem>
|
||||
</CustomTextField>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<CustomTextField
|
||||
select
|
||||
<Autocomplete
|
||||
options={categoryOptions}
|
||||
loading={categoriesLoading}
|
||||
fullWidth
|
||||
id='select-category'
|
||||
value={category}
|
||||
onChange={e => setCategory(e.target.value)}
|
||||
slotProps={{
|
||||
select: { displayEmpty: true }
|
||||
getOptionLabel={option => option.name}
|
||||
value={categoryOptions.find(p => p.id === filter.category_id) || null}
|
||||
onInputChange={(event, newCategoryInput) => {
|
||||
setCategoryInput(newCategoryInput)
|
||||
}}
|
||||
>
|
||||
<MenuItem value=''>Select Category</MenuItem>
|
||||
<MenuItem value='Accessories'>Accessories</MenuItem>
|
||||
<MenuItem value='Home Decor'>Home Decor</MenuItem>
|
||||
<MenuItem value='Electronics'>Electronics</MenuItem>
|
||||
<MenuItem value='Shoes'>Shoes</MenuItem>
|
||||
<MenuItem value='Office'>Office</MenuItem>
|
||||
<MenuItem value='Games'>Games</MenuItem>
|
||||
</CustomTextField>
|
||||
onChange={(event, newValue) => {
|
||||
setFilter({ ...filter, category_id: newValue?.id || '' })
|
||||
}}
|
||||
renderInput={params => (
|
||||
<CustomTextField
|
||||
{...params}
|
||||
fullWidth
|
||||
placeholder='Search Category'
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<>
|
||||
{categoriesLoading && <CircularProgress size={18} />}
|
||||
{params.InputProps.endAdornment}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<CustomTextField
|
||||
|
||||
@ -103,11 +103,13 @@ const StockListTable = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [addInventoryOpen, setAddInventoryOpen] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
// Fetch products with pagination and search
|
||||
const { data, isLoading, error, isFetching } = useInventories({
|
||||
page: currentPage,
|
||||
limit: pageSize
|
||||
limit: pageSize,
|
||||
search
|
||||
})
|
||||
|
||||
const inventories = data?.inventory ?? []
|
||||
@ -150,7 +152,7 @@ const StockListTable = () => {
|
||||
},
|
||||
columnHelper.accessor('product_id', {
|
||||
header: 'Product',
|
||||
cell: ({ row }) => <Typography>{row.original.product_id}</Typography>
|
||||
cell: ({ row }) => <Typography>{row.original.product?.name}</Typography>
|
||||
}),
|
||||
columnHelper.accessor('quantity', {
|
||||
header: 'Quantity',
|
||||
@ -171,32 +173,6 @@ const StockListTable = () => {
|
||||
/>
|
||||
)
|
||||
})
|
||||
// columnHelper.accessor('actions', {
|
||||
// header: 'Actions',
|
||||
// cell: ({ row }) => (
|
||||
// <div className='flex items-center'>
|
||||
// <OptionMenu
|
||||
// iconButtonProps={{ size: 'medium' }}
|
||||
// iconClassName='text-textSecondary'
|
||||
// options={[
|
||||
// { text: 'Download', icon: 'tabler-download' },
|
||||
// {
|
||||
// text: 'Delete',
|
||||
// icon: 'tabler-trash',
|
||||
// menuItemProps: {
|
||||
// onClick: () => {
|
||||
// setOpenConfirm(true)
|
||||
// setProductId(row.original.id)
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// { text: 'Duplicate', icon: 'tabler-copy' }
|
||||
// ]}
|
||||
// />
|
||||
// </div>
|
||||
// ),
|
||||
// enableSorting: false
|
||||
// })
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
@ -226,13 +202,13 @@ const StockListTable = () => {
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader title='Filters' />
|
||||
{/* <CardHeader title='Filters' /> */}
|
||||
{/* <TableFilters setData={() => {}} productData={[]} /> */}
|
||||
<Divider />
|
||||
{/* <Divider /> */}
|
||||
<div className='flex flex-wrap justify-between gap-4 p-6'>
|
||||
<DebouncedInput
|
||||
value={'search'}
|
||||
onChange={value => console.log(value)}
|
||||
value={search}
|
||||
onChange={value => setSearch(String(value))}
|
||||
placeholder='Search Product'
|
||||
className='max-sm:is-full'
|
||||
/>
|
||||
@ -261,7 +237,7 @@ const StockListTable = () => {
|
||||
onClick={() => setAddInventoryOpen(!addInventoryOpen)}
|
||||
startIcon={<i className='tabler-plus' />}
|
||||
>
|
||||
Adjust Inventory
|
||||
Adjust Stock
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -107,11 +107,13 @@ const StockListTable = () => {
|
||||
const [openConfirm, setOpenConfirm] = useState(false)
|
||||
const [productId, setProductId] = useState('')
|
||||
const [addInventoryOpen, setAddInventoryOpen] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
// Fetch products with pagination and search
|
||||
const { data, isLoading, error, isFetching } = useInventories({
|
||||
page: currentPage,
|
||||
limit: pageSize
|
||||
limit: pageSize,
|
||||
search
|
||||
})
|
||||
|
||||
const { mutate: deleteInventory, isPending: isDeleting } = useInventoriesMutation().deleteInventory
|
||||
@ -162,7 +164,7 @@ const StockListTable = () => {
|
||||
},
|
||||
columnHelper.accessor('product_id', {
|
||||
header: 'Product',
|
||||
cell: ({ row }) => <Typography>{row.original.product_id}</Typography>
|
||||
cell: ({ row }) => <Typography>{row.original.product?.name}</Typography>
|
||||
}),
|
||||
columnHelper.accessor('is_low_stock', {
|
||||
header: 'Status',
|
||||
@ -241,8 +243,8 @@ const StockListTable = () => {
|
||||
<Divider />
|
||||
<div className='flex flex-wrap justify-between gap-4 p-6'>
|
||||
<DebouncedInput
|
||||
value={'search'}
|
||||
onChange={value => console.log(value)}
|
||||
value={search}
|
||||
onChange={value => setSearch(value as string)}
|
||||
placeholder='Search Product'
|
||||
className='max-sm:is-full'
|
||||
/>
|
||||
@ -271,7 +273,7 @@ const StockListTable = () => {
|
||||
onClick={() => setAddInventoryOpen(!addInventoryOpen)}
|
||||
startIcon={<i className='tabler-plus' />}
|
||||
>
|
||||
Add Inventory
|
||||
Add Stock
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user