feat: analytic
This commit is contained in:
@@ -1,72 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Components Imports
|
||||
import WebsiteAnalyticsSlider from '@views/dashboards/analytics/WebsiteAnalyticsSlider'
|
||||
import LineAreaDailySalesChart from '@views/dashboards/analytics/LineAreaDailySalesChart'
|
||||
import SalesOverview from '@views/dashboards/analytics/SalesOverview'
|
||||
import EarningReports from '@views/dashboards/analytics/EarningReports'
|
||||
import SupportTracker from '@views/dashboards/analytics/SupportTracker'
|
||||
import SalesByCountries from '@views/dashboards/analytics/SalesByCountries'
|
||||
import TotalEarning from '@views/dashboards/analytics/TotalEarning'
|
||||
import MonthlyCampaignState from '@views/dashboards/analytics/MonthlyCampaignState'
|
||||
import SourceVisits from '@views/dashboards/analytics/SourceVisits'
|
||||
import ProjectsTable from '@views/dashboards/analytics/ProjectsTable'
|
||||
|
||||
/**
|
||||
* ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the
|
||||
* ! `.env` file found at root of your project and also update the API endpoints like `/pages/profile` in below example.
|
||||
* ! Also, remove the above server action import and the action itself from the `src/app/server/actions.ts` file to clean up unused code
|
||||
* ! because we've used the server action for getting our static data.
|
||||
*/
|
||||
|
||||
/* const getProfileData = async () => {
|
||||
// Vars
|
||||
const res = await fetch(`${process.env.API_URL}/pages/profile`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch profileData')
|
||||
}
|
||||
|
||||
return res.json()
|
||||
} */
|
||||
|
||||
const DashboardAnalytics = async () => {
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, lg: 6 }}>
|
||||
<WebsiteAnalyticsSlider />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
|
||||
<LineAreaDailySalesChart />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
|
||||
<SalesOverview />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<EarningReports />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<SupportTracker />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<SalesByCountries />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<TotalEarning />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<MonthlyCampaignState />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<SourceVisits />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 8 }}>
|
||||
<ProjectsTable projectTable={[]} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardAnalytics
|
||||
@@ -1,356 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
BarChart,
|
||||
Bar,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell
|
||||
} from 'recharts'
|
||||
import { useSalesAnalytics } from '../../../../../../services/queries/analytics'
|
||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||
import PickerBasic from '../../../../../../components/date-picker/PickerBasic'
|
||||
import { formatDateDDMMYYYY } from '../../../../../../utils/transform'
|
||||
|
||||
// Tabler icons component
|
||||
const TablerIcon = ({ name = '', className = '' }) => <i className={`tabler-${name} ${className}`} />
|
||||
|
||||
const DashboardCRM = () => {
|
||||
const today = new Date()
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const [dateFrom, setDateFrom] = useState<Date | null>(monthAgo)
|
||||
const [dateTo, setDateTo] = useState<Date | null>(today)
|
||||
|
||||
const { data: analytics, isLoading } = useSalesAnalytics({
|
||||
date_from: formatDateDDMMYYYY(dateFrom!),
|
||||
date_to: formatDateDDMMYYYY(dateTo!)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (analytics) {
|
||||
setDateFrom(new Date(analytics.date_from))
|
||||
setDateTo(new Date(analytics.date_to))
|
||||
}
|
||||
}, [analytics])
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount: any) => {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
// Format date
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// Format long date
|
||||
const formatLongDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// Prepare chart data
|
||||
const chartData = analytics?.data.map((item: any) => ({
|
||||
date: formatDate(item.date),
|
||||
sales: item.sales,
|
||||
orders: item.orders,
|
||||
items: item.items,
|
||||
averageOrderValue: item.orders > 0 ? Math.round(item.sales / item.orders) : 0
|
||||
}))
|
||||
|
||||
// Colors for charts
|
||||
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']
|
||||
|
||||
// Metric cards data
|
||||
const metrics = [
|
||||
{
|
||||
title: 'Total Sales',
|
||||
value: formatCurrency(analytics?.summary.total_sales),
|
||||
icon: 'currency-dollar',
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
change: '+12.5%',
|
||||
changeColor: 'text-green-600'
|
||||
},
|
||||
{
|
||||
title: 'Total Orders',
|
||||
value: analytics?.summary.total_orders.toLocaleString(),
|
||||
icon: 'shopping-cart',
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50',
|
||||
change: '+8.2%',
|
||||
changeColor: 'text-blue-600'
|
||||
},
|
||||
{
|
||||
title: 'Total Items',
|
||||
value: analytics?.summary.total_items.toLocaleString(),
|
||||
icon: 'package',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50',
|
||||
change: '+15.1%',
|
||||
changeColor: 'text-purple-600'
|
||||
},
|
||||
{
|
||||
title: 'Average Order Value',
|
||||
value: formatCurrency(analytics?.summary.average_order_value),
|
||||
icon: 'trending-up',
|
||||
color: 'text-orange-600',
|
||||
bgColor: 'bg-orange-50',
|
||||
change: '+4.3%',
|
||||
changeColor: 'text-orange-600'
|
||||
}
|
||||
]
|
||||
|
||||
// Performance data for pie chart
|
||||
const performanceData = analytics?.data.map((item: any, index: any) => ({
|
||||
name: formatDate(item.date),
|
||||
value: item.sales,
|
||||
color: colors[index % colors.length]
|
||||
}))
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<div className='mx-auto space-y-6'>
|
||||
{/* Header */}
|
||||
<div className='flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8'>
|
||||
<div>
|
||||
<h1 className='text-4xl font-bold text-gray-800 mb-2'>Sales Analytics Dashboard</h1>
|
||||
<PickerBasic dateFrom={dateFrom} dateTo={dateTo} onChangeDateFrom={setDateFrom} onChangeDateTo={setDateTo} />
|
||||
</div>
|
||||
<div className='flex gap-3'>
|
||||
<div className='flex items-center gap-2 bg-blue-50 px-4 py-2 rounded-full'>
|
||||
<TablerIcon name='calendar' className='text-blue-600 text-sm' />
|
||||
<span className='text-blue-700 font-medium'>Grouped by {analytics?.group_by}</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 bg-purple-50 px-4 py-2 rounded-full'>
|
||||
<TablerIcon name='chart-line' className='text-purple-600 text-sm' />
|
||||
<span className='text-purple-700 font-medium'>{analytics?.data.length} data points</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Cards */}
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6'>
|
||||
{metrics.map((metric, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100'
|
||||
>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-start justify-between'>
|
||||
<div className='flex-1'>
|
||||
<p className='text-gray-500 text-sm font-medium mb-2'>{metric.title}</p>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-3'>{metric.value}</h3>
|
||||
<div className='flex items-center gap-1'>
|
||||
<TablerIcon name='trending-up' className={`text-xs ${metric.changeColor}`} />
|
||||
<span className={`text-sm font-medium ${metric.changeColor}`}>{metric.change}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-4 rounded-xl ${metric.bgColor}`}>
|
||||
<TablerIcon name={metric.icon} className={`text-2xl ${metric.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className='grid grid-cols-1 lg:grid-cols-3 gap-6'>
|
||||
{/* Sales Trend Chart */}
|
||||
<div className='lg:col-span-2 bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-line' className='text-blue-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Sales Trend</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||
<XAxis dataKey='date' stroke='#6b7280' />
|
||||
<YAxis tickFormatter={value => `${(value / 1000).toFixed(0)}K`} stroke='#6b7280' />
|
||||
<Tooltip
|
||||
formatter={(value, name) => [
|
||||
name === 'sales' ? formatCurrency(value) : value.toLocaleString(),
|
||||
name === 'sales' ? 'Sales' : name === 'orders' ? 'Orders' : 'Items'
|
||||
]}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='sales'
|
||||
stroke='#3B82F6'
|
||||
strokeWidth={3}
|
||||
dot={{ fill: '#3B82F6', strokeWidth: 2, r: 5 }}
|
||||
activeDot={{ r: 7, stroke: '#3B82F6', strokeWidth: 2 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sales Distribution */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-pie' className='text-purple-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Sales Distribution</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={performanceData}
|
||||
cx='50%'
|
||||
cy='50%'
|
||||
outerRadius={90}
|
||||
dataKey='value'
|
||||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||
>
|
||||
{performanceData?.map((entry: any, index: any) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={value => [formatCurrency(value), 'Sales']}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Orders & Items Chart */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-bar' className='text-green-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Orders & Items Analysis</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||
<XAxis dataKey='date' stroke='#6b7280' />
|
||||
<YAxis stroke='#6b7280' />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey='orders' fill='#10B981' name='Orders' radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey='items' fill='#3B82F6' name='Items' radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed Data Table */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='table' className='text-indigo-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Daily Performance Details</h2>
|
||||
</div>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='bg-gray-50 border-b border-gray-200'>
|
||||
<th className='text-left py-4 px-6 font-semibold text-gray-700'>Date</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Sales</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Orders</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Items</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Avg Order Value</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Net Sales</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{analytics?.data.map((row: any, index: any) => (
|
||||
<tr key={index} className='border-b border-gray-100 hover:bg-gray-50 transition-colors'>
|
||||
<td className='py-4 px-6 font-medium text-gray-800'>{formatLongDate(row.date)}</td>
|
||||
<td className='py-4 px-6 text-right font-semibold text-green-600'>{formatCurrency(row.sales)}</td>
|
||||
<td className='py-4 px-6 text-right text-gray-700'>{row.orders.toLocaleString()}</td>
|
||||
<td className='py-4 px-6 text-right text-gray-700'>{row.items.toLocaleString()}</td>
|
||||
<td className='py-4 px-6 text-right font-medium text-gray-800'>
|
||||
{formatCurrency(row.orders > 0 ? Math.round(row.sales / row.orders) : 0)}
|
||||
</td>
|
||||
<td className='py-4 px-6 text-right font-semibold text-gray-800'>
|
||||
{formatCurrency(row.net_sales)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Footer */}
|
||||
<div className='bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-100'>
|
||||
<div className='p-6'>
|
||||
<div className='grid grid-cols-2 md:grid-cols-4 gap-6'>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{formatCurrency(analytics?.summary.net_sales)}</h3>
|
||||
<p className='text-gray-600 font-medium'>Net Sales</p>
|
||||
</div>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{analytics?.summary.total_orders}</h3>
|
||||
<p className='text-gray-600 font-medium'>Total Orders</p>
|
||||
</div>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{formatCurrency(analytics?.summary.total_tax)}</h3>
|
||||
<p className='text-gray-600 font-medium'>Total Tax</p>
|
||||
</div>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>
|
||||
{formatCurrency(analytics?.summary.total_discount)}
|
||||
</h3>
|
||||
<p className='text-gray-600 font-medium'>Total Discount</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardCRM
|
||||
@@ -1,348 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
BarChart,
|
||||
Bar,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell
|
||||
} from 'recharts'
|
||||
import PickerBasic from '../../../../../../../components/date-picker/PickerBasic'
|
||||
import Loading from '../../../../../../../components/layout/shared/Loading'
|
||||
import { useSalesAnalytics } from '../../../../../../../services/queries/analytics'
|
||||
import { formatDateDDMMYYYY } from '../../../../../../../utils/transform'
|
||||
|
||||
// Tabler icons component
|
||||
const TablerIcon = ({ name = '', className = '' }) => <i className={`tabler-${name} ${className}`} />
|
||||
|
||||
const EcomerceOrderReport = () => {
|
||||
const today = new Date()
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const [dateFrom, setDateFrom] = useState<Date | null>(monthAgo)
|
||||
const [dateTo, setDateTo] = useState<Date | null>(today)
|
||||
|
||||
const { data: analytics, isLoading } = useSalesAnalytics({
|
||||
date_from: formatDateDDMMYYYY(dateFrom!),
|
||||
date_to: formatDateDDMMYYYY(dateTo!)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (analytics) {
|
||||
setDateFrom(new Date(analytics.date_from))
|
||||
setDateTo(new Date(analytics.date_to))
|
||||
}
|
||||
}, [analytics])
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount: any) => {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
// Format date
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// Format long date
|
||||
const formatLongDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// Prepare chart data
|
||||
const chartData = analytics?.data.map((item: any) => ({
|
||||
date: formatDate(item.date),
|
||||
sales: item.sales,
|
||||
orders: item.orders,
|
||||
items: item.items,
|
||||
averageOrderValue: item.orders > 0 ? Math.round(item.sales / item.orders) : 0
|
||||
}))
|
||||
|
||||
// Colors for charts
|
||||
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']
|
||||
|
||||
// Metric cards data
|
||||
const metrics = [
|
||||
{
|
||||
title: 'Total Sales',
|
||||
value: formatCurrency(analytics?.summary.total_sales),
|
||||
icon: 'currency-dollar',
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
change: '+12.5%',
|
||||
changeColor: 'text-green-600'
|
||||
},
|
||||
{
|
||||
title: 'Total Orders',
|
||||
value: analytics?.summary.total_orders.toLocaleString(),
|
||||
icon: 'shopping-cart',
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50',
|
||||
change: '+8.2%',
|
||||
changeColor: 'text-blue-600'
|
||||
},
|
||||
{
|
||||
title: 'Total Items',
|
||||
value: analytics?.summary.total_items.toLocaleString(),
|
||||
icon: 'package',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50',
|
||||
change: '+15.1%',
|
||||
changeColor: 'text-purple-600'
|
||||
},
|
||||
{
|
||||
title: 'Average Order Value',
|
||||
value: formatCurrency(analytics?.summary.average_order_value),
|
||||
icon: 'trending-up',
|
||||
color: 'text-orange-600',
|
||||
bgColor: 'bg-orange-50',
|
||||
change: '+4.3%',
|
||||
changeColor: 'text-orange-600'
|
||||
}
|
||||
]
|
||||
|
||||
// Performance data for pie chart
|
||||
const performanceData = analytics?.data.map((item: any, index: any) => ({
|
||||
name: formatDate(item.date),
|
||||
value: item.sales,
|
||||
color: colors[index % colors.length]
|
||||
}))
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<div className='mx-auto space-y-6'>
|
||||
{/* Header */}
|
||||
<div className='flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8 bg-white shadow p-6 rounded-xl'>
|
||||
<div>
|
||||
<h1 className='text-4xl font-bold text-gray-800 mb-2'>Sales Analytics</h1>
|
||||
</div>
|
||||
<div className='flex gap-3'>
|
||||
<PickerBasic dateFrom={dateFrom} dateTo={dateTo} onChangeDateFrom={setDateFrom} onChangeDateTo={setDateTo} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Cards */}
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6'>
|
||||
{metrics.map((metric, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100'
|
||||
>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-start justify-between'>
|
||||
<div className='flex-1'>
|
||||
<p className='text-gray-500 text-sm font-medium mb-2'>{metric.title}</p>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-3'>{metric.value}</h3>
|
||||
<div className='flex items-center gap-1'>
|
||||
<TablerIcon name='trending-up' className={`text-xs ${metric.changeColor}`} />
|
||||
<span className={`text-sm font-medium ${metric.changeColor}`}>{metric.change}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-4 rounded-xl ${metric.bgColor}`}>
|
||||
<TablerIcon name={metric.icon} className={`text-2xl ${metric.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className='grid grid-cols-1 lg:grid-cols-3 gap-6'>
|
||||
{/* Sales Trend Chart */}
|
||||
<div className='lg:col-span-2 bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-line' className='text-blue-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Sales Trend</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||
<XAxis dataKey='date' stroke='#6b7280' />
|
||||
<YAxis tickFormatter={value => `${(value / 1000).toFixed(0)}K`} stroke='#6b7280' />
|
||||
<Tooltip
|
||||
formatter={(value, name) => [
|
||||
name === 'sales' ? formatCurrency(value) : value.toLocaleString(),
|
||||
name === 'sales' ? 'Sales' : name === 'orders' ? 'Orders' : 'Items'
|
||||
]}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='sales'
|
||||
stroke='#3B82F6'
|
||||
strokeWidth={3}
|
||||
dot={{ fill: '#3B82F6', strokeWidth: 2, r: 5 }}
|
||||
activeDot={{ r: 7, stroke: '#3B82F6', strokeWidth: 2 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sales Distribution */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-pie' className='text-purple-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Sales Distribution</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={performanceData}
|
||||
cx='50%'
|
||||
cy='50%'
|
||||
outerRadius={90}
|
||||
dataKey='value'
|
||||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||
>
|
||||
{performanceData?.map((entry: any, index: any) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={value => [formatCurrency(value), 'Sales']}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Orders & Items Chart */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-bar' className='text-green-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Orders & Items Analysis</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||
<XAxis dataKey='date' stroke='#6b7280' />
|
||||
<YAxis stroke='#6b7280' />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey='orders' fill='#10B981' name='Orders' radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey='items' fill='#3B82F6' name='Items' radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed Data Table */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='table' className='text-indigo-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Daily Performance Details</h2>
|
||||
</div>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='bg-gray-50 border-b border-gray-200'>
|
||||
<th className='text-left py-4 px-6 font-semibold text-gray-700'>Date</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Sales</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Orders</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Items</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Avg Order Value</th>
|
||||
<th className='text-right py-4 px-6 font-semibold text-gray-700'>Net Sales</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{analytics?.data.map((row: any, index: any) => (
|
||||
<tr key={index} className='border-b border-gray-100 hover:bg-gray-50 transition-colors'>
|
||||
<td className='py-4 px-6 font-medium text-gray-800'>{formatLongDate(row.date)}</td>
|
||||
<td className='py-4 px-6 text-right font-semibold text-green-600'>{formatCurrency(row.sales)}</td>
|
||||
<td className='py-4 px-6 text-right text-gray-700'>{row.orders.toLocaleString()}</td>
|
||||
<td className='py-4 px-6 text-right text-gray-700'>{row.items.toLocaleString()}</td>
|
||||
<td className='py-4 px-6 text-right font-medium text-gray-800'>
|
||||
{formatCurrency(row.orders > 0 ? Math.round(row.sales / row.orders) : 0)}
|
||||
</td>
|
||||
<td className='py-4 px-6 text-right font-semibold text-gray-800'>
|
||||
{formatCurrency(row.net_sales)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Footer */}
|
||||
<div className='bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-100'>
|
||||
<div className='p-6'>
|
||||
<div className='grid grid-cols-2 md:grid-cols-4 gap-6'>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{formatCurrency(analytics?.summary.net_sales)}</h3>
|
||||
<p className='text-gray-600 font-medium'>Net Sales</p>
|
||||
</div>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{analytics?.summary.total_orders}</h3>
|
||||
<p className='text-gray-600 font-medium'>Total Orders</p>
|
||||
</div>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{formatCurrency(analytics?.summary.total_tax)}</h3>
|
||||
<p className='text-gray-600 font-medium'>Total Tax</p>
|
||||
</div>
|
||||
<div className='text-center'>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>
|
||||
{formatCurrency(analytics?.summary.total_discount)}
|
||||
</h3>
|
||||
<p className='text-gray-600 font-medium'>Total Discount</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EcomerceOrderReport
|
||||
@@ -1,7 +0,0 @@
|
||||
import EcommerceDashboard from '../../apps/ecommerce/dashboard/page'
|
||||
|
||||
const DashboardECommerce = () => {
|
||||
return <EcommerceDashboard />
|
||||
}
|
||||
|
||||
export default DashboardECommerce
|
||||
@@ -1,397 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
LineChart,
|
||||
Line
|
||||
} from 'recharts'
|
||||
import { useProductSalesAnalytics } from '../../../../../../../services/queries/analytics'
|
||||
import Loading from '../../../../../../../components/layout/shared/Loading'
|
||||
import { formatDateDDMMYYYY } from '../../../../../../../utils/transform'
|
||||
import PickerBasic from '../../../../../../../components/date-picker/PickerBasic'
|
||||
|
||||
// Tabler icons component
|
||||
const TablerIcon = ({ name = '', className = '' }) => <i className={`tabler-${name} ${className}`} />
|
||||
|
||||
const EcomerceProductReport = () => {
|
||||
const today = new Date()
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setDate(today.getDate() - 30)
|
||||
|
||||
const [dateFrom, setDateFrom] = useState<Date | null>(monthAgo)
|
||||
const [dateTo, setDateTo] = useState<Date | null>(today)
|
||||
const [selectedCategory, setSelectedCategory] = useState('all')
|
||||
const [sortBy, setSortBy] = useState('revenue')
|
||||
const [sortOrder, setSortOrder] = useState('desc')
|
||||
|
||||
const { data: analytics, isLoading } = useProductSalesAnalytics({
|
||||
date_from: formatDateDDMMYYYY(dateFrom!),
|
||||
date_to: formatDateDDMMYYYY(dateTo!)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (analytics) {
|
||||
setDateFrom(new Date(analytics.date_from))
|
||||
setDateTo(new Date(analytics.date_to))
|
||||
}
|
||||
}, [analytics])
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount: any) => {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
// Calculate summary metrics
|
||||
const summary = {
|
||||
totalProducts: analytics?.data.length,
|
||||
totalQuantitySold: analytics?.data.reduce((sum, item) => sum + item.quantity_sold, 0),
|
||||
totalRevenue: analytics?.data.reduce((sum, item) => sum + item.revenue, 0),
|
||||
totalOrders: analytics?.data.reduce((sum, item) => sum + item.order_count, 0),
|
||||
averageOrderValue: analytics?.data
|
||||
? analytics!.data.reduce((sum, item) => sum + item.revenue, 0) /
|
||||
analytics!.data.reduce((sum, item) => sum + item.order_count, 0)
|
||||
: 0
|
||||
}
|
||||
|
||||
// Get unique categories
|
||||
const categories = ['all', ...new Set(analytics?.data.map(item => item.category_name))]
|
||||
|
||||
// Filter and sort data
|
||||
const filteredData = analytics?.data
|
||||
.filter(item => selectedCategory === 'all' || item.category_name === selectedCategory)
|
||||
.sort((a: any, b: any) => {
|
||||
const multiplier = sortOrder === 'desc' ? -1 : 1
|
||||
return (a[sortBy] - b[sortBy]) * multiplier
|
||||
})
|
||||
|
||||
// Prepare chart data
|
||||
const chartData = filteredData?.slice(0, 10).map(item => ({
|
||||
name: item.product_name.length > 15 ? item.product_name.substring(0, 15) + '...' : item.product_name,
|
||||
fullName: item.product_name,
|
||||
revenue: item.revenue,
|
||||
quantity: item.quantity_sold,
|
||||
orders: item.order_count,
|
||||
avgPrice: item.average_price
|
||||
}))
|
||||
|
||||
// Category performance data
|
||||
const categoryData = categories
|
||||
.filter(cat => cat !== 'all')
|
||||
.map(category => {
|
||||
const categoryItems = analytics?.data.filter(item => item.category_name === category)
|
||||
return {
|
||||
name: category,
|
||||
revenue: categoryItems?.reduce((sum, item) => sum + item.revenue, 0),
|
||||
quantity: categoryItems?.reduce((sum, item) => sum + item.quantity_sold, 0),
|
||||
products: categoryItems?.length
|
||||
}
|
||||
})
|
||||
|
||||
// Colors for charts
|
||||
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#06B6D4', '#84CC16', '#F97316']
|
||||
|
||||
// Metric cards data
|
||||
const metrics = [
|
||||
{
|
||||
title: 'Total Revenue',
|
||||
value: formatCurrency(summary.totalRevenue),
|
||||
icon: 'currency-dollar',
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
subtitle: 'From all products'
|
||||
},
|
||||
{
|
||||
title: 'Products Sold',
|
||||
value: summary.totalQuantitySold?.toLocaleString(),
|
||||
icon: 'package',
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50',
|
||||
subtitle: `${summary.totalProducts} different products`
|
||||
},
|
||||
{
|
||||
title: 'Total Orders',
|
||||
value: summary.totalOrders?.toLocaleString(),
|
||||
icon: 'shopping-cart',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50',
|
||||
subtitle: 'Completed orders'
|
||||
},
|
||||
{
|
||||
title: 'Avg Order Value',
|
||||
value: formatCurrency(summary.averageOrderValue || 0),
|
||||
icon: 'trending-up',
|
||||
color: 'text-orange-600',
|
||||
bgColor: 'bg-orange-50',
|
||||
subtitle: 'Per order average'
|
||||
}
|
||||
]
|
||||
|
||||
const handleSort = (field: any) => {
|
||||
if (sortBy === field) {
|
||||
setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc')
|
||||
} else {
|
||||
setSortBy(field)
|
||||
setSortOrder('desc')
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<div className='mx-auto space-y-6'>
|
||||
{/* Header */}
|
||||
<div className='flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 mb-8 bg-white shadow p-6 rounded-xl'>
|
||||
<div>
|
||||
<h1 className='text-4xl font-bold text-gray-800 mb-2'>Product Analytics</h1>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className='flex flex-col sm:flex-row gap-4'>
|
||||
<PickerBasic dateFrom={dateFrom} dateTo={dateTo} onChangeDateFrom={setDateFrom} onChangeDateTo={setDateTo} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6'>
|
||||
{metrics.map((metric, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100'
|
||||
>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-start justify-between'>
|
||||
<div className='flex-1'>
|
||||
<p className='text-gray-500 text-sm font-medium mb-2'>{metric.title}</p>
|
||||
<h3 className='text-2xl font-bold text-gray-800 mb-1'>{metric.value}</h3>
|
||||
<p className='text-gray-500 text-xs'>{metric.subtitle}</p>
|
||||
</div>
|
||||
<div className={`p-4 rounded-xl ${metric.bgColor}`}>
|
||||
<TablerIcon name={metric.icon} className={`text-2xl ${metric.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className='grid grid-cols-1 xl:grid-cols-3 gap-6'>
|
||||
{/* Product Revenue Chart */}
|
||||
<div className='xl:col-span-2 bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-bar' className='text-blue-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Top Products by Revenue</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<BarChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 60 }}>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||
<XAxis dataKey='name' stroke='#6b7280' angle={-45} textAnchor='end' height={100} fontSize={12} />
|
||||
<YAxis tickFormatter={value => `${(value / 1000).toFixed(0)}K`} stroke='#6b7280' />
|
||||
<Tooltip
|
||||
formatter={(value, name) => [
|
||||
name === 'revenue' ? formatCurrency(value) : value.toLocaleString(),
|
||||
name === 'revenue' ? 'Revenue' : name === 'quantity' ? 'Quantity Sold' : 'Orders'
|
||||
]}
|
||||
labelFormatter={label => {
|
||||
const item = chartData?.find(d => d.name === label)
|
||||
return item ? item.fullName : label
|
||||
}}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey='revenue' fill='#3B82F6' radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Distribution */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-pie' className='text-purple-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Category Distribution</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={categoryData}
|
||||
cx='50%'
|
||||
cy='50%'
|
||||
outerRadius={80}
|
||||
dataKey='revenue'
|
||||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||
>
|
||||
{categoryData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={value => [formatCurrency(value), 'Revenue']}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quantity vs Orders Chart */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<TablerIcon name='chart-line' className='text-green-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Quantity Sold vs Order Count</h2>
|
||||
</div>
|
||||
<div className='h-80'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#f0f0f0' />
|
||||
<XAxis dataKey='name' stroke='#6b7280' angle={-45} textAnchor='end' height={100} fontSize={12} />
|
||||
<YAxis stroke='#6b7280' />
|
||||
<Tooltip
|
||||
labelFormatter={label => {
|
||||
const item = chartData?.find(d => d.name === label)
|
||||
return item ? item.fullName : label
|
||||
}}
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey='quantity' fill='#10B981' name='Quantity Sold' radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey='orders' fill='#F59E0B' name='Order Count' radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed Products Table */}
|
||||
<div className='bg-white rounded-xl shadow-lg border border-gray-100'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-center justify-between mb-6'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<TablerIcon name='table' className='text-indigo-600 text-2xl' />
|
||||
<h2 className='text-xl font-semibold text-gray-800'>Product Performance Details</h2>
|
||||
</div>
|
||||
<div className='text-sm text-gray-500'>Showing {filteredData?.length} products</div>
|
||||
</div>
|
||||
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='bg-gray-50 border-b border-gray-200'>
|
||||
<th className='text-left py-4 px-6 font-semibold text-gray-700'>Product</th>
|
||||
<th className='text-left py-4 px-6 font-semibold text-gray-700'>Category</th>
|
||||
<th
|
||||
className='text-right py-4 px-6 font-semibold text-gray-700 cursor-pointer hover:text-blue-600 transition-colors'
|
||||
onClick={() => handleSort('quantity_sold')}
|
||||
>
|
||||
<div className='flex items-center justify-end gap-1'>
|
||||
Quantity Sold
|
||||
<TablerIcon
|
||||
name={sortBy === 'quantity_sold' && sortOrder === 'desc' ? 'chevron-down' : 'chevron-up'}
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className='text-right py-4 px-6 font-semibold text-gray-700 cursor-pointer hover:text-blue-600 transition-colors'
|
||||
onClick={() => handleSort('revenue')}
|
||||
>
|
||||
<div className='flex items-center justify-end gap-1'>
|
||||
Revenue
|
||||
<TablerIcon
|
||||
name={sortBy === 'revenue' && sortOrder === 'desc' ? 'chevron-down' : 'chevron-up'}
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className='text-right py-4 px-6 font-semibold text-gray-700 cursor-pointer hover:text-blue-600 transition-colors'
|
||||
onClick={() => handleSort('average_price')}
|
||||
>
|
||||
<div className='flex items-center justify-end gap-1'>
|
||||
Avg Price
|
||||
<TablerIcon
|
||||
name={sortBy === 'average_price' && sortOrder === 'desc' ? 'chevron-down' : 'chevron-up'}
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className='text-right py-4 px-6 font-semibold text-gray-700 cursor-pointer hover:text-blue-600 transition-colors'
|
||||
onClick={() => handleSort('order_count')}
|
||||
>
|
||||
<div className='flex items-center justify-end gap-1'>
|
||||
Orders
|
||||
<TablerIcon
|
||||
name={sortBy === 'order_count' && sortOrder === 'desc' ? 'chevron-down' : 'chevron-up'}
|
||||
className='text-xs'
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredData?.map((product, index) => (
|
||||
<tr key={product.product_id} className='border-b border-gray-100 hover:bg-gray-50 transition-colors'>
|
||||
<td className='py-4 px-6'>
|
||||
<div className='font-medium text-gray-800'>{product.product_name}</div>
|
||||
</td>
|
||||
<td className='py-4 px-6'>
|
||||
<span className='inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800'>
|
||||
{product.category_name}
|
||||
</span>
|
||||
</td>
|
||||
<td className='py-4 px-6 text-right font-semibold text-gray-800'>
|
||||
{product.quantity_sold.toLocaleString()}
|
||||
</td>
|
||||
<td className='py-4 px-6 text-right font-semibold text-green-600'>
|
||||
{formatCurrency(product.revenue)}
|
||||
</td>
|
||||
<td className='py-4 px-6 text-right text-gray-700'>{formatCurrency(product.average_price)}</td>
|
||||
<td className='py-4 px-6 text-right text-gray-700'>{product.order_count.toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EcomerceProductReport
|
||||
@@ -1,355 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const TablerIcon = ({ name = '', className = '' }) => <i className={`tabler-${name} ${className}`} />;
|
||||
|
||||
const PaymentMethodAnalytics = () => {
|
||||
// Default data structure for demonstration
|
||||
const defaultData = {
|
||||
"organization_id": "4ecf5cfb-9ac1-4fbd-94e4-9f149f07460a",
|
||||
"outlet_id": "d5b38fc4-8df7-4d54-99e2-b8825977f5ca",
|
||||
"date_from": "2025-07-05T00:00:00+07:00",
|
||||
"date_to": "2025-08-05T23:59:59.999999999+07:00",
|
||||
"group_by": "day",
|
||||
"summary": {
|
||||
"total_amount": 212000,
|
||||
"total_orders": 6,
|
||||
"total_payments": 6,
|
||||
"average_order_value": 35333.333333333336
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
"payment_method_id": "4b1c0d21-c98a-4fc0-a2f9-8d90a0c9d905",
|
||||
"payment_method_name": "CASH",
|
||||
"payment_method_type": "cash",
|
||||
"total_amount": 212000,
|
||||
"order_count": 6,
|
||||
"payment_count": 6,
|
||||
"percentage": 100
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const analyticsData = defaultData;
|
||||
const { summary, data: paymentMethods } = analyticsData;
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount: any) => {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// Format date
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
// Get icon for payment method
|
||||
const getPaymentMethodIcon = (type: any) => {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'cash':
|
||||
return <TablerIcon name="cash" className="w-5 h-5" />;
|
||||
case 'card':
|
||||
case 'credit_card':
|
||||
case 'debit_card':
|
||||
return <TablerIcon name="credit-card" className="w-5 h-5" />;
|
||||
default:
|
||||
return <TablerIcon name="coins" className="w-5 h-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
// Get color classes for payment method type
|
||||
const getPaymentMethodColors = (type: any) => {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'cash':
|
||||
return {
|
||||
chip: 'bg-green-100 text-green-800 border-green-200',
|
||||
progress: 'bg-green-500'
|
||||
};
|
||||
case 'card':
|
||||
case 'credit_card':
|
||||
case 'debit_card':
|
||||
return {
|
||||
chip: 'bg-blue-100 text-blue-800 border-blue-200',
|
||||
progress: 'bg-blue-500'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
chip: 'bg-gray-100 text-gray-800 border-gray-200',
|
||||
progress: 'bg-gray-500'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Progress Bar Component
|
||||
const ProgressBar = ({ value, className, color }: any) => {
|
||||
return (
|
||||
<div className={`w-full bg-gray-200 rounded-full h-2 ${className}`}>
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-300 ${color}`}
|
||||
style={{ width: `${Math.min(value, 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Chip Component
|
||||
const Chip = ({ label, colorClasses }: any) => {
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${colorClasses}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto space-y-6 bg-gray-50 min-h-screen">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 bg-blue-600 rounded-lg">
|
||||
<TablerIcon name="trending-up" className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
Payment Analytics
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<TablerIcon name="calendar" className="w-4 h-4 text-gray-500" />
|
||||
<p className="text-sm text-gray-600">
|
||||
{formatDate(analyticsData.date_from)} - {formatDate(analyticsData.date_to)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div className="p-6 bg-gradient-to-br from-blue-50 to-blue-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-blue-600 mb-1">
|
||||
Total Revenue
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-blue-900">
|
||||
{formatCurrency(summary.total_amount)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-blue-600 rounded-full">
|
||||
<TablerIcon name="receipt" className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div className="p-6 bg-gradient-to-br from-green-50 to-green-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-green-600 mb-1">
|
||||
Total Orders
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-green-900">
|
||||
{summary.total_orders.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-green-600 rounded-full">
|
||||
<TablerIcon name="cash" className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div className="p-6 bg-gradient-to-br from-purple-50 to-purple-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-purple-600 mb-1">
|
||||
Total Payments
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-purple-900">
|
||||
{summary.total_payments.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-purple-600 rounded-full">
|
||||
<TablerIcon name="credit-card" className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div className="p-6 bg-gradient-to-br from-orange-50 to-orange-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-orange-600 mb-1">
|
||||
Avg Order Value
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-orange-900">
|
||||
{formatCurrency(summary.average_order_value)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-orange-600 rounded-full">
|
||||
<TablerIcon name="trending-up" className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Methods Breakdown */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<TablerIcon name="credit-card" className="w-6 h-6 text-blue-600" />
|
||||
<h2 className="text-xl font-bold text-gray-900">
|
||||
Payment Methods Breakdown
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b border-gray-200">
|
||||
<th className="text-left p-4 font-semibold text-gray-900">Payment Method</th>
|
||||
<th className="text-left p-4 font-semibold text-gray-900">Type</th>
|
||||
<th className="text-right p-4 font-semibold text-gray-900">Amount</th>
|
||||
<th className="text-right p-4 font-semibold text-gray-900">Orders</th>
|
||||
<th className="text-right p-4 font-semibold text-gray-900">Payments</th>
|
||||
<th className="text-right p-4 font-semibold text-gray-900">Percentage</th>
|
||||
<th className="text-left p-4 font-semibold text-gray-900">Usage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paymentMethods.map((method, index) => {
|
||||
const colors = getPaymentMethodColors(method.payment_method_type);
|
||||
return (
|
||||
<tr key={method.payment_method_id || index} className="border-b border-gray-100 hover:bg-gray-50 transition-colors">
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{getPaymentMethodIcon(method.payment_method_type)}
|
||||
<span className="font-medium text-gray-900">
|
||||
{method.payment_method_name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<Chip
|
||||
label={method.payment_method_type.toUpperCase()}
|
||||
colorClasses={colors.chip}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-4 text-right font-semibold text-gray-900">
|
||||
{formatCurrency(method.total_amount)}
|
||||
</td>
|
||||
<td className="p-4 text-right text-gray-700">
|
||||
{method.order_count.toLocaleString()}
|
||||
</td>
|
||||
<td className="p-4 text-right text-gray-700">
|
||||
{method.payment_count.toLocaleString()}
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
<span className="font-bold text-blue-600">
|
||||
{method.percentage.toFixed(1)}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<ProgressBar
|
||||
value={method.percentage}
|
||||
className="flex-1"
|
||||
color={colors.progress}
|
||||
/>
|
||||
<span className="text-xs text-gray-500 min-w-fit">
|
||||
{method.percentage.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{paymentMethods.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<TablerIcon name="credit-card" className="w-12 h-12 text-gray-300 mx-auto mb-4" />
|
||||
<p className="text-gray-500">
|
||||
No payment method data available for the selected period.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Stats */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||
<div className="p-6">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">
|
||||
Key Insights
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 bg-blue-50 rounded-lg border border-blue-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<TablerIcon name="cash" className="w-5 h-5 text-blue-600" />
|
||||
<span className="text-sm font-medium text-blue-900">
|
||||
Most Used Payment Method
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-blue-900">
|
||||
{paymentMethods.length > 0 ? paymentMethods[0].payment_method_name : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 bg-green-50 rounded-lg border border-green-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<TablerIcon name="trending-up" className="w-5 h-5 text-green-600" />
|
||||
<span className="text-sm font-medium text-green-900">
|
||||
Revenue per Payment
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-green-900">
|
||||
{formatCurrency(summary.total_payments > 0 ? summary.total_amount / summary.total_payments : 0)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 bg-purple-50 rounded-lg border border-purple-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<TablerIcon name="receipt" className="w-5 h-5 text-purple-600" />
|
||||
<span className="text-sm font-medium text-purple-900">
|
||||
Payment Success Rate
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-purple-900">
|
||||
{((summary.total_payments / summary.total_orders) * 100).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 bg-orange-50 rounded-lg border border-orange-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<TablerIcon name="coins" className="w-5 h-5 text-orange-600" />
|
||||
<span className="text-sm font-medium text-orange-900">
|
||||
Payment Methods Used
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-orange-900">
|
||||
{paymentMethods.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentMethodAnalytics;
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
||||
import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs'
|
||||
|
||||
// Server Action Imports
|
||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||
import { useSalesAnalytics } from '../../../../../../services/queries/analytics'
|
||||
|
||||
const DashboardOrder = () => {
|
||||
const { data, isLoading } = useSalesAnalytics()
|
||||
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
const transformSalesData = (data: any) => {
|
||||
return [
|
||||
{
|
||||
type: 'items',
|
||||
avatarIcon: 'tabler-shopping-cart',
|
||||
date: data.map((d: any) => formatDate(d.date)),
|
||||
series: [{ data: data.map((d: any) => d.items) }]
|
||||
},
|
||||
{
|
||||
type: 'orders',
|
||||
avatarIcon: 'tabler-chart-bar',
|
||||
date: data.map((d: any) => formatDate(d.date)),
|
||||
series: [{ data: data.map((d: any) => d.orders) }]
|
||||
},
|
||||
{
|
||||
type: 'sales',
|
||||
avatarIcon: 'tabler-currency-dollar',
|
||||
date: data.map((d: any) => formatDate(d.date)),
|
||||
series: [{ data: data.map((d: any) => d.sales) }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Items'
|
||||
value={data?.summary.total_items as number}
|
||||
avatarIcon={'tabler-package'}
|
||||
avatarColor='primary'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Orders'
|
||||
value={data?.summary.total_orders as number}
|
||||
avatarIcon={'tabler-shopping-cart'}
|
||||
avatarColor='info'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Average Orders'
|
||||
value={data?.summary.average_order_value as number}
|
||||
avatarIcon={'tabler-trending-up'}
|
||||
avatarColor='warning'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Sales'
|
||||
value={data?.summary.total_sales as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='success'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 12 }}>
|
||||
<EarningReportsWithTabs data={transformSalesData(data?.data)} />
|
||||
</Grid>
|
||||
{/* <Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<RadarSalesChart />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<SalesByCountries />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<ProjectStatus />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<ActiveProjects />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<LastTransaction />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<ActivityTimeline />
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardOrder
|
||||
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
||||
import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs'
|
||||
|
||||
// Server Action Imports
|
||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||
import { useDashboardAnalytics } from '../../../../../../services/queries/analytics'
|
||||
import { DashboardReport, PaymentDataItem, ProductData, RecentSale } from '../../../../../../types/services/analytic'
|
||||
|
||||
const DashboardOverview = () => {
|
||||
const { data, isLoading } = useDashboardAnalytics()
|
||||
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
const transformSalesData = (data: DashboardReport) => {
|
||||
return [
|
||||
{
|
||||
type: 'products',
|
||||
avatarIcon: 'tabler-package',
|
||||
date: data.top_products.map((d: ProductData) => d.product_name),
|
||||
series: [{ data: data.top_products.map((d: ProductData) => d.revenue) }]
|
||||
},
|
||||
{
|
||||
type: 'orders',
|
||||
avatarIcon: 'tabler-shopping-cart',
|
||||
date: data.recent_sales.map((d: RecentSale) => formatDate(d.date)),
|
||||
series: [{ data: data.recent_sales.map((d: RecentSale) => d.net_sales) }]
|
||||
},
|
||||
{
|
||||
type: 'payments',
|
||||
avatarIcon: 'tabler-credit-card-pay',
|
||||
date: data.payment_methods.map((d: PaymentDataItem) => d.payment_method_name),
|
||||
series: [{ data: data.payment_methods.map((d: PaymentDataItem) => d.total_amount) }]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Customers'
|
||||
value={data?.overview.total_customers as number}
|
||||
avatarIcon={'tabler-users'}
|
||||
avatarColor='primary'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Orders'
|
||||
value={data?.overview.total_orders as number}
|
||||
avatarIcon={'tabler-package'}
|
||||
avatarColor='info'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Average Orders'
|
||||
value={data?.overview.average_order_value as number}
|
||||
avatarIcon={'tabler-trending-up'}
|
||||
avatarColor='warning'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Sales'
|
||||
value={data?.overview.total_sales as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='success'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 12 }}>
|
||||
<EarningReportsWithTabs data={transformSalesData(data!)} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardOverview
|
||||
@@ -0,0 +1,98 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
||||
import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs'
|
||||
|
||||
// Server Action Imports
|
||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||
import { usePaymentAnalytics } from '../../../../../../services/queries/analytics'
|
||||
import { PaymentDataItem } from '../../../../../../types/services/analytic'
|
||||
|
||||
const DashboardPayment = () => {
|
||||
const { data, isLoading } = usePaymentAnalytics()
|
||||
|
||||
const transformSalesData = (data: PaymentDataItem[]) => {
|
||||
return [
|
||||
{
|
||||
type: 'payment',
|
||||
avatarIcon: 'tabler-package',
|
||||
date: data.map((d: PaymentDataItem) => d.payment_method_name),
|
||||
series: [{ data: data.map((d: PaymentDataItem) => d.total_amount) }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Orders'
|
||||
value={data?.summary.total_orders as number}
|
||||
avatarIcon={'tabler-shopping-cart'}
|
||||
avatarColor='primary'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Payment'
|
||||
value={data?.summary.total_payments as number}
|
||||
avatarIcon={'tabler-package'}
|
||||
avatarColor='info'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Average Orders'
|
||||
value={data?.summary.average_order_value as number}
|
||||
avatarIcon={'tabler-trending-up'}
|
||||
avatarColor='warning'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Amount'
|
||||
value={data?.summary.total_amount as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='success'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 12 }}>
|
||||
<EarningReportsWithTabs data={transformSalesData(data!.data)} />
|
||||
</Grid>
|
||||
{/* <Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<RadarSalesChart />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<SalesByCountries />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<ProjectStatus />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<ActiveProjects />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<LastTransaction />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<ActivityTimeline />
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardPayment
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
||||
import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs'
|
||||
|
||||
// Server Action Imports
|
||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||
import { useProductSalesAnalytics } from '../../../../../../services/queries/analytics'
|
||||
|
||||
const DashboardProduct = () => {
|
||||
const { data, isLoading } = useProductSalesAnalytics()
|
||||
|
||||
const summary = {
|
||||
totalProducts: data?.data.length,
|
||||
totalQuantitySold: data?.data.reduce((sum, item) => sum + item.quantity_sold, 0),
|
||||
totalRevenue: data?.data.reduce((sum, item) => sum + item.revenue, 0),
|
||||
totalOrders: data?.data.reduce((sum, item) => sum + item.order_count, 0),
|
||||
averageOrderValue: data?.data
|
||||
? data!.data.reduce((sum, item) => sum + item.revenue, 0) /
|
||||
data!.data.reduce((sum, item) => sum + item.order_count, 0)
|
||||
: 0
|
||||
}
|
||||
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
const transformSalesData = (data: any) => {
|
||||
return [
|
||||
{
|
||||
type: 'products',
|
||||
avatarIcon: 'tabler-package',
|
||||
date: data.map((d: any) => d.product_name),
|
||||
series: [{ data: data.map((d: any) => d.revenue) }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Orders'
|
||||
value={summary.totalOrders as number}
|
||||
avatarIcon={'tabler-shopping-cart'}
|
||||
avatarColor='primary'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Product Sold'
|
||||
value={summary.totalQuantitySold as number}
|
||||
avatarIcon={'tabler-package'}
|
||||
avatarColor='info'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Average Orders'
|
||||
value={summary.averageOrderValue as number}
|
||||
avatarIcon={'tabler-trending-up'}
|
||||
avatarColor='warning'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Sales'
|
||||
value={summary.totalRevenue as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='success'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 12 }}>
|
||||
<EarningReportsWithTabs data={transformSalesData(data?.data)} />
|
||||
</Grid>
|
||||
{/* <Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<RadarSalesChart />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<SalesByCountries />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<ProjectStatus />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<ActiveProjects />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<LastTransaction />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<ActivityTimeline />
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardProduct
|
||||
@@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder'
|
||||
import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs'
|
||||
|
||||
// Server Action Imports
|
||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||
import { useProfitLossAnalytics } from '../../../../../../services/queries/analytics'
|
||||
import {
|
||||
ProductDataReport,
|
||||
ProfitLossReport
|
||||
} from '../../../../../../types/services/analytic'
|
||||
|
||||
function formatMetricName(metric: string): string {
|
||||
const nameMap: { [key: string]: string } = {
|
||||
revenue: 'Revenue',
|
||||
cost: 'Cost',
|
||||
gross_profit: 'Gross Profit',
|
||||
gross_profit_margin: 'Gross Profit Margin (%)',
|
||||
tax: 'Tax',
|
||||
discount: 'Discount',
|
||||
net_profit: 'Net Profit',
|
||||
net_profit_margin: 'Net Profit Margin (%)',
|
||||
orders: 'Orders'
|
||||
}
|
||||
|
||||
return nameMap[metric] || metric.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
||||
}
|
||||
|
||||
const DashboardProfitLoss = () => {
|
||||
const { data, isLoading } = useProfitLossAnalytics()
|
||||
|
||||
const formatDate = (dateString: any) => {
|
||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
const metrics = ['revenue', 'cost', 'gross_profit', 'net_profit']
|
||||
|
||||
const transformSalesData = (data: ProfitLossReport) => {
|
||||
return [
|
||||
{
|
||||
type: 'products',
|
||||
avatarIcon: 'tabler-package',
|
||||
date: data.product_data.map((d: ProductDataReport) => d.product_name),
|
||||
series: [{ data: data.product_data.map((d: ProductDataReport) => d.revenue) }]
|
||||
},
|
||||
// {
|
||||
// type: 'profits',
|
||||
// avatarIcon: 'tabler-currency-dollar',
|
||||
// date: data.data.map((d: DailyData) => formatDate(d.date)),
|
||||
// series: metrics.map(metric => ({
|
||||
// name: formatMetricName(metric as string),
|
||||
// data: data.data.map((item: any) => item[metric] as number)
|
||||
// }))
|
||||
// }
|
||||
]
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Cost'
|
||||
value={data?.summary.total_cost as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='primary'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Total Rvenue'
|
||||
value={data?.summary.total_revenue as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='info'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Gross Profit'
|
||||
value={data?.summary.gross_profit as number}
|
||||
avatarIcon={'tabler-trending-up'}
|
||||
avatarColor='warning'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
|
||||
<DistributedBarChartOrder
|
||||
isLoading={isLoading}
|
||||
title='Net Profit'
|
||||
value={data?.summary.net_profit as number}
|
||||
avatarIcon={'tabler-currency-dollar'}
|
||||
avatarColor='success'
|
||||
avatarSkin='light'
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 12 }}>
|
||||
<EarningReportsWithTabs data={transformSalesData(data!)} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardProfitLoss
|
||||
Reference in New Issue
Block a user