feat: analytic
This commit is contained in:
parent
7beee4c3a1
commit
67b747f0ff
@ -1,72 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Components Imports
|
||||
import WebsiteAnalyticsSlider from '@views/dashboards/analytics/WebsiteAnalyticsSlider'
|
||||
import LineAreaDailySalesChart from '@views/dashboards/analytics/LineAreaDailySalesChart'
|
||||
import SalesOverview from '@views/dashboards/analytics/SalesOverview'
|
||||
import EarningReports from '@views/dashboards/analytics/EarningReports'
|
||||
import SupportTracker from '@views/dashboards/analytics/SupportTracker'
|
||||
import SalesByCountries from '@views/dashboards/analytics/SalesByCountries'
|
||||
import TotalEarning from '@views/dashboards/analytics/TotalEarning'
|
||||
import MonthlyCampaignState from '@views/dashboards/analytics/MonthlyCampaignState'
|
||||
import SourceVisits from '@views/dashboards/analytics/SourceVisits'
|
||||
import ProjectsTable from '@views/dashboards/analytics/ProjectsTable'
|
||||
|
||||
/**
|
||||
* ! 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 `/pages/profile` 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 getProfileData = async () => {
|
||||
// Vars
|
||||
const res = await fetch(`${process.env.API_URL}/pages/profile`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch profileData')
|
||||
}
|
||||
|
||||
return res.json()
|
||||
} */
|
||||
|
||||
const DashboardAnalytics = async () => {
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, lg: 6 }}>
|
||||
<WebsiteAnalyticsSlider />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
|
||||
<LineAreaDailySalesChart />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
|
||||
<SalesOverview />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<EarningReports />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<SupportTracker />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<SalesByCountries />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<TotalEarning />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<MonthlyCampaignState />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<SourceVisits />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 8 }}>
|
||||
<ProjectsTable projectTable={[]} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardAnalytics
|
||||
@ -1,356 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
BarChart,
|
||||
Bar,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell
|
||||
} from 'recharts'
|
||||
import { useSalesAnalytics } from '../../../../../../services/queries/analytics'
|
||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||
import PickerBasic from '../../../../../../components/date-picker/PickerBasic'
|
||||
import { formatDateDDMMYYYY } from '../../../../../../utils/transform'
|
||||
|
||||
// Tabler icons component
|
||||
const TablerIcon = ({ name = '', className = '' }) => <i className={`tabler-${name} ${className}`} />
|
||||
|
||||
const DashboardCRM = () => {
|
||||
const today = new Date()
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const [dateFrom, setDateFrom] = useState<Date | null>(monthAgo)
|
||||
const [dateTo, setDateTo] = useState<Date | null>(today)
|
||||
|
||||
const { data: analytics, isLoading } = useSalesAnalytics({
|
||||
date_from: formatDateDDMMYYYY(dateFrom!),
|
||||
date_to: formatDateDDMMYYYY(dateTo!)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (analytics) {
|
||||
setDateFrom(new Date(analytics.date_from))
|
||||
setDateTo(new Date(analytics.date_to))
|
||||
}
|
||||
}, [analytics])
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount: any) => {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
// Format date
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// Format long date
|
||||
const formatLongDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// Prepare chart data
|
||||
const chartData = analytics?.data.map((item: any) => ({
|
||||
date: formatDate(item.date),
|
||||
sales: item.sales,
|
||||
orders: item.orders,
|
||||
items: item.items,
|
||||
averageOrderValue: item.orders > 0 ? Math.round(item.sales / item.orders) : 0
|
||||
}))
|
||||
|
||||
// Colors for charts
|
||||
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']
|
||||
|
||||
// Metric cards data
|
||||
const metrics = [
|
||||
{
|
||||
title: 'Total Sales',
|
||||
value: formatCurrency(analytics?.summary.total_sales),
|
||||
icon: 'currency-dollar',
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
change: '+12.5%',
|
||||
changeColor: 'text-green-600'
|
||||
},
|
||||
{
|
||||
title: 'Total Orders',
|
||||
value: analytics?.summary.total_orders.toLocaleString(),
|
||||
icon: 'shopping-cart',
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50',
|
||||
change: '+8.2%',
|
||||
changeColor: 'text-blue-600'
|
||||
},
|
||||
{
|
||||
title: 'Total Items',
|
||||
value: analytics?.summary.total_items.toLocaleString(),
|
||||
icon: 'package',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50',
|
||||
change: '+15.1%',
|
||||
changeColor: 'text-purple-600'
|
||||
},
|
||||
{
|
||||
title: 'Average Order Value',
|
||||
value: formatCurrency(analytics?.summary.average_order_value),
|
||||
icon: 'trending-up',
|
||||
color: 'text-orange-600',
|
||||
bgColor: 'bg-orange-50',
|
||||
change: '+4.3%',
|
||||
changeColor: 'text-orange-600'
|
||||
}
|
||||
]
|
||||
|
||||
// Performance data for pie chart
|
||||
const performanceData = analytics?.data.map((item: any, index: any) => ({
|
||||
name: formatDate(item.date),
|
||||
value: item.sales,
|
||||
color: colors[index % colors.length]
|
||||
}))
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<div className='mx-auto space-y-6'>
|
||||
{/* Header */}
|
||||
<div className='flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8'>
|
||||
<div>
|
||||
<h1 className='text-4xl font-bold text-gray-800 mb-2'>Sales Analytics Dashboard</h1>
|
||||
<PickerBasic dateFrom={dateFrom} dateTo={dateTo} onChangeDateFrom={setDateFrom} onChangeDateTo={setDateTo} />
|
||||
</div>
|
||||
<div className='flex gap-3'>
|
||||
<div className='flex items-center gap-2 bg-blue-50 px-4 py-2 rounded-full'>
|
||||
<TablerIcon name='calendar' className='text-blue-600 text-sm' />
|
||||
<span className='text-blue-700 font-medium'>Grouped by {analytics?.group_by}</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 bg-purple-50 px-4 py-2 rounded-full'>
|
||||
<TablerIcon name='chart-line' className='text-purple-600 text-sm' />
|
||||
<span className='text-purple-700 font-medium'>{analytics?.data.length} data points</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Cards */}
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6'>
|
||||
{metrics.map((metric, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100'
|
||||
>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-start justify-between'>
|
||||
<div className='flex-1'>
|
||||
<p className='text-gray-500 text-sm font-medium mb-2'>{metric.title}</p>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-3'>{metric.value}</h3>
|
||||
<div className='flex items-center gap-1'>
|
||||
<TablerIcon name='trending-up' className={`text-xs ${metric.changeColor}`} />
|
||||
<span className={`text-sm font-medium ${metric.changeColor}`}>{metric.change}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-4 rounded-xl ${metric.bgColor}`}>
|
||||
<TablerIcon name={metric.icon} className={`text-2xl ${metric.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className='grid grid-cols-1 lg:grid-cols-3 gap-6'>
|
||||
{/* Sales Trend Chart */}
|
||||
<div className='lg:col-span-2 bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-line' className='text-blue-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Sales Trend</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||
<XAxis dataKey='date' stroke='#6b7280' />
|
||||
<YAxis tickFormatter={value => `${(value / 1000).toFixed(0)}K`} stroke='#6b7280' />
|
||||
<Tooltip
|
||||
formatter={(value, name) => [
|
||||
name === 'sales' ? formatCurrency(value) : value.toLocaleString(),
|
||||
name === 'sales' ? 'Sales' : name === 'orders' ? 'Orders' : 'Items'
|
||||
]}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='sales'
|
||||
stroke='#3B82F6'
|
||||
strokeWidth={3}
|
||||
dot={{ fill: '#3B82F6', strokeWidth: 2, r: 5 }}
|
||||
activeDot={{ r: 7, stroke: '#3B82F6', strokeWidth: 2 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sales Distribution */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-pie' className='text-purple-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Sales Distribution</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={performanceData}
|
||||
cx='50%'
|
||||
cy='50%'
|
||||
outerRadius={90}
|
||||
dataKey='value'
|
||||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||
>
|
||||
{performanceData?.map((entry: any, index: any) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={value => [formatCurrency(value), 'Sales']}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Orders & Items Chart */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-bar' className='text-green-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Orders & Items Analysis</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||
<XAxis dataKey='date' stroke='#6b7280' />
|
||||
<YAxis stroke='#6b7280' />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey='orders' fill='#10B981' name='Orders' radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey='items' fill='#3B82F6' name='Items' radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed Data Table */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='table' className='text-indigo-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Daily Performance Details</h2>
|
||||
</div>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='bg-gray-50 border-b border-gray-200'>
|
||||
<th className='text-left py-4 px-6 font-semibold text-gray-700'>Date</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Sales</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Orders</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Items</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Avg Order Value</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Net Sales</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{analytics?.data.map((row: any, index: any) => (
|
||||
<tr key={index} className='border-b border-gray-100 hover:bg-gray-50 transition-colors'>
|
||||
<td className='py-4 px-6 font-medium text-gray-800'>{formatLongDate(row.date)}</td>
|
||||
<td className='py-4 px-6 text-right font-semibold text-green-600'>{formatCurrency(row.sales)}</td>
|
||||
<td className='py-4 px-6 text-right text-gray-700'>{row.orders.toLocaleString()}</td>
|
||||
<td className='py-4 px-6 text-right text-gray-700'>{row.items.toLocaleString()}</td>
|
||||
<td className='py-4 px-6 text-right font-medium text-gray-800'>
|
||||
{formatCurrency(row.orders > 0 ? Math.round(row.sales / row.orders) : 0)}
|
||||
</td>
|
||||
<td className='py-4 px-6 text-right font-semibold text-gray-800'>
|
||||
{formatCurrency(row.net_sales)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Footer */}
|
||||
<div className='bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-100'>
|
||||
<div className='p-6'>
|
||||
<div className='grid grid-cols-2 md:grid-cols-4 gap-6'>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{formatCurrency(analytics?.summary.net_sales)}</h3>
|
||||
<p className='text-gray-600 font-medium'>Net Sales</p>
|
||||
</div>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{analytics?.summary.total_orders}</h3>
|
||||
<p className='text-gray-600 font-medium'>Total Orders</p>
|
||||
</div>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{formatCurrency(analytics?.summary.total_tax)}</h3>
|
||||
<p className='text-gray-600 font-medium'>Total Tax</p>
|
||||
</div>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>
|
||||
{formatCurrency(analytics?.summary.total_discount)}
|
||||
</h3>
|
||||
<p className='text-gray-600 font-medium'>Total Discount</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardCRM
|
||||
@ -1,348 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
BarChart,
|
||||
Bar,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell
|
||||
} from 'recharts'
|
||||
import PickerBasic from '../../../../../../../components/date-picker/PickerBasic'
|
||||
import Loading from '../../../../../../../components/layout/shared/Loading'
|
||||
import { useSalesAnalytics } from '../../../../../../../services/queries/analytics'
|
||||
import { formatDateDDMMYYYY } from '../../../../../../../utils/transform'
|
||||
|
||||
// Tabler icons component
|
||||
const TablerIcon = ({ name = '', className = '' }) => <i className={`tabler-${name} ${className}`} />
|
||||
|
||||
const EcomerceOrderReport = () => {
|
||||
const today = new Date()
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const [dateFrom, setDateFrom] = useState<Date | null>(monthAgo)
|
||||
const [dateTo, setDateTo] = useState<Date | null>(today)
|
||||
|
||||
const { data: analytics, isLoading } = useSalesAnalytics({
|
||||
date_from: formatDateDDMMYYYY(dateFrom!),
|
||||
date_to: formatDateDDMMYYYY(dateTo!)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (analytics) {
|
||||
setDateFrom(new Date(analytics.date_from))
|
||||
setDateTo(new Date(analytics.date_to))
|
||||
}
|
||||
}, [analytics])
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount: any) => {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
// Format date
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// Format long date
|
||||
const formatLongDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// Prepare chart data
|
||||
const chartData = analytics?.data.map((item: any) => ({
|
||||
date: formatDate(item.date),
|
||||
sales: item.sales,
|
||||
orders: item.orders,
|
||||
items: item.items,
|
||||
averageOrderValue: item.orders > 0 ? Math.round(item.sales / item.orders) : 0
|
||||
}))
|
||||
|
||||
// Colors for charts
|
||||
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']
|
||||
|
||||
// Metric cards data
|
||||
const metrics = [
|
||||
{
|
||||
title: 'Total Sales',
|
||||
value: formatCurrency(analytics?.summary.total_sales),
|
||||
icon: 'currency-dollar',
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
change: '+12.5%',
|
||||
changeColor: 'text-green-600'
|
||||
},
|
||||
{
|
||||
title: 'Total Orders',
|
||||
value: analytics?.summary.total_orders.toLocaleString(),
|
||||
icon: 'shopping-cart',
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50',
|
||||
change: '+8.2%',
|
||||
changeColor: 'text-blue-600'
|
||||
},
|
||||
{
|
||||
title: 'Total Items',
|
||||
value: analytics?.summary.total_items.toLocaleString(),
|
||||
icon: 'package',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50',
|
||||
change: '+15.1%',
|
||||
changeColor: 'text-purple-600'
|
||||
},
|
||||
{
|
||||
title: 'Average Order Value',
|
||||
value: formatCurrency(analytics?.summary.average_order_value),
|
||||
icon: 'trending-up',
|
||||
color: 'text-orange-600',
|
||||
bgColor: 'bg-orange-50',
|
||||
change: '+4.3%',
|
||||
changeColor: 'text-orange-600'
|
||||
}
|
||||
]
|
||||
|
||||
// Performance data for pie chart
|
||||
const performanceData = analytics?.data.map((item: any, index: any) => ({
|
||||
name: formatDate(item.date),
|
||||
value: item.sales,
|
||||
color: colors[index % colors.length]
|
||||
}))
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<div className='mx-auto space-y-6'>
|
||||
{/* Header */}
|
||||
<div className='flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8 bg-white shadow p-6 rounded-xl'>
|
||||
<div>
|
||||
<h1 className='text-4xl font-bold text-gray-800 mb-2'>Sales Analytics</h1>
|
||||
</div>
|
||||
<div className='flex gap-3'>
|
||||
<PickerBasic dateFrom={dateFrom} dateTo={dateTo} onChangeDateFrom={setDateFrom} onChangeDateTo={setDateTo} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Cards */}
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6'>
|
||||
{metrics.map((metric, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100'
|
||||
>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-start justify-between'>
|
||||
<div className='flex-1'>
|
||||
<p className='text-gray-500 text-sm font-medium mb-2'>{metric.title}</p>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-3'>{metric.value}</h3>
|
||||
<div className='flex items-center gap-1'>
|
||||
<TablerIcon name='trending-up' className={`text-xs ${metric.changeColor}`} />
|
||||
<span className={`text-sm font-medium ${metric.changeColor}`}>{metric.change}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-4 rounded-xl ${metric.bgColor}`}>
|
||||
<TablerIcon name={metric.icon} className={`text-2xl ${metric.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className='grid grid-cols-1 lg:grid-cols-3 gap-6'>
|
||||
{/* Sales Trend Chart */}
|
||||
<div className='lg:col-span-2 bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-line' className='text-blue-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Sales Trend</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||
<XAxis dataKey='date' stroke='#6b7280' />
|
||||
<YAxis tickFormatter={value => `${(value / 1000).toFixed(0)}K`} stroke='#6b7280' />
|
||||
<Tooltip
|
||||
formatter={(value, name) => [
|
||||
name === 'sales' ? formatCurrency(value) : value.toLocaleString(),
|
||||
name === 'sales' ? 'Sales' : name === 'orders' ? 'Orders' : 'Items'
|
||||
]}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='sales'
|
||||
stroke='#3B82F6'
|
||||
strokeWidth={3}
|
||||
dot={{ fill: '#3B82F6', strokeWidth: 2, r: 5 }}
|
||||
activeDot={{ r: 7, stroke: '#3B82F6', strokeWidth: 2 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sales Distribution */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-pie' className='text-purple-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Sales Distribution</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={performanceData}
|
||||
cx='50%'
|
||||
cy='50%'
|
||||
outerRadius={90}
|
||||
dataKey='value'
|
||||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||
>
|
||||
{performanceData?.map((entry: any, index: any) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={value => [formatCurrency(value), 'Sales']}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Orders & Items Chart */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-bar' className='text-green-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Orders & Items Analysis</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||
<XAxis dataKey='date' stroke='#6b7280' />
|
||||
<YAxis stroke='#6b7280' />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey='orders' fill='#10B981' name='Orders' radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey='items' fill='#3B82F6' name='Items' radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed Data Table */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='table' className='text-indigo-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Daily Performance Details</h2>
|
||||
</div>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='bg-gray-50 border-b border-gray-200'>
|
||||
<th className='text-left py-4 px-6 font-semibold text-gray-700'>Date</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Sales</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Orders</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Items</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Avg Order Value</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Net Sales</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{analytics?.data.map((row: any, index: any) => (
|
||||
<tr key={index} className='border-b border-gray-100 hover:bg-gray-50 transition-colors'>
|
||||
<td className='py-4 px-6 font-medium text-gray-800'>{formatLongDate(row.date)}</td>
|
||||
<td className='py-4 px-6 text-right font-semibold text-green-600'>{formatCurrency(row.sales)}</td>
|
||||
<td className='py-4 px-6 text-right text-gray-700'>{row.orders.toLocaleString()}</td>
|
||||
<td className='py-4 px-6 text-right text-gray-700'>{row.items.toLocaleString()}</td>
|
||||
<td className='py-4 px-6 text-right font-medium text-gray-800'>
|
||||
{formatCurrency(row.orders > 0 ? Math.round(row.sales / row.orders) : 0)}
|
||||
</td>
|
||||
<td className='py-4 px-6 text-right font-semibold text-gray-800'>
|
||||
{formatCurrency(row.net_sales)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Footer */}
|
||||
<div className='bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-100'>
|
||||
<div className='p-6'>
|
||||
<div className='grid grid-cols-2 md:grid-cols-4 gap-6'>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{formatCurrency(analytics?.summary.net_sales)}</h3>
|
||||
<p className='text-gray-600 font-medium'>Net Sales</p>
|
||||
</div>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{analytics?.summary.total_orders}</h3>
|
||||
<p className='text-gray-600 font-medium'>Total Orders</p>
|
||||
</div>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{formatCurrency(analytics?.summary.total_tax)}</h3>
|
||||
<p className='text-gray-600 font-medium'>Total Tax</p>
|
||||
</div>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>
|
||||
{formatCurrency(analytics?.summary.total_discount)}
|
||||
</h3>
|
||||
<p className='text-gray-600 font-medium'>Total Discount</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EcomerceOrderReport
|
||||
@ -1,7 +0,0 @@
|
||||
import EcommerceDashboard from '../../apps/ecommerce/dashboard/page'
|
||||
|
||||
const DashboardECommerce = () => {
|
||||
return <EcommerceDashboard />
|
||||
}
|
||||
|
||||
export default DashboardECommerce
|
||||
@ -1,397 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
LineChart,
|
||||
Line
|
||||
} from 'recharts'
|
||||
import { useProductSalesAnalytics } from '../../../../../../../services/queries/analytics'
|
||||
import Loading from '../../../../../../../components/layout/shared/Loading'
|
||||
import { formatDateDDMMYYYY } from '../../../../../../../utils/transform'
|
||||
import PickerBasic from '../../../../../../../components/date-picker/PickerBasic'
|
||||
|
||||
// Tabler icons component
|
||||
const TablerIcon = ({ name = '', className = '' }) => <i className={`tabler-${name} ${className}`} />
|
||||
|
||||
const EcomerceProductReport = () => {
|
||||
const today = new Date()
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const [dateFrom, setDateFrom] = useState<Date | null>(monthAgo)
|
||||
const [dateTo, setDateTo] = useState<Date | null>(today)
|
||||
const [selectedCategory, setSelectedCategory] = useState('all')
|
||||
const [sortBy, setSortBy] = useState('revenue')
|
||||
const [sortOrder, setSortOrder] = useState('desc')
|
||||
|
||||
const { data: analytics, isLoading } = useProductSalesAnalytics({
|
||||
date_from: formatDateDDMMYYYY(dateFrom!),
|
||||
date_to: formatDateDDMMYYYY(dateTo!)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (analytics) {
|
||||
setDateFrom(new Date(analytics.date_from))
|
||||
setDateTo(new Date(analytics.date_to))
|
||||
}
|
||||
}, [analytics])
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount: any) => {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
// Calculate summary metrics
|
||||
const summary = {
|
||||
totalProducts: analytics?.data.length,
|
||||
totalQuantitySold: analytics?.data.reduce((sum, item) => sum + item.quantity_sold, 0),
|
||||
totalRevenue: analytics?.data.reduce((sum, item) => sum + item.revenue, 0),
|
||||
totalOrders: analytics?.data.reduce((sum, item) => sum + item.order_count, 0),
|
||||
averageOrderValue: analytics?.data
|
||||
? analytics!.data.reduce((sum, item) => sum + item.revenue, 0) /
|
||||
analytics!.data.reduce((sum, item) => sum + item.order_count, 0)
|
||||
: 0
|
||||
}
|
||||
|
||||
// Get unique categories
|
||||
const categories = ['all', ...new Set(analytics?.data.map(item => item.category_name))]
|
||||
|
||||
// Filter and sort data
|
||||
const filteredData = analytics?.data
|
||||
.filter(item => selectedCategory === 'all' || item.category_name === selectedCategory)
|
||||
.sort((a: any, b: any) => {
|
||||
const multiplier = sortOrder === 'desc' ? -1 : 1
|
||||
return (a[sortBy] - b[sortBy]) * multiplier
|
||||
})
|
||||
|
||||
// Prepare chart data
|
||||
const chartData = filteredData?.slice(0, 10).map(item => ({
|
||||
name: item.product_name.length > 15 ? item.product_name.substring(0, 15) + '...' : item.product_name,
|
||||
fullName: item.product_name,
|
||||
revenue: item.revenue,
|
||||
quantity: item.quantity_sold,
|
||||
orders: item.order_count,
|
||||
avgPrice: item.average_price
|
||||
}))
|
||||
|
||||
// Category performance data
|
||||
const categoryData = categories
|
||||
.filter(cat => cat !== 'all')
|
||||
.map(category => {
|
||||
const categoryItems = analytics?.data.filter(item => item.category_name === category)
|
||||
return {
|
||||
name: category,
|
||||
revenue: categoryItems?.reduce((sum, item) => sum + item.revenue, 0),
|
||||
quantity: categoryItems?.reduce((sum, item) => sum + item.quantity_sold, 0),
|
||||
products: categoryItems?.length
|
||||
}
|
||||
})
|
||||
|
||||
// Colors for charts
|
||||
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#06B6D4', '#84CC16', '#F97316']
|
||||
|
||||
// Metric cards data
|
||||
const metrics = [
|
||||
{
|
||||
title: 'Total Revenue',
|
||||
value: formatCurrency(summary.totalRevenue),
|
||||
icon: 'currency-dollar',
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
subtitle: 'From all products'
|
||||
},
|
||||
{
|
||||
title: 'Products Sold',
|
||||
value: summary.totalQuantitySold?.toLocaleString(),
|
||||
icon: 'package',
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50',
|
||||
subtitle: `${summary.totalProducts} different products`
|
||||
},
|
||||
{
|
||||
title: 'Total Orders',
|
||||
value: summary.totalOrders?.toLocaleString(),
|
||||
icon: 'shopping-cart',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50',
|
||||
subtitle: 'Completed orders'
|
||||
},
|
||||
{
|
||||
title: 'Avg Order Value',
|
||||
value: formatCurrency(summary.averageOrderValue || 0),
|
||||
icon: 'trending-up',
|
||||
color: 'text-orange-600',
|
||||
bgColor: 'bg-orange-50',
|
||||
subtitle: 'Per order average'
|
||||
}
|
||||
]
|
||||
|
||||
const handleSort = (field: any) => {
|
||||
if (sortBy === field) {
|
||||
setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc')
|
||||
} else {
|
||||
setSortBy(field)
|
||||
setSortOrder('desc')
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<div className='mx-auto space-y-6'>
|
||||
{/* Header */}
|
||||
<div className='flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 mb-8 bg-white shadow p-6 rounded-xl'>
|
||||
<div>
|
||||
<h1 className='text-4xl font-bold text-gray-800 mb-2'>Product Analytics</h1>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className='flex flex-col sm:flex-row gap-4'>
|
||||
<PickerBasic dateFrom={dateFrom} dateTo={dateTo} onChangeDateFrom={setDateFrom} onChangeDateTo={setDateTo} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6'>
|
||||
{metrics.map((metric, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100'
|
||||
>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-start justify-between'>
|
||||
<div className='flex-1'>
|
||||
<p className='text-gray-500 text-sm font-medium mb-2'>{metric.title}</p>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{metric.value}</h3>
|
||||
<p className='text-gray-500 text-xs'>{metric.subtitle}</p>
|
||||
</div>
|
||||
<div className={`p-4 rounded-xl ${metric.bgColor}`}>
|
||||
<TablerIcon name={metric.icon} className={`text-2xl ${metric.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className='grid grid-cols-1 xl:grid-cols-3 gap-6'>
|
||||
{/* Product Revenue Chart */}
|
||||
<div className='xl:col-span-2 bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-bar' className='text-blue-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Top Products by Revenue</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<BarChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 60 }}>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||
<XAxis dataKey='name' stroke='#6b7280' angle={-45} textAnchor='end' height={100} fontSize={12} />
|
||||
<YAxis tickFormatter={value => `${(value / 1000).toFixed(0)}K`} stroke='#6b7280' />
|
||||
<Tooltip
|
||||
formatter={(value, name) => [
|
||||
name === 'revenue' ? formatCurrency(value) : value.toLocaleString(),
|
||||
name === 'revenue' ? 'Revenue' : name === 'quantity' ? 'Quantity Sold' : 'Orders'
|
||||
]}
|
||||
labelFormatter={label => {
|
||||
const item = chartData?.find(d => d.name === label)
|
||||
return item ? item.fullName : label
|
||||
}}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey='revenue' fill='#3B82F6' radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Distribution */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-pie' className='text-purple-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Category Distribution</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={categoryData}
|
||||
cx='50%'
|
||||
cy='50%'
|
||||
outerRadius={80}
|
||||
dataKey='revenue'
|
||||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||
>
|
||||
{categoryData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={value => [formatCurrency(value), 'Revenue']}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quantity vs Orders Chart */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-line' className='text-green-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Quantity Sold vs Order Count</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||
<XAxis dataKey='name' stroke='#6b7280' angle={-45} textAnchor='end' height={100} fontSize={12} />
|
||||
<YAxis stroke='#6b7280' />
|
||||
<Tooltip
|
||||
labelFormatter={label => {
|
||||
const item = chartData?.find(d => d.name === label)
|
||||
return item ? item.fullName : label
|
||||
}}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey='quantity' fill='#10B981' name='Quantity Sold' radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey='orders' fill='#F59E0B' name='Order Count' radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed Products Table */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center justify-between mb-6'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<TablerIcon name='table' className='text-indigo-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Product Performance Details</h2>
|
||||
</div>
|
||||
<div className='text-sm text-gray-500'>Showing {filteredData?.length} products</div>
|
||||
</div>
|
||||
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='bg-gray-50 border-b border-gray-200'>
|
||||
<th className='text-left py-4 px-6 font-semibold text-gray-700'>Product</th>
|
||||
<th className='text-left py-4 px-6 font-semibold text-gray-700'>Category</th>
|
||||
<th
|
||||
className='text-right py-4 px-6 font-semibold text-gray-700 cursor-pointer hover:text-blue-600 transition-colors'
|
||||
onClick={() => handleSort('quantity_sold')}
|
||||
>
|
||||
<div className='flex items-center justify-end gap-1'>
|
||||
Quantity Sold
|
||||
<TablerIcon
|
||||
name={sortBy === 'quantity_sold' && sortOrder === 'desc' ? 'chevron-down' : 'chevron-up'}
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className='text-right py-4 px-6 font-semibold text-gray-700 cursor-pointer hover:text-blue-600 transition-colors'
|
||||
onClick={() => handleSort('revenue')}
|
||||
>
|
||||
<div className='flex items-center justify-end gap-1'>
|
||||
Revenue
|
||||
<TablerIcon
|
||||
name={sortBy === 'revenue' && sortOrder === 'desc' ? 'chevron-down' : 'chevron-up'}
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className='text-right py-4 px-6 font-semibold text-gray-700 cursor-pointer hover:text-blue-600 transition-colors'
|
||||
onClick={() => handleSort('average_price')}
|
||||
>
|
||||
<div className='flex items-center justify-end gap-1'>
|
||||
Avg Price
|
||||
<TablerIcon
|
||||
name={sortBy === 'average_price' && sortOrder === 'desc' ? 'chevron-down' : 'chevron-up'}
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className='text-right py-4 px-6 font-semibold text-gray-700 cursor-pointer hover:text-blue-600 transition-colors'
|
||||
onClick={() => handleSort('order_count')}
|
||||
>
|
||||
<div className='flex items-center justify-end gap-1'>
|
||||
Orders
|
||||
<TablerIcon
|
||||
name={sortBy === 'order_count' && sortOrder === 'desc' ? 'chevron-down' : 'chevron-up'}
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredData?.map((product, index) => (
|
||||
<tr key={product.product_id} className='border-b border-gray-100 hover:bg-gray-50 transition-colors'>
|
||||
<td className='py-4 px-6'>
|
||||
<div className='font-medium text-gray-800'>{product.product_name}</div>
|
||||
</td>
|
||||
<td className='py-4 px-6'>
|
||||
<span className='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800'>
|
||||
{product.category_name}
|
||||
</span>
|
||||
</td>
|
||||
<td className='py-4 px-6 text-right font-semibold text-gray-800'>
|
||||
{product.quantity_sold.toLocaleString()}
|
||||
</td>
|
||||
<td className='py-4 px-6 text-right font-semibold text-green-600'>
|
||||
{formatCurrency(product.revenue)}
|
||||
</td>
|
||||
<td className='py-4 px-6 text-right text-gray-700'>{formatCurrency(product.average_price)}</td>
|
||||
<td className='py-4 px-6 text-right text-gray-700'>{product.order_count.toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EcomerceProductReport
|
||||
@ -1,355 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const TablerIcon = ({ name = '', className = '' }) => <i className={`tabler-${name} ${className}`} />;
|
||||
|
||||
const PaymentMethodAnalytics = () => {
|
||||
// Default data structure for demonstration
|
||||
const defaultData = {
|
||||
"organization_id": "4ecf5cfb-9ac1-4fbd-94e4-9f149f07460a",
|
||||
"outlet_id": "d5b38fc4-8df7-4d54-99e2-b8825977f5ca",
|
||||
"date_from": "2025-07-05T00:00:00+07:00",
|
||||
"date_to": "2025-08-05T23:59:59.999999999+07:00",
|
||||
"group_by": "day",
|
||||
"summary": {
|
||||
"total_amount": 212000,
|
||||
"total_orders": 6,
|
||||
"total_payments": 6,
|
||||
"average_order_value": 35333.333333333336
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
"payment_method_id": "4b1c0d21-c98a-4fc0-a2f9-8d90a0c9d905",
|
||||
"payment_method_name": "CASH",
|
||||
"payment_method_type": "cash",
|
||||
"total_amount": 212000,
|
||||
"order_count": 6,
|
||||
"payment_count": 6,
|
||||
"percentage": 100
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const analyticsData = defaultData;
|
||||
const { summary, data: paymentMethods } = analyticsData;
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount: any) => {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// Format date
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
// Get icon for payment method
|
||||
const getPaymentMethodIcon = (type: any) => {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'cash':
|
||||
return <TablerIcon name="cash" className="w-5 h-5" />;
|
||||
case 'card':
|
||||
case 'credit_card':
|
||||
case 'debit_card':
|
||||
return <TablerIcon name="credit-card" className="w-5 h-5" />;
|
||||
default:
|
||||
return <TablerIcon name="coins" className="w-5 h-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
// Get color classes for payment method type
|
||||
const getPaymentMethodColors = (type: any) => {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'cash':
|
||||
return {
|
||||
chip: 'bg-green-100 text-green-800 border-green-200',
|
||||
progress: 'bg-green-500'
|
||||
};
|
||||
case 'card':
|
||||
case 'credit_card':
|
||||
case 'debit_card':
|
||||
return {
|
||||
chip: 'bg-blue-100 text-blue-800 border-blue-200',
|
||||
progress: 'bg-blue-500'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
chip: 'bg-gray-100 text-gray-800 border-gray-200',
|
||||
progress: 'bg-gray-500'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Progress Bar Component
|
||||
const ProgressBar = ({ value, className, color }: any) => {
|
||||
return (
|
||||
<div className={`w-full bg-gray-200 rounded-full h-2 ${className}`}>
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-300 ${color}`}
|
||||
style={{ width: `${Math.min(value, 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Chip Component
|
||||
const Chip = ({ label, colorClasses }: any) => {
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${colorClasses}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto space-y-6 bg-gray-50 min-h-screen">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 bg-blue-600 rounded-lg">
|
||||
<TablerIcon name="trending-up" className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
Payment Analytics
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<TablerIcon name="calendar" className="w-4 h-4 text-gray-500" />
|
||||
<p className="text-sm text-gray-600">
|
||||
{formatDate(analyticsData.date_from)} - {formatDate(analyticsData.date_to)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div className="p-6 bg-gradient-to-br from-blue-50 to-blue-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-blue-600 mb-1">
|
||||
Total Revenue
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-blue-900">
|
||||
{formatCurrency(summary.total_amount)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-blue-600 rounded-full">
|
||||
<TablerIcon name="receipt" className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div className="p-6 bg-gradient-to-br from-green-50 to-green-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-green-600 mb-1">
|
||||
Total Orders
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-green-900">
|
||||
{summary.total_orders.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-green-600 rounded-full">
|
||||
<TablerIcon name="cash" className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div className="p-6 bg-gradient-to-br from-purple-50 to-purple-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-purple-600 mb-1">
|
||||
Total Payments
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-purple-900">
|
||||
{summary.total_payments.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-purple-600 rounded-full">
|
||||
<TablerIcon name="credit-card" className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div className="p-6 bg-gradient-to-br from-orange-50 to-orange-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-orange-600 mb-1">
|
||||
Avg Order Value
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-orange-900">
|
||||
{formatCurrency(summary.average_order_value)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-orange-600 rounded-full">
|
||||
<TablerIcon name="trending-up" className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Methods Breakdown */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<TablerIcon name="credit-card" className="w-6 h-6 text-blue-600" />
|
||||
<h2 className="text-xl font-bold text-gray-900">
|
||||
Payment Methods Breakdown
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b border-gray-200">
|
||||
<th className="text-left p-4 font-semibold text-gray-900">Payment Method</th>
|
||||
<th className="text-left p-4 font-semibold text-gray-900">Type</th>
|
||||
<th className="text-right p-4 font-semibold text-gray-900">Amount</th>
|
||||
<th className="text-right p-4 font-semibold text-gray-900">Orders</th>
|
||||
<th className="text-right p-4 font-semibold text-gray-900">Payments</th>
|
||||
<th className="text-right p-4 font-semibold text-gray-900">Percentage</th>
|
||||
<th className="text-left p-4 font-semibold text-gray-900">Usage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paymentMethods.map((method, index) => {
|
||||
const colors = getPaymentMethodColors(method.payment_method_type);
|
||||
return (
|
||||
<tr key={method.payment_method_id || index} className="border-b border-gray-100 hover:bg-gray-50 transition-colors">
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{getPaymentMethodIcon(method.payment_method_type)}
|
||||
<span className="font-medium text-gray-900">
|
||||
{method.payment_method_name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<Chip
|
||||
label={method.payment_method_type.toUpperCase()}
|
||||
colorClasses={colors.chip}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-4 text-right font-semibold text-gray-900">
|
||||
{formatCurrency(method.total_amount)}
|
||||
</td>
|
||||
<td className="p-4 text-right text-gray-700">
|
||||
{method.order_count.toLocaleString()}
|
||||
</td>
|
||||
<td className="p-4 text-right text-gray-700">
|
||||
{method.payment_count.toLocaleString()}
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
<span className="font-bold text-blue-600">
|
||||
{method.percentage.toFixed(1)}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<ProgressBar
|
||||
value={method.percentage}
|
||||
className="flex-1"
|
||||
color={colors.progress}
|
||||
/>
|
||||
<span className="text-xs text-gray-500 min-w-fit">
|
||||
{method.percentage.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{paymentMethods.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<TablerIcon name="credit-card" className="w-12 h-12 text-gray-300 mx-auto mb-4" />
|
||||
<p className="text-gray-500">
|
||||
No payment method data available for the selected period.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Stats */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||
<div className="p-6">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">
|
||||
Key Insights
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 bg-blue-50 rounded-lg border border-blue-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<TablerIcon name="cash" className="w-5 h-5 text-blue-600" />
|
||||
<span className="text-sm font-medium text-blue-900">
|
||||
Most Used Payment Method
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-blue-900">
|
||||
{paymentMethods.length > 0 ? paymentMethods[0].payment_method_name : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 bg-green-50 rounded-lg border border-green-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<TablerIcon name="trending-up" className="w-5 h-5 text-green-600" />
|
||||
<span className="text-sm font-medium text-green-900">
|
||||
Revenue per Payment
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-green-900">
|
||||
{formatCurrency(summary.total_payments > 0 ? summary.total_amount / summary.total_payments : 0)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 bg-purple-50 rounded-lg border border-purple-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<TablerIcon name="receipt" className="w-5 h-5 text-purple-600" />
|
||||
<span className="text-sm font-medium text-purple-900">
|
||||
Payment Success Rate
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-purple-900">
|
||||
{((summary.total_payments / summary.total_orders) * 100).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 bg-orange-50 rounded-lg border border-orange-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<TablerIcon name="coins" className="w-5 h-5 text-orange-600" />
|
||||
<span className="text-sm font-medium text-orange-900">
|
||||
Payment Methods Used
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-orange-900">
|
||||
{paymentMethods.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentMethodAnalytics;
|
||||
116
src/app/[lang]/(dashboard)/(private)/dashboards/orders/page.tsx
Normal file
116
src/app/[lang]/(dashboard)/(private)/dashboards/orders/page.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
||||
import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs'
|
||||
|
||||
// Server Action Imports
|
||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||
import { useSalesAnalytics } from '../../../../../../services/queries/analytics'
|
||||
|
||||
const DashboardOrder = () => {
|
||||
const { data, isLoading } = useSalesAnalytics()
|
||||
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
const transformSalesData = (data: any) => {
|
||||
return [
|
||||
{
|
||||
type: 'items',
|
||||
avatarIcon: 'tabler-shopping-cart',
|
||||
date: data.map((d: any) => formatDate(d.date)),
|
||||
series: [{ data: data.map((d: any) => d.items) }]
|
||||
},
|
||||
{
|
||||
type: 'orders',
|
||||
avatarIcon: 'tabler-chart-bar',
|
||||
date: data.map((d: any) => formatDate(d.date)),
|
||||
series: [{ data: data.map((d: any) => d.orders) }]
|
||||
},
|
||||
{
|
||||
type: 'sales',
|
||||
avatarIcon: 'tabler-currency-dollar',
|
||||
date: data.map((d: any) => formatDate(d.date)),
|
||||
series: [{ data: data.map((d: any) => d.sales) }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Items'
|
||||
value={data?.summary.total_items as number}
|
||||
avatarIcon={'tabler-package'}
|
||||
avatarColor='primary'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Orders'
|
||||
value={data?.summary.total_orders as number}
|
||||
avatarIcon={'tabler-shopping-cart'}
|
||||
avatarColor='info'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Average Orders'
|
||||
value={data?.summary.average_order_value as number}
|
||||
avatarIcon={'tabler-trending-up'}
|
||||
avatarColor='warning'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Sales'
|
||||
value={data?.summary.total_sales as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='success'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 12 }}>
|
||||
<EarningReportsWithTabs data={transformSalesData(data?.data)} />
|
||||
</Grid>
|
||||
{/* <Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<RadarSalesChart />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<SalesByCountries />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<ProjectStatus />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<ActiveProjects />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<LastTransaction />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<ActivityTimeline />
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardOrder
|
||||
@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
||||
import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs'
|
||||
|
||||
// Server Action Imports
|
||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||
import { useDashboardAnalytics } from '../../../../../../services/queries/analytics'
|
||||
import { DashboardReport, PaymentDataItem, ProductData, RecentSale } from '../../../../../../types/services/analytic'
|
||||
|
||||
const DashboardOverview = () => {
|
||||
const { data, isLoading } = useDashboardAnalytics()
|
||||
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
const transformSalesData = (data: DashboardReport) => {
|
||||
return [
|
||||
{
|
||||
type: 'products',
|
||||
avatarIcon: 'tabler-package',
|
||||
date: data.top_products.map((d: ProductData) => d.product_name),
|
||||
series: [{ data: data.top_products.map((d: ProductData) => d.revenue) }]
|
||||
},
|
||||
{
|
||||
type: 'orders',
|
||||
avatarIcon: 'tabler-shopping-cart',
|
||||
date: data.recent_sales.map((d: RecentSale) => formatDate(d.date)),
|
||||
series: [{ data: data.recent_sales.map((d: RecentSale) => d.net_sales) }]
|
||||
},
|
||||
{
|
||||
type: 'payments',
|
||||
avatarIcon: 'tabler-credit-card-pay',
|
||||
date: data.payment_methods.map((d: PaymentDataItem) => d.payment_method_name),
|
||||
series: [{ data: data.payment_methods.map((d: PaymentDataItem) => d.total_amount) }]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Customers'
|
||||
value={data?.overview.total_customers as number}
|
||||
avatarIcon={'tabler-users'}
|
||||
avatarColor='primary'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Orders'
|
||||
value={data?.overview.total_orders as number}
|
||||
avatarIcon={'tabler-package'}
|
||||
avatarColor='info'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Average Orders'
|
||||
value={data?.overview.average_order_value as number}
|
||||
avatarIcon={'tabler-trending-up'}
|
||||
avatarColor='warning'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Sales'
|
||||
value={data?.overview.total_sales as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='success'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 12 }}>
|
||||
<EarningReportsWithTabs data={transformSalesData(data!)} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardOverview
|
||||
@ -0,0 +1,98 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
||||
import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs'
|
||||
|
||||
// Server Action Imports
|
||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||
import { usePaymentAnalytics } from '../../../../../../services/queries/analytics'
|
||||
import { PaymentDataItem } from '../../../../../../types/services/analytic'
|
||||
|
||||
const DashboardPayment = () => {
|
||||
const { data, isLoading } = usePaymentAnalytics()
|
||||
|
||||
const transformSalesData = (data: PaymentDataItem[]) => {
|
||||
return [
|
||||
{
|
||||
type: 'payment',
|
||||
avatarIcon: 'tabler-package',
|
||||
date: data.map((d: PaymentDataItem) => d.payment_method_name),
|
||||
series: [{ data: data.map((d: PaymentDataItem) => d.total_amount) }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Orders'
|
||||
value={data?.summary.total_orders as number}
|
||||
avatarIcon={'tabler-shopping-cart'}
|
||||
avatarColor='primary'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Payment'
|
||||
value={data?.summary.total_payments as number}
|
||||
avatarIcon={'tabler-package'}
|
||||
avatarColor='info'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Average Orders'
|
||||
value={data?.summary.average_order_value as number}
|
||||
avatarIcon={'tabler-trending-up'}
|
||||
avatarColor='warning'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Amount'
|
||||
value={data?.summary.total_amount as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='success'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 12 }}>
|
||||
<EarningReportsWithTabs data={transformSalesData(data!.data)} />
|
||||
</Grid>
|
||||
{/* <Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<RadarSalesChart />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<SalesByCountries />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<ProjectStatus />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<ActiveProjects />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<LastTransaction />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<ActivityTimeline />
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardPayment
|
||||
@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
||||
import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs'
|
||||
|
||||
// Server Action Imports
|
||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||
import { useProductSalesAnalytics } from '../../../../../../services/queries/analytics'
|
||||
|
||||
const DashboardProduct = () => {
|
||||
const { data, isLoading } = useProductSalesAnalytics()
|
||||
|
||||
const summary = {
|
||||
totalProducts: data?.data.length,
|
||||
totalQuantitySold: data?.data.reduce((sum, item) => sum + item.quantity_sold, 0),
|
||||
totalRevenue: data?.data.reduce((sum, item) => sum + item.revenue, 0),
|
||||
totalOrders: data?.data.reduce((sum, item) => sum + item.order_count, 0),
|
||||
averageOrderValue: data?.data
|
||||
? data!.data.reduce((sum, item) => sum + item.revenue, 0) /
|
||||
data!.data.reduce((sum, item) => sum + item.order_count, 0)
|
||||
: 0
|
||||
}
|
||||
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
const transformSalesData = (data: any) => {
|
||||
return [
|
||||
{
|
||||
type: 'products',
|
||||
avatarIcon: 'tabler-package',
|
||||
date: data.map((d: any) => d.product_name),
|
||||
series: [{ data: data.map((d: any) => d.revenue) }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Orders'
|
||||
value={summary.totalOrders as number}
|
||||
avatarIcon={'tabler-shopping-cart'}
|
||||
avatarColor='primary'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Product Sold'
|
||||
value={summary.totalQuantitySold as number}
|
||||
avatarIcon={'tabler-package'}
|
||||
avatarColor='info'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Average Orders'
|
||||
value={summary.averageOrderValue as number}
|
||||
avatarIcon={'tabler-trending-up'}
|
||||
avatarColor='warning'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Sales'
|
||||
value={summary.totalRevenue as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='success'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 12 }}>
|
||||
<EarningReportsWithTabs data={transformSalesData(data?.data)} />
|
||||
</Grid>
|
||||
{/* <Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<RadarSalesChart />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<SalesByCountries />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<ProjectStatus />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<ActiveProjects />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<LastTransaction />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<ActivityTimeline />
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardProduct
|
||||
@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
||||
import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs'
|
||||
|
||||
// Server Action Imports
|
||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||
import { useProfitLossAnalytics } from '../../../../../../services/queries/analytics'
|
||||
import {
|
||||
ProductDataReport,
|
||||
ProfitLossReport
|
||||
} from '../../../../../../types/services/analytic'
|
||||
|
||||
function formatMetricName(metric: string): string {
|
||||
const nameMap: { [key: string]: string } = {
|
||||
revenue: 'Revenue',
|
||||
cost: 'Cost',
|
||||
gross_profit: 'Gross Profit',
|
||||
gross_profit_margin: 'Gross Profit Margin (%)',
|
||||
tax: 'Tax',
|
||||
discount: 'Discount',
|
||||
net_profit: 'Net Profit',
|
||||
net_profit_margin: 'Net Profit Margin (%)',
|
||||
orders: 'Orders'
|
||||
}
|
||||
|
||||
return nameMap[metric] || metric.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
||||
}
|
||||
|
||||
const DashboardProfitLoss = () => {
|
||||
const { data, isLoading } = useProfitLossAnalytics()
|
||||
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
const metrics = ['revenue', 'cost', '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)
|
||||
// }))
|
||||
// }
|
||||
]
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Cost'
|
||||
value={data?.summary.total_cost as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='primary'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Rvenue'
|
||||
value={data?.summary.total_revenue as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='info'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Gross Profit'
|
||||
value={data?.summary.gross_profit as number}
|
||||
avatarIcon={'tabler-trending-up'}
|
||||
avatarColor='warning'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Net Profit'
|
||||
value={data?.summary.net_profit as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='success'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 12 }}>
|
||||
<EarningReportsWithTabs data={transformSalesData(data!)} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardProfitLoss
|
||||
@ -81,17 +81,11 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
||||
menuSectionStyles={menuSectionStyles(verticalNavOptions, theme)}
|
||||
>
|
||||
<SubMenu label={dictionary['navigation'].dashboards} icon={<i className='tabler-smart-home' />}>
|
||||
<MenuItem href={`/${locale}/dashboards/crm`}>{dictionary['navigation'].crm}</MenuItem>
|
||||
<MenuItem href={`/${locale}/dashboards/analytics`}>{dictionary['navigation'].analytics}</MenuItem>
|
||||
<SubMenu label={dictionary['navigation'].eCommerce}>
|
||||
<MenuItem href={`/${locale}/dashboards/ecommerce/order`}>{dictionary['navigation'].orders}</MenuItem>
|
||||
<MenuItem href={`/${locale}/dashboards/ecommerce/product`}>{dictionary['navigation'].products}</MenuItem>
|
||||
</SubMenu>
|
||||
<SubMenu label={dictionary['navigation'].finance}>
|
||||
<MenuItem href={`/${locale}/dashboards/finance/payment-method`}>
|
||||
{dictionary['navigation'].paymentMethods}
|
||||
</MenuItem>
|
||||
</SubMenu>
|
||||
<MenuItem href={`/${locale}/dashboards/overview`}>{dictionary['navigation'].overview}</MenuItem>
|
||||
<MenuItem href={`/${locale}/dashboards/profit-loss`}>{dictionary['navigation'].profitloss}</MenuItem>
|
||||
<MenuItem href={`/${locale}/dashboards/products`}>{dictionary['navigation'].products}</MenuItem>
|
||||
<MenuItem href={`/${locale}/dashboards/orders`}>{dictionary['navigation'].orders}</MenuItem>
|
||||
<MenuItem href={`/${locale}/dashboards/payment-methods`}>{dictionary['navigation'].paymentMethods}</MenuItem>
|
||||
</SubMenu>
|
||||
<MenuSection label={dictionary['navigation'].appsPages}>
|
||||
<SubMenu label={dictionary['navigation'].eCommerce} icon={<i className='tabler-shopping-cart' />}>
|
||||
|
||||
@ -21,6 +21,8 @@
|
||||
"add": "يضيف",
|
||||
"addjustment": "تعديل",
|
||||
"category": "فئة",
|
||||
"overview": "نظرة عامة",
|
||||
"profitloss": "الربح والخسارة",
|
||||
"finance": "مالية",
|
||||
"paymentMethods": "طرق الدفع",
|
||||
"organization": "المنظمة",
|
||||
|
||||
@ -21,6 +21,8 @@
|
||||
"add": "Add",
|
||||
"addjustment": "Addjustment",
|
||||
"category": "Category",
|
||||
"overview": "Overview",
|
||||
"profitloss": "Profit Loss",
|
||||
"units": "Units",
|
||||
"finance": "Finance",
|
||||
"paymentMethods": "Payment Methods",
|
||||
|
||||
@ -21,6 +21,8 @@
|
||||
"add": "Ajouter",
|
||||
"addjustment": "Ajustement",
|
||||
"category": "Catégorie",
|
||||
"overview": "Aperçu",
|
||||
"profitloss": "Profit et perte",
|
||||
"finance": "Finance",
|
||||
"paymentMethods": "Méthodes de paiement",
|
||||
"organization": "Organisation",
|
||||
|
||||
@ -4,12 +4,14 @@ import { configureStore } from '@reduxjs/toolkit'
|
||||
import productReducer from '@/redux-store/slices/product'
|
||||
import customerReducer from '@/redux-store/slices/customer'
|
||||
import paymentMethodReducer from '@/redux-store/slices/paymentMethod'
|
||||
import ingredientReducer from '@/redux-store/slices/ingredient'
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
productReducer,
|
||||
customerReducer,
|
||||
paymentMethodReducer
|
||||
paymentMethodReducer,
|
||||
ingredientReducer
|
||||
},
|
||||
middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false })
|
||||
})
|
||||
|
||||
52
src/redux-store/slices/ingredient.ts
Normal file
52
src/redux-store/slices/ingredient.ts
Normal file
@ -0,0 +1,52 @@
|
||||
// Third-party Imports
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
|
||||
// Type Imports
|
||||
|
||||
// Data Imports
|
||||
import { IngredientItem } from '../../types/services/ingredient'
|
||||
|
||||
const initialState: { currentIngredient: IngredientItem } = {
|
||||
currentIngredient: {
|
||||
id: '',
|
||||
organization_id: '',
|
||||
outlet_id: '',
|
||||
name: '',
|
||||
unit_id: '',
|
||||
cost: 0,
|
||||
stock: 0,
|
||||
is_semi_finished: false,
|
||||
is_active: true,
|
||||
metadata: {},
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
unit: {
|
||||
id: '',
|
||||
organization_id: '',
|
||||
outlet_id: '',
|
||||
name: '',
|
||||
abbreviation: '',
|
||||
is_active: false,
|
||||
created_at: '',
|
||||
updated_at: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ingredientSlice = createSlice({
|
||||
name: 'ingredient',
|
||||
initialState,
|
||||
reducers: {
|
||||
setIngredient: (state, action: PayloadAction<IngredientItem>) => {
|
||||
state.currentIngredient = action.payload
|
||||
},
|
||||
resetIngredient: state => {
|
||||
state.currentIngredient = initialState.currentIngredient
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { setIngredient, resetIngredient } = ingredientSlice.actions
|
||||
|
||||
export default ingredientSlice.reducer
|
||||
@ -1,52 +1,52 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { CustomerRequest } from '../../types/services/customer'
|
||||
import { api } from '../api'
|
||||
import { toast } from 'react-toastify'
|
||||
import { IngredientRequest } from '../../types/services/ingredient'
|
||||
import { api } from '../api'
|
||||
|
||||
export const useIngredientsMutation = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const createCustomer = useMutation({
|
||||
mutationFn: async (newCustomer: CustomerRequest) => {
|
||||
const response = await api.post('/customers', newCustomer)
|
||||
const createIngredient = useMutation({
|
||||
mutationFn: async (newIngredient: IngredientRequest) => {
|
||||
const response = await api.post('/ingredients', newIngredient)
|
||||
return response.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Customer created successfully!')
|
||||
queryClient.invalidateQueries({ queryKey: ['customers'] })
|
||||
toast.success('Ingredient created successfully!')
|
||||
queryClient.invalidateQueries({ queryKey: ['ingredients'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
|
||||
}
|
||||
})
|
||||
|
||||
const updateCustomer = useMutation({
|
||||
mutationFn: async ({ id, payload }: { id: string; payload: CustomerRequest }) => {
|
||||
const response = await api.put(`/customers/${id}`, payload)
|
||||
const updateIngredient = useMutation({
|
||||
mutationFn: async ({ id, payload }: { id: string; payload: IngredientRequest }) => {
|
||||
const response = await api.put(`/ingredients/${id}`, payload)
|
||||
return response.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Customer updated successfully!')
|
||||
queryClient.invalidateQueries({ queryKey: ['customers'] })
|
||||
toast.success('Ingredient updated successfully!')
|
||||
queryClient.invalidateQueries({ queryKey: ['ingredients'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
|
||||
}
|
||||
})
|
||||
|
||||
const deleteCustomer = useMutation({
|
||||
const deleteIngredient = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const response = await api.delete(`/customers/${id}`)
|
||||
const response = await api.delete(`/ingredients/${id}`)
|
||||
return response.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Customer deleted successfully!')
|
||||
queryClient.invalidateQueries({ queryKey: ['customers'] })
|
||||
toast.success('Ingredient deleted successfully!')
|
||||
queryClient.invalidateQueries({ queryKey: ['ingredients'] })
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
|
||||
}
|
||||
})
|
||||
|
||||
return { createCustomer, updateCustomer, deleteCustomer }
|
||||
return { createIngredient, updateIngredient, deleteIngredient }
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ProductSalesReport, SalesReport } from '../../types/services/analytic'
|
||||
import { DashboardReport, PaymentReport, ProductSalesReport, ProfitLossReport, SalesReport } from '../../types/services/analytic'
|
||||
import { api } from '../api'
|
||||
import { formatDateDDMMYYYY } from '../../utils/transform'
|
||||
|
||||
@ -10,11 +10,11 @@ interface AnalyticQueryParams {
|
||||
|
||||
export function useSalesAnalytics(params: AnalyticQueryParams = {}) {
|
||||
const today = new Date()
|
||||
const sevenDaysAgo = new Date()
|
||||
sevenDaysAgo.setDate(today.getDate() - 30)
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const defaultDateTo = formatDateDDMMYYYY(today)
|
||||
const defaultDateFrom = formatDateDDMMYYYY(sevenDaysAgo)
|
||||
const defaultDateFrom = formatDateDDMMYYYY(monthAgo)
|
||||
|
||||
const { date_from = defaultDateFrom, date_to = defaultDateTo, ...filters } = params
|
||||
|
||||
@ -40,11 +40,11 @@ export function useSalesAnalytics(params: AnalyticQueryParams = {}) {
|
||||
|
||||
export function useProductSalesAnalytics(params: AnalyticQueryParams = {}) {
|
||||
const today = new Date()
|
||||
const sevenDaysAgo = new Date()
|
||||
sevenDaysAgo.setDate(today.getDate() - 30)
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const defaultDateTo = formatDateDDMMYYYY(today)
|
||||
const defaultDateFrom = formatDateDDMMYYYY(sevenDaysAgo)
|
||||
const defaultDateFrom = formatDateDDMMYYYY(monthAgo)
|
||||
|
||||
const { date_from = defaultDateFrom, date_to = defaultDateTo, ...filters } = params
|
||||
|
||||
@ -67,3 +67,93 @@ export function useProductSalesAnalytics(params: AnalyticQueryParams = {}) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function usePaymentAnalytics(params: AnalyticQueryParams = {}) {
|
||||
const today = new Date()
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const defaultDateTo = formatDateDDMMYYYY(today)
|
||||
const defaultDateFrom = formatDateDDMMYYYY(monthAgo)
|
||||
|
||||
const { date_from = defaultDateFrom, date_to = defaultDateTo, ...filters } = params
|
||||
|
||||
return useQuery<PaymentReport>({
|
||||
queryKey: ['analytics-payment-methods', { date_from, date_to, ...filters }],
|
||||
queryFn: async () => {
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
queryParams.append('date_from', date_from)
|
||||
queryParams.append('date_to', date_to)
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
queryParams.append(key, value.toString())
|
||||
}
|
||||
})
|
||||
|
||||
const res = await api.get(`/analytics/payment-methods?${queryParams.toString()}`)
|
||||
return res.data.data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function useDashboardAnalytics(params: AnalyticQueryParams = {}) {
|
||||
const today = new Date()
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const defaultDateTo = formatDateDDMMYYYY(today)
|
||||
const defaultDateFrom = formatDateDDMMYYYY(monthAgo)
|
||||
|
||||
const { date_from = defaultDateFrom, date_to = defaultDateTo, ...filters } = params
|
||||
|
||||
return useQuery<DashboardReport>({
|
||||
queryKey: ['analytics-dashboard', { date_from, date_to, ...filters }],
|
||||
queryFn: async () => {
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
queryParams.append('date_from', date_from)
|
||||
queryParams.append('date_to', date_to)
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
queryParams.append(key, value.toString())
|
||||
}
|
||||
})
|
||||
|
||||
const res = await api.get(`/analytics/dashboard?${queryParams.toString()}`)
|
||||
return res.data.data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function useProfitLossAnalytics(params: AnalyticQueryParams = {}) {
|
||||
const today = new Date()
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const defaultDateTo = formatDateDDMMYYYY(today)
|
||||
const defaultDateFrom = formatDateDDMMYYYY(monthAgo)
|
||||
|
||||
const { date_from = defaultDateFrom, date_to = defaultDateTo, ...filters } = params
|
||||
|
||||
return useQuery<ProfitLossReport>({
|
||||
queryKey: ['analytics-profit-loss', { date_from, date_to, ...filters }],
|
||||
queryFn: async () => {
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
queryParams.append('date_from', date_from)
|
||||
queryParams.append('date_to', date_to)
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
queryParams.append(key, value.toString())
|
||||
}
|
||||
})
|
||||
|
||||
const res = await api.get(`/analytics/profit-loss?${queryParams.toString()}`)
|
||||
return res.data.data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -46,3 +46,112 @@ export interface ProductSalesReport {
|
||||
date_to: string
|
||||
data: ProductData[]
|
||||
}
|
||||
|
||||
export type PaymentDataItem = {
|
||||
payment_method_id: string
|
||||
payment_method_name: string
|
||||
payment_method_type: string
|
||||
total_amount: number
|
||||
order_count: number
|
||||
payment_count: number
|
||||
percentage: number
|
||||
}
|
||||
|
||||
export type SummaryData = {
|
||||
total_amount: number
|
||||
total_orders: number
|
||||
total_payments: number
|
||||
average_order_value: number
|
||||
}
|
||||
|
||||
export type PaymentReport = {
|
||||
organization_id: string
|
||||
outlet_id: string
|
||||
date_from: string
|
||||
date_to: string
|
||||
group_by: string
|
||||
summary: SummaryData
|
||||
data: PaymentDataItem[]
|
||||
}
|
||||
|
||||
export type Overview = {
|
||||
total_sales: number
|
||||
total_orders: number
|
||||
average_order_value: number
|
||||
total_customers: number
|
||||
voided_orders: number
|
||||
refunded_orders: number
|
||||
}
|
||||
|
||||
export type RecentSale = {
|
||||
date: string
|
||||
sales: number
|
||||
orders: number
|
||||
items: number
|
||||
tax: number
|
||||
discount: number
|
||||
net_sales: number
|
||||
}
|
||||
|
||||
export type DashboardReport = {
|
||||
organization_id: string
|
||||
outlet_id: string
|
||||
date_from: string
|
||||
date_to: string
|
||||
overview: Overview
|
||||
top_products: ProductData[]
|
||||
payment_methods: PaymentDataItem[]
|
||||
recent_sales: RecentSale[]
|
||||
}
|
||||
|
||||
export interface ProfitLossReport {
|
||||
organization_id: string;
|
||||
date_from: string; // ISO date string with timezone
|
||||
date_to: string; // ISO date string with timezone
|
||||
group_by: string;
|
||||
summary: Summary;
|
||||
data: DailyData[];
|
||||
product_data: ProductDataReport[];
|
||||
}
|
||||
|
||||
export interface Summary {
|
||||
total_revenue: number;
|
||||
total_cost: number;
|
||||
gross_profit: number;
|
||||
gross_profit_margin: number;
|
||||
total_tax: number;
|
||||
total_discount: number;
|
||||
net_profit: number;
|
||||
net_profit_margin: number;
|
||||
total_orders: number;
|
||||
average_profit: number;
|
||||
profitability_ratio: number;
|
||||
}
|
||||
|
||||
export interface DailyData {
|
||||
date: string; // ISO date string with timezone
|
||||
revenue: number;
|
||||
cost: number;
|
||||
gross_profit: number;
|
||||
gross_profit_margin: number;
|
||||
tax: number;
|
||||
discount: number;
|
||||
net_profit: number;
|
||||
net_profit_margin: number;
|
||||
orders: number;
|
||||
}
|
||||
|
||||
export interface ProductDataReport {
|
||||
product_id: string;
|
||||
product_name: string;
|
||||
category_id: string;
|
||||
category_name: string;
|
||||
quantity_sold: number;
|
||||
revenue: number;
|
||||
cost: number;
|
||||
gross_profit: number;
|
||||
gross_profit_margin: number;
|
||||
average_price: number;
|
||||
average_cost: number;
|
||||
profit_per_unit: number;
|
||||
}
|
||||
|
||||
@ -36,3 +36,12 @@ export type Ingredients = {
|
||||
data: IngredientItem[]
|
||||
pagination: Pagination
|
||||
}
|
||||
|
||||
export type IngredientRequest = {
|
||||
name: string
|
||||
unit_id: string
|
||||
cost: number
|
||||
stock: number
|
||||
is_semi_finished: boolean
|
||||
outlet_id: string
|
||||
}
|
||||
|
||||
@ -6,6 +6,15 @@ export const formatCurrency = (amount: number) => {
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
export const formatShortCurrency = (num: number): string => {
|
||||
if (num >= 1_000_000) {
|
||||
return (num / 1_000_000).toFixed(2) + 'M'
|
||||
} else if (num >= 1_000) {
|
||||
return (num / 1_000).toFixed(2) + 'k'
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
export const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
@ -17,8 +26,8 @@ export const formatDate = (dateString: string) => {
|
||||
}
|
||||
|
||||
export const formatDateDDMMYYYY = (date: Date) => {
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const year = date.getFullYear()
|
||||
return `${day}-${month}-${year}`
|
||||
}
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const year = date.getFullYear()
|
||||
return `${day}-${month}-${year}`
|
||||
}
|
||||
|
||||
@ -1,413 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
|
||||
// MUI Imports
|
||||
import Button from '@mui/material/Button'
|
||||
import Card from '@mui/material/Card'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Rating from '@mui/material/Rating'
|
||||
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,
|
||||
getFacetedMinMaxValues,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable
|
||||
} from '@tanstack/react-table'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { ReviewType } from '@/types/apps/ecommerceTypes'
|
||||
import type { Locale } from '@configs/i18n'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
// Util Imports
|
||||
import { getLocalizedUrl } from '@/utils/i18n'
|
||||
|
||||
// Style Imports
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface FilterFns {
|
||||
fuzzy: FilterFn<unknown>
|
||||
}
|
||||
interface FilterMeta {
|
||||
itemRank: RankingInfo
|
||||
}
|
||||
}
|
||||
|
||||
type ReviewWithActionsType = ReviewType & {
|
||||
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<ReviewWithActionsType>()
|
||||
|
||||
const ManageReviewsTable = ({ reviewsData }: { reviewsData?: ReviewType[] }) => {
|
||||
// States
|
||||
const [status, setStatus] = useState<ReviewType['status']>('All')
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
const [allData, setAllData] = useState(...[reviewsData])
|
||||
const [data, setData] = useState(allData)
|
||||
const [globalFilter, setGlobalFilter] = useState('')
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
const columns = useMemo<ColumnDef<ReviewWithActionsType, 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('product', {
|
||||
header: 'Product',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center gap-4'>
|
||||
<img src={row.original.productImage} width={38} height={38} className='rounded bg-actionHover' />
|
||||
<div className='flex flex-col items-start'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
{row.original.product}
|
||||
</Typography>
|
||||
<Typography variant='body2' className='text-wrap'>
|
||||
{row.original.companyName}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('reviewer', {
|
||||
header: 'Reviewer',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center gap-4'>
|
||||
<CustomAvatar src={row.original.avatar} size={34} />
|
||||
<div className='flex flex-col items-start'>
|
||||
<Typography
|
||||
component={Link}
|
||||
href={getLocalizedUrl('/apps/ecommerce/customers/details/879861', locale as Locale)}
|
||||
color='primary.main'
|
||||
className='font-medium'
|
||||
>
|
||||
{row.original.reviewer}
|
||||
</Typography>
|
||||
<Typography variant='body2'>{row.original.email}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('head', {
|
||||
header: 'Review',
|
||||
sortingFn: (rowA, rowB) => rowA.original.review - rowB.original.review,
|
||||
cell: ({ row }) => (
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Rating
|
||||
name='product-review'
|
||||
readOnly
|
||||
value={row.original.review}
|
||||
emptyIcon={<i className='tabler-star-filled' />}
|
||||
/>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
{row.original.head}
|
||||
</Typography>
|
||||
<Typography variant='body2' className='text-wrap'>
|
||||
{row.original.para}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('date', {
|
||||
header: 'Date',
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const dateA = new Date(rowA.original.date)
|
||||
const dateB = new Date(rowB.original.date)
|
||||
|
||||
return dateA.getTime() - dateB.getTime()
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const date = new Date(row.original.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
year: 'numeric'
|
||||
})
|
||||
|
||||
return <Typography>{date}</Typography>
|
||||
}
|
||||
}),
|
||||
columnHelper.accessor('status', {
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center gap-3'>
|
||||
<Chip
|
||||
label={row.original.status}
|
||||
variant='tonal'
|
||||
color={row.original.status === 'Published' ? 'success' : 'warning'}
|
||||
size='small'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('actions', {
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => (
|
||||
<OptionMenu
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
iconClassName='text-textSecondary'
|
||||
options={[
|
||||
{
|
||||
text: 'View',
|
||||
icon: 'tabler-eye',
|
||||
href: getLocalizedUrl('/apps/ecommerce/orders/details/5434', locale as Locale),
|
||||
linkProps: { className: 'flex items-center gap-2 is-full plb-2 pli-4' }
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
icon: 'tabler-trash',
|
||||
menuItemProps: {
|
||||
onClick: () => setAllData(allData?.filter(review => review.id !== row.original.id)),
|
||||
className: 'flex items-center'
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
),
|
||||
enableSorting: false
|
||||
})
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[data]
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data: data as ReviewType[],
|
||||
columns,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter
|
||||
},
|
||||
state: {
|
||||
rowSelection,
|
||||
globalFilter
|
||||
},
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: 10
|
||||
}
|
||||
},
|
||||
enableRowSelection: true, //enable row selection for all rows
|
||||
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
|
||||
globalFilterFn: fuzzyFilter,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
getFacetedMinMaxValues: getFacetedMinMaxValues()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const filteredData = allData?.filter(review => {
|
||||
if (status !== 'All' && review.status !== status) return false
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
setData(filteredData)
|
||||
}, [status, allData, setData])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<div className='flex flex-wrap justify-between gap-4 p-6'>
|
||||
<DebouncedInput
|
||||
value={globalFilter ?? ''}
|
||||
onChange={value => setGlobalFilter(String(value))}
|
||||
placeholder='Search Product'
|
||||
className='max-sm:is-full'
|
||||
/>
|
||||
<div className='flex max-sm:flex-col sm:items-center gap-4 max-sm:is-full'>
|
||||
<CustomTextField
|
||||
select
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={e => table.setPageSize(Number(e.target.value))}
|
||||
className='sm:is-[140px] flex-auto is-full'
|
||||
>
|
||||
<MenuItem value='10'>10</MenuItem>
|
||||
<MenuItem value='25'>25</MenuItem>
|
||||
<MenuItem value='50'>50</MenuItem>
|
||||
</CustomTextField>
|
||||
<CustomTextField
|
||||
select
|
||||
fullWidth
|
||||
value={status}
|
||||
onChange={e => setStatus(e.target.value)}
|
||||
className='is-full sm:is-[140px] flex-auto'
|
||||
>
|
||||
<MenuItem value='All'>All</MenuItem>
|
||||
<MenuItem value='Published'>Published</MenuItem>
|
||||
<MenuItem value='Pending'>Pending</MenuItem>
|
||||
</CustomTextField>
|
||||
<Button
|
||||
variant='tonal'
|
||||
className='max-sm:is-full'
|
||||
startIcon={<i className='tabler-upload' />}
|
||||
color='secondary'
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='overflow-x-auto'>
|
||||
<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>
|
||||
</div>
|
||||
{/* <TablePagination
|
||||
component={() => <TablePaginationComponent table={table} />}
|
||||
count={table.getFilteredRowModel().rows.length}
|
||||
rowsPerPage={table.getState().pagination.pageSize}
|
||||
page={table.getState().pagination.pageIndex}
|
||||
onPageChange={(_, page) => {
|
||||
table.setPageIndex(page)
|
||||
}}
|
||||
/> */}
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ManageReviewsTable
|
||||
@ -1,127 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// Next Imports
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import type { ApexOptions } from 'apexcharts'
|
||||
|
||||
// Styled Component Imports
|
||||
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
|
||||
|
||||
// Vars
|
||||
const series = [{ data: [32, 52, 72, 94, 116, 94, 72] }]
|
||||
|
||||
const ReviewsStatistics = () => {
|
||||
// Hook
|
||||
const theme = useTheme()
|
||||
|
||||
// Vars
|
||||
const successLightOpacity = 'var(--mui-palette-success-lightOpacity)'
|
||||
|
||||
const options: ApexOptions = {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false }
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 6,
|
||||
distributed: true,
|
||||
columnWidth: '40%'
|
||||
}
|
||||
},
|
||||
legend: { show: false },
|
||||
tooltip: { enabled: false },
|
||||
dataLabels: { enabled: false },
|
||||
colors: [
|
||||
successLightOpacity,
|
||||
successLightOpacity,
|
||||
successLightOpacity,
|
||||
successLightOpacity,
|
||||
'var(--mui-palette-success-main)',
|
||||
successLightOpacity,
|
||||
successLightOpacity
|
||||
],
|
||||
states: {
|
||||
hover: {
|
||||
filter: { type: 'none' }
|
||||
},
|
||||
active: {
|
||||
filter: { type: 'none' }
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
show: false,
|
||||
padding: {
|
||||
top: -30,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: -12
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
categories: ['M', 'T', 'W', 'T', 'F', 'S', 'S'],
|
||||
axisTicks: { show: false },
|
||||
axisBorder: { show: false },
|
||||
tickPlacement: 'on',
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'var(--mui-palette-text-disabled)',
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
fontSize: theme.typography.body2.fontSize as string
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: { show: false },
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 600,
|
||||
options: {
|
||||
chart: {
|
||||
width: 275
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<div className='bs-full flex flex-col items-start justify-between gap-6'>
|
||||
<div className='flex flex-col items-start gap-2'>
|
||||
<Typography variant='h5'>Reviews statistics</Typography>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Typography>12 New reviews</Typography>
|
||||
<Chip label='+8.4%' variant='tonal' size='small' color='success' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col items-start gap-2'>
|
||||
<Typography color='text.primary'>
|
||||
<span className='text-success'>87%</span> Positive reviews
|
||||
</Typography>
|
||||
<Typography variant='body2'>Weekly Report</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }} className='flex justify-center'>
|
||||
<AppReactApexCharts type='bar' width='100%' height={156} series={series} options={options} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReviewsStatistics
|
||||
@ -1,72 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import LinearProgress from '@mui/material/LinearProgress'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
type DataType = {
|
||||
rating: number
|
||||
value: number
|
||||
}
|
||||
|
||||
// Vars
|
||||
const totalReviewsData: DataType[] = [
|
||||
{ rating: 5, value: 109 },
|
||||
{ rating: 4, value: 40 },
|
||||
{ rating: 3, value: 18 },
|
||||
{ rating: 2, value: 12 },
|
||||
{ rating: 1, value: 8 }
|
||||
]
|
||||
|
||||
const TotalReviews = () => {
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'))
|
||||
|
||||
return (
|
||||
<Card className='bs-full'>
|
||||
<CardContent>
|
||||
<div className='flex max-sm:flex-col items-center gap-6'>
|
||||
<div className='flex flex-col items-start gap-2 is-full sm:is-6/12'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Typography variant='h3' color='primary.main'>
|
||||
4.89
|
||||
</Typography>
|
||||
<i className='tabler-star-filled text-[32px] text-primary' />
|
||||
</div>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
Total 187 reviews
|
||||
</Typography>
|
||||
<Typography>All reviews are from genuine customers</Typography>
|
||||
<Chip label='+5 This week' variant='tonal' size='small' color='primary' />
|
||||
</div>
|
||||
<Divider orientation={isSmallScreen ? 'horizontal' : 'vertical'} flexItem />
|
||||
<div className='flex flex-col gap-3 is-full sm:is-6/12'>
|
||||
{totalReviewsData.map((item, index) => (
|
||||
<div key={index} className='flex items-center gap-2'>
|
||||
<Typography variant='body2' className='text-nowrap'>
|
||||
{item.rating} Star
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
color='primary'
|
||||
value={Math.floor((item.value / 185) * 100)}
|
||||
variant='determinate'
|
||||
className='bs-2 is-full'
|
||||
/>
|
||||
<Typography variant='body2'>{item.value}</Typography>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default TotalReviews
|
||||
@ -0,0 +1,265 @@
|
||||
// React Imports
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Button from '@mui/material/Button'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Switch from '@mui/material/Switch'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Third-party Imports
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from '../../../../../redux-store'
|
||||
import { useIngredientsMutation } from '../../../../../services/mutations/ingredients'
|
||||
import { IngredientRequest } from '../../../../../types/services/ingredient'
|
||||
import { Autocomplete, CircularProgress } from '@mui/material'
|
||||
import { useOutlets } from '../../../../../services/queries/outlets'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { useUnits } from '../../../../../services/queries/units'
|
||||
import { resetIngredient } from '../../../../../redux-store/slices/ingredient'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
handleClose: () => void
|
||||
}
|
||||
|
||||
// Vars
|
||||
const initialData = {
|
||||
name: '',
|
||||
unit_id: '',
|
||||
cost: 0,
|
||||
stock: 0,
|
||||
is_semi_finished: false,
|
||||
outlet_id: ''
|
||||
}
|
||||
|
||||
const AddProductIngredientDrawer = (props: Props) => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// Props
|
||||
const { open, handleClose } = props
|
||||
|
||||
const { createIngredient, updateIngredient } = useIngredientsMutation()
|
||||
const { currentIngredient } = useSelector((state: RootState) => state.ingredientReducer)
|
||||
|
||||
// States
|
||||
const [formData, setFormData] = useState<IngredientRequest>(initialData)
|
||||
const [outletInput, setOutletInput] = useState('')
|
||||
const [outletDebouncedInput] = useDebounce(outletInput, 500)
|
||||
const [unitInput, setUnitInput] = useState('')
|
||||
const [unitDebouncedInput] = useDebounce(unitInput, 500)
|
||||
|
||||
const { data: outlets, isLoading: outletsLoading } = useOutlets({
|
||||
search: outletDebouncedInput
|
||||
})
|
||||
const { data: units, isLoading: unitsLoading } = useUnits({
|
||||
search: unitDebouncedInput
|
||||
})
|
||||
|
||||
const outletOptions = useMemo(() => outlets?.outlets || [], [outlets])
|
||||
const unitOptions = useMemo(() => units?.data || [], [units])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentIngredient.id) {
|
||||
setFormData(currentIngredient)
|
||||
}
|
||||
}, [currentIngredient])
|
||||
|
||||
const handleSubmit = (e: any) => {
|
||||
e.preventDefault()
|
||||
|
||||
const { stock, cost, ...rest } = formData
|
||||
|
||||
const payload = {
|
||||
...rest,
|
||||
stock: Number(stock),
|
||||
cost: Number(cost)
|
||||
}
|
||||
|
||||
if (currentIngredient.id) {
|
||||
updateIngredient.mutate(
|
||||
{ id: currentIngredient.id, payload },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleReset()
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
createIngredient.mutate(payload, {
|
||||
onSuccess: () => {
|
||||
handleReset()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
handleClose()
|
||||
dispatch(resetIngredient())
|
||||
setFormData(initialData)
|
||||
}
|
||||
|
||||
const handleInputChange = (e: any) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
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'>{currentIngredient.id ? 'Edit' : 'Add'} 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>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Name'
|
||||
name='name'
|
||||
placeholder='John Doe'
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<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: (
|
||||
<>
|
||||
{outletsLoading && <CircularProgress size={18} />}
|
||||
{params.InputProps.endAdornment}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Autocomplete
|
||||
options={unitOptions}
|
||||
loading={unitsLoading}
|
||||
getOptionLabel={option => option.name}
|
||||
value={unitOptions.find(p => p.id === formData.unit_id) || null}
|
||||
onInputChange={(event, newUnitInput) => {
|
||||
setUnitInput(newUnitInput)
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
unit_id: newValue?.id || ''
|
||||
})
|
||||
}}
|
||||
renderInput={params => (
|
||||
<CustomTextField
|
||||
{...params}
|
||||
className=''
|
||||
label='Unit'
|
||||
fullWidth
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<>
|
||||
{unitsLoading && <CircularProgress size={18} />}
|
||||
{params.InputProps.endAdornment}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
type='number'
|
||||
label='Cost'
|
||||
name='cost'
|
||||
placeholder='$499'
|
||||
value={formData.cost}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<CustomTextField
|
||||
label='Stock'
|
||||
type='number'
|
||||
fullWidth
|
||||
placeholder='100'
|
||||
name='stock'
|
||||
value={formData.stock}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<div className='flex items-center'>
|
||||
<div className='flex flex-col items-start gap-1'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
Semi Finished
|
||||
</Typography>
|
||||
</div>
|
||||
<Switch
|
||||
checked={formData.is_semi_finished}
|
||||
name='is_semi_finished'
|
||||
onChange={e => setFormData({ ...formData, is_semi_finished: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Button
|
||||
variant='contained'
|
||||
type='submit'
|
||||
disabled={createIngredient.isPending || updateIngredient.isPending}
|
||||
>
|
||||
{currentIngredient.id
|
||||
? updateIngredient.isPending
|
||||
? 'Updating...'
|
||||
: 'Update'
|
||||
: createIngredient.isPending
|
||||
? 'Creating...'
|
||||
: 'Create'}
|
||||
</Button>
|
||||
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
|
||||
Discard
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</PerfectScrollbar>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddProductIngredientDrawer
|
||||
@ -34,6 +34,10 @@ import { useUnitsMutation } from '../../../../../services/mutations/units'
|
||||
import { useIngredients } from '../../../../../services/queries/ingredients'
|
||||
import { IngredientItem } from '../../../../../types/services/ingredient'
|
||||
import { formatCurrency } from '../../../../../utils/transform'
|
||||
import AddProductIngredientDrawer from './AddProductIngredientDrawer'
|
||||
import { useIngredientsMutation } from '../../../../../services/mutations/ingredients'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { setIngredient } from '../../../../../redux-store/slices/ingredient'
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface FilterFns {
|
||||
@ -94,13 +98,15 @@ const DebouncedInput = ({
|
||||
const columnHelper = createColumnHelper<IngredientWithActionsType>()
|
||||
|
||||
const ProductIngredientTable = () => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// States
|
||||
const [addUnitOpen, setAddUnitOpen] = useState(false)
|
||||
const [addIngredientOpen, setAddIngredientOpen] = useState(false)
|
||||
const [editUnitOpen, setEditUnitOpen] = useState(false)
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [unitId, setUnitId] = useState('')
|
||||
const [ingredientId, setIngredientId] = useState('')
|
||||
const [openConfirm, setOpenConfirm] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
@ -111,7 +117,7 @@ const ProductIngredientTable = () => {
|
||||
search
|
||||
})
|
||||
|
||||
const { mutate: deleteUnit, isPending: isDeleting } = useUnitsMutation().deleteUnit
|
||||
const { deleteIngredient } = useIngredientsMutation()
|
||||
|
||||
const ingredients = data?.data ?? []
|
||||
const totalCount = data?.pagination.total_count ?? 0
|
||||
@ -128,7 +134,7 @@ const ProductIngredientTable = () => {
|
||||
}, [])
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteUnit(unitId, {
|
||||
deleteIngredient.mutate(ingredientId, {
|
||||
onSuccess: () => setOpenConfirm(false)
|
||||
})
|
||||
}
|
||||
@ -210,7 +216,8 @@ const ProductIngredientTable = () => {
|
||||
<div className='flex items-center'>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setEditUnitOpen(!editUnitOpen)
|
||||
setAddIngredientOpen(!editUnitOpen)
|
||||
dispatch(setIngredient(row.original))
|
||||
}}
|
||||
>
|
||||
<i className='tabler-edit text-textSecondary' />
|
||||
@ -225,7 +232,7 @@ const ProductIngredientTable = () => {
|
||||
icon: 'tabler-trash',
|
||||
menuItemProps: {
|
||||
onClick: () => {
|
||||
setUnitId(row.original.id)
|
||||
setIngredientId(row.original.id)
|
||||
setOpenConfirm(true)
|
||||
}
|
||||
}
|
||||
@ -287,7 +294,7 @@ const ProductIngredientTable = () => {
|
||||
<Button
|
||||
variant='contained'
|
||||
className='max-sm:is-full'
|
||||
onClick={() => setAddUnitOpen(!addUnitOpen)}
|
||||
onClick={() => setAddIngredientOpen(!addIngredientOpen)}
|
||||
startIcon={<i className='tabler-plus' />}
|
||||
>
|
||||
Add Ingredient
|
||||
@ -389,17 +396,15 @@ const ProductIngredientTable = () => {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* <AddUnitDrawer open={addUnitOpen} handleClose={() => setAddUnitOpen(!addUnitOpen)} />
|
||||
|
||||
<EditUnitDrawer open={editUnitOpen} handleClose={() => setEditUnitOpen(!editUnitOpen)} data={currentUnit!} /> */}
|
||||
<AddProductIngredientDrawer open={addIngredientOpen} handleClose={() => setAddIngredientOpen(!addIngredientOpen)} />
|
||||
|
||||
<ConfirmDeleteDialog
|
||||
open={openConfirm}
|
||||
onClose={() => setOpenConfirm(false)}
|
||||
onConfirm={handleDelete}
|
||||
isLoading={isDeleting}
|
||||
title='Delete Unit'
|
||||
message='Are you sure you want to delete this Unit? This action cannot be undone.'
|
||||
isLoading={deleteIngredient.isPending}
|
||||
title='Delete Ingredient'
|
||||
message='Are you sure you want to delete this Ingredient? This action cannot be undone.'
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Types Imports
|
||||
import type { CardStatsHorizontalWithAvatarProps } from '@/types/pages/widgetTypes'
|
||||
|
||||
// Component Imports
|
||||
import CardStatsHorizontalWithAvatar from '@components/card-statistics/HorizontalWithAvatar'
|
||||
|
||||
const HorizontalStatisticsCard = ({ data }: { data?: CardStatsHorizontalWithAvatarProps[] }) => {
|
||||
return (
|
||||
data && (
|
||||
<Grid container spacing={6}>
|
||||
{data.map((item, index) => (
|
||||
<Grid key={index} size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<CardStatsHorizontalWithAvatar {...item} avatarSkin='light' />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default HorizontalStatisticsCard
|
||||
@ -1,101 +0,0 @@
|
||||
// React Imports
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
type DataType = {
|
||||
description: string
|
||||
value: string
|
||||
icon: ReactNode
|
||||
}
|
||||
|
||||
// Vars
|
||||
const data: DataType[] = [
|
||||
{
|
||||
description: 'Create & validate your referral link and get',
|
||||
value: '$50',
|
||||
icon: (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='42' height='42' viewBox='0 0 43 42' fill='none'>
|
||||
<path
|
||||
opacity='0.2'
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M35.5943 24.3473L30.4428 18.1621C30.6396 21.952 29.7045 26.3652 26.817 31.4019L31.7389 35.3394C31.9139 35.4784 32.1215 35.5704 32.342 35.6067C32.5625 35.643 32.7887 35.6224 32.999 35.5468C33.2093 35.4712 33.3968 35.3432 33.5438 35.1748C33.6908 35.0065 33.7923 34.8034 33.8389 34.5848L35.8568 25.4629C35.9055 25.2695 35.907 25.0672 35.8614 24.8731C35.8157 24.679 35.7241 24.4987 35.5943 24.3473ZM7.63806 24.4457L12.7896 18.277C12.5927 22.0668 13.5279 26.4801 16.4154 31.5004L11.4935 35.4379C11.3196 35.5769 11.1132 35.6693 10.8937 35.7065C10.6743 35.7437 10.4489 35.7245 10.2389 35.6507C10.0289 35.5769 9.84115 35.4509 9.69326 35.2845C9.54537 35.1181 9.44223 34.9168 9.39353 34.6996L7.37556 25.5613C7.32691 25.3679 7.32536 25.1657 7.37104 24.9716C7.41671 24.7775 7.50828 24.5971 7.63806 24.4457Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M20.2132 2.47353C20.6217 2.13596 21.1351 1.95117 21.6653 1.95117C22.1971 1.95117 22.7121 2.13707 23.1212 2.47657C24.7301 3.7867 28.2128 7.0357 30.1373 12.0381C30.798 13.7554 31.2701 15.6673 31.4253 17.7625L36.4572 23.8008C36.6883 24.0724 36.8515 24.3951 36.9332 24.7424C37.0142 25.0868 37.0128 25.4453 36.9291 25.7889L34.9138 34.9151L34.9131 34.9182C34.8273 35.3009 34.6455 35.6555 34.385 35.9487C34.1244 36.2418 33.7936 36.4639 33.4236 36.5939C33.0535 36.724 32.6565 36.7579 32.2698 36.6923C31.8831 36.6267 31.5195 36.4638 31.2131 36.219L31.2126 36.2186L26.5646 32.5002H16.7662L12.1182 36.2186L12.1177 36.219C11.8113 36.4638 11.4477 36.6267 11.061 36.6923C10.6743 36.7579 10.2773 36.724 9.90727 36.5939C9.53726 36.4639 9.20641 36.2418 8.94584 35.9487C8.68527 35.6555 8.50355 35.3009 8.41775 34.9182L8.41706 34.9151L6.40177 25.7889C6.31804 25.4453 6.31658 25.0868 6.39762 24.7424C6.47936 24.395 6.64268 24.072 6.87401 23.8004L11.8088 17.8912C11.9513 15.7424 12.4328 13.7842 13.1145 12.029C15.058 7.02537 18.5854 3.77698 20.2132 2.47353ZM29.4558 18.3076C29.4446 18.2387 29.4406 18.169 29.4438 18.0996C29.3181 16.1202 28.879 14.3374 28.2707 12.7563C26.5219 8.21065 23.3318 5.22661 21.8544 4.02428L21.8446 4.01626L21.8446 4.01619C21.7943 3.97418 21.7309 3.95117 21.6653 3.95117C21.5998 3.95117 21.5363 3.97418 21.486 4.01619L21.4697 4.02954C19.9771 5.22365 16.7444 8.20744 14.9788 12.7531C14.3468 14.3804 13.8969 16.2219 13.7896 18.2718C13.7898 18.3084 13.788 18.345 13.7842 18.3815C13.6245 21.8411 14.4416 25.8898 16.9985 30.5002H26.3263C28.8486 25.8569 29.6366 21.7836 29.4558 18.3076ZM34.9245 25.0857L31.4177 20.8775C31.1755 24.0045 30.2142 27.4702 28.197 31.2448L32.4615 34.6565C32.5029 34.6896 32.5521 34.7116 32.6043 34.7204C32.6566 34.7293 32.7102 34.7247 32.7602 34.7071C32.8102 34.6896 32.8549 34.6596 32.8901 34.6199C32.9251 34.5806 32.9496 34.533 32.9613 34.4817L32.9615 34.4807L34.9788 25.3455C34.9809 25.3361 34.9831 25.3266 34.9855 25.3172C34.9951 25.2789 34.9954 25.2389 34.9864 25.2004C34.9773 25.162 34.9592 25.1263 34.9335 25.0963L34.9245 25.0858L34.9245 25.0857ZM11.8405 20.9734L8.40561 25.0865L8.39739 25.0964L8.39732 25.0963C8.37163 25.1263 8.3535 25.162 8.34445 25.2004C8.33541 25.2389 8.33572 25.2789 8.34535 25.3172C8.34772 25.3266 8.34995 25.3361 8.35204 25.3455L10.3693 34.4807L10.3695 34.4817C10.3812 34.533 10.4057 34.5806 10.4407 34.6199C10.4759 34.6596 10.5206 34.6896 10.5706 34.7071C10.6206 34.7247 10.6743 34.7293 10.7265 34.7204C10.7788 34.7116 10.8279 34.6896 10.8693 34.6565L15.1281 31.2495C13.0909 27.5131 12.1056 24.0779 11.8405 20.9734ZM18.0404 36.7502C18.0404 36.1979 18.4881 35.7502 19.0404 35.7502H24.2904C24.8427 35.7502 25.2904 36.1979 25.2904 36.7502C25.2904 37.3025 24.8427 37.7502 24.2904 37.7502H19.0404C18.4881 37.7502 18.0404 37.3025 18.0404 36.7502ZM23.6342 15.7502C23.6342 16.8375 22.7527 17.719 21.6654 17.719C20.5781 17.719 19.6967 16.8375 19.6967 15.7502C19.6967 14.6629 20.5781 13.7815 21.6654 13.7815C22.7527 13.7815 23.6342 14.6629 23.6342 15.7502Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
description: 'For every new signup you get',
|
||||
value: '10%',
|
||||
icon: (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='42' height='42' viewBox='0 0 42 42' fill='none'>
|
||||
<path
|
||||
opacity='0.2'
|
||||
d='M9.1875 6.25H32.8125C32.8954 6.25 32.9749 6.28292 33.0335 6.34153L33.739 5.63603L33.0335 6.34153C33.0921 6.40013 33.125 6.47962 33.125 6.5625V35.4375C33.125 35.5204 33.0921 35.5999 33.0335 35.6585L33.7406 36.3656L33.0335 35.6585C32.9749 35.7171 32.8954 35.75 32.8125 35.75H9.1875C9.10462 35.75 9.02513 35.7171 8.96653 35.6585L8.25942 36.3656L8.96653 35.6585C8.90792 35.5999 8.875 35.5204 8.875 35.4375V6.5625C8.875 6.47962 8.90792 6.40014 8.96653 6.34153C9.02514 6.28292 9.10462 6.25 9.1875 6.25ZM17.5277 27.5092C18.5555 28.1959 19.7639 28.5625 21 28.5625C22.6576 28.5625 24.2473 27.904 25.4194 26.7319C26.5915 25.5598 27.25 23.9701 27.25 22.3125C27.25 21.0764 26.8834 19.868 26.1967 18.8402C25.5099 17.8124 24.5338 17.0113 23.3918 16.5383C22.2497 16.0652 20.9931 15.9414 19.7807 16.1826C18.5683 16.4237 17.4547 17.019 16.5806 17.8931C15.7065 18.7672 15.1112 19.8808 14.8701 21.0932C14.6289 22.3056 14.7527 23.5622 15.2258 24.7043C15.6988 25.8463 16.4999 26.8224 17.5277 27.5092Z'
|
||||
fill='currentColor'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
/>
|
||||
<path
|
||||
d='M21 27.5625C23.8995 27.5625 26.25 25.212 26.25 22.3125C26.25 19.413 23.8995 17.0625 21 17.0625C18.1005 17.0625 15.75 19.413 15.75 22.3125C15.75 25.212 18.1005 27.5625 21 27.5625ZM21 27.5625C19.4718 27.5625 17.9646 27.9183 16.5977 28.6017C15.2309 29.2852 14.0419 30.2774 13.125 31.5M21 27.5625C22.5282 27.5625 24.0354 27.9183 25.4023 28.6017C26.7691 29.2852 27.9581 30.2774 28.875 31.5M15.75 10.5H26.25M34.125 6.5625V35.4375C34.125 36.1624 33.5374 36.75 32.8125 36.75H9.1875C8.46263 36.75 7.875 36.1624 7.875 35.4375V6.5625C7.875 5.83763 8.46263 5.25 9.1875 5.25H32.8125C33.5374 5.25 34.125 5.83763 34.125 6.5625Z'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
description: 'Get other friends to generate link and get',
|
||||
value: '$100',
|
||||
icon: (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='42' height='42' viewBox='0 0 43 42' fill='none'>
|
||||
<path
|
||||
opacity='0.2'
|
||||
d='M34.8347 5.89001L4.25347 14.5033C3.99315 14.5745 3.76109 14.7242 3.58892 14.932C3.41674 15.1398 3.31281 15.3956 3.29129 15.6647C3.26977 15.9337 3.3317 16.2028 3.46865 16.4353C3.60559 16.6679 3.8109 16.8526 4.0566 16.9642L18.1003 23.6088C18.3754 23.7362 18.5964 23.9571 18.7238 24.2322L25.3683 38.2759C25.48 38.5216 25.6647 38.7269 25.8972 38.8639C26.1298 39.0008 26.3989 39.0628 26.6679 39.0412C26.9369 39.0197 27.1927 38.9158 27.4006 38.7436C27.6084 38.5714 27.7581 38.3394 27.8293 38.0791L36.4425 7.49782C36.5078 7.27466 36.5118 7.03804 36.4542 6.81279C36.3966 6.58753 36.2794 6.38192 36.115 6.21751C35.9506 6.0531 35.745 5.93594 35.5198 5.87832C35.2945 5.8207 35.0579 5.82474 34.8347 5.89001Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M35.7676 4.90951C35.3704 4.80791 34.9532 4.81452 34.5595 4.92862L3.98975 13.5387L3.98553 13.5398C3.52858 13.6657 3.12129 13.929 2.81886 14.294C2.5155 14.6602 2.33239 15.1109 2.29448 15.5849C2.25656 16.0589 2.36567 16.533 2.60696 16.9428C2.84676 17.35 3.20553 17.6739 3.63489 17.871L17.6727 24.5127L17.6727 24.5127L17.6801 24.5162C17.7402 24.544 17.7885 24.5923 17.8164 24.6524L17.8163 24.6524L17.8199 24.6599L24.4616 38.6978C24.6587 39.1271 24.9826 39.4858 25.3898 39.7256C25.7995 39.9669 26.2736 40.076 26.7476 40.0381C27.2216 40.0001 27.6724 39.817 28.0385 39.5137C28.4036 39.2113 28.6668 38.804 28.7927 38.347L28.7939 38.3428L37.4023 7.77853L37.4039 7.77315C37.518 7.37938 37.5246 6.96221 37.423 6.56497C37.3209 6.16591 37.1134 5.80166 36.8221 5.5104C36.5309 5.21914 36.1666 5.01159 35.7676 4.90951ZM35.1058 6.85256L34.8347 5.89001L35.1154 6.8498C35.1664 6.83489 35.2205 6.83396 35.2719 6.84713C35.3234 6.86029 35.3704 6.88705 35.4079 6.92461C35.4455 6.96218 35.4723 7.00915 35.4854 7.06061C35.4986 7.11207 35.4977 7.16612 35.4827 7.2171L35.4827 7.21709L35.48 7.22671L26.8667 37.808L26.8667 37.808L26.8647 37.8153C26.8477 37.8773 26.8121 37.9326 26.7626 37.9736C26.7131 38.0146 26.6522 38.0393 26.5881 38.0444C26.5241 38.0496 26.46 38.0348 26.4046 38.0022C26.3493 37.9696 26.3053 37.9207 26.2787 37.8622L26.2722 37.8483L19.7287 24.0181L26.6496 17.0971C27.0402 16.7066 27.0402 16.0734 26.6496 15.6829C26.2591 15.2924 25.626 15.2924 25.2354 15.6829L18.3145 22.6038L4.48428 16.0603L4.47032 16.0538C4.41182 16.0272 4.36294 15.9833 4.33033 15.9279C4.29773 15.8725 4.28298 15.8085 4.28811 15.7444C4.29323 15.6803 4.31798 15.6194 4.35897 15.57C4.39996 15.5205 4.45521 15.4848 4.5172 15.4679L4.5172 15.4679L4.52458 15.4658L35.1058 6.85256Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
const IconStepsCard = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='How to use' subheader='Integrate your referral code in 3 easy steps.' className='pbe-6' />
|
||||
<CardContent className='flex flex-col sm:flex-row items-center justify-around gap-6'>
|
||||
{data.map((item, index) => (
|
||||
<div key={index} className='flex flex-col items-center gap-2 max-is-[185px]'>
|
||||
<div className='flex border border-dashed border-primary rounded-full p-3.5 text-primary'>{item.icon}</div>
|
||||
<Typography className='text-wrap text-center'>{item.description}</Typography>
|
||||
<Typography variant='h6' color='primary.main'>
|
||||
{item.value}
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconStepsCard
|
||||
@ -1,55 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Button from '@mui/material/Button'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Component Imports
|
||||
import CustomIconButton from '@core/components/mui/IconButton'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
const InviteAndShare = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className='flex flex-col gap-[1.11937rem]'>
|
||||
<div>
|
||||
<Typography variant='h5' className='mbe-5'>
|
||||
Invite your friends
|
||||
</Typography>
|
||||
<div className='flex items-end gap-4'>
|
||||
<CustomTextField
|
||||
label="Enter friend's email address & invite them"
|
||||
placeholder='Email Address'
|
||||
className='flex-auto'
|
||||
/>
|
||||
<Button variant='contained' className='min-is-fit'>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant='h5' className='mbe-5'>
|
||||
Share the referral link
|
||||
</Typography>
|
||||
<div className='flex items-end gap-4'>
|
||||
<CustomTextField
|
||||
label='Share referral link in social media'
|
||||
placeholder='pixinvent.com/?ref=6479'
|
||||
className='flex-auto'
|
||||
/>
|
||||
<div className='flex gap-2'>
|
||||
<CustomIconButton variant='contained' className='bg-facebook text-white'>
|
||||
<i className='tabler-brand-facebook' />
|
||||
</CustomIconButton>
|
||||
<CustomIconButton variant='contained' className='bg-twitter text-white'>
|
||||
<i className='tabler-brand-twitter' />
|
||||
</CustomIconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default InviteAndShare
|
||||
@ -1,278 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
|
||||
// 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 Chip from '@mui/material/Chip'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
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,
|
||||
getFacetedMinMaxValues,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable
|
||||
} from '@tanstack/react-table'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { ReferralsType } from '@/types/apps/ecommerceTypes'
|
||||
import type { Locale } from '@configs/i18n'
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Util Imports
|
||||
import { getLocalizedUrl } from '@/utils/i18n'
|
||||
|
||||
// Style Imports
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface FilterFns {
|
||||
fuzzy: FilterFn<unknown>
|
||||
}
|
||||
interface FilterMeta {
|
||||
itemRank: RankingInfo
|
||||
}
|
||||
}
|
||||
|
||||
type userStatusType = {
|
||||
[key: string]: ThemeColor
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Vars
|
||||
const userStatusObj: userStatusType = {
|
||||
Rejected: 'error',
|
||||
Unpaid: 'warning',
|
||||
Paid: 'success'
|
||||
}
|
||||
|
||||
// Column Definitions
|
||||
const columnHelper = createColumnHelper<ReferralsType>()
|
||||
|
||||
const ReferredUsersTable = ({ referralsData }: { referralsData?: ReferralsType[] }) => {
|
||||
// States
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [data, setData] = useState(...[referralsData])
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
const columns = useMemo<ColumnDef<ReferralsType, 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('user', {
|
||||
header: 'Users',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center gap-4'>
|
||||
<CustomAvatar src={row.original.avatar} size={34} />
|
||||
<div className='flex flex-col items-start'>
|
||||
<Typography
|
||||
component={Link}
|
||||
href={getLocalizedUrl('/apps/ecommerce/customers/details/879861', locale as Locale)}
|
||||
color='text.primary'
|
||||
className='font-medium hover:text-primary'
|
||||
>
|
||||
{row.original.user}
|
||||
</Typography>
|
||||
<Typography variant='body2'>{row.original.email}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('referredId', {
|
||||
header: 'Referred ID',
|
||||
cell: ({ row }) => <Typography>{row.original.referredId}</Typography>
|
||||
}),
|
||||
columnHelper.accessor('status', {
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<Chip variant='tonal' label={row.original.status} size='small' color={userStatusObj[row.original.status]} />
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('value', {
|
||||
header: 'Value',
|
||||
cell: ({ row }) => <Typography>{row.original.value}</Typography>
|
||||
}),
|
||||
columnHelper.accessor('earning', {
|
||||
header: 'Earning',
|
||||
cell: ({ row }) => <Typography color='text.primary'>{row.original.earning}</Typography>
|
||||
})
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data: data as ReferralsType[],
|
||||
columns,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter
|
||||
},
|
||||
state: {
|
||||
rowSelection
|
||||
},
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: 10
|
||||
}
|
||||
},
|
||||
enableRowSelection: true, //enable row selection for all rows
|
||||
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
|
||||
globalFilterFn: fuzzyFilter,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
getFacetedMinMaxValues: getFacetedMinMaxValues()
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardContent className='flex flex-wrap justify-between items-center gap-4'>
|
||||
<Typography variant='h5'>Referred Users</Typography>
|
||||
<div className='flex flex-wrap items-center gap-4'>
|
||||
<CustomTextField
|
||||
select
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={e => table.setPageSize(Number(e.target.value))}
|
||||
className='flex-auto is-[70px]'
|
||||
>
|
||||
<MenuItem value='10'>10</MenuItem>
|
||||
<MenuItem value='25'>25</MenuItem>
|
||||
<MenuItem value='50'>50</MenuItem>
|
||||
</CustomTextField>
|
||||
<Button variant='tonal' startIcon={<i className='tabler-upload' />} color='secondary'>
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
<div className='overflow-x-auto'>
|
||||
<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>
|
||||
</div>
|
||||
{/* <TablePagination
|
||||
component={() => <TablePaginationComponent table={table} />}
|
||||
count={table.getExpandedRowModel().rows.length}
|
||||
rowsPerPage={table.getState().pagination.pageSize}
|
||||
page={table.getState().pagination.pageIndex}
|
||||
onPageChange={(_, page) => {
|
||||
table.setPageIndex(page)
|
||||
}}
|
||||
/> */}
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReferredUsersTable
|
||||
@ -1,123 +1,56 @@
|
||||
'use client'
|
||||
|
||||
// Next Imports
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import type { ApexOptions } from 'apexcharts'
|
||||
import classnames from 'classnames'
|
||||
import CustomAvatar, { CustomAvatarProps } from '../../../@core/components/mui/Avatar'
|
||||
import { ThemeColor } from '../../../@core/types'
|
||||
import { Skeleton, Typography } from '@mui/material'
|
||||
import { formatShortCurrency } from '../../../utils/transform'
|
||||
|
||||
// Styled Component Imports
|
||||
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
|
||||
type Props = {
|
||||
title: string
|
||||
value: number
|
||||
isLoading: boolean
|
||||
avatarIcon: string
|
||||
avatarSkin?: CustomAvatarProps['skin']
|
||||
avatarSize?: number
|
||||
avatarColor?: ThemeColor
|
||||
}
|
||||
|
||||
// Vars
|
||||
const series = [{ data: [77, 55, 23, 43, 77, 55, 89] }]
|
||||
const DistributedBarChartOrder = ({
|
||||
title,
|
||||
value,
|
||||
isLoading,
|
||||
avatarIcon,
|
||||
avatarSkin,
|
||||
avatarColor
|
||||
}: Props) => {
|
||||
|
||||
const DistributedBarChartOrder = () => {
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
|
||||
// Vars
|
||||
const actionSelectedColor = 'var(--mui-palette-action-selected)'
|
||||
|
||||
const options: ApexOptions = {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
stacked: false,
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
sparkline: { enabled: true }
|
||||
},
|
||||
tooltip: { enabled: false },
|
||||
legend: { show: false },
|
||||
dataLabels: { enabled: false },
|
||||
colors: ['var(--mui-palette-primary-main)'],
|
||||
states: {
|
||||
hover: {
|
||||
filter: { type: 'none' }
|
||||
},
|
||||
active: {
|
||||
filter: { type: 'none' }
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 3,
|
||||
horizontal: false,
|
||||
columnWidth: '32%',
|
||||
colors: {
|
||||
backgroundBarRadius: 5,
|
||||
backgroundBarColors: [
|
||||
actionSelectedColor,
|
||||
actionSelectedColor,
|
||||
actionSelectedColor,
|
||||
actionSelectedColor,
|
||||
actionSelectedColor
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
show: false,
|
||||
padding: {
|
||||
left: -3,
|
||||
right: 5,
|
||||
top: 15,
|
||||
bottom: 18
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
labels: { show: false },
|
||||
axisTicks: { show: false },
|
||||
axisBorder: { show: false }
|
||||
},
|
||||
yaxis: { show: false },
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 1350,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: { columnWidth: '45%' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
breakpoint: theme.breakpoints.values.lg,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: { columnWidth: '20%' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
breakpoint: 600,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: { columnWidth: '15%' }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
if (isLoading) {
|
||||
return <Skeleton sx={{ bgcolor: 'grey.100' }} variant='rectangular' width={300} height={118} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Order' subheader='Last Week' className='pbe-0' />
|
||||
<CardContent className='flex flex-col'>
|
||||
<AppReactApexCharts type='bar' height={84} width='100%' options={options} series={series} />
|
||||
<div className='flex items-center justify-between flex-wrap gap-x-4 gap-y-0.5'>
|
||||
<Typography variant='h4' color='text.primary'>
|
||||
124k
|
||||
</Typography>
|
||||
<Typography variant='body2' color='success.main'>
|
||||
+12.6%
|
||||
</Typography>
|
||||
<CardContent>
|
||||
<div className='flex items-start justify-between'>
|
||||
<div className='flex-1'>
|
||||
<Typography variant='h6' color='text.disabled'>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography color='text.primary' variant='h4'>
|
||||
{formatShortCurrency(value)}
|
||||
</Typography>
|
||||
</div>
|
||||
<CustomAvatar variant='rounded' skin={avatarSkin} size={52} color={avatarColor}>
|
||||
<i className={classnames(avatarIcon, 'text-[28px]')} />
|
||||
</CustomAvatar>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -1,31 +1,33 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { SyntheticEvent } from 'react'
|
||||
import { useState } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Tab from '@mui/material/Tab'
|
||||
import TabContext from '@mui/lab/TabContext'
|
||||
import TabList from '@mui/lab/TabList'
|
||||
import TabPanel from '@mui/lab/TabPanel'
|
||||
import TabContext from '@mui/lab/TabContext'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import Tab from '@mui/material/Tab'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import type { Theme } from '@mui/material/styles'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Third Party Imports
|
||||
import classnames from 'classnames'
|
||||
import type { ApexOptions } from 'apexcharts'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Components Imports
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
import Loading from '../../../components/layout/shared/Loading'
|
||||
import { formatShortCurrency } from '../../../utils/transform'
|
||||
|
||||
// Styled Component Imports
|
||||
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
|
||||
@ -33,39 +35,14 @@ const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexChart
|
||||
type ApexChartSeries = NonNullable<ApexOptions['series']>
|
||||
type ApexChartSeriesData = Exclude<ApexChartSeries[0], number>
|
||||
|
||||
type TabCategory = 'orders' | 'sales' | 'profit' | 'income'
|
||||
|
||||
type TabType = {
|
||||
type: TabCategory
|
||||
type: string
|
||||
avatarIcon: string
|
||||
date: any
|
||||
series: ApexChartSeries
|
||||
}
|
||||
|
||||
// Vars
|
||||
const tabData: TabType[] = [
|
||||
{
|
||||
type: 'orders',
|
||||
avatarIcon: 'tabler-shopping-cart',
|
||||
series: [{ data: [28, 10, 46, 38, 15, 30, 35, 28, 8] }]
|
||||
},
|
||||
{
|
||||
type: 'sales',
|
||||
avatarIcon: 'tabler-chart-bar',
|
||||
series: [{ data: [35, 25, 15, 40, 42, 25, 48, 8, 30] }]
|
||||
},
|
||||
{
|
||||
type: 'profit',
|
||||
avatarIcon: 'tabler-currency-dollar',
|
||||
series: [{ data: [10, 22, 27, 33, 42, 32, 27, 22, 8] }]
|
||||
},
|
||||
{
|
||||
type: 'income',
|
||||
avatarIcon: 'tabler-chart-pie-2',
|
||||
series: [{ data: [5, 9, 12, 18, 20, 25, 30, 36, 48] }]
|
||||
}
|
||||
]
|
||||
|
||||
const renderTabs = (value: TabCategory) => {
|
||||
const renderTabs = (tabData: TabType[], value: string) => {
|
||||
return tabData.map((item, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
@ -90,7 +67,7 @@ const renderTabs = (value: TabCategory) => {
|
||||
))
|
||||
}
|
||||
|
||||
const renderTabPanels = (value: TabCategory, theme: Theme, options: ApexOptions, colors: string[]) => {
|
||||
const renderTabPanels = (tabData: TabType[], theme: Theme, options: ApexOptions, colors: string[]) => {
|
||||
return tabData.map((item, index) => {
|
||||
const max = Math.max(...((item.series[0] as ApexChartSeriesData).data as number[]))
|
||||
const seriesIndex = ((item.series[0] as ApexChartSeriesData).data as number[]).indexOf(max)
|
||||
@ -101,7 +78,7 @@ const renderTabPanels = (value: TabCategory, theme: Theme, options: ApexOptions,
|
||||
<TabPanel key={index} value={item.type} className='!p-0'>
|
||||
<AppReactApexCharts
|
||||
type='bar'
|
||||
height={233}
|
||||
height={360}
|
||||
width='100%'
|
||||
options={{ ...options, colors: finalColors }}
|
||||
series={item.series}
|
||||
@ -111,9 +88,9 @@ const renderTabPanels = (value: TabCategory, theme: Theme, options: ApexOptions,
|
||||
})
|
||||
}
|
||||
|
||||
const EarningReportsWithTabs = () => {
|
||||
const EarningReportsWithTabs = ({ data }: { data: TabType[] }) => {
|
||||
// States
|
||||
const [value, setValue] = useState<TabCategory>('orders')
|
||||
const [value, setValue] = useState(data[0].type)
|
||||
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
@ -121,7 +98,7 @@ const EarningReportsWithTabs = () => {
|
||||
// Vars
|
||||
const disabledText = 'var(--mui-palette-text-disabled)'
|
||||
|
||||
const handleChange = (event: SyntheticEvent, newValue: TabCategory) => {
|
||||
const handleChange = (event: SyntheticEvent, newValue: string) => {
|
||||
setValue(newValue)
|
||||
}
|
||||
|
||||
@ -145,7 +122,7 @@ const EarningReportsWithTabs = () => {
|
||||
tooltip: { enabled: false },
|
||||
dataLabels: {
|
||||
offsetY: -11,
|
||||
formatter: val => `${val}k`,
|
||||
formatter: val => formatShortCurrency(Number(val)),
|
||||
style: {
|
||||
fontWeight: 500,
|
||||
colors: ['var(--mui-palette-text-primary)'],
|
||||
@ -173,7 +150,7 @@ const EarningReportsWithTabs = () => {
|
||||
xaxis: {
|
||||
axisTicks: { show: false },
|
||||
axisBorder: { color: 'var(--mui-palette-divider)' },
|
||||
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep'],
|
||||
categories: data.find(item => item.type === value)?.date,
|
||||
labels: {
|
||||
style: {
|
||||
colors: disabledText,
|
||||
@ -185,7 +162,7 @@ const EarningReportsWithTabs = () => {
|
||||
yaxis: {
|
||||
labels: {
|
||||
offsetX: -18,
|
||||
formatter: val => `$${val}k`,
|
||||
formatter: val => `${formatShortCurrency(Number(val))}`,
|
||||
style: {
|
||||
colors: disabledText,
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
@ -235,31 +212,33 @@ const EarningReportsWithTabs = () => {
|
||||
/>
|
||||
<CardContent>
|
||||
<TabContext value={value}>
|
||||
<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(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(value, theme, options, colors)}
|
||||
{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)}
|
||||
</TabContext>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -88,12 +88,12 @@ const statusObj: StatusObj = {
|
||||
verified: { text: 'Verified', color: 'success' }
|
||||
}
|
||||
|
||||
const LastTransaction = ({ serverMode }: { serverMode: SystemMode }) => {
|
||||
const LastTransaction = () => {
|
||||
// Hooks
|
||||
const { mode } = useColorScheme()
|
||||
|
||||
// Vars
|
||||
const _mode = (mode === 'system' ? serverMode : mode) || serverMode
|
||||
const _mode = mode as SystemMode
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user