feat: analytics
This commit is contained in:
parent
a5d22db27b
commit
687f59a9fa
@ -1,87 +1,355 @@
|
|||||||
// MUI Imports
|
'use client'
|
||||||
import Grid from '@mui/material/Grid2'
|
|
||||||
|
|
||||||
// Component Imports
|
import React, { useEffect, useState } from 'react'
|
||||||
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
import {
|
||||||
import LineAreaYearlySalesChart from '@views/dashboards/crm/LineAreaYearlySalesChart'
|
LineChart,
|
||||||
import CardStatVertical from '@/components/card-statistics/Vertical'
|
Line,
|
||||||
import BarChartRevenueGrowth from '@views/dashboards/crm/BarChartRevenueGrowth'
|
XAxis,
|
||||||
import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs'
|
YAxis,
|
||||||
import RadarSalesChart from '@views/dashboards/crm/RadarSalesChart'
|
CartesianGrid,
|
||||||
import SalesByCountries from '@views/dashboards/crm/SalesByCountries'
|
Tooltip,
|
||||||
import ProjectStatus from '@views/dashboards/crm/ProjectStatus'
|
ResponsiveContainer,
|
||||||
import ActiveProjects from '@views/dashboards/crm/ActiveProjects'
|
BarChart,
|
||||||
import LastTransaction from '@views/dashboards/crm/LastTransaction'
|
Bar,
|
||||||
import ActivityTimeline from '@views/dashboards/crm/ActivityTimeline'
|
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
|
// Tabler icons component
|
||||||
import { getServerMode } from '@core/utils/serverHelpers'
|
const TablerIcon = ({ name = '', className = '' }) => <i className={`tabler-${name} ${className}`} />
|
||||||
|
|
||||||
const DashboardCRM = async () => {
|
const DashboardCRM = () => {
|
||||||
// Vars
|
const today = new Date()
|
||||||
const serverMode = await getServerMode()
|
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 (
|
return (
|
||||||
<Grid container spacing={6}>
|
<div className='mx-auto space-y-6'>
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
|
{/* Header */}
|
||||||
<DistributedBarChartOrder />
|
<div className='flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8'>
|
||||||
</Grid>
|
<div>
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
|
<h1 className='text-4xl font-bold text-gray-800 mb-2'>Sales Analytics Dashboard</h1>
|
||||||
<LineAreaYearlySalesChart />
|
<PickerBasic dateFrom={dateFrom} dateTo={dateTo} onChangeDateFrom={setDateFrom} onChangeDateTo={setDateTo} />
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
|
<div className='flex gap-3'>
|
||||||
<CardStatVertical
|
<div className='flex items-center gap-2 bg-blue-50 px-4 py-2 rounded-full'>
|
||||||
title='Total Profit'
|
<TablerIcon name='calendar' className='text-blue-600 text-sm' />
|
||||||
subtitle='Last Week'
|
<span className='text-blue-700 font-medium'>Grouped by {analytics?.group_by}</span>
|
||||||
stats='1.28k'
|
</div>
|
||||||
avatarColor='error'
|
<div className='flex items-center gap-2 bg-purple-50 px-4 py-2 rounded-full'>
|
||||||
avatarIcon='tabler-credit-card'
|
<TablerIcon name='chart-line' className='text-purple-600 text-sm' />
|
||||||
avatarSkin='light'
|
<span className='text-purple-700 font-medium'>{analytics?.data.length} data points</span>
|
||||||
avatarSize={44}
|
</div>
|
||||||
chipText='-12.2%'
|
</div>
|
||||||
chipColor='error'
|
</div>
|
||||||
chipVariant='tonal'
|
|
||||||
/>
|
{/* Metrics Cards */}
|
||||||
</Grid>
|
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6'>
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
|
{metrics.map((metric, index) => (
|
||||||
<CardStatVertical
|
<div
|
||||||
title='Total Sales'
|
key={index}
|
||||||
subtitle='Last Week'
|
className='bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100'
|
||||||
stats='24.67k'
|
>
|
||||||
avatarColor='success'
|
<div className='p-6'>
|
||||||
avatarIcon='tabler-currency-dollar'
|
<div className='flex items-start justify-between'>
|
||||||
avatarSkin='light'
|
<div className='flex-1'>
|
||||||
avatarSize={44}
|
<p className='text-gray-500 text-sm font-medium mb-2'>{metric.title}</p>
|
||||||
chipText='+24.67%'
|
<h3 className='text-2xl font-bold text-gray-800 mb-3'>{metric.value}</h3>
|
||||||
chipColor='success'
|
<div className='flex items-center gap-1'>
|
||||||
chipVariant='tonal'
|
<TablerIcon name='trending-up' className={`text-xs ${metric.changeColor}`} />
|
||||||
/>
|
<span className={`text-sm font-medium ${metric.changeColor}`}>{metric.change}</span>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={{ xs: 12, md: 8, lg: 4 }}>
|
</div>
|
||||||
<BarChartRevenueGrowth />
|
<div className={`p-4 rounded-xl ${metric.bgColor}`}>
|
||||||
</Grid>
|
<TablerIcon name={metric.icon} className={`text-2xl ${metric.color}`} />
|
||||||
<Grid size={{ xs: 12, lg: 8 }}>
|
</div>
|
||||||
<EarningReportsWithTabs />
|
</div>
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
</div>
|
||||||
<RadarSalesChart />
|
))}
|
||||||
</Grid>
|
</div>
|
||||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
|
||||||
<SalesByCountries />
|
{/* Charts Section */}
|
||||||
</Grid>
|
<div className='grid grid-cols-1 lg:grid-cols-3 gap-6'>
|
||||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
{/* Sales Trend Chart */}
|
||||||
<ProjectStatus />
|
<div className='lg:col-span-2 bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||||
</Grid>
|
<div className='p-6'>
|
||||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
<div className='flex items-center gap-3 mb-6'>
|
||||||
<ActiveProjects />
|
<TablerIcon name='chart-line' className='text-blue-600 text-2xl' />
|
||||||
</Grid>
|
<h2 className='text-xl font-semibold text-gray-800'>Sales Trend</h2>
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
</div>
|
||||||
<LastTransaction serverMode={serverMode} />
|
<div className='h-80'>
|
||||||
</Grid>
|
<ResponsiveContainer width='100%' height='100%'>
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
<LineChart data={chartData}>
|
||||||
<ActivityTimeline />
|
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||||
</Grid>
|
<XAxis dataKey='date' stroke='#6b7280' />
|
||||||
</Grid>
|
<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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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' />}>
|
<SubMenu label={dictionary['navigation'].dashboards} icon={<i className='tabler-smart-home' />}>
|
||||||
<MenuItem href={`/${locale}/dashboards/crm`}>{dictionary['navigation'].crm}</MenuItem>
|
<MenuItem href={`/${locale}/dashboards/crm`}>{dictionary['navigation'].crm}</MenuItem>
|
||||||
<MenuItem href={`/${locale}/dashboards/analytics`}>{dictionary['navigation'].analytics}</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>
|
</SubMenu>
|
||||||
<MenuSection label={dictionary['navigation'].appsPages}>
|
<MenuSection label={dictionary['navigation'].appsPages}>
|
||||||
<SubMenu label={dictionary['navigation'].eCommerce} icon={<i className='tabler-shopping-cart' />}>
|
<SubMenu label={dictionary['navigation'].eCommerce} icon={<i className='tabler-shopping-cart' />}>
|
||||||
<MenuItem href={`/${locale}/apps/ecommerce/dashboard`}>{dictionary['navigation'].dashboard}</MenuItem>
|
<MenuItem href={`/${locale}/apps/ecommerce/dashboard`}>{dictionary['navigation'].dashboard}</MenuItem>
|
||||||
<SubMenu label={dictionary['navigation'].products}>
|
<SubMenu label={dictionary['navigation'].products}>
|
||||||
<MenuItem href={`/${locale}/apps/ecommerce/products/list`}>{dictionary['navigation'].list}</MenuItem>
|
<MenuItem href={`/${locale}/apps/ecommerce/products/list`}>{dictionary['navigation'].list}</MenuItem>
|
||||||
<MenuItem className='hidden' href={`/${locale}/apps/ecommerce/products/${params.id}/detail`}>{dictionary['navigation'].details}</MenuItem>
|
<MenuItem className='hidden' href={`/${locale}/apps/ecommerce/products/${params.id}/detail`}>
|
||||||
<MenuItem className='hidden' href={`/${locale}/apps/ecommerce/products/${params.id}/edit`}>{dictionary['navigation'].edit}</MenuItem>
|
{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/add`}>{dictionary['navigation'].add}</MenuItem>
|
||||||
<MenuItem href={`/${locale}/apps/ecommerce/products/category`}>
|
<MenuItem href={`/${locale}/apps/ecommerce/products/category`}>
|
||||||
{dictionary['navigation'].category}
|
{dictionary['navigation'].category}
|
||||||
@ -109,7 +121,9 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
|||||||
</SubMenu>
|
</SubMenu>
|
||||||
<SubMenu label={dictionary['navigation'].stock}>
|
<SubMenu label={dictionary['navigation'].stock}>
|
||||||
<MenuItem href={`/${locale}/apps/ecommerce/inventory/list`}>{dictionary['navigation'].list}</MenuItem>
|
<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>
|
</SubMenu>
|
||||||
<MenuItem href={`/${locale}/apps/ecommerce/settings`}>{dictionary['navigation'].settings}</MenuItem>
|
<MenuItem href={`/${locale}/apps/ecommerce/settings`}>{dictionary['navigation'].settings}</MenuItem>
|
||||||
</SubMenu>
|
</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'
|
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?
|
Forgot password?
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<Button fullWidth variant='contained' type='submit'>
|
<Button fullWidth variant='contained' type='submit' disabled={login.isPending}>
|
||||||
Login
|
{login.isPending ? 'Login...' : 'Login'}
|
||||||
</Button>
|
</Button>
|
||||||
<div className='flex justify-center items-center flex-wrap gap-2'>
|
<div className='flex justify-center items-center flex-wrap gap-2'>
|
||||||
<Typography>New on our platform?</Typography>
|
<Typography>New on our platform?</Typography>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user