feat: analytics
This commit is contained in:
parent
a5d22db27b
commit
687f59a9fa
@ -1,87 +1,355 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
'use client'
|
||||
|
||||
// Component Imports
|
||||
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
||||
import LineAreaYearlySalesChart from '@views/dashboards/crm/LineAreaYearlySalesChart'
|
||||
import CardStatVertical from '@/components/card-statistics/Vertical'
|
||||
import BarChartRevenueGrowth from '@views/dashboards/crm/BarChartRevenueGrowth'
|
||||
import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs'
|
||||
import RadarSalesChart from '@views/dashboards/crm/RadarSalesChart'
|
||||
import SalesByCountries from '@views/dashboards/crm/SalesByCountries'
|
||||
import ProjectStatus from '@views/dashboards/crm/ProjectStatus'
|
||||
import ActiveProjects from '@views/dashboards/crm/ActiveProjects'
|
||||
import LastTransaction from '@views/dashboards/crm/LastTransaction'
|
||||
import ActivityTimeline from '@views/dashboards/crm/ActivityTimeline'
|
||||
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'
|
||||
|
||||
// Server Action Imports
|
||||
import { getServerMode } from '@core/utils/serverHelpers'
|
||||
// Tabler icons component
|
||||
const TablerIcon = ({ name = '', className = '' }) => <i className={`tabler-${name} ${className}`} />
|
||||
|
||||
const DashboardCRM = async () => {
|
||||
// Vars
|
||||
const serverMode = await getServerMode()
|
||||
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 (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
|
||||
<DistributedBarChartOrder />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
|
||||
<LineAreaYearlySalesChart />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
|
||||
<CardStatVertical
|
||||
title='Total Profit'
|
||||
subtitle='Last Week'
|
||||
stats='1.28k'
|
||||
avatarColor='error'
|
||||
avatarIcon='tabler-credit-card'
|
||||
avatarSkin='light'
|
||||
avatarSize={44}
|
||||
chipText='-12.2%'
|
||||
chipColor='error'
|
||||
chipVariant='tonal'
|
||||
<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)'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
|
||||
<CardStatVertical
|
||||
title='Total Sales'
|
||||
subtitle='Last Week'
|
||||
stats='24.67k'
|
||||
avatarColor='success'
|
||||
avatarIcon='tabler-currency-dollar'
|
||||
avatarSkin='light'
|
||||
avatarSize={44}
|
||||
chipText='+24.67%'
|
||||
chipColor='success'
|
||||
chipVariant='tonal'
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='sales'
|
||||
stroke='#3B82F6'
|
||||
strokeWidth={3}
|
||||
dot={{ fill: '#3B82F6', strokeWidth: 2, r: 5 }}
|
||||
activeDot={{ r: 7, stroke: '#3B82F6', strokeWidth: 2 }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 8, lg: 4 }}>
|
||||
<BarChartRevenueGrowth />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 8 }}>
|
||||
<EarningReportsWithTabs />
|
||||
</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 serverMode={serverMode} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<ActivityTimeline />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,348 @@
|
||||
'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
|
||||
@ -0,0 +1,397 @@
|
||||
'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
|
||||
@ -0,0 +1,355 @@
|
||||
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;
|
||||
60
src/components/date-picker/PickerBasic.tsx
Normal file
60
src/components/date-picker/PickerBasic.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
// React Imports
|
||||
import React from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Props Type
|
||||
type PickerBasicProps = {
|
||||
dateFrom: Date | null
|
||||
dateTo: Date | null
|
||||
onChangeDateFrom: (date: Date | null) => void
|
||||
onChangeDateTo: (date: Date | null) => void
|
||||
labelFrom?: string
|
||||
labelTo?: string
|
||||
}
|
||||
|
||||
const PickerBasic: React.FC<PickerBasicProps> = ({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
onChangeDateFrom,
|
||||
onChangeDateTo
|
||||
}) => {
|
||||
return (
|
||||
<Grid container spacing={4}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<AppReactDatepicker
|
||||
selected={dateFrom}
|
||||
id='date-from'
|
||||
onChange={onChangeDateFrom}
|
||||
customInput={
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
className='bg-white rounded-lg shadow-sm border border-gray-300'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<AppReactDatepicker
|
||||
selected={dateTo}
|
||||
id='date-to'
|
||||
onChange={onChangeDateTo}
|
||||
customInput={
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
className='bg-white rounded-lg shadow-sm border border-gray-300'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default PickerBasic
|
||||
@ -83,15 +83,27 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
||||
<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>
|
||||
<MenuItem href={`/${locale}/dashboards/ecommerce`}>{dictionary['navigation'].eCommerce}</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>
|
||||
</SubMenu>
|
||||
<MenuSection label={dictionary['navigation'].appsPages}>
|
||||
<SubMenu label={dictionary['navigation'].eCommerce} icon={<i className='tabler-shopping-cart' />}>
|
||||
<MenuItem href={`/${locale}/apps/ecommerce/dashboard`}>{dictionary['navigation'].dashboard}</MenuItem>
|
||||
<SubMenu label={dictionary['navigation'].products}>
|
||||
<MenuItem href={`/${locale}/apps/ecommerce/products/list`}>{dictionary['navigation'].list}</MenuItem>
|
||||
<MenuItem className='hidden' href={`/${locale}/apps/ecommerce/products/${params.id}/detail`}>{dictionary['navigation'].details}</MenuItem>
|
||||
<MenuItem className='hidden' href={`/${locale}/apps/ecommerce/products/${params.id}/edit`}>{dictionary['navigation'].edit}</MenuItem>
|
||||
<MenuItem className='hidden' href={`/${locale}/apps/ecommerce/products/${params.id}/detail`}>
|
||||
{dictionary['navigation'].details}
|
||||
</MenuItem>
|
||||
<MenuItem className='hidden' href={`/${locale}/apps/ecommerce/products/${params.id}/edit`}>
|
||||
{dictionary['navigation'].edit}
|
||||
</MenuItem>
|
||||
<MenuItem href={`/${locale}/apps/ecommerce/products/add`}>{dictionary['navigation'].add}</MenuItem>
|
||||
<MenuItem href={`/${locale}/apps/ecommerce/products/category`}>
|
||||
{dictionary['navigation'].category}
|
||||
@ -109,7 +121,9 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
||||
</SubMenu>
|
||||
<SubMenu label={dictionary['navigation'].stock}>
|
||||
<MenuItem href={`/${locale}/apps/ecommerce/inventory/list`}>{dictionary['navigation'].list}</MenuItem>
|
||||
<MenuItem href={`/${locale}/apps/ecommerce/inventory/adjustment`}>{dictionary['navigation'].addjustment}</MenuItem>
|
||||
<MenuItem href={`/${locale}/apps/ecommerce/inventory/adjustment`}>
|
||||
{dictionary['navigation'].addjustment}
|
||||
</MenuItem>
|
||||
</SubMenu>
|
||||
<MenuItem href={`/${locale}/apps/ecommerce/settings`}>{dictionary['navigation'].settings}</MenuItem>
|
||||
</SubMenu>
|
||||
|
||||
69
src/services/queries/analytics.ts
Normal file
69
src/services/queries/analytics.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ProductSalesReport, SalesReport } from '../../types/services/analytic'
|
||||
import { api } from '../api'
|
||||
import { formatDateDDMMYYYY } from '../../utils/transform'
|
||||
|
||||
interface AnalyticQueryParams {
|
||||
date_from?: string
|
||||
date_to?: string
|
||||
}
|
||||
|
||||
export function useSalesAnalytics(params: AnalyticQueryParams = {}) {
|
||||
const today = new Date()
|
||||
const sevenDaysAgo = new Date()
|
||||
sevenDaysAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const defaultDateTo = formatDateDDMMYYYY(today)
|
||||
const defaultDateFrom = formatDateDDMMYYYY(sevenDaysAgo)
|
||||
|
||||
const { date_from = defaultDateFrom, date_to = defaultDateTo, ...filters } = params
|
||||
|
||||
return useQuery<SalesReport>({
|
||||
queryKey: ['analytics-sales', { 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/sales?${queryParams.toString()}`)
|
||||
return res.data.data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function useProductSalesAnalytics(params: AnalyticQueryParams = {}) {
|
||||
const today = new Date()
|
||||
const sevenDaysAgo = new Date()
|
||||
sevenDaysAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const defaultDateTo = formatDateDDMMYYYY(today)
|
||||
const defaultDateFrom = formatDateDDMMYYYY(sevenDaysAgo)
|
||||
|
||||
const { date_from = defaultDateFrom, date_to = defaultDateTo, ...filters } = params
|
||||
|
||||
return useQuery<ProductSalesReport>({
|
||||
queryKey: ['analytics-products', { 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/products?${queryParams.toString()}`)
|
||||
return res.data.data
|
||||
}
|
||||
})
|
||||
}
|
||||
48
src/types/services/analytic.ts
Normal file
48
src/types/services/analytic.ts
Normal file
@ -0,0 +1,48 @@
|
||||
export interface SalesSummary {
|
||||
total_sales: number;
|
||||
total_orders: number;
|
||||
total_items: number;
|
||||
average_order_value: number;
|
||||
total_tax: number;
|
||||
total_discount: number;
|
||||
net_sales: number;
|
||||
}
|
||||
|
||||
export interface SalesDataItem {
|
||||
date: string; // ISO string, e.g., "2025-08-03T00:00:00Z"
|
||||
sales: number;
|
||||
orders: number;
|
||||
items: number;
|
||||
tax: number;
|
||||
discount: number;
|
||||
net_sales: number;
|
||||
}
|
||||
|
||||
export interface SalesReport {
|
||||
organization_id: string;
|
||||
outlet_id: string;
|
||||
date_from: string; // ISO string with timezone, e.g., "2025-08-01T00:00:00+07:00"
|
||||
date_to: string; // ISO string with timezone
|
||||
group_by: string; // e.g., "day", "month", etc.
|
||||
summary: SalesSummary;
|
||||
data: SalesDataItem[];
|
||||
}
|
||||
|
||||
export interface ProductData {
|
||||
product_id: string
|
||||
product_name: string
|
||||
category_id: string
|
||||
category_name: string
|
||||
quantity_sold: number
|
||||
revenue: number
|
||||
average_price: number
|
||||
order_count: number
|
||||
}
|
||||
|
||||
export interface ProductSalesReport {
|
||||
organization_id: string
|
||||
outlet_id: string
|
||||
date_from: string
|
||||
date_to: string
|
||||
data: ProductData[]
|
||||
}
|
||||
@ -15,3 +15,10 @@ export const formatDate = (dateString: string) => {
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
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}`
|
||||
}
|
||||
|
||||
@ -243,8 +243,8 @@ const Login = ({ mode }: { mode: SystemMode }) => {
|
||||
Forgot password?
|
||||
</Typography>
|
||||
</div>
|
||||
<Button fullWidth variant='contained' type='submit'>
|
||||
Login
|
||||
<Button fullWidth variant='contained' type='submit' disabled={login.isPending}>
|
||||
{login.isPending ? 'Login...' : 'Login'}
|
||||
</Button>
|
||||
<div className='flex justify-center items-center flex-wrap gap-2'>
|
||||
<Typography>New on our platform?</Typography>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user