Compare commits
No commits in common. "3aba6c3df59098bc2a1fb2e595b639bf3374b73a" and "10acc8f3872ee5b7139aa05a1215f98fb6d329d0" have entirely different histories.
3aba6c3df5
...
10acc8f387
@ -1,584 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
// React Imports
|
|
||||||
import type { ChangeEvent } from 'react'
|
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
|
|
||||||
// MUI Imports
|
|
||||||
import Card from '@mui/material/Card'
|
|
||||||
import CardContent from '@mui/material/CardContent'
|
|
||||||
import Typography from '@mui/material/Typography'
|
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
import Grid from '@mui/material/Grid2'
|
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
|
||||||
import Avatar from '@mui/material/Avatar'
|
|
||||||
import Divider from '@mui/material/Divider'
|
|
||||||
import Chip from '@mui/material/Chip'
|
|
||||||
import Alert from '@mui/material/Alert'
|
|
||||||
import CircularProgress from '@mui/material/CircularProgress'
|
|
||||||
|
|
||||||
// Third-party Imports
|
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
// Type Imports
|
|
||||||
import type { CustomInputHorizontalData } from '@core/components/custom-inputs/types'
|
|
||||||
|
|
||||||
// Component Imports
|
|
||||||
import CustomInputHorizontal from '@core/components/custom-inputs/Horizontal'
|
|
||||||
import DirectionalIcon from '@components/DirectionalIcon'
|
|
||||||
import { useSettings } from '@core/hooks/useSettings'
|
|
||||||
import CustomTextField from '@core/components/mui/TextField'
|
|
||||||
|
|
||||||
// Styles Imports
|
|
||||||
import frontCommonStyles from '@views/front-pages/styles.module.css'
|
|
||||||
import { useOrganizationsMutation } from '../../../../../services/mutations/organization'
|
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
|
|
||||||
// Types
|
|
||||||
export interface OrganizationRequest {
|
|
||||||
organization_name: string
|
|
||||||
organization_email?: string | null
|
|
||||||
organization_phone_number?: string | null
|
|
||||||
plan_type: 'basic' | 'premium' | 'enterprise'
|
|
||||||
admin_name: string
|
|
||||||
admin_email: string
|
|
||||||
admin_password: string
|
|
||||||
outlet_name: string
|
|
||||||
outlet_address?: string | null
|
|
||||||
outlet_timezone?: string | null
|
|
||||||
outlet_currency: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data
|
|
||||||
const planData: CustomInputHorizontalData[] = [
|
|
||||||
{
|
|
||||||
title: (
|
|
||||||
<div className='flex items-center gap-4'>
|
|
||||||
<Avatar
|
|
||||||
variant='rounded'
|
|
||||||
className='is-[58px] bs-[34px]'
|
|
||||||
sx={theme => ({
|
|
||||||
backgroundColor: 'var(--mui-palette-primary-light)',
|
|
||||||
color: 'var(--mui-palette-primary-main)'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<i className='tabler-rocket text-2xl' />
|
|
||||||
</Avatar>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<Typography color='text.primary' className='font-medium'>
|
|
||||||
Basic Plan
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='body2' color='text.secondary'>
|
|
||||||
Perfect for small businesses
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
value: 'basic',
|
|
||||||
isSelected: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: (
|
|
||||||
<div className='flex items-center gap-4'>
|
|
||||||
<Avatar
|
|
||||||
variant='rounded'
|
|
||||||
className='is-[58px] bs-[34px]'
|
|
||||||
sx={theme => ({
|
|
||||||
backgroundColor: 'var(--mui-palette-success-light)',
|
|
||||||
color: 'var(--mui-palette-success-main)'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<i className='tabler-crown text-2xl' />
|
|
||||||
</Avatar>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<Typography color='text.primary' className='font-medium'>
|
|
||||||
Premium Plan
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='body2' color='text.secondary'>
|
|
||||||
Most popular choice
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
value: 'premium'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: (
|
|
||||||
<div className='flex items-center gap-4'>
|
|
||||||
<Avatar
|
|
||||||
variant='rounded'
|
|
||||||
className='is-[58px] bs-[34px]'
|
|
||||||
sx={theme => ({
|
|
||||||
backgroundColor: 'var(--mui-palette-warning-light)',
|
|
||||||
color: 'var(--mui-palette-warning-main)'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<i className='tabler-building text-2xl' />
|
|
||||||
</Avatar>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<Typography color='text.primary' className='font-medium'>
|
|
||||||
Enterprise Plan
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='body2' color='text.secondary'>
|
|
||||||
Advanced features for large teams
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
value: 'enterprise'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const currencies = [
|
|
||||||
{ code: 'USD', name: 'US Dollar' },
|
|
||||||
{ code: 'EUR', name: 'Euro' },
|
|
||||||
{ code: 'GBP', name: 'British Pound' },
|
|
||||||
{ code: 'JPY', name: 'Japanese Yen' },
|
|
||||||
{ code: 'AUD', name: 'Australian Dollar' },
|
|
||||||
{ code: 'CAD', name: 'Canadian Dollar' },
|
|
||||||
{ code: 'CHF', name: 'Swiss Franc' },
|
|
||||||
{ code: 'CNY', name: 'Chinese Yuan' },
|
|
||||||
{ code: 'IDR', name: 'Indonesian Rupiah' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const timezones = [
|
|
||||||
'UTC',
|
|
||||||
'America/New_York',
|
|
||||||
'America/Los_Angeles',
|
|
||||||
'Europe/London',
|
|
||||||
'Europe/Paris',
|
|
||||||
'Asia/Tokyo',
|
|
||||||
'Asia/Shanghai',
|
|
||||||
'Asia/Jakarta',
|
|
||||||
'Australia/Sydney'
|
|
||||||
]
|
|
||||||
|
|
||||||
const planPricing = {
|
|
||||||
basic: { price: 29.99, features: ['Up to 5 users', 'Basic reporting', '24/7 support', 'Mobile app'] },
|
|
||||||
premium: {
|
|
||||||
price: 59.99,
|
|
||||||
features: ['Up to 25 users', 'Advanced reporting', 'Priority support', 'API access', 'Custom integrations']
|
|
||||||
},
|
|
||||||
enterprise: {
|
|
||||||
price: 129.99,
|
|
||||||
features: [
|
|
||||||
'Unlimited users',
|
|
||||||
'Enterprise reporting',
|
|
||||||
'Dedicated support',
|
|
||||||
'White-label solution',
|
|
||||||
'Custom development'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateOrganization = () => {
|
|
||||||
const initialSelected: string = planData.filter(item => item.isSelected)[
|
|
||||||
planData.filter(item => item.isSelected).length - 1
|
|
||||||
].value
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
// States
|
|
||||||
const [formData, setFormData] = useState<OrganizationRequest>({
|
|
||||||
organization_name: '',
|
|
||||||
organization_email: null,
|
|
||||||
organization_phone_number: null,
|
|
||||||
plan_type: initialSelected as 'basic' | 'premium' | 'enterprise',
|
|
||||||
admin_name: '',
|
|
||||||
admin_email: '',
|
|
||||||
admin_password: '',
|
|
||||||
outlet_name: '',
|
|
||||||
outlet_address: null,
|
|
||||||
outlet_timezone: null,
|
|
||||||
outlet_currency: 'USD'
|
|
||||||
})
|
|
||||||
|
|
||||||
const [errors, setErrors] = useState<Partial<Record<keyof OrganizationRequest, string>>>({})
|
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
// Hooks
|
|
||||||
const { updatePageSettings } = useSettings()
|
|
||||||
const { createOrganization } = useOrganizationsMutation()
|
|
||||||
|
|
||||||
const handleInputChange = (field: keyof OrganizationRequest) => (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = event.target.value
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[field]: value || null
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Clear error when user starts typing
|
|
||||||
if (errors[field]) {
|
|
||||||
setErrors(prev => ({
|
|
||||||
...prev,
|
|
||||||
[field]: undefined
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePlanChange = (prop: string | ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const planType = typeof prop === 'string' ? prop : prop.target.value
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
plan_type: planType as 'basic' | 'premium' | 'enterprise'
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
|
||||||
const newErrors: Partial<Record<keyof OrganizationRequest, string>> = {}
|
|
||||||
|
|
||||||
// Required fields validation
|
|
||||||
if (!formData.organization_name.trim()) {
|
|
||||||
newErrors.organization_name = 'Organization name is required'
|
|
||||||
} else if (formData.organization_name.length > 255) {
|
|
||||||
newErrors.organization_name = 'Organization name must be 255 characters or less'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.admin_name.trim()) {
|
|
||||||
newErrors.admin_name = 'Admin name is required'
|
|
||||||
} else if (formData.admin_name.length > 255) {
|
|
||||||
newErrors.admin_name = 'Admin name must be 255 characters or less'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.admin_email.trim()) {
|
|
||||||
newErrors.admin_email = 'Admin email is required'
|
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.admin_email)) {
|
|
||||||
newErrors.admin_email = 'Please enter a valid email address'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.admin_password) {
|
|
||||||
newErrors.admin_password = 'Password is required'
|
|
||||||
} else if (formData.admin_password.length < 6) {
|
|
||||||
newErrors.admin_password = 'Password must be at least 6 characters long'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.outlet_name.trim()) {
|
|
||||||
newErrors.outlet_name = 'Outlet name is required'
|
|
||||||
} else if (formData.outlet_name.length > 255) {
|
|
||||||
newErrors.outlet_name = 'Outlet name must be 255 characters or less'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.outlet_currency) {
|
|
||||||
newErrors.outlet_currency = 'Currency is required'
|
|
||||||
} else if (formData.outlet_currency.length !== 3) {
|
|
||||||
newErrors.outlet_currency = 'Currency must be a 3-character ISO code'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional email validation
|
|
||||||
if (formData.organization_email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.organization_email)) {
|
|
||||||
newErrors.organization_email = 'Please enter a valid email address'
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors(newErrors)
|
|
||||||
return Object.keys(newErrors).length === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!validateForm()) return
|
|
||||||
|
|
||||||
setSubmitError(null)
|
|
||||||
|
|
||||||
createOrganization.mutate(formData, {
|
|
||||||
onSuccess: () => {
|
|
||||||
router.push('/login')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// For Page specific settings
|
|
||||||
useEffect(() => {
|
|
||||||
return updatePageSettings({
|
|
||||||
skin: 'default'
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const selectedPlan = planPricing[formData.plan_type]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className={classnames('md:plb-[100px] plb-6', frontCommonStyles.layoutSpacing)}>
|
|
||||||
<Card>
|
|
||||||
<Grid container>
|
|
||||||
<Grid size={{ md: 12, lg: 8 }}>
|
|
||||||
<CardContent className='flex flex-col max-sm:gap-y-5 gap-y-8 sm:p-8 border-be lg:border-be-0 lg:border-e bs-full'>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<Typography variant='h4' className='flex items-center gap-2'>
|
|
||||||
<i className='tabler-building text-primary' />
|
|
||||||
Create Organization
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
Set up your organization with admin account and primary outlet. Choose the plan that best fits your
|
|
||||||
needs.
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{submitError && (
|
|
||||||
<Alert severity='error' className='mb-4'>
|
|
||||||
{submitError}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Plan Selection */}
|
|
||||||
<div>
|
|
||||||
<Typography variant='h5' className='mbe-4 flex items-center gap-2'>
|
|
||||||
<i className='tabler-crown text-warning' />
|
|
||||||
Choose Your Plan
|
|
||||||
</Typography>
|
|
||||||
<Grid container spacing={4}>
|
|
||||||
{planData.map((item, index) => (
|
|
||||||
<CustomInputHorizontal
|
|
||||||
key={index}
|
|
||||||
type='radio'
|
|
||||||
name='plan-type'
|
|
||||||
data={item}
|
|
||||||
selected={formData.plan_type}
|
|
||||||
handleChange={handlePlanChange}
|
|
||||||
gridProps={{ size: { xs: 12, md: 4 } }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Organization Details */}
|
|
||||||
<div>
|
|
||||||
<Typography variant='h5' className='mbe-6 flex items-center gap-2'>
|
|
||||||
<i className='tabler-building text-info' />
|
|
||||||
Organization Details
|
|
||||||
</Typography>
|
|
||||||
<Grid container spacing={5}>
|
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
required
|
|
||||||
label='Organization Name'
|
|
||||||
placeholder='My Company Ltd.'
|
|
||||||
value={formData.organization_name || ''}
|
|
||||||
onChange={handleInputChange('organization_name')}
|
|
||||||
error={!!errors.organization_name}
|
|
||||||
helperText={errors.organization_name}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
label='Organization Email'
|
|
||||||
placeholder='contact@mycompany.com'
|
|
||||||
type='email'
|
|
||||||
value={formData.organization_email || ''}
|
|
||||||
onChange={handleInputChange('organization_email')}
|
|
||||||
error={!!errors.organization_email}
|
|
||||||
helperText={errors.organization_email}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
label='Phone Number'
|
|
||||||
placeholder='+1 (555) 123-4567'
|
|
||||||
value={formData.organization_phone_number || ''}
|
|
||||||
onChange={handleInputChange('organization_phone_number')}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Admin Account */}
|
|
||||||
<div>
|
|
||||||
<Typography variant='h5' className='mbe-6 flex items-center gap-2'>
|
|
||||||
<i className='tabler-user-shield text-success' />
|
|
||||||
Admin Account
|
|
||||||
</Typography>
|
|
||||||
<Grid container spacing={5}>
|
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
required
|
|
||||||
label='Admin Name'
|
|
||||||
placeholder='John Doe'
|
|
||||||
value={formData.admin_name || ''}
|
|
||||||
onChange={handleInputChange('admin_name')}
|
|
||||||
error={!!errors.admin_name}
|
|
||||||
helperText={errors.admin_name}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
required
|
|
||||||
label='Admin Email'
|
|
||||||
placeholder='admin@mycompany.com'
|
|
||||||
type='email'
|
|
||||||
value={formData.admin_email || ''}
|
|
||||||
onChange={handleInputChange('admin_email')}
|
|
||||||
error={!!errors.admin_email}
|
|
||||||
helperText={errors.admin_email}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
required
|
|
||||||
type='password'
|
|
||||||
label='Admin Password'
|
|
||||||
placeholder='Minimum 6 characters'
|
|
||||||
value={formData.admin_password || ''}
|
|
||||||
onChange={handleInputChange('admin_password')}
|
|
||||||
error={!!errors.admin_password}
|
|
||||||
helperText={errors.admin_password}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Outlet Information */}
|
|
||||||
<div>
|
|
||||||
<Typography variant='h5' className='mbe-6 flex items-center gap-2'>
|
|
||||||
<i className='tabler-store text-warning' />
|
|
||||||
Primary Outlet
|
|
||||||
</Typography>
|
|
||||||
<Grid container spacing={5}>
|
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
required
|
|
||||||
label='Outlet Name'
|
|
||||||
placeholder='Main Store'
|
|
||||||
value={formData.outlet_name || ''}
|
|
||||||
onChange={handleInputChange('outlet_name')}
|
|
||||||
error={!!errors.outlet_name}
|
|
||||||
helperText={errors.outlet_name}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
|
||||||
<CustomTextField
|
|
||||||
select
|
|
||||||
fullWidth
|
|
||||||
required
|
|
||||||
label='Currency'
|
|
||||||
value={formData.outlet_currency}
|
|
||||||
onChange={handleInputChange('outlet_currency')}
|
|
||||||
error={!!errors.outlet_currency}
|
|
||||||
helperText={errors.outlet_currency}
|
|
||||||
>
|
|
||||||
{currencies.map(currency => (
|
|
||||||
<MenuItem key={currency.code} value={currency.code}>
|
|
||||||
{currency.code} - {currency.name}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</CustomTextField>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 12 }}>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
label='Outlet Address'
|
|
||||||
placeholder='123 Main Street, City, State, Country'
|
|
||||||
value={formData.outlet_address || ''}
|
|
||||||
onChange={handleInputChange('outlet_address')}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
|
||||||
<CustomTextField
|
|
||||||
select
|
|
||||||
fullWidth
|
|
||||||
label='Timezone'
|
|
||||||
value={formData.outlet_timezone || ''}
|
|
||||||
onChange={handleInputChange('outlet_timezone')}
|
|
||||||
>
|
|
||||||
{timezones.map(tz => (
|
|
||||||
<MenuItem key={tz} value={tz}>
|
|
||||||
{tz}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</CustomTextField>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid size={{ md: 12, lg: 4 }}>
|
|
||||||
<CardContent className='flex flex-col gap-8 sm:p-8'>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<Typography variant='h4' className='flex items-center gap-2'>
|
|
||||||
<i className='tabler-receipt text-primary' />
|
|
||||||
Plan Summary
|
|
||||||
</Typography>
|
|
||||||
<Typography>Review your selected plan and get started with your organization.</Typography>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-col gap-5'>
|
|
||||||
<div className='flex flex-col gap-4 p-6 bg-actionHover rounded'>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<Typography className='font-medium capitalize'>{formData.plan_type} Plan</Typography>
|
|
||||||
<Chip
|
|
||||||
label={formData.plan_type === 'premium' ? 'Most Popular' : 'Selected'}
|
|
||||||
color={formData.plan_type === 'premium' ? 'success' : 'primary'}
|
|
||||||
size='small'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-baseline'>
|
|
||||||
<Typography variant='h1'>${selectedPlan.price}</Typography>
|
|
||||||
<Typography component='sub'>/month</Typography>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
{selectedPlan.features.map((feature, index) => (
|
|
||||||
<div key={index} className='flex items-center gap-2'>
|
|
||||||
<i className='tabler-check text-success text-sm' />
|
|
||||||
<Typography variant='body2'>{feature}</Typography>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className='flex gap-2 items-center justify-between mbe-2'>
|
|
||||||
<Typography>Plan Cost</Typography>
|
|
||||||
<Typography color='text.primary' className='font-medium'>
|
|
||||||
${selectedPlan.price}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div className='flex gap-2 items-center justify-between'>
|
|
||||||
<Typography>Setup Fee</Typography>
|
|
||||||
<Typography color='text.primary' className='font-medium'>
|
|
||||||
Free
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<Divider className='mlb-4' />
|
|
||||||
<div className='flex gap-2 items-center justify-between'>
|
|
||||||
<Typography className='font-medium'>Total</Typography>
|
|
||||||
<Typography color='text.primary' className='font-medium'>
|
|
||||||
${selectedPlan.price}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='contained'
|
|
||||||
size='large'
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={createOrganization.isPending}
|
|
||||||
endIcon={
|
|
||||||
createOrganization.isPending ? (
|
|
||||||
<CircularProgress size={20} color='inherit' />
|
|
||||||
) : (
|
|
||||||
<DirectionalIcon ltrIconClass='tabler-arrow-right' rtlIconClass='tabler-arrow-left' />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{createOrganization.isPending ? 'Creating Organization...' : 'Create Organization'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Typography variant='body2' className='text-center'>
|
|
||||||
By creating an organization, you agree to our Terms of Service and Privacy Policy. You can change your
|
|
||||||
plan anytime.
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreateOrganization
|
|
||||||
@ -3,11 +3,12 @@ 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 ProductImage from '@views/apps/ecommerce/products/add/ProductImage'
|
|
||||||
import ProductInformation from '@views/apps/ecommerce/products/add/ProductInformation'
|
import ProductInformation from '@views/apps/ecommerce/products/add/ProductInformation'
|
||||||
import ProductOrganize from '@views/apps/ecommerce/products/add/ProductOrganize'
|
import ProductImage from '@views/apps/ecommerce/products/add/ProductImage'
|
||||||
import ProductPricing from '@views/apps/ecommerce/products/add/ProductPricing'
|
|
||||||
import ProductVariants from '@views/apps/ecommerce/products/add/ProductVariants'
|
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 ProductOrganize from '@views/apps/ecommerce/products/add/ProductOrganize'
|
||||||
|
|
||||||
const eCommerceProductsEdit = () => {
|
const eCommerceProductsEdit = () => {
|
||||||
return (
|
return (
|
||||||
@ -26,6 +27,9 @@ 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 }}>
|
||||||
@ -23,7 +23,6 @@ const DashboardOrder = () => {
|
|||||||
<DistributedBarChartOrder
|
<DistributedBarChartOrder
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
title='Total Items'
|
title='Total Items'
|
||||||
isCurrency={false}
|
|
||||||
value={data?.summary.total_items as number}
|
value={data?.summary.total_items as number}
|
||||||
avatarIcon={'tabler-package'}
|
avatarIcon={'tabler-package'}
|
||||||
avatarColor='primary'
|
avatarColor='primary'
|
||||||
@ -34,7 +33,6 @@ const DashboardOrder = () => {
|
|||||||
<DistributedBarChartOrder
|
<DistributedBarChartOrder
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
title='Total Orders'
|
title='Total Orders'
|
||||||
isCurrency={false}
|
|
||||||
value={data?.summary.total_orders as number}
|
value={data?.summary.total_orders as number}
|
||||||
avatarIcon={'tabler-shopping-cart'}
|
avatarIcon={'tabler-shopping-cart'}
|
||||||
avatarColor='info'
|
avatarColor='info'
|
||||||
@ -45,7 +43,6 @@ const DashboardOrder = () => {
|
|||||||
<DistributedBarChartOrder
|
<DistributedBarChartOrder
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
title='Average Orders'
|
title='Average Orders'
|
||||||
isCurrency={true}
|
|
||||||
value={data?.summary.average_order_value as number}
|
value={data?.summary.average_order_value as number}
|
||||||
avatarIcon={'tabler-trending-up'}
|
avatarIcon={'tabler-trending-up'}
|
||||||
avatarColor='warning'
|
avatarColor='warning'
|
||||||
@ -56,7 +53,6 @@ const DashboardOrder = () => {
|
|||||||
<DistributedBarChartOrder
|
<DistributedBarChartOrder
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
title='Total Sales'
|
title='Total Sales'
|
||||||
isCurrency={true}
|
|
||||||
value={data?.summary.total_sales as number}
|
value={data?.summary.total_sales as number}
|
||||||
avatarIcon={'tabler-currency-dollar'}
|
avatarIcon={'tabler-currency-dollar'}
|
||||||
avatarColor='success'
|
avatarColor='success'
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useDashboardAnalytics } from '../../../../../../services/queries/analytics'
|
import { useDashboardAnalytics } from '../../../../../../services/queries/analytics'
|
||||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||||
import { formatCurrency, formatDate, formatShortCurrency } from '../../../../../../utils/transform'
|
import { formatCurrency, formatDate } from '../../../../../../utils/transform'
|
||||||
import ProductSales from '../../../../../../views/dashboards/products/ProductSales'
|
import ProductSales from '../../../../../../views/dashboards/products/ProductSales'
|
||||||
import PaymentMethodReport from '../../../../../../views/dashboards/payment-methods/PaymentMethodReport'
|
import PaymentMethodReport from '../../../../../../views/dashboards/payment-methods/PaymentMethodReport'
|
||||||
import OrdersReport from '../../../../../../views/dashboards/orders/OrdersReport'
|
import OrdersReport from '../../../../../../views/dashboards/orders/OrdersReport'
|
||||||
@ -14,15 +14,15 @@ const DashboardOverview = () => {
|
|||||||
|
|
||||||
if (isLoading) return <Loading />
|
if (isLoading) return <Loading />
|
||||||
|
|
||||||
const MetricCard = ({ iconClass, title, value, subtitle, bgColor = 'bg-blue-500', isCurrency = false }: any) => (
|
const MetricCard = ({ iconClass, title, value, subtitle, bgColor = 'bg-blue-500' }: 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'>{isCurrency ? 'Rp ' + value : value}</p>
|
<p className='text-2xl font-bold text-gray-900 mb-1'>{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={`p-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>
|
||||||
@ -52,6 +52,12 @@ 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={formatCurrency(salesData.overview.total_sales)}
|
||||||
|
bgColor='bg-green-500'
|
||||||
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
iconClass='tabler-shopping-cart'
|
iconClass='tabler-shopping-cart'
|
||||||
title='Total Orders'
|
title='Total Orders'
|
||||||
@ -59,19 +65,11 @@ 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={formatCurrency(salesData.overview.average_order_value)}
|
||||||
bgColor='bg-purple-500'
|
bgColor='bg-purple-500'
|
||||||
isCurrency={true}
|
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
iconClass='tabler-users'
|
iconClass='tabler-users'
|
||||||
@ -81,6 +79,24 @@ const DashboardOverview = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Additional Metrics */}
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-6 mb-8'>
|
||||||
|
<div className='bg-white rounded-lg shadow-md p-6'>
|
||||||
|
<div className='flex items-center mb-4'>
|
||||||
|
<i className='tabler-x text-[24px] text-red-500 mr-2'></i>
|
||||||
|
<h3 className='text-lg font-semibold text-gray-900'>Voided Orders</h3>
|
||||||
|
</div>
|
||||||
|
<p className='text-3xl font-bold text-red-600'>{salesData.overview.voided_orders}</p>
|
||||||
|
</div>
|
||||||
|
<div className='bg-white rounded-lg shadow-md p-6'>
|
||||||
|
<div className='flex items-center mb-4'>
|
||||||
|
<i className='tabler-refresh text-[24px] text-yellow-500 mr-2'></i>
|
||||||
|
<h3 className='text-lg font-semibold text-gray-900'>Refunded Orders</h3>
|
||||||
|
</div>
|
||||||
|
<p className='text-3xl font-bold text-yellow-600'>{salesData.overview.refunded_orders}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8'>
|
<div className='grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8'>
|
||||||
{/* Top Products */}
|
{/* Top Products */}
|
||||||
<ProductSales title='Top Products' productData={salesData.top_products} />
|
<ProductSales title='Top Products' productData={salesData.top_products} />
|
||||||
|
|||||||
@ -23,7 +23,6 @@ const DashboardPayment = () => {
|
|||||||
<DistributedBarChartOrder
|
<DistributedBarChartOrder
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
title='Total Orders'
|
title='Total Orders'
|
||||||
isCurrency={false}
|
|
||||||
value={data?.summary.total_orders as number}
|
value={data?.summary.total_orders as number}
|
||||||
avatarIcon={'tabler-shopping-cart'}
|
avatarIcon={'tabler-shopping-cart'}
|
||||||
avatarColor='primary'
|
avatarColor='primary'
|
||||||
@ -34,7 +33,6 @@ const DashboardPayment = () => {
|
|||||||
<DistributedBarChartOrder
|
<DistributedBarChartOrder
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
title='Total Payment'
|
title='Total Payment'
|
||||||
isCurrency={false}
|
|
||||||
value={data?.summary.total_payments as number}
|
value={data?.summary.total_payments as number}
|
||||||
avatarIcon={'tabler-package'}
|
avatarIcon={'tabler-package'}
|
||||||
avatarColor='info'
|
avatarColor='info'
|
||||||
@ -45,7 +43,6 @@ const DashboardPayment = () => {
|
|||||||
<DistributedBarChartOrder
|
<DistributedBarChartOrder
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
title='Average Orders'
|
title='Average Orders'
|
||||||
isCurrency={true}
|
|
||||||
value={data?.summary.average_order_value as number}
|
value={data?.summary.average_order_value as number}
|
||||||
avatarIcon={'tabler-trending-up'}
|
avatarIcon={'tabler-trending-up'}
|
||||||
avatarColor='warning'
|
avatarColor='warning'
|
||||||
@ -56,7 +53,6 @@ const DashboardPayment = () => {
|
|||||||
<DistributedBarChartOrder
|
<DistributedBarChartOrder
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
title='Total Amount'
|
title='Total Amount'
|
||||||
isCurrency={true}
|
|
||||||
value={data?.summary.total_amount as number}
|
value={data?.summary.total_amount as number}
|
||||||
avatarIcon={'tabler-currency-dollar'}
|
avatarIcon={'tabler-currency-dollar'}
|
||||||
avatarColor='success'
|
avatarColor='success'
|
||||||
|
|||||||
@ -1,51 +1,65 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
// MUI Imports
|
||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
|
||||||
|
// Component Imports
|
||||||
|
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
||||||
|
|
||||||
|
// Server Action Imports
|
||||||
|
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||||
import { useProfitLossAnalytics } from '../../../../../../services/queries/analytics'
|
import { useProfitLossAnalytics } from '../../../../../../services/queries/analytics'
|
||||||
import { formatShortCurrency } from '../../../../../../utils/transform'
|
import { DailyData, ProductDataReport, ProfitLossReport } from '../../../../../../types/services/analytic'
|
||||||
|
import EarningReportsWithTabs from '../../../../../../views/dashboards/crm/EarningReportsWithTabs'
|
||||||
import MultipleSeries from '../../../../../../views/dashboards/profit-loss/EarningReportWithTabs'
|
import MultipleSeries from '../../../../../../views/dashboards/profit-loss/EarningReportWithTabs'
|
||||||
import { DailyData, ProfitLossReport } from '../../../../../../types/services/analytic'
|
|
||||||
|
|
||||||
const DashboardProfitloss = () => {
|
function formatMetricName(metric: string): string {
|
||||||
// Sample data - replace with your actual data
|
|
||||||
const { data: profitData, isLoading } = useProfitLossAnalytics()
|
|
||||||
|
|
||||||
const formatCurrency = (amount: any) => {
|
|
||||||
return new Intl.NumberFormat('id-ID', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'IDR',
|
|
||||||
minimumFractionDigits: 0
|
|
||||||
}).format(amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatPercentage = (value: any) => {
|
|
||||||
return `${value.toFixed(2)}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (dateString: any) => {
|
|
||||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getProfitabilityColor = (margin: any) => {
|
|
||||||
if (margin > 50) return 'text-green-600 bg-green-100'
|
|
||||||
if (margin > 0) return 'text-yellow-600 bg-yellow-100'
|
|
||||||
return 'text-red-600 bg-red-100'
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMetricName(metric: string): string {
|
|
||||||
const nameMap: { [key: string]: string } = {
|
const nameMap: { [key: string]: string } = {
|
||||||
revenue: 'Revenue',
|
revenue: 'Revenue',
|
||||||
net_profit: 'Net Profit'
|
cost: 'Cost',
|
||||||
|
gross_profit: 'Gross Profit',
|
||||||
|
gross_profit_margin: 'Gross Profit Margin (%)',
|
||||||
|
tax: 'Tax',
|
||||||
|
discount: 'Discount',
|
||||||
|
net_profit: 'Net Profit',
|
||||||
|
net_profit_margin: 'Net Profit Margin (%)',
|
||||||
|
orders: 'Orders'
|
||||||
}
|
}
|
||||||
|
|
||||||
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardProfitLoss = () => {
|
||||||
|
const { data, isLoading } = useProfitLossAnalytics()
|
||||||
|
|
||||||
|
const formatDate = (dateString: any) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const metrics = ['revenue', 'net_profit']
|
const metrics = ['cost', 'revenue', 'gross_profit', 'net_profit']
|
||||||
|
|
||||||
|
const transformSalesData = (data: ProfitLossReport) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'products',
|
||||||
|
avatarIcon: 'tabler-package',
|
||||||
|
date: data.product_data.map((d: ProductDataReport) => d.product_name),
|
||||||
|
series: [{ data: data.product_data.map((d: ProductDataReport) => d.revenue) }]
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// type: 'profits',
|
||||||
|
// avatarIcon: 'tabler-currency-dollar',
|
||||||
|
// date: data.data.map((d: DailyData) => formatDate(d.date)),
|
||||||
|
// series: metrics.map(metric => ({
|
||||||
|
// name: formatMetricName(metric as string),
|
||||||
|
// data: data.data.map((item: any) => item[metric] as number)
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const transformMultipleData = (data: ProfitLossReport) => {
|
const transformMultipleData = (data: ProfitLossReport) => {
|
||||||
return [
|
return [
|
||||||
@ -61,298 +75,58 @@ const DashboardProfitloss = () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const MetricCard = ({
|
if (isLoading) return <Loading />
|
||||||
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'}`}>
|
|
||||||
{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`}>
|
|
||||||
<i className={`${iconClass} text-[32px] ${bgColor.replace('bg-', 'text-')}`}></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Grid container spacing={6}>
|
||||||
{profitData && (
|
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||||
<div>
|
<DistributedBarChartOrder
|
||||||
{/* Header */}
|
isLoading={isLoading}
|
||||||
<div className='mb-8'>
|
|
||||||
<h1 className='text-3xl font-bold text-gray-900 mb-2'>Profit Analysis Dashboard</h1>
|
|
||||||
<p className='text-gray-600'>
|
|
||||||
{formatDate(profitData.date_from)} - {formatDate(profitData.date_to)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Summary Metrics */}
|
|
||||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8'>
|
|
||||||
<MetricCard
|
|
||||||
iconClass='tabler-currency-dollar'
|
|
||||||
title='Total Revenue'
|
|
||||||
value={formatShortCurrency(profitData.summary.total_revenue)}
|
|
||||||
bgColor='bg-green-500'
|
|
||||||
isCurrency={true}
|
|
||||||
/>
|
|
||||||
<MetricCard
|
|
||||||
iconClass='tabler-receipt'
|
|
||||||
title='Total Cost'
|
title='Total Cost'
|
||||||
value={formatShortCurrency(profitData.summary.total_cost)}
|
value={data?.summary.total_cost as number}
|
||||||
bgColor='bg-red-500'
|
avatarIcon={'tabler-currency-dollar'}
|
||||||
isCurrency={true}
|
avatarColor='primary'
|
||||||
|
avatarSkin='light'
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
</Grid>
|
||||||
iconClass='tabler-trending-up'
|
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||||
|
<DistributedBarChartOrder
|
||||||
|
isLoading={isLoading}
|
||||||
|
title='Total Rvenue'
|
||||||
|
value={data?.summary.total_revenue as number}
|
||||||
|
avatarIcon={'tabler-currency-dollar'}
|
||||||
|
avatarColor='info'
|
||||||
|
avatarSkin='light'
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||||
|
<DistributedBarChartOrder
|
||||||
|
isLoading={isLoading}
|
||||||
title='Gross Profit'
|
title='Gross Profit'
|
||||||
value={formatShortCurrency(profitData.summary.gross_profit)}
|
value={data?.summary.gross_profit as number}
|
||||||
subtitle={`Margin: ${formatPercentage(profitData.summary.gross_profit_margin)}`}
|
avatarIcon={'tabler-trending-up'}
|
||||||
bgColor='bg-blue-500'
|
avatarColor='warning'
|
||||||
isNegative={profitData.summary.gross_profit < 0}
|
avatarSkin='light'
|
||||||
isCurrency={true}
|
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
</Grid>
|
||||||
iconClass='tabler-percentage'
|
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||||
title='Profitability Ratio'
|
<DistributedBarChartOrder
|
||||||
value={formatPercentage(profitData.summary.profitability_ratio)}
|
isLoading={isLoading}
|
||||||
subtitle={`Avg Profit: ${formatShortCurrency(profitData.summary.average_profit)}`}
|
title='Net Profit'
|
||||||
bgColor='bg-purple-500'
|
value={data?.summary.net_profit as number}
|
||||||
|
avatarIcon={'tabler-currency-dollar'}
|
||||||
|
avatarColor='success'
|
||||||
|
avatarSkin='light'
|
||||||
/>
|
/>
|
||||||
</div>
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, lg: 12 }}>
|
||||||
{/* Additional Summary Metrics */}
|
<EarningReportsWithTabs data={transformSalesData(data!)} />
|
||||||
<div className='grid grid-cols-1 md:grid-cols-3 gap-6 mb-8'>
|
</Grid>
|
||||||
<div className='bg-white rounded-lg shadow-md p-6'>
|
<Grid size={{ xs: 12, lg: 12 }}>
|
||||||
<div className='flex items-center mb-4'>
|
<MultipleSeries data={transformMultipleData(data!)} />
|
||||||
<i className='tabler-wallet text-[24px] text-green-600 mr-2'></i>
|
</Grid>
|
||||||
<h3 className='text-lg font-semibold text-gray-900'>Net Profit</h3>
|
</Grid>
|
||||||
</div>
|
|
||||||
<p className='text-3xl font-bold text-green-600 mb-2'>
|
|
||||||
Rp {formatShortCurrency(profitData.summary.net_profit)}
|
|
||||||
</p>
|
|
||||||
<p className='text-sm text-gray-600'>Margin: {formatPercentage(profitData.summary.net_profit_margin)}</p>
|
|
||||||
</div>
|
|
||||||
<div className='bg-white rounded-lg shadow-md p-6'>
|
|
||||||
<div className='flex items-center mb-4'>
|
|
||||||
<i className='tabler-shopping-cart text-[24px] text-blue-600 mr-2'></i>
|
|
||||||
<h3 className='text-lg font-semibold text-gray-900'>Total Orders</h3>
|
|
||||||
</div>
|
|
||||||
<p className='text-3xl font-bold text-blue-600'>{profitData.summary.total_orders}</p>
|
|
||||||
</div>
|
|
||||||
<div className='bg-white rounded-lg shadow-md p-6'>
|
|
||||||
<div className='flex items-center mb-4'>
|
|
||||||
<i className='tabler-discount text-[24px] text-orange-600 mr-2'></i>
|
|
||||||
<h3 className='text-lg font-semibold text-gray-900'>Tax & Discount</h3>
|
|
||||||
</div>
|
|
||||||
<p className='text-xl font-bold text-orange-600 mb-1'>
|
|
||||||
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:{' '}
|
|
||||||
{formatShortCurrency(profitData.summary.total_discount)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Profit Chart */}
|
|
||||||
<div className='mb-8'>
|
|
||||||
<MultipleSeries data={transformMultipleData(profitData)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8'>
|
|
||||||
{/* Daily Breakdown */}
|
|
||||||
<div className='bg-white rounded-lg shadow-md'>
|
|
||||||
<div className='p-6'>
|
|
||||||
<div className='flex items-center mb-6'>
|
|
||||||
<i className='tabler-calendar text-[24px] text-purple-500 mr-2'></i>
|
|
||||||
<h2 className='text-xl font-semibold text-gray-900'>Daily Breakdown</h2>
|
|
||||||
</div>
|
|
||||||
<div className='overflow-x-auto'>
|
|
||||||
<table className='min-w-full'>
|
|
||||||
<thead>
|
|
||||||
<tr className='bg-gray-50'>
|
|
||||||
<th className='px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase'>Date</th>
|
|
||||||
<th className='px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase'>Revenue</th>
|
|
||||||
<th className='px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase'>Cost</th>
|
|
||||||
<th className='px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase'>Profit</th>
|
|
||||||
<th className='px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase'>Margin</th>
|
|
||||||
<th className='px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase'>Orders</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className='bg-white divide-y divide-gray-200'>
|
|
||||||
{profitData.data.map((day, index) => (
|
|
||||||
<tr key={index} className='hover:bg-gray-50'>
|
|
||||||
<td className='px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-900'>
|
|
||||||
{formatDate(day.date)}
|
|
||||||
</td>
|
|
||||||
<td className='px-4 py-4 whitespace-nowrap text-right text-sm text-gray-900'>
|
|
||||||
{formatCurrency(day.revenue)}
|
|
||||||
</td>
|
|
||||||
<td className='px-4 py-4 whitespace-nowrap text-right text-sm text-red-600'>
|
|
||||||
{formatCurrency(day.cost)}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className={`px-4 py-4 whitespace-nowrap text-right text-sm font-medium ${
|
|
||||||
day.gross_profit >= 0 ? 'text-green-600' : 'text-red-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{formatCurrency(day.gross_profit)}
|
|
||||||
</td>
|
|
||||||
<td className='px-4 py-4 whitespace-nowrap text-right'>
|
|
||||||
<span
|
|
||||||
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getProfitabilityColor(
|
|
||||||
day.gross_profit_margin
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
{formatPercentage(day.gross_profit_margin)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className='px-4 py-4 whitespace-nowrap text-right text-sm text-gray-900'>{day.orders}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Top Performing Products */}
|
|
||||||
<div className='bg-white rounded-lg shadow-md'>
|
|
||||||
<div className='p-6'>
|
|
||||||
<div className='flex items-center mb-6'>
|
|
||||||
<i className='tabler-trophy text-[24px] text-gold-500 mr-2'></i>
|
|
||||||
<h2 className='text-xl font-semibold text-gray-900'>Top Performers</h2>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-4'>
|
|
||||||
{profitData.product_data
|
|
||||||
.sort((a, b) => b.gross_profit - a.gross_profit)
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((product, index) => (
|
|
||||||
<div
|
|
||||||
key={product.product_id}
|
|
||||||
className='flex items-center justify-between p-4 bg-gray-50 rounded-lg'
|
|
||||||
>
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<span
|
|
||||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-bold mr-3 ${
|
|
||||||
index === 0
|
|
||||||
? 'bg-yellow-500'
|
|
||||||
: index === 1
|
|
||||||
? 'bg-gray-400'
|
|
||||||
: index === 2
|
|
||||||
? 'bg-orange-500'
|
|
||||||
: 'bg-blue-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<h3 className='font-medium text-gray-900'>{product.product_name}</h3>
|
|
||||||
<p className='text-sm text-gray-600'>{product.category_name}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='text-right'>
|
|
||||||
<p className={`font-bold ${product.gross_profit >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
|
||||||
{formatCurrency(product.gross_profit)}
|
|
||||||
</p>
|
|
||||||
<p className='text-xs text-gray-500'>{formatPercentage(product.gross_profit_margin)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Product Analysis Table */}
|
|
||||||
<div className='bg-white rounded-lg shadow-md'>
|
|
||||||
<div className='p-6'>
|
|
||||||
<div className='flex items-center mb-6'>
|
|
||||||
<i className='tabler-package text-[24px] text-green-500 mr-2'></i>
|
|
||||||
<h2 className='text-xl font-semibold text-gray-900'>Product Analysis</h2>
|
|
||||||
</div>
|
|
||||||
<div className='overflow-x-auto'>
|
|
||||||
<table className='min-w-full'>
|
|
||||||
<thead>
|
|
||||||
<tr className='bg-gray-50'>
|
|
||||||
<th className='px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase'>Product</th>
|
|
||||||
<th className='px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase'>Category</th>
|
|
||||||
<th className='px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase'>Qty</th>
|
|
||||||
<th className='px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase'>Revenue</th>
|
|
||||||
<th className='px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase'>Cost</th>
|
|
||||||
<th className='px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase'>Profit</th>
|
|
||||||
<th className='px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase'>Margin</th>
|
|
||||||
<th className='px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase'>Per Unit</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className='bg-white divide-y divide-gray-200'>
|
|
||||||
{profitData.product_data
|
|
||||||
.sort((a, b) => b.gross_profit - a.gross_profit)
|
|
||||||
.map(product => (
|
|
||||||
<tr key={product.product_id} className='hover:bg-gray-50'>
|
|
||||||
<td className='px-4 py-4 whitespace-nowrap'>
|
|
||||||
<div className='text-sm font-medium text-gray-900'>{product.product_name}</div>
|
|
||||||
</td>
|
|
||||||
<td className='px-4 py-4 whitespace-nowrap'>
|
|
||||||
<span className='inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800'>
|
|
||||||
{product.category_name}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className='px-4 py-4 whitespace-nowrap text-right text-sm text-gray-900'>
|
|
||||||
{product.quantity_sold}
|
|
||||||
</td>
|
|
||||||
<td className='px-4 py-4 whitespace-nowrap text-right text-sm text-gray-900'>
|
|
||||||
{formatCurrency(product.revenue)}
|
|
||||||
</td>
|
|
||||||
<td className='px-4 py-4 whitespace-nowrap text-right text-sm text-red-600'>
|
|
||||||
{formatCurrency(product.cost)}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className={`px-4 py-4 whitespace-nowrap text-right text-sm font-medium ${
|
|
||||||
product.gross_profit >= 0 ? 'text-green-600' : 'text-red-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{formatCurrency(product.gross_profit)}
|
|
||||||
</td>
|
|
||||||
<td className='px-4 py-4 whitespace-nowrap text-right'>
|
|
||||||
<span
|
|
||||||
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getProfitabilityColor(
|
|
||||||
product.gross_profit_margin
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
{formatPercentage(product.gross_profit_margin)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className={`px-4 py-4 whitespace-nowrap text-right text-sm ${
|
|
||||||
product.profit_per_unit >= 0 ? 'text-green-600' : 'text-red-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{formatCurrency(product.profit_per_unit)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DashboardProfitloss
|
export default DashboardProfitLoss
|
||||||
|
|||||||
@ -2,23 +2,24 @@
|
|||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
|
|
||||||
// Type Imports
|
// Type Imports
|
||||||
import type { Locale } from '@configs/i18n'
|
|
||||||
import type { ChildrenType } from '@core/types'
|
import type { ChildrenType } from '@core/types'
|
||||||
|
import type { Locale } from '@configs/i18n'
|
||||||
|
|
||||||
// Layout Imports
|
// Layout Imports
|
||||||
import HorizontalLayout from '@layouts/HorizontalLayout'
|
|
||||||
import LayoutWrapper from '@layouts/LayoutWrapper'
|
import LayoutWrapper from '@layouts/LayoutWrapper'
|
||||||
import VerticalLayout from '@layouts/VerticalLayout'
|
import VerticalLayout from '@layouts/VerticalLayout'
|
||||||
|
import HorizontalLayout from '@layouts/HorizontalLayout'
|
||||||
|
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import AuthGuard from '@/hocs/AuthGuard'
|
|
||||||
import Providers from '@components/Providers'
|
import Providers from '@components/Providers'
|
||||||
import HorizontalFooter from '@components/layout/horizontal/Footer'
|
|
||||||
import Header from '@components/layout/horizontal/Header'
|
|
||||||
import VerticalFooter from '@components/layout/vertical/Footer'
|
|
||||||
import Navbar from '@components/layout/vertical/Navbar'
|
|
||||||
import Navigation from '@components/layout/vertical/Navigation'
|
import Navigation from '@components/layout/vertical/Navigation'
|
||||||
|
import Header from '@components/layout/horizontal/Header'
|
||||||
|
import Navbar from '@components/layout/vertical/Navbar'
|
||||||
|
import VerticalFooter from '@components/layout/vertical/Footer'
|
||||||
|
import HorizontalFooter from '@components/layout/horizontal/Footer'
|
||||||
|
import Customizer from '@core/components/customizer'
|
||||||
import ScrollToTop from '@core/components/scroll-to-top'
|
import ScrollToTop from '@core/components/scroll-to-top'
|
||||||
|
import AuthGuard from '@/hocs/AuthGuard'
|
||||||
|
|
||||||
// Config Imports
|
// Config Imports
|
||||||
import { i18n } from '@configs/i18n'
|
import { i18n } from '@configs/i18n'
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
// MUI Imports
|
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
|
|
||||||
// Type Imports
|
|
||||||
import type { Locale } from '@configs/i18n'
|
|
||||||
import type { ChildrenType } from '@core/types'
|
|
||||||
|
|
||||||
// Layout Imports
|
|
||||||
import HorizontalLayout from '@layouts/HorizontalLayout'
|
|
||||||
import LayoutWrapper from '@layouts/LayoutWrapper'
|
|
||||||
import VerticalLayout from '@layouts/VerticalLayout'
|
|
||||||
|
|
||||||
// Component Imports
|
|
||||||
import Providers from '@components/Providers'
|
|
||||||
import HorizontalFooter from '@components/layout/horizontal/Footer'
|
|
||||||
import Header from '@components/layout/horizontal/Header'
|
|
||||||
import VerticalFooter from '@components/layout/vertical/Footer'
|
|
||||||
import Navbar from '@components/layout/vertical/Navbar'
|
|
||||||
import Navigation from '@components/layout/vertical/Navigation'
|
|
||||||
import ScrollToTop from '@core/components/scroll-to-top'
|
|
||||||
|
|
||||||
// Config Imports
|
|
||||||
import { i18n } from '@configs/i18n'
|
|
||||||
|
|
||||||
// Util Imports
|
|
||||||
import { getDictionary } from '@/utils/getDictionary'
|
|
||||||
import { getMode, getSystemMode } from '@core/utils/serverHelpers'
|
|
||||||
import RolesGuard from '../../../../hocs/RolesGuard'
|
|
||||||
|
|
||||||
const Layout = async (props: ChildrenType & { params: Promise<{ lang: Locale }> }) => {
|
|
||||||
const params = await props.params
|
|
||||||
|
|
||||||
const { children } = props
|
|
||||||
|
|
||||||
// Vars
|
|
||||||
const direction = i18n.langDirection[params.lang]
|
|
||||||
const dictionary = await getDictionary(params.lang)
|
|
||||||
const mode = await getMode()
|
|
||||||
const systemMode = await getSystemMode()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Providers direction={direction}>
|
|
||||||
<RolesGuard locale={params.lang}>
|
|
||||||
<LayoutWrapper
|
|
||||||
systemMode={systemMode}
|
|
||||||
verticalLayout={
|
|
||||||
<VerticalLayout
|
|
||||||
navigation={<Navigation dictionary={dictionary} mode={mode} />}
|
|
||||||
navbar={<Navbar />}
|
|
||||||
footer={<VerticalFooter />}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</VerticalLayout>
|
|
||||||
}
|
|
||||||
horizontalLayout={
|
|
||||||
<HorizontalLayout header={<Header dictionary={dictionary} />} footer={<HorizontalFooter />}>
|
|
||||||
{children}
|
|
||||||
</HorizontalLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ScrollToTop className='mui-fixed'>
|
|
||||||
<Button
|
|
||||||
variant='contained'
|
|
||||||
className='is-10 bs-10 rounded-full p-0 min-is-0 flex items-center justify-center'
|
|
||||||
>
|
|
||||||
<i className='tabler-arrow-up' />
|
|
||||||
</Button>
|
|
||||||
</ScrollToTop>
|
|
||||||
{/* <Customizer dir={direction} /> */}
|
|
||||||
</RolesGuard>
|
|
||||||
</Providers>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Layout
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import OrganizationListTable from '../../../../../../../views/sa/organizations/list/OrganizationListTable'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the
|
|
||||||
* ! `.env` file found at root of your project and also update the API endpoints like `/apps/ecommerce` in below example.
|
|
||||||
* ! Also, remove the above server action import and the action itself from the `src/app/server/actions.ts` file to clean up unused code
|
|
||||||
* ! because we've used the server action for getting our static data.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* const getEcommerceData = async () => {
|
|
||||||
// Vars
|
|
||||||
const res = await fetch(`${process.env.API_URL}/apps/ecommerce`)
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('Failed to fetch ecommerce data')
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json()
|
|
||||||
} */
|
|
||||||
|
|
||||||
const OrganizationsListTablePage = async () => {
|
|
||||||
return <OrganizationListTable />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OrganizationsListTablePage
|
|
||||||
@ -31,7 +31,6 @@ import { useSettings } from '@core/hooks/useSettings'
|
|||||||
import { getLocalizedUrl } from '@/utils/i18n'
|
import { getLocalizedUrl } from '@/utils/i18n'
|
||||||
import { useAuthMutation } from '../../../services/mutations/auth'
|
import { useAuthMutation } from '../../../services/mutations/auth'
|
||||||
import { useAuth } from '../../../contexts/authContext'
|
import { useAuth } from '../../../contexts/authContext'
|
||||||
import { CircularProgress } from '@mui/material'
|
|
||||||
|
|
||||||
// Styled component for badge content
|
// Styled component for badge content
|
||||||
const BadgeContentSpan = styled('span')({
|
const BadgeContentSpan = styled('span')({
|
||||||
@ -163,9 +162,8 @@ const UserDropdown = () => {
|
|||||||
endIcon={<i className='tabler-logout' />}
|
endIcon={<i className='tabler-logout' />}
|
||||||
onClick={handleUserLogout}
|
onClick={handleUserLogout}
|
||||||
sx={{ '& .MuiButton-endIcon': { marginInlineStart: 1.5 } }}
|
sx={{ '& .MuiButton-endIcon': { marginInlineStart: 1.5 } }}
|
||||||
disabled={logout.isPending}
|
|
||||||
>
|
>
|
||||||
Logout {logout.isPending && <CircularProgress size={16} />}
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
|
|||||||
@ -29,8 +29,6 @@ import { getLocalizedUrl } from '@/utils/i18n'
|
|||||||
|
|
||||||
// Style Imports
|
// Style Imports
|
||||||
import navigationCustomStyles from '@core/styles/vertical/navigationCustomStyles'
|
import navigationCustomStyles from '@core/styles/vertical/navigationCustomStyles'
|
||||||
import { useAuth } from '../../../contexts/authContext'
|
|
||||||
import SuperAdminVerticalMenu from './SuperAdminVerticalMenu'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
dictionary: Awaited<ReturnType<typeof getDictionary>>
|
dictionary: Awaited<ReturnType<typeof getDictionary>>
|
||||||
@ -66,8 +64,6 @@ const Navigation = (props: Props) => {
|
|||||||
const { mode: muiMode, systemMode: muiSystemMode } = useColorScheme()
|
const { mode: muiMode, systemMode: muiSystemMode } = useColorScheme()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const { currentUser } = useAuth()
|
|
||||||
|
|
||||||
// Refs
|
// Refs
|
||||||
const shadowRef = useRef(null)
|
const shadowRef = useRef(null)
|
||||||
|
|
||||||
@ -133,12 +129,7 @@ const Navigation = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
</NavHeader>
|
</NavHeader>
|
||||||
<StyledBoxForShadow ref={shadowRef} />
|
<StyledBoxForShadow ref={shadowRef} />
|
||||||
|
|
||||||
{currentUser?.role === 'superadmin' ? (
|
|
||||||
<SuperAdminVerticalMenu dictionary={dictionary} scrollMenu={scrollMenu} />
|
|
||||||
) : (
|
|
||||||
<VerticalMenu dictionary={dictionary} scrollMenu={scrollMenu} />
|
<VerticalMenu dictionary={dictionary} scrollMenu={scrollMenu} />
|
||||||
)}
|
|
||||||
</VerticalNav>
|
</VerticalNav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
// Next Imports
|
|
||||||
import { useParams } from 'next/navigation'
|
|
||||||
|
|
||||||
// MUI Imports
|
|
||||||
import { useTheme } from '@mui/material/styles'
|
|
||||||
|
|
||||||
// Third-party Imports
|
|
||||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
|
||||||
|
|
||||||
// Type Imports
|
|
||||||
import type { getDictionary } from '@/utils/getDictionary'
|
|
||||||
import type { VerticalMenuContextProps } from '@menu/components/vertical-menu/Menu'
|
|
||||||
|
|
||||||
// Component Imports
|
|
||||||
import { Menu, MenuItem, MenuSection, SubMenu } from '@menu/vertical-menu'
|
|
||||||
|
|
||||||
// import { GenerateVerticalMenu } from '@components/GenerateMenu'
|
|
||||||
|
|
||||||
// Hook Imports
|
|
||||||
import useVerticalNav from '@menu/hooks/useVerticalNav'
|
|
||||||
|
|
||||||
// Styled Component Imports
|
|
||||||
import StyledVerticalNavExpandIcon from '@menu/styles/vertical/StyledVerticalNavExpandIcon'
|
|
||||||
|
|
||||||
// Style Imports
|
|
||||||
import menuItemStyles from '@core/styles/vertical/menuItemStyles'
|
|
||||||
import menuSectionStyles from '@core/styles/vertical/menuSectionStyles'
|
|
||||||
|
|
||||||
// Menu Data Imports
|
|
||||||
// import menuData from '@/data/navigation/verticalMenuData'
|
|
||||||
|
|
||||||
type RenderExpandIconProps = {
|
|
||||||
open?: boolean
|
|
||||||
transitionDuration?: VerticalMenuContextProps['transitionDuration']
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
dictionary: Awaited<ReturnType<typeof getDictionary>>
|
|
||||||
scrollMenu: (container: any, isPerfectScrollbar: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const RenderExpandIcon = ({ open, transitionDuration }: RenderExpandIconProps) => (
|
|
||||||
<StyledVerticalNavExpandIcon open={open} transitionDuration={transitionDuration}>
|
|
||||||
<i className='tabler-chevron-right' />
|
|
||||||
</StyledVerticalNavExpandIcon>
|
|
||||||
)
|
|
||||||
|
|
||||||
const SuperAdminVerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
|
||||||
// Hooks
|
|
||||||
const theme = useTheme()
|
|
||||||
const verticalNavOptions = useVerticalNav()
|
|
||||||
const params = useParams()
|
|
||||||
|
|
||||||
// Vars
|
|
||||||
const { isBreakpointReached, transitionDuration } = verticalNavOptions
|
|
||||||
const { lang: locale } = params
|
|
||||||
|
|
||||||
const ScrollWrapper = isBreakpointReached ? 'div' : PerfectScrollbar
|
|
||||||
|
|
||||||
return (
|
|
||||||
// eslint-disable-next-line lines-around-comment
|
|
||||||
/* Custom scrollbar instead of browser scroll, remove if you want browser scroll only */
|
|
||||||
<ScrollWrapper
|
|
||||||
{...(isBreakpointReached
|
|
||||||
? {
|
|
||||||
className: 'bs-full overflow-y-auto overflow-x-hidden',
|
|
||||||
onScroll: container => scrollMenu(container, false)
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
options: { wheelPropagation: false, suppressScrollX: true },
|
|
||||||
onScrollY: container => scrollMenu(container, true)
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{/* Incase you also want to scroll NavHeader to scroll with Vertical Menu, remove NavHeader from above and paste it below this comment */}
|
|
||||||
{/* Vertical Menu */}
|
|
||||||
<Menu
|
|
||||||
popoutMenuOffset={{ mainAxis: 23 }}
|
|
||||||
menuItemStyles={menuItemStyles(verticalNavOptions, theme)}
|
|
||||||
renderExpandIcon={({ open }) => <RenderExpandIcon open={open} transitionDuration={transitionDuration} />}
|
|
||||||
renderExpandedMenuItemIcon={{ icon: <i className='tabler-circle text-xs' /> }}
|
|
||||||
menuSectionStyles={menuSectionStyles(verticalNavOptions, theme)}
|
|
||||||
>
|
|
||||||
<SubMenu label={dictionary['navigation'].organization} icon={<i className='tabler-smart-home' />}>
|
|
||||||
<MenuItem href={`/${locale}/sa/organizations/list`}>{dictionary['navigation'].list}</MenuItem>
|
|
||||||
</SubMenu>
|
|
||||||
</Menu>
|
|
||||||
</ScrollWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SuperAdminVerticalMenu
|
|
||||||
@ -91,8 +91,8 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
|||||||
<MenuItem href={`/${locale}/dashboards/daily-report`}>{dictionary['navigation'].dailyReport}</MenuItem>
|
<MenuItem href={`/${locale}/dashboards/daily-report`}>{dictionary['navigation'].dailyReport}</MenuItem>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
<MenuSection label={dictionary['navigation'].appsPages}>
|
<MenuSection label={dictionary['navigation'].appsPages}>
|
||||||
<SubMenu label={dictionary['navigation'].eCommerce} icon={<i className='tabler-salad' />}>
|
<SubMenu label={dictionary['navigation'].eCommerce} icon={<i className='tabler-shopping-cart' />}>
|
||||||
{/* <MenuItem href={`/${locale}/apps/ecommerce/dashboard`}>{dictionary['navigation'].dashboard}</MenuItem> */}
|
<MenuItem href={`/${locale}/apps/ecommerce/dashboard`}>{dictionary['navigation'].dashboard}</MenuItem>
|
||||||
<SubMenu label={dictionary['navigation'].products}>
|
<SubMenu label={dictionary['navigation'].products}>
|
||||||
<MenuItem href={`/${locale}/apps/ecommerce/products/list`}>{dictionary['navigation'].list}</MenuItem>
|
<MenuItem href={`/${locale}/apps/ecommerce/products/list`}>{dictionary['navigation'].list}</MenuItem>
|
||||||
<MenuItem className='hidden' href={`/${locale}/apps/ecommerce/products/${params.id}/detail`}>
|
<MenuItem className='hidden' href={`/${locale}/apps/ecommerce/products/${params.id}/detail`}>
|
||||||
@ -125,7 +125,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
|||||||
{dictionary['navigation'].adjustment}
|
{dictionary['navigation'].adjustment}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
{/* <MenuItem href={`/${locale}/apps/ecommerce/settings`}>{dictionary['navigation'].settings}</MenuItem> */}
|
<MenuItem href={`/${locale}/apps/ecommerce/settings`}>{dictionary['navigation'].settings}</MenuItem>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
<SubMenu label={dictionary['navigation'].organization} icon={<i className='tabler-sitemap' />}>
|
<SubMenu label={dictionary['navigation'].organization} icon={<i className='tabler-sitemap' />}>
|
||||||
<SubMenu label={dictionary['navigation'].outlet}>
|
<SubMenu label={dictionary['navigation'].outlet}>
|
||||||
|
|||||||
@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
import { createContext, useContext, useEffect, useState } from 'react'
|
import { createContext, useContext, useEffect, useState } from 'react'
|
||||||
import Loading from '../components/layout/shared/Loading'
|
import Loading from '../components/layout/shared/Loading'
|
||||||
import { User } from '../types/services/user'
|
|
||||||
|
|
||||||
type AuthContextType = {
|
type AuthContextType = {
|
||||||
isAuthenticated: boolean
|
isAuthenticated: boolean
|
||||||
token: string | null
|
token: string | null
|
||||||
currentUser: User | null
|
currentUser: any | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType>({
|
const AuthContext = createContext<AuthContextType>({
|
||||||
@ -18,7 +17,7 @@ const AuthContext = createContext<AuthContextType>({
|
|||||||
|
|
||||||
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
const [token, setToken] = useState<string | null>(null)
|
const [token, setToken] = useState<string | null>(null)
|
||||||
const [currentUser, setCurrentUser] = useState<User | null>(null)
|
const [currentUser, setCurrentUser] = useState<any | null>(null)
|
||||||
const [isInitialized, setIsInitialized] = useState(false)
|
const [isInitialized, setIsInitialized] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"navigation": {
|
"navigation": {
|
||||||
"dashboards": "لوحات القيادة",
|
"dashboards": "لوحات القيادة",
|
||||||
"analytics": "تحليلات",
|
"analytics": "تحليلات",
|
||||||
"eCommerce": "تجزئة الكترونية",
|
"eCommerce": "التجارة الإلكترونية",
|
||||||
"stock": "المخزون",
|
"stock": "المخزون",
|
||||||
"academy": "أكاديمية",
|
"academy": "أكاديمية",
|
||||||
"logistics": "اللوجستية",
|
"logistics": "اللوجستية",
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"navigation": {
|
"navigation": {
|
||||||
"dashboards": "Dashboards",
|
"dashboards": "Dashboards",
|
||||||
"analytics": "Analytics",
|
"analytics": "Analytics",
|
||||||
"eCommerce": "Inventory",
|
"eCommerce": "eCommerce",
|
||||||
"stock": "Stock",
|
"stock": "Stock",
|
||||||
"academy": "Academy",
|
"academy": "Academy",
|
||||||
"logistics": "Logistics",
|
"logistics": "Logistics",
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"navigation": {
|
"navigation": {
|
||||||
"dashboards": "Tableaux de bord",
|
"dashboards": "Tableaux de bord",
|
||||||
"analytics": "Analytique",
|
"analytics": "Analytique",
|
||||||
"eCommerce": "Inventaire",
|
"eCommerce": "commerce électronique",
|
||||||
"stock": "Stock",
|
"stock": "Stock",
|
||||||
"academy": "Académie",
|
"academy": "Académie",
|
||||||
"logistics": "Logistique",
|
"logistics": "Logistique",
|
||||||
|
|||||||
@ -12,17 +12,13 @@ import Loading from '../components/layout/shared/Loading'
|
|||||||
import { getLocalizedUrl } from '../utils/i18n'
|
import { getLocalizedUrl } from '../utils/i18n'
|
||||||
|
|
||||||
export default function AuthGuard({ children, locale }: ChildrenType & { locale: Locale }) {
|
export default function AuthGuard({ children, locale }: ChildrenType & { locale: Locale }) {
|
||||||
const { isAuthenticated, currentUser } = useAuth()
|
const { isAuthenticated } = useAuth()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
redirect(getLocalizedUrl('/login', locale))
|
redirect(getLocalizedUrl('/login', locale))
|
||||||
}
|
}
|
||||||
|
}, [isAuthenticated])
|
||||||
|
|
||||||
if (currentUser?.role !== 'admin') {
|
return <>{isAuthenticated ? children : <Loading />}</>
|
||||||
redirect(getLocalizedUrl('/not-found', locale))
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, currentUser])
|
|
||||||
|
|
||||||
return <>{isAuthenticated && currentUser?.role === 'admin' ? children : <Loading />}</>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
// Type Imports
|
|
||||||
import type { Locale } from '@configs/i18n'
|
|
||||||
import type { ChildrenType } from '@core/types'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { useAuth } from '../contexts/authContext'
|
|
||||||
|
|
||||||
// Component Imports
|
|
||||||
import { redirect } from 'next/navigation'
|
|
||||||
import Loading from '../components/layout/shared/Loading'
|
|
||||||
import { getLocalizedUrl } from '../utils/i18n'
|
|
||||||
|
|
||||||
export default function RolesGuard({ children, locale }: ChildrenType & { locale: Locale }) {
|
|
||||||
const { isAuthenticated, currentUser } = useAuth()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
redirect(getLocalizedUrl('/login', locale))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentUser?.role !== 'superadmin') {
|
|
||||||
redirect(getLocalizedUrl('/not-found', locale))
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, currentUser])
|
|
||||||
|
|
||||||
return <>{isAuthenticated && currentUser?.role === 'superadmin' ? children : <Loading />}</>
|
|
||||||
}
|
|
||||||
@ -6,9 +6,6 @@ import customerReducer from '@/redux-store/slices/customer'
|
|||||||
import paymentMethodReducer from '@/redux-store/slices/paymentMethod'
|
import paymentMethodReducer from '@/redux-store/slices/paymentMethod'
|
||||||
import ingredientReducer from '@/redux-store/slices/ingredient'
|
import ingredientReducer from '@/redux-store/slices/ingredient'
|
||||||
import orderReducer from '@/redux-store/slices/order'
|
import orderReducer from '@/redux-store/slices/order'
|
||||||
import productRecipeReducer from '@/redux-store/slices/productRecipe'
|
|
||||||
import organizationReducer from '@/redux-store/slices/organization'
|
|
||||||
import userReducer from '@/redux-store/slices/user'
|
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
@ -16,10 +13,7 @@ export const store = configureStore({
|
|||||||
customerReducer,
|
customerReducer,
|
||||||
paymentMethodReducer,
|
paymentMethodReducer,
|
||||||
ingredientReducer,
|
ingredientReducer,
|
||||||
orderReducer,
|
orderReducer
|
||||||
productRecipeReducer,
|
|
||||||
organizationReducer,
|
|
||||||
userReducer
|
|
||||||
},
|
},
|
||||||
middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false })
|
middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false })
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
// Third-party Imports
|
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
|
||||||
import { createSlice } from '@reduxjs/toolkit'
|
|
||||||
|
|
||||||
// Type Imports
|
|
||||||
|
|
||||||
// Data Imports
|
|
||||||
import { Organization } from '../../types/services/organization'
|
|
||||||
|
|
||||||
const initialState: { currentOrganization: Organization } = {
|
|
||||||
currentOrganization: {
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
phone_number: '',
|
|
||||||
plan_type: 'basic',
|
|
||||||
created_at: '',
|
|
||||||
updated_at: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const organizationSlice = createSlice({
|
|
||||||
name: 'organization',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
setOrganization: (state, action: PayloadAction<Organization>) => {
|
|
||||||
state.currentOrganization = action.payload
|
|
||||||
},
|
|
||||||
resetOrganization: state => {
|
|
||||||
state.currentOrganization = initialState.currentOrganization
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const { setOrganization, resetOrganization } = organizationSlice.actions
|
|
||||||
|
|
||||||
export default organizationSlice.reducer
|
|
||||||
@ -13,6 +13,7 @@ const initialState: { productRequest: ProductRequest } = {
|
|||||||
sku: '',
|
sku: '',
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
barcode: '',
|
||||||
price: 0,
|
price: 0,
|
||||||
cost: 0,
|
cost: 0,
|
||||||
printer_type: '',
|
printer_type: '',
|
||||||
|
|||||||
@ -1,79 +0,0 @@
|
|||||||
// 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: { 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: {
|
|
||||||
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 => {
|
|
||||||
state.currentProductRecipe = initialState.currentProductRecipe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const { setProductVariant, resetProductVariant, setProductRecipe, resetProductRecipe } = productRecipeSlice.actions
|
|
||||||
|
|
||||||
export default productRecipeSlice.reducer
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
// Third-party Imports
|
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
|
||||||
import { createSlice } from '@reduxjs/toolkit'
|
|
||||||
|
|
||||||
// Type Imports
|
|
||||||
|
|
||||||
// Data Imports
|
|
||||||
import { User } from '../../types/services/user'
|
|
||||||
|
|
||||||
const initialState: { currentUser: User } = {
|
|
||||||
currentUser: {
|
|
||||||
id: '',
|
|
||||||
organization_id: '',
|
|
||||||
outlet_id: '',
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
role: '',
|
|
||||||
permissions: {},
|
|
||||||
is_active: false,
|
|
||||||
created_at: '',
|
|
||||||
updated_at: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const userSlice = createSlice({
|
|
||||||
name: 'user',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
setUser: (state, action: PayloadAction<User>) => {
|
|
||||||
state.currentUser = action.payload
|
|
||||||
},
|
|
||||||
resetUser: state => {
|
|
||||||
state.currentUser = initialState.currentUser
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const { setUser, resetUser } = userSlice.actions
|
|
||||||
|
|
||||||
export default userSlice.reducer
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import { OrganizationRequest } from '../../types/services/organization'
|
|
||||||
import { api } from '../api'
|
|
||||||
|
|
||||||
export const useOrganizationsMutation = () => {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
const createOrganization = useMutation({
|
|
||||||
mutationFn: async (newOrganization: OrganizationRequest) => {
|
|
||||||
const response = await api.post('/organizations', newOrganization)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success('Organization created successfully!')
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
|
||||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateOrganization = useMutation({
|
|
||||||
mutationFn: async ({ id, payload }: { id: string; payload: OrganizationRequest }) => {
|
|
||||||
const response = await api.put(`/organizations/${id}`, payload)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success('Organization updated successfully!')
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
|
||||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteOrganization = useMutation({
|
|
||||||
mutationFn: async (id: string) => {
|
|
||||||
const response = await api.delete(`/organizations/${id}`)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success('Organization deleted successfully!')
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['organizations'] })
|
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
|
||||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return { createOrganization, updateOrganization, deleteOrganization }
|
|
||||||
}
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import { ProductRecipeRequest } from '../../types/services/productRecipe'
|
|
||||||
import { api } from '../api'
|
|
||||||
|
|
||||||
export const useProductRecipesMutation = () => {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
const createProductRecipe = useMutation({
|
|
||||||
mutationFn: async (newProductRecipe: ProductRecipeRequest) => {
|
|
||||||
const { variant_id, ...rest } = newProductRecipe
|
|
||||||
|
|
||||||
const cleanRequest = variant_id ? newProductRecipe : rest
|
|
||||||
|
|
||||||
const response = await api.post('/product-recipes', cleanRequest)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success('Product Recipe created successfully!')
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['product-recipes'] })
|
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
|
||||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateProductRecipe = useMutation({
|
|
||||||
mutationFn: async ({ id, payload }: { id: string; payload: ProductRecipeRequest }) => {
|
|
||||||
const response = await api.put(`/product-recipes/${id}`, payload)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success('Product Recipe updated successfully!')
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['product-recipes'] })
|
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
|
||||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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,
|
|
||||||
deleteProductRecipe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,54 +1,52 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { toast } from 'react-toastify'
|
import { CustomerRequest } from '../../types/services/customer'
|
||||||
import { UserRequest } from '../../types/services/user'
|
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
export const useUsersMutation = () => {
|
export const useCustomersMutation = () => {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const createUser = useMutation({
|
const createCustomer = useMutation({
|
||||||
mutationFn: async (newUser: UserRequest) => {
|
mutationFn: async (newCustomer: CustomerRequest) => {
|
||||||
const response = await api.post('/users', newUser)
|
const response = await api.post('/customers', newCustomer)
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('User created successfully!')
|
toast.success('Customer created successfully!')
|
||||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
queryClient.invalidateQueries({ queryKey: ['customers'] })
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateUser = useMutation({
|
const updateCustomer = useMutation({
|
||||||
mutationFn: async ({ id, payload }: { id: string; payload: UserRequest }) => {
|
mutationFn: async ({ id, payload }: { id: string; payload: CustomerRequest }) => {
|
||||||
const {password, ...rest} = payload
|
const response = await api.put(`/customers/${id}`, payload)
|
||||||
|
|
||||||
const response = await api.put(`/users/${id}`, rest)
|
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('User updated successfully!')
|
toast.success('Customer updated successfully!')
|
||||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
queryClient.invalidateQueries({ queryKey: ['customers'] })
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteUser = useMutation({
|
const deleteCustomer = useMutation({
|
||||||
mutationFn: async (id: string) => {
|
mutationFn: async (id: string) => {
|
||||||
const response = await api.delete(`/users/${id}`)
|
const response = await api.delete(`/customers/${id}`)
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('User deleted successfully!')
|
toast.success('Customer deleted successfully!')
|
||||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
queryClient.invalidateQueries({ queryKey: ['customers'] })
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
|
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return { createUser, updateUser, deleteUser }
|
return { createCustomer, updateCustomer, deleteCustomer }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import { Organizations } from '../../types/services/organization'
|
|
||||||
import { api } from '../api'
|
|
||||||
|
|
||||||
interface OrganizationsQueryParams {
|
|
||||||
page?: number
|
|
||||||
limit?: number
|
|
||||||
search?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useOrganizations(params: OrganizationsQueryParams = {}) {
|
|
||||||
const { page = 1, limit = 10, search = '', ...filters } = params
|
|
||||||
|
|
||||||
return useQuery<Organizations>({
|
|
||||||
queryKey: ['organizations', { page, limit, search, ...filters }],
|
|
||||||
queryFn: async () => {
|
|
||||||
const queryParams = new URLSearchParams()
|
|
||||||
|
|
||||||
queryParams.append('page', page.toString())
|
|
||||||
queryParams.append('limit', limit.toString())
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
queryParams.append('search', search)
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(filters).forEach(([key, value]) => {
|
|
||||||
if (value !== undefined && value !== null && value !== '') {
|
|
||||||
queryParams.append(key, value.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await api.get(`/organizations?${queryParams.toString()}`)
|
|
||||||
return res.data.data
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import { ProductRecipe } from '../../types/services/productRecipe'
|
|
||||||
import { api } from '../api'
|
|
||||||
|
|
||||||
export function useProductRecipesByProduct(productId: string) {
|
|
||||||
return useQuery<ProductRecipe[]>({
|
|
||||||
queryKey: ['product-recipes', productId],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await api.get(`/product-recipes/product/${productId}`)
|
|
||||||
return res.data.data
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Product, Products } from '../../types/services/product'
|
import { Products } from '../../types/services/product'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
|
|
||||||
export interface ProductsQueryParams {
|
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 | string
|
is_active?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProducts(params: ProductsQueryParams = {}) {
|
export function useProducts(params: ProductsQueryParams = {}) {
|
||||||
@ -39,7 +39,7 @@ export function useProducts(params: ProductsQueryParams = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useProductById(id: string) {
|
export function useProductById(id: string) {
|
||||||
return useQuery<Product>({
|
return useQuery({
|
||||||
queryKey: ['product', id],
|
queryKey: ['product', id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await api.get(`/products/${id}`)
|
const res = await api.get(`/products/${id}`)
|
||||||
|
|||||||
@ -13,7 +13,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
export interface OrganizationRequest {
|
|
||||||
organization_name: string; // required, 1–255 chars
|
|
||||||
organization_email?: string | null; // optional, must be a valid email if present
|
|
||||||
organization_phone_number?: string | null; // optional
|
|
||||||
plan_type: 'basic' | 'premium' | 'enterprise'; // required, enum
|
|
||||||
|
|
||||||
admin_name: string; // required, 1–255 chars
|
|
||||||
admin_email: string; // required, email
|
|
||||||
admin_password: string; // required, min length 6
|
|
||||||
|
|
||||||
outlet_name: string; // required, 1–255 chars
|
|
||||||
outlet_address?: string | null; // optional
|
|
||||||
outlet_timezone?: string | null; // optional
|
|
||||||
outlet_currency: string; // required, exactly 3 chars (ISO currency code)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Organization {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string | null;
|
|
||||||
phone_number: string | null;
|
|
||||||
plan_type: "basic" | "enterprise"; // can be extended if there are more plan types
|
|
||||||
created_at: string; // ISO date string
|
|
||||||
updated_at: string; // ISO date string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Organizations {
|
|
||||||
organizations: Organization[];
|
|
||||||
total_count: number;
|
|
||||||
page: number;
|
|
||||||
limit: number;
|
|
||||||
total_pages: number;
|
|
||||||
}
|
|
||||||
@ -47,6 +47,7 @@ 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
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
export interface Product {
|
|
||||||
ID: string;
|
|
||||||
OrganizationID: string;
|
|
||||||
CategoryID: string;
|
|
||||||
SKU: string;
|
|
||||||
Name: string;
|
|
||||||
Description: string | null;
|
|
||||||
Price: number;
|
|
||||||
Cost: number;
|
|
||||||
BusinessType: string;
|
|
||||||
ImageURL: string;
|
|
||||||
PrinterType: string;
|
|
||||||
UnitID: string | null;
|
|
||||||
HasIngredients: boolean;
|
|
||||||
Metadata: Record<string, any>;
|
|
||||||
IsActive: boolean;
|
|
||||||
CreatedAt: string; // ISO date string
|
|
||||||
UpdatedAt: string; // ISO date string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Ingredient {
|
|
||||||
id: string;
|
|
||||||
organization_id: string;
|
|
||||||
outlet_id: string | null;
|
|
||||||
name: string;
|
|
||||||
unit_id: string;
|
|
||||||
cost: number;
|
|
||||||
stock: number;
|
|
||||||
is_semi_finished: boolean;
|
|
||||||
is_active: boolean;
|
|
||||||
metadata: Record<string, any>;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductRecipe {
|
|
||||||
id: string;
|
|
||||||
organization_id: string;
|
|
||||||
outlet_id: string | null;
|
|
||||||
product_id: string;
|
|
||||||
variant_id: string | null;
|
|
||||||
ingredient_id: string;
|
|
||||||
quantity: number;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
product: Product;
|
|
||||||
ingredient: Ingredient;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductRecipeRequest {
|
|
||||||
product_id: string;
|
|
||||||
variant_id: string | null;
|
|
||||||
ingredient_id: string;
|
|
||||||
quantity: number;
|
|
||||||
outlet_id: string | null;
|
|
||||||
}
|
|
||||||
@ -5,7 +5,7 @@ export type User = {
|
|||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
permissions: Record<string, boolean>;
|
permissions: Record<string, unknown>;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
created_at: string; // ISO date string
|
created_at: string; // ISO date string
|
||||||
updated_at: string; // ISO date string
|
updated_at: string; // ISO date string
|
||||||
@ -24,11 +24,12 @@ export type Users = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UserRequest = {
|
export type UserRequest = {
|
||||||
|
organization_id: string;
|
||||||
outlet_id: string;
|
outlet_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
role: string;
|
role: string;
|
||||||
permissions: Record<string, boolean>;
|
permissions: Record<string, unknown>;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,23 +7,12 @@ export const formatCurrency = (amount: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const formatShortCurrency = (num: number): string => {
|
export const formatShortCurrency = (num: number): string => {
|
||||||
const formatNumber = (value: number, suffix: string) => {
|
if (num >= 1_000_000) {
|
||||||
const str = value.toFixed(2).replace(/\.00$/, '')
|
return (num / 1_000_000).toFixed(2) + 'M'
|
||||||
return str + suffix
|
} else if (num >= 1_000) {
|
||||||
|
return (num / 1_000).toFixed(2) + 'k'
|
||||||
}
|
}
|
||||||
|
return num.toString()
|
||||||
const absNum = Math.abs(num)
|
|
||||||
let result: string
|
|
||||||
|
|
||||||
if (absNum >= 1_000_000) {
|
|
||||||
result = formatNumber(absNum / 1_000_000, 'M')
|
|
||||||
} else if (absNum >= 1_000) {
|
|
||||||
result = formatNumber(absNum / 1_000, 'k')
|
|
||||||
} else {
|
|
||||||
result = absNum.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return num < 0 ? '-' + result : result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatDate = (dateString: any) => {
|
export const formatDate = (dateString: any) => {
|
||||||
|
|||||||
@ -133,22 +133,11 @@ 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))
|
router.replace(getLocalizedUrl(redirectURL, locale as Locale))
|
||||||
} else {
|
|
||||||
const redirectURL = searchParams.get('redirectTo') ?? '/sa/organizations/list'
|
|
||||||
|
|
||||||
router.replace(getLocalizedUrl(redirectURL, locale as Locale))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
|
||||||
setErrorState(error.response.data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -254,11 +243,7 @@ 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
|
<Typography component={Link} href={getLocalizedUrl('/register', locale as Locale)} color='primary.main'>
|
||||||
component={Link}
|
|
||||||
href={getLocalizedUrl('/organization', locale as Locale)}
|
|
||||||
color='primary.main'
|
|
||||||
>
|
|
||||||
Create an account
|
Create an account
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,7 +17,6 @@ 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')({
|
||||||
@ -30,8 +29,6 @@ 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'
|
||||||
@ -51,11 +48,7 @@ const NotFound = ({ mode }: { mode: SystemMode }) => {
|
|||||||
<Typography variant='h4'>Page Not Found ⚠️</Typography>
|
<Typography variant='h4'>Page Not Found ⚠️</Typography>
|
||||||
<Typography>we couldn't find the page you are looking for.</Typography>
|
<Typography>we couldn't find the page you are looking for.</Typography>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button href='/' component={Link} variant='contained'>
|
||||||
href={currentUser?.role === 'admin' ? '/' : '/sa/organizations/list'}
|
|
||||||
component={Link}
|
|
||||||
variant='contained'
|
|
||||||
>
|
|
||||||
Back To Home
|
Back To Home
|
||||||
</Button>
|
</Button>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@ -221,6 +221,7 @@ const CustomerListTable = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ text: 'Duplicate', icon: 'tabler-copy' }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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 ?? 0} {data?.payments?.length === 1 ? 'Payment' : 'Payments'})
|
Payment Details ({data.payments.length} {data.payments.length === 1 ? 'Payment' : 'Payments'})
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{data?.payments?.length ? data.payments.map((payment, index) => (
|
{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,9 +74,7 @@ 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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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-semibold min-is-[100px]'>
|
<Typography color='text.primary' className='font-medium min-is-[100px]'>
|
||||||
Total:
|
Total:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color='text.primary' className='font-semibold'>
|
<Typography color='text.primary' className='font-medium'>
|
||||||
{formatCurrency(data.total_amount)}
|
{formatCurrency(data.total_amount)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -39,6 +39,9 @@ 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 }}>
|
||||||
@ -46,6 +49,9 @@ 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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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' />}
|
||||||
|
|||||||
@ -126,27 +126,13 @@ 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, description } = useSelector((state: RootState) => state.productReducer.productRequest)
|
const { name, sku, barcode, 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(
|
dispatch(setProduct(product))
|
||||||
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])
|
||||||
|
|
||||||
@ -166,11 +152,9 @@ const ProductInformation = () => {
|
|||||||
Underline
|
Underline
|
||||||
],
|
],
|
||||||
immediatelyRender: false,
|
immediatelyRender: false,
|
||||||
content: params?.id
|
content: `
|
||||||
? description
|
|
||||||
: `
|
|
||||||
<p>
|
<p>
|
||||||
${description}
|
${description || ''}
|
||||||
</p>
|
</p>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
@ -197,7 +181,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, sm: 6 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='Product Name'
|
label='Product Name'
|
||||||
@ -215,6 +199,15 @@ 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'>
|
||||||
|
|||||||
@ -16,22 +16,12 @@ 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 [categoryInput, setCategoryInput] = useState('')
|
const { data: categoriesApi } = useCategories()
|
||||||
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 }))
|
||||||
@ -43,36 +33,25 @@ 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'>
|
||||||
<Autocomplete
|
|
||||||
options={categoryOptions}
|
|
||||||
loading={categoriesLoading}
|
|
||||||
fullWidth
|
|
||||||
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
|
<CustomTextField
|
||||||
{...params}
|
select
|
||||||
className=''
|
|
||||||
label='Category'
|
|
||||||
fullWidth
|
fullWidth
|
||||||
InputProps={{
|
label='Category'
|
||||||
...params.InputProps,
|
value={category_id}
|
||||||
endAdornment: (
|
onChange={e => handleSelectChange('category_id', e.target.value)}
|
||||||
<>
|
>
|
||||||
{categoriesLoading && <CircularProgress size={18} />}
|
{categoriesApi?.categories.length ? (
|
||||||
{params.InputProps.endAdornment}
|
categoriesApi?.categories.map((item: Category, index: number) => (
|
||||||
</>
|
<MenuItem key={index} value={item.id}>
|
||||||
)
|
{item.name}
|
||||||
}}
|
</MenuItem>
|
||||||
/>
|
))
|
||||||
|
) : (
|
||||||
|
<MenuItem disabled value=''>
|
||||||
|
Loading categories...
|
||||||
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
/>
|
</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>
|
||||||
@ -86,6 +65,12 @@ 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>
|
||||||
|
|||||||
@ -1,233 +0,0 @@
|
|||||||
// React Imports
|
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
|
||||||
|
|
||||||
// MUI Imports
|
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
import Divider from '@mui/material/Divider'
|
|
||||||
import Drawer from '@mui/material/Drawer'
|
|
||||||
import IconButton from '@mui/material/IconButton'
|
|
||||||
import Typography from '@mui/material/Typography'
|
|
||||||
|
|
||||||
// Third-party Imports
|
|
||||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
|
||||||
|
|
||||||
// Type Imports
|
|
||||||
|
|
||||||
// Component Imports
|
|
||||||
import CustomTextField from '@core/components/mui/TextField'
|
|
||||||
import { Autocomplete } from '@mui/material'
|
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
|
||||||
import { useDebounce } from 'use-debounce'
|
|
||||||
import { RootState } from '../../../../../redux-store'
|
|
||||||
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
|
|
||||||
handleClose: () => void
|
|
||||||
product: Product
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vars
|
|
||||||
const initialData = {
|
|
||||||
outlet_id: '',
|
|
||||||
product_id: '',
|
|
||||||
variant_id: '',
|
|
||||||
ingredient_id: '',
|
|
||||||
quantity: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddRecipeDrawer = (props: Props) => {
|
|
||||||
const dispatch = useDispatch()
|
|
||||||
|
|
||||||
// Props
|
|
||||||
const { open, handleClose, product } = props
|
|
||||||
|
|
||||||
const { currentVariant, currentProductRecipe } = useSelector((state: RootState) => state.productRecipeReducer)
|
|
||||||
|
|
||||||
const [outletInput, setOutletInput] = useState('')
|
|
||||||
const [outletDebouncedInput] = useDebounce(outletInput, 500)
|
|
||||||
const [ingredientInput, setIngredientInput] = useState('')
|
|
||||||
const [ingredientDebouncedInput] = useDebounce(ingredientInput, 500)
|
|
||||||
const [formData, setFormData] = useState<ProductRecipeRequest>(initialData)
|
|
||||||
|
|
||||||
const { data: outlets, isLoading: outletsLoading } = useOutlets({
|
|
||||||
search: outletDebouncedInput
|
|
||||||
})
|
|
||||||
const { data: ingredients, isLoading: ingredientsLoading } = useIngredients({
|
|
||||||
search: ingredientDebouncedInput
|
|
||||||
})
|
|
||||||
|
|
||||||
const outletOptions = useMemo(() => outlets?.outlets || [], [outlets])
|
|
||||||
const ingredientOptions = useMemo(() => ingredients?.data || [], [ingredients])
|
|
||||||
|
|
||||||
const { createProductRecipe, updateProductRecipe } = useProductRecipesMutation()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentProductRecipe.id) {
|
|
||||||
setFormData(currentProductRecipe)
|
|
||||||
}
|
|
||||||
}, [currentProductRecipe])
|
|
||||||
|
|
||||||
const handleSubmit = (e: any) => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
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(resetProductVariant())
|
|
||||||
setFormData(initialData)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInputChange = (e: any) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
[e.target.name]: e.target.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const setTitleDrawer = (recipe: any) => {
|
|
||||||
const addOrEdit = currentProductRecipe.id ? 'Edit ' : 'Add '
|
|
||||||
|
|
||||||
let title = 'Original'
|
|
||||||
|
|
||||||
if (recipe?.name) {
|
|
||||||
title = recipe?.name
|
|
||||||
}
|
|
||||||
|
|
||||||
return addOrEdit + title
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
open={open}
|
|
||||||
anchor='right'
|
|
||||||
variant='temporary'
|
|
||||||
onClose={handleReset}
|
|
||||||
ModalProps={{ keepMounted: true }}
|
|
||||||
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
|
|
||||||
>
|
|
||||||
<div className='flex items-center justify-between pli-6 plb-5'>
|
|
||||||
<Typography variant='h5'>{setTitleDrawer(currentVariant)} Variant Ingredient</Typography>
|
|
||||||
<IconButton size='small' onClick={handleReset}>
|
|
||||||
<i className='tabler-x text-2xl' />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
<Divider />
|
|
||||||
<PerfectScrollbar options={{ wheelPropagation: false, suppressScrollX: true }}>
|
|
||||||
<div className='p-6'>
|
|
||||||
<form onSubmit={handleSubmit} className='flex flex-col gap-5'>
|
|
||||||
<Typography color='text.primary' className='font-medium'>
|
|
||||||
Basic Information
|
|
||||||
</Typography>
|
|
||||||
<Autocomplete
|
|
||||||
options={outletOptions}
|
|
||||||
loading={outletsLoading}
|
|
||||||
getOptionLabel={option => option.name}
|
|
||||||
value={outletOptions.find(p => p.id === formData.outlet_id) || null}
|
|
||||||
onInputChange={(event, newOutlettInput) => {
|
|
||||||
setOutletInput(newOutlettInput)
|
|
||||||
}}
|
|
||||||
onChange={(event, newValue) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
outlet_id: newValue?.id || ''
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
renderInput={params => (
|
|
||||||
<CustomTextField
|
|
||||||
{...params}
|
|
||||||
className=''
|
|
||||||
label='Outlet'
|
|
||||||
fullWidth
|
|
||||||
InputProps={{
|
|
||||||
...params.InputProps,
|
|
||||||
endAdornment: <>{params.InputProps.endAdornment}</>
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Autocomplete
|
|
||||||
options={ingredientOptions || []}
|
|
||||||
loading={ingredientsLoading}
|
|
||||||
getOptionLabel={option => option.name}
|
|
||||||
value={ingredientOptions?.find(p => p.id === formData.ingredient_id) || null}
|
|
||||||
onInputChange={(event, newIngredientInput) => {
|
|
||||||
setIngredientInput(newIngredientInput)
|
|
||||||
}}
|
|
||||||
onChange={(event, newValue) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
ingredient_id: newValue?.id || ''
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
renderInput={params => (
|
|
||||||
<CustomTextField
|
|
||||||
{...params}
|
|
||||||
className=''
|
|
||||||
label='Ingredient'
|
|
||||||
fullWidth
|
|
||||||
InputProps={{
|
|
||||||
...params.InputProps,
|
|
||||||
endAdornment: <>{params.InputProps.endAdornment}</>
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<CustomTextField
|
|
||||||
type='number'
|
|
||||||
label='Quantity'
|
|
||||||
fullWidth
|
|
||||||
value={formData.quantity}
|
|
||||||
onChange={e => setFormData({ ...formData, quantity: Number(e.target.value) })}
|
|
||||||
/>
|
|
||||||
<div className='flex items-center gap-4'>
|
|
||||||
<Button
|
|
||||||
variant='contained'
|
|
||||||
type='submit'
|
|
||||||
disabled={createProductRecipe.isPending || updateProductRecipe.isPending}
|
|
||||||
>
|
|
||||||
{currentProductRecipe.id
|
|
||||||
? updateProductRecipe.isPending
|
|
||||||
? 'Updating...'
|
|
||||||
: 'Update'
|
|
||||||
: createProductRecipe.isPending
|
|
||||||
? 'Creating...'
|
|
||||||
: 'Create'}
|
|
||||||
</Button>
|
|
||||||
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
|
|
||||||
Discard
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</PerfectScrollbar>
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AddRecipeDrawer
|
|
||||||
@ -2,482 +2,319 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Badge,
|
||||||
Button,
|
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardHeader,
|
CardMedia,
|
||||||
Chip,
|
Chip,
|
||||||
|
Divider,
|
||||||
Grid,
|
Grid,
|
||||||
Paper,
|
List,
|
||||||
Table,
|
ListItem,
|
||||||
TableBody,
|
ListItemIcon,
|
||||||
TableCell,
|
ListItemText,
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
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 React, { useEffect } 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 { useProductRecipesByProduct } from '../../../../../services/queries/productRecipes'
|
import { setProduct } from '../../../../../redux-store/slices/product'
|
||||||
import { useProductById } from '../../../../../services/queries/products'
|
import { useProductById } from '../../../../../services/queries/products'
|
||||||
import { ProductVariant } from '../../../../../types/services/product'
|
import { ProductVariant } from '../../../../../types/services/product'
|
||||||
import { formatCurrency } from '../../../../../utils/transform'
|
import { formatCurrency, formatDate } from '../../../../../utils/transform'
|
||||||
import AddRecipeDrawer from './AddRecipeDrawer'
|
// Tabler icons (using class names)
|
||||||
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
|
const TablerIcon = ({ name, className = '' }: { name: string; className?: string }) => (
|
||||||
import { useProductRecipesMutation } from '../../../../../services/mutations/productRecipes'
|
<i className={`tabler-${name} ${className}`} />
|
||||||
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 [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 { deleteProductRecipe } = useProductRecipesMutation()
|
useEffect(() => {
|
||||||
|
if (product) {
|
||||||
|
dispatch(setProduct(product))
|
||||||
|
}
|
||||||
|
}, [product, dispatch])
|
||||||
|
|
||||||
const handleOpenProductRecipe = (variant: any) => {
|
const getBusinessTypeColor = (type: string) => {
|
||||||
setOpenProductRecipe(true)
|
switch (type.toLowerCase()) {
|
||||||
dispatch(setProductVariant(variant))
|
case 'restaurant':
|
||||||
|
return 'primary'
|
||||||
|
case 'retail':
|
||||||
|
return 'secondary'
|
||||||
|
case 'cafe':
|
||||||
|
return 'info'
|
||||||
|
default:
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenEditProductRecipe = (recipe: ProductRecipe) => {
|
const getPrinterTypeColor = (type: string) => {
|
||||||
setOpenProductRecipe(true)
|
switch (type.toLowerCase()) {
|
||||||
dispatch(setProductRecipe(recipe))
|
case 'kitchen':
|
||||||
|
return 'warning'
|
||||||
|
case 'bar':
|
||||||
|
return 'info'
|
||||||
|
case 'receipt':
|
||||||
|
return 'success'
|
||||||
|
default:
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteRecipe = () => {
|
const getPlainText = (html: string) => {
|
||||||
deleteProductRecipe.mutate(productRecipeId, {
|
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||||
onSuccess: () => {
|
return doc.body.textContent || ''
|
||||||
setOpenConfirm(false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading || isLoadingProductRecipe) return <Loading />
|
if (isLoading) return <Loading />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='max-w-6xl mx-auto p-4 space-y-6'>
|
||||||
<div className='space-y-6'>
|
|
||||||
{/* Header Card */}
|
{/* Header Card */}
|
||||||
<Card>
|
<Card className='shadow-lg'>
|
||||||
<CardHeader
|
<Grid container>
|
||||||
avatar={<Avatar src={product?.image_url || ''} alt={product?.name} className='w-16 h-16' />}
|
<Grid item xs={12} md={4}>
|
||||||
title={
|
<CardMedia
|
||||||
<div className='flex items-center gap-3'>
|
component='img'
|
||||||
<Typography variant='h4' component='h1' className='font-bold'>
|
sx={{ height: 300, objectFit: 'cover' }}
|
||||||
{product?.name}
|
image={product.image_url || '/placeholder-image.jpg'}
|
||||||
|
alt={product.name}
|
||||||
|
className='rounded-l-lg'
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={8}>
|
||||||
|
<CardContent className='h-full flex flex-col justify-between'>
|
||||||
|
<div>
|
||||||
|
<div className='flex items-start justify-between mb-3'>
|
||||||
|
<div>
|
||||||
|
<Typography variant='h4' component='h1' className='font-bold text-gray-800 mb-2'>
|
||||||
|
{product.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<div className='flex items-center gap-2 mb-3'>
|
||||||
<Chip
|
<Chip
|
||||||
label={product?.is_active ? 'Active' : 'Inactive'}
|
icon={<TablerIcon name='barcode' className='text-sm' />}
|
||||||
color={product?.is_active ? 'success' : 'error'}
|
label={product.sku}
|
||||||
|
size='small'
|
||||||
|
variant='outlined'
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
icon={<TablerIcon name={product.is_active ? 'check-circle' : 'x-circle'} className='text-sm' />}
|
||||||
|
label={product.is_active ? 'Active' : 'Inactive'}
|
||||||
|
color={product.is_active ? 'success' : 'error'}
|
||||||
size='small'
|
size='small'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
subheader={
|
</div>
|
||||||
<div className='flex flex-col gap-1 mt-2'>
|
|
||||||
<Typography variant='body2' color='textSecondary'>
|
{product.description && (
|
||||||
SKU: {product?.sku} • Category: {product?.business_type}
|
<Typography variant='body1' className='text-gray-600 mb-4'>
|
||||||
|
{getPlainText(product.description)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className='flex gap-4'>
|
)}
|
||||||
<Typography variant='body2'>
|
|
||||||
<span className='font-semibold'>Price:</span> {formatCurrency(product?.price || 0)}
|
<div className='grid grid-cols-2 gap-4 mb-4'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<TablerIcon name='currency-dollar' className='text-green-600 text-xl' />
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='text-gray-500'>
|
||||||
|
Price
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body2'>
|
<Typography variant='h6' className='font-semibold text-green-600'>
|
||||||
<span className='font-semibold'>Base Cost:</span> {formatCurrency(product?.cost || 0)}
|
{formatCurrency(product.price)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
<div className='flex items-center gap-2'>
|
||||||
|
<TablerIcon name='receipt' className='text-orange-600 text-xl' />
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='text-gray-500'>
|
||||||
|
Cost
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='h6' className='font-semibold text-orange-600'>
|
||||||
|
{formatCurrency(product.cost)}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<Chip
|
||||||
|
icon={<TablerIcon name='building-store' className='text-sm' />}
|
||||||
|
label={product.business_type}
|
||||||
|
color={getBusinessTypeColor(product.business_type)}
|
||||||
|
size='small'
|
||||||
/>
|
/>
|
||||||
|
<Chip
|
||||||
|
icon={<TablerIcon name='printer' className='text-sm' />}
|
||||||
|
label={product.printer_type}
|
||||||
|
color={getPrinterTypeColor(product.printer_type)}
|
||||||
|
size='small'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* {productRecipe && ( */}
|
<Grid container spacing={3}>
|
||||||
<div className='space-y-6'>
|
{/* Product Information */}
|
||||||
{/* Recipe Details by Variant */}
|
<Grid item xs={12} md={8}>
|
||||||
<div className='space-y-4'>
|
<Card className='shadow-md'>
|
||||||
<div className='flex items-center gap-2 mb-4'>
|
|
||||||
<i className='tabler-chef-hat text-textPrimary text-xl' />
|
|
||||||
<Typography variant='h5' component='h2' className='font-semibold'>
|
|
||||||
Recipe Details
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
title={
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div className='flex items-center gap-3'>
|
|
||||||
<i className='tabler-variant text-blue-600 text-lg' />
|
|
||||||
<Typography variant='h6' className='font-semibold'>
|
|
||||||
Original Variant
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div className='flex gap-4 text-sm'>
|
|
||||||
<Chip label={`Cost: ${formatCurrency(product?.cost || 0)}`} variant='outlined' color='primary' />
|
|
||||||
<Chip
|
|
||||||
label={`Price Modifier: ${formatCurrency(product?.price || 0)}`}
|
|
||||||
variant='outlined'
|
|
||||||
color='secondary'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<TableContainer component={Paper} variant='outlined'>
|
<Typography variant='h6' className='font-semibold mb-4 flex items-center gap-2'>
|
||||||
<Table>
|
<TablerIcon name='info-circle' className='text-blue-600 text-xl' />
|
||||||
<TableHead>
|
Product Information
|
||||||
<TableRow className='bg-gray-50'>
|
</Typography>
|
||||||
<TableCell className='font-semibold'>
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<i className='tabler-ingredients text-green-600' />
|
|
||||||
Ingredient
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='font-semibold text-center'>
|
|
||||||
<div className='flex items-center justify-center gap-2'>
|
|
||||||
<i className='tabler-scale text-orange-600' />
|
|
||||||
Quantity
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='font-semibold text-center'>
|
|
||||||
<div className='flex items-center justify-center gap-2'>
|
|
||||||
<i className='tabler-currency-dollar text-purple-600' />
|
|
||||||
Unit Cost
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='font-semibold text-center'>
|
|
||||||
<div className='flex items-center justify-center gap-2'>
|
|
||||||
<i className='tabler-package text-blue-600' />
|
|
||||||
Stock Available
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='font-semibold text-right'>
|
|
||||||
<div className='flex items-center justify-end gap-2'>
|
|
||||||
<i className='tabler-calculator text-red-600' />
|
|
||||||
Total Cost
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell></TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{productRecipe?.length ? (
|
|
||||||
productRecipe
|
|
||||||
.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'>
|
|
||||||
<div className='w-2 h-2 rounded-full bg-green-500' />
|
|
||||||
<div>
|
<div>
|
||||||
<Typography variant='body2' className='font-medium capitalize'>
|
<Typography variant='body2' className='text-gray-500 mb-1'>
|
||||||
{item.ingredient.name}
|
Product ID
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='caption' color='textSecondary'>
|
<Typography variant='body1' className='font-mono text-sm bg-gray-100 p-2 rounded'>
|
||||||
{item.ingredient.is_semi_finished ? 'Semi-finished' : 'Raw ingredient'}
|
{product.id}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='text-gray-500 mb-1'>
|
||||||
|
Category ID
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' className='font-mono text-sm bg-gray-100 p-2 rounded'>
|
||||||
|
{product.category_id}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='text-gray-500 mb-1'>
|
||||||
|
Organization ID
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' className='font-mono text-sm bg-gray-100 p-2 rounded'>
|
||||||
|
{product.organization_id}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='text-gray-500 mb-1'>
|
||||||
|
Profit Margin
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' className='font-semibold text-green-600'>
|
||||||
|
{formatCurrency(product.price - product.cost)}
|
||||||
|
<span className='text-sm text-gray-500 ml-1'>
|
||||||
|
({(((product.price - product.cost) / product.cost) * 100).toFixed(1)}%)
|
||||||
|
</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-center'>
|
|
||||||
<Chip label={item.quantity} size='small' variant='outlined' color='primary' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-center'>{formatCurrency(item.ingredient.cost)}</TableCell>
|
|
||||||
<TableCell className='text-center'>
|
|
||||||
<Chip
|
|
||||||
label={item.ingredient.stock}
|
|
||||||
size='small'
|
|
||||||
color={item.ingredient.stock > 5 ? 'success' : 'warning'}
|
|
||||||
variant='outlined'
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<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 ? (
|
|
||||||
<Box className='mt-4 p-4 bg-blue-50 rounded-lg'>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Typography variant='body2' className='flex items-center gap-2'>
|
|
||||||
<i className='tabler-list-numbers text-blue-600' />
|
|
||||||
<span className='font-semibold'>Total Ingredients:</span>
|
|
||||||
{productRecipe.filter((item: any) => item.variant_id === null).length}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Typography variant='body2' className='flex items-center gap-2'>
|
|
||||||
<i className='tabler-sum text-green-600' />
|
|
||||||
<span className='font-semibold'>Total Recipe Cost:</span>
|
|
||||||
{formatCurrency(
|
|
||||||
productRecipe
|
|
||||||
.filter((item: any) => item.variant_id === null)
|
|
||||||
.reduce((sum: any, item: any) => sum + item.ingredient.cost * item.quantity, 0)
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
</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
|
|
||||||
variant='outlined'
|
|
||||||
fullWidth
|
|
||||||
className='mt-4'
|
|
||||||
startIcon={<i className='tabler-plus' />}
|
|
||||||
onClick={() => handleOpenProductRecipe({ variant: undefined })}
|
|
||||||
>
|
|
||||||
Add Ingredient
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{product?.variants?.length &&
|
{/* Variants Section */}
|
||||||
product.variants.map((variantData: ProductVariant, index: number) => (
|
{product.variants && product.variants.length > 0 && (
|
||||||
<Card key={index}>
|
<Card className='shadow-md mt-4'>
|
||||||
<CardHeader
|
<CardContent>
|
||||||
title={
|
<Typography variant='h6' className='font-semibold mb-4 flex items-center gap-2'>
|
||||||
<div className='flex items-center justify-between'>
|
<TablerIcon name='versions' className='text-purple-600 text-xl' />
|
||||||
<div className='flex items-center gap-3'>
|
Product Variants
|
||||||
<i className='tabler-variant text-blue-600 text-lg' />
|
<Badge badgeContent={product.variants.length} color='primary' />
|
||||||
<Typography variant='h6' className='font-semibold'>
|
</Typography>
|
||||||
{variantData?.name || 'Original'} Variant
|
<List>
|
||||||
|
{product.variants.map((variant: ProductVariant, index: number) => (
|
||||||
|
<React.Fragment key={variant.id}>
|
||||||
|
<ListItem className='px-0'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Avatar className='bg-purple-100 text-purple-600 w-8 h-8 text-sm'>
|
||||||
|
{variant.name.charAt(0)}
|
||||||
|
</Avatar>
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<Typography variant='subtitle1' className='font-medium'>
|
||||||
|
{variant.name}
|
||||||
|
</Typography>
|
||||||
|
<div className='flex gap-3'>
|
||||||
|
<Typography variant='body2' className='text-green-600 font-semibold'>
|
||||||
|
+{formatCurrency(variant.price_modifier)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' className='text-orange-600'>
|
||||||
|
Cost: {formatCurrency(variant.cost)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
|
||||||
<div className='flex gap-4 text-sm'>
|
|
||||||
<Chip
|
|
||||||
label={`Cost: ${formatCurrency(variantData?.cost || 0)}`}
|
|
||||||
variant='outlined'
|
|
||||||
color='primary'
|
|
||||||
/>
|
|
||||||
<Chip
|
|
||||||
label={`Price Modifier: ${formatCurrency(variantData?.price_modifier || 0)}`}
|
|
||||||
variant='outlined'
|
|
||||||
color='secondary'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
secondary={
|
||||||
|
<Typography variant='caption' className='text-gray-500'>
|
||||||
|
Total Price: {formatCurrency(product.price + variant.price_modifier)}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
</ListItem>
|
||||||
<TableContainer component={Paper} variant='outlined'>
|
{index < product.variants.length - 1 && <Divider />}
|
||||||
<Table>
|
</React.Fragment>
|
||||||
<TableHead>
|
))}
|
||||||
<TableRow className='bg-gray-50'>
|
</List>
|
||||||
<TableCell className='font-semibold'>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<i className='tabler-ingredients text-green-600' />
|
|
||||||
Ingredient
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='font-semibold text-center'>
|
|
||||||
<div className='flex items-center justify-center gap-2'>
|
|
||||||
<i className='tabler-scale text-orange-600' />
|
|
||||||
Quantity
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='font-semibold text-center'>
|
|
||||||
<div className='flex items-center justify-center gap-2'>
|
|
||||||
<i className='tabler-currency-dollar text-purple-600' />
|
|
||||||
Unit Cost
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='font-semibold text-center'>
|
|
||||||
<div className='flex items-center justify-center gap-2'>
|
|
||||||
<i className='tabler-package text-blue-600' />
|
|
||||||
Stock Available
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='font-semibold text-right'>
|
|
||||||
<div className='flex items-center justify-end gap-2'>
|
|
||||||
<i className='tabler-calculator text-red-600' />
|
|
||||||
Total Cost
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{productRecipe?.length ? (
|
|
||||||
productRecipe
|
|
||||||
.filter((item: any) => item.variant_id === variantData.id)
|
|
||||||
.map((item: any, index: number) => (
|
|
||||||
<TableRow key={index} className='hover:bg-gray-50'>
|
|
||||||
<TableCell>
|
|
||||||
<div className='flex items-center gap-3'>
|
|
||||||
<div className='w-2 h-2 rounded-full bg-green-500' />
|
|
||||||
<div>
|
|
||||||
<Typography variant='body2' className='font-medium capitalize'>
|
|
||||||
{item.ingredient.name}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='caption' color='textSecondary'>
|
|
||||||
{item.ingredient.is_semi_finished ? 'Semi-finished' : 'Raw ingredient'}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-center'>
|
|
||||||
<Chip label={item.quantity} size='small' variant='outlined' color='primary' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-center'>{formatCurrency(item.ingredient.cost)}</TableCell>
|
|
||||||
<TableCell className='text-center'>
|
|
||||||
<Chip
|
|
||||||
label={item.ingredient.stock}
|
|
||||||
size='small'
|
|
||||||
color={item.ingredient.stock > 5 ? 'success' : 'warning'}
|
|
||||||
variant='outlined'
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<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 ? (
|
|
||||||
<Box className='mt-4 p-4 bg-blue-50 rounded-lg'>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Typography variant='body2' className='flex items-center gap-2'>
|
|
||||||
<i className='tabler-list-numbers text-blue-600' />
|
|
||||||
<span className='font-semibold'>Total Ingredients:</span>
|
|
||||||
{productRecipe.filter((item: any) => item.variant_id === variantData.id).length}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Typography variant='body2' className='flex items-center gap-2'>
|
|
||||||
<i className='tabler-sum text-green-600' />
|
|
||||||
<span className='font-semibold'>Total Recipe Cost:</span>
|
|
||||||
{formatCurrency(
|
|
||||||
productRecipe
|
|
||||||
.filter((item: any) => item.variant_id === variantData.id)
|
|
||||||
.reduce((sum: any, item: any) => sum + item.ingredient.cost * item.quantity, 0)
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
</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
|
|
||||||
variant='outlined'
|
|
||||||
fullWidth
|
|
||||||
className='mt-4'
|
|
||||||
startIcon={<i className='tabler-plus' />}
|
|
||||||
onClick={() => handleOpenProductRecipe(variantData)}
|
|
||||||
>
|
|
||||||
Add Ingredient
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Metadata & Timestamps */}
|
||||||
|
<Grid item xs={12} md={4}>
|
||||||
|
<Card className='shadow-md'>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant='h6' className='font-semibold mb-4 flex items-center gap-2'>
|
||||||
|
<TablerIcon name='clock' className='text-indigo-600 text-xl' />
|
||||||
|
Timestamps
|
||||||
|
</Typography>
|
||||||
|
<div className='space-y-3'>
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='text-gray-500 mb-1'>
|
||||||
|
Created
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' className='text-sm'>
|
||||||
|
{formatDate(product.created_at)}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<div>
|
||||||
|
<Typography variant='body2' className='text-gray-500 mb-1'>
|
||||||
|
Last Updated
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' className='text-sm'>
|
||||||
|
{formatDate(product.updated_at)}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{Object.keys(product.metadata).length > 0 && (
|
||||||
|
<>
|
||||||
|
<Divider className='my-4' />
|
||||||
|
<Typography variant='h6' className='font-semibold mb-3'>
|
||||||
|
Metadata
|
||||||
|
</Typography>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
{Object.entries(product.metadata).map(([key, value]) => (
|
||||||
|
<div key={key}>
|
||||||
|
<Typography variant='body2' className='text-gray-500 mb-1 capitalize'>
|
||||||
|
{key.replace(/_/g, ' ')}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' className='text-sm bg-gray-50 p-2 rounded'>
|
||||||
|
{typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</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.'
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 { ProductsQueryParams, useProducts } from '../../../../../services/queries/products'
|
import { useProducts } from '../../../../../services/queries/products'
|
||||||
import { Product } from '../../../../../types/services/product'
|
import { Product } from '../../../../../types/services/product'
|
||||||
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
|
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
|
||||||
import { useProductsMutation } from '../../../../../services/mutations/products'
|
import { useProductsMutation } from '../../../../../services/mutations/products'
|
||||||
@ -115,11 +115,6 @@ 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()
|
||||||
|
|
||||||
@ -127,8 +122,7 @@ 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
|
||||||
@ -282,7 +276,7 @@ const ProductListTable = () => {
|
|||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader title='Filters' />
|
<CardHeader title='Filters' />
|
||||||
<TableFilters filter={filter} setFilter={setFilter} />
|
<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
|
||||||
|
|||||||
@ -1,45 +1,47 @@
|
|||||||
// React Imports
|
// React Imports
|
||||||
import { useMemo, useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
// MUI Imports
|
// MUI Imports
|
||||||
import CardContent from '@mui/material/CardContent'
|
|
||||||
import Grid from '@mui/material/Grid2'
|
import Grid from '@mui/material/Grid2'
|
||||||
|
import CardContent from '@mui/material/CardContent'
|
||||||
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 }
|
||||||
|
|
||||||
const TableFilters = ({
|
// Vars
|
||||||
filter,
|
const productStockObj: ProductStockType = {
|
||||||
setFilter
|
'In Stock': true,
|
||||||
}: {
|
'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']>('')
|
||||||
|
|
||||||
const [categoryInput, setCategoryInput] = useState('')
|
useEffect(
|
||||||
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
|
||||||
|
|
||||||
const { data: categoriesApi, isLoading: categoriesLoading } = useCategories({
|
return true
|
||||||
search: categoryDebouncedInput
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const categoryOptions = useMemo(() => categoriesApi?.categories || [], [categoriesApi])
|
setData(filteredData ?? [])
|
||||||
|
},
|
||||||
const handleStatusChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
setFilter({ ...filter, is_active: e.target.value === 'Active' ? true : e.target.value === 'Inactive' ? false : '' })
|
[category, stock, status, productData]
|
||||||
}
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@ -49,47 +51,37 @@ const TableFilters = ({
|
|||||||
select
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
id='select-status'
|
id='select-status'
|
||||||
value={filter.is_active ? 'Active' : filter.is_active === false ? 'Inactive' : ''}
|
value={status}
|
||||||
onChange={handleStatusChange}
|
onChange={e => setStatus(e.target.value)}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
select: { displayEmpty: true }
|
select: { displayEmpty: true }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value=''>Select Status</MenuItem>
|
<MenuItem value=''>Select Status</MenuItem>
|
||||||
<MenuItem value='Active'>Active</MenuItem>
|
<MenuItem value='Scheduled'>Scheduled</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 }}>
|
||||||
<Autocomplete
|
|
||||||
options={categoryOptions}
|
|
||||||
loading={categoriesLoading}
|
|
||||||
fullWidth
|
|
||||||
getOptionLabel={option => option.name}
|
|
||||||
value={categoryOptions.find(p => p.id === filter.category_id) || null}
|
|
||||||
onInputChange={(event, newCategoryInput) => {
|
|
||||||
setCategoryInput(newCategoryInput)
|
|
||||||
}}
|
|
||||||
onChange={(event, newValue) => {
|
|
||||||
setFilter({ ...filter, category_id: newValue?.id || '' })
|
|
||||||
}}
|
|
||||||
renderInput={params => (
|
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...params}
|
select
|
||||||
fullWidth
|
fullWidth
|
||||||
placeholder='Search Category'
|
id='select-category'
|
||||||
InputProps={{
|
value={category}
|
||||||
...params.InputProps,
|
onChange={e => setCategory(e.target.value)}
|
||||||
endAdornment: (
|
slotProps={{
|
||||||
<>
|
select: { displayEmpty: true }
|
||||||
{categoriesLoading && <CircularProgress size={18} />}
|
|
||||||
{params.InputProps.endAdornment}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
)}
|
<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>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, sm: 4 }}>
|
<Grid size={{ xs: 12, sm: 4 }}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
|
|||||||
@ -103,13 +103,11 @@ 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 ?? []
|
||||||
@ -152,7 +150,7 @@ const StockListTable = () => {
|
|||||||
},
|
},
|
||||||
columnHelper.accessor('product_id', {
|
columnHelper.accessor('product_id', {
|
||||||
header: 'Product',
|
header: 'Product',
|
||||||
cell: ({ row }) => <Typography>{row.original.product?.name}</Typography>
|
cell: ({ row }) => <Typography>{row.original.product_id}</Typography>
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('quantity', {
|
columnHelper.accessor('quantity', {
|
||||||
header: 'Quantity',
|
header: 'Quantity',
|
||||||
@ -173,6 +171,32 @@ 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
|
||||||
[]
|
[]
|
||||||
@ -202,13 +226,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 => setSearch(String(value))}
|
onChange={value => console.log(value)}
|
||||||
placeholder='Search Product'
|
placeholder='Search Product'
|
||||||
className='max-sm:is-full'
|
className='max-sm:is-full'
|
||||||
/>
|
/>
|
||||||
@ -237,7 +261,7 @@ const StockListTable = () => {
|
|||||||
onClick={() => setAddInventoryOpen(!addInventoryOpen)}
|
onClick={() => setAddInventoryOpen(!addInventoryOpen)}
|
||||||
startIcon={<i className='tabler-plus' />}
|
startIcon={<i className='tabler-plus' />}
|
||||||
>
|
>
|
||||||
Adjust Stock
|
Adjust Inventory
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -107,13 +107,11 @@ 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
|
||||||
@ -164,7 +162,7 @@ const StockListTable = () => {
|
|||||||
},
|
},
|
||||||
columnHelper.accessor('product_id', {
|
columnHelper.accessor('product_id', {
|
||||||
header: 'Product',
|
header: 'Product',
|
||||||
cell: ({ row }) => <Typography>{row.original.product?.name}</Typography>
|
cell: ({ row }) => <Typography>{row.original.product_id}</Typography>
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('is_low_stock', {
|
columnHelper.accessor('is_low_stock', {
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
@ -243,8 +241,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 => setSearch(value as string)}
|
onChange={value => console.log(value)}
|
||||||
placeholder='Search Product'
|
placeholder='Search Product'
|
||||||
className='max-sm:is-full'
|
className='max-sm:is-full'
|
||||||
/>
|
/>
|
||||||
@ -273,7 +271,7 @@ const StockListTable = () => {
|
|||||||
onClick={() => setAddInventoryOpen(!addInventoryOpen)}
|
onClick={() => setAddInventoryOpen(!addInventoryOpen)}
|
||||||
startIcon={<i className='tabler-plus' />}
|
startIcon={<i className='tabler-plus' />}
|
||||||
>
|
>
|
||||||
Add Stock
|
Add Inventory
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// React Imports
|
// React Imports
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
// MUI Imports
|
// MUI Imports
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
@ -10,19 +10,15 @@ import MenuItem from '@mui/material/MenuItem'
|
|||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
|
|
||||||
// Third-party Imports
|
// Third-party Imports
|
||||||
|
import { Controller, useForm } from 'react-hook-form'
|
||||||
|
|
||||||
// Types Imports
|
// Types Imports
|
||||||
|
import type { UsersType } from '@/types/apps/userTypes'
|
||||||
|
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import CustomTextField from '@core/components/mui/TextField'
|
import CustomTextField from '@core/components/mui/TextField'
|
||||||
import { Autocomplete, Checkbox, FormControl, FormControlLabel, FormGroup, FormLabel, Switch } from '@mui/material'
|
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
|
||||||
import { useDebounce } from 'use-debounce'
|
|
||||||
import { RootState } from '../../../../redux-store'
|
|
||||||
import { resetUser } from '../../../../redux-store/slices/user'
|
|
||||||
import { useUsersMutation } from '../../../../services/mutations/users'
|
|
||||||
import { useOutlets } from '../../../../services/queries/outlets'
|
|
||||||
import { UserRequest } from '../../../../types/services/user'
|
import { UserRequest } from '../../../../types/services/user'
|
||||||
|
import { Switch } from '@mui/material'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
@ -35,73 +31,26 @@ const initialData = {
|
|||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
role: '',
|
role: '',
|
||||||
permissions: {
|
permissions: {},
|
||||||
can_create_orders: false,
|
|
||||||
can_void_orders: false
|
|
||||||
},
|
|
||||||
is_active: true,
|
is_active: true,
|
||||||
outlet_id: ''
|
organization_id: '',
|
||||||
|
outlet_id: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddUserDrawer = (props: Props) => {
|
const AddUserDrawer = (props: Props) => {
|
||||||
const dispatch = useDispatch()
|
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const { open, handleClose } = props
|
const { open, handleClose } = props
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const [formData, setFormData] = useState<UserRequest>(initialData)
|
const [formData, setFormData] = useState<UserRequest>(initialData)
|
||||||
const [outletInput, setOutletInput] = useState('')
|
|
||||||
const [outletDebouncedInput] = useDebounce(outletInput, 500)
|
|
||||||
|
|
||||||
const { currentUser } = useSelector((state: RootState) => state.userReducer)
|
const onSubmit = () => {
|
||||||
|
handleClose()
|
||||||
const { createUser, updateUser } = useUsersMutation()
|
setFormData(initialData)
|
||||||
|
|
||||||
const { data: outlets, isLoading: outletsLoading } = useOutlets({
|
|
||||||
search: outletDebouncedInput
|
|
||||||
})
|
|
||||||
|
|
||||||
const outletOptions = useMemo(() => outlets?.outlets || [], [outlets])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentUser.id) {
|
|
||||||
setFormData({
|
|
||||||
name: currentUser.name,
|
|
||||||
email: currentUser.email,
|
|
||||||
role: currentUser.role,
|
|
||||||
password: '',
|
|
||||||
is_active: currentUser.is_active,
|
|
||||||
outlet_id: currentUser.outlet_id,
|
|
||||||
permissions: currentUser.permissions
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [currentUser])
|
|
||||||
|
|
||||||
const handleSubmit = (e: any) => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if (currentUser.id) {
|
|
||||||
updateUser.mutate(
|
|
||||||
{ id: currentUser.id, payload: formData },
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
handleReset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
createUser.mutate(formData, {
|
|
||||||
onSuccess: () => {
|
|
||||||
handleReset()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
handleClose()
|
handleClose()
|
||||||
dispatch(resetUser())
|
|
||||||
setFormData(initialData)
|
setFormData(initialData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,17 +61,6 @@ const AddUserDrawer = (props: Props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCheckBoxChange = (e: any) => {
|
|
||||||
const { name, checked } = e.target
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
permissions: {
|
|
||||||
...prev.permissions,
|
|
||||||
[name]: checked
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
open={open}
|
open={open}
|
||||||
@ -133,14 +71,14 @@ const AddUserDrawer = (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 plb-5 pli-6'>
|
<div className='flex items-center justify-between plb-5 pli-6'>
|
||||||
<Typography variant='h5'>{currentUser.id ? 'Edit' : 'Add'} User</Typography>
|
<Typography variant='h5'>Add New User</Typography>
|
||||||
<IconButton size='small' onClick={handleReset}>
|
<IconButton size='small' onClick={handleReset}>
|
||||||
<i className='tabler-x text-2xl text-textPrimary' />
|
<i className='tabler-x text-2xl text-textPrimary' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={handleSubmit} className='flex flex-col gap-6 p-6'>
|
<form onSubmit={onSubmit} className='flex flex-col gap-6 p-6'>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='Name'
|
label='Name'
|
||||||
@ -158,7 +96,6 @@ const AddUserDrawer = (props: Props) => {
|
|||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
{currentUser.id ? null : (
|
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
type='password'
|
type='password'
|
||||||
@ -168,71 +105,6 @@ const AddUserDrawer = (props: Props) => {
|
|||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<CustomTextField
|
|
||||||
select
|
|
||||||
fullWidth
|
|
||||||
label='Role'
|
|
||||||
placeholder='Select Role'
|
|
||||||
value={formData.role}
|
|
||||||
onChange={e => setFormData({ ...formData, role: e.target.value })}
|
|
||||||
>
|
|
||||||
<MenuItem value={`manager`}>Manager</MenuItem>
|
|
||||||
<MenuItem value={`cashier`}>Cashier</MenuItem>
|
|
||||||
<MenuItem value={`waiter`}>Waiter</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
<Autocomplete
|
|
||||||
options={outletOptions}
|
|
||||||
loading={outletsLoading}
|
|
||||||
getOptionLabel={option => option.name}
|
|
||||||
value={outletOptions.find((p: any) => p.id === formData.outlet_id) || null}
|
|
||||||
onInputChange={(event, newOutlettInput) => {
|
|
||||||
setOutletInput(newOutlettInput)
|
|
||||||
}}
|
|
||||||
onChange={(event, newValue) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
outlet_id: newValue?.id || ''
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
renderInput={params => (
|
|
||||||
<CustomTextField
|
|
||||||
{...params}
|
|
||||||
className=''
|
|
||||||
label='Outlet'
|
|
||||||
fullWidth
|
|
||||||
InputProps={{
|
|
||||||
...params.InputProps,
|
|
||||||
endAdornment: <>{params.InputProps.endAdornment}</>
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormControl component='fieldset' variant='outlined'>
|
|
||||||
<FormLabel component='legend'>Assign permissions</FormLabel>
|
|
||||||
<FormGroup>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={formData.permissions.can_create_orders}
|
|
||||||
onChange={handleCheckBoxChange}
|
|
||||||
name='can_create_orders'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label='Can create orders'
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={formData.permissions.can_void_orders}
|
|
||||||
onChange={handleCheckBoxChange}
|
|
||||||
name='can_void_orders'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label='Can void orders'
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</FormControl>
|
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<div className='flex flex-col items-start gap-1'>
|
<div className='flex flex-col items-start gap-1'>
|
||||||
<Typography color='text.primary' className='font-medium'>
|
<Typography color='text.primary' className='font-medium'>
|
||||||
@ -247,7 +119,7 @@ const AddUserDrawer = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<Button variant='contained' type='submit'>
|
<Button variant='contained' type='submit'>
|
||||||
{createUser.isPending || updateUser.isPending ? 'Saving...' : 'Save'}
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='tonal' color='error' type='reset' onClick={() => handleReset()}>
|
<Button variant='tonal' color='error' type='reset' onClick={() => handleReset()}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@ -47,10 +47,6 @@ import TablePaginationComponent from '../../../../components/TablePaginationComp
|
|||||||
import { useUsers } from '../../../../services/queries/users'
|
import { useUsers } from '../../../../services/queries/users'
|
||||||
import { User } from '../../../../types/services/user'
|
import { User } from '../../../../types/services/user'
|
||||||
import AddUserDrawer from './AddUserDrawer'
|
import AddUserDrawer from './AddUserDrawer'
|
||||||
import { useDispatch } from 'react-redux'
|
|
||||||
import { setUser } from '../../../../redux-store/slices/user'
|
|
||||||
import { useUsersMutation } from '../../../../services/mutations/users'
|
|
||||||
import ConfirmDeleteDialog from '../../../../components/dialogs/confirm-delete'
|
|
||||||
|
|
||||||
declare module '@tanstack/table-core' {
|
declare module '@tanstack/table-core' {
|
||||||
interface FilterFns {
|
interface FilterFns {
|
||||||
@ -127,15 +123,13 @@ const userRoleObj: UserRoleType = {
|
|||||||
const columnHelper = createColumnHelper<UsersTypeWithAction>()
|
const columnHelper = createColumnHelper<UsersTypeWithAction>()
|
||||||
|
|
||||||
const UserListTable = () => {
|
const UserListTable = () => {
|
||||||
const dispatch = useDispatch()
|
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const [addUserOpen, setAddUserOpen] = useState(false)
|
const [addUserOpen, setAddUserOpen] = useState(false)
|
||||||
const [rowSelection, setRowSelection] = useState({})
|
const [rowSelection, setRowSelection] = useState({})
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(10)
|
const [pageSize, setPageSize] = useState(10)
|
||||||
const [openConfirm, setOpenConfirm] = useState(false)
|
const [openConfirm, setOpenConfirm] = useState(false)
|
||||||
const [userId, setUserId] = useState('')
|
const [customerId, setCustomerId] = useState('')
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
@ -147,7 +141,7 @@ const UserListTable = () => {
|
|||||||
search
|
search
|
||||||
})
|
})
|
||||||
|
|
||||||
const { deleteUser } = useUsersMutation()
|
// const { deleteCustomer } = useCustomersMutation()
|
||||||
|
|
||||||
const users = data?.users ?? []
|
const users = data?.users ?? []
|
||||||
const totalCount = data?.pagination.total_count ?? 0
|
const totalCount = data?.pagination.total_count ?? 0
|
||||||
@ -163,11 +157,11 @@ const UserListTable = () => {
|
|||||||
setCurrentPage(1) // Reset to first page
|
setCurrentPage(1) // Reset to first page
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleDelete = () => {
|
// const handleDelete = () => {
|
||||||
deleteUser.mutate(userId, {
|
// deleteCustomer.mutate(customerId, {
|
||||||
onSuccess: () => setOpenConfirm(false)
|
// onSuccess: () => setOpenConfirm(false)
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<UsersTypeWithAction, any>[]>(
|
const columns = useMemo<ColumnDef<UsersTypeWithAction, any>[]>(
|
||||||
() => [
|
() => [
|
||||||
@ -242,27 +236,22 @@ const UserListTable = () => {
|
|||||||
header: 'Action',
|
header: 'Action',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<IconButton
|
<IconButton onClick={() => {}}>
|
||||||
onClick={() => {
|
|
||||||
setUserId(row.original.id)
|
|
||||||
setOpenConfirm(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className='tabler-trash text-textSecondary' />
|
<i className='tabler-trash text-textSecondary' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<OptionMenu
|
<OptionMenu
|
||||||
iconButtonProps={{ size: 'medium' }}
|
iconButtonProps={{ size: 'medium' }}
|
||||||
iconClassName='text-textSecondary'
|
iconClassName='text-textSecondary'
|
||||||
options={[
|
options={[
|
||||||
|
{
|
||||||
|
text: 'Download',
|
||||||
|
icon: 'tabler-download',
|
||||||
|
menuItemProps: { className: 'flex items-center gap-2 text-textSecondary' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Edit',
|
text: 'Edit',
|
||||||
icon: 'tabler-edit',
|
icon: 'tabler-edit',
|
||||||
menuItemProps: {
|
menuItemProps: { className: 'flex items-center gap-2 text-textSecondary' }
|
||||||
onClick: () => {
|
|
||||||
dispatch(setUser(row.original))
|
|
||||||
setAddUserOpen(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@ -309,7 +298,7 @@ const UserListTable = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
{/* <CardHeader title='Filters' className='pbe-4' /> */}
|
<CardHeader title='Filters' className='pbe-4' />
|
||||||
{/* <TableFilters setData={setFilteredData} tableData={data} /> */}
|
{/* <TableFilters setData={setFilteredData} tableData={data} /> */}
|
||||||
<div className='flex justify-between flex-col items-start md:flex-row md:items-center p-6 border-bs gap-4'>
|
<div className='flex justify-between flex-col items-start md:flex-row md:items-center p-6 border-bs gap-4'>
|
||||||
<DebouncedInput
|
<DebouncedInput
|
||||||
@ -443,15 +432,9 @@ const UserListTable = () => {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<AddUserDrawer open={addUserOpen} handleClose={() => setAddUserOpen(!addUserOpen)} />
|
<AddUserDrawer
|
||||||
|
open={addUserOpen}
|
||||||
<ConfirmDeleteDialog
|
handleClose={() => setAddUserOpen(!addUserOpen)}
|
||||||
open={openConfirm}
|
|
||||||
onClose={() => setOpenConfirm(false)}
|
|
||||||
onConfirm={handleDelete}
|
|
||||||
isLoading={deleteUser.isPending}
|
|
||||||
title='Delete User'
|
|
||||||
message='Are you sure you want to delete this User? This action cannot be undone.'
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import Grid from '@mui/material/Grid2'
|
import Grid from '@mui/material/Grid2'
|
||||||
|
|
||||||
// Type Imports
|
// Type Imports
|
||||||
|
import type { UsersType } from '@/types/apps/userTypes'
|
||||||
|
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import UserListTable from './UserListTable'
|
import UserListTable from './UserListTable'
|
||||||
@ -9,6 +10,9 @@ import UserListTable from './UserListTable'
|
|||||||
const UserList = () => {
|
const UserList = () => {
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={6}>
|
<Grid container spacing={6}>
|
||||||
|
{/* <Grid size={{ xs: 12 }}>
|
||||||
|
<UserListCards />
|
||||||
|
</Grid> */}
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<UserListTable />
|
<UserListTable />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import { formatShortCurrency } from '../../../utils/transform'
|
|||||||
type Props = {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
value: number
|
value: number
|
||||||
isCurrency: boolean
|
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
avatarIcon: string
|
avatarIcon: string
|
||||||
avatarSkin?: CustomAvatarProps['skin']
|
avatarSkin?: CustomAvatarProps['skin']
|
||||||
@ -27,12 +26,12 @@ type Props = {
|
|||||||
const DistributedBarChartOrder = ({
|
const DistributedBarChartOrder = ({
|
||||||
title,
|
title,
|
||||||
value,
|
value,
|
||||||
isCurrency = false,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
avatarIcon,
|
avatarIcon,
|
||||||
avatarSkin,
|
avatarSkin,
|
||||||
avatarColor
|
avatarColor
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Skeleton sx={{ bgcolor: 'grey.100' }} variant='rectangular' width={300} height={118} />
|
return <Skeleton sx={{ bgcolor: 'grey.100' }} variant='rectangular' width={300} height={118} />
|
||||||
}
|
}
|
||||||
@ -46,7 +45,7 @@ const DistributedBarChartOrder = ({
|
|||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color='text.primary' variant='h4'>
|
<Typography color='text.primary' variant='h4'>
|
||||||
{isCurrency ? 'Rp ' + formatShortCurrency(value) : formatShortCurrency(value)}
|
{formatShortCurrency(value)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<CustomAvatar variant='rounded' skin={avatarSkin} size={52} color={avatarColor}>
|
<CustomAvatar variant='rounded' skin={avatarSkin} size={52} color={avatarColor}>
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const OrdersReport = ({ orderData, title }: { orderData: RecentSale[]; title: st
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className='bg-white divide-y divide-gray-200'>
|
<tbody className='bg-white divide-y divide-gray-200'>
|
||||||
{orderData && orderData.map((sale, index) => (
|
{orderData.map((sale, index) => (
|
||||||
<tr key={index} className='hover:bg-gray-50'>
|
<tr key={index} className='hover:bg-gray-50'>
|
||||||
<td className='px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-900'>
|
<td className='px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-900'>
|
||||||
{formatDate(sale.date)}
|
{formatDate(sale.date)}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const PaymentMethodReport = ({ payments }: { payments: PaymentDataItem[] }) => {
|
|||||||
<h2 className='text-xl font-semibold text-gray-900'>Payment Methods</h2>
|
<h2 className='text-xl font-semibold text-gray-900'>Payment Methods</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-6'>
|
<div className='space-y-6'>
|
||||||
{payments && payments.map(method => (
|
{payments.map(method => (
|
||||||
<div key={method.payment_method_id} className='border-b border-gray-200 pb-4 last:border-b-0'>
|
<div key={method.payment_method_id} className='border-b border-gray-200 pb-4 last:border-b-0'>
|
||||||
<div className='flex justify-between items-center mb-2'>
|
<div className='flex justify-between items-center mb-2'>
|
||||||
<span className='text-sm font-medium text-gray-900'>{method.payment_method_name}</span>
|
<span className='text-sm font-medium text-gray-900'>{method.payment_method_name}</span>
|
||||||
|
|||||||
@ -34,7 +34,7 @@ const ProductSales = ({ productData, title }: { productData: ProductData[], titl
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className='bg-white divide-y divide-gray-200'>
|
<tbody className='bg-white divide-y divide-gray-200'>
|
||||||
{productData && productData.map((product, index) => (
|
{productData.map((product, index) => (
|
||||||
<tr key={product.product_id} className='hover:bg-gray-50'>
|
<tr key={product.product_id} className='hover:bg-gray-50'>
|
||||||
<td className='px-4 py-4 whitespace-nowrap'>
|
<td className='px-4 py-4 whitespace-nowrap'>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import dynamic from 'next/dynamic'
|
|||||||
|
|
||||||
// MUI Imports
|
// MUI Imports
|
||||||
import TabContext from '@mui/lab/TabContext'
|
import TabContext from '@mui/lab/TabContext'
|
||||||
|
import TabList from '@mui/lab/TabList'
|
||||||
import TabPanel from '@mui/lab/TabPanel'
|
import TabPanel from '@mui/lab/TabPanel'
|
||||||
import Card from '@mui/material/Card'
|
import Card from '@mui/material/Card'
|
||||||
import CardContent from '@mui/material/CardContent'
|
import CardContent from '@mui/material/CardContent'
|
||||||
@ -25,6 +26,7 @@ import classnames from 'classnames'
|
|||||||
// Components Imports
|
// Components Imports
|
||||||
import CustomAvatar from '@core/components/mui/Avatar'
|
import CustomAvatar from '@core/components/mui/Avatar'
|
||||||
import OptionMenu from '@core/components/option-menu'
|
import OptionMenu from '@core/components/option-menu'
|
||||||
|
import Loading from '../../../components/layout/shared/Loading'
|
||||||
import { formatShortCurrency } from '../../../utils/transform'
|
import { formatShortCurrency } from '../../../utils/transform'
|
||||||
|
|
||||||
// Styled Component Imports
|
// Styled Component Imports
|
||||||
@ -205,12 +207,38 @@ const MultipleSeries = ({ data }: { data: TabType[] }) => {
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
title='Earnings Report'
|
title='Profit Reports'
|
||||||
subheader='Monthly Earning Overview'
|
subheader='Yearly Earnings Overview'
|
||||||
action={<OptionMenu options={['Last Week', 'Last Month', 'Last Year']} />}
|
action={<OptionMenu options={['Last Week', 'Last Month', 'Last Year']} />}
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<TabContext value={value}>
|
<TabContext value={value}>
|
||||||
|
{data.length > 1 && (
|
||||||
|
<TabList
|
||||||
|
variant='scrollable'
|
||||||
|
scrollButtons='auto'
|
||||||
|
onChange={handleChange}
|
||||||
|
aria-label='earning report tabs'
|
||||||
|
className='!border-0 mbe-10'
|
||||||
|
sx={{
|
||||||
|
'& .MuiTabs-indicator': { display: 'none !important' },
|
||||||
|
'& .MuiTab-root': { padding: '0 !important', border: '0 !important' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderTabs(data, value)}
|
||||||
|
<Tab
|
||||||
|
disabled
|
||||||
|
value='add'
|
||||||
|
label={
|
||||||
|
<div className='flex flex-col items-center justify-center is-[110px] bs-[100px] border border-dashed rounded-xl'>
|
||||||
|
<CustomAvatar variant='rounded' size={34}>
|
||||||
|
<i className='tabler-plus text-textSecondary' />
|
||||||
|
</CustomAvatar>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TabList>
|
||||||
|
)}
|
||||||
{renderTabPanels(data, theme, options, colors)}
|
{renderTabPanels(data, theme, options, colors)}
|
||||||
</TabContext>
|
</TabContext>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -1,377 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
// React Imports
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
||||||
|
|
||||||
// Next Imports
|
|
||||||
|
|
||||||
// MUI Imports
|
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
import Card from '@mui/material/Card'
|
|
||||||
import CardContent from '@mui/material/CardContent'
|
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
|
||||||
import type { TextFieldProps } from '@mui/material/TextField'
|
|
||||||
import Typography from '@mui/material/Typography'
|
|
||||||
|
|
||||||
// Third-party Imports
|
|
||||||
import type { RankingInfo } from '@tanstack/match-sorter-utils'
|
|
||||||
import { rankItem } from '@tanstack/match-sorter-utils'
|
|
||||||
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
|
|
||||||
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
// Type Imports
|
|
||||||
|
|
||||||
// Component Imports
|
|
||||||
import CustomTextField from '@core/components/mui/TextField'
|
|
||||||
|
|
||||||
// Util Imports
|
|
||||||
|
|
||||||
// Style Imports
|
|
||||||
import tableStyles from '@core/styles/table.module.css'
|
|
||||||
import { Box, Chip, CircularProgress, IconButton, TablePagination } from '@mui/material'
|
|
||||||
import { useDispatch } from 'react-redux'
|
|
||||||
import OptionMenu from '../../../../@core/components/option-menu'
|
|
||||||
import ConfirmDeleteDialog from '../../../../components/dialogs/confirm-delete'
|
|
||||||
import Loading from '../../../../components/layout/shared/Loading'
|
|
||||||
import TablePaginationComponent from '../../../../components/TablePaginationComponent'
|
|
||||||
import { setOrganization } from '../../../../redux-store/slices/organization'
|
|
||||||
import { useOrganizationsMutation } from '../../../../services/mutations/organization'
|
|
||||||
import { useOrganizations } from '../../../../services/queries/organizations'
|
|
||||||
import { Organization } from '../../../../types/services/organization'
|
|
||||||
|
|
||||||
declare module '@tanstack/table-core' {
|
|
||||||
interface FilterFns {
|
|
||||||
fuzzy: FilterFn<unknown>
|
|
||||||
}
|
|
||||||
interface FilterMeta {
|
|
||||||
itemRank: RankingInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SaOrganizationsTypeWithAction = Organization & {
|
|
||||||
actions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
|
||||||
// Rank the item
|
|
||||||
const itemRank = rankItem(row.getValue(columnId), value)
|
|
||||||
|
|
||||||
// Store the itemRank info
|
|
||||||
addMeta({
|
|
||||||
itemRank
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return if the item should be filtered in/out
|
|
||||||
return itemRank.passed
|
|
||||||
}
|
|
||||||
|
|
||||||
const DebouncedInput = ({
|
|
||||||
value: initialValue,
|
|
||||||
onChange,
|
|
||||||
debounce = 500,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
value: string | number
|
|
||||||
onChange: (value: string | number) => void
|
|
||||||
debounce?: number
|
|
||||||
} & Omit<TextFieldProps, 'onChange'>) => {
|
|
||||||
// States
|
|
||||||
const [value, setValue] = useState(initialValue)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValue(initialValue)
|
|
||||||
}, [initialValue])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
onChange(value)
|
|
||||||
}, debounce)
|
|
||||||
|
|
||||||
return () => clearTimeout(timeout)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [value])
|
|
||||||
|
|
||||||
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
|
|
||||||
}
|
|
||||||
|
|
||||||
// Column Definitions
|
|
||||||
const columnHelper = createColumnHelper<SaOrganizationsTypeWithAction>()
|
|
||||||
|
|
||||||
const OrganizationListTable = () => {
|
|
||||||
const dispatch = useDispatch()
|
|
||||||
|
|
||||||
// States
|
|
||||||
const [rowSelection, setRowSelection] = useState({})
|
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
|
||||||
const [pageSize, setPageSize] = useState(10)
|
|
||||||
const [openConfirm, setOpenConfirm] = useState(false)
|
|
||||||
const [organizationId, setOrganizationId] = useState('')
|
|
||||||
const [search, setSearch] = useState('')
|
|
||||||
|
|
||||||
const { data, isLoading, error, isFetching } = useOrganizations({
|
|
||||||
page: currentPage,
|
|
||||||
limit: pageSize,
|
|
||||||
search
|
|
||||||
})
|
|
||||||
|
|
||||||
const { deleteOrganization } = useOrganizationsMutation()
|
|
||||||
|
|
||||||
const organizations = data?.organizations ?? []
|
|
||||||
const totalCount = data?.total_count ?? 0
|
|
||||||
|
|
||||||
const handlePageChange = useCallback((event: unknown, newPage: number) => {
|
|
||||||
setCurrentPage(newPage)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Handle page size change
|
|
||||||
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const newPageSize = parseInt(event.target.value, 10)
|
|
||||||
setPageSize(newPageSize)
|
|
||||||
setCurrentPage(1) // Reset to first page
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
deleteOrganization.mutate(organizationId, {
|
|
||||||
onSuccess: () => setOpenConfirm(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<SaOrganizationsTypeWithAction, any>[]>(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
header: ({ table }) => (
|
|
||||||
<Checkbox
|
|
||||||
{...{
|
|
||||||
checked: table.getIsAllRowsSelected(),
|
|
||||||
indeterminate: table.getIsSomeRowsSelected(),
|
|
||||||
onChange: table.getToggleAllRowsSelectedHandler()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Checkbox
|
|
||||||
{...{
|
|
||||||
checked: row.getIsSelected(),
|
|
||||||
disabled: !row.getCanSelect(),
|
|
||||||
indeterminate: row.getIsSomeSelected(),
|
|
||||||
onChange: row.getToggleSelectedHandler()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
columnHelper.accessor('name', {
|
|
||||||
header: 'Name',
|
|
||||||
cell: ({ row }) => <Typography color='text.primary'>{row.original.name || '-'}</Typography>
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('email', {
|
|
||||||
header: 'Email',
|
|
||||||
cell: ({ row }) => <Typography color='text.primary'>{row.original.email || '-'}</Typography>
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('phone_number', {
|
|
||||||
header: 'Phone',
|
|
||||||
cell: ({ row }) => <Typography>{row.original.phone_number || '-'}</Typography>
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('plan_type', {
|
|
||||||
header: 'Plan Type',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Chip label={row.original.plan_type} variant='tonal' color={row.original.plan_type === 'enterprise' ? 'primary' : 'info'} />
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('actions', {
|
|
||||||
header: 'Actions',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(setOrganization(row.original))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className='tabler-edit text-textSecondary' />
|
|
||||||
</IconButton>
|
|
||||||
<OptionMenu
|
|
||||||
iconButtonProps={{ size: 'medium' }}
|
|
||||||
iconClassName='text-textSecondary'
|
|
||||||
options={[
|
|
||||||
{ text: 'Download', icon: 'tabler-download' },
|
|
||||||
{
|
|
||||||
text: 'Delete',
|
|
||||||
icon: 'tabler-trash',
|
|
||||||
menuItemProps: {
|
|
||||||
onClick: () => {
|
|
||||||
setOpenConfirm(true)
|
|
||||||
setOrganizationId(row.original.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ text: 'Duplicate', icon: 'tabler-copy' }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableSorting: false
|
|
||||||
})
|
|
||||||
],
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: organizations as Organization[],
|
|
||||||
columns,
|
|
||||||
filterFns: {
|
|
||||||
fuzzy: fuzzyFilter
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
rowSelection,
|
|
||||||
pagination: {
|
|
||||||
pageIndex: currentPage,
|
|
||||||
pageSize
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enableRowSelection: true, //enable row selection for all rows
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
// Disable client-side pagination since we're handling it server-side
|
|
||||||
manualPagination: true,
|
|
||||||
pageCount: Math.ceil(totalCount / pageSize)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card>
|
|
||||||
<CardContent className='flex justify-between flex-wrap max-sm:flex-col sm:items-center gap-4'>
|
|
||||||
<DebouncedInput
|
|
||||||
value={search}
|
|
||||||
onChange={value => setSearch(value as string)}
|
|
||||||
placeholder='Search'
|
|
||||||
className='max-sm:is-full'
|
|
||||||
/>
|
|
||||||
<div className='flex max-sm:flex-col items-start sm:items-center gap-4 max-sm:is-full'>
|
|
||||||
<CustomTextField select value={pageSize} onChange={handlePageSizeChange} className='is-full sm:is-[70px]'>
|
|
||||||
<MenuItem value='10'>10</MenuItem>
|
|
||||||
<MenuItem value='25'>25</MenuItem>
|
|
||||||
<MenuItem value='50'>50</MenuItem>
|
|
||||||
<MenuItem value='100'>100</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
<Button
|
|
||||||
variant='tonal'
|
|
||||||
className='max-sm:is-full'
|
|
||||||
color='secondary'
|
|
||||||
startIcon={<i className='tabler-upload' />}
|
|
||||||
>
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<div className='overflow-x-auto'>
|
|
||||||
{isLoading ? (
|
|
||||||
<Loading />
|
|
||||||
) : (
|
|
||||||
<table className={tableStyles.table}>
|
|
||||||
<thead>
|
|
||||||
{table.getHeaderGroups().map(headerGroup => (
|
|
||||||
<tr key={headerGroup.id}>
|
|
||||||
{headerGroup.headers.map(header => (
|
|
||||||
<th key={header.id}>
|
|
||||||
{header.isPlaceholder ? null : (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={classnames({
|
|
||||||
'flex items-center': header.column.getIsSorted(),
|
|
||||||
'cursor-pointer select-none': header.column.getCanSort()
|
|
||||||
})}
|
|
||||||
onClick={header.column.getToggleSortingHandler()}
|
|
||||||
>
|
|
||||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
||||||
{{
|
|
||||||
asc: <i className='tabler-chevron-up text-xl' />,
|
|
||||||
desc: <i className='tabler-chevron-down text-xl' />
|
|
||||||
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</thead>
|
|
||||||
{table.getFilteredRowModel().rows.length === 0 ? (
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
|
||||||
No data available
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
) : (
|
|
||||||
<tbody>
|
|
||||||
{table
|
|
||||||
.getRowModel()
|
|
||||||
.rows.slice(0, table.getState().pagination.pageSize)
|
|
||||||
.map(row => {
|
|
||||||
return (
|
|
||||||
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
|
|
||||||
{row.getVisibleCells().map(cell => (
|
|
||||||
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
)}
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isFetching && !isLoading && (
|
|
||||||
<Box
|
|
||||||
position='absolute'
|
|
||||||
top={0}
|
|
||||||
left={0}
|
|
||||||
right={0}
|
|
||||||
bottom={0}
|
|
||||||
display='flex'
|
|
||||||
alignItems='center'
|
|
||||||
justifyContent='center'
|
|
||||||
bgcolor='rgba(255,255,255,0.7)'
|
|
||||||
zIndex={1}
|
|
||||||
>
|
|
||||||
<CircularProgress size={24} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TablePagination
|
|
||||||
component={() => (
|
|
||||||
<TablePaginationComponent
|
|
||||||
pageIndex={currentPage}
|
|
||||||
pageSize={pageSize}
|
|
||||||
totalCount={totalCount}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
count={totalCount}
|
|
||||||
rowsPerPage={pageSize}
|
|
||||||
page={currentPage}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onRowsPerPageChange={handlePageSizeChange}
|
|
||||||
rowsPerPageOptions={[10, 25, 50]}
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<ConfirmDeleteDialog
|
|
||||||
open={openConfirm}
|
|
||||||
onClose={() => setOpenConfirm(false)}
|
|
||||||
onConfirm={handleDelete}
|
|
||||||
isLoading={deleteOrganization.isPending}
|
|
||||||
title='Delete Organization'
|
|
||||||
message='Are you sure you want to delete this Organization? This action cannot be undone.'
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OrganizationListTable
|
|
||||||
Loading…
x
Reference in New Issue
Block a user