Merge remote-tracking branch 'origin/main' into efril

This commit is contained in:
efrilm 2025-08-14 00:11:27 +07:00
commit 054ca015fd
29 changed files with 440 additions and 209 deletions

View File

@ -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}

View File

@ -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 }}>

View File

@ -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'

View File

@ -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:{' '}

View File

@ -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>

View File

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

View File

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

View File

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

View File

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

View File

@ -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

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

View File

@ -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 = {}) {

View File

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

View File

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

View File

@ -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>

View File

@ -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&#39;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

View File

@ -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>
)

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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' />}

View File

@ -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'>

View File

@ -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>

View File

@ -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

View File

@ -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.'
/>
</>
)
}

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>