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

This commit is contained in:
efrilm 2025-08-13 15:05:14 +07:00
commit 3514e7dc46
42 changed files with 2666 additions and 524 deletions

View File

@ -0,0 +1,584 @@
'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

View File

@ -23,6 +23,7 @@ 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'
@ -33,6 +34,7 @@ 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'
@ -43,6 +45,7 @@ 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'
@ -53,6 +56,7 @@ 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'

View File

@ -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 } from '../../../../../../utils/transform' import { formatCurrency, formatDate, formatShortCurrency } 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'
@ -22,7 +22,7 @@ const DashboardOverview = () => {
<p className='text-2xl font-bold text-gray-900 mb-1'>{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={`p-3 rounded-full ${bgColor} bg-opacity-10`}> <div className={`px-4 py-3 rounded-full ${bgColor} bg-opacity-10`}>
<i className={`${iconClass} text-[32px] ${bgColor.replace('bg-', 'text-')}`}></i> <i className={`${iconClass} text-[32px] ${bgColor.replace('bg-', 'text-')}`}></i>
</div> </div>
</div> </div>
@ -55,7 +55,7 @@ const DashboardOverview = () => {
<MetricCard <MetricCard
iconClass='tabler-cash' iconClass='tabler-cash'
title='Total Sales' title='Total Sales'
value={formatCurrency(salesData.overview.total_sales)} value={formatShortCurrency(salesData.overview.total_sales)}
bgColor='bg-green-500' bgColor='bg-green-500'
/> />
<MetricCard <MetricCard
@ -68,7 +68,7 @@ const DashboardOverview = () => {
<MetricCard <MetricCard
iconClass='tabler-trending-up' iconClass='tabler-trending-up'
title='Average Order Value' title='Average Order Value'
value={formatCurrency(salesData.overview.average_order_value)} value={formatShortCurrency(salesData.overview.average_order_value)}
bgColor='bg-purple-500' bgColor='bg-purple-500'
/> />
<MetricCard <MetricCard
@ -79,24 +79,6 @@ 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} />

View File

@ -23,6 +23,7 @@ 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'
@ -33,6 +34,7 @@ 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'
@ -43,6 +45,7 @@ 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'
@ -53,6 +56,7 @@ 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'

View File

@ -1,65 +1,51 @@
'use client' 'use client'
// MUI Imports import React from 'react'
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 { DailyData, ProductDataReport, ProfitLossReport } from '../../../../../../types/services/analytic' import { formatShortCurrency } from '../../../../../../utils/transform'
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 = () => {
// 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 { function formatMetricName(metric: string): string {
const nameMap: { [key: string]: string } = { const nameMap: { [key: string]: string } = {
revenue: 'Revenue', revenue: 'Revenue',
cost: 'Cost',
gross_profit: 'Gross Profit',
gross_profit_margin: 'Gross Profit Margin (%)',
tax: 'Tax',
discount: 'Discount',
net_profit: 'Net Profit', 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 metrics = ['revenue', 'net_profit']
const { data, isLoading } = useProfitLossAnalytics()
const formatDate = (dateString: any) => {
return new Date(dateString).toLocaleDateString('id-ID', {
month: 'short',
day: 'numeric'
})
}
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 [
@ -75,58 +61,285 @@ const DashboardProfitLoss = () => {
] ]
} }
if (isLoading) return <Loading /> const MetricCard = ({ iconClass, title, value, subtitle, bgColor = 'bg-blue-500', isNegative = false }: any) => (
<div className='bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 p-6'>
<div className='flex items-center justify-between'>
<div className='flex-1'>
<h3 className='text-sm font-medium text-gray-600 mb-2'>{title}</h3>
<p className={`text-2xl font-bold mb-1 ${isNegative ? 'text-red-600' : 'text-gray-900'}`}>{value}</p>
{subtitle && <p className='text-sm text-gray-500'>{subtitle}</p>}
</div>
<div className={`p-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}> <>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}> {profitData && (
<DistributedBarChartOrder <div>
isLoading={isLoading} {/* Header */}
<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'
/>
<MetricCard
iconClass='tabler-receipt'
title='Total Cost' title='Total Cost'
value={data?.summary.total_cost as number} value={formatShortCurrency(profitData.summary.total_cost)}
avatarIcon={'tabler-currency-dollar'} bgColor='bg-red-500'
avatarColor='primary'
avatarSkin='light'
/> />
</Grid> <MetricCard
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}> iconClass='tabler-trending-up'
<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={data?.summary.gross_profit as number} value={formatShortCurrency(profitData.summary.gross_profit)}
avatarIcon={'tabler-trending-up'} subtitle={`Margin: ${formatPercentage(profitData.summary.gross_profit_margin)}`}
avatarColor='warning' bgColor='bg-blue-500'
avatarSkin='light' isNegative={profitData.summary.gross_profit < 0}
/> />
</Grid> <MetricCard
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}> iconClass='tabler-percentage'
<DistributedBarChartOrder title='Profitability Ratio'
isLoading={isLoading} value={formatPercentage(profitData.summary.profitability_ratio)}
title='Net Profit' subtitle={`Avg Profit: ${formatShortCurrency(profitData.summary.average_profit)}`}
value={data?.summary.net_profit as number} bgColor='bg-purple-500'
avatarIcon={'tabler-currency-dollar'}
avatarColor='success'
avatarSkin='light'
/> />
</Grid> </div>
<Grid size={{ xs: 12, lg: 12 }}>
<EarningReportsWithTabs data={transformSalesData(data!)} /> {/* Additional Summary Metrics */}
</Grid> <div className='grid grid-cols-1 md:grid-cols-3 gap-6 mb-8'>
<Grid size={{ xs: 12, lg: 12 }}> <div className='bg-white rounded-lg shadow-md p-6'>
<MultipleSeries data={transformMultipleData(data!)} /> <div className='flex items-center mb-4'>
</Grid> <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>
</div>
<p className='text-3xl font-bold text-green-600 mb-2'>
{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'>
{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

View File

@ -2,24 +2,23 @@
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
// Type Imports // Type Imports
import type { ChildrenType } from '@core/types'
import type { Locale } from '@configs/i18n' import type { Locale } from '@configs/i18n'
import type { ChildrenType } from '@core/types'
// 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 Providers from '@components/Providers'
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 AuthGuard from '@/hocs/AuthGuard' import AuthGuard from '@/hocs/AuthGuard'
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 // Config Imports
import { i18n } from '@configs/i18n' import { i18n } from '@configs/i18n'

View File

@ -0,0 +1,75 @@
// 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

View File

@ -0,0 +1,25 @@
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

View File

@ -31,6 +31,7 @@ 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')({
@ -162,8 +163,9 @@ 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 {logout.isPending && <CircularProgress size={16} />}
</Button> </Button>
</div> </div>
</MenuList> </MenuList>

View File

@ -29,6 +29,8 @@ 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>>
@ -64,6 +66,8 @@ 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)
@ -129,7 +133,12 @@ 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>
) )
} }

View File

@ -0,0 +1,91 @@
// 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

View File

@ -92,7 +92,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
</SubMenu> </SubMenu>
<MenuSection label={dictionary['navigation'].appsPages}> <MenuSection label={dictionary['navigation'].appsPages}>
<SubMenu label={dictionary['navigation'].eCommerce} icon={<i className='tabler-shopping-cart' />}> <SubMenu label={dictionary['navigation'].eCommerce} icon={<i className='tabler-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}>

View File

@ -2,11 +2,12 @@
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: any | null currentUser: User | null
} }
const AuthContext = createContext<AuthContextType>({ const AuthContext = createContext<AuthContextType>({
@ -17,7 +18,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<any | null>(null) const [currentUser, setCurrentUser] = useState<User | null>(null)
const [isInitialized, setIsInitialized] = useState(false) const [isInitialized, setIsInitialized] = useState(false)
useEffect(() => { useEffect(() => {

View File

@ -12,13 +12,17 @@ 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 } = useAuth() const { isAuthenticated, currentUser } = useAuth()
useEffect(() => { useEffect(() => {
if (!isAuthenticated) { if (!isAuthenticated) {
redirect(getLocalizedUrl('/login', locale)) redirect(getLocalizedUrl('/login', locale))
} }
}, [isAuthenticated])
return <>{isAuthenticated ? children : <Loading />}</> if (currentUser?.role !== 'admin') {
redirect(getLocalizedUrl('/not-found', locale))
}
}, [isAuthenticated, currentUser])
return <>{isAuthenticated && currentUser?.role === 'admin' ? children : <Loading />}</>
} }

28
src/hocs/RolesGuard.tsx Normal file
View File

@ -0,0 +1,28 @@
'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 />}</>
}

View File

@ -6,6 +6,9 @@ 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: {
@ -13,7 +16,10 @@ 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 })
}) })

View File

@ -0,0 +1,37 @@
// 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

View File

@ -0,0 +1,28 @@
// Third-party Imports
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
// Type Imports
// Data Imports
const initialState: { currentProductRecipe: any } = {
currentProductRecipe: {}
}
export const productRecipeSlice = createSlice({
name: 'productRecipe',
initialState,
reducers: {
setProductRecipe: (state, action: PayloadAction<any>) => {
state.currentProductRecipe = action.payload
},
resetProductRecipe: state => {
state.currentProductRecipe = initialState.currentProductRecipe
}
}
})
export const { setProductRecipe, resetProductRecipe } = productRecipeSlice.actions
export default productRecipeSlice.reducer

View File

@ -0,0 +1,40 @@
// 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

View File

@ -0,0 +1,52 @@
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 }
}

View File

@ -0,0 +1,45 @@
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')
}
})
return {
createProductRecipe,
updateProductRecipe
}
}

View File

@ -1,52 +1,54 @@
import { useMutation, useQueryClient } from '@tanstack/react-query' import { useMutation, useQueryClient } from '@tanstack/react-query'
import { CustomerRequest } from '../../types/services/customer'
import { api } from '../api'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { UserRequest } from '../../types/services/user'
import { api } from '../api'
export const useCustomersMutation = () => { export const useUsersMutation = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const createCustomer = useMutation({ const createUser = useMutation({
mutationFn: async (newCustomer: CustomerRequest) => { mutationFn: async (newUser: UserRequest) => {
const response = await api.post('/customers', newCustomer) const response = await api.post('/users', newUser)
return response.data return response.data
}, },
onSuccess: () => { onSuccess: () => {
toast.success('Customer created successfully!') toast.success('User created successfully!')
queryClient.invalidateQueries({ queryKey: ['customers'] }) queryClient.invalidateQueries({ queryKey: ['users'] })
}, },
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 updateCustomer = useMutation({ const updateUser = useMutation({
mutationFn: async ({ id, payload }: { id: string; payload: CustomerRequest }) => { mutationFn: async ({ id, payload }: { id: string; payload: UserRequest }) => {
const response = await api.put(`/customers/${id}`, payload) const {password, ...rest} = payload
const response = await api.put(`/users/${id}`, rest)
return response.data return response.data
}, },
onSuccess: () => { onSuccess: () => {
toast.success('Customer updated successfully!') toast.success('User updated successfully!')
queryClient.invalidateQueries({ queryKey: ['customers'] }) queryClient.invalidateQueries({ queryKey: ['users'] })
}, },
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 deleteCustomer = useMutation({ const deleteUser = useMutation({
mutationFn: async (id: string) => { mutationFn: async (id: string) => {
const response = await api.delete(`/customers/${id}`) const response = await api.delete(`/users/${id}`)
return response.data return response.data
}, },
onSuccess: () => { onSuccess: () => {
toast.success('Customer deleted successfully!') toast.success('User deleted successfully!')
queryClient.invalidateQueries({ queryKey: ['customers'] }) queryClient.invalidateQueries({ queryKey: ['users'] })
}, },
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 { createCustomer, updateCustomer, deleteCustomer } return { createUser, updateUser, deleteUser }
} }

View File

@ -0,0 +1,36 @@
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
}
})
}

View File

@ -0,0 +1,13 @@
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
}
})
}

View File

@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { Products } from '../../types/services/product' import { Product, Products } from '../../types/services/product'
import { api } from '../api' import { api } from '../api'
import { ProductRecipe } from '../../types/services/productRecipe'
interface ProductsQueryParams { interface ProductsQueryParams {
page?: number page?: number
@ -39,7 +40,7 @@ export function useProducts(params: ProductsQueryParams = {}) {
} }
export function useProductById(id: string) { export function useProductById(id: string) {
return useQuery({ return useQuery<Product>({
queryKey: ['product', id], queryKey: ['product', id],
queryFn: async () => { queryFn: async () => {
const res = await api.get(`/products/${id}`) const res = await api.get(`/products/${id}`)

View File

@ -0,0 +1,33 @@
export interface OrganizationRequest {
organization_name: string; // required, 1255 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, 1255 chars
admin_email: string; // required, email
admin_password: string; // required, min length 6
outlet_name: string; // required, 1255 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;
}

View File

@ -0,0 +1,56 @@
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;
}

View File

@ -5,7 +5,7 @@ export type User = {
name: string; name: string;
email: string; email: string;
role: string; role: string;
permissions: Record<string, unknown>; permissions: Record<string, boolean>;
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,12 +24,11 @@ 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, unknown>; permissions: Record<string, boolean>;
is_active: boolean; is_active: boolean;
} }

View File

@ -7,12 +7,23 @@ export const formatCurrency = (amount: number) => {
} }
export const formatShortCurrency = (num: number): string => { export const formatShortCurrency = (num: number): string => {
if (num >= 1_000_000) { const formatNumber = (value: number, suffix: string) => {
return (num / 1_000_000).toFixed(2) + 'M' const str = value.toFixed(2).replace(/\.00$/, '')
} else if (num >= 1_000) { return str + suffix
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) => {

View File

@ -243,7 +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 component={Link} href={getLocalizedUrl('/register', locale as Locale)} color='primary.main'> <Typography component={Link} href={getLocalizedUrl('/organization', locale as Locale)} color='primary.main'>
Create an account Create an account
</Typography> </Typography>
</div> </div>

View File

@ -221,7 +221,6 @@ const CustomerListTable = () => {
} }
} }
}, },
{ text: 'Duplicate', icon: 'tabler-copy' }
]} ]}
/> />
</div> </div>

View File

@ -0,0 +1,210 @@
// React Imports
import { 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 { resetProductRecipe } from '../../../../../redux-store/slices/productRecipe'
import { useProductRecipesMutation } from '../../../../../services/mutations/productRecipes'
import { useIngredients } from '../../../../../services/queries/ingredients'
import { useOutlets } from '../../../../../services/queries/outlets'
import { Product } from '../../../../../types/services/product'
import { ProductRecipeRequest } from '../../../../../types/services/productRecipe'
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 { 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()
const handleSubmit = (e: any) => {
e.preventDefault()
createProductRecipe.mutate(
{ ...formData, product_id: product.id, variant_id: currentProductRecipe.id || '' },
{
onSuccess: () => {
handleReset()
}
}
)
}
const handleReset = () => {
handleClose()
dispatch(resetProductRecipe())
setFormData(initialData)
}
const handleInputChange = (e: any) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
const setTitleDrawer = (recipe: any) => {
let title = 'Original'
if (recipe?.name) {
title = recipe?.name
}
return 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(currentProductRecipe)} 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}
>
{createProductRecipe.isPending
? 'Adding...'
: 'Add'}
</Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Discard
</Button>
</div>
</form>
</div>
</PerfectScrollbar>
</Drawer>
)
}
export default AddRecipeDrawer

View File

@ -2,319 +2,381 @@
import { import {
Avatar, Avatar,
Badge, Box,
Button,
Card, Card,
CardContent, CardContent,
CardMedia, CardHeader,
Chip, Chip,
Divider,
Grid, Grid,
List, Paper,
ListItem, Table,
ListItemIcon, TableBody,
ListItemText, TableCell,
TableContainer,
TableHead,
TableRow,
Typography Typography
} from '@mui/material' } from '@mui/material'
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
import React, { useEffect } from 'react' import { useState } from 'react'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import Loading from '../../../../../components/layout/shared/Loading' import Loading from '../../../../../components/layout/shared/Loading'
import { setProduct } from '../../../../../redux-store/slices/product' import { setProductRecipe } from '../../../../../redux-store/slices/productRecipe'
import { useProductRecipesByProduct } from '../../../../../services/queries/productRecipes'
import { useProductById } from '../../../../../services/queries/products' import { useProductById } from '../../../../../services/queries/products'
import { ProductVariant } from '../../../../../types/services/product' import { ProductVariant } from '../../../../../types/services/product'
import { formatCurrency, formatDate } from '../../../../../utils/transform' import { formatCurrency } from '../../../../../utils/transform'
// Tabler icons (using class names) import AddRecipeDrawer from './AddRecipeDrawer'
const TablerIcon = ({ name, className = '' }: { name: string; className?: string }) => (
<i className={`tabler-${name} ${className}`} />
)
const ProductDetail = () => { const ProductDetail = () => {
const dispatch = useDispatch() const dispatch = useDispatch()
const params = useParams() const params = useParams()
const [openProductRecipe, setOpenProductRecipe] = useState(false)
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)
useEffect(() => { const handleOpenProductRecipe = (recipe: any) => {
if (product) { setOpenProductRecipe(true)
dispatch(setProduct(product)) dispatch(setProductRecipe(recipe))
}
}, [product, dispatch])
const getBusinessTypeColor = (type: string) => {
switch (type.toLowerCase()) {
case 'restaurant':
return 'primary'
case 'retail':
return 'secondary'
case 'cafe':
return 'info'
default:
return 'default'
}
} }
const getPrinterTypeColor = (type: string) => { if (isLoading || isLoadingProductRecipe) return <Loading />
switch (type.toLowerCase()) {
case 'kitchen':
return 'warning'
case 'bar':
return 'info'
case 'receipt':
return 'success'
default:
return 'default'
}
}
const getPlainText = (html: string) => {
const doc = new DOMParser().parseFromString(html, 'text/html')
return doc.body.textContent || ''
}
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 className='shadow-lg'> <Card>
<Grid container> <CardHeader
<Grid item xs={12} md={4}> avatar={<Avatar src={product?.image_url || ''} alt={product?.name} className='w-16 h-16' />}
<CardMedia title={
component='img' <div className='flex items-center gap-3'>
sx={{ height: 300, objectFit: 'cover' }} <Typography variant='h4' component='h1' className='font-bold'>
image={product.image_url || '/placeholder-image.jpg'} {product?.name}
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
icon={<TablerIcon name='barcode' className='text-sm' />} label={product?.is_active ? 'Active' : 'Inactive'}
label={product.sku} color={product?.is_active ? 'success' : 'error'}
size='small' size='small'
/>
</div>
}
subheader={
<div className='flex flex-col gap-1 mt-2'>
<Typography variant='body2' color='textSecondary'>
SKU: {product?.sku} Category: {product?.business_type}
</Typography>
<div className='flex gap-4'>
<Typography variant='body2'>
<span className='font-semibold'>Price:</span> {formatCurrency(product?.price || 0)}
</Typography>
<Typography variant='body2'>
<span className='font-semibold'>Base Cost:</span> {formatCurrency(product?.cost || 0)}
</Typography>
</div>
</div>
}
/>
</Card>
{/* {productRecipe && ( */}
<div className='space-y-6'>
{/* Recipe Details by Variant */}
<div className='space-y-4'>
<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>
<TableContainer component={Paper} variant='outlined'>
<Table>
<TableHead>
<TableRow className='bg-gray-50'>
<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 === null)
.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' variant='outlined'
/> />
<Chip </TableCell>
icon={<TablerIcon name={product.is_active ? 'check-circle' : 'x-circle'} className='text-sm' />} <TableCell className='text-right font-medium'>
label={product.is_active ? 'Active' : 'Inactive'} {formatCurrency(item.ingredient.cost * item.quantity)}
color={product.is_active ? 'success' : 'error'} </TableCell>
size='small' </TableRow>
/> ))}
</div> </TableBody>
</div> </Table>
</div> </TableContainer>
{product.description && ( {/* Variant Summary */}
<Typography variant='body1' className='text-gray-600 mb-4'> {productRecipe?.length && (
{getPlainText(product.description)} <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> </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>
)} )}
<div className='grid grid-cols-2 gap-4 mb-4'> <Button
<div className='flex items-center gap-2'> variant='outlined'
<TablerIcon name='currency-dollar' className='text-green-600 text-xl' /> fullWidth
<div> className='mt-4'
<Typography variant='body2' className='text-gray-500'> startIcon={<i className='tabler-plus' />}
Price onClick={() => handleOpenProductRecipe({ variant: undefined })}
</Typography> >
<Typography variant='h6' className='font-semibold text-green-600'> Add Ingredient
{formatCurrency(product.price)} </Button>
</Typography>
</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>
<Grid container spacing={3}>
{/* Product Information */}
<Grid item xs={12} md={8}>
<Card className='shadow-md'>
<CardContent>
<Typography variant='h6' className='font-semibold mb-4 flex items-center gap-2'>
<TablerIcon name='info-circle' className='text-blue-600 text-xl' />
Product Information
</Typography>
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
<div>
<Typography variant='body2' className='text-gray-500 mb-1'>
Product ID
</Typography>
<Typography variant='body1' className='font-mono text-sm bg-gray-100 p-2 rounded'>
{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>
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
{/* Variants Section */} {product?.variants?.length &&
{product.variants && product.variants.length > 0 && ( product.variants.map((variantData: ProductVariant, index: number) => (
<Card className='shadow-md mt-4'> <Card key={index}>
<CardContent> <CardHeader
<Typography variant='h6' className='font-semibold mb-4 flex items-center gap-2'> title={
<TablerIcon name='versions' className='text-purple-600 text-xl' />
Product Variants
<Badge badgeContent={product.variants.length} color='primary' />
</Typography>
<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'> <div className='flex items-center justify-between'>
<Typography variant='subtitle1' className='font-medium'> <div className='flex items-center gap-3'>
{variant.name} <i className='tabler-variant text-blue-600 text-lg' />
</Typography> <Typography variant='h6' className='font-semibold'>
<div className='flex gap-3'> {variantData?.name || 'Original'} Variant
<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>
<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>
}
secondary={
<Typography variant='caption' className='text-gray-500'>
Total Price: {formatCurrency(product.price + variant.price_modifier)}
</Typography>
} }
/> />
</ListItem>
{index < product.variants.length - 1 && <Divider />}
</React.Fragment>
))}
</List>
</CardContent>
</Card>
)}
</Grid>
{/* Metadata & Timestamps */}
<Grid item xs={12} md={4}>
<Card className='shadow-md'>
<CardContent> <CardContent>
<Typography variant='h6' className='font-semibold mb-4 flex items-center gap-2'> <TableContainer component={Paper} variant='outlined'>
<TablerIcon name='clock' className='text-indigo-600 text-xl' /> <Table>
Timestamps <TableHead>
</Typography> <TableRow className='bg-gray-50'>
<div className='space-y-3'> <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> <div>
<Typography variant='body2' className='text-gray-500 mb-1'> <Typography variant='body2' className='font-medium capitalize'>
Created {item.ingredient.name}
</Typography> </Typography>
<Typography variant='body2' className='text-sm'> <Typography variant='caption' color='textSecondary'>
{formatDate(product.created_at)} {item.ingredient.is_semi_finished ? 'Semi-finished' : 'Raw ingredient'}
</Typography> </Typography>
</div> </div>
<Divider /> </div>
<div> </TableCell>
<Typography variant='body2' className='text-gray-500 mb-1'> <TableCell className='text-center'>
Last Updated <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>
</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> </Typography>
<Typography variant='body2' className='text-sm'> </Grid>
{formatDate(product.updated_at)} <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> </Typography>
</Grid>
</Grid>
</Box>
)}
<Button
variant='outlined'
fullWidth
className='mt-4'
startIcon={<i className='tabler-plus' />}
onClick={() => handleOpenProductRecipe(variantData)}
>
Add Ingredient
</Button>
</CardContent>
</Card>
))}
</div>
</div> </div>
</div> </div>
{Object.keys(product.metadata).length > 0 && ( <AddRecipeDrawer open={openProductRecipe} handleClose={() => setOpenProductRecipe(false)} product={product!} />
<>
<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>
</> </>
)}
</CardContent>
</Card>
</Grid>
</Grid>
</div>
) )
} }

View File

@ -1,5 +1,5 @@
// React Imports // React Imports
import { useState } from 'react' import { useEffect, useMemo, useState } from 'react'
// MUI Imports // MUI Imports
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
@ -10,15 +10,19 @@ 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
@ -31,26 +35,73 @@ const initialData = {
email: '', email: '',
password: '', password: '',
role: '', role: '',
permissions: {}, permissions: {
can_create_orders: false,
can_void_orders: false
},
is_active: true, is_active: true,
organization_id: '', outlet_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 onSubmit = () => { const { currentUser } = useSelector((state: RootState) => state.userReducer)
handleClose()
setFormData(initialData) const { createUser, updateUser } = useUsersMutation()
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)
} }
@ -61,6 +112,17 @@ 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}
@ -71,14 +133,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'>Add New User</Typography> <Typography variant='h5'>{currentUser.id ? 'Edit' : 'Add'} 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={onSubmit} className='flex flex-col gap-6 p-6'> <form onSubmit={handleSubmit} className='flex flex-col gap-6 p-6'>
<CustomTextField <CustomTextField
fullWidth fullWidth
label='Name' label='Name'
@ -96,6 +158,7 @@ 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'
@ -105,6 +168,71 @@ 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'>
@ -119,7 +247,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'>
Submit {createUser.isPending || updateUser.isPending ? 'Saving...' : 'Save'}
</Button> </Button>
<Button variant='tonal' color='error' type='reset' onClick={() => handleReset()}> <Button variant='tonal' color='error' type='reset' onClick={() => handleReset()}>
Cancel Cancel

View File

@ -47,6 +47,10 @@ 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 {
@ -123,13 +127,15 @@ 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 [customerId, setCustomerId] = useState('') const [userId, setUserId] = useState('')
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
// Hooks // Hooks
@ -141,7 +147,7 @@ const UserListTable = () => {
search search
}) })
// const { deleteCustomer } = useCustomersMutation() const { deleteUser } = useUsersMutation()
const users = data?.users ?? [] const users = data?.users ?? []
const totalCount = data?.pagination.total_count ?? 0 const totalCount = data?.pagination.total_count ?? 0
@ -157,11 +163,11 @@ const UserListTable = () => {
setCurrentPage(1) // Reset to first page setCurrentPage(1) // Reset to first page
}, []) }, [])
// const handleDelete = () => { const handleDelete = () => {
// deleteCustomer.mutate(customerId, { deleteUser.mutate(userId, {
// onSuccess: () => setOpenConfirm(false) onSuccess: () => setOpenConfirm(false)
// }) })
// } }
const columns = useMemo<ColumnDef<UsersTypeWithAction, any>[]>( const columns = useMemo<ColumnDef<UsersTypeWithAction, any>[]>(
() => [ () => [
@ -236,22 +242,27 @@ const UserListTable = () => {
header: 'Action', header: 'Action',
cell: ({ row }) => ( cell: ({ row }) => (
<div className='flex items-center'> <div className='flex items-center'>
<IconButton onClick={() => {}}> <IconButton
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: { className: 'flex items-center gap-2 text-textSecondary' } menuItemProps: {
onClick: () => {
dispatch(setUser(row.original))
setAddUserOpen(true)
}
}
} }
]} ]}
/> />
@ -298,7 +309,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
@ -432,9 +443,15 @@ const UserListTable = () => {
/> />
</Card> </Card>
<AddUserDrawer <AddUserDrawer open={addUserOpen} handleClose={() => setAddUserOpen(!addUserOpen)} />
open={addUserOpen}
handleClose={() => setAddUserOpen(!addUserOpen)} <ConfirmDeleteDialog
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.'
/> />
</> </>
) )

View File

@ -2,7 +2,6 @@
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'
@ -10,9 +9,6 @@ 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>

View File

@ -16,6 +16,7 @@ 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']
@ -26,12 +27,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} />
} }
@ -45,7 +46,7 @@ const DistributedBarChartOrder = ({
{title} {title}
</Typography> </Typography>
<Typography color='text.primary' variant='h4'> <Typography color='text.primary' variant='h4'>
{formatShortCurrency(value)} {isCurrency ? 'Rp ' + 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}>

View File

@ -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.map((sale, index) => ( {orderData && 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)}

View File

@ -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.map(method => ( {payments && 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>

View File

@ -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.map((product, index) => ( {productData && 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'>

View File

@ -9,7 +9,6 @@ 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'
@ -26,7 +25,6 @@ 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
@ -207,38 +205,12 @@ const MultipleSeries = ({ data }: { data: TabType[] }) => {
return ( return (
<Card> <Card>
<CardHeader <CardHeader
title='Profit Reports' title='Earnings Report'
subheader='Yearly Earnings Overview' subheader='Monthly Earning 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>

View File

@ -0,0 +1,377 @@
'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