fix: product and inventory

This commit is contained in:
ferdiansyah783 2025-08-13 23:53:03 +07:00
parent ea6205cfa7
commit ba7911d04b
28 changed files with 435 additions and 204 deletions

View File

@ -3,12 +3,11 @@ import Grid from '@mui/material/Grid2'
// Component Imports // Component Imports
import ProductAddHeader from '@views/apps/ecommerce/products/add/ProductAddHeader' 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 ProductImage from '@views/apps/ecommerce/products/add/ProductImage'
import ProductVariants from '@views/apps/ecommerce/products/add/ProductVariants' import ProductInformation from '@views/apps/ecommerce/products/add/ProductInformation'
import ProductInventory from '@views/apps/ecommerce/products/add/ProductInventory'
import ProductPricing from '@views/apps/ecommerce/products/add/ProductPricing'
import ProductOrganize from '@views/apps/ecommerce/products/add/ProductOrganize' 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 = () => { const eCommerceProductsEdit = () => {
return ( return (
@ -27,9 +26,6 @@ const eCommerceProductsEdit = () => {
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<ProductVariants /> <ProductVariants />
</Grid> </Grid>
<Grid size={{ xs: 12 }}>
<ProductInventory />
</Grid>
</Grid> </Grid>
</Grid> </Grid>
<Grid size={{ xs: 12, md: 4 }}> <Grid size={{ xs: 12, md: 4 }}>

View File

@ -14,12 +14,12 @@ const DashboardOverview = () => {
if (isLoading) return <Loading /> 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='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 items-center justify-between'>
<div className='flex-1'> <div className='flex-1'>
<h3 className='text-sm font-medium text-gray-600 mb-2'>{title}</h3> <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>} {subtitle && <p className='text-sm text-gray-500'>{subtitle}</p>}
</div> </div>
<div className={`px-4 py-3 rounded-full ${bgColor} bg-opacity-10`}> <div className={`px-4 py-3 rounded-full ${bgColor} bg-opacity-10`}>
@ -52,12 +52,6 @@ const DashboardOverview = () => {
{/* Overview Metrics */} {/* Overview Metrics */}
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8'> <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 <MetricCard
iconClass='tabler-shopping-cart' iconClass='tabler-shopping-cart'
title='Total Orders' title='Total Orders'
@ -65,11 +59,19 @@ const DashboardOverview = () => {
subtitle={`${salesData.overview.voided_orders} voided, ${salesData.overview.refunded_orders} refunded`} subtitle={`${salesData.overview.voided_orders} voided, ${salesData.overview.refunded_orders} refunded`}
bgColor='bg-blue-500' bgColor='bg-blue-500'
/> />
<MetricCard
iconClass='tabler-cash'
title='Total Sales'
value={formatShortCurrency(salesData.overview.total_sales)}
bgColor='bg-green-500'
isCurrency={true}
/>
<MetricCard <MetricCard
iconClass='tabler-trending-up' iconClass='tabler-trending-up'
title='Average Order Value' title='Average Order Value'
value={formatShortCurrency(salesData.overview.average_order_value)} value={formatShortCurrency(salesData.overview.average_order_value)}
bgColor='bg-purple-500' bgColor='bg-purple-500'
isCurrency={true}
/> />
<MetricCard <MetricCard
iconClass='tabler-users' iconClass='tabler-users'

View File

@ -39,7 +39,7 @@ const DashboardProfitloss = () => {
function formatMetricName(metric: string): string { function formatMetricName(metric: string): string {
const nameMap: { [key: string]: string } = { const nameMap: { [key: string]: string } = {
revenue: 'Revenue', revenue: 'Revenue',
net_profit: 'Net Profit', net_profit: 'Net Profit'
} }
return nameMap[metric] || metric.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) 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='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 items-center justify-between'>
<div className='flex-1'> <div className='flex-1'>
<h3 className='text-sm font-medium text-gray-600 mb-2'>{title}</h3> <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>} {subtitle && <p className='text-sm text-gray-500'>{subtitle}</p>}
</div> </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> <i className={`${iconClass} text-[32px] ${bgColor.replace('bg-', 'text-')}`}></i>
</div> </div>
</div> </div>
@ -95,12 +105,14 @@ const DashboardProfitloss = () => {
title='Total Revenue' title='Total Revenue'
value={formatShortCurrency(profitData.summary.total_revenue)} value={formatShortCurrency(profitData.summary.total_revenue)}
bgColor='bg-green-500' bgColor='bg-green-500'
isCurrency={true}
/> />
<MetricCard <MetricCard
iconClass='tabler-receipt' iconClass='tabler-receipt'
title='Total Cost' title='Total Cost'
value={formatShortCurrency(profitData.summary.total_cost)} value={formatShortCurrency(profitData.summary.total_cost)}
bgColor='bg-red-500' bgColor='bg-red-500'
isCurrency={true}
/> />
<MetricCard <MetricCard
iconClass='tabler-trending-up' iconClass='tabler-trending-up'
@ -109,6 +121,7 @@ const DashboardProfitloss = () => {
subtitle={`Margin: ${formatPercentage(profitData.summary.gross_profit_margin)}`} subtitle={`Margin: ${formatPercentage(profitData.summary.gross_profit_margin)}`}
bgColor='bg-blue-500' bgColor='bg-blue-500'
isNegative={profitData.summary.gross_profit < 0} isNegative={profitData.summary.gross_profit < 0}
isCurrency={true}
/> />
<MetricCard <MetricCard
iconClass='tabler-percentage' iconClass='tabler-percentage'
@ -127,7 +140,7 @@ const DashboardProfitloss = () => {
<h3 className='text-lg font-semibold text-gray-900'>Net Profit</h3> <h3 className='text-lg font-semibold text-gray-900'>Net Profit</h3>
</div> </div>
<p className='text-3xl font-bold text-green-600 mb-2'> <p className='text-3xl font-bold text-green-600 mb-2'>
{formatShortCurrency(profitData.summary.net_profit)} Rp {formatShortCurrency(profitData.summary.net_profit)}
</p> </p>
<p className='text-sm text-gray-600'>Margin: {formatPercentage(profitData.summary.net_profit_margin)}</p> <p className='text-sm text-gray-600'>Margin: {formatPercentage(profitData.summary.net_profit_margin)}</p>
</div> </div>
@ -144,7 +157,7 @@ const DashboardProfitloss = () => {
<h3 className='text-lg font-semibold text-gray-900'>Tax & Discount</h3> <h3 className='text-lg font-semibold text-gray-900'>Tax & Discount</h3>
</div> </div>
<p className='text-xl font-bold text-orange-600 mb-1'> <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>
<p className='text-sm text-gray-600'> <p className='text-sm text-gray-600'>
Tax: {formatShortCurrency(profitData.summary.total_tax)} | Discount:{' '} Tax: {formatShortCurrency(profitData.summary.total_tax)} | Discount:{' '}

View File

@ -90,7 +90,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
<MenuItem href={`/${locale}/dashboards/payment-methods`}>{dictionary['navigation'].paymentMethods}</MenuItem> <MenuItem href={`/${locale}/dashboards/payment-methods`}>{dictionary['navigation'].paymentMethods}</MenuItem>
</SubMenu> </SubMenu>
<MenuSection label={dictionary['navigation'].appsPages}> <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> */} {/* <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>

View File

@ -2,7 +2,7 @@
"navigation": { "navigation": {
"dashboards": "لوحات القيادة", "dashboards": "لوحات القيادة",
"analytics": "تحليلات", "analytics": "تحليلات",
"eCommerce": "التجارة الإلكترونية", "eCommerce": "تجزئة الكترونية",
"stock": "المخزون", "stock": "المخزون",
"academy": "أكاديمية", "academy": "أكاديمية",
"logistics": "اللوجستية", "logistics": "اللوجستية",

View File

@ -2,7 +2,7 @@
"navigation": { "navigation": {
"dashboards": "Dashboards", "dashboards": "Dashboards",
"analytics": "Analytics", "analytics": "Analytics",
"eCommerce": "eCommerce", "eCommerce": "Inventory",
"stock": "Stock", "stock": "Stock",
"academy": "Academy", "academy": "Academy",
"logistics": "Logistics", "logistics": "Logistics",

View File

@ -2,7 +2,7 @@
"navigation": { "navigation": {
"dashboards": "Tableaux de bord", "dashboards": "Tableaux de bord",
"analytics": "Analytique", "analytics": "Analytique",
"eCommerce": "commerce électronique", "eCommerce": "Inventaire",
"stock": "Stock", "stock": "Stock",
"academy": "Académie", "academy": "Académie",
"logistics": "Logistique", "logistics": "Logistique",

View File

@ -13,7 +13,6 @@ const initialState: { productRequest: ProductRequest } = {
sku: '', sku: '',
name: '', name: '',
description: '', description: '',
barcode: '',
price: 0, price: 0,
cost: 0, cost: 0,
printer_type: '', printer_type: '',

View File

@ -1,20 +1,71 @@
// Third-party Imports // Third-party Imports
import type { PayloadAction } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit'
import { ProductRecipe } from '../../types/services/productRecipe'
// Type Imports // Type Imports
// Data Imports // Data Imports
const initialState: { currentProductRecipe: any } = { const initialState: { currentVariant: any, currentProductRecipe: ProductRecipe } = {
currentProductRecipe: {} 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({ export const productRecipeSlice = createSlice({
name: 'productRecipe', name: 'productRecipe',
initialState, initialState,
reducers: { 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 state.currentProductRecipe = action.payload
}, },
resetProductRecipe: state => { 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 export default productRecipeSlice.reducer

View File

@ -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 { return {
createProductRecipe, createProductRecipe,
updateProductRecipe updateProductRecipe,
deleteProductRecipe
} }
} }

View File

@ -1,15 +1,14 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { Product, Products } from '../../types/services/product' import { Product, Products } from '../../types/services/product'
import { api } from '../api' import { api } from '../api'
import { ProductRecipe } from '../../types/services/productRecipe'
interface ProductsQueryParams { export interface ProductsQueryParams {
page?: number page?: number
limit?: number limit?: number
search?: string search?: string
// Add other filter parameters as needed // Add other filter parameters as needed
category_id?: string category_id?: string
is_active?: boolean is_active?: boolean | string
} }
export function useProducts(params: ProductsQueryParams = {}) { export function useProducts(params: ProductsQueryParams = {}) {

View File

@ -13,6 +13,7 @@ export interface Inventory {
quantity: number quantity: number
reorder_level: number reorder_level: number
is_low_stock: boolean is_low_stock: boolean
product: any
updated_at: string // ISO 8601 timestamp updated_at: string // ISO 8601 timestamp
} }

View File

@ -47,7 +47,6 @@ export type ProductRequest = {
sku: string sku: string
name: string name: string
description: string description: string
barcode: string
price: number price: number
cost: number cost: number
printer_type: string printer_type: string

View File

@ -133,11 +133,22 @@ const Login = ({ mode }: { mode: SystemMode }) => {
const handleClickShowPassword = () => setIsPasswordShown(show => !show) const handleClickShowPassword = () => setIsPasswordShown(show => !show)
const onSubmit: SubmitHandler<FormData> = async (data: FormData) => { 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 ( return (
@ -243,7 +254,11 @@ const Login = ({ mode }: { mode: SystemMode }) => {
</Button> </Button>
<div className='flex justify-center items-center flex-wrap gap-2'> <div className='flex justify-center items-center flex-wrap gap-2'>
<Typography>New on our platform?</Typography> <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 Create an account
</Typography> </Typography>
</div> </div>

View File

@ -17,6 +17,7 @@ import type { SystemMode } from '@core/types'
// Hook Imports // Hook Imports
import { useImageVariant } from '@core/hooks/useImageVariant' import { useImageVariant } from '@core/hooks/useImageVariant'
import { useAuth } from '../contexts/authContext'
// Styled Components // Styled Components
const MaskImg = styled('img')({ const MaskImg = styled('img')({
@ -29,6 +30,8 @@ const MaskImg = styled('img')({
}) })
const NotFound = ({ mode }: { mode: SystemMode }) => { const NotFound = ({ mode }: { mode: SystemMode }) => {
const { currentUser } = useAuth()
// Vars // Vars
const darkImg = '/images/pages/misc-mask-dark.png' const darkImg = '/images/pages/misc-mask-dark.png'
const lightImg = '/images/pages/misc-mask-light.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 variant='h4'>Page Not Found </Typography>
<Typography>we couldn&#39;t find the page you are looking for.</Typography> <Typography>we couldn&#39;t find the page you are looking for.</Typography>
</div> </div>
<Button href='/' component={Link} variant='contained'> <Button
href={currentUser?.role === 'admin' ? '/' : '/sa/organizations/list'}
component={Link}
variant='contained'
>
Back To Home Back To Home
</Button> </Button>
<img <img

View File

@ -43,11 +43,11 @@ const BillingAddress = ({ data }: { data: Order }) => {
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
<Typography variant='h5'> <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> </Typography>
</div> </div>
</div> </div>
{data.payments.map((payment, index) => ( {data?.payments?.length ? data.payments.map((payment, index) => (
<div key={index}> <div key={index}>
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<CustomAvatar skin='light' color='secondary' size={40}> <CustomAvatar skin='light' color='secondary' size={40}>
@ -74,7 +74,9 @@ const BillingAddress = ({ data }: { data: Order }) => {
</div> </div>
</div> </div>
</div> </div>
))} )) : (
<Typography variant='body2' className='text-secondary'>No payments found</Typography>
)}
</CardContent> </CardContent>
</Card> </Card>
) )

View File

@ -276,10 +276,10 @@ const OrderDetailsCard = ({ data }: { data: Order }) => {
</Typography> </Typography>
</div> </div>
<div className='flex items-center gap-12'> <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: Total:
</Typography> </Typography>
<Typography color='text.primary' className='font-medium'> <Typography color='text.primary' className='font-semibold'>
{formatCurrency(data.total_amount)} {formatCurrency(data.total_amount)}
</Typography> </Typography>
</div> </div>

View File

@ -39,9 +39,6 @@ const OrderDetails = () => {
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<OrderDetailsCard data={data} /> <OrderDetailsCard data={data} />
</Grid> </Grid>
{/* <Grid size={{ xs: 12 }}>
<ShippingActivity order={data.order_number} />
</Grid> */}
</Grid> </Grid>
</Grid> </Grid>
<Grid size={{ xs: 12, md: 4 }}> <Grid size={{ xs: 12, md: 4 }}>
@ -49,9 +46,6 @@ const OrderDetails = () => {
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<CustomerDetails orderData={data} /> <CustomerDetails orderData={data} />
</Grid> </Grid>
{/* <Grid size={{ xs: 12 }}>
<ShippingAddress />
</Grid> */}
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<BillingAddress data={data} /> <BillingAddress data={data} />
</Grid> </Grid>

View File

@ -12,9 +12,9 @@ import OrderListTable from './OrderListTable'
const OrderList = () => { const OrderList = () => {
return ( return (
<Grid container spacing={6}> <Grid container spacing={6}>
<Grid size={{ xs: 12 }}> {/* <Grid size={{ xs: 12 }}>
<OrderCard /> <OrderCard />
</Grid> </Grid> */}
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<OrderListTable /> <OrderListTable />
</Grid> </Grid>

View File

@ -55,7 +55,7 @@ const ProductAddHeader = () => {
<Button variant='tonal' color='secondary'> <Button variant='tonal' color='secondary'>
Discard Discard
</Button> </Button>
<Button variant='tonal'>Save Draft</Button> {/* <Button variant='tonal'>Save Draft</Button> */}
<Button variant='contained' disabled={isEdit ? isUpdating : isCreating} onClick={handleSubmit}> <Button variant='contained' disabled={isEdit ? isUpdating : isCreating} onClick={handleSubmit}>
{isEdit ? 'Update Product' : 'Publish Product'} {isEdit ? 'Update Product' : 'Publish Product'}
{(isCreating || isUpdating) && <CircularProgress color='inherit' size={16} className='ml-2' />} {(isCreating || isUpdating) && <CircularProgress color='inherit' size={16} className='ml-2' />}

View File

@ -126,13 +126,27 @@ const ProductInformation = () => {
const params = useParams() const params = useParams()
const { data: product, isLoading, error } = useProductById(params?.id as string) 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 const isEdit = !!params?.id
useEffect(() => { useEffect(() => {
if (product) { 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]) }, [product, dispatch])
@ -152,9 +166,11 @@ const ProductInformation = () => {
Underline Underline
], ],
immediatelyRender: false, immediatelyRender: false,
content: ` content: params?.id
? description
: `
<p> <p>
${description || ''} ${description}
</p> </p>
` `
}) })
@ -181,7 +197,7 @@ const ProductInformation = () => {
<CardHeader title='Product Information' /> <CardHeader title='Product Information' />
<CardContent> <CardContent>
<Grid container spacing={6} className='mbe-6'> <Grid container spacing={6} className='mbe-6'>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12, sm: 6 }}>
<CustomTextField <CustomTextField
fullWidth fullWidth
label='Product Name' label='Product Name'
@ -199,15 +215,6 @@ const ProductInformation = () => {
onChange={e => handleInputChange('sku', e.target.value)} onChange={e => handleInputChange('sku', e.target.value)}
/> />
</Grid> </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> </Grid>
<Typography className='mbe-1'>Description (Optional)</Typography> <Typography className='mbe-1'>Description (Optional)</Typography>
<Card className='p-0 border shadow-none'> <Card className='p-0 border shadow-none'>

View File

@ -16,12 +16,22 @@ import { RootState } from '../../../../../redux-store'
import { setProductField } from '../../../../../redux-store/slices/product' import { setProductField } from '../../../../../redux-store/slices/product'
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 { useDebounce } from 'use-debounce'
import { useMemo, useState } from 'react'
import { Autocomplete, CircularProgress } from '@mui/material'
const ProductOrganize = () => { const ProductOrganize = () => {
const dispatch = useDispatch() const dispatch = useDispatch()
const { category_id, printer_type } = useSelector((state: RootState) => state.productReducer.productRequest) 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) => { const handleSelectChange = (field: any, value: any) => {
dispatch(setProductField({ field, value })) dispatch(setProductField({ field, value }))
@ -33,25 +43,36 @@ const ProductOrganize = () => {
<CardContent> <CardContent>
<form onSubmit={e => e.preventDefault()} className='flex flex-col gap-6'> <form onSubmit={e => e.preventDefault()} className='flex flex-col gap-6'>
<div className='flex items-end gap-4'> <div className='flex items-end gap-4'>
<CustomTextField <Autocomplete
select options={categoryOptions}
loading={categoriesLoading}
fullWidth fullWidth
label='Category' getOptionLabel={option => option.name}
value={category_id} value={categoryOptions.find(p => p.id === category_id) || null}
onChange={e => handleSelectChange('category_id', e.target.value)} onInputChange={(event, newCategoryInput) => {
> setCategoryInput(newCategoryInput)
{categoriesApi?.categories.length ? ( }}
categoriesApi?.categories.map((item: Category, index: number) => ( onChange={(event, newValue) => {
<MenuItem key={index} value={item.id}> dispatch(setProductField({ field: 'category_id', value: newValue?.id || '' }))
{item.name} }}
</MenuItem> renderInput={params => (
)) <CustomTextField
) : ( {...params}
<MenuItem disabled value=''> className=''
Loading categories... label='Category'
</MenuItem> fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{categoriesLoading && <CircularProgress size={18} />}
{params.InputProps.endAdornment}
</>
)
}}
/>
)} )}
</CustomTextField> />
<CustomIconButton variant='tonal' color='primary' className='min-is-fit'> <CustomIconButton variant='tonal' color='primary' className='min-is-fit'>
<i className='tabler-plus' /> <i className='tabler-plus' />
</CustomIconButton> </CustomIconButton>
@ -65,12 +86,6 @@ const ProductOrganize = () => {
> >
<MenuItem value={`kitchen`}>Kitchen</MenuItem> <MenuItem value={`kitchen`}>Kitchen</MenuItem>
</CustomTextField> </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> </form>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,5 +1,5 @@
// React Imports // React Imports
import { useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
// MUI Imports // MUI Imports
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
@ -19,12 +19,12 @@ import { Autocomplete } from '@mui/material'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { RootState } from '../../../../../redux-store' import { RootState } from '../../../../../redux-store'
import { resetProductRecipe } from '../../../../../redux-store/slices/productRecipe'
import { useProductRecipesMutation } from '../../../../../services/mutations/productRecipes' import { useProductRecipesMutation } from '../../../../../services/mutations/productRecipes'
import { useIngredients } from '../../../../../services/queries/ingredients' import { useIngredients } from '../../../../../services/queries/ingredients'
import { useOutlets } from '../../../../../services/queries/outlets' import { useOutlets } from '../../../../../services/queries/outlets'
import { Product } from '../../../../../types/services/product' import { Product } from '../../../../../types/services/product'
import { ProductRecipeRequest } from '../../../../../types/services/productRecipe' import { ProductRecipeRequest } from '../../../../../types/services/productRecipe'
import { resetProductVariant } from '../../../../../redux-store/slices/productRecipe'
type Props = { type Props = {
open: boolean open: boolean
@ -47,7 +47,7 @@ const AddRecipeDrawer = (props: Props) => {
// Props // Props
const { open, handleClose, product } = 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 [outletInput, setOutletInput] = useState('')
const [outletDebouncedInput] = useDebounce(outletInput, 500) const [outletDebouncedInput] = useDebounce(outletInput, 500)
@ -67,22 +67,39 @@ const AddRecipeDrawer = (props: Props) => {
const { createProductRecipe, updateProductRecipe } = useProductRecipesMutation() const { createProductRecipe, updateProductRecipe } = useProductRecipesMutation()
useEffect(() => {
if (currentProductRecipe.id) {
setFormData(currentProductRecipe)
}
}, [currentProductRecipe])
const handleSubmit = (e: any) => { const handleSubmit = (e: any) => {
e.preventDefault() e.preventDefault()
createProductRecipe.mutate( if (currentProductRecipe.id) {
{ ...formData, product_id: product.id, variant_id: currentProductRecipe.id || '' }, updateProductRecipe.mutate(
{ { id: currentProductRecipe.id, payload: formData },
onSuccess: () => { {
handleReset() onSuccess: () => {
handleReset()
}
} }
} )
) } else {
createProductRecipe.mutate(
{ ...formData, product_id: product.id, variant_id: currentVariant.id || '' },
{
onSuccess: () => {
handleReset()
}
}
)
}
} }
const handleReset = () => { const handleReset = () => {
handleClose() handleClose()
dispatch(resetProductRecipe()) dispatch(resetProductVariant())
setFormData(initialData) setFormData(initialData)
} }
@ -94,13 +111,15 @@ const AddRecipeDrawer = (props: Props) => {
} }
const setTitleDrawer = (recipe: any) => { const setTitleDrawer = (recipe: any) => {
const addOrEdit = currentProductRecipe.id ? 'Edit ' : 'Add '
let title = 'Original' let title = 'Original'
if (recipe?.name) { if (recipe?.name) {
title = recipe?.name title = recipe?.name
} }
return title return addOrEdit + title
} }
return ( return (
@ -113,7 +132,7 @@ const AddRecipeDrawer = (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'>{setTitleDrawer(currentProductRecipe)} Variant Ingredient</Typography> <Typography variant='h5'>{setTitleDrawer(currentVariant)} Variant Ingredient</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>
@ -192,9 +211,13 @@ const AddRecipeDrawer = (props: Props) => {
type='submit' type='submit'
disabled={createProductRecipe.isPending || updateProductRecipe.isPending} disabled={createProductRecipe.isPending || updateProductRecipe.isPending}
> >
{createProductRecipe.isPending {currentProductRecipe.id
? 'Adding...' ? updateProductRecipe.isPending
: 'Add'} ? 'Updating...'
: 'Update'
: createProductRecipe.isPending
? 'Creating...'
: 'Create'}
</Button> </Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}> <Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Discard Discard

View File

@ -16,33 +16,54 @@ import {
TableContainer, TableContainer,
TableHead, TableHead,
TableRow, TableRow,
Tooltip,
Typography Typography
} from '@mui/material' } from '@mui/material'
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
import { useState } from 'react' import { useState } from 'react'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import Loading from '../../../../../components/layout/shared/Loading' import Loading from '../../../../../components/layout/shared/Loading'
import { setProductRecipe } from '../../../../../redux-store/slices/productRecipe'
import { useProductRecipesByProduct } from '../../../../../services/queries/productRecipes' import { useProductRecipesByProduct } from '../../../../../services/queries/productRecipes'
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 } from '../../../../../utils/transform' import { formatCurrency } from '../../../../../utils/transform'
import AddRecipeDrawer from './AddRecipeDrawer' 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 ProductDetail = () => {
const dispatch = useDispatch() const dispatch = useDispatch()
const params = useParams() const params = useParams()
const [openProductRecipe, setOpenProductRecipe] = useState(false) 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: product, isLoading, error } = useProductById(params?.id as string)
const { data: productRecipe, isLoading: isLoadingProductRecipe } = useProductRecipesByProduct(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) setOpenProductRecipe(true)
dispatch(setProductRecipe(recipe)) dispatch(setProductRecipe(recipe))
} }
const handleDeleteRecipe = () => {
deleteProductRecipe.mutate(productRecipeId, {
onSuccess: () => {
setOpenConfirm(false)
}
})
}
if (isLoading || isLoadingProductRecipe) return <Loading /> if (isLoading || isLoadingProductRecipe) return <Loading />
return ( return (
@ -149,13 +170,14 @@ const ProductDetail = () => {
Total Cost Total Cost
</div> </div>
</TableCell> </TableCell>
<TableCell></TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{productRecipe?.length && {productRecipe?.length ? (
productRecipe productRecipe
.filter((item: any) => item.variant_id === null) .filter((item: ProductRecipe) => item.variant_id === null)
.map((item: any, index: number) => ( .map((item: ProductRecipe, index: number) => (
<TableRow key={index} className='hover:bg-gray-50'> <TableRow key={index} className='hover:bg-gray-50'>
<TableCell> <TableCell>
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
@ -185,14 +207,42 @@ const ProductDetail = () => {
<TableCell className='text-right font-medium'> <TableCell className='text-right font-medium'>
{formatCurrency(item.ingredient.cost * item.quantity)} {formatCurrency(item.ingredient.cost * item.quantity)}
</TableCell> </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>
))} ))
) : (
<TableRow>
<TableCell colSpan={5} className='text-center'>
<Typography variant='body2' color='textSecondary'>
No ingredients found for this variant
</Typography>
</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
{/* Variant Summary */} {/* Variant Summary */}
{productRecipe?.length && ( {productRecipe?.length ? (
<Box className='mt-4 p-4 bg-blue-50 rounded-lg'> <Box className='mt-4 p-4 bg-blue-50 rounded-lg'>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
@ -215,6 +265,13 @@ const ProductDetail = () => {
</Grid> </Grid>
</Grid> </Grid>
</Box> </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 <Button
@ -294,7 +351,7 @@ const ProductDetail = () => {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{productRecipe?.length && {productRecipe?.length ? (
productRecipe productRecipe
.filter((item: any) => item.variant_id === variantData.id) .filter((item: any) => item.variant_id === variantData.id)
.map((item: any, index: number) => ( .map((item: any, index: number) => (
@ -327,14 +384,42 @@ const ProductDetail = () => {
<TableCell className='text-right font-medium'> <TableCell className='text-right font-medium'>
{formatCurrency(item.ingredient.cost * item.quantity)} {formatCurrency(item.ingredient.cost * item.quantity)}
</TableCell> </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>
))} ))
) : (
<TableRow>
<TableCell colSpan={5} className='text-center'>
<Typography variant='body2' color='textSecondary'>
No ingredients found for this variant
</Typography>
</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
{/* Variant Summary */} {/* Variant Summary */}
{productRecipe?.length && ( {productRecipe?.length ? (
<Box className='mt-4 p-4 bg-blue-50 rounded-lg'> <Box className='mt-4 p-4 bg-blue-50 rounded-lg'>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
@ -357,6 +442,13 @@ const ProductDetail = () => {
</Grid> </Grid>
</Grid> </Grid>
</Box> </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 <Button
@ -376,6 +468,15 @@ const ProductDetail = () => {
</div> </div>
<AddRecipeDrawer open={openProductRecipe} handleClose={() => setOpenProductRecipe(false)} product={product!} /> <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.'
/>
</> </>
) )
} }

View File

@ -43,7 +43,7 @@ import { getLocalizedUrl } from '@/utils/i18n'
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 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 { 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'
@ -115,6 +115,11 @@ const ProductListTable = () => {
const [productId, setProductId] = useState('') const [productId, setProductId] = useState('')
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [filter, setFilter] = useState<ProductsQueryParams>({
is_active: '',
category_id: ''
})
// Hooks // Hooks
const { lang: locale } = useParams() const { lang: locale } = useParams()
@ -122,7 +127,8 @@ const ProductListTable = () => {
const { data, isLoading, error, isFetching } = useProducts({ const { data, isLoading, error, isFetching } = useProducts({
page: currentPage, page: currentPage,
limit: pageSize, limit: pageSize,
search search: search,
is_active: filter.is_active
}) })
const { mutate: deleteProduct, isPending: isDeleting } = useProductsMutation().deleteProduct const { mutate: deleteProduct, isPending: isDeleting } = useProductsMutation().deleteProduct
@ -276,7 +282,7 @@ const ProductListTable = () => {
<> <>
<Card> <Card>
<CardHeader title='Filters' /> <CardHeader title='Filters' />
<TableFilters setData={() => {}} productData={[]} /> <TableFilters filter={filter} setFilter={setFilter} />
<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

View File

@ -1,47 +1,45 @@
// React Imports // React Imports
import { useState, useEffect } from 'react' import { useMemo, 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 { ProductsQueryParams } from '../../../../../services/queries/products'
import { Product } from '../../../../../types/services/product' 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 } type ProductStockType = { [key: string]: boolean }
// Vars const TableFilters = ({
const productStockObj: ProductStockType = { filter,
'In Stock': true, setFilter
'Out of Stock': false }: {
} filter: ProductsQueryParams
setFilter: (data: ProductsQueryParams) => void
const TableFilters = ({ setData, productData }: { setData: (data: Product[]) => void; productData?: Product[] }) => { }) => {
// States // States
const [category, setCategory] = useState<Product['category_id']>('')
const [stock, setStock] = useState('') const [stock, setStock] = useState('')
const [status, setStatus] = useState<Product['name']>('')
useEffect( const [categoryInput, setCategoryInput] = useState('')
() => { const [categoryDebouncedInput] = useDebounce(categoryInput, 500)
const filteredData = productData?.filter(product => {
if (category && product.category_id !== category) return false
if (stock && product.name !== stock) return false
if (status && product.name !== status) return false
return true const { data: categoriesApi, isLoading: categoriesLoading } = useCategories({
}) search: categoryDebouncedInput
})
setData(filteredData ?? []) const categoryOptions = useMemo(() => categoriesApi?.categories || [], [categoriesApi])
},
// eslint-disable-next-line react-hooks/exhaustive-deps const handleStatusChange = (e: React.ChangeEvent<HTMLInputElement>) => {
[category, stock, status, productData] setFilter({ ...filter, is_active: e.target.value === 'Active' ? true : e.target.value === 'Inactive' ? false : '' })
) }
return ( return (
<CardContent> <CardContent>
@ -51,37 +49,47 @@ const TableFilters = ({ setData, productData }: { setData: (data: Product[]) =>
select select
fullWidth fullWidth
id='select-status' id='select-status'
value={status} value={filter.is_active ? 'Active' : filter.is_active === false ? 'Inactive' : ''}
onChange={e => setStatus(e.target.value)} onChange={handleStatusChange}
slotProps={{ slotProps={{
select: { displayEmpty: true } select: { displayEmpty: true }
}} }}
> >
<MenuItem value=''>Select Status</MenuItem> <MenuItem value=''>Select Status</MenuItem>
<MenuItem value='Scheduled'>Scheduled</MenuItem> <MenuItem value='Active'>Active</MenuItem>
<MenuItem value='Published'>Publish</MenuItem>
<MenuItem value='Inactive'>Inactive</MenuItem> <MenuItem value='Inactive'>Inactive</MenuItem>
</CustomTextField> </CustomTextField>
</Grid> </Grid>
<Grid size={{ xs: 12, sm: 4 }}> <Grid size={{ xs: 12, sm: 4 }}>
<CustomTextField <Autocomplete
select options={categoryOptions}
loading={categoriesLoading}
fullWidth fullWidth
id='select-category' getOptionLabel={option => option.name}
value={category} value={categoryOptions.find(p => p.id === filter.category_id) || null}
onChange={e => setCategory(e.target.value)} onInputChange={(event, newCategoryInput) => {
slotProps={{ setCategoryInput(newCategoryInput)
select: { displayEmpty: true }
}} }}
> onChange={(event, newValue) => {
<MenuItem value=''>Select Category</MenuItem> setFilter({ ...filter, category_id: newValue?.id || '' })
<MenuItem value='Accessories'>Accessories</MenuItem> }}
<MenuItem value='Home Decor'>Home Decor</MenuItem> renderInput={params => (
<MenuItem value='Electronics'>Electronics</MenuItem> <CustomTextField
<MenuItem value='Shoes'>Shoes</MenuItem> {...params}
<MenuItem value='Office'>Office</MenuItem> fullWidth
<MenuItem value='Games'>Games</MenuItem> placeholder='Search Category'
</CustomTextField> InputProps={{
...params.InputProps,
endAdornment: (
<>
{categoriesLoading && <CircularProgress size={18} />}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
/>
</Grid> </Grid>
<Grid size={{ xs: 12, sm: 4 }}> <Grid size={{ xs: 12, sm: 4 }}>
<CustomTextField <CustomTextField

View File

@ -103,11 +103,13 @@ const StockListTable = () => {
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10) const [pageSize, setPageSize] = useState(10)
const [addInventoryOpen, setAddInventoryOpen] = useState(false) const [addInventoryOpen, setAddInventoryOpen] = useState(false)
const [search, setSearch] = useState('')
// Fetch products with pagination and search // Fetch products with pagination and search
const { data, isLoading, error, isFetching } = useInventories({ const { data, isLoading, error, isFetching } = useInventories({
page: currentPage, page: currentPage,
limit: pageSize limit: pageSize,
search
}) })
const inventories = data?.inventory ?? [] const inventories = data?.inventory ?? []
@ -150,7 +152,7 @@ const StockListTable = () => {
}, },
columnHelper.accessor('product_id', { columnHelper.accessor('product_id', {
header: 'Product', header: 'Product',
cell: ({ row }) => <Typography>{row.original.product_id}</Typography> cell: ({ row }) => <Typography>{row.original.product?.name}</Typography>
}), }),
columnHelper.accessor('quantity', { columnHelper.accessor('quantity', {
header: '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 // eslint-disable-next-line react-hooks/exhaustive-deps
[] []
@ -226,13 +202,13 @@ const StockListTable = () => {
return ( return (
<> <>
<Card> <Card>
<CardHeader title='Filters' /> {/* <CardHeader title='Filters' /> */}
{/* <TableFilters setData={() => {}} productData={[]} /> */} {/* <TableFilters setData={() => {}} productData={[]} /> */}
<Divider /> {/* <Divider /> */}
<div className='flex flex-wrap justify-between gap-4 p-6'> <div className='flex flex-wrap justify-between gap-4 p-6'>
<DebouncedInput <DebouncedInput
value={'search'} value={search}
onChange={value => console.log(value)} onChange={value => setSearch(String(value))}
placeholder='Search Product' placeholder='Search Product'
className='max-sm:is-full' className='max-sm:is-full'
/> />
@ -261,7 +237,7 @@ const StockListTable = () => {
onClick={() => setAddInventoryOpen(!addInventoryOpen)} onClick={() => setAddInventoryOpen(!addInventoryOpen)}
startIcon={<i className='tabler-plus' />} startIcon={<i className='tabler-plus' />}
> >
Adjust Inventory Adjust Stock
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -107,11 +107,13 @@ const StockListTable = () => {
const [openConfirm, setOpenConfirm] = useState(false) const [openConfirm, setOpenConfirm] = useState(false)
const [productId, setProductId] = useState('') const [productId, setProductId] = useState('')
const [addInventoryOpen, setAddInventoryOpen] = useState(false) const [addInventoryOpen, setAddInventoryOpen] = useState(false)
const [search, setSearch] = useState('')
// Fetch products with pagination and search // Fetch products with pagination and search
const { data, isLoading, error, isFetching } = useInventories({ const { data, isLoading, error, isFetching } = useInventories({
page: currentPage, page: currentPage,
limit: pageSize limit: pageSize,
search
}) })
const { mutate: deleteInventory, isPending: isDeleting } = useInventoriesMutation().deleteInventory const { mutate: deleteInventory, isPending: isDeleting } = useInventoriesMutation().deleteInventory
@ -162,7 +164,7 @@ const StockListTable = () => {
}, },
columnHelper.accessor('product_id', { columnHelper.accessor('product_id', {
header: 'Product', header: 'Product',
cell: ({ row }) => <Typography>{row.original.product_id}</Typography> cell: ({ row }) => <Typography>{row.original.product?.name}</Typography>
}), }),
columnHelper.accessor('is_low_stock', { columnHelper.accessor('is_low_stock', {
header: 'Status', header: 'Status',
@ -241,8 +243,8 @@ const StockListTable = () => {
<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'
/> />
@ -271,7 +273,7 @@ const StockListTable = () => {
onClick={() => setAddInventoryOpen(!addInventoryOpen)} onClick={() => setAddInventoryOpen(!addInventoryOpen)}
startIcon={<i className='tabler-plus' />} startIcon={<i className='tabler-plus' />}
> >
Add Inventory Add Stock
</Button> </Button>
</div> </div>
</div> </div>